Skip to content

Commit e340d75

Browse files
committed
support decoding byte slices
All of the backend infrastructure was in place to support &[u8], and some functions already take `impl AsRef<[u8]>`. However the main decode() and decode_header() functions require a &str. This change updates a few internal signatures and adds a _bytes() version of decode and decode_header. When you're doing many requests per second, the cost of doing an extra utf-8 check over header payloads is significant. By supporting a &[u8] decode, users can let base64 and the crypto implementation in question handle its own bytewise validity. They already do this today in addition to the extra utf-8 scan.
1 parent afbb44e commit e340d75

File tree

7 files changed

+102
-20
lines changed

7 files changed

+102
-20
lines changed

benches/jwt.rs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use criterion::{black_box, criterion_group, criterion_main, Criterion};
2-
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
2+
use jsonwebtoken::{
3+
decode, decode_bytes, decode_header, decode_header_bytes, encode, Algorithm, DecodingKey,
4+
EncodingKey, Header, Validation,
5+
};
36
use serde::{Deserialize, Serialize};
47

58
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
@@ -18,18 +21,47 @@ fn bench_encode(c: &mut Criterion) {
1821
}
1922

2023
fn bench_decode(c: &mut Criterion) {
21-
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
24+
let token = b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
2225
let key = DecodingKey::from_secret("secret".as_ref());
2326

24-
c.bench_function("bench_decode", |b| {
27+
let mut group = c.benchmark_group("decode");
28+
group.throughput(criterion::Throughput::Bytes(token.len() as u64));
29+
30+
group.bench_function("bytes", |b| {
2531
b.iter(|| {
26-
decode::<Claims>(
32+
decode_bytes::<Claims>(
2733
black_box(token),
2834
black_box(&key),
2935
black_box(&Validation::new(Algorithm::HS256)),
3036
)
3137
})
3238
});
39+
40+
group.bench_function("str", |b| {
41+
b.iter(|| {
42+
decode::<Claims>(
43+
// Simulate the cost of validating &str before decoding
44+
black_box(std::str::from_utf8(black_box(token)).expect("valid utf8")),
45+
black_box(&key),
46+
black_box(&Validation::new(Algorithm::HS256)),
47+
)
48+
})
49+
});
50+
51+
drop(group);
52+
let mut group = c.benchmark_group("header");
53+
group.throughput(criterion::Throughput::Bytes(token.len() as u64));
54+
55+
group.bench_function("str", |b| {
56+
b.iter(|| {
57+
decode_header(
58+
// Simulate the cost of validating &str before decoding
59+
black_box(std::str::from_utf8(black_box(token)).expect("valid utf8")),
60+
)
61+
})
62+
});
63+
64+
group.bench_function("bytes", |b| b.iter(|| decode_header_bytes(black_box(token))));
3365
}
3466

3567
criterion_group!(benches, bench_encode, bench_decode);

src/crypto/mod.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub fn sign(message: &[u8], key: &EncodingKey, algorithm: Algorithm) -> Result<S
4646
/// See Ring docs for more details
4747
fn verify_ring(
4848
alg: &'static dyn signature::VerificationAlgorithm,
49-
signature: &str,
49+
signature: impl AsRef<[u8]>,
5050
message: &[u8],
5151
key: &[u8],
5252
) -> Result<bool> {
@@ -66,16 +66,17 @@ fn verify_ring(
6666
///
6767
/// `message` is base64(header) + "." + base64(claims)
6868
pub fn verify(
69-
signature: &str,
69+
signature: impl AsRef<[u8]>,
7070
message: &[u8],
7171
key: &DecodingKey,
7272
algorithm: Algorithm,
7373
) -> Result<bool> {
74+
let signature = signature.as_ref();
7475
match algorithm {
7576
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
7677
// we just re-sign the message with the key and compare if they are equal
7778
let signed = sign(message, &EncodingKey::from_secret(key.as_bytes()), algorithm)?;
78-
Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok())
79+
Ok(verify_slices_are_equal(signature, signed.as_ref()).is_ok())
7980
}
8081
Algorithm::ES256 | Algorithm::ES384 => verify_ring(
8182
ecdsa::alg_to_ec_verification(algorithm),

src/crypto/rsa.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub(crate) fn sign(
5151
/// Checks that a signature is valid based on the (n, e) RSA pubkey components
5252
pub(crate) fn verify_from_components(
5353
alg: &'static signature::RsaParameters,
54-
signature: &str,
54+
signature: impl AsRef<[u8]>,
5555
message: &[u8],
5656
components: (&[u8], &[u8]),
5757
) -> Result<bool> {

src/decoding.rs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,11 @@ impl DecodingKey {
204204
/// Verify signature of a JWT, and return header object and raw payload
205205
///
206206
/// If the token or its signature is invalid, it will return an error.
207-
fn verify_signature<'a>(
208-
token: &'a str,
207+
fn verify_signature_bytes<'a>(
208+
token: &'a [u8],
209209
key: &DecodingKey,
210210
validation: &Validation,
211-
) -> Result<(Header, &'a str)> {
211+
) -> Result<(Header, &'a [u8])> {
212212
if validation.validate_signature && validation.algorithms.is_empty() {
213213
return Err(new_error(ErrorKind::MissingAlgorithm));
214214
}
@@ -221,15 +221,15 @@ fn verify_signature<'a>(
221221
}
222222
}
223223

224-
let (signature, message) = expect_two!(token.rsplitn(2, '.'));
225-
let (payload, header) = expect_two!(message.rsplitn(2, '.'));
224+
let (signature, message) = expect_two!(token.rsplitn(2, |b| *b == b'.'));
225+
let (header, payload) = expect_two!(message.splitn(2, |b| *b == b'.'));
226226
let header = Header::from_encoded(header)?;
227227

228228
if validation.validate_signature && !validation.algorithms.contains(&header.alg) {
229229
return Err(new_error(ErrorKind::InvalidAlgorithm));
230230
}
231231

232-
if validation.validate_signature && !verify(signature, message.as_bytes(), key, header.alg)? {
232+
if validation.validate_signature && !verify(signature, message, key, header.alg)? {
233233
return Err(new_error(ErrorKind::InvalidSignature));
234234
}
235235

@@ -259,7 +259,38 @@ pub fn decode<T: DeserializeOwned>(
259259
key: &DecodingKey,
260260
validation: &Validation,
261261
) -> Result<TokenData<T>> {
262-
match verify_signature(token, key, validation) {
262+
decode_bytes(token.as_bytes(), key, validation)
263+
}
264+
265+
/// Decode and validate a JWT
266+
///
267+
/// If the token or its signature is invalid or the claims fail validation, it will return an error.
268+
///
269+
/// This differs from decode() in the case that you only have bytes. By decoding as bytes you can
270+
/// avoid taking a pass over your bytes to validate them as a utf-8 string. Since the decoding and
271+
/// validation is all done in terms of bytes, the &str step is unnecessary.
272+
/// If you already have a &str, decode is more convenient. If you have bytes, consider using this.
273+
///
274+
/// ```rust
275+
/// use serde::{Deserialize, Serialize};
276+
/// use jsonwebtoken::{decode_bytes, DecodingKey, Validation, Algorithm};
277+
///
278+
/// #[derive(Debug, Serialize, Deserialize)]
279+
/// struct Claims {
280+
/// sub: String,
281+
/// company: String
282+
/// }
283+
///
284+
/// let token = b"a.jwt.token";
285+
/// // Claims is a struct that implements Deserialize
286+
/// let token_message = decode_bytes::<Claims>(token, &DecodingKey::from_secret("secret".as_ref()), &Validation::new(Algorithm::HS256));
287+
/// ```
288+
pub fn decode_bytes<T: DeserializeOwned>(
289+
token: &[u8],
290+
key: &DecodingKey,
291+
validation: &Validation,
292+
) -> Result<TokenData<T>> {
293+
match verify_signature_bytes(token, key, validation) {
263294
Err(e) => Err(e),
264295
Ok((header, claims)) => {
265296
let decoded_claims = DecodedJwtPartClaims::from_jwt_part_claims(claims)?;
@@ -286,3 +317,19 @@ pub fn decode_header(token: &str) -> Result<Header> {
286317
let (_, header) = expect_two!(message.rsplitn(2, '.'));
287318
Header::from_encoded(header)
288319
}
320+
321+
/// Decode a JWT without any signature verification/validations and return its [Header](struct.Header.html).
322+
///
323+
/// If the token has an invalid format (ie 3 parts separated by a `.`), it will return an error.
324+
///
325+
/// ```rust
326+
/// use jsonwebtoken::decode_header_bytes;
327+
///
328+
/// let token = b"a.jwt.token";
329+
/// let header = decode_header_bytes(token);
330+
/// ```
331+
pub fn decode_header_bytes(token: &[u8]) -> Result<Header> {
332+
let (_, message) = expect_two!(token.rsplitn(2, |b| *b == b'.'));
333+
let (_, header) = expect_two!(message.rsplitn(2, |b| *b == b'.'));
334+
Header::from_encoded(header)
335+
}

src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ mod serialization;
1818
mod validation;
1919

2020
pub use algorithms::Algorithm;
21-
pub use decoding::{decode, decode_header, DecodingKey, TokenData};
21+
pub use decoding::{
22+
decode, decode_bytes, decode_header, decode_header_bytes, DecodingKey, TokenData,
23+
};
2224
pub use encoding::{encode, EncodingKey};
2325
pub use header::Header;
2426
pub use validation::{get_current_timestamp, Validation};

tests/ecdsa/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ fn round_trip_sign_verification_pk8() {
2626
let encrypted =
2727
sign(b"hello world", &EncodingKey::from_ec_der(privkey), Algorithm::ES256).unwrap();
2828
let is_valid =
29-
verify(&encrypted, b"hello world", &DecodingKey::from_ec_der(pubkey), Algorithm::ES256)
29+
verify(encrypted, b"hello world", &DecodingKey::from_ec_der(pubkey), Algorithm::ES256)
3030
.unwrap();
3131
assert!(is_valid);
3232
}
@@ -41,7 +41,7 @@ fn round_trip_sign_verification_pem() {
4141
sign(b"hello world", &EncodingKey::from_ec_pem(privkey_pem).unwrap(), Algorithm::ES256)
4242
.unwrap();
4343
let is_valid = verify(
44-
&encrypted,
44+
encrypted,
4545
b"hello world",
4646
&DecodingKey::from_ec_pem(pubkey_pem).unwrap(),
4747
Algorithm::ES256,

tests/eddsa/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ fn round_trip_sign_verification_pk8() {
2626
let encrypted =
2727
sign(b"hello world", &EncodingKey::from_ed_der(privkey), Algorithm::EdDSA).unwrap();
2828
let is_valid =
29-
verify(&encrypted, b"hello world", &DecodingKey::from_ed_der(pubkey), Algorithm::EdDSA)
29+
verify(encrypted, b"hello world", &DecodingKey::from_ed_der(pubkey), Algorithm::EdDSA)
3030
.unwrap();
3131
assert!(is_valid);
3232
}
@@ -41,7 +41,7 @@ fn round_trip_sign_verification_pem() {
4141
sign(b"hello world", &EncodingKey::from_ed_pem(privkey_pem).unwrap(), Algorithm::EdDSA)
4242
.unwrap();
4343
let is_valid = verify(
44-
&encrypted,
44+
encrypted,
4545
b"hello world",
4646
&DecodingKey::from_ed_pem(pubkey_pem).unwrap(),
4747
Algorithm::EdDSA,

0 commit comments

Comments
 (0)