Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support decoding byte slices #387

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions benches/jwt.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use jsonwebtoken::{
decode, decode_bytes, decode_header, decode_header_bytes, encode, Algorithm, DecodingKey,
EncodingKey, Header, Validation,
};
use serde::{Deserialize, Serialize};

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

fn bench_decode(c: &mut Criterion) {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
let token = b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
let key = DecodingKey::from_secret("secret".as_ref());

c.bench_function("bench_decode", |b| {
let mut group = c.benchmark_group("decode");
group.throughput(criterion::Throughput::Bytes(token.len() as u64));

group.bench_function("bytes", |b| {
b.iter(|| {
decode::<Claims>(
decode_bytes::<Claims>(
black_box(token),
black_box(&key),
black_box(&Validation::new(Algorithm::HS256)),
)
})
});

group.bench_function("str", |b| {
b.iter(|| {
decode::<Claims>(
// Simulate the cost of validating &str before decoding
black_box(std::str::from_utf8(black_box(token)).expect("valid utf8")),
black_box(&key),
black_box(&Validation::new(Algorithm::HS256)),
)
})
});

drop(group);
let mut group = c.benchmark_group("header");
group.throughput(criterion::Throughput::Bytes(token.len() as u64));

group.bench_function("str", |b| {
b.iter(|| {
decode_header(
// Simulate the cost of validating &str before decoding
black_box(std::str::from_utf8(black_box(token)).expect("valid utf8")),
)
})
});

group.bench_function("bytes", |b| b.iter(|| decode_header_bytes(black_box(token))));
}

criterion_group!(benches, bench_encode, bench_decode);
Expand Down
7 changes: 4 additions & 3 deletions src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub fn sign(message: &[u8], key: &EncodingKey, algorithm: Algorithm) -> Result<S
/// See Ring docs for more details
fn verify_ring(
alg: &'static dyn signature::VerificationAlgorithm,
signature: &str,
signature: impl AsRef<[u8]>,
message: &[u8],
key: &[u8],
) -> Result<bool> {
Expand All @@ -66,16 +66,17 @@ fn verify_ring(
///
/// `message` is base64(header) + "." + base64(claims)
pub fn verify(
signature: &str,
signature: impl AsRef<[u8]>,
message: &[u8],
key: &DecodingKey,
algorithm: Algorithm,
) -> Result<bool> {
let signature = signature.as_ref();
match algorithm {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
// we just re-sign the message with the key and compare if they are equal
let signed = sign(message, &EncodingKey::from_secret(key.as_bytes()), algorithm)?;
Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok())
Ok(verify_slices_are_equal(signature, signed.as_ref()).is_ok())
}
Algorithm::ES256 | Algorithm::ES384 => verify_ring(
ecdsa::alg_to_ec_verification(algorithm),
Expand Down
2 changes: 1 addition & 1 deletion src/crypto/rsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub(crate) fn sign(
/// Checks that a signature is valid based on the (n, e) RSA pubkey components
pub(crate) fn verify_from_components(
alg: &'static signature::RsaParameters,
signature: &str,
signature: impl AsRef<[u8]>,
message: &[u8],
components: (&[u8], &[u8]),
) -> Result<bool> {
Expand Down
61 changes: 54 additions & 7 deletions src/decoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,11 @@ impl DecodingKey {
/// Verify signature of a JWT, and return header object and raw payload
///
/// If the token or its signature is invalid, it will return an error.
fn verify_signature<'a>(
token: &'a str,
fn verify_signature_bytes<'a>(
token: &'a [u8],
key: &DecodingKey,
validation: &Validation,
) -> Result<(Header, &'a str)> {
) -> Result<(Header, &'a [u8])> {
if validation.validate_signature && validation.algorithms.is_empty() {
return Err(new_error(ErrorKind::MissingAlgorithm));
}
Expand All @@ -221,15 +221,15 @@ fn verify_signature<'a>(
}
}

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

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

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

Expand Down Expand Up @@ -259,7 +259,38 @@ pub fn decode<T: DeserializeOwned>(
key: &DecodingKey,
validation: &Validation,
) -> Result<TokenData<T>> {
match verify_signature(token, key, validation) {
decode_bytes(token.as_bytes(), key, validation)
}

/// Decode and validate a JWT
///
/// If the token or its signature is invalid or the claims fail validation, it will return an error.
///
/// This differs from decode() in the case that you only have bytes. By decoding as bytes you can
/// avoid taking a pass over your bytes to validate them as a utf-8 string. Since the decoding and
/// validation is all done in terms of bytes, the &str step is unnecessary.
/// If you already have a &str, decode is more convenient. If you have bytes, consider using this.
///
/// ```rust
/// use serde::{Deserialize, Serialize};
/// use jsonwebtoken::{decode_bytes, DecodingKey, Validation, Algorithm};
///
/// #[derive(Debug, Serialize, Deserialize)]
/// struct Claims {
/// sub: String,
/// company: String
/// }
///
/// let token = b"a.jwt.token";
/// // Claims is a struct that implements Deserialize
/// let token_message = decode_bytes::<Claims>(token, &DecodingKey::from_secret("secret".as_ref()), &Validation::new(Algorithm::HS256));
/// ```
pub fn decode_bytes<T: DeserializeOwned>(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to not change decode to take token: impl AsRef<[u8]> and have a single fn?
Same question for decode_header

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I think that would be better. It might be a minor breaking change for your users who use clippy. I was trying to avoid any potential for an api break, but that may not be the right thing to optimize for.
If you're willing to permit it, I'd be happy to see how taking token as AsRef feels!

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it looks good, I'm going to wait a bit to see if there are more breaking changes to add for a new major version though

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, seems like there are some other reasonable candidates for breaking changes in your issue queue.

Thanks for reviewing!

token: &[u8],
key: &DecodingKey,
validation: &Validation,
) -> Result<TokenData<T>> {
match verify_signature_bytes(token, key, validation) {
Err(e) => Err(e),
Ok((header, claims)) => {
let decoded_claims = DecodedJwtPartClaims::from_jwt_part_claims(claims)?;
Expand All @@ -286,3 +317,19 @@ pub fn decode_header(token: &str) -> Result<Header> {
let (_, header) = expect_two!(message.rsplitn(2, '.'));
Header::from_encoded(header)
}

/// Decode a JWT without any signature verification/validations and return its [Header](struct.Header.html).
///
/// If the token has an invalid format (ie 3 parts separated by a `.`), it will return an error.
///
/// ```rust
/// use jsonwebtoken::decode_header_bytes;
///
/// let token = b"a.jwt.token";
/// let header = decode_header_bytes(token);
/// ```
pub fn decode_header_bytes(token: &[u8]) -> Result<Header> {
let (_, message) = expect_two!(token.rsplitn(2, |b| *b == b'.'));
let (_, header) = expect_two!(message.rsplitn(2, |b| *b == b'.'));
Header::from_encoded(header)
}
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ mod serialization;
mod validation;

pub use algorithms::Algorithm;
pub use decoding::{decode, decode_header, DecodingKey, TokenData};
pub use decoding::{
decode, decode_bytes, decode_header, decode_header_bytes, DecodingKey, TokenData,
};
pub use encoding::{encode, EncodingKey};
pub use header::Header;
pub use validation::{get_current_timestamp, Validation};
4 changes: 2 additions & 2 deletions tests/ecdsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ fn round_trip_sign_verification_pk8() {
let encrypted =
sign(b"hello world", &EncodingKey::from_ec_der(privkey), Algorithm::ES256).unwrap();
let is_valid =
verify(&encrypted, b"hello world", &DecodingKey::from_ec_der(pubkey), Algorithm::ES256)
verify(encrypted, b"hello world", &DecodingKey::from_ec_der(pubkey), Algorithm::ES256)
.unwrap();
assert!(is_valid);
}
Expand All @@ -41,7 +41,7 @@ fn round_trip_sign_verification_pem() {
sign(b"hello world", &EncodingKey::from_ec_pem(privkey_pem).unwrap(), Algorithm::ES256)
.unwrap();
let is_valid = verify(
&encrypted,
encrypted,
b"hello world",
&DecodingKey::from_ec_pem(pubkey_pem).unwrap(),
Algorithm::ES256,
Expand Down
4 changes: 2 additions & 2 deletions tests/eddsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ fn round_trip_sign_verification_pk8() {
let encrypted =
sign(b"hello world", &EncodingKey::from_ed_der(privkey), Algorithm::EdDSA).unwrap();
let is_valid =
verify(&encrypted, b"hello world", &DecodingKey::from_ed_der(pubkey), Algorithm::EdDSA)
verify(encrypted, b"hello world", &DecodingKey::from_ed_der(pubkey), Algorithm::EdDSA)
.unwrap();
assert!(is_valid);
}
Expand All @@ -41,7 +41,7 @@ fn round_trip_sign_verification_pem() {
sign(b"hello world", &EncodingKey::from_ed_pem(privkey_pem).unwrap(), Algorithm::EdDSA)
.unwrap();
let is_valid = verify(
&encrypted,
encrypted,
b"hello world",
&DecodingKey::from_ed_pem(pubkey_pem).unwrap(),
Algorithm::EdDSA,
Expand Down
Loading