From 2e460a3d5122cb34de4f7ae4d3d207320335ff6b Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Thu, 2 Aug 2018 17:30:18 -0400 Subject: [PATCH 01/75] JWE support. Resolves #113 - Added JWE AeadAlgorithm and KeyAlgorithm and supporting interfaces/implementations, and refactored SignatureAlgorithm to be an interface instead of an enum to enable custom algorithms - NoneSignatureAlgorithm cleanup. Added UnsupportedKeyExceptionTest. - Added JWK support! --- .gitignore | 1 - .travis.yml | 6 +- README.md | 12 +- api/pom.xml | 2 +- .../io/jsonwebtoken/CompressionCodec.java | 6 +- api/src/main/java/io/jsonwebtoken/Header.java | 79 +++- api/src/main/java/io/jsonwebtoken/Jwe.java | 12 + .../main/java/io/jsonwebtoken/JweHeader.java | 88 ++++ .../main/java/io/jsonwebtoken/JwsHeader.java | 73 ++-- .../main/java/io/jsonwebtoken/JwtBuilder.java | 78 +++- .../io/jsonwebtoken/JwtParserBuilder.java | 13 + api/src/main/java/io/jsonwebtoken/Jwts.java | 24 ++ api/src/main/java/io/jsonwebtoken/Named.java | 14 + .../jsonwebtoken/RequiredTypeException.java | 5 +- .../io/jsonwebtoken/SignatureAlgorithm.java | 2 + .../io/jsonwebtoken/SignatureException.java | 4 +- .../java/io/jsonwebtoken/lang/Assert.java | 6 +- .../io/jsonwebtoken/lang/Collections.java | 11 + .../security/AeadDecryptionRequest.java | 25 ++ .../security/AeadEncryptionAlgorithm.java | 9 + .../security/AeadEncryptionResult.java | 23 ++ .../security/AeadIvEncryptionResult.java | 7 + .../jsonwebtoken/security/AeadIvRequest.java | 9 + .../io/jsonwebtoken/security/AeadRequest.java | 25 ++ .../AeadSymmetricEncryptionAlgorithm.java | 11 + .../security/AssociatedDataSource.java | 25 ++ .../security/AsymmetricKeyAlgorithm.java | 16 + .../AsymmetricKeySignatureAlgorithm.java | 7 + .../security/AuthenticationTagSource.java | 25 ++ .../security/CryptoException.java | 30 ++ .../jsonwebtoken/security/CryptoMessage.java | 10 + .../jsonwebtoken/security/CryptoRequest.java | 36 ++ .../io/jsonwebtoken/security/CurveId.java | 9 + .../io/jsonwebtoken/security/CurveIds.java | 44 ++ .../security/DecryptionKeyResolver.java | 19 + .../jsonwebtoken/security/DefaultCurveId.java | 33 ++ .../java/io/jsonwebtoken/security/EcJwk.java | 13 + .../jsonwebtoken/security/EcJwkBuilder.java | 7 + .../security/EcJwkBuilderFactory.java | 11 + .../jsonwebtoken/security/EcJwkMutator.java | 13 + .../security/EncryptionAlgorithm.java | 15 + .../security/EncryptionAlgorithmLocator.java | 11 + .../security/EncryptionAlgorithmName.java | 75 ++++ .../security/EncryptionAlgorithms.java | 105 +++++ .../security/EncryptionResult.java | 25 ++ .../security/InitializationVectorSource.java | 16 + .../security/InvalidKeyException.java | 4 + .../security/IvEncryptionResult.java | 9 + .../io/jsonwebtoken/security/IvRequest.java | 9 + .../java/io/jsonwebtoken/security/Jwk.java | 30 ++ .../io/jsonwebtoken/security/JwkBuilder.java | 10 + .../security/JwkBuilderFactory.java | 12 + .../io/jsonwebtoken/security/JwkMutator.java | 27 ++ .../security/JwkRsaPrimeInfo.java | 16 + .../security/JwkRsaPrimeInfoBuilder.java | 9 + .../security/JwkRsaPrimeInfoMutator.java | 13 + .../java/io/jsonwebtoken/security/Jwks.java | 14 + .../jsonwebtoken/security/KeyException.java | 4 + .../security/KeyManagementAlgorithmName.java | 106 +++++ .../security/KeyManagementModeName.java | 42 ++ .../java/io/jsonwebtoken/security/Keys.java | 121 +++--- .../security/MalformedKeyException.java | 15 + .../jsonwebtoken/security/PrivateEcJwk.java | 9 + .../security/PrivateEcJwkBuilder.java | 9 + .../security/PrivateEcJwkMutator.java | 9 + .../jsonwebtoken/security/PrivateRsaJwk.java | 23 ++ .../security/PrivateRsaJwkMutator.java | 23 ++ .../io/jsonwebtoken/security/PublicEcJwk.java | 7 + .../security/PublicEcJwkBuilder.java | 7 + .../jsonwebtoken/security/PublicRsaJwk.java | 7 + .../java/io/jsonwebtoken/security/RsaJwk.java | 11 + .../jsonwebtoken/security/RsaJwkMutator.java | 11 + .../security/SignatureAlgorithm.java | 15 + .../security/SignatureAlgorithms.java | 223 ++++++++++ .../security/SignatureException.java | 1 + .../SymmetricEncryptionAlgorithm.java | 9 + .../jsonwebtoken/security/SymmetricJwk.java | 9 + .../security/SymmetricJwkBuilder.java | 7 + .../security/SymmetricJwkMutator.java | 9 + .../security/SymmetricKeyAlgorithm.java | 16 + .../SymmetricKeySignatureAlgorithm.java | 7 + .../security/UnsupportedKeyException.java | 15 + .../security/VerifySignatureRequest.java | 11 + .../EncryptionAlgorithmNameTest.groovy | 61 +++ .../io/jsonwebtoken/lang/ArraysTest.groovy | 45 ++ .../jsonwebtoken/security/CurveIdsTest.groovy | 24 ++ .../KeyManagementAlgorithmNameTest.groovy | 54 +++ .../security/KeyManagementModeNameTest.groovy | 19 + .../io/jsonwebtoken/security/KeysTest.groovy | 82 ++-- .../UnsupportedKeyExceptionTest.groovy | 17 + extensions/gson/pom.xml | 11 +- .../gson/io/GsonDeserializerTest.groovy | 3 + extensions/jackson/pom.xml | 10 +- .../jackson/io/JacksonDeserializer.java | 1 - extensions/orgjson/pom.xml | 10 +- extensions/pom.xml | 2 +- impl/pom.xml | 2 +- .../io/jsonwebtoken/impl/DefaultHeader.java | 20 +- .../jsonwebtoken/impl/DefaultJweHeader.java | 30 ++ .../jsonwebtoken/impl/DefaultJwsHeader.java | 13 +- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 87 ++-- .../jsonwebtoken/impl/DefaultJwtParser.java | 387 +++++++++++++++--- .../impl/DefaultJwtParserBuilder.java | 13 +- .../impl/DefaultTokenizedJwe.java | 23 ++ .../impl/DefaultTokenizedJwt.java | 29 ++ .../jsonwebtoken/impl/DispatchingParser.java | 145 +++++++ .../java/io/jsonwebtoken/impl/JwtMap.java | 14 +- .../io/jsonwebtoken/impl/JwtTokenizer.java | 77 ++++ .../io/jsonwebtoken/impl/TokenizedJwe.java | 8 + .../io/jsonwebtoken/impl/TokenizedJwt.java | 20 + .../impl/TokenizedJwtBuilder.java | 9 + .../impl/crypto/EllipticCurveProvider.java | 120 +----- .../EllipticCurveSignatureValidator.java | 3 +- .../impl/crypto/EllipticCurveSigner.java | 3 +- .../jsonwebtoken/impl/crypto/MacProvider.java | 9 +- .../jsonwebtoken/impl/crypto/RsaProvider.java | 9 +- .../impl/crypto/RsaSignatureValidator.java | 9 +- .../impl/crypto/SignatureProvider.java | 21 +- .../AbstractAeadAesEncryptionAlgorithm.java | 112 +++++ .../impl/security/AbstractEcJwk.java | 65 +++ .../impl/security/AbstractEcJwkBuilder.java | 31 ++ .../impl/security/AbstractEcJwkValidator.java | 39 ++ .../security/AbstractEncryptionAlgorithm.java | 48 +++ .../impl/security/AbstractJwk.java | 191 +++++++++ .../impl/security/AbstractJwkBuilder.java | 79 ++++ .../impl/security/AbstractJwkConverter.java | 86 ++++ .../impl/security/AbstractJwkValidator.java | 40 ++ .../impl/security/AbstractRsaJwk.java | 35 ++ .../security/AbstractRsaJwkValidator.java | 16 + .../security/AbstractSignatureAlgorithm.java | 128 ++++++ .../security/AbstractTypedJwkConverter.java | 33 ++ .../impl/security/CipherAlgorithm.java | 19 + .../impl/security/CipherCallback.java | 8 + .../impl/security/CipherTemplate.java | 54 +++ .../impl/security/CryptoAlgorithm.java | 35 ++ .../DefaultAeadIvEncryptionResult.java | 46 +++ .../impl/security/DefaultAeadIvRequest.java | 50 +++ .../security/DefaultAesEncryptionRequest.java | 26 ++ .../impl/security/DefaultCryptoMessage.java | 24 ++ .../impl/security/DefaultCryptoRequest.java | 37 ++ .../security/DefaultEcJwkBuilderFactory.java | 18 + .../DefaultEncryptionAlgorithmLocator.java | 40 ++ .../security/DefaultEncryptionRequest.java | 50 +++ .../security/DefaultEncryptionResult.java | 18 + .../security/DefaultIvDecryptionRequest.java | 41 ++ .../security/DefaultIvEncryptionResult.java | 45 ++ .../impl/security/DefaultJweFactory.java | 123 ++++++ .../security/DefaultJwkBuilderFactory.java | 18 + .../impl/security/DefaultJwkConverter.java | 58 +++ .../impl/security/DefaultPrivateEcJwk.java | 18 + .../security/DefaultPrivateEcJwkBuilder.java | 24 ++ .../impl/security/DefaultPrivateRsaJwk.java | 87 ++++ .../impl/security/DefaultPublicEcJwk.java | 6 + .../security/DefaultPublicEcJwkBuilder.java | 23 ++ .../impl/security/DefaultSymmetricJwk.java | 28 ++ .../security/DefaultSymmetricJwkBuilder.java | 24 ++ .../DefaultVerifySignatureRequest.java | 23 ++ .../impl/security/DirectEncryptionMode.java | 22 + .../impl/security/DirectKeyAgreementMode.java | 14 + .../DisabledDecryptionKeyResolver.java | 22 + .../impl/security/EcJwkConverter.java | 177 ++++++++ .../EllipticCurveSignatureAlgorithm.java | 253 ++++++++++++ .../impl/security/EncryptKeyRequest.java | 12 + .../security/EncryptedKeyManagementMode.java | 22 + .../security/GcmAesEncryptionAlgorithm.java | 106 +++++ .../impl/security/GetKeyRequest.java | 17 + .../security/HmacAesEncryptionAlgorithm.java | 186 +++++++++ .../impl/security/JwkConverter.java | 11 + .../jsonwebtoken/impl/security/JwkParser.java | 19 + .../impl/security/JwkValidator.java | 9 + .../KeyAgreementWithKeyWrappingMode.java | 12 + .../impl/security/KeyEncryptionMode.java | 12 + .../impl/security/KeyManagementMode.java | 22 + .../impl/security/KeyManagementModes.java | 15 + .../impl/security/KeyWrappingMode.java | 12 + .../impl/security/MacSignatureAlgorithm.java | 181 ++++++++ .../impl/security/NoneSignatureAlgorithm.java | 27 ++ .../impl/security/PrivateEcJwkValidator.java | 18 + .../impl/security/RandomEncryptedKeyMode.java | 38 ++ .../jsonwebtoken/impl/security/Randoms.java | 39 ++ .../jsonwebtoken/impl/security/Recipient.java | 11 + .../impl/security/RsaJwkConverter.java | 28 ++ .../impl/security/RsaSignatureAlgorithm.java | 136 ++++++ .../impl/security/SymmetricJwkConverter.java | 59 +++ .../impl/security/SymmetricJwkValidator.java | 23 ++ .../impl/security/TypedJwkConverter.java | 11 + .../DeprecatedJwtParserTest.groovy | 4 +- .../io/jsonwebtoken/DeprecatedJwtsTest.groovy | 7 +- .../io/jsonwebtoken/JwtParserTest.groovy | 4 +- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 22 +- .../impl/DefaultJweHeaderTest.groovy | 32 ++ .../impl/DefaultJwtBuilderTest.groovy | 85 +++- .../impl/DefaultJwtParserBuilderTest.groovy | 23 +- .../impl/DefaultJwtParserTest.groovy | 7 +- .../impl/DispatchingParserTest.groovy | 15 + .../jsonwebtoken/impl/JwtTokenizerTest.groovy | 24 ++ ...EllipticCurveSignatureValidatorTest.groovy | 140 +------ .../crypto/EllipticCurveSignerTest.groovy | 7 +- .../impl/crypto/Issue542Test.groovy | 17 +- .../impl/crypto/RsaProviderTest.groovy | 3 +- .../crypto/RsaSignatureValidatorTest.groovy | 14 + ...tractAeadAesEncryptionAlgorithmTest.groovy | 111 +++++ .../impl/security/AbstractEcJwkTest.groovy | 116 ++++++ .../AbstractEcJwkValidatorTest.groovy | 57 +++ .../AbstractEncryptionAlgorithmTest.groovy | 56 +++ .../security/AbstractJwkBuilderTest.groovy | 98 +++++ .../impl/security/AbstractJwkTest.groovy | 260 ++++++++++++ .../security/AbstractJwkValidatorTest.groovy | 55 +++ .../AbstractSignatureAlgorithmTest.groovy | 169 ++++++++ .../security/Aes128CbcHmacSha256Test.groovy | 96 +++++ .../security/Aes192CbcHmacSha384Test.groovy | 94 +++++ .../security/Aes256CbcHmacSha512Test.groovy | 93 +++++ .../impl/security/CipherAlgorithmTest.groovy | 57 +++ .../impl/security/CipherTemplateTest.groovy | 93 +++++ .../DefaultAeadIvEncryptionResultTest.groovy | 50 +++ .../security/DefaultCryptoMessageTest.groovy | 16 + ...faultEncryptionAlgorithmLocatorTest.groovy | 98 +++++ .../DefaultIvEncryptionResultTest.groovy | 38 ++ .../security/DefaultJweFactoryTest.groovy | 14 + .../security/DefaultJwkConverterTest.groovy | 103 +++++ .../security/DefaultSymmetricJwkTest.groovy | 43 ++ .../DisabledDecryptionKeyResolverTest.groovy | 16 + ...EllipticCurveSignatureAlgorithmTest.groovy | 224 ++++++++++ .../GcmAesEncryptionServiceTest.groovy | 76 ++++ .../HmacAesEncryptionAlgorithmTest.groovy | 83 ++++ .../impl/security/JwksTest.groovy | 47 +++ .../security/MacSignatureAlgorithmTest.groovy | 128 ++++++ .../NoneSignatureAlgorithmTest.groovy | 24 ++ .../security/PrivateEcJwkValidatorTest.groovy | 31 ++ .../impl/security/RandomsTest.groovy | 14 + .../security/RsaSignatureAlgorithmTest.groovy | 86 ++++ .../security/SymmetricJwkValidatorTest.groovy | 31 ++ .../jsonwebtoken/impl/security/TestJwk.groovy | 7 + .../impl/security/TestJwkValidator.groovy | 18 + .../security/EncryptionAlgorithmsTest.groovy | 102 +++++ .../jsonwebtoken/security/KeysImplTest.groovy | 73 +++- .../security/SignatureAlgorithmsTest.groovy | 19 + pom.xml | 21 +- 238 files changed, 9225 insertions(+), 615 deletions(-) create mode 100644 api/src/main/java/io/jsonwebtoken/Jwe.java create mode 100644 api/src/main/java/io/jsonwebtoken/JweHeader.java create mode 100644 api/src/main/java/io/jsonwebtoken/Named.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AeadDecryptionRequest.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AeadEncryptionAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AeadEncryptionResult.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AeadIvEncryptionResult.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AeadIvRequest.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AeadRequest.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AeadSymmetricEncryptionAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AssociatedDataSource.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AuthenticationTagSource.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/CryptoException.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/CryptoMessage.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/CurveId.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/CurveIds.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/DecryptionKeyResolver.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/DefaultCurveId.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EcJwk.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EcJwkBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EcJwkBuilderFactory.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EcJwkMutator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmLocator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmName.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EncryptionResult.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/InitializationVectorSource.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/IvEncryptionResult.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/IvRequest.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/Jwk.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/JwkBuilderFactory.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/JwkMutator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfo.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoMutator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/Jwks.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyManagementAlgorithmName.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyManagementModeName.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PrivateEcJwk.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkMutator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwk.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwkMutator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PublicEcJwk.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PublicEcJwkBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PublicRsaJwk.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/RsaJwk.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/RsaJwkMutator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SymmetricEncryptionAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SymmetricJwk.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SymmetricJwkBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SymmetricJwkMutator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SymmetricKeyAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SymmetricKeySignatureAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java create mode 100644 api/src/test/groovy/io/jsonwebtoken/EncryptionAlgorithmNameTest.groovy create mode 100644 api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy create mode 100644 api/src/test/groovy/io/jsonwebtoken/security/CurveIdsTest.groovy create mode 100644 api/src/test/groovy/io/jsonwebtoken/security/KeyManagementAlgorithmNameTest.groovy create mode 100644 api/src/test/groovy/io/jsonwebtoken/security/KeyManagementModeNameTest.groovy create mode 100644 api/src/test/groovy/io/jsonwebtoken/security/UnsupportedKeyExceptionTest.groovy create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwe.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/DispatchingParser.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwe.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwt.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwtBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwk.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkValidator.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkValidator.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwk.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwkValidator.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractTypedJwkConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/CipherAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/CipherCallback.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/CipherTemplate.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResult.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAesEncryptionRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoMessage.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcJwkBuilderFactory.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocator.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionResult.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvDecryptionRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvEncryptionResult.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJweFactory.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkBuilderFactory.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwk.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwkBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateRsaJwk.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwk.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwkBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwk.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwkBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySignatureRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DirectEncryptionMode.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAgreementMode.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolver.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/EcJwkConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/EllipticCurveSignatureAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/EncryptKeyRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/EncryptedKeyManagementMode.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesEncryptionAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/GetKeyRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JwkParser.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JwkValidator.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyAgreementWithKeyWrappingMode.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyEncryptionMode.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementMode.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementModes.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyWrappingMode.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/PrivateEcJwkValidator.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/RandomEncryptedKeyMode.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/Randoms.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/Recipient.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/RsaJwkConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/RsaSignatureAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkValidator.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/TypedJwkConverter.java create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/DispatchingParserTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithmTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkValidatorTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithmTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkValidatorTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes128CbcHmacSha256Test.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes192CbcHmacSha384Test.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes256CbcHmacSha512Test.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherAlgorithmTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherTemplateTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResultTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultCryptoMessageTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocatorTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultIvEncryptionResultTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJweFactoryTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkConverterTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSymmetricJwkTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolverTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/EllipticCurveSignatureAlgorithmTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesEncryptionServiceTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithmTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateEcJwkValidatorTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/RandomsTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaSignatureAlgorithmTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/SymmetricJwkValidatorTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwk.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwkValidator.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy diff --git a/.gitignore b/.gitignore index bc177c840..436d64e99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -*.class .DS_Store # Mobile Tools for Java (J2ME) diff --git a/.travis.yml b/.travis.yml index de3c62ff1..8d4354c0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,18 +20,18 @@ before_install: - export MVN_CMD="./mvnw --no-transfer-progress" # hide verbose download messages (log spam) - | if [[ "${TRAVIS_JDK_VERSION}" == "openjdk7" ]]; then - + export MAVEN_OPTS="-Dhttps.protocols=TLSv1.2 -Xmx512m -XX:MaxPermSize=128m" export JAVA_HOME="/usr/lib/jvm/java-7-oracle" # Set JAVA_HOME to where we want to install Oracle JDK 7 export PATH="${JAVA_HOME}/bin:${PATH}" - + if [[ ! -d "${JAVA_HOME}" ]]; then # Download and install Oracle JDK 7: wget https://238dj3282as03k369.s3-us-west-1.amazonaws.com/jdk-7u80-linux-x64.tar.gz -O /tmp/jdk-7u80-linux-x64.tar.gz tar xvfz /tmp/jdk-7u80-linux-x64.tar.gz -C /tmp sudo mv /tmp/jdk1.7.0_80 "${JAVA_HOME}" fi - + # Download and install JCE Unlimited Strength Crypto policies for Oracle JDK 7: curl -q -L -C - https://238dj3282as03k369.s3-us-west-1.amazonaws.com/UnlimitedJCEPolicyJDK7.zip -o /tmp/UnlimitedJCEPolicyJDK7.zip sudo unzip -oj -d "$JAVA_HOME/jre/lib/security" /tmp/UnlimitedJCEPolicyJDK7.zip \*/\*.jar diff --git a/README.md b/README.md index cdc63e4b7..b2fd9b685 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ enforcement. * [Custom Clock](#jws-read-clock-custom) * [Decompression](#jws-read-decompression) +* [Encrypted JWTs](#jwe) * [Compression](#compression) * [Custom Compression Codec](#compression-custom) * [JSON Processor](#json) @@ -372,13 +373,13 @@ Most complexity is hidden behind a convenient and readable builder-based [fluent ```java import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.SignatureAlgorithms; import io.jsonwebtoken.security.Keys; import java.security.Key; // We need a signing key, so we'll create one just for this example. Usually // the key would be read from your application configuration instead. -Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); +Key key = SignatureAlgorithms.HS256.generateKey(); String jws = Jwts.builder().setSubject("Joe").signWith(key).compact(); ``` @@ -528,7 +529,7 @@ key algorithms - identified by the following names: * `PS384`: RSASSA-PSS using SHA-384 and MGF1 with SHA-384 * `PS512`: RSASSA-PSS using SHA-512 and MGF1 with SHA-512 -These are all represented in the `io.jsonwebtoken.SignatureAlgorithm` enum. +These are all represented in the `io.jsonwebtoken.security.SignatureAlgorithms` enum class. What's really important about these algorithms - other than their security properties - is that the JWT specification [RFC 7518, Sections 3.2 through 3.5](https://tools.ietf.org/html/rfc7518#section-3) @@ -1183,7 +1184,10 @@ how to resolve your `CompressionCodec` to decompress the JWT. Please see the [Compression](#compression) section below to see how to decompress JWTs during parsing. - + +## Encrypted JWTs + +TODO: NOTE: A128GCM, A192GCM, A256GCM algorithms require JDK 8 or BouncyCastle. ## Compression diff --git a/api/pom.xml b/api/pom.xml index 61667e7a9..4c1058ea3 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.11.3-SNAPSHOT + 0.12.0-SNAPSHOT ../pom.xml diff --git a/api/src/main/java/io/jsonwebtoken/CompressionCodec.java b/api/src/main/java/io/jsonwebtoken/CompressionCodec.java index 47e54761e..1fbf38c14 100644 --- a/api/src/main/java/io/jsonwebtoken/CompressionCodec.java +++ b/api/src/main/java/io/jsonwebtoken/CompressionCodec.java @@ -25,9 +25,11 @@ public interface CompressionCodec { /** - * The compression algorithm name to use as the JWT's {@code zip} header value. + * The algorithm name to use as the JWT's + * zip header value. * - * @return the compression algorithm name to use as the JWT's {@code zip} header value. + * @return the algorithm name to use as the JWT's + * zip header value. */ String getAlgorithmName(); diff --git a/api/src/main/java/io/jsonwebtoken/Header.java b/api/src/main/java/io/jsonwebtoken/Header.java index 021360758..c96ecdc2f 100644 --- a/api/src/main/java/io/jsonwebtoken/Header.java +++ b/api/src/main/java/io/jsonwebtoken/Header.java @@ -33,7 +33,7 @@ *

Creation

* *

It is easiest to create a {@code Header} instance by calling one of the - * {@link Jwts#header() JWTs.header()} factory methods.

+ * {@link Jwts#header() Jwts.header()} factory methods.

* * @since 0.1 */ @@ -48,6 +48,14 @@ public interface Header> extends Map { /** JWT {@code Content Type} header parameter name: "cty" */ public static final String CONTENT_TYPE = "cty"; + /** + * JWT {@code Algorithm} header parameter name: "alg". + * + * @see JWS Algorithm Header + * @see JWE Algorithm Header + */ + public static final String ALGORITHM = "alg"; + /** JWT {@code Compression Algorithm} header parameter name: "zip" */ public static final String COMPRESSION_ALGORITHM = "zip"; @@ -110,7 +118,60 @@ public interface Header> extends Map { T setContentType(String cty); /** - * Returns the JWT zip (Compression Algorithm) header value or {@code null} if not present. + * Returns the JWT {@code alg} (Algorithm) header value or {@code null} if not present. + * + *
    + *
  • If the JWT is a Signed JWT (a JWS), the + * alg (Algorithm) header parameter identifies the cryptographic algorithm used to secure the + * JWS. Consider using + * {@link io.jsonwebtoken.security.SignatureAlgorithms#forName(String) SignatureAlgorithms.forName} to + * convert this string value to a type-safe enum instance.
  • + *
  • If the JWT is an Encrypted JWT (a JWE), the + * alg (Algorithm) header parameter + * identifies the cryptographic key management algorithm used to encrypt or determine the value of the Content + * Encryption Key (CEK). The encrypted content is not usable if the alg value does not represent a + * supported algorithm, or if the recipient does not have a key that can be used with that algorithm
  • + *
+ * + * @return the {@code alg} header value or {@code null} if not present. This will always be + * {@code non-null} on validly constructed JWT instances, but could be {@code null} during construction. + * @since JJWT_RELEASE_VERSION + */ + String getAlgorithm(); + + /** + * Sets the JWT alg (Algorithm) header value. A {@code null} value will remove the property + * from the JSON map. + *
    + *
  • If the JWT is a Signed JWT (a JWS), the + * alg (Algorithm) header parameter identifies the cryptographic algorithm used to secure the + * JWS. Consider using + * {@link io.jsonwebtoken.security.SignatureAlgorithms#forName(String) SignatureAlgorithms.forName} to + * convert this string value to a type-safe enum instance.
  • + *
  • If the JWT is an Encrypted JWT (a JWE), the + * alg (Algorithm) header parameter + * identifies the cryptographic key management algorithm used to encrypt or determine the value of the Content + * Encryption Key (CEK). The encrypted content is not usable if the alg value does not represent a + * supported algorithm, or if the recipient does not have a key that can be used with that algorithm
  • + *
+ * + * @param alg the {@code alg} header value + * @return this header for method chaining + * @since JJWT_RELEASE_VERSION + */ + T setAlgorithm(String alg); + + /** + * Returns the JWT zip + * (Compression Algorithm) header parameter value or {@code null} if not present. + * + *

Compatiblity Note

+ * + *

While the JWT family of specifications only defines the zip header in the JWE + * (JSON Web Encryption) specification, JJWT will also support compression for JWS as well if you choose to use it. + * However, be aware that if you use compression when creating a JWS token, other libraries may not be able to + * parse the JWS. However, compression when creating JWE tokens should be universally accepted for any library + * that supports JWE.

* * @return the {@code zip} header parameter value or {@code null} if not present. * @since 0.6.0 @@ -118,12 +179,20 @@ public interface Header> extends Map { String getCompressionAlgorithm(); /** - * Sets the JWT zip (Compression Algorithm) header parameter value. A {@code null} value will remove + * Sets the JWT zip + * (Compression Algorithm) header parameter value. A {@code null} value will remove * the property from the JSON map. - * *

The compression algorithm is NOT part of the JWT specification * and must be used carefully since, is not expected that other libraries (including previous versions of this one) - * be able to deserialize a compressed JTW body correctly.

+ * be able to deserialize a compressed JWT body correctly.

+ * + *

Compatibility Note

+ * + *

While the JWT family of specifications only defines the zip header in the JWE + * (JSON Web Encryption) specification, JJWT will also support compression for JWS as well if you choose to use it. + * However, be aware that if you use compression when creating a JWS token, other libraries may not be able to + * parse the JWS. However, Compression when creating JWE tokens should be universally accepted for any library + * that supports JWE.

* * @param zip the JWT compression algorithm {@code zip} value or {@code null} to remove the property from the JSON map. * @return the {@code Header} instance for method chaining. diff --git a/api/src/main/java/io/jsonwebtoken/Jwe.java b/api/src/main/java/io/jsonwebtoken/Jwe.java new file mode 100644 index 000000000..b8e7f99e1 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/Jwe.java @@ -0,0 +1,12 @@ +package io.jsonwebtoken; + +/** + * @param payload type + * @since JJWT_RELEASE_VERSION + */ +public interface Jwe extends Jwt { + + byte[] getInitializationVector(); + + byte[] getAadTag(); +} diff --git a/api/src/main/java/io/jsonwebtoken/JweHeader.java b/api/src/main/java/io/jsonwebtoken/JweHeader.java new file mode 100644 index 000000000..c054efece --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/JweHeader.java @@ -0,0 +1,88 @@ +package io.jsonwebtoken; + +/** + * A JWE header. + * + * @since JJWT_RELEASE_VERSION + */ +public interface JweHeader extends Header { + + /** + * JWE Algorithm Header name: the string literal alg + */ + public static final String ALGORITHM = "alg"; + + /** + * JWE Encryption Algorithm Header name: the string literal enc + */ + public static final String ENCRYPTION_ALGORITHM = "enc"; + + /** + * JWE Compression Algorithm Header name: the string literal zip + */ + public static final String COMPRESSION_ALGORITHM = "zip"; + + /** + * JWE JWK Set URL Header name: the string literal jku + */ + public static final String JWK_SET_URL = "jku"; + + /** + * JWE JSON Web Key Header name: the string literal jwk + */ + public static final String JSON_WEB_KEY = "jwk"; + + /** + * JWE Key ID Header name: the string literal kid + */ + public static final String KEY_ID = "kid"; + + /** + * JWE X.509 URL Header name: the string literal x5u + */ + public static final String X509_URL = "x5u"; + + /** + * JWE X.509 Certificate Chain Header name: the string literal x5c + */ + public static final String X509_CERT_CHAIN = "x5c"; + + /** + * JWE X.509 Certificate SHA-1 Thumbprint Header name: the string literal x5t + */ + public static final String X509_CERT_SHA1_THUMBPRINT = "x5t"; + + /** + * JWE X.509 Certificate SHA-256 Thumbprint Header name: the string literal x5t#S256 + */ + public static final String X509_CERT_SHA256_THUMBPRINT = "x5t#S256"; + + /** + * JWE Critical Header name: the string literal crit + */ + public static final String CRITICAL = "crit"; + + /** + * Returns the JWE enc (Encryption + * Algorithm) header value or {@code null} if not present. + *

The JWE {@code enc} (encryption algorithm) Header Parameter identifies the content encryption algorithm + * used to perform authenticated encryption on the plaintext to produce the ciphertext and the JWE + * {@code Authentication Tag}.

+ * + * @return the JWE {@code enc} (Encryption Algorithm) header value or {@code null} if not present. This will + * always be {@code non-null} on validly constructed JWE instances, but could be {@code null} during construction. + */ + String getEncryptionAlgorithm(); + + /** + * Sets the JWE enc (Encryption + * Algorithm) header value. A {@code null} value will remove the property from the JSON map. + *

The JWE {@code enc} (encryption algorithm) Header Parameter identifies the content encryption algorithm + * used to perform authenticated encryption on the plaintext to produce the ciphertext and the JWE + * {@code Authentication Tag}.

+ * + * @param enc the encryption algorithm identifier + * @return this header for method chaining + */ + JweHeader setEncryptionAlgorithm(String enc); +} diff --git a/api/src/main/java/io/jsonwebtoken/JwsHeader.java b/api/src/main/java/io/jsonwebtoken/JwsHeader.java index aaf08d86b..1b8cfbe5b 100644 --- a/api/src/main/java/io/jsonwebtoken/JwsHeader.java +++ b/api/src/main/java/io/jsonwebtoken/JwsHeader.java @@ -16,74 +16,63 @@ package io.jsonwebtoken; /** - * A JWS header. + * A JWS header. * - * @param header type * @since 0.1 */ -public interface JwsHeader> extends Header { +public interface JwsHeader extends Header { - /** JWS {@code Algorithm} header parameter name: "alg" */ + /** + * JWS Algorithm Header name: the string literal alg + */ public static final String ALGORITHM = "alg"; - /** JWS {@code JWT Set URL} header parameter name: "jku" */ + /** + * JWS JWK Set URL Header name: the string literal jku + */ public static final String JWK_SET_URL = "jku"; - /** JWS {@code JSON Web Key} header parameter name: "jwk" */ + /** + * JWS JSON Web Key Header name: the string literal jwk + */ public static final String JSON_WEB_KEY = "jwk"; - /** JWS {@code Key ID} header parameter name: "kid" */ + /** + * JWS Key ID Header name: the string literal kid + */ public static final String KEY_ID = "kid"; - /** JWS {@code X.509 URL} header parameter name: "x5u" */ + /** + * JWS X.509 URL Header name: the string literal x5u + */ public static final String X509_URL = "x5u"; - /** JWS {@code X.509 Certificate Chain} header parameter name: "x5c" */ + /** + * JWS X.509 Certificate Chain Header name: the string literal x5c + */ public static final String X509_CERT_CHAIN = "x5c"; - /** JWS {@code X.509 Certificate SHA-1 Thumbprint} header parameter name: "x5t" */ + /** + * JWS X.509 Certificate SHA-1 Thumbprint Header name: the string literal x5t + */ public static final String X509_CERT_SHA1_THUMBPRINT = "x5t"; - /** JWS {@code X.509 Certificate SHA-256 Thumbprint} header parameter name: "x5t#S256" */ - public static final String X509_CERT_SHA256_THUMBPRINT = "x5t#S256"; - - /** JWS {@code Critical} header parameter name: "crit" */ - public static final String CRITICAL = "crit"; - /** - * Returns the JWS - * alg (algorithm) header value or {@code null} if not present. - * - *

The algorithm header parameter identifies the cryptographic algorithm used to secure the JWS. Consider - * using {@link io.jsonwebtoken.SignatureAlgorithm#forName(String) SignatureAlgorithm.forName} to convert this - * string value to a type-safe enum instance.

- * - * @return the JWS {@code alg} header value or {@code null} if not present. This will always be - * {@code non-null} on validly constructed JWS instances, but could be {@code null} during construction. + * JWS X.509 Certificate SHA-256 Thumbprint Header name: the string literal x5t#S256 */ - String getAlgorithm(); + public static final String X509_CERT_SHA256_THUMBPRINT = "x5t#S256"; /** - * Sets the JWT - * alg (Algorithm) header value. A {@code null} value will remove the property from the JSON map. - * - *

The algorithm header parameter identifies the cryptographic algorithm used to secure the JWS. Consider - * using a type-safe {@link io.jsonwebtoken.SignatureAlgorithm SignatureAlgorithm} instance and using its - * {@link io.jsonwebtoken.SignatureAlgorithm#getValue() value} as the argument to this method.

- * - * @param alg the JWS {@code alg} header value or {@code null} to remove the property from the JSON map. - * @return the {@code Header} instance for method chaining. + * JWS Critical Header name: the string literal crit */ - T setAlgorithm(String alg); + public static final String CRITICAL = "crit"; /** - * Returns the JWS + * Returns the JWS * kid (Key ID) header value or {@code null} if not present. - * *

The keyId header parameter is a hint indicating which key was used to secure the JWS. This parameter allows * originators to explicitly signal a change of key to recipients. The structure of the keyId value is * unspecified.

- * *

When used with a JWK, the keyId value is used to match a JWK {@code keyId} parameter value.

* * @return the JWS {@code kid} header value or {@code null} if not present. @@ -91,17 +80,15 @@ public interface JwsHeader> extends Header { String getKeyId(); /** - * Sets the JWT + * Sets the JWT * kid (Key ID) header value. A {@code null} value will remove the property from the JSON map. - * *

The keyId header parameter is a hint indicating which key was used to secure the JWS. This parameter allows * originators to explicitly signal a change of key to recipients. The structure of the keyId value is * unspecified.

- * *

When used with a JWK, the keyId value is used to match a JWK {@code keyId} parameter value.

* * @param kid the JWS {@code kid} header value or {@code null} to remove the property from the JSON map. * @return the {@code Header} instance for method chaining. */ - T setKeyId(String kid); + JwsHeader setKeyId(String kid); } diff --git a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java index 238cb08ba..770539517 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -21,7 +21,11 @@ import io.jsonwebtoken.io.Serializer; import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureAlgorithms; + import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; import java.util.Date; import java.util.Map; @@ -32,7 +36,27 @@ */ public interface JwtBuilder extends ClaimsMutator { - //replaces any existing header with the specified header. + /** + * Sets the JCA Provider to use during cryptographic signing or encryption operations, or {@code null} if the + * JCA subsystem preferred provider should be used. + * + * @param provider the JCA Provider to use during cryptographic signing or encryption operations, or {@code null} if the + * JCA subsystem preferred provider should be used. + * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtBuilder setProvider(Provider provider); + + /** + * Sets the {@link SecureRandom} to use during cryptographic signing or encryption operations, or {@code null} if + * a default {@link SecureRandom} should be used. + * + * @param secureRandom the {@link SecureRandom} to use during cryptographic signing or encryption operations, or + * {@code null} if a default {@link SecureRandom} should be used. + * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtBuilder setSecureRandom(SecureRandom secureRandom); /** * Sets (and replaces) any existing header with the specified header. If you do not want to replace the existing @@ -41,7 +65,7 @@ public interface JwtBuilder extends ClaimsMutator { * @param header the header to set (and potentially replace any existing header). * @return the builder for method chaining. */ - JwtBuilder setHeader(Header header); + JwtBuilder setHeader(Header header); //replaces any existing header with the specified header. /** * Sets (and replaces) any existing header with the specified header. If you do not want to replace the existing @@ -345,9 +369,9 @@ public interface JwtBuilder extends ClaimsMutator { /** * Signs the constructed JWT with the specified key using the key's - * {@link SignatureAlgorithm#forSigningKey(Key) recommended signature algorithm}, producing a JWS. If the + * {@link SignatureAlgorithms#forSigningKey(Key) recommended signature algorithm}, producing a JWS. If the * recommended signature algorithm isn't sufficient for your needs, consider using - * {@link #signWith(Key, SignatureAlgorithm)} instead. + * {@link #signWith(Key, io.jsonwebtoken.security.SignatureAlgorithm)} instead. * *

If you are looking to invoke this method with a byte array that you are confident may be used for HMAC-SHA * algorithms, consider using {@link Keys Keys}.{@link Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor(bytes)} to @@ -356,8 +380,8 @@ public interface JwtBuilder extends ClaimsMutator { * @param key the key to use for signing * @return the builder instance for method chaining. * @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification as - * described by {@link SignatureAlgorithm#forSigningKey(Key)}. - * @see #signWith(Key, SignatureAlgorithm) + * described by {@link SignatureAlgorithms#forSigningKey(Key)}. + * @see #signWith(Key, io.jsonwebtoken.security.SignatureAlgorithm) * @since 0.10.0 */ JwtBuilder signWith(Key key) throws InvalidKeyException; @@ -368,17 +392,19 @@ public interface JwtBuilder extends ClaimsMutator { *

Deprecation Notice: Deprecated as of 0.10.0

* *

Use {@link Keys Keys}.{@link Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor(bytes)} to - * obtain the {@code Key} and then invoke {@link #signWith(Key)} or {@link #signWith(Key, SignatureAlgorithm)}.

+ * obtain the {@code Key} and then invoke {@link #signWith(Key)} or + * {@link #signWith(Key, io.jsonwebtoken.security.SignatureAlgorithm)}.

* *

This method will be removed in the 1.0 release.

* * @param alg the JWS algorithm to use to digitally sign the JWT, thereby producing a JWS. * @param secretKey the algorithm-specific signing key to use to digitally sign the JWT. * @return the builder for method chaining. - * @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification as - * described by {@link SignatureAlgorithm#forSigningKey(Key)}. + * @throws InvalidKeyException if the Key is insufficient for the specified algorithm or explicitly disallowed by + * the JWT specification. * @deprecated as of 0.10.0: use {@link Keys Keys}.{@link Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor(bytes)} to - * obtain the {@code Key} and then invoke {@link #signWith(Key)} or {@link #signWith(Key, SignatureAlgorithm)}. + * obtain the {@code Key} and then invoke {@link #signWith(Key)} or + * {@link #signWith(Key, io.jsonwebtoken.security.SignatureAlgorithm)}. * This method will be removed in the 1.0 release. */ @Deprecated @@ -402,7 +428,7 @@ public interface JwtBuilder extends ClaimsMutator { *

{@code String base64EncodedSecretKey = base64Encode(secretKeyBytes);}

* *

However, a non-trivial number of JJWT users were confused by the method signature and attempted to - * use raw password strings as the key argument - for example {@code signWith(HS256, myPassword)} - which is + * use raw password strings as the key argument - for example {@code with(HS256, myPassword)} - which is * almost always incorrect for cryptographic hashes and can produce erroneous or insecure results.

* *

See this @@ -414,7 +440,7 @@ public interface JwtBuilder extends ClaimsMutator { *


      * byte[] keyBytes = {@link Decoders Decoders}.{@link Decoders#BASE64 BASE64}.{@link Decoder#decode(Object) decode(base64EncodedSecretKey)};
      * Key key = {@link Keys Keys}.{@link Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor(keyBytes)};
-     * jwtBuilder.signWith(key); //or {@link #signWith(Key, SignatureAlgorithm)}
+     * jwtBuilder.with(key); //or {@link #signWith(Key, SignatureAlgorithm)}
      * 
*

* @@ -452,6 +478,12 @@ public interface JwtBuilder extends ClaimsMutator { JwtBuilder signWith(SignatureAlgorithm alg, Key key) throws InvalidKeyException; /** + *

Deprecation Notice

+ *

This has been deprecated since JJWT_RELEASE_VERSION. Use + * {@link #signWith(Key, io.jsonwebtoken.security.SignatureAlgorithm)} instead.. Standard JWA algorithms + * are represented as instances of this new interface in the {@link io.jsonwebtoken.security.SignatureAlgorithms} + * enum class.

+ * * Signs the constructed JWT with the specified key using the specified algorithm, producing a JWS. * *

It is typically recommended to call the {@link #signWith(Key)} instead for simplicity. @@ -465,9 +497,29 @@ public interface JwtBuilder extends ClaimsMutator { * the specified algorithm. * @see #signWith(Key) * @since 0.10.0 + * @deprecated since JJWT_RELEASE_VERSION to use a more the more flexible {@link io.jsonwebtoken.security.SignatureAlgorithm}. */ + @Deprecated JwtBuilder signWith(Key key, SignatureAlgorithm alg) throws InvalidKeyException; + /** + * Signs the constructed JWT with the specified key using the specified algorithm, producing a JWS. + * + *

It is typically recommended to call the {@link #signWith(Key)} instead for simplicity. + * However, this method can be useful if the recommended algorithm heuristics do not meet your needs or if + * you want explicit control over the signature algorithm used with the specified key.

+ * + * @param key the signing key to use to digitally sign the JWT. + * @param alg the JWS algorithm to use with the key to digitally sign the JWT, thereby producing a JWS. + * @return the builder for method chaining. + * @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification for + * the specified algorithm. + * @see #signWith(Key) + * @see SignatureAlgorithms#forSigningKey(Key) + * @since JJWT_RELEASE_VERSION + */ + JwtBuilder signWith(Key key, io.jsonwebtoken.security.SignatureAlgorithm alg) throws InvalidKeyException; + /** * Compresses the JWT body using the specified {@link CompressionCodec}. * @@ -477,7 +529,7 @@ public interface JwtBuilder extends ClaimsMutator { * *

Compatibility Warning

* - *

The JWT family of specifications defines compression only for JWE (Json Web Encryption) + *

The JWT family of specifications defines compression only for JWE (JSON Web Encryption) * tokens. Even so, JJWT will also support compression for JWS tokens as well if you choose to use it. * However, be aware that if you use compression when creating a JWS token, other libraries may not be able to * parse that JWS token. When using compression for JWS tokens, be sure that all parties accessing the diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index 2e8dd8794..0e9d85ae9 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -19,6 +19,8 @@ import io.jsonwebtoken.io.Deserializer; import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; import java.util.Date; import java.util.Map; @@ -35,6 +37,17 @@ */ public interface JwtParserBuilder { + /** + * Sets the JCA Provider to use during cryptographic signature and decryption operations, or {@code null} if the + * JCA subsystem preferred provider should be used. + * + * @param provider the JCA Provider to use during cryptographic signature and decryption operations, or {@code null} + * if the JCA subsystem preferred provider should be used. + * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtParserBuilder setProvider(Provider provider); + /** * Ensures that the specified {@code jti} exists in the parsed JWT. If missing or if the parsed * value does not equal the specified value, an exception will be thrown indicating that the diff --git a/api/src/main/java/io/jsonwebtoken/Jwts.java b/api/src/main/java/io/jsonwebtoken/Jwts.java index 80de65a44..d9f62db44 100644 --- a/api/src/main/java/io/jsonwebtoken/Jwts.java +++ b/api/src/main/java/io/jsonwebtoken/Jwts.java @@ -76,6 +76,30 @@ public static JwsHeader jwsHeader(Map header) { return Classes.newInstance("io.jsonwebtoken.impl.DefaultJwsHeader", MAP_ARG, header); } + /** + * Returns a new {@link JweHeader} instance suitable for encrypted JWTs (aka 'JWE's). + * + * @return a new {@link JweHeader} instance suitable for encrypted JWTs (aka 'JWE's). + * @see JwtBuilder#setHeader(Header) + * @since JJWT_RELEASE_VERSION + */ + public static JweHeader jweHeader() { + return Classes.newInstance("io.jsonwebtoken.impl.DefaultJweHeader"); + } + + /** + * Returns a new {@link JweHeader} instance suitable for encrypted JWTs (aka 'JWE's), populated with the + * specified name/value pairs. + * + * @return a new {@link JweHeader} instance suitable for encrypted JWTs (aka 'JWE's), populated with the + * specified name/value pairs. + * @see JwtBuilder#setHeader(Header) + * @since JJWT_RELEASE_VERSION + */ + public static JweHeader jweHeader(Map header) { + return Classes.newInstance("io.jsonwebtoken.impl.DefaultJweHeader", MAP_ARG, header); + } + /** * Returns a new {@link Claims} instance to be used as a JWT body. * diff --git a/api/src/main/java/io/jsonwebtoken/Named.java b/api/src/main/java/io/jsonwebtoken/Named.java new file mode 100644 index 000000000..0bd6064ce --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/Named.java @@ -0,0 +1,14 @@ +package io.jsonwebtoken; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface Named { + + /** + * Returns the string name of the associated object. + * + * @return the string name of the associated object. + */ + String getName(); +} diff --git a/api/src/main/java/io/jsonwebtoken/RequiredTypeException.java b/api/src/main/java/io/jsonwebtoken/RequiredTypeException.java index eeb60d308..44124615d 100644 --- a/api/src/main/java/io/jsonwebtoken/RequiredTypeException.java +++ b/api/src/main/java/io/jsonwebtoken/RequiredTypeException.java @@ -16,12 +16,13 @@ package io.jsonwebtoken; /** - * Exception thrown when {@link Claims#get(String, Class)} is called and the value does not match the type of the - * {@code Class} argument. + * Exception thrown when attempting to obtain a value from a JWT or JWK and the existing value does not match the + * expected type. * * @since 0.6 */ public class RequiredTypeException extends JwtException { + public RequiredTypeException(String message) { super(message); } diff --git a/api/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java index 9f646502d..1bdafddaf 100644 --- a/api/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java @@ -34,7 +34,9 @@ * JSON Web Algorithms specification. * * @since 0.1 + * @deprecated since JJWT_RELEASE_VERSION; use {@link io.jsonwebtoken.security.SignatureAlgorithms} instead. */ +@Deprecated public enum SignatureAlgorithm { /** diff --git a/api/src/main/java/io/jsonwebtoken/SignatureException.java b/api/src/main/java/io/jsonwebtoken/SignatureException.java index e98b4c722..12a2e92d7 100644 --- a/api/src/main/java/io/jsonwebtoken/SignatureException.java +++ b/api/src/main/java/io/jsonwebtoken/SignatureException.java @@ -15,7 +15,7 @@ */ package io.jsonwebtoken; -import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.CryptoException; /** * Exception indicating that either calculating a signature or verifying an existing signature of a JWT failed. @@ -24,7 +24,7 @@ * @deprecated in favor of {@link io.jsonwebtoken.security.SecurityException}; this class will be removed before 1.0 */ @Deprecated -public class SignatureException extends SecurityException { +public class SignatureException extends CryptoException { public SignatureException(String message) { super(message); diff --git a/api/src/main/java/io/jsonwebtoken/lang/Assert.java b/api/src/main/java/io/jsonwebtoken/lang/Assert.java index 7c9e7c367..ab6dd2fd4 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Assert.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Assert.java @@ -77,10 +77,11 @@ public static void isNull(Object object) { * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object is null */ - public static void notNull(Object object, String message) { + public static T notNull(T object, String message) { if (object == null) { throw new IllegalArgumentException(message); } + return object; } /** @@ -196,10 +197,11 @@ public static void notEmpty(Object[] array) { notEmpty(array, "[Assertion failed] - this array must not be empty: it must contain at least 1 element"); } - public static void notEmpty(byte[] array, String msg) { + public static byte[] notEmpty(byte[] array, String msg) { if (Objects.isEmpty(array)) { throw new IllegalArgumentException(msg); } + return array; } /** diff --git a/api/src/main/java/io/jsonwebtoken/lang/Collections.java b/api/src/main/java/io/jsonwebtoken/lang/Collections.java index d775a002a..167f23f2b 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Collections.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Collections.java @@ -28,6 +28,17 @@ public final class Collections { private Collections(){} //prevent instantiation + public static List emptyList() { + return java.util.Collections.emptyList(); + } + + public static List of(T... elements) { + if (elements == null || elements.length == 0) { + return java.util.Collections.emptyList(); + } + return java.util.Collections.unmodifiableList(Arrays.asList(elements)); + } + /** * Return true if the supplied Collection is null * or empty. Otherwise, return false. diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadDecryptionRequest.java b/api/src/main/java/io/jsonwebtoken/security/AeadDecryptionRequest.java new file mode 100644 index 000000000..4f9fa8261 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AeadDecryptionRequest.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AeadDecryptionRequest extends AeadRequest, AuthenticationTagSource { + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadEncryptionAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AeadEncryptionAlgorithm.java new file mode 100644 index 000000000..f903377f1 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AeadEncryptionAlgorithm.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.security; + +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AeadEncryptionAlgorithm, ERes extends AeadEncryptionResult, DReq extends AeadDecryptionRequest> extends EncryptionAlgorithm { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadEncryptionResult.java b/api/src/main/java/io/jsonwebtoken/security/AeadEncryptionResult.java new file mode 100644 index 000000000..292810620 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AeadEncryptionResult.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AeadEncryptionResult extends EncryptionResult, AuthenticationTagSource { + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadIvEncryptionResult.java b/api/src/main/java/io/jsonwebtoken/security/AeadIvEncryptionResult.java new file mode 100644 index 000000000..8524f1d57 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AeadIvEncryptionResult.java @@ -0,0 +1,7 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AeadIvEncryptionResult extends IvEncryptionResult, AeadEncryptionResult { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadIvRequest.java b/api/src/main/java/io/jsonwebtoken/security/AeadIvRequest.java new file mode 100644 index 000000000..05159ef23 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AeadIvRequest.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.security; + +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AeadIvRequest extends IvRequest, AeadDecryptionRequest { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java b/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java new file mode 100644 index 000000000..8112813fe --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AeadRequest extends CryptoRequest, AssociatedDataSource { + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadSymmetricEncryptionAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AeadSymmetricEncryptionAlgorithm.java new file mode 100644 index 000000000..4e74987f7 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AeadSymmetricEncryptionAlgorithm.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.security; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AeadSymmetricEncryptionAlgorithm extends + SymmetricEncryptionAlgorithm, AeadIvEncryptionResult, AeadIvRequest>, + AeadEncryptionAlgorithm, AeadIvEncryptionResult, AeadIvRequest> { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSource.java b/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSource.java new file mode 100644 index 000000000..0acd5555d --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSource.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AssociatedDataSource { + + byte[] getAssociatedData(); + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyAlgorithm.java new file mode 100644 index 000000000..84624225f --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyAlgorithm.java @@ -0,0 +1,16 @@ +package io.jsonwebtoken.security; + +import java.security.KeyPair; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AsymmetricKeyAlgorithm { + + /** + * Generates a new secure-random key pair with a key length suitable for this Algorithm. + * + * @return a new secure-random key pair with a key length suitable for this Algorithm. + */ + KeyPair generateKeyPair(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java new file mode 100644 index 000000000..93279d698 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java @@ -0,0 +1,7 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AsymmetricKeySignatureAlgorithm extends SignatureAlgorithm, AsymmetricKeyAlgorithm { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AuthenticationTagSource.java b/api/src/main/java/io/jsonwebtoken/security/AuthenticationTagSource.java new file mode 100644 index 000000000..b7e1f55ec --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AuthenticationTagSource.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AuthenticationTagSource { + + byte[] getAuthenticationTag(); + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/CryptoException.java b/api/src/main/java/io/jsonwebtoken/security/CryptoException.java new file mode 100644 index 000000000..6d3b26fb3 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/CryptoException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class CryptoException extends SecurityException { + + public CryptoException(String message) { + super(message); + } + + public CryptoException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/CryptoMessage.java b/api/src/main/java/io/jsonwebtoken/security/CryptoMessage.java new file mode 100644 index 000000000..9d904c471 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/CryptoMessage.java @@ -0,0 +1,10 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface CryptoMessage { + + T getData(); //plaintext, ciphertext, or Key for key wrap algorithms + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java b/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java new file mode 100644 index 000000000..489dfe01b --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java @@ -0,0 +1,36 @@ +package io.jsonwebtoken.security; + +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface CryptoRequest extends CryptoMessage { + + /** + * Returns the JCA provider that should be used for cryptographic operations during the request or + * {@code null} if the JCA subsystem preferred provider should be used. + * + * @return the JCA provider that should be used for cryptographic operations during the request or + * {@code null} if the JCA subsystem preferred provider should be used. + */ + Provider getProvider(); + + /** + * Returns the {@code SecureRandom} to use when performing cryptographic operations during the request, or + * {@code null} if a default {@link SecureRandom} should be used. + * + * @return the {@code SecureRandom} to use when performing cryptographic operations during the request, or + * {@code null} if a default {@link SecureRandom} should be used. + */ + SecureRandom getSecureRandom(); + + /** + * Returns the key to use for signing, wrapping, encryption or decryption depending on the type of request. + * + * @return the key to use for signing, wrapping, encryption or decryption depending on the type of request. + */ + K getKey(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/CurveId.java b/api/src/main/java/io/jsonwebtoken/security/CurveId.java new file mode 100644 index 000000000..415783359 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/CurveId.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface CurveId { + + String toString(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/CurveIds.java b/api/src/main/java/io/jsonwebtoken/security/CurveIds.java new file mode 100644 index 000000000..fffefe506 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/CurveIds.java @@ -0,0 +1,44 @@ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Maps; +import io.jsonwebtoken.lang.Strings; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class CurveIds { + + public static final CurveId P256 = new DefaultCurveId("P-256"); + public static final CurveId P384 = new DefaultCurveId("P-384"); + public static final CurveId P521 = new DefaultCurveId("P-521"); // yes, this is supposed to be 521 and not 512 + + private static final Map STANDARD_IDS = Collections.unmodifiableMap(Maps + .of(P256.toString(), P256) + .and(P384.toString(), P384) + .and(P521.toString(), P521) + .build()); + + private static final Set STANDARD_IDS_SET = + Collections.unmodifiableSet(new LinkedHashSet<>(STANDARD_IDS.values())); + + public static Set values() { + return STANDARD_IDS_SET; + } + + public static boolean isStandard(CurveId curveId) { + return curveId != null && STANDARD_IDS.containsKey(curveId.toString()); + } + + public static CurveId forValue(String value) { + value = Strings.clean(value); + Assert.hasText(value, "value argument cannot be null or empty."); + CurveId std = STANDARD_IDS.get(value); + return std != null ? std : new DefaultCurveId(value); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyResolver.java b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyResolver.java new file mode 100644 index 000000000..9e2cbce78 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyResolver.java @@ -0,0 +1,19 @@ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.JweHeader; + +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface DecryptionKeyResolver { + + /** + * Returns the decryption key that should be used to decrypt a corresponding JWE's Ciphertext (payload). + * + * @param header the JWE header to inspect to determine which decryption key should be used + * @return the decryption key that should be used to decrypt a corresponding JWE's Ciphertext (payload). + */ + Key resolveDecryptionKey(JweHeader header); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/DefaultCurveId.java b/api/src/main/java/io/jsonwebtoken/security/DefaultCurveId.java new file mode 100644 index 000000000..479f768fb --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/DefaultCurveId.java @@ -0,0 +1,33 @@ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; + +/** + * @since JJWT_RELEASE_VERSION + */ +final class DefaultCurveId implements CurveId { + + private final String id; + + DefaultCurveId(String id) { + id = Strings.clean(id); + Assert.hasText(id, "id argument cannot be null or empty."); + this.id = id; + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj == this || (obj instanceof CurveId && obj.toString().equals(this.id)); + } + + @Override + public String toString() { + return id; + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcJwk.java b/api/src/main/java/io/jsonwebtoken/security/EcJwk.java new file mode 100644 index 000000000..eba5b1045 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EcJwk.java @@ -0,0 +1,13 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface EcJwk extends Jwk, EcJwkMutator { + + CurveId getCurveId(); + + String getX(); + + String getY(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/EcJwkBuilder.java new file mode 100644 index 000000000..c0ae666f8 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EcJwkBuilder.java @@ -0,0 +1,7 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface EcJwkBuilder extends JwkBuilder, EcJwkMutator { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcJwkBuilderFactory.java b/api/src/main/java/io/jsonwebtoken/security/EcJwkBuilderFactory.java new file mode 100644 index 000000000..f6666e02f --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EcJwkBuilderFactory.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface EcJwkBuilderFactory { + + PublicEcJwkBuilder publicKey(); + + PrivateEcJwkBuilder privateKey(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcJwkMutator.java b/api/src/main/java/io/jsonwebtoken/security/EcJwkMutator.java new file mode 100644 index 000000000..fad155ad9 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EcJwkMutator.java @@ -0,0 +1,13 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface EcJwkMutator extends JwkMutator { + + T setCurveId(CurveId curveId); + + T setX(String x); + + T setY(String y); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithm.java new file mode 100644 index 000000000..a3ba789c2 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithm.java @@ -0,0 +1,15 @@ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.Named; + +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface EncryptionAlgorithm, ERes extends EncryptionResult, DReq extends CryptoRequest> extends Named { + + ERes encrypt(EReq request) throws CryptoException, KeyException; + + byte[] decrypt(DReq request) throws CryptoException, KeyException; +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmLocator.java b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmLocator.java new file mode 100644 index 000000000..ec44fd8b9 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmLocator.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.JweHeader; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface EncryptionAlgorithmLocator { + + EncryptionAlgorithm getEncryptionAlgorithm(JweHeader jweHeader); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmName.java b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmName.java new file mode 100644 index 000000000..9b482d3bf --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmName.java @@ -0,0 +1,75 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public enum EncryptionAlgorithmName { + + A128CBC_HS256("A128CBC-HS256", "AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.3", "AES/CBC/PKCS5Padding"), + A192CBC_HS384("A192CBC-HS384", "AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.4", "AES/CBC/PKCS5Padding"), + A256CBC_HS512("A256CBC-HS512", "AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.5", "AES/CBC/PKCS5Padding"), + A128GCM("A128GCM", "AES GCM using 128-bit key", "AES/GCM/NoPadding"), + A192GCM("A192GCM", "AES GCM using 192-bit key", "AES/GCM/NoPadding"), + A256GCM("A256GCM", "AES GCM using 256-bit key", "AES/GCM/NoPadding"); + + private final String name; + private final String description; + private final String jcaName; + + EncryptionAlgorithmName(String name, String description, String jcaName) { + this.name = name; + this.description = description; + this.jcaName = jcaName; + } + + /** + * Returns the JWA algorithm name constant. + * + * @return the JWA algorithm name constant. + */ + public String getValue() { + return name; + } + + /** + * Returns the JWA algorithm description. + * + * @return the JWA algorithm description. + */ + public String getDescription() { + return description; + } + + /** + * Returns the name of the JCA algorithm used to encrypt or decrypt JWE content. + * + * @return the name of the JCA algorithm used to encrypt or decrypt JWE content. + */ + public String getJcaName() { + return jcaName; + } + + /** + * Returns the corresponding {@code EncryptionAlgorithmName} enum instance based on a + * case-insensitive name comparison of the specified JWE enc value. + * + * @param name the case-insensitive JWE enc header value. + * @return Returns the corresponding {@code EncryptionAlgorithmName} enum instance based on a + * case-insensitive name comparison of the specified JWE enc value. + * @throws IllegalArgumentException if the specified value does not match any JWE {@code EncryptionAlgorithmName} value. + */ + public static EncryptionAlgorithmName forName(String name) throws IllegalArgumentException { + for (EncryptionAlgorithmName enc : values()) { + if (enc.getValue().equalsIgnoreCase(name)) { + return enc; + } + } + + throw new IllegalArgumentException("Unsupported JWE Content Encryption Algorithm name: " + name); + } + + @Override + public String toString() { + return name; + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java new file mode 100644 index 000000000..dbaeecb58 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java @@ -0,0 +1,105 @@ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Classes; +import io.jsonwebtoken.lang.Maps; +import io.jsonwebtoken.lang.Strings; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +/** + * @since JJWT_RELEASE_VERSION + */ +public final class EncryptionAlgorithms { + + //prevent instantiation + private EncryptionAlgorithms() { + } + + private static final Class MAC_CLASS = Classes.forName("io.jsonwebtoken.impl.security.MacSignatureAlgorithm"); + private static final String HMAC = "io.jsonwebtoken.impl.security.HmacAesEncryptionAlgorithm"; + private static final Class[] HMAC_ARGS = new Class[]{String.class, MAC_CLASS}; + + private static final String GCM = "io.jsonwebtoken.impl.security.GcmAesEncryptionAlgorithm"; + private static final Class[] GCM_ARGS = new Class[]{String.class, int.class}; + + private static AeadSymmetricEncryptionAlgorithm hmac(int keyLength) { + int digestLength = keyLength * 2; + String name = "A" + keyLength + "CBC-HS" + digestLength; + SignatureAlgorithm macSigAlg = Classes.newInstance(SignatureAlgorithms.HMAC, SignatureAlgorithms.HMAC_ARGS, name, "HmacSHA" + digestLength, keyLength); + return Classes.newInstance(HMAC, HMAC_ARGS, name, macSigAlg); + } + + private static AeadSymmetricEncryptionAlgorithm gcm(int keyLength) { + String name = "A" + keyLength + "GCM"; + return Classes.newInstance(GCM, GCM_ARGS, name, keyLength); + } + + /** + * AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm, as defined by + * RFC 7518, Section 5.2.3. This algorithm + * requires a 256 bit (32 byte) key. + */ + public static final AeadSymmetricEncryptionAlgorithm A128CBC_HS256 = hmac(128); + + /** + * AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm, as defined by + * RFC 7518, Section 5.2.4. This algorithm + * requires a 384 bit (48 byte) key. + */ + public static final AeadSymmetricEncryptionAlgorithm A192CBC_HS384 = hmac(192); + + /** + * AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm, as defined by + * RFC 7518, Section 5.2.5. This algorithm + * requires a 512 bit (64 byte) key. + */ + public static final AeadSymmetricEncryptionAlgorithm A256CBC_HS512 = hmac(256); + + /** + * "AES GCM using 128-bit key" as defined by + * RFC 7518, Section 5.3. This algorithm requires + * a 128 bit (16 byte) key. + */ + public static final AeadSymmetricEncryptionAlgorithm A128GCM = gcm(128); + + /** + * "AES GCM using 192-bit key" as defined by + * RFC 7518, Section 5.3. This algorithm requires + * a 192 bit (24 byte) key. + */ + public static final AeadSymmetricEncryptionAlgorithm A192GCM = gcm(192); + + /** + * "AES GCM using 256-bit key" as defined by + * RFC 7518, Section 5.3. This algorithm requires + * a 256 bit (32 byte) key. + */ + public static final AeadSymmetricEncryptionAlgorithm A256GCM = gcm(256); + + private static final Map SYMMETRIC_VALUES_BY_NAME = Collections.unmodifiableMap(Maps + .of(A128CBC_HS256.getName(), A128CBC_HS256) + .and(A192CBC_HS384.getName(), A192CBC_HS384) + .and(A256CBC_HS512.getName(), A256CBC_HS512) + .and(A128GCM.getName(), A128GCM) + .and(A192GCM.getName(), A192GCM) + .and(A256GCM.getName(), A256GCM) + .build()); + + public static EncryptionAlgorithm forName(String name) { + Assert.hasText(name, "name cannot be null or empty."); + EncryptionAlgorithm alg = SYMMETRIC_VALUES_BY_NAME.get(name.toUpperCase()); + if (alg == null) { + String msg = "'" + name + "' is not a JWE specification standard name. The standard names are: " + + Strings.collectionToCommaDelimitedString(SYMMETRIC_VALUES_BY_NAME.keySet()); + throw new IllegalArgumentException(msg); + } + return alg; + } + + public static Collection symmetric() { + return SYMMETRIC_VALUES_BY_NAME.values(); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptionResult.java b/api/src/main/java/io/jsonwebtoken/security/EncryptionResult.java new file mode 100644 index 000000000..62b76f6ca --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EncryptionResult.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface EncryptionResult { + + byte[] getCiphertext(); + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSource.java b/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSource.java new file mode 100644 index 000000000..47f1032ed --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSource.java @@ -0,0 +1,16 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface InitializationVectorSource { + + /** + * Returns the secure-random initialization vector used during encryption that must be presented in order + * to decrypt. + * + * @return the secure-random initialization vector used during encryption that must be presented in order + * to decrypt. + */ + byte[] getInitializationVector(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java b/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java index 2e3b84b8a..5d6b4183d 100644 --- a/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java @@ -23,4 +23,8 @@ public class InvalidKeyException extends KeyException { public InvalidKeyException(String message) { super(message); } + + public InvalidKeyException(String msg, Exception cause) { + super(msg, cause); + } } diff --git a/api/src/main/java/io/jsonwebtoken/security/IvEncryptionResult.java b/api/src/main/java/io/jsonwebtoken/security/IvEncryptionResult.java new file mode 100644 index 000000000..ac75f30fb --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/IvEncryptionResult.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface IvEncryptionResult extends EncryptionResult, InitializationVectorSource { + + byte[] compact(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/IvRequest.java b/api/src/main/java/io/jsonwebtoken/security/IvRequest.java new file mode 100644 index 000000000..e4dfbbcc1 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/IvRequest.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.security; + +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface IvRequest extends CryptoRequest, InitializationVectorSource { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/Jwk.java b/api/src/main/java/io/jsonwebtoken/security/Jwk.java new file mode 100644 index 000000000..f527978cf --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/Jwk.java @@ -0,0 +1,30 @@ +package io.jsonwebtoken.security; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface Jwk extends Map, JwkMutator { + + String getType(); + + String getUse(); + + Set getOperations(); + + String getAlgorithm(); + + String getId(); + + URI getX509Url(); + + List getX509CertficateChain(); + + String getX509CertificateSha1Thumbprint(); + + String getX509CertificateSha256Thumbprint(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java new file mode 100644 index 000000000..c5b09062f --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java @@ -0,0 +1,10 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface JwkBuilder extends JwkMutator { + + K build(); + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilderFactory.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilderFactory.java new file mode 100644 index 000000000..406408dff --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/JwkBuilderFactory.java @@ -0,0 +1,12 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface JwkBuilderFactory { + + EcJwkBuilderFactory ellipticCurve(); + + SymmetricJwkBuilder symmetric(); + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkMutator.java b/api/src/main/java/io/jsonwebtoken/security/JwkMutator.java new file mode 100644 index 000000000..ba8ab1cc1 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/JwkMutator.java @@ -0,0 +1,27 @@ +package io.jsonwebtoken.security; + +import java.net.URI; +import java.util.List; +import java.util.Set; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface JwkMutator { + + T setUse(String use); + + T setOperations(Set ops); + + T setAlgorithm(String alg); + + T setId(String id); + + T setX509Url(URI uri); + + T setX509CertificateChain(List chain); + + T setX509CertificateSha1Thumbprint(String thumbprint); + + T setX509CertificateSha256Thumbprint(String thumbprint); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfo.java b/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfo.java new file mode 100644 index 000000000..55eb75030 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfo.java @@ -0,0 +1,16 @@ +package io.jsonwebtoken.security; + +import java.util.Map; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface JwkRsaPrimeInfo extends Map, JwkRsaPrimeInfoMutator { + + String getPrime(); + + String getCrtExponent(); + + String getCrtCoefficient(); + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoBuilder.java new file mode 100644 index 000000000..4307aa57d --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoBuilder.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface JwkRsaPrimeInfoBuilder extends JwkRsaPrimeInfoMutator { + + JwkRsaPrimeInfo build(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoMutator.java b/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoMutator.java new file mode 100644 index 000000000..36a956122 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoMutator.java @@ -0,0 +1,13 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface JwkRsaPrimeInfoMutator { + + T setPrime(String r); + + T setCrtExponent(String d); + + T setCrtCoefficient(String t); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/Jwks.java b/api/src/main/java/io/jsonwebtoken/security/Jwks.java new file mode 100644 index 000000000..08cf45bf3 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/Jwks.java @@ -0,0 +1,14 @@ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.lang.Classes; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class Jwks { + + public static T builder() { + return Classes.newInstance("io.jsonwebtoken.impl.security.DefaultJwkBuilderFactory"); + } + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyException.java b/api/src/main/java/io/jsonwebtoken/security/KeyException.java index 0db219245..da44f9ad8 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyException.java @@ -23,4 +23,8 @@ public class KeyException extends SecurityException { public KeyException(String message) { super(message); } + + public KeyException(String msg, Exception cause) { + super(msg, cause); + } } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyManagementAlgorithmName.java b/api/src/main/java/io/jsonwebtoken/security/KeyManagementAlgorithmName.java new file mode 100644 index 000000000..bb1b169b3 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyManagementAlgorithmName.java @@ -0,0 +1,106 @@ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.lang.Collections; + +import java.util.List; + +/** + * Type-safe representation of standard JWE encryption key management algorithm names as defined in the + * JSON Web Algorithms specification. + * + * @since JJWT_RELEASE_VERSION + */ +public enum KeyManagementAlgorithmName { + + RSA1_5("RSA1_5", "RSAES-PKCS1-v1_5", Collections.emptyList(), "RSA/ECB/PKCS1Padding"), + RSA_OAEP("RSA-OAEP", "RSAES OAEP using default parameters", Collections.emptyList(), "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"), + RSA_OAEP_256("RSA-OAEP-256", "RSAES OAEP using SHA-256 and MGF1 with SHA-256", Collections.emptyList(), "RSA/ECB/OAEPWithSHA-256AndMGF1Padding & MGF1ParameterSpec.SHA256"), + A128KW("A128KW", "AES Key Wrap with default initial value using 128-bit key", Collections.emptyList(), "AESWrap"), + A192KW("A192KW", "AES Key Wrap with default initial value using 192-bit key", Collections.emptyList(), "AESWrap"), + A256KW("A256KW", "AES Key Wrap with default initial value using 256-bit key", Collections.emptyList(), "AESWrap"), + dir("dir", "Direct use of a shared symmetric key as the CEK", Collections.emptyList(), "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"), + ECDH_ES("ECDH-ES", "Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF", Collections.of("epk", "apu", "apv"), "ECDH"), + ECDH_ES_A128KW("ECDH-ES+A128KW", "ECDH-ES using Concat KDF and CEK wrapped with \"A128KW\"", Collections.of("epk", "apu", "apv"), "ECDH???"), + ECDH_ES_A192KW("ECDH-ES+A192KW", "ECDH-ES using Concat KDF and CEK wrapped with \"A192KW\"", Collections.of("epk", "apu", "apv"), "ECDH???"), + ECDH_ES_A256KW("ECDH-ES+A256KW", "ECDH-ES using Concat KDF and CEK wrapped with \"A256KW\"", Collections.of("epk", "apu", "apv"), "ECDH???"), + A128GCMKW("A128GCMKW", "Key wrapping with AES GCM using 128-bit key", Collections.of("iv", "tag"), "???"), + A192GCMKW("A192GCMKW", "Key wrapping with AES GCM using 192-bit key", Collections.of("iv", "tag"), "???"), + A256GCMKW("A256GCMKW", "Key wrapping with AES GCM using 256-bit key", Collections.of("iv", "tag"), "???"), + PBES2_HS256_A128KW("PBES2-HS256+A128KW", "PBES2 with HMAC SHA-256 and \"A128KW\" wrapping", Collections.of("p2s", "p2c"), "???"), + PBES2_HS384_A192KW("PBES2-HS384+A192KW", "PBES2 with HMAC SHA-384 and \"A192KW\" wrapping", Collections.of("p2s", "p2c"), "???"), + PBES2_HS512_A256KW("PBES2-HS512+A256KW", "PBES2 with HMAC SHA-512 and \"A256KW\" wrapping", Collections.of("p2s", "p2c"), "???"); + + private final String value; + private final String description; + private final List moreHeaderParams; + private final String jcaName; + + KeyManagementAlgorithmName(String value, String description, List moreHeaderParams, String jcaName) { + this.value = value; + this.description = description; + this.moreHeaderParams = moreHeaderParams; + this.jcaName = jcaName; + } + + /** + * Returns the JWA algorithm name constant. + * + * @return the JWA algorithm name constant. + */ + public String getValue() { + return value; + } + + /** + * Returns the JWA algorithm description. + * + * @return the JWA algorithm description. + */ + public String getDescription() { + return description; + } + + /** + * Returns a list of header parameters that must exist in the JWE header when evaluating the key management + * algorithm. The list will be empty for algorithms that do not require additional header parameters. + * + * @return a list of header parameters that must exist in the JWE header when evaluating the key management + * algorithm. + */ + public List getMoreHeaderParams() { + return moreHeaderParams; + } + + /** + * Returns the name of the JCA algorithm used to create or validate the Content Encryption Key (CEK). + * + * @return the name of the JCA algorithm used to create or validate the Content Encryption Key (CEK). + */ + public String getJcaName() { + return jcaName; + } + + /** + * Returns the corresponding {@code KeyManagementAlgorithmName} enum instance based on a + * case-insensitive name comparison of the specified JWE alg value. + * + * @param name the case-insensitive JWE alg header value. + * @return Returns the corresponding {@code KeyManagementAlgorithmName} enum instance based on a + * case-insensitive name comparison of the specified JWE alg value. + * @throws IllegalArgumentException if the specified value does not match any JWE {@code KeyManagementAlgorithmName} value. + */ + public static KeyManagementAlgorithmName forName(String name) throws IllegalArgumentException { + for (KeyManagementAlgorithmName alg : values()) { + if (alg.getValue().equalsIgnoreCase(name)) { + return alg; + } + } + + throw new IllegalArgumentException("Unsupported JWE Key Management Algorithm name: " + name); + } + + @Override + public String toString() { + return value; + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyManagementModeName.java b/api/src/main/java/io/jsonwebtoken/security/KeyManagementModeName.java new file mode 100644 index 000000000..4f4968c0f --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyManagementModeName.java @@ -0,0 +1,42 @@ +package io.jsonwebtoken.security; + +/** + * An enum representing the {@code Key Management Mode} names defined in + * RFC 7516, Section 2. + * + * @since JJWT_RELEASE_VERSION + */ +public enum KeyManagementModeName { + + KEY_ENCRYPTION("Key Encryption", + "The CEK value is encrypted to the intended recipient using an asymmetric encryption algorithm"), + + KEY_WRAPPING("Key Wrapping", + "The CEK value is encrypted to the intended recipient using a symmetric key wrapping algorithm."), + + DIRECT_KEY_AGREEMENT("Direct Key Agreement", + "A key agreement algorithm is used to agree upon the CEK value."), + + KEY_AGREEMENT_WITH_KEY_WRAPPING("Key Agreement with Key Wrapping", + "A key agreement algorithm is used to agree upon a symmetric key used to encrypt the CEK value to the " + + "intended recipient using a symmetric key wrapping algorithm."), + + DIRECT_ENCRYPTION("Direct Encryption", + "The CEK value used is the secret symmetric key value shared between the parties."); + + private final String name; + private final String desc; + + KeyManagementModeName(String name, String desc) { + this.name = name; + this.desc = desc; + } + + public String getName() { + return name; + } + + public String getDescription() { + return desc; + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index cb4784248..088be5e9b 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -15,16 +15,11 @@ */ package io.jsonwebtoken.security; -import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Classes; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.KeyPair; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; /** * Utility class for securely generating {@link SecretKey}s and {@link KeyPair}s. @@ -33,36 +28,10 @@ */ public final class Keys { - private static final String MAC = "io.jsonwebtoken.impl.crypto.MacProvider"; - private static final String RSA = "io.jsonwebtoken.impl.crypto.RsaProvider"; - private static final String EC = "io.jsonwebtoken.impl.crypto.EllipticCurveProvider"; - - private static final Class[] SIG_ARG_TYPES = new Class[]{SignatureAlgorithm.class}; - - //purposefully ordered higher to lower: - private static final List PREFERRED_HMAC_ALGS = Collections.unmodifiableList(Arrays.asList( - SignatureAlgorithm.HS512, SignatureAlgorithm.HS384, SignatureAlgorithm.HS256)); - //prevent instantiation private Keys() { } - /* - public static final int bitLength(Key key) throws IllegalArgumentException { - Assert.notNull(key, "Key cannot be null."); - if (key instanceof SecretKey) { - byte[] encoded = key.getEncoded(); - return Arrays.length(encoded) * 8; - } else if (key instanceof RSAKey) { - return ((RSAKey)key).getModulus().bitLength(); - } else if (key instanceof ECKey) { - return ((ECKey)key).getParams().getOrder().bitLength(); - } - - throw new IllegalArgumentException("Unsupported key type: " + key.getClass().getName()); - } - */ - /** * Creates a new SecretKey instance for use with HMAC-SHA algorithms based on the specified key byte array. * @@ -80,23 +49,37 @@ public static SecretKey hmacShaKeyFor(byte[] bytes) throws WeakKeyException { int bitLength = bytes.length * 8; - for (SignatureAlgorithm alg : PREFERRED_HMAC_ALGS) { - if (bitLength >= alg.getMinKeyLength()) { - return new SecretKeySpec(bytes, alg.getJcaName()); - } + //Purposefully ordered higher to lower to ensure the strongest key possible can be generated. + if (bitLength >= 512) { + return new SecretKeySpec(bytes, "HmacSHA512"); + } else if (bitLength >= 384) { + return new SecretKeySpec(bytes, "HmacSHA384"); + } else if (bitLength >= 256) { + return new SecretKeySpec(bytes, "HmacSHA256"); } String msg = "The specified key byte array is " + bitLength + " bits which " + "is not secure enough for any JWT HMAC-SHA algorithm. The JWT " + "JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " + "size >= 256 bits (the key size must be greater than or equal to the hash " + - "output size). Consider using the " + Keys.class.getName() + "#secretKeyFor(SignatureAlgorithm) method " + - "to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm. See " + - "https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; + "output size). Consider using the SignatureAlgorithms.HS256.generateKey() method (or HS384.generateKey() " + + "or HS512.generateKey()) to create a key guaranteed to be secure enough for your preferred HMAC-SHA " + + "algorithm. See https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; throw new WeakKeyException(msg); } /** + *

Deprecation Notice

+ *

As of JJWT JJWT_RELEASE_VERSION, symmetric (secret) key algorithm instances can generate a key of suitable + * length for that specific algorithm by calling their {@code generateKey()} method directly. For example: + *

+     * {@link SignatureAlgorithms#HS256}.generateKey();
+     * {@link SignatureAlgorithms#HS384}.generateKey();
+     * {@link SignatureAlgorithms#HS512}.generateKey();
+     * 
+ * Call those methods as needed instead of this {@code secretKeyFor} helper method. This helper method will be + * removed before the 1.0 final release.

+ *

* Returns a new {@link SecretKey} with a key length suitable for use with the specified {@link SignatureAlgorithm}. * *

JWA Specification (RFC 7518), Section 3.2 @@ -124,23 +107,36 @@ public static SecretKey hmacShaKeyFor(byte[] bytes) throws WeakKeyException { * * @param alg the {@code SignatureAlgorithm} to inspect to determine which key length to use. * @return a new {@link SecretKey} instance suitable for use with the specified {@link SignatureAlgorithm}. - * @throws IllegalArgumentException for any input value other than {@link SignatureAlgorithm#HS256}, - * {@link SignatureAlgorithm#HS384}, or {@link SignatureAlgorithm#HS512} + * @throws IllegalArgumentException for any input value other than {@link io.jsonwebtoken.SignatureAlgorithm#HS256}, + * {@link io.jsonwebtoken.SignatureAlgorithm#HS384}, or {@link io.jsonwebtoken.SignatureAlgorithm#HS512} + * @deprecated since JJWT_RELEASE_VERSION. Use your preferred {@link SymmetricKeySignatureAlgorithm} instance's + * {@link SymmetricKeySignatureAlgorithm#generateKey() generateKey()} method directly. */ - public static SecretKey secretKeyFor(SignatureAlgorithm alg) throws IllegalArgumentException { + @Deprecated + public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - switch (alg) { - case HS256: - case HS384: - case HS512: - return Classes.invokeStatic(MAC, "generateKey", SIG_ARG_TYPES, alg); - default: - String msg = "The " + alg.name() + " algorithm does not support shared secret keys."; - throw new IllegalArgumentException(msg); + SignatureAlgorithm salg = SignatureAlgorithms.forName(alg.name()); + if (!(salg instanceof SymmetricKeySignatureAlgorithm)) { + String msg = "The " + alg.name() + " algorithm does not support shared secret keys."; + throw new IllegalArgumentException(msg); } + return ((SymmetricKeySignatureAlgorithm) salg).generateKey(); } /** + *

Deprecation Notice

+ *

As of JJWT JJWT_RELEASE_VERSION, asymmetric key algorithm instances can generate KeyPairs of suitable strength + * for that specific algorithm by calling their {@code generateKeyPair()} method directly. For example: + *

+     * {@link SignatureAlgorithms#RS256}.generateKeyPair();
+     * {@link SignatureAlgorithms#RS384}.generateKeyPair();
+     * {@link SignatureAlgorithms#RS256}.generateKeyPair();
+     * ... etc ...
+     * {@link SignatureAlgorithms#ES512}.generateKeyPair();
+     * 
+ * Call those methods as needed instead of this {@code keyPairFor} helper method. This helper method will be + * removed before the 1.0 final release.

+ *

* Returns a new {@link KeyPair} suitable for use with the specified asymmetric algorithm. * *

If the {@code alg} argument is an RSA algorithm, a KeyPair is generated based on the following:

@@ -199,7 +195,7 @@ public static SecretKey secretKeyFor(SignatureAlgorithm alg) throws IllegalArgum * * * EC512 - * 512 bits + * 521 bits * {@code P-521} * {@code secp521r1} * @@ -208,24 +204,17 @@ public static SecretKey secretKeyFor(SignatureAlgorithm alg) throws IllegalArgum * @param alg the {@code SignatureAlgorithm} to inspect to determine which asymmetric algorithm to use. * @return a new {@link KeyPair} suitable for use with the specified asymmetric algorithm. * @throws IllegalArgumentException if {@code alg} is not an asymmetric algorithm + * @deprecated since JJWT_RELEASE_VERSION. Use your preferred {@link AsymmetricKeySignatureAlgorithm} instance's + * {@link AsymmetricKeySignatureAlgorithm#generateKeyPair() generateKeyPair()} method directly. */ - public static KeyPair keyPairFor(SignatureAlgorithm alg) throws IllegalArgumentException { + @Deprecated + public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - switch (alg) { - case RS256: - case PS256: - case RS384: - case PS384: - case RS512: - case PS512: - return Classes.invokeStatic(RSA, "generateKeyPair", SIG_ARG_TYPES, alg); - case ES256: - case ES384: - case ES512: - return Classes.invokeStatic(EC, "generateKeyPair", SIG_ARG_TYPES, alg); - default: - String msg = "The " + alg.name() + " algorithm does not support Key Pairs."; - throw new IllegalArgumentException(msg); + SignatureAlgorithm salg = SignatureAlgorithms.forName(alg.name()); + if (!(salg instanceof AsymmetricKeySignatureAlgorithm)) { + String msg = "The " + alg.name() + " algorithm does not support Key Pairs."; + throw new IllegalArgumentException(msg); } + return ((AsymmetricKeySignatureAlgorithm) salg).generateKeyPair(); } } diff --git a/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java b/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java new file mode 100644 index 000000000..a89bbc3df --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java @@ -0,0 +1,15 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class MalformedKeyException extends InvalidKeyException { + + public MalformedKeyException(String message) { + super(message); + } + + public MalformedKeyException(String msg, Exception cause) { + super(msg, cause); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwk.java b/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwk.java new file mode 100644 index 000000000..2130d45d0 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwk.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PrivateEcJwk extends EcJwk, PrivateEcJwkMutator { + + String getD(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkBuilder.java new file mode 100644 index 000000000..543e785db --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkBuilder.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PrivateEcJwkBuilder extends EcJwkBuilder { + + PrivateEcJwkBuilder setD(String d); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkMutator.java b/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkMutator.java new file mode 100644 index 000000000..f6e5f8a23 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkMutator.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PrivateEcJwkMutator extends EcJwkMutator { + + T setD(String d); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwk.java b/api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwk.java new file mode 100644 index 000000000..28976a93c --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwk.java @@ -0,0 +1,23 @@ +package io.jsonwebtoken.security; + +import java.util.List; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PrivateRsaJwk extends RsaJwk, PrivateRsaJwkMutator { + + String getD(); + + String getP(); + + String getQ(); + + String getDP(); + + String getDQ(); + + String getQI(); + + List getOtherPrimesInfo(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwkMutator.java b/api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwkMutator.java new file mode 100644 index 000000000..6ffb3edc6 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwkMutator.java @@ -0,0 +1,23 @@ +package io.jsonwebtoken.security; + +import java.util.List; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PrivateRsaJwkMutator extends RsaJwkMutator { + + T setD(String d); + + T setP(String p); + + T setQ(String q); + + T setDP(String dp); + + T setDQ(String dq); + + T setQI(String qi); + + T setOtherPrimesInfo(List infos); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PublicEcJwk.java b/api/src/main/java/io/jsonwebtoken/security/PublicEcJwk.java new file mode 100644 index 000000000..be532e268 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PublicEcJwk.java @@ -0,0 +1,7 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PublicEcJwk extends EcJwk { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PublicEcJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PublicEcJwkBuilder.java new file mode 100644 index 000000000..ed893ed35 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PublicEcJwkBuilder.java @@ -0,0 +1,7 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PublicEcJwkBuilder extends EcJwkBuilder { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PublicRsaJwk.java b/api/src/main/java/io/jsonwebtoken/security/PublicRsaJwk.java new file mode 100644 index 000000000..99e121368 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PublicRsaJwk.java @@ -0,0 +1,7 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PublicRsaJwk extends RsaJwk { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaJwk.java b/api/src/main/java/io/jsonwebtoken/security/RsaJwk.java new file mode 100644 index 000000000..0a5c2393a --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/RsaJwk.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface RsaJwk extends Jwk, RsaJwkMutator { + + String getModulus(); + + String getExponent(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaJwkMutator.java b/api/src/main/java/io/jsonwebtoken/security/RsaJwkMutator.java new file mode 100644 index 000000000..0df8a74da --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/RsaJwkMutator.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface RsaJwkMutator extends JwkMutator { + + T setModulus(String n); + + T setExponent(String e); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java new file mode 100644 index 000000000..095fe5047 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java @@ -0,0 +1,15 @@ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.Named; + +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface SignatureAlgorithm extends Named { + + byte[] sign(CryptoRequest request) throws SignatureException, KeyException; + + boolean verify(VerifySignatureRequest request) throws SignatureException, KeyException; +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java new file mode 100644 index 000000000..deac14210 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java @@ -0,0 +1,223 @@ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Classes; + +import javax.crypto.SecretKey; +import java.security.Key; +import java.security.PrivateKey; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAKey; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * @since JJWT_RELEASE_VERSION + */ +public final class SignatureAlgorithms { + + // Prevent instantiation + private SignatureAlgorithms() { + } + + static final String HMAC = "io.jsonwebtoken.impl.security.MacSignatureAlgorithm"; + static final Class[] HMAC_ARGS = new Class[]{String.class, String.class, int.class}; + + private static final String RSA = "io.jsonwebtoken.impl.security.RsaSignatureAlgorithm"; + private static final Class[] RSA_ARGS = new Class[]{String.class, String.class, int.class}; + private static final Class[] PSS_ARGS = new Class[]{String.class, String.class, int.class, int.class}; + + private static final String EC = "io.jsonwebtoken.impl.security.EllipticCurveSignatureAlgorithm"; + private static final Class[] EC_ARGS = new Class[]{String.class, String.class, String.class, int.class, int.class}; + + private static SymmetricKeySignatureAlgorithm hmacSha(int minKeyLength) { + return Classes.newInstance(HMAC, HMAC_ARGS, "HS" + minKeyLength, "HmacSHA" + minKeyLength, minKeyLength); + } + + private static AsymmetricKeySignatureAlgorithm rsa(int digestLength, int preferredKeyLength) { + return Classes.newInstance(RSA, RSA_ARGS, "RS" + digestLength, "SHA" + digestLength + "withRSA", preferredKeyLength); + } + + private static AsymmetricKeySignatureAlgorithm pss(int digestLength, int preferredKeyLength) { + return Classes.newInstance(RSA, PSS_ARGS, "PS" + digestLength, "RSASSA-PSS", preferredKeyLength, digestLength); + } + + private static AsymmetricKeySignatureAlgorithm ec(int keySize, int signatureLength) { + int shaSize = keySize == 521 ? 512 : keySize; + return Classes.newInstance(EC, EC_ARGS, "ES" + shaSize, "SHA" + shaSize + "withECDSA", "secp" + keySize + "r1", keySize, signatureLength); + } + + public static final SignatureAlgorithm NONE = Classes.newInstance("io.jsonwebtoken.impl.security.NoneSignatureAlgorithm"); + public static final SymmetricKeySignatureAlgorithm HS256 = hmacSha(256); + public static final SymmetricKeySignatureAlgorithm HS384 = hmacSha(384); + public static final SymmetricKeySignatureAlgorithm HS512 = hmacSha(512); + public static final AsymmetricKeySignatureAlgorithm RS256 = rsa(256, 2048); + public static final AsymmetricKeySignatureAlgorithm RS384 = rsa(384, 3072); + public static final AsymmetricKeySignatureAlgorithm RS512 = rsa(512, 4096); + public static final AsymmetricKeySignatureAlgorithm PS256 = pss(256, 2048); + public static final AsymmetricKeySignatureAlgorithm PS384 = pss(384, 3072); + public static final AsymmetricKeySignatureAlgorithm PS512 = pss(512, 4096); + public static final AsymmetricKeySignatureAlgorithm ES256 = ec(256, 64); + public static final AsymmetricKeySignatureAlgorithm ES384 = ec(384, 96); + public static final AsymmetricKeySignatureAlgorithm ES512 = ec(521, 132); + + private static Map toMap(SignatureAlgorithm... algs) { + Map m = new LinkedHashMap<>(); + for (SignatureAlgorithm alg : algs) { + m.put(alg.getName(), alg); + } + return Collections.unmodifiableMap(m); + } + + private static final Map STANDARD_ALGORITHMS = toMap( + NONE, HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512 + ); + + public static Collection values() { + return STANDARD_ALGORITHMS.values(); + } + + /** + * Looks up and returns the corresponding JWA standard {@code SignatureAlgorithm} instance based on a + * case-insensitive name comparison. + * + * @param name The case-insensitive name of the JWA standard {@code SignatureAlgorithm} instance to return + * @return the corresponding JWA standard {@code SignatureAlgorithm} enum instance based on a + * case-insensitive name comparison. + * @throws SignatureException if the specified value does not match any JWA standard {@code SignatureAlgorithm} + * name. + */ + public static SignatureAlgorithm forName(String name) { + Assert.notNull(name, "name argument cannot be null."); + //try constant time lookup first. This will satisfy 99% of invocations: + SignatureAlgorithm alg = STANDARD_ALGORITHMS.get(name); + if (alg != null) { + return alg; + } + //fall back to case-insensitive lookup: + for (SignatureAlgorithm salg : STANDARD_ALGORITHMS.values()) { + if (name.equalsIgnoreCase(salg.getName())) { + return salg; + } + } + // still no result - error: + throw new SignatureException("Unsupported signature algorithm '" + name + "'"); + } + + /** + * Returns the recommended signature algorithm to be used with the specified key according to the following + * heuristics: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Key Signature Algorithm
If the Key is a:And:With a key size of:The returned SignatureAlgorithm will be:
{@link SecretKey}{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA256")1256 <= size <= 383 2{@link SignatureAlgorithms#HS256 HS256}
{@link SecretKey}{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA384")1384 <= size <= 511{@link SignatureAlgorithms#HS384 HS384}
{@link SecretKey}{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA512")1512 <= size{@link SignatureAlgorithms#HS512 HS512}
{@link ECKey}instanceof {@link PrivateKey}256 <= size <= 383 3{@link SignatureAlgorithms#ES256 ES256}
{@link ECKey}instanceof {@link PrivateKey}384 <= size <= 520 4{@link SignatureAlgorithms#ES384 ES384}
{@link ECKey}instanceof {@link PrivateKey}521 <= size 4{@link SignatureAlgorithms#ES512 ES512}
{@link RSAKey}instanceof {@link PrivateKey}2048 <= size <= 3071 5,6{@link SignatureAlgorithms#RS256 RS256}
{@link RSAKey}instanceof {@link PrivateKey}3072 <= size <= 4095 6{@link SignatureAlgorithms#RS384 RS384}
{@link RSAKey}instanceof {@link PrivateKey}4096 <= size 5{@link SignatureAlgorithms#RS512 RS512}
+ *

Notes:

+ *
    + *
  1. {@code SecretKey} instances must have an {@link Key#getAlgorithm() algorithm} name equal + * to {@code HmacSHA256}, {@code HmacSHA384} or {@code HmacSHA512}. If not, the key bytes might not be + * suitable for HMAC signatures will be rejected with a {@link InvalidKeyException}.
  2. + *
  3. The JWT JWA Specification (RFC 7518, + * Section 3.2) mandates that HMAC-SHA-* signing keys MUST be 256 bits or greater. + * {@code SecretKey}s with key lengths less than 256 bits will be rejected with an + * {@link WeakKeyException}.
  4. + *
  5. The JWT JWA Specification (RFC 7518, + * Section 3.4) mandates that ECDSA signing key lengths MUST be 256 bits or greater. + * {@code ECKey}s with key lengths less than 256 bits will be rejected with a + * {@link WeakKeyException}.
  6. + *
  7. The ECDSA {@code P-521} curve does indeed use keys of 521 bits, not 512 as might be expected. ECDSA + * keys of 384 < size <= 520 are suitable for ES384, while ES512 requires keys >= 521 bits. The '512' part of the + * ES512 name reflects the usage of the SHA-512 algorithm, not the ECDSA key length. ES512 with ECDSA keys less + * than 521 bits will be rejected with a {@link WeakKeyException}.
  8. + *
  9. The JWT JWA Specification (RFC 7518, + * Section 3.3) mandates that RSA signing key lengths MUST be 2048 bits or greater. + * {@code RSAKey}s with key lengths less than 2048 bits will be rejected with a + * {@link WeakKeyException}.
  10. + *
  11. Technically any RSA key of length >= 2048 bits may be used with the {@link #RS256}, {@link #RS384}, and + * {@link #RS512} algorithms, so we assume an RSA signature algorithm based on the key length to + * parallel similar decisions in the JWT specification for HMAC and ECDSA signature algorithms. + * This is not required - just a convenience.
  12. + *
+ *

This implementation does not return the {@link #PS256}, {@link #PS256}, {@link #PS256} RSA variants for any + * specified {@link RSAKey} because the the {@link #RS256}, {@link #RS384}, and {@link #RS512} algorithms are + * available in the JDK by default while the {@code PS}* variants require either JDK 11 or an additional JCA + * Provider (like BouncyCastle).

+ *

Finally, this method will throw an {@link InvalidKeyException} for any key that does not match the + * heuristics and requirements documented above, since that inevitably means the Key is either insufficient or + * explicitly disallowed by the JWT specification.

+ * + * @param key the key to inspect + * @return the recommended signature algorithm to be used with the specified key + * @throws InvalidKeyException for any key that does not match the heuristics and requirements documented above, + * since that inevitably means the Key is either insufficient or explicitly disallowed by the JWT specification. + */ + public static SignatureAlgorithm forSigningKey(Key key) { + io.jsonwebtoken.SignatureAlgorithm alg = io.jsonwebtoken.SignatureAlgorithm.forSigningKey(key); + return forName(alg.getValue()); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureException.java b/api/src/main/java/io/jsonwebtoken/security/SignatureException.java index 7cddb2cac..93253a01b 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureException.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureException.java @@ -18,6 +18,7 @@ /** * @since 0.10.0 */ +@SuppressWarnings("deprecation") public class SignatureException extends io.jsonwebtoken.SignatureException { public SignatureException(String message) { diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricEncryptionAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricEncryptionAlgorithm.java new file mode 100644 index 000000000..a140da83c --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SymmetricEncryptionAlgorithm.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.security; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface SymmetricEncryptionAlgorithm, ERes extends IvEncryptionResult, DReq extends IvRequest> extends EncryptionAlgorithm, SymmetricKeyAlgorithm { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricJwk.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricJwk.java new file mode 100644 index 000000000..6b81fcde1 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SymmetricJwk.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface SymmetricJwk extends Jwk, SymmetricJwkMutator { + + String getK(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricJwkBuilder.java new file mode 100644 index 000000000..af63d4238 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SymmetricJwkBuilder.java @@ -0,0 +1,7 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface SymmetricJwkBuilder extends JwkBuilder, SymmetricJwkMutator { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricJwkMutator.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricJwkMutator.java new file mode 100644 index 000000000..0ab48505e --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SymmetricJwkMutator.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface SymmetricJwkMutator extends JwkMutator { + + T setK(String k); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricKeyAlgorithm.java new file mode 100644 index 000000000..fb074973f --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SymmetricKeyAlgorithm.java @@ -0,0 +1,16 @@ +package io.jsonwebtoken.security; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface SymmetricKeyAlgorithm { + + /** + * Creates and returns a new secure-random key with a length sufficient to be used by this Algorithm. + * + * @return a new secure-random key with a length sufficient to be used by this Algorithm. + */ + SecretKey generateKey(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricKeySignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricKeySignatureAlgorithm.java new file mode 100644 index 000000000..451b6c645 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SymmetricKeySignatureAlgorithm.java @@ -0,0 +1,7 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface SymmetricKeySignatureAlgorithm extends SignatureAlgorithm, SymmetricKeyAlgorithm { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java b/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java new file mode 100644 index 000000000..890dec20a --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java @@ -0,0 +1,15 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class UnsupportedKeyException extends KeyException { + + public UnsupportedKeyException(String message) { + super(message); + } + + public UnsupportedKeyException(String msg, Exception cause) { + super(msg, cause); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java b/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java new file mode 100644 index 000000000..b71a6c1d7 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.security; + +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface VerifySignatureRequest extends CryptoRequest { + + byte[] getSignature(); +} diff --git a/api/src/test/groovy/io/jsonwebtoken/EncryptionAlgorithmNameTest.groovy b/api/src/test/groovy/io/jsonwebtoken/EncryptionAlgorithmNameTest.groovy new file mode 100644 index 000000000..26fd5d0b3 --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/EncryptionAlgorithmNameTest.groovy @@ -0,0 +1,61 @@ +package io.jsonwebtoken + +import io.jsonwebtoken.security.EncryptionAlgorithmName +import org.junit.Test +import static org.junit.Assert.* + +class EncryptionAlgorithmNameTest { + + @Test + void testGetValue() { + assertEquals 'A128CBC-HS256', EncryptionAlgorithmName.A128CBC_HS256.getValue() + assertEquals 'A192CBC-HS384', EncryptionAlgorithmName.A192CBC_HS384.getValue() + assertEquals 'A256CBC-HS512', EncryptionAlgorithmName.A256CBC_HS512.getValue() + assertEquals 'A128GCM', EncryptionAlgorithmName.A128GCM.getValue() + assertEquals 'A192GCM', EncryptionAlgorithmName.A192GCM.getValue() + assertEquals 'A256GCM', EncryptionAlgorithmName.A256GCM.getValue() + } + + @Test + void testGetDescription() { + assertEquals 'AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.3', EncryptionAlgorithmName.A128CBC_HS256.getDescription() + assertEquals 'AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.4', EncryptionAlgorithmName.A192CBC_HS384.getDescription() + assertEquals 'AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.5', EncryptionAlgorithmName.A256CBC_HS512.getDescription() + assertEquals 'AES GCM using 128-bit key', EncryptionAlgorithmName.A128GCM.getDescription() + assertEquals 'AES GCM using 192-bit key', EncryptionAlgorithmName.A192GCM.getDescription() + assertEquals 'AES GCM using 256-bit key', EncryptionAlgorithmName.A256GCM.getDescription() + } + + @Test + void testGetJcaName() { + for( def name : EncryptionAlgorithmName.values() ) { + if (name.getValue().contains("GCM")) { + assertEquals 'AES/GCM/NoPadding', name.getJcaName() + } else { + assertEquals 'AES/CBC/PKCS5Padding', name.getJcaName() + } + } + } + + @Test + void testToString() { + for( def name : EncryptionAlgorithmName.values() ) { + assertEquals name.toString(), name.getValue() + } + } + + @Test + void testForName() { + def name = EncryptionAlgorithmName.forName('A128GCM') + assertSame name, EncryptionAlgorithmName.A128GCM + } + + @Test + void testForNameFailure() { + try { + EncryptionAlgorithmName.forName('foo') + fail() + } catch (IllegalArgumentException expected) { + } + } +} diff --git a/api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy b/api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy new file mode 100644 index 000000000..2702003e6 --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy @@ -0,0 +1,45 @@ +package io.jsonwebtoken.lang + +import org.junit.Test + +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.* + +/** + * @since JJWT_RELEASE_VERSION + */ +class ArraysTest { + + @Test + void testCleanWithNull() { + assertNull Arrays.clean(null) + } + + @Test + void testCleanWithEmpty() { + assertNull Arrays.clean(new byte[0]) + } + + @Test + void testCleanWithElements() { + byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8) + assertSame bytes, Arrays.clean(bytes) + } + + @Test + void testByteArrayLengthWithNull() { + assertEquals 0, Arrays.length(null) + } + + @Test + void testByteArrayLengthWithEmpty() { + assertEquals 0, Arrays.length(new byte[0]) + } + + @Test + void testByteArrayLengthWithElements() { + byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8) + assertEquals 5, Arrays.length(bytes) + } +} diff --git a/api/src/test/groovy/io/jsonwebtoken/security/CurveIdsTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/CurveIdsTest.groovy new file mode 100644 index 000000000..7a1af2dd7 --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/security/CurveIdsTest.groovy @@ -0,0 +1,24 @@ +package io.jsonwebtoken.security + +import org.junit.Test +import static org.junit.Assert.* + +class CurveIdsTest { + + @Test(expected=IllegalArgumentException) + void testNullId() { + CurveIds.forValue(null) + } + + @Test(expected=IllegalArgumentException) + void testEmptyId() { + CurveIds.forValue(' ') + } + + @Test + void testNonStandardId() { + CurveId id = CurveIds.forValue("NonStandard") + assertNotNull id + assertEquals 'NonStandard', id.toString() + } +} diff --git a/api/src/test/groovy/io/jsonwebtoken/security/KeyManagementAlgorithmNameTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/KeyManagementAlgorithmNameTest.groovy new file mode 100644 index 000000000..3a4366b7f --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/security/KeyManagementAlgorithmNameTest.groovy @@ -0,0 +1,54 @@ +package io.jsonwebtoken.security + +import org.junit.Test +import static org.junit.Assert.* + +/** + * @since JJWT_RELEASE_VERSION + */ +class KeyManagementAlgorithmNameTest { + + @Test + void testToString() { + for( def name : KeyManagementAlgorithmName.values()) { + assertEquals name.value, name.toString() + } + } + + @Test + void testGetDescription() { + for( def name : KeyManagementAlgorithmName.values()) { + assertNotNull name.getDescription() //TODO improve this for actual value testing + } + } + + @Test + void testGetMoreHeaderParams() { + for( def name : KeyManagementAlgorithmName.values()) { + assertNotNull name.getMoreHeaderParams() //TODO improve this for actual value testing + } + } + + @Test + void testGetJcaName() { + for( def name : KeyManagementAlgorithmName.values()) { + assertNotNull name.getJcaName() //TODO improve this for actual value testing + } + } + + @Test + void testForName() { + def name = KeyManagementAlgorithmName.forName('A128KW') + assertSame name, KeyManagementAlgorithmName.A128KW + } + + @Test + void testForNameFailure() { + try { + KeyManagementAlgorithmName.forName('foo') + fail() + } catch (IllegalArgumentException expected) { + } + } +} + diff --git a/api/src/test/groovy/io/jsonwebtoken/security/KeyManagementModeNameTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/KeyManagementModeNameTest.groovy new file mode 100644 index 000000000..900894dca --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/security/KeyManagementModeNameTest.groovy @@ -0,0 +1,19 @@ +package io.jsonwebtoken.security + +import org.junit.Test +import static org.junit.Assert.* + +/** + * @since JJWT_RELEASE_VERSION + */ +class KeyManagementModeNameTest { + + @Test + void test() { + //todo, write a real test: + for(KeyManagementModeName modeName : KeyManagementModeName.values()) { + assertNotNull modeName.getName() + assertNotNull modeName.getDescription() + } + } +} diff --git a/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy index 7635491cc..8fa9148f3 100644 --- a/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy @@ -16,18 +16,19 @@ package io.jsonwebtoken.security import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.lang.Classes import org.junit.Test import org.junit.runner.RunWith import org.powermock.core.classloader.annotations.PrepareForTest +import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor import org.powermock.modules.junit4.PowerMockRunner import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec import java.security.KeyPair +import java.security.SecureRandom import static org.easymock.EasyMock.eq import static org.easymock.EasyMock.expect -import static org.easymock.EasyMock.same import static org.junit.Assert.* import static org.powermock.api.easymock.PowerMock.* @@ -37,9 +38,18 @@ import static org.powermock.api.easymock.PowerMock.* * The actual implementation assertions are done in KeysImplTest in the impl module. */ @RunWith(PowerMockRunner) -@PrepareForTest([Classes, Keys]) +@PrepareForTest([SignatureAlgorithms, Keys]) +@SuppressStaticInitializationFor("io.jsonwebtoken.security.SignatureAlgorithms") class KeysTest { + private static final Random RANDOM = new SecureRandom() + + static byte[] bytes(int sizeInBits) { + byte[] bytes = new byte[sizeInBits / Byte.SIZE] + RANDOM.nextBytes(bytes) + return bytes + } + @Test void testPrivateCtor() { //for code coverage only new Keys() @@ -65,14 +75,35 @@ class KeysTest { "is not secure enough for any JWT HMAC-SHA algorithm. The JWT " + "JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " + "size >= 256 bits (the key size must be greater than or equal to the hash " + - "output size). Consider using the " + Keys.class.getName() + "#secretKeyFor(SignatureAlgorithm) method " + - "to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm. See " + + "output size). Consider using the SignatureAlgorithms.HS256.generateKey() method (or " + + "HS384.generateKey() or HS512.generateKey()) to create a key guaranteed to be secure enough " + + "for your preferred HMAC-SHA algorithm. See " + "https://tools.ietf.org/html/rfc7518#section-3.2 for more information." as String, expected.message } } + @Test + void testHmacShaWithValidSizes() { + for (int i : [256, 384, 512]) { + byte[] bytes = bytes(i) + def key = Keys.hmacShaKeyFor(bytes) + assertTrue key instanceof SecretKeySpec + assertEquals "HmacSHA$i" as String, key.getAlgorithm() + assertTrue Arrays.equals(bytes, key.getEncoded()) + } + } + + @Test + void testHmacShaLargerThan512() { + def key = Keys.hmacShaKeyFor(bytes(520)) + assertTrue key instanceof SecretKeySpec + assertEquals 'HmacSHA512', key.getAlgorithm() + assertTrue key.getEncoded().length * Byte.SIZE >= 512 + } + @Test void testSecretKeyFor() { + mockStatic(SignatureAlgorithms) for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { @@ -80,61 +111,66 @@ class KeysTest { if (name.startsWith('H')) { - mockStatic(Classes) - def key = createMock(SecretKey) - expect(Classes.invokeStatic(eq(Keys.MAC), eq("generateKey"), same(Keys.SIG_ARG_TYPES), same(alg))).andReturn(key) + def salg = createMock(SymmetricKeySignatureAlgorithm) - replay Classes, key + expect(SignatureAlgorithms.forName(eq(name))).andReturn(salg) + expect(salg.generateKey()).andReturn(key) + replay SignatureAlgorithms, salg, key assertSame key, Keys.secretKeyFor(alg) - verify Classes, key - - reset Classes, key + verify SignatureAlgorithms, salg, key + reset SignatureAlgorithms, salg, key } else { + def salg = name == 'NONE' ? createMock(io.jsonwebtoken.security.SignatureAlgorithm) : createMock(AsymmetricKeySignatureAlgorithm) + expect(SignatureAlgorithms.forName(eq(name))).andReturn(salg) + replay SignatureAlgorithms, salg try { Keys.secretKeyFor(alg) fail() } catch (IllegalArgumentException expected) { assertEquals "The $name algorithm does not support shared secret keys." as String, expected.message } - + verify SignatureAlgorithms, salg + reset SignatureAlgorithms, salg } } - } @Test void testKeyPairFor() { + mockStatic SignatureAlgorithms for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { String name = alg.name() if (name.equals('NONE') || name.startsWith('H')) { + def salg = name == 'NONE' ? createMock(io.jsonwebtoken.security.SignatureAlgorithm) : createMock(SymmetricKeySignatureAlgorithm) + expect(SignatureAlgorithms.forName(eq(name))).andReturn(salg) + replay SignatureAlgorithms, salg try { Keys.keyPairFor(alg) fail() } catch (IllegalArgumentException expected) { assertEquals "The $name algorithm does not support Key Pairs." as String, expected.message } + verify SignatureAlgorithms, salg + reset SignatureAlgorithms, salg } else { - String fqcn = name.startsWith('E') ? Keys.EC : Keys.RSA - - mockStatic Classes - def pair = createMock(KeyPair) - expect(Classes.invokeStatic(eq(fqcn), eq("generateKeyPair"), same(Keys.SIG_ARG_TYPES), same(alg))).andReturn(pair) + def salg = createMock(AsymmetricKeySignatureAlgorithm) - replay Classes, pair + expect(SignatureAlgorithms.forName(eq(name))).andReturn(salg) + expect(salg.generateKeyPair()).andReturn(pair) + replay SignatureAlgorithms, pair, salg assertSame pair, Keys.keyPairFor(alg) - verify Classes, pair - - reset Classes, pair + verify SignatureAlgorithms, pair, salg + reset SignatureAlgorithms, pair, salg } } } diff --git a/api/src/test/groovy/io/jsonwebtoken/security/UnsupportedKeyExceptionTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/UnsupportedKeyExceptionTest.groovy new file mode 100644 index 000000000..1738defb9 --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/security/UnsupportedKeyExceptionTest.groovy @@ -0,0 +1,17 @@ +package io.jsonwebtoken.security + +import org.junit.Test + +import static org.junit.Assert.* + +class UnsupportedKeyExceptionTest { + + @Test + void testCauseWithMessage() { + def cause = new IllegalStateException() + def msg = 'foo' + def ex = new UnsupportedKeyException(msg, cause) + assertEquals msg, ex.getMessage() + assertSame cause, ex.getCause() + } +} diff --git a/extensions/gson/pom.xml b/extensions/gson/pom.xml index 6b6985e83..ed554b0e0 100644 --- a/extensions/gson/pom.xml +++ b/extensions/gson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.11.3-SNAPSHOT + 0.12.0-SNAPSHOT ../../pom.xml @@ -44,4 +44,13 @@ + + + + com.github.siom79.japicmp + japicmp-maven-plugin + + + + \ No newline at end of file diff --git a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy index ae1223965..581de42ca 100644 --- a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy +++ b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy @@ -21,6 +21,9 @@ import io.jsonwebtoken.io.Deserializer import io.jsonwebtoken.lang.Strings import org.junit.Test +import java.text.DecimalFormat +import java.text.NumberFormat + import static org.easymock.EasyMock.* import static org.junit.Assert.* import static org.hamcrest.CoreMatchers.instanceOf diff --git a/extensions/jackson/pom.xml b/extensions/jackson/pom.xml index 79b0959ab..ae26aec49 100644 --- a/extensions/jackson/pom.xml +++ b/extensions/jackson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.11.3-SNAPSHOT + 0.12.0-SNAPSHOT ../../pom.xml @@ -66,14 +66,6 @@ com.github.siom79.japicmp japicmp-maven-plugin - - - - - ${project.build.directory}/${project.artifactId}-${project.version}-deprecated.${project.packaging} - - - diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java index a9ea111ea..c208ed7eb 100644 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java @@ -37,7 +37,6 @@ public class JacksonDeserializer implements Deserializer { private final Class returnType; private final ObjectMapper objectMapper; - @SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator public JacksonDeserializer() { this(JacksonSerializer.DEFAULT_OBJECT_MAPPER); } diff --git a/extensions/orgjson/pom.xml b/extensions/orgjson/pom.xml index f0ea88409..afe5b6bfb 100644 --- a/extensions/orgjson/pom.xml +++ b/extensions/orgjson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.11.3-SNAPSHOT + 0.12.0-SNAPSHOT ../../pom.xml @@ -66,14 +66,6 @@ com.github.siom79.japicmp japicmp-maven-plugin - - - - - ${project.build.directory}/${project.artifactId}-${project.version}-deprecated.${project.packaging} - - - diff --git a/extensions/pom.xml b/extensions/pom.xml index ee13b0f20..9259cd713 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.11.3-SNAPSHOT + 0.12.0-SNAPSHOT ../pom.xml diff --git a/impl/pom.xml b/impl/pom.xml index b1aaf92e1..ef51219f2 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.11.3-SNAPSHOT + 0.12.0-SNAPSHOT ../pom.xml diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java index 3560afc4a..7dba1763d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java @@ -53,14 +53,26 @@ public T setContentType(String cty) { return (T)this; } + @Override + public String getAlgorithm() { + return getString(ALGORITHM); + } + + @Override + public T setAlgorithm(String alg) { + setValue(ALGORITHM, alg); + return (T)this; + } + @SuppressWarnings("deprecation") @Override public String getCompressionAlgorithm() { - String alg = getString(COMPRESSION_ALGORITHM); - if (!Strings.hasText(alg)) { - alg = getString(DEPRECATED_COMPRESSION_ALGORITHM); + String s = getString(COMPRESSION_ALGORITHM); + if (!Strings.hasText(s)) { + //backwards compatibility TODO: remove when releasing 1.0 + s = getString(DEPRECATED_COMPRESSION_ALGORITHM); } - return alg; + return s; } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java new file mode 100644 index 000000000..db69ab44b --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java @@ -0,0 +1,30 @@ +package io.jsonwebtoken.impl; + +import io.jsonwebtoken.JweHeader; + +import java.util.Map; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultJweHeader extends DefaultHeader implements JweHeader { + + public DefaultJweHeader() { + super(); + } + + public DefaultJweHeader(Map map) { + super(map); + } + + @Override + public String getEncryptionAlgorithm() { + return getString(ENCRYPTION_ALGORITHM); + } + + @Override + public JweHeader setEncryptionAlgorithm(String enc) { + setValue(ENCRYPTION_ALGORITHM, enc); + return this; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java index cb747e3dd..bb23dff15 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java @@ -19,7 +19,7 @@ import java.util.Map; -public class DefaultJwsHeader extends DefaultHeader implements JwsHeader { +public class DefaultJwsHeader extends DefaultHeader implements JwsHeader { public DefaultJwsHeader() { super(); @@ -29,17 +29,6 @@ public DefaultJwsHeader(Map map) { super(map); } - @Override - public String getAlgorithm() { - return getString(ALGORITHM); - } - - @Override - public JwsHeader setAlgorithm(String alg) { - setValue(ALGORITHM, alg); - return this; - } - @Override public String getKeyId() { return getString(KEY_ID); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index 2c7ab1177..78421cfc5 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -21,10 +21,8 @@ import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.impl.crypto.DefaultJwtSigner; -import io.jsonwebtoken.impl.crypto.JwtSigner; import io.jsonwebtoken.impl.lang.LegacyServices; +import io.jsonwebtoken.impl.security.DefaultCryptoRequest; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Encoder; import io.jsonwebtoken.io.Encoders; @@ -33,31 +31,55 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.CryptoRequest; import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.SignatureAlgorithm; +import io.jsonwebtoken.security.SignatureAlgorithms; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; import java.util.Date; import java.util.Map; public class DefaultJwtBuilder implements JwtBuilder { + private static final byte[] TEST_MESSAGE_BYTES = "Test message".getBytes(StandardCharsets.UTF_8); + + private Provider provider; + private SecureRandom secureRandom; + private Header header; private Claims claims; private String payload; - private SignatureAlgorithm algorithm; + private SignatureAlgorithm algorithm = SignatureAlgorithms.NONE; + private Key key; - private Serializer> serializer; + private Serializer> serializer; private Encoder base64UrlEncoder = Encoders.BASE64URL; private CompressionCodec compressionCodec; @Override - public JwtBuilder serializeToJsonWith(Serializer> serializer) { + public JwtBuilder setProvider(Provider provider) { + this.provider = provider; + return this; + } + + @Override + public JwtBuilder setSecureRandom(SecureRandom secureRandom) { + this.secureRandom = secureRandom; + return this; + } + + @Override + public JwtBuilder serializeToJsonWith(Serializer> serializer) { Assert.notNull(serializer, "Serializer cannot be null."); this.serializer = serializer; return this; @@ -111,7 +133,7 @@ public JwtBuilder setHeaderParam(String name, Object value) { @Override public JwtBuilder signWith(Key key) throws InvalidKeyException { Assert.notNull(key, "Key argument cannot be null."); - SignatureAlgorithm alg = SignatureAlgorithm.forSigningKey(key); + SignatureAlgorithm alg = SignatureAlgorithms.forSigningKey(key); return signWith(key, alg); } @@ -119,14 +141,19 @@ public JwtBuilder signWith(Key key) throws InvalidKeyException { public JwtBuilder signWith(Key key, SignatureAlgorithm alg) throws InvalidKeyException { Assert.notNull(key, "Key argument cannot be null."); Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - alg.assertValidSigningKey(key); //since 0.10.0 for https://github.com/jwtk/jjwt/issues/334 this.algorithm = alg; this.key = key; return this; } @Override - public JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKeyBytes) throws InvalidKeyException { + public JwtBuilder signWith(Key key, io.jsonwebtoken.SignatureAlgorithm alg) throws InvalidKeyException { + Assert.notNull(alg, "SignatureAlgorithm cannot be null."); + return signWith(key, SignatureAlgorithms.forName(alg.getValue())); + } + + @Override + public JwtBuilder signWith(io.jsonwebtoken.SignatureAlgorithm alg, byte[] secretKeyBytes) throws InvalidKeyException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); Assert.notEmpty(secretKeyBytes, "secret key byte array cannot be null or empty."); Assert.isTrue(alg.isHmac(), "Key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead."); @@ -135,7 +162,7 @@ public JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKeyBytes) throws } @Override - public JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey) throws InvalidKeyException { + public JwtBuilder signWith(io.jsonwebtoken.SignatureAlgorithm alg, String base64EncodedSecretKey) throws InvalidKeyException { Assert.hasText(base64EncodedSecretKey, "base64-encoded secret key cannot be null or empty."); Assert.isTrue(alg.isHmac(), "Base64-encoded key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead."); byte[] bytes = Decoders.BASE64.decode(base64EncodedSecretKey); @@ -143,7 +170,7 @@ public JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey } @Override - public JwtBuilder signWith(SignatureAlgorithm alg, Key key) { + public JwtBuilder signWith(io.jsonwebtoken.SignatureAlgorithm alg, Key key) { return signWith(key, alg); } @@ -315,21 +342,11 @@ public String compact() { jwsHeader = (JwsHeader) header; } else { //noinspection unchecked - jwsHeader = new DefaultJwsHeader(header); - } - - if (key != null) { - jwsHeader.setAlgorithm(algorithm.getValue()); - } else { - //no signature - plaintext JWT: - jwsHeader.setAlgorithm(SignatureAlgorithm.NONE.getValue()); - } - - if (compressionCodec != null) { - jwsHeader.setCompressionAlgorithm(compressionCodec.getAlgorithmName()); + header = jwsHeader = new DefaultJwsHeader(header); } - String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json."); + Assert.state(algorithm != null, "algorithm instance should never be null."); // invariant + jwsHeader.setAlgorithm(algorithm.getName()); byte[] bytes; try { @@ -339,19 +356,20 @@ public String compact() { } if (compressionCodec != null) { + header.setCompressionAlgorithm(compressionCodec.getAlgorithmName()); bytes = compressionCodec.compress(bytes); } + String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json."); String base64UrlEncodedBody = base64UrlEncoder.encode(bytes); String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody; if (key != null) { //jwt must be signed: - - JwtSigner signer = createSigner(algorithm, key); - - String base64UrlSignature = signer.sign(jwt); - + byte[] data = jwt.getBytes(StandardCharsets.US_ASCII); + CryptoRequest request = new DefaultCryptoRequest<>(data, key, provider, secureRandom); + byte[] signature = algorithm.sign(request); + String base64UrlSignature = base64UrlEncoder.encode(signature); jwt += JwtParser.SEPARATOR_CHAR + base64UrlSignature; } else { // no signature (plaintext), but must terminate w/ a period, see @@ -362,17 +380,10 @@ public String compact() { return jwt; } - /* - * @since 0.5 mostly to allow testing overrides - */ - protected JwtSigner createSigner(SignatureAlgorithm alg, Key key) { - return new DefaultJwtSigner(alg, key, base64UrlEncoder); - } - @Deprecated // remove before 1.0 - call the serializer and base64UrlEncoder directly protected String base64UrlEncode(Object o, String errMsg) { Assert.isInstanceOf(Map.class, o, "object argument must be a map."); - Map m = (Map)o; + Map m = (Map) o; byte[] bytes; try { bytes = toJson(m); @@ -387,7 +398,7 @@ protected String base64UrlEncode(Object o, String errMsg) { @Deprecated //remove before 1.0 - call the serializer directly protected byte[] toJson(Object object) throws SerializationException { Assert.isInstanceOf(Map.class, object, "object argument must be a map."); - Map m = (Map)object; + Map m = (Map) object; return serializer.serialize(m); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 591433f3c..a31588e41 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -33,26 +33,35 @@ import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.MissingClaimException; import io.jsonwebtoken.PrematureJwtException; -import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; -import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator; -import io.jsonwebtoken.impl.crypto.JwtSignatureValidator; +import io.jsonwebtoken.impl.crypto.DefaultSignatureValidatorFactory; +import io.jsonwebtoken.impl.crypto.SignatureValidator; +import io.jsonwebtoken.impl.crypto.SignatureValidatorFactory; import io.jsonwebtoken.impl.lang.LegacyServices; +import io.jsonwebtoken.impl.security.DefaultVerifySignatureRequest; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.DecodingException; +import io.jsonwebtoken.io.DeserializationException; import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.DateFormats; import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.SignatureAlgorithm; +import io.jsonwebtoken.security.SignatureAlgorithms; import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.SymmetricKeySignatureAlgorithm; +import io.jsonwebtoken.security.VerifySignatureRequest; import io.jsonwebtoken.security.WeakKeyException; import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; import java.security.Key; +import java.security.Provider; import java.util.Date; import java.util.Map; @@ -61,7 +70,11 @@ public class DefaultJwtParser implements JwtParser { private static final int MILLISECONDS_PER_SECOND = 1000; + private static final JwtTokenizer jwtTokenizer = new JwtTokenizer(); + // TODO: make the folling fields final for v1.0 + private Provider provider; + private byte[] keyBytes; private Key key; @@ -82,12 +95,15 @@ public class DefaultJwtParser implements JwtParser { /** * TODO: remove this constructor before 1.0 + * * @deprecated for backward compatibility only, see other constructors. */ @Deprecated - public DefaultJwtParser() { } + public DefaultJwtParser() { + } - DefaultJwtParser(SigningKeyResolver signingKeyResolver, + DefaultJwtParser(Provider provider, + SigningKeyResolver signingKeyResolver, Key key, byte[] keyBytes, Clock clock, @@ -96,6 +112,7 @@ public DefaultJwtParser() { } Decoder base64UrlDecoder, Deserializer> deserializer, CompressionCodecResolver compressionCodecResolver) { + this.provider = provider; this.signingKeyResolver = signingKeyResolver; this.key = key; this.keyBytes = keyBytes; @@ -244,6 +261,16 @@ public boolean isSigned(String jwt) { return false; } + /* ======================================================================== + + JWE PARSING LOGIC TEMPORARILY DISABLED + Until we find out a cleaner design (delegation, separation of concerns, etc) + + private static void malformed(String type, String part) { + String msg = "Required " + type + " " + part + " is missing."; + throw new MalformedJwtException(msg); + } + @Override public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException { @@ -257,14 +284,16 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, Assert.hasText(jwt, "JWT String argument cannot be null or empty."); - if ("..".equals(jwt)) { - String msg = "JWT string '..' is missing a header."; - throw new MalformedJwtException(msg); - } + //parse the constituent parts of the compact string: + String base64UrlEncodedHeader = null; //JWS or JWE + + String base64UrlEncodedCek = null; //JWE only + String base64UrlEncodedPayload = null; //JWS or JWE - String base64UrlEncodedHeader = null; - String base64UrlEncodedPayload = null; - String base64UrlEncodedDigest = null; + String base64UrlEncodedIv = null; //JWE only + + String base64UrlEncodedTag = null; //JWE only + String base64UrlEncodedDigest = null; //JWS only int delimiterCount = 0; @@ -272,15 +301,31 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, for (char c : jwt.toCharArray()) { + if (Character.isWhitespace(c)) { + String msg = "Compact JWT strings cannot contain whitespace."; + throw new MalformedJwtException(msg); + } + if (c == SEPARATOR_CHAR) { CharSequence tokenSeq = Strings.clean(sb); String token = tokenSeq != null ? tokenSeq.toString() : null; - if (delimiterCount == 0) { - base64UrlEncodedHeader = token; - } else if (delimiterCount == 1) { - base64UrlEncodedPayload = token; + switch (delimiterCount) { + case 0: + base64UrlEncodedHeader = token; + break; + case 1: + //we'll figure out if we have a compact JWE or JWS after finishing inspecting the char array: + base64UrlEncodedCek = token; + base64UrlEncodedPayload = token; + break; + case 2: + base64UrlEncodedIv = token; + break; + case 3: + base64UrlEncodedPayload = token; //ciphertext + break; } delimiterCount++; @@ -290,27 +335,255 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, } } - if (delimiterCount != 2) { - String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount; + boolean jwe; + if (delimiterCount == 2) { // JWT or JWS + //noinspection ConstantConditions + jwe = false; + } else if (delimiterCount == 4) { // JWE + jwe = true; + } else { + String msg = "Invalid compact JWT string. JWSs must have exactly 2 period characters, " + + "JWEs must have exactly 4. Found: " + delimiterCount + "."; throw new MalformedJwtException(msg); } + + String type = jwe ? "JWE" : "JWS"; + if (sb.length() > 0) { - base64UrlEncodedDigest = sb.toString(); + String value = sb.toString(); + if (jwe) { + base64UrlEncodedTag = value; + } else { + base64UrlEncodedDigest = value; + } + } + + if (base64UrlEncodedHeader == null) { + malformed(type, "Protected Header"); } + if (base64UrlEncodedPayload == null) { + malformed(type, jwe ? "Ciphertext" : "Payload"); + } + + if (jwe) { + if (base64UrlEncodedIv == null) { + malformed(type, "Initialization Vector"); + } + if (base64UrlEncodedTag == null) { + malformed(type, "Authentication Tag"); + } + } + + // =============== Header ================= + Header header; + + CompressionCodec compressionCodec; + + byte[] bytes = base64UrlDecode(base64UrlEncodedHeader); + String origValue = new String(bytes, Strings.UTF_8); + Map m = (Map) readValue(origValue); + + if (base64UrlEncodedDigest != null) { + header = new DefaultJwsHeader(m); + } else if (jwe) { + header = new DefaultJweHeader(m); + } else { + header = new DefaultHeader(m); + } + + compressionCodec = compressionCodecResolver.resolveCompressionCodec(header); + + // =============== Body ================= + bytes = base64UrlDecoder.decode(base64UrlEncodedPayload); + if (compressionCodec != null) { + bytes = compressionCodec.decompress(bytes); + } + String payload = new String(bytes, Strings.UTF_8); + + Claims claims = null; + + if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it: + Map claimsMap = (Map) readValue(payload); + claims = new DefaultClaims(claimsMap); + } + + // =============== Signature ================= + if (base64UrlEncodedDigest != null) { //it is signed - validate the signature + + JwsHeader jwsHeader = (JwsHeader) header; + + SignatureAlgorithm algorithm = null; + + String alg = jwsHeader.getAlgorithm(); + if (Strings.hasText(alg)) { + algorithm = SignatureAlgorithm.forName(alg); + } + + if (algorithm == null || algorithm == SignatureAlgorithm.NONE) { + //it is plaintext, but it has a signature. This is invalid: + String msg = "JWT string has a digest/signature, but the header does not reference a valid signature " + + "algorithm."; + throw new MalformedJwtException(msg); + } + + if (key != null && keyBytes != null) { + throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either."); + } else if ((key != null || keyBytes != null) && signingKeyResolver != null) { + String object = key != null ? "a key object" : "key bytes"; + throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either."); + } + + //digitally signed, let's assert the signature: + Key key = this.key; + + if (key == null) { //fall back to keyBytes + + byte[] keyBytes = this.keyBytes; + + if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver + if (claims != null) { + key = signingKeyResolver.resolveSigningKey(jwsHeader, claims); + } else { + key = signingKeyResolver.resolveSigningKey(jwsHeader, payload); + } + } + + if (!Objects.isEmpty(keyBytes)) { + + Assert.isTrue(algorithm.isHmac(), + "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); + + key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); + } + } + + Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed."); + + //re-create the jwt part without the signature. This is what needs to be signed for verification: + String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload; + + JwtSignatureValidator validator; + try { + algorithm.assertValidVerificationKey(key); //since 0.10.0: https://github.com/jwtk/jjwt/issues/334 + validator = createSignatureValidator(algorithm, key); + } catch (WeakKeyException e) { + throw e; + } catch (InvalidKeyException | IllegalArgumentException e) { + String algName = algorithm.getValue(); + String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " + + "algorithm, but the specified signing key of type " + key.getClass().getName() + + " may not be used to validate " + algName + " signatures. Because the specified " + + "signing key reflects a specific and expected algorithm, and the JWT does not reflect " + + "this algorithm, it is likely that the JWT was not expected and therefore should not be " + + "trusted. Another possibility is that the parser was configured with the incorrect " + + "signing key, but this cannot be assumed for security reasons."; + throw new UnsupportedJwtException(msg, e); + } + + if (!validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) { + String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + + "asserted and should not be trusted."; + throw new SignatureException(msg); + } + } + + final boolean allowSkew = this.allowedClockSkewMillis > 0; + + //since 0.3: + if (claims != null) { + + final Date now = this.clock.now(); + long nowTime = now.getTime(); + + //https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.4 + //token MUST NOT be accepted on or after any specified exp time: + Date exp = claims.getExpiration(); + if (exp != null) { + + long maxTime = nowTime - this.allowedClockSkewMillis; + Date max = allowSkew ? new Date(maxTime) : now; + if (max.after(exp)) { + String expVal = DateFormats.formatIso8601(exp, false); + String nowVal = DateFormats.formatIso8601(now, false); + + long differenceMillis = maxTime - exp.getTime(); + + String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " + + differenceMillis + " milliseconds. Allowed clock skew: " + + this.allowedClockSkewMillis + " milliseconds."; + throw new ExpiredJwtException(header, claims, msg); + } + } + + //https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.5 + //token MUST NOT be accepted before any specified nbf time: + Date nbf = claims.getNotBefore(); + if (nbf != null) { + + long minTime = nowTime + this.allowedClockSkewMillis; + Date min = allowSkew ? new Date(minTime) : now; + if (min.before(nbf)) { + String nbfVal = DateFormats.formatIso8601(nbf, false); + String nowVal = DateFormats.formatIso8601(now, false); + + long differenceMillis = nbf.getTime() - minTime; + + String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal + + ", a difference of " + + differenceMillis + " milliseconds. Allowed clock skew: " + + this.allowedClockSkewMillis + " milliseconds."; + throw new PrematureJwtException(header, claims, msg); + } + } + + validateExpectedClaims(header, claims); + } + + Object body = claims != null ? claims : payload; + + if (base64UrlEncodedDigest != null) { + return new DefaultJws<>((JwsHeader) header, body, base64UrlEncodedDigest); + } else { + return new DefaultJwt<>(header, body); + } + } + + ======================================================================== */ + + @Override + public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException { + + // TODO, this logic is only need for a now deprecated code path + // remove this block in v1.0 (the equivalent is already in DefaultJwtParserBuilder) + if (this.deserializer == null) { + // try to find one based on the services available + // TODO: This util class will throw a UnavailableImplementationException here to retain behavior of previous version, remove in v1.0 + this.deserializer = LegacyServices.loadFirst(Deserializer.class); + } + + Assert.hasText(jwt, "JWT String argument cannot be null or empty."); + + if ("..".equals(jwt)) { + String msg = "JWT string '..' is missing a header."; + throw new MalformedJwtException(msg); + } + + TokenizedJwt tokenized = jwtTokenizer.tokenize(jwt); // =============== Header ================= Header header = null; CompressionCodec compressionCodec = null; - if (base64UrlEncodedHeader != null) { - byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedHeader); + if (tokenized.getProtected() != null) { + + byte[] bytes = base64UrlDecode(tokenized.getProtected()); String origValue = new String(bytes, Strings.UTF_8); Map m = (Map) readValue(origValue); - if (base64UrlEncodedDigest != null) { - header = new DefaultJwsHeader(m); + if (tokenized.getDigest() != null) { + header = tokenized instanceof TokenizedJwe ? new DefaultJweHeader(m) : new DefaultJwsHeader(m); } else { header = new DefaultHeader(m); } @@ -320,8 +593,8 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, // =============== Body ================= String payload = ""; // https://github.com/jwtk/jjwt/pull/540 - if (base64UrlEncodedPayload != null) { - byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedPayload); + if (tokenized.getBody() != null) { + byte[] bytes = base64UrlDecode(tokenized.getBody()); if (compressionCodec != null) { bytes = compressionCodec.decompress(bytes); } @@ -336,7 +609,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, } // =============== Signature ================= - if (base64UrlEncodedDigest != null) { //it is signed - validate the signature + if (tokenized.getDigest() != null) { //it is signed - validate the signature JwsHeader jwsHeader = (JwsHeader) header; @@ -345,11 +618,11 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, if (header != null) { String alg = jwsHeader.getAlgorithm(); if (Strings.hasText(alg)) { - algorithm = SignatureAlgorithm.forName(alg); + algorithm = SignatureAlgorithms.forName(alg); } } - if (algorithm == null || algorithm == SignatureAlgorithm.NONE) { + if (algorithm == null || algorithm == SignatureAlgorithms.NONE) { //it is plaintext, but it has a signature. This is invalid: String msg = "JWT string has a digest/signature, but the header does not reference a valid signature " + "algorithm."; @@ -379,30 +652,38 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, } if (!Objects.isEmpty(keyBytes)) { - - Assert.isTrue(algorithm.isHmac(), - "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); - - key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); + Assert.isTrue(algorithm instanceof SymmetricKeySignatureAlgorithm, + "Key bytes can only be specified for symmetric key signatures. Please specify a PublicKey or PrivateKey instance."); + key = new SecretKeySpec(keyBytes, ((SymmetricKeySignatureAlgorithm) algorithm).generateKey().getAlgorithm()); } } Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed."); //re-create the jwt part without the signature. This is what needs to be signed for verification: - String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR; - if (base64UrlEncodedPayload != null) { - jwtWithoutSignature += base64UrlEncodedPayload; + String jwtWithoutSignature = tokenized.getProtected() + SEPARATOR_CHAR; + if (tokenized.getBody() != null) { + jwtWithoutSignature += tokenized.getBody(); } - JwtSignatureValidator validator; + byte[] data = jwtWithoutSignature.getBytes(StandardCharsets.US_ASCII); + byte[] signature = base64UrlDecode(tokenized.getDigest()); + try { - algorithm.assertValidVerificationKey(key); //since 0.10.0: https://github.com/jwtk/jjwt/issues/334 - validator = createSignatureValidator(algorithm, key); + VerifySignatureRequest request = + new DefaultVerifySignatureRequest(data, key, this.provider, null, signature); + + //SignatureValidator validator = DefaultSignatureValidatorFactory.INSTANCE.createSignatureValidator(io.jsonwebtoken.SignatureAlgorithm.forName(algorithm.getName()), key); + // if (!validator.isValid(data, signature)) { + if (!algorithm.verify(request)) { + String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + + "asserted and should not be trusted."; + throw new SignatureException(msg); + } } catch (WeakKeyException e) { throw e; } catch (InvalidKeyException | IllegalArgumentException e) { - String algName = algorithm.getValue(); + String algName = algorithm.getName(); String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " + "algorithm, but the specified signing key of type " + key.getClass().getName() + " may not be used to validate " + algName + " signatures. Because the specified " + @@ -412,12 +693,6 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, "signing key, but this cannot be assumed for security reasons."; throw new UnsupportedJwtException(msg, e); } - - if (!validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) { - String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + - "asserted and should not be trusted."; - throw new SignatureException(msg); - } } final boolean allowSkew = this.allowedClockSkewMillis > 0; @@ -474,8 +749,8 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, Object body = claims != null ? claims : payload; - if (base64UrlEncodedDigest != null) { - return new DefaultJws<>((JwsHeader) header, body, base64UrlEncodedDigest); + if (tokenized.getDigest() != null) { + return new DefaultJws<>((JwsHeader) header, body, tokenized.getDigest()); } else { return new DefaultJwt<>(header, body); } @@ -533,13 +808,6 @@ private void validateExpectedClaims(Header header, Claims claims) { } } - /* - * @since 0.5 mostly to allow testing overrides - */ - protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) { - return new DefaultJwtSignatureValidator(alg, key, base64UrlDecoder); - } - @Override public T parse(String compact, JwtHandler handler) throws ExpiredJwtException, MalformedJwtException, SignatureException { @@ -614,6 +882,15 @@ public Jws onClaimsJws(Jws jws) { }); } + protected byte[] base64UrlDecode(String base64UrlEncoded) { + try { + return base64UrlDecoder.decode(base64UrlEncoded); + } catch (DecodingException e) { + String msg = "Invalid Base64Url string: " + base64UrlEncoded; + throw new MalformedJwtException(msg, e); + } + } + @SuppressWarnings("unchecked") protected Map readValue(String val) { byte[] bytes = val.getBytes(Strings.UTF_8); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index 3b299ebfb..d86f9ec12 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -22,13 +22,14 @@ import io.jsonwebtoken.JwtParserBuilder; import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; +import io.jsonwebtoken.impl.lang.Services; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.impl.lang.Services; import java.security.Key; +import java.security.Provider; import java.util.Date; import java.util.Map; @@ -49,6 +50,8 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { static final String MAX_CLOCK_SKEW_ILLEGAL_MSG = "Illegal allowedClockSkewMillis value: multiplying this " + "value by 1000 to obtain the number of milliseconds would cause a numeric overflow."; + private Provider provider; + private byte[] keyBytes; private Key key; @@ -67,6 +70,11 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { private long allowedClockSkewMillis = 0; + @Override + public JwtParserBuilder setProvider(Provider provider) { + this.provider = provider; + return this; + } @Override public JwtParserBuilder deserializeJsonWith(Deserializer> deserializer) { @@ -197,7 +205,8 @@ public JwtParser build() { } return new ImmutableJwtParser( - new DefaultJwtParser(signingKeyResolver, + new DefaultJwtParser(provider, + signingKeyResolver, key, keyBytes, clock, diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwe.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwe.java new file mode 100644 index 000000000..68724dc2a --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwe.java @@ -0,0 +1,23 @@ +package io.jsonwebtoken.impl; + +class DefaultTokenizedJwe extends DefaultTokenizedJwt implements TokenizedJwe { + + private final String encryptedKey; + private final String iv; + + DefaultTokenizedJwe(String protectedHeader, String body, String digest, String encryptedKey, String iv) { + super(protectedHeader, body, digest); + this.encryptedKey = encryptedKey; + this.iv = iv; + } + + @Override + public String getEncryptedKey() { + return this.encryptedKey; + } + + @Override + public String getIv() { + return this.iv; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java new file mode 100644 index 000000000..cf7d1ae52 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java @@ -0,0 +1,29 @@ +package io.jsonwebtoken.impl; + +class DefaultTokenizedJwt implements TokenizedJwt { + + private final String protectedHeader; + private final String body; + private final String digest; + + DefaultTokenizedJwt(String protectedHeader, String body, String digest) { + this.protectedHeader = protectedHeader; + this.body = body; + this.digest = digest; + } + + @Override + public String getProtected() { + return this.protectedHeader; + } + + @Override + public String getBody() { + return this.body; + } + + @Override + public String getDigest() { + return this.digest; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DispatchingParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DispatchingParser.java new file mode 100644 index 000000000..99b465b63 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DispatchingParser.java @@ -0,0 +1,145 @@ +package io.jsonwebtoken.impl; + +import io.jsonwebtoken.lang.Assert; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DispatchingParser { + + static final char DELIMITER = '.'; + + /* + + public void parse(String compactJwe) { + + //parse the constituent parts of the compact JWE: + + String base64UrlEncodedHeader = null; //JWT, JWS or JWE + + String base64UrlEncodedCek = null; //JWE only + String base64UrlEncodedPayload = null; //JWT or JWS + + String base64UrlEncodedIv = null; //JWE only + String base64UrlEncodedCiphertext = null; //JWE only + + String base64UrlEncodedTag = null; //JWE only + String base64UrlencodedDigest = null; //JWS only + + StringBuilder sb = new StringBuilder(); + + char[] chars = compactJwe.toCharArray(); + + int tokenIndex = 0; + + for (char c : chars) { + + Assert.isTrue(!Character.isWhitespace(c), "Compact JWT strings cannot contain whitespace."); + + if (c == DELIMITER) { + + String value = sb.length() > 0 ? sb.toString() : null; + + switch (tokenIndex) { + case 0: + base64UrlEncodedHeader = value; + break; + case 1: + //we'll figure out if we have a compact JWE or JWS after finishing inspecting the char array: + base64UrlEncodedCek = value; + base64UrlEncodedPayload = value; + case 2: + base64UrlEncodedIv = value; + break; + case 3: + base64UrlEncodedCiphertext = value; + break; + } + + sb = new StringBuilder(); + tokenIndex++; + } else { + sb.append(c); + } + } + + boolean jwe = false; + if (tokenIndex == 2) { // JWT or JWS + jwe = false; + } else if (tokenIndex == 4) { // JWE + jwe = true; + } else { + String msg = "Invalid compact JWT string - invalid number of period character delimiters: " + tokenIndex + + ". JWTs and JWSs must have exactly 2 periods, JWEs must have exactly 4 periods."; + throw new IllegalArgumentException(msg); + } + + if (sb.length() > 0) { + String value = sb.toString(); + if (jwe) { + base64UrlEncodedTag = value; + } else { + base64UrlencodedDigest = value; + } + } + + throw new UnsupportedOperationException("Not yet implemented."); + + /* + + + base64UrlEncodedTag = sb.toString(); + + Assert.notNull(base64UrlEncodedHeader, "Invalid compact JWE: base64Url JWE Protected Header is missing."); + Assert.notNull(base64UrlEncodedIv, "Invalid compact JWE: base64Url JWE Initialization Vector is missing."); + Assert.notNull(base64UrlEncodedCiphertext, "Invalid compact JWE: base64Url JWE Ciphertext is missing."); + Assert.notNull(base64UrlEncodedTag, "Invalid compact JWE: base64Url JWE Authentication Tag is missing."); + + //find which encryption key was used so we can decrypt: + final byte[] headerBytes = base64UrlDecode(base64UrlEncodedHeader); + final DefaultHeaders headers = serializationCodec.deserialize(headerBytes, DefaultHeaders.class); + + SecretKey secretKey = secretKeyResolver.getSecretKey(headers); + if (secretKey == null) { + String msg = "SecretKeyResolver did not return a secret key for headers " + headers + + ". This is required for message decryption."; + throw new CryptoException(msg); + } + + byte[] aad = base64UrlEncodedHeader.getBytes(StandardCharsets.US_ASCII); + byte[] iv = base64UrlDecode(base64UrlEncodedIv); + byte[] ciphertext = base64UrlDecode(base64UrlEncodedCiphertext); + byte[] tag = base64UrlDecode(base64UrlEncodedTag); + + DecryptionRequest dreq = DecryptionRequests.builder() + .setKey(secretKey.getEncoded()) + .setAdditionalAuthenticatedData(aad) + .setInitializationVector(iv) + .setCiphertext(ciphertext) + .setAuthenticationTag(tag) + .build(); + + byte[] plaintext = encryptionService.decrypt(dreq); + + CompressionAlgorithm calg = headers.getCompressionAlgorithm(); + if (calg != null) { + plaintext = calg.getCodec().decompress(plaintext); + } + + Object body = null; + + val = headers.get(JAVA_TYPE_HEADER_NAME); + if (val != null) { + String jtyp = val.toString(); + if (jtyp != null) { + Class bodyType = ClassUtils.forName(jtyp); + body = serializationCodec.deserialize(plaintext, bodyType); + } + } + + message.getHeaders().putAll(headers); + message.setBody(body); + + } + */ +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 0f410dd2c..92c2c6891 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -16,7 +16,11 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.DateFormats; +import io.jsonwebtoken.lang.Strings; + +import java.lang.reflect.Array; import java.text.ParseException; import java.util.Calendar; import java.util.Collection; @@ -100,8 +104,16 @@ protected static Date toSpecDate(Object v, String name) { return toDate(v, name); } + protected static boolean isReduceableToNull(Object v) { + return v == null || + (v instanceof String && !Strings.hasText((String)v)) || + (v instanceof Collection && Collections.isEmpty((Collection) v)) || + (v instanceof Map && Collections.isEmpty((Map)v)) || + (v.getClass().isArray() && Array.getLength(v) == 0); + } + protected void setValue(String name, Object v) { - if (v == null) { + if (isReduceableToNull(v)) { map.remove(name); } else { map.put(name, v); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java new file mode 100644 index 000000000..30910a0da --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java @@ -0,0 +1,77 @@ +package io.jsonwebtoken.impl; + +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; + +public class JwtTokenizer { + + static final char DELIMITER = '.'; + + private static final String DELIM_ERR_MSG_PREFIX = "Invalid compact JWT string: Compact JWSs must contain " + + "exactly 2 period characters, and compact JWEs must contain exactly 4. Found: "; + + @SuppressWarnings("unchecked") + public T tokenize(String jwt) { + + Assert.hasText(jwt, "Argument cannot be null or empty."); + + String protectedHeader = null; //Both JWS and JWE + String body = null; //JWS Payload or JWE Ciphertext + String digest = null; //JWS Signature or JWE AAD Tag + String encryptedKey = null; //JWE only + String iv = null; //JWE only + + int delimiterCount = 0; + + StringBuilder sb = new StringBuilder(128); + + for (char c : jwt.toCharArray()) { + + Assert.isTrue(!Character.isWhitespace(c), "Compact JWT strings may not contain whitespace."); + + if (c == DELIMITER) { + + CharSequence tokenSeq = Strings.clean(sb); + String token = tokenSeq != null ? tokenSeq.toString() : null; + + switch (delimiterCount) { + case 0: + protectedHeader = token; + break; + case 1: + body = token; //for JWS + encryptedKey = token; //for JWE + break; + case 2: + body = null; //clear out value set for JWS + iv = token; + break; + case 3: + body = token; + break; + } + + sb.setLength(0); + delimiterCount++; + } else { + sb.append(c); + } + } + + if (delimiterCount != 2 && delimiterCount != 4) { + String msg = DELIM_ERR_MSG_PREFIX + delimiterCount; + throw new MalformedJwtException(msg); + } + + if (sb.length() > 0) { + digest = Strings.clean(sb.toString()); + } + + if (delimiterCount == 2) { + return (T) new DefaultTokenizedJwt(protectedHeader, body, digest); + } + + return (T) new DefaultTokenizedJwe(protectedHeader, body, digest, encryptedKey, iv); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwe.java b/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwe.java new file mode 100644 index 000000000..6e790ac10 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwe.java @@ -0,0 +1,8 @@ +package io.jsonwebtoken.impl; + +public interface TokenizedJwe extends TokenizedJwt { + + String getEncryptedKey(); + + String getIv(); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwt.java b/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwt.java new file mode 100644 index 000000000..3db2dbe2e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwt.java @@ -0,0 +1,20 @@ +package io.jsonwebtoken.impl; + +public interface TokenizedJwt { + + /** + * Protected header. + * @return protected header. + */ + String getProtected(); + + /** + * Payload for JWS, Ciphertext for JWE + */ + String getBody(); + + /** + * Signature for JWS, AAD Tag for JWE. + */ + String getDigest(); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwtBuilder.java new file mode 100644 index 000000000..cd4ad5edd --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwtBuilder.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.impl; + +public interface TokenizedJwtBuilder { + + TokenizedJwtBuilder append(String token); + + T build(); + +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java index 6296289fb..0f6f4d46f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java @@ -17,6 +17,8 @@ import io.jsonwebtoken.JwtException; import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.impl.security.EllipticCurveSignatureAlgorithm; +import io.jsonwebtoken.impl.security.Randoms; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Strings; @@ -68,19 +70,19 @@ public static KeyPair generateKeyPair() { /** * Generates a new secure-random key pair of sufficient strength for the specified Elliptic Curve {@link * SignatureAlgorithm} (must be one of {@code ES256}, {@code ES384} or {@code ES512}) using JJWT's default {@link - * SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. This is a convenience method that immediately + * Randoms#secureRandom() SecureRandom instance}. This is a convenience method that immediately * delegates to {@link #generateKeyPair(SignatureAlgorithm, SecureRandom)}. * * @param alg the algorithm indicating strength, must be one of {@code ES256}, {@code ES384} or {@code ES512} * @return a new secure-randomly generated key pair of sufficient strength for the specified {@link * SignatureAlgorithm} (must be one of {@code ES256}, {@code ES384} or {@code ES512}) using JJWT's default {@link - * SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. + * Randoms#secureRandom() SecureRandom instance}. * @see #generateKeyPair() * @see #generateKeyPair(SignatureAlgorithm, SecureRandom) * @see #generateKeyPair(String, String, SignatureAlgorithm, SecureRandom) */ public static KeyPair generateKeyPair(SignatureAlgorithm alg) { - return generateKeyPair(alg, DEFAULT_SECURE_RANDOM); + return generateKeyPair(alg, Randoms.secureRandom()); } /** @@ -169,7 +171,6 @@ public static int getSignatureByteArrayLength(final SignatureAlgorithm alg) } } - /** * Transcodes the JCA ASN.1/DER-encoded signature into the concatenated * R + S format expected by ECDSA JWS. @@ -178,52 +179,11 @@ public static int getSignatureByteArrayLength(final SignatureAlgorithm alg) * @param outputLength The expected length of the ECDSA JWS signature. * @return The ECDSA JWS encoded signature. * @throws JwtException If the ASN.1/DER signature format is invalid. + * @deprecated since JJWT_RELEASE_VERSION. Use {@code ElliptiCurveSignatureAlgorithm.transcodeSignatureToConcat} instead. */ + @Deprecated public static byte[] transcodeSignatureToConcat(final byte[] derSignature, int outputLength) throws JwtException { - - if (derSignature.length < 8 || derSignature[0] != 48) { - throw new JwtException("Invalid ECDSA signature format"); - } - - int offset; - if (derSignature[1] > 0) { - offset = 2; - } else if (derSignature[1] == (byte) 0x81) { - offset = 3; - } else { - throw new JwtException("Invalid ECDSA signature format"); - } - - byte rLength = derSignature[offset + 1]; - - int i = rLength; - while ((i > 0) && (derSignature[(offset + 2 + rLength) - i] == 0)) { - i--; - } - - byte sLength = derSignature[offset + 2 + rLength + 1]; - - int j = sLength; - while ((j > 0) && (derSignature[(offset + 2 + rLength + 2 + sLength) - j] == 0)) { - j--; - } - - int rawLen = Math.max(i, j); - rawLen = Math.max(rawLen, outputLength / 2); - - if ((derSignature[offset - 1] & 0xff) != derSignature.length - offset - || (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength - || derSignature[offset] != 2 - || derSignature[offset + 2 + rLength] != 2) { - throw new JwtException("Invalid ECDSA signature format"); - } - - final byte[] concatSignature = new byte[2 * rawLen]; - - System.arraycopy(derSignature, (offset + 2 + rLength) - i, concatSignature, rawLen - i, i); - System.arraycopy(derSignature, (offset + 2 + rLength + 2 + sLength) - j, concatSignature, 2 * rawLen - j, j); - - return concatSignature; + return EllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(derSignature, outputLength); } @@ -236,68 +196,10 @@ public static byte[] transcodeSignatureToConcat(final byte[] derSignature, int o * {@code null}. * @return The ASN.1/DER encoded signature. * @throws JwtException If the ECDSA JWS signature format is invalid. + * @deprecated since JJWT_RELEASE_VERSION. Use {@link EllipticCurveSignatureAlgorithm#transcodeSignatureToDER(byte[])} instead. */ + @Deprecated public static byte[] transcodeSignatureToDER(byte[] jwsSignature) throws JwtException { - - int rawLen = jwsSignature.length / 2; - - int i = rawLen; - - while ((i > 0) && (jwsSignature[rawLen - i] == 0)) { - i--; - } - - int j = i; - - if (jwsSignature[rawLen - i] < 0) { - j += 1; - } - - int k = rawLen; - - while ((k > 0) && (jwsSignature[2 * rawLen - k] == 0)) { - k--; - } - - int l = k; - - if (jwsSignature[2 * rawLen - k] < 0) { - l += 1; - } - - int len = 2 + j + 2 + l; - - if (len > 255) { - throw new JwtException("Invalid ECDSA signature format"); - } - - int offset; - - final byte derSignature[]; - - if (len < 128) { - derSignature = new byte[2 + 2 + j + 2 + l]; - offset = 1; - } else { - derSignature = new byte[3 + 2 + j + 2 + l]; - derSignature[1] = (byte) 0x81; - offset = 2; - } - - derSignature[0] = 48; - derSignature[offset++] = (byte) len; - derSignature[offset++] = 2; - derSignature[offset++] = (byte) j; - - System.arraycopy(jwsSignature, rawLen - i, derSignature, (offset + j) - i, i); - - offset += j; - - derSignature[offset++] = 2; - derSignature[offset++] = (byte) l; - - System.arraycopy(jwsSignature, 2 * rawLen - k, derSignature, (offset + l) - k, k); - - return derSignature; + return EllipticCurveSignatureAlgorithm.transcodeSignatureToDER(jwsSignature); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java index 3fc1fd9a7..9f8729daa 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java @@ -16,6 +16,7 @@ package io.jsonwebtoken.impl.crypto; import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.impl.security.EllipticCurveSignatureAlgorithm; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.SignatureException; @@ -48,7 +49,7 @@ public boolean isValid(byte[] data, byte[] signature) { * and backwards compatibility will possibly be removed in a future version of this library. * * **/ - byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : EllipticCurveProvider.transcodeSignatureToDER(signature); + byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : EllipticCurveSignatureAlgorithm.transcodeSignatureToDER(signature); return doVerify(sig, publicKey, data, derSignature); } catch (Exception e) { String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage(); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java index 9aeb1e8b3..3d0922fd4 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java @@ -17,6 +17,7 @@ import io.jsonwebtoken.JwtException; import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.impl.security.EllipticCurveSignatureAlgorithm; import io.jsonwebtoken.security.SignatureException; import java.security.InvalidKeyException; @@ -54,6 +55,6 @@ protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.S Signature sig = createSignatureInstance(); sig.initSign(privateKey); sig.update(data); - return transcodeSignatureToConcat(sig.sign(), getSignatureByteArrayLength(alg)); + return EllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(sig.sign(), getSignatureByteArrayLength(alg)); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java index b0117411c..e577b6262 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java @@ -16,6 +16,7 @@ package io.jsonwebtoken.impl.crypto; import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.impl.security.Randoms; import io.jsonwebtoken.lang.Assert; import javax.crypto.KeyGenerator; @@ -48,19 +49,19 @@ public static SecretKey generateKey() { /** * Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures * according to the specified {@code SignatureAlgorithm} using JJWT's default {@link - * SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. This is a convenience method that immediately + * Randoms#secureRandom() SecureRandom instance}. This is a convenience method that immediately * delegates to {@link #generateKey(SignatureAlgorithm, SecureRandom)}. * * @param alg the desired signature algorithm * @return a new secure-random secret key of a length suitable for creating and verifying HMAC signatures according - * to the specified {@code SignatureAlgorithm} using JJWT's default {@link SignatureProvider#DEFAULT_SECURE_RANDOM - * SecureRandom instance}. + * to the specified {@code SignatureAlgorithm} using JJWT's default {@link + * Randoms#secureRandom() SecureRandom instance}. * @see #generateKey() * @see #generateKey(SignatureAlgorithm, SecureRandom) * @since 0.5 */ public static SecretKey generateKey(SignatureAlgorithm alg) { - return generateKey(alg, DEFAULT_SECURE_RANDOM); + return generateKey(alg, Randoms.secureRandom()); } /** diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java index bce2b1c7a..11a91102e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java @@ -16,6 +16,7 @@ package io.jsonwebtoken.impl.crypto; import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.impl.security.Randoms; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.RuntimeEnvironment; import io.jsonwebtoken.security.SignatureException; @@ -105,7 +106,7 @@ public static KeyPair generateKeyPair() { /** * Generates a new RSA secure-randomly key pair of the specified size using JJWT's default {@link - * SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. This is a convenience method that immediately + * Randoms#secureRandom() SecureRandom instance}. This is a convenience method that immediately * delegates to {@link #generateKeyPair(int, SecureRandom)}. * * @param keySizeInBits the key size in bits (NOT bytes). @@ -116,12 +117,12 @@ public static KeyPair generateKeyPair() { * @since 0.5 */ public static KeyPair generateKeyPair(int keySizeInBits) { - return generateKeyPair(keySizeInBits, DEFAULT_SECURE_RANDOM); + return generateKeyPair(keySizeInBits, Randoms.secureRandom()); } /** * Generates a new RSA secure-randomly key pair suitable for the specified SignatureAlgorithm using JJWT's - * default {@link SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. This is a convenience method + * default {@link Randoms#secureRandom() SecureRandom instance}. This is a convenience method * that immediately delegates to {@link #generateKeyPair(int)} based on the relevant key size for the specified * algorithm. * @@ -146,7 +147,7 @@ public static KeyPair generateKeyPair(SignatureAlgorithm alg) { keySizeInBits = 3072; break; } - return generateKeyPair(keySizeInBits, DEFAULT_SECURE_RANDOM); + return generateKeyPair(keySizeInBits, Randoms.secureRandom()); } /** diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java index ba36be1be..0b4cfd024 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java @@ -33,16 +33,19 @@ public class RsaSignatureValidator extends RsaProvider implements SignatureValid public RsaSignatureValidator(SignatureAlgorithm alg, Key key) { super(alg, key); - Assert.isTrue(key instanceof RSAPrivateKey || key instanceof RSAPublicKey, - "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance."); + Assert.isTrue(isRsaKey(key), "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance."); this.SIGNER = key instanceof RSAPrivateKey ? new RsaSigner(alg, key) : null; } + protected static boolean isRsaKey(Key key) { + return key instanceof RSAPrivateKey || key instanceof RSAPublicKey; + } + @Override public boolean isValid(byte[] data, byte[] signature) { if (key instanceof PublicKey) { - Signature sig = createSignatureInstance(); PublicKey publicKey = (PublicKey) key; + Signature sig = createSignatureInstance(); try { return doVerify(sig, publicKey, data, signature); } catch (Exception e) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java index 7419a478b..8665565fb 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java @@ -16,6 +16,7 @@ package io.jsonwebtoken.impl.crypto; import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.impl.security.Randoms; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.RuntimeEnvironment; import io.jsonwebtoken.security.SignatureException; @@ -28,24 +29,10 @@ abstract class SignatureProvider { /** - * JJWT's default SecureRandom number generator. This RNG is initialized using the JVM default as follows: - * - *

-     * static {
-     *     DEFAULT_SECURE_RANDOM = new SecureRandom();
-     *     DEFAULT_SECURE_RANDOM.nextBytes(new byte[64]);
-     * }
-     * 
- * - *

nextBytes is called to force the RNG to initialize itself if not already initialized. The - * byte array is not used and discarded immediately for garbage collection.

+ * @deprecated use {@link Randoms#secureRandom() Randoms.secureRandom()} instead. */ - public static final SecureRandom DEFAULT_SECURE_RANDOM; - - static { - DEFAULT_SECURE_RANDOM = new SecureRandom(); - DEFAULT_SECURE_RANDOM.nextBytes(new byte[64]); - } + @Deprecated //TODO: remove for 1.0 + public static final SecureRandom DEFAULT_SECURE_RANDOM = Randoms.secureRandom(); protected final SignatureAlgorithm alg; protected final Key key; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithm.java new file mode 100644 index 000000000..7760420c0 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithm.java @@ -0,0 +1,112 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadIvRequest; +import io.jsonwebtoken.security.AeadIvEncryptionResult; +import io.jsonwebtoken.security.AeadRequest; +import io.jsonwebtoken.security.AeadSymmetricEncryptionAlgorithm; +import io.jsonwebtoken.security.AssociatedDataSource; +import io.jsonwebtoken.security.CryptoException; +import io.jsonwebtoken.security.CryptoRequest; +import io.jsonwebtoken.security.InitializationVectorSource; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.security.SecureRandom; + +import static io.jsonwebtoken.lang.Arrays.*; + +/** + * @since JJWT_RELEASE_VERSION + */ +abstract class AbstractAeadAesEncryptionAlgorithm + extends AbstractEncryptionAlgorithm, AeadIvEncryptionResult, AeadIvRequest> + implements AeadSymmetricEncryptionAlgorithm { + + protected static final int AES_BLOCK_SIZE_BYTES = 16; + protected static final int AES_BLOCK_SIZE_BITS = AES_BLOCK_SIZE_BYTES * Byte.SIZE; + public static final String INVALID_GENERATED_IV_LENGTH = + "generatedIvLengthInBits must be a positive number <= " + AES_BLOCK_SIZE_BITS; + + protected static final String DECRYPT_NO_IV = "This EncryptionAlgorithm implementation rejects decryption " + + "requests that do not include initialization vectors. AES ciphertext without an IV is weak and should " + + "never be used."; + + private final int generatedIvByteLength; + private final int requiredKeyByteLength; + private final int requiredKeyBitLength; + + public AbstractAeadAesEncryptionAlgorithm(String name, String transformationString, int generatedIvLengthInBits, int requiredKeySizeInBits) { + + super(name, transformationString); + + Assert.isTrue(generatedIvLengthInBits > 0 && generatedIvLengthInBits <= AES_BLOCK_SIZE_BITS, INVALID_GENERATED_IV_LENGTH); + Assert.isTrue((generatedIvLengthInBits % Byte.SIZE) == 0, "generatedIvLengthInBits must be evenly divisible by 8."); + this.generatedIvByteLength = generatedIvLengthInBits / Byte.SIZE; + + Assert.isTrue(requiredKeySizeInBits > 0, "requiredKeyLengthInBits must be greater than zero."); + Assert.isTrue((requiredKeySizeInBits % Byte.SIZE) == 0, "requiredKeyLengthInBits must be evenly divisible by 8."); + this.requiredKeyBitLength = requiredKeySizeInBits; + this.requiredKeyByteLength = requiredKeySizeInBits / Byte.SIZE; + } + + public int getRequiredKeyByteLength() { + return this.requiredKeyByteLength; + } + + @Override + public SecretKey generateKey() { + try { + return doGenerateKey(); + } catch (Exception e) { + throw new CryptoException("Unable to generate a new " + getName() + " SecretKey: " + e.getMessage(), e); + } + } + + protected SecretKey doGenerateKey() throws Exception { + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + keyGenerator.init(this.requiredKeyBitLength); + return keyGenerator.generateKey(); + } + + byte[] ensureInitializationVector(AeadRequest request) { + byte[] iv = null; + if (request instanceof InitializationVectorSource) { + iv = Arrays.clean(((InitializationVectorSource)request).getInitializationVector()); + } + if (Arrays.length(iv) == 0) { + iv = new byte[this.generatedIvByteLength]; + SecureRandom random = ensureSecureRandom(request); + random.nextBytes(iv); + } + return iv; + } + + SecretKey assertKey(CryptoRequest request) { + SecretKey key = request.getKey(); + return assertKeyLength(key); + } + + SecretKey assertKeyLength(SecretKey key) { + int length = length(key.getEncoded()); + if (length != requiredKeyByteLength) { + throw new CryptoException("The " + getName() + " algorithm requires that keys have a key length of " + + "(preferably secure-random) " + requiredKeyBitLength + " bits (" + + requiredKeyByteLength + " bytes). The provided key has a length of " + length * Byte.SIZE + + " bits (" + length + " bytes)."); + } + return key; + } + + byte[] assertDecryptionIv(InitializationVectorSource src) throws IllegalArgumentException { + byte[] iv = src.getInitializationVector(); + Assert.notEmpty(iv, DECRYPT_NO_IV); + return iv; + } + + byte[] getAAD(AssociatedDataSource src) { + byte[] aad = src.getAssociatedData(); + return io.jsonwebtoken.lang.Arrays.clean(aad); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwk.java new file mode 100644 index 000000000..ade7425a7 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwk.java @@ -0,0 +1,65 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.CurveId; +import io.jsonwebtoken.security.CurveIds; +import io.jsonwebtoken.security.EcJwk; +import io.jsonwebtoken.security.MalformedKeyException; + +@SuppressWarnings("unchecked") +class AbstractEcJwk extends AbstractJwk implements EcJwk { + + static final String TYPE_VALUE = "EC"; + static final String CURVE_ID = "crv"; + static final String X = "x"; + static final String Y = "y"; + + AbstractEcJwk() { + super(TYPE_VALUE); + } + + @Override + public CurveId getCurveId() { + Object val = get(CURVE_ID); + if (val == null) { + return null; + } + if (val instanceof CurveId) { + return (CurveId) val; + } + if (val instanceof String) { + CurveId id = CurveIds.forValue((String) val); + setCurveId(id); //replace string with type safe value + return id; + } + throw new MalformedKeyException("EC JWK 'crv' value must be an CurveId or a String. Value has type: " + + val.getClass().getName()); + } + + @Override + public T setCurveId(CurveId curveId) { + return setRequiredValue(CURVE_ID, curveId, "curve id"); + } + + @Override + public String getX() { + return getString(X); + } + + @Override + public T setX(String x) { + return setRequiredValue(X, x, "x coordinate"); + } + + @Override + public String getY() { + return getString(Y); + } + + @Override + public T setY(String y) { + y = Strings.clean(y); + setValue(Y, y); + return (T) this; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkBuilder.java new file mode 100644 index 000000000..e7588022e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkBuilder.java @@ -0,0 +1,31 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.CurveId; +import io.jsonwebtoken.security.EcJwk; +import io.jsonwebtoken.security.EcJwkBuilder; + +@SuppressWarnings("unchecked") +abstract class AbstractEcJwkBuilder extends AbstractJwkBuilder implements EcJwkBuilder { + + AbstractEcJwkBuilder(JwkValidator validator) { + super(validator); + } + + @Override + public T setCurveId(CurveId curveId) { + this.jwk.setCurveId(curveId); + return (T) this; + } + + @Override + public T setX(String x) { + this.jwk.setX(x); + return (T) this; + } + + @Override + public T setY(String y) { + this.jwk.setY(y); + return (T) this; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkValidator.java new file mode 100644 index 000000000..eeabbcdc1 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkValidator.java @@ -0,0 +1,39 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.CurveId; +import io.jsonwebtoken.security.CurveIds; +import io.jsonwebtoken.security.EcJwk; +import io.jsonwebtoken.security.KeyException; + +abstract class AbstractEcJwkValidator extends AbstractJwkValidator { + + AbstractEcJwkValidator() { + super(AbstractEcJwk.TYPE_VALUE); + } + + @Override + final void validateJwk(T jwk) throws KeyException { + + CurveId curveId = jwk.getCurveId(); + if (curveId == null) { // https://tools.ietf.org/html/rfc7518#section-6.2.1 + malformed("EC JWK curve id ('crv' property) must be specified."); + } + + String x = jwk.getX(); + if (!Strings.hasText(x)) { // https://tools.ietf.org/html/rfc7518#section-6.2.1 + malformed("EC JWK x coordinate ('x' property) must be specified."); + } + + // https://tools.ietf.org/html/rfc7518#section-6.2.1 (last sentence): + if (CurveIds.isStandard(curveId) && !Strings.hasText(jwk.getY())) { + malformed(curveId + " EC JWK y coordinate ('y' property) must be specified."); + } + + //TODO: RFC length validation for x and y values + + validateEcJwk(jwk); + } + + abstract void validateEcJwk(T jwk); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithm.java new file mode 100644 index 000000000..b2f3cd0fa --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithm.java @@ -0,0 +1,48 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.CryptoException; +import io.jsonwebtoken.security.CryptoRequest; +import io.jsonwebtoken.security.EncryptionAlgorithm; +import io.jsonwebtoken.security.EncryptionResult; + +import java.security.Key; + +abstract class AbstractEncryptionAlgorithm, + ERes extends EncryptionResult, DReq extends CryptoRequest> + extends CipherAlgorithm implements EncryptionAlgorithm { + + AbstractEncryptionAlgorithm(String name, String transformationString) { + super(name, transformationString); + } + + @Override + public ERes encrypt(EReq req) throws CryptoException { + try { + Assert.notNull(req, "Encryption request cannot be null."); + return doEncrypt(req); + } catch (CryptoException ce) { + throw ce; //propagate + } catch (Exception e) { + String msg = "Unable to perform " + getName() + " encryption: " + e.getMessage(); + throw new CryptoException(msg, e); + } + } + + protected abstract ERes doEncrypt(EReq req) throws Exception; + + @Override + public byte[] decrypt(DReq req) throws CryptoException { + try { + Assert.notNull(req, "Decryption request cannot be null."); + return doDecrypt(req); + } catch (CryptoException ce) { + throw ce; //propagate + } catch (Exception e) { + String msg = "Unable to perform " + getName() + " decryption: " + e.getMessage(); + throw new CryptoException(msg, e); + } + } + + protected abstract byte[] doDecrypt(DReq req) throws Exception; +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java new file mode 100644 index 000000000..309f6ef18 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java @@ -0,0 +1,191 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.JwtMap; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.MalformedKeyException; + +import java.lang.reflect.Array; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +@SuppressWarnings("unchecked") +abstract class AbstractJwk extends JwtMap implements Jwk { + + static final String TYPE = "kty"; + static final String USE = "use"; + static final String OPERATIONS = "key_ops"; + static final String ALGORITHM = "alg"; + static final String ID = "kid"; + static final String X509_URL = "x5u"; + static final String X509_CERT_CHAIN = "x5c"; + static final String X509_SHA1_THUMBPRINT = "x5t"; + static final String X509_SHA256_THUMBPRINT = "x5t#S256"; + + AbstractJwk(String type) { + type = Strings.clean(type); + Assert.notNull(type, "JWK type cannot be null or empty."); + put(TYPE, type); + } + + T setRequiredValue(String key, Object value, String name) { + boolean reduceable = value != null && isReduceableToNull(value); + if (reduceable) { + value = null; + } + if (value == null) { + String msg = getType() + " JWK " + name + " ('" + key + "' property) cannot be null"; + if (reduceable) { + msg += " or empty"; + } + msg += "."; + throw new IllegalArgumentException(msg); + } + setValue(key, value); + return (T) this; + } + + protected List getList(String name) { + Object value = get(name); + if (value == null) { + return null; + } + List list = new ArrayList<>(); + if (value instanceof Collection) { + Collection c = (Collection)value; + for (Object o : c) { + list.add(o == null ? null : String.valueOf(o)); + } + } else if (value.getClass().isArray()) { + int length = Array.getLength(value); + for (int i = 0; i < length; i ++) { + Object o = Array.get(value, i); + list.add(o == null ? null : String.valueOf(o)); + } + } + return list; + } + + @Override + public String getType() { + return getString(TYPE); + } + + @Override + public String getUse() { + return getString(USE); + } + + @Override + public T setUse(String use) { + setValue(USE, Strings.clean(use)); + return (T)this; + } + + @Override + public Set getOperations() { + Object val = get(OPERATIONS); + if (val instanceof Set) { + return (Set)val; + } + List list = getList(OPERATIONS); + return val == null ? null : new LinkedHashSet<>(list); + } + + @Override + public T setOperations(Set ops) { + Set operations = Collections.isEmpty(ops) ? null : new LinkedHashSet<>(ops); + setValue(OPERATIONS, operations); + return (T)this; + } + + @Override + public String getAlgorithm() { + return getString(ALGORITHM); + } + + @Override + public T setAlgorithm(String alg) { + setValue(ALGORITHM, Strings.clean(alg)); + return (T)this; + } + + @Override + public String getId() { + return getString(ID); + } + + @Override + public T setId(String id) { + setValue(ID, Strings.clean(id)); + return (T)this; + } + + @Override + public URI getX509Url() { + Object val = get(X509_URL); + if (val == null) { + return null; + } + if (val instanceof URI) { + return (URI)val; + } + String sval = String.valueOf(val); + URI uri; + try { + uri = new URI(sval); + setValue(X509_URL, uri); //replace with constructed instance + } catch (URISyntaxException e) { + String msg = getType() + " JWK x5u value cannot be converted to a URI instance: " + sval; + throw new MalformedKeyException(msg, e); + } + return uri; + } + + @Override + public T setX509Url(URI url) { + setValue(X509_URL, url); + return (T)this; + } + + @Override + public List getX509CertficateChain() { + return getList(X509_CERT_CHAIN); + } + + @Override + public T setX509CertificateChain(List chain) { + chain = Collections.isEmpty(chain) ? null : new ArrayList<>(new LinkedHashSet<>(chain)); //guarantee no duplicate elements + setValue(X509_CERT_CHAIN, chain); + return (T)this; + } + + @Override + public String getX509CertificateSha1Thumbprint() { + return getString(X509_SHA1_THUMBPRINT); + } + + @Override + public T setX509CertificateSha1Thumbprint(String thumbprint) { + setValue(X509_SHA1_THUMBPRINT, Strings.clean(thumbprint)); + return (T)this; + } + + @Override + public String getX509CertificateSha256Thumbprint() { + return getString(X509_SHA256_THUMBPRINT); + } + + @Override + public T setX509CertificateSha256Thumbprint(String thumbprint) { + setValue(X509_SHA256_THUMBPRINT, Strings.clean(thumbprint)); + return (T)this; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java new file mode 100644 index 000000000..548b2b6b3 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java @@ -0,0 +1,79 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.JwkBuilder; + +import java.net.URI; +import java.util.List; +import java.util.Set; + +@SuppressWarnings("unchecked") +abstract class AbstractJwkBuilder implements JwkBuilder { + + protected final K jwk; + + private final JwkValidator validator; + + AbstractJwkBuilder(JwkValidator validator) { + Assert.notNull(validator, "validator cannot be null."); + this.validator = validator; + this.jwk = newJwk(); + Assert.notNull(this.jwk, "newJwk implementation cannot return a null instance."); + } + + abstract K newJwk(); + + public final K build() { + validator.validate(this.jwk); + return jwk; + } + + @Override + public T setUse(String use) { + this.jwk.setUse(use); + return (T)this; + } + + @Override + public T setOperations(Set ops) { + this.jwk.setOperations(ops); + return (T)this; + } + + @Override + public T setAlgorithm(String alg) { + this.jwk.setAlgorithm(alg); + return (T)this; + } + + @Override + public T setId(String id) { + this.jwk.setId(id); + return (T)this; + } + + @Override + public T setX509Url(URI url) { + this.jwk.setX509Url(url); + return (T)this; + } + + @Override + public T setX509CertificateChain(List chain) { + this.jwk.setX509CertificateChain(chain); + return (T)this; + } + + @Override + public T setX509CertificateSha1Thumbprint(String thumbprint) { + this.jwk.setX509CertificateSha1Thumbprint(thumbprint); + return (T)this; + } + + @Override + public T setX509CertificateSha256Thumbprint(String thumbprint) { + this.jwk.setX509CertificateSha256Thumbprint(thumbprint); + return (T)this; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkConverter.java new file mode 100644 index 000000000..fd8fc71f0 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkConverter.java @@ -0,0 +1,86 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.KeyException; +import io.jsonwebtoken.security.MalformedKeyException; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.util.Map; + +abstract class AbstractJwkConverter implements JwkConverter { + + private static Map assertNotEmpty(Map m) { + if (m == null || m.isEmpty()) { + throw new InvalidKeyException("JWK map cannot be null or empty."); + } + return m; + } + + static void malformed(String msg) { + throw new MalformedKeyException(msg); + } + + static String getRequiredString(Map m, String name) { + assertNotEmpty(m); + Object value = m.get(name); + if (value == null) { + malformed("JWK is missing required case-sensitive '" + name + "' member."); + } + String s = String.valueOf(value); + if (!Strings.hasText(s)) { + malformed("JWK '" + name + "' member cannot be null or empty."); + } + return s; + } + + static BigInteger getRequiredBigInt(Map m, String name) { + String s = getRequiredString(m, name); + try { + byte[] bytes = Decoders.BASE64URL.decode(s); + return new BigInteger(bytes); + } catch (Exception e) { + String msg = "Unable to decode JWK member '" + name + "' to integer from value: " + s; + throw new MalformedKeyException(msg, e); + } + } + + // Copied from Apache Commons Codec 1.14: + // https://github.com/apache/commons-codec/blob/af7b94750e2178b8437d9812b28e36ac87a455f2/src/main/java/org/apache/commons/codec/binary/Base64.java#L746-L775 + static byte[] toUnsignedBytes(BigInteger bigInt) { + int bitlen = bigInt.bitLength(); + // round bitlen + bitlen = ((bitlen + 7) >> 3) << 3; + final byte[] bigBytes = bigInt.toByteArray(); + + if (((bigInt.bitLength() % 8) != 0) && (((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) { + return bigBytes; + } + // set up params for copying everything but sign bit + int startSrc = 0; + int len = bigBytes.length; + + // if bigInt is exactly byte-aligned, just skip signbit in copy + if ((bigInt.bitLength() % 8) == 0) { + startSrc = 1; + len--; + } + final int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec + final byte[] resizedBytes = new byte[bitlen / 8]; + System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len); + return resizedBytes; + } + + KeyFactory getKeyFactory(String alg) { + try { + return KeyFactory.getInstance(alg); + } catch (NoSuchAlgorithmException e) { + String msg = "Unable to obtain JCA KeyFactory instance for algorithm: " + alg; + throw new KeyException(msg, e); + } + } + +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkValidator.java new file mode 100644 index 000000000..e004ddf42 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkValidator.java @@ -0,0 +1,40 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.KeyException; +import io.jsonwebtoken.security.MalformedKeyException; + +abstract class AbstractJwkValidator implements JwkValidator { + + private final String TYPE_VALUE; + + AbstractJwkValidator(String kty) { + kty = Strings.clean(kty); + Assert.notNull(kty); + this.TYPE_VALUE = kty; + } + + static void malformed(String msg) throws MalformedKeyException { + throw new MalformedKeyException(msg); + } + + @Override + public final void validate(T jwk) throws KeyException { + + String type = jwk.getType(); + if (!Strings.hasText(type)) { + malformed("JWKs must have a key type ('kty') property value."); + } + + if (!TYPE_VALUE.equals(type)) { + malformed("JWK does not have expected key type ('kty') value of '" + + TYPE_VALUE + "'. Value found: " + type); + } + + validateJwk(jwk); + } + + abstract void validateJwk(T jwk) throws KeyException; +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwk.java new file mode 100644 index 000000000..960d13b0c --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwk.java @@ -0,0 +1,35 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.RsaJwk; + +@SuppressWarnings("unchecked") +public class AbstractRsaJwk extends AbstractJwk implements RsaJwk { + + static final String TYPE_VALUE = "RSA"; + static final String MODULUS = "n"; + static final String EXPONENT = "e"; + + AbstractRsaJwk() { + super(TYPE_VALUE); + } + + @Override + public String getModulus() { + return getString(MODULUS); + } + + @Override + public T setModulus(String value) { + return setRequiredValue(MODULUS, value, "modulus"); + } + + @Override + public String getExponent() { + return getString(EXPONENT); + } + + @Override + public T setExponent(String value) { + return setRequiredValue(EXPONENT, value, "exponent"); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwkValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwkValidator.java new file mode 100644 index 000000000..f438a1804 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwkValidator.java @@ -0,0 +1,16 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.KeyException; +import io.jsonwebtoken.security.RsaJwk; + +public class AbstractRsaJwkValidator extends AbstractJwkValidator { + + AbstractRsaJwkValidator() { + super(AbstractRsaJwk.TYPE_VALUE); + } + + @Override + void validateJwk(T jwk) throws KeyException { + + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java new file mode 100644 index 000000000..398089dfb --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java @@ -0,0 +1,128 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.RuntimeEnvironment; +import io.jsonwebtoken.security.CryptoRequest; +import io.jsonwebtoken.security.KeyException; +import io.jsonwebtoken.security.SignatureAlgorithm; +import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.VerifySignatureRequest; + +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Signature; +import java.security.spec.AlgorithmParameterSpec; + +abstract class AbstractSignatureAlgorithm extends CryptoAlgorithm implements SignatureAlgorithm { + + AbstractSignatureAlgorithm(String name, String jcaName) { + super(name, jcaName); + } + + //visible for testing + protected boolean isBouncyCastleAvailable() { + return RuntimeEnvironment.BOUNCY_CASTLE_AVAILABLE; + } + + protected Signature createSignatureInstance(Provider provider, AlgorithmParameterSpec spec) { + + Signature sig; + try { + sig = getSignatureInstance(provider); + } catch (NoSuchAlgorithmException e) { + + String msg = "JWT signature algorithm '" + getName() + "' uses the JCA algorithm '" + getJcaName() + + "', which is not "; + + if (provider != null) { + msg += "supported by the specified JCA Provider {" + provider + "}. Try "; + } else { + msg += "available in the current JVM. Try "; + } + + if (!isBouncyCastleAvailable()) { + msg += "including BouncyCastle in the runtime classpath, or "; + } + + msg += "explicitly supplying a JCA Provider that supports the JCA algorithm name '" + getJcaName() + + "'. Cause: " + e.getMessage(); + + throw new SignatureException(msg, e); + } + + if (spec != null) { + try { + setParameter(sig, spec); + } catch (InvalidAlgorithmParameterException e) { + String msg = "Unsupported " + getJcaName() + " parameter {" + spec + "}: " + e.getMessage(); + throw new SignatureException(msg, e); + } + } + + return sig; + } + + //for testing overrides + protected Signature getSignatureInstance(Provider provider) throws NoSuchAlgorithmException { + final String jcaName = getJcaName(); + return provider != null ? + Signature.getInstance(jcaName, provider) : + Signature.getInstance(jcaName); + } + + //for testing overrides + protected void setParameter(Signature sig, AlgorithmParameterSpec spec) throws InvalidAlgorithmParameterException { + sig.setParameter(spec); + } + + protected static String keyType(boolean signing) { + return signing ? "signing" : "verification"; + } + + protected abstract void validateKey(Key key, boolean signing); + + @Override + public byte[] sign(CryptoRequest request) throws SignatureException, KeyException { + final Key key = Assert.notNull(request.getKey(), "Signature request key cannot be null."); + Assert.notEmpty(request.getData(), "Signature request data byte array cannot be null or empty."); + try { + validateKey(key, true); + return doSign(request); + } catch (SignatureException | KeyException e) { + throw e; //propagate + } catch (Exception e) { + String msg = "Unable to compute " + getName() + " signature with JCA algorithm '" + getJcaName() + "' " + + "using key {" + key + "}: " + e.getMessage(); + throw new SignatureException(msg, e); + } + } + + protected abstract byte[] doSign(CryptoRequest request) throws Exception; + + @Override + public boolean verify(VerifySignatureRequest request) throws SignatureException, KeyException { + final Key key = Assert.notNull(request.getKey(), "Signature verification key cannot be null."); + Assert.notEmpty(request.getData(), "Signature verification data byte array cannot be null or empty."); + Assert.notEmpty(request.getSignature(), "Signature byte array cannot be null or empty."); + try { + validateKey(key, false); + return doVerify(request); + } catch (SignatureException | KeyException e) { + throw e; //propagate + } catch (Exception e) { + String msg = "Unable to verify " + getName() + " signature with JCA algorithm '" + getJcaName() + "' " + + "using key {" + key + "}: " + e.getMessage(); + throw new SignatureException(msg, e); + } + } + + protected boolean doVerify(VerifySignatureRequest request) throws Exception { + byte[] providedSignature = request.getSignature(); + Assert.notEmpty(providedSignature, "Request signature byte array cannot be null or empty."); + byte[] computedSignature = sign(request); + return MessageDigest.isEqual(providedSignature, computedSignature); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractTypedJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractTypedJwkConverter.java new file mode 100644 index 000000000..b52258c50 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractTypedJwkConverter.java @@ -0,0 +1,33 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; + +import java.security.KeyFactory; +import java.util.HashMap; +import java.util.Map; + +abstract class AbstractTypedJwkConverter extends AbstractJwkConverter implements TypedJwkConverter { + + private final String keyType; + + AbstractTypedJwkConverter(String keyType) { + Assert.hasText(keyType, "keyType argument cannot be null or empty."); + this.keyType = keyType; + } + + @Override + public String getKeyType() { + return this.keyType; + } + + KeyFactory getKeyFactory() { + return getKeyFactory(getKeyType()); + } + + Map newJwkMap() { + Map m = new HashMap<>(); + m.put("kty", getKeyType()); + return m; + } + +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CipherAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CipherAlgorithm.java new file mode 100644 index 000000000..0276097ae --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/CipherAlgorithm.java @@ -0,0 +1,19 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.CryptoRequest; + +abstract class CipherAlgorithm extends CryptoAlgorithm { + + private final String transformation; + + CipherAlgorithm(String name, String transformation) { + super(name, transformation); + Assert.hasText(transformation, "Transformation string cannot be null or empty."); + this.transformation = transformation; + } + + CipherTemplate newCipherTemplate(CryptoRequest request) { + return new CipherTemplate(this.transformation, request != null ? request.getProvider() : null); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CipherCallback.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CipherCallback.java new file mode 100644 index 000000000..575d1318f --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/CipherCallback.java @@ -0,0 +1,8 @@ +package io.jsonwebtoken.impl.security; + +import javax.crypto.Cipher; + +public interface CipherCallback { + + T doWithCipher(Cipher cipher) throws Exception; +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CipherTemplate.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CipherTemplate.java new file mode 100644 index 000000000..f1757fe87 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/CipherTemplate.java @@ -0,0 +1,54 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.CryptoException; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; + +class CipherTemplate { + + private final Provider provider; + + private final String transformation; + + CipherTemplate(String transformation, Provider provider) { + Assert.hasText(transformation, "Transformation string cannot be null or empty."); + this.transformation = transformation; + this.provider = provider; + } + + //for testing visibility + Cipher getCipherInstance(String transformation, Provider provider) + throws NoSuchPaddingException, NoSuchAlgorithmException { + return provider != null ? + Cipher.getInstance(transformation, provider) : + Cipher.getInstance(transformation); + } + + private Cipher newCipher() throws CryptoException { + try { + return getCipherInstance(transformation, provider); + } catch (Exception e) { + String msg = "Unable to obtain cipher from "; + if (provider != null) { + msg += "specified Provider {" + provider + "} "; + } else { + msg += "default JCA Provider "; + } + msg += "for transformation '" + transformation + "': " + e.getMessage(); + throw new CryptoException(msg, e); + } + } + + T execute(CipherCallback callback) throws CryptoException { + Cipher cipher = newCipher(); + try { + return callback.doWithCipher(cipher); + } catch (Exception e) { + throw new CryptoException("Cipher callback execution failed: " + e.getMessage(), e); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java new file mode 100644 index 000000000..8afc6776b --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java @@ -0,0 +1,35 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.Named; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.CryptoRequest; + +import java.security.SecureRandom; + +abstract class CryptoAlgorithm implements Named { + + private final String name; + + private final String jcaName; + + CryptoAlgorithm(String name, String jcaName) { + Assert.hasText(name, "name cannot be null or empty."); + this.name = name; + Assert.hasText(jcaName, "jcaName cannot be null or empty."); + this.jcaName = jcaName; + } + + @Override + public String getName() { + return this.name; + } + + String getJcaName() { + return this.jcaName; + } + + SecureRandom ensureSecureRandom(CryptoRequest request) { + SecureRandom random = request.getSecureRandom(); + return random != null ? random : Randoms.secureRandom(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResult.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResult.java new file mode 100644 index 000000000..434bee5c6 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResult.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadIvEncryptionResult; + +/** + * @since JJWT_RELEASE_VERSION + */ +class DefaultAeadIvEncryptionResult extends DefaultIvEncryptionResult implements AeadIvEncryptionResult { + + private final byte[] tag; + + DefaultAeadIvEncryptionResult(byte[] ciphertext, byte[] iv, byte[] tag) { + super(ciphertext, iv); + this.tag = Assert.notEmpty(tag, "authentication tag cannot be null or empty."); + } + + @Override + public byte[] getAuthenticationTag() { + return this.tag; + } + + @Override + public byte[] compact() { + byte[] output = new byte[iv.length + ciphertext.length + tag.length]; + System.arraycopy(iv, 0, output, 0, iv.length); // iv first + System.arraycopy(ciphertext, 0, output, iv.length, ciphertext.length); // then ciphertext + System.arraycopy(tag, 0, output, iv.length + ciphertext.length, tag.length); // finally tag + return output; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvRequest.java new file mode 100644 index 000000000..0ecc0d885 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvRequest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadIvRequest; + +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultAeadIvRequest extends DefaultIvDecryptionRequest + implements AeadIvRequest { + + private final byte[] aad; + + private final byte[] tag; + + public DefaultAeadIvRequest(T data, K key, Provider provider, SecureRandom secureRandom, byte[] iv, byte[] aad, byte[] tag) { + super(data, key, provider, secureRandom, iv); + this.aad = aad; + this.tag = Assert.notEmpty(tag, "Authentication tag cannot be null or empty."); + } + + @Override + public byte[] getAssociatedData() { + return this.aad; + } + + @Override + public byte[] getAuthenticationTag() { + return this.tag; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAesEncryptionRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAesEncryptionRequest.java new file mode 100644 index 000000000..f6927653a --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAesEncryptionRequest.java @@ -0,0 +1,26 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.AeadRequest; + +import javax.crypto.SecretKey; +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultAesEncryptionRequest extends DefaultCryptoRequest implements AeadRequest { + + private final byte[] aad; + + public DefaultAesEncryptionRequest(T data, SecretKey key, Provider provider, SecureRandom secureRandom, byte[] aad) { + super(data, key, provider, secureRandom); + this.aad = aad; + } + + public DefaultAesEncryptionRequest(T data, SecretKey key, byte[] aad) { + this(data, key, null, null, aad); + } + + @Override + public byte[] getAssociatedData() { + return this.aad; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoMessage.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoMessage.java new file mode 100644 index 000000000..0adfa0383 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoMessage.java @@ -0,0 +1,24 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.CryptoMessage; + +import java.security.Key; +import java.security.Provider; + +class DefaultCryptoMessage implements CryptoMessage { + + private final T data; + + DefaultCryptoMessage(T data) { + this.data = Assert.notNull(data, "data cannot be null."); + if (data instanceof byte[] && ((byte[]) data).length == 0) { + throw new IllegalArgumentException("data byte array cannot be empty."); + } + } + + @Override + public T getData() { + return data; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java new file mode 100644 index 000000000..de0cbf763 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java @@ -0,0 +1,37 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.CryptoRequest; + +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultCryptoRequest extends DefaultCryptoMessage implements CryptoRequest { + + private final Provider provider; + private final SecureRandom secureRandom; + private final K key; + + public DefaultCryptoRequest(T data, K key, Provider provider, SecureRandom secureRandom) { + super(data); + this.key = Assert.notNull(key, "key cannot be null."); + this.provider = provider; + this.secureRandom = secureRandom; + } + + @Override + public K getKey() { + return this.key; + } + + @Override + public Provider getProvider() { + return this.provider; + } + + @Override + public SecureRandom getSecureRandom() { + return this.secureRandom; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcJwkBuilderFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcJwkBuilderFactory.java new file mode 100644 index 000000000..ca3c544e4 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcJwkBuilderFactory.java @@ -0,0 +1,18 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.EcJwkBuilderFactory; +import io.jsonwebtoken.security.PrivateEcJwkBuilder; +import io.jsonwebtoken.security.PublicEcJwkBuilder; + +final class DefaultEcJwkBuilderFactory implements EcJwkBuilderFactory { + + @Override + public PublicEcJwkBuilder publicKey() { + return new DefaultPublicEcJwkBuilder(); + } + + @Override + public PrivateEcJwkBuilder privateKey() { + return new DefaultPrivateEcJwkBuilder(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocator.java new file mode 100644 index 000000000..9c8668c25 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocator.java @@ -0,0 +1,40 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.EncryptionAlgorithm; +import io.jsonwebtoken.security.EncryptionAlgorithmLocator; +import io.jsonwebtoken.security.EncryptionAlgorithms; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultEncryptionAlgorithmLocator implements EncryptionAlgorithmLocator { + + @Override + public EncryptionAlgorithm getEncryptionAlgorithm(JweHeader jweHeader) { + + String enc = Strings.clean(jweHeader.getEncryptionAlgorithm()); + //TODO: this check needs to be in the parser, to be enforced regardless of the locator implementation + if (enc == null) { + String msg = "JWE header does not contain an 'enc' header parameter. This header parameter is mandatory " + + "per the JWE Specification, Section 4.1.2. See " + + "https://tools.ietf.org/html/rfc7516#section-4.1.2 for more information."; + throw new MalformedJwtException(msg); + } + + try { + return EncryptionAlgorithms.forName(enc); //TODO: change to findByName and let the parser throw on null return. See below: + } catch (IllegalArgumentException e) { + //TODO: move this check to the parser - needs to be enforced if the locator returns null or throws a non-JWT exception + //couldn't find one: + String msg = "JWE 'enc' header parameter value of '" + enc + "' does not match a JWE standard algorithm " + + "identifier. If '" + enc + "' represents a custom algorithm, the JwtParser must be configured with " + + "a custom EncryptionAlgorithmLocator instance that knows how to return a compatible " + + "EncryptionAlgorithm instance. Otherwise, this JWE is invalid and may not be used safely."; + throw new UnsupportedJwtException(msg, e); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionRequest.java new file mode 100644 index 000000000..c897fb259 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionRequest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.AeadRequest; +import io.jsonwebtoken.security.AssociatedDataSource; +import io.jsonwebtoken.security.InitializationVectorSource; + +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultEncryptionRequest extends DefaultCryptoRequest implements AeadRequest, InitializationVectorSource { + + private final byte[] iv; + + private final byte[] aad; + + public DefaultEncryptionRequest(T data, K key, Provider provider, SecureRandom secureRandom, byte[] iv, byte[] aad) { + super(data, key, provider, secureRandom); + this.iv = iv; + this.aad = aad; + } + + @Override + public byte[] getAssociatedData() { + return this.aad; + } + + @Override + public byte[] getInitializationVector() { + return this.iv; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionResult.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionResult.java new file mode 100644 index 000000000..8910b85a1 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionResult.java @@ -0,0 +1,18 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.EncryptionResult; + +class DefaultEncryptionResult implements EncryptionResult { + + protected final byte[] ciphertext; + + DefaultEncryptionResult(byte[] ciphertext) { + this.ciphertext = Assert.notEmpty(ciphertext, "ciphertext cannot be null or empty."); + } + + @Override + public byte[] getCiphertext() { + return this.ciphertext; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvDecryptionRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvDecryptionRequest.java new file mode 100644 index 000000000..d230c7c49 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvDecryptionRequest.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2016 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.InitializationVectorSource; + +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultIvDecryptionRequest extends DefaultCryptoRequest implements InitializationVectorSource { + + private final byte[] iv; + + public DefaultIvDecryptionRequest(T data, K key, Provider provider, SecureRandom secureRandom, byte[] iv) { + super(data, key, provider, secureRandom); + this.iv = Assert.notEmpty(iv, "Initialization Vector cannot be null or empty."); + } + + @Override + public byte[] getInitializationVector() { + return this.iv; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvEncryptionResult.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvEncryptionResult.java new file mode 100644 index 000000000..b8b190305 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvEncryptionResult.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.IvEncryptionResult; + +/** + * @since JJWT_RELEASE_VERSION + */ +class DefaultIvEncryptionResult extends DefaultEncryptionResult implements IvEncryptionResult { + + protected final byte[] iv; + + DefaultIvEncryptionResult(byte[] ciphertext, byte[] iv) { + super(ciphertext); + this.iv = Assert.notEmpty(iv, "initialization vector cannot be null or empty."); + } + + @Override + public byte[] getInitializationVector() { + return this.iv; + } + + @Override + public byte[] compact() { + byte[] output = new byte[iv.length + ciphertext.length]; + System.arraycopy(iv, 0, output, 0, iv.length); // iv first + System.arraycopy(ciphertext, 0, output, iv.length, ciphertext.length); // then ciphertext + return output; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJweFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJweFactory.java new file mode 100644 index 000000000..052ad7719 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJweFactory.java @@ -0,0 +1,123 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.Services; +import io.jsonwebtoken.io.Decoder; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.EncryptionAlgorithm; +import io.jsonwebtoken.security.EncryptionAlgorithms; + +import java.util.Map; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultJweFactory { + + private final Decoder base64UrlDecoder; + + private final Deserializer> deserializer; + + private final EncryptionAlgorithm encryptionAlgorithm; + + private static Deserializer> loadDeserializer() { + Deserializer deserializer = Services.loadFirst(Deserializer.class); + //noinspection unchecked + return (Deserializer>) deserializer; + } + + public DefaultJweFactory() { + this(Decoders.BASE64URL, loadDeserializer(), EncryptionAlgorithms.A256GCM); + } + + public DefaultJweFactory(Decoder base64UrlDecoder, + Deserializer> deserializer, + EncryptionAlgorithm encryptionAlgorithm) { + this.base64UrlDecoder = Assert.notNull(base64UrlDecoder, "Base64Url TextCodec cannot be null."); + this.deserializer = Assert.notNull(deserializer, "Deserializer cannot be null."); + this.encryptionAlgorithm = Assert.notNull(encryptionAlgorithm, "EncryptionAlgorithm cannot be null."); + } + + /* + + public Jwe createJwe(String base64UrlProtectedHeader, String base64UrlEncryptedKey, String base64UrlIv, + String base64UrlCiphertext, String base64UrlAuthenticationTag) { + + // ==================================================================== + // https://tools.ietf.org/html/rfc7516#section-5.2 #2 + // ==================================================================== + + final byte[] headerBytes = base64UrlDecode(base64UrlProtectedHeader, "Protected Header"); + + // encrypted key can be null with Direct Key or Direct Key Agreement + // https://tools.ietf.org/html/rfc7516#section-5.2 + // so we use a 'null safe' variant: + final byte[] encryptedKeyBytes = nullSafeBase64UrlDecode(base64UrlEncryptedKey, "Encrypted Key"); + + final byte[] iv = base64UrlDecode(base64UrlIv, "Initialization Vector"); + + final byte[] ciphertext = base64UrlDecode(base64UrlCiphertext, "Ciphertext"); + + final byte[] authcTag = base64UrlDecode(base64UrlAuthenticationTag, "Authentication Tag"); + + // ==================================================================== + // https://tools.ietf.org/html/rfc7516#section-5.2 #3 + // ==================================================================== + + Map protectedHeader; + try { + protectedHeader = parseJson(headerBytes); + } catch (Exception e) { + String msg = "JWE Protected Header must be a valid JSON object."; + throw new IllegalArgumentException(msg, e); + } + Assert.notEmpty(protectedHeader, "JWE Protected Header cannot be a null or empty JSON object."); + + DefaultJweHeader header = new DefaultJweHeader(protectedHeader); + + // ==================================================================== + // https://tools.ietf.org/html/rfc7516#section-5.2 #4 + // ==================================================================== + + // we currently don't support JSON serialization (just compact), so we can skip #4 + + // ==================================================================== + // https://tools.ietf.org/html/rfc7516#section-5.2 #11 and #12 + // ==================================================================== + + + throw new UnsupportedOperationException("Not yet finished."); + + } + + protected byte[] nullSafeBase64UrlDecode(String base64UrlEncoded, String jweName) { + if (base64UrlEncoded == null) { + return null; + } + return base64UrlDecode(base64UrlEncoded, jweName); + } + + protected byte[] base64UrlDecode(String base64UrlEncoded, String jweName) { + + if (base64UrlEncoded == null) { + String msg = "Invalid compact JWE: base64url JWE " + jweName + " is missing."; + throw new IllegalArgumentException(msg); + } + + try { + return base64UrlDecoder.decode(base64UrlEncoded); + } catch (Exception e) { + String msg = "Invalid compact JWE: JWE " + jweName + + " fragment is invalid and cannot be Base64Url-decoded: " + base64UrlEncoded; + throw new IllegalArgumentException(msg, e); + } + } + + @SuppressWarnings("unchecked") + protected Map parseJson(byte[] json) { + return deserializer.deserialize(json); + } + + */ +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkBuilderFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkBuilderFactory.java new file mode 100644 index 000000000..2a55f6062 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkBuilderFactory.java @@ -0,0 +1,18 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.EcJwkBuilderFactory; +import io.jsonwebtoken.security.JwkBuilderFactory; +import io.jsonwebtoken.security.SymmetricJwkBuilder; + +public final class DefaultJwkBuilderFactory implements JwkBuilderFactory { + + @Override + public EcJwkBuilderFactory ellipticCurve() { + return new DefaultEcJwkBuilderFactory(); + } + + @Override + public SymmetricJwkBuilder symmetric() { + return new DefaultSymmetricJwkBuilder(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkConverter.java new file mode 100644 index 000000000..52e963ec6 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkConverter.java @@ -0,0 +1,58 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import java.security.Key; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DefaultJwkConverter extends AbstractJwkConverter { + + private final Map converters = new HashMap<>(); + + public DefaultJwkConverter() { + this(Collections.of( + new SymmetricJwkConverter(), + new EcJwkConverter(), + new RsaJwkConverter())); + } + + public DefaultJwkConverter(List converters) { + Assert.notEmpty(converters, "Converters cannot be null or empty."); + for(TypedJwkConverter converter : converters) { + this.converters.put(converter.getKeyType(), converter); + } + } + + private JwkConverter getConverter(String kty) { + JwkConverter converter = converters.get(kty); + if (converter == null) { + String msg = "Unrecognized JWK kty (key type) value: " + kty; + throw new UnsupportedKeyException(msg); + } + return converter; + } + + @Override + public Key toKey(Map jwk) { + String type = getRequiredString(jwk, "kty"); + JwkConverter converter = getConverter(type); + return converter.toKey(jwk); + } + + @Override + public Map toJwk(Key key) { + Assert.notNull(key, "Key argument cannot be null."); + for(TypedJwkConverter converter : converters.values()) { + if (converter.supports(key)) { + return converter.toJwk(key); + } + } + + String msg = "Unable to determine JWK converter for key of type " + key.getClass(); + throw new UnsupportedKeyException(msg); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwk.java new file mode 100644 index 000000000..66650e8f4 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwk.java @@ -0,0 +1,18 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.PrivateEcJwk; + +class DefaultPrivateEcJwk extends AbstractEcJwk implements PrivateEcJwk { + + static final String D = "d"; + + @Override + public String getD() { + return getString(D); + } + + @Override + public PrivateEcJwk setD(String d) { + return setRequiredValue(D, d, "private key"); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwkBuilder.java new file mode 100644 index 000000000..33d3ad98f --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwkBuilder.java @@ -0,0 +1,24 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.PrivateEcJwk; +import io.jsonwebtoken.security.PrivateEcJwkBuilder; + +class DefaultPrivateEcJwkBuilder extends AbstractEcJwkBuilder implements PrivateEcJwkBuilder { + + private static final JwkValidator VALIDATOR = new PrivateEcJwkValidator(); + + DefaultPrivateEcJwkBuilder() { + super(VALIDATOR); + } + + @Override + PrivateEcJwk newJwk() { + return new DefaultPrivateEcJwk(); + } + + @Override + public PrivateEcJwkBuilder setD(String d) { + this.jwk.setD(d); + return this; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateRsaJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateRsaJwk.java new file mode 100644 index 000000000..f945f7c07 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateRsaJwk.java @@ -0,0 +1,87 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.JwkRsaPrimeInfo; +import io.jsonwebtoken.security.PrivateRsaJwk; + +import java.util.List; + +public class DefaultPrivateRsaJwk extends AbstractRsaJwk implements PrivateRsaJwk { + + static String PRIVATE_EXPONENT = "d"; + static String FIRST_PRIME = "p"; + static String SECOND_PRIME = "q"; + static String FIRST_CRT_EXPONENT = "dp"; + static String SECOND_CRT_EXPONENT = "dq"; + static String FIRST_CRT_COEFFICIENT = "qi"; + static String OTHER_PRIMES_INFO = "oth"; + + @Override + public String getD() { + return getString(PRIVATE_EXPONENT); + } + + @Override + public PrivateRsaJwk setD(String d) { + return setRequiredValue(PRIVATE_EXPONENT, d, "private exponent"); + } + + @Override + public String getP() { + return getString(FIRST_PRIME); + } + + @Override + public PrivateRsaJwk setP(String p) { + return setRequiredValue(FIRST_PRIME, p, "first prime factor"); + } + + @Override + public String getQ() { + return getString(SECOND_PRIME); + } + + @Override + public PrivateRsaJwk setQ(String q) { + return setRequiredValue(FIRST_PRIME, q, "second prime factor"); + } + + @Override + public String getDP() { + return getString(FIRST_CRT_EXPONENT); + } + + @Override + public PrivateRsaJwk setDP(String dp) { + return setRequiredValue(FIRST_CRT_EXPONENT, dp, "first crt exponent"); + } + + @Override + public String getDQ() { + return getString(SECOND_CRT_EXPONENT); + } + + @Override + public PrivateRsaJwk setDQ(String dq) { + return setRequiredValue(SECOND_CRT_EXPONENT, dq, "second crt exponent"); + } + + @Override + public String getQI() { + return getString(FIRST_CRT_COEFFICIENT); + } + + @Override + public PrivateRsaJwk setQI(String qi) { + return setRequiredValue(FIRST_CRT_COEFFICIENT, qi, "first crt coefficient"); + } + + @Override + public List getOtherPrimesInfo() { + throw new UnsupportedOperationException("Not yet implemented."); + } + + @Override + public PrivateRsaJwk setOtherPrimesInfo(List infos) { + throw new UnsupportedOperationException("Not yet implemented."); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwk.java new file mode 100644 index 000000000..bdc6c1854 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwk.java @@ -0,0 +1,6 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.PublicEcJwk; + +class DefaultPublicEcJwk extends AbstractEcJwk implements PublicEcJwk { +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwkBuilder.java new file mode 100644 index 000000000..99ec5bdbc --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwkBuilder.java @@ -0,0 +1,23 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.PublicEcJwk; +import io.jsonwebtoken.security.PublicEcJwkBuilder; + +class DefaultPublicEcJwkBuilder extends AbstractEcJwkBuilder implements PublicEcJwkBuilder { + + private static final JwkValidator VALIDATOR = new AbstractEcJwkValidator() { + @Override + protected void validateEcJwk(PublicEcJwk jwk) { + //nothing additional to do + } + }; + + DefaultPublicEcJwkBuilder() { + super(VALIDATOR); + } + + @Override + PublicEcJwk newJwk() { + return new DefaultPublicEcJwk(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwk.java new file mode 100644 index 000000000..cd4d6d598 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwk.java @@ -0,0 +1,28 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.SymmetricJwk; + +final class DefaultSymmetricJwk extends AbstractJwk implements SymmetricJwk { + + static final String TYPE_VALUE = "oct"; + static final String K = "k"; + + DefaultSymmetricJwk() { + super(TYPE_VALUE); + } + + @Override + public String getK() { + return getString(K); + } + + @Override + public SymmetricJwk setK(String k) { + k = Strings.clean(k); + Assert.notNull(k, "SymmetricJwk 'k' property cannot be null or empty."); + setValue(K, k); + return this; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwkBuilder.java new file mode 100644 index 000000000..eed168c92 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwkBuilder.java @@ -0,0 +1,24 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.SymmetricJwk; +import io.jsonwebtoken.security.SymmetricJwkBuilder; + +final class DefaultSymmetricJwkBuilder extends AbstractJwkBuilder implements SymmetricJwkBuilder { + + private static final JwkValidator VALIDATOR = new SymmetricJwkValidator(); + + DefaultSymmetricJwkBuilder() { + super(VALIDATOR); + } + + @Override + public SymmetricJwkBuilder setK(String k) { + this.jwk.setK(k); + return this; + } + + @Override + SymmetricJwk newJwk() { + return new DefaultSymmetricJwk(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySignatureRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySignatureRequest.java new file mode 100644 index 000000000..5ede1eef9 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySignatureRequest.java @@ -0,0 +1,23 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.VerifySignatureRequest; + +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultVerifySignatureRequest extends DefaultCryptoRequest implements VerifySignatureRequest { + + private final byte[] signature; + + public DefaultVerifySignatureRequest(byte[] data, Key key, Provider provider, SecureRandom secureRandom, byte[] signature) { + super(data, key, provider, secureRandom); + this.signature = Assert.notEmpty(signature, "Signature byte array cannot be null or empty."); + } + + @Override + public byte[] getSignature() { + return this.signature; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DirectEncryptionMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DirectEncryptionMode.java new file mode 100644 index 000000000..70c5c75f1 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DirectEncryptionMode.java @@ -0,0 +1,22 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DirectEncryptionMode implements KeyManagementMode { + + private final SecretKey key; + + DirectEncryptionMode(SecretKey key) { + this.key = Assert.notNull(key, "SecretKey argument cannot be null."); + } + + @Override + public SecretKey getKey(GetKeyRequest ignored) { + return this.key; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAgreementMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAgreementMode.java new file mode 100644 index 000000000..e6e40363d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAgreementMode.java @@ -0,0 +1,14 @@ +package io.jsonwebtoken.impl.security; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DirectKeyAgreementMode implements KeyManagementMode { + + @Override + public SecretKey getKey(GetKeyRequest request) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolver.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolver.java new file mode 100644 index 000000000..e7f4962a3 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolver.java @@ -0,0 +1,22 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.security.DecryptionKeyResolver; + +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DisabledDecryptionKeyResolver implements DecryptionKeyResolver { + + /** + * Singleton instance that may be used if direct instantiation is not desired. + */ + public static final DisabledDecryptionKeyResolver INSTANCE = new DisabledDecryptionKeyResolver(); + + @Override + public Key resolveDecryptionKey(JweHeader header) { + return null; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcJwkConverter.java new file mode 100644 index 000000000..529da692b --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcJwkConverter.java @@ -0,0 +1,177 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.Jwks; +import io.jsonwebtoken.security.KeyException; +import io.jsonwebtoken.security.PublicEcJwkBuilder; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.Key; +import java.security.KeyFactory; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.util.HashMap; +import java.util.Map; + +public class EcJwkConverter extends AbstractTypedJwkConverter { + + private static final Map EC_CURVE_NAMES_BY_JWA_ID = createEcCurveNameMap(); + + private static Map createEcCurveNameMap() { + Map m = new HashMap<>(); + m.put("P-256", "secp256r1"); + m.put("P-384", "secp384r1"); + m.put("P-521", "secp521r1"); + return m; + } + + private static ECParameterSpec getStandardNameSpec(String stdName) throws KeyException { + try { + AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC"); + parameters.init(new ECGenParameterSpec(stdName)); + return parameters.getParameterSpec(ECParameterSpec.class); + } catch (Exception e) { + String msg = "Unable to obtain JVM ECParameterSpec for JWA curve ID '" + stdName + "'."; + throw new KeyException(msg, e); + } + } + + private static ECParameterSpec getCurveIdSpec(String curveId) { + String stdName = EC_CURVE_NAMES_BY_JWA_ID.get(curveId); + if (stdName == null) { + String msg = "Unrecognized JWA curve id '" + curveId + "'"; + throw new UnsupportedKeyException(msg); + } + return getStandardNameSpec(stdName); + } + + /** + * https://tools.ietf.org/html/rfc7518#section-6.2.1.2 indicates that this algorithm logic is defined in + * http://www.secg.org/sec1-v2.pdf Section 2.3.5. + * @param fieldSize EC field size + * @param coordinate EC point coordinate (e.g. x or y) + * @return A base64Url-encoded String representing the EC field per the RFC format + */ + // Algorithm defined in http://www.secg.org/sec1-v2.pdf Section 2.3.5 + static String encodeCoordinate(int fieldSize, BigInteger coordinate) { + byte[] bytes = toUnsignedBytes(coordinate); + int mlen = (int)Math.ceil(fieldSize / 8d); + if (mlen > bytes.length) { + byte[] m = new byte[mlen]; + System.arraycopy(bytes, 0, m, mlen - bytes.length, bytes.length); + bytes = m; + } + return Encoders.BASE64URL.encode(bytes); + } + + EcJwkConverter() { + super("EC"); + } + + @Override + public boolean supports(Key key) { + return key instanceof ECPrivateKey || key instanceof ECPublicKey; + } + + @Override + public Key toKey(Map jwk) { + Assert.notNull(jwk, "JWK map argument cannot be null."); + if (jwk.containsKey("d")) { + return toPrivateKey(jwk); + } + return toPublicKey(jwk); + } + + @Override + public Map toJwk(Key key) { + if (key instanceof ECPrivateKey) { + return toPrivateJwk((ECPrivateKey)key); + } + Assert.isInstanceOf(ECPublicKey.class, key, "Key argument must be an ECPublicKey or ECPrivateKey instance."); + return toPublicJwk((ECPublicKey)key); + } + + private ECPublicKey toPublicKey(Map jwk) { + String curveId = getRequiredString(jwk, "crv"); + BigInteger x = getRequiredBigInt(jwk, "x"); + BigInteger y = getRequiredBigInt(jwk, "y"); + + ECParameterSpec spec = getCurveIdSpec(curveId); + ECPoint point = new ECPoint(x, y); + ECPublicKeySpec pubSpec = new ECPublicKeySpec(point, spec); + + try { + KeyFactory kf = getKeyFactory(); + return (ECPublicKey)kf.generatePublic(pubSpec); + } catch (Exception e) { + String msg = "Unable to obtain ECPublicKey for curve '" + curveId + "'."; + throw new KeyException(msg, e); + } + } + + public ECPrivateKey toPrivateKey(Map jwk) { + String curveId = getRequiredString(jwk, "crv"); + BigInteger d = getRequiredBigInt(jwk, "d"); + + // We don't actually need these two values for JVM lookup, but the + // [JWA spec](https://tools.ietf.org/html/rfc7518#section-6.2.2) + // requires them to be present and valid for the private key as well, so we assert that here: + getRequiredBigInt(jwk, "x"); + getRequiredBigInt(jwk, "y"); + + ECParameterSpec spec = getCurveIdSpec(curveId); + ECPrivateKeySpec privateSpec = new ECPrivateKeySpec(d, spec); + + try { + KeyFactory kf = getKeyFactory(); + return (ECPrivateKey)kf.generatePrivate(privateSpec); + } catch (Exception e) { + String msg = "Unable to obtain ECPrivateKey from specified jwk for curve '" + curveId + "'."; + throw new KeyException(msg, e); + } + } + + public Map toPublicJwk(ECPublicKey key) { + + PublicEcJwkBuilder builder = Jwks.builder().ellipticCurve().publicKey(); + + Map m = newJwkMap(); + + System.out.println(key.getAlgorithm()); + + ECParameterSpec spec = key.getParams(); + + //TODO: need a ECPublicKey-to-CurveId function + + SignatureAlgorithm alg = SignatureAlgorithm.forSigningKey(key); + + int bitLength = spec.getOrder().bitLength(); + + int fieldSize = spec.getCurve().getField().getFieldSize(); + + String x = encodeCoordinate(fieldSize, spec.getGenerator().getAffineX()); + String y = encodeCoordinate(fieldSize, spec.getGenerator().getAffineY()); + + + builder.setX(x).setY(y); + + //return (Map)builder.build(); + + throw new UnsupportedOperationException("Not yet implemented."); + } + + + + public Map toPrivateJwk(ECPrivateKey key) { + throw new UnsupportedOperationException("Not yet implemented."); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EllipticCurveSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EllipticCurveSignatureAlgorithm.java new file mode 100644 index 000000000..c21604595 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EllipticCurveSignatureAlgorithm.java @@ -0,0 +1,253 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AsymmetricKeySignatureAlgorithm; +import io.jsonwebtoken.security.CryptoRequest; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.VerifySignatureRequest; +import io.jsonwebtoken.security.WeakKeyException; + +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.interfaces.ECKey; +import java.security.spec.ECGenParameterSpec; + +@SuppressWarnings("unused") //used via reflection in the io.jsonwebtoken.security.SignatureAlgorithms class +public class EllipticCurveSignatureAlgorithm extends AbstractSignatureAlgorithm implements AsymmetricKeySignatureAlgorithm { + + private static final String EC_PUBLIC_KEY_REQD_MSG = + "Elliptic Curve signature validation requires an ECPublicKey instance."; + + private static final int MIN_KEY_LENGTH_BITS = 256; + + private final String curveName; + + private final int minKeyLength; //in bits + + private final int signatureLength; + + public EllipticCurveSignatureAlgorithm(String name, String jcaName, String curveName, int minKeyLength, int signatureLength) { + super(name, jcaName); + Assert.hasText(curveName, "Curve name cannot be null or empty."); + this.curveName = curveName; + if (minKeyLength < MIN_KEY_LENGTH_BITS) { + String msg = "minKeyLength bits must be greater than the JWA mandatory minimum key length of " + MIN_KEY_LENGTH_BITS; + throw new IllegalArgumentException(msg); + } + this.minKeyLength = minKeyLength; + Assert.isTrue(signatureLength > 0, "signatureLength must be greater than zero."); + this.signatureLength = signatureLength; + } + + @Override + public KeyPair generateKeyPair() { + KeyPairGenerator keyGenerator; + try { + keyGenerator = KeyPairGenerator.getInstance("EC"); + ECGenParameterSpec spec = new ECGenParameterSpec(this.curveName); + keyGenerator.initialize(spec, Randoms.secureRandom()); + } catch (Exception e) { + throw new IllegalStateException("Unable to obtain an EllipticCurve KeyPairGenerator: " + e.getMessage(), e); + } + return keyGenerator.genKeyPair(); + } + + @Override + protected void validateKey(Key key, boolean signing) { + + if (!(key instanceof ECKey)) { + String msg = "EC " + keyType(signing) + " keys must be an ECKey. The specified key is of type: " + + key.getClass().getName(); + throw new InvalidKeyException(msg); + } + + if (signing) { + // https://github.com/jwtk/jjwt/issues/68 + // Instead of checking for an instance of ECPrivateKey, check for PrivateKey (and ECKey assertion is above): + if (!(key instanceof PrivateKey)) { + String msg = "Asymmetric key signatures must be created with PrivateKeys. The specified key is of type: " + + key.getClass().getName(); + throw new InvalidKeyException(msg); + } + } else { //verification + if (!(key instanceof PublicKey)) { + throw new InvalidKeyException(EC_PUBLIC_KEY_REQD_MSG); + } + } + + final String name = getName(); + ECKey ecKey = (ECKey) key; + int size = ecKey.getParams().getOrder().bitLength(); + if (size < this.minKeyLength) { + String msg = "The " + keyType(signing) + " key's size (ECParameterSpec order) is " + size + + " bits which is not secure enough for the " + name + " algorithm. The JWT " + + "JWA Specification (RFC 7518, Section 3.4) states that keys used with " + + name + " MUST have a size >= " + this.minKeyLength + + " bits. Consider using the SignatureAlgorithms." + name + ".generateKeyPair() " + + "method to create a key pair guaranteed to be secure enough for " + name + ". See " + + "https://tools.ietf.org/html/rfc7518#section-3.4 for more information."; + throw new WeakKeyException(msg); + } + } + + @Override + protected byte[] doSign(CryptoRequest request) throws Exception { + PrivateKey privateKey = (PrivateKey) request.getKey(); + Signature sig = createSignatureInstance(request.getProvider(), null); + sig.initSign(privateKey); + sig.update(request.getData()); + return transcodeSignatureToConcat(sig.sign(), signatureLength); + } + + @Override + protected boolean doVerify(VerifySignatureRequest request) throws Exception { + final Key key = request.getKey(); + PublicKey publicKey = (PublicKey) key; + Signature sig = createSignatureInstance(request.getProvider(), null); + byte[] signature = request.getSignature(); + /* + * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature. + * This fallback is for backwards compatibility ONLY (to support tokens generated by previous versions of jjwt) + * and backwards compatibility will possibly be removed in a future version of this library. + */ + byte[] derSignature = this.signatureLength != signature.length && signature[0] == 0x30 ? signature : transcodeSignatureToDER(signature); + sig.initVerify(publicKey); + sig.update(request.getData()); + return sig.verify(derSignature); + } + + /** + * Transcodes the JCA ASN.1/DER-encoded signature into the concatenated + * R + S format expected by ECDSA JWS. + * + * @param derSignature The ASN1./DER-encoded. Must not be {@code null}. + * @param outputLength The expected length of the ECDSA JWS signature. + * @return The ECDSA JWS encoded signature. + * @throws JwtException If the ASN.1/DER signature format is invalid. + */ + public static byte[] transcodeSignatureToConcat(final byte[] derSignature, int outputLength) throws JwtException { + + if (derSignature.length < 8 || derSignature[0] != 48) { + throw new JwtException("Invalid ECDSA signature format"); + } + + int offset; + if (derSignature[1] > 0) { + offset = 2; + } else if (derSignature[1] == (byte) 0x81) { + offset = 3; + } else { + throw new JwtException("Invalid ECDSA signature format"); + } + + byte rLength = derSignature[offset + 1]; + + int i = rLength; + while ((i > 0) && (derSignature[(offset + 2 + rLength) - i] == 0)) { + i--; + } + + byte sLength = derSignature[offset + 2 + rLength + 1]; + + int j = sLength; + while ((j > 0) && (derSignature[(offset + 2 + rLength + 2 + sLength) - j] == 0)) { + j--; + } + + int rawLen = Math.max(i, j); + rawLen = Math.max(rawLen, outputLength / 2); + + if ((derSignature[offset - 1] & 0xff) != derSignature.length - offset + || (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength + || derSignature[offset] != 2 + || derSignature[offset + 2 + rLength] != 2) { + throw new JwtException("Invalid ECDSA signature format"); + } + + final byte[] concatSignature = new byte[2 * rawLen]; + + System.arraycopy(derSignature, (offset + 2 + rLength) - i, concatSignature, rawLen - i, i); + System.arraycopy(derSignature, (offset + 2 + rLength + 2 + sLength) - j, concatSignature, 2 * rawLen - j, j); + + return concatSignature; + } + + /** + * Transcodes the ECDSA JWS signature into ASN.1/DER format for use by + * the JCA verifier. + * + * @param jwsSignature The JWS signature, consisting of the + * concatenated R and S values. Must not be + * {@code null}. + * @return The ASN.1/DER encoded signature. + * @throws JwtException If the ECDSA JWS signature format is invalid. + */ + public static byte[] transcodeSignatureToDER(byte[] jwsSignature) throws JwtException { + + int rawLen = jwsSignature.length / 2; + + int i = rawLen; + + while ((i > 0) && (jwsSignature[rawLen - i] == 0)) { + i--; + } + + int j = i; + + if (jwsSignature[rawLen - i] < 0) { + j += 1; + } + + int k = rawLen; + + while ((k > 0) && (jwsSignature[2 * rawLen - k] == 0)) { + k--; + } + + int l = k; + + if (jwsSignature[2 * rawLen - k] < 0) { + l += 1; + } + + int len = 2 + j + 2 + l; + + if (len > 255) { + throw new JwtException("Invalid ECDSA signature format"); + } + + int offset; + + final byte[] derSignature; + + if (len < 128) { + derSignature = new byte[2 + 2 + j + 2 + l]; + offset = 1; + } else { + derSignature = new byte[3 + 2 + j + 2 + l]; + derSignature[1] = (byte) 0x81; + offset = 2; + } + + derSignature[0] = 48; + derSignature[offset++] = (byte) len; + derSignature[offset++] = 2; + derSignature[offset++] = (byte) j; + + System.arraycopy(jwsSignature, rawLen - i, derSignature, (offset + j) - i, i); + + offset += j; + + derSignature[offset++] = 2; + derSignature[offset++] = (byte) l; + + System.arraycopy(jwsSignature, 2 * rawLen - k, derSignature, (offset + l) - k, k); + + return derSignature; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptKeyRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptKeyRequest.java new file mode 100644 index 000000000..6770c63d1 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptKeyRequest.java @@ -0,0 +1,12 @@ +package io.jsonwebtoken.impl.security; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface EncryptKeyRequest { + + SecretKey getKey(); + +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptedKeyManagementMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptedKeyManagementMode.java new file mode 100644 index 000000000..9a6003ecf --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptedKeyManagementMode.java @@ -0,0 +1,22 @@ +package io.jsonwebtoken.impl.security; + +/** + * A {@code KeyManagementMode} that encrypts the JWE encryption key itself. This is used when embedding the content + * encryption key in the JWE as an encrypted value. This technique allows 1) two or more parties to use the same + * randomly generated key and 2) have an encrypted form of that key specific to each party, ensuring only intended + * recipients may access the random key. This tends to also be a faster approach since an asymmetric key algorithm + * (which can be slow) can be used to encrypt just a key and a symmetric key (which is generally faster) can be used + * to encrypt the main (larger) payload/claims. + * + * @since JJWT_RELEASE_VERSION + */ +public interface EncryptedKeyManagementMode extends KeyManagementMode { + + /** + * Encrypts the key represented by the specified request. + * @param request they key request + * @return the encrypted content encryption key. + */ + byte[] encryptKey(EncryptKeyRequest request); + +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesEncryptionAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesEncryptionAlgorithm.java new file mode 100644 index 000000000..df8cdba15 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesEncryptionAlgorithm.java @@ -0,0 +1,106 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.RuntimeEnvironment; +import io.jsonwebtoken.security.AeadIvRequest; +import io.jsonwebtoken.security.AeadRequest; +import io.jsonwebtoken.security.AeadIvEncryptionResult; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class GcmAesEncryptionAlgorithm extends AbstractAeadAesEncryptionAlgorithm { + + //TODO: Remove this static block when JDK 7 support is removed + // JDK <= 7 does not support AES GCM mode natively and so BouncyCastle is required + static { + RuntimeEnvironment.enableBouncyCastleIfPossible(); + } + + private static final int GCM_IV_SIZE_BITS = 96; // https://tools.ietf.org/html/rfc7518#section-5.3 + private static final String TRANSFORMATION_STRING = "AES/GCM/NoPadding"; + + public GcmAesEncryptionAlgorithm(String name, int requiredKeyLengthInBits) { + super(name, TRANSFORMATION_STRING, GCM_IV_SIZE_BITS, requiredKeyLengthInBits); + //Standard AES only supports 128, 192, and 256 key lengths, respectively: + Assert.isTrue(requiredKeyLengthInBits == 128 || requiredKeyLengthInBits == 192 || requiredKeyLengthInBits == 256, "Invalid AES Key length."); + } + + @Override + protected AeadIvEncryptionResult doEncrypt(final AeadRequest req) throws Exception { + + //Ensure IV: + final byte[] iv = ensureInitializationVector(req); + + //Ensure Key: + final SecretKey encryptionKey = assertKey(req); + + //See if there is any AAD: + final byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty + + byte[] ciphertext = newCipherTemplate(req).execute(new CipherCallback() { + @Override + public byte[] doWithCipher(Cipher cipher) throws Exception { + cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new GCMParameterSpec(AES_BLOCK_SIZE_BITS, iv)); + if (aad != null) { + cipher.updateAAD(aad); + } + return cipher.doFinal(req.getData()); + } + }); + + // When using GCM mode, the JDK actually appends the authentication tag to the ciphertext, so let's + // represent this appropriately: + byte[] taggedCiphertext = ciphertext; + + // Now separate the tag from the ciphertext (tag has a length of AES_BLOCK_SIZE_BITS): + int ciphertextLength = taggedCiphertext.length - AES_BLOCK_SIZE_BYTES; + ciphertext = new byte[ciphertextLength]; + System.arraycopy(taggedCiphertext, 0, ciphertext, 0, ciphertextLength); + + byte[] tag = new byte[AES_BLOCK_SIZE_BYTES]; + System.arraycopy(taggedCiphertext, ciphertextLength, tag, 0, AES_BLOCK_SIZE_BYTES); + + return new DefaultAeadIvEncryptionResult(ciphertext, iv, tag); + } + + @Override + protected byte[] doDecrypt(AeadIvRequest req) throws Exception { + + final byte[] tag = req.getAuthenticationTag(); + Assert.notEmpty(tag, "AeadDecryptionRequests must include a non-empty authentication tag."); + + final byte[] iv = assertDecryptionIv(req); + + //Ensure Key: + final SecretKey decryptionKey = assertKey(req); + + //See if there is any AAD: + final byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty + + final byte[] ciphertext = req.getData(); + + return newCipherTemplate(req).execute(new CipherCallback() { + @Override + public byte[] doWithCipher(Cipher cipher) throws Exception { + cipher.init(Cipher.DECRYPT_MODE, decryptionKey, new GCMParameterSpec(AES_BLOCK_SIZE_BITS, iv)); + + if (aad != null) { + cipher.updateAAD(aad); + } + + //for tagged GCM, the JVM spec requires that the tag be appended to the end of the ciphertext + //byte array. So we'll append it here: + byte[] taggedCiphertext = new byte[ciphertext.length + tag.length]; + System.arraycopy(ciphertext, 0, taggedCiphertext, 0, ciphertext.length); + System.arraycopy(tag, 0, taggedCiphertext, ciphertext.length, tag.length); + + return cipher.doFinal(taggedCiphertext); + } + }); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/GetKeyRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/GetKeyRequest.java new file mode 100644 index 000000000..bb4e1f7d8 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/GetKeyRequest.java @@ -0,0 +1,17 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.EncryptionAlgorithm; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface GetKeyRequest { + + /** + * Returns the encryption algorithm that will be used to encrypt the JWE payload. A {@link KeyManagementMode} + * implementation can inspect this to return or generate a key that matches the required algorithm key length. + * + * @return the encryption algorithm that will be used to encrypt the JWE payload. + */ + EncryptionAlgorithm getEncryptionAlgorithm(); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithm.java new file mode 100644 index 000000000..c2e20fc1c --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithm.java @@ -0,0 +1,186 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadIvRequest; +import io.jsonwebtoken.security.AeadIvEncryptionResult; +import io.jsonwebtoken.security.AeadRequest; +import io.jsonwebtoken.security.CryptoRequest; +import io.jsonwebtoken.security.SignatureException; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; +import java.util.Arrays; + +/** + * @since JJWT_RELEASE_VERSION + */ +@SuppressWarnings("unused") //used via reflection in the io.jsonwebtoken.security.EncryptionAlgorithms class +public class HmacAesEncryptionAlgorithm extends AbstractAeadAesEncryptionAlgorithm { + + private static final String TRANSFORMATION_STRING = "AES/CBC/PKCS5Padding"; + + private final MacSignatureAlgorithm SIGALG; + + public HmacAesEncryptionAlgorithm(String name, MacSignatureAlgorithm sigAlg) { + super(name, TRANSFORMATION_STRING, AES_BLOCK_SIZE_BITS, sigAlg.getMinKeyLength() * 2); + this.SIGALG = sigAlg; + } + + @Override + protected SecretKey doGenerateKey() throws Exception { + + int subKeyLength = getRequiredKeyByteLength() / 2; + + byte[] macKeyBytes = this.SIGALG.generateKey().getEncoded(); + Assert.notEmpty(macKeyBytes, "Generated HMAC key byte array cannot be null or empty."); + + if (macKeyBytes.length > subKeyLength) { + byte[] subKeyBytes = new byte[subKeyLength]; + System.arraycopy(macKeyBytes, 0, subKeyBytes, 0, subKeyLength); + macKeyBytes = subKeyBytes; + } + + if (macKeyBytes.length != subKeyLength) { + String msg = "The delegate MacSignatureAlgorithm instance of type {" + SIGALG.getClass().getName() + "} " + + "generated a key " + macKeyBytes.length + " bytes (" + + macKeyBytes.length * Byte.SIZE + " bits) long. The " + getName() + " algorithm requires " + + "SignatureAlgorithm keys to be " + subKeyLength + " bytes (" + + subKeyLength * Byte.SIZE + " bits) long."; + throw new IllegalStateException(msg); + } + + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + keyGenerator.init(subKeyLength * Byte.SIZE); + + SecretKey encKey = keyGenerator.generateKey(); + byte[] encKeyBytes = encKey.getEncoded(); + + //return as one single key per https://tools.ietf.org/html/rfc7518#section-5.2.2.1 + + byte[] combinedKeyBytes = new byte[macKeyBytes.length + encKeyBytes.length]; + + System.arraycopy(macKeyBytes, 0, combinedKeyBytes, 0, macKeyBytes.length); + System.arraycopy(encKeyBytes, 0, combinedKeyBytes, macKeyBytes.length, encKeyBytes.length); + + return new SecretKeySpec(combinedKeyBytes, "AES"); + } + + byte[] assertKeyBytes(CryptoRequest request) { + SecretKey key = assertKey(request); + return key.getEncoded(); + } + + @Override + protected AeadIvEncryptionResult doEncrypt(final AeadRequest req) throws Exception { + + //Ensure IV: + final byte[] iv = ensureInitializationVector(req); + + //Ensure Key: + byte[] keyBytes = assertKeyBytes(req); + + //See if there is any AAD: + final byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty + + int halfCount = keyBytes.length / 2; // https://tools.ietf.org/html/rfc7518#section-5.2 + byte[] macKeyBytes = Arrays.copyOfRange(keyBytes, 0, halfCount); + keyBytes = Arrays.copyOfRange(keyBytes, halfCount, keyBytes.length); + + final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES"); + + final byte[] ciphertext = newCipherTemplate(req).execute(new CipherCallback() { + @Override + public byte[] doWithCipher(Cipher cipher) throws Exception { + cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new IvParameterSpec(iv)); + byte[] plaintext = req.getData(); + return cipher.doFinal(plaintext); + } + }); + + byte[] tag = sign(aad, iv, ciphertext, macKeyBytes); + + return new DefaultAeadIvEncryptionResult(ciphertext, iv, tag); + } + + private byte[] sign(byte[] aad, byte[] iv, byte[] ciphertext, byte[] macKeyBytes) { + + long aadLength = io.jsonwebtoken.lang.Arrays.length(aad); + long aadLengthInBits = aadLength * Byte.SIZE; + long aadLengthInBitsAsUnsignedInt = aadLengthInBits & 0xffffffffL; + byte[] AL = toBytes(aadLengthInBitsAsUnsignedInt); + + byte[] toHash = new byte[(int) aadLength + iv.length + ciphertext.length + AL.length]; + + if (aad != null) { + System.arraycopy(aad, 0, toHash, 0, aad.length); + System.arraycopy(iv, 0, toHash, aad.length, iv.length); + System.arraycopy(ciphertext, 0, toHash, aad.length + iv.length, ciphertext.length); + System.arraycopy(AL, 0, toHash, aad.length + iv.length + ciphertext.length, AL.length); + } else { + System.arraycopy(iv, 0, toHash, 0, iv.length); + System.arraycopy(ciphertext, 0, toHash, iv.length, ciphertext.length); + System.arraycopy(AL, 0, toHash, iv.length + ciphertext.length, AL.length); + } + + Key key = new SecretKeySpec(macKeyBytes, SIGALG.getJcaName()); + CryptoRequest request = new DefaultCryptoRequest<>(toHash, key, null, null); + byte[] digest = SIGALG.sign(request); + + // https://tools.ietf.org/html/rfc7518#section-5.2.2.1 #5 requires truncating the signature + // to be the same length as the macKey/encKey: + return Arrays.copyOfRange(digest, 0, macKeyBytes.length); + } + + private static byte[] toBytes(long l) { + byte[] b = new byte[8]; + for (int i = 7; i > 0; i--) { + b[i] = (byte) l; + l >>>= 8; + } + b[0] = (byte) l; + return b; + } + + @Override + protected byte[] doDecrypt(AeadIvRequest req) throws Exception { + + byte[] tag = req.getAuthenticationTag(); + Assert.notEmpty(tag, "AeadDecryptionRequests must include a non-empty authentication tag."); + + final byte[] iv = assertDecryptionIv(req); + + //Ensure Key: + byte[] keyBytes = assertKeyBytes(req); + + //See if there is any AAD: + byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty + + int halfCount = keyBytes.length / 2; // https://tools.ietf.org/html/rfc7518#section-5.2 + byte[] macKeyBytes = Arrays.copyOfRange(keyBytes, 0, halfCount); + keyBytes = Arrays.copyOfRange(keyBytes, halfCount, keyBytes.length); + + final SecretKey decryptionKey = new SecretKeySpec(keyBytes, "AES"); + + final byte[] ciphertext = req.getData(); + + // Assert that the aad + iv + ciphertext provided, when signed, equals the tag provided, + // thereby indicating none of it has been tampered with: + byte[] digest = sign(aad, iv, ciphertext, macKeyBytes); + if (!Arrays.equals(digest, tag)) { + String msg = "Ciphertext decryption failed: Authentication tag verification failed."; + throw new SignatureException(msg); + } + + return newCipherTemplate(req).execute(new CipherCallback() { + @Override + public byte[] doWithCipher(Cipher cipher) throws Exception { + cipher.init(Cipher.DECRYPT_MODE, decryptionKey, new IvParameterSpec(iv)); + return cipher.doFinal(ciphertext); + } + }); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java new file mode 100644 index 000000000..48566900a --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.impl.security; + +import java.security.Key; +import java.util.Map; + +public interface JwkConverter { + + Key toKey(Map jwk); + + Map toJwk(Key key); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkParser.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkParser.java new file mode 100644 index 000000000..23ca38d32 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkParser.java @@ -0,0 +1,19 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.KeyException; + +import java.security.Key; +import java.util.Map; + +public interface JwkParser { + + Key parse(String json) throws KeyException; + + Key parse(Map jwkMap) throws KeyException; + + Jwk parseToJwk(String json) throws KeyException; + + Jwk parseToJwk(Map jwkMap) throws KeyException; + +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkValidator.java new file mode 100644 index 000000000..df75bcc20 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkValidator.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.KeyException; + +public interface JwkValidator { + + void validate(T jwk) throws KeyException; +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAgreementWithKeyWrappingMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAgreementWithKeyWrappingMode.java new file mode 100644 index 000000000..579df1a5c --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAgreementWithKeyWrappingMode.java @@ -0,0 +1,12 @@ +package io.jsonwebtoken.impl.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class KeyAgreementWithKeyWrappingMode extends RandomEncryptedKeyMode { + + @Override + public byte[] encryptKey(EncryptKeyRequest request) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyEncryptionMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyEncryptionMode.java new file mode 100644 index 000000000..ddec0534c --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyEncryptionMode.java @@ -0,0 +1,12 @@ +package io.jsonwebtoken.impl.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class KeyEncryptionMode extends RandomEncryptedKeyMode { + + @Override + public byte[] encryptKey(EncryptKeyRequest request) { + throw new UnsupportedOperationException("Not Yet Implemented."); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementMode.java new file mode 100644 index 000000000..2a0093363 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementMode.java @@ -0,0 +1,22 @@ +package io.jsonwebtoken.impl.security; + +import javax.crypto.SecretKey; + +/** + * A Key Management Mode determines the content encryption key to use to encrypt a JWE's payload. + *

+ * If a mode encrypts the encryption key itself for one or more recipients, that mode would implement the + * {@link EncryptedKeyManagementMode} instead of this interface. + * + * @see EncryptedKeyManagementMode + * @since JJWT_RELEASE_VERSION + */ +public interface KeyManagementMode { + + /** + * Returns the key used to encrypt the JWE payload. + * + * @return the key used to encrypt the JWE payload. + */ + SecretKey getKey(GetKeyRequest request); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementModes.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementModes.java new file mode 100644 index 000000000..1c327542f --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementModes.java @@ -0,0 +1,15 @@ +package io.jsonwebtoken.impl.security; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public final class KeyManagementModes { + + private KeyManagementModes(){} + + public static KeyManagementMode direct(SecretKey secretKey) { + return new DirectEncryptionMode(secretKey); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyWrappingMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyWrappingMode.java new file mode 100644 index 000000000..a4d3ccd1e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyWrappingMode.java @@ -0,0 +1,12 @@ +package io.jsonwebtoken.impl.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class KeyWrappingMode extends RandomEncryptedKeyMode { + + @Override + public byte[] encryptKey(EncryptKeyRequest request) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java new file mode 100644 index 000000000..42da42ae6 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java @@ -0,0 +1,181 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.CryptoRequest; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.SymmetricKeySignatureAlgorithm; +import io.jsonwebtoken.security.WeakKeyException; + +import javax.crypto.KeyGenerator; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.util.LinkedHashSet; +import java.util.Set; + +@SuppressWarnings("unused") //used via reflection in the io.jsonwebtoken.security.SignatureAlgorithms class +public class MacSignatureAlgorithm extends AbstractSignatureAlgorithm implements SymmetricKeySignatureAlgorithm { + + private final int minKeyLength; //in bits + + private static final Set JWA_STANDARD_NAMES = new LinkedHashSet<>(Collections.of("HS256", "HS384", "HS512")); + + // PKCS12 OIDs are added to these lists per https://bugs.openjdk.java.net/browse/JDK-8243551 + private static final Set HS256_JCA_NAMES = new LinkedHashSet<>(Collections.of("HMACSHA256", "1.2.840.113549.2.9")); + private static final Set HS384_JCA_NAMES = new LinkedHashSet<>(Collections.of("HMACSHA384", "1.2.840.113549.2.10")); + private static final Set HS512_JCA_NAMES = new LinkedHashSet<>(Collections.of("HMACSHA512", "1.2.840.113549.2.11")); + + private static final Set VALID_HS256_JCA_NAMES; + private static final Set VALID_HS384_JCA_NAMES; + + static { + VALID_HS384_JCA_NAMES = new LinkedHashSet<>(HS384_JCA_NAMES); + VALID_HS384_JCA_NAMES.addAll(HS512_JCA_NAMES); + VALID_HS256_JCA_NAMES = new LinkedHashSet<>(HS256_JCA_NAMES); + VALID_HS256_JCA_NAMES.addAll(VALID_HS384_JCA_NAMES); + } + + public MacSignatureAlgorithm(String name, String jcaName, int minKeyLength) { + super(name, jcaName); + Assert.isTrue(minKeyLength > 0, "minKeyLength must be greater than zero."); + this.minKeyLength = minKeyLength; + } + + int getMinKeyLength() { + return this.minKeyLength; + } + + private boolean isJwaStandard() { + return JWA_STANDARD_NAMES.contains(getName()); + } + + private boolean isJwaStandardJcaName(String jcaName) { + return VALID_HS256_JCA_NAMES.contains(jcaName.toUpperCase()); + } + + //For testing + KeyGenerator doGetKeyGenerator(String jcaName) throws NoSuchAlgorithmException { + return KeyGenerator.getInstance(jcaName); + } + + private KeyGenerator getKeyGenerator() { + String jcaName = getJcaName(); + try { + return doGetKeyGenerator(jcaName); + } catch (NoSuchAlgorithmException e) { + String msg = "There is no JCA Provider available that supports the algorithm name '" + jcaName + + "'. Ensure this is a JCA standard name or you have registered a JCA security provider that " + + "supports this name."; + throw new UnsupportedOperationException(msg, e); + } + } + + @Override + public SecretKey generateKey() { + KeyGenerator generator = getKeyGenerator(); + generator.init(Randoms.secureRandom()); + return generator.generateKey(); + } + + //For testing + Mac doGetMacInstance(String jcaName, Provider provider) throws NoSuchAlgorithmException { + return provider == null ? + Mac.getInstance(jcaName) : + Mac.getInstance(jcaName, provider); + } + + private Mac getMacInstance(CryptoRequest req) { + Provider provider = req.getProvider(); + String jcaName = getJcaName(); + try { + return doGetMacInstance(jcaName, provider); + } catch (NoSuchAlgorithmException e) { + String msg; + if (provider != null) { + msg = "The specified JCA Provider {" + provider + "} does not support "; + } else { + msg = "There is no JCA Provider available that supports "; + } + msg += "MAC algorithm name '" + jcaName + "'."; + throw new SignatureException(msg, e); + } + } + + @Override + protected void validateKey(Key k, boolean signing) { + + final String keyType = keyType(signing); + + if (k == null) { + throw new IllegalArgumentException("Signature " + keyType + " key cannot be null."); + } + + if (!(k instanceof SecretKey)) { + String msg = "MAC " + keyType(signing) + " keys must be SecretKey instances. Specified key is of type " + + k.getClass().getName(); + throw new InvalidKeyException(msg); + } + + final SecretKey key = (SecretKey) k; + + final String name = getName(); + + String alg = key.getAlgorithm(); + if (!Strings.hasText(alg)) { + String msg = "The " + keyType + " key's algorithm cannot be null or empty."; + throw new InvalidKeyException(msg); + } + + //assert key's jca name is valid if it's a JWA standard algorithm: + if (isJwaStandard() && !isJwaStandardJcaName(alg)) { + throw new InvalidKeyException("The " + keyType + " key's algorithm '" + alg + "' does not equal a valid " + + "HmacSHA* algorithm name or PKCS12 OID and cannot be used with " + name + "."); + } + + byte[] encoded = null; + + // https://github.com/jwtk/jjwt/issues/478 + // + // Some HSM modules will not allow applications or libraries to obtain the secret key's encoded bytes. In + // these cases, key length assertions cannot be made, so we'll need to skip the key length checks if so. + try { + encoded = key.getEncoded(); + } catch (Exception ignored) { + } + + if (encoded != null) { //we can perform key length assertions + int size = Arrays.length(encoded) * Byte.SIZE; + if (size < this.minKeyLength) { + String msg = "The " + keyType + " key's size is " + size + " bits which " + + "is not secure enough for the " + name + " algorithm."; + + if (isJwaStandard() && isJwaStandardJcaName(getJcaName())) { //JWA standard algorithm name - reference the spec: + msg += " The JWT " + + "JWA Specification (RFC 7518, Section 3.2) states that keys used with " + name + " MUST have a " + + "size >= " + minKeyLength + " bits (the key size must be greater than or equal to the hash " + + "output size). Consider using the SignatureAlgorithms." + name + ".generateKey() " + + "method to create a key guaranteed to be secure enough for " + name + ". See " + + "https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; + } else { //custom algorithm - just indicate required key length: + msg += " The " + name + " algorithm requires keys to have a size >= " + minKeyLength + " bits."; + } + + throw new WeakKeyException(msg); + } + } + } + + @Override + public byte[] doSign(CryptoRequest request) throws Exception { + Key key = request.getKey(); + Mac mac = getMacInstance(request); + mac.init(key); + return mac.doFinal(request.getData()); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java new file mode 100644 index 000000000..6c8fb1458 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java @@ -0,0 +1,27 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.CryptoRequest; +import io.jsonwebtoken.security.SignatureAlgorithm; +import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.VerifySignatureRequest; + +public class NoneSignatureAlgorithm implements SignatureAlgorithm { + + private static final String NAME = "none"; + + @Override + public String getName() { + return NAME; + } + + @SuppressWarnings("rawtypes") + @Override + public byte[] sign(CryptoRequest request) throws SignatureException { + throw new SignatureException("The 'none' algorithm cannot be used to create signatures."); + } + + @Override + public boolean verify(VerifySignatureRequest request) throws SignatureException { + throw new SignatureException("The 'none' algorithm cannot be used to verify signatures."); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/PrivateEcJwkValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/PrivateEcJwkValidator.java new file mode 100644 index 000000000..86e1dc749 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/PrivateEcJwkValidator.java @@ -0,0 +1,18 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.MalformedKeyException; +import io.jsonwebtoken.security.PrivateEcJwk; + +class PrivateEcJwkValidator extends AbstractEcJwkValidator { + + @Override + void validateEcJwk(PrivateEcJwk jwk) { + if (!Strings.hasText(jwk.getD())) { + String msg = "Private EC JWK private key ('d' property') must be specified."; + throw new MalformedKeyException(msg); + } + + //TODO: RFC octet length validation for d value + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RandomEncryptedKeyMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RandomEncryptedKeyMode.java new file mode 100644 index 000000000..14739ab0a --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RandomEncryptedKeyMode.java @@ -0,0 +1,38 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.EncryptionAlgorithm; +import io.jsonwebtoken.security.SymmetricEncryptionAlgorithm; + +import javax.crypto.SecretKey; + +/** + * Abstract class that implements {@link KeyManagementMode#getKey(GetKeyRequest) getKey} and leaves + * {@link EncryptedKeyManagementMode#encryptKey(EncryptKeyRequest)} to subclasses. + * + * @since JJWT_RELEASE_VERSION + */ +public abstract class RandomEncryptedKeyMode implements EncryptedKeyManagementMode { + + @Override + public SecretKey getKey(GetKeyRequest request) { + + Assert.notNull(request, "GetKeyRequest cannot be null."); + + EncryptionAlgorithm alg = Assert.notNull(request.getEncryptionAlgorithm(), + "GetKeyRequest encryptionAlgorithm cannot be null."); + + if (!(alg instanceof SymmetricEncryptionAlgorithm)) { + String msg = "The standard JWE Encrypted Key Management Modes only support symmetric encryption " + + "algorithms. The specified GetKeyRequest encryptionAlgorithm is an instance of " + + alg.getClass().getName() + " which does not implement " + + SymmetricEncryptionAlgorithm.class.getName() + ". Either specify a JWE-standard symmetric " + + "encryption algorithm or create a custom (non-standard) EncryptedKeyManagementMode implementation."; + throw new IllegalArgumentException(msg); + } + + SymmetricEncryptionAlgorithm salg = (SymmetricEncryptionAlgorithm) alg; + + return salg.generateKey(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Randoms.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Randoms.java new file mode 100644 index 000000000..e7acc54e3 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Randoms.java @@ -0,0 +1,39 @@ +package io.jsonwebtoken.impl.security; + +import java.security.SecureRandom; + +/** + * @since JJWT_RELEASE_VERSION + */ +public final class Randoms { + + private static final SecureRandom DEFAULT_SECURE_RANDOM; + + static { + DEFAULT_SECURE_RANDOM = new SecureRandom(); + DEFAULT_SECURE_RANDOM.nextBytes(new byte[64]); + } + + private Randoms() { + } + + /** + * Returns JJWT's default SecureRandom number generator - a static singleton which may be cached if desired. + * The RNG is initialized using the JVM default as follows: + * + *


+     * static {
+     *     DEFAULT_SECURE_RANDOM = new SecureRandom();
+     *     DEFAULT_SECURE_RANDOM.nextBytes(new byte[64]);
+     * }
+     * 
+ * + *

nextBytes is called to force the RNG to initialize itself if not already initialized. The + * byte array is not used and discarded immediately for garbage collection.

+ * + * @return JJWT's default SecureRandom number generator. + */ + public static SecureRandom secureRandom() { + return DEFAULT_SECURE_RANDOM; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Recipient.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Recipient.java new file mode 100644 index 000000000..ba93e32b7 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Recipient.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.impl.security; + +import java.util.Map; + +/** + * An intended recipient of an Encrypted JWT. + * + * @since JJWT_RELEASE_VERSION + */ +public interface Recipient extends Map { +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaJwkConverter.java new file mode 100644 index 000000000..86b2623dd --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaJwkConverter.java @@ -0,0 +1,28 @@ +package io.jsonwebtoken.impl.security; + +import java.security.Key; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Map; + +public class RsaJwkConverter extends AbstractTypedJwkConverter { + + public RsaJwkConverter() { + super("RSA"); + } + + @Override + public boolean supports(Key key) { + return key instanceof RSAPublicKey || key instanceof RSAPrivateKey; + } + + @Override + public Key toKey(Map jwk) { + throw new UnsupportedOperationException("Not yet implemented."); + } + + @Override + public Map toJwk(Key key) { + throw new UnsupportedOperationException("Not yet implemented."); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaSignatureAlgorithm.java new file mode 100644 index 000000000..be7b2b061 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaSignatureAlgorithm.java @@ -0,0 +1,136 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.RuntimeEnvironment; +import io.jsonwebtoken.security.AsymmetricKeySignatureAlgorithm; +import io.jsonwebtoken.security.CryptoRequest; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.VerifySignatureRequest; +import io.jsonwebtoken.security.WeakKeyException; + +import java.security.InvalidParameterException; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.interfaces.RSAKey; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; + +@SuppressWarnings("unused") //used via reflection in the io.jsonwebtoken.security.SignatureAlgorithms class +public class RsaSignatureAlgorithm extends AbstractSignatureAlgorithm implements AsymmetricKeySignatureAlgorithm { + + static { + RuntimeEnvironment.enableBouncyCastleIfPossible(); //PS256, PS384, PS512 on <= JDK 10 require BC + } + + private static final int MIN_KEY_LENGTH_BITS = 2048; + + private static AlgorithmParameterSpec pssParamFromSaltBitLength(int saltBitLength) { + MGF1ParameterSpec ps = new MGF1ParameterSpec("SHA-" + saltBitLength); + //MGF1ParameterSpec ps = MGF1ParameterSpec.SHA256; + int saltByteLength = saltBitLength / Byte.SIZE; + return new PSSParameterSpec(ps.getDigestAlgorithm(), "MGF1", ps, saltByteLength, 1); + } + + private final int preferredKeyLength; + + private final AlgorithmParameterSpec algorithmParameterSpec; + + public RsaSignatureAlgorithm(String name, String jcaName, int preferredKeyLengthBits, AlgorithmParameterSpec algParam) { + super(name, jcaName); + if (preferredKeyLengthBits < MIN_KEY_LENGTH_BITS) { + String msg = "preferredKeyLengthBits must be greater than the JWA mandatory minimum key length of " + MIN_KEY_LENGTH_BITS; + throw new IllegalArgumentException(msg); + } + this.preferredKeyLength = preferredKeyLengthBits; + this.algorithmParameterSpec = algParam; + } + + public RsaSignatureAlgorithm(String name, String jcaName, int preferredKeyLengthBits) { + this(name, jcaName, preferredKeyLengthBits, null); + } + + public RsaSignatureAlgorithm(String name, String jcaName, int preferredKeyLengthBits, int pssSaltLengthBits) { + this(name, jcaName, preferredKeyLengthBits, pssParamFromSaltBitLength(pssSaltLengthBits)); + } + + //for testing visibility + protected KeyPairGenerator getKeyPairGenerator() throws NoSuchAlgorithmException, InvalidParameterException { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(preferredKeyLength, Randoms.secureRandom()); + return generator; + } + + @Override + public KeyPair generateKeyPair() { + KeyPairGenerator generator; + try { + generator = getKeyPairGenerator(); + } catch (Exception e) { + throw new IllegalStateException("Unable to obtain an RSA KeyPairGenerator: " + e.getMessage(), e); + } + return generator.genKeyPair(); + } + + @Override + protected void validateKey(Key key, boolean signing) { + + if (!(key instanceof RSAKey)) { + String msg = "RSA " + keyType(signing) + " keys must be an RSAKey. The specified key is of type: " + + key.getClass().getName(); + throw new InvalidKeyException(msg); + } + + // https://github.com/jwtk/jjwt/issues/68 + // Instead of checking for an instance of RSAPrivateKey, check for PrivateKey (RSAKey assertion is above): + if (signing && !(key instanceof PrivateKey)) { + String msg = "Asymmetric key signatures must be created with PrivateKeys. The specified key is of type: " + + key.getClass().getName(); + throw new InvalidKeyException(msg); + } + + RSAKey rsaKey = (RSAKey) key; + int size = rsaKey.getModulus().bitLength(); + if (size < MIN_KEY_LENGTH_BITS) { + + String name = getName(); + + String section = name.startsWith("PS") ? "3.5" : "3.3"; + + String msg = "The " + keyType(signing) + " key's size is " + size + " bits which is not secure " + + "enough for the " + name + " algorithm. The JWT JWA Specification (RFC 7518, Section " + + section + ") states that RSA keys MUST have a size >= " + + MIN_KEY_LENGTH_BITS + " bits. Consider using the SignatureAlgorithms." + name + ".generateKeyPair() " + + "method to create a key pair guaranteed to be secure enough for " + name + ". See " + + "https://tools.ietf.org/html/rfc7518#section-" + section + " for more information."; + throw new WeakKeyException(msg); + } + } + + @Override + protected byte[] doSign(CryptoRequest request) throws Exception { + PrivateKey privateKey = (PrivateKey) request.getKey(); + Signature sig = createSignatureInstance(request.getProvider(), this.algorithmParameterSpec); + sig.initSign(privateKey); + sig.update(request.getData()); + return sig.sign(); + } + + @Override + protected boolean doVerify(VerifySignatureRequest request) throws Exception { + final Key key = request.getKey(); + if (key instanceof PrivateKey) { + return super.doVerify(request); + } + + PublicKey publicKey = (PublicKey) key; + Signature sig = createSignatureInstance(request.getProvider(), this.algorithmParameterSpec); + sig.initVerify(publicKey); + sig.update(request.getData()); + return sig.verify(request.getSignature()); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkConverter.java new file mode 100644 index 000000000..5d3a50b66 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkConverter.java @@ -0,0 +1,59 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.DecodingException; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.security.MalformedKeyException; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; +import java.util.HashMap; +import java.util.Map; + +public class SymmetricJwkConverter extends AbstractTypedJwkConverter { + + public SymmetricJwkConverter() { + super("oct"); + } + + @Override + public boolean supports(Key key) { + return key instanceof SecretKey; + } + + @Override + public Map toJwk(Key key) { + String k; + try { + byte[] encoded = key.getEncoded(); + if (encoded == null || encoded.length == 0) { + throw new IllegalArgumentException("SecretKey argument does not have any encoded bytes."); + } + k = Encoders.BASE64URL.encode(encoded); + } catch (Exception e) { + String msg = "Unable to encode secret key to JWK."; + throw new UnsupportedKeyException(msg, e); + } + + Map m = new HashMap<>(); + m.put("kty", "oct"); + m.put("k", k); + + return m; + } + + @Override + public SecretKey toKey(Map jwk) { + String oct = getRequiredString(jwk, "oct"); + byte[] bytes; + try { + bytes = Decoders.BASE64URL.decode(oct); + } catch (DecodingException e) { + String msg = "Unable to Base64Url-decode JWK 'oct' member value: " + oct; + throw new MalformedKeyException(msg, e); + } + return new SecretKeySpec(bytes, "AES"); //TODO: what about other algorithms? + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkValidator.java new file mode 100644 index 000000000..f51863ee2 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkValidator.java @@ -0,0 +1,23 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.KeyException; +import io.jsonwebtoken.security.SymmetricJwk; + +class SymmetricJwkValidator extends AbstractJwkValidator { + + SymmetricJwkValidator() { + super(DefaultSymmetricJwk.TYPE_VALUE); + } + + @Override + void validateJwk(SymmetricJwk jwk) throws KeyException { + + String k = jwk.getK(); + if (!Strings.hasText(k)) { + malformed("Symmetric JWK key value ('k' property) must be specified."); + } + + //TODO: k length validation? + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/TypedJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/TypedJwkConverter.java new file mode 100644 index 000000000..d7e025e52 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/TypedJwkConverter.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.impl.security; + +import java.security.Key; + +public interface TypedJwkConverter extends JwkConverter { + + String getKeyType(); + + boolean supports(Key key); + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy index fa8f717eb..895b24f37 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy @@ -17,6 +17,7 @@ package io.jsonwebtoken import io.jsonwebtoken.impl.DefaultClock import io.jsonwebtoken.impl.FixedClock +import io.jsonwebtoken.impl.JwtTokenizer import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.SignatureException @@ -1503,7 +1504,8 @@ class DeprecatedJwtParserTest { Jwts.parser().setSigningKey(randomKey()).parse(bad) fail() } catch (MalformedJwtException se) { - assertEquals 'JWT strings must contain exactly 2 period characters. Found: 3', se.message + String expected = JwtTokenizer.DELIM_ERR_MSG_PREFIX + '3' + assertEquals expected, se.message } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy index d537ba4bc..6b58d234e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy @@ -17,6 +17,7 @@ package io.jsonwebtoken import io.jsonwebtoken.impl.DefaultHeader import io.jsonwebtoken.impl.DefaultJwsHeader +import io.jsonwebtoken.impl.JwtTokenizer import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec import io.jsonwebtoken.io.Encoders @@ -147,7 +148,8 @@ class DeprecatedJwtsTest { Jwts.parser().parse('foo') fail() } catch (MalformedJwtException e) { - assertEquals e.message, "JWT strings must contain exactly 2 period characters. Found: 0" + String expected = JwtTokenizer.DELIM_ERR_MSG_PREFIX + '0' + assertEquals expected, e.message } } @@ -157,7 +159,8 @@ class DeprecatedJwtsTest { Jwts.parser().parse('.') fail() } catch (MalformedJwtException e) { - assertEquals e.message, "JWT strings must contain exactly 2 period characters. Found: 1" + String expected = JwtTokenizer.DELIM_ERR_MSG_PREFIX + '1' + assertEquals expected, e.message } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index e5b46bb64..659d57637 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -17,6 +17,7 @@ package io.jsonwebtoken import io.jsonwebtoken.impl.DefaultClock import io.jsonwebtoken.impl.FixedClock +import io.jsonwebtoken.impl.JwtTokenizer import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.Keys @@ -1581,7 +1582,8 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKey(randomKey()).build().parse(bad) fail() } catch (MalformedJwtException se) { - assertEquals 'JWT strings must contain exactly 2 period characters. Found: 3', se.message + String expected = JwtTokenizer.DELIM_ERR_MSG_PREFIX + '3' + assertEquals expected, se.message } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index caef2c85e..e44584ace 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -16,7 +16,9 @@ package io.jsonwebtoken import io.jsonwebtoken.impl.DefaultHeader +import io.jsonwebtoken.impl.DefaultJweHeader import io.jsonwebtoken.impl.DefaultJwsHeader +import io.jsonwebtoken.impl.JwtTokenizer import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec import io.jsonwebtoken.io.Encoders @@ -80,6 +82,19 @@ class JwtsTest { assertEquals header.getAlgorithm(), 'HS256' } + @Test + void testJweHeaderWithNoArgs() { + def header = Jwts.jweHeader() + assertTrue header instanceof DefaultJweHeader + } + + @Test + void testJweHeaderWithMapArg() { + def header = Jwts.jweHeader([enc: 'foo']) + assertTrue header instanceof DefaultJweHeader + assertEquals header.getEncryptionAlgorithm(), 'foo' + } + @Test void testClaims() { Claims claims = Jwts.claims() @@ -147,7 +162,8 @@ class JwtsTest { Jwts.parserBuilder().build().parse('foo') fail() } catch (MalformedJwtException e) { - assertEquals e.message, "JWT strings must contain exactly 2 period characters. Found: 0" + String expected = JwtTokenizer.DELIM_ERR_MSG_PREFIX + '0' + assertEquals expected, e.message } } @@ -157,7 +173,8 @@ class JwtsTest { Jwts.parserBuilder().build().parse('.') fail() } catch (MalformedJwtException e) { - assertEquals e.message, "JWT strings must contain exactly 2 period characters. Found: 1" + String expected = JwtTokenizer.DELIM_ERR_MSG_PREFIX + '1' + assertEquals expected, e.message } } @@ -168,6 +185,7 @@ class JwtsTest { fail() } catch (MalformedJwtException e) { assertEquals e.message, "JWT string '..' is missing a header." +// assertEquals "Required JWS Protected Header is missing.", e.message } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy new file mode 100644 index 000000000..8d434ac97 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy @@ -0,0 +1,32 @@ +package io.jsonwebtoken.impl + +import io.jsonwebtoken.JweHeader +import org.junit.Test + +import static org.junit.Assert.* + +/** + * @since JJWT_RELEASE_VERSION + */ +class DefaultJweHeaderTest { + + @Test + void testAlgorithm() { + JweHeader header = new DefaultJweHeader() + header.setAlgorithm('foo') + assertEquals 'foo', header.getAlgorithm() + + header = new DefaultJweHeader([alg: 'bar']) + assertEquals 'bar', header.getAlgorithm() + } + + @Test + void testEncryptionAlgorithm() { + JweHeader header = new DefaultJweHeader() + header.setEncryptionAlgorithm('foo') + assertEquals 'foo', header.getEncryptionAlgorithm() + + header = new DefaultJweHeader([enc: 'bar']) + assertEquals 'bar', header.getEncryptionAlgorithm() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index 4c9984233..61d7b0e4a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -19,23 +19,100 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.CompressionCodecs import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.impl.security.Randoms import io.jsonwebtoken.io.Encoder import io.jsonwebtoken.io.EncodingException import io.jsonwebtoken.io.SerializationException import io.jsonwebtoken.io.Serializer -import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.KeyGenerator -import javax.crypto.SecretKeyFactory -import java.security.KeyFactory +import java.security.Key +import java.security.Provider +import java.security.SecureRandom +import static org.easymock.EasyMock.* import static org.junit.Assert.* class DefaultJwtBuilderTest { private static ObjectMapper objectMapper = new ObjectMapper(); + @Test + void testSetProvider() { + + Provider provider = createMock(Provider) + + final boolean[] called = new boolean[1] + + io.jsonwebtoken.security.SignatureAlgorithm alg = new io.jsonwebtoken.security.SignatureAlgorithm() { + @Override + byte[] sign(CryptoRequest request) throws SignatureException, KeyException { + assertSame provider, request.getProvider() + called[0] = true + //simulate a digest: + byte[] bytes = new byte[32] + Randoms.secureRandom().nextBytes(bytes) + return bytes + } + + @Override + boolean verify(VerifySignatureRequest request) throws SignatureException, KeyException { + throw new IllegalStateException("should not be called during build") + } + + @Override + String getName() { + return "test" + } + } + + replay provider + def b = new DefaultJwtBuilder().setProvider(provider) + .setSubject('me').signWith(SignatureAlgorithms.HS256.generateKey(), alg) + assertSame provider, b.provider + b.compact() + verify provider + assertTrue called[0] + } + + @Test + void testSetSecureRandom() { + + final SecureRandom random = new SecureRandom() + + final boolean[] called = new boolean[1] + + io.jsonwebtoken.security.SignatureAlgorithm alg = new io.jsonwebtoken.security.SignatureAlgorithm() { + @Override + byte[] sign(CryptoRequest request) throws SignatureException, KeyException { + assertSame random, request.getSecureRandom() + called[0] = true + //simulate a digest: + byte[] bytes = new byte[32] + Randoms.secureRandom().nextBytes(bytes) + return bytes + } + + @Override + boolean verify(VerifySignatureRequest request) throws SignatureException, KeyException { + throw new IllegalStateException("should not be called during build") + } + + @Override + String getName() { + return "test" + } + } + + def b = new DefaultJwtBuilder().setSecureRandom(random) + .setSubject('me').signWith(SignatureAlgorithms.HS256.generateKey(), alg) + assertSame random, b.secureRandom + b.compact() + assertTrue called[0] + } + @Test void testSetHeader() { def h = Jwts.header() @@ -167,7 +244,7 @@ class DefaultJwtBuilderTest { def key = Keys.secretKeyFor(alg) b.signWith(key, alg) String s1 = b.compact() - //ensure deprecated signWith(alg, key) produces the same result: + //ensure deprecated with(alg, key) produces the same result: b.signWith(alg, key) String s2 = b.compact() assertEquals s1, s2 diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy index 0ba131d10..f230f57d9 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy @@ -18,7 +18,6 @@ package io.jsonwebtoken.impl import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.JwtParser import io.jsonwebtoken.Jwts -import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.io.Decoder import io.jsonwebtoken.io.DecodingException import io.jsonwebtoken.io.DeserializationException @@ -28,6 +27,12 @@ import org.hamcrest.CoreMatchers import org.junit.Test import static org.easymock.EasyMock.niceMock +import io.jsonwebtoken.security.SignatureAlgorithms +import org.junit.Test + +import java.security.Provider + +import static org.easymock.EasyMock.* import static org.junit.Assert.assertEquals import static org.junit.Assert.assertSame import static org.hamcrest.MatcherAssert.assertThat @@ -40,6 +45,17 @@ class DefaultJwtParserBuilderTest { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + @Test + void testSetProvider() { + Provider provider = createMock(Provider) + replay provider + + def parser = new DefaultJwtParserBuilder().setProvider(provider).build() + + assertSame provider, parser.jwtParser.provider + verify provider + } + @Test(expected = IllegalArgumentException) void testBase64UrlDecodeWithNullArgument() { new DefaultJwtParserBuilder().base64UrlDecodeWith(null) @@ -73,9 +89,10 @@ class DefaultJwtParserBuilderTest { def p = new DefaultJwtParserBuilder().deserializeJsonWith(deserializer) assertSame deserializer, p.deserializer - def key = Keys.secretKeyFor(SignatureAlgorithm.HS256) + def alg = SignatureAlgorithms.HS256 + def key = alg.generateKey() - String jws = Jwts.builder().claim('foo', 'bar').signWith(key, SignatureAlgorithm.HS256).compact() + String jws = Jwts.builder().claim('foo', 'bar').signWith(key, alg).compact() assertEquals 'bar', p.setSigningKey(key).build().parseClaimsJws(jws).getBody().get('foo') } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy index cad6d3d82..c86d89a70 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy @@ -45,7 +45,7 @@ class DefaultJwtParserTest { } @Test - void testBase64UrlEncodeWithCustomDecoder() { + void testBase64UrlDecodeWithCustomDecoder() { def decoder = new Decoder() { @Override Object decode(Object o) throws DecodingException { @@ -56,6 +56,11 @@ class DefaultJwtParserTest { assertSame decoder, b.base64UrlDecoder } + @Test(expected = MalformedJwtException) + void testBase64UrlDecodeWithInvalidInput() { + new DefaultJwtParser().base64UrlDecode('20:SLDKJF;3993;----') + } + @Test(expected = IllegalArgumentException) void testDeserializeJsonWithNullArgument() { new DefaultJwtParser().deserializeJsonWith(null) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DispatchingParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DispatchingParserTest.groovy new file mode 100644 index 000000000..071594f1e --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DispatchingParserTest.groovy @@ -0,0 +1,15 @@ +package io.jsonwebtoken.impl + +import org.junit.Test + +/** + * @since JJWT_RELEASE_VERSION + */ +class DispatchingParserTest { + + @Test + void testCtor() { + new DispatchingParser() + } +} + diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy new file mode 100644 index 000000000..aa8c6e953 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy @@ -0,0 +1,24 @@ +package io.jsonwebtoken.impl + +import static org.junit.Assert.* +import org.junit.Test + +class JwtTokenizerTest { + + @Test + void testJwe() { + + def input = 'header.encryptedKey.initializationVector.body.authenticationTag' + + def t = new JwtTokenizer().tokenize(input) + + assertNotNull t + assertTrue t instanceof TokenizedJwe + TokenizedJwe tjwe = (TokenizedJwe)t + assertEquals 'header', tjwe.getProtected() + assertEquals 'encryptedKey', tjwe.getEncryptedKey() + assertEquals 'initializationVector', tjwe.getIv() + assertEquals 'body', tjwe.getBody() + assertEquals 'authenticationTag', tjwe.getDigest() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy index a0acb029c..6e706f8bd 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy @@ -17,6 +17,7 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.JwtException import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.impl.security.Randoms import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.security.SignatureException import org.junit.Test @@ -43,8 +44,8 @@ class EllipticCurveSignatureValidatorTest { byte[] bytes = new byte[16] byte[] signature = new byte[16] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes) - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) + Randoms.secureRandom().nextBytes(bytes) + Randoms.secureRandom().nextBytes(signature) try { v.isValid(bytes, signature) @@ -55,36 +56,6 @@ class EllipticCurveSignatureValidatorTest { } } - @Test - void ecdsaSignatureComplianceTest() { - def fact = KeyFactory.getInstance("EC"); - def publicKey = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQASisgweVL1tAtIvfmpoqvdXF8sPKTV9YTKNxBwkdkm+/auh4pR8TbaIfsEzcsGUVv61DFNFXb0ozJfurQ59G2XcgAn3vROlSSnpbIvuhKrzL5jwWDTaYa5tVF1Zjwia/5HUhKBkcPuWGXg05nMjWhZfCuEetzMLoGcHmtvabugFrqsAg=" - def pub = fact.generatePublic(new X509EncodedKeySpec(Decoders.BASE64.decode(publicKey))) - def v = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, pub) - def verifier = { token -> - def signatureStart = token.lastIndexOf('.') - def withoutSignature = token.substring(0, signatureStart) - def signature = token.substring(signatureStart + 1) - assert v.isValid(withoutSignature.getBytes("US-ASCII"), Decoders.BASE64URL.decode(signature)), "Signature do not match that of other implementations" - } - //Test verification for token created using https://github.com/auth0/node-jsonwebtoken/tree/v7.0.1 - verifier("eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30.Aab4x7HNRzetjgZ88AMGdYV2Ml7kzFbl8Ql2zXvBores7iRqm2nK6810ANpVo5okhHa82MQf2Q_Zn4tFyLDR9z4GAcKFdcAtopxq1h8X58qBWgNOc0Bn40SsgUc8wOX4rFohUCzEtnUREePsvc9EfXjjAH78WD2nq4tn-N94vf14SncQ") - //Test verification for token created using https://github.com/jwt/ruby-jwt/tree/v1.5.4 - verifier("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJ0ZXN0IjoidGVzdCJ9.AV26tERbSEwcoDGshneZmhokg-tAKUk0uQBoHBohveEd51D5f6EIs6cskkgwtfzs4qAGfx2rYxqQXr7LTXCNquKiAJNkTIKVddbPfped3_TQtmHZTmMNiqmWjiFj7Y9eTPMMRRu26w4gD1a8EQcBF-7UGgeH4L_1CwHJWAXGbtu7uMUn") - } - - @Test - void legacySignatureCompatTest() { - def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def keypair = EllipticCurveProvider.generateKeyPair() - def signature = Signature.getInstance(SignatureAlgorithm.ES512.jcaName) - def data = withoutSignature.getBytes("US-ASCII") - signature.initSign(keypair.private) - signature.update(data) - def signed = signature.sign() - assert new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, keypair.public).isValid(data, signed) - } - @Test void invalidAlgorithmTest() { def invalidAlgorithm = SignatureAlgorithm.HS256 @@ -95,109 +66,4 @@ class EllipticCurveSignatureValidatorTest { assertEquals e.message, 'Unsupported Algorithm: ' + invalidAlgorithm.name() } } - - @Test - void invalidECDSASignatureFormatTest() { - try { - def signature = new byte[257] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) - EllipticCurveProvider.transcodeSignatureToDER(signature) - fail() - } catch (JwtException e) { - assertEquals e.message, 'Invalid ECDSA signature format' - } - } - - @Test - void invalidDERSignatureToJoseFormatTest() { - def verify = { signature -> - try { - EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) - fail() - } catch (JwtException e) { - assertEquals e.message, 'Invalid ECDSA signature format' - } - } - def signature = new byte[257] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) - //invalid type - signature[0] = 34 - verify(signature) - def shortSignature = new byte[7] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(shortSignature) - verify(shortSignature) - signature[0] = 48 -// signature[1] = 0x81 - signature[1] = -10 - verify(signature) - } - - @Test - void edgeCaseSignatureLengthTest() { - def signature = new byte[1] - EllipticCurveProvider.transcodeSignatureToDER(signature) - } - - @Test - void testPaddedSignatureToDER() { - def signature = new byte[32] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) - signature[0] = 0 as byte - EllipticCurveProvider.transcodeSignatureToDER(signature) //no exception - } - - @Test - void edgeCaseSignatureToConcatLengthTest() { - try { - def signature = Decoders.BASE64.decode("MIEAAGg3OVb/ZeX12cYrhK3c07TsMKo7Kc6SiqW++4CAZWCX72DkZPGTdCv2duqlupsnZL53hiG3rfdOLj8drndCU+KHGrn5EotCATdMSLCXJSMMJoHMM/ZPG+QOHHPlOWnAvpC1v4lJb32WxMFNz1VAIWrl9Aa6RPG1GcjCTScKjvEE") - EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) - fail() - } catch (JwtException e) { - - } - } - - @Test - void edgeCaseSignatureToConcatInvalidSignatureTest() { - try { - def signature = Decoders.BASE64.decode("MIGBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") - EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) - fail() - } catch (JwtException e) { - assertEquals e.message, 'Invalid ECDSA signature format' - } - } - - @Test - void edgeCaseSignatureToConcatInvalidSignatureBranchTest() { - try { - def signature = Decoders.BASE64.decode("MIGBAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") - EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) - fail() - } catch (JwtException e) { - assertEquals e.message, 'Invalid ECDSA signature format' - } - } - - @Test - void edgeCaseSignatureToConcatInvalidSignatureBranch2Test() { - try { - def signature = Decoders.BASE64.decode("MIGBAj4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") - EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) - fail() - } catch (JwtException e) { - assertEquals e.message, 'Invalid ECDSA signature format' - } - } - - @Test - void verifySwarmTest() { - for (SignatureAlgorithm algorithm : [SignatureAlgorithm.ES256, SignatureAlgorithm.ES384, SignatureAlgorithm.ES512]) { - def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def keypair = EllipticCurveProvider.generateKeyPair() - def data = withoutSignature.getBytes("US-ASCII") - def signature = new EllipticCurveSigner(algorithm, keypair.private).sign(data) - assert new EllipticCurveSignatureValidator(algorithm, keypair.public).isValid(data, signature) - } - } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy index 97baac2d6..6c418fec0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy @@ -17,6 +17,7 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.JwtException import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.impl.security.Randoms import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.SignatureException import org.junit.Test @@ -73,7 +74,7 @@ class EllipticCurveSignerTest { } byte[] bytes = new byte[16] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes) + Randoms.secureRandom().nextBytes(bytes) try { signer.sign(bytes) @@ -102,7 +103,7 @@ class EllipticCurveSignerTest { } byte[] bytes = new byte[16] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes) + Randoms.secureRandom().nextBytes(bytes) try { signer.sign(bytes) @@ -131,7 +132,7 @@ class EllipticCurveSignerTest { } byte[] bytes = new byte[16] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes) + Randoms.secureRandom().nextBytes(bytes) try { signer.sign(bytes) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/Issue542Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/Issue542Test.groovy index 8636d053e..ccfff63f2 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/Issue542Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/Issue542Test.groovy @@ -1,7 +1,8 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.Jwts -import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.security.SignatureAlgorithm +import io.jsonwebtoken.security.SignatureAlgorithms import org.bouncycastle.asn1.pkcs.PrivateKeyInfo import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter @@ -31,9 +32,9 @@ class Issue542Test { private static String PS512_0_10_7 = 'eyJhbGciOiJQUzUxMiJ9.eyJpc3MiOiJqb2UifQ.r6sisG-FVaMoIJacMSdYZLWFBVoT6bXmf3X3humLZqzoGfsRw3q9-wJ2oIiR4ua2L_mPnJqyPcjFWoXLUzw-URFSyQEAX_S2mWTBn7avCFsmJUh2fMkplG0ynbIHCqReRDl3moQGallbl-SYgArSRI2HbpVt05xsVbk3BmxB8N8buKbBPfUqwZMicRqNpHxoOc-IXaClc7y93gFNfGBMEwXn2nK_ZFXY03pMBL_MHVsJprPmtGfQw0ZZUv29zZbZTkRb6W6bRCi3jIP8sBMnYDqG3_Oyz9sF74IeOoD9sCpgAuRnrSAXhEb3tr1uBwyT__DOI1ZdT8QGFiRRNpUZDm7g4ub7njhXQ6ppkEY6kEKCCoxSq5sAh6EzZQgAfbpKNXy5VIu8s1nR-iJ8GDpeTcpLRhbX8havNzWjc-kSnU95_D5NFoaKfIjofKideVU46lUdCk-m7q8mOoFz8UEK1cXq3t7ay2jLG_sNvv7oZPe2TC4ovQGiQP0Mt446XBuIvyXSvygD3_ACpRSfpAqVoP7Ce98NkV2QCJxYNX1cZ4Zj4HrNoNWMx81TFoyU7RoUhj4tHcgBt_3_jbCO0OCejwswAFhwYRXP3jXeE2QhLaN1QJ7p97ly8WxjkBRac3I2WAeJhOM4CWhtgDmHAER9571MWp-7n4h4bnx9tXXfV7k' private static Map JWS_0_10_7_VALUES = [ - (SignatureAlgorithm.PS256): PS256_0_10_7, - (SignatureAlgorithm.PS384): PS384_0_10_7, - (SignatureAlgorithm.PS512): PS512_0_10_7 + (SignatureAlgorithms.PS256): PS256_0_10_7, + (SignatureAlgorithms.PS384): PS384_0_10_7, + (SignatureAlgorithms.PS512): PS512_0_10_7 ] private static JcaX509CertificateConverter X509_CERT_CONVERTER = new JcaX509CertificateConverter() @@ -45,7 +46,7 @@ class Issue542Test { } private static PublicKey readTestPublicKey(SignatureAlgorithm alg) { - PEMParser parser = getParser(alg.name() + '.crt.pem') + PEMParser parser = getParser(alg.getName() + '.crt.pem') X509CertificateHolder holder = parser.readObject() as X509CertificateHolder try { return X509_CERT_CONVERTER.getCertificate(holder).getPublicKey() @@ -55,7 +56,7 @@ class Issue542Test { } private static PrivateKey readTestPrivateKey(SignatureAlgorithm alg) { - PEMParser parser = getParser(alg.name() + '.key.pem') + PEMParser parser = getParser(alg.getName() + '.key.pem') PrivateKeyInfo info = parser.readObject() as PrivateKeyInfo try { return PEM_KEY_CONVERTER.getPrivateKey(info) @@ -70,7 +71,7 @@ class Issue542Test { @Test void testRsaSsaPssBackwardsCompatibility() { - def algs = [SignatureAlgorithm.PS256, SignatureAlgorithm.PS384, SignatureAlgorithm.PS512] + def algs = [SignatureAlgorithms.PS256, SignatureAlgorithms.PS384, SignatureAlgorithms.PS512] for (alg in algs) { PublicKey key = readTestPublicKey(alg) @@ -85,7 +86,7 @@ class Issue542Test { * class. This method implementation was retained only to demonstrate how they were created for future reference. */ static void main(String[] args) { - def algs = [SignatureAlgorithm.PS256, SignatureAlgorithm.PS384, SignatureAlgorithm.PS512] + def algs = [SignatureAlgorithms.PS256, SignatureAlgorithms.PS384, SignatureAlgorithms.PS512] for (alg in algs) { PrivateKey privateKey = readTestPrivateKey(alg) String jws = Jwts.builder().setIssuer('joe').signWith(privateKey, alg).compact() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy index 5d27a2959..05c547f98 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy @@ -16,6 +16,7 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.impl.security.Randoms import io.jsonwebtoken.security.SignatureException import org.junit.Test @@ -41,7 +42,7 @@ class RsaProviderTest { @Test void testGenerateKeyPairWithInvalidProviderName() { try { - RsaProvider.generateKeyPair('foo', 1024, SignatureProvider.DEFAULT_SECURE_RANDOM) + RsaProvider.generateKeyPair('foo', 1024, Randoms.secureRandom()) fail() } catch (IllegalStateException ise) { assertTrue ise.message.startsWith("Unable to obtain an RSA KeyPairGenerator: ") diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy index 99bf2ea47..5bd20bc14 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy @@ -38,6 +38,20 @@ class RsaSignatureValidatorTest { } } + @Test + void testConstructorWithRsaPublicKey() { + def pair = RsaProvider.generateKeyPair(2048) + def validator = new RsaSignatureValidator(SignatureAlgorithm.RS256, pair.getPublic()); + assertNull validator.SIGNER + } + + @Test + void testConstructorWithRsaPrivateKey() { + def pair = RsaProvider.generateKeyPair(2048) + def validator = new RsaSignatureValidator(SignatureAlgorithm.RS256, pair.getPrivate()); + assertTrue validator.SIGNER instanceof RsaSigner + } + @Test void testDoVerifyWithInvalidKeyException() { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithmTest.groovy new file mode 100644 index 000000000..174d03c07 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithmTest.groovy @@ -0,0 +1,111 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.* +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.security.SecureRandom + +import static org.junit.Assert.* + +/** + * @since JJWT_RELEASE_VERSION + */ +class AbstractAeadAesEncryptionAlgorithmTest { + + @Test(expected = IllegalArgumentException) + void testConstructorWithIvLargerThanAesBlockSize() { + new TestAesEncryptionAlgorithm('foo', 'foo', 136, 128) + } + + @Test(expected = IllegalArgumentException) + void testConstructorWithoutIvLength() { + new TestAesEncryptionAlgorithm('foo', 'foo', 0, 128) + } + + @Test(expected = IllegalArgumentException) + void testConstructorWithoutRequiredKeyLength() { + new TestAesEncryptionAlgorithm('foo', 'foo', 128, 0) + } + + @Test + void testDoEncryptFailure() { + + def alg = new TestAesEncryptionAlgorithm('foo', 'foo', 128, 128) { + @Override + protected AeadIvEncryptionResult doEncrypt(AeadRequest req) throws Exception { + throw new IllegalArgumentException('broken') + } + } + + def req = new DefaultAesEncryptionRequest<>('bar'.getBytes(), alg.generateKey(), 'foo'.getBytes()); + + try { + alg.encrypt(req) + } catch (CryptoException expected) { + assertTrue expected.getCause() instanceof IllegalArgumentException + assertTrue expected.getCause().getMessage().equals('broken') + } + } + + @Test + void testAssertKeyLength() { + + def requiredKeyLength = 16 + + def alg = new TestAesEncryptionAlgorithm('foo', 'foo', 128, requiredKeyLength) + + byte[] bytes = new byte[requiredKeyLength + 1] //not same as requiredKeyByteLength, but it should be + Randoms.secureRandom().nextBytes(bytes) + + try { + alg.assertKeyLength(new SecretKeySpec(bytes, "AES")) + fail() + } catch (CryptoException expected) { + } + } + + @Test + void testGetSecureRandomWhenRequestHasSpecifiedASecureRandom() { + + def alg = new TestAesEncryptionAlgorithm('foo', 'foo', 128, 128) + + def secureRandom = new SecureRandom() + + def req = new DefaultAesEncryptionRequest('data'.getBytes(), alg.generateKey(), null, secureRandom, 'aad'.getBytes()) + + def returnedSecureRandom = alg.ensureSecureRandom(req) + + assertSame(secureRandom, returnedSecureRandom) + } + + @Test(expected = CryptoException) + void testDoGenerateKeyException() { + def alg = new TestAesEncryptionAlgorithm('foo', 'foo', 128, 128) { + @Override + protected SecretKey doGenerateKey() throws Exception { + throw new IllegalStateException("testmsg") + } + } + alg.generateKey() + } + + static class TestAesEncryptionAlgorithm extends AbstractAeadAesEncryptionAlgorithm { + + TestAesEncryptionAlgorithm(String name, String transformationString, int generatedIvLengthInBits, int requiredKeyLengthInBits) { + super(name, transformationString, generatedIvLengthInBits, requiredKeyLengthInBits) + } + + @Override + protected AeadIvEncryptionResult doEncrypt(AeadRequest secretKeyAeadRequest) throws Exception { + return null + } + + @Override + protected byte[] doDecrypt(AeadIvRequest secretKeyAeadIvDecryptionRequest) throws Exception { + return new byte[0] + } + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkTest.groovy new file mode 100644 index 000000000..0839fad8d --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkTest.groovy @@ -0,0 +1,116 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.CurveId +import io.jsonwebtoken.security.CurveIds +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test +import static org.junit.Assert.* + +class AbstractEcJwkTest { + + class TestEcJwk extends AbstractEcJwk { + } + + @Test + void testType() { + assertEquals 'EC', new TestEcJwk().getType() + } + + @Test + void testSetNullX() { + try { + new TestEcJwk().setX(null) + fail() + } catch (IllegalArgumentException e) { + assertEquals "EC JWK x coordinate ('x' property) cannot be null.", e.getMessage() + } + } + + @Test + void testSetEmptyX() { + try { + new TestEcJwk().setX(' ') + fail() + } catch (IllegalArgumentException e) { + assertEquals "EC JWK x coordinate ('x' property) cannot be null or empty.", e.getMessage() + } + } + + @Test + void testX() { + def jwk = new TestEcJwk() + assertEquals 'x', AbstractEcJwk.X + String val = UUID.randomUUID().toString() + jwk.setX(val) + assertEquals val, jwk.get(AbstractEcJwk.X) + assertEquals val, jwk.getX() + } + + @Test + void testY() { + def jwk = new TestEcJwk() + assertEquals 'y', AbstractEcJwk.Y + + jwk.setY(null) //is allowed to be null for non-standard curves + assertNull jwk.get(AbstractEcJwk.Y) + assertNull jwk.getY() + + jwk.setY(' ') + assertNull jwk.get(AbstractEcJwk.Y) + assertNull jwk.getY() + + String val = UUID.randomUUID().toString() + jwk.setY(val) + assertEquals val, jwk.get(AbstractEcJwk.Y) + assertEquals val, jwk.getY() + } + + @Test + void testSetNullCurveId() { + try { + new TestEcJwk().setCurveId(null) + fail() + } catch (IllegalArgumentException iae) { + assertEquals "EC JWK curve id ('crv' property) cannot be null.", iae.getMessage() + } + } + + @Test + void testCurveId() { + def jwk = new TestEcJwk() + assertEquals 'crv', AbstractEcJwk.CURVE_ID + assertNull jwk.getCurveId() + + for(CurveId id : CurveIds.values()) { + jwk.setCurveId(id) + assertEquals id, jwk.get(AbstractEcJwk.CURVE_ID) + assertEquals id, jwk.getCurveId() + jwk.remove(AbstractEcJwk.CURVE_ID) + } + + //assert string conversion works: + for(CurveId id : CurveIds.values()) { + String sval = id.toString() + jwk.put(AbstractEcJwk.CURVE_ID, sval) + CurveId returned = jwk.getCurveId() + assertEquals id, returned + assertEquals id, jwk.get(AbstractEcJwk.CURVE_ID) //ensure conversion occurred + } + } + + @Test + void testGetCurveIdWithInvalidValueType() { + + def jwk = new TestEcJwk() + + def val = new Integer(5) + jwk.put(AbstractEcJwk.CURVE_ID, val) + + try { + jwk.getCurveId() + fail() + } catch (MalformedKeyException e) { + assertEquals "EC JWK 'crv' value must be an CurveId or a String. Value has type: " + val.getClass().getName(), e.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkValidatorTest.groovy new file mode 100644 index 000000000..db289613c --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkValidatorTest.groovy @@ -0,0 +1,57 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.CurveIds +import io.jsonwebtoken.security.MalformedKeyException +import io.jsonwebtoken.security.PublicEcJwk +import org.junit.Test + +class AbstractEcJwkValidatorTest { + + static AbstractEcJwkValidator VALIDATOR = + DefaultPublicEcJwkBuilder.VALIDATOR as AbstractEcJwkValidator + + @Test + void testValid() { + def jwk = new DefaultPublicEcJwk().setCurveId(CurveIds.P256).setX('x').setY('y') + VALIDATOR.validate(jwk) + } + + @Test(expected = MalformedKeyException) + void testIncorrectType() { + def jwk = new DefaultPublicEcJwk() + jwk.put('kty', 'foo') + VALIDATOR.validate(jwk) + } + + @Test(expected = MalformedKeyException) + void testNullCurveId() { + def jwk = new DefaultPublicEcJwk().setX('x').setY('y') + VALIDATOR.validate(jwk) + } + + @Test(expected = MalformedKeyException) + void testNullX() { + def jwk = new DefaultPublicEcJwk().setCurveId(CurveIds.P521) + VALIDATOR.validate(jwk) + } + + @Test(expected = MalformedKeyException) + void testEmptyX() { + def jwk = new DefaultPublicEcJwk().setCurveId(CurveIds.P521) + jwk.put('x', ' ') + VALIDATOR.validate(jwk) + } + + @Test(expected = MalformedKeyException) + void testNullY() { + def jwk = new DefaultPublicEcJwk().setCurveId(CurveIds.P521).setX('x') + VALIDATOR.validate(jwk) + } + + @Test(expected = MalformedKeyException) + void testEmptyY() { + def jwk = new DefaultPublicEcJwk().setCurveId(CurveIds.P521).setX('x') + jwk.put('y', ' ') + VALIDATOR.validate(jwk) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithmTest.groovy new file mode 100644 index 000000000..e074e724a --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithmTest.groovy @@ -0,0 +1,56 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.CryptoException +import io.jsonwebtoken.security.CryptoRequest +import io.jsonwebtoken.security.EncryptionResult +import org.junit.Test + +import javax.crypto.spec.SecretKeySpec + +import static org.junit.Assert.assertSame + +class AbstractEncryptionAlgorithmTest { + + @Test + void testDoEncryptCryptoExceptionPropagates() { + + final CryptoException expected = new CryptoException("foo") + + AbstractEncryptionAlgorithm alg = new AbstractEncryptionAlgorithm('foo', 'foo') { + protected EncryptionResult doEncrypt(CryptoRequest cryptoRequest) throws Exception { + throw expected + } + protected byte[] doDecrypt(CryptoRequest cryptoRequest) throws Exception { + throw new IllegalStateException("should not be called") + } + } + + try { + alg.encrypt(new DefaultCryptoRequest(new byte[1], new SecretKeySpec(new byte[1], 'AES'), null, null)) + } catch (CryptoException thrown) { + assertSame expected, thrown + } + } + + @Test + void testDecryptWithNonCryptoExceptionThrowsCryptoException() { + + final IllegalStateException expected = new IllegalStateException("decrypt") + + AbstractEncryptionAlgorithm alg = new AbstractEncryptionAlgorithm('foo', 'foo') { + protected EncryptionResult doEncrypt(CryptoRequest cryptoRequest) throws Exception { + throw new IllegalStateException("should not be called") + } + protected byte[] doDecrypt(CryptoRequest cryptoRequest) throws Exception { + throw expected + } + } + + try { + alg.decrypt(new DefaultCryptoRequest(new byte[1], new SecretKeySpec(new byte[1], 'AES'), null, null)) + } catch (CryptoException thrown) { + assertSame expected, thrown.getCause() + } + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy new file mode 100644 index 000000000..bf0ae0612 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy @@ -0,0 +1,98 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.Jwk +import org.junit.Test +import static org.junit.Assert.* + +class AbstractJwkBuilderTest { + + static final JwkValidator TEST_VALIDATOR = new TestJwkValidator() + + class TestJwkBuilder extends AbstractJwkBuilder { + def TestJwkBuilder(JwkValidator validator=TEST_VALIDATOR) { + super(validator) + } + @Override + def Jwk newJwk() { + return new TestJwk() + } + } + + class NullJwkBuilder extends AbstractJwkBuilder { + def NullJwkBuilder(JwkValidator validator=TEST_VALIDATOR) { + super(validator) + } + @Override + def Jwk newJwk() { + return null + } + } + + @Test(expected = IllegalArgumentException) + void testCtorWithNullValidator() { + new TestJwkBuilder(null) + } + + @Test + void testCtorNonNullNewJwk() { + def builder = new TestJwkBuilder() + assertTrue builder.jwk instanceof TestJwk + } + + @Test(expected=IllegalArgumentException) + void testCtorWithSubclassNullJwk() { + new NullJwkBuilder() + } + + @Test + void testUse() { + def val = UUID.randomUUID().toString() + assertEquals val, new TestJwkBuilder().setUse(val).build().getUse() + } + + @Test + void testOperations() { + def a = UUID.randomUUID().toString() + def b = UUID.randomUUID().toString() + def set = [a, b] as Set + assertEquals set, new TestJwkBuilder().setOperations(set).build().getOperations() + } + + @Test + void testAlgorithm() { + def val = UUID.randomUUID().toString() + assertEquals val, new TestJwkBuilder().setAlgorithm(val).build().getAlgorithm() + } + + @Test + void testId() { + def val = UUID.randomUUID().toString() + assertEquals val, new TestJwkBuilder().setId(val).build().getId() + } + + @Test + void testX509Url() { + def val = new URI(UUID.randomUUID().toString()) + assertEquals val, new TestJwkBuilder().setX509Url(val).build().getX509Url() + } + + @Test + void testX509CertificateChain() { + def a = UUID.randomUUID().toString() + def b = UUID.randomUUID().toString() + def val = [a, b] as List + assertEquals val, new TestJwkBuilder().setX509CertificateChain(val).build().getX509CertficateChain() + } + + @Test + void testX509CertificateSha1Thumbprint() { + def val = UUID.randomUUID().toString() + assertEquals val, new TestJwkBuilder().setX509CertificateSha1Thumbprint(val).build().getX509CertificateSha1Thumbprint() + } + + @Test + void testX509CertificateSha256Thumbprint() { + def val = UUID.randomUUID().toString() + assertEquals val, new TestJwkBuilder().setX509CertificateSha256Thumbprint(val).build().getX509CertificateSha256Thumbprint() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy new file mode 100644 index 000000000..a3b772c5a --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy @@ -0,0 +1,260 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.Jwk +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test +import static org.junit.Assert.* + +class AbstractJwkTest { + + class TestJwk extends AbstractJwk { + TestJwk() { + super("test") + } + } + + class NullTypeJwk extends AbstractJwk { + NullTypeJwk() { + super(null) + } + } + + class EmptyTypeJwk extends AbstractJwk { + EmptyTypeJwk() { + super(" ") + } + } + + @Test(expected=IllegalArgumentException) + void testNullType() { + new NullTypeJwk() + } + + @Test(expected=IllegalArgumentException) + void testEmptyType() { + new EmptyTypeJwk() + } + + @Test + void testType() { + assertEquals "test", new TestJwk().getType() + } + + @Test + void testUse() { + def jwk = new TestJwk() + + assertEquals 'use', AbstractJwk.USE + + jwk.setUse(null) + assertNull jwk.get(AbstractJwk.USE) + assertNull jwk.getUse() + + jwk.setUse(' ') //empty should remove + assertNull jwk.get(AbstractJwk.USE) + assertNull jwk.getUse() + + String val = UUID.randomUUID().toString() + + jwk.setUse(val) + assertEquals val, jwk.get(AbstractJwk.USE) + assertEquals val, jwk.getUse() + } + + @Test + void testOperations() { + + def jwk = new TestJwk() + + assertEquals 'key_ops', AbstractJwk.OPERATIONS + + jwk.setOperations(null) + assertNull jwk.get(AbstractJwk.OPERATIONS) + assertNull jwk.getOperations() + + jwk.setOperations([] as Set) //empty should remove + assertNull jwk.get(AbstractJwk.OPERATIONS) + assertNull jwk.getOperations() + + def set = ['a', 'b'] as Set + + jwk.setOperations(set) + assertEquals set, jwk.get(AbstractJwk.OPERATIONS) + assertEquals set, jwk.getOperations() + } + + @Test + void testAlgorithm() { + def jwk = new TestJwk() + + assertEquals 'alg', AbstractJwk.ALGORITHM + + jwk.setAlgorithm(null) + assertNull jwk.get(AbstractJwk.ALGORITHM) + assertNull jwk.getAlgorithm() + + jwk.setAlgorithm(' ') //empty should remove + assertNull jwk.get(AbstractJwk.ALGORITHM) + assertNull jwk.getAlgorithm() + + String val = UUID.randomUUID().toString() + jwk.setAlgorithm(val) + assertEquals val, jwk.get(AbstractJwk.ALGORITHM) + assertEquals val, jwk.getAlgorithm() + } + + @Test + void testId() { + def jwk = new TestJwk() + + assertEquals 'kid', AbstractJwk.ID + + jwk.setId(null) + assertNull jwk.get(AbstractJwk.ID) + assertNull jwk.getId() + + jwk.setId(' ') //empty should remove + assertNull jwk.get(AbstractJwk.ID) + assertNull jwk.getId() + + String val = UUID.randomUUID().toString() + jwk.setId(val) + assertEquals val, jwk.get(AbstractJwk.ID) + assertEquals val, jwk.getId() + } + + @Test + void testX509Sha1Thumbprint() { + def jwk = new TestJwk() + + assertEquals 'x5t', AbstractJwk.X509_SHA1_THUMBPRINT + + jwk.setX509CertificateSha1Thumbprint(null) + assertNull jwk.get(AbstractJwk.X509_SHA1_THUMBPRINT) + assertNull jwk.getX509CertificateSha1Thumbprint() + + jwk.setX509CertificateSha1Thumbprint(' ') //empty should remove + assertNull jwk.get(AbstractJwk.X509_SHA1_THUMBPRINT) + assertNull jwk.getX509CertificateSha1Thumbprint() + + String val = UUID.randomUUID().toString() + jwk.setX509CertificateSha1Thumbprint(val) + assertEquals val, jwk.get(AbstractJwk.X509_SHA1_THUMBPRINT) + assertEquals val, jwk.getX509CertificateSha1Thumbprint() + } + + @Test + void testX509Sha256Thumbprint() { + def jwk = new TestJwk() + + assertEquals 'x5t#S256', AbstractJwk.X509_SHA256_THUMBPRINT + + jwk.setX509CertificateSha1Thumbprint(null) + assertNull jwk.get(AbstractJwk.X509_SHA256_THUMBPRINT) + assertNull jwk.getX509CertificateSha256Thumbprint() + + jwk.setX509CertificateSha256Thumbprint(' ') //empty should remove + assertNull jwk.get(AbstractJwk.X509_SHA256_THUMBPRINT) + assertNull jwk.getX509CertificateSha256Thumbprint() + + String val = UUID.randomUUID().toString() + jwk.setX509CertificateSha256Thumbprint(val) + assertEquals val, jwk.get(AbstractJwk.X509_SHA256_THUMBPRINT) + assertEquals val, jwk.getX509CertificateSha256Thumbprint() + } + + @Test + void testX509Url() { + + def jwk = new TestJwk() + + assertEquals 'x5u', AbstractJwk.X509_URL + + jwk.setX509Url(null) + assertNull jwk.get(AbstractJwk.X509_URL) + assertNull jwk.getX509Url() + + String suri = 'https://whatever.com/cert' + def uri = new URI(suri) + + jwk.put(AbstractJwk.X509_URL, uri) + assertEquals uri, jwk.get(AbstractJwk.X509_URL) + assertEquals uri, jwk.getX509Url() + + jwk.put(AbstractJwk.X509_URL, suri) + assertEquals suri, jwk.get(AbstractJwk.X509_URL) //string here + assertEquals uri, jwk.getX509Url() //conversion here + assertEquals uri, jwk.get(AbstractJwk.X509_URL) //ensure replaced with URI instance + + jwk.remove(AbstractJwk.X509_URL) //clear for next test + + jwk.setX509Url(uri) + assertEquals uri, jwk.get(AbstractJwk.X509_URL) + assertEquals uri, jwk.getX509Url() + } + + @Test + void testGetX509UrlWithInvalidUri() { + def jwk = new TestJwk() + def uri = '|not-a-uri|' + jwk.put(AbstractJwk.X509_URL, uri) + try { + jwk.getX509Url() + fail() + } catch (MalformedKeyException e) { + assertEquals 'test JWK x5u value cannot be converted to a URI instance: ' + uri, e.getMessage() + assertTrue e.getCause() instanceof URISyntaxException + } + } + + @Test + void testGetListWithNullValue() { + assertNull new TestJwk().getList("foo") + } + + @Test + void testGetX509CertChainWithSet() { + def jwk = new TestJwk() + jwk.put('x5c', new LinkedHashSet<>(['a', null, 'b'])) + def chain = jwk.getX509CertficateChain() + assertTrue chain instanceof List + assertEquals 3, chain.size() + assertEquals 'a', chain[0] + assertNull chain[1] + assertEquals 'b', chain[2] + } + + @Test + void testGetX509CertChainWithArray() { + def jwk = new TestJwk() + jwk.put('x5c', ['a', null, 'b'] as String[]) + def chain = jwk.getX509CertficateChain() + assertTrue chain instanceof List + assertEquals 3, chain.size() + assertEquals 'a', chain[0] + assertNull chain[1] + assertEquals 'b', chain[2] + } + + @Test + void testSetX509CertChain() { + + def jwk = new TestJwk() + + assertEquals 'x5c', AbstractJwk.X509_CERT_CHAIN + + jwk.setX509CertificateChain(null) + assertNull jwk.get(AbstractJwk.X509_CERT_CHAIN) + assertNull jwk.getX509CertficateChain() + + jwk.setX509CertificateChain([]) + assertNull jwk.get(AbstractJwk.X509_CERT_CHAIN) + assertNull jwk.getX509CertficateChain() + + String val = UUID.randomUUID().toString() + def chain = [val] + jwk.setX509CertificateChain(chain) + assertEquals chain, jwk.get(AbstractJwk.X509_CERT_CHAIN) + assertEquals chain, jwk.getX509CertficateChain() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkValidatorTest.groovy new file mode 100644 index 000000000..832cd9508 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkValidatorTest.groovy @@ -0,0 +1,55 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test +import static org.junit.Assert.* + +class AbstractJwkValidatorTest { + + static final String malformedMsg = "JWKs must have a key type ('kty') property value." + + @Test + void testValidateWithNullType() { + def jwk = new TestJwk() + jwk.remove('kty') + try { + new TestJwkValidator<>().validate(jwk) + fail() + } catch (MalformedKeyException e) { + assertEquals malformedMsg, e.getMessage() + } + } + + @Test + void testValidateWithEmptyType() { + def jwk = new TestJwk() + jwk.put('kty', ' ') + try { + new TestJwkValidator<>().validate(jwk) + fail() + } catch (MalformedKeyException e) { + assertEquals malformedMsg, e.getMessage() + } + } + + @Test + void testIncorrectType() { + def jwk = new TestJwk() + jwk.put('kty', 'foo') + try { + new TestJwkValidator<>().validate(jwk) + fail() + } catch (MalformedKeyException e) { + assertEquals "JWK does not have expected key type ('kty') value of 'test'. Value found: foo", + e.getMessage() + } + } + + @Test + void testValid() { + def jwk = new TestJwk() + def validator = new TestJwkValidator() + validator.validate(jwk) + assertEquals jwk, validator.jwk + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy new file mode 100644 index 000000000..69b89c86a --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy @@ -0,0 +1,169 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.CryptoRequest +import io.jsonwebtoken.security.SignatureAlgorithms +import io.jsonwebtoken.security.SignatureException +import io.jsonwebtoken.security.VerifySignatureRequest +import org.junit.Test + +import javax.xml.crypto.dsig.spec.HMACParameterSpec +import java.nio.charset.StandardCharsets +import java.security.* +import java.security.spec.AlgorithmParameterSpec + +import static org.easymock.EasyMock.createMock +import static org.junit.Assert.* + +class AbstractSignatureAlgorithmTest { + + @Test + void testCreateSignatureInstanceFailureNoProvider() { + + def alg = new TestAbstractSignatureAlgorithm() { + @Override + protected Signature getSignatureInstance(Provider provider) throws NoSuchAlgorithmException { + throw new NoSuchAlgorithmException('message-here') + } + } + + try { + alg.createSignatureInstance(null, null) + } catch (SignatureException e) { + assertEquals 'JWT signature algorithm \'test\' uses the JCA algorithm \'test\', which is not available in the current JVM. Try explicitly supplying a JCA Provider that supports the JCA algorithm name \'test\'. Cause: message-here', e.getMessage() + } + } + + @Test + void testCreateSignatureInstanceFailureWithoutBouncyCastle() { + def alg = new TestAbstractSignatureAlgorithm() { + @Override + protected Signature getSignatureInstance(Provider provider) throws NoSuchAlgorithmException { + throw new NoSuchAlgorithmException('message-here') + } + + @Override + protected boolean isBouncyCastleAvailable() { + return false + } + } + + try { + alg.createSignatureInstance(null, null) + } catch (SignatureException e) { + assertEquals 'JWT signature algorithm \'test\' uses the JCA algorithm \'test\', which is not available in the current JVM. Try including BouncyCastle in the runtime classpath, or explicitly supplying a JCA Provider that supports the JCA algorithm name \'test\'. Cause: message-here', e.getMessage() + } + + } + + @Test + void testCreateSignatureInstanceFailureWithProvider() { + + def mockProvider = createMock(Provider) + + def alg = new TestAbstractSignatureAlgorithm() { + @Override + protected Signature getSignatureInstance(Provider provider) throws NoSuchAlgorithmException { + throw new NoSuchAlgorithmException('message-here') + } + } + + try { + alg.createSignatureInstance(mockProvider, null) + } catch (SignatureException e) { + assertEquals 'JWT signature algorithm \'test\' uses the JCA algorithm \'test\', which is not supported by the specified JCA Provider {EasyMock for class java.security.Provider}. Try explicitly supplying a JCA Provider that supports the JCA algorithm name \'test\'. Cause: message-here', e.getMessage() + } + } + + @Test + void testCreateSignatureInstanceWithBadAlgParam() { + def alg = new AbstractSignatureAlgorithm('RS256', 'SHA256withRSA') { + @Override + protected void validateKey(Key key, boolean signing) { + } + + @Override + protected byte[] doSign(CryptoRequest request) throws Exception { + return new byte[0] + } + + @Override + protected void setParameter(Signature sig, AlgorithmParameterSpec spec) throws InvalidAlgorithmParameterException { + throw new InvalidAlgorithmParameterException("whatevs") + } + } + + try { + alg.createSignatureInstance(null, new HMACParameterSpec(256)) //not RSA at all + } catch (SignatureException expected) { + String msg = expected.getMessage() + assertTrue msg.startsWith('Unsupported SHA256withRSA parameter {') + assertTrue msg.endsWith('}: whatevs') + } + + } + + @Test + void testSignAndVerifyWithExplicitProvider() { + Provider provider = Security.getProvider('BC') + KeyPair pair = SignatureAlgorithms.RS256.generateKeyPair() + byte[] data = 'foo'.getBytes(StandardCharsets.UTF_8) + byte[] signature = SignatureAlgorithms.RS256.sign(new DefaultCryptoRequest(data, pair.getPrivate(), provider, null)) + assertTrue SignatureAlgorithms.RS256.verify(new DefaultVerifySignatureRequest(data, pair.getPublic(), provider, null, signature)) + } + + @Test + void testSignFailsWithAnExternalException() { + KeyPair pair = SignatureAlgorithms.RS256.generateKeyPair() + def ise = new IllegalStateException('foo') + def alg = new TestAbstractSignatureAlgorithm() { + @Override + protected byte[] doSign(CryptoRequest request) throws Exception { + throw ise + } + } + try { + alg.sign(new DefaultCryptoRequest('foo'.getBytes(StandardCharsets.UTF_8), pair.getPrivate(), null, null)) + } catch (SignatureException e) { + assertTrue e.getMessage().startsWith('Unable to compute test signature with JCA algorithm \'test\' using key {') + assertTrue e.getMessage().endsWith('}: foo') + assertSame ise, e.getCause() + } + } + + @Test + void testVerifyFailsWithExternalException() { + KeyPair pair = SignatureAlgorithms.RS256.generateKeyPair() + def ise = new IllegalStateException('foo') + def alg = new TestAbstractSignatureAlgorithm() { + @Override + protected boolean doVerify(VerifySignatureRequest request) throws Exception { + throw ise + } + } + def data = 'foo'.getBytes(StandardCharsets.UTF_8) + try { + byte[] signature = alg.sign(new DefaultCryptoRequest(data, pair.getPrivate(), null, null)) + alg.verify(new DefaultVerifySignatureRequest(data, pair.getPublic(), null, null, signature)) + } catch (SignatureException e) { + assertTrue e.getMessage().startsWith('Unable to verify test signature with JCA algorithm \'test\' using key {') + assertTrue e.getMessage().endsWith('}: foo') + assertSame ise, e.getCause() + } + } + + class TestAbstractSignatureAlgorithm extends AbstractSignatureAlgorithm { + + def TestAbstractSignatureAlgorithm() { + super('test', 'test') + } + + @Override + protected void validateKey(Key key, boolean signing) { + } + + @Override + protected byte[] doSign(CryptoRequest request) throws Exception { + return new byte[1] + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes128CbcHmacSha256Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes128CbcHmacSha256Test.groovy new file mode 100644 index 000000000..038618fd2 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes128CbcHmacSha256Test.groovy @@ -0,0 +1,96 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.* +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec + +import static org.junit.Assert.assertArrayEquals +import static org.junit.Assert.assertTrue + +/** + * Test case defined in https://tools.ietf.org/html/rfc7518#appendix-B.1 + * @since JJWT_RELEASE_VERSION + */ +class Aes128CbcHmacSha256Test { + + final byte[] K = + [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f] as byte[] + final SecretKey KEY = new SecretKeySpec(K, "AES") + + final byte[] MAC_KEY = + [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f] as byte[] + + final byte[] ENC_KEY = + [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f] as byte[] + + final byte[] P = + [0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, + 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, + 0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69, + 0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f, + 0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65] as byte[] + + final byte[] IV = + [0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04] as byte[] + + final byte[] A = + [0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, + 0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, + 0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73] as byte[] + + final byte[] AL = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50] as byte[] + + final byte[] E = + [0xc8, 0x0e, 0xdf, 0xa3, 0x2d, 0xdf, 0x39, 0xd5, 0xef, 0x00, 0xc0, 0xb4, 0x68, 0x83, 0x42, 0x79, + 0xa2, 0xe4, 0x6a, 0x1b, 0x80, 0x49, 0xf7, 0x92, 0xf7, 0x6b, 0xfe, 0x54, 0xb9, 0x03, 0xa9, 0xc9, + 0xa9, 0x4a, 0xc9, 0xb4, 0x7a, 0xd2, 0x65, 0x5c, 0x5f, 0x10, 0xf9, 0xae, 0xf7, 0x14, 0x27, 0xe2, + 0xfc, 0x6f, 0x9b, 0x3f, 0x39, 0x9a, 0x22, 0x14, 0x89, 0xf1, 0x63, 0x62, 0xc7, 0x03, 0x23, 0x36, + 0x09, 0xd4, 0x5a, 0xc6, 0x98, 0x64, 0xe3, 0x32, 0x1c, 0xf8, 0x29, 0x35, 0xac, 0x40, 0x96, 0xc8, + 0x6e, 0x13, 0x33, 0x14, 0xc5, 0x40, 0x19, 0xe8, 0xca, 0x79, 0x80, 0xdf, 0xa4, 0xb9, 0xcf, 0x1b, + 0x38, 0x4c, 0x48, 0x6f, 0x3a, 0x54, 0xc5, 0x10, 0x78, 0x15, 0x8e, 0xe5, 0xd7, 0x9d, 0xe5, 0x9f, + 0xbd, 0x34, 0xd8, 0x48, 0xb3, 0xd6, 0x95, 0x50, 0xa6, 0x76, 0x46, 0x34, 0x44, 0x27, 0xad, 0xe5, + 0x4b, 0x88, 0x51, 0xff, 0xb5, 0x98, 0xf7, 0xf8, 0x00, 0x74, 0xb9, 0x47, 0x3c, 0x82, 0xe2, 0xdb] as byte[] + + final byte[] M = + [0x65, 0x2c, 0x3f, 0xa3, 0x6b, 0x0a, 0x7c, 0x5b, 0x32, 0x19, 0xfa, 0xb3, 0xa3, 0x0b, 0xc1, 0xc4, + 0xe6, 0xe5, 0x45, 0x82, 0x47, 0x65, 0x15, 0xf0, 0xad, 0x9f, 0x75, 0xa2, 0xb7, 0x1c, 0x73, 0xef] as byte[] + + final byte[] T = + [0x65, 0x2c, 0x3f, 0xa3, 0x6b, 0x0a, 0x7c, 0x5b, 0x32, 0x19, 0xfa, 0xb3, 0xa3, 0x0b, 0xc1, 0xc4] as byte[] + + @Test + void test() { + + def alg = EncryptionAlgorithms.A128CBC_HS256 + + def request = new DefaultEncryptionRequest(P, KEY, null, null, IV, A) + + def r = alg.encrypt(request); + + assertTrue r instanceof AeadIvEncryptionResult + AeadIvEncryptionResult result = r as AeadIvEncryptionResult; + + byte[] ciphertext = result.getCiphertext() + byte[] tag = result.getAuthenticationTag() + byte[] iv = result.getInitializationVector() + + assertArrayEquals E, ciphertext + assertArrayEquals T, tag + assertArrayEquals IV, iv //shouldn't have been altered + + // now test decryption: + + def dreq = new DefaultAeadIvRequest(ciphertext, KEY, null, null, iv, A, tag) + + byte[] decryptionResult = alg.decrypt(dreq) + + assertArrayEquals(P, decryptionResult) + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes192CbcHmacSha384Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes192CbcHmacSha384Test.groovy new file mode 100644 index 000000000..f1d0f8711 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes192CbcHmacSha384Test.groovy @@ -0,0 +1,94 @@ +package io.jsonwebtoken.impl.security + + +import io.jsonwebtoken.security.AeadIvEncryptionResult +import io.jsonwebtoken.security.EncryptionAlgorithms +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec + +import static org.junit.Assert.assertArrayEquals +import static org.junit.Assert.assertTrue + +/** + * Test case defined in https://tools.ietf.org/html/rfc7518#appendix-B.2 + * @since JJWT_RELEASE_VERSION + */ +class Aes192CbcHmacSha384Test { + + final byte[] K = + [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f] as byte[] + final SecretKey KEY = new SecretKeySpec(K, "AES") + + final byte[] P = + [0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, + 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, + 0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69, + 0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f, + 0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65] as byte[] + + final byte[] IV = + [0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04] as byte[] + + final byte[] A = + [0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, + 0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, + 0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73] as byte[] + + final byte[] AL = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50] as byte[] + + final byte[] E = + [0xea, 0x65, 0xda, 0x6b, 0x59, 0xe6, 0x1e, 0xdb, 0x41, 0x9b, 0xe6, 0x2d, 0x19, 0x71, 0x2a, 0xe5, + 0xd3, 0x03, 0xee, 0xb5, 0x00, 0x52, 0xd0, 0xdf, 0xd6, 0x69, 0x7f, 0x77, 0x22, 0x4c, 0x8e, 0xdb, + 0x00, 0x0d, 0x27, 0x9b, 0xdc, 0x14, 0xc1, 0x07, 0x26, 0x54, 0xbd, 0x30, 0x94, 0x42, 0x30, 0xc6, + 0x57, 0xbe, 0xd4, 0xca, 0x0c, 0x9f, 0x4a, 0x84, 0x66, 0xf2, 0x2b, 0x22, 0x6d, 0x17, 0x46, 0x21, + 0x4b, 0xf8, 0xcf, 0xc2, 0x40, 0x0a, 0xdd, 0x9f, 0x51, 0x26, 0xe4, 0x79, 0x66, 0x3f, 0xc9, 0x0b, + 0x3b, 0xed, 0x78, 0x7a, 0x2f, 0x0f, 0xfc, 0xbf, 0x39, 0x04, 0xbe, 0x2a, 0x64, 0x1d, 0x5c, 0x21, + 0x05, 0xbf, 0xe5, 0x91, 0xba, 0xe2, 0x3b, 0x1d, 0x74, 0x49, 0xe5, 0x32, 0xee, 0xf6, 0x0a, 0x9a, + 0xc8, 0xbb, 0x6c, 0x6b, 0x01, 0xd3, 0x5d, 0x49, 0x78, 0x7b, 0xcd, 0x57, 0xef, 0x48, 0x49, 0x27, + 0xf2, 0x80, 0xad, 0xc9, 0x1a, 0xc0, 0xc4, 0xe7, 0x9c, 0x7b, 0x11, 0xef, 0xc6, 0x00, 0x54, 0xe3] as byte[] + + final byte[] M = + [0x84, 0x90, 0xac, 0x0e, 0x58, 0x94, 0x9b, 0xfe, 0x51, 0x87, 0x5d, 0x73, 0x3f, 0x93, 0xac, 0x20, + 0x75, 0x16, 0x80, 0x39, 0xcc, 0xc7, 0x33, 0xd7, 0x45, 0x94, 0xf8, 0x86, 0xb3, 0xfa, 0xaf, 0xd4, + 0x86, 0xf2, 0x5c, 0x71, 0x31, 0xe3, 0x28, 0x1e, 0x36, 0xc7, 0xa2, 0xd1, 0x30, 0xaf, 0xde, 0x57] as byte[] + + final byte[] T = + [0x84, 0x90, 0xac, 0x0e, 0x58, 0x94, 0x9b, 0xfe, 0x51, 0x87, 0x5d, 0x73, 0x3f, 0x93, 0xac, 0x20, + 0x75, 0x16, 0x80, 0x39, 0xcc, 0xc7, 0x33, 0xd7] as byte[] + + @Test + void test() { + + def alg = EncryptionAlgorithms.A192CBC_HS384 + + def req = new DefaultEncryptionRequest(P, KEY, null, null, IV, A); + + def r = alg.encrypt(req) + + assertTrue r instanceof AeadIvEncryptionResult + AeadIvEncryptionResult result = r as AeadIvEncryptionResult; + + byte[] resultCiphertext = result.getCiphertext() + byte[] resultTag = result.getAuthenticationTag(); + byte[] resultIv = result.getInitializationVector(); + + assertArrayEquals E, resultCiphertext + assertArrayEquals T, resultTag + assertArrayEquals IV, resultIv //shouldn't have been altered + + // now test decryption: + + def dreq = new DefaultAeadIvRequest(resultCiphertext, KEY, null, null, resultIv, A, resultTag) + + byte[] decryptionResult = alg.decrypt(dreq) + + assertArrayEquals(P, decryptionResult); + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes256CbcHmacSha512Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes256CbcHmacSha512Test.groovy new file mode 100644 index 000000000..900a2d755 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes256CbcHmacSha512Test.groovy @@ -0,0 +1,93 @@ +package io.jsonwebtoken.impl.security + + +import io.jsonwebtoken.security.AeadIvEncryptionResult +import io.jsonwebtoken.security.EncryptionAlgorithms +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec + +import static org.junit.Assert.assertArrayEquals +import static org.junit.Assert.assertTrue + +/** + * Test case defined in https://tools.ietf.org/html/rfc7518#appendix-B.3 + * @since JJWT_RELEASE_VERSION + */ +class Aes256CbcHmacSha512Test { + + final byte[] K = + [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f] as byte[] + final SecretKey KEY = new SecretKeySpec(K, "AES") + + final byte[] P = + [0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, + 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, + 0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69, + 0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f, + 0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65] as byte[] + + final byte[] IV = + [0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04] as byte[] + + final byte[] A = + [0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, + 0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, + 0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73] as byte[] + + final byte[] AL = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50] as byte[] + + final byte[] E = + [0x4a, 0xff, 0xaa, 0xad, 0xb7, 0x8c, 0x31, 0xc5, 0xda, 0x4b, 0x1b, 0x59, 0x0d, 0x10, 0xff, 0xbd, + 0x3d, 0xd8, 0xd5, 0xd3, 0x02, 0x42, 0x35, 0x26, 0x91, 0x2d, 0xa0, 0x37, 0xec, 0xbc, 0xc7, 0xbd, + 0x82, 0x2c, 0x30, 0x1d, 0xd6, 0x7c, 0x37, 0x3b, 0xcc, 0xb5, 0x84, 0xad, 0x3e, 0x92, 0x79, 0xc2, + 0xe6, 0xd1, 0x2a, 0x13, 0x74, 0xb7, 0x7f, 0x07, 0x75, 0x53, 0xdf, 0x82, 0x94, 0x10, 0x44, 0x6b, + 0x36, 0xeb, 0xd9, 0x70, 0x66, 0x29, 0x6a, 0xe6, 0x42, 0x7e, 0xa7, 0x5c, 0x2e, 0x08, 0x46, 0xa1, + 0x1a, 0x09, 0xcc, 0xf5, 0x37, 0x0d, 0xc8, 0x0b, 0xfe, 0xcb, 0xad, 0x28, 0xc7, 0x3f, 0x09, 0xb3, + 0xa3, 0xb7, 0x5e, 0x66, 0x2a, 0x25, 0x94, 0x41, 0x0a, 0xe4, 0x96, 0xb2, 0xe2, 0xe6, 0x60, 0x9e, + 0x31, 0xe6, 0xe0, 0x2c, 0xc8, 0x37, 0xf0, 0x53, 0xd2, 0x1f, 0x37, 0xff, 0x4f, 0x51, 0x95, 0x0b, + 0xbe, 0x26, 0x38, 0xd0, 0x9d, 0xd7, 0xa4, 0x93, 0x09, 0x30, 0x80, 0x6d, 0x07, 0x03, 0xb1, 0xf6] as byte[] + + final byte[] M = + [0x4d, 0xd3, 0xb4, 0xc0, 0x88, 0xa7, 0xf4, 0x5c, 0x21, 0x68, 0x39, 0x64, 0x5b, 0x20, 0x12, 0xbf, + 0x2e, 0x62, 0x69, 0xa8, 0xc5, 0x6a, 0x81, 0x6d, 0xbc, 0x1b, 0x26, 0x77, 0x61, 0x95, 0x5b, 0xc5, + 0xfd, 0x30, 0xa5, 0x65, 0xc6, 0x16, 0xff, 0xb2, 0xf3, 0x64, 0xba, 0xec, 0xe6, 0x8f, 0xc4, 0x07, + 0x53, 0xbc, 0xfc, 0x02, 0x5d, 0xde, 0x36, 0x93, 0x75, 0x4a, 0xa1, 0xf5, 0xc3, 0x37, 0x3b, 0x9c] as byte[] + + final byte[] T = + [0x4d, 0xd3, 0xb4, 0xc0, 0x88, 0xa7, 0xf4, 0x5c, 0x21, 0x68, 0x39, 0x64, 0x5b, 0x20, 0x12, 0xbf, + 0x2e, 0x62, 0x69, 0xa8, 0xc5, 0x6a, 0x81, 0x6d, 0xbc, 0x1b, 0x26, 0x77, 0x61, 0x95, 0x5b, 0xc5] as byte[] + + @Test + void test() { + + def alg = EncryptionAlgorithms.A256CBC_HS512 + + def req = new DefaultEncryptionRequest(P, KEY, null, null, IV, A) + + def r = alg.encrypt(req) + + assertTrue r instanceof AeadIvEncryptionResult + AeadIvEncryptionResult result = r as AeadIvEncryptionResult; + + byte[] resultCiphertext = result.getCiphertext() + byte[] resultTag = result.getAuthenticationTag(); + byte[] resultIv = result.getInitializationVector(); + + assertArrayEquals E, resultCiphertext + assertArrayEquals T, resultTag + assertArrayEquals IV, resultIv //shouldn't have been altered + + // now test decryption: + def dreq = new DefaultAeadIvRequest(resultCiphertext, KEY, null, null, resultIv, A, resultTag) + byte[] decryptionResult = alg.decrypt(dreq) + assertArrayEquals(P, decryptionResult); + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherAlgorithmTest.groovy new file mode 100644 index 000000000..e5fb6daab --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherAlgorithmTest.groovy @@ -0,0 +1,57 @@ +package io.jsonwebtoken.impl.security + +import org.junit.Test + +import javax.crypto.spec.SecretKeySpec +import java.security.Provider + +import static org.easymock.EasyMock.createMock +import static org.junit.Assert.* + +class CipherAlgorithmTest { + + @Test + void testNewCipherTemplateNullRequest() { + def alg = new TestCipherAlgorithm() + def template = alg.newCipherTemplate(null) + assertNull template.provider + assertEquals 'AES/CBC/PKCS5Padding', template.transformation + } + + @Test + void testNewCipherTemplate() { + + byte[] data = new byte[32] + Randoms.secureRandom().nextBytes(data) + byte[] keyBytes = new byte[32] + Randoms.secureRandom().nextBytes(keyBytes) + def key = new SecretKeySpec(keyBytes, 'AES') + + def alg = new TestCipherAlgorithm() + def template = alg.newCipherTemplate(new DefaultCryptoRequest(data, key, null, null)) + assertNull template.provider + assertEquals 'AES/CBC/PKCS5Padding', template.transformation + } + + @Test + void testNewCipherTemplateWithProvider() { + + Provider provider = createMock(Provider) + byte[] data = new byte[32] + Randoms.secureRandom().nextBytes(data) + byte[] keyBytes = new byte[32] + Randoms.secureRandom().nextBytes(keyBytes) + def key = new SecretKeySpec(keyBytes, 'AES') + + def alg = new TestCipherAlgorithm() + def template = alg.newCipherTemplate(new DefaultCryptoRequest(data, key, provider, null)) + assertSame provider, template.provider + assertEquals 'AES/CBC/PKCS5Padding', template.transformation + } + + static class TestCipherAlgorithm extends CipherAlgorithm { + def TestCipherAlgorithm() { + super('AES', 'AES/CBC/PKCS5Padding') + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherTemplateTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherTemplateTest.groovy new file mode 100644 index 000000000..93ea88d4b --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherTemplateTest.groovy @@ -0,0 +1,93 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.CryptoException +import org.junit.Test + +import javax.crypto.Cipher +import javax.crypto.NoSuchPaddingException +import java.security.NoSuchAlgorithmException +import java.security.Provider +import java.security.Security + +import static org.junit.Assert.* + +class CipherTemplateTest { + + @Test + void testNewCipherWithExplicitProvider() { + Provider provider = Security.getProvider('SunJCE') + def template = new CipherTemplate('AES/CBC/PKCS5Padding', provider) + template.execute(new CipherCallback() { + @Override + byte[] doWithCipher(Cipher cipher) throws Exception { + assertNotNull cipher + assertSame provider, cipher.provider + } + }) + } + + @Test + void testNewCipherFailedWithDefaultProvider() { + def ex = new IllegalStateException('testing') + def template = new CipherTemplate('AES/CBC/PKCS5Padding', null) { + @Override + Cipher getCipherInstance(String transformation, Provider provider) throws NoSuchPaddingException, NoSuchAlgorithmException { + throw ex + } + } + + try { + template.execute(new CipherCallback() { + @Override + byte[] doWithCipher(Cipher cipher) throws Exception { + return null + } + }) + } catch (CryptoException expected) { + assertEquals 'Unable to obtain cipher from default JCA Provider for transformation \'AES/CBC/PKCS5Padding\': testing', expected.getMessage() + assertSame ex, expected.getCause() + } + } + + @Test + void testNewCipherFailedWithExplicitProvider() { + def ex = new IllegalStateException('testing') + Provider provider = Security.getProvider('SunJCE') + def template = new CipherTemplate('AES/CBC/PKCS5Padding', provider) { + @Override + Cipher getCipherInstance(String transformation, Provider p) throws NoSuchPaddingException, NoSuchAlgorithmException { + throw ex + } + } + + try { + template.execute(new CipherCallback() { + @Override + byte[] doWithCipher(Cipher cipher) throws Exception { + return null + } + }) + } catch (CryptoException expected) { + assertTrue expected.getMessage().startsWith('Unable to obtain cipher from specified Provider {') + assertTrue expected.getMessage().endsWith('} for transformation \'AES/CBC/PKCS5Padding\': testing') + assertSame ex, expected.getCause() + } + } + + @Test + void testCallbackThrowsException() { + def ex = new Exception("testing") + def template = new CipherTemplate('AES/CBC/PKCS5Padding', null) + try { + template.execute(new CipherCallback() { + @Override + byte[] doWithCipher(Cipher cipher) throws Exception { + throw ex + } + }) + } catch (CryptoException e) { + assertEquals 'Cipher callback execution failed: testing', e.getMessage() + assertSame ex, e.getCause() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResultTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResultTest.groovy new file mode 100644 index 000000000..efa307cd2 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResultTest.groovy @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security + +import org.junit.Test + +import static org.junit.Assert.assertTrue + +/** + * @since JJWT_RELEASE_VERSION + */ +class DefaultAeadIvEncryptionResultTest { + + private static byte[] generateData() { + byte[] data = new byte[32]; + new Random().nextBytes(data) //does not need to be secure for this test + return data; + } + + @Test + void testCompactWithIv() { + + byte[] iv = generateData() + byte[] ciphertext = generateData() + byte[] tag = generateData() + + byte[] combined = new byte[iv.length + ciphertext.length + tag.length]; + System.arraycopy(iv, 0, combined, 0, iv.length) + System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length); + System.arraycopy(tag, 0, combined, iv.length + ciphertext.length, tag.length); + + def res = new DefaultAeadIvEncryptionResult(ciphertext, iv, tag) + byte[] compact = res.compact() + + assertTrue(Arrays.equals(combined, compact)) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultCryptoMessageTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultCryptoMessageTest.groovy new file mode 100644 index 000000000..e3ac5961f --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultCryptoMessageTest.groovy @@ -0,0 +1,16 @@ +package io.jsonwebtoken.impl.security + +import org.junit.Test + +class DefaultCryptoMessageTest { + + @Test(expected = IllegalArgumentException) + void testNullData() { + new DefaultCryptoMessage<>(null) + } + + @Test(expected = IllegalArgumentException) + void testEmptyByteArrayData() { + new DefaultCryptoMessage<>(new byte[0]) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocatorTest.groovy new file mode 100644 index 000000000..d128efb2f --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocatorTest.groovy @@ -0,0 +1,98 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.JweHeader +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.MalformedJwtException +import io.jsonwebtoken.UnsupportedJwtException +import io.jsonwebtoken.security.EncryptionAlgorithms +import org.junit.Before +import org.junit.Test +import static org.junit.Assert.* + +/** + * @since JJWT_RELEASE_VERSION + */ +class DefaultEncryptionAlgorithmLocatorTest { + + private DefaultEncryptionAlgorithmLocator locator + + @Before + void setUp() { + locator = new DefaultEncryptionAlgorithmLocator() + } + + private static JweHeader header(String enc) { + return Jwts.jweHeader().setEncryptionAlgorithm(enc) + } + + @Test + void testA128CBCHS256() { + assertSame EncryptionAlgorithms.A128CBC_HS256, locator.getEncryptionAlgorithm(header('A128CBC-HS256')) + } + + @Test + void testA192CBCHS384() { + assertSame EncryptionAlgorithms.A192CBC_HS384, locator.getEncryptionAlgorithm(header('A192CBC-HS384')) + } + + @Test + void testA256CBCHS512() { + assertSame EncryptionAlgorithms.A256CBC_HS512, locator.getEncryptionAlgorithm(header('A256CBC-HS512')) + } + + @Test + void testA128GCM() { + assertSame EncryptionAlgorithms.A128GCM, locator.getEncryptionAlgorithm(header('A128GCM')) + } + + @Test + void testA192GCM() { + assertSame EncryptionAlgorithms.A192GCM, locator.getEncryptionAlgorithm(header('A192GCM')) + } + + @Test + void testA256GCM() { + assertSame EncryptionAlgorithms.A256GCM, locator.getEncryptionAlgorithm(header('A256GCM')) + } + + @Test + void testMissingEncAlg() { + try { + locator.getEncryptionAlgorithm(Jwts.jweHeader()) + fail() + } catch (MalformedJwtException expected) { + } + } + + @Test + void testNullEncAlg() { + try { + locator.getEncryptionAlgorithm(header(null)) + fail() + } catch (MalformedJwtException expected) { + } + } + + @Test + void testEmptyEncAlg() { + try { + locator.getEncryptionAlgorithm(header(' ')) + fail() + } catch (MalformedJwtException expected) { + } + } + + @Test + void testUnknownEncAlg() { + try { + locator.getEncryptionAlgorithm(header('foo')) + fail() + } catch (UnsupportedJwtException e) { + assertEquals "JWE 'enc' header parameter value of 'foo' does not match a JWE standard algorithm " + + "identifier. If 'foo' represents a custom algorithm, the JwtParser must be configured " + + "with a custom EncryptionAlgorithmLocator instance that knows how to return a compatible " + + "EncryptionAlgorithm instance. Otherwise, this JWE is invalid and may not be used safely.", e.message + } + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultIvEncryptionResultTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultIvEncryptionResultTest.groovy new file mode 100644 index 000000000..1da967a29 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultIvEncryptionResultTest.groovy @@ -0,0 +1,38 @@ +package io.jsonwebtoken.impl.security + +import org.junit.Test + +import static org.junit.Assert.assertSame +import static org.junit.Assert.assertTrue + +/** + * @since JJWT_RELEASE_VERSION + */ +class DefaultIvEncryptionResultTest { + + private byte[] generateData() { + byte[] data = new byte[32]; + new Random().nextBytes(data) //does not need to be secure for this test + return data; + } + + @Test(expected=IllegalArgumentException) + void testCompactWithoutIv() { + def ciphertext = generateData() + new DefaultIvEncryptionResult(ciphertext, null) + } + + @Test + void testCompactWithIv() { + def ciphertext = generateData() + def iv = generateData() + + byte[] result = new DefaultIvEncryptionResult(ciphertext, iv).compact() + + byte[] combined = new byte[iv.length + ciphertext.length]; + System.arraycopy(iv, 0, combined, 0, iv.length); + System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length); + + assertTrue Arrays.equals(combined, result) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJweFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJweFactoryTest.groovy new file mode 100644 index 000000000..292513dc5 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJweFactoryTest.groovy @@ -0,0 +1,14 @@ +package io.jsonwebtoken.impl.security + +import org.junit.Test + +/** + * @since JJWT_RELEASE_VERSION + */ +class DefaultJweFactoryTest { + + @Test + void testDefaultCtor() { + new DefaultJweFactory() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkConverterTest.groovy new file mode 100644 index 000000000..d1d1d53a4 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkConverterTest.groovy @@ -0,0 +1,103 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.SignatureAlgorithms +import io.jsonwebtoken.security.UnsupportedKeyException +import org.junit.Ignore + +import java.security.KeyPair +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey + +import static org.junit.Assert.* + +import io.jsonwebtoken.io.Encoders +import org.junit.Test + +class DefaultJwkConverterTest { + + @Test + void testNullJwk() { + try { + new DefaultJwkConverter().toKey(null) + fail() + } catch (InvalidKeyException expected) { + assertEquals 'JWK map cannot be null or empty.', expected.message + } + } + + @Test + void testEmptyJwk() { + try { + new DefaultJwkConverter().toKey([:]) + fail() + } catch (InvalidKeyException expected) { + assertEquals 'JWK map cannot be null or empty.', expected.message + } + } + + @Test + void testUnknownKeyType() { + + def jwk = [ + 'kty': 'foo' + ] + + DefaultJwkConverter converter = new DefaultJwkConverter() + try { + converter.toKey(jwk) + fail() + } catch (UnsupportedKeyException e) { + assertEquals 'Unrecognized JWK kty (key type) value: foo', e.getMessage() + } + } + + @Test + void testEcKeyPairToKey() { + + def jwk = [ + 'kty': 'EC', + 'crv': 'P-256', + "x":"gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", + "y":"SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps", + "d":"0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo" + ] + + DefaultJwkConverter converter = new DefaultJwkConverter() + + def key = converter.toKey(jwk) + assertTrue key instanceof ECPrivateKey + key = key as ECPrivateKey + String d = EcJwkConverter.encodeCoordinate(key.params.curve.field.fieldSize, key.s) + assertEquals jwk.d, d + + //remove the 'd' mapping to represent only a public key: + jwk.remove('d') + + key = converter.toKey(jwk) + assertTrue key instanceof ECPublicKey + key = key as ECPublicKey + String x = EcJwkConverter.encodeCoordinate(key.params.curve.field.fieldSize, key.w.affineX) + String y = EcJwkConverter.encodeCoordinate(key.params.curve.field.fieldSize, key.w.affineY) + assertEquals jwk.x, x + assertEquals jwk.y, y + } + + @Test + @Ignore //TODO re-enable + void testEcKeyPairToJwk() { + + KeyPair pair = SignatureAlgorithms.ES256.generateKeyPair() + ECPublicKey pubKey = (ECPublicKey) pair.getPublic() + + DefaultJwkConverter converter = new DefaultJwkConverter() + + Map jwk = converter.toJwk(pubKey) + + assertNotNull jwk + assertEquals "EC", jwk.kty + assertEquals Encoders.BASE64URL.encode(pubKey.w.affineX.toByteArray()), jwk.x + assertEquals Encoders.BASE64URL.encode(pubKey.w.affineY.toByteArray()), jwk.y + assertNull jwk.d //public keys should not populate the private key 'd' parameter + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSymmetricJwkTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSymmetricJwkTest.groovy new file mode 100644 index 000000000..8719acbf6 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSymmetricJwkTest.groovy @@ -0,0 +1,43 @@ +package io.jsonwebtoken.impl.security + +import org.junit.Test +import static org.junit.Assert.* + +class DefaultSymmetricJwkTest { + + @Test + void testType() { + assertEquals 'oct', DefaultSymmetricJwk.TYPE_VALUE + assertEquals DefaultSymmetricJwk.TYPE_VALUE, new DefaultSymmetricJwk().getType() + } + + @Test + void testSetNullK() { + try { + new DefaultSymmetricJwk().setK(null) + fail() + } catch (IllegalArgumentException e) { + assertEquals "SymmetricJwk 'k' property cannot be null or empty.", e.getMessage() + } + } + + @Test + void testSetEmptyK() { + try { + new DefaultSymmetricJwk().setK(' ') + fail() + } catch (IllegalArgumentException e) { + assertEquals "SymmetricJwk 'k' property cannot be null or empty.", e.getMessage() + } + } + + @Test + void testK() { + def jwk = new DefaultSymmetricJwk() + assertEquals 'k', DefaultSymmetricJwk.K + String val = UUID.randomUUID().toString() + jwk.setK(val) + assertEquals val, jwk.get(DefaultSymmetricJwk.K) + assertEquals val, jwk.getK() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolverTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolverTest.groovy new file mode 100644 index 000000000..6af3c9012 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolverTest.groovy @@ -0,0 +1,16 @@ +package io.jsonwebtoken.impl.security + +import org.junit.Test + +import static org.junit.Assert.assertNull + +/** + * @since JJWT_RELEASE_VERSION + */ +class DisabledDecryptionKeyResolverTest { + + @Test + void test() { + assertNull DisabledDecryptionKeyResolver.INSTANCE.resolveDecryptionKey(null) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EllipticCurveSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EllipticCurveSignatureAlgorithmTest.groovy new file mode 100644 index 000000000..2d0072347 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EllipticCurveSignatureAlgorithmTest.groovy @@ -0,0 +1,224 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.JwtException +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.SignatureAlgorithms +import io.jsonwebtoken.security.WeakKeyException +import org.junit.Test + +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets +import java.security.* +import java.security.interfaces.ECPublicKey +import java.security.spec.X509EncodedKeySpec + +import static org.easymock.EasyMock.createMock +import static org.junit.Assert.* + +class EllipticCurveSignatureAlgorithmTest { + + @Test + void testConstructorWithWeakKeyLength() { + try { + new EllipticCurveSignatureAlgorithm('ES256', 'SHA256withECDSA', 'secp256r1', 128, 256) + } catch (IllegalArgumentException iae) { + assertEquals 'minKeyLength bits must be greater than the JWA mandatory minimum key length of 256', iae.getMessage() + } + } + + @Test(expected=IllegalStateException) + void testGenerateKeyPairInvalidCurveName() { + def alg = new EllipticCurveSignatureAlgorithm('ES256', 'SHA256withECDSA', 'notreal', 256, 256) + alg.generateKeyPair() + } + + @Test + void testValidateKeyEcKey() { + def request = new DefaultCryptoRequest(new byte[1], new SecretKeySpec(new byte[1], 'foo'), null, null) + try { + SignatureAlgorithms.ES256.sign(request) + } catch (InvalidKeyException e) { + assertTrue e.getMessage().contains("must be an ECKey") + } + } + + @Test + void testValidateSigningKeyNotPrivate() { + ECPublicKey key = createMock(ECPublicKey) + def request = new DefaultCryptoRequest(new byte[1], key, null, null) + try { + SignatureAlgorithms.ES256.sign(request) + } catch (InvalidKeyException e) { + assertTrue e.getMessage().startsWith("Asymmetric key signatures must be created with PrivateKeys. The specified key is of type: ") + } + } + + @Test + void testValidateSigningKeyWeakKey() { + def gen = KeyPairGenerator.getInstance("EC") + gen.initialize(192) //too week for any JWA EC algorithm + def pair = gen.generateKeyPair() + + def request = new DefaultCryptoRequest(new byte[1], pair.getPrivate(), null, null) + SignatureAlgorithms.values().findAll({it.getName().startsWith('ES')}).each { + try { + it.sign(request) + } catch (WeakKeyException expected) { + } + } + } + + @Test + void testVerifyWithPrivateKey() { + byte[] data = 'foo'.getBytes(StandardCharsets.UTF_8) + SignatureAlgorithms.values().findAll({it instanceof EllipticCurveSignatureAlgorithm}).each { + KeyPair pair = it.generateKeyPair() + def signRequest = new DefaultCryptoRequest(data, pair.getPrivate(), null, null) + byte[] signature = it.sign(signRequest) + def verifyRequest = new DefaultVerifySignatureRequest(data, pair.getPrivate(), null, null, signature) + try { + it.verify(verifyRequest) + } catch (InvalidKeyException e) { + assertEquals 'Elliptic Curve signature validation requires an ECPublicKey instance.', e.getMessage() + } + } + } + + @Test + void invalidDERSignatureToJoseFormatTest() { + def verify = { signature -> + try { + EllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + def signature = new byte[257] + Randoms.secureRandom().nextBytes(signature) + //invalid type + signature[0] = 34 + verify(signature) + def shortSignature = new byte[7] + Randoms.secureRandom().nextBytes(shortSignature) + verify(shortSignature) + signature[0] = 48 +// signature[1] = 0x81 + signature[1] = -10 + verify(signature) + } + + @Test + void edgeCaseSignatureToConcatInvalidSignatureTest() { + try { + def signature = Decoders.BASE64.decode("MIGBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + EllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + + @Test + void edgeCaseSignatureToConcatInvalidSignatureBranchTest() { + try { + def signature = Decoders.BASE64.decode("MIGBAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + EllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + + @Test + void edgeCaseSignatureToConcatInvalidSignatureBranch2Test() { + try { + def signature = Decoders.BASE64.decode("MIGBAj4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + EllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + + @Test + void edgeCaseSignatureToConcatLengthTest() { + try { + def signature = Decoders.BASE64.decode("MIEAAGg3OVb/ZeX12cYrhK3c07TsMKo7Kc6SiqW++4CAZWCX72DkZPGTdCv2duqlupsnZL53hiG3rfdOLj8drndCU+KHGrn5EotCATdMSLCXJSMMJoHMM/ZPG+QOHHPlOWnAvpC1v4lJb32WxMFNz1VAIWrl9Aa6RPG1GcjCTScKjvEE") + EllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException expected) { + + } + } + + @Test + void invalidECDSASignatureFormatTest() { + try { + def signature = new byte[257] + Randoms.secureRandom().nextBytes(signature) + EllipticCurveSignatureAlgorithm.transcodeSignatureToDER(signature) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + + @Test + void edgeCaseSignatureLengthTest() { + def signature = new byte[1] + EllipticCurveSignatureAlgorithm.transcodeSignatureToDER(signature) + } + + @Test + void testPaddedSignatureToDER() { + def signature = new byte[32] + Randoms.secureRandom().nextBytes(signature) + signature[0] = 0 as byte + EllipticCurveSignatureAlgorithm.transcodeSignatureToDER(signature) //no exception + } + + @Test + void ecdsaSignatureCompatTest() { + def fact = KeyFactory.getInstance("EC"); + def publicKey = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQASisgweVL1tAtIvfmpoqvdXF8sPKTV9YTKNxBwkdkm+/auh4pR8TbaIfsEzcsGUVv61DFNFXb0ozJfurQ59G2XcgAn3vROlSSnpbIvuhKrzL5jwWDTaYa5tVF1Zjwia/5HUhKBkcPuWGXg05nMjWhZfCuEetzMLoGcHmtvabugFrqsAg=" + def pub = fact.generatePublic(new X509EncodedKeySpec(Decoders.BASE64.decode(publicKey))) + def alg = SignatureAlgorithms.ES512 + def verifier = { token -> + def signatureStart = token.lastIndexOf('.') + def withoutSignature = token.substring(0, signatureStart) + def data = withoutSignature.getBytes("US-ASCII") + def signature = Decoders.BASE64URL.decode(token.substring(signatureStart + 1)) + assertTrue"Signature do not match that of other implementations", alg.verify(new DefaultVerifySignatureRequest(data, pub, null, null, signature)) + } + //Test verification for token created using https://github.com/auth0/node-jsonwebtoken/tree/v7.0.1 + verifier("eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30.Aab4x7HNRzetjgZ88AMGdYV2Ml7kzFbl8Ql2zXvBores7iRqm2nK6810ANpVo5okhHa82MQf2Q_Zn4tFyLDR9z4GAcKFdcAtopxq1h8X58qBWgNOc0Bn40SsgUc8wOX4rFohUCzEtnUREePsvc9EfXjjAH78WD2nq4tn-N94vf14SncQ") + //Test verification for token created using https://github.com/jwt/ruby-jwt/tree/v1.5.4 + verifier("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJ0ZXN0IjoidGVzdCJ9.AV26tERbSEwcoDGshneZmhokg-tAKUk0uQBoHBohveEd51D5f6EIs6cskkgwtfzs4qAGfx2rYxqQXr7LTXCNquKiAJNkTIKVddbPfped3_TQtmHZTmMNiqmWjiFj7Y9eTPMMRRu26w4gD1a8EQcBF-7UGgeH4L_1CwHJWAXGbtu7uMUn") + } + + @Test + void legacySignatureCompatTest() { + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def alg = SignatureAlgorithms.ES512 + def keypair = alg.generateKeyPair() + def signature = Signature.getInstance(alg.jcaName) + def data = withoutSignature.getBytes("US-ASCII") + signature.initSign(keypair.private) + signature.update(data) + def signed = signature.sign() + assertTrue alg.verify(new DefaultVerifySignatureRequest(data, keypair.public, null, null, signed)) + } + + @Test + void verifySwarmTest() { + SignatureAlgorithms.values().findAll({it.getName().startsWith('ES')}).each {alg -> + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def keypair = alg.generateKeyPair() + def data = withoutSignature.getBytes("US-ASCII") + def signature = alg.sign(new DefaultCryptoRequest(data, keypair.private, null, null)) + assertTrue alg.verify(new DefaultVerifySignatureRequest(data, keypair.public, null, null, signature)) + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesEncryptionServiceTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesEncryptionServiceTest.groovy new file mode 100644 index 000000000..79daaa0ea --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesEncryptionServiceTest.groovy @@ -0,0 +1,76 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.* +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec + +import static org.junit.Assert.* + +/** + * @since JJWT_RELEASE_VERSION + */ +class GcmAesEncryptionServiceTest { + + final byte[] K = + [0xb1, 0xa1, 0xf4, 0x80, 0x54, 0x8f, 0xe1, 0x73, 0x3f, 0xb4, 0x3, 0xff, 0x6b, 0x9a, 0xd4, 0xf6, + 0x8a, 0x7, 0x6e, 0x5b, 0x70, 0x2e, 0x22, 0x69, 0x2f, 0x82, 0xcb, 0x2e, 0x7a, 0xea, 0x40, 0xfc] as byte[] + final SecretKey KEY = new SecretKeySpec(K, "AES") + + final byte[] P = "The true sign of intelligence is not knowledge but imagination.".getBytes("UTF-8") + + final byte[] IV = [0xe3, 0xc5, 0x75, 0xfc, 0x2, 0xdb, 0xe9, 0x44, 0xb4, 0xe1, 0x4d, 0xdb] as byte[] + + final byte[] AAD = + [0x65, 0x79, 0x4a, 0x68, 0x62, 0x47, 0x63, 0x69, 0x4f, 0x69, 0x4a, 0x53, 0x55, 0x30, 0x45, 0x74, + 0x54, 0x30, 0x46, 0x46, 0x55, 0x43, 0x49, 0x73, 0x49, 0x6d, 0x56, 0x75, 0x59, 0x79, 0x49, 0x36, + 0x49, 0x6b, 0x45, 0x79, 0x4e, 0x54, 0x5a, 0x48, 0x51, 0x30, 0x30, 0x69, 0x66, 0x51] as byte[] + + final byte[] E = + [0xe5, 0xec, 0xa6, 0xf1, 0x35, 0xbf, 0x73, 0xc4, 0xae, 0x2b, 0x49, 0x6d, 0x27, 0x7a, 0xe9, 0x60, + 0x8c, 0xce, 0x78, 0x34, 0x33, 0xed, 0x30, 0xb, 0xbe, 0xdb, 0xba, 0x50, 0x6f, 0x68, 0x32, 0x8e, + 0x2f, 0xa7, 0x3b, 0x3d, 0xb5, 0x7f, 0xc4, 0x15, 0x28, 0x52, 0xf2, 0x20, 0x7b, 0x8f, 0xa8, 0xe2, + 0x49, 0xd8, 0xb0, 0x90, 0x8a, 0xf7, 0x6a, 0x3c, 0x10, 0xcd, 0xa0, 0x6d, 0x40, 0x3f, 0xc0] as byte[] + + final byte[] T = + [0x5c, 0x50, 0x68, 0x31, 0x85, 0x19, 0xa1, 0xd7, 0xad, 0x65, 0xdb, 0xd3, 0x88, 0x5b, 0xd2, 0x91] as byte[] + + /** + * Test that reflects https://tools.ietf.org/html/rfc7516#appendix-A.1 + */ + @Test + void testEncryptionAndDecryption() { + + def alg = EncryptionAlgorithms.A256GCM + + def req = new DefaultEncryptionRequest(P, KEY, null, null, IV, AAD) + + def r = alg.encrypt(req) + + assertTrue r instanceof AeadIvEncryptionResult + AeadEncryptionResult result = r as AeadIvEncryptionResult + + byte[] ciphertext = result.getCiphertext() + byte[] tag = result.getAuthenticationTag() + byte[] iv = result.getInitializationVector() + + assertArrayEquals E, ciphertext + assertArrayEquals T, tag + assertArrayEquals IV, iv //shouldn't have been altered + + // now test decryption: + def dreq = new DefaultAeadIvRequest(ciphertext, KEY, null, null, iv, AAD, tag) + byte[] decryptionResult = alg.decrypt(dreq) + assertArrayEquals(P, decryptionResult); + } + + @Test + void testInstantiationWithInvalidKeyLength() { + try { + new GcmAesEncryptionAlgorithm('A128GCM', 5); + fail() + } catch (IllegalArgumentException expected) { + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithmTest.groovy new file mode 100644 index 000000000..c08a5e475 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithmTest.groovy @@ -0,0 +1,83 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.AeadIvEncryptionResult +import io.jsonwebtoken.security.CryptoException +import io.jsonwebtoken.security.EncryptionAlgorithms +import io.jsonwebtoken.security.SignatureException +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec + +import static org.junit.Assert.assertEquals + +/** + * @since JJWT_RELEASE_VERSION + */ +class HmacAesEncryptionAlgorithmTest { + + @Test(expected = SignatureException) + void testDecryptWithInvalidTag() { + + def alg = EncryptionAlgorithms.A128CBC_HS256; + + SecretKey key = alg.generateKey() + + def plaintext = "Hello World! Nice to meet you!".getBytes("UTF-8") + + def req = new DefaultEncryptionRequest(plaintext, key, null, null, null, null) + def result = alg.encrypt(req); + assert result instanceof AeadIvEncryptionResult + + def realTag = result.getAuthenticationTag(); + + //fake it: + def fakeTag = new byte[realTag.length] + Randoms.secureRandom().nextBytes(fakeTag) + + def dreq = new DefaultAeadIvRequest(result.getCiphertext(), key, null, null, result.getInitializationVector(), null, fakeTag) + alg.decrypt(dreq) + } + + @Test(expected = CryptoException) + void testGenerateKeyWithWeakSigAlgKey() { + final byte[] bytes = new byte[24] // less than 32 bytes/256 bits + Randoms.secureRandom().nextBytes(bytes) + + def sigAlg = new MacSignatureAlgorithm('HS256', 'HmacSHA256', 256) { + @Override + SecretKey generateKey() { + return new SecretKeySpec(bytes, 'HmacSHA256') + } + } + def alg = new HmacAesEncryptionAlgorithm("A128CBC-HS256", sigAlg) + alg.generateKey() + } + + @Test + void testGenerateKeyWithLongerThanExpectedSigAlgKey() { + final byte[] macKeyBytes = new byte[64] // more than required 32 bytes / 256 bits + Randoms.secureRandom().nextBytes(macKeyBytes) + + def sigAlg = new MacSignatureAlgorithm('HS256', 'HmacSHA256', 256) { + @Override + SecretKey generateKey() { + return new SecretKeySpec(macKeyBytes, 'HmacSHA256') + } + } + def alg = new HmacAesEncryptionAlgorithm("A128CBC-HS256", sigAlg) + def key = alg.generateKey() + + def encryptionKeyBytes = key.getEncoded() + + assertEquals 512, encryptionKeyBytes.length * Byte.SIZE + + //per https://tools.ietf.org/html/rfc7518#section-5.2.2.1 ensure the first half of the generated encryption + // key is the first 32 bytes of the larger-than-expected mac key + byte[] macKeyFirst32Bytes = new byte[32] + byte[] encKeyFirst32Bytes = new byte[32] + System.arraycopy(macKeyBytes, 0, macKeyFirst32Bytes, 0, 32) + System.arraycopy(encryptionKeyBytes, 0, encKeyFirst32Bytes, 0, 32) + assert Arrays.equals(macKeyFirst32Bytes, encKeyFirst32Bytes) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy new file mode 100644 index 000000000..7b199b5ba --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -0,0 +1,47 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.CurveIds +import io.jsonwebtoken.security.Jwks +import org.junit.Test + +import static org.junit.Assert.* + +class JwksTest { + + @Test + void testBuilder() { + assertTrue Jwks.builder() instanceof DefaultJwkBuilderFactory + } + + @Test + void testBuilderSymmetric() { + assertTrue Jwks.builder().symmetric() instanceof DefaultSymmetricJwkBuilder + } + + @Test + void testBuilderEc() { + assertTrue Jwks.builder().ellipticCurve() instanceof DefaultEcJwkBuilderFactory + } + + @Test + void testBuilderEcPublicKey() { + assertTrue Jwks.builder().ellipticCurve().publicKey() instanceof DefaultPublicEcJwkBuilder + } + + @Test + void testBuilderEcPrivateKey() { + assertTrue Jwks.builder().ellipticCurve().privateKey() instanceof DefaultPrivateEcJwkBuilder + } + + @Test + void testSymmetric() { + println Jwks.builder().symmetric().setUse("signature").setId(UUID.randomUUID().toString()).setK("foo").build() + } + + @Test + void testFoo() { + println Jwks.builder().ellipticCurve().publicKey().setCurveId(CurveIds.P256).setX("xval").setY("yval").build() + println Jwks.builder().ellipticCurve().publicKey().setCurveId(CurveIds.P384).setX("x").setY("y").build() + println Jwks.builder().ellipticCurve().privateKey().setCurveId(CurveIds.P521).setX("x").setY("y").setD("d").build() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy new file mode 100644 index 000000000..43eef21e2 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy @@ -0,0 +1,128 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.SignatureAlgorithm +import io.jsonwebtoken.security.SignatureException +import io.jsonwebtoken.security.WeakKeyException +import org.junit.Test + +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import java.security.Key +import java.security.NoSuchAlgorithmException +import java.security.Provider +import java.security.Security + +import static org.easymock.EasyMock.createMock +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNotNull +import static org.junit.Assert.assertSame + +class MacSignatureAlgorithmTest { + + static MacSignatureAlgorithm newAlg() { + return new MacSignatureAlgorithm('HS256', 'HmacSHA256', 256) + } + + @Test(expected = UnsupportedOperationException) + void testKeyGeneratorNoSuchAlgorithm() { + MacSignatureAlgorithm alg = new MacSignatureAlgorithm('HS256', 'foo', 256); + alg.generateKey() + } + + @Test + void testDoGetMacInstanceWithProvider() { + Provider provider = Security.getProvider("SunJCE") + MacSignatureAlgorithm alg = newAlg() + assertNotNull alg.doGetMacInstance('HmacSHA256', provider) + } + + @Test + void testGetMacInstanceDefault() { + def expected = new NoSuchAlgorithmException('test') + MacSignatureAlgorithm alg = new MacSignatureAlgorithm('HS256', 'HmacSHA256', 256) { + @Override + def Mac doGetMacInstance(String jcaName, Provider provider) throws NoSuchAlgorithmException { + throw expected + } + } + try { + alg.sign(new DefaultCryptoRequest(new byte[1], new SecretKeySpec(new byte[32], 'HmacSHA256'), null, null)) + } catch (SignatureException e) { + assertEquals 'There is no JCA Provider available that supports MAC algorithm name \'HmacSHA256\'.', e.getMessage() + } + } + + @Test + void testGetMacInstanceWithProvider() { + Provider provider = createMock(Provider) + String providerString = provider.toString() + def expected = new NoSuchAlgorithmException('test') + MacSignatureAlgorithm alg = new MacSignatureAlgorithm('HS256', 'HmacSHA256', 256) { + @Override + def Mac doGetMacInstance(String jcaName, Provider p) throws NoSuchAlgorithmException { + throw expected + } + } + try { + alg.sign(new DefaultCryptoRequest(new byte[1], new SecretKeySpec(new byte[32], 'HmacSHA256'), provider, null)) + } catch (SignatureException e) { + assertEquals 'The specified JCA Provider {' + providerString + '} does not support MAC algorithm name \'HmacSHA256\'.', e.getMessage() + } + } + + @Test(expected = IllegalArgumentException) + void testValidateNullKey() { + newAlg().validateKey(null, true) + } + + @Test(expected = InvalidKeyException) + void testValidateKeyNoAlgorithm() { + newAlg().validateKey(new SecretKeySpec(new byte[1], ' '), true) + } + + @Test(expected = InvalidKeyException) + void testValidateKeyInvalidJcaAlgorithm() { + newAlg().validateKey(new SecretKeySpec(new byte[1], 'foo'), true) + } + + @Test + void testValidateKeyEncodedNotAvailable() { + def key = new SecretKeySpec(new byte[1], 'HmacSHA256') { + @Override + byte[] getEncoded() { + throw new UnsupportedOperationException("HSM: not allowed") + } + } + newAlg().validateKey(key, true) + } + + @Test + void testValidateKeyStandardAlgorithmWeakKey() { + byte[] bytes = new byte[24] + Randoms.secureRandom().nextBytes(bytes) + try { + newAlg().validateKey(new SecretKeySpec(bytes, 'HmacSHA256'), true) + } catch (WeakKeyException expected) { + String msg = 'The signing key\'s size is 192 bits which is not secure enough for the HS256 algorithm. ' + + 'The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HS256 MUST have a ' + + 'size >= 256 bits (the key size must be greater than or equal to the hash output size). ' + + 'Consider using the SignatureAlgorithms.HS256.generateKey() method to create a key guaranteed ' + + 'to be secure enough for HS256. See https://tools.ietf.org/html/rfc7518#section-3.2 for more ' + + 'information.' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testValidateKeyCustomAlgorithmWeakKey() { + byte[] bytes = new byte[24] + Randoms.secureRandom().nextBytes(bytes) + MacSignatureAlgorithm alg = new MacSignatureAlgorithm('foo', 'foo', 256); + try { + alg.validateKey(new SecretKeySpec(bytes, 'HmacSHA256'), true) + } catch (WeakKeyException expected) { + assertEquals 'The signing key\'s size is 192 bits which is not secure enough for the foo algorithm. The foo algorithm requires keys to have a size >= 256 bits.', expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy new file mode 100644 index 000000000..b3315d61b --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy @@ -0,0 +1,24 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.SignatureException +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class NoneSignatureAlgorithmTest { + + @Test + void testName() { + assertEquals "none", new NoneSignatureAlgorithm().getName(); + } + + @Test(expected = SignatureException) + void testSign() { + new NoneSignatureAlgorithm().sign(null) + } + + @Test(expected = SignatureException) + void testVerify() { + new NoneSignatureAlgorithm().verify(null) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateEcJwkValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateEcJwkValidatorTest.groovy new file mode 100644 index 000000000..33665fe27 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateEcJwkValidatorTest.groovy @@ -0,0 +1,31 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.CurveIds +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test + +class PrivateEcJwkValidatorTest { + + static PrivateEcJwkValidator validator() { + return new PrivateEcJwkValidator() + } + + @Test(expected = MalformedKeyException) + void testNullD() { + def jwk = new DefaultPrivateEcJwk().setCurveId(CurveIds.P521).setX('x').setY('y') + validator().validate(jwk) + } + + @Test(expected = MalformedKeyException) + void testEmptyD() { + def jwk = new DefaultPrivateEcJwk().setCurveId(CurveIds.P521).setX('x').setY('y') + jwk.put('d', ' ') + validator().validate(jwk) + } + + @Test + void testValid() { + def jwk = new DefaultPrivateEcJwk().setCurveId(CurveIds.P521).setX('x').setY('y').setD('d') + validator().validate(jwk) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RandomsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RandomsTest.groovy new file mode 100644 index 000000000..23d65ac1e --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RandomsTest.groovy @@ -0,0 +1,14 @@ +package io.jsonwebtoken.impl.security + +import org.junit.Test + +/** + * @since JJWT_RELEASE_VERSION + */ +class RandomsTest { + + @Test + void testPrivateCtor() { //for code coverage only + new Randoms() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaSignatureAlgorithmTest.groovy new file mode 100644 index 000000000..b4d69cacd --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaSignatureAlgorithmTest.groovy @@ -0,0 +1,86 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.SignatureAlgorithms +import io.jsonwebtoken.security.WeakKeyException +import org.junit.Test + +import javax.crypto.spec.SecretKeySpec +import java.security.InvalidParameterException +import java.security.Key +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.NoSuchAlgorithmException +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey + +import static org.easymock.EasyMock.createMock +import static org.junit.Assert.* + +class RsaSignatureAlgorithmTest { + + @Test + void testGenerateKeyPair() { + SignatureAlgorithms.values().findAll({it.name.startsWith("RS") || it.name.startsWith("PS")}).each { + KeyPair pair = it.generateKeyPair() + assertNotNull pair.public + assertTrue pair.public instanceof RSAPublicKey + assertEquals it.preferredKeyLength, pair.public.modulus.bitLength() + assertTrue pair.private instanceof RSAPrivateKey + assertEquals it.preferredKeyLength, pair.private.modulus.bitLength() + } + } + + @Test(expected = IllegalStateException) + void testGenerateKeyGeneratorException() { + def src = SignatureAlgorithms.RS256 + def alg = new RsaSignatureAlgorithm(src.name, src.jcaName, src.preferredKeyLength) { + @Override + protected KeyPairGenerator getKeyPairGenerator() throws NoSuchAlgorithmException, InvalidParameterException { + throw new NoSuchAlgorithmException("testing") + } + } + alg.generateKeyPair() + } + + @Test(expected = IllegalArgumentException) + void testWeakPreferredKeyLength() { + new RsaSignatureAlgorithm('RS256', 'SHA256withRSA', 1024) //must be >= 2048 + } + + @Test + void testValidateKeyRsaKey() { + def request = new DefaultCryptoRequest(new byte[1], new SecretKeySpec(new byte[1], 'foo'), null, null) + try { + SignatureAlgorithms.RS256.sign(request) + } catch (InvalidKeyException e) { + assertTrue e.getMessage().contains("must be an RSAKey") + } + } + + @Test + void testValidateSigningKeyNotPrivate() { + RSAPublicKey key = createMock(RSAPublicKey) + def request = new DefaultCryptoRequest(new byte[1], key, null, null) + try { + SignatureAlgorithms.RS256.sign(request) + } catch (InvalidKeyException e) { + assertTrue e.getMessage().startsWith("Asymmetric key signatures must be created with PrivateKeys. The specified key is of type: ") + } + } + + @Test + void testValidateSigningKeyWeakKey() { + def gen = KeyPairGenerator.getInstance("RSA") + gen.initialize(1024) //too week for any JWA RSA algorithm + def pair = gen.generateKeyPair() + + def request = new DefaultCryptoRequest(new byte[1], pair.getPrivate(), null, null) + SignatureAlgorithms.values().findAll({it.name.startsWith('RS') || it.name.startsWith('PS')}).each { + try { + it.sign(request) + } catch (WeakKeyException expected) { + } + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/SymmetricJwkValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/SymmetricJwkValidatorTest.groovy new file mode 100644 index 000000000..c7d71cc9f --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/SymmetricJwkValidatorTest.groovy @@ -0,0 +1,31 @@ +package io.jsonwebtoken.impl.security + + +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test + +class SymmetricJwkValidatorTest { + + static SymmetricJwkValidator validator() { + return new SymmetricJwkValidator() + } + + @Test(expected = MalformedKeyException) + void testNullK() { + def jwk = new DefaultSymmetricJwk() + validator().validate(jwk) + } + + @Test(expected = MalformedKeyException) + void testEmptyK() { + def jwk = new DefaultSymmetricJwk() + jwk.put('k', ' ') + validator().validate(jwk) + } + + @Test + void testValid() { + def jwk = new DefaultSymmetricJwk().setK('k') + validator().validate(jwk) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwk.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwk.groovy new file mode 100644 index 000000000..d118dc743 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwk.groovy @@ -0,0 +1,7 @@ +package io.jsonwebtoken.impl.security + +class TestJwk extends AbstractJwk { + def TestJwk() { + super("test") + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwkValidator.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwkValidator.groovy new file mode 100644 index 000000000..7eba2544c --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwkValidator.groovy @@ -0,0 +1,18 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.Jwk +import io.jsonwebtoken.security.KeyException + +class TestJwkValidator extends AbstractJwkValidator { + + T jwk; + + def TestJwkValidator(String kty="test") { + super(kty) + } + + @Override + void validateJwk(T jwk) throws KeyException { + this.jwk = jwk; + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy new file mode 100644 index 000000000..6d5c1ca59 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy @@ -0,0 +1,102 @@ +package io.jsonwebtoken.security + +import io.jsonwebtoken.impl.security.DefaultAeadIvRequest +import io.jsonwebtoken.impl.security.DefaultAesEncryptionRequest +import io.jsonwebtoken.impl.security.DefaultEncryptionRequest +import io.jsonwebtoken.impl.security.GcmAesEncryptionAlgorithm +import org.junit.Test + +import static org.junit.Assert.* + +/** + * @since JJWT_RELEASE_VERSION + */ +class EncryptionAlgorithmsTest { + + private static final String PLAINTEXT = + '''Bacon ipsum dolor amet venison beef pork chop, doner jowl pastrami ground round alcatra. + Beef leberkas filet mignon ball tip pork spare ribs kevin short loin ribeye ground round + biltong jerky short ribs corned beef. Strip steak turducken meatball porchetta beef ribs + shoulder pork belly doner salami corned beef kielbasa cow filet mignon drumstick. Bacon + tenderloin pancetta flank frankfurter ham kevin leberkas meatball turducken beef ribs. + Cupim short loin short ribs shankle tenderloin. Ham ribeye hamburger flank tenderloin + cupim t-bone, shank tri-tip venison salami sausage pancetta. Pork belly chuck salami + alcatra sirloin. + + 以ケ ホゥ婧詃 橎ちゅぬ蛣埣 禧ざしゃ蟨廩 椥䤥グ曣わ 基覧 滯っ䶧きょメ Ủ䧞以ケ妣 择禤槜谣お 姨のドゥ, + らボみょば䪩 苯礊觊ツュ婃 䩦ディふげセ げセりょ 禤槜 Ủ䧞以ケ妣 せがみゅちょ䰯 择禤槜谣お 難ゞ滧 蝥ちゃ, + 滯っ䶧きょメ らボみょば䪩 礯みゃ楦と饥 椥䤥グ ウァ槚 訤をりゃしゑ びゃ驨も氩簥 栨キョ奎婨榞 ヌに楃 以ケ, + 姚奊べ 椥䤥グ曣わ 栨キョ奎婨榞 ちょ䰯 Ủ䧞以ケ妣 誧姨のドゥろ よ苯礊 く涥, りゅぽ槞 馣ぢゃ尦䦎ぎ + 大た䏩䰥ぐ 郎きや楺橯 䧎キェ, 難ゞ滧 栧择 谯䧟簨訧ぎょ 椥䤥グ曣わ''' + + private static final byte[] PLAINTEXT_BYTES = PLAINTEXT.getBytes("UTF-8") + + private static final String AAD = 'You can get with this, or you can get with that' + private static final byte[] AAD_BYTES = AAD.getBytes("UTF-8") + + @Test + void testPrivateCtor() { //for code coverage only + new EncryptionAlgorithms() + } + + @Test + void testWithoutAad() { + + for (EncryptionAlgorithm alg : EncryptionAlgorithms.symmetric()) { + + assert alg instanceof AeadSymmetricEncryptionAlgorithm + + def key = alg.generateKey() + + def request = new DefaultAesEncryptionRequest(PLAINTEXT_BYTES, key, null) + + def result = alg.encrypt(request) + assert result instanceof AeadIvEncryptionResult + + byte[] tag = result.getAuthenticationTag() //there is always a tag, even if there is no AAD + assertNotNull tag + + byte[] ciphertext = result.getCiphertext() + + boolean gcm = alg instanceof GcmAesEncryptionAlgorithm + + if (gcm) { //AES GCM always results in ciphertext the same length as the plaintext: + assertEquals(ciphertext.length, PLAINTEXT_BYTES.length) + } + + def dreq = new DefaultAeadIvRequest(result.getCiphertext(), key, null, null, result.getInitializationVector(), null, tag) + + byte[] decryptedPlaintextBytes = alg.decrypt(dreq) + + assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes); + } + } + + @Test + void testWithAad() { + + for (EncryptionAlgorithm alg : EncryptionAlgorithms.symmetric()) { + + assert alg instanceof AeadSymmetricEncryptionAlgorithm + + def key = alg.generateKey() + + def req = new DefaultEncryptionRequest(PLAINTEXT_BYTES, key, null, null, null, AAD_BYTES) + + def result = alg.encrypt(req) + assert result instanceof AeadIvEncryptionResult + + byte[] ciphertext = result.getCiphertext() + + boolean gcm = alg instanceof GcmAesEncryptionAlgorithm + + if (gcm) { + assertEquals(ciphertext.length, PLAINTEXT_BYTES.length) + } + + def dreq = new DefaultAeadIvRequest(result.getCiphertext(), key, null, null, result.getInitializationVector(), AAD_BYTES, result.getAuthenticationTag()) + byte[] decryptedPlaintextBytes = alg.decrypt(dreq) + assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes) + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy index 80e571bd8..8e86fc2b3 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy @@ -15,7 +15,8 @@ */ package io.jsonwebtoken.security -import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.impl.security.EllipticCurveSignatureAlgorithm +import io.jsonwebtoken.impl.security.RsaSignatureAlgorithm import org.junit.Test import javax.crypto.SecretKey @@ -37,9 +38,10 @@ class KeysImplTest { } @Test - void testSecretKeyFor() { + @Deprecated + void testDeprecatedSecretKeyFor() { - for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { + for (io.jsonwebtoken.SignatureAlgorithm alg : io.jsonwebtoken.SignatureAlgorithm.values()) { String name = alg.name() @@ -49,7 +51,7 @@ class KeysImplTest { assertEquals alg.jcaName, key.algorithm alg.assertValidSigningKey(key) alg.assertValidVerificationKey(key) - assertEquals alg, SignatureAlgorithm.forSigningKey(key) // https://github.com/jwtk/jjwt/issues/381 + assertEquals alg, io.jsonwebtoken.SignatureAlgorithm.forSigningKey(key) // https://github.com/jwtk/jjwt/issues/381 } else { try { Keys.secretKeyFor(alg) @@ -63,9 +65,22 @@ class KeysImplTest { } @Test - void testKeyPairFor() { + void testSecretKeyFor() { + for (SignatureAlgorithm alg : SignatureAlgorithms.values()) { + if (alg instanceof SymmetricKeySignatureAlgorithm) { + SecretKey key = alg.generateKey() + assertEquals alg.minKeyLength, key.getEncoded().length * 8 //convert byte count to bit count + assertEquals alg.jcaName, key.algorithm + assertEquals alg, SignatureAlgorithms.forSigningKey(key) // https://github.com/jwtk/jjwt/issues/381 + } + } + } + + @Test + @Deprecated + void testDeprecatedKeyPairFor() { - for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { + for (io.jsonwebtoken.SignatureAlgorithm alg : io.jsonwebtoken.SignatureAlgorithm.values()) { String name = alg.name() @@ -116,4 +131,50 @@ class KeysImplTest { } } } + + @Test + void testKeyPairFor() { + + for (SignatureAlgorithm alg : SignatureAlgorithms.values()) { + + if (alg instanceof RsaSignatureAlgorithm) { + + KeyPair pair = alg.generateKeyPair() + assertNotNull pair + + PublicKey pub = pair.getPublic() + assert pub instanceof RSAPublicKey + assertEquals alg.preferredKeyLength, pub.modulus.bitLength() + + PrivateKey priv = pair.getPrivate() + assert priv instanceof RSAPrivateKey + assertEquals alg.preferredKeyLength, priv.modulus.bitLength() + + } else if (alg instanceof EllipticCurveSignatureAlgorithm) { + + KeyPair pair = alg.generateKeyPair() + assertNotNull pair + + int len = alg.minKeyLength + String asn1oid = "secp${len}r1" + String suffix = len == 256 ? ", X9.62 prime${len}v1" : '' //the JDK only adds this extra suffix to the secp256r1 curve name and not secp384r1 or secp521r1 curve names + String jdkParamName = "$asn1oid [NIST P-${len}${suffix}]" as String + + PublicKey pub = pair.getPublic() + assert pub instanceof ECPublicKey + assertEquals "EC", pub.algorithm + assertEquals jdkParamName, pub.params.name + assertEquals alg.minKeyLength, pub.params.order.bitLength() + + PrivateKey priv = pair.getPrivate() + assert priv instanceof ECPrivateKey + assertEquals "EC", priv.algorithm + assertEquals jdkParamName, priv.params.name + assertEquals alg.minKeyLength, priv.params.order.bitLength() + + } else { + assertFalse alg instanceof AsymmetricKeySignatureAlgorithm //assert we've accounted for all asymmetric ones above + } + } + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy new file mode 100644 index 000000000..c01045953 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy @@ -0,0 +1,19 @@ +package io.jsonwebtoken.security + +import static org.junit.Assert.* +import org.junit.Test + +class SignatureAlgorithmsTest { + + @Test + void testPrivateCtor() { + new SignatureAlgorithms() // for code coverage only + } + + @Test + void testForNameCaseInsensitive() { + for(SignatureAlgorithm alg : SignatureAlgorithms.STANDARD_ALGORITHMS.values()) { + assertSame alg, SignatureAlgorithms.forName(alg.getName().toLowerCase()) + } + } +} diff --git a/pom.xml b/pom.xml index cd244dd80..0135e563b 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ io.jsonwebtoken jjwt-root - 0.11.3-SNAPSHOT + 0.12.0-SNAPSHOT JJWT JSON Web Token support for the JVM and Android pom @@ -63,7 +63,7 @@ ${basedir} - 0.10.7 + 0.11.2 3.0.2 3.8.0 @@ -86,7 +86,7 @@ 1.2.3 3.6 4.12 - 2.0.0-beta.5 + 2.0.7 2.22.0 2.22.0 4.2.1 @@ -272,9 +272,18 @@ true - true + - + true + + + @@ -540,7 +549,7 @@ -Xdoclint:none 1.8.1 4.2 - 2.0.2 + 2.0.7 From f5ab0648f0eec462ccdb573f567bfa43059ac09f Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Tue, 18 Aug 2020 13:56:35 -0400 Subject: [PATCH 02/75] IF SQUASHING, DO NOT SQUASH THIS COMMIT UNTIL MERGING TO MASTER: Removed the previous SignatureAlgorithm implementation concepts (Provider/Signer/Validator implementations). Implementations are now interface-driven and fully pluggable. --- api/src/main/java/io/jsonwebtoken/Claims.java | 14 +- .../java/io/jsonwebtoken/ClaimsMutator.java | 2 +- api/src/main/java/io/jsonwebtoken/Header.java | 29 +- .../java/io/jsonwebtoken/Identifiable.java | 29 + api/src/main/java/io/jsonwebtoken/Jwe.java | 15 + .../main/java/io/jsonwebtoken/JweBuilder.java | 34 + .../main/java/io/jsonwebtoken/JweHeader.java | 37 +- .../main/java/io/jsonwebtoken/JwsHeader.java | 18 +- api/src/main/java/io/jsonwebtoken/Jwt.java | 2 +- .../main/java/io/jsonwebtoken/JwtBuilder.java | 56 +- .../main/java/io/jsonwebtoken/JwtHandler.java | 14 +- .../io/jsonwebtoken/JwtHandlerAdapter.java | 18 +- .../main/java/io/jsonwebtoken/JwtParser.java | 43 +- .../io/jsonwebtoken/JwtParserBuilder.java | 116 ++- api/src/main/java/io/jsonwebtoken/Jwts.java | 18 +- .../main/java/io/jsonwebtoken/Locator.java | 24 + .../java/io/jsonwebtoken/LocatorAdapter.java | 48 ++ api/src/main/java/io/jsonwebtoken/Named.java | 14 - .../io/jsonwebtoken/SignatureException.java | 6 +- .../io/jsonwebtoken/SigningKeyResolver.java | 4 +- .../SigningKeyResolverAdapter.java | 40 +- .../java/io/jsonwebtoken/lang/Arrays.java | 10 + .../java/io/jsonwebtoken/lang/Assert.java | 19 +- .../java/io/jsonwebtoken/lang/Classes.java | 19 +- .../io/jsonwebtoken/lang/Collections.java | 18 + .../java/io/jsonwebtoken/lang/Objects.java | 29 +- .../java/io/jsonwebtoken/lang/Strings.java | 31 +- .../jsonwebtoken/security/AeadAlgorithm.java | 28 + .../security/AeadEncryptionAlgorithm.java | 9 - .../security/AeadIvEncryptionResult.java | 7 - .../jsonwebtoken/security/AeadIvRequest.java | 9 - .../io/jsonwebtoken/security/AeadRequest.java | 7 +- .../io/jsonwebtoken/security/AeadResult.java | 22 + .../AeadSymmetricEncryptionAlgorithm.java | 11 - ...ource.java => AssociatedDataSupplier.java} | 4 +- .../jsonwebtoken/security/AsymmetricJwk.java | 37 + .../security/AsymmetricJwkBuilder.java | 39 + .../security/AsymmetricKeyAlgorithm.java | 16 - .../security/AsymmetricKeyGenerator.java | 31 + .../AsymmetricKeySignatureAlgorithm.java | 20 +- .../jsonwebtoken/security/CryptoMessage.java | 10 - .../jsonwebtoken/security/CryptoRequest.java | 44 +- .../io/jsonwebtoken/security/CurveId.java | 9 - .../io/jsonwebtoken/security/CurveIds.java | 44 -- ...ionResult.java => DecryptAeadRequest.java} | 5 +- ...Request.java => DecryptionKeyRequest.java} | 5 +- .../security/DecryptionKeyResolver.java | 19 - .../jsonwebtoken/security/DefaultCurveId.java | 33 - ...ryptionResult.java => DigestSupplier.java} | 6 +- .../java/io/jsonwebtoken/security/EcJwk.java | 13 - .../jsonwebtoken/security/EcJwkBuilder.java | 7 - .../security/EcJwkBuilderFactory.java | 11 - .../jsonwebtoken/security/EcJwkMutator.java | 13 - .../jsonwebtoken/security/EcKeyAlgorithm.java | 26 + .../jsonwebtoken/security/EcPrivateJwk.java | 25 + .../security/EcPrivateJwkBuilder.java | 25 + .../io/jsonwebtoken/security/EcPublicJwk.java | 24 + .../security/EcPublicJwkBuilder.java | 25 + .../EllipticCurveSignatureAlgorithm.java | 26 + .../security/EncryptionAlgorithm.java | 15 - .../security/EncryptionAlgorithmLocator.java | 11 - .../security/EncryptionAlgorithmName.java | 75 -- .../security/EncryptionAlgorithms.java | 88 +-- .../security/InitializationVectorSource.java | 16 - .../InitializationVectorSupplier.java | 31 + .../security/IvEncryptionResult.java | 9 - .../io/jsonwebtoken/security/IvRequest.java | 9 - .../java/io/jsonwebtoken/security/Jwk.java | 38 +- .../io/jsonwebtoken/security/JwkBuilder.java | 43 +- .../security/JwkBuilderFactory.java | 12 - .../io/jsonwebtoken/security/JwkMutator.java | 27 - .../security/JwkRsaPrimeInfo.java | 16 - .../security/JwkRsaPrimeInfoBuilder.java | 9 - .../security/JwkRsaPrimeInfoMutator.java | 13 - .../java/io/jsonwebtoken/security/Jwks.java | 22 +- .../jsonwebtoken/security/KeyAlgorithm.java | 39 + .../jsonwebtoken/security/KeyAlgorithms.java | 83 ++ .../security/KeyManagementAlgorithmName.java | 106 --- .../security/KeyManagementModeName.java | 42 - .../io/jsonwebtoken/security/KeyRequest.java | 30 + .../io/jsonwebtoken/security/KeyResult.java | 24 + .../io/jsonwebtoken/security/KeySupplier.java | 31 + .../java/io/jsonwebtoken/security/Keys.java | 42 +- .../security/MalformedKeyException.java | 15 + .../security/PayloadSupplier.java | 25 + .../java/io/jsonwebtoken/security/PbeKey.java | 41 + .../jsonwebtoken/security/PbeKeyBuilder.java | 52 ++ .../jsonwebtoken/security/PrivateEcJwk.java | 9 - .../security/PrivateEcJwkBuilder.java | 9 - .../security/PrivateEcJwkMutator.java | 9 - .../io/jsonwebtoken/security/PrivateJwk.java | 31 + .../security/PrivateJwkBuilder.java | 29 + .../jsonwebtoken/security/PrivateRsaJwk.java | 23 - .../security/PrivateRsaJwkMutator.java | 23 - .../security/ProtoJwkBuilder.java | 44 ++ .../io/jsonwebtoken/security/PublicEcJwk.java | 7 - .../security/PublicEcJwkBuilder.java | 7 - .../io/jsonwebtoken/security/PublicJwk.java | 24 + .../security/PublicJwkBuilder.java | 27 + .../jsonwebtoken/security/PublicRsaJwk.java | 7 - .../java/io/jsonwebtoken/security/RsaJwk.java | 11 - .../jsonwebtoken/security/RsaJwkMutator.java | 11 - .../security/RsaKeyAlgorithm.java | 26 + .../jsonwebtoken/security/RsaPrivateJwk.java | 25 + .../security/RsaPrivateJwkBuilder.java | 25 + .../jsonwebtoken/security/RsaPublicJwk.java | 24 + ...xception.java => RsaPublicJwkBuilder.java} | 14 +- .../security/RsaSignatureAlgorithm.java | 26 + ...nticationTagSource.java => SecretJwk.java} | 9 +- .../security/SecretJwkBuilder.java | 24 + .../security/SecretKeyGenerator.java | 31 + .../security/SecretKeySignatureAlgorithm.java | 24 + .../security/SecurityRequest.java | 43 + .../security/SignatureAlgorithm.java | 23 +- .../security/SignatureAlgorithms.java | 123 ++- .../security/SignatureRequest.java | 24 + .../SymmetricEncryptionAlgorithm.java | 9 - .../jsonwebtoken/security/SymmetricJwk.java | 9 - .../security/SymmetricJwkBuilder.java | 7 - .../security/SymmetricJwkMutator.java | 9 - .../security/SymmetricKeyAlgorithm.java | 16 - .../SymmetricKeySignatureAlgorithm.java | 7 - .../security/UnsupportedKeyException.java | 15 + .../security/VerifySignatureRequest.java | 19 +- .../EncryptionAlgorithmNameTest.groovy | 61 -- .../jsonwebtoken/JwtHandlerAdapterTest.groovy | 8 +- .../io/jsonwebtoken/lang/ArraysTest.groovy | 2 +- .../jsonwebtoken/security/CurveIdsTest.groovy | 24 - .../KeyManagementAlgorithmNameTest.groovy | 54 -- .../security/KeyManagementModeNameTest.groovy | 19 - .../io/jsonwebtoken/security/KeysTest.groovy | 12 +- .../impl/CompressionCodecLocator.java | 21 + .../io/jsonwebtoken/impl/DefaultHeader.java | 17 +- .../java/io/jsonwebtoken/impl/DefaultJwe.java | 44 ++ .../jsonwebtoken/impl/DefaultJweBuilder.java | 181 +++++ .../jsonwebtoken/impl/DefaultJweHeader.java | 2 +- .../java/io/jsonwebtoken/impl/DefaultJws.java | 29 +- .../jsonwebtoken/impl/DefaultJwsHeader.java | 2 +- .../java/io/jsonwebtoken/impl/DefaultJwt.java | 32 +- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 230 +++--- .../jsonwebtoken/impl/DefaultJwtParser.java | 743 ++++++++---------- .../impl/DefaultJwtParserBuilder.java | 138 +++- .../impl/DefaultTokenizedJwe.java | 9 + .../impl/DefaultTokenizedJwt.java | 14 + .../jsonwebtoken/impl/DispatchingParser.java | 4 +- .../java/io/jsonwebtoken/impl/IdLocator.java | 62 ++ .../java/io/jsonwebtoken/impl/IdRegistry.java | 47 ++ .../jsonwebtoken/impl/ImmutableJwtParser.java | 20 +- .../java/io/jsonwebtoken/impl/JwtMap.java | 7 +- .../io/jsonwebtoken/impl/JwtTokenizer.java | 25 +- .../io/jsonwebtoken/impl/TokenizedJwt.java | 7 + .../crypto/DefaultJwtSignatureValidator.java | 63 -- .../impl/crypto/DefaultJwtSigner.java | 63 -- .../DefaultSignatureValidatorFactory.java | 52 -- .../impl/crypto/DefaultSignerFactory.java | 52 -- .../impl/crypto/EllipticCurveProvider.java | 205 ----- .../EllipticCurveSignatureValidator.java | 67 -- .../impl/crypto/EllipticCurveSigner.java | 60 -- .../impl/crypto/JwtSignatureValidator.java | 21 - .../jsonwebtoken/impl/crypto/JwtSigner.java | 21 - .../jsonwebtoken/impl/crypto/MacProvider.java | 101 --- .../jsonwebtoken/impl/crypto/MacSigner.java | 68 -- .../impl/crypto/MacValidator.java | 36 - .../jsonwebtoken/impl/crypto/RsaProvider.java | 197 ----- .../jsonwebtoken/impl/crypto/RsaSigner.java | 59 -- .../impl/crypto/SignatureProvider.java | 66 -- .../impl/crypto/SignatureValidator.java | 22 - .../crypto/SignatureValidatorFactory.java | 25 - .../io/jsonwebtoken/impl/crypto/Signer.java | 23 - .../impl/crypto/SignerFactory.java | 25 - .../jsonwebtoken/impl/io/CodecConverter.java | 32 + .../java/io/jsonwebtoken/impl/io/Codecs.java | 10 + .../io/jsonwebtoken/impl/lang/BiFunction.java | 6 + .../java/io/jsonwebtoken/impl/lang/Bytes.java | 112 +++ .../impl/lang/CheckedFunction.java | 5 + .../impl/lang/CollectionConverter.java | 89 +++ .../impl/lang/ConstantFunction.java | 29 + .../io/jsonwebtoken/impl/lang/Converter.java | 8 + .../io/jsonwebtoken/impl/lang/Converters.java | 31 + .../impl/lang/EncodedObjectConverter.java | 33 + .../io/jsonwebtoken/impl/lang/Function.java | 6 + .../impl/lang/LocatorFunction.java | 19 + .../jsonwebtoken/impl/lang/NoConverter.java | 28 + .../impl/lang/NullSafeConverter.java | 22 + .../lang/PropagatingExceptionFunction.java | 35 + .../io/jsonwebtoken/impl/lang/Registry.java | 8 + .../impl/lang/UriStringConverter.java | 21 + .../jsonwebtoken/impl/lang/ValueGetter.java | 18 + .../AbstractAeadAesEncryptionAlgorithm.java | 112 --- .../impl/security/AbstractAsymmetricJwk.java | 47 ++ .../AbstractAsymmetricJwkBuilder.java | 249 ++++++ .../impl/security/AbstractEcJwk.java | 65 -- .../impl/security/AbstractEcJwkBuilder.java | 31 - .../impl/security/AbstractEcJwkFactory.java | 221 ++++++ .../impl/security/AbstractEcJwkValidator.java | 39 - .../security/AbstractEncryptionAlgorithm.java | 48 -- .../security/AbstractFamilyJwkFactory.java | 114 +++ .../impl/security/AbstractJwk.java | 183 ++--- .../impl/security/AbstractJwkBuilder.java | 99 ++- .../impl/security/AbstractJwkConverter.java | 86 -- .../impl/security/AbstractJwkValidator.java | 40 - .../impl/security/AbstractPrivateJwk.java | 34 + .../impl/security/AbstractPublicJwk.java | 11 + .../impl/security/AbstractRsaJwk.java | 35 - .../security/AbstractRsaJwkValidator.java | 16 - .../security/AbstractSignatureAlgorithm.java | 97 +-- .../security/AbstractTypedJwkConverter.java | 33 - .../impl/security/AesAlgorithm.java | 144 ++++ .../impl/security/AesGcmKeyAlgorithm.java | 90 +++ .../impl/security/AesWrapKeyAlgorithm.java | 62 ++ .../impl/security/AsymmetricJwkFactory.java | 39 + .../impl/security/CipherAlgorithm.java | 19 - .../impl/security/CipherCallback.java | 8 - .../impl/security/CipherTemplate.java | 54 -- .../jsonwebtoken/impl/security/ConcatKDF.java | 137 ++++ .../impl/security/ConstantKeyLocator.java | 48 ++ .../impl/security/CryptoAlgorithm.java | 61 +- .../DefaultAeadIvEncryptionResult.java | 46 -- .../impl/security/DefaultAeadIvRequest.java | 50 -- .../impl/security/DefaultAeadRequest.java | 39 + .../impl/security/DefaultAeadResult.java | 25 + .../security/DefaultAesEncryptionRequest.java | 26 - .../impl/security/DefaultCryptoMessage.java | 24 - .../impl/security/DefaultCryptoRequest.java | 8 +- .../security/DefaultDecryptionKeyRequest.java | 24 + .../security/DefaultEcJwkBuilderFactory.java | 18 - .../impl/security/DefaultEcPrivateJwk.java | 19 + .../impl/security/DefaultEcPublicJwk.java | 17 + ...faultEllipticCurveSignatureAlgorithm.java} | 84 +- .../DefaultEncryptionAlgorithmLocator.java | 40 - .../security/DefaultEncryptionRequest.java | 50 -- .../security/DefaultEncryptionResult.java | 18 - .../security/DefaultIvDecryptionRequest.java | 41 - .../security/DefaultIvEncryptionResult.java | 45 -- .../impl/security/DefaultJweFactory.java | 123 --- .../security/DefaultJwkBuilderFactory.java | 18 - .../impl/security/DefaultJwkContext.java | 422 ++++++++++ .../impl/security/DefaultJwkConverter.java | 58 -- .../impl/security/DefaultKeyRequest.java | 32 + .../impl/security/DefaultKeyResult.java | 32 + .../impl/security/DefaultKeyUseStrategy.java | 31 + .../impl/security/DefaultKeyedRequest.java | 23 + .../impl/security/DefaultPayloadSupplier.java | 31 + .../impl/security/DefaultPbeKey.java | 90 +++ .../impl/security/DefaultPbeKeyBuilder.java | 29 + .../impl/security/DefaultPrivateEcJwk.java | 18 - .../security/DefaultPrivateEcJwkBuilder.java | 24 - .../impl/security/DefaultPrivateRsaJwk.java | 87 -- .../impl/security/DefaultProtoJwkBuilder.java | 85 ++ .../impl/security/DefaultPublicEcJwk.java | 6 - .../security/DefaultPublicEcJwkBuilder.java | 23 - .../impl/security/DefaultRsaKeyAlgorithm.java | 76 ++ .../impl/security/DefaultRsaPrivateJwk.java | 40 + .../impl/security/DefaultRsaPublicJwk.java | 16 + ...java => DefaultRsaSignatureAlgorithm.java} | 90 +-- .../impl/security/DefaultSecretJwk.java | 18 + .../impl/security/DefaultSecurityRequest.java | 27 + .../security/DefaultSignatureRequest.java | 17 + .../impl/security/DefaultSymmetricJwk.java | 28 - .../security/DefaultSymmetricJwkBuilder.java | 24 - .../impl/security/DefaultValueGetter.java | 149 ++++ .../DefaultVerifySignatureRequest.java | 8 +- .../impl/security/DirectEncryptionMode.java | 22 - .../impl/security/DirectKeyAgreementMode.java | 14 - .../impl/security/DirectKeyAlgorithm.java | 36 + .../DisabledDecryptionKeyResolver.java | 22 - .../impl/security/DispatchingJwkFactory.java | 81 ++ .../impl/security/EcJwkConverter.java | 177 ----- .../impl/security/EcPrivateJwkFactory.java | 83 ++ .../impl/security/EcPublicJwkFactory.java | 77 ++ .../impl/security/EncryptKeyRequest.java | 12 - .../security/EncryptedKeyManagementMode.java | 22 - .../security/EncryptionAlgorithmsBridge.java | 48 ++ .../impl/security/FamilyJwkFactory.java | 11 + .../impl/security/GcmAesAeadAlgorithm.java | 94 +++ .../security/GcmAesEncryptionAlgorithm.java | 106 --- .../impl/security/GetKeyRequest.java | 17 - .../impl/security/HmacAesAeadAlgorithm.java | 151 ++++ .../security/HmacAesEncryptionAlgorithm.java | 186 ----- .../jsonwebtoken/impl/security/JcaPbeKey.java | 51 ++ .../impl/security/JcaTemplate.java | 130 +++ .../impl/security/JwkBuilders.java | 16 + .../impl/security/JwkContext.java | 63 ++ .../impl/security/JwkConverter.java | 11 - .../impl/security/JwkFactory.java | 10 + .../impl/security/JwkValidator.java | 9 - .../impl/security/JwkX509StringConverter.java | 53 ++ .../KeyAgreementWithKeyWrappingMode.java | 12 - .../impl/security/KeyAlgorithmsBridge.java | 244 ++++++ .../impl/security/KeyEncryptionMode.java | 12 - .../impl/security/KeyManagementMode.java | 22 - .../impl/security/KeyManagementModes.java | 15 - .../jsonwebtoken/impl/security/KeyUsage.java | 64 ++ .../impl/security/KeyUseStrategy.java | 8 + .../impl/security/KeyWrappingMode.java | 12 - .../impl/security/KeysBridge.java | 22 + .../impl/security/MacSignatureAlgorithm.java | 133 ++-- .../impl/security/NoneSignatureAlgorithm.java | 31 +- .../impl/security/Pbes2HsAkwAlgorithm.java | 193 +++++ .../impl/security/PrivateEcJwkValidator.java | 18 - .../impl/security/RandomEncryptedKeyMode.java | 38 - .../jsonwebtoken/impl/security/Recipient.java | 11 - .../impl/security/RsaJwkConverter.java | 28 - .../impl/security/RsaPrivateJwkFactory.java | 239 ++++++ .../impl/security/RsaPublicJwkFactory.java | 46 ++ .../impl/security/SecretJwkFactory.java | 66 ++ .../security/SignatureAlgorithmsBridge.java | 56 ++ .../impl/security/SymmetricJwkConverter.java | 59 -- .../impl/security/SymmetricJwkValidator.java | 23 - .../impl/security/TypedJwkConverter.java | 11 - .../DeprecatedJwtParserTest.groovy | 128 +-- .../io/jsonwebtoken/DeprecatedJwtsTest.groovy | 32 +- .../io/jsonwebtoken/JwtParserTest.groovy | 131 +-- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 37 +- .../impl/DefaultJweBuilderTest.groovy | 101 +++ .../impl/DefaultJwtBuilderTest.groovy | 26 +- .../impl/DefaultJwtParserTest.groovy | 2 +- .../jsonwebtoken/impl/JwtTokenizerTest.groovy | 32 + .../DefaultJwtSignatureValidatorTest.groovy | 53 -- .../impl/crypto/DefaultJwtSignerTest.groovy | 55 -- ...efaultSignatureValidatorFactoryTest.groovy | 37 - .../crypto/DefaultSignerFactoryTest.groovy | 40 - .../crypto/EllipticCurveProviderTest.groovy | 68 -- ...EllipticCurveSignatureValidatorTest.groovy | 69 -- .../crypto/EllipticCurveSignerTest.groovy | 145 ---- .../impl/crypto/MacProviderTest.groovy | 55 -- .../impl/crypto/MacSignerTest.groovy | 103 --- .../crypto/PowermockMacProviderTest.groovy | 65 -- .../impl/crypto/RsaProviderTest.groovy | 70 -- .../crypto/RsaSignatureValidatorTest.groovy | 87 -- .../impl/crypto/RsaSignerTest.groovy | 177 ----- .../impl/crypto/SignatureProviderTest.groovy | 87 -- .../PropagatingExceptionFunctionTest.groovy | 28 + ...tractAeadAesEncryptionAlgorithmTest.groovy | 111 --- .../AbstractAsymmetricJwkBuilderTest.groovy | 64 ++ .../impl/security/AbstractEcJwkTest.groovy | 116 --- .../AbstractEcJwkValidatorTest.groovy | 57 -- .../AbstractEncryptionAlgorithmTest.groovy | 56 -- .../security/AbstractJwkBuilderTest.groovy | 127 +-- .../security/AbstractJwkValidatorTest.groovy | 55 -- .../AbstractSignatureAlgorithmTest.groovy | 107 +-- .../impl/security/AesAlgorithmTest.groovy | 68 ++ .../security/AesGcmKeyAlgorithmTest.groovy | 166 ++++ .../impl/security/CertUtils.groovy | 64 ++ .../impl/security/CipherAlgorithmTest.groovy | 57 -- .../impl/security/CipherTemplateTest.groovy | 93 --- .../security/ConstantKeyLocatorTest.groovy | 52 ++ .../impl/security/CryptoAlgorithmTest.groovy | 51 ++ .../DefaultAeadIvEncryptionResultTest.groovy | 50 -- ...llipticCurveSignatureAlgorithmTest.groovy} | 49 +- ...faultEncryptionAlgorithmLocatorTest.groovy | 98 --- .../DefaultIvEncryptionResultTest.groovy | 38 - .../security/DefaultJweFactoryTest.groovy | 14 - .../security/DefaultJwkConverterTest.groovy | 103 --- ...ctJwkTest.groovy => DefaultJwkTest.groovy} | 57 +- ...oovy => DefaultPayloadSupplierTest.groovy} | 6 +- ...> DefaultRsaSignatureAlgorithmTest.groovy} | 29 +- .../security/DefaultSymmetricJwkTest.groovy | 43 - .../security/DirectKeyAlgorithmTest.groovy | 74 ++ .../DisabledDecryptionKeyResolverTest.groovy | 16 - .../security/DispatchingJwkFactoryTest.groovy | 95 +++ ....groovy => GcmAesAeadAlgorithmTest.groovy} | 29 +- .../security/HmacAesAeadAlgorithmTest.groovy | 45 ++ .../HmacAesEncryptionAlgorithmTest.groovy | 83 -- .../{crypto => security}/Issue542Test.groovy | 42 +- .../impl/security/JcaTemplateTest.groovy | 113 +++ .../impl/security/JwksTest.groovy | 162 +++- .../security/MacSignatureAlgorithmTest.groovy | 56 +- .../NoneSignatureAlgorithmTest.groovy | 7 +- .../security/Pbes2HsAkwAlgorithmTest.groovy | 44 ++ .../security/PrivateEcJwkValidatorTest.groovy | 31 - .../security/RFC7516AppendixA1Test.groovy | 165 ++++ .../security/RFC7516AppendixA2Test.groovy | 159 ++++ .../security/RFC7516AppendixA3Test.groovy | 125 +++ .../security/RFC7517AppendixA1Test.groovy | 67 ++ .../security/RFC7517AppendixA2Test.groovy | 116 +++ .../security/RFC7517AppendixA3Test.groovy | 58 ++ .../impl/security/RFC7517AppendixBTest.groovy | 67 ++ .../impl/security/RFC7517AppendixCTest.groovy | 327 ++++++++ ...st.groovy => RFC7518AppendixB1Test.groovy} | 30 +- ...st.groovy => RFC7518AppendixB2Test.groovy} | 37 +- ...st.groovy => RFC7518AppendixB3Test.groovy} | 28 +- .../impl/security/RandomsTest.groovy | 10 + .../security/SymmetricJwkValidatorTest.groovy | 31 - .../jsonwebtoken/impl/security/TestJwk.groovy | 7 - .../impl/security/TestJwkValidator.groovy | 18 - .../security/EncryptionAlgorithmsTest.groovy | 41 +- .../security/KeyAlgorithmsTest.groovy | 48 ++ .../jsonwebtoken/security/KeysImplTest.groovy | 10 +- .../security/SignatureAlgorithmsTest.groovy | 7 +- .../impl/{crypto => security}/PS256.crt.pem | 0 .../impl/{crypto => security}/PS256.key.pem | 0 .../impl/{crypto => security}/PS384.crt.pem | 0 .../impl/{crypto => security}/PS384.key.pem | 0 .../impl/{crypto => security}/PS512.crt.pem | 0 .../impl/{crypto => security}/PS512.key.pem | 0 .../impl/{crypto => security}/README.md | 0 .../impl/{crypto => security}/RS256.crt.pem | 0 .../impl/{crypto => security}/RS256.key.pem | 0 .../impl/{crypto => security}/RS384.crt.pem | 0 .../impl/{crypto => security}/RS384.key.pem | 0 .../impl/{crypto => security}/RS512.crt.pem | 0 .../impl/{crypto => security}/RS512.key.pem | 0 .../impl/{crypto => security}/rsa2048.crt.pem | 0 .../impl/{crypto => security}/rsa2048.key.pem | 0 .../impl/{crypto => security}/rsa3072.crt.pem | 0 .../impl/{crypto => security}/rsa3072.key.pem | 0 .../impl/{crypto => security}/rsa4096.crt.pem | 0 .../impl/{crypto => security}/rsa4096.key.pem | 0 409 files changed, 10768 insertions(+), 8661 deletions(-) create mode 100644 api/src/main/java/io/jsonwebtoken/Identifiable.java create mode 100644 api/src/main/java/io/jsonwebtoken/JweBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/Locator.java create mode 100644 api/src/main/java/io/jsonwebtoken/LocatorAdapter.java delete mode 100644 api/src/main/java/io/jsonwebtoken/Named.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/AeadEncryptionAlgorithm.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/AeadIvEncryptionResult.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/AeadIvRequest.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AeadResult.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/AeadSymmetricEncryptionAlgorithm.java rename api/src/main/java/io/jsonwebtoken/security/{AssociatedDataSource.java => AssociatedDataSupplier.java} (89%) create mode 100644 api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyGenerator.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/CryptoMessage.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/CurveId.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/CurveIds.java rename api/src/main/java/io/jsonwebtoken/security/{AeadEncryptionResult.java => DecryptAeadRequest.java} (81%) rename api/src/main/java/io/jsonwebtoken/security/{AeadDecryptionRequest.java => DecryptionKeyRequest.java} (81%) delete mode 100644 api/src/main/java/io/jsonwebtoken/security/DecryptionKeyResolver.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/DefaultCurveId.java rename api/src/main/java/io/jsonwebtoken/security/{EncryptionResult.java => DigestSupplier.java} (86%) delete mode 100644 api/src/main/java/io/jsonwebtoken/security/EcJwk.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/EcJwkBuilder.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/EcJwkBuilderFactory.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/EcJwkMutator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EcKeyAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EcPrivateJwkBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EcPublicJwkBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithm.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmLocator.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmName.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/InitializationVectorSource.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/InitializationVectorSupplier.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/IvEncryptionResult.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/IvRequest.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/JwkBuilderFactory.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/JwkMutator.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfo.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoBuilder.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoMutator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyManagementAlgorithmName.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyManagementModeName.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyRequest.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyResult.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/KeySupplier.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PayloadSupplier.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PbeKey.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PbeKeyBuilder.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/PrivateEcJwk.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkBuilder.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkMutator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PrivateJwkBuilder.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwk.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwkMutator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/PublicEcJwk.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/PublicEcJwkBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PublicJwk.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PublicJwkBuilder.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/PublicRsaJwk.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/RsaJwk.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/RsaJwkMutator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwkBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java rename api/src/main/java/io/jsonwebtoken/security/{CryptoException.java => RsaPublicJwkBuilder.java} (68%) create mode 100644 api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java rename api/src/main/java/io/jsonwebtoken/security/{AuthenticationTagSource.java => SecretJwk.java} (84%) create mode 100644 api/src/main/java/io/jsonwebtoken/security/SecretJwkBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SecretKeyGenerator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SecretKeySignatureAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SecurityRequest.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/SymmetricEncryptionAlgorithm.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/SymmetricJwk.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/SymmetricJwkBuilder.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/SymmetricJwkMutator.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/SymmetricKeyAlgorithm.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/SymmetricKeySignatureAlgorithm.java delete mode 100644 api/src/test/groovy/io/jsonwebtoken/EncryptionAlgorithmNameTest.groovy delete mode 100644 api/src/test/groovy/io/jsonwebtoken/security/CurveIdsTest.groovy delete mode 100644 api/src/test/groovy/io/jsonwebtoken/security/KeyManagementAlgorithmNameTest.groovy delete mode 100644 api/src/test/groovy/io/jsonwebtoken/security/KeyManagementModeNameTest.groovy create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/DefaultJwe.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/IdLocator.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/IdRegistry.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/JwtSignatureValidator.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/JwtSigner.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidator.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/Signer.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/SignerFactory.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/io/CodecConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/io/Codecs.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/BiFunction.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/CheckedFunction.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/CollectionConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/ConstantFunction.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/Converter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/Function.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/LocatorFunction.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/NoConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/NullSafeConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/Registry.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/UriStringConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/ValueGetter.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwk.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkValidator.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkConverter.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkValidator.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPublicJwk.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwk.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwkValidator.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractTypedJwkConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AsymmetricJwkFactory.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/CipherAlgorithm.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/CipherCallback.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/CipherTemplate.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/ConstantKeyLocator.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResult.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadResult.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAesEncryptionRequest.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoMessage.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultDecryptionKeyRequest.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcJwkBuilderFactory.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java rename impl/src/main/java/io/jsonwebtoken/impl/security/{EllipticCurveSignatureAlgorithm.java => DefaultEllipticCurveSignatureAlgorithm.java} (71%) delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocator.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionRequest.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionResult.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvDecryptionRequest.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvEncryptionResult.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJweFactory.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkBuilderFactory.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyResult.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyUseStrategy.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyedRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPayloadSupplier.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKey.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKeyBuilder.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwk.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwkBuilder.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateRsaJwk.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwk.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwkBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java rename impl/src/main/java/io/jsonwebtoken/impl/security/{RsaSignatureAlgorithm.java => DefaultRsaSignatureAlgorithm.java} (55%) create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecurityRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSignatureRequest.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwk.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwkBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DirectEncryptionMode.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAgreementMode.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAlgorithm.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolver.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/EcJwkConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/EncryptKeyRequest.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/EncryptedKeyManagementMode.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/FamilyJwkFactory.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesEncryptionAlgorithm.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/GetKeyRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JcaPbeKey.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JwkBuilders.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JwkFactory.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JwkValidator.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JwkX509StringConverter.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyAgreementWithKeyWrappingMode.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyEncryptionMode.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementMode.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementModes.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyUsage.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyUseStrategy.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyWrappingMode.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/PrivateEcJwkValidator.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/RandomEncryptedKeyMode.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/Recipient.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/RsaJwkConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkConverter.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkValidator.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/TypedJwkConverter.java create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidatorTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignerTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/PowermockMacProviderTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/PropagatingExceptionFunctionTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithmTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkValidatorTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithmTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkValidatorTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/CertUtils.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherAlgorithmTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherTemplateTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/ConstantKeyLocatorTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResultTest.groovy rename impl/src/test/groovy/io/jsonwebtoken/impl/security/{EllipticCurveSignatureAlgorithmTest.groovy => DefaultEllipticCurveSignatureAlgorithmTest.groovy} (78%) delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocatorTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultIvEncryptionResultTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJweFactoryTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkConverterTest.groovy rename impl/src/test/groovy/io/jsonwebtoken/impl/security/{AbstractJwkTest.groovy => DefaultJwkTest.groovy} (85%) rename impl/src/test/groovy/io/jsonwebtoken/impl/security/{DefaultCryptoMessageTest.groovy => DefaultPayloadSupplierTest.groovy} (64%) rename impl/src/test/groovy/io/jsonwebtoken/impl/security/{RsaSignatureAlgorithmTest.groovy => DefaultRsaSignatureAlgorithmTest.groovy} (60%) delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSymmetricJwkTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolverTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy rename impl/src/test/groovy/io/jsonwebtoken/impl/security/{GcmAesEncryptionServiceTest.groovy => GcmAesAeadAlgorithmTest.groovy} (74%) create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithmTest.groovy rename impl/src/test/groovy/io/jsonwebtoken/impl/{crypto => security}/Issue542Test.groovy (69%) create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/JcaTemplateTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateEcJwkValidatorTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA1Test.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA2Test.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA1Test.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA3Test.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixBTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy rename impl/src/test/groovy/io/jsonwebtoken/impl/security/{Aes128CbcHmacSha256Test.groovy => RFC7518AppendixB1Test.groovy} (85%) rename impl/src/test/groovy/io/jsonwebtoken/impl/security/{Aes192CbcHmacSha384Test.groovy => RFC7518AppendixB2Test.groovy} (82%) rename impl/src/test/groovy/io/jsonwebtoken/impl/security/{Aes256CbcHmacSha512Test.groovy => RFC7518AppendixB3Test.groovy} (86%) delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/SymmetricJwkValidatorTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwk.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwkValidator.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/PS256.crt.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/PS256.key.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/PS384.crt.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/PS384.key.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/PS512.crt.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/PS512.key.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/README.md (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/RS256.crt.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/RS256.key.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/RS384.crt.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/RS384.key.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/RS512.crt.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/RS512.key.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/rsa2048.crt.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/rsa2048.key.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/rsa3072.crt.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/rsa3072.key.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/rsa4096.crt.pem (100%) rename impl/src/test/resources/io/jsonwebtoken/impl/{crypto => security}/rsa4096.key.pem (100%) diff --git a/api/src/main/java/io/jsonwebtoken/Claims.java b/api/src/main/java/io/jsonwebtoken/Claims.java index b5a404fb9..fafbe953e 100644 --- a/api/src/main/java/io/jsonwebtoken/Claims.java +++ b/api/src/main/java/io/jsonwebtoken/Claims.java @@ -41,25 +41,25 @@ public interface Claims extends Map, ClaimsMutator { /** JWT {@code Issuer} claims parameter name: "iss" */ - public static final String ISSUER = "iss"; + String ISSUER = "iss"; /** JWT {@code Subject} claims parameter name: "sub" */ - public static final String SUBJECT = "sub"; + String SUBJECT = "sub"; /** JWT {@code Audience} claims parameter name: "aud" */ - public static final String AUDIENCE = "aud"; + String AUDIENCE = "aud"; /** JWT {@code Expiration} claims parameter name: "exp" */ - public static final String EXPIRATION = "exp"; + String EXPIRATION = "exp"; /** JWT {@code Not Before} claims parameter name: "nbf" */ - public static final String NOT_BEFORE = "nbf"; + String NOT_BEFORE = "nbf"; /** JWT {@code Issued At} claims parameter name: "iat" */ - public static final String ISSUED_AT = "iat"; + String ISSUED_AT = "iat"; /** JWT {@code JWT ID} claims parameter name: "jti" */ - public static final String ID = "jti"; + String ID = "jti"; /** * Returns the JWT diff --git a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java index 66528d8ff..34333d99f 100644 --- a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java +++ b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java @@ -25,7 +25,7 @@ * @see io.jsonwebtoken.Claims * @since 0.2 */ -public interface ClaimsMutator { +public interface ClaimsMutator> { /** * Sets the JWT diff --git a/api/src/main/java/io/jsonwebtoken/Header.java b/api/src/main/java/io/jsonwebtoken/Header.java index c96ecdc2f..49bdccb22 100644 --- a/api/src/main/java/io/jsonwebtoken/Header.java +++ b/api/src/main/java/io/jsonwebtoken/Header.java @@ -40,13 +40,13 @@ public interface Header> extends Map { /** JWT {@code Type} (typ) value: "JWT" */ - public static final String JWT_TYPE = "JWT"; + String JWT_TYPE = "JWT"; /** JWT {@code Type} header parameter name: "typ" */ - public static final String TYPE = "typ"; + String TYPE = "typ"; /** JWT {@code Content Type} header parameter name: "cty" */ - public static final String CONTENT_TYPE = "cty"; + String CONTENT_TYPE = "cty"; /** * JWT {@code Algorithm} header parameter name: "alg". @@ -54,15 +54,15 @@ public interface Header> extends Map { * @see JWS Algorithm Header * @see JWE Algorithm Header */ - public static final String ALGORITHM = "alg"; + String ALGORITHM = "alg"; /** JWT {@code Compression Algorithm} header parameter name: "zip" */ - public static final String COMPRESSION_ALGORITHM = "zip"; + String COMPRESSION_ALGORITHM = "zip"; /** JJWT legacy/deprecated compression algorithm header parameter name: "calg" * @deprecated use {@link #COMPRESSION_ALGORITHM} instead. */ @Deprecated - public static final String DEPRECATED_COMPRESSION_ALGORITHM = "calg"; + String DEPRECATED_COMPRESSION_ALGORITHM = "calg"; /** * Returns the @@ -124,13 +124,15 @@ public interface Header> extends Map { *
  • If the JWT is a Signed JWT (a JWS), the * alg (Algorithm) header parameter identifies the cryptographic algorithm used to secure the * JWS. Consider using - * {@link io.jsonwebtoken.security.SignatureAlgorithms#forName(String) SignatureAlgorithms.forName} to - * convert this string value to a type-safe enum instance.
  • + * {@link io.jsonwebtoken.security.SignatureAlgorithms#findById(String) SignatureAlgorithms.findById} to + * convert this string value to a type-safe SignatureAlgorithm instance. *
  • If the JWT is an Encrypted JWT (a JWE), the * alg (Algorithm) header parameter * identifies the cryptographic key management algorithm used to encrypt or determine the value of the Content * Encryption Key (CEK). The encrypted content is not usable if the alg value does not represent a - * supported algorithm, or if the recipient does not have a key that can be used with that algorithm
  • + * supported algorithm, or if the recipient does not have a key that can be used with that algorithm. Consider + * using {@link io.jsonwebtoken.security.KeyAlgorithms#findById(String) KeyAlgorithms.findById} to convert this + * string value to a type-safe KeyAlgorithm instance. * * * @return the {@code alg} header value or {@code null} if not present. This will always be @@ -145,14 +147,12 @@ public interface Header> extends Map { *
      *
    • If the JWT is a Signed JWT (a JWS), the * alg (Algorithm) header parameter identifies the cryptographic algorithm used to secure the - * JWS. Consider using - * {@link io.jsonwebtoken.security.SignatureAlgorithms#forName(String) SignatureAlgorithms.forName} to - * convert this string value to a type-safe enum instance.
    • + * JWS. *
    • If the JWT is an Encrypted JWT (a JWE), the * alg (Algorithm) header parameter * identifies the cryptographic key management algorithm used to encrypt or determine the value of the Content * Encryption Key (CEK). The encrypted content is not usable if the alg value does not represent a - * supported algorithm, or if the recipient does not have a key that can be used with that algorithm
    • + * supported algorithm, or if the recipient does not have a key that can be used with that algorithm. *
    * * @param alg the {@code alg} header value @@ -182,9 +182,6 @@ public interface Header> extends Map { * Sets the JWT zip * (Compression Algorithm) header parameter value. A {@code null} value will remove * the property from the JSON map. - *

    The compression algorithm is NOT part of the JWT specification - * and must be used carefully since, is not expected that other libraries (including previous versions of this one) - * be able to deserialize a compressed JWT body correctly.

    * *

    Compatibility Note

    * diff --git a/api/src/main/java/io/jsonwebtoken/Identifiable.java b/api/src/main/java/io/jsonwebtoken/Identifiable.java new file mode 100644 index 000000000..082a50ddc --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/Identifiable.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface Identifiable { + + /** + * Returns the unique string identifier of the associated object. + * + * @return the unique string identifier of the associated object. + */ + String getId(); +} diff --git a/api/src/main/java/io/jsonwebtoken/Jwe.java b/api/src/main/java/io/jsonwebtoken/Jwe.java index b8e7f99e1..9f8abf51b 100644 --- a/api/src/main/java/io/jsonwebtoken/Jwe.java +++ b/api/src/main/java/io/jsonwebtoken/Jwe.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken; /** diff --git a/api/src/main/java/io/jsonwebtoken/JweBuilder.java b/api/src/main/java/io/jsonwebtoken/JweBuilder.java new file mode 100644 index 000000000..970877462 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/JweBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken; + +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.KeyAlgorithm; + +import javax.crypto.SecretKey; +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface JweBuilder extends JwtBuilder { + + JweBuilder encryptWith(AeadAlgorithm enc); + + JweBuilder withKey(SecretKey key); + + JweBuilder withKeyFrom(K key, KeyAlgorithm alg); +} diff --git a/api/src/main/java/io/jsonwebtoken/JweHeader.java b/api/src/main/java/io/jsonwebtoken/JweHeader.java index c054efece..1a408aa9f 100644 --- a/api/src/main/java/io/jsonwebtoken/JweHeader.java +++ b/api/src/main/java/io/jsonwebtoken/JweHeader.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken; /** @@ -10,57 +25,57 @@ public interface JweHeader extends Header { /** * JWE Algorithm Header name: the string literal alg */ - public static final String ALGORITHM = "alg"; + String ALGORITHM = "alg"; /** * JWE Encryption Algorithm Header name: the string literal enc */ - public static final String ENCRYPTION_ALGORITHM = "enc"; + String ENCRYPTION_ALGORITHM = "enc"; /** * JWE Compression Algorithm Header name: the string literal zip */ - public static final String COMPRESSION_ALGORITHM = "zip"; + String COMPRESSION_ALGORITHM = "zip"; /** * JWE JWK Set URL Header name: the string literal jku */ - public static final String JWK_SET_URL = "jku"; + String JWK_SET_URL = "jku"; /** * JWE JSON Web Key Header name: the string literal jwk */ - public static final String JSON_WEB_KEY = "jwk"; + String JSON_WEB_KEY = "jwk"; /** * JWE Key ID Header name: the string literal kid */ - public static final String KEY_ID = "kid"; + String KEY_ID = "kid"; /** * JWE X.509 URL Header name: the string literal x5u */ - public static final String X509_URL = "x5u"; + String X509_URL = "x5u"; /** * JWE X.509 Certificate Chain Header name: the string literal x5c */ - public static final String X509_CERT_CHAIN = "x5c"; + String X509_CERT_CHAIN = "x5c"; /** * JWE X.509 Certificate SHA-1 Thumbprint Header name: the string literal x5t */ - public static final String X509_CERT_SHA1_THUMBPRINT = "x5t"; + String X509_CERT_SHA1_THUMBPRINT = "x5t"; /** * JWE X.509 Certificate SHA-256 Thumbprint Header name: the string literal x5t#S256 */ - public static final String X509_CERT_SHA256_THUMBPRINT = "x5t#S256"; + String X509_CERT_SHA256_THUMBPRINT = "x5t#S256"; /** * JWE Critical Header name: the string literal crit */ - public static final String CRITICAL = "crit"; + String CRITICAL = "crit"; /** * Returns the JWE enc (Encryption diff --git a/api/src/main/java/io/jsonwebtoken/JwsHeader.java b/api/src/main/java/io/jsonwebtoken/JwsHeader.java index 1b8cfbe5b..055f3d0a9 100644 --- a/api/src/main/java/io/jsonwebtoken/JwsHeader.java +++ b/api/src/main/java/io/jsonwebtoken/JwsHeader.java @@ -25,47 +25,47 @@ public interface JwsHeader extends Header { /** * JWS Algorithm Header name: the string literal alg */ - public static final String ALGORITHM = "alg"; + String ALGORITHM = "alg"; /** * JWS JWK Set URL Header name: the string literal jku */ - public static final String JWK_SET_URL = "jku"; + String JWK_SET_URL = "jku"; /** * JWS JSON Web Key Header name: the string literal jwk */ - public static final String JSON_WEB_KEY = "jwk"; + String JSON_WEB_KEY = "jwk"; /** * JWS Key ID Header name: the string literal kid */ - public static final String KEY_ID = "kid"; + String KEY_ID = "kid"; /** * JWS X.509 URL Header name: the string literal x5u */ - public static final String X509_URL = "x5u"; + String X509_URL = "x5u"; /** * JWS X.509 Certificate Chain Header name: the string literal x5c */ - public static final String X509_CERT_CHAIN = "x5c"; + String X509_CERT_CHAIN = "x5c"; /** * JWS X.509 Certificate SHA-1 Thumbprint Header name: the string literal x5t */ - public static final String X509_CERT_SHA1_THUMBPRINT = "x5t"; + String X509_CERT_SHA1_THUMBPRINT = "x5t"; /** * JWS X.509 Certificate SHA-256 Thumbprint Header name: the string literal x5t#S256 */ - public static final String X509_CERT_SHA256_THUMBPRINT = "x5t#S256"; + String X509_CERT_SHA256_THUMBPRINT = "x5t#S256"; /** * JWS Critical Header name: the string literal crit */ - public static final String CRITICAL = "crit"; + String CRITICAL = "crit"; /** * Returns the JWS diff --git a/api/src/main/java/io/jsonwebtoken/Jwt.java b/api/src/main/java/io/jsonwebtoken/Jwt.java index 1de5c6e31..1344e6abc 100644 --- a/api/src/main/java/io/jsonwebtoken/Jwt.java +++ b/api/src/main/java/io/jsonwebtoken/Jwt.java @@ -22,7 +22,7 @@ * * @since 0.1 */ -public interface Jwt { +public interface Jwt, B> { /** * Returns the JWT {@link Header} or {@code null} if not present. diff --git a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java index 770539517..60d1f5d1b 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -34,7 +34,7 @@ * * @since 0.1 */ -public interface JwtBuilder extends ClaimsMutator { +public interface JwtBuilder> extends ClaimsMutator { /** * Sets the JCA Provider to use during cryptographic signing or encryption operations, or {@code null} if the @@ -45,7 +45,7 @@ public interface JwtBuilder extends ClaimsMutator { * @return the builder for method chaining. * @since JJWT_RELEASE_VERSION */ - JwtBuilder setProvider(Provider provider); + T setProvider(Provider provider); /** * Sets the {@link SecureRandom} to use during cryptographic signing or encryption operations, or {@code null} if @@ -56,7 +56,7 @@ public interface JwtBuilder extends ClaimsMutator { * @return the builder for method chaining. * @since JJWT_RELEASE_VERSION */ - JwtBuilder setSecureRandom(SecureRandom secureRandom); + T setSecureRandom(SecureRandom secureRandom); /** * Sets (and replaces) any existing header with the specified header. If you do not want to replace the existing @@ -65,7 +65,7 @@ public interface JwtBuilder extends ClaimsMutator { * @param header the header to set (and potentially replace any existing header). * @return the builder for method chaining. */ - JwtBuilder setHeader(Header header); //replaces any existing header with the specified header. + T setHeader(Header header); //replaces any existing header with the specified header. /** * Sets (and replaces) any existing header with the specified header. If you do not want to replace the existing @@ -74,7 +74,7 @@ public interface JwtBuilder extends ClaimsMutator { * @param header the header to set (and potentially replace any existing header). * @return the builder for method chaining. */ - JwtBuilder setHeader(Map header); + T setHeader(Map header); /** * Applies the specified name/value pairs to the header. If a header does not yet exist at the time this method @@ -83,7 +83,7 @@ public interface JwtBuilder extends ClaimsMutator { * @param params the header name/value pairs to append to the header. * @return the builder for method chaining. */ - JwtBuilder setHeaderParams(Map params); + T setHeaderParams(Map params); //sets the specified header parameter, overwriting any previous value under the same name. @@ -95,7 +95,7 @@ public interface JwtBuilder extends ClaimsMutator { * @param value the header parameter value * @return the builder for method chaining. */ - JwtBuilder setHeaderParam(String name, Object value); + T setHeaderParam(String name, Object value); /** * Sets the JWT's payload to be a plaintext (non-JSON) string. If you want the JWT body to be JSON, use the @@ -106,7 +106,7 @@ public interface JwtBuilder extends ClaimsMutator { * @param payload the plaintext (non-JSON) string that will be the body of the JWT. * @return the builder for method chaining. */ - JwtBuilder setPayload(String payload); + T setPayload(String payload); /** * Sets the JWT payload to be a JSON Claims instance. If you do not want the JWT body to be JSON and instead want @@ -117,7 +117,7 @@ public interface JwtBuilder extends ClaimsMutator { * @param claims the JWT claims to be set as the JWT body. * @return the builder for method chaining. */ - JwtBuilder setClaims(Claims claims); + T setClaims(Claims claims); /** * Sets the JWT payload to be a JSON Claims instance populated by the specified name/value pairs. If you do not @@ -129,7 +129,7 @@ public interface JwtBuilder extends ClaimsMutator { * @param claims the JWT claims to be set as the JWT body. * @return the builder for method chaining. */ - JwtBuilder setClaims(Map claims); + T setClaims(Map claims); /** * Adds all given name/value pairs to the JSON Claims in the payload. If a Claims instance does not yet exist at the @@ -141,7 +141,7 @@ public interface JwtBuilder extends ClaimsMutator { * @return the builder for method chaining. * @since 0.8 */ - JwtBuilder addClaims(Map claims); + T addClaims(Map claims); /** * Sets the JWT Claims @@ -168,7 +168,7 @@ public interface JwtBuilder extends ClaimsMutator { */ @Override //only for better/targeted JavaDoc - JwtBuilder setIssuer(String iss); + T setIssuer(String iss); /** * Sets the JWT Claims @@ -195,7 +195,7 @@ public interface JwtBuilder extends ClaimsMutator { */ @Override //only for better/targeted JavaDoc - JwtBuilder setSubject(String sub); + T setSubject(String sub); /** * Sets the JWT Claims @@ -222,7 +222,7 @@ public interface JwtBuilder extends ClaimsMutator { */ @Override //only for better/targeted JavaDoc - JwtBuilder setAudience(String aud); + T setAudience(String aud); /** * Sets the JWT Claims @@ -251,7 +251,7 @@ public interface JwtBuilder extends ClaimsMutator { */ @Override //only for better/targeted JavaDoc - JwtBuilder setExpiration(Date exp); + T setExpiration(Date exp); /** * Sets the JWT Claims @@ -280,7 +280,7 @@ public interface JwtBuilder extends ClaimsMutator { */ @Override //only for better/targeted JavaDoc - JwtBuilder setNotBefore(Date nbf); + T setNotBefore(Date nbf); /** * Sets the JWT Claims @@ -309,7 +309,7 @@ public interface JwtBuilder extends ClaimsMutator { */ @Override //only for better/targeted JavaDoc - JwtBuilder setIssuedAt(Date iat); + T setIssuedAt(Date iat); /** * Sets the JWT Claims @@ -340,7 +340,7 @@ public interface JwtBuilder extends ClaimsMutator { */ @Override //only for better/targeted JavaDoc - JwtBuilder setId(String jti); + T setId(String jti); /** * Sets a custom JWT Claims parameter value. A {@code null} value will remove the property from the Claims. @@ -365,7 +365,7 @@ public interface JwtBuilder extends ClaimsMutator { * @return the builder instance for method chaining. * @since 0.2 */ - JwtBuilder claim(String name, Object value); + T claim(String name, Object value); /** * Signs the constructed JWT with the specified key using the key's @@ -384,7 +384,7 @@ public interface JwtBuilder extends ClaimsMutator { * @see #signWith(Key, io.jsonwebtoken.security.SignatureAlgorithm) * @since 0.10.0 */ - JwtBuilder signWith(Key key) throws InvalidKeyException; + T signWith(Key key) throws InvalidKeyException; /** * Signs the constructed JWT using the specified algorithm with the specified key, producing a JWS. @@ -408,7 +408,7 @@ public interface JwtBuilder extends ClaimsMutator { * This method will be removed in the 1.0 release. */ @Deprecated - JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKey) throws InvalidKeyException; + T signWith(SignatureAlgorithm alg, byte[] secretKey) throws InvalidKeyException; /** * Signs the constructed JWT using the specified algorithm with the specified key, producing a JWS. @@ -456,7 +456,7 @@ public interface JwtBuilder extends ClaimsMutator { * method will be removed in the 1.0 release. */ @Deprecated - JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey) throws InvalidKeyException; + T signWith(SignatureAlgorithm alg, String base64EncodedSecretKey) throws InvalidKeyException; /** * Signs the constructed JWT using the specified algorithm with the specified key, producing a JWS. @@ -475,7 +475,7 @@ public interface JwtBuilder extends ClaimsMutator { * in the 1.0 release. */ @Deprecated - JwtBuilder signWith(SignatureAlgorithm alg, Key key) throws InvalidKeyException; + T signWith(SignatureAlgorithm alg, Key key) throws InvalidKeyException; /** *

    Deprecation Notice

    @@ -500,7 +500,7 @@ public interface JwtBuilder extends ClaimsMutator { * @deprecated since JJWT_RELEASE_VERSION to use a more the more flexible {@link io.jsonwebtoken.security.SignatureAlgorithm}. */ @Deprecated - JwtBuilder signWith(Key key, SignatureAlgorithm alg) throws InvalidKeyException; + T signWith(Key key, SignatureAlgorithm alg) throws InvalidKeyException; /** * Signs the constructed JWT with the specified key using the specified algorithm, producing a JWS. @@ -518,7 +518,7 @@ public interface JwtBuilder extends ClaimsMutator { * @see SignatureAlgorithms#forSigningKey(Key) * @since JJWT_RELEASE_VERSION */ - JwtBuilder signWith(Key key, io.jsonwebtoken.security.SignatureAlgorithm alg) throws InvalidKeyException; + T signWith(K key, io.jsonwebtoken.security.SignatureAlgorithm alg) throws InvalidKeyException; /** * Compresses the JWT body using the specified {@link CompressionCodec}. @@ -543,7 +543,7 @@ public interface JwtBuilder extends ClaimsMutator { * @see io.jsonwebtoken.CompressionCodecs * @since 0.6.0 */ - JwtBuilder compressWith(CompressionCodec codec); + T compressWith(CompressionCodec codec); /** * Perform Base64Url encoding with the specified Encoder. @@ -555,7 +555,7 @@ public interface JwtBuilder extends ClaimsMutator { * @return the builder for method chaining. * @since 0.10.0 */ - JwtBuilder base64UrlEncodeWith(Encoder base64UrlEncoder); + T base64UrlEncodeWith(Encoder base64UrlEncoder); /** * Performs object-to-JSON serialization with the specified Serializer. This is used by the builder to convert @@ -569,7 +569,7 @@ public interface JwtBuilder extends ClaimsMutator { * @return the builder for method chaining. * @since 0.10.0 */ - JwtBuilder serializeToJsonWith(Serializer> serializer); + T serializeToJsonWith(Serializer> serializer); /** * Actually builds the JWT and serializes it to a compact, URL-safe string according to the diff --git a/api/src/main/java/io/jsonwebtoken/JwtHandler.java b/api/src/main/java/io/jsonwebtoken/JwtHandler.java index 0e23f8339..621cdf2e8 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtHandler.java +++ b/api/src/main/java/io/jsonwebtoken/JwtHandler.java @@ -31,7 +31,7 @@ public interface JwtHandler { * @param jwt the parsed plaintext JWT * @return any object to be used after inspecting the JWT, or {@code null} if no return value is necessary. */ - T onPlaintextJwt(Jwt jwt); + T onPlaintextJwt(Jwt jwt); /** * This method is invoked when a {@link io.jsonwebtoken.JwtParser JwtParser} determines that the parsed JWT is @@ -40,7 +40,7 @@ public interface JwtHandler { * @param jwt the parsed claims JWT * @return any object to be used after inspecting the JWT, or {@code null} if no return value is necessary. */ - T onClaimsJwt(Jwt jwt); + T onClaimsJwt(Jwt jwt); /** * This method is invoked when a {@link io.jsonwebtoken.JwtParser JwtParser} determines that the parsed JWT is @@ -65,4 +65,14 @@ public interface JwtHandler { */ T onClaimsJws(Jws jws); + /** + * @since JJWT_RELEASE_VERSION + */ + T onPlaintextJwe(Jwe jwe); + + /** + * @since JJWT_RELEASE_VERSION + */ + T onClaimsJwe(Jwe jwe); + } diff --git a/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java b/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java index 749488372..c21485a80 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java @@ -31,22 +31,32 @@ public class JwtHandlerAdapter implements JwtHandler { @Override - public T onPlaintextJwt(Jwt jwt) { + public T onPlaintextJwt(Jwt jwt) { throw new UnsupportedJwtException("Unsigned plaintext JWTs are not supported."); } @Override - public T onClaimsJwt(Jwt jwt) { + public T onClaimsJwt(Jwt jwt) { throw new UnsupportedJwtException("Unsigned Claims JWTs are not supported."); } @Override public T onPlaintextJws(Jws jws) { - throw new UnsupportedJwtException("Signed plaintext JWSs are not supported."); + throw new UnsupportedJwtException("Signed plaintext JWTs are not supported."); } @Override public T onClaimsJws(Jws jws) { - throw new UnsupportedJwtException("Signed Claims JWSs are not supported."); + throw new UnsupportedJwtException("Signed Claims JWTs are not supported."); + } + + @Override + public T onPlaintextJwe(Jwe jwe) { + throw new UnsupportedJwtException("Encrypted plaintext JWEs are not supported."); + } + + @Override + public T onClaimsJwe(Jwe jwe) { + throw new UnsupportedJwtException("Encrypted Claims JWEs are not supported."); } } diff --git a/api/src/main/java/io/jsonwebtoken/JwtParser.java b/api/src/main/java/io/jsonwebtoken/JwtParser.java index 7e86b9113..25fa7222b 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParser.java @@ -30,14 +30,14 @@ */ public interface JwtParser { - public static final char SEPARATOR_CHAR = '.'; + char SEPARATOR_CHAR = '.'; /** * Ensures that the specified {@code jti} exists in the parsed JWT. If missing or if the parsed * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param id + * @param id the id to assert exists * @return the parser method for chaining. * @see MissingClaimException * @see IncorrectClaimException @@ -54,7 +54,7 @@ public interface JwtParser { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param subject + * @param subject the subject value to assert * @return the parser for method chaining. * @see MissingClaimException * @see IncorrectClaimException @@ -71,7 +71,7 @@ public interface JwtParser { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param audience + * @param audience the tag value to assert * @return the parser for method chaining. * @see MissingClaimException * @see IncorrectClaimException @@ -88,7 +88,7 @@ public interface JwtParser { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param issuer + * @param issuer the issuer value to assert * @return the parser for method chaining. * @see MissingClaimException * @see IncorrectClaimException @@ -105,7 +105,7 @@ public interface JwtParser { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param issuedAt + * @param issuedAt the {@code iat} value to assert. * @return the parser for method chaining. * @see MissingClaimException * @see IncorrectClaimException @@ -122,7 +122,7 @@ public interface JwtParser { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param expiration + * @param expiration the {@code exp} value to assert. * @return the parser for method chaining. * @see MissingClaimException * @see IncorrectClaimException @@ -139,7 +139,7 @@ public interface JwtParser { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param notBefore + * @param notBefore the {@code nbf} value to assert. * @return the parser for method chaining * @see MissingClaimException * @see IncorrectClaimException @@ -156,8 +156,8 @@ public interface JwtParser { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param claimName - * @param value + * @param claimName the name of the claim to assert + * @param value the value of the claim to assert * @return the parser for method chaining. * @see MissingClaimException * @see IncorrectClaimException @@ -319,7 +319,8 @@ public interface JwtParser { * immutable JwtParser. *

    NOTE: this method will be removed before version 1.0 */ - @Deprecated + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated // TODO: remove for 1.0 JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver); /** @@ -394,11 +395,11 @@ public interface JwtParser { *

    Note that if you are reasonably sure that the token is signed, it is more efficient to attempt to * parse the token (and catching exceptions if necessary) instead of calling this method first before parsing.

    * - * @param jwt the compact serialized JWT to check + * @param compact the compact serialized JWT to check * @return {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false} * otherwise. */ - boolean isSigned(String jwt); + boolean isSigned(String compact); /** * Parses the specified compact serialized JWT string based on the builder's current configuration state and @@ -425,7 +426,7 @@ public interface JwtParser { * @see #parsePlaintextJws(String) * @see #parseClaimsJws(String) */ - Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; /** * Parses the specified compact serialized JWT string based on the builder's current configuration state and @@ -503,7 +504,7 @@ T parse(String jwt, JwtHandler handler) * @see #parse(String) * @since 0.2 */ - Jwt parsePlaintextJwt(String plaintextJwt) + > Jwt parsePlaintextJwt(String plaintextJwt) throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; /** @@ -534,7 +535,7 @@ Jwt parsePlaintextJwt(String plaintextJwt) * @see #parse(String) * @since 0.2 */ - Jwt parseClaimsJwt(String claimsJwt) + > Jwt parseClaimsJwt(String claimsJwt) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; /** @@ -593,4 +594,14 @@ Jws parsePlaintextJws(String plaintextJws) */ Jws parseClaimsJws(String claimsJws) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + + /** + * @since JJWT_RELEASE_VERSION + */ + Jwe parsePlaintextJwe(String plaintextJwe) throws JwtException; + + /** + * @since JJWT_RELEASE_VERSION + */ + Jwe parseClaimsJwe(String claimsJwe) throws JwtException; } diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index 0e9d85ae9..875086ea0 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -17,10 +17,13 @@ import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.SignatureAlgorithm; import java.security.Key; import java.security.Provider; -import java.security.SecureRandom; +import java.util.Collection; import java.util.Date; import java.util.Map; @@ -53,7 +56,7 @@ public interface JwtParserBuilder { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param id + * @param id {@code jti} value * @return the parser builder for method chaining. * @see MissingClaimException * @see IncorrectClaimException @@ -65,7 +68,7 @@ public interface JwtParserBuilder { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param subject + * @param subject the required subject value * @return the parser builder for method chaining. * @see MissingClaimException * @see IncorrectClaimException @@ -77,7 +80,7 @@ public interface JwtParserBuilder { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param audience + * @param audience the required audience value * @return the parser builder for method chaining. * @see MissingClaimException * @see IncorrectClaimException @@ -89,7 +92,7 @@ public interface JwtParserBuilder { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param issuer + * @param issuer the required issuer value * @return the parser builder for method chaining. * @see MissingClaimException * @see IncorrectClaimException @@ -101,7 +104,7 @@ public interface JwtParserBuilder { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param issuedAt + * @param issuedAt the required issuedAt value * @return the parser builder for method chaining. * @see MissingClaimException * @see IncorrectClaimException @@ -113,7 +116,7 @@ public interface JwtParserBuilder { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param expiration + * @param expiration the required expiration value * @return the parser builder for method chaining. * @see MissingClaimException * @see IncorrectClaimException @@ -125,7 +128,7 @@ public interface JwtParserBuilder { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param notBefore + * @param notBefore the required not before {@code nbf} value. * @return the parser builder for method chaining * @see MissingClaimException * @see IncorrectClaimException @@ -137,8 +140,8 @@ public interface JwtParserBuilder { * value does not equal the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * - * @param claimName - * @param value + * @param claimName the name of the claim to require + * @param value the value the claim value must equal * @return the parser builder for method chaining. * @see MissingClaimException * @see IncorrectClaimException @@ -218,25 +221,93 @@ public interface JwtParserBuilder { * @param base64EncodedSecretKey the BASE64-encoded algorithm-specific signature verification key to use to validate * any discovered JWS digital signature. * @return the parser builder for method chaining. + * @deprecated as of 0.10.0. */ + @Deprecated JwtParserBuilder setSigningKey(String base64EncodedSecretKey); /** - * Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not - * a JWS (no signature), this key is not used. + * Sets the signature verification key used to verify all encountered JWS signatures. If the encountered JWT + * string is not a JWS (e.g. unsigned or a JWE), this key is not used. + *

    + *

    This is a convenience method to use in specific circumstances: when the parser will only ever encounter + * JWSs with signatures that can always be verified by a single key. This also implies that this key + * MUST be a valid key for the signature algorithm ({@code alg} header) used for the JWS.

    + *

    + *

    If there is any chance that the parser will encounter JWSs + * that need different signature verification keys based on the JWS being parsed, it is strongly + * recommended to configure your own {@link Locator Locator} via the + * {@link #setKeyLocator(Locator) setKeyLocator} method instead of using this one.

    + *

    + *

    Calling this method overrides any previously set signature verification key.

    + * + * @param key the algorithm-specific signature verification key to use to verify all encountered JWS digital + * signatures. + * @return the parser builder for method chaining. + */ + JwtParserBuilder setSigningKey(Key key); + + /** + * Sets the decryption key to be used to decrypt all encountered JWEs. If the encountered JWT string is not a + * JWE (e.g. a JWS), this key is not used. + *

    + *

    This is a convenience method to use in specific circumstances: when the parser will only ever encounter + * JWEs that can always be decrypted by a single key. This also implies that this key MUST be a valid + * key for both the key management algorithm ({@code alg} header) and the content encryption algorithm + * ({@code enc} header) used for the JWE.

    + *

    + *

    If there is any chance that the parser will encounter JWEs + * that need different decryption keys based on the JWE being parsed, it is strongly recommended to configure + * your own {@link Locator Locator} via the {@link #setKeyLocator(Locator) setKeyLocator} method instead of + * using this one.

    + *

    + *

    Calling this method overrides any previously set decryption key.

    + * @param key the algorithm-specific decryption key to use to decrypt all encountered JWEs. + * @return the parser builder for method chaining. + */ + JwtParserBuilder decryptWith(Key key); + + /** + * Sets the {@link Locator} used to acquire any signature verification or decryption key needed during parsing. + *
      + *
    • If the parsed String is a JWS, the {@code Locator} will be called to find the appropriate key + * necessary to verify the JWS signature.
    • + *
    • If the parsed String is a JWE, it will be called to find the appropriate decryption key.
    • + *
    *

    - *

    Note that this key MUST be a valid key for the signature algorithm found in the JWT header - * (as the {@code alg} header parameter).

    + *

    Specifying a key {@code Locator} is necessary when the signing or decryption key is not already known before + * parsing the JWT and the JWT header must be inspected first to determine how to + * look up the verification or decryption key. Once returned by the locator, the JwtParser will then either + * verify the JWS signature or decrypt the JWE payload with the returned key. For example:

    *

    - *

    This method overwrites any previously set key.

    + *
    +     * Jws<Claims> jws = Jwts.parser().setKeyLocator(new Locator<Header,Key>() {
    +     *         @Override
    +     *         public Key locate(Header header) {
    +     *             if (header instanceof JwsHeader) {
    +     *                 return getSignatureVerificationKey((JwsHeader)header); // implement me
    +     *             } else {
    +     *                 return getDecryptionKey((JweHeader)header); // implement me
    +     *             }
    +     *         }})
    +     *     .parseClaimsJws(compact);
    +     * 
    + *

    + *

    A Key {@code Locator} is invoked once during parsing before performing decryption or signature verification.

    * - * @param key the algorithm-specific signature verification key to use to validate any discovered JWS digital - * signature. + * @param keyLocator the locator used to retrieve decryption or signature verification keys. * @return the parser builder for method chaining. + * @since JJWT_RELEASE_VERSION */ - JwtParserBuilder setSigningKey(Key key); + JwtParserBuilder setKeyLocator(Locator, Key> keyLocator); /** + *

    Deprecation Notice

    + *

    This method has been deprecated as of JJWT version JJWT_RELEASE_VERSION because it only supports key location + * for JWSs (signed JWTs) instead of both signed (JWS) and encrypted (JWE) scenarios. Use the + * {@link #setKeyLocator(Locator) setKeyLocator} method instead to ensure a locator that can work for both JWS and + * JWE inputs. This method will be removed for the 1.0 release.

    + *

    Previous Documentation

    * Sets the {@link SigningKeyResolver} used to acquire the signing key that should be used to verify * a JWS's signature. If the parsed String is not a JWS (no signature), this resolver is not used. *

    @@ -260,11 +331,20 @@ public interface JwtParserBuilder { *

    This method should only be used if a signing key is not provided by the other {@code setSigningKey*} builder * methods.

    * + * @deprecated since JJWT_RELEASE_VERSION * @param signingKeyResolver the signing key resolver used to retrieve the signing key. * @return the parser builder for method chaining. */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated JwtParserBuilder setSigningKeyResolver(SigningKeyResolver signingKeyResolver); + JwtParserBuilder addEncryptionAlgorithms(Collection encAlgs); + + JwtParserBuilder addSignatureAlgorithms(Collection> sigAlgs); + + JwtParserBuilder addKeyAlgorithms(Collection> keyAlgs); + /** * Sets the {@link CompressionCodecResolver} used to acquire the {@link CompressionCodec} that should be used to * decompress the JWT body. If the parsed JWT is not compressed, this resolver is not used. diff --git a/api/src/main/java/io/jsonwebtoken/Jwts.java b/api/src/main/java/io/jsonwebtoken/Jwts.java index d9f62db44..4dc5643d7 100644 --- a/api/src/main/java/io/jsonwebtoken/Jwts.java +++ b/api/src/main/java/io/jsonwebtoken/Jwts.java @@ -27,6 +27,7 @@ */ public final class Jwts { + @SuppressWarnings("rawtypes") private static final Class[] MAP_ARG = new Class[]{Map.class}; private Jwts() { @@ -39,7 +40,7 @@ private Jwts() { * * @return a new {@link Header} instance suitable for plaintext (not digitally signed) JWTs. */ - public static Header header() { + public static Header header() { return Classes.newInstance("io.jsonwebtoken.impl.DefaultHeader"); } @@ -50,7 +51,7 @@ public static Header header() { * * @return a new {@link Header} instance suitable for plaintext (not digitally signed) JWTs. */ - public static Header header(Map header) { + public static Header header(Map header) { return Classes.newInstance("io.jsonwebtoken.impl.DefaultHeader", MAP_ARG, header); } @@ -161,7 +162,18 @@ public static JwtParserBuilder parserBuilder() { * @return a new {@link JwtBuilder} instance that can be configured and then used to create JWT compact serialized * strings. */ - public static JwtBuilder builder() { + public static JwtBuilder builder() { return Classes.newInstance("io.jsonwebtoken.impl.DefaultJwtBuilder"); } + + /** + * Returns a new {@link JweBuilder} instance that can be configured and then used to create encrypted JWT compact + * serialized strings. + * + * @return a new {@link JweBuilder} instance that can be configured and then used to create encrypted JWT compact + * serialized strings. + */ + public static JweBuilder jweBuilder() { + return Classes.newInstance("io.jsonwebtoken.impl.DefaultJweBuilder"); + } } diff --git a/api/src/main/java/io/jsonwebtoken/Locator.java b/api/src/main/java/io/jsonwebtoken/Locator.java new file mode 100644 index 000000000..38a70b60f --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/Locator.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface Locator, R> { + + R locate(H header); +} diff --git a/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java b/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java new file mode 100644 index 000000000..753d1966a --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken; + +import io.jsonwebtoken.lang.Assert; + +/** + * @since JJWT_RELEASE_VERSION + */ +public abstract class LocatorAdapter, R> implements Locator { + + @Override + public final R locate(H header) { + Assert.notNull(header, "Header cannot be null."); + if (header instanceof JwsHeader) { + return locate((JwsHeader) header); + } else if (header instanceof JweHeader) { + return locate((JweHeader) header); + } else { + return doLocate(header); + } + } + + protected R locate(JweHeader header) { + return null; + } + + protected R locate(JwsHeader header) { + return null; + } + + protected R doLocate(Header header) { + return null; + } +} diff --git a/api/src/main/java/io/jsonwebtoken/Named.java b/api/src/main/java/io/jsonwebtoken/Named.java deleted file mode 100644 index 0bd6064ce..000000000 --- a/api/src/main/java/io/jsonwebtoken/Named.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.jsonwebtoken; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface Named { - - /** - * Returns the string name of the associated object. - * - * @return the string name of the associated object. - */ - String getName(); -} diff --git a/api/src/main/java/io/jsonwebtoken/SignatureException.java b/api/src/main/java/io/jsonwebtoken/SignatureException.java index 12a2e92d7..365ec34d7 100644 --- a/api/src/main/java/io/jsonwebtoken/SignatureException.java +++ b/api/src/main/java/io/jsonwebtoken/SignatureException.java @@ -15,16 +15,16 @@ */ package io.jsonwebtoken; -import io.jsonwebtoken.security.CryptoException; +import io.jsonwebtoken.security.SecurityException; /** * Exception indicating that either calculating a signature or verifying an existing signature of a JWT failed. * * @since 0.1 - * @deprecated in favor of {@link io.jsonwebtoken.security.SecurityException}; this class will be removed before 1.0 + * @deprecated in favor of {@link io.jsonwebtoken.security.SignatureException}; this class will be removed before 1.0 */ @Deprecated -public class SignatureException extends CryptoException { +public class SignatureException extends SecurityException { public SignatureException(String message) { super(message); diff --git a/api/src/main/java/io/jsonwebtoken/SigningKeyResolver.java b/api/src/main/java/io/jsonwebtoken/SigningKeyResolver.java index fbd9887f9..502b2078f 100644 --- a/api/src/main/java/io/jsonwebtoken/SigningKeyResolver.java +++ b/api/src/main/java/io/jsonwebtoken/SigningKeyResolver.java @@ -44,9 +44,11 @@ * the {@link io.jsonwebtoken.SigningKeyResolverAdapter} and overriding only the method you need to support instead of * implementing this interface directly.

    * - * @see io.jsonwebtoken.SigningKeyResolverAdapter * @since 0.4 + * @deprecated since JJWT_RELEASE_VERSION. Implement {@link io.jsonwebtoken.Locator Locator} instead. + * @see io.jsonwebtoken.JwtParserBuilder#setKeyLocator(Locator) */ +@Deprecated public interface SigningKeyResolver { /** diff --git a/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java b/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java index 1be7ec556..7142aa3a9 100644 --- a/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java @@ -21,10 +21,19 @@ import java.security.Key; /** + *

    Deprecation Notice

    + *

    As of JJWT JJWT_RELEASE_VERSION, various Resolver concepts (including the {@code SigningKeyResolver}) have been + * unified into a single {@link Locator} interface. For key location, (for both signing and encryption keys), + * use the {@link JwtParserBuilder#setKeyLocator(Locator)} to configure a parser with your desired Key locator instead + * of using a {@code SigningKeyResolver}. Also see {@link LocatorAdapter} for the Adapter pattern parallel of this + * class. This {@code SigningKeyResolverAdapter} class will be removed before the 1.0 release.

    + * + *

    Previous Documentation

    * An
    Adapter implementation of the * {@link SigningKeyResolver} interface that allows subclasses to process only the type of JWS body that * is known/expected for a particular case. * + *

    Previous Documentation

    *

    The {@link #resolveSigningKey(JwsHeader, Claims)} and {@link #resolveSigningKey(JwsHeader, String)} method * implementations delegate to the * {@link #resolveSigningKeyBytes(JwsHeader, Claims)} and {@link #resolveSigningKeyBytes(JwsHeader, String)} methods @@ -36,17 +45,22 @@ * are not overridden, one (or both) of the *KeyBytes variants must be overridden depending on your expected * use case. You do not have to override any method that does not represent an expected condition.

    * + * @see io.jsonwebtoken.JwtParserBuilder#setKeyLocator(Locator) + * @see LocatorAdapter * @since 0.4 + * @deprecated since JJWT_RELEASE_VERSION. Use {@link LocatorAdapter LocatorAdapter} with + * {@link JwtParserBuilder#setKeyLocator(Locator)} */ +@Deprecated public class SigningKeyResolverAdapter implements SigningKeyResolver { @Override public Key resolveSigningKey(JwsHeader header, Claims claims) { SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, Claims) implementation cannot be " + - "used for asymmetric key algorithms (RSA, Elliptic Curve). " + - "Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " + - "Key instance appropriate for the " + alg.name() + " algorithm."); + "used for asymmetric key algorithms (RSA, Elliptic Curve). " + + "Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " + + "Key instance appropriate for the " + alg.name() + " algorithm."); byte[] keyBytes = resolveSigningKeyBytes(header, claims); return new SecretKeySpec(keyBytes, alg.getJcaName()); } @@ -55,9 +69,9 @@ public Key resolveSigningKey(JwsHeader header, Claims claims) { public Key resolveSigningKey(JwsHeader header, String plaintext) { SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, String) implementation cannot be " + - "used for asymmetric key algorithms (RSA, Elliptic Curve). " + - "Override the resolveSigningKey(JwsHeader, String) method instead and return a " + - "Key instance appropriate for the " + alg.name() + " algorithm."); + "used for asymmetric key algorithms (RSA, Elliptic Curve). " + + "Override the resolveSigningKey(JwsHeader, String) method instead and return a " + + "Key instance appropriate for the " + alg.name() + " algorithm."); byte[] keyBytes = resolveSigningKeyBytes(header, plaintext); return new SecretKeySpec(keyBytes, alg.getJcaName()); } @@ -76,9 +90,9 @@ public Key resolveSigningKey(JwsHeader header, String plaintext) { */ public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + - "Claims JWS signing key resolution. Consider overriding either the " + - "resolveSigningKey(JwsHeader, Claims) method or, for HMAC algorithms, the " + - "resolveSigningKeyBytes(JwsHeader, Claims) method."); + "Claims JWS signing key resolution. Consider overriding either the " + + "resolveSigningKey(JwsHeader, Claims) method or, for HMAC algorithms, the " + + "resolveSigningKeyBytes(JwsHeader, Claims) method."); } /** @@ -86,14 +100,14 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { * key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must * override this method or the {@link #resolveSigningKey(JwsHeader, String)} method instead. * - * @param header the parsed {@link JwsHeader} + * @param header the parsed {@link JwsHeader} * @param payload the parsed String plaintext payload * @return the signing key bytes to use to verify the JWS signature. */ public byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + - "plaintext JWS signing key resolution. Consider overriding either the " + - "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + - "resolveSigningKeyBytes(JwsHeader, String) method."); + "plaintext JWS signing key resolution. Consider overriding either the " + + "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + + "resolveSigningKeyBytes(JwsHeader, String) method."); } } diff --git a/api/src/main/java/io/jsonwebtoken/lang/Arrays.java b/api/src/main/java/io/jsonwebtoken/lang/Arrays.java index 6b58a376e..9c0cec8ad 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Arrays.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Arrays.java @@ -15,6 +15,8 @@ */ package io.jsonwebtoken.lang; +import java.util.List; + /** * @since 0.6 */ @@ -22,6 +24,14 @@ public final class Arrays { private Arrays(){} //prevent instantiation + public static int length(T[] a) { + return a == null ? 0 : a.length; + } + + public static List asList(T[] a) { + return a == null ? Collections.emptyList() : java.util.Arrays.asList(a); + } + public static int length(byte[] bytes) { return bytes != null ? bytes.length : 0; } diff --git a/api/src/main/java/io/jsonwebtoken/lang/Assert.java b/api/src/main/java/io/jsonwebtoken/lang/Assert.java index ab6dd2fd4..2068d8d34 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Assert.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Assert.java @@ -128,10 +128,11 @@ public static void hasLength(String text) { * @param message the exception message to use if the assertion fails * @see Strings#hasText */ - public static void hasText(String text, String message) { + public static String hasText(String text, String message) { if (!Strings.hasText(text)) { throw new IllegalArgumentException(message); } + return text; } /** @@ -204,6 +205,13 @@ public static byte[] notEmpty(byte[] array, String msg) { return array; } + public static char[] notEmpty(char[] chars, String msg) { + if (Objects.isEmpty(chars)) { + throw new IllegalArgumentException(msg); + } + return chars; + } + /** * Assert that an array has no null elements. * Note: Does not complain if the array is empty! @@ -241,10 +249,11 @@ public static void noNullElements(Object[] array) { * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the collection is null or has no elements */ - public static void notEmpty(Collection collection, String message) { + public static > T notEmpty(T collection, String message) { if (Collections.isEmpty(collection)) { throw new IllegalArgumentException(message); } + return collection; } /** @@ -267,10 +276,11 @@ public static void notEmpty(Collection collection) { * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the map is null or has no entries */ - public static void notEmpty(Map map, String message) { + public static > T notEmpty(T map, String message) { if (Collections.isEmpty(map)) { throw new IllegalArgumentException(message); } + return map; } /** @@ -309,13 +319,14 @@ public static void isInstanceOf(Class clazz, Object obj) { * @throws IllegalArgumentException if the object is not an instance of clazz * @see Class#isInstance */ - public static void isInstanceOf(Class type, Object obj, String message) { + public static T isInstanceOf(Class type, Object obj, String message) { notNull(type, "Type to check against must not be null"); if (!type.isInstance(obj)) { throw new IllegalArgumentException(message + "Object of class [" + (obj != null ? obj.getClass().getName() : "null") + "] must be an instance of " + type); } + return type.cast(obj); } /** diff --git a/api/src/main/java/io/jsonwebtoken/lang/Classes.java b/api/src/main/java/io/jsonwebtoken/lang/Classes.java index 27d4d18c2..544350007 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Classes.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Classes.java @@ -189,15 +189,28 @@ public static T instantiate(Constructor ctor, Object... args) { /** * @since 0.10.0 */ + public static T invokeStatic(String fqcn, String methodName, Class[] argTypes, Object... args) { + try { + Class clazz = Classes.forName(fqcn); + return invokeStatic(clazz, methodName, argTypes, args); + } catch (Exception e) { + String msg = "Unable to invoke class method " + fqcn + "#" + methodName + ". Ensure the necessary " + + "implementation is in the runtime classpath."; + throw new IllegalStateException(msg, e); + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ @SuppressWarnings("unchecked") - public static T invokeStatic(String fqcn, String methodName, Class[] argTypes, Object... args) { + public static T invokeStatic(Class clazz, String methodName, Class[] argTypes, Object... args) { try { - Class clazz = Classes.forName(fqcn); Method method = clazz.getDeclaredMethod(methodName, argTypes); method.setAccessible(true); return(T)method.invoke(null, args); } catch (Exception e) { - String msg = "Unable to invoke class method " + fqcn + "#" + methodName + ". Ensure the necessary " + + String msg = "Unable to invoke class method " + clazz.getName() + "#" + methodName + ". Ensure the necessary " + "implementation is in the runtime classpath."; throw new IllegalStateException(msg, e); } diff --git a/api/src/main/java/io/jsonwebtoken/lang/Collections.java b/api/src/main/java/io/jsonwebtoken/lang/Collections.java index 167f23f2b..439271b5a 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Collections.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Collections.java @@ -20,9 +20,11 @@ import java.util.Collection; import java.util.Enumeration; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; public final class Collections { @@ -32,6 +34,14 @@ public static List emptyList() { return java.util.Collections.emptyList(); } + public static Set emptySet() { + return java.util.Collections.emptySet(); + } + + public static Map emptyMap() { + return java.util.Collections.emptyMap(); + } + public static List of(T... elements) { if (elements == null || elements.length == 0) { return java.util.Collections.emptyList(); @@ -39,6 +49,14 @@ public static List of(T... elements) { return java.util.Collections.unmodifiableList(Arrays.asList(elements)); } + public static Set setOf(T... elements) { + if (elements == null || elements.length == 0) { + return java.util.Collections.emptySet(); + } + Set set = new LinkedHashSet<>(Arrays.asList(elements)); + return java.util.Collections.unmodifiableSet(set); + } + /** * Return true if the supplied Collection is null * or empty. Otherwise, return false. diff --git a/api/src/main/java/io/jsonwebtoken/lang/Objects.java b/api/src/main/java/io/jsonwebtoken/lang/Objects.java index 4960b7355..2b84b88e4 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Objects.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Objects.java @@ -22,16 +22,17 @@ public final class Objects { - private Objects(){} //prevent instantiation + private Objects() { + } //prevent instantiation private static final int INITIAL_HASH = 7; - private static final int MULTIPLIER = 31; + private static final int MULTIPLIER = 31; - private static final String EMPTY_STRING = ""; - private static final String NULL_STRING = "null"; - private static final String ARRAY_START = "{"; - private static final String ARRAY_END = "}"; - private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END; + private static final String EMPTY_STRING = ""; + private static final String NULL_STRING = "null"; + private static final String ARRAY_START = "{"; + private static final String ARRAY_END = "}"; + private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END; private static final String ARRAY_ELEMENT_SEPARATOR = ", "; /** @@ -102,6 +103,16 @@ public static boolean isEmpty(byte[] array) { return array == null || array.length == 0; } + /** + * Returns {@code true} if the specified character array is null or of zero length, {@code false} otherwise. + * + * @param chars the character array to check + * @return {@code true} if the specified character array is null or of zero length, {@code false} otherwise. + */ + public static boolean isEmpty(char[] chars) { + return chars == null || chars.length == 0; + } + /** * Check whether the given array contains the given element. * @@ -171,7 +182,7 @@ public static > E caseInsensitiveValueOf(E[] enumValues, Strin } throw new IllegalArgumentException( String.format("constant [%s] does not exist in enum type %s", - constant, enumValues.getClass().getComponentType().getName())); + constant, enumValues.getClass().getComponentType().getName())); } /** @@ -347,7 +358,7 @@ public static int nullSafeHashCode(Object obj) { * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. */ - public static int nullSafeHashCode(Object[] array) { + public static int nullSafeHashCode(Object... array) { if (array == null) { return 0; } diff --git a/api/src/main/java/io/jsonwebtoken/lang/Strings.java b/api/src/main/java/io/jsonwebtoken/lang/Strings.java index 541ac29e1..ec0d76c26 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Strings.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Strings.java @@ -16,6 +16,7 @@ package io.jsonwebtoken.lang; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -41,7 +42,7 @@ public final class Strings { private static final char EXTENSION_SEPARATOR = '.'; - public static final Charset UTF_8 = Charset.forName("UTF-8"); + public static final Charset UTF_8 = StandardCharsets.UTF_8; private Strings(){} //prevent instantiation @@ -194,6 +195,34 @@ public static CharSequence clean(CharSequence str) { return str; } + public static String toBinary(byte b) { + String bString = Integer.toBinaryString(b & 0xFF); + return String.format("%8s", bString).replace((char)Character.SPACE_SEPARATOR, '0'); + } + + public static String toBinary(byte[] bytes) { + StringBuilder sb = new StringBuilder(19); //16 characters + 3 space characters + for(byte b : bytes) { + if (sb.length() > 0) { + sb.append((char)Character.SPACE_SEPARATOR); + } + String val = toBinary(b); + sb.append(val); + } + return sb.toString(); + } + + public static String toHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte temp : bytes) { + if (result.length() > 0) { + result.append((char)Character.SPACE_SEPARATOR); + } + result.append(String.format("%02x", temp)); + } + return result.toString(); + } + /** * Trim all whitespace from the given String: * leading, trailing, and intermediate characters. diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java new file mode 100644 index 000000000..50aa77034 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.Identifiable; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AeadAlgorithm extends Identifiable, SecretKeyGenerator { + + AeadResult encrypt(AeadRequest request) throws SecurityException; + + PayloadSupplier decrypt(DecryptAeadRequest request) throws SecurityException; +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadEncryptionAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AeadEncryptionAlgorithm.java deleted file mode 100644 index f903377f1..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/AeadEncryptionAlgorithm.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jsonwebtoken.security; - -import java.security.Key; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface AeadEncryptionAlgorithm, ERes extends AeadEncryptionResult, DReq extends AeadDecryptionRequest> extends EncryptionAlgorithm { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadIvEncryptionResult.java b/api/src/main/java/io/jsonwebtoken/security/AeadIvEncryptionResult.java deleted file mode 100644 index 8524f1d57..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/AeadIvEncryptionResult.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface AeadIvEncryptionResult extends IvEncryptionResult, AeadEncryptionResult { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadIvRequest.java b/api/src/main/java/io/jsonwebtoken/security/AeadIvRequest.java deleted file mode 100644 index 05159ef23..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/AeadIvRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jsonwebtoken.security; - -import java.security.Key; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface AeadIvRequest extends IvRequest, AeadDecryptionRequest { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java b/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java index 8112813fe..6e4306020 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 jsonwebtoken.io + * Copyright (C) 2021 jsonwebtoken.io * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,10 @@ */ package io.jsonwebtoken.security; -import java.security.Key; +import javax.crypto.SecretKey; /** * @since JJWT_RELEASE_VERSION */ -public interface AeadRequest extends CryptoRequest, AssociatedDataSource { - +public interface AeadRequest extends CryptoRequest, AssociatedDataSupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadResult.java b/api/src/main/java/io/jsonwebtoken/security/AeadResult.java new file mode 100644 index 000000000..999987a6a --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AeadResult.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AeadResult extends PayloadSupplier, DigestSupplier, InitializationVectorSupplier { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadSymmetricEncryptionAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AeadSymmetricEncryptionAlgorithm.java deleted file mode 100644 index 4e74987f7..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/AeadSymmetricEncryptionAlgorithm.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.jsonwebtoken.security; - -import javax.crypto.SecretKey; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface AeadSymmetricEncryptionAlgorithm extends - SymmetricEncryptionAlgorithm, AeadIvEncryptionResult, AeadIvRequest>, - AeadEncryptionAlgorithm, AeadIvEncryptionResult, AeadIvRequest> { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSource.java b/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSupplier.java similarity index 89% rename from api/src/main/java/io/jsonwebtoken/security/AssociatedDataSource.java rename to api/src/main/java/io/jsonwebtoken/security/AssociatedDataSupplier.java index 0acd5555d..9960988be 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSource.java +++ b/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 jsonwebtoken.io + * Copyright (C) 2021 jsonwebtoken.io * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface AssociatedDataSource { +public interface AssociatedDataSupplier { byte[] getAssociatedData(); diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java new file mode 100644 index 000000000..767dee4f5 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.net.URI; +import java.security.Key; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AsymmetricJwk extends Jwk { + + String getPublicKeyUse(); + + URI getX509Url(); + + List getX509CertificateChain(); + + byte[] getX509CertificateSha1Thumbprint(); + + byte[] getX509CertificateSha256Thumbprint(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java new file mode 100644 index 000000000..bfa1f3521 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.net.URI; +import java.security.Key; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AsymmetricJwkBuilder, T extends AsymmetricJwkBuilder> extends JwkBuilder { + + T setPublicKeyUse(String use); + + T setX509CertificateChain(List chain); + + T setX509Url(URI uri); + + T withX509KeyUse(boolean enable); + + T withX509Sha1Thumbprint(boolean enable); + + T withX509Sha256Thumbprint(boolean enable); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyAlgorithm.java deleted file mode 100644 index 84624225f..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyAlgorithm.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.jsonwebtoken.security; - -import java.security.KeyPair; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface AsymmetricKeyAlgorithm { - - /** - * Generates a new secure-random key pair with a key length suitable for this Algorithm. - * - * @return a new secure-random key pair with a key length suitable for this Algorithm. - */ - KeyPair generateKeyPair(); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyGenerator.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyGenerator.java new file mode 100644 index 000000000..96c00f82b --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyGenerator.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.KeyPair; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface AsymmetricKeyGenerator { + + /** + * Generates a new secure-random key pair with a key length suitable for the associated Algorithm. + * + * @return a new secure-random key pair with a key length suitable for the associated Algorithm. + */ + KeyPair generateKeyPair(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java index 93279d698..314acfef8 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java @@ -1,7 +1,25 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; +import java.security.PrivateKey; +import java.security.PublicKey; + /** * @since JJWT_RELEASE_VERSION */ -public interface AsymmetricKeySignatureAlgorithm extends SignatureAlgorithm, AsymmetricKeyAlgorithm { +public interface AsymmetricKeySignatureAlgorithm extends SignatureAlgorithm, AsymmetricKeyGenerator { } diff --git a/api/src/main/java/io/jsonwebtoken/security/CryptoMessage.java b/api/src/main/java/io/jsonwebtoken/security/CryptoMessage.java deleted file mode 100644 index 9d904c471..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/CryptoMessage.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface CryptoMessage { - - T getData(); //plaintext, ciphertext, or Key for key wrap algorithms - -} diff --git a/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java b/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java index 489dfe01b..afa9476d8 100644 --- a/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java @@ -1,36 +1,24 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; import java.security.Key; -import java.security.Provider; -import java.security.SecureRandom; /** * @since JJWT_RELEASE_VERSION */ -public interface CryptoRequest extends CryptoMessage { - - /** - * Returns the JCA provider that should be used for cryptographic operations during the request or - * {@code null} if the JCA subsystem preferred provider should be used. - * - * @return the JCA provider that should be used for cryptographic operations during the request or - * {@code null} if the JCA subsystem preferred provider should be used. - */ - Provider getProvider(); - - /** - * Returns the {@code SecureRandom} to use when performing cryptographic operations during the request, or - * {@code null} if a default {@link SecureRandom} should be used. - * - * @return the {@code SecureRandom} to use when performing cryptographic operations during the request, or - * {@code null} if a default {@link SecureRandom} should be used. - */ - SecureRandom getSecureRandom(); - - /** - * Returns the key to use for signing, wrapping, encryption or decryption depending on the type of request. - * - * @return the key to use for signing, wrapping, encryption or decryption depending on the type of request. - */ - K getKey(); +public interface CryptoRequest extends SecurityRequest, KeySupplier, PayloadSupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/CurveId.java b/api/src/main/java/io/jsonwebtoken/security/CurveId.java deleted file mode 100644 index 415783359..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/CurveId.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface CurveId { - - String toString(); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/CurveIds.java b/api/src/main/java/io/jsonwebtoken/security/CurveIds.java deleted file mode 100644 index fffefe506..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/CurveIds.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.jsonwebtoken.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Maps; -import io.jsonwebtoken.lang.Strings; - -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class CurveIds { - - public static final CurveId P256 = new DefaultCurveId("P-256"); - public static final CurveId P384 = new DefaultCurveId("P-384"); - public static final CurveId P521 = new DefaultCurveId("P-521"); // yes, this is supposed to be 521 and not 512 - - private static final Map STANDARD_IDS = Collections.unmodifiableMap(Maps - .of(P256.toString(), P256) - .and(P384.toString(), P384) - .and(P521.toString(), P521) - .build()); - - private static final Set STANDARD_IDS_SET = - Collections.unmodifiableSet(new LinkedHashSet<>(STANDARD_IDS.values())); - - public static Set values() { - return STANDARD_IDS_SET; - } - - public static boolean isStandard(CurveId curveId) { - return curveId != null && STANDARD_IDS.containsKey(curveId.toString()); - } - - public static CurveId forValue(String value) { - value = Strings.clean(value); - Assert.hasText(value, "value argument cannot be null or empty."); - CurveId std = STANDARD_IDS.get(value); - return std != null ? std : new DefaultCurveId(value); - } -} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadEncryptionResult.java b/api/src/main/java/io/jsonwebtoken/security/DecryptAeadRequest.java similarity index 81% rename from api/src/main/java/io/jsonwebtoken/security/AeadEncryptionResult.java rename to api/src/main/java/io/jsonwebtoken/security/DecryptAeadRequest.java index 292810620..c34e06b1a 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AeadEncryptionResult.java +++ b/api/src/main/java/io/jsonwebtoken/security/DecryptAeadRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 jsonwebtoken.io + * Copyright (C) 2021 jsonwebtoken.io * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,5 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface AeadEncryptionResult extends EncryptionResult, AuthenticationTagSource { - +public interface DecryptAeadRequest extends AeadRequest, InitializationVectorSupplier, DigestSupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadDecryptionRequest.java b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java similarity index 81% rename from api/src/main/java/io/jsonwebtoken/security/AeadDecryptionRequest.java rename to api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java index 4f9fa8261..08040646a 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AeadDecryptionRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 jsonwebtoken.io + * Copyright (C) 2021 jsonwebtoken.io * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,5 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface AeadDecryptionRequest extends AeadRequest, AuthenticationTagSource { - +public interface DecryptionKeyRequest extends KeyRequest, PayloadSupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyResolver.java b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyResolver.java deleted file mode 100644 index 9e2cbce78..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyResolver.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.jsonwebtoken.security; - -import io.jsonwebtoken.JweHeader; - -import java.security.Key; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface DecryptionKeyResolver { - - /** - * Returns the decryption key that should be used to decrypt a corresponding JWE's Ciphertext (payload). - * - * @param header the JWE header to inspect to determine which decryption key should be used - * @return the decryption key that should be used to decrypt a corresponding JWE's Ciphertext (payload). - */ - Key resolveDecryptionKey(JweHeader header); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/DefaultCurveId.java b/api/src/main/java/io/jsonwebtoken/security/DefaultCurveId.java deleted file mode 100644 index 479f768fb..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/DefaultCurveId.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.jsonwebtoken.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Strings; - -/** - * @since JJWT_RELEASE_VERSION - */ -final class DefaultCurveId implements CurveId { - - private final String id; - - DefaultCurveId(String id) { - id = Strings.clean(id); - Assert.hasText(id, "id argument cannot be null or empty."); - this.id = id; - } - - @Override - public int hashCode() { - return id.hashCode(); - } - - @Override - public boolean equals(Object obj) { - return obj == this || (obj instanceof CurveId && obj.toString().equals(this.id)); - } - - @Override - public String toString() { - return id; - } -} diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptionResult.java b/api/src/main/java/io/jsonwebtoken/security/DigestSupplier.java similarity index 86% rename from api/src/main/java/io/jsonwebtoken/security/EncryptionResult.java rename to api/src/main/java/io/jsonwebtoken/security/DigestSupplier.java index 62b76f6ca..fdc71f619 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EncryptionResult.java +++ b/api/src/main/java/io/jsonwebtoken/security/DigestSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 jsonwebtoken.io + * Copyright (C) 2021 jsonwebtoken.io * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,8 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface EncryptionResult { +public interface DigestSupplier { - byte[] getCiphertext(); + byte[] getDigest(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/EcJwk.java b/api/src/main/java/io/jsonwebtoken/security/EcJwk.java deleted file mode 100644 index eba5b1045..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/EcJwk.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface EcJwk extends Jwk, EcJwkMutator { - - CurveId getCurveId(); - - String getX(); - - String getY(); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/EcJwkBuilder.java deleted file mode 100644 index c0ae666f8..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/EcJwkBuilder.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface EcJwkBuilder extends JwkBuilder, EcJwkMutator { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcJwkBuilderFactory.java b/api/src/main/java/io/jsonwebtoken/security/EcJwkBuilderFactory.java deleted file mode 100644 index f6666e02f..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/EcJwkBuilderFactory.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface EcJwkBuilderFactory { - - PublicEcJwkBuilder publicKey(); - - PrivateEcJwkBuilder privateKey(); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcJwkMutator.java b/api/src/main/java/io/jsonwebtoken/security/EcJwkMutator.java deleted file mode 100644 index fad155ad9..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/EcJwkMutator.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface EcJwkMutator extends JwkMutator { - - T setCurveId(CurveId curveId); - - T setX(String x); - - T setY(String y); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/EcKeyAlgorithm.java new file mode 100644 index 000000000..ccb5567d4 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EcKeyAlgorithm.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.ECKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface EcKeyAlgorithm extends KeyAlgorithm { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java new file mode 100644 index 000000000..7ed6da5fd --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface EcPrivateJwk extends PrivateJwk { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwkBuilder.java new file mode 100644 index 000000000..fd9f4d60a --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwkBuilder.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface EcPrivateJwkBuilder extends PrivateJwkBuilder { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java b/api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java new file mode 100644 index 000000000..fb51aac29 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.interfaces.ECPublicKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface EcPublicJwk extends PublicJwk { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPublicJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/EcPublicJwkBuilder.java new file mode 100644 index 000000000..30de09f38 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EcPublicJwkBuilder.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface EcPublicJwkBuilder extends PublicJwkBuilder { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java new file mode 100644 index 000000000..b8c5b44a2 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.ECKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface EllipticCurveSignatureAlgorithm extends AsymmetricKeySignatureAlgorithm { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithm.java deleted file mode 100644 index a3ba789c2..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithm.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.jsonwebtoken.security; - -import io.jsonwebtoken.Named; - -import java.security.Key; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface EncryptionAlgorithm, ERes extends EncryptionResult, DReq extends CryptoRequest> extends Named { - - ERes encrypt(EReq request) throws CryptoException, KeyException; - - byte[] decrypt(DReq request) throws CryptoException, KeyException; -} diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmLocator.java b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmLocator.java deleted file mode 100644 index ec44fd8b9..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmLocator.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.jsonwebtoken.security; - -import io.jsonwebtoken.JweHeader; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface EncryptionAlgorithmLocator { - - EncryptionAlgorithm getEncryptionAlgorithm(JweHeader jweHeader); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmName.java b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmName.java deleted file mode 100644 index 9b482d3bf..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmName.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public enum EncryptionAlgorithmName { - - A128CBC_HS256("A128CBC-HS256", "AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.3", "AES/CBC/PKCS5Padding"), - A192CBC_HS384("A192CBC-HS384", "AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.4", "AES/CBC/PKCS5Padding"), - A256CBC_HS512("A256CBC-HS512", "AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.5", "AES/CBC/PKCS5Padding"), - A128GCM("A128GCM", "AES GCM using 128-bit key", "AES/GCM/NoPadding"), - A192GCM("A192GCM", "AES GCM using 192-bit key", "AES/GCM/NoPadding"), - A256GCM("A256GCM", "AES GCM using 256-bit key", "AES/GCM/NoPadding"); - - private final String name; - private final String description; - private final String jcaName; - - EncryptionAlgorithmName(String name, String description, String jcaName) { - this.name = name; - this.description = description; - this.jcaName = jcaName; - } - - /** - * Returns the JWA algorithm name constant. - * - * @return the JWA algorithm name constant. - */ - public String getValue() { - return name; - } - - /** - * Returns the JWA algorithm description. - * - * @return the JWA algorithm description. - */ - public String getDescription() { - return description; - } - - /** - * Returns the name of the JCA algorithm used to encrypt or decrypt JWE content. - * - * @return the name of the JCA algorithm used to encrypt or decrypt JWE content. - */ - public String getJcaName() { - return jcaName; - } - - /** - * Returns the corresponding {@code EncryptionAlgorithmName} enum instance based on a - * case-insensitive name comparison of the specified JWE enc value. - * - * @param name the case-insensitive JWE enc header value. - * @return Returns the corresponding {@code EncryptionAlgorithmName} enum instance based on a - * case-insensitive name comparison of the specified JWE enc value. - * @throws IllegalArgumentException if the specified value does not match any JWE {@code EncryptionAlgorithmName} value. - */ - public static EncryptionAlgorithmName forName(String name) throws IllegalArgumentException { - for (EncryptionAlgorithmName enc : values()) { - if (enc.getValue().equalsIgnoreCase(name)) { - return enc; - } - } - - throw new IllegalArgumentException("Unsupported JWE Content Encryption Algorithm name: " + name); - } - - @Override - public String toString() { - return name; - } -} diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java index dbaeecb58..393e5e718 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java @@ -1,13 +1,24 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Classes; -import io.jsonwebtoken.lang.Maps; -import io.jsonwebtoken.lang.Strings; import java.util.Collection; -import java.util.Collections; -import java.util.Map; /** * @since JJWT_RELEASE_VERSION @@ -18,23 +29,30 @@ public final class EncryptionAlgorithms { private EncryptionAlgorithms() { } - private static final Class MAC_CLASS = Classes.forName("io.jsonwebtoken.impl.security.MacSignatureAlgorithm"); - private static final String HMAC = "io.jsonwebtoken.impl.security.HmacAesEncryptionAlgorithm"; - private static final Class[] HMAC_ARGS = new Class[]{String.class, MAC_CLASS}; + private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.EncryptionAlgorithmsBridge"; + private static final Class[] ID_ARG_TYPES = new Class[]{String.class}; - private static final String GCM = "io.jsonwebtoken.impl.security.GcmAesEncryptionAlgorithm"; - private static final Class[] GCM_ARGS = new Class[]{String.class, int.class}; + public static Collection values() { + return Classes.invokeStatic(BRIDGE_CLASSNAME, "values", null, (Object[]) null); + } - private static AeadSymmetricEncryptionAlgorithm hmac(int keyLength) { - int digestLength = keyLength * 2; - String name = "A" + keyLength + "CBC-HS" + digestLength; - SignatureAlgorithm macSigAlg = Classes.newInstance(SignatureAlgorithms.HMAC, SignatureAlgorithms.HMAC_ARGS, name, "HmacSHA" + digestLength, keyLength); - return Classes.newInstance(HMAC, HMAC_ARGS, name, macSigAlg); + /** + * Returns the JWE Encryption Algorithm with the specified + * {@code enc} algorithm identifier or + * {@code null} if an algorithm for the specified {@code id} cannot be found. + * + * @param id a JWE standard {@code enc} algorithm identifier + * @return the associated Encryption Algorithm instance or {@code null} otherwise. + * @see RFC 7518, Section 5.1 + */ + public static AeadAlgorithm findById(String id) { + Assert.hasText(id, "id cannot be null or empty."); + return Classes.invokeStatic(BRIDGE_CLASSNAME, "findById", ID_ARG_TYPES, id); } - private static AeadSymmetricEncryptionAlgorithm gcm(int keyLength) { - String name = "A" + keyLength + "GCM"; - return Classes.newInstance(GCM, GCM_ARGS, name, keyLength); + private static AeadAlgorithm forId(String id) { + Assert.hasText(id, "id cannot be null or empty."); + return Classes.invokeStatic(BRIDGE_CLASSNAME, "forId", ID_ARG_TYPES, id); } /** @@ -42,64 +60,40 @@ private static AeadSymmetricEncryptionAlgorithm gcm(int keyLength) { * RFC 7518, Section 5.2.3. This algorithm * requires a 256 bit (32 byte) key. */ - public static final AeadSymmetricEncryptionAlgorithm A128CBC_HS256 = hmac(128); + public static final AeadAlgorithm A128CBC_HS256 = forId("A128CBC-HS256"); /** * AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm, as defined by * RFC 7518, Section 5.2.4. This algorithm * requires a 384 bit (48 byte) key. */ - public static final AeadSymmetricEncryptionAlgorithm A192CBC_HS384 = hmac(192); + public static final AeadAlgorithm A192CBC_HS384 = forId("A192CBC-HS384"); /** * AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm, as defined by * RFC 7518, Section 5.2.5. This algorithm * requires a 512 bit (64 byte) key. */ - public static final AeadSymmetricEncryptionAlgorithm A256CBC_HS512 = hmac(256); + public static final AeadAlgorithm A256CBC_HS512 = forId("A256CBC-HS512"); /** * "AES GCM using 128-bit key" as defined by * RFC 7518, Section 5.3. This algorithm requires * a 128 bit (16 byte) key. */ - public static final AeadSymmetricEncryptionAlgorithm A128GCM = gcm(128); + public static final AeadAlgorithm A128GCM = forId("A128GCM"); /** * "AES GCM using 192-bit key" as defined by * RFC 7518, Section 5.3. This algorithm requires * a 192 bit (24 byte) key. */ - public static final AeadSymmetricEncryptionAlgorithm A192GCM = gcm(192); + public static final AeadAlgorithm A192GCM = forId("A192GCM"); /** * "AES GCM using 256-bit key" as defined by * RFC 7518, Section 5.3. This algorithm requires * a 256 bit (32 byte) key. */ - public static final AeadSymmetricEncryptionAlgorithm A256GCM = gcm(256); - - private static final Map SYMMETRIC_VALUES_BY_NAME = Collections.unmodifiableMap(Maps - .of(A128CBC_HS256.getName(), A128CBC_HS256) - .and(A192CBC_HS384.getName(), A192CBC_HS384) - .and(A256CBC_HS512.getName(), A256CBC_HS512) - .and(A128GCM.getName(), A128GCM) - .and(A192GCM.getName(), A192GCM) - .and(A256GCM.getName(), A256GCM) - .build()); - - public static EncryptionAlgorithm forName(String name) { - Assert.hasText(name, "name cannot be null or empty."); - EncryptionAlgorithm alg = SYMMETRIC_VALUES_BY_NAME.get(name.toUpperCase()); - if (alg == null) { - String msg = "'" + name + "' is not a JWE specification standard name. The standard names are: " + - Strings.collectionToCommaDelimitedString(SYMMETRIC_VALUES_BY_NAME.keySet()); - throw new IllegalArgumentException(msg); - } - return alg; - } - - public static Collection symmetric() { - return SYMMETRIC_VALUES_BY_NAME.values(); - } + public static final AeadAlgorithm A256GCM = forId("A256GCM"); } diff --git a/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSource.java b/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSource.java deleted file mode 100644 index 47f1032ed..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSource.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface InitializationVectorSource { - - /** - * Returns the secure-random initialization vector used during encryption that must be presented in order - * to decrypt. - * - * @return the secure-random initialization vector used during encryption that must be presented in order - * to decrypt. - */ - byte[] getInitializationVector(); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSupplier.java b/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSupplier.java new file mode 100644 index 000000000..b0b397e5a --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSupplier.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface InitializationVectorSupplier { + + /** + * Returns the secure-random initialization vector used during encryption that must be presented in order + * to decrypt. + * + * @return the secure-random initialization vector used during encryption that must be presented in order + * to decrypt. + */ + byte[] getInitializationVector(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/IvEncryptionResult.java b/api/src/main/java/io/jsonwebtoken/security/IvEncryptionResult.java deleted file mode 100644 index ac75f30fb..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/IvEncryptionResult.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface IvEncryptionResult extends EncryptionResult, InitializationVectorSource { - - byte[] compact(); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/IvRequest.java b/api/src/main/java/io/jsonwebtoken/security/IvRequest.java deleted file mode 100644 index e4dfbbcc1..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/IvRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jsonwebtoken.security; - -import java.security.Key; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface IvRequest extends CryptoRequest, InitializationVectorSource { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/Jwk.java b/api/src/main/java/io/jsonwebtoken/security/Jwk.java index f527978cf..4ab0c81a2 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Jwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/Jwk.java @@ -1,30 +1,36 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; -import java.net.URI; -import java.util.List; +import io.jsonwebtoken.Identifiable; + +import java.security.Key; import java.util.Map; import java.util.Set; /** * @since JJWT_RELEASE_VERSION */ -public interface Jwk extends Map, JwkMutator { - - String getType(); - - String getUse(); - - Set getOperations(); +public interface Jwk extends Identifiable, Map { String getAlgorithm(); - String getId(); - - URI getX509Url(); - - List getX509CertficateChain(); + Set getOperations(); - String getX509CertificateSha1Thumbprint(); + String getType(); - String getX509CertificateSha256Thumbprint(); + K toKey(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java index c5b09062f..c7c5162f9 100644 --- a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java @@ -1,10 +1,49 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; +import java.security.Key; +import java.security.Provider; +import java.util.Map; +import java.util.Set; + /** * @since JJWT_RELEASE_VERSION */ -public interface JwkBuilder extends JwkMutator { +public interface JwkBuilder, T extends JwkBuilder> { + + T put(String name, Object value); + + T putAll(Map values); + + T setAlgorithm(String alg); + + T setId(String id); + + T setOperations(Set ops); - K build(); + /** + * Sets the JCA Provider to use during key operations, or {@code null} if the + * JCA subsystem preferred provider should be used. + * + * @param provider the JCA Provider to use during key operations, or {@code null} if the + * JCA subsystem preferred provider should be used. + * @return the builder for method chaining. + */ + T setProvider(Provider provider); + J build(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilderFactory.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilderFactory.java deleted file mode 100644 index 406408dff..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/JwkBuilderFactory.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface JwkBuilderFactory { - - EcJwkBuilderFactory ellipticCurve(); - - SymmetricJwkBuilder symmetric(); - -} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkMutator.java b/api/src/main/java/io/jsonwebtoken/security/JwkMutator.java deleted file mode 100644 index ba8ab1cc1..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/JwkMutator.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.jsonwebtoken.security; - -import java.net.URI; -import java.util.List; -import java.util.Set; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface JwkMutator { - - T setUse(String use); - - T setOperations(Set ops); - - T setAlgorithm(String alg); - - T setId(String id); - - T setX509Url(URI uri); - - T setX509CertificateChain(List chain); - - T setX509CertificateSha1Thumbprint(String thumbprint); - - T setX509CertificateSha256Thumbprint(String thumbprint); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfo.java b/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfo.java deleted file mode 100644 index 55eb75030..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfo.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.jsonwebtoken.security; - -import java.util.Map; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface JwkRsaPrimeInfo extends Map, JwkRsaPrimeInfoMutator { - - String getPrime(); - - String getCrtExponent(); - - String getCrtCoefficient(); - -} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoBuilder.java deleted file mode 100644 index 4307aa57d..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoBuilder.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface JwkRsaPrimeInfoBuilder extends JwkRsaPrimeInfoMutator { - - JwkRsaPrimeInfo build(); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoMutator.java b/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoMutator.java deleted file mode 100644 index 36a956122..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/JwkRsaPrimeInfoMutator.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface JwkRsaPrimeInfoMutator { - - T setPrime(String r); - - T setCrtExponent(String d); - - T setCrtCoefficient(String t); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/Jwks.java b/api/src/main/java/io/jsonwebtoken/security/Jwks.java index 08cf45bf3..1f14530f2 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Jwks.java +++ b/api/src/main/java/io/jsonwebtoken/security/Jwks.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; import io.jsonwebtoken.lang.Classes; @@ -7,8 +22,9 @@ */ public class Jwks { - public static T builder() { - return Classes.newInstance("io.jsonwebtoken.impl.security.DefaultJwkBuilderFactory"); - } + private static final String CNAME = "io.jsonwebtoken.impl.security.DefaultProtoJwkBuilder"; + public static ProtoJwkBuilder builder() { + return Classes.newInstance(CNAME); + } } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java new file mode 100644 index 000000000..92be15d78 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.Identifiable; + +import javax.crypto.SecretKey; +import java.security.Key; + +/** + * A {@code KeyAlgorithm} produces the {@link SecretKey} used to encrypt or decrypt a JWE. The {@code KeyAlgorithm} + * used for a particular JWE is {@link #getId() identified} in the JWE's + * {@code alg} header. + *

    + *

    The {@code KeyAlgorithm} interface is JJWT's idiomatic approach to the JWE specification's + * {@code Key Management Mode} concept.

    + * + * @since JJWT_RELEASE_VERSION + * @see RFC 7561, Section 2: JWE Key (Management) Algorithms + */ +public interface KeyAlgorithm extends Identifiable { + + KeyResult getEncryptionKey(KeyRequest request) throws SecurityException; + + SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException; +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java new file mode 100644 index 000000000..71ea9eee4 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Classes; + +import javax.crypto.SecretKey; +import java.util.Collection; + +/** + * @since JJWT_RELEASE_VERSION + */ +@SuppressWarnings("rawtypes") +public final class KeyAlgorithms { + + //prevent instantiation + private KeyAlgorithms() { + } + + private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.KeyAlgorithmsBridge"; + private static final Class[] ID_ARG_TYPES = new Class[]{String.class}; + private static final Class[] ESTIMATE_ITERATIONS_ARG_TYPES = new Class[]{KeyAlgorithm.class, long.class}; + + public static Collection> values() { + return Classes.invokeStatic(BRIDGE_CLASSNAME, "values", null, (Object[]) null); + } + + /** + * Returns the JWE KeyAlgorithm with the specified + * {@code alg} key algorithm identifier or + * {@code null} if an algorithm for the specified {@code id} cannot be found. + * + * @param id a JWE standard {@code alg} key algorithm identifier + * @return the associated KeyAlgorithm instance or {@code null} otherwise. + * @see RFC 7518, Section 4.1 + */ + public static KeyAlgorithm findById(String id) { + Assert.hasText(id, "id cannot be null or empty."); + return Classes.invokeStatic(BRIDGE_CLASSNAME, "findById", ID_ARG_TYPES, id); + } + + public static KeyAlgorithm forId(String id) { + return forId0(id); + } + + // do not change this visibility. Raw type method signature not be publicly exposed + private static T forId0(String id) { + Assert.hasText(id, "id cannot be null or empty."); + return Classes.invokeStatic(BRIDGE_CLASSNAME, "forId", ID_ARG_TYPES, id); + } + + public static final KeyAlgorithm DIRECT = forId0("dir"); + public static final KeyAlgorithm A128KW = forId0("A128KW"); + public static final KeyAlgorithm A192KW = forId0("A192KW"); + public static final KeyAlgorithm A256KW = forId0("A256KW"); + public static final KeyAlgorithm A128GCMKW = forId0("A128GCMKW"); + public static final KeyAlgorithm A192GCMKW = forId0("A192GCMKW"); + public static final KeyAlgorithm A256GCMKW = forId0("A256GCMKW"); + public static final KeyAlgorithm PBES2_HS256_A128KW = forId0("PBES2-HS256+A128KW"); + public static final KeyAlgorithm PBES2_HS384_A192KW = forId0("PBES2-HS384+A192KW"); + public static final KeyAlgorithm PBES2_HS512_A256KW = forId0("PBES2-HS512+A256KW"); + public static final RsaKeyAlgorithm RSA1_5 = forId0("RSA1_5"); + public static final RsaKeyAlgorithm RSA_OAEP = forId0("RSA-OAEP"); + public static final RsaKeyAlgorithm RSA_OAEP_256 = forId0("RSA-OAEP-256"); + + public static int estimateIterations(KeyAlgorithm alg, long desiredMillis) { + return Classes.invokeStatic(BRIDGE_CLASSNAME, "estimateIterations", ESTIMATE_ITERATIONS_ARG_TYPES, alg, desiredMillis); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyManagementAlgorithmName.java b/api/src/main/java/io/jsonwebtoken/security/KeyManagementAlgorithmName.java deleted file mode 100644 index bb1b169b3..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/KeyManagementAlgorithmName.java +++ /dev/null @@ -1,106 +0,0 @@ -package io.jsonwebtoken.security; - -import io.jsonwebtoken.lang.Collections; - -import java.util.List; - -/** - * Type-safe representation of standard JWE encryption key management algorithm names as defined in the - * JSON Web Algorithms specification. - * - * @since JJWT_RELEASE_VERSION - */ -public enum KeyManagementAlgorithmName { - - RSA1_5("RSA1_5", "RSAES-PKCS1-v1_5", Collections.emptyList(), "RSA/ECB/PKCS1Padding"), - RSA_OAEP("RSA-OAEP", "RSAES OAEP using default parameters", Collections.emptyList(), "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"), - RSA_OAEP_256("RSA-OAEP-256", "RSAES OAEP using SHA-256 and MGF1 with SHA-256", Collections.emptyList(), "RSA/ECB/OAEPWithSHA-256AndMGF1Padding & MGF1ParameterSpec.SHA256"), - A128KW("A128KW", "AES Key Wrap with default initial value using 128-bit key", Collections.emptyList(), "AESWrap"), - A192KW("A192KW", "AES Key Wrap with default initial value using 192-bit key", Collections.emptyList(), "AESWrap"), - A256KW("A256KW", "AES Key Wrap with default initial value using 256-bit key", Collections.emptyList(), "AESWrap"), - dir("dir", "Direct use of a shared symmetric key as the CEK", Collections.emptyList(), "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"), - ECDH_ES("ECDH-ES", "Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF", Collections.of("epk", "apu", "apv"), "ECDH"), - ECDH_ES_A128KW("ECDH-ES+A128KW", "ECDH-ES using Concat KDF and CEK wrapped with \"A128KW\"", Collections.of("epk", "apu", "apv"), "ECDH???"), - ECDH_ES_A192KW("ECDH-ES+A192KW", "ECDH-ES using Concat KDF and CEK wrapped with \"A192KW\"", Collections.of("epk", "apu", "apv"), "ECDH???"), - ECDH_ES_A256KW("ECDH-ES+A256KW", "ECDH-ES using Concat KDF and CEK wrapped with \"A256KW\"", Collections.of("epk", "apu", "apv"), "ECDH???"), - A128GCMKW("A128GCMKW", "Key wrapping with AES GCM using 128-bit key", Collections.of("iv", "tag"), "???"), - A192GCMKW("A192GCMKW", "Key wrapping with AES GCM using 192-bit key", Collections.of("iv", "tag"), "???"), - A256GCMKW("A256GCMKW", "Key wrapping with AES GCM using 256-bit key", Collections.of("iv", "tag"), "???"), - PBES2_HS256_A128KW("PBES2-HS256+A128KW", "PBES2 with HMAC SHA-256 and \"A128KW\" wrapping", Collections.of("p2s", "p2c"), "???"), - PBES2_HS384_A192KW("PBES2-HS384+A192KW", "PBES2 with HMAC SHA-384 and \"A192KW\" wrapping", Collections.of("p2s", "p2c"), "???"), - PBES2_HS512_A256KW("PBES2-HS512+A256KW", "PBES2 with HMAC SHA-512 and \"A256KW\" wrapping", Collections.of("p2s", "p2c"), "???"); - - private final String value; - private final String description; - private final List moreHeaderParams; - private final String jcaName; - - KeyManagementAlgorithmName(String value, String description, List moreHeaderParams, String jcaName) { - this.value = value; - this.description = description; - this.moreHeaderParams = moreHeaderParams; - this.jcaName = jcaName; - } - - /** - * Returns the JWA algorithm name constant. - * - * @return the JWA algorithm name constant. - */ - public String getValue() { - return value; - } - - /** - * Returns the JWA algorithm description. - * - * @return the JWA algorithm description. - */ - public String getDescription() { - return description; - } - - /** - * Returns a list of header parameters that must exist in the JWE header when evaluating the key management - * algorithm. The list will be empty for algorithms that do not require additional header parameters. - * - * @return a list of header parameters that must exist in the JWE header when evaluating the key management - * algorithm. - */ - public List getMoreHeaderParams() { - return moreHeaderParams; - } - - /** - * Returns the name of the JCA algorithm used to create or validate the Content Encryption Key (CEK). - * - * @return the name of the JCA algorithm used to create or validate the Content Encryption Key (CEK). - */ - public String getJcaName() { - return jcaName; - } - - /** - * Returns the corresponding {@code KeyManagementAlgorithmName} enum instance based on a - * case-insensitive name comparison of the specified JWE alg value. - * - * @param name the case-insensitive JWE alg header value. - * @return Returns the corresponding {@code KeyManagementAlgorithmName} enum instance based on a - * case-insensitive name comparison of the specified JWE alg value. - * @throws IllegalArgumentException if the specified value does not match any JWE {@code KeyManagementAlgorithmName} value. - */ - public static KeyManagementAlgorithmName forName(String name) throws IllegalArgumentException { - for (KeyManagementAlgorithmName alg : values()) { - if (alg.getValue().equalsIgnoreCase(name)) { - return alg; - } - } - - throw new IllegalArgumentException("Unsupported JWE Key Management Algorithm name: " + name); - } - - @Override - public String toString() { - return value; - } -} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyManagementModeName.java b/api/src/main/java/io/jsonwebtoken/security/KeyManagementModeName.java deleted file mode 100644 index 4f4968c0f..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/KeyManagementModeName.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * An enum representing the {@code Key Management Mode} names defined in - * RFC 7516, Section 2. - * - * @since JJWT_RELEASE_VERSION - */ -public enum KeyManagementModeName { - - KEY_ENCRYPTION("Key Encryption", - "The CEK value is encrypted to the intended recipient using an asymmetric encryption algorithm"), - - KEY_WRAPPING("Key Wrapping", - "The CEK value is encrypted to the intended recipient using a symmetric key wrapping algorithm."), - - DIRECT_KEY_AGREEMENT("Direct Key Agreement", - "A key agreement algorithm is used to agree upon the CEK value."), - - KEY_AGREEMENT_WITH_KEY_WRAPPING("Key Agreement with Key Wrapping", - "A key agreement algorithm is used to agree upon a symmetric key used to encrypt the CEK value to the " + - "intended recipient using a symmetric key wrapping algorithm."), - - DIRECT_ENCRYPTION("Direct Encryption", - "The CEK value used is the secret symmetric key value shared between the parties."); - - private final String name; - private final String desc; - - KeyManagementModeName(String name, String desc) { - this.name = name; - this.desc = desc; - } - - public String getName() { - return name; - } - - public String getDescription() { - return desc; - } -} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java b/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java new file mode 100644 index 000000000..c81eb5e06 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.JweHeader; + +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface KeyRequest extends SecurityRequest, KeySupplier { + + AeadAlgorithm getEncryptionAlgorithm(); + + JweHeader getHeader(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyResult.java b/api/src/main/java/io/jsonwebtoken/security/KeyResult.java new file mode 100644 index 000000000..2426dfbc6 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyResult.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface KeyResult extends PayloadSupplier, KeySupplier { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeySupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeySupplier.java new file mode 100644 index 000000000..219a28264 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeySupplier.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface KeySupplier { + + /** + * Returns the key to use for signing, wrapping, encryption or decryption depending on the type of operation. + * + * @return the key to use for signing, wrapping, encryption or decryption depending on the type of operation. + */ + K getKey(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index 088be5e9b..9347e68da 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -16,8 +16,10 @@ package io.jsonwebtoken.security; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Classes; import javax.crypto.SecretKey; +import javax.crypto.interfaces.PBEKey; import javax.crypto.spec.SecretKeySpec; import java.security.KeyPair; @@ -28,6 +30,10 @@ */ public final class Keys { + private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.KeysBridge"; + @SuppressWarnings("rawtypes") + private static final Class[] TO_PBE_ARG_TYPES = new Class[]{PBEKey.class}; + //prevent instantiation private Keys() { } @@ -109,18 +115,18 @@ public static SecretKey hmacShaKeyFor(byte[] bytes) throws WeakKeyException { * @return a new {@link SecretKey} instance suitable for use with the specified {@link SignatureAlgorithm}. * @throws IllegalArgumentException for any input value other than {@link io.jsonwebtoken.SignatureAlgorithm#HS256}, * {@link io.jsonwebtoken.SignatureAlgorithm#HS384}, or {@link io.jsonwebtoken.SignatureAlgorithm#HS512} - * @deprecated since JJWT_RELEASE_VERSION. Use your preferred {@link SymmetricKeySignatureAlgorithm} instance's - * {@link SymmetricKeySignatureAlgorithm#generateKey() generateKey()} method directly. + * @deprecated since JJWT_RELEASE_VERSION. Use your preferred {@link SecretKeySignatureAlgorithm} instance's + * {@link SecretKeySignatureAlgorithm#generateKey() generateKey()} method directly. */ @Deprecated public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - SignatureAlgorithm salg = SignatureAlgorithms.forName(alg.name()); - if (!(salg instanceof SymmetricKeySignatureAlgorithm)) { + SignatureAlgorithm salg = SignatureAlgorithms.forId(alg.name()); + if (!(salg instanceof SecretKeySignatureAlgorithm)) { String msg = "The " + alg.name() + " algorithm does not support shared secret keys."; throw new IllegalArgumentException(msg); } - return ((SymmetricKeySignatureAlgorithm) salg).generateKey(); + return ((SecretKeySignatureAlgorithm) salg).generateKey(); } /** @@ -210,11 +216,33 @@ public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) thr @Deprecated public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - SignatureAlgorithm salg = SignatureAlgorithms.forName(alg.name()); + SignatureAlgorithm salg = SignatureAlgorithms.forId(alg.name()); if (!(salg instanceof AsymmetricKeySignatureAlgorithm)) { String msg = "The " + alg.name() + " algorithm does not support Key Pairs."; throw new IllegalArgumentException(msg); } - return ((AsymmetricKeySignatureAlgorithm) salg).generateKeyPair(); + AsymmetricKeySignatureAlgorithm asalg = ((AsymmetricKeySignatureAlgorithm) salg); + return asalg.generateKeyPair(); + } + + /** + * Returns a JJWT {@link PbeKey} directly backed by the specified JCA {@link PBEKey}. The returned instance + * is directly linked to the specified {@code PBEKey} - a call to either key's {@link SecretKey#destroy() destroy} + * method will destroy the other to ensure correct/safe cleanup for both. + * + * @param key the {@code PBEKey} to represent as a {@code PbeKey} instance. + * @return a JJWT {@link PbeKey} instance that wraps the specified JCA {@link PBEKey} + * @since JJWT_RELEASE_VERSION + */ + public static PbeKey toPbeKey(PBEKey key) { + return Classes.invokeStatic(BRIDGE_CLASSNAME, "toPbeKey", TO_PBE_ARG_TYPES, new Object[]{key}); + } + + /** + * Returns a new {@link PbeKeyBuilder} to use to construct a {@link PbeKey} instance. + * @return + */ + public static PbeKeyBuilder forPbe() { + return Classes.invokeStatic(BRIDGE_CLASSNAME, "forPbe", null, (Object[]) null); } } diff --git a/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java b/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java index a89bbc3df..ea44962e6 100644 --- a/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; /** diff --git a/api/src/main/java/io/jsonwebtoken/security/PayloadSupplier.java b/api/src/main/java/io/jsonwebtoken/security/PayloadSupplier.java new file mode 100644 index 000000000..49e1116c5 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PayloadSupplier.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PayloadSupplier { + + T getPayload(); //plaintext, ciphertext, or Key to be wrapped + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PbeKey.java b/api/src/main/java/io/jsonwebtoken/security/PbeKey.java new file mode 100644 index 000000000..258980a79 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PbeKey.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PbeKey extends SecretKey { + + /** + * Returns a clone of the underlying password character array represented by this Key. Like all + * {@code SecretKey} implementations, if you wish to clear the backing password character array for + * safety/security reasons, call the {@link #destroy()} method, ensuring the key instance can no longer + * be used. + * + * @return a clone of the underlying password character array represented by this Key. + */ + char[] getPassword(); + + /** + * Returns the number of hashing iterations to perform. + * + * @return the number of hashing iterations to perform. + */ + int getIterations(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PbeKeyBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PbeKeyBuilder.java new file mode 100644 index 000000000..4f1e8e8ef --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PbeKeyBuilder.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PbeKeyBuilder { + + /** + * Sets the password character array for the constructed key. This does not clone the argument - changes made + * to the backing array will be reflected by the constructed key and any {@link PbeKey#destroy()} call will do + * the same. This is to ensure that any clearing of the password argument for security/safety reasons also + * guarantees the resulting key is also cleared and vice versa. + * + * @param password password character array for the constructed key + * @return this builder for method chaining + */ + PbeKeyBuilder setPassword(char[] password); + + /** + * Sets the number of hashing iterations to perform when deriving an encryption key. + * + * @param iterations the number of hashing iterations to perform when deriving an encryption key. + * @return @return this builder for method chaining + */ + PbeKeyBuilder setIterations(int iterations); + + /** + * Constructs a new {@link PbeKey} that shares the {@link #setPassword(char[]) specified} password character array. + * Changes to that char array will be reflected in the returned key, and similarly, + * any call to the key's {@link PbeKey#destroy() destroy} method will clear/overwrite the shared char array. + * This is to ensure that any clearing of the password char array for security/safety reasons also + * guarantees the key is also cleared and vice versa. + * + * @return a new {@link PbeKey} that shares the {@link #setPassword(char[]) specified} password character array. + */ + K build(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwk.java b/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwk.java deleted file mode 100644 index 2130d45d0..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwk.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface PrivateEcJwk extends EcJwk, PrivateEcJwkMutator { - - String getD(); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkBuilder.java deleted file mode 100644 index 543e785db..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkBuilder.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface PrivateEcJwkBuilder extends EcJwkBuilder { - - PrivateEcJwkBuilder setD(String d); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkMutator.java b/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkMutator.java deleted file mode 100644 index f6e5f8a23..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/PrivateEcJwkMutator.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface PrivateEcJwkMutator extends EcJwkMutator { - - T setD(String d); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java new file mode 100644 index 000000000..a19148274 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PrivateJwk> extends AsymmetricJwk { + + M toPublicJwk(); + + KeyPair toKeyPair(); + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PrivateJwkBuilder.java new file mode 100644 index 000000000..4b2646980 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateJwkBuilder.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PrivateJwkBuilder, M extends PrivateJwk, + T extends PrivateJwkBuilder> extends AsymmetricJwkBuilder { + + T setPublicKey(L publicKey); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwk.java b/api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwk.java deleted file mode 100644 index 28976a93c..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwk.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.jsonwebtoken.security; - -import java.util.List; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface PrivateRsaJwk extends RsaJwk, PrivateRsaJwkMutator { - - String getD(); - - String getP(); - - String getQ(); - - String getDP(); - - String getDQ(); - - String getQI(); - - List getOtherPrimesInfo(); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwkMutator.java b/api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwkMutator.java deleted file mode 100644 index 6ffb3edc6..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/PrivateRsaJwkMutator.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.jsonwebtoken.security; - -import java.util.List; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface PrivateRsaJwkMutator extends RsaJwkMutator { - - T setD(String d); - - T setP(String p); - - T setQ(String q); - - T setDP(String dp); - - T setDQ(String dq); - - T setQI(String qi); - - T setOtherPrimesInfo(List infos); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java new file mode 100644 index 000000000..209aa766e --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import javax.crypto.SecretKey; +import java.security.Key; +import java.security.KeyPair; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface ProtoJwkBuilder, T extends JwkBuilder> extends JwkBuilder { + + SecretJwkBuilder setKey(SecretKey key); + + RsaPublicJwkBuilder setKey(RSAPublicKey key); + + RsaPrivateJwkBuilder setKey(RSAPrivateKey key); + + EcPublicJwkBuilder setKey(ECPublicKey key); + + EcPrivateJwkBuilder setKey(ECPrivateKey key); + + RsaPrivateJwkBuilder setKeyPairRsa(KeyPair keyPair); + + EcPrivateJwkBuilder setKeyPairEc(KeyPair keyPair); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PublicEcJwk.java b/api/src/main/java/io/jsonwebtoken/security/PublicEcJwk.java deleted file mode 100644 index be532e268..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/PublicEcJwk.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface PublicEcJwk extends EcJwk { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/PublicEcJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PublicEcJwkBuilder.java deleted file mode 100644 index ed893ed35..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/PublicEcJwkBuilder.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface PublicEcJwkBuilder extends EcJwkBuilder { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java b/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java new file mode 100644 index 000000000..22cfae9b4 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.PublicKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PublicJwk extends AsymmetricJwk { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PublicJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PublicJwkBuilder.java new file mode 100644 index 000000000..69a2b84fb --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PublicJwkBuilder.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface PublicJwkBuilder, M extends PrivateJwk, P extends PrivateJwkBuilder, T extends PublicJwkBuilder> extends AsymmetricJwkBuilder { + + P setPrivateKey(L privateKey); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PublicRsaJwk.java b/api/src/main/java/io/jsonwebtoken/security/PublicRsaJwk.java deleted file mode 100644 index 99e121368..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/PublicRsaJwk.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface PublicRsaJwk extends RsaJwk { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaJwk.java b/api/src/main/java/io/jsonwebtoken/security/RsaJwk.java deleted file mode 100644 index 0a5c2393a..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/RsaJwk.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface RsaJwk extends Jwk, RsaJwkMutator { - - String getModulus(); - - String getExponent(); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaJwkMutator.java b/api/src/main/java/io/jsonwebtoken/security/RsaJwkMutator.java deleted file mode 100644 index 0df8a74da..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/RsaJwkMutator.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface RsaJwkMutator extends JwkMutator { - - T setModulus(String n); - - T setExponent(String e); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java new file mode 100644 index 000000000..8e2ac2727 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.RSAKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface RsaKeyAlgorithm extends KeyAlgorithm { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java new file mode 100644 index 000000000..ab7dd5025 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface RsaPrivateJwk extends PrivateJwk { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwkBuilder.java new file mode 100644 index 000000000..bda3a9417 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwkBuilder.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface RsaPrivateJwkBuilder extends PrivateJwkBuilder { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java b/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java new file mode 100644 index 000000000..093a345f9 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.interfaces.RSAPublicKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface RsaPublicJwk extends PublicJwk { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/CryptoException.java b/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwkBuilder.java similarity index 68% rename from api/src/main/java/io/jsonwebtoken/security/CryptoException.java rename to api/src/main/java/io/jsonwebtoken/security/RsaPublicJwkBuilder.java index 6d3b26fb3..d850c5725 100644 --- a/api/src/main/java/io/jsonwebtoken/security/CryptoException.java +++ b/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwkBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 jsonwebtoken.io + * Copyright (C) 2021 jsonwebtoken.io * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,16 +15,12 @@ */ package io.jsonwebtoken.security; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + /** * @since JJWT_RELEASE_VERSION */ -public class CryptoException extends SecurityException { - - public CryptoException(String message) { - super(message); - } +public interface RsaPublicJwkBuilder extends PublicJwkBuilder { - public CryptoException(String message, Throwable cause) { - super(message, cause); - } } diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java new file mode 100644 index 000000000..46450534e --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.RSAKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface RsaSignatureAlgorithm extends AsymmetricKeySignatureAlgorithm { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AuthenticationTagSource.java b/api/src/main/java/io/jsonwebtoken/security/SecretJwk.java similarity index 84% rename from api/src/main/java/io/jsonwebtoken/security/AuthenticationTagSource.java rename to api/src/main/java/io/jsonwebtoken/security/SecretJwk.java index b7e1f55ec..862d6c39a 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AuthenticationTagSource.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecretJwk.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 jsonwebtoken.io + * Copyright (C) 2021 jsonwebtoken.io * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,10 @@ */ package io.jsonwebtoken.security; +import javax.crypto.SecretKey; + /** * @since JJWT_RELEASE_VERSION */ -public interface AuthenticationTagSource { - - byte[] getAuthenticationTag(); - +public interface SecretJwk extends Jwk { } diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/SecretJwkBuilder.java new file mode 100644 index 000000000..a3d737ab8 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecretJwkBuilder.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface SecretJwkBuilder extends JwkBuilder { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretKeyGenerator.java b/api/src/main/java/io/jsonwebtoken/security/SecretKeyGenerator.java new file mode 100644 index 000000000..cfcbb3ef7 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecretKeyGenerator.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface SecretKeyGenerator { + + /** + * Creates and returns a new secure-random key with a length sufficient to be used by the associated Algorithm. + * + * @return a new secure-random key with a length sufficient to be used by the associated Algorithm. + */ + SecretKey generateKey(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretKeySignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SecretKeySignatureAlgorithm.java new file mode 100644 index 000000000..e14e22af0 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecretKeySignatureAlgorithm.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface SecretKeySignatureAlgorithm extends SignatureAlgorithm, SecretKeyGenerator { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SecurityRequest.java b/api/src/main/java/io/jsonwebtoken/security/SecurityRequest.java new file mode 100644 index 000000000..0da0b1d98 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecurityRequest.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.Provider; +import java.security.SecureRandom; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface SecurityRequest { + + /** + * Returns the JCA provider that should be used for cryptographic operations during the request or + * {@code null} if the JCA subsystem preferred provider should be used. + * + * @return the JCA provider that should be used for cryptographic operations during the request or + * {@code null} if the JCA subsystem preferred provider should be used. + */ + Provider getProvider(); + + /** + * Returns the {@code SecureRandom} to use when performing cryptographic operations during the request, or + * {@code null} if a default {@link SecureRandom} should be used. + * + * @return the {@code SecureRandom} to use when performing cryptographic operations during the request, or + * {@code null} if a default {@link SecureRandom} should be used. + */ + SecureRandom getSecureRandom(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java index 095fe5047..5a0eff67e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java @@ -1,15 +1,30 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; -import io.jsonwebtoken.Named; +import io.jsonwebtoken.Identifiable; import java.security.Key; /** * @since JJWT_RELEASE_VERSION */ -public interface SignatureAlgorithm extends Named { +public interface SignatureAlgorithm extends Identifiable { - byte[] sign(CryptoRequest request) throws SignatureException, KeyException; + byte[] sign(SignatureRequest request) throws SecurityException; - boolean verify(VerifySignatureRequest request) throws SignatureException, KeyException; + boolean verify(VerifySignatureRequest request) throws SecurityException; } diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java index deac14210..4d9944825 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; import io.jsonwebtoken.lang.Assert; @@ -9,102 +24,51 @@ import java.security.interfaces.ECKey; import java.security.interfaces.RSAKey; import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; /** * @since JJWT_RELEASE_VERSION */ +@SuppressWarnings("rawtypes") public final class SignatureAlgorithms { // Prevent instantiation private SignatureAlgorithms() { } - static final String HMAC = "io.jsonwebtoken.impl.security.MacSignatureAlgorithm"; - static final Class[] HMAC_ARGS = new Class[]{String.class, String.class, int.class}; - - private static final String RSA = "io.jsonwebtoken.impl.security.RsaSignatureAlgorithm"; - private static final Class[] RSA_ARGS = new Class[]{String.class, String.class, int.class}; - private static final Class[] PSS_ARGS = new Class[]{String.class, String.class, int.class, int.class}; + private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.SignatureAlgorithmsBridge"; + private static final Class[] ID_ARG_TYPES = new Class[]{String.class}; - private static final String EC = "io.jsonwebtoken.impl.security.EllipticCurveSignatureAlgorithm"; - private static final Class[] EC_ARGS = new Class[]{String.class, String.class, String.class, int.class, int.class}; - - private static SymmetricKeySignatureAlgorithm hmacSha(int minKeyLength) { - return Classes.newInstance(HMAC, HMAC_ARGS, "HS" + minKeyLength, "HmacSHA" + minKeyLength, minKeyLength); + public static Collection> values() { + return Classes.invokeStatic(BRIDGE_CLASSNAME, "values", null, (Object[]) null); } - private static AsymmetricKeySignatureAlgorithm rsa(int digestLength, int preferredKeyLength) { - return Classes.newInstance(RSA, RSA_ARGS, "RS" + digestLength, "SHA" + digestLength + "withRSA", preferredKeyLength); + public static SignatureAlgorithm findById(String id) { + Assert.hasText(id, "id cannot be null or empty."); + return Classes.invokeStatic(BRIDGE_CLASSNAME, "findById", ID_ARG_TYPES, id); } - private static AsymmetricKeySignatureAlgorithm pss(int digestLength, int preferredKeyLength) { - return Classes.newInstance(RSA, PSS_ARGS, "PS" + digestLength, "RSASSA-PSS", preferredKeyLength, digestLength); + public static SignatureAlgorithm forId(String id) { + return forId0(id); } - private static AsymmetricKeySignatureAlgorithm ec(int keySize, int signatureLength) { - int shaSize = keySize == 521 ? 512 : keySize; - return Classes.newInstance(EC, EC_ARGS, "ES" + shaSize, "SHA" + shaSize + "withECDSA", "secp" + keySize + "r1", keySize, signatureLength); + static T forId0(String id) { + Assert.hasText(id, "id cannot be null or empty."); + return Classes.invokeStatic(BRIDGE_CLASSNAME, "forId", ID_ARG_TYPES, id); } - public static final SignatureAlgorithm NONE = Classes.newInstance("io.jsonwebtoken.impl.security.NoneSignatureAlgorithm"); - public static final SymmetricKeySignatureAlgorithm HS256 = hmacSha(256); - public static final SymmetricKeySignatureAlgorithm HS384 = hmacSha(384); - public static final SymmetricKeySignatureAlgorithm HS512 = hmacSha(512); - public static final AsymmetricKeySignatureAlgorithm RS256 = rsa(256, 2048); - public static final AsymmetricKeySignatureAlgorithm RS384 = rsa(384, 3072); - public static final AsymmetricKeySignatureAlgorithm RS512 = rsa(512, 4096); - public static final AsymmetricKeySignatureAlgorithm PS256 = pss(256, 2048); - public static final AsymmetricKeySignatureAlgorithm PS384 = pss(384, 3072); - public static final AsymmetricKeySignatureAlgorithm PS512 = pss(512, 4096); - public static final AsymmetricKeySignatureAlgorithm ES256 = ec(256, 64); - public static final AsymmetricKeySignatureAlgorithm ES384 = ec(384, 96); - public static final AsymmetricKeySignatureAlgorithm ES512 = ec(521, 132); - - private static Map toMap(SignatureAlgorithm... algs) { - Map m = new LinkedHashMap<>(); - for (SignatureAlgorithm alg : algs) { - m.put(alg.getName(), alg); - } - return Collections.unmodifiableMap(m); - } - - private static final Map STANDARD_ALGORITHMS = toMap( - NONE, HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512 - ); - - public static Collection values() { - return STANDARD_ALGORITHMS.values(); - } - - /** - * Looks up and returns the corresponding JWA standard {@code SignatureAlgorithm} instance based on a - * case-insensitive name comparison. - * - * @param name The case-insensitive name of the JWA standard {@code SignatureAlgorithm} instance to return - * @return the corresponding JWA standard {@code SignatureAlgorithm} enum instance based on a - * case-insensitive name comparison. - * @throws SignatureException if the specified value does not match any JWA standard {@code SignatureAlgorithm} - * name. - */ - public static SignatureAlgorithm forName(String name) { - Assert.notNull(name, "name argument cannot be null."); - //try constant time lookup first. This will satisfy 99% of invocations: - SignatureAlgorithm alg = STANDARD_ALGORITHMS.get(name); - if (alg != null) { - return alg; - } - //fall back to case-insensitive lookup: - for (SignatureAlgorithm salg : STANDARD_ALGORITHMS.values()) { - if (name.equalsIgnoreCase(salg.getName())) { - return salg; - } - } - // still no result - error: - throw new SignatureException("Unsupported signature algorithm '" + name + "'"); - } + public static final SignatureAlgorithm NONE = forId0("none"); + public static final SecretKeySignatureAlgorithm HS256 = forId0("HS256"); + public static final SecretKeySignatureAlgorithm HS384 = forId0("HS384"); + public static final SecretKeySignatureAlgorithm HS512 = forId0("HS512"); + public static final RsaSignatureAlgorithm RS256 = forId0("RS256"); + public static final RsaSignatureAlgorithm RS384 = forId0("RS384"); + public static final RsaSignatureAlgorithm RS512 = forId0("RS512"); + public static final RsaSignatureAlgorithm PS256 = forId0("PS256"); + public static final RsaSignatureAlgorithm PS384 = forId0("PS384"); + public static final RsaSignatureAlgorithm PS512 = forId0("PS512"); + public static final EllipticCurveSignatureAlgorithm ES256 = forId0("ES256"); + public static final EllipticCurveSignatureAlgorithm ES384 = forId0("ES384"); + public static final EllipticCurveSignatureAlgorithm ES512 = forId0("ES512"); /** * Returns the recommended signature algorithm to be used with the specified key according to the following @@ -216,8 +180,9 @@ public static SignatureAlgorithm forName(String name) { * @throws InvalidKeyException for any key that does not match the heuristics and requirements documented above, * since that inevitably means the Key is either insufficient or explicitly disallowed by the JWT specification. */ - public static SignatureAlgorithm forSigningKey(Key key) { + public static SignatureAlgorithm forSigningKey(Key key) { + @SuppressWarnings("deprecation") io.jsonwebtoken.SignatureAlgorithm alg = io.jsonwebtoken.SignatureAlgorithm.forSigningKey(key); - return forName(alg.getValue()); + return forId(alg.getValue()); } } diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java b/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java new file mode 100644 index 000000000..d053fff7e --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface SignatureRequest extends CryptoRequest { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricEncryptionAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricEncryptionAlgorithm.java deleted file mode 100644 index a140da83c..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/SymmetricEncryptionAlgorithm.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jsonwebtoken.security; - -import javax.crypto.SecretKey; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface SymmetricEncryptionAlgorithm, ERes extends IvEncryptionResult, DReq extends IvRequest> extends EncryptionAlgorithm, SymmetricKeyAlgorithm { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricJwk.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricJwk.java deleted file mode 100644 index 6b81fcde1..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/SymmetricJwk.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface SymmetricJwk extends Jwk, SymmetricJwkMutator { - - String getK(); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricJwkBuilder.java deleted file mode 100644 index af63d4238..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/SymmetricJwkBuilder.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface SymmetricJwkBuilder extends JwkBuilder, SymmetricJwkMutator { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricJwkMutator.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricJwkMutator.java deleted file mode 100644 index 0ab48505e..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/SymmetricJwkMutator.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface SymmetricJwkMutator extends JwkMutator { - - T setK(String k); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricKeyAlgorithm.java deleted file mode 100644 index fb074973f..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/SymmetricKeyAlgorithm.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.jsonwebtoken.security; - -import javax.crypto.SecretKey; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface SymmetricKeyAlgorithm { - - /** - * Creates and returns a new secure-random key with a length sufficient to be used by this Algorithm. - * - * @return a new secure-random key with a length sufficient to be used by this Algorithm. - */ - SecretKey generateKey(); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricKeySignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricKeySignatureAlgorithm.java deleted file mode 100644 index 451b6c645..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/SymmetricKeySignatureAlgorithm.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface SymmetricKeySignatureAlgorithm extends SignatureAlgorithm, SymmetricKeyAlgorithm { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java b/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java index 890dec20a..f12ab4efa 100644 --- a/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; /** diff --git a/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java b/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java index b71a6c1d7..2b6632da3 100644 --- a/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; import java.security.Key; @@ -5,7 +20,5 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface VerifySignatureRequest extends CryptoRequest { - - byte[] getSignature(); +public interface VerifySignatureRequest extends SignatureRequest, DigestSupplier { } diff --git a/api/src/test/groovy/io/jsonwebtoken/EncryptionAlgorithmNameTest.groovy b/api/src/test/groovy/io/jsonwebtoken/EncryptionAlgorithmNameTest.groovy deleted file mode 100644 index 26fd5d0b3..000000000 --- a/api/src/test/groovy/io/jsonwebtoken/EncryptionAlgorithmNameTest.groovy +++ /dev/null @@ -1,61 +0,0 @@ -package io.jsonwebtoken - -import io.jsonwebtoken.security.EncryptionAlgorithmName -import org.junit.Test -import static org.junit.Assert.* - -class EncryptionAlgorithmNameTest { - - @Test - void testGetValue() { - assertEquals 'A128CBC-HS256', EncryptionAlgorithmName.A128CBC_HS256.getValue() - assertEquals 'A192CBC-HS384', EncryptionAlgorithmName.A192CBC_HS384.getValue() - assertEquals 'A256CBC-HS512', EncryptionAlgorithmName.A256CBC_HS512.getValue() - assertEquals 'A128GCM', EncryptionAlgorithmName.A128GCM.getValue() - assertEquals 'A192GCM', EncryptionAlgorithmName.A192GCM.getValue() - assertEquals 'A256GCM', EncryptionAlgorithmName.A256GCM.getValue() - } - - @Test - void testGetDescription() { - assertEquals 'AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.3', EncryptionAlgorithmName.A128CBC_HS256.getDescription() - assertEquals 'AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.4', EncryptionAlgorithmName.A192CBC_HS384.getDescription() - assertEquals 'AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.5', EncryptionAlgorithmName.A256CBC_HS512.getDescription() - assertEquals 'AES GCM using 128-bit key', EncryptionAlgorithmName.A128GCM.getDescription() - assertEquals 'AES GCM using 192-bit key', EncryptionAlgorithmName.A192GCM.getDescription() - assertEquals 'AES GCM using 256-bit key', EncryptionAlgorithmName.A256GCM.getDescription() - } - - @Test - void testGetJcaName() { - for( def name : EncryptionAlgorithmName.values() ) { - if (name.getValue().contains("GCM")) { - assertEquals 'AES/GCM/NoPadding', name.getJcaName() - } else { - assertEquals 'AES/CBC/PKCS5Padding', name.getJcaName() - } - } - } - - @Test - void testToString() { - for( def name : EncryptionAlgorithmName.values() ) { - assertEquals name.toString(), name.getValue() - } - } - - @Test - void testForName() { - def name = EncryptionAlgorithmName.forName('A128GCM') - assertSame name, EncryptionAlgorithmName.A128GCM - } - - @Test - void testForNameFailure() { - try { - EncryptionAlgorithmName.forName('foo') - fail() - } catch (IllegalArgumentException expected) { - } - } -} diff --git a/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy b/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy index c54b430d5..e09ae37c3 100644 --- a/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy @@ -16,7 +16,9 @@ package io.jsonwebtoken import org.junit.Test -import static org.junit.Assert.* + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail class JwtHandlerAdapterTest { @@ -49,7 +51,7 @@ class JwtHandlerAdapterTest { handler.onPlaintextJws(null) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed plaintext JWSs are not supported.' + assertEquals e.getMessage(), 'Signed plaintext JWTs are not supported.' } } @@ -60,7 +62,7 @@ class JwtHandlerAdapterTest { handler.onClaimsJws(null) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed Claims JWSs are not supported.' + assertEquals e.getMessage(), 'Signed Claims JWTs are not supported.' } } } diff --git a/api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy b/api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy index 2702003e6..df4fd5bad 100644 --- a/api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy @@ -29,7 +29,7 @@ class ArraysTest { @Test void testByteArrayLengthWithNull() { - assertEquals 0, Arrays.length(null) + assertEquals 0, Arrays.length((byte[])null) } @Test diff --git a/api/src/test/groovy/io/jsonwebtoken/security/CurveIdsTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/CurveIdsTest.groovy deleted file mode 100644 index 7a1af2dd7..000000000 --- a/api/src/test/groovy/io/jsonwebtoken/security/CurveIdsTest.groovy +++ /dev/null @@ -1,24 +0,0 @@ -package io.jsonwebtoken.security - -import org.junit.Test -import static org.junit.Assert.* - -class CurveIdsTest { - - @Test(expected=IllegalArgumentException) - void testNullId() { - CurveIds.forValue(null) - } - - @Test(expected=IllegalArgumentException) - void testEmptyId() { - CurveIds.forValue(' ') - } - - @Test - void testNonStandardId() { - CurveId id = CurveIds.forValue("NonStandard") - assertNotNull id - assertEquals 'NonStandard', id.toString() - } -} diff --git a/api/src/test/groovy/io/jsonwebtoken/security/KeyManagementAlgorithmNameTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/KeyManagementAlgorithmNameTest.groovy deleted file mode 100644 index 3a4366b7f..000000000 --- a/api/src/test/groovy/io/jsonwebtoken/security/KeyManagementAlgorithmNameTest.groovy +++ /dev/null @@ -1,54 +0,0 @@ -package io.jsonwebtoken.security - -import org.junit.Test -import static org.junit.Assert.* - -/** - * @since JJWT_RELEASE_VERSION - */ -class KeyManagementAlgorithmNameTest { - - @Test - void testToString() { - for( def name : KeyManagementAlgorithmName.values()) { - assertEquals name.value, name.toString() - } - } - - @Test - void testGetDescription() { - for( def name : KeyManagementAlgorithmName.values()) { - assertNotNull name.getDescription() //TODO improve this for actual value testing - } - } - - @Test - void testGetMoreHeaderParams() { - for( def name : KeyManagementAlgorithmName.values()) { - assertNotNull name.getMoreHeaderParams() //TODO improve this for actual value testing - } - } - - @Test - void testGetJcaName() { - for( def name : KeyManagementAlgorithmName.values()) { - assertNotNull name.getJcaName() //TODO improve this for actual value testing - } - } - - @Test - void testForName() { - def name = KeyManagementAlgorithmName.forName('A128KW') - assertSame name, KeyManagementAlgorithmName.A128KW - } - - @Test - void testForNameFailure() { - try { - KeyManagementAlgorithmName.forName('foo') - fail() - } catch (IllegalArgumentException expected) { - } - } -} - diff --git a/api/src/test/groovy/io/jsonwebtoken/security/KeyManagementModeNameTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/KeyManagementModeNameTest.groovy deleted file mode 100644 index 900894dca..000000000 --- a/api/src/test/groovy/io/jsonwebtoken/security/KeyManagementModeNameTest.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package io.jsonwebtoken.security - -import org.junit.Test -import static org.junit.Assert.* - -/** - * @since JJWT_RELEASE_VERSION - */ -class KeyManagementModeNameTest { - - @Test - void test() { - //todo, write a real test: - for(KeyManagementModeName modeName : KeyManagementModeName.values()) { - assertNotNull modeName.getName() - assertNotNull modeName.getDescription() - } - } -} diff --git a/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy index 8fa9148f3..f877fcc3c 100644 --- a/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy @@ -112,9 +112,9 @@ class KeysTest { if (name.startsWith('H')) { def key = createMock(SecretKey) - def salg = createMock(SymmetricKeySignatureAlgorithm) + def salg = createMock(SecretKeySignatureAlgorithm) - expect(SignatureAlgorithms.forName(eq(name))).andReturn(salg) + expect(SignatureAlgorithms.forId(eq(name))).andReturn(salg) expect(salg.generateKey()).andReturn(key) replay SignatureAlgorithms, salg, key @@ -125,7 +125,7 @@ class KeysTest { } else { def salg = name == 'NONE' ? createMock(io.jsonwebtoken.security.SignatureAlgorithm) : createMock(AsymmetricKeySignatureAlgorithm) - expect(SignatureAlgorithms.forName(eq(name))).andReturn(salg) + expect(SignatureAlgorithms.forId(eq(name))).andReturn(salg) replay SignatureAlgorithms, salg try { Keys.secretKeyFor(alg) @@ -148,8 +148,8 @@ class KeysTest { String name = alg.name() if (name.equals('NONE') || name.startsWith('H')) { - def salg = name == 'NONE' ? createMock(io.jsonwebtoken.security.SignatureAlgorithm) : createMock(SymmetricKeySignatureAlgorithm) - expect(SignatureAlgorithms.forName(eq(name))).andReturn(salg) + def salg = name == 'NONE' ? createMock(io.jsonwebtoken.security.SignatureAlgorithm) : createMock(SecretKeySignatureAlgorithm) + expect(SignatureAlgorithms.forId(eq(name))).andReturn(salg) replay SignatureAlgorithms, salg try { Keys.keyPairFor(alg) @@ -163,7 +163,7 @@ class KeysTest { def pair = createMock(KeyPair) def salg = createMock(AsymmetricKeySignatureAlgorithm) - expect(SignatureAlgorithms.forName(eq(name))).andReturn(salg) + expect(SignatureAlgorithms.forId(eq(name))).andReturn(salg) expect(salg.generateKeyPair()).andReturn(pair) replay SignatureAlgorithms, pair, salg diff --git a/impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java b/impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java new file mode 100644 index 000000000..a413593ff --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java @@ -0,0 +1,21 @@ +package io.jsonwebtoken.impl; + +import io.jsonwebtoken.CompressionCodec; +import io.jsonwebtoken.CompressionCodecResolver; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.lang.Assert; + +public class CompressionCodecLocator> implements Function { + + private final CompressionCodecResolver resolver; + + public CompressionCodecLocator(CompressionCodecResolver resolver) { + this.resolver = Assert.notNull(resolver, "CompressionCodecResolver cannot be null."); + } + + @Override + public CompressionCodec apply(H header) { + return resolver.resolveCompressionCodec(header); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java index 7dba1763d..cc9b59013 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java @@ -20,17 +20,21 @@ import java.util.Map; -@SuppressWarnings("unchecked") public class DefaultHeader> extends JwtMap implements Header { public DefaultHeader() { super(); } - public DefaultHeader(Map map) { + public DefaultHeader(Map map) { super(map); } + @SuppressWarnings("unchecked") + protected T tthis() { + return (T)this; + } + @Override public String getType() { return getString(TYPE); @@ -39,7 +43,7 @@ public String getType() { @Override public T setType(String typ) { setValue(TYPE, typ); - return (T)this; + return tthis(); } @Override @@ -50,7 +54,7 @@ public String getContentType() { @Override public T setContentType(String cty) { setValue(CONTENT_TYPE, cty); - return (T)this; + return tthis(); } @Override @@ -61,7 +65,7 @@ public String getAlgorithm() { @Override public T setAlgorithm(String alg) { setValue(ALGORITHM, alg); - return (T)this; + return tthis(); } @SuppressWarnings("deprecation") @@ -78,7 +82,6 @@ public String getCompressionAlgorithm() { @Override public T setCompressionAlgorithm(String compressionAlgorithm) { setValue(COMPRESSION_ALGORITHM, compressionAlgorithm); - return (T) this; + return tthis(); } - } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwe.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwe.java new file mode 100644 index 000000000..667755466 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwe.java @@ -0,0 +1,44 @@ +package io.jsonwebtoken.impl; + +import io.jsonwebtoken.Jwe; +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; + +public class DefaultJwe extends DefaultJwt implements Jwe { + + private final byte[] iv; + private final byte[] aadTag; + + public DefaultJwe(JweHeader header, B body, byte[] iv, byte[] aadTag) { + super(header, body); + this.iv = Assert.notEmpty(iv, "Initialization vector cannot be null or empty."); + this.aadTag = Assert.notEmpty(aadTag, "AAD tag cannot be null or empty."); + } + + @Override + public byte[] getInitializationVector() { + return this.iv; + } + + @Override + public byte[] getAadTag() { + return this.aadTag; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Jwe) { + Jwe jwe = (Jwe)obj; + return super.equals(jwe) && + Objects.nullSafeEquals(iv, jwe.getInitializationVector()) && + Objects.nullSafeEquals(aadTag, jwe.getAadTag()); + } + return false; + } + + @Override + public int hashCode() { + return Objects.nullSafeHashCode(getHeader(), getBody(), iv, aadTag); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java new file mode 100644 index 000000000..240046a9d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java @@ -0,0 +1,181 @@ +package io.jsonwebtoken.impl; + +import io.jsonwebtoken.JweBuilder; +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.impl.lang.PropagatingExceptionFunction; +import io.jsonwebtoken.impl.lang.Services; +import io.jsonwebtoken.impl.security.DefaultAeadRequest; +import io.jsonwebtoken.impl.security.DefaultKeyRequest; +import io.jsonwebtoken.io.SerializationException; +import io.jsonwebtoken.io.Serializer; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.AeadRequest; +import io.jsonwebtoken.security.AeadResult; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.KeyAlgorithms; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.PbeKey; +import io.jsonwebtoken.security.SecurityException; + +import javax.crypto.SecretKey; +import javax.crypto.interfaces.PBEKey; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Map; + +public class DefaultJweBuilder extends DefaultJwtBuilder implements JweBuilder { + + private AeadAlgorithm enc; // MUST be Symmetric AEAD per https://tools.ietf.org/html/rfc7516#section-4.1.2 + private Function encFunction; + + private KeyAlgorithm alg; + private Function, KeyResult> algFunction; + + private Key key; + + protected Function wrap(String msg, Function fn) { + return new PropagatingExceptionFunction<>(SecurityException.class, msg, fn); + } + + //TODO for 1.0: delete this method when the parent class's implementation has changed to SerializationException + @Override + protected Function, byte[]> wrap(final Serializer> serializer, String which) { + return new PropagatingExceptionFunction<>(SerializationException.class, + "Unable to serialize " + which + " to JSON.", new Function, byte[]>() { + @Override + public byte[] apply(Map map) { + return serializer.serialize(map); + } + } + ); + } + + @Override + public JweBuilder setPayload(String payload) { + Assert.hasLength(payload, "payload cannot be null or empty."); //allowed for JWS, but not JWE + return super.setPayload(payload); + } + + @Override + public JweBuilder encryptWith(final AeadAlgorithm enc) { + this.enc = Assert.notNull(enc, "Encryption algorithm cannot be null."); + Assert.hasText(enc.getId(), "Encryption algorithm id cannot be null or empty."); + String encMsg = enc.getId() + " encryption failed."; + this.encFunction = wrap(encMsg, new Function() { + @Override + public AeadResult apply(AeadRequest request) { + return enc.encrypt(request); + } + }); + return this; + } + + @Override + public JweBuilder withKey(SecretKey key) { + if (key instanceof PBEKey) { + key = Keys.toPbeKey((PBEKey) key); + } + if (key instanceof PbeKey) { + return withKeyFrom((PbeKey) key, KeyAlgorithms.PBES2_HS512_A256KW); + } + return withKeyFrom(key, KeyAlgorithms.DIRECT); + } + + @Override + public JweBuilder withKeyFrom(K key, final KeyAlgorithm alg) { + this.key = Assert.notNull(key, "key cannot be null."); + //noinspection unchecked + this.alg = (KeyAlgorithm) Assert.notNull(alg, "KeyAlgorithm cannot be null."); + final KeyAlgorithm keyAlg = this.alg; + Assert.hasText(alg.getId(), "KeyAlgorithm id cannot be null or empty."); + + String cekMsg = "Unable to obtain content encryption key from key management algorithm '" + alg.getId() + "'."; + this.algFunction = wrap(cekMsg, new Function, KeyResult>() { + @Override + public KeyResult apply(KeyRequest request) { + return keyAlg.getEncryptionKey(request); + } + }); + + return this; + } + + @Override + public String compact() { + + if (!Strings.hasLength(payload) && Collections.isEmpty(claims)) { + throw new IllegalStateException("Either 'claims' or a non-empty 'payload' must be specified."); + } + + if (Strings.hasLength(payload) && !Collections.isEmpty(claims)) { + throw new IllegalStateException("Both 'payload' and 'claims' cannot both be specified. Choose either one."); + } + + Assert.state(key != null, "Key is required."); + Assert.state(enc != null, "Encryption algorithm is required."); + assert alg != null : "KeyAlgorithm is required."; //always set by withKey calling withKeyFrom + + if (this.serializer == null) { // try to find one based on the services available + //noinspection unchecked + serializeToJsonWith(Services.loadFirst(Serializer.class)); + } + + header = ensureHeader(); + + JweHeader jweHeader; + if (header instanceof JweHeader) { + jweHeader = (JweHeader) header; + } else { + header = jweHeader = new DefaultJweHeader(header); + } + + byte[] plaintext = this.payload != null ? payload.getBytes(Strings.UTF_8) : claimsSerializer.apply(claims); + Assert.state(Arrays.length(plaintext) > 0, "Payload bytes cannot be empty."); // JWE invariant (JWS can be empty however) + + if (compressionCodec != null) { + plaintext = compressionCodec.compress(plaintext); + jweHeader.setCompressionAlgorithm(compressionCodec.getAlgorithmName()); + } + + KeyRequest keyRequest = new DefaultKeyRequest<>(this.provider, this.secureRandom, this.key, jweHeader, enc); + KeyResult keyResult = algFunction.apply(keyRequest); + + Assert.state(keyResult != null, "KeyAlgorithm must return a KeyResult."); + SecretKey cek = Assert.notNull(keyResult.getKey(), "KeyResult must return a content encryption key."); + byte[] encryptedCek = Assert.notNull(keyResult.getPayload(), "KeyResult must return an encrypted key byte array, even if empty."); + + jweHeader.setAlgorithm(alg.getId()); + jweHeader.setEncryptionAlgorithm(enc.getId()); + + byte[] headerBytes = this.headerSerializer.apply(jweHeader); + final String base64UrlEncodedHeader = base64UrlEncoder.encode(headerBytes); + byte[] aad = base64UrlEncodedHeader.getBytes(StandardCharsets.US_ASCII); + + AeadRequest encRequest = new DefaultAeadRequest(provider, secureRandom, plaintext, cek, aad); + AeadResult encResult = encFunction.apply(encRequest); + + byte[] iv = Assert.notEmpty(encResult.getInitializationVector(), "Encryption result must have a non-empty initialization vector."); + byte[] ciphertext = Assert.notEmpty(encResult.getPayload(), "Encryption result must have non-empty ciphertext (result.getData())."); + byte[] tag = Assert.notEmpty(encResult.getDigest(), "Encryption result must have a non-empty authentication tag."); + + String base64UrlEncodedEncryptedCek = base64UrlEncoder.encode(encryptedCek); + String base64UrlEncodedIv = base64UrlEncoder.encode(iv); + String base64UrlEncodedCiphertext = base64UrlEncoder.encode(ciphertext); + String base64UrlEncodedTag = base64UrlEncoder.encode(tag); + + return + base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + + base64UrlEncodedEncryptedCek + JwtParser.SEPARATOR_CHAR + + base64UrlEncodedIv + JwtParser.SEPARATOR_CHAR + + base64UrlEncodedCiphertext + JwtParser.SEPARATOR_CHAR + + base64UrlEncodedTag; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java index db69ab44b..cd4cdf84d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java @@ -13,7 +13,7 @@ public DefaultJweHeader() { super(); } - public DefaultJweHeader(Map map) { + public DefaultJweHeader(Map map) { super(map); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java index fe83244ce..19429c33b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java @@ -17,36 +17,39 @@ import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.lang.Objects; -public class DefaultJws implements Jws { +public class DefaultJws extends DefaultJwt implements Jws { - private final JwsHeader header; - private final B body; private final String signature; public DefaultJws(JwsHeader header, B body, String signature) { - this.header = header; - this.body = body; + super(header, body); this.signature = signature; } @Override - public JwsHeader getHeader() { - return this.header; + public String getSignature() { + return this.signature; } @Override - public B getBody() { - return this.body; + public String toString() { + return super.toString() + ",signature=" + signature; } @Override - public String getSignature() { - return this.signature; + public boolean equals(Object obj) { + if (obj instanceof Jws) { + Jws jws = (Jws) obj; + return super.equals(jws) && + Objects.nullSafeEquals(signature, jws.getSignature()); + } + return false; } @Override - public String toString() { - return "header=" + header + ",body=" + body + ",signature=" + signature; + public int hashCode() { + return Objects.nullSafeHashCode(getHeader(), getBody(), signature); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java index bb23dff15..e4a02fe09 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java @@ -25,7 +25,7 @@ public DefaultJwsHeader() { super(); } - public DefaultJwsHeader(Map map) { + public DefaultJwsHeader(Map map) { super(map); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwt.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwt.java index e09bd0065..68a5c54e8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwt.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwt.java @@ -17,19 +17,21 @@ import io.jsonwebtoken.Header; import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; -public class DefaultJwt implements Jwt { +public class DefaultJwt, B> implements Jwt { - private final Header header; + private final H header; private final B body; - public DefaultJwt(Header header, B body) { - this.header = header; - this.body = body; + public DefaultJwt(H header, B body) { + this.header = Assert.notNull(header, "header cannot be null."); + this.body = Assert.notNull(body, "body cannot be null."); } @Override - public Header getHeader() { + public H getHeader() { return header; } @@ -42,4 +44,22 @@ public B getBody() { public String toString() { return "header=" + header + ",body=" + body; } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Jwt) { + Jwt jwt = (Jwt)obj; + return Objects.nullSafeEquals(header, jwt.getHeader()) && + Objects.nullSafeEquals(body, jwt.getBody()); + } + return false; + } + + @Override + public int hashCode() { + return Objects.nullSafeHashCode(header, body); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index 78421cfc5..a291d1933 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -21,20 +21,24 @@ import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.impl.lang.Function; import io.jsonwebtoken.impl.lang.LegacyServices; -import io.jsonwebtoken.impl.security.DefaultCryptoRequest; +import io.jsonwebtoken.impl.lang.PropagatingExceptionFunction; +import io.jsonwebtoken.impl.security.DefaultSignatureRequest; +import io.jsonwebtoken.impl.security.SignatureAlgorithmsBridge; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Encoder; import io.jsonwebtoken.io.Encoders; -import io.jsonwebtoken.io.SerializationException; import io.jsonwebtoken.io.Serializer; +import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.CryptoRequest; import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.SignatureAlgorithm; import io.jsonwebtoken.security.SignatureAlgorithms; +import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.SignatureRequest; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -45,115 +49,139 @@ import java.util.Date; import java.util.Map; -public class DefaultJwtBuilder implements JwtBuilder { +@SuppressWarnings("unchecked") +public class DefaultJwtBuilder> implements JwtBuilder { - private static final byte[] TEST_MESSAGE_BYTES = "Test message".getBytes(StandardCharsets.UTF_8); + protected Provider provider; + protected SecureRandom secureRandom; - private Provider provider; - private SecureRandom secureRandom; + protected Header header; + protected Claims claims; + protected String payload; - private Header header; - private Claims claims; - private String payload; - - private SignatureAlgorithm algorithm = SignatureAlgorithms.NONE; + private SignatureAlgorithm algorithm = SignatureAlgorithms.NONE; + private Function, byte[]> signFunction; private Key key; - private Serializer> serializer; - - private Encoder base64UrlEncoder = Encoders.BASE64URL; + protected Serializer> serializer; + protected Function, byte[]> headerSerializer; + protected Function, byte[]> claimsSerializer; - private CompressionCodec compressionCodec; + protected Encoder base64UrlEncoder = Encoders.BASE64URL; + protected CompressionCodec compressionCodec; @Override - public JwtBuilder setProvider(Provider provider) { + public T setProvider(Provider provider) { this.provider = provider; - return this; + return (T)this; } @Override - public JwtBuilder setSecureRandom(SecureRandom secureRandom) { + public T setSecureRandom(SecureRandom secureRandom) { this.secureRandom = secureRandom; - return this; + return (T)this; + } + + @SuppressWarnings("rawtypes") + protected Function, byte[]> wrap(final Serializer> serializer, String which) { + // TODO for 1.0 - these should throw SerializationException not IllegalArgumentException + // IAE is being retained for backwards pre-1.0 behavior compatibility + Class clazz = "header".equals(which) ? IllegalStateException.class : IllegalArgumentException.class; + return new PropagatingExceptionFunction<>(clazz, + "Unable to serialize " + which + " to JSON.", + new Function, byte[]>() { + @Override + public byte[] apply(Map map) { + return serializer.serialize(map); + } + } + ); } @Override - public JwtBuilder serializeToJsonWith(Serializer> serializer) { + public T serializeToJsonWith(final Serializer> serializer) { Assert.notNull(serializer, "Serializer cannot be null."); this.serializer = serializer; - return this; + this.headerSerializer = wrap(serializer, "header"); + this.claimsSerializer = wrap(serializer, "claims"); + return (T)this; } @Override - public JwtBuilder base64UrlEncodeWith(Encoder base64UrlEncoder) { + public T base64UrlEncodeWith(Encoder base64UrlEncoder) { Assert.notNull(base64UrlEncoder, "base64UrlEncoder cannot be null."); this.base64UrlEncoder = base64UrlEncoder; - return this; + return (T)this; } @Override - public JwtBuilder setHeader(Header header) { + public T setHeader(Header header) { this.header = header; - return this; + return (T)this; } @Override - public JwtBuilder setHeader(Map header) { - this.header = new DefaultHeader(header); - return this; + public T setHeader(Map header) { + this.header = new DefaultHeader<>(header); + return (T)this; } @Override - public JwtBuilder setHeaderParams(Map params) { + public T setHeaderParams(Map params) { if (!Collections.isEmpty(params)) { - - Header header = ensureHeader(); - - for (Map.Entry entry : params.entrySet()) { - header.put(entry.getKey(), entry.getValue()); - } + Header header = ensureHeader(); + header.putAll(params); } - return this; + return (T)this; } - protected Header ensureHeader() { + protected Header ensureHeader() { if (this.header == null) { - this.header = new DefaultHeader(); + this.header = new DefaultHeader<>(); } return this.header; } @Override - public JwtBuilder setHeaderParam(String name, Object value) { + public T setHeaderParam(String name, Object value) { ensureHeader().put(name, value); - return this; + return (T)this; } @Override - public JwtBuilder signWith(Key key) throws InvalidKeyException { + public T signWith(Key key) throws InvalidKeyException { Assert.notNull(key, "Key argument cannot be null."); - SignatureAlgorithm alg = SignatureAlgorithms.forSigningKey(key); + SignatureAlgorithm alg = (SignatureAlgorithm)SignatureAlgorithms.forSigningKey(key); return signWith(key, alg); } @Override - public JwtBuilder signWith(Key key, SignatureAlgorithm alg) throws InvalidKeyException { + public T signWith(K key, final SignatureAlgorithm alg) throws InvalidKeyException { Assert.notNull(key, "Key argument cannot be null."); Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - this.algorithm = alg; this.key = key; - return this; + this.algorithm = (SignatureAlgorithm)alg; + this.signFunction = new PropagatingExceptionFunction<>(SignatureException.class, + "Unable to compute " + alg.getId() + " signature.", new Function, byte[]>() { + @Override + public byte[] apply(SignatureRequest request) { + return algorithm.sign(request); + } + }); + return (T)this; } + @SuppressWarnings("deprecation") // TODO: remove method for 1.0 @Override - public JwtBuilder signWith(Key key, io.jsonwebtoken.SignatureAlgorithm alg) throws InvalidKeyException { + public T signWith(Key key, io.jsonwebtoken.SignatureAlgorithm alg) throws InvalidKeyException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - return signWith(key, SignatureAlgorithms.forName(alg.getValue())); + return signWith(key, (SignatureAlgorithm)SignatureAlgorithmsBridge.forId(alg.getValue())); } + @SuppressWarnings("deprecation") // TODO: remove method for 1.0 @Override - public JwtBuilder signWith(io.jsonwebtoken.SignatureAlgorithm alg, byte[] secretKeyBytes) throws InvalidKeyException { + public T signWith(io.jsonwebtoken.SignatureAlgorithm alg, byte[] secretKeyBytes) throws InvalidKeyException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); Assert.notEmpty(secretKeyBytes, "secret key byte array cannot be null or empty."); Assert.isTrue(alg.isHmac(), "Key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead."); @@ -161,30 +189,32 @@ public JwtBuilder signWith(io.jsonwebtoken.SignatureAlgorithm alg, byte[] secret return signWith(key, alg); } + @SuppressWarnings("deprecation") // TODO: remove method for 1.0 @Override - public JwtBuilder signWith(io.jsonwebtoken.SignatureAlgorithm alg, String base64EncodedSecretKey) throws InvalidKeyException { + public T signWith(io.jsonwebtoken.SignatureAlgorithm alg, String base64EncodedSecretKey) throws InvalidKeyException { Assert.hasText(base64EncodedSecretKey, "base64-encoded secret key cannot be null or empty."); Assert.isTrue(alg.isHmac(), "Base64-encoded key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead."); byte[] bytes = Decoders.BASE64.decode(base64EncodedSecretKey); return signWith(alg, bytes); } + @SuppressWarnings("deprecation") // TODO: remove method for 1.0 @Override - public JwtBuilder signWith(io.jsonwebtoken.SignatureAlgorithm alg, Key key) { + public T signWith(io.jsonwebtoken.SignatureAlgorithm alg, Key key) { return signWith(key, alg); } @Override - public JwtBuilder compressWith(CompressionCodec compressionCodec) { + public T compressWith(CompressionCodec compressionCodec) { Assert.notNull(compressionCodec, "compressionCodec cannot be null"); this.compressionCodec = compressionCodec; - return this; + return (T)this; } @Override - public JwtBuilder setPayload(String payload) { + public T setPayload(String payload) { this.payload = payload; - return this; + return (T)this; } protected Claims ensureClaims() { @@ -195,25 +225,25 @@ protected Claims ensureClaims() { } @Override - public JwtBuilder setClaims(Claims claims) { + public T setClaims(Claims claims) { this.claims = claims; - return this; + return (T)this; } @Override - public JwtBuilder setClaims(Map claims) { + public T setClaims(Map claims) { this.claims = new DefaultClaims(claims); - return this; + return (T)this; } @Override - public JwtBuilder addClaims(Map claims) { + public T addClaims(Map claims) { ensureClaims().putAll(claims); - return this; + return (T)this; } @Override - public JwtBuilder setIssuer(String iss) { + public T setIssuer(String iss) { if (Strings.hasText(iss)) { ensureClaims().setIssuer(iss); } else { @@ -221,11 +251,11 @@ public JwtBuilder setIssuer(String iss) { claims.setIssuer(iss); } } - return this; + return (T)this; } @Override - public JwtBuilder setSubject(String sub) { + public T setSubject(String sub) { if (Strings.hasText(sub)) { ensureClaims().setSubject(sub); } else { @@ -233,11 +263,11 @@ public JwtBuilder setSubject(String sub) { claims.setSubject(sub); } } - return this; + return (T)this; } @Override - public JwtBuilder setAudience(String aud) { + public T setAudience(String aud) { if (Strings.hasText(aud)) { ensureClaims().setAudience(aud); } else { @@ -245,11 +275,11 @@ public JwtBuilder setAudience(String aud) { claims.setAudience(aud); } } - return this; + return (T)this; } @Override - public JwtBuilder setExpiration(Date exp) { + public T setExpiration(Date exp) { if (exp != null) { ensureClaims().setExpiration(exp); } else { @@ -258,11 +288,11 @@ public JwtBuilder setExpiration(Date exp) { this.claims.setExpiration(exp); } } - return this; + return (T)this; } @Override - public JwtBuilder setNotBefore(Date nbf) { + public T setNotBefore(Date nbf) { if (nbf != null) { ensureClaims().setNotBefore(nbf); } else { @@ -271,11 +301,11 @@ public JwtBuilder setNotBefore(Date nbf) { this.claims.setNotBefore(nbf); } } - return this; + return (T)this; } @Override - public JwtBuilder setIssuedAt(Date iat) { + public T setIssuedAt(Date iat) { if (iat != null) { ensureClaims().setIssuedAt(iat); } else { @@ -284,11 +314,11 @@ public JwtBuilder setIssuedAt(Date iat) { this.claims.setIssuedAt(iat); } } - return this; + return (T)this; } @Override - public JwtBuilder setId(String jti) { + public T setId(String jti) { if (Strings.hasText(jti)) { ensureClaims().setId(jti); } else { @@ -296,11 +326,11 @@ public JwtBuilder setId(String jti) { claims.setId(jti); } } - return this; + return (T)this; } @Override - public JwtBuilder claim(String name, Object value) { + public T claim(String name, Object value) { Assert.hasText(name, "Claim property name cannot be null or empty."); if (this.claims == null) { if (value != null) { @@ -314,7 +344,7 @@ public JwtBuilder claim(String name, Object value) { } } - return this; + return (T)this; } @Override @@ -324,7 +354,8 @@ public String compact() { // try to find one based on the services available // TODO: This util class will throw a UnavailableImplementationException here to retain behavior of previous version, remove in v1.0 // use the previous commented out line instead - this.serializer = LegacyServices.loadFirst(Serializer.class); + //noinspection deprecation + serializeToJsonWith(LegacyServices.loadFirst(Serializer.class)); } if (payload == null && Collections.isEmpty(claims)) { @@ -335,40 +366,35 @@ public String compact() { throw new IllegalStateException("Both 'payload' and 'claims' cannot both be specified. Choose either one."); } - Header header = ensureHeader(); + Header header = ensureHeader(); JwsHeader jwsHeader; if (header instanceof JwsHeader) { jwsHeader = (JwsHeader) header; } else { - //noinspection unchecked header = jwsHeader = new DefaultJwsHeader(header); } Assert.state(algorithm != null, "algorithm instance should never be null."); // invariant - jwsHeader.setAlgorithm(algorithm.getName()); + jwsHeader.setAlgorithm(algorithm.getId()); - byte[] bytes; - try { - bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : toJson(claims); - } catch (SerializationException e) { - throw new IllegalArgumentException("Unable to serialize claims object to json: " + e.getMessage(), e); - } + byte[] bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : claimsSerializer.apply(claims); - if (compressionCodec != null) { + if (Arrays.length(bytes) > 0 && compressionCodec != null) { header.setCompressionAlgorithm(compressionCodec.getAlgorithmName()); bytes = compressionCodec.compress(bytes); } - String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json."); + byte[] headerBytes = headerSerializer.apply(jwsHeader); + String base64UrlEncodedHeader = base64UrlEncoder.encode(headerBytes); String base64UrlEncodedBody = base64UrlEncoder.encode(bytes); String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody; if (key != null) { //jwt must be signed: byte[] data = jwt.getBytes(StandardCharsets.US_ASCII); - CryptoRequest request = new DefaultCryptoRequest<>(data, key, provider, secureRandom); - byte[] signature = algorithm.sign(request); + SignatureRequest request = new DefaultSignatureRequest<>(provider, secureRandom, data, key); + byte[] signature = signFunction.apply(request); String base64UrlSignature = base64UrlEncoder.encode(signature); jwt += JwtParser.SEPARATOR_CHAR + base64UrlSignature; } else { @@ -379,26 +405,4 @@ public String compact() { return jwt; } - - @Deprecated // remove before 1.0 - call the serializer and base64UrlEncoder directly - protected String base64UrlEncode(Object o, String errMsg) { - Assert.isInstanceOf(Map.class, o, "object argument must be a map."); - Map m = (Map) o; - byte[] bytes; - try { - bytes = toJson(m); - } catch (SerializationException e) { - throw new IllegalStateException(errMsg, e); - } - - return base64UrlEncoder.encode(bytes); - } - - @SuppressWarnings("unchecked") - @Deprecated //remove before 1.0 - call the serializer directly - protected byte[] toJson(Object object) throws SerializationException { - Assert.isInstanceOf(Map.class, object, "object argument must be a map."); - Map m = (Map) object; - return serializer.serialize(m); - } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index a31588e41..b74b03e55 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -22,11 +22,15 @@ import io.jsonwebtoken.CompressionCodecResolver; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Header; +import io.jsonwebtoken.Identifiable; import io.jsonwebtoken.IncorrectClaimException; import io.jsonwebtoken.InvalidClaimException; +import io.jsonwebtoken.Jwe; +import io.jsonwebtoken.JweHeader; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtHandler; import io.jsonwebtoken.JwtHandlerAdapter; import io.jsonwebtoken.JwtParser; @@ -36,32 +40,45 @@ import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; -import io.jsonwebtoken.impl.crypto.DefaultSignatureValidatorFactory; -import io.jsonwebtoken.impl.crypto.SignatureValidator; -import io.jsonwebtoken.impl.crypto.SignatureValidatorFactory; +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.ConstantFunction; +import io.jsonwebtoken.impl.lang.Function; import io.jsonwebtoken.impl.lang.LegacyServices; +import io.jsonwebtoken.impl.security.ConstantKeyLocator; +import io.jsonwebtoken.impl.security.DefaultAeadResult; +import io.jsonwebtoken.impl.security.DefaultDecryptionKeyRequest; import io.jsonwebtoken.impl.security.DefaultVerifySignatureRequest; +import io.jsonwebtoken.impl.security.EncryptionAlgorithmsBridge; +import io.jsonwebtoken.impl.security.KeyAlgorithmsBridge; +import io.jsonwebtoken.impl.security.SignatureAlgorithmsBridge; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.DecodingException; import io.jsonwebtoken.io.DeserializationException; import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.DateFormats; -import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.DecryptAeadRequest; +import io.jsonwebtoken.security.DecryptionKeyRequest; import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.PayloadSupplier; import io.jsonwebtoken.security.SignatureAlgorithm; import io.jsonwebtoken.security.SignatureAlgorithms; import io.jsonwebtoken.security.SignatureException; -import io.jsonwebtoken.security.SymmetricKeySignatureAlgorithm; import io.jsonwebtoken.security.VerifySignatureRequest; import io.jsonwebtoken.security.WeakKeyException; -import javax.crypto.spec.SecretKeySpec; +import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.security.Key; import java.security.Provider; +import java.util.Collection; import java.util.Date; import java.util.Map; @@ -72,16 +89,62 @@ public class DefaultJwtParser implements JwtParser { private static final JwtTokenizer jwtTokenizer = new JwtTokenizer(); - // TODO: make the folling fields final for v1.0 - private Provider provider; + public static final String MISSING_JWS_ALG_MSG = + "JWS header does not contain a required 'alg' (Algorithm) header parameter. " + + "This header parameter is mandatory per the JWS Specification, Section 4.1.1. See " + + "https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.1 for more information."; + + public static final String MISSING_JWE_ALG_MSG = + "JWE header does not contain a required 'alg' (Algorithm) header parameter. " + + "This header parameter is mandatory per the JWE Specification, Section 4.1.1. See " + + "https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.1 for more information."; + + private static final String MISSING_ENC_MSG = + "JWE header does not contain a required 'enc' (Encryption Algorithm) header parameter. " + + "This header parameter is mandatory per the JWE Specification, Section 4.1.2. See " + + "https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.2 for more information."; + + private static , R extends Identifiable> Function backup(String id, String msg, Collection extras) { + if (Collections.isEmpty(extras)) { + return ConstantFunction.forNull(); + } else { + return new IdLocator<>(id, msg, new IdRegistry<>(extras), ConstantFunction.forNull()); + } + } - private byte[] keyBytes; + private static , R extends Identifiable> Function locFn(String id, String msg, Function reg, Collection extras) { + Function backup = backup(id, msg, extras); + return new IdLocator<>(id, msg, reg, backup); + } - private Key key; + private static Function> sigFn(Collection> extras) { + return locFn(JwsHeader.ALGORITHM, MISSING_JWS_ALG_MSG, SignatureAlgorithmsBridge.REGISTRY, extras); + } + + private static Function encFn(Collection extras) { + return locFn(JweHeader.ENCRYPTION_ALGORITHM, MISSING_ENC_MSG, EncryptionAlgorithmsBridge.REGISTRY, extras); + } + + private static Function> keyFn(Collection> extras) { + return locFn(JweHeader.ALGORITHM, MISSING_JWE_ALG_MSG, KeyAlgorithmsBridge.REGISTRY, extras); + } + + // TODO: make the following fields final for v1.0 + private Provider provider; + @SuppressWarnings("deprecation") // will remove for 1.0 private SigningKeyResolver signingKeyResolver; - private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); + @SuppressWarnings("rawtypes") + private Function compressionCodecLocator; + + private final Function> signatureAlgorithmLocator; + + private final Function encryptionAlgorithmLocator; + + private final Function> keyAlgorithmLocator; + + private final Function keyLocator; private Decoder base64UrlDecoder = Decoders.BASE64URL; @@ -98,30 +161,44 @@ public class DefaultJwtParser implements JwtParser { * * @deprecated for backward compatibility only, see other constructors. */ + @SuppressWarnings("DeprecatedIsStillUsed") // will remove before 1.0 @Deprecated public DefaultJwtParser() { + ConstantKeyLocator constantKeyLocator = new ConstantKeyLocator<>(null, null); + this.keyLocator = constantKeyLocator; + this.signingKeyResolver = constantKeyLocator; + this.signatureAlgorithmLocator = sigFn(Collections.>emptyList()); + this.keyAlgorithmLocator = keyFn(Collections.>emptyList()); + this.encryptionAlgorithmLocator = encFn(Collections.emptyList()); + this.compressionCodecLocator = new CompressionCodecLocator<>(new DefaultCompressionCodecResolver()); } + @SuppressWarnings("deprecation") + //SigningKeyResolver will be removed for 1.0 DefaultJwtParser(Provider provider, SigningKeyResolver signingKeyResolver, - Key key, - byte[] keyBytes, + Function keyLocator, Clock clock, long allowedClockSkewMillis, Claims expectedClaims, Decoder base64UrlDecoder, Deserializer> deserializer, - CompressionCodecResolver compressionCodecResolver) { + CompressionCodecResolver compressionCodecResolver, + Collection> extraSigAlgs, + Collection> extraKeyAlgs, + Collection extraEncAlgs) { this.provider = provider; - this.signingKeyResolver = signingKeyResolver; - this.key = key; - this.keyBytes = keyBytes; + this.signingKeyResolver = Assert.notNull(signingKeyResolver, "SigningKeyResolver cannot be null."); + this.keyLocator = Assert.notNull(keyLocator, "Key Locator cannot be null."); this.clock = clock; this.allowedClockSkewMillis = allowedClockSkewMillis; this.expectedClaims = expectedClaims; this.base64UrlDecoder = base64UrlDecoder; this.deserializer = deserializer; - this.compressionCodecResolver = compressionCodecResolver; + this.signatureAlgorithmLocator = sigFn(extraSigAlgs); + this.keyAlgorithmLocator = keyFn(extraKeyAlgs); + this.encryptionAlgorithmLocator = encFn(extraEncAlgs); + this.compressionCodecLocator = new CompressionCodecLocator<>(compressionCodecResolver); } @Override @@ -205,24 +282,24 @@ public JwtParser setAllowedClockSkewSeconds(long seconds) throws IllegalArgument @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); - this.keyBytes = key; - return this; + return setSigningKey(Keys.hmacShaKeyFor(key)); } @Override public JwtParser setSigningKey(String base64EncodedSecretKey) { Assert.hasText(base64EncodedSecretKey, "signing key cannot be null or empty."); - this.keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey); - return this; + byte[] bytes = Decoders.BASE64.decode(base64EncodedSecretKey); + return setSigningKey(bytes); } @Override - public JwtParser setSigningKey(Key key) { + public JwtParser setSigningKey(final Key key) { Assert.notNull(key, "signing key cannot be null."); - this.key = key; + setSigningKeyResolver(new ConstantKeyLocator<>(key, null)); return this; } + @SuppressWarnings("deprecation") // required until 1.0 @Override public JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver) { Assert.notNull(signingKeyResolver, "SigningKeyResolver cannot be null."); @@ -233,21 +310,20 @@ public JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver) { @Override public JwtParser setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver) { Assert.notNull(compressionCodecResolver, "compressionCodecResolver cannot be null."); - this.compressionCodecResolver = compressionCodecResolver; + this.compressionCodecLocator = new CompressionCodecLocator<>(compressionCodecResolver); return this; } @Override - public boolean isSigned(String jwt) { - - if (jwt == null) { + public boolean isSigned(String compact) { + if (compact == null) { return false; } int delimiterCount = 0; - for (int i = 0; i < jwt.length(); i++) { - char c = jwt.charAt(i); + for (int i = 0; i < compact.length(); i++) { + char c = compact.charAt(i); if (delimiterCount == 2) { return !Character.isWhitespace(c) && c != SEPARATOR_CHAR; @@ -261,420 +337,222 @@ public boolean isSigned(String jwt) { return false; } - /* ======================================================================== - - JWE PARSING LOGIC TEMPORARILY DISABLED - Until we find out a cleaner design (delegation, separation of concerns, etc) - - private static void malformed(String type, String part) { - String msg = "Required " + type + " " + part + " is missing."; - throw new MalformedJwtException(msg); + private static boolean hasContentType(Header header) { + return header != null && Strings.hasText(header.getContentType()); } @Override - public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException { + public Jwt parse(String compact) throws ExpiredJwtException, MalformedJwtException, SignatureException { // TODO, this logic is only need for a now deprecated code path // remove this block in v1.0 (the equivalent is already in DefaultJwtParserBuilder) if (this.deserializer == null) { // try to find one based on the services available // TODO: This util class will throw a UnavailableImplementationException here to retain behavior of previous version, remove in v1.0 - this.deserializeJsonWith(LegacyServices.loadFirst(Deserializer.class)); + //noinspection deprecation + this.deserializer = LegacyServices.loadFirst(Deserializer.class); } - Assert.hasText(jwt, "JWT String argument cannot be null or empty."); - - //parse the constituent parts of the compact string: - String base64UrlEncodedHeader = null; //JWS or JWE - - String base64UrlEncodedCek = null; //JWE only - String base64UrlEncodedPayload = null; //JWS or JWE - - String base64UrlEncodedIv = null; //JWE only - - String base64UrlEncodedTag = null; //JWE only - String base64UrlEncodedDigest = null; //JWS only + Assert.hasText(compact, "JWT String cannot be null or empty."); - int delimiterCount = 0; - - StringBuilder sb = new StringBuilder(128); - - for (char c : jwt.toCharArray()) { - - if (Character.isWhitespace(c)) { - String msg = "Compact JWT strings cannot contain whitespace."; - throw new MalformedJwtException(msg); - } - - if (c == SEPARATOR_CHAR) { - - CharSequence tokenSeq = Strings.clean(sb); - String token = tokenSeq != null ? tokenSeq.toString() : null; - - switch (delimiterCount) { - case 0: - base64UrlEncodedHeader = token; - break; - case 1: - //we'll figure out if we have a compact JWE or JWS after finishing inspecting the char array: - base64UrlEncodedCek = token; - base64UrlEncodedPayload = token; - break; - case 2: - base64UrlEncodedIv = token; - break; - case 3: - base64UrlEncodedPayload = token; //ciphertext - break; - } - - delimiterCount++; - sb.setLength(0); - } else { - sb.append(c); - } - } - - boolean jwe; - if (delimiterCount == 2) { // JWT or JWS - //noinspection ConstantConditions - jwe = false; - } else if (delimiterCount == 4) { // JWE - jwe = true; - } else { - String msg = "Invalid compact JWT string. JWSs must have exactly 2 period characters, " + - "JWEs must have exactly 4. Found: " + delimiterCount + "."; + final TokenizedJwt tokenized = jwtTokenizer.tokenize(compact); + final String base64UrlHeader = tokenized.getProtected(); + if (!Strings.hasText(base64UrlHeader)) { + String msg = "Compact JWT strings MUST always have a Base64Url protected header per https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4)."; throw new MalformedJwtException(msg); } - String type = jwe ? "JWE" : "JWS"; - - if (sb.length() > 0) { - String value = sb.toString(); - if (jwe) { - base64UrlEncodedTag = value; - } else { - base64UrlEncodedDigest = value; - } - } - - if (base64UrlEncodedHeader == null) { - malformed(type, "Protected Header"); - } - - if (base64UrlEncodedPayload == null) { - malformed(type, jwe ? "Ciphertext" : "Payload"); - } - - if (jwe) { - if (base64UrlEncodedIv == null) { - malformed(type, "Initialization Vector"); - } - if (base64UrlEncodedTag == null) { - malformed(type, "Authentication Tag"); - } - } - // =============== Header ================= - Header header; - - CompressionCodec compressionCodec; - - byte[] bytes = base64UrlDecode(base64UrlEncodedHeader); - String origValue = new String(bytes, Strings.UTF_8); - Map m = (Map) readValue(origValue); - - if (base64UrlEncodedDigest != null) { - header = new DefaultJwsHeader(m); - } else if (jwe) { - header = new DefaultJweHeader(m); + final byte[] headerBytes = base64UrlDecode(base64UrlHeader, "protected header"); + String origValue = new String(headerBytes, Strings.UTF_8); + Map m = readValue(origValue, "protected header"); + Header header = tokenized.createHeader(m); + + // https://tools.ietf.org/html/rfc7515#section-10.7 , second-to-last bullet point, note the use of 'always': + // + // * Require that the "alg" Header Parameter be carried in the JWS + // Protected Header. (This is always the case when using the JWS + // Compact Serialization and is the approach taken by CMS [RFC6211].) + // + final String alg = Strings.clean(header.getAlgorithm()); + if (!Strings.hasText(alg)) { + String msg = tokenized instanceof TokenizedJwe ? MISSING_JWE_ALG_MSG : MISSING_JWS_ALG_MSG; + throw new MalformedJwtException(msg); } else { - header = new DefaultHeader(m); + if (!SignatureAlgorithms.NONE.getId().equals(alg) && !Strings.hasText(tokenized.getDigest())) { + String type = tokenized instanceof TokenizedJwe ? "JWE" : "JWS"; + String algType = tokenized instanceof TokenizedJwe ? "key management" : "signature"; + String digestType = tokenized instanceof TokenizedJwe ? "an AAD authentication tag" : "a signature"; + String msg = "The " + type + " header references " + algType + " algorithm '" + alg + "' but the " + + "compact " + type + " string does not have " + digestType + " token."; + throw new MalformedJwtException(msg); + } } - compressionCodec = compressionCodecResolver.resolveCompressionCodec(header); - // =============== Body ================= - bytes = base64UrlDecoder.decode(base64UrlEncodedPayload); + CompressionCodec compressionCodec = compressionCodecLocator.apply(header); + byte[] bytes = base64UrlDecode(tokenized.getBody(), "payload"); // Only JWS body can be empty per https://github.com/jwtk/jjwt/pull/540 + if (tokenized instanceof TokenizedJwe && Arrays.length(bytes) == 0) { + String msg = "Compact JWE strings MUST always contain a payload (ciphertext)."; + throw new MalformedJwtException(msg); + } if (compressionCodec != null) { bytes = compressionCodec.decompress(bytes); } - String payload = new String(bytes, Strings.UTF_8); - - Claims claims = null; - - if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it: - Map claimsMap = (Map) readValue(payload); - claims = new DefaultClaims(claimsMap); - } - - // =============== Signature ================= - if (base64UrlEncodedDigest != null) { //it is signed - validate the signature - JwsHeader jwsHeader = (JwsHeader) header; + byte[] iv = null; + byte[] tag = null; + if (tokenized instanceof TokenizedJwe) { //need to decrypt the ciphertext - SignatureAlgorithm algorithm = null; + TokenizedJwe tokenizedJwe = (TokenizedJwe) tokenized; + JweHeader jweHeader = (JweHeader) header; - String alg = jwsHeader.getAlgorithm(); - if (Strings.hasText(alg)) { - algorithm = SignatureAlgorithm.forName(alg); + byte[] cekBytes = Bytes.EMPTY; //ignored unless using an encrypted key algorithm + String base64Url = tokenizedJwe.getEncryptedKey(); + if (Strings.hasText(base64Url)) { + cekBytes = base64UrlDecode(base64Url, "JWE encrypted key"); + if (Arrays.length(cekBytes) == 0) { + String msg = "Compact JWE string represents an encrypted key, but the key is empty."; + throw new MalformedJwtException(msg); + } } - if (algorithm == null || algorithm == SignatureAlgorithm.NONE) { - //it is plaintext, but it has a signature. This is invalid: - String msg = "JWT string has a digest/signature, but the header does not reference a valid signature " + - "algorithm."; - throw new MalformedJwtException(msg); + base64Url = tokenizedJwe.getIv(); + if (Strings.hasText(base64Url)) { + iv = base64UrlDecode(base64Url, "JWE Initialization Vector"); } - - if (key != null && keyBytes != null) { - throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either."); - } else if ((key != null || keyBytes != null) && signingKeyResolver != null) { - String object = key != null ? "a key object" : "key bytes"; - throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either."); + if (Arrays.length(iv) == 0) { + String msg = "Compact JWE strings must always contain an Initialization Vector."; + throw new MalformedJwtException(msg); } - //digitally signed, let's assert the signature: - Key key = this.key; + // The AAD (Additional Authenticated Data) scheme for compact JWEs is to use the ASCII bytes of the + // raw base64url text as the AAD, and NOT the base64url-decoded bytes per + // https://datatracker.ietf.org/doc/html/rfc7516#section-5.1, Step 14. + final byte[] aad = base64UrlHeader.getBytes(StandardCharsets.US_ASCII); - if (key == null) { //fall back to keyBytes - - byte[] keyBytes = this.keyBytes; - - if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver - if (claims != null) { - key = signingKeyResolver.resolveSigningKey(jwsHeader, claims); - } else { - key = signingKeyResolver.resolveSigningKey(jwsHeader, payload); - } - } - - if (!Objects.isEmpty(keyBytes)) { - - Assert.isTrue(algorithm.isHmac(), - "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); - - key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); - } + base64Url = tokenizedJwe.getDigest(); + if (Strings.hasText(base64Url)) { + tag = base64UrlDecode(base64Url, "JWE AAD Authentication Tag"); } - - Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed."); - - //re-create the jwt part without the signature. This is what needs to be signed for verification: - String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload; - - JwtSignatureValidator validator; - try { - algorithm.assertValidVerificationKey(key); //since 0.10.0: https://github.com/jwtk/jjwt/issues/334 - validator = createSignatureValidator(algorithm, key); - } catch (WeakKeyException e) { - throw e; - } catch (InvalidKeyException | IllegalArgumentException e) { - String algName = algorithm.getValue(); - String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " + - "algorithm, but the specified signing key of type " + key.getClass().getName() + - " may not be used to validate " + algName + " signatures. Because the specified " + - "signing key reflects a specific and expected algorithm, and the JWT does not reflect " + - "this algorithm, it is likely that the JWT was not expected and therefore should not be " + - "trusted. Another possibility is that the parser was configured with the incorrect " + - "signing key, but this cannot be assumed for security reasons."; - throw new UnsupportedJwtException(msg, e); + if (Arrays.length(tag) == 0) { + String msg = "Compact JWE strings must always contain an AAD Authentication Tag."; + throw new MalformedJwtException(msg); } - if (!validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) { - String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + - "asserted and should not be trusted."; - throw new SignatureException(msg); + String enc = jweHeader.getEncryptionAlgorithm(); + if (!Strings.hasText(enc)) { + throw new MalformedJwtException(MISSING_ENC_MSG); } - } - - final boolean allowSkew = this.allowedClockSkewMillis > 0; - - //since 0.3: - if (claims != null) { - - final Date now = this.clock.now(); - long nowTime = now.getTime(); - - //https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.4 - //token MUST NOT be accepted on or after any specified exp time: - Date exp = claims.getExpiration(); - if (exp != null) { - - long maxTime = nowTime - this.allowedClockSkewMillis; - Date max = allowSkew ? new Date(maxTime) : now; - if (max.after(exp)) { - String expVal = DateFormats.formatIso8601(exp, false); - String nowVal = DateFormats.formatIso8601(now, false); - - long differenceMillis = maxTime - exp.getTime(); - - String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " + - differenceMillis + " milliseconds. Allowed clock skew: " + - this.allowedClockSkewMillis + " milliseconds."; - throw new ExpiredJwtException(header, claims, msg); - } + final AeadAlgorithm encAlg = this.encryptionAlgorithmLocator.apply(jweHeader); + if (encAlg == null) { + String msg = "Unrecognized JWE encryption algorithm '" + enc + "'."; + throw new UnsupportedJwtException(msg); } - //https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.5 - //token MUST NOT be accepted before any specified nbf time: - Date nbf = claims.getNotBefore(); - if (nbf != null) { - - long minTime = nowTime + this.allowedClockSkewMillis; - Date min = allowSkew ? new Date(minTime) : now; - if (min.before(nbf)) { - String nbfVal = DateFormats.formatIso8601(nbf, false); - String nowVal = DateFormats.formatIso8601(now, false); - - long differenceMillis = nbf.getTime() - minTime; - - String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal + - ", a difference of " + - differenceMillis + " milliseconds. Allowed clock skew: " + - this.allowedClockSkewMillis + " milliseconds."; - throw new PrematureJwtException(header, claims, msg); - } + @SuppressWarnings("rawtypes") final KeyAlgorithm keyAlg = this.keyAlgorithmLocator.apply(jweHeader); + if (keyAlg == null) { + String msg = "Unrecognized JWE key algorithm '" + alg + "'."; + throw new UnsupportedJwtException(msg); } - validateExpectedClaims(header, claims); - } + final Key key = ((Function) this.keyLocator).apply(jweHeader); + if (key == null) { + String msg = "Cannot decrypt JWE payload: unable to locate key for JWE with header: " + jweHeader; + throw new UnsupportedJwtException(msg); + } - Object body = claims != null ? claims : payload; + DecryptionKeyRequest request = + new DefaultDecryptionKeyRequest<>(this.provider, null, key, jweHeader, encAlg, cekBytes); + final SecretKey cek = keyAlg.getDecryptionKey(request); + if (cek == null) { + String msg = "The '" + keyAlg.getId() + "' JWE key algorithm did not return a decryption key. " + + "Unable to perform '" + encAlg.getId() + "' decryption."; + throw new IllegalStateException(msg); + } - if (base64UrlEncodedDigest != null) { - return new DefaultJws<>((JwsHeader) header, body, base64UrlEncodedDigest); - } else { - return new DefaultJwt<>(header, body); + DecryptAeadRequest decryptRequest = + new DefaultAeadResult(this.provider, null, bytes, cek, aad, tag, iv); + PayloadSupplier result = encAlg.decrypt(decryptRequest); + bytes = result.getPayload(); } - } - - ======================================================================== */ - - @Override - public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException { - // TODO, this logic is only need for a now deprecated code path - // remove this block in v1.0 (the equivalent is already in DefaultJwtParserBuilder) - if (this.deserializer == null) { - // try to find one based on the services available - // TODO: This util class will throw a UnavailableImplementationException here to retain behavior of previous version, remove in v1.0 - this.deserializer = LegacyServices.loadFirst(Deserializer.class); - } - - Assert.hasText(jwt, "JWT String argument cannot be null or empty."); + String payload = new String(bytes, Strings.UTF_8); - if ("..".equals(jwt)) { - String msg = "JWT string '..' is missing a header."; - throw new MalformedJwtException(msg); + Claims claims = null; + if (!payload.isEmpty() && !hasContentType(header) && payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it: + Map claimsMap = readValue(payload, "claims"); + claims = new DefaultClaims(claimsMap); } - TokenizedJwt tokenized = jwtTokenizer.tokenize(jwt); - - // =============== Header ================= - Header header = null; - - CompressionCodec compressionCodec = null; - - if (tokenized.getProtected() != null) { - - byte[] bytes = base64UrlDecode(tokenized.getProtected()); - String origValue = new String(bytes, Strings.UTF_8); - Map m = (Map) readValue(origValue); - - if (tokenized.getDigest() != null) { - header = tokenized instanceof TokenizedJwe ? new DefaultJweHeader(m) : new DefaultJwsHeader(m); + Jwt jwt; + Object body = claims != null ? claims : payload; + if (header instanceof JweHeader) { + jwt = new DefaultJwe<>((JweHeader) header, body, iv, tag); + } else { // JWS + if (!Strings.hasText(tokenized.getDigest()) && SignatureAlgorithms.NONE.getId().equalsIgnoreCase(alg)) { + //noinspection rawtypes + jwt = new DefaultJwt(header, body); } else { - header = new DefaultHeader(m); - } - - compressionCodec = compressionCodecResolver.resolveCompressionCodec(header); - } - - // =============== Body ================= - String payload = ""; // https://github.com/jwtk/jjwt/pull/540 - if (tokenized.getBody() != null) { - byte[] bytes = base64UrlDecode(tokenized.getBody()); - if (compressionCodec != null) { - bytes = compressionCodec.decompress(bytes); + jwt = new DefaultJws<>((JwsHeader) header, body, tokenized.getDigest()); } - payload = new String(bytes, Strings.UTF_8); - } - - Claims claims = null; - - if (!payload.isEmpty() && payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it: - Map claimsMap = (Map) readValue(payload); - claims = new DefaultClaims(claimsMap); } // =============== Signature ================= - if (tokenized.getDigest() != null) { //it is signed - validate the signature + if (jwt instanceof Jws) { // it's a JWS, validate the signature - JwsHeader jwsHeader = (JwsHeader) header; + Jws jws = (Jws) jwt; - SignatureAlgorithm algorithm = null; + final JwsHeader jwsHeader = jws.getHeader(); - if (header != null) { - String alg = jwsHeader.getAlgorithm(); - if (Strings.hasText(alg)) { - algorithm = SignatureAlgorithms.forName(alg); - } + SignatureAlgorithm algorithm; + try { + algorithm = (SignatureAlgorithm) signatureAlgorithmLocator.apply(jwsHeader); + } catch (UnsupportedJwtException e) { + //For backwards compatibility. TODO: remove this try/catch block for 1.0 and let UnsupportedJwtException propagate + String msg = "Unsupported signature algorithm '" + alg + "'"; + throw new SignatureException(msg, e); } + if (algorithm == null) { + String msg = "Unrecognized JWS signature algorithm '" + alg + "'."; + throw new UnsupportedJwtException(msg); + } + + String digest = tokenized.getDigest(); - if (algorithm == null || algorithm == SignatureAlgorithms.NONE) { - //it is plaintext, but it has a signature. This is invalid: - String msg = "JWT string has a digest/signature, but the header does not reference a valid signature " + - "algorithm."; + if (SignatureAlgorithms.NONE.equals(algorithm) && Strings.hasText(digest)) { + //'none' algorithm, but it has a signature. This is invalid: + String msg = "The JWS header references signature algorithm '" + alg + "' yet the compact JWS string has a digest/signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6."; + throw new MalformedJwtException(msg); + } else if (!Strings.hasText(digest)) { + String msg = "The JWS header references signature algorithm '" + alg + "' but the compact JWS string does not have a signature token."; throw new MalformedJwtException(msg); } - if (key != null && keyBytes != null) { - throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either."); - } else if ((key != null || keyBytes != null) && signingKeyResolver != null) { - String object = key != null ? "a key object" : "key bytes"; - throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either."); - } + assert this.signingKeyResolver != null : "SigningKeyResolver cannot be null (invariant)."; //digitally signed, let's assert the signature: - Key key = this.key; - - if (key == null) { //fall back to keyBytes - - byte[] keyBytes = this.keyBytes; - - if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver - if (claims != null) { - key = signingKeyResolver.resolveSigningKey(jwsHeader, claims); - } else { - key = signingKeyResolver.resolveSigningKey(jwsHeader, payload); - } - } - - if (!Objects.isEmpty(keyBytes)) { - Assert.isTrue(algorithm instanceof SymmetricKeySignatureAlgorithm, - "Key bytes can only be specified for symmetric key signatures. Please specify a PublicKey or PrivateKey instance."); - key = new SecretKeySpec(keyBytes, ((SymmetricKeySignatureAlgorithm) algorithm).generateKey().getAlgorithm()); - } + Key key; + if (claims != null) { + key = signingKeyResolver.resolveSigningKey(jwsHeader, claims); + } else { + key = signingKeyResolver.resolveSigningKey(jwsHeader, payload); } - - Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed."); - - //re-create the jwt part without the signature. This is what needs to be signed for verification: - String jwtWithoutSignature = tokenized.getProtected() + SEPARATOR_CHAR; - if (tokenized.getBody() != null) { - jwtWithoutSignature += tokenized.getBody(); + if (key == null) { + String msg = "Cannot verify JWS signature: unable to locate signature verification key for JWS with header: " + jwsHeader; + throw new UnsupportedJwtException(msg); } + //re-create the jwt part without the signature. This is what is needed for signature verification: + String jwtWithoutSignature = tokenized.getProtected() + SEPARATOR_CHAR + tokenized.getBody(); + byte[] data = jwtWithoutSignature.getBytes(StandardCharsets.US_ASCII); - byte[] signature = base64UrlDecode(tokenized.getDigest()); + byte[] signature = base64UrlDecode(tokenized.getDigest(), "JWS signature"); try { - VerifySignatureRequest request = - new DefaultVerifySignatureRequest(data, key, this.provider, null, signature); + VerifySignatureRequest request = + new DefaultVerifySignatureRequest<>(this.provider, null, data, key, signature); - //SignatureValidator validator = DefaultSignatureValidatorFactory.INSTANCE.createSignatureValidator(io.jsonwebtoken.SignatureAlgorithm.forName(algorithm.getName()), key); - // if (!validator.isValid(data, signature)) { if (!algorithm.verify(request)) { String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + "asserted and should not be trusted."; @@ -683,14 +561,14 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, } catch (WeakKeyException e) { throw e; } catch (InvalidKeyException | IllegalArgumentException e) { - String algName = algorithm.getName(); - String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " + - "algorithm, but the specified signing key of type " + key.getClass().getName() + - " may not be used to validate " + algName + " signatures. Because the specified " + - "signing key reflects a specific and expected algorithm, and the JWT does not reflect " + + String algId = algorithm.getId(); + String msg = "The parsed JWT indicates it was signed with the " + algId + " signature " + + "algorithm, but the specified verification key of type " + key.getClass().getName() + + " may not be used to validate " + algId + " signatures. Because the verification " + + "key reflects a specific and expected algorithm, and the JWT does not reflect " + "this algorithm, it is likely that the JWT was not expected and therefore should not be " + - "trusted. Another possibility is that the parser was configured with the incorrect " + - "signing key, but this cannot be assumed for security reasons."; + "trusted. Another possibility is that the parser was supplied with the incorrect " + + "verification key, but this cannot be assumed for security reasons."; throw new UnsupportedJwtException(msg, e); } } @@ -747,13 +625,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, validateExpectedClaims(header, claims); } - Object body = claims != null ? claims : payload; - - if (tokenized.getDigest() != null) { - return new DefaultJws<>((JwsHeader) header, body, tokenized.getDigest()); - } else { - return new DefaultJwt<>(header, body); - } + return jwt; } /** @@ -766,7 +638,7 @@ private static Object normalize(Object o) { return o; } - private void validateExpectedClaims(Header header, Claims claims) { + private void validateExpectedClaims(Header header, Claims claims) { for (String expectedClaimName : expectedClaims.keySet()) { @@ -814,62 +686,62 @@ public T parse(String compact, JwtHandler handler) Assert.notNull(handler, "JwtHandler argument cannot be null."); Assert.hasText(compact, "JWT String argument cannot be null or empty."); - Jwt jwt = parse(compact); + Jwt jwt = parse(compact); if (jwt instanceof Jws) { - Jws jws = (Jws) jwt; + Jws jws = (Jws) jwt; Object body = jws.getBody(); if (body instanceof Claims) { return handler.onClaimsJws((Jws) jws); } else { return handler.onPlaintextJws((Jws) jws); } + } else if (jwt instanceof Jwe) { + Jwe jwe = (Jwe) jwt; + Object body = jwe.getBody(); + if (body instanceof Claims) { + return handler.onClaimsJwe((Jwe) jwe); + } else { + return handler.onPlaintextJwe((Jwe) jwe); + } } else { Object body = jwt.getBody(); if (body instanceof Claims) { - return handler.onClaimsJwt((Jwt) jwt); + return handler.onClaimsJwt((Jwt) jwt); } else { - return handler.onPlaintextJwt((Jwt) jwt); + return handler.onPlaintextJwt((Jwt) jwt); } } } @Override - public Jwt parsePlaintextJwt(String plaintextJwt) { - return parse(plaintextJwt, new JwtHandlerAdapter>() { + public Jwt parsePlaintextJwt(String plaintextJwt) { + return parse(plaintextJwt, new JwtHandlerAdapter>() { @Override - public Jwt onPlaintextJwt(Jwt jwt) { + public Jwt onPlaintextJwt(Jwt jwt) { return jwt; } }); } @Override - public Jwt parseClaimsJwt(String claimsJwt) { - try { - return parse(claimsJwt, new JwtHandlerAdapter>() { - @Override - public Jwt onClaimsJwt(Jwt jwt) { - return jwt; - } - }); - } catch (IllegalArgumentException iae) { - throw new UnsupportedJwtException("Signed JWSs are not supported.", iae); - } + public Jwt parseClaimsJwt(String claimsJwt) { + return parse(claimsJwt, new JwtHandlerAdapter>() { + @Override + public Jwt onClaimsJwt(Jwt jwt) { + return jwt; + } + }); } @Override public Jws parsePlaintextJws(String plaintextJws) { - try { - return parse(plaintextJws, new JwtHandlerAdapter>() { - @Override - public Jws onPlaintextJws(Jws jws) { - return jws; - } - }); - } catch (IllegalArgumentException iae) { - throw new UnsupportedJwtException("Signed JWSs are not supported.", iae); - } + return parse(plaintextJws, new JwtHandlerAdapter>() { + @Override + public Jws onPlaintextJws(Jws jws) { + return jws; + } + }); } @Override @@ -882,18 +754,41 @@ public Jws onClaimsJws(Jws jws) { }); } - protected byte[] base64UrlDecode(String base64UrlEncoded) { + @Override + public Jwe parsePlaintextJwe(String plaintextJwe) throws JwtException { + return parse(plaintextJwe, new JwtHandlerAdapter>() { + @Override + public Jwe onPlaintextJwe(Jwe jwe) { + return jwe; + } + }); + } + + @Override + public Jwe parseClaimsJwe(String claimsJwe) throws JwtException { + return parse(claimsJwe, new JwtHandlerAdapter>() { + @Override + public Jwe onClaimsJwe(Jwe jwe) { + return jwe; + } + }); + } + + protected byte[] base64UrlDecode(String base64UrlEncoded, String name) { try { return base64UrlDecoder.decode(base64UrlEncoded); } catch (DecodingException e) { - String msg = "Invalid Base64Url string: " + base64UrlEncoded; + String msg = "Invalid Base64Url " + name + ": " + base64UrlEncoded; throw new MalformedJwtException(msg, e); } } - @SuppressWarnings("unchecked") - protected Map readValue(String val) { - byte[] bytes = val.getBytes(Strings.UTF_8); - return deserializer.deserialize(bytes); + protected Map readValue(String val, final String name) { + try { + byte[] bytes = val.getBytes(Strings.UTF_8); + return deserializer.deserialize(bytes); + } catch (DeserializationException e) { + throw new MalformedJwtException("Unable to read " + name + " JSON: " + val, e); + } } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index d86f9ec12..cc64d3bbe 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -18,19 +18,33 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.Clock; import io.jsonwebtoken.CompressionCodecResolver; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.JwtParserBuilder; +import io.jsonwebtoken.Locator; import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; +import io.jsonwebtoken.impl.lang.ConstantFunction; +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.impl.lang.LocatorFunction; import io.jsonwebtoken.impl.lang.Services; +import io.jsonwebtoken.impl.security.ConstantKeyLocator; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureAlgorithm; import java.security.Key; import java.security.Provider; +import java.util.Collection; import java.util.Date; +import java.util.LinkedHashSet; import java.util.Map; /** @@ -42,7 +56,7 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { /** * To prevent overflow per Issue 583. - * + *

    * Package-protected on purpose to allow use in backwards-compatible {@link DefaultJwtParser} implementation. * TODO: enable private modifier on these two variables when deleting DefaultJwtParser */ @@ -52,24 +66,33 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { private Provider provider; - private byte[] keyBytes; + @SuppressWarnings({"rawtypes"}) + private Function keyLocator = ConstantFunction.forNull(); - private Key key; + @SuppressWarnings("deprecation") //TODO: remove for 1.0 + private SigningKeyResolver signingKeyResolver = new ConstantKeyLocator<>(null , null); - private SigningKeyResolver signingKeyResolver; + private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); - private CompressionCodecResolver compressionCodecResolver; + private final Collection extraEncryptionAlgorithms = new LinkedHashSet<>(); + + private final Collection> extraKeyAlgorithms = new LinkedHashSet<>(); + + private final Collection> extraSignatureAlgorithms = new LinkedHashSet<>(); private Decoder base64UrlDecoder = Decoders.BASE64URL; private Deserializer> deserializer; - private Claims expectedClaims = new DefaultClaims(); + private final Claims expectedClaims = new DefaultClaims(); private Clock clock = DefaultClock.INSTANCE; private long allowedClockSkewMillis = 0; + private Key signatureVerificationKey; + private Key decryptionKey; + @Override public JwtParserBuilder setProvider(Provider provider) { this.provider = provider; @@ -157,24 +180,50 @@ public JwtParserBuilder setAllowedClockSkewSeconds(long seconds) throws IllegalA @Override public JwtParserBuilder setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); - this.keyBytes = key; - return this; + return setSigningKey(Keys.hmacShaKeyFor(key)); } @Override public JwtParserBuilder setSigningKey(String base64EncodedSecretKey) { Assert.hasText(base64EncodedSecretKey, "signing key cannot be null or empty."); - this.keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey); + byte[] bytes = Decoders.BASE64.decode(base64EncodedSecretKey); + return setSigningKey(bytes); + } + + @Override + public JwtParserBuilder setSigningKey(final Key key) { + this.signatureVerificationKey = Assert.notNull(key, "signing key cannot be null."); + return setSigningKeyResolver(new ConstantKeyLocator<>(key, null)); + } + + @Override + public JwtParserBuilder decryptWith(final Key key) { + this.decryptionKey = Assert.notNull(key, "decryption key cannot be null."); return this; } @Override - public JwtParserBuilder setSigningKey(Key key) { - Assert.notNull(key, "signing key cannot be null."); - this.key = key; + public JwtParserBuilder addEncryptionAlgorithms(Collection encAlgs) { + Assert.notEmpty(encAlgs, "Additional AeadAlgorithm collection cannot be null or empty."); + this.extraEncryptionAlgorithms.addAll(encAlgs); return this; } + @Override + public JwtParserBuilder addSignatureAlgorithms(Collection> sigAlgs) { + Assert.notEmpty(sigAlgs, "Additional SignatureAlgorithm collection cannot be null or empty."); + this.extraSignatureAlgorithms.addAll(sigAlgs); + return this; + } + + @Override + public JwtParserBuilder addKeyAlgorithms(Collection> keyAlgs) { + Assert.notEmpty(keyAlgs, "Additional KeyAlgorithm collection cannot be null or empty."); + this.extraKeyAlgorithms.addAll(keyAlgs); + return this; + } + + @SuppressWarnings("deprecation") //TODO: remove for 1.0 @Override public JwtParserBuilder setSigningKeyResolver(SigningKeyResolver signingKeyResolver) { Assert.notNull(signingKeyResolver, "SigningKeyResolver cannot be null."); @@ -182,6 +231,18 @@ public JwtParserBuilder setSigningKeyResolver(SigningKeyResolver signingKeyResol return this; } + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Function coerce(Function f) { + return (Function) f; + } + + @Override + public JwtParserBuilder setKeyLocator(Locator, Key> keyLocator) { + Assert.notNull(keyLocator, "Key locator cannot be null."); + this.keyLocator = coerce(new LocatorFunction<>(keyLocator)); + return this; + } + @Override public JwtParserBuilder setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver) { Assert.notNull(compressionCodecResolver, "compressionCodecResolver cannot be null."); @@ -189,6 +250,7 @@ public JwtParserBuilder setCompressionCodecResolver(CompressionCodecResolver com return this; } + @SuppressWarnings("rawtypes") @Override public JwtParser build() { @@ -196,24 +258,48 @@ public JwtParser build() { // that is NOT exposed as a service and no other implementations are available for lookup. if (this.deserializer == null) { // try to find one based on the services available: + //noinspection unchecked this.deserializer = Services.loadFirst(Deserializer.class); } - // if the compressionCodecResolver is not set default it. - if (this.compressionCodecResolver == null) { - this.compressionCodecResolver = new DefaultCompressionCodecResolver(); + final Function existing1 = this.keyLocator; + if (this.signatureVerificationKey != null) { + this.keyLocator = new Function() { + @Override + public Key apply(Header header) { + return header instanceof JwsHeader ? signatureVerificationKey : existing1.apply(header); + } + }; + } + final Function existing2 = this.keyLocator; + if (this.decryptionKey != null) { + this.keyLocator = new Function() { + @Override + public Key apply(Header header) { + return header instanceof JweHeader ? decryptionKey : existing2.apply(header); + } + }; } - return new ImmutableJwtParser( - new DefaultJwtParser(provider, - signingKeyResolver, - key, - keyBytes, - clock, - allowedClockSkewMillis, - expectedClaims, - base64UrlDecoder, - new JwtDeserializer<>(deserializer), - compressionCodecResolver)); + // Invariants. If these are ever violated, it's an error in this class implementation + // (we default to non-null instances, and the setters should never allow null): + assert this.keyLocator != null : "Key locator should never be null."; + assert this.signingKeyResolver != null : "SigningKeyResolver should never be null."; + assert this.compressionCodecResolver != null : "CompressionCodecResolver should never be null."; + + return new ImmutableJwtParser(new DefaultJwtParser( + provider, + signingKeyResolver, + keyLocator, + clock, + allowedClockSkewMillis, + expectedClaims, + base64UrlDecoder, + deserializer, + compressionCodecResolver, + extraSignatureAlgorithms, + extraKeyAlgorithms, + extraEncryptionAlgorithms + )); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwe.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwe.java index 68724dc2a..c592c2130 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwe.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwe.java @@ -1,5 +1,9 @@ package io.jsonwebtoken.impl; +import io.jsonwebtoken.Header; + +import java.util.Map; + class DefaultTokenizedJwe extends DefaultTokenizedJwt implements TokenizedJwe { private final String encryptedKey; @@ -20,4 +24,9 @@ public String getEncryptedKey() { public String getIv() { return this.iv; } + + @Override + public Header createHeader(Map m) { + return new DefaultJweHeader(m); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java index cf7d1ae52..2489b09e6 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java @@ -1,5 +1,11 @@ package io.jsonwebtoken.impl; + +import io.jsonwebtoken.Header; +import io.jsonwebtoken.lang.Strings; + +import java.util.Map; + class DefaultTokenizedJwt implements TokenizedJwt { private final String protectedHeader; @@ -26,4 +32,12 @@ public String getBody() { public String getDigest() { return this.digest; } + + @Override + public Header createHeader(Map m) { + if (Strings.hasText(getDigest())) { + return new DefaultJwsHeader(m); + } + return new DefaultHeader<>(m); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DispatchingParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DispatchingParser.java index 99b465b63..3ef6dc6a1 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DispatchingParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DispatchingParser.java @@ -1,7 +1,5 @@ package io.jsonwebtoken.impl; -import io.jsonwebtoken.lang.Assert; - /** * @since JJWT_RELEASE_VERSION */ @@ -103,7 +101,7 @@ public void parse(String compactJwe) { if (secretKey == null) { String msg = "SecretKeyResolver did not return a secret key for headers " + headers + ". This is required for message decryption."; - throw new CryptoException(msg); + throw new SecurityException(msg); } byte[] aad = base64UrlEncodedHeader.getBytes(StandardCharsets.US_ASCII); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/IdLocator.java b/impl/src/main/java/io/jsonwebtoken/impl/IdLocator.java new file mode 100644 index 000000000..dbaf57d18 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/IdLocator.java @@ -0,0 +1,62 @@ +package io.jsonwebtoken.impl; + +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; + +public class IdLocator, R> implements Function { + + private final String headerName; + private final String requiredMsg; + private final boolean headerValueRequired; + private final Function primary; + private final Function backup; + + public IdLocator(String headerName, String requiredMsg, Function primary, Function backup) { + this.headerName = Assert.hasText(headerName, "Header name cannot be null or empty."); + this.requiredMsg = requiredMsg; + this.headerValueRequired = Strings.hasText(requiredMsg); + this.primary = Assert.notNull(primary, "Primary lookup function cannot be null."); + this.backup = Assert.notNull(backup, "Backup locator cannot be null."); + } + + private static String type(Header header) { + if (header instanceof JweHeader) { + return "JWE"; + } else if (header instanceof JwsHeader) { + return "JWS"; + } else { + return "JWT"; + } + } + + @Override + public R apply(H header) { + + Assert.notNull(header, "Header argument cannot be null."); + + Object val = header.get(this.headerName); + String id = val != null ? String.valueOf(val) : null; + + if (this.headerValueRequired && !Strings.hasText(id)) { + throw new MalformedJwtException(requiredMsg); + } + + R instance = primary.apply(id); + if (instance == null) { + instance = backup.apply(header); + } + + if (this.headerValueRequired && instance == null) { + String msg = "Unrecognized " + type(header) + " '" + this.headerName + "' header value: " + id; + throw new UnsupportedJwtException(msg); + } + + return instance; + } +} \ No newline at end of file diff --git a/impl/src/main/java/io/jsonwebtoken/impl/IdRegistry.java b/impl/src/main/java/io/jsonwebtoken/impl/IdRegistry.java new file mode 100644 index 000000000..b39072a87 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/IdRegistry.java @@ -0,0 +1,47 @@ +package io.jsonwebtoken.impl; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.impl.lang.Registry; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +public class IdRegistry implements Registry { + + private final Map INSTANCES; + + public IdRegistry(Collection instances) { + Assert.notEmpty(instances, "Collection of Identifiable instances may not be null or empty."); + Map m = new LinkedHashMap<>(instances.size()); + for (T instance : instances) { + String id = Assert.hasText(Strings.clean(instance.getId()), "All Identifiable instances within the collection cannot be null or empty."); + m.put(id, instance); + } + this.INSTANCES = java.util.Collections.unmodifiableMap(m); + } + + @Override + public T apply(String id) { + Assert.hasText(id, "id argument cannot be null or empty."); + //try constant time lookup first. This will satisfy 99% of invocations: + T instance = INSTANCES.get(id); + if (instance != null) { + return instance; + } + //fall back to case-insensitive lookup: + for (T i : INSTANCES.values()) { + if (id.equalsIgnoreCase(i.getId())) { + return i; + } + } + return null; //no match + } + + @Override + public Collection values() { + return this.INSTANCES.values(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/ImmutableJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/ImmutableJwtParser.java index 157864cc8..5dc1460d3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/ImmutableJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/ImmutableJwtParser.java @@ -20,8 +20,10 @@ import io.jsonwebtoken.CompressionCodecResolver; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwe; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtHandler; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.MalformedJwtException; @@ -139,8 +141,8 @@ public JwtParser deserializeJsonWith(Deserializer> deserializer) } @Override - public boolean isSigned(String jwt) { - return this.jwtParser.isSigned(jwt); + public boolean isSigned(String compact) { + return this.jwtParser.isSigned(compact); } @Override @@ -154,12 +156,12 @@ public T parse(String jwt, JwtHandler handler) throws ExpiredJwtException } @Override - public Jwt parsePlaintextJwt(String plaintextJwt) throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { + public > Jwt parsePlaintextJwt(String plaintextJwt) throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { return this.jwtParser.parsePlaintextJwt(plaintextJwt); } @Override - public Jwt parseClaimsJwt(String claimsJwt) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { + public > Jwt parseClaimsJwt(String claimsJwt) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { return this.jwtParser.parseClaimsJwt(claimsJwt); } @@ -172,4 +174,14 @@ public Jws parsePlaintextJws(String plaintextJws) throws UnsupportedJwtE public Jws parseClaimsJws(String claimsJws) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { return this.jwtParser.parseClaimsJws(claimsJws); } + + @Override + public Jwe parsePlaintextJwe(String plaintextJwe) throws JwtException { + return this.jwtParser.parsePlaintextJwe(plaintextJwe); + } + + @Override + public Jwe parseClaimsJwe(String claimsJwe) throws JwtException { + return this.jwtParser.parseClaimsJwe(claimsJwe); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 92c2c6891..3c2ce03af 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -104,11 +104,11 @@ protected static Date toSpecDate(Object v, String name) { return toDate(v, name); } - protected static boolean isReduceableToNull(Object v) { + public static boolean isReduceableToNull(Object v) { return v == null || (v instanceof String && !Strings.hasText((String)v)) || - (v instanceof Collection && Collections.isEmpty((Collection) v)) || - (v instanceof Map && Collections.isEmpty((Map)v)) || + (v instanceof Collection && Collections.isEmpty((Collection) v)) || + (v instanceof Map && Collections.isEmpty((Map)v)) || (v.getClass().isArray() && Array.getLength(v) == 0); } @@ -178,7 +178,6 @@ public Object remove(Object o) { return map.remove(o); } - @SuppressWarnings("NullableProblems") @Override public void putAll(Map m) { if (m == null) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java index 30910a0da..467eda226 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java @@ -2,7 +2,6 @@ import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Strings; public class JwtTokenizer { @@ -16,11 +15,11 @@ public T tokenize(String jwt) { Assert.hasText(jwt, "Argument cannot be null or empty."); - String protectedHeader = null; //Both JWS and JWE - String body = null; //JWS Payload or JWE Ciphertext - String digest = null; //JWS Signature or JWE AAD Tag - String encryptedKey = null; //JWE only - String iv = null; //JWE only + String protectedHeader = ""; //Both JWS and JWE + String body = ""; //JWS Payload or JWE Ciphertext + String encryptedKey = ""; //JWE only + String iv = ""; //JWE only + String digest; //JWS Signature or JWE AAD Tag int delimiterCount = 0; @@ -28,12 +27,14 @@ public T tokenize(String jwt) { for (char c : jwt.toCharArray()) { - Assert.isTrue(!Character.isWhitespace(c), "Compact JWT strings may not contain whitespace."); + if (Character.isWhitespace(c)) { + String msg = "Compact JWT strings may not contain whitespace."; + throw new MalformedJwtException(msg); + } if (c == DELIMITER) { - CharSequence tokenSeq = Strings.clean(sb); - String token = tokenSeq != null ? tokenSeq.toString() : null; + String token = sb.toString(); switch (delimiterCount) { case 0: @@ -44,7 +45,7 @@ public T tokenize(String jwt) { encryptedKey = token; //for JWE break; case 2: - body = null; //clear out value set for JWS + body = ""; //clear out value set for JWS iv = token; break; case 3: @@ -64,9 +65,7 @@ public T tokenize(String jwt) { throw new MalformedJwtException(msg); } - if (sb.length() > 0) { - digest = Strings.clean(sb.toString()); - } + digest = sb.toString(); if (delimiterCount == 2) { return (T) new DefaultTokenizedJwt(protectedHeader, body, digest); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwt.java b/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwt.java index 3db2dbe2e..4b4dc04f3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwt.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwt.java @@ -1,9 +1,14 @@ package io.jsonwebtoken.impl; +import io.jsonwebtoken.Header; + +import java.util.Map; + public interface TokenizedJwt { /** * Protected header. + * * @return protected header. */ String getProtected(); @@ -17,4 +22,6 @@ public interface TokenizedJwt { * Signature for JWS, AAD Tag for JWE. */ String getDigest(); + + Header createHeader(Map m); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java deleted file mode 100644 index 2a4f746a6..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.io.Decoder; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.lang.Assert; - -import java.nio.charset.Charset; -import java.security.Key; - -public class DefaultJwtSignatureValidator implements JwtSignatureValidator { - - private static final Charset US_ASCII = Charset.forName("US-ASCII"); - - private final SignatureValidator signatureValidator; - private final Decoder base64UrlDecoder; - - @Deprecated - public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key) { - this(DefaultSignatureValidatorFactory.INSTANCE, alg, key, Decoders.BASE64URL); - } - - public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key, Decoder base64UrlDecoder) { - this(DefaultSignatureValidatorFactory.INSTANCE, alg, key, base64UrlDecoder); - } - - @Deprecated - public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key) { - this(factory, alg, key, Decoders.BASE64URL); - } - - public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key, Decoder base64UrlDecoder) { - Assert.notNull(factory, "SignerFactory argument cannot be null."); - Assert.notNull(base64UrlDecoder, "Base64Url decoder argument cannot be null."); - this.signatureValidator = factory.createSignatureValidator(alg, key); - this.base64UrlDecoder = base64UrlDecoder; - } - - @Override - public boolean isValid(String jwtWithoutSignature, String base64UrlEncodedSignature) { - - byte[] data = jwtWithoutSignature.getBytes(US_ASCII); - - byte[] signature = base64UrlDecoder.decode(base64UrlEncodedSignature); - - return this.signatureValidator.isValid(data, signature); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java deleted file mode 100644 index 6b8ae49c7..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.io.Encoder; -import io.jsonwebtoken.io.Encoders; -import io.jsonwebtoken.lang.Assert; - -import java.nio.charset.Charset; -import java.security.Key; - -public class DefaultJwtSigner implements JwtSigner { - - private static final Charset US_ASCII = Charset.forName("US-ASCII"); - - private final Signer signer; - private final Encoder base64UrlEncoder; - - @Deprecated - public DefaultJwtSigner(SignatureAlgorithm alg, Key key) { - this(DefaultSignerFactory.INSTANCE, alg, key, Encoders.BASE64URL); - } - - public DefaultJwtSigner(SignatureAlgorithm alg, Key key, Encoder base64UrlEncoder) { - this(DefaultSignerFactory.INSTANCE, alg, key, base64UrlEncoder); - } - - @Deprecated - public DefaultJwtSigner(SignerFactory factory, SignatureAlgorithm alg, Key key) { - this(factory, alg, key, Encoders.BASE64URL); - } - - public DefaultJwtSigner(SignerFactory factory, SignatureAlgorithm alg, Key key, Encoder base64UrlEncoder) { - Assert.notNull(factory, "SignerFactory argument cannot be null."); - Assert.notNull(base64UrlEncoder, "Base64Url Encoder cannot be null."); - this.base64UrlEncoder = base64UrlEncoder; - this.signer = factory.createSigner(alg, key); - } - - @Override - public String sign(String jwtWithoutSignature) { - - byte[] bytesToSign = jwtWithoutSignature.getBytes(US_ASCII); - - byte[] signature = signer.sign(bytesToSign); - - return base64UrlEncoder.encode(signature); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java deleted file mode 100644 index 82916847c..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; - -import java.security.Key; - -public class DefaultSignatureValidatorFactory implements SignatureValidatorFactory { - - public static final SignatureValidatorFactory INSTANCE = new DefaultSignatureValidatorFactory(); - - @Override - public SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) { - Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - Assert.notNull(key, "Signing Key cannot be null."); - - switch (alg) { - case HS256: - case HS384: - case HS512: - return new MacValidator(alg, key); - case RS256: - case RS384: - case RS512: - case PS256: - case PS384: - case PS512: - return new RsaSignatureValidator(alg, key); - case ES256: - case ES384: - case ES512: - return new EllipticCurveSignatureValidator(alg, key); - default: - throw new IllegalArgumentException("The '" + alg.name() + "' algorithm cannot be used for signing."); - } - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java deleted file mode 100644 index 5eee74ce3..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; - -import java.security.Key; - -public class DefaultSignerFactory implements SignerFactory { - - public static final SignerFactory INSTANCE = new DefaultSignerFactory(); - - @Override - public Signer createSigner(SignatureAlgorithm alg, Key key) { - Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - Assert.notNull(key, "Signing Key cannot be null."); - - switch (alg) { - case HS256: - case HS384: - case HS512: - return new MacSigner(alg, key); - case RS256: - case RS384: - case RS512: - case PS256: - case PS384: - case PS512: - return new RsaSigner(alg, key); - case ES256: - case ES384: - case ES512: - return new EllipticCurveSigner(alg, key); - default: - throw new IllegalArgumentException("The '" + alg.name() + "' algorithm cannot be used for signing."); - } - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java deleted file mode 100644 index 0f6f4d46f..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright (C) 2015 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.impl.security.EllipticCurveSignatureAlgorithm; -import io.jsonwebtoken.impl.security.Randoms; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Strings; - -import java.security.Key; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.SecureRandom; -import java.security.spec.ECGenParameterSpec; -import java.util.HashMap; -import java.util.Map; - -/** - * ElliptiCurve crypto provider. - * - * @since 0.5 - */ -public abstract class EllipticCurveProvider extends SignatureProvider { - - private static final Map EC_CURVE_NAMES = createEcCurveNames(); - - private static Map createEcCurveNames() { - Map m = new HashMap(); //alg to ASN1 OID name - m.put(SignatureAlgorithm.ES256, "secp256r1"); - m.put(SignatureAlgorithm.ES384, "secp384r1"); - m.put(SignatureAlgorithm.ES512, "secp521r1"); - return m; - } - - protected EllipticCurveProvider(SignatureAlgorithm alg, Key key) { - super(alg, key); - Assert.isTrue(alg.isEllipticCurve(), "SignatureAlgorithm must be an Elliptic Curve algorithm."); - } - - /** - * Generates a new secure-random key pair assuming strength enough for the {@link - * SignatureAlgorithm#ES512} algorithm. This is a convenience method that immediately delegates to {@link - * #generateKeyPair(SignatureAlgorithm)} using {@link SignatureAlgorithm#ES512} as the method argument. - * - * @return a new secure-randomly-generated key pair assuming strength enough for the {@link - * SignatureAlgorithm#ES512} algorithm. - * @see #generateKeyPair(SignatureAlgorithm) - * @see #generateKeyPair(SignatureAlgorithm, SecureRandom) - * @see #generateKeyPair(String, String, SignatureAlgorithm, SecureRandom) - */ - public static KeyPair generateKeyPair() { - return generateKeyPair(SignatureAlgorithm.ES512); - } - - /** - * Generates a new secure-random key pair of sufficient strength for the specified Elliptic Curve {@link - * SignatureAlgorithm} (must be one of {@code ES256}, {@code ES384} or {@code ES512}) using JJWT's default {@link - * Randoms#secureRandom() SecureRandom instance}. This is a convenience method that immediately - * delegates to {@link #generateKeyPair(SignatureAlgorithm, SecureRandom)}. - * - * @param alg the algorithm indicating strength, must be one of {@code ES256}, {@code ES384} or {@code ES512} - * @return a new secure-randomly generated key pair of sufficient strength for the specified {@link - * SignatureAlgorithm} (must be one of {@code ES256}, {@code ES384} or {@code ES512}) using JJWT's default {@link - * Randoms#secureRandom() SecureRandom instance}. - * @see #generateKeyPair() - * @see #generateKeyPair(SignatureAlgorithm, SecureRandom) - * @see #generateKeyPair(String, String, SignatureAlgorithm, SecureRandom) - */ - public static KeyPair generateKeyPair(SignatureAlgorithm alg) { - return generateKeyPair(alg, Randoms.secureRandom()); - } - - /** - * Generates a new secure-random key pair of sufficient strength for the specified Elliptic Curve {@link - * SignatureAlgorithm} (must be one of {@code ES256}, {@code ES384} or {@code ES512}) using the specified {@link - * SecureRandom} random number generator. This is a convenience method that immediately delegates to {@link - * #generateKeyPair(String, String, SignatureAlgorithm, SecureRandom)} using {@code "EC"} as the {@code - * jcaAlgorithmName}. - * - * @param alg alg the algorithm indicating strength, must be one of {@code ES256}, {@code ES384} or {@code - * ES512} - * @param random the SecureRandom generator to use during key generation. - * @return a new secure-randomly generated key pair of sufficient strength for the specified {@link - * SignatureAlgorithm} (must be one of {@code ES256}, {@code ES384} or {@code ES512}) using the specified {@link - * SecureRandom} random number generator. - * @see #generateKeyPair() - * @see #generateKeyPair(SignatureAlgorithm) - * @see #generateKeyPair(String, String, SignatureAlgorithm, SecureRandom) - */ - public static KeyPair generateKeyPair(SignatureAlgorithm alg, SecureRandom random) { - return generateKeyPair("EC", null, alg, random); - } - - /** - * Generates a new secure-random key pair of sufficient strength for the specified Elliptic Curve {@link - * SignatureAlgorithm} (must be one of {@code ES256}, {@code ES384} or {@code ES512}) using the specified {@link - * SecureRandom} random number generator via the specified JCA provider and algorithm name. - * - * @param jcaAlgorithmName the JCA name of the algorithm to use for key pair generation, for example, {@code - * ECDSA}. - * @param jcaProviderName the JCA provider name of the algorithm implementation (for example {@code "BC"} for - * BouncyCastle) or {@code null} if the default provider should be used. - * @param alg alg the algorithm indicating strength, must be one of {@code ES256}, {@code ES384} or - * {@code ES512} - * @param random the SecureRandom generator to use during key generation. - * @return a new secure-randomly generated key pair of sufficient strength for the specified Elliptic Curve {@link - * SignatureAlgorithm} (must be one of {@code ES256}, {@code ES384} or {@code ES512}) using the specified {@link - * SecureRandom} random number generator via the specified JCA provider and algorithm name. - * @see #generateKeyPair() - * @see #generateKeyPair(SignatureAlgorithm) - * @see #generateKeyPair(SignatureAlgorithm, SecureRandom) - */ - public static KeyPair generateKeyPair(String jcaAlgorithmName, String jcaProviderName, SignatureAlgorithm alg, - SecureRandom random) { - Assert.notNull(alg, "SignatureAlgorithm argument cannot be null."); - Assert.isTrue(alg.isEllipticCurve(), "SignatureAlgorithm argument must represent an Elliptic Curve algorithm."); - try { - KeyPairGenerator g; - - if (Strings.hasText(jcaProviderName)) { - g = KeyPairGenerator.getInstance(jcaAlgorithmName, jcaProviderName); - } else { - g = KeyPairGenerator.getInstance(jcaAlgorithmName); - } - - String paramSpecCurveName = EC_CURVE_NAMES.get(alg); - ECGenParameterSpec spec = new ECGenParameterSpec(paramSpecCurveName); - g.initialize(spec, random); - return g.generateKeyPair(); - } catch (Exception e) { - throw new IllegalStateException("Unable to generate Elliptic Curve KeyPair: " + e.getMessage(), e); - } - } - - /** - * Returns the expected signature byte array length (R + S parts) for - * the specified ECDSA algorithm. - * - * @param alg The ECDSA algorithm. Must be supported and not - * {@code null}. - * @return The expected byte array length for the signature. - * @throws JwtException If the algorithm is not supported. - */ - public static int getSignatureByteArrayLength(final SignatureAlgorithm alg) - throws JwtException { - - switch (alg) { - case ES256: - return 64; - case ES384: - return 96; - case ES512: - return 132; - default: - throw new JwtException("Unsupported Algorithm: " + alg.name()); - } - } - - /** - * Transcodes the JCA ASN.1/DER-encoded signature into the concatenated - * R + S format expected by ECDSA JWS. - * - * @param derSignature The ASN1./DER-encoded. Must not be {@code null}. - * @param outputLength The expected length of the ECDSA JWS signature. - * @return The ECDSA JWS encoded signature. - * @throws JwtException If the ASN.1/DER signature format is invalid. - * @deprecated since JJWT_RELEASE_VERSION. Use {@code ElliptiCurveSignatureAlgorithm.transcodeSignatureToConcat} instead. - */ - @Deprecated - public static byte[] transcodeSignatureToConcat(final byte[] derSignature, int outputLength) throws JwtException { - return EllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(derSignature, outputLength); - } - - - /** - * Transcodes the ECDSA JWS signature into ASN.1/DER format for use by - * the JCA verifier. - * - * @param jwsSignature The JWS signature, consisting of the - * concatenated R and S values. Must not be - * {@code null}. - * @return The ASN.1/DER encoded signature. - * @throws JwtException If the ECDSA JWS signature format is invalid. - * @deprecated since JJWT_RELEASE_VERSION. Use {@link EllipticCurveSignatureAlgorithm#transcodeSignatureToDER(byte[])} instead. - */ - @Deprecated - public static byte[] transcodeSignatureToDER(byte[] jwsSignature) throws JwtException { - return EllipticCurveSignatureAlgorithm.transcodeSignatureToDER(jwsSignature); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java deleted file mode 100644 index 9f8729daa..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2015 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.impl.security.EllipticCurveSignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.SignatureException; - -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.PublicKey; -import java.security.Signature; -import java.security.interfaces.ECPublicKey; - -public class EllipticCurveSignatureValidator extends EllipticCurveProvider implements SignatureValidator { - - private static final String EC_PUBLIC_KEY_REQD_MSG = - "Elliptic Curve signature validation requires an ECPublicKey instance."; - - public EllipticCurveSignatureValidator(SignatureAlgorithm alg, Key key) { - super(alg, key); - Assert.isTrue(key instanceof ECPublicKey, EC_PUBLIC_KEY_REQD_MSG); - } - - @Override - public boolean isValid(byte[] data, byte[] signature) { - Signature sig = createSignatureInstance(); - PublicKey publicKey = (PublicKey) key; - try { - int expectedSize = getSignatureByteArrayLength(alg); - /** - * - * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature. - * This fallback is for backwards compatibility ONLY (to support tokens generated by previous versions of jjwt) - * and backwards compatibility will possibly be removed in a future version of this library. - * - * **/ - byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : EllipticCurveSignatureAlgorithm.transcodeSignatureToDER(signature); - return doVerify(sig, publicKey, data, derSignature); - } catch (Exception e) { - String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage(); - throw new SignatureException(msg, e); - } - } - - protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature) - throws InvalidKeyException, java.security.SignatureException { - sig.initVerify(publicKey); - sig.update(data); - return sig.verify(signature); - } - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java deleted file mode 100644 index 3d0922fd4..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2015 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.impl.security.EllipticCurveSignatureAlgorithm; -import io.jsonwebtoken.security.SignatureException; - -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.PrivateKey; -import java.security.Signature; -import java.security.interfaces.ECKey; - -public class EllipticCurveSigner extends EllipticCurveProvider implements Signer { - - public EllipticCurveSigner(SignatureAlgorithm alg, Key key) { - super(alg, key); - if (!(key instanceof PrivateKey && key instanceof ECKey)) { - String msg = "Elliptic Curve signatures must be computed using an EC PrivateKey. The specified key of " + - "type " + key.getClass().getName() + " is not an EC PrivateKey."; - throw new IllegalArgumentException(msg); - } - } - - @Override - public byte[] sign(byte[] data) { - try { - return doSign(data); - } catch (InvalidKeyException e) { - throw new SignatureException("Invalid Elliptic Curve PrivateKey. " + e.getMessage(), e); - } catch (java.security.SignatureException e) { - throw new SignatureException("Unable to calculate signature using Elliptic Curve PrivateKey. " + e.getMessage(), e); - } catch (JwtException e) { - throw new SignatureException("Unable to convert signature to JOSE format. " + e.getMessage(), e); - } - } - - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JwtException { - PrivateKey privateKey = (PrivateKey)key; - Signature sig = createSignatureInstance(); - sig.initSign(privateKey); - sig.update(data); - return EllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(sig.sign(), getSignatureByteArrayLength(alg)); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/JwtSignatureValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/JwtSignatureValidator.java deleted file mode 100644 index a7175070b..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/JwtSignatureValidator.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -public interface JwtSignatureValidator { - - boolean isValid(String jwtWithoutSignature, String base64UrlEncodedSignature); -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/JwtSigner.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/JwtSigner.java deleted file mode 100644 index 450b707f7..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/JwtSigner.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -public interface JwtSigner { - - String sign(String jwtWithoutSignature); -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java deleted file mode 100644 index e577b6262..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.impl.security.Randoms; -import io.jsonwebtoken.lang.Assert; - -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import java.security.Key; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; - -public abstract class MacProvider extends SignatureProvider { - - protected MacProvider(SignatureAlgorithm alg, Key key) { - super(alg, key); - Assert.isTrue(alg.isHmac(), "SignatureAlgorithm must be a HMAC SHA algorithm."); - } - - /** - * Generates a new secure-random 512 bit secret key suitable for creating and verifying HMAC-SHA signatures. This - * is a convenience method that immediately delegates to {@link #generateKey(SignatureAlgorithm)} using {@link - * SignatureAlgorithm#HS512} as the method argument. - * - * @return a new secure-random 512 bit secret key suitable for creating and verifying HMAC-SHA signatures. - * @see #generateKey(SignatureAlgorithm) - * @see #generateKey(SignatureAlgorithm, SecureRandom) - * @since 0.5 - */ - public static SecretKey generateKey() { - return generateKey(SignatureAlgorithm.HS512); - } - - /** - * Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures - * according to the specified {@code SignatureAlgorithm} using JJWT's default {@link - * Randoms#secureRandom() SecureRandom instance}. This is a convenience method that immediately - * delegates to {@link #generateKey(SignatureAlgorithm, SecureRandom)}. - * - * @param alg the desired signature algorithm - * @return a new secure-random secret key of a length suitable for creating and verifying HMAC signatures according - * to the specified {@code SignatureAlgorithm} using JJWT's default {@link - * Randoms#secureRandom() SecureRandom instance}. - * @see #generateKey() - * @see #generateKey(SignatureAlgorithm, SecureRandom) - * @since 0.5 - */ - public static SecretKey generateKey(SignatureAlgorithm alg) { - return generateKey(alg, Randoms.secureRandom()); - } - - /** - * Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures - * according to the specified {@code SignatureAlgorithm} using the specified SecureRandom number generator. This - * implementation returns secure-random key sizes as follows: - * - * - * - *
    Key Sizes
    Signature Algorithm Generated Key Size
    HS256 256 bits (32 bytes)
    HS384 384 bits (48 bytes)
    HS512 512 bits (64 bytes)
    - * - * @param alg the signature algorithm that will be used with the generated key - * @param random the secure random number generator used during key generation - * @return a new secure-random secret key of a length suitable for creating and verifying HMAC signatures according - * to the specified {@code SignatureAlgorithm} using the specified SecureRandom number generator. - * @see #generateKey() - * @see #generateKey(SignatureAlgorithm) - * @since 0.5 - * @deprecated since 0.10.0 - use {@link #generateKey(SignatureAlgorithm)} instead. - */ - @Deprecated - public static SecretKey generateKey(SignatureAlgorithm alg, SecureRandom random) { - - Assert.isTrue(alg.isHmac(), "SignatureAlgorithm argument must represent an HMAC algorithm."); - - KeyGenerator gen; - - try { - gen = KeyGenerator.getInstance(alg.getJcaName()); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("The " + alg.getJcaName() + " algorithm is not available. " + - "This should never happen on JDK 7 or later - please report this to the JJWT developers.", e); - } - - return gen.generateKey(); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java deleted file mode 100644 index 07379f537..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.SignatureException; - -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.NoSuchAlgorithmException; - -public class MacSigner extends MacProvider implements Signer { - - public MacSigner(SignatureAlgorithm alg, byte[] key) { - this(alg, new SecretKeySpec(key, alg.getJcaName())); - } - - public MacSigner(SignatureAlgorithm alg, Key key) { - super(alg, key); - Assert.isTrue(alg.isHmac(), "The MacSigner only supports HMAC signature algorithms."); - if (!(key instanceof SecretKey)) { - String msg = "MAC signatures must be computed and verified using a SecretKey. The specified key of " + - "type " + key.getClass().getName() + " is not a SecretKey."; - throw new IllegalArgumentException(msg); - } - } - - @Override - public byte[] sign(byte[] data) { - Mac mac = getMacInstance(); - return mac.doFinal(data); - } - - protected Mac getMacInstance() throws SignatureException { - try { - return doGetMacInstance(); - } catch (NoSuchAlgorithmException e) { - String msg = "Unable to obtain JCA MAC algorithm '" + alg.getJcaName() + "': " + e.getMessage(); - throw new SignatureException(msg, e); - } catch (InvalidKeyException e) { - String msg = "The specified signing key is not a valid " + alg.name() + " key: " + e.getMessage(); - throw new SignatureException(msg, e); - } - } - - protected Mac doGetMacInstance() throws NoSuchAlgorithmException, InvalidKeyException { - Mac mac = Mac.getInstance(alg.getJcaName()); - mac.init(key); - return mac; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java deleted file mode 100644 index 2e5428361..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; - -import java.security.Key; -import java.security.MessageDigest; - -public class MacValidator implements SignatureValidator { - - private final MacSigner signer; - - public MacValidator(SignatureAlgorithm alg, Key key) { - this.signer = new MacSigner(alg, key); - } - - @Override - public boolean isValid(byte[] data, byte[] signature) { - byte[] computed = this.signer.sign(data); - return MessageDigest.isEqual(computed, signature); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java deleted file mode 100644 index 11a91102e..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.impl.security.Randoms; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.RuntimeEnvironment; -import io.jsonwebtoken.security.SignatureException; - -import java.security.InvalidAlgorithmParameterException; -import java.security.Key; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.Signature; -import java.security.spec.MGF1ParameterSpec; -import java.security.spec.PSSParameterSpec; -import java.util.HashMap; -import java.util.Map; - -public abstract class RsaProvider extends SignatureProvider { - - private static final Map PSS_PARAMETER_SPECS = createPssParameterSpecs(); - - private static Map createPssParameterSpecs() { - - Map m = new HashMap(); - - MGF1ParameterSpec ps = MGF1ParameterSpec.SHA256; - PSSParameterSpec spec = new PSSParameterSpec(ps.getDigestAlgorithm(), "MGF1", ps, 32, 1); - m.put(SignatureAlgorithm.PS256, spec); - - ps = MGF1ParameterSpec.SHA384; - spec = new PSSParameterSpec(ps.getDigestAlgorithm(), "MGF1", ps, 48, 1); - m.put(SignatureAlgorithm.PS384, spec); - - ps = MGF1ParameterSpec.SHA512; - spec = new PSSParameterSpec(ps.getDigestAlgorithm(), "MGF1", ps, 64, 1); - m.put(SignatureAlgorithm.PS512, spec); - - return m; - } - - static { - RuntimeEnvironment.enableBouncyCastleIfPossible(); //PS256, PS384, PS512 on <= JDK 10 require BC - } - - protected RsaProvider(SignatureAlgorithm alg, Key key) { - super(alg, key); - Assert.isTrue(alg.isRsa(), "SignatureAlgorithm must be an RSASSA or RSASSA-PSS algorithm."); - } - - protected Signature createSignatureInstance() { - - Signature sig = super.createSignatureInstance(); - - PSSParameterSpec spec = PSS_PARAMETER_SPECS.get(alg); - if (spec != null) { - setParameter(sig, spec); - } - return sig; - } - - protected void setParameter(Signature sig, PSSParameterSpec spec) { - try { - doSetParameter(sig, spec); - } catch (InvalidAlgorithmParameterException e) { - String msg = "Unsupported RSASSA-PSS parameter '" + spec + "': " + e.getMessage(); - throw new SignatureException(msg, e); - } - } - - protected void doSetParameter(Signature sig, PSSParameterSpec spec) throws InvalidAlgorithmParameterException { - sig.setParameter(spec); - } - - /** - * Generates a new RSA secure-random 4096 bit key pair. 4096 bits is JJWT's current recommended minimum key size - * for use in modern applications (during or after year 2015). This is a convenience method that immediately - * delegates to {@link #generateKeyPair(int)}. - * - * @return a new RSA secure-random 4096 bit key pair. - * @see #generateKeyPair(int) - * @see #generateKeyPair(int, SecureRandom) - * @see #generateKeyPair(String, int, SecureRandom) - * @since 0.5 - */ - public static KeyPair generateKeyPair() { - return generateKeyPair(4096); - } - - /** - * Generates a new RSA secure-randomly key pair of the specified size using JJWT's default {@link - * Randoms#secureRandom() SecureRandom instance}. This is a convenience method that immediately - * delegates to {@link #generateKeyPair(int, SecureRandom)}. - * - * @param keySizeInBits the key size in bits (NOT bytes). - * @return a new RSA secure-random key pair of the specified size. - * @see #generateKeyPair() - * @see #generateKeyPair(int, SecureRandom) - * @see #generateKeyPair(String, int, SecureRandom) - * @since 0.5 - */ - public static KeyPair generateKeyPair(int keySizeInBits) { - return generateKeyPair(keySizeInBits, Randoms.secureRandom()); - } - - /** - * Generates a new RSA secure-randomly key pair suitable for the specified SignatureAlgorithm using JJWT's - * default {@link Randoms#secureRandom() SecureRandom instance}. This is a convenience method - * that immediately delegates to {@link #generateKeyPair(int)} based on the relevant key size for the specified - * algorithm. - * - * @param alg the signature algorithm to inspect to determine a size in bits. - * @return a new RSA secure-random key pair of the specified size. - * @see #generateKeyPair() - * @see #generateKeyPair(int, SecureRandom) - * @see #generateKeyPair(String, int, SecureRandom) - * @since 0.10.0 - */ - @SuppressWarnings("unused") //used by io.jsonwebtoken.security.Keys - public static KeyPair generateKeyPair(SignatureAlgorithm alg) { - Assert.isTrue(alg.isRsa(), "Only RSA algorithms are supported by this method."); - int keySizeInBits = 4096; - switch (alg) { - case RS256: - case PS256: - keySizeInBits = 2048; - break; - case RS384: - case PS384: - keySizeInBits = 3072; - break; - } - return generateKeyPair(keySizeInBits, Randoms.secureRandom()); - } - - /** - * Generates a new RSA secure-random key pair of the specified size using the given SecureRandom number generator. - * This is a convenience method that immediately delegates to {@link #generateKeyPair(String, int, SecureRandom)} - * using {@code RSA} as the {@code jcaAlgorithmName} argument. - * - * @param keySizeInBits the key size in bits (NOT bytes) - * @param random the secure random number generator to use during key generation. - * @return a new RSA secure-random key pair of the specified size using the given SecureRandom number generator. - * @see #generateKeyPair() - * @see #generateKeyPair(int) - * @see #generateKeyPair(String, int, SecureRandom) - * @since 0.5 - */ - public static KeyPair generateKeyPair(int keySizeInBits, SecureRandom random) { - return generateKeyPair("RSA", keySizeInBits, random); - } - - /** - * Generates a new secure-random key pair of the specified size using the specified SecureRandom according to the - * specified {@code jcaAlgorithmName}. - * - * @param jcaAlgorithmName the name of the JCA algorithm to use for key pair generation, for example, {@code RSA}. - * @param keySizeInBits the key size in bits (NOT bytes) - * @param random the SecureRandom generator to use during key generation. - * @return a new secure-randomly generated key pair of the specified size using the specified SecureRandom according - * to the specified {@code jcaAlgorithmName}. - * @see #generateKeyPair() - * @see #generateKeyPair(int) - * @see #generateKeyPair(int, SecureRandom) - * @since 0.5 - */ - protected static KeyPair generateKeyPair(String jcaAlgorithmName, int keySizeInBits, SecureRandom random) { - KeyPairGenerator keyGenerator; - try { - keyGenerator = KeyPairGenerator.getInstance(jcaAlgorithmName); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Unable to obtain an RSA KeyPairGenerator: " + e.getMessage(), e); - } - - keyGenerator.initialize(keySizeInBits, random); - - return keyGenerator.genKeyPair(); - } - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java deleted file mode 100644 index 6bc879313..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.security.SignatureException; - -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.PrivateKey; -import java.security.Signature; -import java.security.interfaces.RSAKey; - -public class RsaSigner extends RsaProvider implements Signer { - - public RsaSigner(SignatureAlgorithm alg, Key key) { - super(alg, key); - // https://github.com/jwtk/jjwt/issues/68 - // Instead of checking for an instance of RSAPrivateKey, check for PrivateKey and RSAKey: - if (!(key instanceof PrivateKey && key instanceof RSAKey)) { - String msg = "RSA signatures must be computed using an RSA PrivateKey. The specified key of type " + - key.getClass().getName() + " is not an RSA PrivateKey."; - throw new IllegalArgumentException(msg); - } - } - - @Override - public byte[] sign(byte[] data) { - try { - return doSign(data); - } catch (InvalidKeyException e) { - throw new SignatureException("Invalid RSA PrivateKey. " + e.getMessage(), e); - } catch (java.security.SignatureException e) { - throw new SignatureException("Unable to calculate signature using RSA PrivateKey. " + e.getMessage(), e); - } - } - - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException { - PrivateKey privateKey = (PrivateKey)key; - Signature sig = createSignatureInstance(); - sig.initSign(privateKey); - sig.update(data); - return sig.sign(); - } - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java deleted file mode 100644 index 8665565fb..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.impl.security.Randoms; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.RuntimeEnvironment; -import io.jsonwebtoken.security.SignatureException; - -import java.security.Key; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.Signature; - -abstract class SignatureProvider { - - /** - * @deprecated use {@link Randoms#secureRandom() Randoms.secureRandom()} instead. - */ - @Deprecated //TODO: remove for 1.0 - public static final SecureRandom DEFAULT_SECURE_RANDOM = Randoms.secureRandom(); - - protected final SignatureAlgorithm alg; - protected final Key key; - - protected SignatureProvider(SignatureAlgorithm alg, Key key) { - Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - Assert.notNull(key, "Key cannot be null."); - this.alg = alg; - this.key = key; - } - - protected Signature createSignatureInstance() { - try { - return getSignatureInstance(); - } catch (NoSuchAlgorithmException e) { - String msg = "Unavailable " + alg.getFamilyName() + " Signature algorithm '" + alg.getJcaName() + "'."; - if (!alg.isJdkStandard() && !isBouncyCastleAvailable()) { - msg += " This is not a standard JDK algorithm. Try including BouncyCastle in the runtime classpath."; - } - throw new SignatureException(msg, e); - } - } - - protected Signature getSignatureInstance() throws NoSuchAlgorithmException { - return Signature.getInstance(alg.getJcaName()); - } - - protected boolean isBouncyCastleAvailable() { - return RuntimeEnvironment.BOUNCY_CASTLE_AVAILABLE; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidator.java deleted file mode 100644 index edeccb7e7..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidator.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -public interface SignatureValidator { - - boolean isValid(byte[] data, byte[] signature); - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java deleted file mode 100644 index 1e84b620e..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; - -import java.security.Key; - -public interface SignatureValidatorFactory { - - SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key); -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/Signer.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/Signer.java deleted file mode 100644 index 18600f242..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/Signer.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.security.SignatureException; - -public interface Signer { - - byte[] sign(byte[] data) throws SignatureException; -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignerFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignerFactory.java deleted file mode 100644 index e0e460675..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignerFactory.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; - -import java.security.Key; - -public interface SignerFactory { - - Signer createSigner(SignatureAlgorithm alg, Key key); -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/CodecConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/io/CodecConverter.java new file mode 100644 index 000000000..afb3dd33d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/CodecConverter.java @@ -0,0 +1,32 @@ +package io.jsonwebtoken.impl.io; + +import io.jsonwebtoken.impl.lang.Converter; +import io.jsonwebtoken.io.Decoder; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.Encoder; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Assert; + +public class CodecConverter implements Converter { + + public static final CodecConverter BASE64 = new CodecConverter<>(Encoders.BASE64, Decoders.BASE64); + public static final CodecConverter BASE64URL = new CodecConverter<>(Encoders.BASE64URL, Decoders.BASE64URL); + + private final Encoder encoder; + private final Decoder decoder; + + public CodecConverter(Encoder encoder, Decoder decoder) { + this.encoder = Assert.notNull(encoder, "Encoder cannot be null."); + this.decoder = Assert.notNull(decoder, "Decoder cannot be null."); + } + + @Override + public B applyTo(A a) { + return this.encoder.encode(a); + } + + @Override + public A applyFrom(B b) { + return this.decoder.decode(b); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/Codecs.java b/impl/src/main/java/io/jsonwebtoken/impl/io/Codecs.java new file mode 100644 index 000000000..a8023486f --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/Codecs.java @@ -0,0 +1,10 @@ +package io.jsonwebtoken.impl.io; + +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.Encoders; + +public class Codecs { + + public static final CodecConverter BASE64 = new CodecConverter<>(Encoders.BASE64, Decoders.BASE64); + public static final CodecConverter BASE64URL = new CodecConverter<>(Encoders.BASE64URL, Decoders.BASE64URL); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/BiFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/BiFunction.java new file mode 100644 index 000000000..dba519e74 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/BiFunction.java @@ -0,0 +1,6 @@ +package io.jsonwebtoken.impl.lang; + +public interface BiFunction { + + R apply(T t, U u); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java new file mode 100644 index 000000000..050157404 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java @@ -0,0 +1,112 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; + +public final class Bytes { + + public static final byte[] EMPTY = new byte[0]; + + private static final int LONG_BYTE_LENGTH = Long.SIZE / Byte.SIZE; + private static final int INT_BYTE_LENGTH = Integer.SIZE / Byte.SIZE; + public static final String LONG_REQD_MSG = "Long byte arrays must be " + LONG_BYTE_LENGTH + " bytes in length."; + public static final String INT_REQD_MSG = "Integer byte arrays must be " + INT_BYTE_LENGTH + " bytes in length."; + + //prevent instantiation + private Bytes() { + } + + public static byte[] toBytes(int i) { + return new byte[]{ + (byte) (i >>> 24), + (byte) (i >>> 16), + (byte) (i >>> 8), + (byte) i + }; + } + + public static byte[] toBytes(long l) { + return new byte[]{ + (byte) (l >>> 56), + (byte) (l >>> 48), + (byte) (l >>> 40), + (byte) (l >>> 32), + (byte) (l >>> 24), + (byte) (l >>> 16), + (byte) (l >>> 8), + (byte) l + }; + } + + public static long toLong(byte[] bytes) { + Assert.isTrue(Arrays.length(bytes) == LONG_BYTE_LENGTH, LONG_REQD_MSG); + return ((bytes[0] & 0xFFL) << 56) | + ((bytes[1] & 0xFFL) << 48) | + ((bytes[2] & 0xFFL) << 40) | + ((bytes[3] & 0xFFL) << 32) | + ((bytes[4] & 0xFFL) << 24) | + ((bytes[5] & 0xFFL) << 16) | + ((bytes[6] & 0xFFL) << 8) | + ((bytes[7] & 0xFFL)); + } + + public static int toInt(byte[] bytes) { + Assert.isTrue(Arrays.length(bytes) == INT_BYTE_LENGTH, INT_REQD_MSG); + return ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + (bytes[3] & 0xFF); + } + + public static int[] toInts(byte[] bytes) { + int[] ints = new int[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + ints[i] = bytes[i] & 0xFF; + } + return ints; + } + + public static byte[] concat(byte[]... arrays) { + int len = 0; + int count = Arrays.length(arrays); + for (int i = 0; i < count; i++) { + len += arrays[i].length; + } + byte[] output = new byte[len]; + int position = 0; + if (len > 0) { + for (byte[] array : arrays) { + int alen = Arrays.length(array); + if (alen > 0) { + System.arraycopy(array, 0, output, position, alen); + position += alen; + } + } + } + return output; + } + + public static int byteLength(byte[] bytes) { + return bytes == null ? 0 : bytes.length; + } + + public static long bitLength(byte[] bytes) { + return bytes == null ? 0 : bytes.length * (long) Byte.SIZE; + } + + public static String bitsMsg(long bitLength) { + return bitLength + " bits (" + bitLength / Byte.SIZE + " bytes)"; + } + + public static String bytesMsg(int byteArrayLength) { + return bitsMsg((long) byteArrayLength * Byte.SIZE); + } + + public static void increment(byte[] a) { + for (int i = a.length - 1; i >= 0; --i) { + if (++a[i] != 0) { + break; + } + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/CheckedFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/CheckedFunction.java new file mode 100644 index 000000000..7737a3b63 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/CheckedFunction.java @@ -0,0 +1,5 @@ +package io.jsonwebtoken.impl.lang; + +public interface CheckedFunction { + R apply(T t) throws Exception; +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/CollectionConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/CollectionConverter.java new file mode 100644 index 000000000..80d7c87c4 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/CollectionConverter.java @@ -0,0 +1,89 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +class CollectionConverter> implements Converter { + + private final Converter elementConverter; + private final Function fn; + + public static CollectionConverter> forList(Converter elementConverter) { + return new CollectionConverter<>(elementConverter, new CreateListFunction()); + } + + public static CollectionConverter> forSet(Converter elementConverter) { + return new CollectionConverter<>(elementConverter, new CreateSetFunction()); + } + + public CollectionConverter(Converter elementConverter, Function fn) { + this.elementConverter = Assert.notNull(elementConverter, "Element converter cannot be null."); + this.fn = Assert.notNull(fn, "Collection function cannot be null."); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public Object applyTo(C ts) { + if (Collections.isEmpty(ts)) { + return ts; + } + Collection c = fn.apply(ts.size()); + for (T element : ts) { + Object encoded = elementConverter.applyTo(element); + c.add(encoded); + } + return c; + } + + private C toElementList(Collection c) { + Assert.notEmpty(c, "Collection cannot be null or empty."); + C result = fn.apply(c.size()); + for (Object o : c) { + T element = elementConverter.applyFrom(o); + result.add(element); + } + return result; + } + + @Override + public C applyFrom(Object value) { + if (value == null) { + return null; + } + Collection c; + if (value.getClass().isArray() && !value.getClass().getComponentType().isPrimitive()) { + c = Collections.arrayToList(value); + } else if (value instanceof Collection) { + c = (Collection) value; + } else { + c = java.util.Collections.singletonList(value); + } + C result; + if (Collections.isEmpty(c)) { + result = fn.apply(0); + } else { + result = toElementList(c); + } + return result; + } + + private static class CreateListFunction implements Function> { + @Override + public List apply(Integer size) { + return size > 0 ? new ArrayList(size) : new ArrayList(); + } + } + + private static class CreateSetFunction implements Function> { + @Override + public Set apply(Integer size) { + return size > 0 ? new LinkedHashSet(size) : new LinkedHashSet(); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/ConstantFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/ConstantFunction.java new file mode 100644 index 000000000..11754d9b1 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/ConstantFunction.java @@ -0,0 +1,29 @@ +package io.jsonwebtoken.impl.lang; + + +/** + * Function that always returns the same value + * + * @param Input type + * @param Return value type + */ +public final class ConstantFunction implements Function { + + private static final Function NULL = new ConstantFunction<>(null); + + private final R value; + + public ConstantFunction(R value) { + this.value = value; + } + + @SuppressWarnings("unchecked") + public static Function forNull() { + return (Function) NULL; + } + + @Override + public R apply(T t) { + return this.value; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Converter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Converter.java new file mode 100644 index 000000000..6453b3575 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Converter.java @@ -0,0 +1,8 @@ +package io.jsonwebtoken.impl.lang; + +public interface Converter { + + B applyTo(A a); + + A applyFrom(B b); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java new file mode 100644 index 000000000..c850a7cff --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java @@ -0,0 +1,31 @@ +package io.jsonwebtoken.impl.lang; + +import java.util.List; +import java.util.Set; + +public final class Converters { + + //prevent instantiation + private Converters() { + } + + public static Converter none(Class clazz) { + return new NoConverter<>(clazz); + } + + public static Converter, Object> forSet(Converter elementConverter) { + return CollectionConverter.forSet(elementConverter); + } + + public static Converter, Object> forSetOf(Class clazz) { + return forSet(none(clazz)); + } + + public static Converter, Object> forList(Converter elementConverter) { + return CollectionConverter.forList(elementConverter); + } + + public static Converter forEncoded(Class elementType, Converter elementConverter) { + return new EncodedObjectConverter(elementType, elementConverter); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java new file mode 100644 index 000000000..39250408d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java @@ -0,0 +1,33 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; + +public class EncodedObjectConverter implements Converter { + + private final Class type; + private final Converter converter; + + public EncodedObjectConverter(Class type, Converter converter) { + this.type = Assert.notNull(type, "Value type cannot be null."); + this.converter = Assert.notNull(converter, "Value converter cannot be null."); + } + + @Override + public Object applyTo(T t) { + return converter.applyTo(t); + } + + @Override + public T applyFrom(Object value) { + Assert.notNull(value, "Value argument cannot be null."); + if (type.isInstance(value)) { + return type.cast(value); + } else if (value instanceof String) { + return converter.applyFrom((String) value); + } else { + String msg = "Values must be either String or " + type.getName() + + " instances. Value type found: " + value.getClass().getName() + ". Value: " + value; + throw new IllegalArgumentException(msg); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Function.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Function.java new file mode 100644 index 000000000..a3831241a --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Function.java @@ -0,0 +1,6 @@ +package io.jsonwebtoken.impl.lang; + +public interface Function { + + R apply(T t); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/LocatorFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/LocatorFunction.java new file mode 100644 index 000000000..98da49d46 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/LocatorFunction.java @@ -0,0 +1,19 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Locator; +import io.jsonwebtoken.lang.Assert; + +public class LocatorFunction, R> implements Function { + + private final Locator locator; + + public LocatorFunction(Locator locator) { + this.locator = Assert.notNull(locator, "Locator instance cannot be null."); + } + + @Override + public R apply(H h) { + return this.locator.locate(h); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/NoConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/NoConverter.java new file mode 100644 index 000000000..5327cbf16 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/NoConverter.java @@ -0,0 +1,28 @@ +package io.jsonwebtoken.impl.lang; + +class NoConverter implements Converter { + + private final Class type; + + public NoConverter(Class type) { + this.type = type; + } + + @Override + public Object applyTo(T t) { + return t; + } + + @Override + public T applyFrom(Object o) { + if (o == null) { + return null; + } + Class clazz = o.getClass(); + if (!type.isAssignableFrom(clazz)) { + String msg = "Unsupported value type: " + clazz.getName(); + throw new IllegalArgumentException(msg); + } + return type.cast(o); + } +} \ No newline at end of file diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/NullSafeConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/NullSafeConverter.java new file mode 100644 index 000000000..a1ad3b830 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/NullSafeConverter.java @@ -0,0 +1,22 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; + +public class NullSafeConverter implements Converter { + + private final Converter converter; + + public NullSafeConverter(Converter converter) { + this.converter = Assert.notNull(converter, "Delegate converter cannot be null."); + } + + @Override + public B applyTo(A a) { + return a == null ? null : converter.applyTo(a); + } + + @Override + public A applyFrom(B b) { + return b == null ? null : converter.applyFrom(b); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java new file mode 100644 index 000000000..59dabe4bc --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java @@ -0,0 +1,35 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Classes; + +import java.lang.reflect.Constructor; + +public class PropagatingExceptionFunction implements Function { + + private final Function function; + private final Class clazz; + private final String msg; + + public PropagatingExceptionFunction(Class exceptionClass, String msg, Function f) { + this.function = Assert.notNull(f, "Function cannot be null."); + this.clazz = Assert.notNull(exceptionClass, "Exception class cannot be null."); + Assert.hasText(msg, "String message cannot be null or empty."); + this.msg = msg; + } + + @SuppressWarnings("unchecked") + public R apply(T t) { + try { + return function.apply(t); + } catch (Exception e) { + if (clazz.isAssignableFrom(e.getClass())) { + throw clazz.cast(e); + } + String msg = this.msg + " Cause: " + e.getMessage(); + Class clazzz = (Class)clazz; + Constructor ctor = Classes.getConstructor(clazzz, String.class, Throwable.class); + throw Classes.instantiate(ctor, msg, e); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Registry.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Registry.java new file mode 100644 index 000000000..2e0dd0ca3 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Registry.java @@ -0,0 +1,8 @@ +package io.jsonwebtoken.impl.lang; + +import java.util.Collection; + +public interface Registry extends Function { + + Collection values(); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/UriStringConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/UriStringConverter.java new file mode 100644 index 000000000..b7509daa1 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/UriStringConverter.java @@ -0,0 +1,21 @@ +package io.jsonwebtoken.impl.lang; + +import java.net.URI; + +public class UriStringConverter implements Converter { + + @Override + public String applyTo(URI uri) { + return uri.toString(); + } + + @Override + public URI applyFrom(String s) { + try { + return URI.create(s); + } catch (Exception e) { + String msg = "Unable to convert String value '" + s + "' to URI instance: " + e.getMessage(); + throw new IllegalArgumentException(msg, e); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/ValueGetter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/ValueGetter.java new file mode 100644 index 000000000..36075d7a2 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/ValueGetter.java @@ -0,0 +1,18 @@ +package io.jsonwebtoken.impl.lang; + +import java.math.BigInteger; + +public interface ValueGetter { + + String getRequiredString(String key); + + int getRequiredInteger(String key); + + int getRequiredPositiveInteger(String key); + + byte[] getRequiredBytes(String key); + + byte[] getRequiredBytes(String key, int requiredByteLength); + + BigInteger getRequiredBigInt(String key, boolean sensitive); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithm.java deleted file mode 100644 index 7760420c0..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithm.java +++ /dev/null @@ -1,112 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Arrays; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.AeadIvRequest; -import io.jsonwebtoken.security.AeadIvEncryptionResult; -import io.jsonwebtoken.security.AeadRequest; -import io.jsonwebtoken.security.AeadSymmetricEncryptionAlgorithm; -import io.jsonwebtoken.security.AssociatedDataSource; -import io.jsonwebtoken.security.CryptoException; -import io.jsonwebtoken.security.CryptoRequest; -import io.jsonwebtoken.security.InitializationVectorSource; - -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import java.security.SecureRandom; - -import static io.jsonwebtoken.lang.Arrays.*; - -/** - * @since JJWT_RELEASE_VERSION - */ -abstract class AbstractAeadAesEncryptionAlgorithm - extends AbstractEncryptionAlgorithm, AeadIvEncryptionResult, AeadIvRequest> - implements AeadSymmetricEncryptionAlgorithm { - - protected static final int AES_BLOCK_SIZE_BYTES = 16; - protected static final int AES_BLOCK_SIZE_BITS = AES_BLOCK_SIZE_BYTES * Byte.SIZE; - public static final String INVALID_GENERATED_IV_LENGTH = - "generatedIvLengthInBits must be a positive number <= " + AES_BLOCK_SIZE_BITS; - - protected static final String DECRYPT_NO_IV = "This EncryptionAlgorithm implementation rejects decryption " + - "requests that do not include initialization vectors. AES ciphertext without an IV is weak and should " + - "never be used."; - - private final int generatedIvByteLength; - private final int requiredKeyByteLength; - private final int requiredKeyBitLength; - - public AbstractAeadAesEncryptionAlgorithm(String name, String transformationString, int generatedIvLengthInBits, int requiredKeySizeInBits) { - - super(name, transformationString); - - Assert.isTrue(generatedIvLengthInBits > 0 && generatedIvLengthInBits <= AES_BLOCK_SIZE_BITS, INVALID_GENERATED_IV_LENGTH); - Assert.isTrue((generatedIvLengthInBits % Byte.SIZE) == 0, "generatedIvLengthInBits must be evenly divisible by 8."); - this.generatedIvByteLength = generatedIvLengthInBits / Byte.SIZE; - - Assert.isTrue(requiredKeySizeInBits > 0, "requiredKeyLengthInBits must be greater than zero."); - Assert.isTrue((requiredKeySizeInBits % Byte.SIZE) == 0, "requiredKeyLengthInBits must be evenly divisible by 8."); - this.requiredKeyBitLength = requiredKeySizeInBits; - this.requiredKeyByteLength = requiredKeySizeInBits / Byte.SIZE; - } - - public int getRequiredKeyByteLength() { - return this.requiredKeyByteLength; - } - - @Override - public SecretKey generateKey() { - try { - return doGenerateKey(); - } catch (Exception e) { - throw new CryptoException("Unable to generate a new " + getName() + " SecretKey: " + e.getMessage(), e); - } - } - - protected SecretKey doGenerateKey() throws Exception { - KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); - keyGenerator.init(this.requiredKeyBitLength); - return keyGenerator.generateKey(); - } - - byte[] ensureInitializationVector(AeadRequest request) { - byte[] iv = null; - if (request instanceof InitializationVectorSource) { - iv = Arrays.clean(((InitializationVectorSource)request).getInitializationVector()); - } - if (Arrays.length(iv) == 0) { - iv = new byte[this.generatedIvByteLength]; - SecureRandom random = ensureSecureRandom(request); - random.nextBytes(iv); - } - return iv; - } - - SecretKey assertKey(CryptoRequest request) { - SecretKey key = request.getKey(); - return assertKeyLength(key); - } - - SecretKey assertKeyLength(SecretKey key) { - int length = length(key.getEncoded()); - if (length != requiredKeyByteLength) { - throw new CryptoException("The " + getName() + " algorithm requires that keys have a key length of " + - "(preferably secure-random) " + requiredKeyBitLength + " bits (" + - requiredKeyByteLength + " bytes). The provided key has a length of " + length * Byte.SIZE - + " bits (" + length + " bytes)."); - } - return key; - } - - byte[] assertDecryptionIv(InitializationVectorSource src) throws IllegalArgumentException { - byte[] iv = src.getInitializationVector(); - Assert.notEmpty(iv, DECRYPT_NO_IV); - return iv; - } - - byte[] getAAD(AssociatedDataSource src) { - byte[] aad = src.getAssociatedData(); - return io.jsonwebtoken.lang.Arrays.clean(aad); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java new file mode 100644 index 000000000..f23327a8d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java @@ -0,0 +1,47 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.AsymmetricJwk; + +import java.net.URI; +import java.security.Key; +import java.security.cert.X509Certificate; +import java.util.List; + +abstract class AbstractAsymmetricJwk extends AbstractJwk implements AsymmetricJwk { + + static final String PUBLIC_KEY_USE = "use"; + static final String X509_URL = "x5u"; + static final String X509_CERT_CHAIN = "x5c"; + static final String X509_SHA1_THUMBPRINT = "x5t"; + static final String X509_SHA256_THUMBPRINT = "x5t#S256"; + + AbstractAsymmetricJwk(JwkContext ctx) { + super(ctx); + } + + @Override + public String getPublicKeyUse() { + return this.context.getPublicKeyUse(); + } + + @Override + public URI getX509Url() { + return this.context.getX509Url(); + } + + @Override + public List getX509CertificateChain() { + return this.context.getX509CertificateChain(); + } + + @Override + public byte[] getX509CertificateSha1Thumbprint() { + return this.context.getX509CertificateSha1Thumbprint(); + } + + @Override + public byte[] getX509CertificateSha256Thumbprint() { + return this.context.getX509CertificateSha256Thumbprint(); + } + +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java new file mode 100644 index 000000000..7ca63f3dc --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java @@ -0,0 +1,249 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.AsymmetricJwk; +import io.jsonwebtoken.security.AsymmetricJwkBuilder; +import io.jsonwebtoken.security.EcPrivateJwk; +import io.jsonwebtoken.security.EcPrivateJwkBuilder; +import io.jsonwebtoken.security.EcPublicJwk; +import io.jsonwebtoken.security.EcPublicJwkBuilder; +import io.jsonwebtoken.security.MalformedKeyException; +import io.jsonwebtoken.security.PrivateJwk; +import io.jsonwebtoken.security.PrivateJwkBuilder; +import io.jsonwebtoken.security.PublicJwk; +import io.jsonwebtoken.security.PublicJwkBuilder; +import io.jsonwebtoken.security.RsaPrivateJwk; +import io.jsonwebtoken.security.RsaPrivateJwkBuilder; +import io.jsonwebtoken.security.RsaPublicJwk; +import io.jsonwebtoken.security.RsaPublicJwkBuilder; + +import java.net.URI; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.List; +import java.util.Set; + +abstract class AbstractAsymmetricJwkBuilder, + T extends AsymmetricJwkBuilder> + extends AbstractJwkBuilder implements AsymmetricJwkBuilder { + + protected boolean computeX509Sha1Thumbprint; + /** + * Boolean object indicates 3 states: 1) not configured 2) configured as true, 3) configured as false + */ + protected Boolean computeX509Sha256Thumbprint = null; + protected Boolean applyX509KeyUse = null; + private KeyUseStrategy keyUseStrategy = DefaultKeyUseStrategy.INSTANCE; + + public AbstractAsymmetricJwkBuilder(JwkContext ctx) { + super(ctx); + } + + AbstractAsymmetricJwkBuilder(AbstractAsymmetricJwkBuilder b, K key, Set privateNames) { + super(new DefaultJwkContext<>(privateNames, b.jwkContext, key)); + this.computeX509Sha1Thumbprint = b.computeX509Sha1Thumbprint; + this.computeX509Sha256Thumbprint = b.computeX509Sha256Thumbprint; + this.applyX509KeyUse = b.applyX509KeyUse; + this.keyUseStrategy = b.keyUseStrategy; + } + + @Override + public T setPublicKeyUse(String use) { + Assert.hasText(use, "publicKeyUse cannot be null or empty."); + return put(AbstractAsymmetricJwk.PUBLIC_KEY_USE, use); + } + + public T setKeyUseStrategy(KeyUseStrategy strategy) { + this.keyUseStrategy = Assert.notNull(strategy, "KeyUseStrategy cannot be null."); + return tthis(); + } + + @Override + public T setX509CertificateChain(List chain) { + Assert.notEmpty(chain, "X509Certificate chain cannot be null or empty."); + this.jwkContext.setX509CertificateChain(chain); + return tthis(); + } + + @Override + public T setX509Url(URI url) { + Assert.notNull(url, "X509Url cannot be null."); + this.jwkContext.setX509Url(url); + return tthis(); + } + + @Override + public T withX509KeyUse(boolean enable) { + this.applyX509KeyUse = enable; + return tthis(); + } + + @Override + public T withX509Sha1Thumbprint(boolean enable) { + this.computeX509Sha1Thumbprint = enable; + return tthis(); + } + + @Override + public T withX509Sha256Thumbprint(boolean enable) { + this.computeX509Sha256Thumbprint = enable; + return tthis(); + } + + private byte[] computeThumbprint(final X509Certificate cert, final String jcaName) { + try { + byte[] encoded = cert.getEncoded(); + MessageDigest digest = MessageDigest.getInstance(jcaName); + return digest.digest(encoded); + } catch (CertificateEncodingException e) { + String msg = "Unable to access X509Certificate encoded bytes necessary to compute a " + jcaName + + " thumbprint. Certificate: {" + cert + "}. Cause: " + e.getMessage(); + throw new MalformedKeyException(msg, e); + } catch (NoSuchAlgorithmException e) { + String msg = "JCA Algorithm Name '" + jcaName + "' is not available: " + e.getMessage(); + throw new IllegalStateException(msg, e); + } + } + + @Override + public J build() { + X509Certificate firstCert = null; + List chain = this.jwkContext.getX509CertificateChain(); + if (!Collections.isEmpty(chain)) { + firstCert = chain.get(0); + } + + if (applyX509KeyUse == null) { //if not specified, enable by default if possible: + applyX509KeyUse = firstCert != null && !Strings.hasText(this.jwkContext.getPublicKeyUse()); + } + if (computeX509Sha256Thumbprint == null) { //if not specified, enable by default if possible: + computeX509Sha256Thumbprint = firstCert != null && !computeX509Sha1Thumbprint; + } + + if (firstCert != null) { + if (applyX509KeyUse) { + KeyUsage usage = new KeyUsage(firstCert); + String use = keyUseStrategy.toJwkValue(usage); + if (Strings.hasText(use)) { + setPublicKeyUse(use); + } + } + if (computeX509Sha1Thumbprint) { + byte[] thumbprint = computeThumbprint(firstCert, "SHA-1"); + put(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT, thumbprint); + } + if (computeX509Sha256Thumbprint) { + byte[] thumbprint = computeThumbprint(firstCert, "SHA-256"); + put(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT, thumbprint); + } + } + return super.build(); + } + + private abstract static class DefaultPublicJwkBuilder, M extends PrivateJwk, P extends PrivateJwkBuilder, + T extends PublicJwkBuilder> + extends AbstractAsymmetricJwkBuilder + implements PublicJwkBuilder { + + DefaultPublicJwkBuilder(JwkContext ctx) { + super(ctx); + } + + @Override + public P setPrivateKey(L privateKey) { + Assert.notNull(privateKey, "PrivateKey argument cannot be null."); + final K publicKey = Assert.notNull(jwkContext.getKey(), "PublicKey cannot be null."); + return newPrivateBuilder(privateKey).setPublicKey(publicKey); + } + + protected abstract P newPrivateBuilder(L privateKey); + } + + private abstract static class DefaultPrivateJwkBuilder, M extends PrivateJwk, + T extends PrivateJwkBuilder> + extends AbstractAsymmetricJwkBuilder + implements PrivateJwkBuilder { + + DefaultPrivateJwkBuilder(JwkContext ctx) { + super(ctx); + } + + DefaultPrivateJwkBuilder(DefaultPublicJwkBuilder b, K key, Set privateNames) { + super(b, key, privateNames); + this.jwkContext.setPublicKey(b.jwkContext.getKey()); + } + + @Override + public T setPublicKey(L publicKey) { + this.jwkContext.setPublicKey(publicKey); + return tthis(); + } + } + + static class DefaultEcPublicJwkBuilder + extends DefaultPublicJwkBuilder + implements EcPublicJwkBuilder { + + DefaultEcPublicJwkBuilder(JwkContext src, ECPublicKey key) { + super(new DefaultJwkContext<>(DefaultEcPrivateJwk.PRIVATE_NAMES, src, key)); + } + + @Override + protected EcPrivateJwkBuilder newPrivateBuilder(ECPrivateKey key) { + return new DefaultEcPrivateJwkBuilder(this, key); + } + } + + static class DefaultRsaPublicJwkBuilder + extends DefaultPublicJwkBuilder + implements RsaPublicJwkBuilder { + + DefaultRsaPublicJwkBuilder(JwkContext ctx, RSAPublicKey key) { + super(new DefaultJwkContext<>(DefaultRsaPrivateJwk.PRIVATE_NAMES, ctx, key)); + } + + @Override + protected RsaPrivateJwkBuilder newPrivateBuilder(RSAPrivateKey key) { + return new DefaultRsaPrivateJwkBuilder(this, key); + } + } + + static class DefaultEcPrivateJwkBuilder + extends DefaultPrivateJwkBuilder + implements EcPrivateJwkBuilder { + + DefaultEcPrivateJwkBuilder(JwkContext src, ECPrivateKey key) { + super(new DefaultJwkContext<>(DefaultEcPrivateJwk.PRIVATE_NAMES, src, key)); + } + + DefaultEcPrivateJwkBuilder(DefaultEcPublicJwkBuilder b, ECPrivateKey key) { + super(b, key, DefaultEcPrivateJwk.PRIVATE_NAMES); + } + } + + static class DefaultRsaPrivateJwkBuilder + extends DefaultPrivateJwkBuilder + implements RsaPrivateJwkBuilder { + + DefaultRsaPrivateJwkBuilder(JwkContext src, RSAPrivateKey key) { + super(new DefaultJwkContext<>(DefaultRsaPrivateJwk.PRIVATE_NAMES, src, key)); + } + + DefaultRsaPrivateJwkBuilder(DefaultRsaPublicJwkBuilder b, RSAPrivateKey key) { + super(b, key, DefaultRsaPrivateJwk.PRIVATE_NAMES); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwk.java deleted file mode 100644 index ade7425a7..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwk.java +++ /dev/null @@ -1,65 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.CurveId; -import io.jsonwebtoken.security.CurveIds; -import io.jsonwebtoken.security.EcJwk; -import io.jsonwebtoken.security.MalformedKeyException; - -@SuppressWarnings("unchecked") -class AbstractEcJwk extends AbstractJwk implements EcJwk { - - static final String TYPE_VALUE = "EC"; - static final String CURVE_ID = "crv"; - static final String X = "x"; - static final String Y = "y"; - - AbstractEcJwk() { - super(TYPE_VALUE); - } - - @Override - public CurveId getCurveId() { - Object val = get(CURVE_ID); - if (val == null) { - return null; - } - if (val instanceof CurveId) { - return (CurveId) val; - } - if (val instanceof String) { - CurveId id = CurveIds.forValue((String) val); - setCurveId(id); //replace string with type safe value - return id; - } - throw new MalformedKeyException("EC JWK 'crv' value must be an CurveId or a String. Value has type: " + - val.getClass().getName()); - } - - @Override - public T setCurveId(CurveId curveId) { - return setRequiredValue(CURVE_ID, curveId, "curve id"); - } - - @Override - public String getX() { - return getString(X); - } - - @Override - public T setX(String x) { - return setRequiredValue(X, x, "x coordinate"); - } - - @Override - public String getY() { - return getString(Y); - } - - @Override - public T setY(String y) { - y = Strings.clean(y); - setValue(Y, y); - return (T) this; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkBuilder.java deleted file mode 100644 index e7588022e..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkBuilder.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.CurveId; -import io.jsonwebtoken.security.EcJwk; -import io.jsonwebtoken.security.EcJwkBuilder; - -@SuppressWarnings("unchecked") -abstract class AbstractEcJwkBuilder extends AbstractJwkBuilder implements EcJwkBuilder { - - AbstractEcJwkBuilder(JwkValidator validator) { - super(validator); - } - - @Override - public T setCurveId(CurveId curveId) { - this.jwk.setCurveId(curveId); - return (T) this; - } - - @Override - public T setX(String x) { - this.jwk.setX(x); - return (T) this; - } - - @Override - public T setY(String y) { - this.jwk.setY(y); - return (T) this; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java new file mode 100644 index 000000000..1825b5dee --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java @@ -0,0 +1,221 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.Key; +import java.security.KeyFactory; +import java.security.interfaces.ECKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.util.LinkedHashMap; +import java.util.Map; + +abstract class AbstractEcJwkFactory> extends AbstractFamilyJwkFactory { + + private static final BigInteger TWO = new BigInteger("2"); + private static final BigInteger THREE = new BigInteger("3"); + private static final Map EC_SPECS_BY_JWA_ID; + private static final Map JWA_IDS_BY_CURVE; + + private static ECParameterSpec getJcaParameterSpec(String jcaAlgorithmName) throws IllegalStateException { + try { + AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC"); + parameters.init(new ECGenParameterSpec(jcaAlgorithmName)); + return parameters.getParameterSpec(ECParameterSpec.class); + } catch (Exception e) { + String msg = "Unable to obtain JVM ECParameterSpec for JCA algorithm name '" + jcaAlgorithmName + "'."; + throw new IllegalStateException(msg, e); + } + } + + static { + EC_SPECS_BY_JWA_ID = new LinkedHashMap<>(); + JWA_IDS_BY_CURVE = new LinkedHashMap<>(); + + // P-256 <--> secp256r1 + // P-384 <--> secp384r1 + // P-521 <--> secp521r1 (yes, this is supposed to be 521 and not 512) + int[] sizes = new int[]{256, 384, 521}; + + for (int i : sizes) { + final String jwaId = "P-" + i; + final String jcaId = "secp" + i + "r1"; + ECParameterSpec spec = getJcaParameterSpec(jcaId); + EC_SPECS_BY_JWA_ID.put(jwaId, spec); + JWA_IDS_BY_CURVE.put(spec.getCurve(), jwaId); + } + } + + protected static ECParameterSpec getCurveByJwaId(String jwaCurveId) { + ECParameterSpec spec = EC_SPECS_BY_JWA_ID.get(jwaCurveId); + if (spec == null) { + String msg = "Unrecognized JWA curve id '" + jwaCurveId + "'"; + throw new UnsupportedKeyException(msg); + } + return spec; + } + + protected static String getJwaIdByCurve(EllipticCurve curve) { + String jwaCurveId = JWA_IDS_BY_CURVE.get(curve); + if (jwaCurveId == null) { + String msg = "The specified ECKey curve does not match a JWA standard curve id."; + throw new UnsupportedKeyException(msg); + } + return jwaCurveId; + } + + /** + * https://tools.ietf.org/html/rfc7518#section-6.2.1.2 indicates that this algorithm logic is defined in + * http://www.secg.org/sec1-v2.pdf Section 2.3.5. + * + * @param fieldSize EC field size + * @param coordinate EC point coordinate (e.g. x or y) + * @return A base64Url-encoded String representing the EC field per the RFC format + */ + // Algorithm defined in http://www.secg.org/sec1-v2.pdf Section 2.3.5 + static String toOctetString(int fieldSize, BigInteger coordinate) { + byte[] bytes = toUnsignedBytes(coordinate); + int mlen = (int) Math.ceil(fieldSize / 8d); + if (mlen > bytes.length) { + byte[] m = new byte[mlen]; + System.arraycopy(bytes, 0, m, mlen - bytes.length, bytes.length); + bytes = m; + } + return Encoders.BASE64URL.encode(bytes); + } + + /** + * Returns {@code true} if a given elliptic {@code curve} contains the specified {@code point}, {@code false} + * otherwise. Assumes elliptic curves over finite fields adhering to the reduced (a.k.a short or narrow) + * Weierstrass form: + *

    + * y2 = x3 + ax + b + *

    + * + * @param curve the Elliptic Curve to check + * @param point a point that may or may not be defined on the specified elliptic curve + * @return {@code true} if a given elliptic curve contains the specified {@code point}, {@code false} otherwise. + */ + static boolean contains(EllipticCurve curve, ECPoint point) { + final BigInteger a = curve.getA(); + final BigInteger b = curve.getB(); + final BigInteger x = point.getAffineX(); + final BigInteger y = point.getAffineY(); + + // The reduced Weierstrass form y^2 = x^3 + ax + b reflects an elliptic curve E over any field K (e.g. all real + // numbers or all complex numbers, etc). For computational simplicity, cryptographic (e.g. NIST) elliptic curves + // restrict K to be a field of integers modulo a prime number 'p'. As such, we apply modulo p (the field prime) + // to the equation to account for the restricted field. For a nice overview of the math behind EC curves and + // their application in cryptography, see + // https://web.northeastern.edu/dummit/docs/cryptography_5_elliptic_curves_in_cryptography.pdf + final BigInteger p = ((ECFieldFp) curve.getField()).getP(); + final BigInteger lhs = y.pow(2).mod(p); //mod p to account for field prime + final BigInteger rhs = x.pow(3).add(a.multiply(x)).add(b).mod(p); //mod p to account for field prime + + return lhs.equals(rhs); + } + + /** + * Multiply a point {@code p} by scalar {@code s} on the curve identified by {@code spec}. + * + * @param p the Elliptic Curve point to multiply + * @param s the scalar value to multiply + * @param spec the domain parameters that identify the Elliptic Curve containing point {@code p}. + */ + private static ECPoint multiply(ECPoint p, BigInteger s, ECParameterSpec spec) { + if (ECPoint.POINT_INFINITY.equals(p)) { + return p; + } + + EllipticCurve curve = spec.getCurve(); + BigInteger n = spec.getOrder(); + BigInteger k = s.mod(n); + + ECPoint r0 = ECPoint.POINT_INFINITY; + ECPoint r1 = p; + + // Montgomery Ladder implementation to mitigate side-channel attacks (i.e. an 'add' operation and a 'double' + // operation is calculated for every loop iteration, regardless if the 'add'' is needed or not) + // See: https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Montgomery_ladder + while (k.compareTo(BigInteger.ZERO) > 0) { + ECPoint temp = add(r0, r1, curve); + r0 = k.testBit(0) ? temp : r0; + r1 = doublePoint(r1, curve); + k = k.shiftRight(1); + } + + return r0; + } + + private static ECPoint add(ECPoint P, ECPoint Q, EllipticCurve curve) { + + if (ECPoint.POINT_INFINITY.equals(P)) { + return Q; + } else if (ECPoint.POINT_INFINITY.equals(Q)) { + return P; + } else if (P.equals(Q)) { + return doublePoint(P, curve); + } + + final BigInteger Px = P.getAffineX(); + final BigInteger Py = P.getAffineY(); + final BigInteger Qx = Q.getAffineX(); + final BigInteger Qy = Q.getAffineY(); + final BigInteger prime = ((ECFieldFp) curve.getField()).getP(); + final BigInteger slope = Qy.subtract(Py).multiply(Qx.subtract(Px).modInverse(prime)).mod(prime); + final BigInteger Rx = (slope.modPow(TWO, prime).subtract(Px)).subtract(Qx).mod(prime); + final BigInteger Ry = (Qy.negate().mod(prime)).add(slope.multiply(Qx.subtract(Rx))).mod(prime); + + return new ECPoint(Rx, Ry); + } + + private static ECPoint doublePoint(ECPoint P, EllipticCurve curve) { + + if (ECPoint.POINT_INFINITY.equals(P)) { + return P; + } + + final BigInteger Px = P.getAffineX(); + final BigInteger Py = P.getAffineY(); + final BigInteger p = ((ECFieldFp) curve.getField()).getP(); + final BigInteger a = curve.getA(); + final BigInteger s = ((Px.pow(2)).multiply(THREE).add(a)).multiply(Py.multiply(TWO).modInverse(p)); + final BigInteger x = s.pow(2).subtract(Px.multiply(TWO)).mod(p); + final BigInteger y = (Py.negate()).add(s.multiply(Px.subtract(x))).mod(p); + + return new ECPoint(x, y); + } + + AbstractEcJwkFactory(Class keyType) { + super(DefaultEcPublicJwk.TYPE_VALUE, keyType); + } + + protected ECPublicKey derivePublic(final JwkContext ctx) { + final ECPrivateKey key = ctx.getKey(); + final ECParameterSpec params = key.getParams(); + final ECPoint w = multiply(params.getGenerator(), key.getS(), params); + final ECPublicKeySpec spec = new ECPublicKeySpec(w, params); + return generateKey(ctx, ECPublicKey.class, new CheckedFunction() { + @Override + public ECPublicKey apply(KeyFactory kf) { + try { + return (ECPublicKey) kf.generatePublic(spec); + } catch (Exception e) { + String msg = "Unable to derive ECPublicKey from ECPrivateKey {" + ctx + "}."; + throw new UnsupportedKeyException(msg); + } + } + }); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkValidator.java deleted file mode 100644 index eeabbcdc1..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkValidator.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.CurveId; -import io.jsonwebtoken.security.CurveIds; -import io.jsonwebtoken.security.EcJwk; -import io.jsonwebtoken.security.KeyException; - -abstract class AbstractEcJwkValidator extends AbstractJwkValidator { - - AbstractEcJwkValidator() { - super(AbstractEcJwk.TYPE_VALUE); - } - - @Override - final void validateJwk(T jwk) throws KeyException { - - CurveId curveId = jwk.getCurveId(); - if (curveId == null) { // https://tools.ietf.org/html/rfc7518#section-6.2.1 - malformed("EC JWK curve id ('crv' property) must be specified."); - } - - String x = jwk.getX(); - if (!Strings.hasText(x)) { // https://tools.ietf.org/html/rfc7518#section-6.2.1 - malformed("EC JWK x coordinate ('x' property) must be specified."); - } - - // https://tools.ietf.org/html/rfc7518#section-6.2.1 (last sentence): - if (CurveIds.isStandard(curveId) && !Strings.hasText(jwk.getY())) { - malformed(curveId + " EC JWK y coordinate ('y' property) must be specified."); - } - - //TODO: RFC length validation for x and y values - - validateEcJwk(jwk); - } - - abstract void validateEcJwk(T jwk); -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithm.java deleted file mode 100644 index b2f3cd0fa..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithm.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.CryptoException; -import io.jsonwebtoken.security.CryptoRequest; -import io.jsonwebtoken.security.EncryptionAlgorithm; -import io.jsonwebtoken.security.EncryptionResult; - -import java.security.Key; - -abstract class AbstractEncryptionAlgorithm, - ERes extends EncryptionResult, DReq extends CryptoRequest> - extends CipherAlgorithm implements EncryptionAlgorithm { - - AbstractEncryptionAlgorithm(String name, String transformationString) { - super(name, transformationString); - } - - @Override - public ERes encrypt(EReq req) throws CryptoException { - try { - Assert.notNull(req, "Encryption request cannot be null."); - return doEncrypt(req); - } catch (CryptoException ce) { - throw ce; //propagate - } catch (Exception e) { - String msg = "Unable to perform " + getName() + " encryption: " + e.getMessage(); - throw new CryptoException(msg, e); - } - } - - protected abstract ERes doEncrypt(EReq req) throws Exception; - - @Override - public byte[] decrypt(DReq req) throws CryptoException { - try { - Assert.notNull(req, "Decryption request cannot be null."); - return doDecrypt(req); - } catch (CryptoException ce) { - throw ce; //propagate - } catch (Exception e) { - String msg = "Unable to perform " + getName() + " decryption: " + e.getMessage(); - throw new CryptoException(msg, e); - } - } - - protected abstract byte[] doDecrypt(DReq req) throws Exception; -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java new file mode 100644 index 000000000..9fe607c92 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java @@ -0,0 +1,114 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.Jwk; + +import java.math.BigInteger; +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +abstract class AbstractFamilyJwkFactory> implements FamilyJwkFactory { + + // Copied from Apache Commons Codec 1.14: + // https://github.com/apache/commons-codec/blob/af7b94750e2178b8437d9812b28e36ac87a455f2/src/main/java/org/apache/commons/codec/binary/Base64.java#L746-L775 + static byte[] toUnsignedBytes(BigInteger bigInt) { + Assert.notNull(bigInt, "BigInteger argument cannot be null."); + final int bitlen = bigInt.bitLength(); + // round bitlen + final int roundedBitlen = ((bitlen + 7) >> 3) << 3; + final byte[] bigBytes = bigInt.toByteArray(); + + if (((bitlen % 8) != 0) && (((bitlen / 8) + 1) == (roundedBitlen / 8))) { + return bigBytes; + } + // set up params for copying everything but sign bit + int startSrc = 0; + int len = bigBytes.length; + + // if bigInt is exactly byte-aligned, just skip signbit in copy + if ((bitlen % 8) == 0) { + startSrc = 1; + len--; + } + final int startDst = roundedBitlen / 8 - len; // to pad w/ nulls as per spec + final byte[] resizedBytes = new byte[roundedBitlen / 8]; + System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len); + return resizedBytes; + } + + protected static String encode(BigInteger bigInt) { + byte[] unsigned = toUnsignedBytes(bigInt); + return Encoders.BASE64URL.encode(unsigned); + } + + private final String ktyValue; + private final Class keyType; + + AbstractFamilyJwkFactory(String ktyValue, Class keyType) { + this.ktyValue = Assert.hasText(ktyValue, "keyType argument cannot be null or empty."); + this.keyType = Assert.notNull(keyType, "keyType class cannot be null."); + } + + @Override + public String getId() { + return this.ktyValue; + } + + @Override + public boolean supports(JwkContext ctx) { + return supportsKey(ctx.getKey()) || supportsKeyValues(ctx); + } + + protected boolean supportsKeyValues(JwkContext ctx) { + return this.ktyValue.equals(ctx.getType()); + } + + protected boolean supportsKey(Key key) { + return this.keyType.isInstance(key); + } + + protected K generateKey(final JwkContext ctx, final CheckedFunction fn) { + return generateKey(ctx, this.keyType, fn); + } + + protected T generateKey(final JwkContext ctx, final Class type, final CheckedFunction fn) { + return new JcaTemplate(getId(), ctx.getProvider()).execute(KeyFactory.class, new CheckedFunction() { + @Override + public T apply(KeyFactory instance) throws Exception { + try { + return fn.apply(instance); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + String msg = "Unable to create " + type.getSimpleName() + " from JWK {" + ctx + "}: " + e.getMessage(); + throw new InvalidKeyException(msg, e); + } + } + }); + } + + @Override + public final J createJwk(JwkContext ctx) { + Assert.notNull(ctx, "JwkContext argument cannot be null."); + if (!supports(ctx)) { //should be asserted by caller, but assert just in case: + String msg = "Unsupported JwkContext."; + throw new IllegalArgumentException(msg); + } + K key = ctx.getKey(); + if (key != null) { + ctx.setType(this.ktyValue); + return createJwkFromKey(ctx); + } else { + return createJwkFromValues(ctx); + } + } + + //when called, ctx.getKey() is guaranteed to be non-null + protected abstract J createJwkFromKey(JwkContext ctx); + + //when called ctx.getType() is guaranteed to equal this.ktyValue + protected abstract J createJwkFromValues(JwkContext ctx); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java index 309f6ef18..ffb9290a9 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java @@ -1,191 +1,134 @@ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.impl.JwtMap; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Collections; -import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.Jwk; -import io.jsonwebtoken.security.MalformedKeyException; -import java.lang.reflect.Array; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; +import java.security.Key; import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.List; +import java.util.Map; import java.util.Set; -@SuppressWarnings("unchecked") -abstract class AbstractJwk extends JwtMap implements Jwk { +abstract class AbstractJwk implements Jwk { static final String TYPE = "kty"; - static final String USE = "use"; static final String OPERATIONS = "key_ops"; static final String ALGORITHM = "alg"; static final String ID = "kid"; - static final String X509_URL = "x5u"; - static final String X509_CERT_CHAIN = "x5c"; - static final String X509_SHA1_THUMBPRINT = "x5t"; - static final String X509_SHA256_THUMBPRINT = "x5t#S256"; + static final String REDACTED_VALUE = ""; + public static final String IMMUTABLE_MSG = "JWKs are immutable may not be modified."; + protected final JwkContext context; - AbstractJwk(String type) { - type = Strings.clean(type); - Assert.notNull(type, "JWK type cannot be null or empty."); - put(TYPE, type); + AbstractJwk(JwkContext ctx) { + this.context = Assert.notNull(ctx, "JwkContext cannot be null."); + Assert.isTrue(!ctx.isEmpty(), "JwkContext cannot be empty."); + Assert.hasText(ctx.getType(), "JwkContext type cannot be null or empty."); + Assert.notNull(ctx.getKey(), "JwkContext key cannot be null."); } - T setRequiredValue(String key, Object value, String name) { - boolean reduceable = value != null && isReduceableToNull(value); - if (reduceable) { - value = null; - } - if (value == null) { - String msg = getType() + " JWK " + name + " ('" + key + "' property) cannot be null"; - if (reduceable) { - msg += " or empty"; - } - msg += "."; - throw new IllegalArgumentException(msg); - } - setValue(key, value); - return (T) this; + @Override + public String getType() { + return this.context.getType(); } - protected List getList(String name) { - Object value = get(name); - if (value == null) { - return null; - } - List list = new ArrayList<>(); - if (value instanceof Collection) { - Collection c = (Collection)value; - for (Object o : c) { - list.add(o == null ? null : String.valueOf(o)); - } - } else if (value.getClass().isArray()) { - int length = Array.getLength(value); - for (int i = 0; i < length; i ++) { - Object o = Array.get(value, i); - list.add(o == null ? null : String.valueOf(o)); - } - } - return list; + @Override + public Set getOperations() { + return this.context.getOperations(); } @Override - public String getType() { - return getString(TYPE); + public String getAlgorithm() { + return this.context.getAlgorithm(); } @Override - public String getUse() { - return getString(USE); + public String getId() { + return this.context.getId(); } @Override - public T setUse(String use) { - setValue(USE, Strings.clean(use)); - return (T)this; + public K toKey() { + return this.context.getKey(); } @Override - public Set getOperations() { - Object val = get(OPERATIONS); - if (val instanceof Set) { - return (Set)val; - } - List list = getList(OPERATIONS); - return val == null ? null : new LinkedHashSet<>(list); + public int size() { + return this.context.size(); } @Override - public T setOperations(Set ops) { - Set operations = Collections.isEmpty(ops) ? null : new LinkedHashSet<>(ops); - setValue(OPERATIONS, operations); - return (T)this; + public boolean isEmpty() { + return this.context.isEmpty(); } @Override - public String getAlgorithm() { - return getString(ALGORITHM); + public boolean containsKey(Object key) { + return this.context.containsKey(key); } @Override - public T setAlgorithm(String alg) { - setValue(ALGORITHM, Strings.clean(alg)); - return (T)this; + public boolean containsValue(Object value) { + return this.context.containsValue(value); } @Override - public String getId() { - return getString(ID); + public Object get(Object key) { + return this.context.get(key); } @Override - public T setId(String id) { - setValue(ID, Strings.clean(id)); - return (T)this; + public Set keySet() { + return this.context.keySet(); } @Override - public URI getX509Url() { - Object val = get(X509_URL); - if (val == null) { - return null; - } - if (val instanceof URI) { - return (URI)val; - } - String sval = String.valueOf(val); - URI uri; - try { - uri = new URI(sval); - setValue(X509_URL, uri); //replace with constructed instance - } catch (URISyntaxException e) { - String msg = getType() + " JWK x5u value cannot be converted to a URI instance: " + sval; - throw new MalformedKeyException(msg, e); - } - return uri; + public Collection values() { + return this.context.values(); } @Override - public T setX509Url(URI url) { - setValue(X509_URL, url); - return (T)this; + public Set> entrySet() { + return this.context.entrySet(); + } + + private static Object immutable() { + throw new UnsupportedOperationException(IMMUTABLE_MSG); + } + + @Override + public Object put(String s, Object o) { + return immutable(); } @Override - public List getX509CertficateChain() { - return getList(X509_CERT_CHAIN); + public Object remove(Object o) { + return immutable(); } @Override - public T setX509CertificateChain(List chain) { - chain = Collections.isEmpty(chain) ? null : new ArrayList<>(new LinkedHashSet<>(chain)); //guarantee no duplicate elements - setValue(X509_CERT_CHAIN, chain); - return (T)this; + public void putAll(Map m) { + immutable(); } @Override - public String getX509CertificateSha1Thumbprint() { - return getString(X509_SHA1_THUMBPRINT); + public void clear() { + immutable(); } @Override - public T setX509CertificateSha1Thumbprint(String thumbprint) { - setValue(X509_SHA1_THUMBPRINT, Strings.clean(thumbprint)); - return (T)this; + public String toString() { + return this.context.toString(); } @Override - public String getX509CertificateSha256Thumbprint() { - return getString(X509_SHA256_THUMBPRINT); + public int hashCode() { + return this.context.hashCode(); } @Override - public T setX509CertificateSha256Thumbprint(String thumbprint) { - setValue(X509_SHA256_THUMBPRINT, Strings.clean(thumbprint)); - return (T)this; + public boolean equals(Object obj) { + if (obj instanceof Map) { + return this.context.equals(obj); + } + return false; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java index 548b2b6b3..532cf6831 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java @@ -3,77 +3,94 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.Jwk; import io.jsonwebtoken.security.JwkBuilder; +import io.jsonwebtoken.security.SecretJwk; +import io.jsonwebtoken.security.SecretJwkBuilder; -import java.net.URI; -import java.util.List; +import javax.crypto.SecretKey; +import java.security.Key; +import java.security.Provider; +import java.util.Map; import java.util.Set; -@SuppressWarnings("unchecked") -abstract class AbstractJwkBuilder implements JwkBuilder { +abstract class AbstractJwkBuilder, T extends JwkBuilder> implements JwkBuilder { - protected final K jwk; + protected final JwkContext jwkContext; + protected final JwkFactory jwkFactory; - private final JwkValidator validator; - - AbstractJwkBuilder(JwkValidator validator) { - Assert.notNull(validator, "validator cannot be null."); - this.validator = validator; - this.jwk = newJwk(); - Assert.notNull(this.jwk, "newJwk implementation cannot return a null instance."); + @SuppressWarnings("unchecked") + protected AbstractJwkBuilder(JwkContext jwkContext) { + this.jwkContext = Assert.notNull(jwkContext, "JwkContext cannot be null."); + this.jwkFactory = (JwkFactory) DispatchingJwkFactory.DEFAULT_INSTANCE; } - abstract K newJwk(); - - public final K build() { - validator.validate(this.jwk); - return jwk; + @Override + public T setProvider(Provider provider) { + Assert.notNull(provider, "Provider cannot be null."); + jwkContext.setProvider(provider); + return tthis(); } @Override - public T setUse(String use) { - this.jwk.setUse(use); - return (T)this; + public T put(String name, Object value) { + jwkContext.put(name, value); + return tthis(); } @Override - public T setOperations(Set ops) { - this.jwk.setOperations(ops); - return (T)this; + public T putAll(Map values) { + jwkContext.putAll(values); + return tthis(); } @Override public T setAlgorithm(String alg) { - this.jwk.setAlgorithm(alg); - return (T)this; + Assert.hasText(alg, "Algorithm cannot be null or empty."); + jwkContext.setAlgorithm(alg); + return tthis(); } @Override public T setId(String id) { - this.jwk.setId(id); - return (T)this; + Assert.hasText(id, "Id cannot be null or empty."); + jwkContext.setId(id); + return tthis(); } @Override - public T setX509Url(URI url) { - this.jwk.setX509Url(url); - return (T)this; + public T setOperations(Set ops) { + Assert.notEmpty(ops, "Operations cannot be null or empty."); + jwkContext.setOperations(ops); + return tthis(); } - @Override - public T setX509CertificateChain(List chain) { - this.jwk.setX509CertificateChain(chain); - return (T)this; + @SuppressWarnings("unchecked") + protected final T tthis() { + return (T) this; } @Override - public T setX509CertificateSha1Thumbprint(String thumbprint) { - this.jwk.setX509CertificateSha1Thumbprint(thumbprint); - return (T)this; + public J build() { + + assert this.jwkContext != null; //should always exist as there isn't a way to set it outside the constructor + + K key = this.jwkContext.getKey(); + if (key == null && this.jwkContext.isEmpty()) { + String msg = "A " + Key.class.getName() + " or one or more name/value pairs must be provided to create a JWK."; + throw new IllegalStateException(msg); + } + try { + return jwkFactory.createJwk(this.jwkContext); + } catch (IllegalArgumentException iae) { + //if we get an IAE, it means the builder state wasn't configured enough in order to create + String msg = "Unable to create JWK: " + iae.getMessage(); + throw new IllegalStateException(msg, iae); + } } - @Override - public T setX509CertificateSha256Thumbprint(String thumbprint) { - this.jwk.setX509CertificateSha256Thumbprint(thumbprint); - return (T)this; + static class DefaultSecretJwkBuilder extends AbstractJwkBuilder + implements SecretJwkBuilder { + public DefaultSecretJwkBuilder(JwkContext ctx, SecretKey key) { + super(new DefaultJwkContext<>(DefaultSecretJwk.PRIVATE_NAMES, ctx, key)); + } } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkConverter.java deleted file mode 100644 index fd8fc71f0..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkConverter.java +++ /dev/null @@ -1,86 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.InvalidKeyException; -import io.jsonwebtoken.security.KeyException; -import io.jsonwebtoken.security.MalformedKeyException; - -import java.math.BigInteger; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.util.Map; - -abstract class AbstractJwkConverter implements JwkConverter { - - private static Map assertNotEmpty(Map m) { - if (m == null || m.isEmpty()) { - throw new InvalidKeyException("JWK map cannot be null or empty."); - } - return m; - } - - static void malformed(String msg) { - throw new MalformedKeyException(msg); - } - - static String getRequiredString(Map m, String name) { - assertNotEmpty(m); - Object value = m.get(name); - if (value == null) { - malformed("JWK is missing required case-sensitive '" + name + "' member."); - } - String s = String.valueOf(value); - if (!Strings.hasText(s)) { - malformed("JWK '" + name + "' member cannot be null or empty."); - } - return s; - } - - static BigInteger getRequiredBigInt(Map m, String name) { - String s = getRequiredString(m, name); - try { - byte[] bytes = Decoders.BASE64URL.decode(s); - return new BigInteger(bytes); - } catch (Exception e) { - String msg = "Unable to decode JWK member '" + name + "' to integer from value: " + s; - throw new MalformedKeyException(msg, e); - } - } - - // Copied from Apache Commons Codec 1.14: - // https://github.com/apache/commons-codec/blob/af7b94750e2178b8437d9812b28e36ac87a455f2/src/main/java/org/apache/commons/codec/binary/Base64.java#L746-L775 - static byte[] toUnsignedBytes(BigInteger bigInt) { - int bitlen = bigInt.bitLength(); - // round bitlen - bitlen = ((bitlen + 7) >> 3) << 3; - final byte[] bigBytes = bigInt.toByteArray(); - - if (((bigInt.bitLength() % 8) != 0) && (((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) { - return bigBytes; - } - // set up params for copying everything but sign bit - int startSrc = 0; - int len = bigBytes.length; - - // if bigInt is exactly byte-aligned, just skip signbit in copy - if ((bigInt.bitLength() % 8) == 0) { - startSrc = 1; - len--; - } - final int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec - final byte[] resizedBytes = new byte[bitlen / 8]; - System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len); - return resizedBytes; - } - - KeyFactory getKeyFactory(String alg) { - try { - return KeyFactory.getInstance(alg); - } catch (NoSuchAlgorithmException e) { - String msg = "Unable to obtain JCA KeyFactory instance for algorithm: " + alg; - throw new KeyException(msg, e); - } - } - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkValidator.java deleted file mode 100644 index e004ddf42..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkValidator.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.Jwk; -import io.jsonwebtoken.security.KeyException; -import io.jsonwebtoken.security.MalformedKeyException; - -abstract class AbstractJwkValidator implements JwkValidator { - - private final String TYPE_VALUE; - - AbstractJwkValidator(String kty) { - kty = Strings.clean(kty); - Assert.notNull(kty); - this.TYPE_VALUE = kty; - } - - static void malformed(String msg) throws MalformedKeyException { - throw new MalformedKeyException(msg); - } - - @Override - public final void validate(T jwk) throws KeyException { - - String type = jwk.getType(); - if (!Strings.hasText(type)) { - malformed("JWKs must have a key type ('kty') property value."); - } - - if (!TYPE_VALUE.equals(type)) { - malformed("JWK does not have expected key type ('kty') value of '" + - TYPE_VALUE + "'. Value found: " + type); - } - - validateJwk(jwk); - } - - abstract void validateJwk(T jwk) throws KeyException; -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java new file mode 100644 index 000000000..323ec2dbb --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java @@ -0,0 +1,34 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.PrivateJwk; +import io.jsonwebtoken.security.PublicJwk; + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; + +abstract class AbstractPrivateJwk> + extends AbstractAsymmetricJwk implements PrivateJwk { + + private final M publicJwk; + private final KeyPair keyPair; + + AbstractPrivateJwk(JwkContext ctx, M pubJwk) { + super(ctx); + this.publicJwk = Assert.notNull(pubJwk, "PublicJwk instance cannot be null."); + L publicKey = Assert.notNull(pubJwk.toKey(), "PublicJwk key instance cannot be null."); + this.context.setPublicKey(publicKey); + this.keyPair = new KeyPair(publicKey, toKey()); + } + + @Override + public M toPublicJwk() { + return this.publicJwk; + } + + @Override + public KeyPair toKeyPair() { + return this.keyPair; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPublicJwk.java new file mode 100644 index 000000000..1f3660377 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPublicJwk.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.PublicJwk; + +import java.security.PublicKey; + +abstract class AbstractPublicJwk extends AbstractAsymmetricJwk implements PublicJwk { + AbstractPublicJwk(JwkContext ctx) { + super(ctx); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwk.java deleted file mode 100644 index 960d13b0c..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwk.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.RsaJwk; - -@SuppressWarnings("unchecked") -public class AbstractRsaJwk extends AbstractJwk implements RsaJwk { - - static final String TYPE_VALUE = "RSA"; - static final String MODULUS = "n"; - static final String EXPONENT = "e"; - - AbstractRsaJwk() { - super(TYPE_VALUE); - } - - @Override - public String getModulus() { - return getString(MODULUS); - } - - @Override - public T setModulus(String value) { - return setRequiredValue(MODULUS, value, "modulus"); - } - - @Override - public String getExponent() { - return getString(EXPONENT); - } - - @Override - public T setExponent(String value) { - return setRequiredValue(EXPONENT, value, "exponent"); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwkValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwkValidator.java deleted file mode 100644 index f438a1804..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractRsaJwkValidator.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.KeyException; -import io.jsonwebtoken.security.RsaJwk; - -public class AbstractRsaJwkValidator extends AbstractJwkValidator { - - AbstractRsaJwkValidator() { - super(AbstractRsaJwk.TYPE_VALUE); - } - - @Override - void validateJwk(T jwk) throws KeyException { - - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java index 398089dfb..baeb1e274 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java @@ -1,81 +1,20 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.RuntimeEnvironment; -import io.jsonwebtoken.security.CryptoRequest; import io.jsonwebtoken.security.KeyException; +import io.jsonwebtoken.security.SecurityException; import io.jsonwebtoken.security.SignatureAlgorithm; import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.SignatureRequest; import io.jsonwebtoken.security.VerifySignatureRequest; -import java.security.InvalidAlgorithmParameterException; import java.security.Key; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.Provider; -import java.security.Signature; -import java.security.spec.AlgorithmParameterSpec; -abstract class AbstractSignatureAlgorithm extends CryptoAlgorithm implements SignatureAlgorithm { +abstract class AbstractSignatureAlgorithm extends CryptoAlgorithm implements SignatureAlgorithm { - AbstractSignatureAlgorithm(String name, String jcaName) { - super(name, jcaName); - } - - //visible for testing - protected boolean isBouncyCastleAvailable() { - return RuntimeEnvironment.BOUNCY_CASTLE_AVAILABLE; - } - - protected Signature createSignatureInstance(Provider provider, AlgorithmParameterSpec spec) { - - Signature sig; - try { - sig = getSignatureInstance(provider); - } catch (NoSuchAlgorithmException e) { - - String msg = "JWT signature algorithm '" + getName() + "' uses the JCA algorithm '" + getJcaName() + - "', which is not "; - - if (provider != null) { - msg += "supported by the specified JCA Provider {" + provider + "}. Try "; - } else { - msg += "available in the current JVM. Try "; - } - - if (!isBouncyCastleAvailable()) { - msg += "including BouncyCastle in the runtime classpath, or "; - } - - msg += "explicitly supplying a JCA Provider that supports the JCA algorithm name '" + getJcaName() + - "'. Cause: " + e.getMessage(); - - throw new SignatureException(msg, e); - } - - if (spec != null) { - try { - setParameter(sig, spec); - } catch (InvalidAlgorithmParameterException e) { - String msg = "Unsupported " + getJcaName() + " parameter {" + spec + "}: " + e.getMessage(); - throw new SignatureException(msg, e); - } - } - - return sig; - } - - //for testing overrides - protected Signature getSignatureInstance(Provider provider) throws NoSuchAlgorithmException { - final String jcaName = getJcaName(); - return provider != null ? - Signature.getInstance(jcaName, provider) : - Signature.getInstance(jcaName); - } - - //for testing overrides - protected void setParameter(Signature sig, AlgorithmParameterSpec spec) throws InvalidAlgorithmParameterException { - sig.setParameter(spec); + AbstractSignatureAlgorithm(String id, String jcaName) { + super(id, jcaName); } protected static String keyType(boolean signing) { @@ -85,44 +24,44 @@ protected static String keyType(boolean signing) { protected abstract void validateKey(Key key, boolean signing); @Override - public byte[] sign(CryptoRequest request) throws SignatureException, KeyException { - final Key key = Assert.notNull(request.getKey(), "Signature request key cannot be null."); - Assert.notEmpty(request.getData(), "Signature request data byte array cannot be null or empty."); + public byte[] sign(SignatureRequest request) throws SecurityException { + final SK key = Assert.notNull(request.getKey(), "Request key cannot be null."); + Assert.notEmpty(request.getPayload(), "Request payload cannot be null or empty."); try { validateKey(key, true); return doSign(request); } catch (SignatureException | KeyException e) { throw e; //propagate } catch (Exception e) { - String msg = "Unable to compute " + getName() + " signature with JCA algorithm '" + getJcaName() + "' " + + String msg = "Unable to compute " + getId() + " signature with JCA algorithm '" + getJcaName() + "' " + "using key {" + key + "}: " + e.getMessage(); throw new SignatureException(msg, e); } } - protected abstract byte[] doSign(CryptoRequest request) throws Exception; + protected abstract byte[] doSign(SignatureRequest request) throws Exception; @Override - public boolean verify(VerifySignatureRequest request) throws SignatureException, KeyException { - final Key key = Assert.notNull(request.getKey(), "Signature verification key cannot be null."); - Assert.notEmpty(request.getData(), "Signature verification data byte array cannot be null or empty."); - Assert.notEmpty(request.getSignature(), "Signature byte array cannot be null or empty."); + public boolean verify(VerifySignatureRequest request) throws SecurityException { + final VK key = Assert.notNull(request.getKey(), "Request key cannot be null."); + Assert.notEmpty(request.getPayload(), "Request payload cannot be null or empty."); + Assert.notEmpty(request.getDigest(), "Request signature byte array cannot be null or empty."); try { validateKey(key, false); return doVerify(request); } catch (SignatureException | KeyException e) { throw e; //propagate } catch (Exception e) { - String msg = "Unable to verify " + getName() + " signature with JCA algorithm '" + getJcaName() + "' " + + String msg = "Unable to verify " + getId() + " signature with JCA algorithm '" + getJcaName() + "' " + "using key {" + key + "}: " + e.getMessage(); throw new SignatureException(msg, e); } } - protected boolean doVerify(VerifySignatureRequest request) throws Exception { - byte[] providedSignature = request.getSignature(); + protected boolean doVerify(VerifySignatureRequest request) throws Exception { + byte[] providedSignature = request.getDigest(); Assert.notEmpty(providedSignature, "Request signature byte array cannot be null or empty."); - byte[] computedSignature = sign(request); + @SuppressWarnings("unchecked") byte[] computedSignature = sign((SignatureRequest)request); return MessageDigest.isEqual(providedSignature, computedSignature); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractTypedJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractTypedJwkConverter.java deleted file mode 100644 index b52258c50..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractTypedJwkConverter.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; - -import java.security.KeyFactory; -import java.util.HashMap; -import java.util.Map; - -abstract class AbstractTypedJwkConverter extends AbstractJwkConverter implements TypedJwkConverter { - - private final String keyType; - - AbstractTypedJwkConverter(String keyType) { - Assert.hasText(keyType, "keyType argument cannot be null or empty."); - this.keyType = keyType; - } - - @Override - public String getKeyType() { - return this.keyType; - } - - KeyFactory getKeyFactory() { - return getKeyFactory(getKeyType()); - } - - Map newJwkMap() { - Map m = new HashMap<>(); - m.put("kty", getKeyType()); - return m; - } - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java new file mode 100644 index 000000000..c2d52bbd7 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java @@ -0,0 +1,144 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AssociatedDataSupplier; +import io.jsonwebtoken.security.InitializationVectorSupplier; +import io.jsonwebtoken.security.KeySupplier; +import io.jsonwebtoken.security.SecretKeyGenerator; +import io.jsonwebtoken.security.SecurityRequest; +import io.jsonwebtoken.security.WeakKeyException; + +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.IvParameterSpec; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; + +import static io.jsonwebtoken.lang.Arrays.*; + + +abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerator { + + protected static final String KEY_ALG_NAME = "AES"; + protected static final int BLOCK_SIZE = 128; + protected static final int BLOCK_BYTE_SIZE = BLOCK_SIZE / Byte.SIZE; + protected static final int GCM_IV_SIZE = 96; // https://tools.ietf.org/html/rfc7518#section-5.3 + protected static final int GCM_IV_BYTE_SIZE = GCM_IV_SIZE / Byte.SIZE; + protected static final String DECRYPT_NO_IV = "This algorithm implementation rejects decryption " + + "requests that do not include initialization vectors. AES ciphertext without an IV is weak and " + + "susceptible to attack."; + + protected final int keyBitLength; + protected final int ivBitLength; + protected final int tagBitLength; + protected final boolean gcm; + + AesAlgorithm(String id, String jcaTransformation, int keyBitLength) { + super(id, jcaTransformation); + Assert.isTrue(keyBitLength == 128 || keyBitLength == 192 || keyBitLength == 256, "Invalid AES key length: it must equal 128, 192, or 256."); + this.keyBitLength = keyBitLength; + this.gcm = jcaTransformation.startsWith("AES/GCM"); + this.ivBitLength = jcaTransformation.equals("AESWrap") ? 0 : (this.gcm ? GCM_IV_SIZE : BLOCK_SIZE); + // https://tools.ietf.org/html/rfc7518#section-5.2.3 through https://tools.ietf.org/html/rfc7518#section-5.3 : + this.tagBitLength = this.gcm ? BLOCK_SIZE : this.keyBitLength; + } + + @Override + public SecretKey generateKey() { + return new JcaTemplate(KEY_ALG_NAME, null).generateSecretKey(this.keyBitLength); + //TODO: assert generated key length? + } + + protected SecretKey assertKey(KeySupplier request) { + SecretKey key = Assert.notNull(request.getKey(), "Request key cannot be null."); + validateLengthIfPossible(key); + return key; + } + + private void validateLengthIfPossible(SecretKey key) { + validateLength(key, this.keyBitLength, false); + } + + protected static String lengthMsg(String id, String type, int requiredLengthInBits, int actualLengthInBits) { + return "The '" + id + "' algorithm requires " + type + " with a length of " + + Bytes.bitsMsg(requiredLengthInBits) + ". The provided key has a length of " + + Bytes.bitsMsg(actualLengthInBits) + "."; + } + + protected byte[] validateLength(SecretKey key, int requiredBitLength, boolean propagate) { + byte[] keyBytes = null; + + try { + keyBytes = key.getEncoded(); + } catch (RuntimeException re) { + if (propagate) { + throw re; + } + //can't get the bytes to validate, e.g. hardware security module or later Android, so just return: + return keyBytes; + } + int keyBitLength = keyBytes.length * Byte.SIZE; + if (keyBitLength < requiredBitLength) { + throw new WeakKeyException(lengthMsg(getId(), "keys", requiredBitLength, keyBitLength)); + } + + return keyBytes; + } + + byte[] assertIvLength(final byte[] iv) { + int length = length(iv); + if ((this.ivBitLength / Byte.SIZE) != length) { + String msg = lengthMsg(getId(), "initialization vectors", this.ivBitLength, length * Byte.SIZE); + throw new IllegalArgumentException(msg); + } + return iv; + } + + byte[] assertTag(byte[] tag) { + int len = Arrays.length(tag) * Byte.SIZE; + if (this.tagBitLength != len) { + String msg = lengthMsg(getId(), "authentication tags", this.tagBitLength, len); + throw new IllegalArgumentException(msg); + } + return tag; + } + + byte[] assertDecryptionIv(InitializationVectorSupplier src) throws IllegalArgumentException { + byte[] iv = src.getInitializationVector(); + Assert.notEmpty(iv, DECRYPT_NO_IV); + return assertIvLength(iv); + } + + protected byte[] ensureInitializationVector(SecurityRequest request) { + byte[] iv = null; + if (request instanceof InitializationVectorSupplier) { + iv = Arrays.clean(((InitializationVectorSupplier) request).getInitializationVector()); + } + int ivByteLength = this.ivBitLength / Byte.SIZE; + if (iv == null || iv.length == 0) { + iv = new byte[ivByteLength]; + SecureRandom random = ensureSecureRandom(request); + random.nextBytes(iv); + } else { + assertIvLength(iv); + } + return iv; + } + + protected AlgorithmParameterSpec getIvSpec(byte[] iv) { + if (Arrays.length(iv) == 0) { + return null; + } + return this.gcm ? new GCMParameterSpec(BLOCK_SIZE, iv) : new IvParameterSpec(iv); + } + + protected byte[] getAAD(SecurityRequest request) { + byte[] aad = null; + if (request instanceof AssociatedDataSupplier) { + aad = Arrays.clean(((AssociatedDataSupplier) request).getAssociatedData()); + } + return aad; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java new file mode 100644 index 000000000..74c0afecc --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java @@ -0,0 +1,90 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.DecryptionKeyRequest; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.SecurityException; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.security.Key; +import java.security.spec.AlgorithmParameterSpec; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class AesGcmKeyAlgorithm extends AesAlgorithm implements KeyAlgorithm { + + public static final String TRANSFORMATION = "AES/GCM/NoPadding"; + + public AesGcmKeyAlgorithm(int keyLen) { + super("A" + keyLen + "GCMKW", TRANSFORMATION, keyLen); + } + + @Override + public KeyResult getEncryptionKey(final KeyRequest request) throws SecurityException { + + Assert.notNull(request, "request cannot be null."); + final SecretKey kek = assertKey(request); + AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); + final SecretKey cek = Assert.notNull(enc.generateKey(), "Request encryption algorithm cannot generate a null key."); + final byte[] iv = ensureInitializationVector(request); + final AlgorithmParameterSpec ivSpec = getIvSpec(iv); + + byte[] taggedCiphertext = execute(request, Cipher.class, new CheckedFunction() { + @Override + public byte[] apply(Cipher cipher) throws Exception { + cipher.init(Cipher.WRAP_MODE, kek, ivSpec); + return cipher.wrap(cek); + } + }); + + int tagByteLength = this.tagBitLength / Byte.SIZE; + // When using GCM mode, the JDK appends the authentication tag to the ciphertext, so let's extract it: + int ciphertextLength = taggedCiphertext.length - tagByteLength; + byte[] ciphertext = new byte[ciphertextLength]; + System.arraycopy(taggedCiphertext, 0, ciphertext, 0, ciphertextLength); + byte[] tag = new byte[tagByteLength]; + System.arraycopy(taggedCiphertext, ciphertextLength, tag, 0, tagByteLength); + + String encodedIv = Encoders.BASE64URL.encode(iv); + String encodedTag = Encoders.BASE64URL.encode(tag); + request.getHeader().put("iv", encodedIv); + request.getHeader().put("tag", encodedTag); + + return new DefaultKeyResult(cek, ciphertext); + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + Assert.notNull(request, "request cannot be null."); + final SecretKey kek = assertKey(request); + final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty."); + final JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); + final ValueGetter getter = new DefaultValueGetter(header); + final byte[] tag = getter.getRequiredBytes("tag", this.tagBitLength / Byte.SIZE); + final byte[] iv = getter.getRequiredBytes("iv", this.ivBitLength / Byte.SIZE); + final AlgorithmParameterSpec ivSpec = getIvSpec(iv); + + //for tagged GCM, the JCA spec requires that the tag be appended to the end of the ciphertext byte array: + final byte[] taggedCiphertext = Bytes.concat(cekBytes, tag); + + return execute(request, Cipher.class, new CheckedFunction() { + @Override + public SecretKey apply(Cipher cipher) throws Exception { + cipher.init(Cipher.UNWRAP_MODE, kek, ivSpec); + Key key = cipher.unwrap(taggedCiphertext, KEY_ALG_NAME, Cipher.SECRET_KEY); + Assert.state(key instanceof SecretKey, "cipher.unwrap must produce a SecretKey instance."); + return (SecretKey) key; + } + }); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java new file mode 100644 index 000000000..a70dfd1d5 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java @@ -0,0 +1,62 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.DecryptionKeyRequest; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.SecurityException; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class AesWrapKeyAlgorithm extends AesAlgorithm implements KeyAlgorithm { + + private static final String TRANSFORMATION = "AESWrap"; + + public AesWrapKeyAlgorithm(int keyLen) { + super("A" + keyLen + "KW", TRANSFORMATION, keyLen); + } + + @Override + public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + Assert.notNull(request, "request cannot be null."); + final SecretKey kek = assertKey(request); + AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); + final SecretKey cek = enc.generateKey(); + Assert.notNull(cek, "Request encryption algorithm cannot generate a null key."); + + byte[] ciphertext = execute(request, Cipher.class, new CheckedFunction() { + @Override + public byte[] apply(Cipher cipher) throws Exception { + cipher.init(Cipher.WRAP_MODE, kek); + return cipher.wrap(cek); + } + }); + + return new DefaultKeyResult(cek, ciphertext); + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + Assert.notNull(request, "request cannot be null."); + final SecretKey kek = assertKey(request); + final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Request encrypted key (request.getPayload()) cannot be null or empty."); + + return execute(request, Cipher.class, new CheckedFunction() { + @Override + public SecretKey apply(Cipher cipher) throws Exception { + cipher.init(Cipher.UNWRAP_MODE, kek); + Key key = cipher.unwrap(cekBytes, KEY_ALG_NAME, Cipher.SECRET_KEY); + Assert.state(key instanceof SecretKey, "Cipher unwrap must return a SecretKey instance."); + return (SecretKey) key; + } + }); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AsymmetricJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AsymmetricJwkFactory.java new file mode 100644 index 000000000..cb429353e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AsymmetricJwkFactory.java @@ -0,0 +1,39 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.Jwk; + +import java.security.Key; + +class AsymmetricJwkFactory implements FamilyJwkFactory> { + + private final String id; + private final FamilyJwkFactory> publicFactory; + private final FamilyJwkFactory> privateFactory; + + @SuppressWarnings({"unchecked", "rawtypes"}) + AsymmetricJwkFactory(FamilyJwkFactory publicFactory, FamilyJwkFactory privateFactory) { + this.publicFactory = (FamilyJwkFactory>) Assert.notNull(publicFactory, "publicFactory cannot be null."); + this.privateFactory = (FamilyJwkFactory>) Assert.notNull(privateFactory, "privateFactory cannot be null."); + this.id = Assert.notNull(publicFactory.getId(), "publicFactory id cannot be null or empty."); + Assert.isTrue(this.id.equals(privateFactory.getId()), "privateFactory id must equal publicFactory id"); + } + + @Override + public String getId() { + return this.id; + } + + @Override + public boolean supports(JwkContext ctx) { + return this.id.equals(ctx.getType()) || privateFactory.supports(ctx) || publicFactory.supports(ctx); + } + + @Override + public Jwk createJwk(JwkContext ctx) { + if (privateFactory.supports(ctx)) { + return this.privateFactory.createJwk(ctx); + } + return this.publicFactory.createJwk(ctx); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CipherAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CipherAlgorithm.java deleted file mode 100644 index 0276097ae..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/CipherAlgorithm.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.CryptoRequest; - -abstract class CipherAlgorithm extends CryptoAlgorithm { - - private final String transformation; - - CipherAlgorithm(String name, String transformation) { - super(name, transformation); - Assert.hasText(transformation, "Transformation string cannot be null or empty."); - this.transformation = transformation; - } - - CipherTemplate newCipherTemplate(CryptoRequest request) { - return new CipherTemplate(this.transformation, request != null ? request.getProvider() : null); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CipherCallback.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CipherCallback.java deleted file mode 100644 index 575d1318f..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/CipherCallback.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import javax.crypto.Cipher; - -public interface CipherCallback { - - T doWithCipher(Cipher cipher) throws Exception; -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CipherTemplate.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CipherTemplate.java deleted file mode 100644 index f1757fe87..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/CipherTemplate.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.CryptoException; - -import javax.crypto.Cipher; -import javax.crypto.NoSuchPaddingException; -import java.security.NoSuchAlgorithmException; -import java.security.Provider; - -class CipherTemplate { - - private final Provider provider; - - private final String transformation; - - CipherTemplate(String transformation, Provider provider) { - Assert.hasText(transformation, "Transformation string cannot be null or empty."); - this.transformation = transformation; - this.provider = provider; - } - - //for testing visibility - Cipher getCipherInstance(String transformation, Provider provider) - throws NoSuchPaddingException, NoSuchAlgorithmException { - return provider != null ? - Cipher.getInstance(transformation, provider) : - Cipher.getInstance(transformation); - } - - private Cipher newCipher() throws CryptoException { - try { - return getCipherInstance(transformation, provider); - } catch (Exception e) { - String msg = "Unable to obtain cipher from "; - if (provider != null) { - msg += "specified Provider {" + provider + "} "; - } else { - msg += "default JCA Provider "; - } - msg += "for transformation '" + transformation + "': " + e.getMessage(); - throw new CryptoException(msg, e); - } - } - - T execute(CipherCallback callback) throws CryptoException { - Cipher cipher = newCipher(); - try { - return callback.doWithCipher(cipher); - } catch (Exception e) { - throw new CryptoException("Cipher callback execution failed: " + e.getMessage(), e); - } - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java new file mode 100644 index 000000000..47e5b6117 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java @@ -0,0 +1,137 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.security.Key; +import java.security.MessageDigest; + +import static io.jsonwebtoken.impl.lang.Bytes.*; + +/** + * 'Clean room' implementation of the Concat KDF algorithm based solely on + * NIST.800-56A, + * Section 5.8.1.1. Call the {@link #deriveKey(SecretKey, long, byte[]) deriveKey} method. + */ +final class ConcatKDF extends CryptoAlgorithm { + + private static final long MAX_REP_COUNT = 0xFFFFFFFFL; + private static final long MAX_HASH_INPUT_BYTE_LENGTH = Integer.MAX_VALUE; //no Java byte arrays bigger than this + private static final long MAX_HASH_INPUT_BIT_LENGTH = MAX_HASH_INPUT_BYTE_LENGTH * Byte.SIZE; + + private final int hashBitLength; + private final long MAX_DERIVED_KEY_BIT_LENGTH; + + ConcatKDF(String jcaName) { + super("ConcatKDF", jcaName); + int hashByteLength = execute(MessageDigest.class, new CheckedFunction() { + @Override + public Integer apply(MessageDigest instance) { + return instance.getDigestLength(); + } + }); + this.hashBitLength = hashByteLength * Byte.SIZE; + assert this.hashBitLength > 0 : "MessageDigest length must be a positive value."; + MAX_DERIVED_KEY_BIT_LENGTH = this.hashBitLength * MAX_REP_COUNT; + } + + /** + * 'Clean room' implementation of the Concat KDF algorithm based solely on + * NIST.800-56A, + * Section 5.8.1.1. + * + * @param sharedSecretKey shared secret key to use to seed the derived secret. key.getEncoded() must not be empty. + * @param derivedKeyBitLength the total number of bits (not bytes) required in the returned derived + * key. + * @param OtherInfo any additional party info to be associated with the derived key. May be null/empty. + * @return the derived key + * @throws UnsupportedKeyException if unable to obtain {@code sharedSecretKey}'s + * {@link Key#getEncoded() encoded byte array}. + * @throws SecurityException if unable to perform the necessary {@link MessageDigest} computations to + * generate the derived key. + */ + public SecretKey deriveKey(SecretKey sharedSecretKey, final long derivedKeyBitLength, final byte[] OtherInfo) + throws UnsupportedKeyException, SecurityException { + + // OtherInfo argument assertions: + final int otherInfoByteLength = Arrays.length(OtherInfo); + + // sharedSecretKey argument assertions: + Assert.notNull(sharedSecretKey, "sharedSecretKey cannot be null."); + final byte[] Z = SecretJwkFactory.getRequiredEncoded(sharedSecretKey, + "use this key to create a Concat KDF derived key."); + + // derivedKeyBitLength argument assertions: + Assert.isTrue(derivedKeyBitLength > 0, "derivedKeyBitLength must be a positive number."); + final long derivedKeyByteLength = derivedKeyBitLength / Byte.SIZE; + if (derivedKeyByteLength > Integer.MAX_VALUE) { // Java byte arrays can't be bigger than this + throw new IllegalArgumentException("derivedKeyBitLength cannot reflect a byte array size greater than Integer.MAX_VALUE."); + } + // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf, Section 5.8.1.1, Input requirement #2: + if (derivedKeyBitLength > MAX_DERIVED_KEY_BIT_LENGTH) { + String msg = "derivedKeyBitLength for " + getJcaName() + "-derived keys may not exceed " + + bitsMsg(MAX_DERIVED_KEY_BIT_LENGTH) + ". Specified size: " + bitsMsg(derivedKeyBitLength) + "."; + throw new IllegalArgumentException(msg); + } + + // Section 5.8.1.1, Process step #1: + final double repsd = derivedKeyBitLength / (double) this.hashBitLength; + final long reps = (long) (Math.ceil(repsd)); + + // Section 5.8.1.1, Process step #2: + assert reps <= MAX_REP_COUNT : "derivedKeyBitLength is too large."; + + // Section 5.8.1.1, Process step #3: + final byte[] counter = new byte[]{0, 0, 0, 0, 0, 0, 0, 1}; // same as 0x01L, but no extra step to convert to byte[] + + // Section 5.8.1.1, Process step #4: + long inputBitLength = bitLength(counter) + bitLength(Z) + bitLength(OtherInfo); + assert inputBitLength <= MAX_HASH_INPUT_BIT_LENGTH : "Hash input is too large."; + + byte[] derivedKeyBytes = new JcaTemplate(getJcaName(), null).execute(MessageDigest.class, new CheckedFunction() { + @Override + public byte[] apply(MessageDigest md) throws Exception { + + final ByteArrayOutputStream stream = new ByteArrayOutputStream((int) derivedKeyByteLength); + long kLastIndex = reps - 1; + + // Section 5.8.1.1, Process step #5: + for (long i = 0; i < reps; i++) { + + md.update(counter); + md.update(Z); + if (otherInfoByteLength > 0) { + md.update(OtherInfo); + } + // Section 5.8.1.1, Process step #5.1: + byte[] Ki = md.digest(); + + // Section 5.8.1.1, Process step #5.2: + increment(counter); + + // Section 5.8.1.1, Process step #6: + if (i == kLastIndex && repsd != (double) reps) { //repsd calculation above didn't result in a whole number: + long leftmostBitLength = derivedKeyBitLength % hashBitLength; + int leftmostByteLength = (int) (leftmostBitLength / Byte.SIZE); + byte[] kLast = new byte[leftmostByteLength]; + System.arraycopy(Ki, 0, kLast, 0, kLast.length); + Ki = kLast; + } + + stream.write(Ki); + } + + // Section 5.8.1.1, Process step #7: + return stream.toByteArray(); + } + }); + + return new SecretKeySpec(derivedKeyBytes, "AES"); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ConstantKeyLocator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ConstantKeyLocator.java new file mode 100644 index 000000000..ccc5fa183 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ConstantKeyLocator.java @@ -0,0 +1,48 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.LocatorAdapter; +import io.jsonwebtoken.SigningKeyResolver; +import io.jsonwebtoken.impl.lang.Function; + +import java.security.Key; + +@SuppressWarnings("deprecation") +public class ConstantKeyLocator> extends LocatorAdapter implements SigningKeyResolver, Function { + + private final Key jwsKey; + private final Key jweKey; + + public ConstantKeyLocator(Key jwsKey, Key jweKey) { + this.jwsKey = jwsKey; + this.jweKey = jweKey; + } + + @Override + public Key resolveSigningKey(JwsHeader header, Claims claims) { + return locate(header); + } + + @Override + public Key resolveSigningKey(JwsHeader header, String plaintext) { + return locate(header); + } + + @Override + protected Key locate(JwsHeader header) { + return this.jwsKey; + } + + @Override + protected Key locate(JweHeader header) { + return this.jweKey; + } + + @Override + public Key apply(H header) { + return locate(header); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java index 8afc6776b..50ffe6731 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java @@ -1,35 +1,74 @@ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.Named; +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.CryptoRequest; +import io.jsonwebtoken.security.SecurityRequest; +import java.security.Provider; import java.security.SecureRandom; -abstract class CryptoAlgorithm implements Named { +abstract class CryptoAlgorithm implements Identifiable { - private final String name; + private final String ID; private final String jcaName; - CryptoAlgorithm(String name, String jcaName) { - Assert.hasText(name, "name cannot be null or empty."); - this.name = name; + CryptoAlgorithm(String id, String jcaName) { + Assert.hasText(id, "id cannot be null or empty."); + this.ID = id; Assert.hasText(jcaName, "jcaName cannot be null or empty."); this.jcaName = jcaName; } @Override - public String getName() { - return this.name; + public String getId() { + return this.ID; } String getJcaName() { return this.jcaName; } - SecureRandom ensureSecureRandom(CryptoRequest request) { - SecureRandom random = request.getSecureRandom(); + SecureRandom ensureSecureRandom(SecurityRequest request) { + SecureRandom random = request != null ? request.getSecureRandom() : null; return random != null ? random : Randoms.secureRandom(); } + + protected R execute(Class clazz, CheckedFunction fn) { + return new JcaTemplate(getJcaName(), null).execute(clazz, fn); + } + + protected T execute(SecurityRequest request, Class clazz, CheckedFunction fn) { + Assert.notNull(request, "request cannot be null."); + Provider provider = request.getProvider(); + SecureRandom random = ensureSecureRandom(request); + JcaTemplate template = new JcaTemplate(getJcaName(), provider, random); + return template.execute(clazz, fn); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof CryptoAlgorithm) { + CryptoAlgorithm other = (CryptoAlgorithm)obj; + return this.ID.equals(other.getId()) && this.jcaName.equals(other.getJcaName()); + } + return false; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 31 * hash + ID.hashCode(); + hash = 31 * hash + jcaName.hashCode(); + return hash; + } + + @Override + public String toString() { + return ID; + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResult.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResult.java deleted file mode 100644 index 434bee5c6..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResult.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2016 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.AeadIvEncryptionResult; - -/** - * @since JJWT_RELEASE_VERSION - */ -class DefaultAeadIvEncryptionResult extends DefaultIvEncryptionResult implements AeadIvEncryptionResult { - - private final byte[] tag; - - DefaultAeadIvEncryptionResult(byte[] ciphertext, byte[] iv, byte[] tag) { - super(ciphertext, iv); - this.tag = Assert.notEmpty(tag, "authentication tag cannot be null or empty."); - } - - @Override - public byte[] getAuthenticationTag() { - return this.tag; - } - - @Override - public byte[] compact() { - byte[] output = new byte[iv.length + ciphertext.length + tag.length]; - System.arraycopy(iv, 0, output, 0, iv.length); // iv first - System.arraycopy(ciphertext, 0, output, iv.length, ciphertext.length); // then ciphertext - System.arraycopy(tag, 0, output, iv.length + ciphertext.length, tag.length); // finally tag - return output; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvRequest.java deleted file mode 100644 index 0ecc0d885..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadIvRequest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2016 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.AeadIvRequest; - -import java.security.Key; -import java.security.Provider; -import java.security.SecureRandom; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class DefaultAeadIvRequest extends DefaultIvDecryptionRequest - implements AeadIvRequest { - - private final byte[] aad; - - private final byte[] tag; - - public DefaultAeadIvRequest(T data, K key, Provider provider, SecureRandom secureRandom, byte[] iv, byte[] aad, byte[] tag) { - super(data, key, provider, secureRandom, iv); - this.aad = aad; - this.tag = Assert.notEmpty(tag, "Authentication tag cannot be null or empty."); - } - - @Override - public byte[] getAssociatedData() { - return this.aad; - } - - @Override - public byte[] getAuthenticationTag() { - return this.tag; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadRequest.java new file mode 100644 index 000000000..586ea1e6c --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadRequest.java @@ -0,0 +1,39 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.AeadRequest; +import io.jsonwebtoken.security.InitializationVectorSupplier; + +import javax.crypto.SecretKey; +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultAeadRequest extends DefaultCryptoRequest implements AeadRequest, InitializationVectorSupplier { + + private final byte[] IV; + + private final byte[] AAD; + + DefaultAeadRequest(Provider provider, SecureRandom secureRandom, byte[] data, SecretKey key, byte[] aad, byte[] iv) { + super(provider, secureRandom, data, key); + this.AAD = aad; + this.IV = iv; + } + + public DefaultAeadRequest(Provider provider, SecureRandom secureRandom, byte[] data, SecretKey key, byte[] aad) { + this(provider, secureRandom, data, key, aad, null); + } + + public DefaultAeadRequest(byte[] data, SecretKey key, byte[] aad) { + this(null, null, data, key, aad, null); + } + + @Override + public byte[] getAssociatedData() { + return this.AAD; + } + + @Override + public byte[] getInitializationVector() { + return this.IV; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadResult.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadResult.java new file mode 100644 index 000000000..0965d52fa --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadResult.java @@ -0,0 +1,25 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadResult; +import io.jsonwebtoken.security.DecryptAeadRequest; + +import javax.crypto.SecretKey; +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultAeadResult extends DefaultAeadRequest implements AeadResult, DecryptAeadRequest { + + private final byte[] TAG; + + public DefaultAeadResult(Provider provider, SecureRandom secureRandom, byte[] data, SecretKey key, byte[] aad, byte[] tag, byte[] iv) { + super(provider, secureRandom, data, key, aad, iv); + Assert.notEmpty(iv, "initialization vector cannot be null or empty."); + this.TAG = Assert.notEmpty(tag, "authentication tag cannot be null or empty."); + } + + @Override + public byte[] getDigest() { + return this.TAG; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAesEncryptionRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAesEncryptionRequest.java deleted file mode 100644 index f6927653a..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAesEncryptionRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.AeadRequest; - -import javax.crypto.SecretKey; -import java.security.Provider; -import java.security.SecureRandom; - -public class DefaultAesEncryptionRequest extends DefaultCryptoRequest implements AeadRequest { - - private final byte[] aad; - - public DefaultAesEncryptionRequest(T data, SecretKey key, Provider provider, SecureRandom secureRandom, byte[] aad) { - super(data, key, provider, secureRandom); - this.aad = aad; - } - - public DefaultAesEncryptionRequest(T data, SecretKey key, byte[] aad) { - this(data, key, null, null, aad); - } - - @Override - public byte[] getAssociatedData() { - return this.aad; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoMessage.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoMessage.java deleted file mode 100644 index 0adfa0383..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoMessage.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.CryptoMessage; - -import java.security.Key; -import java.security.Provider; - -class DefaultCryptoMessage implements CryptoMessage { - - private final T data; - - DefaultCryptoMessage(T data) { - this.data = Assert.notNull(data, "data cannot be null."); - if (data instanceof byte[] && ((byte[]) data).length == 0) { - throw new IllegalArgumentException("data byte array cannot be empty."); - } - } - - @Override - public T getData() { - return data; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java index de0cbf763..f7e5f20fd 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java @@ -7,17 +7,17 @@ import java.security.Provider; import java.security.SecureRandom; -public class DefaultCryptoRequest extends DefaultCryptoMessage implements CryptoRequest { +public class DefaultCryptoRequest extends DefaultPayloadSupplier implements CryptoRequest{ private final Provider provider; private final SecureRandom secureRandom; private final K key; - public DefaultCryptoRequest(T data, K key, Provider provider, SecureRandom secureRandom) { - super(data); - this.key = Assert.notNull(key, "key cannot be null."); + public DefaultCryptoRequest(Provider provider, SecureRandom secureRandom, T payload, K key) { + super(payload); this.provider = provider; this.secureRandom = secureRandom; + this.key = Assert.notNull(key, "key cannot be null."); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultDecryptionKeyRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultDecryptionKeyRequest.java new file mode 100644 index 000000000..ff5748ad3 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultDecryptionKeyRequest.java @@ -0,0 +1,24 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.DecryptionKeyRequest; + +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultDecryptionKeyRequest extends DefaultKeyRequest implements DecryptionKeyRequest { + + private final byte[] payload; + + public DefaultDecryptionKeyRequest(Provider provider, SecureRandom secureRandom, K key, JweHeader header, AeadAlgorithm encryptionAlgorithm, byte[] payload) { + super(provider, secureRandom, key, header, encryptionAlgorithm); + this.payload = payload; + } + + @Override + public byte[] getPayload() { + return this.payload; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcJwkBuilderFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcJwkBuilderFactory.java deleted file mode 100644 index ca3c544e4..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcJwkBuilderFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.EcJwkBuilderFactory; -import io.jsonwebtoken.security.PrivateEcJwkBuilder; -import io.jsonwebtoken.security.PublicEcJwkBuilder; - -final class DefaultEcJwkBuilderFactory implements EcJwkBuilderFactory { - - @Override - public PublicEcJwkBuilder publicKey() { - return new DefaultPublicEcJwkBuilder(); - } - - @Override - public PrivateEcJwkBuilder privateKey() { - return new DefaultPrivateEcJwkBuilder(); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java new file mode 100644 index 000000000..47dfa008e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java @@ -0,0 +1,19 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.EcPrivateJwk; +import io.jsonwebtoken.security.EcPublicJwk; + +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.util.Set; + +class DefaultEcPrivateJwk extends AbstractPrivateJwk implements EcPrivateJwk { + + static final String D = "d"; + static final Set PRIVATE_NAMES = Collections.setOf(D); + + DefaultEcPrivateJwk(JwkContext ctx, EcPublicJwk pubJwk) { + super(ctx, pubJwk); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java new file mode 100644 index 000000000..2970b519c --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java @@ -0,0 +1,17 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.EcPublicJwk; + +import java.security.interfaces.ECPublicKey; + +class DefaultEcPublicJwk extends AbstractPublicJwk implements EcPublicJwk { + + static final String TYPE_VALUE = "EC"; + static final String CURVE_ID = "crv"; + static final String X = "x"; + static final String Y = "y"; + + DefaultEcPublicJwk(JwkContext ctx) { + super(ctx); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EllipticCurveSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java similarity index 71% rename from impl/src/main/java/io/jsonwebtoken/impl/security/EllipticCurveSignatureAlgorithm.java rename to impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java index c21604595..ee7e61b5f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EllipticCurveSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java @@ -1,10 +1,11 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.AsymmetricKeySignatureAlgorithm; -import io.jsonwebtoken.security.CryptoRequest; +import io.jsonwebtoken.security.EllipticCurveSignatureAlgorithm; import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.SignatureRequest; import io.jsonwebtoken.security.VerifySignatureRequest; import io.jsonwebtoken.security.WeakKeyException; @@ -17,8 +18,7 @@ import java.security.interfaces.ECKey; import java.security.spec.ECGenParameterSpec; -@SuppressWarnings("unused") //used via reflection in the io.jsonwebtoken.security.SignatureAlgorithms class -public class EllipticCurveSignatureAlgorithm extends AbstractSignatureAlgorithm implements AsymmetricKeySignatureAlgorithm { +public class DefaultEllipticCurveSignatureAlgorithm extends AbstractSignatureAlgorithm implements EllipticCurveSignatureAlgorithm { private static final String EC_PUBLIC_KEY_REQD_MSG = "Elliptic Curve signature validation requires an ECPublicKey instance."; @@ -31,7 +31,15 @@ public class EllipticCurveSignatureAlgorithm extends AbstractSignatureAlgorithm private final int signatureLength; - public EllipticCurveSignatureAlgorithm(String name, String jcaName, String curveName, int minKeyLength, int signatureLength) { + private static int shaSize(int keyBitLength) { + return keyBitLength == 521 ? 512 : keyBitLength; + } + + public DefaultEllipticCurveSignatureAlgorithm(int keyBitLength, int signatureLength) { + this("ES" + shaSize(keyBitLength), "SHA" + shaSize(keyBitLength) + "withECDSA", "secp" + keyBitLength + "r1", keyBitLength, signatureLength); + } + + public DefaultEllipticCurveSignatureAlgorithm(String name, String jcaName, String curveName, int minKeyLength, int signatureLength) { super(name, jcaName); Assert.hasText(curveName, "Curve name cannot be null or empty."); this.curveName = curveName; @@ -46,15 +54,15 @@ public EllipticCurveSignatureAlgorithm(String name, String jcaName, String curve @Override public KeyPair generateKeyPair() { - KeyPairGenerator keyGenerator; - try { - keyGenerator = KeyPairGenerator.getInstance("EC"); - ECGenParameterSpec spec = new ECGenParameterSpec(this.curveName); - keyGenerator.initialize(spec, Randoms.secureRandom()); - } catch (Exception e) { - throw new IllegalStateException("Unable to obtain an EllipticCurve KeyPairGenerator: " + e.getMessage(), e); - } - return keyGenerator.genKeyPair(); + final ECGenParameterSpec spec = new ECGenParameterSpec(this.curveName); + JcaTemplate template = new JcaTemplate("EC", null); + return template.execute(KeyPairGenerator.class, new CheckedFunction() { + @Override + public KeyPair apply(KeyPairGenerator generator) throws Exception { + generator.initialize(spec, Randoms.secureRandom()); + return generator.generateKeyPair(); + } + }); } @Override @@ -80,7 +88,7 @@ protected void validateKey(Key key, boolean signing) { } } - final String name = getName(); + final String name = getId(); ECKey ecKey = (ECKey) key; int size = ecKey.getParams().getOrder().bitLength(); if (size < this.minKeyLength) { @@ -96,29 +104,35 @@ protected void validateKey(Key key, boolean signing) { } @Override - protected byte[] doSign(CryptoRequest request) throws Exception { - PrivateKey privateKey = (PrivateKey) request.getKey(); - Signature sig = createSignatureInstance(request.getProvider(), null); - sig.initSign(privateKey); - sig.update(request.getData()); - return transcodeSignatureToConcat(sig.sign(), signatureLength); + protected byte[] doSign(final SignatureRequest request) { + return execute(request, Signature.class, new CheckedFunction() { + @Override + public byte[] apply(Signature sig) throws Exception { + sig.initSign(request.getKey()); + sig.update(request.getPayload()); + byte[] signature = sig.sign(); + return transcodeSignatureToConcat(signature, signatureLength); + } + }); } @Override - protected boolean doVerify(VerifySignatureRequest request) throws Exception { - final Key key = request.getKey(); - PublicKey publicKey = (PublicKey) key; - Signature sig = createSignatureInstance(request.getProvider(), null); - byte[] signature = request.getSignature(); - /* - * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature. - * This fallback is for backwards compatibility ONLY (to support tokens generated by previous versions of jjwt) - * and backwards compatibility will possibly be removed in a future version of this library. - */ - byte[] derSignature = this.signatureLength != signature.length && signature[0] == 0x30 ? signature : transcodeSignatureToDER(signature); - sig.initVerify(publicKey); - sig.update(request.getData()); - return sig.verify(derSignature); + protected boolean doVerify(final VerifySignatureRequest request) { + return execute(request, Signature.class, new CheckedFunction() { + @Override + public Boolean apply(Signature sig) throws Exception { + byte[] signature = request.getDigest(); + /* + * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature. + * This fallback is for backwards compatibility ONLY (to support tokens generated by previous versions of jjwt) + * and backwards compatibility will possibly be removed in a future version of this library. + */ + byte[] derSignature = signatureLength != signature.length && signature[0] == 0x30 ? signature : transcodeSignatureToDER(signature); + sig.initVerify(request.getKey()); + sig.update(request.getPayload()); + return sig.verify(derSignature); + } + }); } /** diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocator.java deleted file mode 100644 index 9c8668c25..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocator.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.JweHeader; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.UnsupportedJwtException; -import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.EncryptionAlgorithm; -import io.jsonwebtoken.security.EncryptionAlgorithmLocator; -import io.jsonwebtoken.security.EncryptionAlgorithms; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class DefaultEncryptionAlgorithmLocator implements EncryptionAlgorithmLocator { - - @Override - public EncryptionAlgorithm getEncryptionAlgorithm(JweHeader jweHeader) { - - String enc = Strings.clean(jweHeader.getEncryptionAlgorithm()); - //TODO: this check needs to be in the parser, to be enforced regardless of the locator implementation - if (enc == null) { - String msg = "JWE header does not contain an 'enc' header parameter. This header parameter is mandatory " + - "per the JWE Specification, Section 4.1.2. See " + - "https://tools.ietf.org/html/rfc7516#section-4.1.2 for more information."; - throw new MalformedJwtException(msg); - } - - try { - return EncryptionAlgorithms.forName(enc); //TODO: change to findByName and let the parser throw on null return. See below: - } catch (IllegalArgumentException e) { - //TODO: move this check to the parser - needs to be enforced if the locator returns null or throws a non-JWT exception - //couldn't find one: - String msg = "JWE 'enc' header parameter value of '" + enc + "' does not match a JWE standard algorithm " + - "identifier. If '" + enc + "' represents a custom algorithm, the JwtParser must be configured with " + - "a custom EncryptionAlgorithmLocator instance that knows how to return a compatible " + - "EncryptionAlgorithm instance. Otherwise, this JWE is invalid and may not be used safely."; - throw new UnsupportedJwtException(msg, e); - } - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionRequest.java deleted file mode 100644 index c897fb259..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionRequest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2016 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.AeadRequest; -import io.jsonwebtoken.security.AssociatedDataSource; -import io.jsonwebtoken.security.InitializationVectorSource; - -import java.security.Key; -import java.security.Provider; -import java.security.SecureRandom; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class DefaultEncryptionRequest extends DefaultCryptoRequest implements AeadRequest, InitializationVectorSource { - - private final byte[] iv; - - private final byte[] aad; - - public DefaultEncryptionRequest(T data, K key, Provider provider, SecureRandom secureRandom, byte[] iv, byte[] aad) { - super(data, key, provider, secureRandom); - this.iv = iv; - this.aad = aad; - } - - @Override - public byte[] getAssociatedData() { - return this.aad; - } - - @Override - public byte[] getInitializationVector() { - return this.iv; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionResult.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionResult.java deleted file mode 100644 index 8910b85a1..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEncryptionResult.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.EncryptionResult; - -class DefaultEncryptionResult implements EncryptionResult { - - protected final byte[] ciphertext; - - DefaultEncryptionResult(byte[] ciphertext) { - this.ciphertext = Assert.notEmpty(ciphertext, "ciphertext cannot be null or empty."); - } - - @Override - public byte[] getCiphertext() { - return this.ciphertext; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvDecryptionRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvDecryptionRequest.java deleted file mode 100644 index d230c7c49..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvDecryptionRequest.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2016 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.InitializationVectorSource; - -import java.security.Key; -import java.security.Provider; -import java.security.SecureRandom; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class DefaultIvDecryptionRequest extends DefaultCryptoRequest implements InitializationVectorSource { - - private final byte[] iv; - - public DefaultIvDecryptionRequest(T data, K key, Provider provider, SecureRandom secureRandom, byte[] iv) { - super(data, key, provider, secureRandom); - this.iv = Assert.notEmpty(iv, "Initialization Vector cannot be null or empty."); - } - - @Override - public byte[] getInitializationVector() { - return this.iv; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvEncryptionResult.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvEncryptionResult.java deleted file mode 100644 index b8b190305..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultIvEncryptionResult.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2016 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.IvEncryptionResult; - -/** - * @since JJWT_RELEASE_VERSION - */ -class DefaultIvEncryptionResult extends DefaultEncryptionResult implements IvEncryptionResult { - - protected final byte[] iv; - - DefaultIvEncryptionResult(byte[] ciphertext, byte[] iv) { - super(ciphertext); - this.iv = Assert.notEmpty(iv, "initialization vector cannot be null or empty."); - } - - @Override - public byte[] getInitializationVector() { - return this.iv; - } - - @Override - public byte[] compact() { - byte[] output = new byte[iv.length + ciphertext.length]; - System.arraycopy(iv, 0, output, 0, iv.length); // iv first - System.arraycopy(ciphertext, 0, output, iv.length, ciphertext.length); // then ciphertext - return output; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJweFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJweFactory.java deleted file mode 100644 index 052ad7719..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJweFactory.java +++ /dev/null @@ -1,123 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.impl.lang.Services; -import io.jsonwebtoken.io.Decoder; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.io.Deserializer; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.EncryptionAlgorithm; -import io.jsonwebtoken.security.EncryptionAlgorithms; - -import java.util.Map; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class DefaultJweFactory { - - private final Decoder base64UrlDecoder; - - private final Deserializer> deserializer; - - private final EncryptionAlgorithm encryptionAlgorithm; - - private static Deserializer> loadDeserializer() { - Deserializer deserializer = Services.loadFirst(Deserializer.class); - //noinspection unchecked - return (Deserializer>) deserializer; - } - - public DefaultJweFactory() { - this(Decoders.BASE64URL, loadDeserializer(), EncryptionAlgorithms.A256GCM); - } - - public DefaultJweFactory(Decoder base64UrlDecoder, - Deserializer> deserializer, - EncryptionAlgorithm encryptionAlgorithm) { - this.base64UrlDecoder = Assert.notNull(base64UrlDecoder, "Base64Url TextCodec cannot be null."); - this.deserializer = Assert.notNull(deserializer, "Deserializer cannot be null."); - this.encryptionAlgorithm = Assert.notNull(encryptionAlgorithm, "EncryptionAlgorithm cannot be null."); - } - - /* - - public Jwe createJwe(String base64UrlProtectedHeader, String base64UrlEncryptedKey, String base64UrlIv, - String base64UrlCiphertext, String base64UrlAuthenticationTag) { - - // ==================================================================== - // https://tools.ietf.org/html/rfc7516#section-5.2 #2 - // ==================================================================== - - final byte[] headerBytes = base64UrlDecode(base64UrlProtectedHeader, "Protected Header"); - - // encrypted key can be null with Direct Key or Direct Key Agreement - // https://tools.ietf.org/html/rfc7516#section-5.2 - // so we use a 'null safe' variant: - final byte[] encryptedKeyBytes = nullSafeBase64UrlDecode(base64UrlEncryptedKey, "Encrypted Key"); - - final byte[] iv = base64UrlDecode(base64UrlIv, "Initialization Vector"); - - final byte[] ciphertext = base64UrlDecode(base64UrlCiphertext, "Ciphertext"); - - final byte[] authcTag = base64UrlDecode(base64UrlAuthenticationTag, "Authentication Tag"); - - // ==================================================================== - // https://tools.ietf.org/html/rfc7516#section-5.2 #3 - // ==================================================================== - - Map protectedHeader; - try { - protectedHeader = parseJson(headerBytes); - } catch (Exception e) { - String msg = "JWE Protected Header must be a valid JSON object."; - throw new IllegalArgumentException(msg, e); - } - Assert.notEmpty(protectedHeader, "JWE Protected Header cannot be a null or empty JSON object."); - - DefaultJweHeader header = new DefaultJweHeader(protectedHeader); - - // ==================================================================== - // https://tools.ietf.org/html/rfc7516#section-5.2 #4 - // ==================================================================== - - // we currently don't support JSON serialization (just compact), so we can skip #4 - - // ==================================================================== - // https://tools.ietf.org/html/rfc7516#section-5.2 #11 and #12 - // ==================================================================== - - - throw new UnsupportedOperationException("Not yet finished."); - - } - - protected byte[] nullSafeBase64UrlDecode(String base64UrlEncoded, String jweName) { - if (base64UrlEncoded == null) { - return null; - } - return base64UrlDecode(base64UrlEncoded, jweName); - } - - protected byte[] base64UrlDecode(String base64UrlEncoded, String jweName) { - - if (base64UrlEncoded == null) { - String msg = "Invalid compact JWE: base64url JWE " + jweName + " is missing."; - throw new IllegalArgumentException(msg); - } - - try { - return base64UrlDecoder.decode(base64UrlEncoded); - } catch (Exception e) { - String msg = "Invalid compact JWE: JWE " + jweName + - " fragment is invalid and cannot be Base64Url-decoded: " + base64UrlEncoded; - throw new IllegalArgumentException(msg, e); - } - } - - @SuppressWarnings("unchecked") - protected Map parseJson(byte[] json) { - return deserializer.deserialize(json); - } - - */ -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkBuilderFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkBuilderFactory.java deleted file mode 100644 index 2a55f6062..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkBuilderFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.EcJwkBuilderFactory; -import io.jsonwebtoken.security.JwkBuilderFactory; -import io.jsonwebtoken.security.SymmetricJwkBuilder; - -public final class DefaultJwkBuilderFactory implements JwkBuilderFactory { - - @Override - public EcJwkBuilderFactory ellipticCurve() { - return new DefaultEcJwkBuilderFactory(); - } - - @Override - public SymmetricJwkBuilder symmetric() { - return new DefaultSymmetricJwkBuilder(); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java new file mode 100644 index 000000000..9c3f7260e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java @@ -0,0 +1,422 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.impl.JwtMap; +import io.jsonwebtoken.impl.io.CodecConverter; +import io.jsonwebtoken.impl.lang.BiFunction; +import io.jsonwebtoken.impl.lang.Converter; +import io.jsonwebtoken.impl.lang.Converters; +import io.jsonwebtoken.impl.lang.NullSafeConverter; +import io.jsonwebtoken.impl.lang.UriStringConverter; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.MalformedKeyException; + +import java.net.URI; +import java.security.Key; +import java.security.Provider; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class DefaultJwkContext implements JwkContext { + + private static final Converter THUMBPRINT_CONVERTER = + Converters.forEncoded(byte[].class, CodecConverter.BASE64URL); + + private static final Converter X509_CONVERTER = + Converters.forEncoded(X509Certificate.class, new JwkX509StringConverter()); + + private static final Converter URI_CONVERTER = + Converters.forEncoded(URI.class, new UriStringConverter()); + + private static final Set DEFAULT_PRIVATE_NAMES; + private static final Map> SETTERS; + + static { + Set set = new LinkedHashSet<>(); + set.addAll(DefaultRsaPrivateJwk.PRIVATE_NAMES); + set.addAll(DefaultEcPrivateJwk.PRIVATE_NAMES); + set.addAll(DefaultSecretJwk.PRIVATE_NAMES); + DEFAULT_PRIVATE_NAMES = java.util.Collections.unmodifiableSet(set); + + @SuppressWarnings("RedundantTypeArguments") + List> fns = Collections.>of( + Canonicalizer.forKey(AbstractJwk.ALGORITHM, "Algorithm"), + Canonicalizer.forKey(AbstractJwk.ID, "Key ID"), + Canonicalizer.forKey(AbstractJwk.OPERATIONS, "Key Operations", Converters.forSetOf(String.class)), + Canonicalizer.forKey(AbstractAsymmetricJwk.PUBLIC_KEY_USE, "Public Key Use"), + Canonicalizer.forKey(AbstractJwk.TYPE, "Key Type"), + Canonicalizer.forKey(AbstractAsymmetricJwk.X509_CERT_CHAIN, "X.509 Certificate Chain", Converters.forList(X509_CONVERTER)), + Canonicalizer.forKey(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT, "X.509 Certificate SHA-1 Thumbprint", THUMBPRINT_CONVERTER), + Canonicalizer.forKey(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT, "X.509 Certificate SHA-256 Thumbprint", THUMBPRINT_CONVERTER), + Canonicalizer.forKey(AbstractAsymmetricJwk.X509_URL, "X.509 URL", URI_CONVERTER) + ); + Map> s = new LinkedHashMap<>(); + for (Canonicalizer fn : fns) { + s.put(fn.getId(), fn); + } + SETTERS = java.util.Collections.unmodifiableMap(s); + } + + private final Map values; // canonical values formatted per RFC requirements + private final Map idiomaticValues; // the values map with any string/encoded values converted to Java type-safe values where possible + private final Map redactedValues; // the values map with any sensitive/secret values redacted. Used in the toString implementation. + private final Set privateMemberNames; // names of values that should be redacted for toString output + private K key; + private PublicKey publicKey; + private Provider provider; + + public DefaultJwkContext() { + // For the default constructor case, we don't know how it will be used or what values will be populated, + // so we can't know ahead of time what the sensitive data is. As such, for security reasons, we assume all + // the known private names for all supported algorithms in case it is used for any of them: + this(DEFAULT_PRIVATE_NAMES); + } + + public DefaultJwkContext(Set privateMemberNames) { + this.privateMemberNames = Assert.notEmpty(privateMemberNames, "privateMemberNames cannot be null or empty."); + this.values = new LinkedHashMap<>(); + this.idiomaticValues = new LinkedHashMap<>(); + this.redactedValues = new LinkedHashMap<>(); + } + + public DefaultJwkContext(Set privateMemberNames, K key) { + this(privateMemberNames); + this.key = Assert.notNull(key, "Key cannot be null."); + } + + public DefaultJwkContext(Set privateMemberNames, JwkContext other) { + this(privateMemberNames, other, true); + } + + public DefaultJwkContext(Set privateMemberNames, JwkContext other, K key) { + //if the key is null or a PublicKey, we don't want to redact - we want to fully remove the items that are + //private names (public JWKs should never contain any private key fields, even if redacted): + this(privateMemberNames, other, (key == null || key instanceof PublicKey)); + this.key = Assert.notNull(key, "Key cannot be null."); + } + + private DefaultJwkContext(Set privateMemberNames, JwkContext other, boolean removePrivate) { + this.privateMemberNames = Assert.notEmpty(privateMemberNames, "privateMemberNames cannot be null or empty."); + Assert.notNull(other, "JwkContext cannot be null."); + Assert.isInstanceOf(DefaultJwkContext.class, other, "JwkContext must be a DefaultJwkContext instance."); + DefaultJwkContext src = (DefaultJwkContext) other; + this.provider = other.getProvider(); + this.values = new LinkedHashMap<>(src.values); + this.idiomaticValues = new LinkedHashMap<>(src.idiomaticValues); + this.redactedValues = new LinkedHashMap<>(src.redactedValues); + if (removePrivate) { + for (String name : this.privateMemberNames) { + remove(name); + } + } + } + + protected Object nullSafePut(String name, Object value) { + if (JwtMap.isReduceableToNull(value)) { + return remove(name); + } else { + Object redactedValue = this.privateMemberNames.contains(name) ? AbstractJwk.REDACTED_VALUE : value; + this.redactedValues.put(name, redactedValue); + this.idiomaticValues.put(name, value); + return this.values.put(name, value); + } + } + + @Override + public Object put(String name, Object value) { + name = Assert.notNull(Strings.clean(name), "JWK member name cannot be null or empty."); + if (value instanceof String) { + value = Strings.clean((String) value); + } else if (Objects.isArray(value) && !value.getClass().getComponentType().isPrimitive()) { + value = Collections.arrayToList(value); + } + return idiomaticPut(name, value); + } + + // ensures that if a property name matches an RFC-specified name, that value can be represented + // as an idiomatic type-safe Java value in addition to the canonical RFC/encoded value. + private Object idiomaticPut(String name, Object value) { + assert name != null; //asserted by caller. + Canonicalizer fn = SETTERS.get(name); + if (fn != null) { //Setting a JWA-standard property - let's ensure we can represent it idiomatically: + return fn.apply(this, value); + } else { //non-standard/custom property: + return nullSafePut(name, value); + } + } + + @Override + public void putAll(Map m) { + Assert.notEmpty(m, "JWK values cannot be null or empty."); + for (Map.Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public Object remove(Object key) { + this.redactedValues.remove(key); + this.idiomaticValues.remove(key); + return this.values.remove(key); + } + + @Override + public int size() { + return this.values.size(); + } + + @Override + public boolean isEmpty() { + return this.values.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.values.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return this.values.containsValue(value); + } + + @Override + public Object get(Object key) { + return this.values.get(key); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Cannot clear JwkContext objects."); + } + + @Override + public Set keySet() { + return this.values.keySet(); + } + + @Override + public Collection values() { + return this.values.values(); + } + + @Override + public Set> entrySet() { + return this.values.entrySet(); + } + + @Override + public String getAlgorithm() { + return (String) this.values.get(AbstractJwk.ALGORITHM); + } + + @Override + public JwkContext setAlgorithm(String algorithm) { + put(AbstractJwk.ALGORITHM, algorithm); + return this; + } + + @Override + public String getId() { + return (String) this.values.get(AbstractJwk.ID); + } + + @Override + public JwkContext setId(String id) { + put(AbstractJwk.ID, id); + return this; + } + + @Override + public Set getOperations() { + //noinspection unchecked + return (Set) this.idiomaticValues.get(AbstractJwk.OPERATIONS); + } + + @Override + public JwkContext setOperations(Set ops) { + put(AbstractJwk.OPERATIONS, ops); + return this; + } + + @Override + public String getType() { + return (String) this.values.get(AbstractJwk.TYPE); + } + + @Override + public JwkContext setType(String type) { + put(AbstractJwk.TYPE, type); + return this; + } + + @Override + public String getPublicKeyUse() { + return (String) this.values.get(AbstractAsymmetricJwk.PUBLIC_KEY_USE); + } + + @Override + public JwkContext setPublicKeyUse(String use) { + put(AbstractAsymmetricJwk.PUBLIC_KEY_USE, use); + return this; + } + + @Override + public List getX509CertificateChain() { + //noinspection unchecked + return (List) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_CERT_CHAIN); + } + + @Override + public JwkContext setX509CertificateChain(List x5c) { + put(AbstractAsymmetricJwk.X509_CERT_CHAIN, x5c); + return this; + } + + @Override + public byte[] getX509CertificateSha1Thumbprint() { + return (byte[]) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT); + } + + @Override + public JwkContext setX509CertificateSha1Thumbprint(byte[] x5t) { + put(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT, x5t); + return this; + } + + @Override + public byte[] getX509CertificateSha256Thumbprint() { + return (byte[]) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT); + } + + @Override + public JwkContext setX509CertificateSha256Thumbprint(byte[] x5ts256) { + put(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT, x5ts256); + return this; + } + + @Override + public URI getX509Url() { + return (URI) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_URL); + } + + @Override + public JwkContext setX509Url(URI url) { + put(AbstractAsymmetricJwk.X509_URL, url); + return this; + } + + @Override + public K getKey() { + return this.key; + } + + @Override + public JwkContext setKey(K key) { + this.key = key; + return this; + } + + @Override + public PublicKey getPublicKey() { + return this.publicKey; + } + + @Override + public JwkContext setPublicKey(PublicKey publicKey) { + this.publicKey = publicKey; + return this; + } + + @Override + public Provider getProvider() { + return this.provider; + } + + @Override + public JwkContext setProvider(Provider provider) { + this.provider = provider; + return this; + } + + @Override + public Set getPrivateMemberNames() { + return this.privateMemberNames; + } + + @Override + public int hashCode() { + return this.values.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Map) { + return this.values.equals(obj); + } + return false; + } + + @Override + public String toString() { + return this.redactedValues.toString(); + } + + private static class Canonicalizer implements BiFunction, Object, T>, Identifiable { + + private final String id; + private final String title; + private final Converter converter; + + public static Canonicalizer forKey(String id, String title) { + return forKey(id, title, Converters.none(String.class)); + } + + public static Canonicalizer forKey(String id, String title, Converter converter) { + return new Canonicalizer<>(id, title, new NullSafeConverter<>(converter)); + } + + public Canonicalizer(String id, String title, Converter converter) { + this.id = id; + this.title = title; + this.converter = converter; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public T apply(DefaultJwkContext ctx, Object rawValue) { + + if (JwtMap.isReduceableToNull(rawValue)) { + ctx.remove(id); + return null; + } + + T idiomaticValue; // preferred Java format + Object canonicalValue; //as required by the RFC + try { + idiomaticValue = converter.applyFrom(rawValue); + canonicalValue = converter.applyTo(idiomaticValue); + } catch (Exception e) { + String msg = "Invalid JWK '" + id + "' (" + title + ") value [" + rawValue + "]: " + e.getMessage(); + throw new MalformedKeyException(msg, e); + } + ctx.nullSafePut(id, canonicalValue); + ctx.idiomaticValues.put(id, idiomaticValue); + //noinspection unchecked + return (T) canonicalValue; + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkConverter.java deleted file mode 100644 index 52e963ec6..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkConverter.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Collections; -import io.jsonwebtoken.security.UnsupportedKeyException; - -import java.security.Key; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class DefaultJwkConverter extends AbstractJwkConverter { - - private final Map converters = new HashMap<>(); - - public DefaultJwkConverter() { - this(Collections.of( - new SymmetricJwkConverter(), - new EcJwkConverter(), - new RsaJwkConverter())); - } - - public DefaultJwkConverter(List converters) { - Assert.notEmpty(converters, "Converters cannot be null or empty."); - for(TypedJwkConverter converter : converters) { - this.converters.put(converter.getKeyType(), converter); - } - } - - private JwkConverter getConverter(String kty) { - JwkConverter converter = converters.get(kty); - if (converter == null) { - String msg = "Unrecognized JWK kty (key type) value: " + kty; - throw new UnsupportedKeyException(msg); - } - return converter; - } - - @Override - public Key toKey(Map jwk) { - String type = getRequiredString(jwk, "kty"); - JwkConverter converter = getConverter(type); - return converter.toKey(jwk); - } - - @Override - public Map toJwk(Key key) { - Assert.notNull(key, "Key argument cannot be null."); - for(TypedJwkConverter converter : converters.values()) { - if (converter.supports(key)) { - return converter.toJwk(key); - } - } - - String msg = "Unable to determine JWK converter for key of type " + key.getClass(); - throw new UnsupportedKeyException(msg); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyRequest.java new file mode 100644 index 000000000..57a40d912 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyRequest.java @@ -0,0 +1,32 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.KeyRequest; + +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultKeyRequest extends DefaultKeyedRequest implements KeyRequest { + + private final JweHeader header; + private final AeadAlgorithm encryptionAlgorithm; + + public DefaultKeyRequest(Provider provider, SecureRandom secureRandom, K key, JweHeader header, AeadAlgorithm encryptionAlgorithm) { + super(provider, secureRandom, key); + this.header = Assert.notNull(header, "JweHeader cannot be null."); + this.encryptionAlgorithm = Assert.notNull(encryptionAlgorithm, "AeadAlgorithm argument cannot be null."); + } + + @Override + public JweHeader getHeader() { + return this.header; + } + + @Override + public AeadAlgorithm getEncryptionAlgorithm() { + return this.encryptionAlgorithm; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyResult.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyResult.java new file mode 100644 index 000000000..609520e4b --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyResult.java @@ -0,0 +1,32 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.KeyResult; + +import javax.crypto.SecretKey; + +public class DefaultKeyResult implements KeyResult { + + private final byte[] payload; + private final SecretKey key; + + public DefaultKeyResult(SecretKey key) { + this(key, Bytes.EMPTY); + } + + public DefaultKeyResult(SecretKey key, byte[] encryptedKey) { + this.payload = Assert.notNull(encryptedKey, "encryptedKey cannot be null (but can be empty)."); + this.key = Assert.notNull(key, "Key argument cannot be null."); + } + + @Override + public byte[] getPayload() { + return this.payload; + } + + @Override + public SecretKey getKey() { + return this.key; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyUseStrategy.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyUseStrategy.java new file mode 100644 index 000000000..2b7263796 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyUseStrategy.java @@ -0,0 +1,31 @@ +package io.jsonwebtoken.impl.security; + +public class DefaultKeyUseStrategy implements KeyUseStrategy { + + static final KeyUseStrategy INSTANCE = new DefaultKeyUseStrategy(); + + // values from https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 + private static final String SIGNATURE = "sig"; + private static final String ENCRYPTION = "enc"; + + @Override + public String toJwkValue(KeyUsage usage) { + + // states 2, 3, 4 + if (usage.isKeyEncipherment() || usage.isDataEncipherment() || usage.isKeyAgreement()) { + return ENCRYPTION; + } + + // states 0, 1, 5, 6 + if (usage.isDigitalSignature() || usage.isNonRepudiation() || usage.isKeyCertSign() || usage.isCRLSign()) { + return SIGNATURE; + } + + // We don't need to check for encipherOnly (7) and decipherOnly (8) because per + // [RFC 5280, Section 4.2.1.3](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3), + // those two states are only relevant when keyAgreement (4) is true, and that is covered in the first + // conditional above + + return null; //can't infer + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyedRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyedRequest.java new file mode 100644 index 000000000..b45431db4 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyedRequest.java @@ -0,0 +1,23 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.KeySupplier; + +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultKeyedRequest extends DefaultSecurityRequest implements KeySupplier { + + private final K key; + + public DefaultKeyedRequest(Provider provider, SecureRandom secureRandom, K key) { + super(provider, secureRandom); + this.key = Assert.notNull(key, "Key cannot be null."); + } + + @Override + public K getKey() { + return this.key; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPayloadSupplier.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPayloadSupplier.java new file mode 100644 index 000000000..efa777ae5 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPayloadSupplier.java @@ -0,0 +1,31 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.PayloadSupplier; + +import java.security.Key; + +class DefaultPayloadSupplier implements PayloadSupplier { + + private final T payload; + + DefaultPayloadSupplier(T payload) { + this.payload = assertValidPayload(payload); + } + + protected T assertValidPayload(T payload) throws IllegalArgumentException { + Assert.notNull(payload, "payload cannot be null."); + if (payload instanceof byte[]) { + Assert.notEmpty((byte[])payload, "payload byte array cannot be empty."); + } else if (!(payload instanceof Key)) { + String msg = "payload must be either a byte array or a java.security.Key instance."; + throw new IllegalArgumentException(msg); + } + return payload; + } + + @Override + public T getPayload() { + return payload; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKey.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKey.java new file mode 100644 index 000000000..3d9be0f06 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKey.java @@ -0,0 +1,90 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.security.PbeKey; + +public class DefaultPbeKey implements PbeKey { + + private static final String RAW_FORMAT = "RAW"; + private static final String NONE_ALGORITHM = "NONE"; + + private volatile boolean destroyed; + private final char[] chars; + private final int iterations; + + public DefaultPbeKey(char[] password, int iterations) { + if (iterations <= 0) { + String msg = "iterations must be a positive integer. Value: " + iterations; + throw new IllegalArgumentException(msg); + } + this.iterations = iterations; + this.chars = Assert.notEmpty(password, "Password character array cannot be null or empty."); + } + + private void assertActive() { + if (destroyed) { + String msg = "PBKey has been destroyed. Password characters or bytes may not be obtained."; + throw new IllegalStateException(msg); + } + } + + @Override + public char[] getPassword() { + assertActive(); + return this.chars.clone(); + } + + @Override + public int getIterations() { + return this.iterations; + } + + @Override + public String getAlgorithm() { + return NONE_ALGORITHM; + } + + @Override + public String getFormat() { + return RAW_FORMAT; + } + + @Override + public byte[] getEncoded() { + throw new UnsupportedOperationException("getEncoded is not supported for PbeKey instances."); + } + + @Override + public void destroy() { + if (!destroyed && chars != null) { + java.util.Arrays.fill(chars, '\u0000'); + } + this.destroyed = true; + } + + @Override + public boolean isDestroyed() { + return destroyed; + } + + @Override + public int hashCode() { + return Objects.nullSafeHashCode(this.chars); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof DefaultPbeKey) { + DefaultPbeKey other = (DefaultPbeKey) obj; + return this.iterations == other.iterations && + Objects.nullSafeEquals(this.chars, other.chars); + } + return false; + } + + @Override + public String toString() { + return "password=, iterations=" + this.iterations; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKeyBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKeyBuilder.java new file mode 100644 index 000000000..af7ab4225 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKeyBuilder.java @@ -0,0 +1,29 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.PbeKey; +import io.jsonwebtoken.security.PbeKeyBuilder; + +public class DefaultPbeKeyBuilder implements PbeKeyBuilder { + + private char[] password; + private int iterations; + + @Override + public DefaultPbeKeyBuilder setPassword(final char[] password) { + this.password = Assert.notEmpty(password, "password cannot be null or empty."); + return this; + } + + @Override + public DefaultPbeKeyBuilder setIterations(final int iterations) { + Assert.isTrue(iterations > 0, "iterations must be a positive integer."); + this.iterations = iterations; + return this; + } + + @Override + public PbeKey build() { + return new DefaultPbeKey(this.password, this.iterations); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwk.java deleted file mode 100644 index 66650e8f4..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwk.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.PrivateEcJwk; - -class DefaultPrivateEcJwk extends AbstractEcJwk implements PrivateEcJwk { - - static final String D = "d"; - - @Override - public String getD() { - return getString(D); - } - - @Override - public PrivateEcJwk setD(String d) { - return setRequiredValue(D, d, "private key"); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwkBuilder.java deleted file mode 100644 index 33d3ad98f..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateEcJwkBuilder.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.PrivateEcJwk; -import io.jsonwebtoken.security.PrivateEcJwkBuilder; - -class DefaultPrivateEcJwkBuilder extends AbstractEcJwkBuilder implements PrivateEcJwkBuilder { - - private static final JwkValidator VALIDATOR = new PrivateEcJwkValidator(); - - DefaultPrivateEcJwkBuilder() { - super(VALIDATOR); - } - - @Override - PrivateEcJwk newJwk() { - return new DefaultPrivateEcJwk(); - } - - @Override - public PrivateEcJwkBuilder setD(String d) { - this.jwk.setD(d); - return this; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateRsaJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateRsaJwk.java deleted file mode 100644 index f945f7c07..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPrivateRsaJwk.java +++ /dev/null @@ -1,87 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.JwkRsaPrimeInfo; -import io.jsonwebtoken.security.PrivateRsaJwk; - -import java.util.List; - -public class DefaultPrivateRsaJwk extends AbstractRsaJwk implements PrivateRsaJwk { - - static String PRIVATE_EXPONENT = "d"; - static String FIRST_PRIME = "p"; - static String SECOND_PRIME = "q"; - static String FIRST_CRT_EXPONENT = "dp"; - static String SECOND_CRT_EXPONENT = "dq"; - static String FIRST_CRT_COEFFICIENT = "qi"; - static String OTHER_PRIMES_INFO = "oth"; - - @Override - public String getD() { - return getString(PRIVATE_EXPONENT); - } - - @Override - public PrivateRsaJwk setD(String d) { - return setRequiredValue(PRIVATE_EXPONENT, d, "private exponent"); - } - - @Override - public String getP() { - return getString(FIRST_PRIME); - } - - @Override - public PrivateRsaJwk setP(String p) { - return setRequiredValue(FIRST_PRIME, p, "first prime factor"); - } - - @Override - public String getQ() { - return getString(SECOND_PRIME); - } - - @Override - public PrivateRsaJwk setQ(String q) { - return setRequiredValue(FIRST_PRIME, q, "second prime factor"); - } - - @Override - public String getDP() { - return getString(FIRST_CRT_EXPONENT); - } - - @Override - public PrivateRsaJwk setDP(String dp) { - return setRequiredValue(FIRST_CRT_EXPONENT, dp, "first crt exponent"); - } - - @Override - public String getDQ() { - return getString(SECOND_CRT_EXPONENT); - } - - @Override - public PrivateRsaJwk setDQ(String dq) { - return setRequiredValue(SECOND_CRT_EXPONENT, dq, "second crt exponent"); - } - - @Override - public String getQI() { - return getString(FIRST_CRT_COEFFICIENT); - } - - @Override - public PrivateRsaJwk setQI(String qi) { - return setRequiredValue(FIRST_CRT_COEFFICIENT, qi, "first crt coefficient"); - } - - @Override - public List getOtherPrimesInfo() { - throw new UnsupportedOperationException("Not yet implemented."); - } - - @Override - public PrivateRsaJwk setOtherPrimesInfo(List infos) { - throw new UnsupportedOperationException("Not yet implemented."); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java new file mode 100644 index 000000000..4827d108c --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java @@ -0,0 +1,85 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.EcPrivateJwkBuilder; +import io.jsonwebtoken.security.EcPublicJwkBuilder; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.JwkBuilder; +import io.jsonwebtoken.security.ProtoJwkBuilder; +import io.jsonwebtoken.security.RsaPrivateJwkBuilder; +import io.jsonwebtoken.security.RsaPublicJwkBuilder; +import io.jsonwebtoken.security.SecretJwkBuilder; + +import javax.crypto.SecretKey; +import java.security.Key; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Set; + +public class DefaultProtoJwkBuilder, T extends JwkBuilder> + extends AbstractJwkBuilder implements ProtoJwkBuilder { + + public DefaultProtoJwkBuilder() { + super(new DefaultJwkContext()); + } + + @Override + public SecretJwkBuilder setKey(SecretKey key) { + return new AbstractJwkBuilder.DefaultSecretJwkBuilder(this.jwkContext, key); + } + + @Override + public RsaPublicJwkBuilder setKey(RSAPublicKey key) { + return new AbstractAsymmetricJwkBuilder.DefaultRsaPublicJwkBuilder(this.jwkContext, key); + } + + @Override + public RsaPrivateJwkBuilder setKey(RSAPrivateKey key) { + return new AbstractAsymmetricJwkBuilder.DefaultRsaPrivateJwkBuilder(this.jwkContext, key); + } + + @Override + public EcPublicJwkBuilder setKey(ECPublicKey key) { + return new AbstractAsymmetricJwkBuilder.DefaultEcPublicJwkBuilder(this.jwkContext, key); + } + + @Override + public EcPrivateJwkBuilder setKey(ECPrivateKey key) { + return new AbstractAsymmetricJwkBuilder.DefaultEcPrivateJwkBuilder(this.jwkContext, key); + } + + private static T assertKeyPairChild(Class clazz, Key key) { + String type = PrivateKey.class.isAssignableFrom(clazz) ? "private" : "public"; + if (key == null) { + String msg = "KeyPair " + type + " key cannot be null."; + throw new IllegalArgumentException(msg); + } + if (!clazz.isInstance(key)) { + String msg = "The specified KeyPair's " + type + " key must be an instance of " + clazz.getName() + + ". Type found: " + key.getClass().getName(); + throw new IllegalArgumentException(msg); + } + return clazz.cast(key); + } + + @Override + public RsaPrivateJwkBuilder setKeyPairRsa(KeyPair keyPair) { + Assert.notNull(keyPair, "KeyPair cannot be null."); + RSAPublicKey pub = assertKeyPairChild(RSAPublicKey.class, keyPair.getPublic()); + RSAPrivateKey priv = assertKeyPairChild(RSAPrivateKey.class, keyPair.getPrivate()); + return setKey(priv).setPublicKey(pub); + } + + @Override + public EcPrivateJwkBuilder setKeyPairEc(KeyPair keyPair) { + Assert.notNull(keyPair, "KeyPair cannot be null."); + ECPublicKey pub = assertKeyPairChild(ECPublicKey.class, keyPair.getPublic()); + ECPrivateKey priv = assertKeyPairChild(ECPrivateKey.class, keyPair.getPrivate()); + return setKey(priv).setPublicKey(pub); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwk.java deleted file mode 100644 index bdc6c1854..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwk.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.PublicEcJwk; - -class DefaultPublicEcJwk extends AbstractEcJwk implements PublicEcJwk { -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwkBuilder.java deleted file mode 100644 index 99ec5bdbc..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPublicEcJwkBuilder.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.PublicEcJwk; -import io.jsonwebtoken.security.PublicEcJwkBuilder; - -class DefaultPublicEcJwkBuilder extends AbstractEcJwkBuilder implements PublicEcJwkBuilder { - - private static final JwkValidator VALIDATOR = new AbstractEcJwkValidator() { - @Override - protected void validateEcJwk(PublicEcJwk jwk) { - //nothing additional to do - } - }; - - DefaultPublicEcJwkBuilder() { - super(VALIDATOR); - } - - @Override - PublicEcJwk newJwk() { - return new DefaultPublicEcJwk(); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java new file mode 100644 index 000000000..782954e00 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java @@ -0,0 +1,76 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.DecryptionKeyRequest; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.RsaKeyAlgorithm; +import io.jsonwebtoken.security.SecurityException; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.RSAKey; +import java.security.spec.AlgorithmParameterSpec; + +public class DefaultRsaKeyAlgorithm extends CryptoAlgorithm + implements RsaKeyAlgorithm { + + private final AlgorithmParameterSpec SPEC; //can be null + + public DefaultRsaKeyAlgorithm(String id, String jcaTransformationString) { + this(id, jcaTransformationString, null); + } + + public DefaultRsaKeyAlgorithm(String id, String jcaTransformationString, AlgorithmParameterSpec spec) { + super(id, jcaTransformationString); + this.SPEC = spec; //can be null + } + + @Override + public KeyResult getEncryptionKey(final KeyRequest request) throws SecurityException { + Assert.notNull(request, "Request cannot be null."); + final E kek = Assert.notNull(request.getKey(), "Request key encryption key cannot be null."); + AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); + final SecretKey cek = Assert.notNull(enc.generateKey(), "Request encryption algorithm cannot generate a null key."); + + byte[] ciphertext = execute(request, Cipher.class, new CheckedFunction() { + @Override + public byte[] apply(Cipher cipher) throws Exception { + if (SPEC == null) { + cipher.init(Cipher.WRAP_MODE, kek, ensureSecureRandom(request)); + } else { + cipher.init(Cipher.WRAP_MODE, kek, SPEC, ensureSecureRandom(request)); + } + return cipher.wrap(cek); + } + }); + + return new DefaultKeyResult(cek, ciphertext); + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + Assert.notNull(request, "request cannot be null."); + final D kek = Assert.notNull(request.getKey(), "Request key decryption key cannot be null."); + final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Request encrypted key (request.getPayload()) cannot be null or empty."); + + return execute(request, Cipher.class, new CheckedFunction() { + @Override + public SecretKey apply(Cipher cipher) throws Exception { + if (SPEC == null) { + cipher.init(Cipher.UNWRAP_MODE, kek); + } else { + cipher.init(Cipher.UNWRAP_MODE, kek, SPEC); + } + Key key = cipher.unwrap(cekBytes, "AES", Cipher.SECRET_KEY); + Assert.state(key instanceof SecretKey, "Cipher unwrap must return a SecretKey instance."); + return (SecretKey) key; + } + }); + } +} \ No newline at end of file diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java new file mode 100644 index 000000000..72969f870 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java @@ -0,0 +1,40 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.RsaPrivateJwk; +import io.jsonwebtoken.security.RsaPublicJwk; + +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.LinkedHashSet; +import java.util.Set; + +class DefaultRsaPrivateJwk extends AbstractPrivateJwk implements RsaPrivateJwk { + + static String PRIVATE_EXPONENT = "d"; + static String FIRST_PRIME = "p"; + static String SECOND_PRIME = "q"; + static String FIRST_CRT_EXPONENT = "dp"; + static String SECOND_CRT_EXPONENT = "dq"; + static String FIRST_CRT_COEFFICIENT = "qi"; + static String OTHER_PRIMES_INFO = "oth"; + static String PRIME_FACTOR = "r"; + static String FACTOR_CRT_EXPONENT = "d"; + static String FACTOR_CRT_COEFFICIENT = "t"; + + static final Set PRIVATE_NAMES = Collections.setOf( + PRIVATE_EXPONENT, FIRST_PRIME, SECOND_PRIME, + FIRST_CRT_EXPONENT, SECOND_CRT_EXPONENT, + FIRST_CRT_COEFFICIENT, OTHER_PRIMES_INFO); + + static final Set OPTIONAL_PRIVATE_NAMES; + + static { + OPTIONAL_PRIVATE_NAMES = new LinkedHashSet<>(PRIVATE_NAMES); + OPTIONAL_PRIVATE_NAMES.remove(PRIVATE_EXPONENT); + } + + DefaultRsaPrivateJwk(JwkContext ctx, RsaPublicJwk pubJwk) { + super(ctx, pubJwk); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java new file mode 100644 index 000000000..13e4fba51 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java @@ -0,0 +1,16 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.RsaPublicJwk; + +import java.security.interfaces.RSAPublicKey; + +class DefaultRsaPublicJwk extends AbstractPublicJwk implements RsaPublicJwk { + + static final String TYPE_VALUE = "RSA"; + static final String MODULUS = "n"; + static final String PUBLIC_EXPONENT = "e"; + + DefaultRsaPublicJwk(JwkContext ctx) { + super(ctx); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java similarity index 55% rename from impl/src/main/java/io/jsonwebtoken/impl/security/RsaSignatureAlgorithm.java rename to impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java index be7b2b061..b061e4ca7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java @@ -1,17 +1,15 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.RuntimeEnvironment; -import io.jsonwebtoken.security.AsymmetricKeySignatureAlgorithm; -import io.jsonwebtoken.security.CryptoRequest; import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.RsaSignatureAlgorithm; +import io.jsonwebtoken.security.SignatureRequest; import io.jsonwebtoken.security.VerifySignatureRequest; import io.jsonwebtoken.security.WeakKeyException; -import java.security.InvalidParameterException; import java.security.Key; import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; @@ -20,8 +18,7 @@ import java.security.spec.MGF1ParameterSpec; import java.security.spec.PSSParameterSpec; -@SuppressWarnings("unused") //used via reflection in the io.jsonwebtoken.security.SignatureAlgorithms class -public class RsaSignatureAlgorithm extends AbstractSignatureAlgorithm implements AsymmetricKeySignatureAlgorithm { +public class DefaultRsaSignatureAlgorithm extends AbstractSignatureAlgorithm implements RsaSignatureAlgorithm { static { RuntimeEnvironment.enableBouncyCastleIfPossible(); //PS256, PS384, PS512 on <= JDK 10 require BC @@ -31,7 +28,6 @@ public class RsaSignatureAlgorithm extends AbstractSignatureAlgorithm implements private static AlgorithmParameterSpec pssParamFromSaltBitLength(int saltBitLength) { MGF1ParameterSpec ps = new MGF1ParameterSpec("SHA-" + saltBitLength); - //MGF1ParameterSpec ps = MGF1ParameterSpec.SHA256; int saltByteLength = saltBitLength / Byte.SIZE; return new PSSParameterSpec(ps.getDigestAlgorithm(), "MGF1", ps, saltByteLength, 1); } @@ -40,7 +36,7 @@ private static AlgorithmParameterSpec pssParamFromSaltBitLength(int saltBitLengt private final AlgorithmParameterSpec algorithmParameterSpec; - public RsaSignatureAlgorithm(String name, String jcaName, int preferredKeyLengthBits, AlgorithmParameterSpec algParam) { + public DefaultRsaSignatureAlgorithm(String name, String jcaName, int preferredKeyLengthBits, AlgorithmParameterSpec algParam) { super(name, jcaName); if (preferredKeyLengthBits < MIN_KEY_LENGTH_BITS) { String msg = "preferredKeyLengthBits must be greater than the JWA mandatory minimum key length of " + MIN_KEY_LENGTH_BITS; @@ -50,30 +46,25 @@ public RsaSignatureAlgorithm(String name, String jcaName, int preferredKeyLength this.algorithmParameterSpec = algParam; } - public RsaSignatureAlgorithm(String name, String jcaName, int preferredKeyLengthBits) { - this(name, jcaName, preferredKeyLengthBits, null); + public DefaultRsaSignatureAlgorithm(int digestBitLength, int preferredKeyBitLength) { + this("RS" + digestBitLength, "SHA" + digestBitLength + "withRSA", preferredKeyBitLength); } - public RsaSignatureAlgorithm(String name, String jcaName, int preferredKeyLengthBits, int pssSaltLengthBits) { - this(name, jcaName, preferredKeyLengthBits, pssParamFromSaltBitLength(pssSaltLengthBits)); + public DefaultRsaSignatureAlgorithm(int digestBitLength, int preferredKeyBitLength, int pssSaltBitLength) { + this("PS" + digestBitLength, "RSASSA-PSS", preferredKeyBitLength, pssSaltBitLength); + } + + public DefaultRsaSignatureAlgorithm(String name, String jcaName, int preferredKeyLengthBits) { + this(name, jcaName, preferredKeyLengthBits, null); } - //for testing visibility - protected KeyPairGenerator getKeyPairGenerator() throws NoSuchAlgorithmException, InvalidParameterException { - KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); - generator.initialize(preferredKeyLength, Randoms.secureRandom()); - return generator; + public DefaultRsaSignatureAlgorithm(String name, String jcaName, int preferredKeyLengthBits, int pssSaltLengthBits) { + this(name, jcaName, preferredKeyLengthBits, pssParamFromSaltBitLength(pssSaltLengthBits)); } @Override public KeyPair generateKeyPair() { - KeyPairGenerator generator; - try { - generator = getKeyPairGenerator(); - } catch (Exception e) { - throw new IllegalStateException("Unable to obtain an RSA KeyPairGenerator: " + e.getMessage(), e); - } - return generator.genKeyPair(); + return new JcaTemplate("RSA", null).generateKeyPair(this.preferredKeyLength); } @Override @@ -97,40 +88,51 @@ protected void validateKey(Key key, boolean signing) { int size = rsaKey.getModulus().bitLength(); if (size < MIN_KEY_LENGTH_BITS) { - String name = getName(); + String id = getId(); - String section = name.startsWith("PS") ? "3.5" : "3.3"; + String section = id.startsWith("PS") ? "3.5" : "3.3"; String msg = "The " + keyType(signing) + " key's size is " + size + " bits which is not secure " + - "enough for the " + name + " algorithm. The JWT JWA Specification (RFC 7518, Section " + + "enough for the " + id + " algorithm. The JWT JWA Specification (RFC 7518, Section " + section + ") states that RSA keys MUST have a size >= " + - MIN_KEY_LENGTH_BITS + " bits. Consider using the SignatureAlgorithms." + name + ".generateKeyPair() " + - "method to create a key pair guaranteed to be secure enough for " + name + ". See " + + MIN_KEY_LENGTH_BITS + " bits. Consider using the SignatureAlgorithms." + id + ".generateKeyPair() " + + "method to create a key pair guaranteed to be secure enough for " + id + ". See " + "https://tools.ietf.org/html/rfc7518#section-" + section + " for more information."; throw new WeakKeyException(msg); } } @Override - protected byte[] doSign(CryptoRequest request) throws Exception { - PrivateKey privateKey = (PrivateKey) request.getKey(); - Signature sig = createSignatureInstance(request.getProvider(), this.algorithmParameterSpec); - sig.initSign(privateKey); - sig.update(request.getData()); - return sig.sign(); + protected byte[] doSign(final SignatureRequest request) { + return execute(request, Signature.class, new CheckedFunction() { + @Override + public byte[] apply(Signature sig) throws Exception { + if (algorithmParameterSpec != null) { + sig.setParameter(algorithmParameterSpec); + } + sig.initSign(request.getKey()); + sig.update(request.getPayload()); + return sig.sign(); + } + }); } @Override - protected boolean doVerify(VerifySignatureRequest request) throws Exception { + protected boolean doVerify(final VerifySignatureRequest request) throws Exception { final Key key = request.getKey(); - if (key instanceof PrivateKey) { + if (key instanceof PrivateKey) { //legacy support only return super.doVerify(request); } - - PublicKey publicKey = (PublicKey) key; - Signature sig = createSignatureInstance(request.getProvider(), this.algorithmParameterSpec); - sig.initVerify(publicKey); - sig.update(request.getData()); - return sig.verify(request.getSignature()); + return execute(request, Signature.class, new CheckedFunction() { + @Override + public Boolean apply(Signature sig) throws Exception { + if (algorithmParameterSpec != null) { + sig.setParameter(algorithmParameterSpec); + } + sig.initVerify(request.getKey()); + sig.update(request.getPayload()); + return sig.verify(request.getDigest()); + } + }); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java new file mode 100644 index 000000000..60250bcd3 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java @@ -0,0 +1,18 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.SecretJwk; + +import javax.crypto.SecretKey; +import java.util.Set; + +class DefaultSecretJwk extends AbstractJwk implements SecretJwk { + + static final String TYPE_VALUE = "oct"; + static final String K = "k"; + static final Set PRIVATE_NAMES = Collections.setOf(K); + + DefaultSecretJwk(JwkContext ctx) { + super(ctx); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecurityRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecurityRequest.java new file mode 100644 index 000000000..5efaa08d2 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecurityRequest.java @@ -0,0 +1,27 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.SecurityRequest; + +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultSecurityRequest implements SecurityRequest { + + private final Provider provider; + private final SecureRandom secureRandom; + + public DefaultSecurityRequest(Provider provider, SecureRandom secureRandom) { + this.provider = provider; + this.secureRandom = secureRandom; + } + + @Override + public Provider getProvider() { + return this.provider; + } + + @Override + public SecureRandom getSecureRandom() { + return this.secureRandom; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSignatureRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSignatureRequest.java new file mode 100644 index 000000000..028601b33 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSignatureRequest.java @@ -0,0 +1,17 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.SignatureRequest; + +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultSignatureRequest extends DefaultCryptoRequest implements SignatureRequest { + + public DefaultSignatureRequest(Provider provider, SecureRandom secureRandom, byte[] data, K key) { + super(provider, secureRandom, data, key); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwk.java deleted file mode 100644 index cd4d6d598..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwk.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.SymmetricJwk; - -final class DefaultSymmetricJwk extends AbstractJwk implements SymmetricJwk { - - static final String TYPE_VALUE = "oct"; - static final String K = "k"; - - DefaultSymmetricJwk() { - super(TYPE_VALUE); - } - - @Override - public String getK() { - return getString(K); - } - - @Override - public SymmetricJwk setK(String k) { - k = Strings.clean(k); - Assert.notNull(k, "SymmetricJwk 'k' property cannot be null or empty."); - setValue(K, k); - return this; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwkBuilder.java deleted file mode 100644 index eed168c92..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricJwkBuilder.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.SymmetricJwk; -import io.jsonwebtoken.security.SymmetricJwkBuilder; - -final class DefaultSymmetricJwkBuilder extends AbstractJwkBuilder implements SymmetricJwkBuilder { - - private static final JwkValidator VALIDATOR = new SymmetricJwkValidator(); - - DefaultSymmetricJwkBuilder() { - super(VALIDATOR); - } - - @Override - public SymmetricJwkBuilder setK(String k) { - this.jwk.setK(k); - return this; - } - - @Override - SymmetricJwk newJwk() { - return new DefaultSymmetricJwk(); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java new file mode 100644 index 000000000..c57b3a49b --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java @@ -0,0 +1,149 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.MalformedKeyException; + +import java.math.BigInteger; +import java.util.Map; + +/** + * Allows use af shared assertions across codebase, regardless of inheritance hierarchy. + */ +public class DefaultValueGetter implements ValueGetter { + + private final Map values; + + public DefaultValueGetter(Map values) { + this.values = Assert.notEmpty(values, "Values cannot be null or empty."); + } + + private String name() { + if (values instanceof JweHeader) { + return "JWE header"; + } else if (values instanceof JwsHeader) { + return "JWS header"; + } else if (values instanceof Header) { + return "JWT header"; + } else if (values instanceof Jwk || values instanceof JwkContext) { + Object value = values.get(AbstractJwk.TYPE); + if (DefaultSecretJwk.TYPE_VALUE.equals(value)) { + value = "Secret"; + } + return value instanceof String ? value + " JWK" : "JWK"; + } else { + return "Map"; + } + } + + private JwtException malformed(String msg) { + if (values instanceof JwkContext || values instanceof Jwk) { + return new MalformedKeyException(msg); + } else { + return new MalformedJwtException(msg); + } + } + + protected Object getRequiredValue(String key) { + Object value = this.values.get(key); + if (value == null) { + String msg = name() + " is missing required '" + key + "' value."; + throw malformed(msg); + } + return value; + } + + @Override + public String getRequiredString(String key) { + Object value = getRequiredValue(key); + if (!(value instanceof String)) { + String msg = name() + " '" + key + "' value must be a String. Actual type: " + value.getClass().getName(); + throw malformed(msg); + } + String sval = Strings.clean((String) value); + if (!Strings.hasText(sval)) { + String msg = name() + " '" + key + "' string value cannot be null or empty."; + throw malformed(msg); + } + return (String) value; + } + + @Override + public int getRequiredInteger(String key) { + Object value = getRequiredValue(key); + if (!(value instanceof Integer)) { + String msg = name() + " '" + key + "' value must be an Integer. Actual type: " + value.getClass().getName(); + throw malformed(msg); + } + return (Integer) value; + } + + @Override + public int getRequiredPositiveInteger(String key) { + int value = getRequiredInteger(key); + if (value <= 0) { + String msg = name() + " '" + key + "' value must be a positive Integer. Value: " + value; + throw malformed(msg); + } + return value; + } + + @Override + public byte[] getRequiredBytes(String key) { + + String encoded = getRequiredString(key); + + byte[] decoded; + try { + decoded = Decoders.BASE64URL.decode(encoded); + } catch (Exception e) { + String msg = name() + " '" + key + "' value is not a valid Base64URL String: " + e.getMessage(); + throw malformed(msg); + } + + if (Arrays.length(decoded) == 0) { + String msg = name() + " '" + key + "' decoded byte array cannot be empty."; + throw malformed(msg); + } + + return decoded; + } + + @Override + public byte[] getRequiredBytes(String key, int requiredByteLength) { + byte[] decoded = getRequiredBytes(key); + int len = Arrays.length(decoded); + if (len != requiredByteLength) { + String msg = name() + " '" + key + "' decoded byte array must be " + Bytes.bytesMsg(requiredByteLength) + + " long. Actual length: " + Bytes.bytesMsg(len) + "."; + throw malformed(msg); + } + return decoded; + } + + @Override + public BigInteger getRequiredBigInt(String key, boolean sensitive) { + String s = getRequiredString(key); + try { + byte[] bytes = Decoders.BASE64URL.decode(s); + return new BigInteger(1, bytes); + } catch (Exception e) { + String msg = "Unable to decode " + name() + " '" + key + "' value"; + if (!sensitive) { + msg += " '" + s + "'"; + } + msg += " to BigInteger: " + e.getMessage(); + throw malformed(msg); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySignatureRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySignatureRequest.java index 5ede1eef9..0c7502e52 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySignatureRequest.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySignatureRequest.java @@ -7,17 +7,17 @@ import java.security.Provider; import java.security.SecureRandom; -public class DefaultVerifySignatureRequest extends DefaultCryptoRequest implements VerifySignatureRequest { +public class DefaultVerifySignatureRequest extends DefaultSignatureRequest implements VerifySignatureRequest { private final byte[] signature; - public DefaultVerifySignatureRequest(byte[] data, Key key, Provider provider, SecureRandom secureRandom, byte[] signature) { - super(data, key, provider, secureRandom); + public DefaultVerifySignatureRequest(Provider provider, SecureRandom secureRandom, byte[] data, K key, byte[] signature) { + super(provider, secureRandom, data, key); this.signature = Assert.notEmpty(signature, "Signature byte array cannot be null or empty."); } @Override - public byte[] getSignature() { + public byte[] getDigest() { return this.signature; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DirectEncryptionMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DirectEncryptionMode.java deleted file mode 100644 index 70c5c75f1..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DirectEncryptionMode.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; - -import javax.crypto.SecretKey; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class DirectEncryptionMode implements KeyManagementMode { - - private final SecretKey key; - - DirectEncryptionMode(SecretKey key) { - this.key = Assert.notNull(key, "SecretKey argument cannot be null."); - } - - @Override - public SecretKey getKey(GetKeyRequest ignored) { - return this.key; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAgreementMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAgreementMode.java deleted file mode 100644 index e6e40363d..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAgreementMode.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import javax.crypto.SecretKey; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class DirectKeyAgreementMode implements KeyManagementMode { - - @Override - public SecretKey getKey(GetKeyRequest request) { - throw new UnsupportedOperationException("Not Yet Implemented"); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAlgorithm.java new file mode 100644 index 000000000..d6c9a1284 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAlgorithm.java @@ -0,0 +1,36 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.DecryptionKeyRequest; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.SecurityException; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DirectKeyAlgorithm implements KeyAlgorithm { + + static final String ID = "dir"; + + @Override + public String getId() { + return ID; + } + + @Override + public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + Assert.notNull(request, "request cannot be null."); + SecretKey key = Assert.notNull(request.getKey(), "request.getKey() cannot be null."); + return new DefaultKeyResult(key); + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + Assert.notNull(request, "request cannot be null."); + return Assert.notNull(request.getKey(), "request.getKey() cannot be null."); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolver.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolver.java deleted file mode 100644 index e7f4962a3..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolver.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.JweHeader; -import io.jsonwebtoken.security.DecryptionKeyResolver; - -import java.security.Key; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class DisabledDecryptionKeyResolver implements DecryptionKeyResolver { - - /** - * Singleton instance that may be used if direct instantiation is not desired. - */ - public static final DisabledDecryptionKeyResolver INSTANCE = new DisabledDecryptionKeyResolver(); - - @Override - public Key resolveDecryptionKey(JweHeader header) { - return null; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java new file mode 100644 index 000000000..e14b49a29 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java @@ -0,0 +1,81 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import java.security.Key; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +class DispatchingJwkFactory implements JwkFactory> { + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Collection> createDefaultFactories() { + List families = new ArrayList<>(3); + families.add(new SecretJwkFactory()); + families.add(new AsymmetricJwkFactory(EcPublicJwkFactory.DEFAULT_INSTANCE, new EcPrivateJwkFactory())); + families.add(new AsymmetricJwkFactory(RsaPublicJwkFactory.DEFAULT_INSTANCE, new RsaPrivateJwkFactory())); + return families; + } + private static final Collection> DEFAULT_FACTORIES = createDefaultFactories(); + static final JwkFactory> DEFAULT_INSTANCE = new DispatchingJwkFactory(); + + private final Collection> factories; + + DispatchingJwkFactory() { + this(DEFAULT_FACTORIES); + } + + @SuppressWarnings("unchecked") + DispatchingJwkFactory(Collection> factories) { + Assert.notEmpty(factories, "FamilyJwkFactory collection cannot be null or empty."); + this.factories = new ArrayList<>(factories.size()); + for (FamilyJwkFactory factory : factories) { + if (!Strings.hasText(factory.getId())) { + String msg = "FamilyJwkFactory instance of type " + factory.getClass().getName() + " does not " + + "have a required algorithm family id (factory.getFactoryId() cannot be null or empty)."; + throw new IllegalArgumentException(msg); + } + this.factories.add((FamilyJwkFactory) factory); + } + } + + @Override + public Jwk createJwk(JwkContext ctx) { + + Assert.notNull(ctx, "JwkContext cannot be null."); + + final Key key = ctx.getKey(); + final String kty = Strings.clean(ctx.getType()); + + if (key == null && kty == null) { + String msg = "Either a Key instance or a '" + AbstractJwk.TYPE + "' value is required to create a JWK."; + throw new IllegalArgumentException(msg); + } + + for (FamilyJwkFactory factory : this.factories) { + if (factory.supports(ctx)) { + String algFamilyId = Assert.hasText(factory.getId(), "factory id cannot be null or empty."); + if (kty == null) { + ctx.setType(algFamilyId); //ensure the kty is available for the rest of the creation process + } + return factory.createJwk(ctx); + } + } + + // if nothing has been returned at this point, no factory supported the JwkContext, so that's an error: + String reason; + if (key != null) { + reason = "key of type " + key.getClass().getName(); + } else { + reason = "kty value '" + kty + "'"; + } + + String msg = "Unable to create JWK for unrecognized " + reason + ": there is " + + "no known JWK Factory capable of creating JWKs for this key type."; + throw new UnsupportedKeyException(msg); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcJwkConverter.java deleted file mode 100644 index 529da692b..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcJwkConverter.java +++ /dev/null @@ -1,177 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.io.Encoders; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.Jwks; -import io.jsonwebtoken.security.KeyException; -import io.jsonwebtoken.security.PublicEcJwkBuilder; -import io.jsonwebtoken.security.UnsupportedKeyException; - -import java.math.BigInteger; -import java.security.AlgorithmParameters; -import java.security.Key; -import java.security.KeyFactory; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; -import java.security.spec.ECGenParameterSpec; -import java.security.spec.ECParameterSpec; -import java.security.spec.ECPoint; -import java.security.spec.ECPrivateKeySpec; -import java.security.spec.ECPublicKeySpec; -import java.util.HashMap; -import java.util.Map; - -public class EcJwkConverter extends AbstractTypedJwkConverter { - - private static final Map EC_CURVE_NAMES_BY_JWA_ID = createEcCurveNameMap(); - - private static Map createEcCurveNameMap() { - Map m = new HashMap<>(); - m.put("P-256", "secp256r1"); - m.put("P-384", "secp384r1"); - m.put("P-521", "secp521r1"); - return m; - } - - private static ECParameterSpec getStandardNameSpec(String stdName) throws KeyException { - try { - AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC"); - parameters.init(new ECGenParameterSpec(stdName)); - return parameters.getParameterSpec(ECParameterSpec.class); - } catch (Exception e) { - String msg = "Unable to obtain JVM ECParameterSpec for JWA curve ID '" + stdName + "'."; - throw new KeyException(msg, e); - } - } - - private static ECParameterSpec getCurveIdSpec(String curveId) { - String stdName = EC_CURVE_NAMES_BY_JWA_ID.get(curveId); - if (stdName == null) { - String msg = "Unrecognized JWA curve id '" + curveId + "'"; - throw new UnsupportedKeyException(msg); - } - return getStandardNameSpec(stdName); - } - - /** - * https://tools.ietf.org/html/rfc7518#section-6.2.1.2 indicates that this algorithm logic is defined in - * http://www.secg.org/sec1-v2.pdf Section 2.3.5. - * @param fieldSize EC field size - * @param coordinate EC point coordinate (e.g. x or y) - * @return A base64Url-encoded String representing the EC field per the RFC format - */ - // Algorithm defined in http://www.secg.org/sec1-v2.pdf Section 2.3.5 - static String encodeCoordinate(int fieldSize, BigInteger coordinate) { - byte[] bytes = toUnsignedBytes(coordinate); - int mlen = (int)Math.ceil(fieldSize / 8d); - if (mlen > bytes.length) { - byte[] m = new byte[mlen]; - System.arraycopy(bytes, 0, m, mlen - bytes.length, bytes.length); - bytes = m; - } - return Encoders.BASE64URL.encode(bytes); - } - - EcJwkConverter() { - super("EC"); - } - - @Override - public boolean supports(Key key) { - return key instanceof ECPrivateKey || key instanceof ECPublicKey; - } - - @Override - public Key toKey(Map jwk) { - Assert.notNull(jwk, "JWK map argument cannot be null."); - if (jwk.containsKey("d")) { - return toPrivateKey(jwk); - } - return toPublicKey(jwk); - } - - @Override - public Map toJwk(Key key) { - if (key instanceof ECPrivateKey) { - return toPrivateJwk((ECPrivateKey)key); - } - Assert.isInstanceOf(ECPublicKey.class, key, "Key argument must be an ECPublicKey or ECPrivateKey instance."); - return toPublicJwk((ECPublicKey)key); - } - - private ECPublicKey toPublicKey(Map jwk) { - String curveId = getRequiredString(jwk, "crv"); - BigInteger x = getRequiredBigInt(jwk, "x"); - BigInteger y = getRequiredBigInt(jwk, "y"); - - ECParameterSpec spec = getCurveIdSpec(curveId); - ECPoint point = new ECPoint(x, y); - ECPublicKeySpec pubSpec = new ECPublicKeySpec(point, spec); - - try { - KeyFactory kf = getKeyFactory(); - return (ECPublicKey)kf.generatePublic(pubSpec); - } catch (Exception e) { - String msg = "Unable to obtain ECPublicKey for curve '" + curveId + "'."; - throw new KeyException(msg, e); - } - } - - public ECPrivateKey toPrivateKey(Map jwk) { - String curveId = getRequiredString(jwk, "crv"); - BigInteger d = getRequiredBigInt(jwk, "d"); - - // We don't actually need these two values for JVM lookup, but the - // [JWA spec](https://tools.ietf.org/html/rfc7518#section-6.2.2) - // requires them to be present and valid for the private key as well, so we assert that here: - getRequiredBigInt(jwk, "x"); - getRequiredBigInt(jwk, "y"); - - ECParameterSpec spec = getCurveIdSpec(curveId); - ECPrivateKeySpec privateSpec = new ECPrivateKeySpec(d, spec); - - try { - KeyFactory kf = getKeyFactory(); - return (ECPrivateKey)kf.generatePrivate(privateSpec); - } catch (Exception e) { - String msg = "Unable to obtain ECPrivateKey from specified jwk for curve '" + curveId + "'."; - throw new KeyException(msg, e); - } - } - - public Map toPublicJwk(ECPublicKey key) { - - PublicEcJwkBuilder builder = Jwks.builder().ellipticCurve().publicKey(); - - Map m = newJwkMap(); - - System.out.println(key.getAlgorithm()); - - ECParameterSpec spec = key.getParams(); - - //TODO: need a ECPublicKey-to-CurveId function - - SignatureAlgorithm alg = SignatureAlgorithm.forSigningKey(key); - - int bitLength = spec.getOrder().bitLength(); - - int fieldSize = spec.getCurve().getField().getFieldSize(); - - String x = encodeCoordinate(fieldSize, spec.getGenerator().getAffineX()); - String y = encodeCoordinate(fieldSize, spec.getGenerator().getAffineY()); - - - builder.setX(x).setY(y); - - //return (Map)builder.build(); - - throw new UnsupportedOperationException("Not yet implemented."); - } - - - - public Map toPrivateJwk(ECPrivateKey key) { - throw new UnsupportedOperationException("Not yet implemented."); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java new file mode 100644 index 000000000..1932f0d5e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java @@ -0,0 +1,83 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.EcPrivateJwk; +import io.jsonwebtoken.security.EcPublicJwk; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPrivateKeySpec; + +class EcPrivateJwkFactory extends AbstractEcJwkFactory { + + private static final String ECPUBKEY_ERR_MSG = "JwkContext publicKey must be an " + ECPublicKey.class.getName() + " instance."; + + EcPrivateJwkFactory() { + super(ECPrivateKey.class); + } + + @Override + protected boolean supportsKeyValues(JwkContext ctx) { + return super.supportsKeyValues(ctx) && ctx.containsKey(DefaultEcPrivateJwk.D); + } + + @Override + protected EcPrivateJwk createJwkFromKey(JwkContext ctx) { + + ECPrivateKey key = ctx.getKey(); + ECPublicKey ecPublicKey; + + PublicKey publicKey = ctx.getPublicKey(); + if (publicKey != null) { + ecPublicKey = Assert.isInstanceOf(ECPublicKey.class, publicKey, ECPUBKEY_ERR_MSG); + } else { + ecPublicKey = derivePublic(ctx); + } + + // [JWA spec](https://tools.ietf.org/html/rfc7518#section-6.2.2) + // requires public values to be present in private JWKs, so add them: + JwkContext pubCtx = new DefaultJwkContext<>(DefaultEcPrivateJwk.PRIVATE_NAMES, ctx, ecPublicKey); + EcPublicJwk pubJwk = EcPublicJwkFactory.DEFAULT_INSTANCE.createJwk(pubCtx); + ctx.putAll(pubJwk); // add public values to private key context + + int fieldSize = key.getParams().getCurve().getField().getFieldSize(); + String d = toOctetString(fieldSize, key.getS()); + ctx.put(DefaultEcPrivateJwk.D, d); + + return new DefaultEcPrivateJwk(ctx, pubJwk); + } + + @Override + protected EcPrivateJwk createJwkFromValues(final JwkContext ctx) { + + ValueGetter getter = new DefaultValueGetter(ctx); + String curveId = getter.getRequiredString(DefaultEcPublicJwk.CURVE_ID); + BigInteger d = getter.getRequiredBigInt(DefaultEcPrivateJwk.D, true); + + // We don't actually need the public x,y point coordinates for JVM lookup, but the + // [JWA spec](https://tools.ietf.org/html/rfc7518#section-6.2.2) + // requires them to be present and valid for the private key as well, so we assert that here: + JwkContext pubCtx = new DefaultJwkContext<>(DefaultEcPrivateJwk.PRIVATE_NAMES, ctx); + EcPublicJwk pubJwk = EcPublicJwkFactory.DEFAULT_INSTANCE.createJwk(pubCtx); + + ECParameterSpec spec = getCurveByJwaId(curveId); + final ECPrivateKeySpec privateSpec = new ECPrivateKeySpec(d, spec); + + ECPrivateKey key = generateKey(ctx, new CheckedFunction() { + @Override + public ECPrivateKey apply(KeyFactory kf) throws Exception { + return (ECPrivateKey) kf.generatePrivate(privateSpec); + } + }); + + ctx.setKey(key); + + return new DefaultEcPrivateJwk(ctx, pubJwk); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java new file mode 100644 index 000000000..a8e3d858a --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java @@ -0,0 +1,77 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.security.EcPublicJwk; +import io.jsonwebtoken.security.InvalidKeyException; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; + +class EcPublicJwkFactory extends AbstractEcJwkFactory { + + static final EcPublicJwkFactory DEFAULT_INSTANCE = new EcPublicJwkFactory(); + + EcPublicJwkFactory() { + super(ECPublicKey.class); + } + + @Override + protected EcPublicJwk createJwkFromKey(JwkContext ctx) { + + ECPublicKey key = ctx.getKey(); + + ECParameterSpec spec = key.getParams(); + EllipticCurve curve = spec.getCurve(); + ECPoint point = key.getW(); + + String curveId = getJwaIdByCurve(curve); + ctx.put(DefaultEcPublicJwk.CURVE_ID, curveId); + + int fieldSize = curve.getField().getFieldSize(); + String x = toOctetString(fieldSize, point.getAffineX()); + ctx.put(DefaultEcPublicJwk.X, x); + + String y = toOctetString(fieldSize, point.getAffineY()); + ctx.put(DefaultEcPublicJwk.Y, y); + + return new DefaultEcPublicJwk(ctx); + } + + @Override + protected EcPublicJwk createJwkFromValues(final JwkContext ctx) { + + ValueGetter getter = new DefaultValueGetter(ctx); + String curveId = getter.getRequiredString(DefaultEcPublicJwk.CURVE_ID); + BigInteger x = getter.getRequiredBigInt(DefaultEcPublicJwk.X, false); + BigInteger y = getter.getRequiredBigInt(DefaultEcPublicJwk.Y, false); + + ECParameterSpec spec = getCurveByJwaId(curveId); + ECPoint point = new ECPoint(x, y); + + if (!contains(spec.getCurve(), point)) { + String msg = "EC JWK x,y coordinates do not match a point on the '" + curveId + "' elliptic curve. This " + + "could be due simply to an incorrectly-created JWK or possibly an attempted Invalid Curve Attack " + + "(see https://safecurves.cr.yp.to/twist.html for more information). JWK: {" + ctx + "}."; + throw new InvalidKeyException(msg); + } + + final ECPublicKeySpec pubSpec = new ECPublicKeySpec(point, spec); + + ECPublicKey key = generateKey(ctx, new CheckedFunction() { + @Override + public ECPublicKey apply(KeyFactory kf) throws Exception { + return (ECPublicKey) kf.generatePublic(pubSpec); + } + }); + + ctx.setKey(key); + + return new DefaultEcPublicJwk(ctx); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptKeyRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptKeyRequest.java deleted file mode 100644 index 6770c63d1..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptKeyRequest.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import javax.crypto.SecretKey; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface EncryptKeyRequest { - - SecretKey getKey(); - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptedKeyManagementMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptedKeyManagementMode.java deleted file mode 100644 index 9a6003ecf..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptedKeyManagementMode.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.jsonwebtoken.impl.security; - -/** - * A {@code KeyManagementMode} that encrypts the JWE encryption key itself. This is used when embedding the content - * encryption key in the JWE as an encrypted value. This technique allows 1) two or more parties to use the same - * randomly generated key and 2) have an encrypted form of that key specific to each party, ensuring only intended - * recipients may access the random key. This tends to also be a faster approach since an asymmetric key algorithm - * (which can be slow) can be used to encrypt just a key and a symmetric key (which is generally faster) can be used - * to encrypt the main (larger) payload/claims. - * - * @since JJWT_RELEASE_VERSION - */ -public interface EncryptedKeyManagementMode extends KeyManagementMode { - - /** - * Encrypts the key represented by the specified request. - * @param request they key request - * @return the encrypted content encryption key. - */ - byte[] encryptKey(EncryptKeyRequest request); - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java new file mode 100644 index 000000000..7fd1f3677 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java @@ -0,0 +1,48 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.impl.IdRegistry; +import io.jsonwebtoken.impl.lang.Registry; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.AeadAlgorithm; + +import java.util.Collection; + +@SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.EncryptionAlgorithms implementation +public class EncryptionAlgorithmsBridge { + + // prevent instantiation + private EncryptionAlgorithmsBridge() { + } + + //For parser implementation - do not expose outside the impl module: + public static final Registry REGISTRY; + + static { + REGISTRY = new IdRegistry<>(Collections.of( + (AeadAlgorithm) new HmacAesAeadAlgorithm(128), + new HmacAesAeadAlgorithm(192), + new HmacAesAeadAlgorithm(256), + new GcmAesAeadAlgorithm(128), + new GcmAesAeadAlgorithm(192), + new GcmAesAeadAlgorithm(256) + )); + } + + public static Collection values() { + return REGISTRY.values(); + } + + public static AeadAlgorithm findById(String id) { + return REGISTRY.apply(id); + } + + public static AeadAlgorithm forId(String id) { + AeadAlgorithm alg = findById(id); + if (alg == null) { + String msg = "Unrecognized JWA AeadAlgorithm identifier: " + id; + throw new UnsupportedJwtException(msg); + } + return alg; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/FamilyJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/FamilyJwkFactory.java new file mode 100644 index 000000000..8738ee273 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/FamilyJwkFactory.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.security.Jwk; + +import java.security.Key; + +public interface FamilyJwkFactory> extends JwkFactory, Identifiable { + + boolean supports(JwkContext context); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java new file mode 100644 index 000000000..ae571e7ad --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java @@ -0,0 +1,94 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.RuntimeEnvironment; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.AeadRequest; +import io.jsonwebtoken.security.AeadResult; +import io.jsonwebtoken.security.DecryptAeadRequest; +import io.jsonwebtoken.security.PayloadSupplier; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.security.spec.AlgorithmParameterSpec; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class GcmAesAeadAlgorithm extends AesAlgorithm implements AeadAlgorithm { + + //TODO: Remove this static block when JDK 7 support is removed + // JDK <= 7 does not support AES GCM mode natively and so BouncyCastle is required + static { + RuntimeEnvironment.enableBouncyCastleIfPossible(); + } + + private static final String TRANSFORMATION_STRING = "AES/GCM/NoPadding"; + + public GcmAesAeadAlgorithm(int keyLength) { + super("A" + keyLength + "GCM", TRANSFORMATION_STRING, keyLength); + } + + @Override + public AeadResult encrypt(final AeadRequest req) throws SecurityException { + + Assert.notNull(req, "Request cannot be null."); + final SecretKey key = assertKey(req); + final byte[] plaintext = Assert.notEmpty(req.getPayload(), "Request payload (plaintext) cannot be null or empty."); + final byte[] aad = getAAD(req); + final byte[] iv = ensureInitializationVector(req); + final AlgorithmParameterSpec ivSpec = getIvSpec(iv); + + byte[] taggedCiphertext = execute(req, Cipher.class, new CheckedFunction() { + @Override + public byte[] apply(Cipher cipher) throws Exception { + cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec); + if (Arrays.length(aad) > 0) { + cipher.updateAAD(aad); + } + return cipher.doFinal(plaintext); + } + }); + + // When using GCM mode, the JDK appends the authentication tag to the ciphertext, so let's extract it: + // (tag has a length of BLOCK_SIZE_BITS): + int ciphertextLength = taggedCiphertext.length - BLOCK_BYTE_SIZE; + byte[] ciphertext = new byte[ciphertextLength]; + System.arraycopy(taggedCiphertext, 0, ciphertext, 0, ciphertextLength); + byte[] tag = new byte[BLOCK_BYTE_SIZE]; + System.arraycopy(taggedCiphertext, ciphertextLength, tag, 0, BLOCK_BYTE_SIZE); + + return new DefaultAeadResult(req.getProvider(), req.getSecureRandom(), ciphertext, key, aad, tag, iv); + } + + @Override + public PayloadSupplier decrypt(final DecryptAeadRequest req) throws SecurityException { + + Assert.notNull(req, "Request cannot be null."); + final SecretKey key = assertKey(req); + final byte[] ciphertext = Assert.notEmpty(req.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty."); + final byte[] aad = getAAD(req); + final byte[] tag = Assert.notEmpty(req.getDigest(), "Decryption request authentication tag cannot be null or empty."); + final byte[] iv = assertDecryptionIv(req); + final AlgorithmParameterSpec ivSpec = getIvSpec(iv); + + //for tagged GCM, the JCA spec requires that the tag be appended to the end of the ciphertext byte array: + final byte[] taggedCiphertext = Bytes.concat(ciphertext, tag); + + byte[] plaintext = execute(req, Cipher.class, new CheckedFunction() { + @Override + public byte[] apply(Cipher cipher) throws Exception { + cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); + if (Arrays.length(aad) > 0) { + cipher.updateAAD(aad); + } + return cipher.doFinal(taggedCiphertext); + } + }); + + return new DefaultPayloadSupplier<>(plaintext); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesEncryptionAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesEncryptionAlgorithm.java deleted file mode 100644 index df8cdba15..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesEncryptionAlgorithm.java +++ /dev/null @@ -1,106 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.RuntimeEnvironment; -import io.jsonwebtoken.security.AeadIvRequest; -import io.jsonwebtoken.security.AeadRequest; -import io.jsonwebtoken.security.AeadIvEncryptionResult; - -import javax.crypto.Cipher; -import javax.crypto.SecretKey; -import javax.crypto.spec.GCMParameterSpec; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class GcmAesEncryptionAlgorithm extends AbstractAeadAesEncryptionAlgorithm { - - //TODO: Remove this static block when JDK 7 support is removed - // JDK <= 7 does not support AES GCM mode natively and so BouncyCastle is required - static { - RuntimeEnvironment.enableBouncyCastleIfPossible(); - } - - private static final int GCM_IV_SIZE_BITS = 96; // https://tools.ietf.org/html/rfc7518#section-5.3 - private static final String TRANSFORMATION_STRING = "AES/GCM/NoPadding"; - - public GcmAesEncryptionAlgorithm(String name, int requiredKeyLengthInBits) { - super(name, TRANSFORMATION_STRING, GCM_IV_SIZE_BITS, requiredKeyLengthInBits); - //Standard AES only supports 128, 192, and 256 key lengths, respectively: - Assert.isTrue(requiredKeyLengthInBits == 128 || requiredKeyLengthInBits == 192 || requiredKeyLengthInBits == 256, "Invalid AES Key length."); - } - - @Override - protected AeadIvEncryptionResult doEncrypt(final AeadRequest req) throws Exception { - - //Ensure IV: - final byte[] iv = ensureInitializationVector(req); - - //Ensure Key: - final SecretKey encryptionKey = assertKey(req); - - //See if there is any AAD: - final byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty - - byte[] ciphertext = newCipherTemplate(req).execute(new CipherCallback() { - @Override - public byte[] doWithCipher(Cipher cipher) throws Exception { - cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new GCMParameterSpec(AES_BLOCK_SIZE_BITS, iv)); - if (aad != null) { - cipher.updateAAD(aad); - } - return cipher.doFinal(req.getData()); - } - }); - - // When using GCM mode, the JDK actually appends the authentication tag to the ciphertext, so let's - // represent this appropriately: - byte[] taggedCiphertext = ciphertext; - - // Now separate the tag from the ciphertext (tag has a length of AES_BLOCK_SIZE_BITS): - int ciphertextLength = taggedCiphertext.length - AES_BLOCK_SIZE_BYTES; - ciphertext = new byte[ciphertextLength]; - System.arraycopy(taggedCiphertext, 0, ciphertext, 0, ciphertextLength); - - byte[] tag = new byte[AES_BLOCK_SIZE_BYTES]; - System.arraycopy(taggedCiphertext, ciphertextLength, tag, 0, AES_BLOCK_SIZE_BYTES); - - return new DefaultAeadIvEncryptionResult(ciphertext, iv, tag); - } - - @Override - protected byte[] doDecrypt(AeadIvRequest req) throws Exception { - - final byte[] tag = req.getAuthenticationTag(); - Assert.notEmpty(tag, "AeadDecryptionRequests must include a non-empty authentication tag."); - - final byte[] iv = assertDecryptionIv(req); - - //Ensure Key: - final SecretKey decryptionKey = assertKey(req); - - //See if there is any AAD: - final byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty - - final byte[] ciphertext = req.getData(); - - return newCipherTemplate(req).execute(new CipherCallback() { - @Override - public byte[] doWithCipher(Cipher cipher) throws Exception { - cipher.init(Cipher.DECRYPT_MODE, decryptionKey, new GCMParameterSpec(AES_BLOCK_SIZE_BITS, iv)); - - if (aad != null) { - cipher.updateAAD(aad); - } - - //for tagged GCM, the JVM spec requires that the tag be appended to the end of the ciphertext - //byte array. So we'll append it here: - byte[] taggedCiphertext = new byte[ciphertext.length + tag.length]; - System.arraycopy(ciphertext, 0, taggedCiphertext, 0, ciphertext.length); - System.arraycopy(tag, 0, taggedCiphertext, ciphertext.length, tag.length); - - return cipher.doFinal(taggedCiphertext); - } - }); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/GetKeyRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/GetKeyRequest.java deleted file mode 100644 index bb4e1f7d8..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/GetKeyRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.EncryptionAlgorithm; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface GetKeyRequest { - - /** - * Returns the encryption algorithm that will be used to encrypt the JWE payload. A {@link KeyManagementMode} - * implementation can inspect this to return or generate a key that matches the required algorithm key length. - * - * @return the encryption algorithm that will be used to encrypt the JWE payload. - */ - EncryptionAlgorithm getEncryptionAlgorithm(); -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java new file mode 100644 index 000000000..6f14555cb --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java @@ -0,0 +1,151 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.AeadRequest; +import io.jsonwebtoken.security.AeadResult; +import io.jsonwebtoken.security.CryptoRequest; +import io.jsonwebtoken.security.DecryptAeadRequest; +import io.jsonwebtoken.security.PayloadSupplier; +import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.SignatureRequest; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.MessageDigest; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class HmacAesAeadAlgorithm extends AesAlgorithm implements AeadAlgorithm { + + private static final String TRANSFORMATION_STRING = "AES/CBC/PKCS5Padding"; + + private final MacSignatureAlgorithm SIGALG; + + private static int digestLength(int keyLength) { + return keyLength * 2; + } + + private static String id(int keyLength) { + return "A" + keyLength + "CBC-HS" + digestLength(keyLength); + } + + public HmacAesAeadAlgorithm(String id, MacSignatureAlgorithm sigAlg) { + super(id, TRANSFORMATION_STRING, sigAlg.getMinKeyLength()); + this.SIGALG = sigAlg; + } + + public HmacAesAeadAlgorithm(int keyBitLength) { + this(id(keyBitLength), new MacSignatureAlgorithm(id(keyBitLength), "HmacSHA" + digestLength(keyBitLength), keyBitLength)); + } + + @Override + public SecretKey generateKey() { + return new JcaTemplate("AES", null).generateSecretKey(this.keyBitLength * 2); + } + + byte[] assertKeyBytes(CryptoRequest request) { + SecretKey key = Assert.notNull(request.getKey(), "Request key cannot be null."); + return validateLength(key, this.keyBitLength * 2, true); + } + + @Override + public AeadResult encrypt(final AeadRequest req) { + + Assert.notNull(req, "Request cannot be null."); + + byte[] compositeKeyBytes = assertKeyBytes(req); + int halfCount = compositeKeyBytes.length / 2; // https://tools.ietf.org/html/rfc7518#section-5.2 + byte[] macKeyBytes = Arrays.copyOfRange(compositeKeyBytes, 0, halfCount); + byte[] encKeyBytes = Arrays.copyOfRange(compositeKeyBytes, halfCount, compositeKeyBytes.length); + final SecretKey encryptionKey = new SecretKeySpec(encKeyBytes, "AES"); + + final byte[] plaintext = Assert.notEmpty(req.getPayload(), "Request payload (plaintext) cannot be null or empty."); + final byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty + final byte[] iv = ensureInitializationVector(req); + final AlgorithmParameterSpec ivSpec = getIvSpec(iv); + + final byte[] ciphertext = execute(req, Cipher.class, new CheckedFunction() { + @Override + public byte[] apply(Cipher cipher) throws Exception { + cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, ivSpec); + return cipher.doFinal(plaintext); + } + }); + + byte[] tag = sign(aad, iv, ciphertext, macKeyBytes); + + return new DefaultAeadResult(req.getProvider(), req.getSecureRandom(), ciphertext, encryptionKey, aad, tag, iv); + } + + private byte[] sign(byte[] aad, byte[] iv, byte[] ciphertext, byte[] macKeyBytes) { + + long aadLength = io.jsonwebtoken.lang.Arrays.length(aad); + long aadLengthInBits = aadLength * Byte.SIZE; + long aadLengthInBitsAsUnsignedInt = aadLengthInBits & 0xffffffffL; + byte[] AL = Bytes.toBytes(aadLengthInBitsAsUnsignedInt); + + byte[] toHash = new byte[(int) aadLength + iv.length + ciphertext.length + AL.length]; + + if (aad != null) { + System.arraycopy(aad, 0, toHash, 0, aad.length); + System.arraycopy(iv, 0, toHash, aad.length, iv.length); + System.arraycopy(ciphertext, 0, toHash, aad.length + iv.length, ciphertext.length); + System.arraycopy(AL, 0, toHash, aad.length + iv.length + ciphertext.length, AL.length); + } else { + System.arraycopy(iv, 0, toHash, 0, iv.length); + System.arraycopy(ciphertext, 0, toHash, iv.length, ciphertext.length); + System.arraycopy(AL, 0, toHash, iv.length + ciphertext.length, AL.length); + } + + SecretKey key = new SecretKeySpec(macKeyBytes, SIGALG.getJcaName()); + SignatureRequest request = new DefaultSignatureRequest<>(null, null, toHash, key); + byte[] digest = SIGALG.sign(request); + + // https://tools.ietf.org/html/rfc7518#section-5.2.2.1 #5 requires truncating the signature + // to be the same length as the macKey/encKey: + return assertTag(Arrays.copyOfRange(digest, 0, macKeyBytes.length)); + } + + @Override + public PayloadSupplier decrypt(final DecryptAeadRequest req) { + + Assert.notNull(req, "Request cannot be null."); + + byte[] compositeKeyBytes = assertKeyBytes(req); + int halfCount = compositeKeyBytes.length / 2; // https://tools.ietf.org/html/rfc7518#section-5.2 + byte[] macKeyBytes = Arrays.copyOfRange(compositeKeyBytes, 0, halfCount); + byte[] encKeyBytes = Arrays.copyOfRange(compositeKeyBytes, halfCount, compositeKeyBytes.length); + final SecretKey decryptionKey = new SecretKeySpec(encKeyBytes, "AES"); + + final byte[] ciphertext = Assert.notEmpty(req.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty."); + final byte[] aad = getAAD(req); + final byte[] tag = assertTag(req.getDigest()); + final byte[] iv = assertDecryptionIv(req); + final AlgorithmParameterSpec ivSpec = getIvSpec(iv); + + // Assert that the aad + iv + ciphertext provided, when signed, equals the tag provided, + // thereby verifying none of it has been tampered with: + byte[] digest = sign(aad, iv, ciphertext, macKeyBytes); + if (!MessageDigest.isEqual(digest, tag)) { //constant time comparison to avoid side-channel attacks + String msg = "Ciphertext decryption failed: Authentication tag verification failed."; + throw new SignatureException(msg); + } + + byte[] plaintext = execute(req, Cipher.class, new CheckedFunction() { + @Override + public byte[] apply(Cipher cipher) throws Exception { + cipher.init(Cipher.DECRYPT_MODE, decryptionKey, ivSpec); + return cipher.doFinal(ciphertext); + } + }); + + return new DefaultPayloadSupplier<>(plaintext); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithm.java deleted file mode 100644 index c2e20fc1c..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithm.java +++ /dev/null @@ -1,186 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.AeadIvRequest; -import io.jsonwebtoken.security.AeadIvEncryptionResult; -import io.jsonwebtoken.security.AeadRequest; -import io.jsonwebtoken.security.CryptoRequest; -import io.jsonwebtoken.security.SignatureException; - -import javax.crypto.Cipher; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; -import java.security.Key; -import java.util.Arrays; - -/** - * @since JJWT_RELEASE_VERSION - */ -@SuppressWarnings("unused") //used via reflection in the io.jsonwebtoken.security.EncryptionAlgorithms class -public class HmacAesEncryptionAlgorithm extends AbstractAeadAesEncryptionAlgorithm { - - private static final String TRANSFORMATION_STRING = "AES/CBC/PKCS5Padding"; - - private final MacSignatureAlgorithm SIGALG; - - public HmacAesEncryptionAlgorithm(String name, MacSignatureAlgorithm sigAlg) { - super(name, TRANSFORMATION_STRING, AES_BLOCK_SIZE_BITS, sigAlg.getMinKeyLength() * 2); - this.SIGALG = sigAlg; - } - - @Override - protected SecretKey doGenerateKey() throws Exception { - - int subKeyLength = getRequiredKeyByteLength() / 2; - - byte[] macKeyBytes = this.SIGALG.generateKey().getEncoded(); - Assert.notEmpty(macKeyBytes, "Generated HMAC key byte array cannot be null or empty."); - - if (macKeyBytes.length > subKeyLength) { - byte[] subKeyBytes = new byte[subKeyLength]; - System.arraycopy(macKeyBytes, 0, subKeyBytes, 0, subKeyLength); - macKeyBytes = subKeyBytes; - } - - if (macKeyBytes.length != subKeyLength) { - String msg = "The delegate MacSignatureAlgorithm instance of type {" + SIGALG.getClass().getName() + "} " + - "generated a key " + macKeyBytes.length + " bytes (" + - macKeyBytes.length * Byte.SIZE + " bits) long. The " + getName() + " algorithm requires " + - "SignatureAlgorithm keys to be " + subKeyLength + " bytes (" + - subKeyLength * Byte.SIZE + " bits) long."; - throw new IllegalStateException(msg); - } - - KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); - keyGenerator.init(subKeyLength * Byte.SIZE); - - SecretKey encKey = keyGenerator.generateKey(); - byte[] encKeyBytes = encKey.getEncoded(); - - //return as one single key per https://tools.ietf.org/html/rfc7518#section-5.2.2.1 - - byte[] combinedKeyBytes = new byte[macKeyBytes.length + encKeyBytes.length]; - - System.arraycopy(macKeyBytes, 0, combinedKeyBytes, 0, macKeyBytes.length); - System.arraycopy(encKeyBytes, 0, combinedKeyBytes, macKeyBytes.length, encKeyBytes.length); - - return new SecretKeySpec(combinedKeyBytes, "AES"); - } - - byte[] assertKeyBytes(CryptoRequest request) { - SecretKey key = assertKey(request); - return key.getEncoded(); - } - - @Override - protected AeadIvEncryptionResult doEncrypt(final AeadRequest req) throws Exception { - - //Ensure IV: - final byte[] iv = ensureInitializationVector(req); - - //Ensure Key: - byte[] keyBytes = assertKeyBytes(req); - - //See if there is any AAD: - final byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty - - int halfCount = keyBytes.length / 2; // https://tools.ietf.org/html/rfc7518#section-5.2 - byte[] macKeyBytes = Arrays.copyOfRange(keyBytes, 0, halfCount); - keyBytes = Arrays.copyOfRange(keyBytes, halfCount, keyBytes.length); - - final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES"); - - final byte[] ciphertext = newCipherTemplate(req).execute(new CipherCallback() { - @Override - public byte[] doWithCipher(Cipher cipher) throws Exception { - cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new IvParameterSpec(iv)); - byte[] plaintext = req.getData(); - return cipher.doFinal(plaintext); - } - }); - - byte[] tag = sign(aad, iv, ciphertext, macKeyBytes); - - return new DefaultAeadIvEncryptionResult(ciphertext, iv, tag); - } - - private byte[] sign(byte[] aad, byte[] iv, byte[] ciphertext, byte[] macKeyBytes) { - - long aadLength = io.jsonwebtoken.lang.Arrays.length(aad); - long aadLengthInBits = aadLength * Byte.SIZE; - long aadLengthInBitsAsUnsignedInt = aadLengthInBits & 0xffffffffL; - byte[] AL = toBytes(aadLengthInBitsAsUnsignedInt); - - byte[] toHash = new byte[(int) aadLength + iv.length + ciphertext.length + AL.length]; - - if (aad != null) { - System.arraycopy(aad, 0, toHash, 0, aad.length); - System.arraycopy(iv, 0, toHash, aad.length, iv.length); - System.arraycopy(ciphertext, 0, toHash, aad.length + iv.length, ciphertext.length); - System.arraycopy(AL, 0, toHash, aad.length + iv.length + ciphertext.length, AL.length); - } else { - System.arraycopy(iv, 0, toHash, 0, iv.length); - System.arraycopy(ciphertext, 0, toHash, iv.length, ciphertext.length); - System.arraycopy(AL, 0, toHash, iv.length + ciphertext.length, AL.length); - } - - Key key = new SecretKeySpec(macKeyBytes, SIGALG.getJcaName()); - CryptoRequest request = new DefaultCryptoRequest<>(toHash, key, null, null); - byte[] digest = SIGALG.sign(request); - - // https://tools.ietf.org/html/rfc7518#section-5.2.2.1 #5 requires truncating the signature - // to be the same length as the macKey/encKey: - return Arrays.copyOfRange(digest, 0, macKeyBytes.length); - } - - private static byte[] toBytes(long l) { - byte[] b = new byte[8]; - for (int i = 7; i > 0; i--) { - b[i] = (byte) l; - l >>>= 8; - } - b[0] = (byte) l; - return b; - } - - @Override - protected byte[] doDecrypt(AeadIvRequest req) throws Exception { - - byte[] tag = req.getAuthenticationTag(); - Assert.notEmpty(tag, "AeadDecryptionRequests must include a non-empty authentication tag."); - - final byte[] iv = assertDecryptionIv(req); - - //Ensure Key: - byte[] keyBytes = assertKeyBytes(req); - - //See if there is any AAD: - byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty - - int halfCount = keyBytes.length / 2; // https://tools.ietf.org/html/rfc7518#section-5.2 - byte[] macKeyBytes = Arrays.copyOfRange(keyBytes, 0, halfCount); - keyBytes = Arrays.copyOfRange(keyBytes, halfCount, keyBytes.length); - - final SecretKey decryptionKey = new SecretKeySpec(keyBytes, "AES"); - - final byte[] ciphertext = req.getData(); - - // Assert that the aad + iv + ciphertext provided, when signed, equals the tag provided, - // thereby indicating none of it has been tampered with: - byte[] digest = sign(aad, iv, ciphertext, macKeyBytes); - if (!Arrays.equals(digest, tag)) { - String msg = "Ciphertext decryption failed: Authentication tag verification failed."; - throw new SignatureException(msg); - } - - return newCipherTemplate(req).execute(new CipherCallback() { - @Override - public byte[] doWithCipher(Cipher cipher) throws Exception { - cipher.init(Cipher.DECRYPT_MODE, decryptionKey, new IvParameterSpec(iv)); - return cipher.doFinal(ciphertext); - } - }); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JcaPbeKey.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JcaPbeKey.java new file mode 100644 index 000000000..aa2e147de --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JcaPbeKey.java @@ -0,0 +1,51 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.PbeKey; + +import javax.crypto.interfaces.PBEKey; +import javax.security.auth.DestroyFailedException; + +public class JcaPbeKey implements PbeKey { + + private final PBEKey jcaKey; + + public JcaPbeKey(PBEKey jcaKey) { + this.jcaKey = Assert.notNull(jcaKey, "PBEKey cannot be null."); + } + + @Override + public char[] getPassword() { + return this.jcaKey.getPassword(); + } + + @Override + public int getIterations() { + return this.jcaKey.getIterationCount(); + } + + @Override + public String getAlgorithm() { + return this.jcaKey.getAlgorithm(); + } + + @Override + public String getFormat() { + return this.jcaKey.getFormat(); + } + + @Override + public byte[] getEncoded() { + return this.jcaKey.getEncoded(); + } + + @Override + public void destroy() throws DestroyFailedException { + this.jcaKey.destroy(); + } + + @Override + public boolean isDestroyed() { + return this.jcaKey.isDestroyed(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java new file mode 100644 index 000000000..cc4bc6a76 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java @@ -0,0 +1,130 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Classes; +import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.SignatureException; + +import javax.crypto.KeyGenerator; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.Signature; + +public class JcaTemplate { + + private final String jcaName; + private final Provider provider; + private final SecureRandom secureRandom; + + JcaTemplate(String jcaName, Provider provider) { + this(jcaName, provider, Randoms.secureRandom()); + } + + JcaTemplate(String jcaName, Provider provider, SecureRandom secureRandom) { + Assert.hasText(jcaName, "jcaName string cannot be null or empty."); + this.jcaName = jcaName; + this.provider = provider; + this.secureRandom = Assert.notNull(secureRandom, "SecureRandom cannot be null."); + } + + public R execute(Class clazz, CheckedFunction fn) throws SecurityException { + return execute(new JcaInstanceSupplier<>(clazz, this.jcaName, this.provider), fn); + } + + public SecretKey generateSecretKey(final int keyBitLength) { + return execute(KeyGenerator.class, new CheckedFunction() { + @Override + public SecretKey apply(KeyGenerator generator) { + generator.init(keyBitLength, secureRandom); + return generator.generateKey(); + } + }); + } + + public KeyPair generateKeyPair(final int keyBitLength) { + return execute(KeyPairGenerator.class, new CheckedFunction() { + @Override + public KeyPair apply(KeyPairGenerator generator) { + generator.initialize(keyBitLength, secureRandom); + return generator.generateKeyPair(); + } + }); + } + + private R execute(JcaInstanceSupplier supplier, CheckedFunction callback) throws SecurityException { + try { + T instance = supplier.getInstance(); + return callback.apply(instance); + } catch (SecurityException se) { + throw se; //propagate + } catch (Exception e) { + throw new SecurityException(supplier.getName() + " callback execution failed: " + e.getMessage(), e); + } + } + + private interface InstanceSupplier { + T getInstance() throws Exception; + } + + //visible for testing + static class JcaInstanceSupplier implements InstanceSupplier { + + private static final String METHOD_NAME = "getInstance"; + private static final Class[] PROVIDER_ARGS = new Class[]{String.class, Provider.class}; + private static final Class[] NAME_ARGS = new Class[]{String.class}; + + private final Class clazz; + private final String name; + private final String jcaName; + private final Provider provider; + + JcaInstanceSupplier(Class clazz, String jcaName, Provider provider) { + this.clazz = Assert.notNull(clazz, "Clazz cannot be null."); + this.name = clazz.getSimpleName(); + this.jcaName = jcaName; + Assert.hasText(jcaName, "jcaName cannot be null or empty."); + this.provider = provider; + } + + public String getName() { + return name; + } + + @Override + public final T getInstance() throws Exception { + try { + return doGetInstance(); + } catch (Exception e) { + String msg = "Unable to obtain " + this.name + " instance from "; + if (this.provider != null) { + msg += "specified Provider {" + this.provider + "} "; + } else { + msg += "default JCA Provider "; + } + msg += "for JCA algorithm '" + this.jcaName + "': " + e.getMessage(); + throw wrap(msg, e); + } + } + + protected Exception wrap(String msg, Exception cause) { + if (cause instanceof SecurityException) { + return cause; + } + if (Signature.class.isAssignableFrom(clazz) || Mac.class.isAssignableFrom(clazz)) { + return new SignatureException(msg, cause); + } + return new SecurityException(msg, cause); + } + + protected T doGetInstance() { + return provider != null ? + Classes.invokeStatic(clazz, METHOD_NAME, PROVIDER_ARGS, jcaName, provider) : + Classes.invokeStatic(clazz, METHOD_NAME, NAME_ARGS, jcaName); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkBuilders.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkBuilders.java new file mode 100644 index 000000000..a4854e42d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkBuilders.java @@ -0,0 +1,16 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.ProtoJwkBuilder; + +// Implementation bridge to concrete implementations so the API module doesn't need to know their +// internals. The API module just needs to call this class via reflection, and the internal Classes/Subclasses +// can change without requiring an API module change. +public final class JwkBuilders { + + private JwkBuilders() { + } + + public static ProtoJwkBuilder builder() { + return new DefaultProtoJwkBuilder<>(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java new file mode 100644 index 000000000..d6c84ac83 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java @@ -0,0 +1,63 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.Identifiable; + +import java.net.URI; +import java.security.Key; +import java.security.Provider; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public interface JwkContext extends Identifiable, Map { + + JwkContext setId(String id); + + String getType(); + + JwkContext setType(String type); + + Set getOperations(); + + JwkContext setOperations(Set operations); + + String getAlgorithm(); + + JwkContext setAlgorithm(String algorithm); + + String getPublicKeyUse(); + + JwkContext setPublicKeyUse(String use); + + URI getX509Url(); + + JwkContext setX509Url(URI url); + + List getX509CertificateChain(); + + JwkContext setX509CertificateChain(List x5c); + + byte[] getX509CertificateSha1Thumbprint(); + + JwkContext setX509CertificateSha1Thumbprint(byte[] x5t); + + byte[] getX509CertificateSha256Thumbprint(); + + JwkContext setX509CertificateSha256Thumbprint(byte[] x5ts256); + + K getKey(); + + JwkContext setKey(K key); + + PublicKey getPublicKey(); + + JwkContext setPublicKey(PublicKey publicKey); + + Set getPrivateMemberNames(); + + Provider getProvider(); + + JwkContext setProvider(Provider provider); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java deleted file mode 100644 index 48566900a..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import java.security.Key; -import java.util.Map; - -public interface JwkConverter { - - Key toKey(Map jwk); - - Map toJwk(Key key); -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkFactory.java new file mode 100644 index 000000000..aca1ae239 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkFactory.java @@ -0,0 +1,10 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.Jwk; + +import java.security.Key; + +public interface JwkFactory> { + + J createJwk(JwkContext ctx); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkValidator.java deleted file mode 100644 index df75bcc20..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkValidator.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.Jwk; -import io.jsonwebtoken.security.KeyException; - -public interface JwkValidator { - - void validate(T jwk) throws KeyException; -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkX509StringConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkX509StringConverter.java new file mode 100644 index 000000000..23602ecab --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkX509StringConverter.java @@ -0,0 +1,53 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.Converter; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.MalformedKeyException; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +public class JwkX509StringConverter implements Converter { + + static final JwkX509StringConverter INSTANCE = new JwkX509StringConverter(); + + // Returns a Base64 encoded (NOT Base64Url encoded) string of the cert's encoded byte array + // per https://datatracker.ietf.org/doc/html/rfc7517#section-4.7 + @Override + public String applyTo(X509Certificate cert) { + Assert.notNull(cert, "X509Certificate cannot be null."); + byte[] der; + try { + der = cert.getEncoded(); + } catch (CertificateEncodingException e) { + String msg = "Unable to access X509Certificate encoded bytes necessary to perform DER " + + "Base64-encoding. Certificate: {" + cert + "}. Cause: " + e.getMessage(); + throw new InvalidKeyException(msg, e); + } + if (Arrays.length(der) == 0) { + String msg = "X509Certificate encoded bytes cannot be null or empty. Certificate: {" + cert + "}."; + throw new InvalidKeyException(msg); + } + return Encoders.BASE64.encode(der); + } + + @Override + public X509Certificate applyFrom(String s) { + try { + byte[] der = Decoders.BASE64.decode(s); //RFC requires Base64, not Base64Url + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + InputStream stream = new ByteArrayInputStream(der); + return (X509Certificate)cf.generateCertificate(stream); + } catch (Exception e) { + String msg = "Unable to convert String value '" + s + "' to X509Certificate instance: " + e.getMessage(); + throw new MalformedKeyException(msg, e); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAgreementWithKeyWrappingMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAgreementWithKeyWrappingMode.java deleted file mode 100644 index 579df1a5c..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAgreementWithKeyWrappingMode.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.jsonwebtoken.impl.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class KeyAgreementWithKeyWrappingMode extends RandomEncryptedKeyMode { - - @Override - public byte[] encryptKey(EncryptKeyRequest request) { - throw new UnsupportedOperationException("Not Yet Implemented"); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java new file mode 100644 index 000000000..96fb29087 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java @@ -0,0 +1,244 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.impl.DefaultJweHeader; +import io.jsonwebtoken.impl.IdRegistry; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.Registry; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.DecryptionKeyRequest; +import io.jsonwebtoken.security.EncryptionAlgorithms; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.PbeKey; +import io.jsonwebtoken.security.SecurityException; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.MGF1ParameterSpec; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.KeyAlgorithms implementation +public final class KeyAlgorithmsBridge { + + // prevent instantiation + private KeyAlgorithmsBridge() { + } + + private static final String RSA1_5_ID = "RSA1_5"; + private static final String RSA1_5_TRANSFORMATION = "RSA/ECB/PKCS1Padding"; + private static final String RSA_OAEP_ID = "RSA-OAEP"; + private static final String RSA_OAEP_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + private static final String RSA_OAEP_256_ID = "RSA-OAEP-256"; + private static final String RSA_OAEP_256_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; + private static final AlgorithmParameterSpec RSA_OAEP_256_SPEC = + new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT); + + //For parser implementation - do not expose outside the impl module + public static final Registry> REGISTRY; + + static { + REGISTRY = new IdRegistry<>(Collections.>of( + new DirectKeyAlgorithm(), + new AesWrapKeyAlgorithm(128), + new AesWrapKeyAlgorithm(192), + new AesWrapKeyAlgorithm(256), + new AesGcmKeyAlgorithm(128), + new AesGcmKeyAlgorithm(192), + new AesGcmKeyAlgorithm(256), + new Pbes2HsAkwAlgorithm(128), + new Pbes2HsAkwAlgorithm(192), + new Pbes2HsAkwAlgorithm(256), + new DefaultRsaKeyAlgorithm<>(RSA1_5_ID, RSA1_5_TRANSFORMATION), + new DefaultRsaKeyAlgorithm<>(RSA_OAEP_ID, RSA_OAEP_TRANSFORMATION), + new DefaultRsaKeyAlgorithm<>(RSA_OAEP_256_ID, RSA_OAEP_256_TRANSFORMATION, RSA_OAEP_256_SPEC) + )); + } + + public static Collection> values() { + return REGISTRY.values(); + } + + public static KeyAlgorithm findById(String id) { + return REGISTRY.apply(id); + } + + public static KeyAlgorithm forId(String id) { + KeyAlgorithm instance = findById(id); + if (instance == null) { + String msg = "Unrecognized JWA KeyAlgorithm identifier: " + id; + throw new UnsupportedJwtException(msg); + } + return instance; + } + + private static KeyAlgorithm lean(final Pbes2HsAkwAlgorithm alg) { + + // ensure we use the same key factory over and over so that time spent acquiring one is not repeated: + JcaTemplate template = new JcaTemplate(alg.getJcaName(), null, Randoms.secureRandom()); + final SecretKeyFactory factory = template.execute(SecretKeyFactory.class, new CheckedFunction() { + @Override + public SecretKeyFactory apply(SecretKeyFactory secretKeyFactory) { + return secretKeyFactory; + } + }); + + // pre-compute the salt so we don't spend time doing that on each iteration. Doesn't need to be random for a + // computation-only test: + final byte[] rfcSalt = alg.toRfcSalt(alg.generateInputSalt(null)); + + // ensure that the bare minimum steps are performed to hash, ensuring our time sampling pertains only to + // hashing and not ancillary steps needed to setup the hashing/derivation + return new KeyAlgorithm() { + @Override + public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + int iterations = request.getKey().getIterations(); + char[] password = request.getKey().getPassword(); + try { + alg.deriveKey(factory, password, rfcSalt, iterations); + } catch (Exception e) { + throw new SecurityException("Unable to derive key", e); + } + return null; + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + throw new UnsupportedOperationException("Not intended to be called."); + } + + @Override + public String getId() { + return alg.getId(); + } + }; + } + + private static char randomChar() { + return (char) Randoms.secureRandom().nextInt(Character.MAX_VALUE); + } + + private static char[] randomChars(int length) { + char[] chars = new char[length]; + for (int i = 0; i < length; i++) { + chars[i] = randomChar(); + } + return chars; + } + + public static int estimateIterations(KeyAlgorithm alg, long desiredMillis) { + + // The number of computational samples that land in our 'sweet spot' timing range matching desiredMillis. + // These samples will be averaged and the final average will be the return value of this method + // representing the number of iterations that should be taken for any given PBE hashing attempt to get + // reasonably close to desiredMillis: + final int NUM_SAMPLES = 30; + final int SKIP = 3; + // More important than the actual password (or characters) is the password length. + // 8 characters is a commonly-found minimum required length in many systems circa 2021. + final int PASSWORD_LENGTH = 8; + + final JweHeader HEADER = new DefaultJweHeader(); // not used during execution, needed to satisfy API call. + final AeadAlgorithm ENC_ALG = EncryptionAlgorithms.A128GCM; // not used, needed to satisfy API + + if (alg instanceof Pbes2HsAkwAlgorithm) { + // Strip away all things that cause time during computation except for the actual hashing algorithm: + alg = lean((Pbes2HsAkwAlgorithm) alg); + } + + int workFactor = 1000; // same as iterations for PBKDF2. Different concept for Bcrypt/Scrypt + int minWorkFactor = workFactor; + List points = new ArrayList<>(NUM_SAMPLES); + for (int i = 0; points.size() < NUM_SAMPLES; i++) { + + char[] password = randomChars(PASSWORD_LENGTH); + PbeKey pbeKey = Keys.forPbe().setPassword(password).setIterations(workFactor).build(); + KeyRequest request = new DefaultKeyRequest<>(null, null, pbeKey, HEADER, ENC_ALG); + + long start = System.currentTimeMillis(); + alg.getEncryptionKey(request); // <-- Computation occurs here. Don't need the result, just need to exec + long end = System.currentTimeMillis(); + long duration = end - start; + + // Exclude the first SKIP number of attempts from the average due to initial JIT optimization/slowness. + // After a few attempts, the JVM should be relatively optimized and the subsequent + // PBE hashing times are the ones we want to include in our analysis + boolean warmedUp = i >= SKIP; + + // how close we were on this hashing attempt to reach our desiredMillis target: + // A number under 1 means we weren't slow enough, a number greater than 1 means we were too slow: + double durationPercentAchieved = (double) duration / (double) desiredMillis; + + // we only want to collect timing samples if : + // 1. we're warmed up (to account for JIT optimization) + // 2. The attempt time at least met (>=) the desiredMillis target + boolean collectSample = warmedUp && duration >= desiredMillis; + if (collectSample) { + // For each attempt, the x axis is the workFactor, and the y axis is how long it took to compute: + points.add(new Point(workFactor, duration)); + //System.out.println("Collected point: workFactor=" + workFactor + ", duration=" + duration + " ms, %achieved=" + durationPercentAchieved); + } else { + minWorkFactor = Math.max(minWorkFactor, workFactor); + //System.out.println(" Excluding sample: workFactor=" + workFactor + ", duration=" + duration + " ms, %achieved=" + durationPercentAchieved); + } + + // amount to increase or decrease the workFactor for the next hashing iteration. We increase if + // we haven't met the desired millisecond time, and decrease if we're over it a little too much, always + // trying to stay in that desired timing sweet spot + double percentAdjust = workFactor * 0.0075; // 3/4ths of a percent + if (durationPercentAchieved < 1d) { + // Under target. Let's increase by the amount that should get right at (or near) 100%: + double ratio = desiredMillis / (double) duration; + if (ratio > 1) { + double result = workFactor * ratio; + workFactor = (int) result; + } else { + double difference = workFactor * (1 - durationPercentAchieved); + workFactor += Math.max(percentAdjust, difference); + } + } else if (durationPercentAchieved > 1.01d) { + // Over target. Let's decrease gently to get closer. + double difference = workFactor * (durationPercentAchieved - 1.01); + difference = Math.min(percentAdjust, difference); + // math.max here because the min allowed is 1000 per the JWA RFC, so we never want to go below that. + workFactor = (int) Math.max(1000, workFactor - difference); + } else { + // we're at our target (desiredMillis); let's increase by a teeny bit to see where we get + // (and the JVM might optimize with the same inputs, so we want to prevent that here) + workFactor += 100; + } + } + + // We've collected all of our samples, now let's find the workFactor average number + // That average is the best estimate for ensuring PBE hashes for the specified algorithm meet the + // desiredMillis target on the current JVM/CPU platform: + double sumX = 0; + for (Point p : points) { + sumX += p.x; + } + double average = sumX / points.size(); + //ensure our average is at least as much as the smallest work factor that got us closest to desiredMillis: + return (int) Math.max(average, minWorkFactor); + } + + private static class Point { + long x; + long y; + double lnY; + + public Point(long x, long y) { + this.x = x; + this.y = y; + this.lnY = Math.log((double) y); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyEncryptionMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyEncryptionMode.java deleted file mode 100644 index ddec0534c..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyEncryptionMode.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.jsonwebtoken.impl.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class KeyEncryptionMode extends RandomEncryptedKeyMode { - - @Override - public byte[] encryptKey(EncryptKeyRequest request) { - throw new UnsupportedOperationException("Not Yet Implemented."); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementMode.java deleted file mode 100644 index 2a0093363..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementMode.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import javax.crypto.SecretKey; - -/** - * A Key Management Mode determines the content encryption key to use to encrypt a JWE's payload. - *

    - * If a mode encrypts the encryption key itself for one or more recipients, that mode would implement the - * {@link EncryptedKeyManagementMode} instead of this interface. - * - * @see EncryptedKeyManagementMode - * @since JJWT_RELEASE_VERSION - */ -public interface KeyManagementMode { - - /** - * Returns the key used to encrypt the JWE payload. - * - * @return the key used to encrypt the JWE payload. - */ - SecretKey getKey(GetKeyRequest request); -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementModes.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementModes.java deleted file mode 100644 index 1c327542f..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyManagementModes.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import javax.crypto.SecretKey; - -/** - * @since JJWT_RELEASE_VERSION - */ -public final class KeyManagementModes { - - private KeyManagementModes(){} - - public static KeyManagementMode direct(SecretKey secretKey) { - return new DirectEncryptionMode(secretKey); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyUsage.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyUsage.java new file mode 100644 index 000000000..4e078f301 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyUsage.java @@ -0,0 +1,64 @@ +package io.jsonwebtoken.impl.security; + +import java.security.cert.X509Certificate; + +public final class KeyUsage { + + private static final boolean[] NO_FLAGS = new boolean[9]; + + // Direct from X509Certificate#getKeyUsage() JavaDoc. For an understand of when/how to use these + // flags, read https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 + private static final int + digitalSignature = 0, + nonRepudiation = 1, + keyEncipherment = 2, + dataEncipherment = 3, + keyAgreement = 4, + keyCertSign = 5, + cRLSign = 6, + encipherOnly = 7, //if keyAgreement, then only encipher data during key agreement + decipherOnly = 8; //if keyAgreement, then only decipher data during key agreement + + private final boolean[] is; //for readability: i.e. is[nonRepudiation] simulates isNonRepudiation, etc. + + public KeyUsage(X509Certificate cert) { + boolean[] arr = cert != null ? cert.getKeyUsage() : NO_FLAGS; + this.is = arr != null ? arr : NO_FLAGS; + } + + public boolean isDigitalSignature() { + return is[digitalSignature]; + } + + public boolean isNonRepudiation() { + return is[nonRepudiation]; + } + + public boolean isKeyEncipherment() { + return is[keyEncipherment]; + } + + public boolean isDataEncipherment() { + return is[dataEncipherment]; + } + + public boolean isKeyAgreement() { + return is[keyAgreement]; + } + + public boolean isKeyCertSign() { + return is[keyCertSign]; + } + + public boolean isCRLSign() { + return is[cRLSign]; + } + + public boolean isEncipherOnly() { + return is[encipherOnly]; + } + + public boolean isDecipherOnly() { + return is[decipherOnly]; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyUseStrategy.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyUseStrategy.java new file mode 100644 index 000000000..ee74ae088 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyUseStrategy.java @@ -0,0 +1,8 @@ +package io.jsonwebtoken.impl.security; + +//TODO: Make a non-impl concept? +public interface KeyUseStrategy { + + //TODO: change argument to have more information? + String toJwkValue(KeyUsage keyUses); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyWrappingMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyWrappingMode.java deleted file mode 100644 index a4d3ccd1e..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyWrappingMode.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.jsonwebtoken.impl.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class KeyWrappingMode extends RandomEncryptedKeyMode { - - @Override - public byte[] encryptKey(EncryptKeyRequest request) { - throw new UnsupportedOperationException("Not Yet Implemented"); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java new file mode 100644 index 000000000..32c7cfd7d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java @@ -0,0 +1,22 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.PbeKey; +import io.jsonwebtoken.security.PbeKeyBuilder; + +import javax.crypto.interfaces.PBEKey; + +@SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.Keys implementation +public class KeysBridge { + + // prevent instantiation + private KeysBridge() { + } + + public static PbeKey toPbeKey(PBEKey key) { + return new JcaPbeKey(key); + } + + public static PbeKeyBuilder forPbe() { + return new DefaultPbeKeyBuilder(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java index 42da42ae6..abcd949f3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java @@ -1,30 +1,27 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.CryptoRequest; import io.jsonwebtoken.security.InvalidKeyException; -import io.jsonwebtoken.security.SignatureException; -import io.jsonwebtoken.security.SymmetricKeySignatureAlgorithm; +import io.jsonwebtoken.security.SecretKeySignatureAlgorithm; +import io.jsonwebtoken.security.SignatureRequest; import io.jsonwebtoken.security.WeakKeyException; -import javax.crypto.KeyGenerator; import javax.crypto.Mac; import javax.crypto.SecretKey; import java.security.Key; -import java.security.NoSuchAlgorithmException; -import java.security.Provider; import java.util.LinkedHashSet; +import java.util.Locale; import java.util.Set; -@SuppressWarnings("unused") //used via reflection in the io.jsonwebtoken.security.SignatureAlgorithms class -public class MacSignatureAlgorithm extends AbstractSignatureAlgorithm implements SymmetricKeySignatureAlgorithm { +public class MacSignatureAlgorithm extends AbstractSignatureAlgorithm implements SecretKeySignatureAlgorithm { private final int minKeyLength; //in bits - private static final Set JWA_STANDARD_NAMES = new LinkedHashSet<>(Collections.of("HS256", "HS384", "HS512")); + private static final Set JWA_STANDARD_IDS = new LinkedHashSet<>(Collections.of("HS256", "HS384", "HS512")); // PKCS12 OIDs are added to these lists per https://bugs.openjdk.java.net/browse/JDK-8243551 private static final Set HS256_JCA_NAMES = new LinkedHashSet<>(Collections.of("HMACSHA256", "1.2.840.113549.2.9")); @@ -41,8 +38,12 @@ public class MacSignatureAlgorithm extends AbstractSignatureAlgorithm implements VALID_HS256_JCA_NAMES.addAll(VALID_HS384_JCA_NAMES); } - public MacSignatureAlgorithm(String name, String jcaName, int minKeyLength) { - super(name, jcaName); + public MacSignatureAlgorithm(int digestBitLength) { + this("HS" + digestBitLength, "HmacSHA" + digestBitLength, digestBitLength); + } + + public MacSignatureAlgorithm(String id, String jcaName, int minKeyLength) { + super(id, jcaName); Assert.isTrue(minKeyLength > 0, "minKeyLength must be greater than zero."); this.minKeyLength = minKeyLength; } @@ -52,59 +53,16 @@ int getMinKeyLength() { } private boolean isJwaStandard() { - return JWA_STANDARD_NAMES.contains(getName()); + return JWA_STANDARD_IDS.contains(getId()); } private boolean isJwaStandardJcaName(String jcaName) { - return VALID_HS256_JCA_NAMES.contains(jcaName.toUpperCase()); - } - - //For testing - KeyGenerator doGetKeyGenerator(String jcaName) throws NoSuchAlgorithmException { - return KeyGenerator.getInstance(jcaName); - } - - private KeyGenerator getKeyGenerator() { - String jcaName = getJcaName(); - try { - return doGetKeyGenerator(jcaName); - } catch (NoSuchAlgorithmException e) { - String msg = "There is no JCA Provider available that supports the algorithm name '" + jcaName + - "'. Ensure this is a JCA standard name or you have registered a JCA security provider that " + - "supports this name."; - throw new UnsupportedOperationException(msg, e); - } + return VALID_HS256_JCA_NAMES.contains(jcaName.toUpperCase(Locale.ENGLISH)); } @Override public SecretKey generateKey() { - KeyGenerator generator = getKeyGenerator(); - generator.init(Randoms.secureRandom()); - return generator.generateKey(); - } - - //For testing - Mac doGetMacInstance(String jcaName, Provider provider) throws NoSuchAlgorithmException { - return provider == null ? - Mac.getInstance(jcaName) : - Mac.getInstance(jcaName, provider); - } - - private Mac getMacInstance(CryptoRequest req) { - Provider provider = req.getProvider(); - String jcaName = getJcaName(); - try { - return doGetMacInstance(jcaName, provider); - } catch (NoSuchAlgorithmException e) { - String msg; - if (provider != null) { - msg = "The specified JCA Provider {" + provider + "} does not support "; - } else { - msg = "There is no JCA Provider available that supports "; - } - msg += "MAC algorithm name '" + jcaName + "'."; - throw new SignatureException(msg, e); - } + return new JcaTemplate(getJcaName(), null).generateSecretKey(minKeyLength); } @Override @@ -124,7 +82,7 @@ protected void validateKey(Key k, boolean signing) { final SecretKey key = (SecretKey) k; - final String name = getName(); + final String id = getId(); String alg = key.getAlgorithm(); if (!Strings.hasText(alg)) { @@ -135,47 +93,54 @@ protected void validateKey(Key k, boolean signing) { //assert key's jca name is valid if it's a JWA standard algorithm: if (isJwaStandard() && !isJwaStandardJcaName(alg)) { throw new InvalidKeyException("The " + keyType + " key's algorithm '" + alg + "' does not equal a valid " + - "HmacSHA* algorithm name or PKCS12 OID and cannot be used with " + name + "."); + "HmacSHA* algorithm name or PKCS12 OID and cannot be used with " + id + "."); } byte[] encoded = null; // https://github.com/jwtk/jjwt/issues/478 // - // Some HSM modules will not allow applications or libraries to obtain the secret key's encoded bytes. In - // these cases, key length assertions cannot be made, so we'll need to skip the key length checks if so. + // Some KeyStore implementations (like Hardware Security Modules and later versions of Android) will not allow + // applications or libraries to obtain the secret key's encoded bytes. In these cases, key length assertions + // cannot be made, so we'll need to skip the key length checks if so. try { encoded = key.getEncoded(); } catch (Exception ignored) { } - if (encoded != null) { //we can perform key length assertions - int size = Arrays.length(encoded) * Byte.SIZE; - if (size < this.minKeyLength) { - String msg = "The " + keyType + " key's size is " + size + " bits which " + - "is not secure enough for the " + name + " algorithm."; - - if (isJwaStandard() && isJwaStandardJcaName(getJcaName())) { //JWA standard algorithm name - reference the spec: - msg += " The JWT " + - "JWA Specification (RFC 7518, Section 3.2) states that keys used with " + name + " MUST have a " + - "size >= " + minKeyLength + " bits (the key size must be greater than or equal to the hash " + - "output size). Consider using the SignatureAlgorithms." + name + ".generateKey() " + - "method to create a key guaranteed to be secure enough for " + name + ". See " + - "https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; - } else { //custom algorithm - just indicate required key length: - msg += " The " + name + " algorithm requires keys to have a size >= " + minKeyLength + " bits."; - } - - throw new WeakKeyException(msg); + // We can only perform length validation if key.getEncoded() is not null or does not throw an exception + // per https://github.com/jwtk/jjwt/issues/478 and https://github.com/jwtk/jjwt/issues/619 + // so return early if we can't: + if (encoded == null) return; + + int size = Arrays.length(encoded) * Byte.SIZE; + if (size < this.minKeyLength) { + String msg = "The " + keyType + " key's size is " + size + " bits which " + + "is not secure enough for the " + id + " algorithm."; + + if (isJwaStandard() && isJwaStandardJcaName(getJcaName())) { //JWA standard algorithm name - reference the spec: + msg += " The JWT " + + "JWA Specification (RFC 7518, Section 3.2) states that keys used with " + id + " MUST have a " + + "size >= " + minKeyLength + " bits (the key size must be greater than or equal to the hash " + + "output size). Consider using the SignatureAlgorithms." + id + ".generateKey() " + + "method to create a key guaranteed to be secure enough for " + id + ". See " + + "https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; + } else { //custom algorithm - just indicate required key length: + msg += " The " + id + " algorithm requires keys to have a size >= " + minKeyLength + " bits."; } + + throw new WeakKeyException(msg); } } @Override - public byte[] doSign(CryptoRequest request) throws Exception { - Key key = request.getKey(); - Mac mac = getMacInstance(request); - mac.init(key); - return mac.doFinal(request.getData()); + public byte[] doSign(final SignatureRequest request) throws Exception { + return execute(request, Mac.class, new CheckedFunction() { + @Override + public byte[] apply(Mac mac) throws Exception { + mac.init(request.getKey()); + return mac.doFinal(request.getPayload()); + } + }); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java index 6c8fb1458..2b19f4b85 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java @@ -1,27 +1,40 @@ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.security.CryptoRequest; +import io.jsonwebtoken.security.SecurityException; import io.jsonwebtoken.security.SignatureAlgorithm; import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.SignatureRequest; import io.jsonwebtoken.security.VerifySignatureRequest; -public class NoneSignatureAlgorithm implements SignatureAlgorithm { +import java.security.Key; - private static final String NAME = "none"; +public class NoneSignatureAlgorithm implements SignatureAlgorithm { + + private static final String ID = "none"; @Override - public String getName() { - return NAME; + public String getId() { + return ID; } - @SuppressWarnings("rawtypes") @Override - public byte[] sign(CryptoRequest request) throws SignatureException { - throw new SignatureException("The 'none' algorithm cannot be used to create signatures."); + public byte[] sign(SignatureRequest request) throws SecurityException { + throw new SignatureException("The 'none' algorithm cannot be used to verify signatures."); } @Override - public boolean verify(VerifySignatureRequest request) throws SignatureException { + public boolean verify(VerifySignatureRequest request) throws SignatureException { throw new SignatureException("The 'none' algorithm cannot be used to verify signatures."); } + + @Override + public boolean equals(Object obj) { + return this == obj || + (obj instanceof SignatureAlgorithm && ID.equalsIgnoreCase(((SignatureAlgorithm) obj).getId())); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java new file mode 100644 index 000000000..4c93aa4fe --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java @@ -0,0 +1,193 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.DecryptionKeyRequest; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.PbeKey; +import io.jsonwebtoken.security.SecurityException; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.interfaces.PBEKey; +import javax.crypto.spec.PBEKeySpec; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; + +public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm { + + private static final String SALT_HEADER_NAME = "p2s"; + private static final String ITERATION_HEADER_NAME = "p2c"; // iteration count + private static final int MIN_RECOMMENDED_ITERATIONS = 1000; // https://datatracker.ietf.org/doc/html/rfc7518#section-4.8.1.2 + + private final int HASH_BYTE_LENGTH; + private final int DERIVED_KEY_BIT_LENGTH; + private final byte[] SALT_PREFIX; + private final KeyAlgorithm wrapAlg; + + private static byte[] toRfcSaltPrefix(byte[] bytes) { + // last byte must always be zero as it is a delimiter per + // https://datatracker.ietf.org/doc/html/rfc7518#section-4.8.1.1 + // We ensure this by creating a byte array that is one element larger than bytes.length since Java defaults all + // new byte array indices to 0x00, meaning the last one will be our zero delimiter: + byte[] output = new byte[bytes.length + 1]; + System.arraycopy(bytes, 0, output, 0, bytes.length); + return output; + } + + private static int hashBitLength(int keyBitLength) { + return keyBitLength * 2; + } + + private static String idFor(int hashBitLength, KeyAlgorithm wrapAlg) { + Assert.notNull(wrapAlg, "wrapAlg argument cannot be null."); + return "PBES2-HS" + hashBitLength + "+" + wrapAlg.getId(); + } + + public static int assertIterations(int iterations) { + if (iterations < MIN_RECOMMENDED_ITERATIONS) { + String msg = "[JWA RFC 7518, Section 4.8.1.2](https://datatracker.ietf.org/doc/html/rfc7518#section-4.8.1.2) " + + "recommends password-based-encryption iterations be greater than or equal to " + + MIN_RECOMMENDED_ITERATIONS + ". Provided: " + iterations; + throw new IllegalArgumentException(msg); + } + return iterations; + } + + public Pbes2HsAkwAlgorithm(int keyBitLength) { + this(hashBitLength(keyBitLength), new AesWrapKeyAlgorithm(keyBitLength)); + } + + private Pbes2HsAkwAlgorithm(int hashBitLength, KeyAlgorithm wrapAlg) { + super(idFor(hashBitLength, wrapAlg), "PBKDF2WithHmacSHA" + hashBitLength); + this.wrapAlg = wrapAlg; // no need to assert non-null due to 'idFor' implementation above + + // There's some white box knowledge here: there is no need to assert the value of hashBitLength + // because that is done implicitly in the constructor when instantiating AesWrapKeyAlgorithm. See that class's + // implementation to see the assertion: + this.HASH_BYTE_LENGTH = hashBitLength / Byte.SIZE; + + // https://datatracker.ietf.org/doc/html/rfc7518#section-4.8, 2nd paragraph, last sentence: + // "Their derived-key lengths respectively are 16, 24, and 32 octets." : + this.DERIVED_KEY_BIT_LENGTH = hashBitLength / 2; // results in 128, 192, or 256 + + this.SALT_PREFIX = toRfcSaltPrefix(getId().getBytes(StandardCharsets.UTF_8)); + } + + // protected visibility for testing + protected SecretKey deriveKey(SecretKeyFactory factory, final char[] password, final byte[] rfcSalt, int iterations) throws Exception { + PBEKeySpec spec = null; + try { + spec = new PBEKeySpec(password, rfcSalt, iterations, DERIVED_KEY_BIT_LENGTH); + return factory.generateSecret(spec); + } finally { + if (spec != null) { + spec.clearPassword(); + } + } + } + + private SecretKey deriveKey(final KeyRequest request, final char[] password, final byte[] salt, final int iterations) { + try { + return execute(request, SecretKeyFactory.class, new CheckedFunction() { + @Override + public SecretKey apply(SecretKeyFactory factory) throws Exception { + return deriveKey(factory, password, salt, iterations); + } + }); + } finally { + if (password != null) { + java.util.Arrays.fill(password, '\u0000'); + } + } + } + + protected byte[] generateInputSalt(KeyRequest request) { + byte[] inputSalt = new byte[this.HASH_BYTE_LENGTH]; + ensureSecureRandom(request).nextBytes(inputSalt); + return inputSalt; + } + + // protected visibility for testing + protected byte[] toRfcSalt(byte[] inputSalt) { + return Bytes.concat(this.SALT_PREFIX, inputSalt); + } + + @Override + public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + + Assert.notNull(request, "request cannot be null."); + final PbeKey pbeKey = Assert.notNull(request.getKey(), "request.getKey() cannot be null."); + + final int iterations = assertIterations(pbeKey.getIterations()); + byte[] inputSalt = generateInputSalt(request); + final byte[] rfcSalt = toRfcSalt(inputSalt); + final String p2s = Encoders.BASE64URL.encode(inputSalt); + char[] password = pbeKey.getPassword(); // will be safely cleaned/zeroed in deriveKey next: + final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations); + + // now get a new CEK that is encrypted ('wrapped') with the PBE-derived key: + DefaultKeyRequest wrapReq = new DefaultKeyRequest<>(request.getProvider(), + request.getSecureRandom(), derivedKek, request.getHeader(), request.getEncryptionAlgorithm()); + KeyResult result = wrapAlg.getEncryptionKey(wrapReq); + + request.getHeader().put(SALT_HEADER_NAME, p2s); + request.getHeader().put(ITERATION_HEADER_NAME, iterations); + + return result; + } + + private static char[] toChars(byte[] bytes) { + // use bytebuffer/charbuffer so we don't create a String that remains in the JVM string memory table (heap) + // the respective byte and char arrays will be cleared by the caller + ByteBuffer buf = ByteBuffer.wrap(bytes); + CharBuffer cbuf = StandardCharsets.UTF_8.decode(buf); + return cbuf.compact().array(); + } + + private char[] toPasswordChars(SecretKey key) { + if (key instanceof PBEKey) { + return ((PBEKey) key).getPassword(); + } + if (key instanceof PbeKey) { + return ((PbeKey) key).getPassword(); + } + // convert bytes to UTF-8 characters: + byte[] keyBytes = null; + try { + keyBytes = key.getEncoded(); + return toChars(keyBytes); + } finally { + if (keyBytes != null) { + java.util.Arrays.fill(keyBytes, (byte) 0); + } + } + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + + JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); + final SecretKey key = Assert.notNull(request.getKey(), "Request Key cannot be null."); + + ValueGetter getter = new DefaultValueGetter(header); + final byte[] inputSalt = getter.getRequiredBytes(SALT_HEADER_NAME); + final byte[] rfcSalt = Bytes.concat(SALT_PREFIX, inputSalt); + final int iterations = getter.getRequiredPositiveInteger(ITERATION_HEADER_NAME); + final char[] password = toPasswordChars(key); // will be safely cleaned/zeroed in deriveKey next: + + final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations); + + DecryptionKeyRequest unwrapReq = new DefaultDecryptionKeyRequest<>(request.getProvider(), + request.getSecureRandom(), derivedKek, header, request.getEncryptionAlgorithm(), request.getPayload()); + + return wrapAlg.getDecryptionKey(unwrapReq); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/PrivateEcJwkValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/PrivateEcJwkValidator.java deleted file mode 100644 index 86e1dc749..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/PrivateEcJwkValidator.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.MalformedKeyException; -import io.jsonwebtoken.security.PrivateEcJwk; - -class PrivateEcJwkValidator extends AbstractEcJwkValidator { - - @Override - void validateEcJwk(PrivateEcJwk jwk) { - if (!Strings.hasText(jwk.getD())) { - String msg = "Private EC JWK private key ('d' property') must be specified."; - throw new MalformedKeyException(msg); - } - - //TODO: RFC octet length validation for d value - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RandomEncryptedKeyMode.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RandomEncryptedKeyMode.java deleted file mode 100644 index 14739ab0a..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RandomEncryptedKeyMode.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.EncryptionAlgorithm; -import io.jsonwebtoken.security.SymmetricEncryptionAlgorithm; - -import javax.crypto.SecretKey; - -/** - * Abstract class that implements {@link KeyManagementMode#getKey(GetKeyRequest) getKey} and leaves - * {@link EncryptedKeyManagementMode#encryptKey(EncryptKeyRequest)} to subclasses. - * - * @since JJWT_RELEASE_VERSION - */ -public abstract class RandomEncryptedKeyMode implements EncryptedKeyManagementMode { - - @Override - public SecretKey getKey(GetKeyRequest request) { - - Assert.notNull(request, "GetKeyRequest cannot be null."); - - EncryptionAlgorithm alg = Assert.notNull(request.getEncryptionAlgorithm(), - "GetKeyRequest encryptionAlgorithm cannot be null."); - - if (!(alg instanceof SymmetricEncryptionAlgorithm)) { - String msg = "The standard JWE Encrypted Key Management Modes only support symmetric encryption " + - "algorithms. The specified GetKeyRequest encryptionAlgorithm is an instance of " + - alg.getClass().getName() + " which does not implement " + - SymmetricEncryptionAlgorithm.class.getName() + ". Either specify a JWE-standard symmetric " + - "encryption algorithm or create a custom (non-standard) EncryptedKeyManagementMode implementation."; - throw new IllegalArgumentException(msg); - } - - SymmetricEncryptionAlgorithm salg = (SymmetricEncryptionAlgorithm) alg; - - return salg.generateKey(); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Recipient.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Recipient.java deleted file mode 100644 index ba93e32b7..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/Recipient.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import java.util.Map; - -/** - * An intended recipient of an Encrypted JWT. - * - * @since JJWT_RELEASE_VERSION - */ -public interface Recipient extends Map { -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaJwkConverter.java deleted file mode 100644 index 86b2623dd..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaJwkConverter.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import java.security.Key; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; -import java.util.Map; - -public class RsaJwkConverter extends AbstractTypedJwkConverter { - - public RsaJwkConverter() { - super("RSA"); - } - - @Override - public boolean supports(Key key) { - return key instanceof RSAPublicKey || key instanceof RSAPrivateKey; - } - - @Override - public Key toKey(Map jwk) { - throw new UnsupportedOperationException("Not yet implemented."); - } - - @Override - public Map toJwk(Key key) { - throw new UnsupportedOperationException("Not yet implemented."); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java new file mode 100644 index 000000000..58f119208 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java @@ -0,0 +1,239 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.Converter; +import io.jsonwebtoken.impl.lang.Converters; +import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.MalformedKeyException; +import io.jsonwebtoken.security.RsaPrivateJwk; +import io.jsonwebtoken.security.RsaPublicJwk; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.interfaces.RSAMultiPrimePrivateCrtKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.KeySpec; +import java.security.spec.RSAMultiPrimePrivateCrtKeySpec; +import java.security.spec.RSAOtherPrimeInfo; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +class RsaPrivateJwkFactory extends AbstractFamilyJwkFactory { + + static final Converter, Object> RSA_OTHER_PRIMES_CONVERTER = + Converters.forList(new RSAOtherPrimeInfoConverter()); + + private static final String PUBKEY_ERR_MSG = "JwkContext publicKey must be an " + RSAPublicKey.class.getName() + " instance."; + + RsaPrivateJwkFactory() { + super(DefaultRsaPublicJwk.TYPE_VALUE, RSAPrivateKey.class); + } + + @Override + protected boolean supportsKeyValues(JwkContext ctx) { + return super.supportsKeyValues(ctx) && ctx.containsKey(DefaultRsaPrivateJwk.PRIVATE_EXPONENT); + } + + private static BigInteger getPublicExponent(RSAPrivateKey key) { + if (key instanceof RSAPrivateCrtKey) { + return ((RSAPrivateCrtKey) key).getPublicExponent(); + } else if (key instanceof RSAMultiPrimePrivateCrtKey) { + return ((RSAMultiPrimePrivateCrtKey) key).getPublicExponent(); + } + + String msg = "Unable to derive RSAPublicKey from RSAPrivateKey implementation [" + + key.getClass().getName() + "]. Supported keys implement the " + + RSAPrivateCrtKey.class.getName() + " or " + RSAMultiPrimePrivateCrtKey.class.getName() + + " interfaces. If the specified RSAPrivateKey cannot be one of these two, you must explicitly " + + "provide an RSAPublicKey in addition to the RSAPrivateKey, as the " + + "[JWA RFC, Section 6.3.2](https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.2) " + + "requires public values to be present in private RSA JWKs."; + throw new UnsupportedKeyException(msg); + } + + private RSAPublicKey derivePublic(final JwkContext ctx) { + RSAPrivateKey key = ctx.getKey(); + BigInteger modulus = key.getModulus(); + BigInteger publicExponent = getPublicExponent(key); + final RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent); + return generateKey(ctx, RSAPublicKey.class, new CheckedFunction() { + @Override + public RSAPublicKey apply(KeyFactory kf) { + try { + return (RSAPublicKey) kf.generatePublic(spec); + } catch (Exception e) { + String msg = "Unable to derive RSAPublicKey from RSAPrivateKey {" + ctx + "}."; + throw new UnsupportedKeyException(msg); + } + } + }); + } + + @Override + protected RsaPrivateJwk createJwkFromKey(JwkContext ctx) { + + RSAPrivateKey key = ctx.getKey(); + RSAPublicKey rsaPublicKey; + + PublicKey publicKey = ctx.getPublicKey(); + if (publicKey != null) { + rsaPublicKey = Assert.isInstanceOf(RSAPublicKey.class, publicKey, PUBKEY_ERR_MSG); + } else { + rsaPublicKey = derivePublic(ctx); + } + + // The [JWA Spec](https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.1) + // requires public values to be present in private JWKs, so add them: + JwkContext pubCtx = new DefaultJwkContext<>(DefaultRsaPrivateJwk.PRIVATE_NAMES, ctx, rsaPublicKey); + RsaPublicJwk pubJwk = RsaPublicJwkFactory.DEFAULT_INSTANCE.createJwk(pubCtx); + ctx.putAll(pubJwk); // add public values to private key context + + ctx.put(DefaultRsaPrivateJwk.PRIVATE_EXPONENT, encode(key.getPrivateExponent())); + + if (key instanceof RSAPrivateCrtKey) { + RSAPrivateCrtKey ckey = (RSAPrivateCrtKey) key; + ctx.put(DefaultRsaPrivateJwk.FIRST_PRIME, encode(ckey.getPrimeP())); + ctx.put(DefaultRsaPrivateJwk.SECOND_PRIME, encode(ckey.getPrimeQ())); + ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, encode(ckey.getPrimeExponentP())); + ctx.put(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, encode(ckey.getPrimeExponentQ())); + ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, encode(ckey.getCrtCoefficient())); + } else if (key instanceof RSAMultiPrimePrivateCrtKey) { + RSAMultiPrimePrivateCrtKey ckey = (RSAMultiPrimePrivateCrtKey) key; + ctx.put(DefaultRsaPrivateJwk.FIRST_PRIME, encode(ckey.getPrimeP())); + ctx.put(DefaultRsaPrivateJwk.SECOND_PRIME, encode(ckey.getPrimeQ())); + ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, encode(ckey.getPrimeExponentP())); + ctx.put(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, encode(ckey.getPrimeExponentQ())); + ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, encode(ckey.getCrtCoefficient())); + List infos = Arrays.asList(ckey.getOtherPrimeInfo()); + if (!Collections.isEmpty(infos)) { + Object val = RSA_OTHER_PRIMES_CONVERTER.applyTo(infos); + ctx.put(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO, val); + } + } + + return new DefaultRsaPrivateJwk(ctx, pubJwk); + } + + @Override + protected RsaPrivateJwk createJwkFromValues(JwkContext ctx) { + + final ValueGetter getter = new DefaultValueGetter(ctx); + final BigInteger privateExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.PRIVATE_EXPONENT, true); + + //The [JWA Spec, Section 6.3.2](https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.2) requires + //RSA Private Keys to also encode the public key values, so we assert that we can acquire it successfully: + JwkContext pubCtx = new DefaultJwkContext<>(DefaultRsaPrivateJwk.PRIVATE_NAMES, ctx); + RsaPublicJwk pubJwk = RsaPublicJwkFactory.DEFAULT_INSTANCE.createJwkFromValues(pubCtx); + RSAPublicKey pubKey = pubJwk.toKey(); + final BigInteger modulus = pubKey.getModulus(); + final BigInteger publicExponent = pubKey.getPublicExponent(); + + // JWA Section 6.3.2 also indicates that if any of the optional private names are present, then *all* of those + // optional values must be present (except 'oth', which is handled separately next). Quote: + // + // If the producer includes any of the other private key parameters, then all of the others MUST + // be present, with the exception of "oth", which MUST only be present when more than two prime + // factors were used + // + boolean containsOptional = false; + for (String optionalPrivateName : DefaultRsaPrivateJwk.OPTIONAL_PRIVATE_NAMES) { + if (ctx.containsKey(optionalPrivateName)) { + containsOptional = true; + break; + } + } + + KeySpec spec; + + if (containsOptional) { //if any one optional field exists, they are all required per JWA Section 6.3.2: + BigInteger firstPrime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_PRIME, true); + BigInteger secondPrime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.SECOND_PRIME, true); + BigInteger firstCrtExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, true); + BigInteger secondCrtExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, true); + BigInteger firstCrtCoefficient = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, true); + + // Other Primes Info is actually optional even if the above ones are required: + if (ctx.containsKey(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO)) { + + Object value = ctx.get(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO); + List otherPrimes = RSA_OTHER_PRIMES_CONVERTER.applyFrom(value); + + RSAOtherPrimeInfo[] arr = new RSAOtherPrimeInfo[otherPrimes.size()]; + otherPrimes.toArray(arr); + + spec = new RSAMultiPrimePrivateCrtKeySpec(modulus, publicExponent, privateExponent, firstPrime, + secondPrime, firstCrtExponent, secondCrtExponent, firstCrtCoefficient, arr); + } else { + spec = new RSAPrivateCrtKeySpec(modulus, publicExponent, privateExponent, firstPrime, secondPrime, + firstCrtExponent, secondCrtExponent, firstCrtCoefficient); + } + } else { + spec = new RSAPrivateKeySpec(modulus, privateExponent); + } + + final KeySpec keySpec = spec; + RSAPrivateKey key = generateKey(ctx, new CheckedFunction() { + @Override + public RSAPrivateKey apply(KeyFactory kf) throws Exception { + return (RSAPrivateKey) kf.generatePrivate(keySpec); + } + }); + ctx.setKey(key); + + return new DefaultRsaPrivateJwk(ctx, pubJwk); + } + + static class RSAOtherPrimeInfoConverter implements Converter { + + @Override + public Object applyTo(RSAOtherPrimeInfo info) { + Map m = new LinkedHashMap<>(3); + m.put(DefaultRsaPrivateJwk.PRIME_FACTOR, encode(info.getPrime())); + m.put(DefaultRsaPrivateJwk.FACTOR_CRT_EXPONENT, encode(info.getExponent())); + m.put(DefaultRsaPrivateJwk.FACTOR_CRT_COEFFICIENT, encode(info.getCrtCoefficient())); + return m; + } + + @Override + public RSAOtherPrimeInfo applyFrom(Object o) { + if (o == null) { + throw new MalformedKeyException("RSA JWK 'oth' Other Prime Info element cannot be null."); + } + if (!(o instanceof Map)) { + String msg = "RSA JWK 'oth' Other Prime Info list must contain map elements of name/value pairs. " + + "Element type found: " + o.getClass().getName(); + throw new MalformedKeyException(msg); + } + Map m = (Map) o; + if (Collections.isEmpty(m)) { + throw new MalformedKeyException("RSA JWK 'oth' Other Prime Info element map cannot be empty."); + } + + // Need to add the values to a Context instance to satisfy the API contract of the getRequired* methods + // called below. It's less than ideal, but it works: + JwkContext ctx = new DefaultJwkContext<>(DefaultRsaPrivateJwk.PRIVATE_NAMES); + for (Map.Entry entry : m.entrySet()) { + String name = String.valueOf(entry.getKey()); + ctx.put(name, entry.getValue()); + } + + final ValueGetter getter = new DefaultValueGetter(ctx); + BigInteger prime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.PRIME_FACTOR, true); + BigInteger primeExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FACTOR_CRT_EXPONENT, true); + BigInteger crtCoefficient = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FACTOR_CRT_COEFFICIENT, true); + + return new RSAOtherPrimeInfo(prime, primeExponent, crtCoefficient); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java new file mode 100644 index 000000000..7889857e5 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java @@ -0,0 +1,46 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.security.RsaPublicJwk; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAPublicKeySpec; + +class RsaPublicJwkFactory extends AbstractFamilyJwkFactory { + + static final RsaPublicJwkFactory DEFAULT_INSTANCE = new RsaPublicJwkFactory(); + + RsaPublicJwkFactory() { + super(DefaultRsaPublicJwk.TYPE_VALUE, RSAPublicKey.class); + } + + @Override + protected RsaPublicJwk createJwkFromKey(JwkContext ctx) { + RSAPublicKey key = ctx.getKey(); + ctx.put(DefaultRsaPublicJwk.MODULUS, encode(key.getModulus())); + ctx.put(DefaultRsaPublicJwk.PUBLIC_EXPONENT, encode(key.getPublicExponent())); + return new DefaultRsaPublicJwk(ctx); + } + + @Override + protected RsaPublicJwk createJwkFromValues(JwkContext ctx) { + ValueGetter getter = new DefaultValueGetter(ctx); + BigInteger modulus = getter.getRequiredBigInt(DefaultRsaPublicJwk.MODULUS, false); + BigInteger publicExponent = getter.getRequiredBigInt(DefaultRsaPublicJwk.PUBLIC_EXPONENT, false); + final RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent); + + RSAPublicKey key = generateKey(ctx, new CheckedFunction() { + @Override + public RSAPublicKey apply(KeyFactory keyFactory) throws Exception { + return (RSAPublicKey) keyFactory.generatePublic(spec); + } + }); + + ctx.setKey(key); + + return new DefaultRsaPublicJwk(ctx); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java new file mode 100644 index 000000000..7cb524c0a --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java @@ -0,0 +1,66 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.SecretJwk; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +class SecretJwkFactory extends AbstractFamilyJwkFactory { + + SecretJwkFactory() { + super(DefaultSecretJwk.TYPE_VALUE, SecretKey.class); + } + + static byte[] getRequiredEncoded(SecretKey key, String reason) { + Assert.notNull(key, "SecretKey argument cannot be null."); + Assert.hasText(reason, "Reason string argument cannot be null or empty."); + byte[] encoded = null; + Exception cause = null; + try { + encoded = key.getEncoded(); + } catch (Exception e) { + cause = e; + } + + if (Arrays.length(encoded) == 0) { + String msg = "SecretKey argument does not have any encoded bytes, or the key's backing JCA Provider " + + "is preventing key.getEncoded() from returning any bytes. In either case, it is not possible to " + + reason + "."; + throw new UnsupportedKeyException(msg, cause); + } + + return encoded; + } + + @Override + protected SecretJwk createJwkFromKey(JwkContext ctx) { + SecretKey key = Assert.notNull(ctx.getKey(), "JwkContext key cannot be null."); + String k; + try { + byte[] encoded = getRequiredEncoded(key, "represent the SecretKey instance as a JWK"); + k = Encoders.BASE64URL.encode(encoded); + } catch (Exception e) { + String msg = "Unable to encode SecretKey to JWK: " + e.getMessage(); + throw new UnsupportedKeyException(msg, e); + } + + assert k != null : "k value is mandatory."; + ctx.put(DefaultSecretJwk.K, k); + + return new DefaultSecretJwk(ctx); + } + + @Override + protected SecretJwk createJwkFromValues(JwkContext ctx) { + ValueGetter getter = new DefaultValueGetter(ctx); + byte[] bytes = getter.getRequiredBytes(DefaultSecretJwk.K); + SecretKey key = new SecretKeySpec(bytes, "NONE"); //TODO: do we need a JCA-specific ID here? + ctx.setKey(key); + return new DefaultSecretJwk(ctx); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java new file mode 100644 index 000000000..eb182b3ee --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java @@ -0,0 +1,56 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.impl.IdRegistry; +import io.jsonwebtoken.impl.lang.Registry; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.SignatureAlgorithm; + +import java.util.Collection; + +@SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.SignatureAlgorithms implementation +public class SignatureAlgorithmsBridge { + + //prevent instantiation + private SignatureAlgorithmsBridge() { + } + + //For parser implementation - do not expose outside the impl module + public static final Registry> REGISTRY; + + static { + //noinspection RedundantTypeArguments + REGISTRY = new IdRegistry<>(Collections.>of( + new NoneSignatureAlgorithm(), + new MacSignatureAlgorithm(256), + new MacSignatureAlgorithm(384), + new MacSignatureAlgorithm(512), + new DefaultRsaSignatureAlgorithm<>(256, 2048), + new DefaultRsaSignatureAlgorithm<>(384, 3072), + new DefaultRsaSignatureAlgorithm<>(512, 4096), + new DefaultRsaSignatureAlgorithm<>(256, 2048, 256), + new DefaultRsaSignatureAlgorithm<>(384, 3072, 384), + new DefaultRsaSignatureAlgorithm<>(512, 4096, 512), + new DefaultEllipticCurveSignatureAlgorithm<>(256, 64), + new DefaultEllipticCurveSignatureAlgorithm<>(384, 96), + new DefaultEllipticCurveSignatureAlgorithm<>(521, 132) + )); + } + + public static Collection> values() { + return REGISTRY.values(); + } + + public static SignatureAlgorithm findById(String id) { + return REGISTRY.apply(id); + } + + public static SignatureAlgorithm forId(String id) { + SignatureAlgorithm instance = findById(id); + if (instance == null) { + String msg = "Unrecognized JWA SignatureAlgorithm identifier: " + id; + throw new UnsupportedJwtException(msg); + } + return instance; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkConverter.java deleted file mode 100644 index 5d3a50b66..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkConverter.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.io.DecodingException; -import io.jsonwebtoken.io.Encoders; -import io.jsonwebtoken.security.MalformedKeyException; -import io.jsonwebtoken.security.UnsupportedKeyException; - -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.security.Key; -import java.util.HashMap; -import java.util.Map; - -public class SymmetricJwkConverter extends AbstractTypedJwkConverter { - - public SymmetricJwkConverter() { - super("oct"); - } - - @Override - public boolean supports(Key key) { - return key instanceof SecretKey; - } - - @Override - public Map toJwk(Key key) { - String k; - try { - byte[] encoded = key.getEncoded(); - if (encoded == null || encoded.length == 0) { - throw new IllegalArgumentException("SecretKey argument does not have any encoded bytes."); - } - k = Encoders.BASE64URL.encode(encoded); - } catch (Exception e) { - String msg = "Unable to encode secret key to JWK."; - throw new UnsupportedKeyException(msg, e); - } - - Map m = new HashMap<>(); - m.put("kty", "oct"); - m.put("k", k); - - return m; - } - - @Override - public SecretKey toKey(Map jwk) { - String oct = getRequiredString(jwk, "oct"); - byte[] bytes; - try { - bytes = Decoders.BASE64URL.decode(oct); - } catch (DecodingException e) { - String msg = "Unable to Base64Url-decode JWK 'oct' member value: " + oct; - throw new MalformedKeyException(msg, e); - } - return new SecretKeySpec(bytes, "AES"); //TODO: what about other algorithms? - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkValidator.java deleted file mode 100644 index f51863ee2..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/SymmetricJwkValidator.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.KeyException; -import io.jsonwebtoken.security.SymmetricJwk; - -class SymmetricJwkValidator extends AbstractJwkValidator { - - SymmetricJwkValidator() { - super(DefaultSymmetricJwk.TYPE_VALUE); - } - - @Override - void validateJwk(SymmetricJwk jwk) throws KeyException { - - String k = jwk.getK(); - if (!Strings.hasText(k)) { - malformed("Symmetric JWK key value ('k' property) must be specified."); - } - - //TODO: k length validation? - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/TypedJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/TypedJwkConverter.java deleted file mode 100644 index d7e025e52..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/TypedJwkConverter.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import java.security.Key; - -public interface TypedJwkConverter extends JwkConverter { - - String getKeyType(); - - boolean supports(Key key); - -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy index 895b24f37..04c3eeb81 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy @@ -23,7 +23,6 @@ import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.SignatureException import org.junit.Test -import javax.crypto.spec.SecretKeySpec import java.security.SecureRandom import static ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE @@ -46,23 +45,6 @@ class DeprecatedJwtParserTest { return Encoders.BASE64URL.encode(bytes) } - @Test - void testSetDuplicateSigningKeys() { - - byte[] keyBytes = randomKey() - - SecretKeySpec key = new SecretKeySpec(keyBytes, "HmacSHA256") - - String compact = Jwts.builder().setPayload('Hello World!').signWith(SignatureAlgorithm.HS256, keyBytes).compact() - - try { - Jwts.parser().setSigningKey(keyBytes).setSigningKey(key).parse(compact) - fail() - } catch (IllegalStateException ise) { - assertEquals ise.getMessage(), 'A key object and key bytes cannot both be specified. Choose either.' - } - } - @Test void testIsSignedWithNullArgument() { assertFalse Jwts.parser().isSigned(null) @@ -85,6 +67,7 @@ class DeprecatedJwtParserTest { fail() } catch (MalformedJwtException expected) { assertEquals expected.getMessage(), 'Malformed JWT JSON: ' + junkPayload + assertEquals 'Unable to read claims JSON: ' + junkPayload, expected.getMessage() } } @@ -144,7 +127,7 @@ class DeprecatedJwtParserTest { Jwts.parserBuilder().setSigningKey(randomKey()).build().parse(bad) fail() } catch (MalformedJwtException se) { - assertEquals se.getMessage(), 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.' + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string has a digest/signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.getMessage() } } @@ -293,10 +276,10 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, randomKey()).compact() try { - Jwts.parser().parsePlaintextJws(compact) + Jwts.parser().parsePlaintextJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Cannot verify JWS signature: unable to locate signature verification key for JWS with header: {alg=HS256}', e.getMessage() } } @@ -306,10 +289,10 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').signWith(SignatureAlgorithm.HS256, randomKey()).compact() try { - Jwts.parser().parsePlaintextJws(compact) + Jwts.parser().parsePlaintextJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Cannot verify JWS signature: unable to locate signature verification key for JWS with header: {alg=HS256}', e.getMessage() } } @@ -340,7 +323,7 @@ class DeprecatedJwtParserTest { Jwts.parser().parseClaimsJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' + assertEquals 'Unsigned plaintext JWTs are not supported.', e.getMessage() } } @@ -355,7 +338,7 @@ class DeprecatedJwtParserTest { Jwts.parser().parseClaimsJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Cannot verify JWS signature: unable to locate signature verification key for JWS with header: {alg=HS256}', e.getMessage() } } @@ -368,7 +351,7 @@ class DeprecatedJwtParserTest { Jwts.parser().parseClaimsJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Cannot verify JWS signature: unable to locate signature verification key for JWS with header: {alg=HS256}', e.getMessage() } } @@ -435,7 +418,7 @@ class DeprecatedJwtParserTest { Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' + assertEquals 'Unsigned plaintext JWTs are not supported.', e.getMessage() } } @@ -452,7 +435,7 @@ class DeprecatedJwtParserTest { Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' + assertEquals 'Unsigned Claims JWTs are not supported.', e.getMessage() } } @@ -469,7 +452,7 @@ class DeprecatedJwtParserTest { Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed Claims JWSs are not supported.' + assertEquals 'Signed Claims JWTs are not supported.', e.getMessage() } } @@ -548,7 +531,7 @@ class DeprecatedJwtParserTest { Jwts.parser().setSigningKey(key).parseClaimsJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' + assertEquals 'Unsigned plaintext JWTs are not supported.', e.getMessage() } } @@ -565,24 +548,24 @@ class DeprecatedJwtParserTest { Jwts.parser().setSigningKey(key).parseClaimsJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' + assertEquals 'Unsigned Claims JWTs are not supported.', e.getMessage() } } @Test void testParseClaimsJwsWithPlaintextJws() { - String subject = 'Joe' + String payload = 'Hello world' byte[] key = randomKey() - String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() + String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, key).compact() try { - Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + Jwts.parser().setSigningKey(key).parseClaimsJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed Claims JWSs are not supported.' + assertEquals 'Signed plaintext JWTs are not supported.', e.getMessage() } } @@ -635,54 +618,6 @@ class DeprecatedJwtParserTest { } } - @Test - void testParseClaimsWithSigningKeyResolverAndKey() { - - String subject = 'Joe' - - SecretKeySpec key = new SecretKeySpec(randomKey(), "HmacSHA256") - - String compact = Jwts.builder().setSubject(subject).signWith(key, SignatureAlgorithm.HS256).compact() - - def signingKeyResolver = new SigningKeyResolverAdapter() { - @Override - byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { - return randomKey() - } - } - - try { - Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) - fail() - } catch (IllegalStateException ise) { - assertEquals ise.getMessage(), 'A signing key resolver and a key object cannot both be specified. Choose either.' - } - } - - @Test - void testParseClaimsWithSigningKeyResolverAndKeyBytes() { - - String subject = 'Joe' - - byte[] key = randomKey() - - String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() - - def signingKeyResolver = new SigningKeyResolverAdapter() { - @Override - byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { - return randomKey() - } - } - - try { - Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) - fail() - } catch (IllegalStateException ise) { - assertEquals ise.getMessage(), 'A signing key resolver and key bytes cannot both be specified. Choose either.' - } - } - @Test void testParseClaimsWithNullSigningKeyResolver() { @@ -1510,32 +1445,17 @@ class DeprecatedJwtParserTest { } @Test - void testNoHeaderNoSig() { + void testNoProtectedHeader() { - String payload = '{"subject":"Joe"}' + String payload = '{"sub":"Joe"}' String jwtStr = '.' + base64Url(payload) + '.' - Jwt jwt = Jwts.parser().parse(jwtStr) - - assertTrue jwt.header == null - assertEquals 'Joe', jwt.body.get('subject') - } - - @Test - void testNoHeaderSig() { - - String payload = '{"subject":"Joe"}' - - String sig = ";aklsjdf;kajsd;fkjas;dklfj" - - String jwtStr = '.' + base64Url(payload) + '.' + base64Url(sig) - try { - Jwts.parser().parse(jwtStr) + Jwts.parserBuilder().build().parse(jwtStr) fail() - } catch (MalformedJwtException se) { - assertEquals 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.', se.message + } catch (MalformedJwtException e) { + assertEquals 'Compact JWT strings MUST always have a Base64Url protected header per https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4).', e.getMessage() } } @@ -1554,7 +1474,7 @@ class DeprecatedJwtParserTest { Jwts.parser().parse(jwtStr) fail() } catch (MalformedJwtException se) { - assertEquals 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.', se.message + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string has a digest/signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.message } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy index 6b58d234e..94c47e723 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy @@ -164,14 +164,9 @@ class DeprecatedJwtsTest { } } - @Test + @Test(expected = MalformedJwtException) void testParseWithTwoPeriodsOnly() { - try { - Jwts.parser().parse('..') - fail() - } catch (MalformedJwtException e) { - assertEquals e.message, "JWT string '..' is missing a header." - } + Jwts.parser().parse('..') } @Test @@ -181,24 +176,9 @@ class DeprecatedJwtsTest { assertEquals("none", jwt.getHeader().get("alg")) } - @Test + @Test(expected = MalformedJwtException) void testParseWithSignatureOnly() { - try { - Jwts.parser().parse('..bar') - fail() - } catch (MalformedJwtException e) { - assertEquals e.message, "JWT string has a digest/signature, but the header does not reference a valid signature algorithm." - } - } - - @Test - void testWithInvalidCompressionAlgorithm() { - try { - - Jwts.builder().setHeaderParam(Header.COMPRESSION_ALGORITHM, "CUSTOM").setId("andId").compact() - } catch (CompressionException e) { - assertEquals "Unsupported compression algorithm 'CUSTOM'", e.getMessage() - } + Jwts.parser().parse('..bar') } @Test @@ -616,7 +596,7 @@ class DeprecatedJwtsTest { String forged = Jwts.builder().setSubject("Not Joe").compact() //assert that our forged header has a 'NONE' algorithm: - assertEquals Jwts.parser().parseClaimsJwt(forged).getHeader().get('alg'), 'none' + assertEquals 'none', Jwts.parser().parseClaimsJwt(forged).getHeader().get('alg') //now let's forge it by appending the signature the server expects: forged += signature @@ -626,7 +606,7 @@ class DeprecatedJwtsTest { Jwts.parser().setSigningKey(key).parse(forged) fail("Parsing must fail for a forged token.") } catch (MalformedJwtException expected) { - assertEquals expected.message, 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.' + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string has a digest/signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', expected.message } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 659d57637..139863b2a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -25,7 +25,6 @@ import io.jsonwebtoken.security.SignatureException import org.junit.Test import javax.crypto.SecretKey -import javax.crypto.spec.SecretKeySpec import java.security.SecureRandom import static ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE @@ -33,6 +32,7 @@ import static ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE import static io.jsonwebtoken.DateTestUtils.truncateMillis import static org.junit.Assert.* +@SuppressWarnings('GrDeprecatedAPIUsage') class JwtParserTest { private static final SecureRandom random = new SecureRandom() //doesn't need to be seeded - just testing @@ -49,23 +49,6 @@ class JwtParserTest { return Encoders.BASE64URL.encode(bytes) } - @Test - void testSetDuplicateSigningKeys() { - - byte[] keyBytes = randomKey() - - SecretKeySpec key = new SecretKeySpec(keyBytes, "HmacSHA256") - - String compact = Jwts.builder().setPayload('Hello World!').signWith(SignatureAlgorithm.HS256, keyBytes).compact() - - try { - Jwts.parserBuilder().setSigningKey(keyBytes).setSigningKey(key).build().parse(compact) - fail() - } catch (IllegalStateException ise) { - assertEquals ise.getMessage(), 'A key object and key bytes cannot both be specified. Choose either.' - } - } - @Test void testIsSignedWithNullArgument() { assertFalse Jwts.parserBuilder().build().isSigned(null) @@ -87,7 +70,7 @@ class JwtParserTest { Jwts.parserBuilder().build().parse(bad) fail() } catch (MalformedJwtException expected) { - assertEquals expected.getMessage(), 'Malformed JWT JSON: ' + junkPayload + assertEquals 'Unable to read claims JSON: ' + junkPayload, expected.getMessage() } } @@ -147,7 +130,7 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKey(randomKey()).build().parse(bad) fail() } catch (MalformedJwtException se) { - assertEquals se.getMessage(), 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.' + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string has a digest/signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.getMessage() } } @@ -334,23 +317,24 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, randomKey()).compact() try { - Jwts.parserBuilder().build().parsePlaintextJws(compact) + Jwts.parserBuilder().build().parsePlaintextJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Cannot verify JWS signature: unable to locate signature verification key for JWS with header: {alg=HS256}', e.getMessage() } } @Test void testParsePlaintextJwtWithClaimsJws() { - String compact = Jwts.builder().setSubject('Joe').signWith(SignatureAlgorithm.HS256, randomKey()).compact() + def key = randomKey() + String compact = Jwts.builder().setSubject('Joe').signWith(SignatureAlgorithm.HS256, key).compact() try { - Jwts.parserBuilder().build().parsePlaintextJws(compact) + Jwts.parserBuilder().setSigningKey(key).build().parsePlaintextJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Signed Claims JWTs are not supported.', e.getMessage() } } @@ -381,7 +365,7 @@ class JwtParserTest { Jwts.parserBuilder().build().parseClaimsJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' + assertEquals 'Unsigned plaintext JWTs are not supported.', e.getMessage() } } @@ -396,20 +380,21 @@ class JwtParserTest { Jwts.parserBuilder().build().parseClaimsJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Cannot verify JWS signature: unable to locate signature verification key for JWS with header: {alg=HS256}', e.getMessage() } } @Test void testParseClaimsJwtWithClaimsJws() { - String compact = Jwts.builder().setSubject('Joe').signWith(SignatureAlgorithm.HS256, randomKey()).compact() + def key = randomKey() + String compact = Jwts.builder().setSubject('Joe').signWith(SignatureAlgorithm.HS256, key).compact() try { - Jwts.parserBuilder().build().parseClaimsJwt(compact) + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Signed Claims JWTs are not supported.', e.getMessage() } } @@ -481,7 +466,7 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKey(key).build().parsePlaintextJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' + assertEquals 'Unsigned plaintext JWTs are not supported.', e.getMessage() } } @@ -498,7 +483,7 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKey(key).build().parsePlaintextJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' + assertEquals 'Unsigned Claims JWTs are not supported.', e.getMessage() } } @@ -515,7 +500,7 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKey(key).build().parsePlaintextJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed Claims JWSs are not supported.' + assertEquals 'Signed Claims JWTs are not supported.', e.getMessage() } } @@ -594,7 +579,7 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' + assertEquals 'Unsigned plaintext JWTs are not supported.', e.getMessage() } } @@ -613,7 +598,7 @@ class JwtParserTest { parseClaimsJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' + assertEquals 'Unsigned Claims JWTs are not supported.', e.getMessage() } } @@ -630,7 +615,7 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKey(key).build().parsePlaintextJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed Claims JWSs are not supported.' + assertEquals 'Signed Claims JWTs are not supported.', e.getMessage() } } @@ -679,55 +664,7 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKeyResolver(signingKeyResolver).build().parseClaimsJws(compact) fail() } catch (SignatureException se) { - assertEquals se.getMessage(), 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.' - } - } - - @Test - void testParseClaimsWithSigningKeyResolverAndKey() { - - String subject = 'Joe' - - SecretKeySpec key = new SecretKeySpec(randomKey(), "HmacSHA256") - - String compact = Jwts.builder().setSubject(subject).signWith(key, SignatureAlgorithm.HS256).compact() - - def signingKeyResolver = new SigningKeyResolverAdapter() { - @Override - byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { - return randomKey() - } - } - - try { - Jwts.parserBuilder().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).build().parseClaimsJws(compact) - fail() - } catch (IllegalStateException ise) { - assertEquals ise.getMessage(), 'A signing key resolver and a key object cannot both be specified. Choose either.' - } - } - - @Test - void testParseClaimsWithSigningKeyResolverAndKeyBytes() { - - String subject = 'Joe' - - byte[] key = randomKey() - - String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() - - def signingKeyResolver = new SigningKeyResolverAdapter() { - @Override - byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { - return randomKey() - } - } - - try { - Jwts.parserBuilder().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).build().parseClaimsJws(compact) - fail() - } catch (IllegalStateException ise) { - assertEquals ise.getMessage(), 'A signing key resolver and key bytes cannot both be specified. Choose either.' + assertEquals 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.', se.getMessage() } } @@ -744,7 +681,7 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKeyResolver(null).build().parseClaimsJws(compact) fail() } catch (IllegalArgumentException iae) { - assertEquals iae.getMessage(), 'SigningKeyResolver cannot be null.' + assertEquals 'SigningKeyResolver cannot be null.', iae.getMessage() } } @@ -763,9 +700,9 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKeyResolver(signingKeyResolver).build().parseClaimsJws(compact) fail() } catch (UnsupportedJwtException ex) { - assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support ' + + assertEquals 'The specified SigningKeyResolver implementation does not support ' + 'Claims JWS signing key resolution. Consider overriding either the resolveSigningKey(JwsHeader, Claims) method ' + - 'or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, Claims) method.' + 'or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, Claims) method.', ex.getMessage() } } @@ -844,7 +781,7 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKeyResolver(signingKeyResolver).build().parsePlaintextJws(compact) fail() } catch (SignatureException se) { - assertEquals se.getMessage(), 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.' + assertEquals 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.', se.getMessage() } } @@ -1519,7 +1456,7 @@ class JwtParserTest { try { Jwts.parserBuilder().setSigningKey(key). require("aDate", aDate). - build(). + build(). parseClaimsJws(compact) fail() } catch (MissingClaimException e) { @@ -1594,10 +1531,12 @@ class JwtParserTest { String jwtStr = '.' + base64Url(payload) + '.' - Jwt jwt = Jwts.parserBuilder().build().parse(jwtStr) - - assertTrue jwt.header == null - assertEquals 'Joe', jwt.body.get('subject') + try { + Jwts.parserBuilder().build().parse(jwtStr) + fail() + } catch (MalformedJwtException e) { + assertEquals 'Compact JWT strings MUST always have a Base64Url protected header per https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4).', e.getMessage() + } } @Test @@ -1613,7 +1552,7 @@ class JwtParserTest { Jwts.parserBuilder().build().parse(jwtStr) fail() } catch (MalformedJwtException se) { - assertEquals 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.', se.message + assertEquals 'Compact JWT strings MUST always have a Base64Url protected header per https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4).', se.message } } @@ -1632,7 +1571,7 @@ class JwtParserTest { Jwts.parserBuilder().build().parse(jwtStr) fail() } catch (MalformedJwtException se) { - assertEquals 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.', se.message + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string has a digest/signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.message } } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index e44584ace..8da70d799 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -15,26 +15,28 @@ */ package io.jsonwebtoken +import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.impl.DefaultHeader import io.jsonwebtoken.impl.DefaultJweHeader import io.jsonwebtoken.impl.DefaultJwsHeader import io.jsonwebtoken.impl.JwtTokenizer import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec +import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer -import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.lang.Strings -import io.jsonwebtoken.security.Keys -import io.jsonwebtoken.security.WeakKeyException +import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import java.nio.charset.Charset +import java.security.Key import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey +import java.security.interfaces.RSAPrivateKey import static org.junit.Assert.* @@ -184,8 +186,7 @@ class JwtsTest { Jwts.parserBuilder().build().parse('..') fail() } catch (MalformedJwtException e) { - assertEquals e.message, "JWT string '..' is missing a header." -// assertEquals "Required JWS Protected Header is missing.", e.message + assertEquals 'Compact JWT strings MUST always have a Base64Url protected header per https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4).', e.message } } @@ -202,7 +203,21 @@ class JwtsTest { Jwts.parserBuilder().build().parse('..bar') fail() } catch (MalformedJwtException e) { - assertEquals e.message, "JWT string has a digest/signature, but the header does not reference a valid signature algorithm." + assertEquals 'Compact JWT strings MUST always have a Base64Url protected header per https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4).', e.message + } + } + + @Test + void testParseWithMissingRequiredSignature() { + Key key = SignatureAlgorithms.HS256.generateKey() + String compact = Jwts.builder().setSubject('foo').signWith(key).compact() + int i = compact.lastIndexOf('.') + String missingSig = compact.substring(0, i + 1) + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(missingSig) + fail() + } catch (MalformedJwtException expected) { + assertEquals 'The JWS header references signature algorithm \'HS256\' but the compact JWS string does not have a signature token.', expected.getMessage() } } @@ -641,7 +656,7 @@ class JwtsTest { Jwts.parserBuilder().setSigningKey(key).build().parse(forged) fail("Parsing must fail for a forged token.") } catch (MalformedJwtException expected) { - assertEquals expected.message, 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.' + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string has a digest/signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', expected.message } } @@ -800,5 +815,13 @@ class JwtsTest { //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims } + + void testFoo() { + RSAPrivateKey key; + Jwts.jweBuilder() + .encryptWith(EncryptionAlgorithms.A128GCM) + .usingKey(key) + .fromKeyAlgorithm(KeyAlgorithms.PBES2_HS256_A128KW.withIterations(1203023)) + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy new file mode 100644 index 000000000..f6b4be1c6 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy @@ -0,0 +1,101 @@ +package io.jsonwebtoken.impl + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.EncryptionAlgorithms +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +class DefaultJweBuilderTest { + + static DefaultJweBuilder builder() { + return new DefaultJweBuilder() + } + + @Test + void testCompactWithoutPayloadOrClaims() { + try { + builder().compact() + fail() + } catch (IllegalStateException ise) { + assertEquals "Either 'claims' or a non-empty 'payload' must be specified.", ise.message + } + } + + @Test + void testCompactWithoutBothPayloadAndClaims() { + try { + builder().setPayload("hi").setIssuer("me").compact() + } catch (IllegalStateException ise) { + assertEquals "Both 'payload' and 'claims' cannot both be specified. Choose either one.", ise.message + } + } + + @Test + void testCompactWithoutKey() { + try { + builder().setIssuer("me").compact() + } catch (IllegalStateException ise) { + assertEquals 'Key is required.', ise.message + } + } + + @Test + void testCompactWithoutEncryptionAlgorithm() { + def key = EncryptionAlgorithms.A128GCM.generateKey() + try { + builder().setIssuer("me").withKey(key).compact() + } catch (IllegalStateException ise) { + assertEquals 'Encryption algorithm is required.', ise.message + } + } + + @Test + void testCompactSimplestPayload() { + def enc = EncryptionAlgorithms.A128GCM + def key = enc.generateKey() + def jwe = builder().setPayload("me").encryptWith(enc).withKey(key).compact() + def jwt = Jwts.parserBuilder().decryptWith(key).build().parsePlaintextJwe(jwe) + assertEquals 'me', jwt.getBody() + } + + @Test + void testCompactSimplestClaims() { + def enc = EncryptionAlgorithms.A128GCM + def key = enc.generateKey() + def jwe = builder().setSubject('joe').encryptWith(enc).withKey(key).compact() + def jwt = Jwts.parserBuilder().decryptWith(key).build().parseClaimsJwe(jwe) + assertEquals 'joe', jwt.getBody().getSubject() + } + + /* + @Test + void testFullSymmetryForAllJweAlgorithms() { + + for( KeyAlgorithm keyAlg : KeyAlgorithms.values() ) { + + for(AeadAlgorithm encAlg : EncryptionAlgorithms.values() ) { + Key kek = encAlg.generateKey(); + String jwe = builder().setSubject('joe').encryptWith(encAlg).withKeyFrom(kek, keyAlg).compact() + } + } + } + */ + + @Test + void testBuild() { + def enc = EncryptionAlgorithms.A128GCM; + def key = enc.generateKey() + + String jwe = new DefaultJweBuilder() + .setSubject('joe') + .encryptWith(enc) + .withKey(key) + .compact() + + //TODO create assertions + //println jwe + //println new String(Decoders.BASE64URL.decode(jwe.substring(0, jwe.indexOf('.'))), StandardCharsets.UTF_8) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index 61d7b0e4a..bceed48bc 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -48,7 +48,7 @@ class DefaultJwtBuilderTest { io.jsonwebtoken.security.SignatureAlgorithm alg = new io.jsonwebtoken.security.SignatureAlgorithm() { @Override - byte[] sign(CryptoRequest request) throws SignatureException, KeyException { + byte[] sign(SignatureRequest request) throws SignatureException, KeyException { assertSame provider, request.getProvider() called[0] = true //simulate a digest: @@ -63,7 +63,7 @@ class DefaultJwtBuilderTest { } @Override - String getName() { + String getId() { return "test" } } @@ -86,7 +86,7 @@ class DefaultJwtBuilderTest { io.jsonwebtoken.security.SignatureAlgorithm alg = new io.jsonwebtoken.security.SignatureAlgorithm() { @Override - byte[] sign(CryptoRequest request) throws SignatureException, KeyException { + byte[] sign(SignatureRequest request) throws SignatureException, KeyException { assertSame random, request.getSecureRandom() called[0] = true //simulate a digest: @@ -101,7 +101,7 @@ class DefaultJwtBuilderTest { } @Override - String getName() { + String getId() { return "test" } } @@ -252,14 +252,13 @@ class DefaultJwtBuilderTest { @Test void testBase64UrlEncodeError() { - - def b = new DefaultJwtBuilder() { + def serializer = new Serializer() { @Override - protected byte[] toJson(Object o) throws SerializationException { + byte[] serialize(Object o) throws SerializationException { throw new SerializationException('foo', new Exception()) } } - + def b = new DefaultJwtBuilder().serializeToJsonWith(serializer) try { b.setPayload('foo').compact() fail() @@ -270,23 +269,20 @@ class DefaultJwtBuilderTest { @Test void testCompactCompressionCodecJsonProcessingException() { - def b = new DefaultJwtBuilder() { + def serializer = new Serializer() { @Override - protected byte[] toJson(Object o) throws SerializationException { - if (o instanceof DefaultJwsHeader) { - return super.toJson(o) - } + byte[] serialize(Object o) throws SerializationException { throw new SerializationException('dummy text', new Exception()) } } - + def b = new DefaultJwtBuilder().serializeToJsonWith(serializer) def c = Jwts.claims().setSubject("Joe"); try { b.setClaims(c).compressWith(CompressionCodecs.DEFLATE).compact() fail() } catch (IllegalArgumentException iae) { - assertEquals iae.message, 'Unable to serialize claims object to json: dummy text' + assertEquals iae.message, 'Unable to serialize claims to JSON. Cause: dummy text' } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy index c86d89a70..413e29977 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy @@ -58,7 +58,7 @@ class DefaultJwtParserTest { @Test(expected = MalformedJwtException) void testBase64UrlDecodeWithInvalidInput() { - new DefaultJwtParser().base64UrlDecode('20:SLDKJF;3993;----') + new DefaultJwtParser().base64UrlDecode('20:SLDKJF;3993;----', 'test') } @Test(expected = IllegalArgumentException) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy index aa8c6e953..759b8758e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy @@ -1,10 +1,42 @@ package io.jsonwebtoken.impl +import io.jsonwebtoken.MalformedJwtException + import static org.junit.Assert.* import org.junit.Test class JwtTokenizerTest { + @Test(expected= MalformedJwtException) + void testParseWithWhitespaceInBase64UrlHeader() { + def input = 'header .body.signature' + new JwtTokenizer().tokenize(input) + } + + @Test(expected= MalformedJwtException) + void testParseWithWhitespaceInBase64UrlBody() { + def input = 'header. body.signature' + new JwtTokenizer().tokenize(input) + } + + @Test(expected= MalformedJwtException) + void testParseWithWhitespaceInBase64UrlSignature() { + def input = 'header.body. signature' + new JwtTokenizer().tokenize(input) + } + + @Test(expected= MalformedJwtException) + void testParseWithWhitespaceInBase64UrlJweBody() { + def input = 'header.encryptedKey.initializationVector. body.authenticationTag' + new JwtTokenizer().tokenize(input) + } + + @Test(expected= MalformedJwtException) + void testParseWithWhitespaceInBase64UrlJweTag() { + def input = 'header.encryptedKey.initializationVector.body. authenticationTag' + new JwtTokenizer().tokenize(input) + } + @Test void testJwe() { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidatorTest.groovy deleted file mode 100644 index 5362d40cc..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidatorTest.groovy +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.io.Decoders -import io.jsonwebtoken.security.Keys -import org.junit.Test - -import static org.junit.Assert.assertNotNull -import static org.junit.Assert.assertSame - -class DefaultJwtSignatureValidatorTest { - - @Test - //TODO: remove this before 1.0 since it tests a deprecated method - @Deprecated - void testDeprecatedTwoArgCtor() { - - def alg = SignatureAlgorithm.HS256 - def key = Keys.secretKeyFor(alg) - def validator = new DefaultJwtSignatureValidator(alg, key) - - assertNotNull validator.signatureValidator - assertSame Decoders.BASE64URL, validator.base64UrlDecoder - } - - @Test - //TODO: remove this before 1.0 since it tests a deprecated method - @Deprecated - void testDeprecatedThreeArgCtor() { - - def alg = SignatureAlgorithm.HS256 - def key = Keys.secretKeyFor(alg) - def validator = new DefaultJwtSignatureValidator(DefaultSignatureValidatorFactory.INSTANCE, alg, key) - - assertNotNull validator.signatureValidator - assertSame Decoders.BASE64URL, validator.base64UrlDecoder - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignerTest.groovy deleted file mode 100644 index e0f1b7ea8..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignerTest.groovy +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.io.Encoders -import io.jsonwebtoken.security.Keys -import org.junit.Test - -import static org.junit.Assert.assertNotNull -import static org.junit.Assert.assertSame - -class DefaultJwtSignerTest { - - @Test - //TODO: remove this before 1.0 since it tests a deprecated method - @Deprecated - //remove just before 1.0.0 release - void testDeprecatedTwoArgCtor() { - - def alg = SignatureAlgorithm.HS256 - def key = Keys.secretKeyFor(alg) - def signer = new DefaultJwtSigner(alg, key) - - assertNotNull signer.signer - assertSame Encoders.BASE64URL, signer.base64UrlEncoder - } - - @Test - //TODO: remove this before 1.0 since it tests a deprecated method - @Deprecated - //remove just before 1.0.0 release - void testDeprecatedThreeArgCtor() { - - def alg = SignatureAlgorithm.HS256 - def key = Keys.secretKeyFor(alg) - def signer = new DefaultJwtSigner(DefaultSignerFactory.INSTANCE, alg, key) - - assertNotNull signer.signer - assertSame Encoders.BASE64URL, signer.base64UrlEncoder - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy deleted file mode 100644 index a09c198a0..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2015 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.Keys -import org.junit.Test - -import static org.junit.Assert.assertEquals -import static org.junit.Assert.fail - -class DefaultSignatureValidatorFactoryTest { - - @Test - void testNoneAlgorithm() { - try { - new DefaultSignatureValidatorFactory().createSignatureValidator( - SignatureAlgorithm.NONE, Keys.secretKeyFor(SignatureAlgorithm.HS256)) - fail() - } catch (IllegalArgumentException iae) { - assertEquals iae.message, "The 'NONE' algorithm cannot be used for signing." - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy deleted file mode 100644 index 5d33fd743..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.Keys -import org.junit.Test - -import static org.junit.Assert.assertEquals -import static org.junit.Assert.fail - -class DefaultSignerFactoryTest { - - @Test - void testCreateSignerWithNoneAlgorithm() { - - def factory = new DefaultSignerFactory(); - - try { - factory.createSigner(SignatureAlgorithm.NONE, Keys.secretKeyFor(SignatureAlgorithm.HS256)) - fail(); - } catch (IllegalArgumentException iae) { - assertEquals iae.message, "The 'NONE' algorithm cannot be used for signing." - } - } - -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy deleted file mode 100644 index 7e61b8f95..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2015 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import org.junit.Test - -import java.security.KeyPair -import java.security.NoSuchProviderException -import java.security.interfaces.ECPrivateKey -import java.security.interfaces.ECPublicKey - -import static org.junit.Assert.* - -class EllipticCurveProviderTest { - - @Test - void testGenerateKeyPair() { - KeyPair pair = EllipticCurveProvider.generateKeyPair() - assertNotNull pair - assertTrue pair.public instanceof ECPublicKey - assertTrue pair.private instanceof ECPrivateKey - } - - @Test - void testGenerateKeyPairWithInvalidProviderName() { - try { - EllipticCurveProvider.generateKeyPair("EC", "Foo", SignatureAlgorithm.ES256, null) - fail() - } catch (IllegalStateException ise) { - assertEquals ise.message, "Unable to generate Elliptic Curve KeyPair: no such provider: Foo" - assertTrue ise.cause instanceof NoSuchProviderException - } - } - - @Test - void testGenerateKeyPairWithNullAlgorithm() { - try { - EllipticCurveProvider.generateKeyPair("EC", "Foo", null, null) - fail() - } catch (IllegalArgumentException ise) { - assertEquals ise.message, "SignatureAlgorithm argument cannot be null." - } - } - - @Test - void testGenerateKeyPairWithNonEllipticCurveAlgorithm() { - try { - EllipticCurveProvider.generateKeyPair("EC", "Foo", SignatureAlgorithm.HS256, null) - fail() - } catch (IllegalArgumentException ise) { - assertEquals ise.message, "SignatureAlgorithm argument must represent an Elliptic Curve algorithm." - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy deleted file mode 100644 index 6e706f8bd..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2015 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto - -import io.jsonwebtoken.JwtException -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.impl.security.Randoms -import io.jsonwebtoken.io.Decoders -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import java.security.* -import java.security.spec.X509EncodedKeySpec - -import static org.junit.Assert.* - -class EllipticCurveSignatureValidatorTest { - - @Test - void testDoVerifyWithInvalidKeyException() { - - String msg = 'foo' - final InvalidKeyException ex = new InvalidKeyException(msg) - - def v = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, EllipticCurveProvider.generateKeyPair().public) { - @Override - protected boolean doVerify(Signature sig, PublicKey pk, byte[] data, byte[] signature) throws InvalidKeyException, java.security.SignatureException { - throw ex; - } - } - - byte[] bytes = new byte[16] - byte[] signature = new byte[16] - Randoms.secureRandom().nextBytes(bytes) - Randoms.secureRandom().nextBytes(signature) - - try { - v.isValid(bytes, signature) - fail(); - } catch (SignatureException se) { - assertEquals se.message, 'Unable to verify Elliptic Curve signature using configured ECPublicKey. ' + msg - assertSame se.cause, ex - } - } - - @Test - void invalidAlgorithmTest() { - def invalidAlgorithm = SignatureAlgorithm.HS256 - try { - EllipticCurveProvider.getSignatureByteArrayLength(invalidAlgorithm) - fail() - } catch (JwtException e) { - assertEquals e.message, 'Unsupported Algorithm: ' + invalidAlgorithm.name() - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy deleted file mode 100644 index 6c418fec0..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2015 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto - -import io.jsonwebtoken.JwtException -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.impl.security.Randoms -import io.jsonwebtoken.security.Keys -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import java.security.InvalidKeyException -import java.security.KeyPair -import java.security.PrivateKey -import java.security.PublicKey - -import static org.junit.Assert.* - -class EllipticCurveSignerTest { - - @Test - void testConstructorWithoutECAlg() { - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - try { - new EllipticCurveSigner(alg, Keys.secretKeyFor(alg)) - fail('EllipticCurveSigner should reject non ECPrivateKeys') - } catch (IllegalArgumentException expected) { - assertEquals expected.message, 'SignatureAlgorithm must be an Elliptic Curve algorithm.' - } - } - - @Test - void testConstructorWithoutECPrivateKey() { - def key = Keys.secretKeyFor(SignatureAlgorithm.HS256) - try { - new EllipticCurveSigner(SignatureAlgorithm.ES256, key) - fail('EllipticCurveSigner should reject non ECPrivateKey instances.') - } catch (IllegalArgumentException expected) { - assertEquals expected.message, "Elliptic Curve signatures must be computed using an EC PrivateKey. The specified key of " + - "type " + key.getClass().getName() + " is not an EC PrivateKey." - } - } - - @Test - void testDoSignWithInvalidKeyException() { - - SignatureAlgorithm alg = SignatureAlgorithm.ES256 - - KeyPair kp = Keys.keyPairFor(alg) - PublicKey publicKey = kp.getPublic() - PrivateKey privateKey = kp.getPrivate() - - String msg = 'foo' - final InvalidKeyException ex = new InvalidKeyException(msg) - - def signer = new EllipticCurveSigner(alg, privateKey) { - @Override - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException { - throw ex - } - } - - byte[] bytes = new byte[16] - Randoms.secureRandom().nextBytes(bytes) - - try { - signer.sign(bytes) - fail(); - } catch (SignatureException se) { - assertEquals se.message, 'Invalid Elliptic Curve PrivateKey. ' + msg - assertSame se.cause, ex - } - } - - @Test - void testDoSignWithJoseSignatureFormatException() { - - KeyPair kp = EllipticCurveProvider.generateKeyPair() - PublicKey publicKey = kp.getPublic(); - PrivateKey privateKey = kp.getPrivate(); - - String msg = 'foo' - final JwtException ex = new JwtException(msg) - - def signer = new EllipticCurveSigner(SignatureAlgorithm.ES256, privateKey) { - @Override - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JwtException { - throw ex - } - } - - byte[] bytes = new byte[16] - Randoms.secureRandom().nextBytes(bytes) - - try { - signer.sign(bytes) - fail(); - } catch (SignatureException se) { - assertEquals se.message, 'Unable to convert signature to JOSE format. ' + msg - assertSame se.cause, ex - } - } - - @Test - void testDoSignWithJdkSignatureException() { - - KeyPair kp = EllipticCurveProvider.generateKeyPair() - PublicKey publicKey = kp.getPublic(); - PrivateKey privateKey = kp.getPrivate(); - - String msg = 'foo' - final java.security.SignatureException ex = new java.security.SignatureException(msg) - - def signer = new EllipticCurveSigner(SignatureAlgorithm.ES256, privateKey) { - @Override - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException { - throw ex - } - } - - byte[] bytes = new byte[16] - Randoms.secureRandom().nextBytes(bytes) - - try { - signer.sign(bytes) - fail(); - } catch (SignatureException se) { - assertEquals se.message, 'Unable to calculate signature using Elliptic Curve PrivateKey. ' + msg - assertSame se.cause, ex - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy deleted file mode 100644 index 086028cd8..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2015 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import org.junit.Test - -import javax.crypto.SecretKey - -import static org.junit.Assert.assertEquals - -class MacProviderTest { - - private void testHmac(SignatureAlgorithm alg) { - testHmac(alg, MacProvider.generateKey(alg)) - } - - private void testHmac(SignatureAlgorithm alg, SecretKey key) { - assertEquals alg.jcaName, key.algorithm - assertEquals alg.digestLength / 8 as int, key.encoded.length - } - - @Test - void testDefault() { - testHmac(SignatureAlgorithm.HS512, MacProvider.generateKey()) - } - - @Test - void testHS256() { - testHmac(SignatureAlgorithm.HS256) - } - - @Test - void testHS384() { - testHmac(SignatureAlgorithm.HS384) - } - - @Test - void testHS512() { - testHmac(SignatureAlgorithm.HS512) - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy deleted file mode 100644 index ab6ebca7a..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import javax.crypto.Mac -import java.security.InvalidKeyException -import java.security.Key -import java.security.NoSuchAlgorithmException - -import static org.junit.Assert.* - -class MacSignerTest { - - private static final Random rng = new Random(); //doesn't need to be secure - we're just testing - - @Test - void testCtorArgNotASecretKey() { - - def key = new Key() { - @Override - String getAlgorithm() { - return null - } - - @Override - String getFormat() { - return null - } - - @Override - byte[] getEncoded() { - return new byte[0] - } - } - - try { - new MacSigner(SignatureAlgorithm.HS256, key) - fail() - } catch (IllegalArgumentException expected) { - } - } - - @Test - void testNoSuchAlgorithmException() { - byte[] key = new byte[32]; - byte[] data = new byte[32]; - rng.nextBytes(key); - rng.nextBytes(data); - - def s = new MacSigner(SignatureAlgorithm.HS256, key) { - @Override - protected Mac doGetMacInstance() throws NoSuchAlgorithmException, InvalidKeyException { - throw new NoSuchAlgorithmException("foo"); - } - } - try { - s.sign(data); - fail(); - } catch (SignatureException e) { - assertTrue e.cause instanceof NoSuchAlgorithmException - assertEquals e.cause.message, 'foo' - } - } - - @Test - void testInvalidKeyException() { - byte[] key = new byte[32]; - byte[] data = new byte[32]; - rng.nextBytes(key); - rng.nextBytes(data); - - def s = new MacSigner(SignatureAlgorithm.HS256, key) { - @Override - protected Mac doGetMacInstance() throws NoSuchAlgorithmException, InvalidKeyException { - throw new InvalidKeyException("foo"); - } - } - try { - s.sign(data); - fail(); - } catch (SignatureException e) { - assertTrue e.cause instanceof InvalidKeyException - assertEquals e.cause.message, 'foo' - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/PowermockMacProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/PowermockMacProviderTest.groovy deleted file mode 100644 index 4058d422a..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/PowermockMacProviderTest.groovy +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import org.junit.Test -import org.junit.runner.RunWith -import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.modules.junit4.PowerMockRunner - -import javax.crypto.KeyGenerator -import java.security.NoSuchAlgorithmException - -import static org.easymock.EasyMock.eq -import static org.easymock.EasyMock.expect -import static org.junit.Assert.* -import static org.powermock.api.easymock.PowerMock.* - -/** - * This needs to be a separate class beyond MacProviderTest because it mocks the KeyGenerator class which messes up - * the other implementation tests in MacProviderTest. - */ -@RunWith(PowerMockRunner.class) -@PrepareForTest([KeyGenerator]) -class PowermockMacProviderTest { - - @Test - void testNoSuchAlgorithm() { - - mockStatic(KeyGenerator) - - def alg = SignatureAlgorithm.HS256 - def ex = new NoSuchAlgorithmException('foo') - - expect(KeyGenerator.getInstance(eq(alg.jcaName))).andThrow(ex) - - replay KeyGenerator - - try { - MacProvider.generateKey(alg) - fail() - } catch (IllegalStateException e) { - assertEquals 'The HmacSHA256 algorithm is not available. This should never happen on JDK 7 or later - ' + - 'please report this to the JJWT developers.', e.message - assertSame ex, e.getCause() - } - - verify KeyGenerator - - reset KeyGenerator - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy deleted file mode 100644 index 05c547f98..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2015 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.impl.security.Randoms -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import java.security.InvalidAlgorithmParameterException -import java.security.KeyPair -import java.security.Signature -import java.security.interfaces.RSAPrivateKey -import java.security.interfaces.RSAPublicKey -import java.security.spec.PSSParameterSpec - -import static org.junit.Assert.* - -class RsaProviderTest { - - @Test - void testGenerateKeyPair() { - KeyPair pair = RsaProvider.generateKeyPair() - assertNotNull pair - assertTrue pair.public instanceof RSAPublicKey - assertTrue pair.private instanceof RSAPrivateKey - } - - @Test - void testGenerateKeyPairWithInvalidProviderName() { - try { - RsaProvider.generateKeyPair('foo', 1024, Randoms.secureRandom()) - fail() - } catch (IllegalStateException ise) { - assertTrue ise.message.startsWith("Unable to obtain an RSA KeyPairGenerator: ") - } - } - - @Test - void testCreateSignatureInstanceWithInvalidPSSParameterSpecAlgorithm() { - - def p = new RsaProvider(SignatureAlgorithm.PS256, RsaProvider.generateKeyPair(512).public) { - @Override - protected void doSetParameter(Signature sig, PSSParameterSpec spec) throws InvalidAlgorithmParameterException { - throw new InvalidAlgorithmParameterException('foo') - } - } - - try { - p.createSignatureInstance() - fail() - } catch (SignatureException se) { - assertTrue se.message.startsWith('Unsupported RSASSA-PSS parameter') - assertEquals se.cause.message, 'foo' - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy deleted file mode 100644 index 5bd20bc14..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2015 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.Keys -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import java.security.* - -import static org.junit.Assert.* - -class RsaSignatureValidatorTest { - - private static final Random rng = new Random(); //doesn't need to be secure - we're just testing - - @Test - void testConstructorWithNonRsaKey() { - try { - new RsaSignatureValidator(SignatureAlgorithm.RS256, Keys.secretKeyFor(SignatureAlgorithm.HS256)); - fail() - } catch (IllegalArgumentException iae) { - assertEquals "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance.", iae.message - } - } - - @Test - void testConstructorWithRsaPublicKey() { - def pair = RsaProvider.generateKeyPair(2048) - def validator = new RsaSignatureValidator(SignatureAlgorithm.RS256, pair.getPublic()); - assertNull validator.SIGNER - } - - @Test - void testConstructorWithRsaPrivateKey() { - def pair = RsaProvider.generateKeyPair(2048) - def validator = new RsaSignatureValidator(SignatureAlgorithm.RS256, pair.getPrivate()); - assertTrue validator.SIGNER instanceof RsaSigner - } - - @Test - void testDoVerifyWithInvalidKeyException() { - - SignatureAlgorithm alg = SignatureAlgorithm.RS256 - - KeyPair kp = Keys.keyPairFor(alg) - PublicKey publicKey = kp.getPublic() - PrivateKey privateKey = kp.getPrivate() - - String msg = 'foo' - final InvalidKeyException ex = new InvalidKeyException(msg) - - RsaSignatureValidator v = new RsaSignatureValidator(alg, publicKey) { - @Override - protected boolean doVerify(Signature sig, PublicKey pk, byte[] data, byte[] signature) throws InvalidKeyException, java.security.SignatureException { - throw ex; - } - } - - byte[] bytes = new byte[16] - byte[] signature = new byte[16] - rng.nextBytes(bytes) - rng.nextBytes(signature) - - try { - v.isValid(bytes, signature) - fail(); - } catch (SignatureException se) { - assertEquals se.message, 'Unable to verify RSA signature using configured PublicKey. ' + msg - assertSame se.cause, ex - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy deleted file mode 100644 index d0470636a..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (C) 2015 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import javax.crypto.spec.SecretKeySpec -import java.security.* - -import static org.junit.Assert.* - -class RsaSignerTest { - - private static final Random rng = new Random(); //doesn't need to be secure - we're just testing - - @Test - void testConstructorWithoutRsaAlg() { - - byte[] bytes = new byte[16] - rng.nextBytes(bytes) - SecretKeySpec key = new SecretKeySpec(bytes, 'HmacSHA256') - - try { - new RsaSigner(SignatureAlgorithm.HS256, key); - fail('RsaSigner should reject non RSA algorithms.') - } catch (IllegalArgumentException expected) { - assertEquals expected.message, 'SignatureAlgorithm must be an RSASSA or RSASSA-PSS algorithm.'; - } - } - - @Test - void testConstructorWithoutPrivateKey() { - - byte[] bytes = new byte[16] - rng.nextBytes(bytes) - SecretKeySpec key = new SecretKeySpec(bytes, 'HmacSHA256') - - try { - //noinspection GroovyResultOfObjectAllocationIgnored - new RsaSigner(SignatureAlgorithm.RS256, key); - fail('RsaSigner should reject non RSAPrivateKey instances.') - } catch (IllegalArgumentException expected) { - assertEquals expected.message, "RSA signatures must be computed using an RSA PrivateKey. The specified key of type " + - key.getClass().getName() + " is not an RSA PrivateKey."; - } - } - - @Test - void testConstructorWithoutRSAKey() { - - //private key, but not an RSAKey instance: - PrivateKey key = new PrivateKey() { - @Override - String getAlgorithm() { - return null - } - - @Override - String getFormat() { - return null - } - - @Override - byte[] getEncoded() { - return new byte[0] - } - } - - try { - //noinspection GroovyResultOfObjectAllocationIgnored - new RsaSigner(SignatureAlgorithm.RS256, key); - fail('RsaSigner should reject non RSAPrivateKey instances.') - } catch (IllegalArgumentException expected) { - assertEquals expected.message, "RSA signatures must be computed using an RSA PrivateKey. The specified key of type " + - key.getClass().getName() + " is not an RSA PrivateKey."; - } - } - - @Test - void testDoSignWithInvalidKeyException() { - - KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA"); - keyGenerator.initialize(1024); - - KeyPair kp = keyGenerator.genKeyPair(); - PublicKey publicKey = kp.getPublic(); - PrivateKey privateKey = kp.getPrivate(); - - String msg = 'foo' - final InvalidKeyException ex = new InvalidKeyException(msg) - - RsaSigner signer = new RsaSigner(SignatureAlgorithm.RS256, privateKey) { - @Override - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException { - throw ex - } - } - - byte[] bytes = new byte[16] - rng.nextBytes(bytes) - - try { - signer.sign(bytes) - fail(); - } catch (SignatureException se) { - assertEquals se.message, 'Invalid RSA PrivateKey. ' + msg - assertSame se.cause, ex - } - } - - @Test - void testDoSignWithJdkSignatureException() { - - KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA"); - keyGenerator.initialize(1024); - - KeyPair kp = keyGenerator.genKeyPair(); - PublicKey publicKey = kp.getPublic(); - PrivateKey privateKey = kp.getPrivate(); - - String msg = 'foo' - final java.security.SignatureException ex = new java.security.SignatureException(msg) - - RsaSigner signer = new RsaSigner(SignatureAlgorithm.RS256, privateKey) { - @Override - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException { - throw ex - } - } - - byte[] bytes = new byte[16] - rng.nextBytes(bytes) - - try { - signer.sign(bytes) - fail(); - } catch (SignatureException se) { - assertEquals se.message, 'Unable to calculate signature using RSA PrivateKey. ' + msg - assertSame se.cause, ex - } - } - - @Test - void testSignSuccessful() { - - KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA"); - keyGenerator.initialize(1024); - - KeyPair kp = keyGenerator.genKeyPair(); - PrivateKey privateKey = kp.getPrivate(); - - byte[] bytes = new byte[16] - rng.nextBytes(bytes) - - RsaSigner signer = new RsaSigner(SignatureAlgorithm.RS256, privateKey); - byte[] out1 = signer.sign(bytes) - - byte[] out2 = signer.sign(bytes) - - assertTrue(MessageDigest.isEqual(out1, out2)) - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy deleted file mode 100644 index 55fa2e9e2..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2015 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import java.security.NoSuchAlgorithmException -import java.security.Signature - -import static org.junit.Assert.* - -class SignatureProviderTest { - - @Test - void testCreateSignatureInstanceNoSuchAlgorithm() { - - def p = new SignatureProvider(SignatureAlgorithm.HS256, MacProvider.generateKey()) { - @Override - protected Signature getSignatureInstance() throws NoSuchAlgorithmException { - throw new NoSuchAlgorithmException('foo') - } - } - - try { - p.createSignatureInstance() - fail() - } catch (SignatureException se) { - assertEquals se.cause.message, 'foo' - } - } - - @Test - void testCreateSignatureInstanceNoSuchAlgorithmNonStandardAlgorithm() { - - def p = new SignatureProvider(SignatureAlgorithm.PS256, RsaProvider.generateKeyPair().getPrivate()) { - @Override - protected Signature getSignatureInstance() throws NoSuchAlgorithmException { - throw new NoSuchAlgorithmException('foo') - } - } - - try { - p.createSignatureInstance() - fail() - } catch (SignatureException se) { - assertEquals se.cause.message, 'foo' - } - } - - @Test - void testCreateSignatureInstanceNoSuchAlgorithmNonStandardAlgorithmWithoutBouncyCastle() { - - def p = new SignatureProvider(SignatureAlgorithm.PS256, RsaProvider.generateKeyPair().getPrivate()) { - @Override - protected Signature getSignatureInstance() throws NoSuchAlgorithmException { - throw new NoSuchAlgorithmException('foo') - } - - @Override - protected boolean isBouncyCastleAvailable() { - return false - } - } - - try { - p.createSignatureInstance() - fail() - } catch (SignatureException se) { - assertTrue se.message.contains('This is not a standard JDK algorithm. Try including BouncyCastle in the runtime classpath.') - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/PropagatingExceptionFunctionTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/PropagatingExceptionFunctionTest.groovy new file mode 100644 index 000000000..25759f66c --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/PropagatingExceptionFunctionTest.groovy @@ -0,0 +1,28 @@ +package io.jsonwebtoken.impl.lang + +import io.jsonwebtoken.security.SecurityException +import org.junit.Test + +import static org.junit.Assert.assertSame + +class PropagatingExceptionFunctionTest { + + @Test + void testAssignableException() { + + def ex = new SecurityException("test") + + def fn = new PropagatingExceptionFunction<>(SecurityException.class, "foo", new Function() { + @Override + Object apply(Object t) { + throw ex + } + }) + + try { + fn.apply("hi") + } catch (Exception thrown) { + assertSame ex, thrown //because it was assignable, 'thrown' should not be a wrapper exception + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithmTest.groovy deleted file mode 100644 index 174d03c07..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAeadAesEncryptionAlgorithmTest.groovy +++ /dev/null @@ -1,111 +0,0 @@ -package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.security.* -import org.junit.Test - -import javax.crypto.SecretKey -import javax.crypto.spec.SecretKeySpec -import java.security.SecureRandom - -import static org.junit.Assert.* - -/** - * @since JJWT_RELEASE_VERSION - */ -class AbstractAeadAesEncryptionAlgorithmTest { - - @Test(expected = IllegalArgumentException) - void testConstructorWithIvLargerThanAesBlockSize() { - new TestAesEncryptionAlgorithm('foo', 'foo', 136, 128) - } - - @Test(expected = IllegalArgumentException) - void testConstructorWithoutIvLength() { - new TestAesEncryptionAlgorithm('foo', 'foo', 0, 128) - } - - @Test(expected = IllegalArgumentException) - void testConstructorWithoutRequiredKeyLength() { - new TestAesEncryptionAlgorithm('foo', 'foo', 128, 0) - } - - @Test - void testDoEncryptFailure() { - - def alg = new TestAesEncryptionAlgorithm('foo', 'foo', 128, 128) { - @Override - protected AeadIvEncryptionResult doEncrypt(AeadRequest req) throws Exception { - throw new IllegalArgumentException('broken') - } - } - - def req = new DefaultAesEncryptionRequest<>('bar'.getBytes(), alg.generateKey(), 'foo'.getBytes()); - - try { - alg.encrypt(req) - } catch (CryptoException expected) { - assertTrue expected.getCause() instanceof IllegalArgumentException - assertTrue expected.getCause().getMessage().equals('broken') - } - } - - @Test - void testAssertKeyLength() { - - def requiredKeyLength = 16 - - def alg = new TestAesEncryptionAlgorithm('foo', 'foo', 128, requiredKeyLength) - - byte[] bytes = new byte[requiredKeyLength + 1] //not same as requiredKeyByteLength, but it should be - Randoms.secureRandom().nextBytes(bytes) - - try { - alg.assertKeyLength(new SecretKeySpec(bytes, "AES")) - fail() - } catch (CryptoException expected) { - } - } - - @Test - void testGetSecureRandomWhenRequestHasSpecifiedASecureRandom() { - - def alg = new TestAesEncryptionAlgorithm('foo', 'foo', 128, 128) - - def secureRandom = new SecureRandom() - - def req = new DefaultAesEncryptionRequest('data'.getBytes(), alg.generateKey(), null, secureRandom, 'aad'.getBytes()) - - def returnedSecureRandom = alg.ensureSecureRandom(req) - - assertSame(secureRandom, returnedSecureRandom) - } - - @Test(expected = CryptoException) - void testDoGenerateKeyException() { - def alg = new TestAesEncryptionAlgorithm('foo', 'foo', 128, 128) { - @Override - protected SecretKey doGenerateKey() throws Exception { - throw new IllegalStateException("testmsg") - } - } - alg.generateKey() - } - - static class TestAesEncryptionAlgorithm extends AbstractAeadAesEncryptionAlgorithm { - - TestAesEncryptionAlgorithm(String name, String transformationString, int generatedIvLengthInBits, int requiredKeyLengthInBits) { - super(name, transformationString, generatedIvLengthInBits, requiredKeyLengthInBits) - } - - @Override - protected AeadIvEncryptionResult doEncrypt(AeadRequest secretKeyAeadRequest) throws Exception { - return null - } - - @Override - protected byte[] doDecrypt(AeadIvRequest secretKeyAeadIvDecryptionRequest) throws Exception { - return new byte[0] - } - } - -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy new file mode 100644 index 000000000..412b410ed --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy @@ -0,0 +1,64 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.lang.Assert +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPublicJwkBuilder +import io.jsonwebtoken.security.SignatureAlgorithms +import org.junit.Test + +import java.security.cert.X509Certificate +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class AbstractAsymmetricJwkBuilderTest { + + private static final X509Certificate CERT = CertUtils.readTestCertificate(SignatureAlgorithms.RS256) + private static final List CHAIN = [CERT] + private static final RSAPublicKey PUB_KEY = (RSAPublicKey) CERT.getPublicKey() + + private static RsaPublicJwkBuilder builder() { + return Jwks.builder().setKey(PUB_KEY) + } + + @Test + void testUse() { + def val = UUID.randomUUID().toString() + def jwk = builder().setPublicKeyUse(val).build() + assertEquals val, jwk.getPublicKeyUse() + assertEquals val, jwk.use + + def privateKey = CertUtils.readTestPrivateKey(SignatureAlgorithms.RS256) + + jwk = builder().setPublicKeyUse(val).setPrivateKey((RSAPrivateKey) privateKey).build() + assertEquals val, jwk.getPublicKeyUse() + assertEquals val, jwk.use + } + + @Test + void testX509Url() { + def val = new URI(UUID.randomUUID().toString()) + assertSame val, builder().setX509Url(val).build().getX509Url() + } + + @Test + void testX509CertificateChain() { + assertEquals CHAIN, builder().setX509CertificateChain(CHAIN).build().getX509CertificateChain() + } + + @Test + void testX509CertificateSha1Thumbprint() { + def jwk = builder().setX509CertificateChain(CHAIN).withX509Sha1Thumbprint(true).build() + Assert.notEmpty(jwk.getX509CertificateSha1Thumbprint()) + Assert.hasText(jwk.get(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT) as String) + } + + @Test + void testX509CertificateSha256Thumbprint() { + def jwk = builder().setX509CertificateChain(CHAIN).withX509Sha256Thumbprint(true).build() + Assert.notEmpty(jwk.getX509CertificateSha256Thumbprint()) + Assert.hasText(jwk.get(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT) as String) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkTest.groovy deleted file mode 100644 index 0839fad8d..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkTest.groovy +++ /dev/null @@ -1,116 +0,0 @@ -package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.security.CurveId -import io.jsonwebtoken.security.CurveIds -import io.jsonwebtoken.security.MalformedKeyException -import org.junit.Test -import static org.junit.Assert.* - -class AbstractEcJwkTest { - - class TestEcJwk extends AbstractEcJwk { - } - - @Test - void testType() { - assertEquals 'EC', new TestEcJwk().getType() - } - - @Test - void testSetNullX() { - try { - new TestEcJwk().setX(null) - fail() - } catch (IllegalArgumentException e) { - assertEquals "EC JWK x coordinate ('x' property) cannot be null.", e.getMessage() - } - } - - @Test - void testSetEmptyX() { - try { - new TestEcJwk().setX(' ') - fail() - } catch (IllegalArgumentException e) { - assertEquals "EC JWK x coordinate ('x' property) cannot be null or empty.", e.getMessage() - } - } - - @Test - void testX() { - def jwk = new TestEcJwk() - assertEquals 'x', AbstractEcJwk.X - String val = UUID.randomUUID().toString() - jwk.setX(val) - assertEquals val, jwk.get(AbstractEcJwk.X) - assertEquals val, jwk.getX() - } - - @Test - void testY() { - def jwk = new TestEcJwk() - assertEquals 'y', AbstractEcJwk.Y - - jwk.setY(null) //is allowed to be null for non-standard curves - assertNull jwk.get(AbstractEcJwk.Y) - assertNull jwk.getY() - - jwk.setY(' ') - assertNull jwk.get(AbstractEcJwk.Y) - assertNull jwk.getY() - - String val = UUID.randomUUID().toString() - jwk.setY(val) - assertEquals val, jwk.get(AbstractEcJwk.Y) - assertEquals val, jwk.getY() - } - - @Test - void testSetNullCurveId() { - try { - new TestEcJwk().setCurveId(null) - fail() - } catch (IllegalArgumentException iae) { - assertEquals "EC JWK curve id ('crv' property) cannot be null.", iae.getMessage() - } - } - - @Test - void testCurveId() { - def jwk = new TestEcJwk() - assertEquals 'crv', AbstractEcJwk.CURVE_ID - assertNull jwk.getCurveId() - - for(CurveId id : CurveIds.values()) { - jwk.setCurveId(id) - assertEquals id, jwk.get(AbstractEcJwk.CURVE_ID) - assertEquals id, jwk.getCurveId() - jwk.remove(AbstractEcJwk.CURVE_ID) - } - - //assert string conversion works: - for(CurveId id : CurveIds.values()) { - String sval = id.toString() - jwk.put(AbstractEcJwk.CURVE_ID, sval) - CurveId returned = jwk.getCurveId() - assertEquals id, returned - assertEquals id, jwk.get(AbstractEcJwk.CURVE_ID) //ensure conversion occurred - } - } - - @Test - void testGetCurveIdWithInvalidValueType() { - - def jwk = new TestEcJwk() - - def val = new Integer(5) - jwk.put(AbstractEcJwk.CURVE_ID, val) - - try { - jwk.getCurveId() - fail() - } catch (MalformedKeyException e) { - assertEquals "EC JWK 'crv' value must be an CurveId or a String. Value has type: " + val.getClass().getName(), e.getMessage() - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkValidatorTest.groovy deleted file mode 100644 index db289613c..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkValidatorTest.groovy +++ /dev/null @@ -1,57 +0,0 @@ -package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.security.CurveIds -import io.jsonwebtoken.security.MalformedKeyException -import io.jsonwebtoken.security.PublicEcJwk -import org.junit.Test - -class AbstractEcJwkValidatorTest { - - static AbstractEcJwkValidator VALIDATOR = - DefaultPublicEcJwkBuilder.VALIDATOR as AbstractEcJwkValidator - - @Test - void testValid() { - def jwk = new DefaultPublicEcJwk().setCurveId(CurveIds.P256).setX('x').setY('y') - VALIDATOR.validate(jwk) - } - - @Test(expected = MalformedKeyException) - void testIncorrectType() { - def jwk = new DefaultPublicEcJwk() - jwk.put('kty', 'foo') - VALIDATOR.validate(jwk) - } - - @Test(expected = MalformedKeyException) - void testNullCurveId() { - def jwk = new DefaultPublicEcJwk().setX('x').setY('y') - VALIDATOR.validate(jwk) - } - - @Test(expected = MalformedKeyException) - void testNullX() { - def jwk = new DefaultPublicEcJwk().setCurveId(CurveIds.P521) - VALIDATOR.validate(jwk) - } - - @Test(expected = MalformedKeyException) - void testEmptyX() { - def jwk = new DefaultPublicEcJwk().setCurveId(CurveIds.P521) - jwk.put('x', ' ') - VALIDATOR.validate(jwk) - } - - @Test(expected = MalformedKeyException) - void testNullY() { - def jwk = new DefaultPublicEcJwk().setCurveId(CurveIds.P521).setX('x') - VALIDATOR.validate(jwk) - } - - @Test(expected = MalformedKeyException) - void testEmptyY() { - def jwk = new DefaultPublicEcJwk().setCurveId(CurveIds.P521).setX('x') - jwk.put('y', ' ') - VALIDATOR.validate(jwk) - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithmTest.groovy deleted file mode 100644 index e074e724a..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEncryptionAlgorithmTest.groovy +++ /dev/null @@ -1,56 +0,0 @@ -package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.security.CryptoException -import io.jsonwebtoken.security.CryptoRequest -import io.jsonwebtoken.security.EncryptionResult -import org.junit.Test - -import javax.crypto.spec.SecretKeySpec - -import static org.junit.Assert.assertSame - -class AbstractEncryptionAlgorithmTest { - - @Test - void testDoEncryptCryptoExceptionPropagates() { - - final CryptoException expected = new CryptoException("foo") - - AbstractEncryptionAlgorithm alg = new AbstractEncryptionAlgorithm('foo', 'foo') { - protected EncryptionResult doEncrypt(CryptoRequest cryptoRequest) throws Exception { - throw expected - } - protected byte[] doDecrypt(CryptoRequest cryptoRequest) throws Exception { - throw new IllegalStateException("should not be called") - } - } - - try { - alg.encrypt(new DefaultCryptoRequest(new byte[1], new SecretKeySpec(new byte[1], 'AES'), null, null)) - } catch (CryptoException thrown) { - assertSame expected, thrown - } - } - - @Test - void testDecryptWithNonCryptoExceptionThrowsCryptoException() { - - final IllegalStateException expected = new IllegalStateException("decrypt") - - AbstractEncryptionAlgorithm alg = new AbstractEncryptionAlgorithm('foo', 'foo') { - protected EncryptionResult doEncrypt(CryptoRequest cryptoRequest) throws Exception { - throw new IllegalStateException("should not be called") - } - protected byte[] doDecrypt(CryptoRequest cryptoRequest) throws Exception { - throw expected - } - } - - try { - alg.decrypt(new DefaultCryptoRequest(new byte[1], new SecretKeySpec(new byte[1], 'AES'), null, null)) - } catch (CryptoException thrown) { - assertSame expected, thrown.getCause() - } - } - -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy index bf0ae0612..81a1a9785 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy @@ -1,98 +1,113 @@ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.security.Jwk +import io.jsonwebtoken.security.EncryptionAlgorithms +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.SecretJwk import org.junit.Test -import static org.junit.Assert.* -class AbstractJwkBuilderTest { +import javax.crypto.SecretKey +import java.security.Security - static final JwkValidator TEST_VALIDATOR = new TestJwkValidator() +import static org.junit.Assert.* - class TestJwkBuilder extends AbstractJwkBuilder { - def TestJwkBuilder(JwkValidator validator=TEST_VALIDATOR) { - super(validator) - } - @Override - def Jwk newJwk() { - return new TestJwk() - } - } +class AbstractJwkBuilderTest { - class NullJwkBuilder extends AbstractJwkBuilder { - def NullJwkBuilder(JwkValidator validator=TEST_VALIDATOR) { - super(validator) - } - @Override - def Jwk newJwk() { - return null - } - } + private static final SecretKey SKEY = EncryptionAlgorithms.A256GCM.generateKey(); - @Test(expected = IllegalArgumentException) - void testCtorWithNullValidator() { - new TestJwkBuilder(null) + private static AbstractJwkBuilder builder() { + return (AbstractJwkBuilder)Jwks.builder().setKey(SKEY) } @Test - void testCtorNonNullNewJwk() { - def builder = new TestJwkBuilder() - assertTrue builder.jwk instanceof TestJwk + void testKeyType() { + def jwk = builder().build() + assertEquals 'oct', jwk.getType() + assertNotNull jwk.k // JWA id for raw key value } - @Test(expected=IllegalArgumentException) - void testCtorWithSubclassNullJwk() { - new NullJwkBuilder() + @Test + void testPut() { + def a = UUID.randomUUID() + def builder = builder() + builder.put('foo', a) + assertEquals a, builder.build().get('foo') } @Test - void testUse() { - def val = UUID.randomUUID().toString() - assertEquals val, new TestJwkBuilder().setUse(val).build().getUse() + void testPutAll() { + def foo = UUID.randomUUID() + def bar = UUID.randomUUID().toString() //different type + def m = [foo: foo, bar: bar] + def jwk = builder().putAll(m).build() + assertEquals foo, jwk.foo + assertEquals bar, jwk.bar } @Test - void testOperations() { - def a = UUID.randomUUID().toString() - def b = UUID.randomUUID().toString() - def set = [a, b] as Set - assertEquals set, new TestJwkBuilder().setOperations(set).build().getOperations() + void testAlgorithm() { + def alg = 'someAlgorithm' + def jwk = builder().setAlgorithm(alg).build() + assertEquals alg, jwk.getAlgorithm() + assertEquals alg, jwk.alg //test raw get via JWA member id } @Test - void testAlgorithm() { - def val = UUID.randomUUID().toString() - assertEquals val, new TestJwkBuilder().setAlgorithm(val).build().getAlgorithm() + void testAlgorithmByPut() { + def alg = 'someAlgorithm' + def jwk = builder().put('alg', alg).build() //ensure direct put still is handled properly + assertEquals alg, jwk.getAlgorithm() + assertEquals alg, jwk.alg //test raw get via JWA member id } @Test void testId() { - def val = UUID.randomUUID().toString() - assertEquals val, new TestJwkBuilder().setId(val).build().getId() + def kid = UUID.randomUUID().toString() + def jwk = builder().setId(kid).build() + assertEquals kid, jwk.getId() + assertEquals kid, jwk.kid //test raw get via JWA member id } @Test - void testX509Url() { - def val = new URI(UUID.randomUUID().toString()) - assertEquals val, new TestJwkBuilder().setX509Url(val).build().getX509Url() + void testIdByPut() { + def kid = UUID.randomUUID().toString() + def jwk = builder().put('kid', kid).build() + assertEquals kid, jwk.getId() + assertEquals kid, jwk.kid //test raw get via JWA member id } @Test - void testX509CertificateChain() { + void testOperations() { def a = UUID.randomUUID().toString() def b = UUID.randomUUID().toString() - def val = [a, b] as List - assertEquals val, new TestJwkBuilder().setX509CertificateChain(val).build().getX509CertficateChain() + def set = [a, b] as Set + def jwk = builder().setOperations(set).build() + assertEquals set, jwk.getOperations() + assertEquals set, jwk.key_ops } @Test - void testX509CertificateSha1Thumbprint() { - def val = UUID.randomUUID().toString() - assertEquals val, new TestJwkBuilder().setX509CertificateSha1Thumbprint(val).build().getX509CertificateSha1Thumbprint() + void testOperationsByPut() { + def a = UUID.randomUUID().toString() + def b = UUID.randomUUID().toString() + def set = [a, b] as Set + def jwk = builder().put('key_ops', set).build() + assertEquals set, jwk.getOperations() + assertEquals set, jwk.key_ops + } + + @Test //ensures that even if a raw single value is present it is represented as a Set per the JWA spec (string array) + void testOperationsByPutSingleValue() { + def a = UUID.randomUUID().toString() + def set = [a] as Set + def jwk = builder().put('key_ops', a).build() // <-- put uses single raw value, not a set + assertEquals set, jwk.getOperations() // <-- still get a set + assertEquals set, jwk.key_ops // <-- still get a set } @Test - void testX509CertificateSha256Thumbprint() { - def val = UUID.randomUUID().toString() - assertEquals val, new TestJwkBuilder().setX509CertificateSha256Thumbprint(val).build().getX509CertificateSha256Thumbprint() + void testProvider() { + def provider = Security.getProvider("BC") + def jwk = builder().setProvider(provider).build() + assertEquals 'oct', jwk.getType() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkValidatorTest.groovy deleted file mode 100644 index 832cd9508..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkValidatorTest.groovy +++ /dev/null @@ -1,55 +0,0 @@ -package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.security.MalformedKeyException -import org.junit.Test -import static org.junit.Assert.* - -class AbstractJwkValidatorTest { - - static final String malformedMsg = "JWKs must have a key type ('kty') property value." - - @Test - void testValidateWithNullType() { - def jwk = new TestJwk() - jwk.remove('kty') - try { - new TestJwkValidator<>().validate(jwk) - fail() - } catch (MalformedKeyException e) { - assertEquals malformedMsg, e.getMessage() - } - } - - @Test - void testValidateWithEmptyType() { - def jwk = new TestJwk() - jwk.put('kty', ' ') - try { - new TestJwkValidator<>().validate(jwk) - fail() - } catch (MalformedKeyException e) { - assertEquals malformedMsg, e.getMessage() - } - } - - @Test - void testIncorrectType() { - def jwk = new TestJwk() - jwk.put('kty', 'foo') - try { - new TestJwkValidator<>().validate(jwk) - fail() - } catch (MalformedKeyException e) { - assertEquals "JWK does not have expected key type ('kty') value of 'test'. Value found: foo", - e.getMessage() - } - } - - @Test - void testValid() { - def jwk = new TestJwk() - def validator = new TestJwkValidator() - validator.validate(jwk) - assertEquals jwk, validator.jwk - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy index 69b89c86a..6cb252f82 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy @@ -1,114 +1,25 @@ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.security.CryptoRequest import io.jsonwebtoken.security.SignatureAlgorithms import io.jsonwebtoken.security.SignatureException +import io.jsonwebtoken.security.SignatureRequest import io.jsonwebtoken.security.VerifySignatureRequest import org.junit.Test -import javax.xml.crypto.dsig.spec.HMACParameterSpec import java.nio.charset.StandardCharsets import java.security.* -import java.security.spec.AlgorithmParameterSpec -import static org.easymock.EasyMock.createMock import static org.junit.Assert.* class AbstractSignatureAlgorithmTest { - @Test - void testCreateSignatureInstanceFailureNoProvider() { - - def alg = new TestAbstractSignatureAlgorithm() { - @Override - protected Signature getSignatureInstance(Provider provider) throws NoSuchAlgorithmException { - throw new NoSuchAlgorithmException('message-here') - } - } - - try { - alg.createSignatureInstance(null, null) - } catch (SignatureException e) { - assertEquals 'JWT signature algorithm \'test\' uses the JCA algorithm \'test\', which is not available in the current JVM. Try explicitly supplying a JCA Provider that supports the JCA algorithm name \'test\'. Cause: message-here', e.getMessage() - } - } - - @Test - void testCreateSignatureInstanceFailureWithoutBouncyCastle() { - def alg = new TestAbstractSignatureAlgorithm() { - @Override - protected Signature getSignatureInstance(Provider provider) throws NoSuchAlgorithmException { - throw new NoSuchAlgorithmException('message-here') - } - - @Override - protected boolean isBouncyCastleAvailable() { - return false - } - } - - try { - alg.createSignatureInstance(null, null) - } catch (SignatureException e) { - assertEquals 'JWT signature algorithm \'test\' uses the JCA algorithm \'test\', which is not available in the current JVM. Try including BouncyCastle in the runtime classpath, or explicitly supplying a JCA Provider that supports the JCA algorithm name \'test\'. Cause: message-here', e.getMessage() - } - - } - - @Test - void testCreateSignatureInstanceFailureWithProvider() { - - def mockProvider = createMock(Provider) - - def alg = new TestAbstractSignatureAlgorithm() { - @Override - protected Signature getSignatureInstance(Provider provider) throws NoSuchAlgorithmException { - throw new NoSuchAlgorithmException('message-here') - } - } - - try { - alg.createSignatureInstance(mockProvider, null) - } catch (SignatureException e) { - assertEquals 'JWT signature algorithm \'test\' uses the JCA algorithm \'test\', which is not supported by the specified JCA Provider {EasyMock for class java.security.Provider}. Try explicitly supplying a JCA Provider that supports the JCA algorithm name \'test\'. Cause: message-here', e.getMessage() - } - } - - @Test - void testCreateSignatureInstanceWithBadAlgParam() { - def alg = new AbstractSignatureAlgorithm('RS256', 'SHA256withRSA') { - @Override - protected void validateKey(Key key, boolean signing) { - } - - @Override - protected byte[] doSign(CryptoRequest request) throws Exception { - return new byte[0] - } - - @Override - protected void setParameter(Signature sig, AlgorithmParameterSpec spec) throws InvalidAlgorithmParameterException { - throw new InvalidAlgorithmParameterException("whatevs") - } - } - - try { - alg.createSignatureInstance(null, new HMACParameterSpec(256)) //not RSA at all - } catch (SignatureException expected) { - String msg = expected.getMessage() - assertTrue msg.startsWith('Unsupported SHA256withRSA parameter {') - assertTrue msg.endsWith('}: whatevs') - } - - } - @Test void testSignAndVerifyWithExplicitProvider() { Provider provider = Security.getProvider('BC') KeyPair pair = SignatureAlgorithms.RS256.generateKeyPair() byte[] data = 'foo'.getBytes(StandardCharsets.UTF_8) - byte[] signature = SignatureAlgorithms.RS256.sign(new DefaultCryptoRequest(data, pair.getPrivate(), provider, null)) - assertTrue SignatureAlgorithms.RS256.verify(new DefaultVerifySignatureRequest(data, pair.getPublic(), provider, null, signature)) + byte[] signature = SignatureAlgorithms.RS256.sign(new DefaultSignatureRequest(provider, null, data, pair.getPrivate())) + assertTrue SignatureAlgorithms.RS256.verify(new DefaultVerifySignatureRequest(provider, null, data, pair.getPublic(), signature)) } @Test @@ -117,12 +28,12 @@ class AbstractSignatureAlgorithmTest { def ise = new IllegalStateException('foo') def alg = new TestAbstractSignatureAlgorithm() { @Override - protected byte[] doSign(CryptoRequest request) throws Exception { + protected byte[] doSign(SignatureRequest request) throws Exception { throw ise } } try { - alg.sign(new DefaultCryptoRequest('foo'.getBytes(StandardCharsets.UTF_8), pair.getPrivate(), null, null)) + alg.sign(new DefaultSignatureRequest(null, null, 'foo'.getBytes(StandardCharsets.UTF_8), pair.getPrivate())) } catch (SignatureException e) { assertTrue e.getMessage().startsWith('Unable to compute test signature with JCA algorithm \'test\' using key {') assertTrue e.getMessage().endsWith('}: foo') @@ -142,8 +53,8 @@ class AbstractSignatureAlgorithmTest { } def data = 'foo'.getBytes(StandardCharsets.UTF_8) try { - byte[] signature = alg.sign(new DefaultCryptoRequest(data, pair.getPrivate(), null, null)) - alg.verify(new DefaultVerifySignatureRequest(data, pair.getPublic(), null, null, signature)) + byte[] signature = alg.sign(new DefaultSignatureRequest(null, null, data, pair.getPrivate())) + alg.verify(new DefaultVerifySignatureRequest(null, null, data, pair.getPublic(), signature)) } catch (SignatureException e) { assertTrue e.getMessage().startsWith('Unable to verify test signature with JCA algorithm \'test\' using key {') assertTrue e.getMessage().endsWith('}: foo') @@ -153,7 +64,7 @@ class AbstractSignatureAlgorithmTest { class TestAbstractSignatureAlgorithm extends AbstractSignatureAlgorithm { - def TestAbstractSignatureAlgorithm() { + TestAbstractSignatureAlgorithm() { super('test', 'test') } @@ -162,7 +73,7 @@ class AbstractSignatureAlgorithmTest { } @Override - protected byte[] doSign(CryptoRequest request) throws Exception { + protected byte[] doSign(SignatureRequest request) throws Exception { return new byte[1] } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy new file mode 100644 index 000000000..667cd9d68 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy @@ -0,0 +1,68 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.* +import org.junit.Test + +import java.security.SecureRandom + +import static org.junit.Assert.assertSame +import static org.junit.Assert.fail + +/** + * @since JJWT_RELEASE_VERSION + */ +class AesAlgorithmTest { + + @Test(expected = IllegalArgumentException) + void testConstructorWithoutRequiredKeyLength() { + new TestAesAlgorithm('foo', 'foo', 0) + } + + @Test + void testAssertKeyLength() { + + def alg = new TestAesAlgorithm('foo', 'foo', 192) + + def key = EncryptionAlgorithms.A128GCM.generateKey() //weaker than required + + def request = new DefaultCryptoRequest(null, null, new byte[1], key) + + try { + alg.assertKey(request) + fail() + } catch (SecurityException expected) { + } + } + + @Test + void testGetSecureRandomWhenRequestHasSpecifiedASecureRandom() { + + def alg = new TestAesAlgorithm('foo', 'foo', 128) + + def secureRandom = new SecureRandom() + + def req = new DefaultAeadRequest(null, secureRandom, 'data'.getBytes(), alg.generateKey(), 'aad'.getBytes()) + + def returnedSecureRandom = alg.ensureSecureRandom(req) + + assertSame(secureRandom, returnedSecureRandom) + } + + static class TestAesAlgorithm extends AesAlgorithm implements AeadAlgorithm { + + TestAesAlgorithm(String name, String transformationString, int requiredKeyLengthInBits) { + super(name, transformationString, requiredKeyLengthInBits) + } + + @Override + AeadResult encrypt(AeadRequest symmetricAeadRequest) { + return null + } + + @Override + PayloadSupplier decrypt(DecryptAeadRequest symmetricAeadDecryptionRequest) { + return null + } + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy new file mode 100644 index 000000000..5230be4ca --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy @@ -0,0 +1,166 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.MalformedJwtException +import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.impl.lang.Bytes +import io.jsonwebtoken.impl.lang.CheckedFunction +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.lang.Arrays +import io.jsonwebtoken.security.EncryptionAlgorithms +import org.junit.Test + +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.* + +class AesGcmKeyAlgorithmTest { + + /** + * This tests asserts that our AeadAlgorithm implementation and the JCA 'AES/GCM/NoPadding' wrap algorithm + * produce the exact same values. This should be the case when the transformation is identical, even though + * one uses Cipher.WRAP_MODE and the other uses a raw plaintext byte array. + */ + @Test + void testAesWrapProducesSameResultAsAesAeadEncryptionAlgorithm() { + + def alg = new GcmAesAeadAlgorithm(256) + + def iv = new byte[12]; + Randoms.secureRandom().nextBytes(iv); + + def kek = alg.generateKey(); + def cek = alg.generateKey(); + + JcaTemplate template = new JcaTemplate("AES/GCM/NoPadding", null) + byte[] jcaResult = template.execute(Cipher.class, new CheckedFunction() { + @Override + byte[] apply(Cipher cipher) throws Exception { + cipher.init(Cipher.WRAP_MODE, kek, new GCMParameterSpec(128, iv)) + return cipher.wrap(cek) + } + }) + + //separate tag from jca ciphertext: + int ciphertextLength = jcaResult.length - 16; //AES block size in bytes (128 bits) + byte[] ciphertext = new byte[ciphertextLength] + System.arraycopy(jcaResult, 0, ciphertext, 0, ciphertextLength) + + byte[] tag = new byte[16] + System.arraycopy(jcaResult, ciphertextLength, tag, 0, 16) + def resultA = new DefaultAeadResult(null, null, ciphertext, kek, null, tag, iv) + + def encRequest = new DefaultAeadRequest(null, null, cek.getEncoded(), kek, null, iv) + def encResult = EncryptionAlgorithms.A256GCM.encrypt(encRequest) + + assertArrayEquals resultA.digest, encResult.digest + assertArrayEquals resultA.initializationVector, encResult.initializationVector + assertArrayEquals resultA.payload, encResult.payload + } + + static void assertAlgorithm(int keyLength) { + + def alg = new AesGcmKeyAlgorithm(keyLength) + assertEquals 'A' + keyLength + 'GCMKW', alg.getId() + + def template = new JcaTemplate('AES', null) + + def header = new DefaultJweHeader() + def kek = template.generateSecretKey(keyLength) + def cek = template.generateSecretKey(keyLength) + def enc = new GcmAesAeadAlgorithm(keyLength) { + @Override + SecretKey generateKey() { + return cek; + } + } + + def ereq = new DefaultKeyRequest(null, null, kek, header, enc) + + def result = alg.getEncryptionKey(ereq) + + byte[] encryptedKeyBytes = result.getPayload() + assertFalse "encryptedKey must be populated", Arrays.length(encryptedKeyBytes) == 0 + + def dcek = alg.getDecryptionKey(new DefaultDecryptionKeyRequest(null, null, kek, header, enc, encryptedKeyBytes)) + + //Assert the decrypted key matches the original cek + assertEquals cek.algorithm, dcek.algorithm + assertArrayEquals cek.encoded, dcek.encoded + } + + @Test + void testResultSymmetry() { + assertAlgorithm(128) + assertAlgorithm(192) + assertAlgorithm(256) + } + + static void testDecryptionHeader(String headerName, Object value, String exmsg) { + int keyLength = 128 + def alg = new AesGcmKeyAlgorithm(keyLength) + def template = new JcaTemplate('AES', null) + def header = new DefaultJweHeader() + def kek = template.generateSecretKey(keyLength) + def cek = template.generateSecretKey(keyLength) + def enc = new GcmAesAeadAlgorithm(keyLength) { + @Override + SecretKey generateKey() { + return cek + } + } + def ereq = new DefaultKeyRequest(null, null, kek, header, enc) + def result = alg.getEncryptionKey(ereq) + + header.put(headerName, value) //null value will remove it + + byte[] encryptedKeyBytes = result.getPayload() + + try { + alg.getDecryptionKey(new DefaultDecryptionKeyRequest(null, null, kek, header, enc, encryptedKeyBytes)) + fail() + } catch (MalformedJwtException iae) { + assertEquals exmsg, iae.getMessage() + } + } + + String missing(String name) { + return "JWE header is missing required '${name}' value." as String + } + String type(String name) { + return "JWE header '${name}' value must be a String. Actual type: java.lang.Integer" as String + } + String base64Url(String name) { + return "JWE header '${name}' value is not a valid Base64URL String: Illegal base64url character: '#'" + } + String length(String name, int requiredBitLength) { + return "JWE header '${name}' decoded byte array must be ${Bytes.bitsMsg(requiredBitLength)} long. Actual length: ${Bytes.bitsMsg(16)}." + } + + @Test + void testMissingHeaders() { + testDecryptionHeader('iv', null, missing('iv')) + testDecryptionHeader('tag', null, missing('tag')) + } + + @Test + void testIncorrectTypeHeaders() { + testDecryptionHeader('iv', 14, type('iv')) + testDecryptionHeader('tag', 14, type('tag')) + } + + @Test + void testInvalidBase64UrlHeaders() { + testDecryptionHeader('iv', 'T#ZW@#', base64Url('iv')) + testDecryptionHeader('tag', 'T#ZW@#', base64Url('tag')) + } + + @Test + void testIncorrectLengths() { + def value = Encoders.BASE64URL.encode("hi".getBytes(StandardCharsets.US_ASCII)) + testDecryptionHeader('iv', value, length('iv', 96)) + testDecryptionHeader('tag', value, length('tag', 128)) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CertUtils.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CertUtils.groovy new file mode 100644 index 000000000..698683924 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CertUtils.groovy @@ -0,0 +1,64 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.lang.Classes +import io.jsonwebtoken.security.SignatureAlgorithm +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.openssl.PEMParser +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter + +import java.nio.charset.StandardCharsets +import java.security.PrivateKey +import java.security.PublicKey +import java.security.cert.X509Certificate + +/** + * For test cases that need to read certificate and/or PEM files. Encapsulates BouncyCastle API to + * this class so it doesn't need to propagate across other test classes. + * + * MAINTAINERS NOTE: + * + * If this logic is ever needed in the impl or api modules, do not keep the + * name of this class - it was quickly thrown together and it isn't appropriately named for exposure in a public + * module. Thought/design is necessary to see if/how cert/pem reading should be exposed in an easy-to-use and + * maintain API (e.g. probably a builder). + * + * The only purpose of this class and its methods are to: + * 1) be used in Test classes only, and + * 2) encapsulate the BouncyCastle API so it is not exposed to other Test classes. + */ +class CertUtils { + + private static JcaX509CertificateConverter X509_CERT_CONVERTER = new JcaX509CertificateConverter() + private static JcaPEMKeyConverter PEM_KEY_CONVERTER = new JcaPEMKeyConverter() + + private static PEMParser getParser(String filename) { + InputStream is = Classes.getResourceAsStream('io/jsonwebtoken/impl/security/' + filename) + return new PEMParser(new BufferedReader(new InputStreamReader(is, StandardCharsets.ISO_8859_1))) + } + + static X509Certificate readTestCertificate(SignatureAlgorithm alg) { + PEMParser parser = getParser(alg.getId() + '.crt.pem') + try { + X509CertificateHolder holder = parser.readObject() as X509CertificateHolder + return X509_CERT_CONVERTER.getCertificate(holder) + } finally { + parser.close() + } + } + + static PublicKey readTestPublicKey(SignatureAlgorithm alg) { + return readTestCertificate(alg).getPublicKey(); + } + + static PrivateKey readTestPrivateKey(SignatureAlgorithm alg) { + PEMParser parser = getParser(alg.getId() + '.key.pem') + try { + PrivateKeyInfo info = parser.readObject() as PrivateKeyInfo + return PEM_KEY_CONVERTER.getPrivateKey(info) + } finally { + parser.close() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherAlgorithmTest.groovy deleted file mode 100644 index e5fb6daab..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherAlgorithmTest.groovy +++ /dev/null @@ -1,57 +0,0 @@ -package io.jsonwebtoken.impl.security - -import org.junit.Test - -import javax.crypto.spec.SecretKeySpec -import java.security.Provider - -import static org.easymock.EasyMock.createMock -import static org.junit.Assert.* - -class CipherAlgorithmTest { - - @Test - void testNewCipherTemplateNullRequest() { - def alg = new TestCipherAlgorithm() - def template = alg.newCipherTemplate(null) - assertNull template.provider - assertEquals 'AES/CBC/PKCS5Padding', template.transformation - } - - @Test - void testNewCipherTemplate() { - - byte[] data = new byte[32] - Randoms.secureRandom().nextBytes(data) - byte[] keyBytes = new byte[32] - Randoms.secureRandom().nextBytes(keyBytes) - def key = new SecretKeySpec(keyBytes, 'AES') - - def alg = new TestCipherAlgorithm() - def template = alg.newCipherTemplate(new DefaultCryptoRequest(data, key, null, null)) - assertNull template.provider - assertEquals 'AES/CBC/PKCS5Padding', template.transformation - } - - @Test - void testNewCipherTemplateWithProvider() { - - Provider provider = createMock(Provider) - byte[] data = new byte[32] - Randoms.secureRandom().nextBytes(data) - byte[] keyBytes = new byte[32] - Randoms.secureRandom().nextBytes(keyBytes) - def key = new SecretKeySpec(keyBytes, 'AES') - - def alg = new TestCipherAlgorithm() - def template = alg.newCipherTemplate(new DefaultCryptoRequest(data, key, provider, null)) - assertSame provider, template.provider - assertEquals 'AES/CBC/PKCS5Padding', template.transformation - } - - static class TestCipherAlgorithm extends CipherAlgorithm { - def TestCipherAlgorithm() { - super('AES', 'AES/CBC/PKCS5Padding') - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherTemplateTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherTemplateTest.groovy deleted file mode 100644 index 93ea88d4b..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CipherTemplateTest.groovy +++ /dev/null @@ -1,93 +0,0 @@ -package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.security.CryptoException -import org.junit.Test - -import javax.crypto.Cipher -import javax.crypto.NoSuchPaddingException -import java.security.NoSuchAlgorithmException -import java.security.Provider -import java.security.Security - -import static org.junit.Assert.* - -class CipherTemplateTest { - - @Test - void testNewCipherWithExplicitProvider() { - Provider provider = Security.getProvider('SunJCE') - def template = new CipherTemplate('AES/CBC/PKCS5Padding', provider) - template.execute(new CipherCallback() { - @Override - byte[] doWithCipher(Cipher cipher) throws Exception { - assertNotNull cipher - assertSame provider, cipher.provider - } - }) - } - - @Test - void testNewCipherFailedWithDefaultProvider() { - def ex = new IllegalStateException('testing') - def template = new CipherTemplate('AES/CBC/PKCS5Padding', null) { - @Override - Cipher getCipherInstance(String transformation, Provider provider) throws NoSuchPaddingException, NoSuchAlgorithmException { - throw ex - } - } - - try { - template.execute(new CipherCallback() { - @Override - byte[] doWithCipher(Cipher cipher) throws Exception { - return null - } - }) - } catch (CryptoException expected) { - assertEquals 'Unable to obtain cipher from default JCA Provider for transformation \'AES/CBC/PKCS5Padding\': testing', expected.getMessage() - assertSame ex, expected.getCause() - } - } - - @Test - void testNewCipherFailedWithExplicitProvider() { - def ex = new IllegalStateException('testing') - Provider provider = Security.getProvider('SunJCE') - def template = new CipherTemplate('AES/CBC/PKCS5Padding', provider) { - @Override - Cipher getCipherInstance(String transformation, Provider p) throws NoSuchPaddingException, NoSuchAlgorithmException { - throw ex - } - } - - try { - template.execute(new CipherCallback() { - @Override - byte[] doWithCipher(Cipher cipher) throws Exception { - return null - } - }) - } catch (CryptoException expected) { - assertTrue expected.getMessage().startsWith('Unable to obtain cipher from specified Provider {') - assertTrue expected.getMessage().endsWith('} for transformation \'AES/CBC/PKCS5Padding\': testing') - assertSame ex, expected.getCause() - } - } - - @Test - void testCallbackThrowsException() { - def ex = new Exception("testing") - def template = new CipherTemplate('AES/CBC/PKCS5Padding', null) - try { - template.execute(new CipherCallback() { - @Override - byte[] doWithCipher(Cipher cipher) throws Exception { - throw ex - } - }) - } catch (CryptoException e) { - assertEquals 'Cipher callback execution failed: testing', e.getMessage() - assertSame ex, e.getCause() - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConstantKeyLocatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConstantKeyLocatorTest.groovy new file mode 100644 index 000000000..647634b1e --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConstantKeyLocatorTest.groovy @@ -0,0 +1,52 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.UnsupportedJwtException +import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.impl.DefaultJwsHeader +import org.junit.Test + +import javax.crypto.spec.SecretKeySpec + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class ConstantKeyLocatorTest { + + @Test + void testSignatureVerificationKey() { + def key = new SecretKeySpec(new byte[1], 'AES') //dummy key for testing + assertSame key, new ConstantKeyLocator(key, null).locate(new DefaultJwsHeader()) + } + + @Test + void testSignatureVerificationKeyMissing() { + def locator = new ConstantKeyLocator(null, null) + try { + locator.locate(new DefaultJwsHeader()) + } catch (UnsupportedJwtException uje) { + String msg = 'Signed JWTs are not supported: the JwtParser has not been configured with a signature ' + + 'verification key or a KeyResolver. Consider configuring the JwtParserBuilder with one of these ' + + 'to ensure it can use the necessary key to verify JWS signatures.' + assertEquals msg, uje.getMessage() + } + } + + @Test + void testDecryptionKey() { + def key = new SecretKeySpec(new byte[1], 'AES') //dummy key for testing + assertSame key, new ConstantKeyLocator(null, key).locate(new DefaultJweHeader()) + } + + @Test + void testDecryptionKeyMissing() { + def locator = new ConstantKeyLocator(null, null) + try { + locator.locate(new DefaultJweHeader()) + } catch (UnsupportedJwtException uje) { + String msg = 'Encrypted JWTs are not supported: the JwtParser has not been configured with a decryption ' + + 'key or a KeyResolver. Consider configuring the JwtParserBuilder with one of these ' + + 'to ensure it can use the necessary key to decrypt JWEs.' + assertEquals msg, uje.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy new file mode 100644 index 000000000..8f10e5779 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy @@ -0,0 +1,51 @@ +package io.jsonwebtoken.impl.security + +import org.junit.Test +import static org.junit.Assert.* + +class CryptoAlgorithmTest { + + @Test + void testEqualsSameInstance() { + def alg = new TestCryptoAlgorithm('test', 'test') + assertEquals alg, alg + } + + @Test + void testEqualsSameNameAndJcaName() { + def alg1 = new TestCryptoAlgorithm('test', 'test') + def alg2 = new TestCryptoAlgorithm('test', 'test') + assertEquals alg1, alg2 + } + + @Test + void testEqualsSameNameButDifferentJcaName() { + def alg1 = new TestCryptoAlgorithm('test', 'test1') + def alg2 = new TestCryptoAlgorithm('test', 'test2') + assertNotEquals alg1, alg2 + } + + @Test + void testEqualsOtherType() { + assertNotEquals new TestCryptoAlgorithm('test', 'test'), new Object() + } + + @Test + void testToString() { + assertEquals 'test', new TestCryptoAlgorithm('test', 'whatever').toString() + } + + @Test + void testHashCode() { + int hash = 7 + hash = 31 * hash + 'name'.hashCode() + hash = 31 * hash + 'jcaName'.hashCode() + assertEquals hash, new TestCryptoAlgorithm('name', 'jcaName').hashCode() + } + + class TestCryptoAlgorithm extends CryptoAlgorithm { + TestCryptoAlgorithm(String id, String jcaName) { + super(id, jcaName) + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResultTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResultTest.groovy deleted file mode 100644 index efa307cd2..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultAeadIvEncryptionResultTest.groovy +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2016 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.security - -import org.junit.Test - -import static org.junit.Assert.assertTrue - -/** - * @since JJWT_RELEASE_VERSION - */ -class DefaultAeadIvEncryptionResultTest { - - private static byte[] generateData() { - byte[] data = new byte[32]; - new Random().nextBytes(data) //does not need to be secure for this test - return data; - } - - @Test - void testCompactWithIv() { - - byte[] iv = generateData() - byte[] ciphertext = generateData() - byte[] tag = generateData() - - byte[] combined = new byte[iv.length + ciphertext.length + tag.length]; - System.arraycopy(iv, 0, combined, 0, iv.length) - System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length); - System.arraycopy(tag, 0, combined, iv.length + ciphertext.length, tag.length); - - def res = new DefaultAeadIvEncryptionResult(ciphertext, iv, tag) - byte[] compact = res.compact() - - assertTrue(Arrays.equals(combined, compact)) - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EllipticCurveSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy similarity index 78% rename from impl/src/test/groovy/io/jsonwebtoken/impl/security/EllipticCurveSignatureAlgorithmTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy index 2d0072347..8cddef096 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EllipticCurveSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy @@ -3,6 +3,7 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.JwtException import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.SecurityException import io.jsonwebtoken.security.SignatureAlgorithms import io.jsonwebtoken.security.WeakKeyException import org.junit.Test @@ -16,26 +17,26 @@ import java.security.spec.X509EncodedKeySpec import static org.easymock.EasyMock.createMock import static org.junit.Assert.* -class EllipticCurveSignatureAlgorithmTest { +class DefaultEllipticCurveSignatureAlgorithmTest { @Test void testConstructorWithWeakKeyLength() { try { - new EllipticCurveSignatureAlgorithm('ES256', 'SHA256withECDSA', 'secp256r1', 128, 256) + new DefaultEllipticCurveSignatureAlgorithm('ES256', 'SHA256withECDSA', 'secp256r1', 128, 256) } catch (IllegalArgumentException iae) { assertEquals 'minKeyLength bits must be greater than the JWA mandatory minimum key length of 256', iae.getMessage() } } - @Test(expected=IllegalStateException) + @Test(expected = SecurityException) void testGenerateKeyPairInvalidCurveName() { - def alg = new EllipticCurveSignatureAlgorithm('ES256', 'SHA256withECDSA', 'notreal', 256, 256) + def alg = new DefaultEllipticCurveSignatureAlgorithm('ES256', 'SHA256withECDSA', 'notreal', 256, 256) alg.generateKeyPair() } @Test void testValidateKeyEcKey() { - def request = new DefaultCryptoRequest(new byte[1], new SecretKeySpec(new byte[1], 'foo'), null, null) + def request = new DefaultSignatureRequest(null, null, new byte[1], new SecretKeySpec(new byte[1], 'foo')) try { SignatureAlgorithms.ES256.sign(request) } catch (InvalidKeyException e) { @@ -46,7 +47,7 @@ class EllipticCurveSignatureAlgorithmTest { @Test void testValidateSigningKeyNotPrivate() { ECPublicKey key = createMock(ECPublicKey) - def request = new DefaultCryptoRequest(new byte[1], key, null, null) + def request = new DefaultSignatureRequest(null, null, new byte[1], key) try { SignatureAlgorithms.ES256.sign(request) } catch (InvalidKeyException e) { @@ -60,8 +61,8 @@ class EllipticCurveSignatureAlgorithmTest { gen.initialize(192) //too week for any JWA EC algorithm def pair = gen.generateKeyPair() - def request = new DefaultCryptoRequest(new byte[1], pair.getPrivate(), null, null) - SignatureAlgorithms.values().findAll({it.getName().startsWith('ES')}).each { + def request = new DefaultSignatureRequest(null, null, new byte[1], pair.getPrivate()) + SignatureAlgorithms.values().findAll({ it.getId().startsWith('ES') }).each { try { it.sign(request) } catch (WeakKeyException expected) { @@ -72,11 +73,11 @@ class EllipticCurveSignatureAlgorithmTest { @Test void testVerifyWithPrivateKey() { byte[] data = 'foo'.getBytes(StandardCharsets.UTF_8) - SignatureAlgorithms.values().findAll({it instanceof EllipticCurveSignatureAlgorithm}).each { + SignatureAlgorithms.values().findAll({ it instanceof DefaultEllipticCurveSignatureAlgorithm }).each { KeyPair pair = it.generateKeyPair() - def signRequest = new DefaultCryptoRequest(data, pair.getPrivate(), null, null) + def signRequest = new DefaultSignatureRequest(null, null, data, pair.getPrivate()) byte[] signature = it.sign(signRequest) - def verifyRequest = new DefaultVerifySignatureRequest(data, pair.getPrivate(), null, null, signature) + def verifyRequest = new DefaultVerifySignatureRequest(null, null, data, pair.getPrivate(), signature) try { it.verify(verifyRequest) } catch (InvalidKeyException e) { @@ -89,7 +90,7 @@ class EllipticCurveSignatureAlgorithmTest { void invalidDERSignatureToJoseFormatTest() { def verify = { signature -> try { - EllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) + DefaultEllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) fail() } catch (JwtException e) { assertEquals e.message, 'Invalid ECDSA signature format' @@ -113,7 +114,7 @@ class EllipticCurveSignatureAlgorithmTest { void edgeCaseSignatureToConcatInvalidSignatureTest() { try { def signature = Decoders.BASE64.decode("MIGBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") - EllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) + DefaultEllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) fail() } catch (JwtException e) { assertEquals e.message, 'Invalid ECDSA signature format' @@ -124,7 +125,7 @@ class EllipticCurveSignatureAlgorithmTest { void edgeCaseSignatureToConcatInvalidSignatureBranchTest() { try { def signature = Decoders.BASE64.decode("MIGBAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") - EllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) + DefaultEllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) fail() } catch (JwtException e) { assertEquals e.message, 'Invalid ECDSA signature format' @@ -135,7 +136,7 @@ class EllipticCurveSignatureAlgorithmTest { void edgeCaseSignatureToConcatInvalidSignatureBranch2Test() { try { def signature = Decoders.BASE64.decode("MIGBAj4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") - EllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) + DefaultEllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) fail() } catch (JwtException e) { assertEquals e.message, 'Invalid ECDSA signature format' @@ -146,7 +147,7 @@ class EllipticCurveSignatureAlgorithmTest { void edgeCaseSignatureToConcatLengthTest() { try { def signature = Decoders.BASE64.decode("MIEAAGg3OVb/ZeX12cYrhK3c07TsMKo7Kc6SiqW++4CAZWCX72DkZPGTdCv2duqlupsnZL53hiG3rfdOLj8drndCU+KHGrn5EotCATdMSLCXJSMMJoHMM/ZPG+QOHHPlOWnAvpC1v4lJb32WxMFNz1VAIWrl9Aa6RPG1GcjCTScKjvEE") - EllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) + DefaultEllipticCurveSignatureAlgorithm.transcodeSignatureToConcat(signature, 132) fail() } catch (JwtException expected) { @@ -158,7 +159,7 @@ class EllipticCurveSignatureAlgorithmTest { try { def signature = new byte[257] Randoms.secureRandom().nextBytes(signature) - EllipticCurveSignatureAlgorithm.transcodeSignatureToDER(signature) + DefaultEllipticCurveSignatureAlgorithm.transcodeSignatureToDER(signature) fail() } catch (JwtException e) { assertEquals e.message, 'Invalid ECDSA signature format' @@ -168,7 +169,7 @@ class EllipticCurveSignatureAlgorithmTest { @Test void edgeCaseSignatureLengthTest() { def signature = new byte[1] - EllipticCurveSignatureAlgorithm.transcodeSignatureToDER(signature) + DefaultEllipticCurveSignatureAlgorithm.transcodeSignatureToDER(signature) } @Test @@ -176,7 +177,7 @@ class EllipticCurveSignatureAlgorithmTest { def signature = new byte[32] Randoms.secureRandom().nextBytes(signature) signature[0] = 0 as byte - EllipticCurveSignatureAlgorithm.transcodeSignatureToDER(signature) //no exception + DefaultEllipticCurveSignatureAlgorithm.transcodeSignatureToDER(signature) //no exception } @Test @@ -190,7 +191,7 @@ class EllipticCurveSignatureAlgorithmTest { def withoutSignature = token.substring(0, signatureStart) def data = withoutSignature.getBytes("US-ASCII") def signature = Decoders.BASE64URL.decode(token.substring(signatureStart + 1)) - assertTrue"Signature do not match that of other implementations", alg.verify(new DefaultVerifySignatureRequest(data, pub, null, null, signature)) + assertTrue "Signature do not match that of other implementations", alg.verify(new DefaultVerifySignatureRequest(null, null, data, pub, signature)) } //Test verification for token created using https://github.com/auth0/node-jsonwebtoken/tree/v7.0.1 verifier("eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30.Aab4x7HNRzetjgZ88AMGdYV2Ml7kzFbl8Ql2zXvBores7iRqm2nK6810ANpVo5okhHa82MQf2Q_Zn4tFyLDR9z4GAcKFdcAtopxq1h8X58qBWgNOc0Bn40SsgUc8wOX4rFohUCzEtnUREePsvc9EfXjjAH78WD2nq4tn-N94vf14SncQ") @@ -208,17 +209,17 @@ class EllipticCurveSignatureAlgorithmTest { signature.initSign(keypair.private) signature.update(data) def signed = signature.sign() - assertTrue alg.verify(new DefaultVerifySignatureRequest(data, keypair.public, null, null, signed)) + assertTrue alg.verify(new DefaultVerifySignatureRequest(null, null, data, keypair.public, signed)) } @Test void verifySwarmTest() { - SignatureAlgorithms.values().findAll({it.getName().startsWith('ES')}).each {alg -> + SignatureAlgorithms.values().findAll({ it.getId().startsWith('ES') }).each { alg -> def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def keypair = alg.generateKeyPair() def data = withoutSignature.getBytes("US-ASCII") - def signature = alg.sign(new DefaultCryptoRequest(data, keypair.private, null, null)) - assertTrue alg.verify(new DefaultVerifySignatureRequest(data, keypair.public, null, null, signature)) + def signature = alg.sign(new DefaultSignatureRequest(null, null, data, keypair.private)) + assertTrue alg.verify(new DefaultVerifySignatureRequest(null, null, data, keypair.public, signature)) } } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocatorTest.groovy deleted file mode 100644 index d128efb2f..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEncryptionAlgorithmLocatorTest.groovy +++ /dev/null @@ -1,98 +0,0 @@ -package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.JweHeader -import io.jsonwebtoken.Jwts -import io.jsonwebtoken.MalformedJwtException -import io.jsonwebtoken.UnsupportedJwtException -import io.jsonwebtoken.security.EncryptionAlgorithms -import org.junit.Before -import org.junit.Test -import static org.junit.Assert.* - -/** - * @since JJWT_RELEASE_VERSION - */ -class DefaultEncryptionAlgorithmLocatorTest { - - private DefaultEncryptionAlgorithmLocator locator - - @Before - void setUp() { - locator = new DefaultEncryptionAlgorithmLocator() - } - - private static JweHeader header(String enc) { - return Jwts.jweHeader().setEncryptionAlgorithm(enc) - } - - @Test - void testA128CBCHS256() { - assertSame EncryptionAlgorithms.A128CBC_HS256, locator.getEncryptionAlgorithm(header('A128CBC-HS256')) - } - - @Test - void testA192CBCHS384() { - assertSame EncryptionAlgorithms.A192CBC_HS384, locator.getEncryptionAlgorithm(header('A192CBC-HS384')) - } - - @Test - void testA256CBCHS512() { - assertSame EncryptionAlgorithms.A256CBC_HS512, locator.getEncryptionAlgorithm(header('A256CBC-HS512')) - } - - @Test - void testA128GCM() { - assertSame EncryptionAlgorithms.A128GCM, locator.getEncryptionAlgorithm(header('A128GCM')) - } - - @Test - void testA192GCM() { - assertSame EncryptionAlgorithms.A192GCM, locator.getEncryptionAlgorithm(header('A192GCM')) - } - - @Test - void testA256GCM() { - assertSame EncryptionAlgorithms.A256GCM, locator.getEncryptionAlgorithm(header('A256GCM')) - } - - @Test - void testMissingEncAlg() { - try { - locator.getEncryptionAlgorithm(Jwts.jweHeader()) - fail() - } catch (MalformedJwtException expected) { - } - } - - @Test - void testNullEncAlg() { - try { - locator.getEncryptionAlgorithm(header(null)) - fail() - } catch (MalformedJwtException expected) { - } - } - - @Test - void testEmptyEncAlg() { - try { - locator.getEncryptionAlgorithm(header(' ')) - fail() - } catch (MalformedJwtException expected) { - } - } - - @Test - void testUnknownEncAlg() { - try { - locator.getEncryptionAlgorithm(header('foo')) - fail() - } catch (UnsupportedJwtException e) { - assertEquals "JWE 'enc' header parameter value of 'foo' does not match a JWE standard algorithm " + - "identifier. If 'foo' represents a custom algorithm, the JwtParser must be configured " + - "with a custom EncryptionAlgorithmLocator instance that knows how to return a compatible " + - "EncryptionAlgorithm instance. Otherwise, this JWE is invalid and may not be used safely.", e.message - } - } - -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultIvEncryptionResultTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultIvEncryptionResultTest.groovy deleted file mode 100644 index 1da967a29..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultIvEncryptionResultTest.groovy +++ /dev/null @@ -1,38 +0,0 @@ -package io.jsonwebtoken.impl.security - -import org.junit.Test - -import static org.junit.Assert.assertSame -import static org.junit.Assert.assertTrue - -/** - * @since JJWT_RELEASE_VERSION - */ -class DefaultIvEncryptionResultTest { - - private byte[] generateData() { - byte[] data = new byte[32]; - new Random().nextBytes(data) //does not need to be secure for this test - return data; - } - - @Test(expected=IllegalArgumentException) - void testCompactWithoutIv() { - def ciphertext = generateData() - new DefaultIvEncryptionResult(ciphertext, null) - } - - @Test - void testCompactWithIv() { - def ciphertext = generateData() - def iv = generateData() - - byte[] result = new DefaultIvEncryptionResult(ciphertext, iv).compact() - - byte[] combined = new byte[iv.length + ciphertext.length]; - System.arraycopy(iv, 0, combined, 0, iv.length); - System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length); - - assertTrue Arrays.equals(combined, result) - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJweFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJweFactoryTest.groovy deleted file mode 100644 index 292513dc5..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJweFactoryTest.groovy +++ /dev/null @@ -1,14 +0,0 @@ -package io.jsonwebtoken.impl.security - -import org.junit.Test - -/** - * @since JJWT_RELEASE_VERSION - */ -class DefaultJweFactoryTest { - - @Test - void testDefaultCtor() { - new DefaultJweFactory() - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkConverterTest.groovy deleted file mode 100644 index d1d1d53a4..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkConverterTest.groovy +++ /dev/null @@ -1,103 +0,0 @@ -package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.security.InvalidKeyException -import io.jsonwebtoken.security.SignatureAlgorithms -import io.jsonwebtoken.security.UnsupportedKeyException -import org.junit.Ignore - -import java.security.KeyPair -import java.security.interfaces.ECPrivateKey -import java.security.interfaces.ECPublicKey - -import static org.junit.Assert.* - -import io.jsonwebtoken.io.Encoders -import org.junit.Test - -class DefaultJwkConverterTest { - - @Test - void testNullJwk() { - try { - new DefaultJwkConverter().toKey(null) - fail() - } catch (InvalidKeyException expected) { - assertEquals 'JWK map cannot be null or empty.', expected.message - } - } - - @Test - void testEmptyJwk() { - try { - new DefaultJwkConverter().toKey([:]) - fail() - } catch (InvalidKeyException expected) { - assertEquals 'JWK map cannot be null or empty.', expected.message - } - } - - @Test - void testUnknownKeyType() { - - def jwk = [ - 'kty': 'foo' - ] - - DefaultJwkConverter converter = new DefaultJwkConverter() - try { - converter.toKey(jwk) - fail() - } catch (UnsupportedKeyException e) { - assertEquals 'Unrecognized JWK kty (key type) value: foo', e.getMessage() - } - } - - @Test - void testEcKeyPairToKey() { - - def jwk = [ - 'kty': 'EC', - 'crv': 'P-256', - "x":"gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", - "y":"SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps", - "d":"0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo" - ] - - DefaultJwkConverter converter = new DefaultJwkConverter() - - def key = converter.toKey(jwk) - assertTrue key instanceof ECPrivateKey - key = key as ECPrivateKey - String d = EcJwkConverter.encodeCoordinate(key.params.curve.field.fieldSize, key.s) - assertEquals jwk.d, d - - //remove the 'd' mapping to represent only a public key: - jwk.remove('d') - - key = converter.toKey(jwk) - assertTrue key instanceof ECPublicKey - key = key as ECPublicKey - String x = EcJwkConverter.encodeCoordinate(key.params.curve.field.fieldSize, key.w.affineX) - String y = EcJwkConverter.encodeCoordinate(key.params.curve.field.fieldSize, key.w.affineY) - assertEquals jwk.x, x - assertEquals jwk.y, y - } - - @Test - @Ignore //TODO re-enable - void testEcKeyPairToJwk() { - - KeyPair pair = SignatureAlgorithms.ES256.generateKeyPair() - ECPublicKey pubKey = (ECPublicKey) pair.getPublic() - - DefaultJwkConverter converter = new DefaultJwkConverter() - - Map jwk = converter.toJwk(pubKey) - - assertNotNull jwk - assertEquals "EC", jwk.kty - assertEquals Encoders.BASE64URL.encode(pubKey.w.affineX.toByteArray()), jwk.x - assertEquals Encoders.BASE64URL.encode(pubKey.w.affineY.toByteArray()), jwk.y - assertNull jwk.d //public keys should not populate the private key 'd' parameter - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkTest.groovy similarity index 85% rename from impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkTest.groovy index a3b772c5a..53f3241fb 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkTest.groovy @@ -1,38 +1,26 @@ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.security.Jwk + import io.jsonwebtoken.security.MalformedKeyException +import io.jsonwebtoken.security.SignatureAlgorithms import org.junit.Test -import static org.junit.Assert.* -class AbstractJwkTest { +import javax.crypto.SecretKey +import java.security.Key +import java.security.cert.X509Certificate - class TestJwk extends AbstractJwk { - TestJwk() { - super("test") - } - } +import static org.junit.Assert.* - class NullTypeJwk extends AbstractJwk { - NullTypeJwk() { - super(null) - } - } +class DefaultJwkTest { - class EmptyTypeJwk extends AbstractJwk { - EmptyTypeJwk() { - super(" ") - } - } + /* - @Test(expected=IllegalArgumentException) - void testNullType() { - new NullTypeJwk() - } + private static final SecretKey TEST_KEY = SignatureAlgorithms.HS512.generateKey(); - @Test(expected=IllegalArgumentException) - void testEmptyType() { - new EmptyTypeJwk() + class TestJwk extends AbstractJwk { + TestJwk(String type = "test", String use = null, Set operations = null, String algorithm = null, String id = null, URI x509url = null, List certChain = null, byte[] x509Sha1Thumbprint = null, byte[] x509Sha256Thumbprint = null, Key key = TEST_KEY) { + super(type, use, operations, algorithm, id, x509url, certChain, x509Sha1Thumbprint, x509Sha256Thumbprint, TEST_KEY, null) + } } @Test @@ -45,18 +33,15 @@ class AbstractJwkTest { def jwk = new TestJwk() assertEquals 'use', AbstractJwk.USE - - jwk.setUse(null) assertNull jwk.get(AbstractJwk.USE) assertNull jwk.getUse() - jwk.setUse(' ') //empty should remove + jwk = new TestJwk(use: ' ') //empty should remove assertNull jwk.get(AbstractJwk.USE) assertNull jwk.getUse() String val = UUID.randomUUID().toString() - - jwk.setUse(val) + jwk = new TestJwk(use: val) assertEquals val, jwk.get(AbstractJwk.USE) assertEquals val, jwk.getUse() } @@ -216,7 +201,7 @@ class AbstractJwkTest { void testGetX509CertChainWithSet() { def jwk = new TestJwk() jwk.put('x5c', new LinkedHashSet<>(['a', null, 'b'])) - def chain = jwk.getX509CertficateChain() + def chain = jwk.getX509CertificateChain() assertTrue chain instanceof List assertEquals 3, chain.size() assertEquals 'a', chain[0] @@ -228,7 +213,7 @@ class AbstractJwkTest { void testGetX509CertChainWithArray() { def jwk = new TestJwk() jwk.put('x5c', ['a', null, 'b'] as String[]) - def chain = jwk.getX509CertficateChain() + def chain = jwk.getX509CertificateChain() assertTrue chain instanceof List assertEquals 3, chain.size() assertEquals 'a', chain[0] @@ -245,16 +230,18 @@ class AbstractJwkTest { jwk.setX509CertificateChain(null) assertNull jwk.get(AbstractJwk.X509_CERT_CHAIN) - assertNull jwk.getX509CertficateChain() + assertNull jwk.getX509CertificateChain() jwk.setX509CertificateChain([]) assertNull jwk.get(AbstractJwk.X509_CERT_CHAIN) - assertNull jwk.getX509CertficateChain() + assertNull jwk.getX509CertificateChain() String val = UUID.randomUUID().toString() def chain = [val] jwk.setX509CertificateChain(chain) assertEquals chain, jwk.get(AbstractJwk.X509_CERT_CHAIN) - assertEquals chain, jwk.getX509CertficateChain() + assertEquals chain, jwk.getX509CertificateChain() } + + */ } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultCryptoMessageTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultPayloadSupplierTest.groovy similarity index 64% rename from impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultCryptoMessageTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultPayloadSupplierTest.groovy index e3ac5961f..7a2199241 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultCryptoMessageTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultPayloadSupplierTest.groovy @@ -2,15 +2,15 @@ package io.jsonwebtoken.impl.security import org.junit.Test -class DefaultCryptoMessageTest { +class DefaultPayloadSupplierTest { @Test(expected = IllegalArgumentException) void testNullData() { - new DefaultCryptoMessage<>(null) + new DefaultPayloadSupplier<>(null) } @Test(expected = IllegalArgumentException) void testEmptyByteArrayData() { - new DefaultCryptoMessage<>(new byte[0]) + new DefaultPayloadSupplier<>(new byte[0]) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy similarity index 60% rename from impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaSignatureAlgorithmTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy index b4d69cacd..dbad8e005 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy @@ -6,22 +6,19 @@ import io.jsonwebtoken.security.WeakKeyException import org.junit.Test import javax.crypto.spec.SecretKeySpec -import java.security.InvalidParameterException -import java.security.Key import java.security.KeyPair import java.security.KeyPairGenerator -import java.security.NoSuchAlgorithmException import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import static org.easymock.EasyMock.createMock import static org.junit.Assert.* -class RsaSignatureAlgorithmTest { +class DefaultRsaSignatureAlgorithmTest { @Test void testGenerateKeyPair() { - SignatureAlgorithms.values().findAll({it.name.startsWith("RS") || it.name.startsWith("PS")}).each { + SignatureAlgorithms.values().findAll({it.id.startsWith("RS") || it.id.startsWith("PS")}).each { KeyPair pair = it.generateKeyPair() assertNotNull pair.public assertTrue pair.public instanceof RSAPublicKey @@ -31,26 +28,14 @@ class RsaSignatureAlgorithmTest { } } - @Test(expected = IllegalStateException) - void testGenerateKeyGeneratorException() { - def src = SignatureAlgorithms.RS256 - def alg = new RsaSignatureAlgorithm(src.name, src.jcaName, src.preferredKeyLength) { - @Override - protected KeyPairGenerator getKeyPairGenerator() throws NoSuchAlgorithmException, InvalidParameterException { - throw new NoSuchAlgorithmException("testing") - } - } - alg.generateKeyPair() - } - @Test(expected = IllegalArgumentException) void testWeakPreferredKeyLength() { - new RsaSignatureAlgorithm('RS256', 'SHA256withRSA', 1024) //must be >= 2048 + new DefaultRsaSignatureAlgorithm('RS256', 'SHA256withRSA', 1024) //must be >= 2048 } @Test void testValidateKeyRsaKey() { - def request = new DefaultCryptoRequest(new byte[1], new SecretKeySpec(new byte[1], 'foo'), null, null) + def request = new DefaultSignatureRequest(null, null, new byte[1], new SecretKeySpec(new byte[1], 'foo')) try { SignatureAlgorithms.RS256.sign(request) } catch (InvalidKeyException e) { @@ -61,7 +46,7 @@ class RsaSignatureAlgorithmTest { @Test void testValidateSigningKeyNotPrivate() { RSAPublicKey key = createMock(RSAPublicKey) - def request = new DefaultCryptoRequest(new byte[1], key, null, null) + def request = new DefaultSignatureRequest(null, null, new byte[1], key) try { SignatureAlgorithms.RS256.sign(request) } catch (InvalidKeyException e) { @@ -75,8 +60,8 @@ class RsaSignatureAlgorithmTest { gen.initialize(1024) //too week for any JWA RSA algorithm def pair = gen.generateKeyPair() - def request = new DefaultCryptoRequest(new byte[1], pair.getPrivate(), null, null) - SignatureAlgorithms.values().findAll({it.name.startsWith('RS') || it.name.startsWith('PS')}).each { + def request = new DefaultSignatureRequest(null, null, new byte[1], pair.getPrivate()) + SignatureAlgorithms.values().findAll({it.id.startsWith('RS') || it.id.startsWith('PS')}).each { try { it.sign(request) } catch (WeakKeyException expected) { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSymmetricJwkTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSymmetricJwkTest.groovy deleted file mode 100644 index 8719acbf6..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSymmetricJwkTest.groovy +++ /dev/null @@ -1,43 +0,0 @@ -package io.jsonwebtoken.impl.security - -import org.junit.Test -import static org.junit.Assert.* - -class DefaultSymmetricJwkTest { - - @Test - void testType() { - assertEquals 'oct', DefaultSymmetricJwk.TYPE_VALUE - assertEquals DefaultSymmetricJwk.TYPE_VALUE, new DefaultSymmetricJwk().getType() - } - - @Test - void testSetNullK() { - try { - new DefaultSymmetricJwk().setK(null) - fail() - } catch (IllegalArgumentException e) { - assertEquals "SymmetricJwk 'k' property cannot be null or empty.", e.getMessage() - } - } - - @Test - void testSetEmptyK() { - try { - new DefaultSymmetricJwk().setK(' ') - fail() - } catch (IllegalArgumentException e) { - assertEquals "SymmetricJwk 'k' property cannot be null or empty.", e.getMessage() - } - } - - @Test - void testK() { - def jwk = new DefaultSymmetricJwk() - assertEquals 'k', DefaultSymmetricJwk.K - String val = UUID.randomUUID().toString() - jwk.setK(val) - assertEquals val, jwk.get(DefaultSymmetricJwk.K) - assertEquals val, jwk.getK() - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy new file mode 100644 index 000000000..e440b78db --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy @@ -0,0 +1,74 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.lang.Arrays +import io.jsonwebtoken.security.DecryptionKeyRequest +import io.jsonwebtoken.security.EncryptionAlgorithms +import org.junit.Test + +import javax.crypto.spec.SecretKeySpec +import java.security.Key + +import static org.easymock.EasyMock.* +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class DirectKeyAlgorithmTest { + + @Test + void testId() { + assertEquals "dir", new DirectKeyAlgorithm().getId() + } + + @Test + void testGetEncryptionKey() { + def alg = new DirectKeyAlgorithm() + def key = new SecretKeySpec(new byte[1], "AES") + def request = new DefaultKeyRequest(null, null, key, new DefaultJweHeader(), EncryptionAlgorithms.A128GCM) + def result = alg.getEncryptionKey(request) + assertSame key, result.getKey() + assertEquals 0, Arrays.length(result.getPayload()) //must not have an encrypted key + } + + @Test(expected = IllegalArgumentException) + void testGetEncryptionKeyWithNullRequest() { + new DirectKeyAlgorithm().getEncryptionKey(null) + } + + @Test(expected = IllegalArgumentException) + void testGetEncryptionKeyWithNullRequestKey() { + def key = new SecretKeySpec(new byte[1], "AES") + def request = new DefaultKeyRequest(null, null, key, new DefaultJweHeader(), EncryptionAlgorithms.A128GCM) { + @Override + Key getKey() { + return null + } + } + new DirectKeyAlgorithm().getEncryptionKey(request) + } + + @Test + void testGetDecryptionKey() { + def alg = new DirectKeyAlgorithm() + DecryptionKeyRequest req = createMock(DecryptionKeyRequest) + def key = EncryptionAlgorithms.A128GCM.generateKey() + expect(req.getKey()).andReturn(key) + replay(req) + def result = alg.getDecryptionKey(req) + verify(req) + assertSame key, result + } + + @Test(expected = IllegalArgumentException) + void testGetDecryptionKeyWithNullRequest() { + new DirectKeyAlgorithm().getDecryptionKey(null) + } + + @Test(expected = IllegalArgumentException) + void testGetDecryptionKeyWithNullRequestKey() { + DecryptionKeyRequest req = createMock(DecryptionKeyRequest) + expect(req.getKey()).andReturn(null) + replay(req) + new DirectKeyAlgorithm().getDecryptionKey(req) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolverTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolverTest.groovy deleted file mode 100644 index 6af3c9012..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DisabledDecryptionKeyResolverTest.groovy +++ /dev/null @@ -1,16 +0,0 @@ -package io.jsonwebtoken.impl.security - -import org.junit.Test - -import static org.junit.Assert.assertNull - -/** - * @since JJWT_RELEASE_VERSION - */ -class DisabledDecryptionKeyResolverTest { - - @Test - void test() { - assertNull DisabledDecryptionKeyResolver.INSTANCE.resolveDecryptionKey(null) - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy new file mode 100644 index 000000000..6aef4e7b8 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy @@ -0,0 +1,95 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.EcPrivateJwk +import io.jsonwebtoken.security.EcPublicJwk +import io.jsonwebtoken.security.SignatureAlgorithms +import io.jsonwebtoken.security.UnsupportedKeyException +import org.junit.Ignore +import org.junit.Test + +import java.security.Key +import java.security.KeyPair +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey + +import static org.junit.Assert.* + +class DispatchingJwkFactoryTest { + + @Test(expected = IllegalArgumentException) + void testNullJwk() { + new DispatchingJwkFactory().createJwk(null) + } + + @Test(expected = IllegalArgumentException) + void testEmptyJwk() { + new DispatchingJwkFactory().createJwk(new DefaultJwkContext()) + } + + @Test(expected = UnsupportedKeyException) + void testUnknownKeyType() { + def ctx = new DefaultJwkContext(); + ctx.put('kty', 'foo') + new DispatchingJwkFactory().createJwk(ctx) + } + + @Test + void testEcKeyPairToKey() { + + Map m = [ + 'kty': 'EC', + 'crv': 'P-256', + "x" : "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", + "y" : "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps", + "d" : "0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo" + ] + + def ctx = new DefaultJwkContext() + ctx.putAll(m) + + DispatchingJwkFactory factory = new DispatchingJwkFactory() + + def jwk = factory.createJwk(ctx) as EcPrivateJwk + assertTrue jwk instanceof EcPrivateJwk + def key = jwk.toKey() + assertTrue key instanceof ECPrivateKey + String x = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, jwk.toPublicJwk().toKey().w.affineX) + String y = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, jwk.toPublicJwk().toKey().w.affineY) + String d = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, key.s) + assertEquals jwk.d, d + + //remove the 'd' mapping to represent only a public key: + m.remove(DefaultEcPrivateJwk.D) + ctx = new DefaultJwkContext() + ctx.putAll(m) + + jwk = factory.createJwk(ctx) as EcPublicJwk + assertTrue jwk instanceof EcPublicJwk + key = jwk.toKey() as ECPublicKey + assertTrue key instanceof ECPublicKey + assertEquals jwk.x, x + assertEquals jwk.y, y + } + + @Test + @Ignore + //TODO re-enable + void testEcKeyPairToJwk() { + + KeyPair pair = SignatureAlgorithms.ES256.generateKeyPair() + ECPublicKey pubKey = (ECPublicKey) pair.getPublic() + def ctx = new DefaultJwkContext() + ctx.setKey(pubKey) + + DispatchingJwkFactory factory = new DispatchingJwkFactory() + + def jwk = factory.createJwk(ctx) + + assertNotNull jwk + assertEquals "EC", jwk.kty + assertEquals Encoders.BASE64URL.encode(pubKey.w.affineX.toByteArray()), jwk.x + assertEquals Encoders.BASE64URL.encode(pubKey.w.affineY.toByteArray()), jwk.y + assertNull jwk.d //public keys should not populate the private key 'd' parameter + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesEncryptionServiceTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithmTest.groovy similarity index 74% rename from impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesEncryptionServiceTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithmTest.groovy index 79daaa0ea..bcfe91277 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesEncryptionServiceTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithmTest.groovy @@ -1,17 +1,19 @@ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.security.* + +import io.jsonwebtoken.security.EncryptionAlgorithms import org.junit.Test import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec -import static org.junit.Assert.* +import static org.junit.Assert.assertArrayEquals +import static org.junit.Assert.fail /** * @since JJWT_RELEASE_VERSION */ -class GcmAesEncryptionServiceTest { +class GcmAesAeadAlgorithmTest { final byte[] K = [0xb1, 0xa1, 0xf4, 0x80, 0x54, 0x8f, 0xe1, 0x73, 0x3f, 0xb4, 0x3, 0xff, 0x6b, 0x9a, 0xd4, 0xf6, @@ -44,15 +46,12 @@ class GcmAesEncryptionServiceTest { def alg = EncryptionAlgorithms.A256GCM - def req = new DefaultEncryptionRequest(P, KEY, null, null, IV, AAD) - - def r = alg.encrypt(req) + def req = new DefaultAeadRequest(null, null, P, KEY, AAD, IV) - assertTrue r instanceof AeadIvEncryptionResult - AeadEncryptionResult result = r as AeadIvEncryptionResult + def result = alg.encrypt(req) - byte[] ciphertext = result.getCiphertext() - byte[] tag = result.getAuthenticationTag() + byte[] ciphertext = result.getPayload() + byte[] tag = result.getDigest() byte[] iv = result.getInitializationVector() assertArrayEquals E, ciphertext @@ -60,17 +59,17 @@ class GcmAesEncryptionServiceTest { assertArrayEquals IV, iv //shouldn't have been altered // now test decryption: - def dreq = new DefaultAeadIvRequest(ciphertext, KEY, null, null, iv, AAD, tag) - byte[] decryptionResult = alg.decrypt(dreq) - assertArrayEquals(P, decryptionResult); + def dreq = new DefaultAeadResult(null, null, ciphertext, KEY, AAD, tag, iv) + byte[] decryptionResult = alg.decrypt(dreq).getPayload() + assertArrayEquals(P, decryptionResult) } @Test void testInstantiationWithInvalidKeyLength() { try { - new GcmAesEncryptionAlgorithm('A128GCM', 5); + new GcmAesAeadAlgorithm(5) fail() - } catch (IllegalArgumentException expected) { + } catch (IllegalArgumentException ignored) { } } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy new file mode 100644 index 000000000..9f4279b23 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy @@ -0,0 +1,45 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.EncryptionAlgorithms +import io.jsonwebtoken.security.SignatureException +import org.junit.Test + +import javax.crypto.SecretKey + +import static org.junit.Assert.assertEquals + +/** + * @since JJWT_RELEASE_VERSION + */ +class HmacAesAeadAlgorithmTest { + + @Test + void testGenerateKey() { + def alg = EncryptionAlgorithms.A128CBC_HS256 + SecretKey key = alg.generateKey(); + int algKeyByteLength = (alg.keyBitLength * 2) / Byte.SIZE + assertEquals algKeyByteLength, key.getEncoded().length + } + + @Test(expected = SignatureException) + void testDecryptWithInvalidTag() { + + def alg = EncryptionAlgorithms.A128CBC_HS256; + + SecretKey key = alg.generateKey() + + def plaintext = "Hello World! Nice to meet you!".getBytes("UTF-8") + + def req = new DefaultAeadRequest(null, null, plaintext, key, null) + def result = alg.encrypt(req); + + def realTag = result.getDigest(); + + //fake it: + def fakeTag = new byte[realTag.length] + Randoms.secureRandom().nextBytes(fakeTag) + + def dreq = new DefaultAeadResult(null, null, result.getPayload(), key, null, fakeTag, result.getInitializationVector()) + alg.decrypt(dreq) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithmTest.groovy deleted file mode 100644 index c08a5e475..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesEncryptionAlgorithmTest.groovy +++ /dev/null @@ -1,83 +0,0 @@ -package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.security.AeadIvEncryptionResult -import io.jsonwebtoken.security.CryptoException -import io.jsonwebtoken.security.EncryptionAlgorithms -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import javax.crypto.SecretKey -import javax.crypto.spec.SecretKeySpec - -import static org.junit.Assert.assertEquals - -/** - * @since JJWT_RELEASE_VERSION - */ -class HmacAesEncryptionAlgorithmTest { - - @Test(expected = SignatureException) - void testDecryptWithInvalidTag() { - - def alg = EncryptionAlgorithms.A128CBC_HS256; - - SecretKey key = alg.generateKey() - - def plaintext = "Hello World! Nice to meet you!".getBytes("UTF-8") - - def req = new DefaultEncryptionRequest(plaintext, key, null, null, null, null) - def result = alg.encrypt(req); - assert result instanceof AeadIvEncryptionResult - - def realTag = result.getAuthenticationTag(); - - //fake it: - def fakeTag = new byte[realTag.length] - Randoms.secureRandom().nextBytes(fakeTag) - - def dreq = new DefaultAeadIvRequest(result.getCiphertext(), key, null, null, result.getInitializationVector(), null, fakeTag) - alg.decrypt(dreq) - } - - @Test(expected = CryptoException) - void testGenerateKeyWithWeakSigAlgKey() { - final byte[] bytes = new byte[24] // less than 32 bytes/256 bits - Randoms.secureRandom().nextBytes(bytes) - - def sigAlg = new MacSignatureAlgorithm('HS256', 'HmacSHA256', 256) { - @Override - SecretKey generateKey() { - return new SecretKeySpec(bytes, 'HmacSHA256') - } - } - def alg = new HmacAesEncryptionAlgorithm("A128CBC-HS256", sigAlg) - alg.generateKey() - } - - @Test - void testGenerateKeyWithLongerThanExpectedSigAlgKey() { - final byte[] macKeyBytes = new byte[64] // more than required 32 bytes / 256 bits - Randoms.secureRandom().nextBytes(macKeyBytes) - - def sigAlg = new MacSignatureAlgorithm('HS256', 'HmacSHA256', 256) { - @Override - SecretKey generateKey() { - return new SecretKeySpec(macKeyBytes, 'HmacSHA256') - } - } - def alg = new HmacAesEncryptionAlgorithm("A128CBC-HS256", sigAlg) - def key = alg.generateKey() - - def encryptionKeyBytes = key.getEncoded() - - assertEquals 512, encryptionKeyBytes.length * Byte.SIZE - - //per https://tools.ietf.org/html/rfc7518#section-5.2.2.1 ensure the first half of the generated encryption - // key is the first 32 bytes of the larger-than-expected mac key - byte[] macKeyFirst32Bytes = new byte[32] - byte[] encKeyFirst32Bytes = new byte[32] - System.arraycopy(macKeyBytes, 0, macKeyFirst32Bytes, 0, 32) - System.arraycopy(encryptionKeyBytes, 0, encKeyFirst32Bytes, 0, 32) - assert Arrays.equals(macKeyFirst32Bytes, encKeyFirst32Bytes) - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/Issue542Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Issue542Test.groovy similarity index 69% rename from impl/src/test/groovy/io/jsonwebtoken/impl/crypto/Issue542Test.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/Issue542Test.groovy index ccfff63f2..fd4adf38a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/Issue542Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Issue542Test.groovy @@ -1,16 +1,10 @@ -package io.jsonwebtoken.impl.crypto +package io.jsonwebtoken.impl.security import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.SignatureAlgorithm import io.jsonwebtoken.security.SignatureAlgorithms -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo -import org.bouncycastle.cert.X509CertificateHolder -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter -import org.bouncycastle.openssl.PEMParser -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter import org.junit.Test -import java.nio.charset.StandardCharsets import java.security.PrivateKey import java.security.PublicKey @@ -37,34 +31,6 @@ class Issue542Test { (SignatureAlgorithms.PS512): PS512_0_10_7 ] - private static JcaX509CertificateConverter X509_CERT_CONVERTER = new JcaX509CertificateConverter() - private static JcaPEMKeyConverter PEM_KEY_CONVERTER = new JcaPEMKeyConverter() - - private static PEMParser getParser(String filename) { - InputStream is = Issue542Test.class.getResourceAsStream(filename) - return new PEMParser(new BufferedReader(new InputStreamReader(is, StandardCharsets.ISO_8859_1))) - } - - private static PublicKey readTestPublicKey(SignatureAlgorithm alg) { - PEMParser parser = getParser(alg.getName() + '.crt.pem') - X509CertificateHolder holder = parser.readObject() as X509CertificateHolder - try { - return X509_CERT_CONVERTER.getCertificate(holder).getPublicKey() - } finally { - parser.close() - } - } - - private static PrivateKey readTestPrivateKey(SignatureAlgorithm alg) { - PEMParser parser = getParser(alg.getName() + '.key.pem') - PrivateKeyInfo info = parser.readObject() as PrivateKeyInfo - try { - return PEM_KEY_CONVERTER.getPrivateKey(info) - } finally { - parser.close() - } - } - /** * Asserts backwards-compatibility for https://github.com/jwtk/jjwt/issues/542 */ @@ -74,7 +40,7 @@ class Issue542Test { def algs = [SignatureAlgorithms.PS256, SignatureAlgorithms.PS384, SignatureAlgorithms.PS512] for (alg in algs) { - PublicKey key = readTestPublicKey(alg) + PublicKey key = CertUtils.readTestPublicKey(alg) String jws = JWS_0_10_7_VALUES[alg] def token = Jwts.parser().setSigningKey(key).parseClaimsJws(jws) assert 'joe' == token.body.getIssuer() @@ -88,9 +54,9 @@ class Issue542Test { static void main(String[] args) { def algs = [SignatureAlgorithms.PS256, SignatureAlgorithms.PS384, SignatureAlgorithms.PS512] for (alg in algs) { - PrivateKey privateKey = readTestPrivateKey(alg) + PrivateKey privateKey = CertUtils.readTestPrivateKey(alg) String jws = Jwts.builder().setIssuer('joe').signWith(privateKey, alg).compact() - println "private static String ${alg.name()}_0_10_7 = '$jws'" + println "private static String ${alg.id()}_0_10_7 = '$jws'" } } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JcaTemplateTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JcaTemplateTest.groovy new file mode 100644 index 000000000..88d6d9c27 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JcaTemplateTest.groovy @@ -0,0 +1,113 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.impl.lang.CheckedFunction +import io.jsonwebtoken.security.SecurityException +import io.jsonwebtoken.security.SignatureException +import org.junit.Test + +import javax.crypto.Cipher +import javax.crypto.Mac +import java.security.Provider +import java.security.Security +import java.security.Signature + +import static org.junit.Assert.* + +class JcaTemplateTest { + + @Test + void testNewCipherWithExplicitProvider() { + Provider provider = Security.getProvider('SunJCE') + def template = new JcaTemplate('AES/CBC/PKCS5Padding', provider) + template.execute(Cipher.class, new CheckedFunction() { + @Override + byte[] apply(Cipher cipher) throws Exception { + assertNotNull cipher + assertSame provider, cipher.provider + return new byte[0] + } + }) + } + + @Test + void testGetInstanceFailureWithExplicitProvider() { + //noinspection GroovyUnusedAssignment + Provider provider = Security.getProvider('SunJCE') + def supplier = new JcaTemplate.JcaInstanceSupplier(Cipher.class, "AES", provider) { + @Override + protected Cipher doGetInstance() { + throw new IllegalStateException("foo") + } + } + + try { + supplier.getInstance() + } catch (SecurityException ce) { //should be wrapped as SecurityException + String msg = ce.getMessage() + //we check for starts-with/ends-with logic here instead of equals because the JCE provider String value + //contains the JCE version number, and that can differ across JDK versions. Since we use different JDK + //versions in the test machine matrix, we don't want test failures from JDKs that run on higher versions + assertTrue msg.startsWith('Unable to obtain Cipher instance from specified Provider {SunJCE') + assertTrue msg.endsWith('} for JCA algorithm \'AES\': foo') + } + } + + @Test + void testGetInstanceDoesNotWrapCryptoExceptions() { + def ex = new SecurityException("foo") + def supplier = new JcaTemplate.JcaInstanceSupplier(Cipher.class, 'AES', null) { + @Override + protected Cipher doGetInstance() { + throw ex + } + } + + try { + supplier.getInstance() + } catch (SecurityException ce) { + assertSame ex, ce + } + } + + static void wrapInSignatureException(Class instanceType, String jcaName) { + def ex = new IllegalArgumentException("foo") + def supplier = new JcaTemplate.JcaInstanceSupplier(instanceType, jcaName, null) { + @Override + protected Object doGetInstance() { + throw ex + } + } + + try { + supplier.getInstance() + } catch (SignatureException se) { + assertSame ex, se.getCause() + String msg = "Unable to obtain ${instanceType.simpleName} instance from default JCA Provider for JCA algorithm '${jcaName}': foo" + assertEquals msg, se.getMessage() + } + } + + @Test + void testNonCryptoExceptionForSignatureOrMacInstanceIsWrappedInSignatureException() { + wrapInSignatureException(Signature.class, 'RSA') + wrapInSignatureException(Mac.class, 'HmacSHA256') + } + + @Test + void testCallbackThrowsException() { + def ex = new Exception("testing") + def template = new JcaTemplate('AES/CBC/PKCS5Padding', null) + try { + template.execute(Cipher.class, new CheckedFunction() { + @Override + byte[] apply(Cipher cipher) throws Exception { + throw ex + } + }) + } catch (SecurityException e) { + assertEquals 'Cipher callback execution failed: testing', e.getMessage() + assertSame ex, e.getCause() + } + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index 7b199b5ba..451fbf84a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -1,47 +1,173 @@ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.security.CurveIds -import io.jsonwebtoken.security.Jwks + +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.* import org.junit.Test +import javax.crypto.SecretKey +import java.security.KeyPair +import java.security.MessageDigest +import java.security.PrivateKey +import java.security.PublicKey +import java.security.cert.X509Certificate +import java.security.interfaces.ECKey + import static org.junit.Assert.* class JwksTest { + private static final SecretKey SKEY = SignatureAlgorithms.HS256.generateKey(); + private static final KeyPair EC_PAIR = SignatureAlgorithms.ES256.generateKeyPair(); + + private static String srandom() { + byte[] random = new byte[16]; + Randoms.secureRandom().nextBytes(random) + return Encoders.BASE64URL.encode(random); + } + + static void testProperty(String name, String id, def val, def expectedFieldValue=val) { + String cap = "${name.capitalize()}" + def key = name == 'publicKeyUse' || name == 'x509CertificateChain' ? EC_PAIR.public : SKEY + + //test non-null value: + def builder = Jwks.builder().setKey(key) + builder."set${cap}"(val) + def jwk = builder.build() + assertEquals val, jwk."get${cap}"() + assertEquals expectedFieldValue, jwk."${id}" + + //test null value: + builder = Jwks.builder().setKey(key) + try { + builder."set${cap}"(null) + fail("IAE should have been thrown") + } catch (IllegalArgumentException ignored) { + } + jwk = builder.build() + assertNull jwk."get${cap}"() + assertNull jwk."$id" + assertFalse jwk.containsKey(id) + + //test empty string value + builder = Jwks.builder().setKey(key) + if (val instanceof String) { + try { + builder."set${cap}"(' ' as String) + fail("IAE should have been thrown") + } catch (IllegalArgumentException ignored) { + } + jwk = builder.build() + assertNull jwk."get${cap}"() + assertNull jwk."$id" + assertFalse jwk.containsKey(id) + } + + //test empty value + if (val instanceof List) { + val = Collections.emptyList() + } else if (val instanceof Set) { + val = Collections.emptySet() + } + if (val instanceof Collection) { + try { + builder."set${cap}"(val) + fail("IAE should have been thrown") + } catch (IllegalArgumentException ignored) { + } + jwk = builder.build() + assertNull jwk."get${cap}"() + assertNull jwk."$id" + assertFalse jwk.containsKey(id) + } + } + @Test - void testBuilder() { - assertTrue Jwks.builder() instanceof DefaultJwkBuilderFactory + void testBuilderWithSecretKey() { + def jwk = Jwks.builder().setKey(SKEY).build() + assertEquals 'oct', jwk.getType() + assertEquals 'oct', jwk.kty + assertNotNull jwk.k + assertTrue jwk.k instanceof String + assertTrue MessageDigest.isEqual(SKEY.encoded, Decoders.BASE64URL.decode(jwk.k as String)) } @Test - void testBuilderSymmetric() { - assertTrue Jwks.builder().symmetric() instanceof DefaultSymmetricJwkBuilder + void testAlgorithm() { + testProperty('algorithm', 'alg', srandom()) } @Test - void testBuilderEc() { - assertTrue Jwks.builder().ellipticCurve() instanceof DefaultEcJwkBuilderFactory + void testId() { + testProperty('id', 'kid', srandom()) } @Test - void testBuilderEcPublicKey() { - assertTrue Jwks.builder().ellipticCurve().publicKey() instanceof DefaultPublicEcJwkBuilder + void testOperations() { + testProperty('operations', 'key_ops', ['foo', 'bar'] as Set) } @Test - void testBuilderEcPrivateKey() { - assertTrue Jwks.builder().ellipticCurve().privateKey() instanceof DefaultPrivateEcJwkBuilder + void testPublicKeyUse() { + testProperty('publicKeyUse', 'use', srandom()) } @Test - void testSymmetric() { - println Jwks.builder().symmetric().setUse("signature").setId(UUID.randomUUID().toString()).setK("foo").build() + void testX509CertChain() { + //get a test cert: + X509Certificate cert = CertUtils.readTestCertificate(SignatureAlgorithms.RS256) + def sval = JwkX509StringConverter.INSTANCE.applyTo(cert) + testProperty('x509CertificateChain', 'x5c', [cert], [sval]) } @Test - void testFoo() { - println Jwks.builder().ellipticCurve().publicKey().setCurveId(CurveIds.P256).setX("xval").setY("yval").build() - println Jwks.builder().ellipticCurve().publicKey().setCurveId(CurveIds.P384).setX("x").setY("y").build() - println Jwks.builder().ellipticCurve().privateKey().setCurveId(CurveIds.P521).setX("x").setY("y").setD("d").build() + void testSecretJwks() { + Collection algs = SignatureAlgorithms.values().findAll({it instanceof SecretKeySignatureAlgorithm}) as Collection + for(def alg : algs) { + SecretKey secretKey = alg.generateKey() + def jwk = Jwks.builder().setKey(secretKey).setId('id').build() + assertEquals 'oct', jwk.getType() + assertTrue jwk.containsKey('k') + assertEquals 'id', jwk.getId() + assertEquals secretKey, jwk.toKey() + } + } + + @Test + void testAsymmetricJwks() { + + Collection algs = SignatureAlgorithms.values().findAll({it instanceof AsymmetricKeySignatureAlgorithm}) as Collection + + for(def alg : algs) { + + def pair = alg.generateKeyPair() + PublicKey pub = pair.getPublic() + PrivateKey priv = pair.getPrivate() + + // test individual keys + PublicJwk pubJwk = Jwks.builder().setKey(pub).setPublicKeyUse("sig").build() + assertEquals pub, pubJwk.toKey() + PrivateJwk privJwk = Jwks.builder().setKey(priv).setPublicKeyUse("sig").build() + assertEquals priv, privJwk.toKey() + PublicJwk privPubJwk = privJwk.toPublicJwk() + assertEquals pubJwk, privPubJwk + assertEquals pub, pubJwk.toKey() + def jwkPair = privJwk.toKeyPair() + assertEquals pub, jwkPair.getPublic() + assertEquals priv, jwkPair.getPrivate() + + // test pair + privJwk = pub instanceof ECKey ? + Jwks.builder().setKeyPairEc(pair).setPublicKeyUse("sig").build() : + Jwks.builder().setKeyPairRsa(pair).setPublicKeyUse("sig").build() + assertEquals priv, privJwk.toKey() + privPubJwk = privJwk.toPublicJwk() + assertEquals pubJwk, privPubJwk + assertEquals pub, pubJwk.toKey() + jwkPair = privJwk.toKeyPair() + assertEquals pub, jwkPair.getPublic() + assertEquals priv, jwkPair.getPrivate() + } } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy index 43eef21e2..303878171 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy @@ -1,22 +1,14 @@ package io.jsonwebtoken.impl.security + import io.jsonwebtoken.security.InvalidKeyException -import io.jsonwebtoken.security.SignatureAlgorithm -import io.jsonwebtoken.security.SignatureException +import io.jsonwebtoken.security.SecurityException import io.jsonwebtoken.security.WeakKeyException import org.junit.Test -import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec -import java.security.Key -import java.security.NoSuchAlgorithmException -import java.security.Provider -import java.security.Security -import static org.easymock.EasyMock.createMock import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertNotNull -import static org.junit.Assert.assertSame class MacSignatureAlgorithmTest { @@ -24,51 +16,19 @@ class MacSignatureAlgorithmTest { return new MacSignatureAlgorithm('HS256', 'HmacSHA256', 256) } - @Test(expected = UnsupportedOperationException) + @Test(expected = SecurityException) void testKeyGeneratorNoSuchAlgorithm() { MacSignatureAlgorithm alg = new MacSignatureAlgorithm('HS256', 'foo', 256); alg.generateKey() } @Test - void testDoGetMacInstanceWithProvider() { - Provider provider = Security.getProvider("SunJCE") - MacSignatureAlgorithm alg = newAlg() - assertNotNull alg.doGetMacInstance('HmacSHA256', provider) - } - - @Test - void testGetMacInstanceDefault() { - def expected = new NoSuchAlgorithmException('test') - MacSignatureAlgorithm alg = new MacSignatureAlgorithm('HS256', 'HmacSHA256', 256) { - @Override - def Mac doGetMacInstance(String jcaName, Provider provider) throws NoSuchAlgorithmException { - throw expected - } - } - try { - alg.sign(new DefaultCryptoRequest(new byte[1], new SecretKeySpec(new byte[32], 'HmacSHA256'), null, null)) - } catch (SignatureException e) { - assertEquals 'There is no JCA Provider available that supports MAC algorithm name \'HmacSHA256\'.', e.getMessage() - } - } + void testKeyGeneratorKeyLength() { + MacSignatureAlgorithm alg = new MacSignatureAlgorithm('HS256', 'HmacSHA256', 256); + assertEquals 256, alg.generateKey().getEncoded().length * Byte.SIZE - @Test - void testGetMacInstanceWithProvider() { - Provider provider = createMock(Provider) - String providerString = provider.toString() - def expected = new NoSuchAlgorithmException('test') - MacSignatureAlgorithm alg = new MacSignatureAlgorithm('HS256', 'HmacSHA256', 256) { - @Override - def Mac doGetMacInstance(String jcaName, Provider p) throws NoSuchAlgorithmException { - throw expected - } - } - try { - alg.sign(new DefaultCryptoRequest(new byte[1], new SecretKeySpec(new byte[32], 'HmacSHA256'), provider, null)) - } catch (SignatureException e) { - assertEquals 'The specified JCA Provider {' + providerString + '} does not support MAC algorithm name \'HmacSHA256\'.', e.getMessage() - } + alg = new MacSignatureAlgorithm('A128CBC-HS256', 'HmacSHA256', 128) + assertEquals 128, alg.generateKey().getEncoded().length * Byte.SIZE } @Test(expected = IllegalArgumentException) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy index b3315d61b..d9261b37d 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy @@ -9,7 +9,7 @@ class NoneSignatureAlgorithmTest { @Test void testName() { - assertEquals "none", new NoneSignatureAlgorithm().getName(); + assertEquals "none", new NoneSignatureAlgorithm().getId(); } @Test(expected = SignatureException) @@ -21,4 +21,9 @@ class NoneSignatureAlgorithmTest { void testVerify() { new NoneSignatureAlgorithm().verify(null) } + + @Test + void testHashCode() { + assertEquals 'none'.hashCode(), new NoneSignatureAlgorithm().hashCode() + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy new file mode 100644 index 000000000..6e85d53b7 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy @@ -0,0 +1,44 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.security.EncryptionAlgorithms +import io.jsonwebtoken.security.KeyAlgorithms +import io.jsonwebtoken.security.Keys +import org.junit.Ignore +import org.junit.Test + +class Pbes2HsAkwAlgorithmTest { + + @Ignore // for manual/developer testing only. Takes a long time and there is no deterministic output to assert + @Test + void test() { + + def alg = KeyAlgorithms.PBES2_HS256_A128KW + + int desiredMillis = 100 + int iterations = KeyAlgorithms.estimateIterations(alg, desiredMillis) + println "Estimated iterations: $iterations" + + int tries = 30 + int skip = 6 + //double scale = 0.5035246727 + + def password = 'hellowor'.toCharArray() + def key = Keys.forPbe().setPassword(password).setIterations(iterations).build() + def req = new DefaultKeyRequest(null, null, key, new DefaultJweHeader(), EncryptionAlgorithms.A128GCM) + int sum = 0; + for(int i = 0; i < tries; i++) { + long start = System.currentTimeMillis() + alg.getEncryptionKey(req) + long end = System.currentTimeMillis() + long duration = end - start; + if (i >= skip) { + sum+= duration + } + println "Try $i: ${alg.id} took $duration millis" + } + long avg = Math.round(sum / (tries - skip)) + println "Average duration: $avg" + println "scale factor: ${desiredMillis / avg}" + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateEcJwkValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateEcJwkValidatorTest.groovy deleted file mode 100644 index 33665fe27..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateEcJwkValidatorTest.groovy +++ /dev/null @@ -1,31 +0,0 @@ -package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.security.CurveIds -import io.jsonwebtoken.security.MalformedKeyException -import org.junit.Test - -class PrivateEcJwkValidatorTest { - - static PrivateEcJwkValidator validator() { - return new PrivateEcJwkValidator() - } - - @Test(expected = MalformedKeyException) - void testNullD() { - def jwk = new DefaultPrivateEcJwk().setCurveId(CurveIds.P521).setX('x').setY('y') - validator().validate(jwk) - } - - @Test(expected = MalformedKeyException) - void testEmptyD() { - def jwk = new DefaultPrivateEcJwk().setCurveId(CurveIds.P521).setX('x').setY('y') - jwk.put('d', ' ') - validator().validate(jwk) - } - - @Test - void testValid() { - def jwk = new DefaultPrivateEcJwk().setCurveId(CurveIds.P521).setX('x').setY('y').setD('d') - validator().validate(jwk) - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA1Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA1Test.groovy new file mode 100644 index 000000000..94e79581a --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA1Test.groovy @@ -0,0 +1,165 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.Jwe +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPrivateJwk +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets +import java.security.interfaces.RSAPrivateKey + +import static org.junit.Assert.assertArrayEquals +import static org.junit.Assert.assertEquals + +/** + * Tests successful parsing/decryption of a 'JWE using RSAES-OAEP and AES GCM' as defined in + * RFC 7516, Appendix A.1 + * + * @since JJWT_RELEASE_VERSION + */ +class RFC7516AppendixA1Test { + + static String encode(byte[] b) { + return Encoders.BASE64URL.encode(b) + } + + static byte[] decode(String val) { + return Decoders.BASE64URL.decode(val) + } + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.1 : + final static String PLAINTEXT = 'The true sign of intelligence is not knowledge but imagination.' as String + final static byte[] PLAINTEXT_BYTES = [84, 104, 101, 32, 116, 114, 117, 101, 32, 115, 105, 103, 110, 32, + 111, 102, 32, 105, 110, 116, 101, 108, 108, 105, 103, 101, 110, 99, + 101, 32, 105, 115, 32, 110, 111, 116, 32, 107, 110, 111, 119, 108, + 101, 100, 103, 101, 32, 98, 117, 116, 32, 105, 109, 97, 103, 105, + 110, 97, 116, 105, 111, 110, 46] as byte[] + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.1.1 : + final static String PROT_HEADER_STRING = '{"alg":"RSA-OAEP","enc":"A256GCM"}' as String + final static String encodedHeader = 'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ' as String + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.1.2 + final static byte[] CEK_BYTES = [177, 161, 244, 128, 84, 143, 225, 115, 63, 180, 3, 255, 107, 154, + 212, 246, 138, 7, 110, 91, 112, 46, 34, 105, 47, 130, 203, 46, 122, + 234, 64, 252] as byte[] + final static SecretKey CEK = new SecretKeySpec(CEK_BYTES, "AES") + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.1.3 + final static Map KEK_VALUES = [ + "kty": "RSA", + "n" : "oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUW" + + "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S" + + "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a" + + "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS" + + "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj" + + "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw", + "e" : "AQAB", + "d" : "kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5N" + + "WV5KntaEeXS1j82E375xxhWMHXyvjYecPT9fpwR_M9gV8n9Hrh2anTpTD9" + + "3Dt62ypW3yDsJzBnTnrYu1iwWRgBKrEYY46qAZIrA2xAwnm2X7uGR1hghk" + + "qDp0Vqj3kbSCz1XyfCs6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vl" + + "t3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSnd" + + "VTIznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ", + "p" : "1r52Xk46c-LsfB5P442p7atdPUrxQSy4mti_tZI3Mgf2EuFVbUoDBvaRQ-" + + "SWxkbkmoEzL7JXroSBjSrK3YIQgYdMgyAEPTPjXv_hI2_1eTSPVZfzL0lf" + + "fNn03IXqWF5MDFuoUYE0hzb2vhrlN_rKrbfDIwUbTrjjgieRbwC6Cl0", + "q" : "wLb35x7hmQWZsWJmB_vle87ihgZ19S8lBEROLIsZG4ayZVe9Hi9gDVCOBm" + + "UDdaDYVTSNx_8Fyw1YYa9XGrGnDew00J28cRUoeBB_jKI1oma0Orv1T9aX" + + "IWxKwd4gvxFImOWr3QRL9KEBRzk2RatUBnmDZJTIAfwTs0g68UZHvtc", + "dp" : "ZK-YwE7diUh0qR1tR7w8WHtolDx3MZ_OTowiFvgfeQ3SiresXjm9gZ5KL" + + "hMXvo-uz-KUJWDxS5pFQ_M0evdo1dKiRTjVw_x4NyqyXPM5nULPkcpU827" + + "rnpZzAJKpdhWAgqrXGKAECQH0Xt4taznjnd_zVpAmZZq60WPMBMfKcuE", + "dq" : "Dq0gfgJ1DdFGXiLvQEZnuKEN0UUmsJBxkjydc3j4ZYdBiMRAy86x0vHCj" + + "ywcMlYYg4yoC4YZa9hNVcsjqA3FeiL19rk8g6Qn29Tt0cj8qqyFpz9vNDB" + + "UfCAiJVeESOjJDZPYHdHY8v1b-o-Z2X5tvLx-TCekf7oxyeKDUqKWjis", + "qi" : "VIMpMYbPf47dT1w_zDUXfPimsSegnMOA1zTaX7aGk_8urY6R8-ZW1FxU7" + + "AlWAyLWybqq6t16VFd7hQd0y6flUK4SlOydB61gwanOsXGOAOv82cHq0E3" + + "eL4HrtZkUuKvnPrMnsUUFlfUdybVzxyjz9JF_XyaY14ardLSjf4L_FNY" + ] + + final static byte[] ENCRYPTED_CEK_BYTES = [56, 163, 154, 192, 58, 53, 222, 4, 105, 218, 136, 218, 29, 94, 203, + 22, 150, 92, 129, 94, 211, 232, 53, 89, 41, 60, 138, 56, 196, 216, + 82, 98, 168, 76, 37, 73, 70, 7, 36, 8, 191, 100, 136, 196, 244, 220, + 145, 158, 138, 155, 4, 117, 141, 230, 199, 247, 173, 45, 182, 214, + 74, 177, 107, 211, 153, 11, 205, 196, 171, 226, 162, 128, 171, 182, + 13, 237, 239, 99, 193, 4, 91, 219, 121, 223, 107, 167, 61, 119, 228, + 173, 156, 137, 134, 200, 80, 219, 74, 253, 56, 185, 91, 177, 34, 158, + 89, 154, 205, 96, 55, 18, 138, 43, 96, 218, 215, 128, 124, 75, 138, + 243, 85, 25, 109, 117, 140, 26, 155, 249, 67, 167, 149, 231, 100, 6, + 41, 65, 214, 251, 232, 87, 72, 40, 182, 149, 154, 168, 31, 193, 126, + 215, 89, 28, 111, 219, 125, 182, 139, 235, 195, 197, 23, 234, 55, 58, + 63, 180, 68, 202, 206, 149, 75, 205, 248, 176, 67, 39, 178, 60, 98, + 193, 32, 238, 122, 96, 158, 222, 57, 183, 111, 210, 55, 188, 215, + 206, 180, 166, 150, 166, 106, 250, 55, 229, 72, 40, 69, 214, 216, + 104, 23, 40, 135, 212, 28, 127, 41, 80, 175, 174, 168, 115, 171, 197, + 89, 116, 92, 103, 246, 83, 216, 182, 176, 84, 37, 147, 35, 45, 219, + 172, 99, 226, 233, 73, 37, 124, 42, 72, 49, 242, 35, 127, 184, 134, + 117, 114, 135, 206] as byte[] + + final static String encodedEncryptedCek = 'OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe' + + 'ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb' + + 'Sv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaV' + + 'mqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je8' + + '1860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi' + + '6UklfCpIMfIjf7iGdXKHzg' as String + + // https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.1.4 + final static byte[] IV = [227, 197, 117, 252, 2, 219, 233, 68, 180, 225, 77, 219] as byte[] + final static String encodedIv = '48V1_ALb6US04U3b' as String + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.1.5 + final static byte[] AAD_BYTES = [101, 121, 74, 104, 98, 71, 99, 105, 79, 105, 74, 83, 85, 48, 69, + 116, 84, 48, 70, 70, 85, 67, 73, 115, 73, 109, 86, 117, 89, 121, 73, + 54, 73, 107, 69, 121, 78, 84, 90, 72, 81, 48, 48, 105, 102, 81] as byte[] + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.1.6 + final static byte[] CIPHERTEXT = [229, 236, 166, 241, 53, 191, 115, 196, 174, 43, 73, 109, 39, 122, + 233, 96, 140, 206, 120, 52, 51, 237, 48, 11, 190, 219, 186, 80, 111, + 104, 50, 142, 47, 167, 59, 61, 181, 127, 196, 21, 40, 82, 242, 32, + 123, 143, 168, 226, 73, 216, 176, 144, 138, 247, 106, 60, 16, 205, + 160, 109, 64, 63, 192] as byte[] + final static String encodedCiphertext = + '5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A' as String + + final static byte[] TAG = [92, 80, 104, 49, 133, 25, 161, 215, 173, 101, 219, 211, 136, 91, 210, 145] as byte[] + final static String encodedTag = 'XFBoMYUZodetZdvTiFvSkQ' + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.1.7 + final static String COMPLETE_JWE = '' + + 'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.' + + 'OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe' + + 'ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb' + + 'Sv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaV' + + 'mqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je8' + + '1860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi' + + '6UklfCpIMfIjf7iGdXKHzg.' + + '48V1_ALb6US04U3b.' + + '5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6ji' + + 'SdiwkIr3ajwQzaBtQD_A.' + + 'XFBoMYUZodetZdvTiFvSkQ' as String + + @Test + void test() { + //ensure our test constants are correctly copied and match the RFC values: + assertEquals PLAINTEXT, new String(PLAINTEXT_BYTES, StandardCharsets.UTF_8) + assertEquals PROT_HEADER_STRING, new String(decode(encodedHeader), StandardCharsets.UTF_8) + assertEquals encodedEncryptedCek, encode(ENCRYPTED_CEK_BYTES) + assertEquals encodedIv, encode(IV) + assertArrayEquals AAD_BYTES, encodedHeader.getBytes(StandardCharsets.US_ASCII) + assertArrayEquals CIPHERTEXT, decode(encodedCiphertext) + assertArrayEquals TAG, decode(encodedTag) + + //read the RFC Test JWK to get the private key for decrypting + RsaPrivateJwk jwk = Jwks.builder().putAll(KEK_VALUES).build() as RsaPrivateJwk + RSAPrivateKey privKey = jwk.toKey() + + Jwe jwe = Jwts.parserBuilder().decryptWith(privKey).build().parsePlaintextJwe(COMPLETE_JWE) + assertEquals PLAINTEXT, jwe.getBody() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA2Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA2Test.groovy new file mode 100644 index 000000000..aef018129 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA2Test.groovy @@ -0,0 +1,159 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.Jwe +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPrivateJwk +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets +import java.security.interfaces.RSAPrivateKey + +import static org.junit.Assert.assertArrayEquals +import static org.junit.Assert.assertEquals + +/** + * Tests successful parsing/decryption of a 'JWE using RSAES-PKCS1-v1_5 and AES_128_CBC_HMAC_SHA_256' as defined in + * RFC 7516, Appendix A.2 + * + * @since JJWT_RELEASE_VERSION + */ +class RFC7516AppendixA2Test { + + static String encode(byte[] b) { + return Encoders.BASE64URL.encode(b) + } + + static byte[] decode(String val) { + return Decoders.BASE64URL.decode(val) + } + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.2 : + final static String PLAINTEXT = 'Live long and prosper.' as String + final static byte[] PLAINTEXT_BYTES = [76, 105, 118, 101, 32, 108, 111, 110, 103, 32, 97, 110, 100, 32, + 112, 114, 111, 115, 112, 101, 114, 46] as byte[] + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.2.1 : + final static String PROT_HEADER_STRING = '{"alg":"RSA1_5","enc":"A128CBC-HS256"}' as String + final static String encodedHeader = 'eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0' as String + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.2.2 + final static byte[] CEK_BYTES = [4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, + 206, 107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, + 44, 207] as byte[] + final static SecretKey CEK = new SecretKeySpec(CEK_BYTES, "AES") + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.2.3 + final static Map KEK_VALUES = [ + "kty": "RSA", + "n" : "sXchDaQebHnPiGvyDOAT4saGEUetSyo9MKLOoWFsueri23bOdgWp4Dy1Wl" + + "UzewbgBHod5pcM9H95GQRV3JDXboIRROSBigeC5yjU1hGzHHyXss8UDpre" + + "cbAYxknTcQkhslANGRUZmdTOQ5qTRsLAt6BTYuyvVRdhS8exSZEy_c4gs_" + + "7svlJJQ4H9_NxsiIoLwAEk7-Q3UXERGYw_75IDrGA84-lA_-Ct4eTlXHBI" + + "Y2EaV7t7LjJaynVJCpkv4LKjTTAumiGUIuQhrNhZLuF_RJLqHpM2kgWFLU" + + "7-VTdL1VbC2tejvcI2BlMkEpk1BzBZI0KQB0GaDWFLN-aEAw3vRw", + "e" : "AQAB", + "d" : "VFCWOqXr8nvZNyaaJLXdnNPXZKRaWCjkU5Q2egQQpTBMwhprMzWzpR8Sxq" + + "1OPThh_J6MUD8Z35wky9b8eEO0pwNS8xlh1lOFRRBoNqDIKVOku0aZb-ry" + + "nq8cxjDTLZQ6Fz7jSjR1Klop-YKaUHc9GsEofQqYruPhzSA-QgajZGPbE_" + + "0ZaVDJHfyd7UUBUKunFMScbflYAAOYJqVIVwaYR5zWEEceUjNnTNo_CVSj" + + "-VvXLO5VZfCUAVLgW4dpf1SrtZjSt34YLsRarSb127reG_DUwg9Ch-Kyvj" + + "T1SkHgUWRVGcyly7uvVGRSDwsXypdrNinPA4jlhoNdizK2zF2CWQ", + "p" : "9gY2w6I6S6L0juEKsbeDAwpd9WMfgqFoeA9vEyEUuk4kLwBKcoe1x4HG68" + + "ik918hdDSE9vDQSccA3xXHOAFOPJ8R9EeIAbTi1VwBYnbTp87X-xcPWlEP" + + "krdoUKW60tgs1aNd_Nnc9LEVVPMS390zbFxt8TN_biaBgelNgbC95sM", + "q" : "uKlCKvKv_ZJMVcdIs5vVSU_6cPtYI1ljWytExV_skstvRSNi9r66jdd9-y" + + "BhVfuG4shsp2j7rGnIio901RBeHo6TPKWVVykPu1iYhQXw1jIABfw-MVsN" + + "-3bQ76WLdt2SDxsHs7q7zPyUyHXmps7ycZ5c72wGkUwNOjYelmkiNS0", + "dp" : "w0kZbV63cVRvVX6yk3C8cMxo2qCM4Y8nsq1lmMSYhG4EcL6FWbX5h9yuv" + + "ngs4iLEFk6eALoUS4vIWEwcL4txw9LsWH_zKI-hwoReoP77cOdSL4AVcra" + + "Hawlkpyd2TWjE5evgbhWtOxnZee3cXJBkAi64Ik6jZxbvk-RR3pEhnCs", + "dq" : "o_8V14SezckO6CNLKs_btPdFiO9_kC1DsuUTd2LAfIIVeMZ7jn1Gus_Ff" + + "7B7IVx3p5KuBGOVF8L-qifLb6nQnLysgHDh132NDioZkhH7mI7hPG-PYE_" + + "odApKdnqECHWw0J-F0JWnUd6D2B_1TvF9mXA2Qx-iGYn8OVV1Bsmp6qU", + "qi" : "eNho5yRBEBxhGBtQRww9QirZsB66TrfFReG_CcteI1aCneT0ELGhYlRlC" + + "tUkTRclIfuEPmNsNDPbLoLqqCVznFbvdB7x-Tl-m0l_eFTj2KiqwGqE9PZ" + + "B9nNTwMVvH3VRRSLWACvPnSiwP8N5Usy-WRXS-V7TbpxIhvepTfE0NNo" + ] + + final static byte[] ENCRYPTED_CEK_BYTES = [80, 104, 72, 58, 11, 130, 236, 139, 132, 189, 255, 205, 61, 86, 151, + 176, 99, 40, 44, 233, 176, 189, 205, 70, 202, 169, 72, 40, 226, 181, + 156, 223, 120, 156, 115, 232, 150, 209, 145, 133, 104, 112, 237, 156, + 116, 250, 65, 102, 212, 210, 103, 240, 177, 61, 93, 40, 71, 231, 223, + 226, 240, 157, 15, 31, 150, 89, 200, 215, 198, 203, 108, 70, 117, 66, + 212, 238, 193, 205, 23, 161, 169, 218, 243, 203, 128, 214, 127, 253, + 215, 139, 43, 17, 135, 103, 179, 220, 28, 2, 212, 206, 131, 158, 128, + 66, 62, 240, 78, 186, 141, 125, 132, 227, 60, 137, 43, 31, 152, 199, + 54, 72, 34, 212, 115, 11, 152, 101, 70, 42, 219, 233, 142, 66, 151, + 250, 126, 146, 141, 216, 190, 73, 50, 177, 146, 5, 52, 247, 28, 197, + 21, 59, 170, 247, 181, 89, 131, 241, 169, 182, 246, 99, 15, 36, 102, + 166, 182, 172, 197, 136, 230, 120, 60, 58, 219, 243, 149, 94, 222, + 150, 154, 194, 110, 227, 225, 112, 39, 89, 233, 112, 207, 211, 241, + 124, 174, 69, 221, 179, 107, 196, 225, 127, 167, 112, 226, 12, 242, + 16, 24, 28, 120, 182, 244, 213, 244, 153, 194, 162, 69, 160, 244, + 248, 63, 165, 141, 4, 207, 249, 193, 79, 131, 0, 169, 233, 127, 167, + 101, 151, 125, 56, 112, 111, 248, 29, 232, 90, 29, 147, 110, 169, + 146, 114, 165, 204, 71, 136, 41, 252] as byte[] + + final static String encodedEncryptedCek = 'UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm' + + '1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7Pc' + + 'HALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIF' + + 'NPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPhcCdZ6XDP0_F8' + + 'rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPgwCp6X-nZZd9OHBv' + + '-B3oWh2TbqmScqXMR4gp_A' as String + + // https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.2.4 + final static byte[] IV = [3, 22, 60, 12, 43, 67, 104, 105, 108, 108, 105, 99, 111, 116, 104, 101] as byte[] + final static String encodedIv = 'AxY8DCtDaGlsbGljb3RoZQ' as String + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.2.5 + final static byte[] AAD_BYTES = [101, 121, 74, 104, 98, 71, 99, 105, 79, 105, 74, 83, 85, 48, 69, + 120, 88, 122, 85, 105, 76, 67, 74, 108, 98, 109, 77, 105, 79, 105, + 74, 66, 77, 84, 73, 52, 81, 48, 74, 68, 76, 85, 104, 84, 77, 106, 85, + 50, 73, 110, 48] as byte[] + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.2.6 + final static byte[] CIPHERTEXT = [40, 57, 83, 181, 119, 33, 133, 148, 198, 185, 243, 24, 152, 230, 6, + 75, 129, 223, 127, 19, 210, 82, 183, 230, 168, 33, 215, 104, 143, + 112, 56, 102] as byte[] + final static String encodedCiphertext = 'KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY' as String + + final static byte[] TAG = [246, 17, 244, 190, 4, 95, 98, 3, 231, 0, 115, 157, 242, 203, 100, 191] as byte[] + final static String encodedTag = '9hH0vgRfYgPnAHOd8stkvw' + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.2.7 + final static String COMPLETE_JWE = + "eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0." + + "UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm" + + "1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7Pc" + + "HALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIF" + + "NPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPhcCdZ6XDP0_F8" + + "rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPgwCp6X-nZZd9OHBv" + + "-B3oWh2TbqmScqXMR4gp_A." + + "AxY8DCtDaGlsbGljb3RoZQ." + + "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY." + + "9hH0vgRfYgPnAHOd8stkvw" as String + + @Test + void test() { + //ensure our test constants are correctly copied and match the RFC values: + assertEquals PLAINTEXT, new String(PLAINTEXT_BYTES, StandardCharsets.UTF_8) + assertEquals PROT_HEADER_STRING, new String(decode(encodedHeader), StandardCharsets.UTF_8) + assertEquals encodedEncryptedCek, encode(ENCRYPTED_CEK_BYTES) + assertEquals encodedIv, encode(IV) + assertArrayEquals AAD_BYTES, encodedHeader.getBytes(StandardCharsets.US_ASCII) + assertArrayEquals CIPHERTEXT, decode(encodedCiphertext) + assertArrayEquals TAG, decode(encodedTag) + + //read the RFC Test JWK to get the private key for decrypting + RsaPrivateJwk jwk = Jwks.builder().putAll(KEK_VALUES).build() as RsaPrivateJwk + RSAPrivateKey privKey = jwk.toKey() + + Jwe jwe = Jwts.parserBuilder().decryptWith(privKey).build().parsePlaintextJwe(COMPLETE_JWE) + assertEquals PLAINTEXT, jwe.getBody() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy new file mode 100644 index 000000000..8ead52ed7 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy @@ -0,0 +1,125 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.Jwe +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.* +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.assertArrayEquals +import static org.junit.Assert.assertEquals + +class RFC7516AppendixA3Test { + + static String encode(byte[] b) { + return Encoders.BASE64URL.encode(b) + } + + static byte[] decode(String val) { + return Decoders.BASE64URL.decode(val) + } + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.3 : + final static String PLAINTEXT = 'Live long and prosper.' as String + final static byte[] PLAINTEXT_BYTES = [76, 105, 118, 101, 32, 108, 111, 110, 103, 32, 97, 110, 100, 32, + 112, 114, 111, 115, 112, 101, 114, 46] as byte[] + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.3.1 : + final static String PROT_HEADER_STRING = '{"alg":"A128KW","enc":"A128CBC-HS256"}' as String + final static String encodedHeader = 'eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0' as String + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.2.2 + final static byte[] CEK_BYTES = [4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, + 206, 107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, + 44, 207] as byte[] + final static SecretKey CEK = new SecretKeySpec(CEK_BYTES, "AES") + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.3.3 + final static Map KEK_VALUES = [ + "kty": "oct", + "k" : "GawgguFyGrWKav7AX4VKUg" + ] + + final static byte[] ENCRYPTED_CEK_BYTES = [232, 160, 123, 211, 183, 76, 245, 132, 200, 128, 123, 75, 190, 216, + 22, 67, 201, 138, 193, 186, 9, 91, 122, 31, 246, 90, 28, 139, 57, 3, + 76, 124, 193, 11, 98, 37, 173, 61, 104, 57] as byte[] + + final static String encodedEncryptedCek = '6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ' as String + + // https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.3.4 + final static byte[] IV = [3, 22, 60, 12, 43, 67, 104, 105, 108, 108, 105, 99, 111, 116, 104, 101] as byte[] + final static String encodedIv = 'AxY8DCtDaGlsbGljb3RoZQ' as String + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.3.5 + final static byte[] AAD_BYTES = [101, 121, 74, 104, 98, 71, 99, 105, 79, 105, 74, 66, 77, 84, 73, 52, + 83, 49, 99, 105, 76, 67, 74, 108, 98, 109, 77, 105, 79, 105, 74, 66, + 77, 84, 73, 52, 81, 48, 74, 68, 76, 85, 104, 84, 77, 106, 85, 50, 73, + 110, 48] as byte[] + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.3.6 + final static byte[] CIPHERTEXT = [40, 57, 83, 181, 119, 33, 133, 148, 198, 185, 243, 24, 152, 230, 6, + 75, 129, 223, 127, 19, 210, 82, 183, 230, 168, 33, 215, 104, 143, + 112, 56, 102] as byte[] + final static String encodedCiphertext = 'KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY' as String + + final static byte[] TAG = [83, 73, 191, 98, 104, 205, 211, 128, 201, 189, 199, 133, 32, 38, 194, 85] as byte[] + final static String encodedTag = 'U0m_YmjN04DJvceFICbCVQ' + + // defined in https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.3.7 + final static String COMPLETE_JWE = + 'eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.' + + '6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ.' + + 'AxY8DCtDaGlsbGljb3RoZQ.' + + 'KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.' + + 'U0m_YmjN04DJvceFICbCVQ' as String + + @Test + void test() { + //ensure our test constants are correctly copied and match the RFC values: + assertEquals PLAINTEXT, new String(PLAINTEXT_BYTES, StandardCharsets.UTF_8) + assertEquals PROT_HEADER_STRING, new String(decode(encodedHeader), StandardCharsets.UTF_8) + assertEquals encodedEncryptedCek, encode(ENCRYPTED_CEK_BYTES) + assertEquals encodedIv, encode(IV) + assertArrayEquals AAD_BYTES, encodedHeader.getBytes(StandardCharsets.US_ASCII) + assertArrayEquals CIPHERTEXT, decode(encodedCiphertext) + assertArrayEquals TAG, decode(encodedTag) + + //read the RFC Test JWK to get the private key for decrypting + SecretJwk jwk = Jwks.builder().putAll(KEK_VALUES).build() as SecretJwk + SecretKey kek = jwk.toKey() + + // test decryption per the RFC + Jwe jwe = Jwts.parserBuilder().decryptWith(kek).build().parsePlaintextJwe(COMPLETE_JWE) + assertEquals PLAINTEXT, jwe.getBody() + + // now ensure that when JJWT does the encryption (i.e. a compact value is produced from JJWT, not from the RFC text), + // that the resulting compact string is identical to the RFC as described in + // https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.3.8 : + + //ensure that the algorithm reflects the test harness values: + AeadAlgorithm enc = new HmacAesAeadAlgorithm(128) { + @Override + protected byte[] ensureInitializationVector(SecurityRequest request) { + return IV; + } + + @Override + SecretKey generateKey() { + return CEK; + } + } + + String compact = Jwts.jweBuilder() + .setPayload(PLAINTEXT) + .encryptWith(enc) + .withKeyFrom(kek, KeyAlgorithms.A128KW) + .compact() + + assertEquals COMPLETE_JWE, compact + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA1Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA1Test.groovy new file mode 100644 index 000000000..60b2f7c7b --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA1Test.groovy @@ -0,0 +1,67 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.EcPublicJwk +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPublicJwk +import org.junit.Test + +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPublicKey + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertTrue + +/** + * https://datatracker.ietf.org/doc/html/rfc7517#appendix-A.1 + */ +class RFC7517AppendixA1Test { + + private static final List> keys = [ + [ + "kty": "EC", + "crv": "P-256", + "x" : "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", + "y" : "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", + "use": "enc", + "kid": "1" + ], + [ + "kty": "RSA", + "n" : "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx" + + "4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMs" + + "tn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2" + + "QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbI" + + "SD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqb" + + "w0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw" as String, + "e" : "AQAB", + "alg": "RS256", + "kid": "2011-04-29" + ] + ] + + @Test + void test() { // asserts we can parse and verify RFC values + + def m = keys[0] + EcPublicJwk ecPubJwk = Jwks.builder().putAll(m).build() as EcPublicJwk + assertTrue ecPubJwk.toKey() instanceof ECPublicKey + assertEquals m.size(), ecPubJwk.size() + assertEquals m.kty, ecPubJwk.getType() + assertEquals m.crv, ecPubJwk.get('crv') + assertEquals m.x, ecPubJwk.get('x') + assertEquals m.y, ecPubJwk.get('y') + assertEquals m.use, ecPubJwk.getPublicKeyUse() + assertEquals m.kid, ecPubJwk.getId() + + m = keys[1] + RsaPublicJwk rsaPublicJwk = Jwks.builder().putAll(m).build() as RsaPublicJwk + assertTrue rsaPublicJwk.toKey() instanceof RSAPublicKey + assertEquals m.size(), rsaPublicJwk.size() + assertEquals m.kty, rsaPublicJwk.getType() + assertEquals m.n, rsaPublicJwk.get('n') + assertEquals m.e, rsaPublicJwk.get('e') + assertEquals m.alg, rsaPublicJwk.getAlgorithm() + assertEquals m.kid, rsaPublicJwk.getId() + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy new file mode 100644 index 000000000..19c8afa02 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy @@ -0,0 +1,116 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.EcPrivateJwk +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPrivateJwk +import org.junit.Test + +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.RSAPrivateCrtKey + +import static org.junit.Assert.* + +/** + * https://datatracker.ietf.org/doc/html/rfc7517#appendix-A.2 + */ +class RFC7517AppendixA2Test { + + private static final String ecEncode(int fieldSize, BigInteger coord) { + return AbstractEcJwkFactory.toOctetString(fieldSize, coord) + } + + private static final String rsaEncode(BigInteger i) { + return AbstractFamilyJwkFactory.encode(i) + } + + private static final List> keys = [ + [ + "kty": "EC", + "crv": "P-256", + "x" : "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", + "y" : "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", + "d" : "870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE", + "use": "enc", + "kid": "1" + ], + [ + "kty": "RSA", + "n" : "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4" + + "cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMst" + + "n64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2Q" + + "vzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbIS" + + "D08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw" + + "0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", + "e" : "AQAB", + "d" : "X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9" + + "M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqij" + + "wp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d" + + "_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBz" + + "nbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFz" + + "me1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q", + "p" : "83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPV" + + "nwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqV" + + "WlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs", + "q" : "3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyum" + + "qjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgx" + + "kIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk", + "dp" : "G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oim" + + "YwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_Nmtu" + + "YZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0", + "dq" : "s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUU" + + "vMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9" + + "GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk", + "qi" : "GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzg" + + "UIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rx" + + "yR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU", + "alg": "RS256", + "kid": "2011-04-29" + ] + ] + + @Test + void test() { // asserts we can parse and verify RFC values + + def m = keys[0] + def jwk = Jwks.builder().putAll(m).build() as EcPrivateJwk + def key = jwk.toKey() + int fieldSize = key.params.curve.field.fieldSize + assertTrue key instanceof ECPrivateKey + assertEquals m.size(), jwk.size() + assertEquals m.kty, jwk.getType() + assertEquals m.crv, jwk.get('crv') + assertEquals m.x, jwk.get('x') + assertEquals m.x, ecEncode(fieldSize, jwk.toPublicJwk().toKey().w.affineX) + assertEquals m.y, jwk.get('y') + assertEquals m.y, ecEncode(fieldSize, jwk.toPublicJwk().toKey().w.affineY) + assertEquals m.d, jwk.get('d') + assertEquals m.d, ecEncode(fieldSize, key.s) + assertEquals m.use, jwk.getPublicKeyUse() + assertEquals m.kid, jwk.getId() + + m = keys[1] + jwk = Jwks.builder().putAll(m).build() as RsaPrivateJwk + key = jwk.toKey() as RSAPrivateCrtKey + assertNotNull key + assertEquals m.size(), jwk.size() + assertEquals m.kty, jwk.getType() + assertEquals m.n, jwk.get('n') + assertEquals m.n, rsaEncode(key.modulus) + assertEquals m.e, jwk.get('e') + assertEquals m.e, rsaEncode(jwk.toPublicJwk().toKey().publicExponent) + assertEquals m.d, jwk.get('d') + assertEquals m.d, rsaEncode(key.privateExponent) + assertEquals m.p, jwk.get('p') + assertEquals m.p, rsaEncode(key.getPrimeP()) + assertEquals m.q, jwk.get('q') + assertEquals m.q, rsaEncode(key.getPrimeQ()) + assertEquals m.dp, jwk.get('dp') + assertEquals m.dp, rsaEncode(key.getPrimeExponentP()) + assertEquals m.dq, jwk.get('dq') + assertEquals m.dq, rsaEncode(key.getPrimeExponentQ()) + assertEquals m.qi, jwk.get('qi') + assertEquals m.qi, rsaEncode(key.getCrtCoefficient()) + assertEquals m.alg, jwk.getAlgorithm() + assertEquals m.kid, jwk.getId() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA3Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA3Test.groovy new file mode 100644 index 000000000..3bcf49840 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA3Test.groovy @@ -0,0 +1,58 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.SecretJwk +import org.junit.Test + +import javax.crypto.SecretKey + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNotNull + +/** + * https://datatracker.ietf.org/doc/html/rfc7517#appendix-A.3 + */ +class RFC7517AppendixA3Test { + + private static final String encode(SecretKey key) { + return Encoders.BASE64URL.encode(key.getEncoded()) + } + + private static final List> keys = [ + [ + "kty": "oct", + "alg": "A128KW", + "k" : "GawgguFyGrWKav7AX4VKUg" + ], + [ + "kty": "oct", + "k" : "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow", + "kid": "HMAC key used in JWS spec Appendix A.1 example" + ] + ] + + @Test + void test() { // asserts we can parse and verify RFC values + + def m = keys[0] + SecretJwk jwk = Jwks.builder().putAll(m).build() as SecretJwk + def key = jwk.toKey() as SecretKey + assertNotNull key + assertEquals m.size(), jwk.size() + assertEquals m.kty, jwk.getType() + assertEquals m.alg, jwk.getAlgorithm() + assertEquals m.k, jwk.get('k') + assertEquals m.k, encode(key) + + m = keys[1] + jwk = Jwks.builder().putAll(m).build() as SecretJwk + key = jwk.toKey() as SecretKey + assertNotNull key + assertEquals m.size(), jwk.size() + assertEquals m.kty, jwk.getType() + assertEquals m.k, jwk.get('k') + assertEquals m.k, encode(key) + assertEquals m.kid, jwk.getId() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixBTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixBTest.groovy new file mode 100644 index 000000000..60b69b890 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixBTest.groovy @@ -0,0 +1,67 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPublicJwk +import org.junit.Test + +import java.security.interfaces.RSAPublicKey + +import static org.junit.Assert.* + +/** + * https://datatracker.ietf.org/doc/html/rfc7517#appendix-B + */ +class RFC7517AppendixBTest { + + private static final Map jwkPairs = [ + "kty": "RSA", + "use": "sig", + "kid": "1b94c", + "n" : "vrjOfz9Ccdgx5nQudyhdoR17V-IubWMeOZCwX_jj0hgAsz2J_pqYW08" + + "PLbK_PdiVGKPrqzmDIsLI7sA25VEnHU1uCLNwBuUiCO11_-7dYbsr4iJmG0Q" + + "u2j8DsVyT1azpJC_NG84Ty5KKthuCaPod7iI7w0LK9orSMhBEwwZDCxTWq4a" + + "YWAchc8t-emd9qOvWtVMDC2BXksRngh6X5bUYLy6AyHKvj-nUy1wgzjYQDwH" + + "MTplCoLtU-o-8SNnZ1tmRoGE9uJkBLdh5gFENabWnU5m1ZqZPdwS-qo-meMv" + + "VfJb6jJVWRpl2SUtCnYG2C32qvbWbjZ_jBPD5eunqsIo1vQ", + "e" : "AQAB", + "x5c": [ + "MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCSqGSIb3DQEBBQUAMGIxCzAJB" + + "gNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYD" + + "VQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1" + + "wYmVsbDAeFw0xMzAyMjEyMzI5MTVaFw0xODA4MTQyMjI5MTVaMGIxCzAJBg" + + "NVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYDV" + + "QQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1w" + + "YmVsbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL64zn8/QnH" + + "YMeZ0LncoXaEde1fiLm1jHjmQsF/449IYALM9if6amFtPDy2yvz3YlRij66" + + "s5gyLCyO7ANuVRJx1NbgizcAblIgjtdf/u3WG7K+IiZhtELto/A7Fck9Ws6" + + "SQvzRvOE8uSirYbgmj6He4iO8NCyvaK0jIQRMMGQwsU1quGmFgHIXPLfnpn" + + "fajr1rVTAwtgV5LEZ4Iel+W1GC8ugMhyr4/p1MtcIM42EA8BzE6ZQqC7VPq" + + "PvEjZ2dbZkaBhPbiZAS3YeYBRDWm1p1OZtWamT3cEvqqPpnjL1XyW+oyVVk" + + "aZdklLQp2Btgt9qr21m42f4wTw+Xrp6rCKNb0CAwEAATANBgkqhkiG9w0BA" + + "QUFAAOCAQEAh8zGlfSlcI0o3rYDPBB07aXNswb4ECNIKG0CETTUxmXl9KUL" + + "+9gGlqCz5iWLOgWsnrcKcY0vXPG9J1r9AqBNTqNgHq2G03X09266X5CpOe1" + + "zFo+Owb1zxtp3PehFdfQJ610CDLEaS9V9Rqp17hCyybEpOGVwe8fnk+fbEL" + + "2Bo3UPGrpsHzUoaGpDftmWssZkhpBJKVMJyf/RuP2SmmaIzmnw9JiSlYhzo" + + "4tpzd5rFXhjRbg4zW9C+2qok+2+qDM1iJ684gPHMIY8aLWrdgQTxkumGmTq" + + "gawR+N5MDtdPTEQ0XfIBc2cJEUyMTY5MPvACWpkA6SdS4xSvdXK3IVfOWA==" + ] + ] + + @Test + void test() { + def m = jwkPairs + RsaPublicJwk jwk = Jwks.builder().putAll(m).build() as RsaPublicJwk + RSAPublicKey key = jwk.toKey() + assertNotNull key + assertEquals m.size(), jwk.size() + assertEquals m.kty, jwk.getType() + assertEquals m.use, jwk.getPublicKeyUse() + assertEquals m.kid, jwk.getId() + assertEquals m.n, AbstractFamilyJwkFactory.encode(key.getModulus()) + assertEquals m.e, AbstractFamilyJwkFactory.encode(key.getPublicExponent()) + def chain = jwk.getX509CertificateChain() + assertNotNull chain + assertFalse chain.isEmpty() + assertEquals 1, chain.size() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy new file mode 100644 index 000000000..9fdd8c44c --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy @@ -0,0 +1,327 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.Jwe +import io.jsonwebtoken.JweHeader +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.io.SerializationException +import io.jsonwebtoken.io.Serializer +import io.jsonwebtoken.security.KeyRequest +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.PbeKey +import io.jsonwebtoken.security.SecurityRequest +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.* + +/** + * https://datatracker.ietf.org/doc/html/rfc7517#appendix-C + */ +class RFC7517AppendixCTest { + + private static final String rfcString(String s) { + return s.replaceAll('[\\s]', '') + } + + // https://datatracker.ietf.org/doc/html/rfc7517#appendix-C.1 + private static final String RFC_JWK_JSON = rfcString(''' + { + "kty":"RSA", + "kid":"juliet@capulet.lit", + "use":"enc", + "n":"t6Q8PWSi1dkJj9hTP8hNYFlvadM7DflW9mWepOJhJ66w7nyoK1gPNqFMSQRy + O125Gp-TEkodhWr0iujjHVx7BcV0llS4w5ACGgPrcAd6ZcSR0-Iqom-QFcNP + 8Sjg086MwoqQU_LYywlAGZ21WSdS_PERyGFiNnj3QQlO8Yns5jCtLCRwLHL0 + Pb1fEv45AuRIuUfVcPySBWYnDyGxvjYGDSM-AqWS9zIQ2ZilgT-GqUmipg0X + OC0Cc20rgLe2ymLHjpHciCKVAbY5-L32-lSeZO-Os6U15_aXrk9Gw8cPUaX1 + _I8sLGuSiVdt3C_Fn2PZ3Z8i744FPFGGcG1qs2Wz-Q", + "e":"AQAB", + "d":"GRtbIQmhOZtyszfgKdg4u_N-R_mZGU_9k7JQ_jn1DnfTuMdSNprTeaSTyWfS + NkuaAwnOEbIQVy1IQbWVV25NY3ybc_IhUJtfri7bAXYEReWaCl3hdlPKXy9U + vqPYGR0kIXTQRqns-dVJ7jahlI7LyckrpTmrM8dWBo4_PMaenNnPiQgO0xnu + ToxutRZJfJvG4Ox4ka3GORQd9CsCZ2vsUDmsXOfUENOyMqADC6p1M3h33tsu + rY15k9qMSpG9OX_IJAXmxzAh_tWiZOwk2K4yxH9tS3Lq1yX8C1EWmeRDkK2a + hecG85-oLKQt5VEpWHKmjOi_gJSdSgqcN96X52esAQ", + "p":"2rnSOV4hKSN8sS4CgcQHFbs08XboFDqKum3sc4h3GRxrTmQdl1ZK9uw-PIHf + QP0FkxXVrx-WE-ZEbrqivH_2iCLUS7wAl6XvARt1KkIaUxPPSYB9yk31s0Q8 + UK96E3_OrADAYtAJs-M3JxCLfNgqh56HDnETTQhH3rCT5T3yJws", + "q":"1u_RiFDP7LBYh3N4GXLT9OpSKYP0uQZyiaZwBtOCBNJgQxaj10RWjsZu0c6I + edis4S7B_coSKB0Kj9PaPaBzg-IySRvvcQuPamQu66riMhjVtG6TlV8CLCYK + rYl52ziqK0E_ym2QnkwsUX7eYTB7LbAHRK9GqocDE5B0f808I4s", + "dp":"KkMTWqBUefVwZ2_Dbj1pPQqyHSHjj90L5x_MOzqYAJMcLMZtbUtwKqvVDq3 + tbEo3ZIcohbDtt6SbfmWzggabpQxNxuBpoOOf_a_HgMXK_lhqigI4y_kqS1w + Y52IwjUn5rgRrJ-yYo1h41KR-vz2pYhEAeYrhttWtxVqLCRViD6c", + "dq":"AvfS0-gRxvn0bwJoMSnFxYcK1WnuEjQFluMGfwGitQBWtfZ1Er7t1xDkbN9 + GQTB9yqpDoYaN06H7CFtrkxhJIBQaj6nkF5KKS3TQtQ5qCzkOkmxIe3KRbBy + mXxkb5qwUpX5ELD5xFc6FeiafWYY63TmmEAu_lRFCOJ3xDea-ots", + "qi":"lSQi-w9CpyUReMErP1RsBLk7wNtOvs5EQpPqmuMvqW57NBUczScEoPwmUqq + abu9V0-Py4dQ57_bapoKRu1R90bvuFnU63SHWEFglZQvJDMeAvmj4sm-Fp0o + Yu_neotgQ0hzbI5gry7ajdYy9-2lNx_76aBZoOUu9HCJ-UsfSOI8" + } + ''') + + private static final byte[] RFC_JWK_JSON_BYTES = + [123, 34, 107, 116, 121, 34, 58, 34, 82, 83, 65, 34, 44, 34, 107, + 105, 100, 34, 58, 34, 106, 117, 108, 105, 101, 116, 64, 99, 97, 112, + 117, 108, 101, 116, 46, 108, 105, 116, 34, 44, 34, 117, 115, 101, 34, + 58, 34, 101, 110, 99, 34, 44, 34, 110, 34, 58, 34, 116, 54, 81, 56, + 80, 87, 83, 105, 49, 100, 107, 74, 106, 57, 104, 84, 80, 56, 104, 78, + 89, 70, 108, 118, 97, 100, 77, 55, 68, 102, 108, 87, 57, 109, 87, + 101, 112, 79, 74, 104, 74, 54, 54, 119, 55, 110, 121, 111, 75, 49, + 103, 80, 78, 113, 70, 77, 83, 81, 82, 121, 79, 49, 50, 53, 71, 112, + 45, 84, 69, 107, 111, 100, 104, 87, 114, 48, 105, 117, 106, 106, 72, + 86, 120, 55, 66, 99, 86, 48, 108, 108, 83, 52, 119, 53, 65, 67, 71, + 103, 80, 114, 99, 65, 100, 54, 90, 99, 83, 82, 48, 45, 73, 113, 111, + 109, 45, 81, 70, 99, 78, 80, 56, 83, 106, 103, 48, 56, 54, 77, 119, + 111, 113, 81, 85, 95, 76, 89, 121, 119, 108, 65, 71, 90, 50, 49, 87, + 83, 100, 83, 95, 80, 69, 82, 121, 71, 70, 105, 78, 110, 106, 51, 81, + 81, 108, 79, 56, 89, 110, 115, 53, 106, 67, 116, 76, 67, 82, 119, 76, + 72, 76, 48, 80, 98, 49, 102, 69, 118, 52, 53, 65, 117, 82, 73, 117, + 85, 102, 86, 99, 80, 121, 83, 66, 87, 89, 110, 68, 121, 71, 120, 118, + 106, 89, 71, 68, 83, 77, 45, 65, 113, 87, 83, 57, 122, 73, 81, 50, + 90, 105, 108, 103, 84, 45, 71, 113, 85, 109, 105, 112, 103, 48, 88, + 79, 67, 48, 67, 99, 50, 48, 114, 103, 76, 101, 50, 121, 109, 76, 72, + 106, 112, 72, 99, 105, 67, 75, 86, 65, 98, 89, 53, 45, 76, 51, 50, + 45, 108, 83, 101, 90, 79, 45, 79, 115, 54, 85, 49, 53, 95, 97, 88, + 114, 107, 57, 71, 119, 56, 99, 80, 85, 97, 88, 49, 95, 73, 56, 115, + 76, 71, 117, 83, 105, 86, 100, 116, 51, 67, 95, 70, 110, 50, 80, 90, + 51, 90, 56, 105, 55, 52, 52, 70, 80, 70, 71, 71, 99, 71, 49, 113, + 115, 50, 87, 122, 45, 81, 34, 44, 34, 101, 34, 58, 34, 65, 81, 65, + 66, 34, 44, 34, 100, 34, 58, 34, 71, 82, 116, 98, 73, 81, 109, 104, + 79, 90, 116, 121, 115, 122, 102, 103, 75, 100, 103, 52, 117, 95, 78, + 45, 82, 95, 109, 90, 71, 85, 95, 57, 107, 55, 74, 81, 95, 106, 110, + 49, 68, 110, 102, 84, 117, 77, 100, 83, 78, 112, 114, 84, 101, 97, + 83, 84, 121, 87, 102, 83, 78, 107, 117, 97, 65, 119, 110, 79, 69, 98, + 73, 81, 86, 121, 49, 73, 81, 98, 87, 86, 86, 50, 53, 78, 89, 51, 121, + 98, 99, 95, 73, 104, 85, 74, 116, 102, 114, 105, 55, 98, 65, 88, 89, + 69, 82, 101, 87, 97, 67, 108, 51, 104, 100, 108, 80, 75, 88, 121, 57, + 85, 118, 113, 80, 89, 71, 82, 48, 107, 73, 88, 84, 81, 82, 113, 110, + 115, 45, 100, 86, 74, 55, 106, 97, 104, 108, 73, 55, 76, 121, 99, + 107, 114, 112, 84, 109, 114, 77, 56, 100, 87, 66, 111, 52, 95, 80, + 77, 97, 101, 110, 78, 110, 80, 105, 81, 103, 79, 48, 120, 110, 117, + 84, 111, 120, 117, 116, 82, 90, 74, 102, 74, 118, 71, 52, 79, 120, + 52, 107, 97, 51, 71, 79, 82, 81, 100, 57, 67, 115, 67, 90, 50, 118, + 115, 85, 68, 109, 115, 88, 79, 102, 85, 69, 78, 79, 121, 77, 113, 65, + 68, 67, 54, 112, 49, 77, 51, 104, 51, 51, 116, 115, 117, 114, 89, 49, + 53, 107, 57, 113, 77, 83, 112, 71, 57, 79, 88, 95, 73, 74, 65, 88, + 109, 120, 122, 65, 104, 95, 116, 87, 105, 90, 79, 119, 107, 50, 75, + 52, 121, 120, 72, 57, 116, 83, 51, 76, 113, 49, 121, 88, 56, 67, 49, + 69, 87, 109, 101, 82, 68, 107, 75, 50, 97, 104, 101, 99, 71, 56, 53, + 45, 111, 76, 75, 81, 116, 53, 86, 69, 112, 87, 72, 75, 109, 106, 79, + 105, 95, 103, 74, 83, 100, 83, 103, 113, 99, 78, 57, 54, 88, 53, 50, + 101, 115, 65, 81, 34, 44, 34, 112, 34, 58, 34, 50, 114, 110, 83, 79, + 86, 52, 104, 75, 83, 78, 56, 115, 83, 52, 67, 103, 99, 81, 72, 70, + 98, 115, 48, 56, 88, 98, 111, 70, 68, 113, 75, 117, 109, 51, 115, 99, + 52, 104, 51, 71, 82, 120, 114, 84, 109, 81, 100, 108, 49, 90, 75, 57, + 117, 119, 45, 80, 73, 72, 102, 81, 80, 48, 70, 107, 120, 88, 86, 114, + 120, 45, 87, 69, 45, 90, 69, 98, 114, 113, 105, 118, 72, 95, 50, 105, + 67, 76, 85, 83, 55, 119, 65, 108, 54, 88, 118, 65, 82, 116, 49, 75, + 107, 73, 97, 85, 120, 80, 80, 83, 89, 66, 57, 121, 107, 51, 49, 115, + 48, 81, 56, 85, 75, 57, 54, 69, 51, 95, 79, 114, 65, 68, 65, 89, 116, + 65, 74, 115, 45, 77, 51, 74, 120, 67, 76, 102, 78, 103, 113, 104, 53, + 54, 72, 68, 110, 69, 84, 84, 81, 104, 72, 51, 114, 67, 84, 53, 84, + 51, 121, 74, 119, 115, 34, 44, 34, 113, 34, 58, 34, 49, 117, 95, 82, + 105, 70, 68, 80, 55, 76, 66, 89, 104, 51, 78, 52, 71, 88, 76, 84, 57, + 79, 112, 83, 75, 89, 80, 48, 117, 81, 90, 121, 105, 97, 90, 119, 66, + 116, 79, 67, 66, 78, 74, 103, 81, 120, 97, 106, 49, 48, 82, 87, 106, + 115, 90, 117, 48, 99, 54, 73, 101, 100, 105, 115, 52, 83, 55, 66, 95, + 99, 111, 83, 75, 66, 48, 75, 106, 57, 80, 97, 80, 97, 66, 122, 103, + 45, 73, 121, 83, 82, 118, 118, 99, 81, 117, 80, 97, 109, 81, 117, 54, + 54, 114, 105, 77, 104, 106, 86, 116, 71, 54, 84, 108, 86, 56, 67, 76, + 67, 89, 75, 114, 89, 108, 53, 50, 122, 105, 113, 75, 48, 69, 95, 121, + 109, 50, 81, 110, 107, 119, 115, 85, 88, 55, 101, 89, 84, 66, 55, 76, + 98, 65, 72, 82, 75, 57, 71, 113, 111, 99, 68, 69, 53, 66, 48, 102, + 56, 48, 56, 73, 52, 115, 34, 44, 34, 100, 112, 34, 58, 34, 75, 107, + 77, 84, 87, 113, 66, 85, 101, 102, 86, 119, 90, 50, 95, 68, 98, 106, + 49, 112, 80, 81, 113, 121, 72, 83, 72, 106, 106, 57, 48, 76, 53, 120, + 95, 77, 79, 122, 113, 89, 65, 74, 77, 99, 76, 77, 90, 116, 98, 85, + 116, 119, 75, 113, 118, 86, 68, 113, 51, 116, 98, 69, 111, 51, 90, + 73, 99, 111, 104, 98, 68, 116, 116, 54, 83, 98, 102, 109, 87, 122, + 103, 103, 97, 98, 112, 81, 120, 78, 120, 117, 66, 112, 111, 79, 79, + 102, 95, 97, 95, 72, 103, 77, 88, 75, 95, 108, 104, 113, 105, 103, + 73, 52, 121, 95, 107, 113, 83, 49, 119, 89, 53, 50, 73, 119, 106, 85, + 110, 53, 114, 103, 82, 114, 74, 45, 121, 89, 111, 49, 104, 52, 49, + 75, 82, 45, 118, 122, 50, 112, 89, 104, 69, 65, 101, 89, 114, 104, + 116, 116, 87, 116, 120, 86, 113, 76, 67, 82, 86, 105, 68, 54, 99, 34, + 44, 34, 100, 113, 34, 58, 34, 65, 118, 102, 83, 48, 45, 103, 82, 120, + 118, 110, 48, 98, 119, 74, 111, 77, 83, 110, 70, 120, 89, 99, 75, 49, + 87, 110, 117, 69, 106, 81, 70, 108, 117, 77, 71, 102, 119, 71, 105, + 116, 81, 66, 87, 116, 102, 90, 49, 69, 114, 55, 116, 49, 120, 68, + 107, 98, 78, 57, 71, 81, 84, 66, 57, 121, 113, 112, 68, 111, 89, 97, + 78, 48, 54, 72, 55, 67, 70, 116, 114, 107, 120, 104, 74, 73, 66, 81, + 97, 106, 54, 110, 107, 70, 53, 75, 75, 83, 51, 84, 81, 116, 81, 53, + 113, 67, 122, 107, 79, 107, 109, 120, 73, 101, 51, 75, 82, 98, 66, + 121, 109, 88, 120, 107, 98, 53, 113, 119, 85, 112, 88, 53, 69, 76, + 68, 53, 120, 70, 99, 54, 70, 101, 105, 97, 102, 87, 89, 89, 54, 51, + 84, 109, 109, 69, 65, 117, 95, 108, 82, 70, 67, 79, 74, 51, 120, 68, + 101, 97, 45, 111, 116, 115, 34, 44, 34, 113, 105, 34, 58, 34, 108, + 83, 81, 105, 45, 119, 57, 67, 112, 121, 85, 82, 101, 77, 69, 114, 80, + 49, 82, 115, 66, 76, 107, 55, 119, 78, 116, 79, 118, 115, 53, 69, 81, + 112, 80, 113, 109, 117, 77, 118, 113, 87, 53, 55, 78, 66, 85, 99, + 122, 83, 99, 69, 111, 80, 119, 109, 85, 113, 113, 97, 98, 117, 57, + 86, 48, 45, 80, 121, 52, 100, 81, 53, 55, 95, 98, 97, 112, 111, 75, + 82, 117, 49, 82, 57, 48, 98, 118, 117, 70, 110, 85, 54, 51, 83, 72, + 87, 69, 70, 103, 108, 90, 81, 118, 74, 68, 77, 101, 65, 118, 109, + 106, 52, 115, 109, 45, 70, 112, 48, 111, 89, 117, 95, 110, 101, 111, + 116, 103, 81, 48, 104, 122, 98, 73, 53, 103, 114, 121, 55, 97, 106, + 100, 89, 121, 57, 45, 50, 108, 78, 120, 95, 55, 54, 97, 66, 90, 111, + 79, 85, 117, 57, 72, 67, 74, 45, 85, 115, 102, 83, 79, 73, 56, 34, + 125] as byte[] + + // https://datatracker.ietf.org/doc/html/rfc7517#appendix-C.2 + private static final String RFC_JWE_PROTECTED_HEADER_JSON = rfcString(''' + { + "alg":"PBES2-HS256+A128KW", + "p2s":"2WCTcJZ1Rvd_CJuJripQ1w", + "p2c":4096, + "enc":"A128CBC-HS256", + "cty":"jwk+json" + } + ''') + private static final byte[] RFC_P2S = [217, 96, 147, 112, 150, 117, 70, 247, 127, 8, 155, 137, 174, 42, 80, 215] as byte[] + private static final int RFC_P2C = 4096 + private static final String RFC_ENCODED_JWE_PROTECTED_HEADER = rfcString(''' + eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJwMnMiOiIyV0NUY0paMVJ2ZF9DSn + VKcmlwUTF3IiwicDJjIjo0MDk2LCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiY3R5Ijoi + andrK2pzb24ifQ + ''') + + // https://datatracker.ietf.org/doc/html/rfc7517#appendix-C.3 + private static final byte[] RFC_CEK_BYTES = [111, 27, 25, 52, 66, 29, 20, 78, 92, 176, 56, 240, 65, 208, 82, 112, + 161, 131, 36, 55, 202, 236, 185, 172, 129, 23, 153, 194, 195, 48, + 253, 182] as byte[] + private static final SecretKey RFC_CEK = new SecretKeySpec(RFC_CEK_BYTES, "AES") + + // https://datatracker.ietf.org/doc/html/rfc7517#appendix-C.4 + private static final String RFC_SHARED_PASSPHRASE = 'Thus from my lips, by yours, my sin is purged.' + private static final byte[] RFC_SHARED_PASSPHRASE_BYTES = [ + 84, 104, 117, 115, 32, 102, 114, 111, 109, 32, 109, 121, 32, 108, + 105, 112, 115, 44, 32, 98, 121, 32, 121, 111, 117, 114, 115, 44, 32, + 109, 121, 32, 115, 105, 110, 32, 105, 115, 32, 112, 117, 114, 103, + 101, 100, 46] as byte[] + + // "The Salt value (UTF8(Alg) || 0x00 || Salt Input) is": + private static final byte[] RFC_SALT_VALUE = [80, 66, 69, 83, 50, 45, 72, 83, 50, 53, 54, 43, 65, 49, 50, 56, 75, + 87, 0, 217, 96, 147, 112, 150, 117, 70, 247, 127, 8, 155, 137, 174, + 42, 80, 215] as byte[] + private static final byte[] RFC_PBKDF2_DERIVED_KEY_BYTES = + [110, 171, 169, 92, 129, 92, 109, 117, 233, 242, 116, 233, 170, 14, 24, 75] + + // https://datatracker.ietf.org/doc/html/rfc7517#appendix-C.6 + private static final byte[] RFC_IV = [97, 239, 99, 214, 171, 54, 216, 57, 145, 72, 7, 93, 34, 31, 149, 156] as byte[] + + // https://datatracker.ietf.org/doc/html/rfc7517#appendix-C.9 + private static final String RFC_COMPACT_JWE = rfcString(''' + eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJwMnMiOiIyV0NUY0paMVJ2ZF9DSn + VKcmlwUTF3IiwicDJjIjo0MDk2LCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiY3R5Ijoi + andrK2pzb24ifQ. + TrqXOwuNUfDV9VPTNbyGvEJ9JMjefAVn-TR1uIxR9p6hsRQh9Tk7BA. + Ye9j1qs22DmRSAddIh-VnA. + AwhB8lxrlKjFn02LGWEqg27H4Tg9fyZAbFv3p5ZicHpj64QyHC44qqlZ3JEmnZTgQo + wIqZJ13jbyHB8LgePiqUJ1hf6M2HPLgzw8L-mEeQ0jvDUTrE07NtOerBk8bwBQyZ6g + 0kQ3DEOIglfYxV8-FJvNBYwbqN1Bck6d_i7OtjSHV-8DIrp-3JcRIe05YKy3Oi34Z_ + GOiAc1EK21B11c_AE11PII_wvvtRiUiG8YofQXakWd1_O98Kap-UgmyWPfreUJ3lJP + nbD4Ve95owEfMGLOPflo2MnjaTDCwQokoJ_xplQ2vNPz8iguLcHBoKllyQFJL2mOWB + wqhBo9Oj-O800as5mmLsvQMTflIrIEbbTMzHMBZ8EFW9fWwwFu0DWQJGkMNhmBZQ-3 + lvqTc-M6-gWA6D8PDhONfP2Oib2HGizwG1iEaX8GRyUpfLuljCLIe1DkGOewhKuKkZ + h04DKNM5Nbugf2atmU9OP0Ldx5peCUtRG1gMVl7Qup5ZXHTjgPDr5b2N731UooCGAU + qHdgGhg0JVJ_ObCTdjsH4CF1SJsdUhrXvYx3HJh2Xd7CwJRzU_3Y1GxYU6-s3GFPbi + rfqqEipJDBTHpcoCmyrwYjYHFgnlqBZRotRrS95g8F95bRXqsaDY7UgQGwBQBwy665 + d0zpvTasvfXf_c0MWAl-neFaKOW_Px6g4EUDjG1GWSXV9cLStLw_0ovdApDIFLHYHe + PyagyHjouQUuGiq7BsYwYrwaF06tgB8hV8omLNfMEmDPJaZUzMuHw6tBDwGkzD-tS_ + ub9hxrpJ4UsOWnt5rGUyoN2N_c1-TQlXxm5oto14MxnoAyBQBpwIEgSH3Y4ZhwKBhH + PjSo0cdwuNdYbGPpb-YUvF-2NZzODiQ1OvWQBRHSbPWYz_xbGkgD504LRtqRwCO7CC + _CyyURi1sEssPVsMJRX_U4LFEOc82TiDdqjKOjRUfKK5rqLi8nBE9soQ0DSaOoFQZi + GrBrqxDsNYiAYAmxxkos-i3nX4qtByVx85sCE5U_0MqG7COxZWMOPEFrDaepUV-cOy + rvoUIng8i8ljKBKxETY2BgPegKBYCxsAUcAkKamSCC9AiBxA0UOHyhTqtlvMksO7AE + hNC2-YzPyx1FkhMoS4LLe6E_pFsMlmjA6P1NSge9C5G5tETYXGAn6b1xZbHtmwrPSc + ro9LWhVmAaA7_bxYObnFUxgWtK4vzzQBjZJ36UTk4OTB-JvKWgfVWCFsaw5WCHj6Oo + 4jpO7d2yN7WMfAj2hTEabz9wumQ0TMhBduZ-QON3pYObSy7TSC1vVme0NJrwF_cJRe + hKTFmdlXGVldPxZCplr7ZQqRQhF8JP-l4mEQVnCaWGn9ONHlemczGOS-A-wwtnmwjI + B1V_vgJRf4FdpV-4hUk4-QLpu3-1lWFxrtZKcggq3tWTduRo5_QebQbUUT_VSCgsFc + OmyWKoj56lbxthN19hq1XGWbLGfrrR6MWh23vk01zn8FVwi7uFwEnRYSafsnWLa1Z5 + TpBj9GvAdl2H9NHwzpB5NqHpZNkQ3NMDj13Fn8fzO0JB83Etbm_tnFQfcb13X3bJ15 + Cz-Ww1MGhvIpGGnMBT_ADp9xSIyAM9dQ1yeVXk-AIgWBUlN5uyWSGyCxp0cJwx7HxM + 38z0UIeBu-MytL-eqndM7LxytsVzCbjOTSVRmhYEMIzUAnS1gs7uMQAGRdgRIElTJE + SGMjb_4bZq9s6Ve1LKkSi0_QDsrABaLe55UY0zF4ZSfOV5PMyPtocwV_dcNPlxLgNA + D1BFX_Z9kAdMZQW6fAmsfFle0zAoMe4l9pMESH0JB4sJGdCKtQXj1cXNydDYozF7l8 + H00BV_Er7zd6VtIw0MxwkFCTatsv_R-GsBCH218RgVPsfYhwVuT8R4HarpzsDBufC4 + r8_c8fc9Z278sQ081jFjOja6L2x0N_ImzFNXU6xwO-Ska-QeuvYZ3X_L31ZOX4Llp- + 7QSfgDoHnOxFv1Xws-D5mDHD3zxOup2b2TppdKTZb9eW2vxUVviM8OI9atBfPKMGAO + v9omA-6vv5IxUH0-lWMiHLQ_g8vnswp-Jav0c4t6URVUzujNOoNd_CBGGVnHiJTCHl + 88LQxsqLHHIu4Fz-U2SGnlxGTj0-ihit2ELGRv4vO8E1BosTmf0cx3qgG0Pq0eOLBD + IHsrdZ_CCAiTc0HVkMbyq1M6qEhM-q5P6y1QCIrwg. + 0HFmhOzsQ98nNWJjIHkR7A +''') + + @Test + void test() { + + //ensure the bytes of the JSON string copied from the RFC matches the array definition in the RFC: + assertArrayEquals RFC_JWK_JSON_BYTES, RFC_JWK_JSON.getBytes(StandardCharsets.ISO_8859_1) + assertEquals RFC_ENCODED_JWE_PROTECTED_HEADER, Encoders.BASE64URL.encode(RFC_JWE_PROTECTED_HEADER_JSON.getBytes(StandardCharsets.UTF_8)) + assertArrayEquals RFC_SHARED_PASSPHRASE_BYTES, RFC_SHARED_PASSPHRASE.getBytes(StandardCharsets.UTF_8) + + //ensure that the KeyAlgorithm reflects test harness values: + def encAlg = new HmacAesAeadAlgorithm(128) { + @Override + SecretKey generateKey() { + return RFC_CEK; + } + + @Override + protected byte[] ensureInitializationVector(SecurityRequest request) { + return RFC_IV; + } + } + def keyAlg = new Pbes2HsAkwAlgorithm(128) { + @Override + protected byte[] generateInputSalt(KeyRequest request) { + return RFC_P2S; + } + } + def serializer = new Serializer() { + @Override + byte[] serialize(Object o) throws SerializationException { + assertTrue o instanceof JweHeader + JweHeader header = (JweHeader) o + + //assert the 5 values have been set per the RFC: + assertEquals 5, header.size() + assertEquals 'PBES2-HS256+A128KW', header.getAlgorithm() + assertEquals '2WCTcJZ1Rvd_CJuJripQ1w', header.p2s + assertEquals 4096, header.p2c + assertEquals 'A128CBC-HS256', header.getEncryptionAlgorithm() + assertEquals 'jwk+json', header.cty + + //JSON serialization order isn't guaranteed, so now that we've asserted the values are correct, + //return the exact serialization order expected in the RFC test: + return RFC_JWE_PROTECTED_HEADER_JSON.getBytes(StandardCharsets.UTF_8); + } + } + + PbeKey pbeKey = Keys.forPbe().setPassword(RFC_SHARED_PASSPHRASE.toCharArray()).setIterations(RFC_P2C).build() + + String compact = Jwts.jweBuilder() + .setPayload(RFC_JWK_JSON) + .setHeaderParam('cty', 'jwk+json') + .encryptWith(encAlg) + .withKeyFrom(pbeKey, keyAlg) + .serializeToJsonWith(serializer) //ensure JJWT created the header as expected with an assertion serializer + .compact(); + + assertEquals RFC_COMPACT_JWE, compact + + //ensure we can decrypt now: + Jwe jwe = Jwts.parserBuilder() + .decryptWith(new SecretKeySpec(RFC_SHARED_PASSPHRASE_BYTES, "RAW")) + .build() + .parsePlaintextJwe(compact) + + assertEquals RFC_JWK_JSON, jwe.getBody() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes128CbcHmacSha256Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB1Test.groovy similarity index 85% rename from impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes128CbcHmacSha256Test.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB1Test.groovy index 038618fd2..3b8623712 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes128CbcHmacSha256Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB1Test.groovy @@ -1,19 +1,21 @@ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.security.* + +import io.jsonwebtoken.security.EncryptionAlgorithms import org.junit.Test import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec import static org.junit.Assert.assertArrayEquals -import static org.junit.Assert.assertTrue /** - * Test case defined in https://tools.ietf.org/html/rfc7518#appendix-B.1 + * Tests successful encryption and decryption using 'AES_128_CBC_HMAC_SHA_256' as defined in + * RFC 7518, Appendix B.1 + * * @since JJWT_RELEASE_VERSION */ -class Aes128CbcHmacSha256Test { +class RFC7518AppendixB1Test { final byte[] K = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, @@ -68,16 +70,11 @@ class Aes128CbcHmacSha256Test { void test() { def alg = EncryptionAlgorithms.A128CBC_HS256 + def request = new DefaultAeadRequest(null, null, P, KEY, A, IV) + def result = alg.encrypt(request); - def request = new DefaultEncryptionRequest(P, KEY, null, null, IV, A) - - def r = alg.encrypt(request); - - assertTrue r instanceof AeadIvEncryptionResult - AeadIvEncryptionResult result = r as AeadIvEncryptionResult; - - byte[] ciphertext = result.getCiphertext() - byte[] tag = result.getAuthenticationTag() + byte[] ciphertext = result.getPayload() + byte[] tag = result.getDigest() byte[] iv = result.getInitializationVector() assertArrayEquals E, ciphertext @@ -85,11 +82,8 @@ class Aes128CbcHmacSha256Test { assertArrayEquals IV, iv //shouldn't have been altered // now test decryption: - - def dreq = new DefaultAeadIvRequest(ciphertext, KEY, null, null, iv, A, tag) - - byte[] decryptionResult = alg.decrypt(dreq) - + def dreq = new DefaultAeadResult(null, null, ciphertext, KEY, A, tag, iv) + byte[] decryptionResult = alg.decrypt(dreq).getPayload() assertArrayEquals(P, decryptionResult) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes192CbcHmacSha384Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB2Test.groovy similarity index 82% rename from impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes192CbcHmacSha384Test.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB2Test.groovy index f1d0f8711..aebb8ab6f 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes192CbcHmacSha384Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB2Test.groovy @@ -1,7 +1,7 @@ package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.security.AeadIvEncryptionResult +import io.jsonwebtoken.security.AeadRequest +import io.jsonwebtoken.security.AeadResult import io.jsonwebtoken.security.EncryptionAlgorithms import org.junit.Test @@ -9,13 +9,14 @@ import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec import static org.junit.Assert.assertArrayEquals -import static org.junit.Assert.assertTrue /** - * Test case defined in https://tools.ietf.org/html/rfc7518#appendix-B.2 + * Tests successful encryption and decryption using 'AES_192_CBC_HMAC_SHA_384' as defined in + * RFC 7518, Appendix B.2 + * * @since JJWT_RELEASE_VERSION */ -class Aes192CbcHmacSha384Test { +class RFC7518AppendixB2Test { final byte[] K = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, @@ -41,6 +42,7 @@ class Aes192CbcHmacSha384Test { 0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, 0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73] as byte[] + @SuppressWarnings('unused') final byte[] AL = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50] as byte[] final byte[] E = @@ -54,6 +56,7 @@ class Aes192CbcHmacSha384Test { 0xc8, 0xbb, 0x6c, 0x6b, 0x01, 0xd3, 0x5d, 0x49, 0x78, 0x7b, 0xcd, 0x57, 0xef, 0x48, 0x49, 0x27, 0xf2, 0x80, 0xad, 0xc9, 0x1a, 0xc0, 0xc4, 0xe7, 0x9c, 0x7b, 0x11, 0xef, 0xc6, 0x00, 0x54, 0xe3] as byte[] + @SuppressWarnings('unused') final byte[] M = [0x84, 0x90, 0xac, 0x0e, 0x58, 0x94, 0x9b, 0xfe, 0x51, 0x87, 0x5d, 0x73, 0x3f, 0x93, 0xac, 0x20, 0x75, 0x16, 0x80, 0x39, 0xcc, 0xc7, 0x33, 0xd7, 0x45, 0x94, 0xf8, 0x86, 0xb3, 0xfa, 0xaf, 0xd4, @@ -67,28 +70,20 @@ class Aes192CbcHmacSha384Test { void test() { def alg = EncryptionAlgorithms.A192CBC_HS384 + AeadRequest req = new DefaultAeadRequest(null, null, P, KEY, A, IV) + AeadResult result = alg.encrypt(req) - def req = new DefaultEncryptionRequest(P, KEY, null, null, IV, A); - - def r = alg.encrypt(req) - - assertTrue r instanceof AeadIvEncryptionResult - AeadIvEncryptionResult result = r as AeadIvEncryptionResult; - - byte[] resultCiphertext = result.getCiphertext() - byte[] resultTag = result.getAuthenticationTag(); - byte[] resultIv = result.getInitializationVector(); + byte[] resultCiphertext = result.getPayload() + byte[] resultTag = result.getDigest() + byte[] resultIv = result.getInitializationVector() assertArrayEquals E, resultCiphertext assertArrayEquals T, resultTag assertArrayEquals IV, resultIv //shouldn't have been altered // now test decryption: - - def dreq = new DefaultAeadIvRequest(resultCiphertext, KEY, null, null, resultIv, A, resultTag) - - byte[] decryptionResult = alg.decrypt(dreq) - - assertArrayEquals(P, decryptionResult); + def dreq = new DefaultAeadResult(null, null, resultCiphertext, KEY, A, resultTag, resultIv) + byte[] decryptionResult = alg.decrypt(dreq).getPayload() + assertArrayEquals(P, decryptionResult) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes256CbcHmacSha512Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB3Test.groovy similarity index 86% rename from impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes256CbcHmacSha512Test.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB3Test.groovy index 900a2d755..8d0e4500c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Aes256CbcHmacSha512Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB3Test.groovy @@ -1,7 +1,7 @@ package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.security.AeadIvEncryptionResult +import io.jsonwebtoken.security.AeadRequest +import io.jsonwebtoken.security.AeadResult import io.jsonwebtoken.security.EncryptionAlgorithms import org.junit.Test @@ -9,13 +9,14 @@ import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec import static org.junit.Assert.assertArrayEquals -import static org.junit.Assert.assertTrue /** - * Test case defined in https://tools.ietf.org/html/rfc7518#appendix-B.3 + * Tests successful encryption and decryption using 'AES_256_CBC_HMAC_SHA_512' as defined in + * RFC 7518, Appendix B.3 + * * @since JJWT_RELEASE_VERSION */ -class Aes256CbcHmacSha512Test { +class RFC7518AppendixB3Test { final byte[] K = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, @@ -69,16 +70,11 @@ class Aes256CbcHmacSha512Test { void test() { def alg = EncryptionAlgorithms.A256CBC_HS512 + AeadRequest req = new DefaultAeadRequest(null, null, P, KEY, A, IV) + AeadResult result = alg.encrypt(req) - def req = new DefaultEncryptionRequest(P, KEY, null, null, IV, A) - - def r = alg.encrypt(req) - - assertTrue r instanceof AeadIvEncryptionResult - AeadIvEncryptionResult result = r as AeadIvEncryptionResult; - - byte[] resultCiphertext = result.getCiphertext() - byte[] resultTag = result.getAuthenticationTag(); + byte[] resultCiphertext = result.getPayload() + byte[] resultTag = result.getDigest(); byte[] resultIv = result.getInitializationVector(); assertArrayEquals E, resultCiphertext @@ -86,8 +82,8 @@ class Aes256CbcHmacSha512Test { assertArrayEquals IV, resultIv //shouldn't have been altered // now test decryption: - def dreq = new DefaultAeadIvRequest(resultCiphertext, KEY, null, null, resultIv, A, resultTag) - byte[] decryptionResult = alg.decrypt(dreq) + def dreq = new DefaultAeadResult(null, null, resultCiphertext, KEY, A, resultTag, resultIv); + byte[] decryptionResult = alg.decrypt(dreq).getPayload() assertArrayEquals(P, decryptionResult); } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RandomsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RandomsTest.groovy index 23d65ac1e..63d9353dc 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RandomsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RandomsTest.groovy @@ -2,6 +2,10 @@ package io.jsonwebtoken.impl.security import org.junit.Test +import java.security.SecureRandom + +import static org.junit.Assert.assertTrue + /** * @since JJWT_RELEASE_VERSION */ @@ -11,4 +15,10 @@ class RandomsTest { void testPrivateCtor() { //for code coverage only new Randoms() } + + @Test + void testSecureRandom() { + def random = Randoms.secureRandom() + assertTrue random instanceof SecureRandom + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/SymmetricJwkValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/SymmetricJwkValidatorTest.groovy deleted file mode 100644 index c7d71cc9f..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/SymmetricJwkValidatorTest.groovy +++ /dev/null @@ -1,31 +0,0 @@ -package io.jsonwebtoken.impl.security - - -import io.jsonwebtoken.security.MalformedKeyException -import org.junit.Test - -class SymmetricJwkValidatorTest { - - static SymmetricJwkValidator validator() { - return new SymmetricJwkValidator() - } - - @Test(expected = MalformedKeyException) - void testNullK() { - def jwk = new DefaultSymmetricJwk() - validator().validate(jwk) - } - - @Test(expected = MalformedKeyException) - void testEmptyK() { - def jwk = new DefaultSymmetricJwk() - jwk.put('k', ' ') - validator().validate(jwk) - } - - @Test - void testValid() { - def jwk = new DefaultSymmetricJwk().setK('k') - validator().validate(jwk) - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwk.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwk.groovy deleted file mode 100644 index d118dc743..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwk.groovy +++ /dev/null @@ -1,7 +0,0 @@ -package io.jsonwebtoken.impl.security - -class TestJwk extends AbstractJwk { - def TestJwk() { - super("test") - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwkValidator.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwkValidator.groovy deleted file mode 100644 index 7eba2544c..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestJwkValidator.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.security.Jwk -import io.jsonwebtoken.security.KeyException - -class TestJwkValidator extends AbstractJwkValidator { - - T jwk; - - def TestJwkValidator(String kty="test") { - super(kty) - } - - @Override - void validateJwk(T jwk) throws KeyException { - this.jwk = jwk; - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy index 6d5c1ca59..845ea316d 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy @@ -1,9 +1,8 @@ package io.jsonwebtoken.security -import io.jsonwebtoken.impl.security.DefaultAeadIvRequest -import io.jsonwebtoken.impl.security.DefaultAesEncryptionRequest -import io.jsonwebtoken.impl.security.DefaultEncryptionRequest -import io.jsonwebtoken.impl.security.GcmAesEncryptionAlgorithm +import io.jsonwebtoken.impl.security.DefaultAeadRequest +import io.jsonwebtoken.impl.security.DefaultAeadResult +import io.jsonwebtoken.impl.security.GcmAesAeadAlgorithm import org.junit.Test import static org.junit.Assert.* @@ -42,60 +41,54 @@ class EncryptionAlgorithmsTest { @Test void testWithoutAad() { - for (EncryptionAlgorithm alg : EncryptionAlgorithms.symmetric()) { - - assert alg instanceof AeadSymmetricEncryptionAlgorithm + for (AeadAlgorithm alg : EncryptionAlgorithms.values()) { def key = alg.generateKey() - def request = new DefaultAesEncryptionRequest(PLAINTEXT_BYTES, key, null) + def request = new DefaultAeadRequest(PLAINTEXT_BYTES, key, null) def result = alg.encrypt(request) - assert result instanceof AeadIvEncryptionResult - byte[] tag = result.getAuthenticationTag() //there is always a tag, even if there is no AAD + byte[] tag = result.getDigest() //there is always a tag, even if there is no AAD assertNotNull tag - byte[] ciphertext = result.getCiphertext() + byte[] ciphertext = result.getPayload() - boolean gcm = alg instanceof GcmAesEncryptionAlgorithm + boolean gcm = alg instanceof GcmAesAeadAlgorithm if (gcm) { //AES GCM always results in ciphertext the same length as the plaintext: assertEquals(ciphertext.length, PLAINTEXT_BYTES.length) } - def dreq = new DefaultAeadIvRequest(result.getCiphertext(), key, null, null, result.getInitializationVector(), null, tag) + def dreq = new DefaultAeadResult(null, null, ciphertext, key, null, tag, result.getInitializationVector()) - byte[] decryptedPlaintextBytes = alg.decrypt(dreq) + byte[] decryptedPlaintextBytes = alg.decrypt(dreq).payload - assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes); + assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes) } } @Test void testWithAad() { - for (EncryptionAlgorithm alg : EncryptionAlgorithms.symmetric()) { - - assert alg instanceof AeadSymmetricEncryptionAlgorithm + for (AeadAlgorithm alg : EncryptionAlgorithms.values()) { def key = alg.generateKey() - def req = new DefaultEncryptionRequest(PLAINTEXT_BYTES, key, null, null, null, AAD_BYTES) + def req = new DefaultAeadRequest(null, null, PLAINTEXT_BYTES, key, AAD_BYTES) def result = alg.encrypt(req) - assert result instanceof AeadIvEncryptionResult - byte[] ciphertext = result.getCiphertext() + byte[] ciphertext = result.getPayload() - boolean gcm = alg instanceof GcmAesEncryptionAlgorithm + boolean gcm = alg instanceof GcmAesAeadAlgorithm if (gcm) { assertEquals(ciphertext.length, PLAINTEXT_BYTES.length) } - def dreq = new DefaultAeadIvRequest(result.getCiphertext(), key, null, null, result.getInitializationVector(), AAD_BYTES, result.getAuthenticationTag()) - byte[] decryptedPlaintextBytes = alg.decrypt(dreq) + def dreq = new DefaultAeadResult(null, null, result.getPayload(), key, AAD_BYTES, result.getDigest(), result.getInitializationVector()); + byte[] decryptedPlaintextBytes = alg.decrypt(dreq).getPayload() assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy new file mode 100644 index 000000000..8dac33ff7 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy @@ -0,0 +1,48 @@ +package io.jsonwebtoken.security + +import org.junit.Test + +import java.security.Key + +import static org.junit.Assert.* + +class KeyAlgorithmsTest { + + @Test + void testPrivateCtor() { //for code coverage only + new KeyAlgorithms() + } + + static boolean contains(KeyAlgorithm alg) { + return KeyAlgorithms.values().contains(alg) + } + + @Test + void testValues() { + assertEquals 13, KeyAlgorithms.values().size() + assertTrue(contains(KeyAlgorithms.DIRECT) && + contains(KeyAlgorithms.A128KW) && + contains(KeyAlgorithms.A192KW) && + contains(KeyAlgorithms.A256KW) && + contains(KeyAlgorithms.A128GCMKW) && + contains(KeyAlgorithms.A192GCMKW) && + contains(KeyAlgorithms.A256GCMKW) && + contains(KeyAlgorithms.PBES2_HS256_A128KW) && + contains(KeyAlgorithms.PBES2_HS384_A192KW) && + contains(KeyAlgorithms.PBES2_HS512_A256KW) && + contains(KeyAlgorithms.RSA1_5) && + contains(KeyAlgorithms.RSA_OAEP) && + contains(KeyAlgorithms.RSA_OAEP_256) + ) + } + + @Test + void testFindByExactId() { + assertSame KeyAlgorithms.A128KW, KeyAlgorithms.findById('A128KW') + } + + @Test + void testFindByIdCaseInsensitive() { + assertSame KeyAlgorithms.A128GCMKW, KeyAlgorithms.findById('a128GcMkW') + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy index 8e86fc2b3..427bbfea2 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy @@ -15,8 +15,8 @@ */ package io.jsonwebtoken.security -import io.jsonwebtoken.impl.security.EllipticCurveSignatureAlgorithm -import io.jsonwebtoken.impl.security.RsaSignatureAlgorithm +import io.jsonwebtoken.impl.security.DefaultEllipticCurveSignatureAlgorithm +import io.jsonwebtoken.impl.security.DefaultRsaSignatureAlgorithm import org.junit.Test import javax.crypto.SecretKey @@ -67,7 +67,7 @@ class KeysImplTest { @Test void testSecretKeyFor() { for (SignatureAlgorithm alg : SignatureAlgorithms.values()) { - if (alg instanceof SymmetricKeySignatureAlgorithm) { + if (alg instanceof SecretKeySignatureAlgorithm) { SecretKey key = alg.generateKey() assertEquals alg.minKeyLength, key.getEncoded().length * 8 //convert byte count to bit count assertEquals alg.jcaName, key.algorithm @@ -137,7 +137,7 @@ class KeysImplTest { for (SignatureAlgorithm alg : SignatureAlgorithms.values()) { - if (alg instanceof RsaSignatureAlgorithm) { + if (alg instanceof DefaultRsaSignatureAlgorithm) { KeyPair pair = alg.generateKeyPair() assertNotNull pair @@ -150,7 +150,7 @@ class KeysImplTest { assert priv instanceof RSAPrivateKey assertEquals alg.preferredKeyLength, priv.modulus.bitLength() - } else if (alg instanceof EllipticCurveSignatureAlgorithm) { + } else if (alg instanceof DefaultEllipticCurveSignatureAlgorithm) { KeyPair pair = alg.generateKeyPair() assertNotNull pair diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy index c01045953..859b508fe 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy @@ -1,8 +1,9 @@ package io.jsonwebtoken.security -import static org.junit.Assert.* import org.junit.Test +import static org.junit.Assert.assertSame + class SignatureAlgorithmsTest { @Test @@ -12,8 +13,8 @@ class SignatureAlgorithmsTest { @Test void testForNameCaseInsensitive() { - for(SignatureAlgorithm alg : SignatureAlgorithms.STANDARD_ALGORITHMS.values()) { - assertSame alg, SignatureAlgorithms.forName(alg.getName().toLowerCase()) + for(SignatureAlgorithm alg : SignatureAlgorithms.values()) { + assertSame alg, SignatureAlgorithms.forId(alg.getId().toLowerCase()) } } } diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS256.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.crt.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS256.crt.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.crt.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS256.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.key.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS256.key.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.key.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS384.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.crt.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS384.crt.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.crt.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS384.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.key.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS384.key.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.key.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS512.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.crt.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS512.crt.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.crt.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS512.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.key.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS512.key.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.key.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/README.md b/impl/src/test/resources/io/jsonwebtoken/impl/security/README.md similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/README.md rename to impl/src/test/resources/io/jsonwebtoken/impl/security/README.md diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS256.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.crt.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS256.crt.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.crt.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS256.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.key.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS256.key.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.key.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS384.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.crt.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS384.crt.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.crt.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS384.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.key.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS384.key.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.key.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS512.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.crt.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS512.crt.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.crt.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS512.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.key.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS512.key.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.key.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa2048.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.crt.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa2048.crt.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.crt.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa2048.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.key.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa2048.key.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.key.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa3072.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.crt.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa3072.crt.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.crt.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa3072.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.key.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa3072.key.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.key.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa4096.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.crt.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa4096.crt.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.crt.pem diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa4096.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.key.pem similarity index 100% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa4096.key.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.key.pem From 93b1783f5687e4762ac7b5fd6fd0a9d9a3cbeaf1 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 13 Oct 2021 12:41:41 -0700 Subject: [PATCH 03/75] cleanup after rebasing from master --- .../impl/DefaultJwtParserBuilder.java | 2 +- .../impl/crypto/RsaSignatureValidator.java | 69 ------------------- .../DeprecatedJwtParserTest.groovy | 1 - .../io/jsonwebtoken/JwtParserTest.groovy | 2 +- .../impl/DefaultJwtParserBuilderTest.groovy | 8 +-- 5 files changed, 4 insertions(+), 78 deletions(-) delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index cc64d3bbe..9976b9b81 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -295,7 +295,7 @@ public Key apply(Header header) { allowedClockSkewMillis, expectedClaims, base64UrlDecoder, - deserializer, + new JwtDeserializer<>(deserializer), compressionCodecResolver, extraSignatureAlgorithms, extraKeyAlgorithms, diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java deleted file mode 100644 index 0b4cfd024..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.SignatureException; - -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.MessageDigest; -import java.security.PublicKey; -import java.security.Signature; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; - -public class RsaSignatureValidator extends RsaProvider implements SignatureValidator { - - private final RsaSigner SIGNER; - - public RsaSignatureValidator(SignatureAlgorithm alg, Key key) { - super(alg, key); - Assert.isTrue(isRsaKey(key), "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance."); - this.SIGNER = key instanceof RSAPrivateKey ? new RsaSigner(alg, key) : null; - } - - protected static boolean isRsaKey(Key key) { - return key instanceof RSAPrivateKey || key instanceof RSAPublicKey; - } - - @Override - public boolean isValid(byte[] data, byte[] signature) { - if (key instanceof PublicKey) { - PublicKey publicKey = (PublicKey) key; - Signature sig = createSignatureInstance(); - try { - return doVerify(sig, publicKey, data, signature); - } catch (Exception e) { - String msg = "Unable to verify RSA signature using configured PublicKey. " + e.getMessage(); - throw new SignatureException(msg, e); - } - } else { - Assert.notNull(this.SIGNER, "RSA Signer instance cannot be null. This is a bug. Please report it."); - byte[] computed = this.SIGNER.sign(data); - return MessageDigest.isEqual(computed, signature); - } - } - - protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature) - throws InvalidKeyException, java.security.SignatureException { - sig.initVerify(publicKey); - sig.update(data); - return sig.verify(signature); - } - -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy index 04c3eeb81..9bfcc529b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy @@ -66,7 +66,6 @@ class DeprecatedJwtParserTest { Jwts.parser().parse(bad) fail() } catch (MalformedJwtException expected) { - assertEquals expected.getMessage(), 'Malformed JWT JSON: ' + junkPayload assertEquals 'Unable to read claims JSON: ' + junkPayload, expected.getMessage() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 139863b2a..38a72368b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -70,7 +70,7 @@ class JwtParserTest { Jwts.parserBuilder().build().parse(bad) fail() } catch (MalformedJwtException expected) { - assertEquals 'Unable to read claims JSON: ' + junkPayload, expected.getMessage() + assertEquals expected.getMessage(), 'Malformed JWT JSON: ' + junkPayload } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy index f230f57d9..d7cecd76b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy @@ -22,20 +22,16 @@ import io.jsonwebtoken.io.Decoder import io.jsonwebtoken.io.DecodingException import io.jsonwebtoken.io.DeserializationException import io.jsonwebtoken.io.Deserializer -import io.jsonwebtoken.security.Keys -import org.hamcrest.CoreMatchers -import org.junit.Test - -import static org.easymock.EasyMock.niceMock import io.jsonwebtoken.security.SignatureAlgorithms +import org.hamcrest.CoreMatchers import org.junit.Test import java.security.Provider import static org.easymock.EasyMock.* +import static org.hamcrest.MatcherAssert.assertThat import static org.junit.Assert.assertEquals import static org.junit.Assert.assertSame -import static org.hamcrest.MatcherAssert.assertThat // NOTE to the casual reader: even though this test class appears mostly empty, the DefaultJwtParserBuilder // implementation is tested to 100% coverage. The vast majority of its tests are in the JwtsTest class. This class From ac64d20d689d164bc70741d4c02f320c473921dd Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 13 Oct 2021 18:47:51 -0700 Subject: [PATCH 04/75] adding tests, working towards 100% coverage. Moved api static factory class tests to impl module to avoid mocking static calls due to bridge/reflection logic. --- .../java/io/jsonwebtoken/lang/Classes.java | 11 +- .../security/EncryptionAlgorithms.java | 9 +- .../security/InvalidKeyException.java | 7 + .../jsonwebtoken/security/KeyAlgorithms.java | 9 +- .../java/io/jsonwebtoken/security/Keys.java | 8 +- .../security/ProtoJwkBuilder.java | 10 + .../security/SignatureAlgorithms.java | 7 +- .../security/InvalidKeyExceptionTest.groovy | 25 +++ .../io/jsonwebtoken/security/KeysTest.groovy | 177 ------------------ .../security/MalformedKeyExceptionTest.groovy | 25 +++ .../java/io/jsonwebtoken/impl/io/Codecs.java | 10 - .../impl/security/DefaultProtoJwkBuilder.java | 50 ++++- .../SignatureAlgorithmTest.groovy | 0 .../impl/lang/CollectionConverterTest.groovy | 95 ++++++++++ .../impl/lang/ConvertersTest.groovy | 11 ++ .../impl/security/JwksTest.groovy | 37 ++++ .../security/EncryptionAlgorithmsTest.groovy | 43 ++++- .../security/KeyAlgorithmsTest.groovy | 48 ++++- .../{KeysImplTest.groovy => KeysTest.groovy} | 88 ++++++++- .../security/SignatureAlgorithmsTest.groovy | 39 +++- .../jsonwebtoken/impl/security/ES256.crt.pem | 12 ++ .../jsonwebtoken/impl/security/ES256.key.pem | 5 + .../jsonwebtoken/impl/security/ES384.crt.pem | 13 ++ .../jsonwebtoken/impl/security/ES384.key.pem | 6 + .../jsonwebtoken/impl/security/ES512.crt.pem | 15 ++ .../jsonwebtoken/impl/security/ES512.key.pem | 7 + .../io/jsonwebtoken/impl/security/README.md | 27 ++- 27 files changed, 563 insertions(+), 231 deletions(-) create mode 100644 api/src/test/groovy/io/jsonwebtoken/security/InvalidKeyExceptionTest.groovy delete mode 100644 api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy create mode 100644 api/src/test/groovy/io/jsonwebtoken/security/MalformedKeyExceptionTest.groovy delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/io/Codecs.java rename {api => impl}/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy (100%) create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/CollectionConverterTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/ConvertersTest.groovy rename impl/src/test/groovy/io/jsonwebtoken/security/{KeysImplTest.groovy => KeysTest.groovy} (66%) create mode 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.crt.pem create mode 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.key.pem create mode 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.crt.pem create mode 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.key.pem create mode 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.crt.pem create mode 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.key.pem diff --git a/api/src/main/java/io/jsonwebtoken/lang/Classes.java b/api/src/main/java/io/jsonwebtoken/lang/Classes.java index 544350007..c8405006b 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Classes.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Classes.java @@ -17,6 +17,7 @@ import java.io.InputStream; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** @@ -209,9 +210,13 @@ public static T invokeStatic(Class clazz, String methodName, Class[] a Method method = clazz.getDeclaredMethod(methodName, argTypes); method.setAccessible(true); return(T)method.invoke(null, args); - } catch (Exception e) { - String msg = "Unable to invoke class method " + clazz.getName() + "#" + methodName + ". Ensure the necessary " + - "implementation is in the runtime classpath."; + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) { + throw ((RuntimeException) cause); //propagate + } + String msg = "Unable to invoke class method " + clazz.getName() + "#" + methodName + + ". Ensure the necessary implementation is in the runtime classpath."; throw new IllegalStateException(msg, e); } } diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java index 393e5e718..27907dc59 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java @@ -30,10 +30,11 @@ private EncryptionAlgorithms() { } private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.EncryptionAlgorithmsBridge"; + private static final Class BRIDGE_CLASS = Classes.forName(BRIDGE_CLASSNAME); private static final Class[] ID_ARG_TYPES = new Class[]{String.class}; public static Collection values() { - return Classes.invokeStatic(BRIDGE_CLASSNAME, "values", null, (Object[]) null); + return Classes.invokeStatic(BRIDGE_CLASS, "values", null, (Object[]) null); } /** @@ -47,12 +48,12 @@ public static Collection values() { */ public static AeadAlgorithm findById(String id) { Assert.hasText(id, "id cannot be null or empty."); - return Classes.invokeStatic(BRIDGE_CLASSNAME, "findById", ID_ARG_TYPES, id); + return Classes.invokeStatic(BRIDGE_CLASS, "findById", ID_ARG_TYPES, id); } - private static AeadAlgorithm forId(String id) { + public static AeadAlgorithm forId(String id) { Assert.hasText(id, "id cannot be null or empty."); - return Classes.invokeStatic(BRIDGE_CLASSNAME, "forId", ID_ARG_TYPES, id); + return Classes.invokeStatic(BRIDGE_CLASS, "forId", ID_ARG_TYPES, id); } /** diff --git a/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java b/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java index 5d6b4183d..6fd7a585d 100644 --- a/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java @@ -24,6 +24,13 @@ public InvalidKeyException(String message) { super(message); } + /** + * Creates a new {@code InvalidKeyException} with the specified message and cause. + * + * @param msg exception message + * @param cause triggering cause for the InvalidKeyException + * @since JJWT_RELEASE_VERSION + */ public InvalidKeyException(String msg, Exception cause) { super(msg, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index 71ea9eee4..7d611a07c 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -32,11 +32,12 @@ private KeyAlgorithms() { } private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.KeyAlgorithmsBridge"; + private static final Class BRIDGE_CLASS = Classes.forName(BRIDGE_CLASSNAME); private static final Class[] ID_ARG_TYPES = new Class[]{String.class}; private static final Class[] ESTIMATE_ITERATIONS_ARG_TYPES = new Class[]{KeyAlgorithm.class, long.class}; public static Collection> values() { - return Classes.invokeStatic(BRIDGE_CLASSNAME, "values", null, (Object[]) null); + return Classes.invokeStatic(BRIDGE_CLASS, "values", null, (Object[]) null); } /** @@ -50,7 +51,7 @@ private KeyAlgorithms() { */ public static KeyAlgorithm findById(String id) { Assert.hasText(id, "id cannot be null or empty."); - return Classes.invokeStatic(BRIDGE_CLASSNAME, "findById", ID_ARG_TYPES, id); + return Classes.invokeStatic(BRIDGE_CLASS, "findById", ID_ARG_TYPES, id); } public static KeyAlgorithm forId(String id) { @@ -60,7 +61,7 @@ private KeyAlgorithms() { // do not change this visibility. Raw type method signature not be publicly exposed private static T forId0(String id) { Assert.hasText(id, "id cannot be null or empty."); - return Classes.invokeStatic(BRIDGE_CLASSNAME, "forId", ID_ARG_TYPES, id); + return Classes.invokeStatic(BRIDGE_CLASS, "forId", ID_ARG_TYPES, id); } public static final KeyAlgorithm DIRECT = forId0("dir"); @@ -78,6 +79,6 @@ private static T forId0(String id) { public static final RsaKeyAlgorithm RSA_OAEP_256 = forId0("RSA-OAEP-256"); public static int estimateIterations(KeyAlgorithm alg, long desiredMillis) { - return Classes.invokeStatic(BRIDGE_CLASSNAME, "estimateIterations", ESTIMATE_ITERATIONS_ARG_TYPES, alg, desiredMillis); + return Classes.invokeStatic(BRIDGE_CLASS, "estimateIterations", ESTIMATE_ITERATIONS_ARG_TYPES, alg, desiredMillis); } } diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index 9347e68da..038aeeb2b 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -31,6 +31,7 @@ public final class Keys { private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.KeysBridge"; + private static final Class BRIDGE_CLASS = Classes.forName(BRIDGE_CLASSNAME); @SuppressWarnings("rawtypes") private static final Class[] TO_PBE_ARG_TYPES = new Class[]{PBEKey.class}; @@ -235,14 +236,15 @@ public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws * @since JJWT_RELEASE_VERSION */ public static PbeKey toPbeKey(PBEKey key) { - return Classes.invokeStatic(BRIDGE_CLASSNAME, "toPbeKey", TO_PBE_ARG_TYPES, new Object[]{key}); + return Classes.invokeStatic(BRIDGE_CLASS, "toPbeKey", TO_PBE_ARG_TYPES, new Object[]{key}); } /** * Returns a new {@link PbeKeyBuilder} to use to construct a {@link PbeKey} instance. - * @return + * + * @return a new {@link PbeKeyBuilder} to use to construct a {@link PbeKey} instance. */ public static PbeKeyBuilder forPbe() { - return Classes.invokeStatic(BRIDGE_CLASSNAME, "forPbe", null, (Object[]) null); + return Classes.invokeStatic(BRIDGE_CLASS, "forPbe", null, (Object[]) null); } } diff --git a/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java index 209aa766e..129aafb1e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java @@ -18,10 +18,12 @@ import javax.crypto.SecretKey; import java.security.Key; import java.security.KeyPair; +import java.security.cert.X509Certificate; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.util.List; /** * @since JJWT_RELEASE_VERSION @@ -32,10 +34,18 @@ public interface ProtoJwkBuilder, T extends JwkB RsaPublicJwkBuilder setKey(RSAPublicKey key); + RsaPublicJwkBuilder forRsaChain(X509Certificate... chain); + + RsaPublicJwkBuilder forRsaChain(List x509CertificateChain); + RsaPrivateJwkBuilder setKey(RSAPrivateKey key); EcPublicJwkBuilder setKey(ECPublicKey key); + EcPublicJwkBuilder forEcChain(X509Certificate... chain); + + EcPublicJwkBuilder forEcChain(List chain); + EcPrivateJwkBuilder setKey(ECPrivateKey key); RsaPrivateJwkBuilder setKeyPairRsa(KeyPair keyPair); diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java index 4d9944825..5ff1e421e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java @@ -36,15 +36,16 @@ private SignatureAlgorithms() { } private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.SignatureAlgorithmsBridge"; + private static final Class BRIDGE_CLASS = Classes.forName(BRIDGE_CLASSNAME); private static final Class[] ID_ARG_TYPES = new Class[]{String.class}; public static Collection> values() { - return Classes.invokeStatic(BRIDGE_CLASSNAME, "values", null, (Object[]) null); + return Classes.invokeStatic(BRIDGE_CLASS, "values", null, (Object[]) null); } public static SignatureAlgorithm findById(String id) { Assert.hasText(id, "id cannot be null or empty."); - return Classes.invokeStatic(BRIDGE_CLASSNAME, "findById", ID_ARG_TYPES, id); + return Classes.invokeStatic(BRIDGE_CLASS, "findById", ID_ARG_TYPES, id); } public static SignatureAlgorithm forId(String id) { @@ -53,7 +54,7 @@ public static SignatureAlgorithm forId(String id) { static T forId0(String id) { Assert.hasText(id, "id cannot be null or empty."); - return Classes.invokeStatic(BRIDGE_CLASSNAME, "forId", ID_ARG_TYPES, id); + return Classes.invokeStatic(BRIDGE_CLASS, "forId", ID_ARG_TYPES, id); } public static final SignatureAlgorithm NONE = forId0("none"); diff --git a/api/src/test/groovy/io/jsonwebtoken/security/InvalidKeyExceptionTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/InvalidKeyExceptionTest.groovy new file mode 100644 index 000000000..a92fa5e73 --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/security/InvalidKeyExceptionTest.groovy @@ -0,0 +1,25 @@ +package io.jsonwebtoken.security + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class InvalidKeyExceptionTest { + + @Test + void testDefaultConstructor() { + def msg = "my message" + def exception = new InvalidKeyException(msg) + assertEquals msg, exception.getMessage() + } + + @Test + void testConstructorWithCause() { + def rootMsg = 'root error' + def msg = 'wrapping' + def ioException = new IOException(rootMsg) + def exception = new InvalidKeyException(msg, ioException) + assertEquals msg, exception.getMessage() + assertEquals ioException, exception.getCause() + } +} diff --git a/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy deleted file mode 100644 index f877fcc3c..000000000 --- a/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.security - -import io.jsonwebtoken.SignatureAlgorithm -import org.junit.Test -import org.junit.runner.RunWith -import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor -import org.powermock.modules.junit4.PowerMockRunner - -import javax.crypto.SecretKey -import javax.crypto.spec.SecretKeySpec -import java.security.KeyPair -import java.security.SecureRandom - -import static org.easymock.EasyMock.eq -import static org.easymock.EasyMock.expect -import static org.junit.Assert.* -import static org.powermock.api.easymock.PowerMock.* - -/** - * This test class is for cursory API-level testing only (what is available to the API module at build time). - * - * The actual implementation assertions are done in KeysImplTest in the impl module. - */ -@RunWith(PowerMockRunner) -@PrepareForTest([SignatureAlgorithms, Keys]) -@SuppressStaticInitializationFor("io.jsonwebtoken.security.SignatureAlgorithms") -class KeysTest { - - private static final Random RANDOM = new SecureRandom() - - static byte[] bytes(int sizeInBits) { - byte[] bytes = new byte[sizeInBits / Byte.SIZE] - RANDOM.nextBytes(bytes) - return bytes - } - - @Test - void testPrivateCtor() { //for code coverage only - new Keys() - } - - @Test - void testHmacShaKeyForWithNullArgument() { - try { - Keys.hmacShaKeyFor(null) - } catch (InvalidKeyException expected) { - assertEquals 'SecretKey byte array cannot be null.', expected.message - } - } - - @Test - void testHmacShaKeyForWithWeakKey() { - int numBytes = 31 - int numBits = numBytes * 8 - try { - Keys.hmacShaKeyFor(new byte[numBytes]) - } catch (WeakKeyException expected) { - assertEquals "The specified key byte array is " + numBits + " bits which " + - "is not secure enough for any JWT HMAC-SHA algorithm. The JWT " + - "JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " + - "size >= 256 bits (the key size must be greater than or equal to the hash " + - "output size). Consider using the SignatureAlgorithms.HS256.generateKey() method (or " + - "HS384.generateKey() or HS512.generateKey()) to create a key guaranteed to be secure enough " + - "for your preferred HMAC-SHA algorithm. See " + - "https://tools.ietf.org/html/rfc7518#section-3.2 for more information." as String, expected.message - } - } - - @Test - void testHmacShaWithValidSizes() { - for (int i : [256, 384, 512]) { - byte[] bytes = bytes(i) - def key = Keys.hmacShaKeyFor(bytes) - assertTrue key instanceof SecretKeySpec - assertEquals "HmacSHA$i" as String, key.getAlgorithm() - assertTrue Arrays.equals(bytes, key.getEncoded()) - } - } - - @Test - void testHmacShaLargerThan512() { - def key = Keys.hmacShaKeyFor(bytes(520)) - assertTrue key instanceof SecretKeySpec - assertEquals 'HmacSHA512', key.getAlgorithm() - assertTrue key.getEncoded().length * Byte.SIZE >= 512 - } - - @Test - void testSecretKeyFor() { - mockStatic(SignatureAlgorithms) - - for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { - - String name = alg.name() - - if (name.startsWith('H')) { - - def key = createMock(SecretKey) - def salg = createMock(SecretKeySignatureAlgorithm) - - expect(SignatureAlgorithms.forId(eq(name))).andReturn(salg) - expect(salg.generateKey()).andReturn(key) - replay SignatureAlgorithms, salg, key - - assertSame key, Keys.secretKeyFor(alg) - - verify SignatureAlgorithms, salg, key - reset SignatureAlgorithms, salg, key - - } else { - def salg = name == 'NONE' ? createMock(io.jsonwebtoken.security.SignatureAlgorithm) : createMock(AsymmetricKeySignatureAlgorithm) - expect(SignatureAlgorithms.forId(eq(name))).andReturn(salg) - replay SignatureAlgorithms, salg - try { - Keys.secretKeyFor(alg) - fail() - } catch (IllegalArgumentException expected) { - assertEquals "The $name algorithm does not support shared secret keys." as String, expected.message - } - verify SignatureAlgorithms, salg - reset SignatureAlgorithms, salg - } - } - } - - @Test - void testKeyPairFor() { - mockStatic SignatureAlgorithms - - for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { - - String name = alg.name() - - if (name.equals('NONE') || name.startsWith('H')) { - def salg = name == 'NONE' ? createMock(io.jsonwebtoken.security.SignatureAlgorithm) : createMock(SecretKeySignatureAlgorithm) - expect(SignatureAlgorithms.forId(eq(name))).andReturn(salg) - replay SignatureAlgorithms, salg - try { - Keys.keyPairFor(alg) - fail() - } catch (IllegalArgumentException expected) { - assertEquals "The $name algorithm does not support Key Pairs." as String, expected.message - } - verify SignatureAlgorithms, salg - reset SignatureAlgorithms, salg - } else { - def pair = createMock(KeyPair) - def salg = createMock(AsymmetricKeySignatureAlgorithm) - - expect(SignatureAlgorithms.forId(eq(name))).andReturn(salg) - expect(salg.generateKeyPair()).andReturn(pair) - replay SignatureAlgorithms, pair, salg - - assertSame pair, Keys.keyPairFor(alg) - - verify SignatureAlgorithms, pair, salg - reset SignatureAlgorithms, pair, salg - } - } - } -} diff --git a/api/src/test/groovy/io/jsonwebtoken/security/MalformedKeyExceptionTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/MalformedKeyExceptionTest.groovy new file mode 100644 index 000000000..c72a3b5ac --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/security/MalformedKeyExceptionTest.groovy @@ -0,0 +1,25 @@ +package io.jsonwebtoken.security + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class MalformedKeyExceptionTest { + + @Test + void testDefaultConstructor() { + def msg = "my message" + def exception = new MalformedKeyException(msg) + assertEquals msg, exception.getMessage() + } + + @Test + void testConstructorWithCause() { + def rootMsg = 'root error' + def msg = 'wrapping' + def ioException = new IOException(rootMsg) + def exception = new MalformedKeyException(msg, ioException) + assertEquals msg, exception.getMessage() + assertEquals ioException, exception.getCause() + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/Codecs.java b/impl/src/main/java/io/jsonwebtoken/impl/io/Codecs.java deleted file mode 100644 index a8023486f..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/Codecs.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.jsonwebtoken.impl.io; - -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.io.Encoders; - -public class Codecs { - - public static final CodecConverter BASE64 = new CodecConverter<>(Encoders.BASE64, Decoders.BASE64); - public static final CodecConverter BASE64URL = new CodecConverter<>(Encoders.BASE64URL, Decoders.BASE64URL); -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java index 4827d108c..1b142f259 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java @@ -1,7 +1,7 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.EcPrivateJwkBuilder; import io.jsonwebtoken.security.EcPublicJwkBuilder; import io.jsonwebtoken.security.Jwk; @@ -15,11 +15,13 @@ import java.security.Key; import java.security.KeyPair; import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; -import java.util.Set; +import java.util.List; public class DefaultProtoJwkBuilder, T extends JwkBuilder> extends AbstractJwkBuilder implements ProtoJwkBuilder { @@ -38,6 +40,36 @@ public RsaPublicJwkBuilder setKey(RSAPublicKey key) { return new AbstractAsymmetricJwkBuilder.DefaultRsaPublicJwkBuilder(this.jwkContext, key); } + @Override + public RsaPublicJwkBuilder forRsaChain(X509Certificate... chain) { + Assert.notEmpty(chain, "chain cannot be null or empty."); + return forRsaChain(Arrays.asList(chain)); + } + + @Override + public RsaPublicJwkBuilder forRsaChain(List chain) { + Assert.notEmpty(chain, "X509Certificate chain cannot be empty."); + X509Certificate cert = chain.get(0); + PublicKey key = Assert.notNull(cert.getPublicKey(), "The first X509Certificate's PublicKey cannot be null."); + RSAPublicKey pubKey = assertChildKey(RSAPublicKey.class, key, "first X509Certificate's"); + return setKey(pubKey).setX509CertificateChain(chain); + } + + @Override + public EcPublicJwkBuilder forEcChain(X509Certificate... chain) { + Assert.notEmpty(chain, "chain cannot be null or empty."); + return forEcChain(Arrays.asList(chain)); + } + + @Override + public EcPublicJwkBuilder forEcChain(List chain) { + Assert.notEmpty(chain, "X509Certificate chain cannot be empty."); + X509Certificate cert = chain.get(0); + PublicKey key = Assert.notNull(cert.getPublicKey(), "The first X509Certificate's PublicKey cannot be null."); + ECPublicKey pubKey = assertChildKey(ECPublicKey.class, key, "first X509Certificate's"); + return setKey(pubKey).setX509CertificateChain(chain); + } + @Override public RsaPrivateJwkBuilder setKey(RSAPrivateKey key) { return new AbstractAsymmetricJwkBuilder.DefaultRsaPrivateJwkBuilder(this.jwkContext, key); @@ -53,14 +85,14 @@ public EcPrivateJwkBuilder setKey(ECPrivateKey key) { return new AbstractAsymmetricJwkBuilder.DefaultEcPrivateJwkBuilder(this.jwkContext, key); } - private static T assertKeyPairChild(Class clazz, Key key) { + private static T assertChildKey(Class clazz, Key key, String parentName) { String type = PrivateKey.class.isAssignableFrom(clazz) ? "private" : "public"; if (key == null) { - String msg = "KeyPair " + type + " key cannot be null."; + String msg = "The " + parentName + " " + type + " key cannot be null."; throw new IllegalArgumentException(msg); } if (!clazz.isInstance(key)) { - String msg = "The specified KeyPair's " + type + " key must be an instance of " + clazz.getName() + + String msg = "The " + parentName + " " + type + " key must be an instance of " + clazz.getName() + ". Type found: " + key.getClass().getName(); throw new IllegalArgumentException(msg); } @@ -70,16 +102,16 @@ private static T assertKeyPairChild(Class clazz, Key key) { @Override public RsaPrivateJwkBuilder setKeyPairRsa(KeyPair keyPair) { Assert.notNull(keyPair, "KeyPair cannot be null."); - RSAPublicKey pub = assertKeyPairChild(RSAPublicKey.class, keyPair.getPublic()); - RSAPrivateKey priv = assertKeyPairChild(RSAPrivateKey.class, keyPair.getPrivate()); + RSAPublicKey pub = assertChildKey(RSAPublicKey.class, keyPair.getPublic(), "KeyPair"); + RSAPrivateKey priv = assertChildKey(RSAPrivateKey.class, keyPair.getPrivate(), "KeyPair"); return setKey(priv).setPublicKey(pub); } @Override public EcPrivateJwkBuilder setKeyPairEc(KeyPair keyPair) { Assert.notNull(keyPair, "KeyPair cannot be null."); - ECPublicKey pub = assertKeyPairChild(ECPublicKey.class, keyPair.getPublic()); - ECPrivateKey priv = assertKeyPairChild(ECPrivateKey.class, keyPair.getPrivate()); + ECPublicKey pub = assertChildKey(ECPublicKey.class, keyPair.getPublic(), "KeyPair"); + ECPrivateKey priv = assertChildKey(ECPrivateKey.class, keyPair.getPrivate(), "KeyPair"); return setKey(priv).setPublicKey(pub); } } diff --git a/api/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy similarity index 100% rename from api/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/CollectionConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/CollectionConverterTest.groovy new file mode 100644 index 000000000..55c7de005 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/CollectionConverterTest.groovy @@ -0,0 +1,95 @@ +package io.jsonwebtoken.impl.lang + +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Encoders +import org.junit.Test + +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.* + +class CollectionConverterTest { + + private static final UriStringConverter ELEMENT_CONVERTER = new UriStringConverter(); //any will do + + @Test + void testApplyToNull() { + assertNull Converters.forSet(ELEMENT_CONVERTER).applyTo(null) + assertNull Converters.forList(ELEMENT_CONVERTER).applyTo(null) + } + + @Test + void testApplyToEmpty() { + def set = [] as Set + assertSame set, Converters.forSet(ELEMENT_CONVERTER).applyTo(set) + def list = [] as List + assertSame list, Converters.forList(ELEMENT_CONVERTER).applyTo(list) + } + + @Test + void testApplyFromNull() { + assertNull Converters.forSet(ELEMENT_CONVERTER).applyFrom(null) + assertNull Converters.forList(ELEMENT_CONVERTER).applyFrom(null) + } + + @Test + void testApplyFromEmpty() { + def set = Converters.forSet(ELEMENT_CONVERTER).applyFrom([] as Set) + assertNotNull set + assertTrue set.isEmpty() + def list = Converters.forList(ELEMENT_CONVERTER).applyFrom([]) + assertNotNull list + assertTrue list.isEmpty() + } + + @Test + void testApplyFromNonPrimitiveArray() { + + String url = 'https://github.com/jwtk/jjwt' + URI uri = ELEMENT_CONVERTER.applyFrom(url) + def array = [url] as String[] + + def set = Converters.forSet(ELEMENT_CONVERTER).applyFrom(array) + assertNotNull set + assertEquals 1, set.size() + assertEquals uri, set.iterator().next() + + def list = Converters.forList(ELEMENT_CONVERTER).applyFrom(array) + assertNotNull list + assertEquals 1, list.size() + assertEquals uri, set.iterator().next() + } + + @Test + void testApplyFromPrimitiveArray() { + + // ensure the primitive array is not converted to a collection. That is, + // a byte array of length 4 should not return a collection of size 4. It should return a collection of size 1 + // and that element is the byte array + + Converter converter = new Converter() { + @Override + Object applyTo(String s) { + return Decoders.BASE64URL.decode(s); + } + + @Override + String applyFrom(Object o) { + return Encoders.BASE64URL.encode((byte[]) o); + } + } + + byte[] bytes = "1234".getBytes(StandardCharsets.UTF_8) + String s = converter.applyFrom(bytes) + + def set = Converters.forSet(converter).applyFrom(bytes) + assertNotNull set + assertEquals 1, set.size() + assertEquals s, set.iterator().next() + + def list = Converters.forList(converter).applyFrom(bytes) + assertNotNull list + assertEquals 1, list.size() + assertEquals s, set.iterator().next() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/ConvertersTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/ConvertersTest.groovy new file mode 100644 index 000000000..4a716269a --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/ConvertersTest.groovy @@ -0,0 +1,11 @@ +package io.jsonwebtoken.impl.lang + +import org.junit.Test + +class ConvertersTest { + + @Test + void testPrivateCtor() { // only for code coverage + new Converters() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index 451fbf84a..5092069b5 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -13,6 +13,7 @@ import java.security.PrivateKey import java.security.PublicKey import java.security.cert.X509Certificate import java.security.interfaces.ECKey +import java.security.interfaces.RSAPublicKey import static org.junit.Assert.* @@ -121,6 +122,42 @@ class JwksTest { testProperty('x509CertificateChain', 'x5c', [cert], [sval]) } + @Test + void testX509Sha1Thumbprint() { + testThumbprint(1) + } + + @Test + void testX509Sha256Thumbprint() { + testThumbprint(256) + } + + static void testThumbprint(int number) { + def algs = SignatureAlgorithms.values().findAll {it instanceof AsymmetricKeySignatureAlgorithm} + + for(def alg : algs) { + //get test cert: + X509Certificate cert = CertUtils.readTestCertificate(alg) + def pubKey = cert.getPublicKey() + + def builder = pubKey instanceof RSAPublicKey ? + Jwks.builder().forRsaChain(cert) : + Jwks.builder().forEcChain(cert) + + if (number == 1) { + builder.withX509Sha1Thumbprint(true) + } // otherwise, when a chain is present, a sha256 thumbprint is calculated automatically + + def jwkFromKey = builder.build() as PublicJwk + byte[] thumbprint = jwkFromKey."getX509CertificateSha${number}Thumbprint"() + assertNotNull thumbprint + + //ensure base64url encoding/decoding of the thumbprint works: + def jwkFromValues = Jwks.builder().putAll(jwkFromKey).build() as PublicJwk + assertArrayEquals thumbprint, jwkFromValues."getX509CertificateSha${number}Thumbprint"() + } + } + @Test void testSecretJwks() { Collection algs = SignatureAlgorithms.values().findAll({it instanceof SecretKeySignatureAlgorithm}) as Collection diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy index 845ea316d..8af3d51b5 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy @@ -1,5 +1,6 @@ package io.jsonwebtoken.security +import io.jsonwebtoken.UnsupportedJwtException import io.jsonwebtoken.impl.security.DefaultAeadRequest import io.jsonwebtoken.impl.security.DefaultAeadResult import io.jsonwebtoken.impl.security.GcmAesAeadAlgorithm @@ -38,6 +39,46 @@ class EncryptionAlgorithmsTest { new EncryptionAlgorithms() } + @Test + void testForId() { + for (AeadAlgorithm alg : EncryptionAlgorithms.values()) { + assertSame alg, EncryptionAlgorithms.forId(alg.getId()) + } + } + + @Test + void testForIdCaseInsensitive() { + for (AeadAlgorithm alg : EncryptionAlgorithms.values()) { + assertSame alg, EncryptionAlgorithms.forId(alg.getId().toLowerCase()) + } + } + + @Test(expected = UnsupportedJwtException) + void testForIdWithInvalidId() { + //unlike the 'find' paradigm, 'for' requires the value to exist + EncryptionAlgorithms.forId('invalid') + } + + @Test + void testFindById() { + for (AeadAlgorithm alg : EncryptionAlgorithms.values()) { + assertSame alg, EncryptionAlgorithms.findById(alg.getId()) + } + } + + @Test + void testFindByIdCaseInsensitive() { + for (AeadAlgorithm alg : EncryptionAlgorithms.values()) { + assertSame alg, EncryptionAlgorithms.findById(alg.getId().toLowerCase()) + } + } + + @Test + void testFindByIdWithInvalidId() { + // 'find' paradigm can return null if not found + assertNull EncryptionAlgorithms.findById('invalid') + } + @Test void testWithoutAad() { @@ -87,7 +128,7 @@ class EncryptionAlgorithmsTest { assertEquals(ciphertext.length, PLAINTEXT_BYTES.length) } - def dreq = new DefaultAeadResult(null, null, result.getPayload(), key, AAD_BYTES, result.getDigest(), result.getInitializationVector()); + def dreq = new DefaultAeadResult(null, null, result.getPayload(), key, AAD_BYTES, result.getDigest(), result.getInitializationVector()) byte[] decryptedPlaintextBytes = alg.decrypt(dreq).getPayload() assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy index 8dac33ff7..c9fbb1864 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy @@ -1,5 +1,7 @@ package io.jsonwebtoken.security +import io.jsonwebtoken.UnsupportedJwtException +import io.jsonwebtoken.impl.security.Pbes2HsAkwAlgorithm import org.junit.Test import java.security.Key @@ -37,12 +39,52 @@ class KeyAlgorithmsTest { } @Test - void testFindByExactId() { - assertSame KeyAlgorithms.A128KW, KeyAlgorithms.findById('A128KW') + void testForId() { + for (KeyAlgorithm alg : KeyAlgorithms.values()) { + assertSame alg, KeyAlgorithms.forId(alg.getId()) + } + } + + @Test + void testForIdCaseInsensitive() { + for (KeyAlgorithm alg : KeyAlgorithms.values()) { + assertSame alg, KeyAlgorithms.forId(alg.getId().toLowerCase()) + } + } + + @Test(expected = UnsupportedJwtException) + void testForIdWithInvalidId() { + //unlike the 'find' paradigm, 'for' requires the value to exist + KeyAlgorithms.forId('invalid') + } + + @Test + void testFindById() { + for (KeyAlgorithm alg : KeyAlgorithms.values()) { + assertSame alg, KeyAlgorithms.findById(alg.getId()) + } } @Test void testFindByIdCaseInsensitive() { - assertSame KeyAlgorithms.A128GCMKW, KeyAlgorithms.findById('a128GcMkW') + for (KeyAlgorithm alg : KeyAlgorithms.values()) { + assertSame alg, KeyAlgorithms.findById(alg.getId().toLowerCase()) + } + } + + @Test + void testFindByIdWithInvalidId() { + // 'find' paradigm can return null if not found + assertNull KeyAlgorithms.findById('invalid') + } + + @Test + void testEstimateIterations() { + // keep it super short so we don't hammer the test server or slow down the build too much: + long desiredMillis = 50; + + int result = KeyAlgorithms.estimateIterations(KeyAlgorithms.PBES2_HS256_A128KW, desiredMillis) + + assertTrue result > Pbes2HsAkwAlgorithm.MIN_RECOMMENDED_ITERATIONS } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy similarity index 66% rename from impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy index 427bbfea2..bdc21e75e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy @@ -17,26 +17,85 @@ package io.jsonwebtoken.security import io.jsonwebtoken.impl.security.DefaultEllipticCurveSignatureAlgorithm import io.jsonwebtoken.impl.security.DefaultRsaSignatureAlgorithm +import io.jsonwebtoken.impl.security.JcaPbeKey import org.junit.Test import javax.crypto.SecretKey +import javax.crypto.interfaces.PBEKey +import javax.crypto.spec.SecretKeySpec import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey +import java.security.SecureRandom import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey +import static org.easymock.EasyMock.* import static org.junit.Assert.* -class KeysImplTest { +class KeysTest { + + private static final Random RANDOM = new SecureRandom() + + static byte[] bytes(int sizeInBits) { + byte[] bytes = new byte[sizeInBits / Byte.SIZE] + RANDOM.nextBytes(bytes) + return bytes + } @Test void testPrivateCtor() { //for code coverage purposes only new Keys() } + @Test + void testHmacShaKeyForWithNullArgument() { + try { + Keys.hmacShaKeyFor(null) + } catch (InvalidKeyException expected) { + assertEquals 'SecretKey byte array cannot be null.', expected.message + } + } + + @Test + void testHmacShaKeyForWithWeakKey() { + int numBytes = 31 + int numBits = numBytes * 8 + try { + Keys.hmacShaKeyFor(new byte[numBytes]) + } catch (WeakKeyException expected) { + assertEquals "The specified key byte array is " + numBits + " bits which " + + "is not secure enough for any JWT HMAC-SHA algorithm. The JWT " + + "JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " + + "size >= 256 bits (the key size must be greater than or equal to the hash " + + "output size). Consider using the SignatureAlgorithms.HS256.generateKey() method (or " + + "HS384.generateKey() or HS512.generateKey()) to create a key guaranteed to be secure enough " + + "for your preferred HMAC-SHA algorithm. See " + + "https://tools.ietf.org/html/rfc7518#section-3.2 for more information." as String, expected.message + } + } + + @Test + void testHmacShaWithValidSizes() { + for (int i : [256, 384, 512]) { + byte[] bytes = bytes(i) + def key = Keys.hmacShaKeyFor(bytes) + assertTrue key instanceof SecretKeySpec + assertEquals "HmacSHA$i" as String, key.getAlgorithm() + assertTrue Arrays.equals(bytes, key.getEncoded()) + } + } + + @Test + void testHmacShaLargerThan512() { + def key = Keys.hmacShaKeyFor(bytes(520)) + assertTrue key instanceof SecretKeySpec + assertEquals 'HmacSHA512', key.getAlgorithm() + assertTrue key.getEncoded().length * Byte.SIZE >= 512 + } + @Test @Deprecated void testDeprecatedSecretKeyFor() { @@ -51,7 +110,8 @@ class KeysImplTest { assertEquals alg.jcaName, key.algorithm alg.assertValidSigningKey(key) alg.assertValidVerificationKey(key) - assertEquals alg, io.jsonwebtoken.SignatureAlgorithm.forSigningKey(key) // https://github.com/jwtk/jjwt/issues/381 + assertEquals alg, io.jsonwebtoken.SignatureAlgorithm.forSigningKey(key) + // https://github.com/jwtk/jjwt/issues/381 } else { try { Keys.secretKeyFor(alg) @@ -69,7 +129,7 @@ class KeysImplTest { for (SignatureAlgorithm alg : SignatureAlgorithms.values()) { if (alg instanceof SecretKeySignatureAlgorithm) { SecretKey key = alg.generateKey() - assertEquals alg.minKeyLength, key.getEncoded().length * 8 //convert byte count to bit count + assertEquals alg.minKeyLength, key.getEncoded().length * 8 //convert byte count to bit count assertEquals alg.jcaName, key.algorithm assertEquals alg, SignatureAlgorithms.forSigningKey(key) // https://github.com/jwtk/jjwt/issues/381 } @@ -106,7 +166,8 @@ class KeysImplTest { int len = alg.minKeyLength String asn1oid = "secp${len}r1" - String suffix = len == 256 ? ", X9.62 prime${len}v1" : '' //the JDK only adds this extra suffix to the secp256r1 curve name and not secp384r1 or secp521r1 curve names + String suffix = len == 256 ? ", X9.62 prime${len}v1" : '' + //the JDK only adds this extra suffix to the secp256r1 curve name and not secp384r1 or secp521r1 curve names String jdkParamName = "$asn1oid [NIST P-${len}${suffix}]" as String PublicKey pub = pair.getPublic() @@ -157,7 +218,8 @@ class KeysImplTest { int len = alg.minKeyLength String asn1oid = "secp${len}r1" - String suffix = len == 256 ? ", X9.62 prime${len}v1" : '' //the JDK only adds this extra suffix to the secp256r1 curve name and not secp384r1 or secp521r1 curve names + String suffix = len == 256 ? ", X9.62 prime${len}v1" : '' + //the JDK only adds this extra suffix to the secp256r1 curve name and not secp384r1 or secp521r1 curve names String jdkParamName = "$asn1oid [NIST P-${len}${suffix}]" as String PublicKey pub = pair.getPublic() @@ -173,8 +235,22 @@ class KeysImplTest { assertEquals alg.minKeyLength, priv.params.order.bitLength() } else { - assertFalse alg instanceof AsymmetricKeySignatureAlgorithm //assert we've accounted for all asymmetric ones above + assertFalse alg instanceof AsymmetricKeySignatureAlgorithm + //assert we've accounted for all asymmetric ones above } } } + + @Test + void testToPbeKey() { + + PBEKey jcaKey = createMock(PBEKey) + //asserts key is wrapped and no methods are called (i.e. we don't need to copy any values) + replay(jcaKey) + + PbeKey key = Keys.toPbeKey(jcaKey) + assertTrue key instanceof JcaPbeKey + + verify jcaKey + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy index 859b508fe..a47f6a206 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy @@ -1,7 +1,9 @@ package io.jsonwebtoken.security +import io.jsonwebtoken.UnsupportedJwtException import org.junit.Test +import static org.junit.Assert.assertNull import static org.junit.Assert.assertSame class SignatureAlgorithmsTest { @@ -12,9 +14,42 @@ class SignatureAlgorithmsTest { } @Test - void testForNameCaseInsensitive() { - for(SignatureAlgorithm alg : SignatureAlgorithms.values()) { + void testForId() { + for (SignatureAlgorithm alg : SignatureAlgorithms.values()) { + assertSame alg, SignatureAlgorithms.forId(alg.getId()) + } + } + + @Test + void testForIdCaseInsensitive() { + for (SignatureAlgorithm alg : SignatureAlgorithms.values()) { assertSame alg, SignatureAlgorithms.forId(alg.getId().toLowerCase()) } } + + @Test(expected = UnsupportedJwtException) + void testForIdWithInvalidId() { + //unlike the 'find' paradigm, 'for' requires the value to exist + SignatureAlgorithms.forId('invalid') + } + + @Test + void testFindById() { + for (SignatureAlgorithm alg : SignatureAlgorithms.values()) { + assertSame alg, SignatureAlgorithms.findById(alg.getId()) + } + } + + @Test + void testFindByIdCaseInsensitive() { + for (SignatureAlgorithm alg : SignatureAlgorithms.values()) { + assertSame alg, SignatureAlgorithms.findById(alg.getId().toLowerCase()) + } + } + + @Test + void testFindByIdWithInvalidId() { + // 'find' paradigm can return null if not found + assertNull SignatureAlgorithms.findById('invalid') + } } diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.crt.pem new file mode 100644 index 000000000..fe2793f88 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.crt.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBuDCCAV4CCQDHcF3Ya5gnbzAKBggqhkjOPQQDAjBjMQswCQYDVQQGEwJVUzET +MBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYG +A1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIxMTAxNDAw +MjAyOVoYDzMwMjExMDIyMDAyMDI5WjBjMQswCQYDVQQGEwJVUzETMBEGA1UECAwK +Q2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwPanNv +bndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAExNKMMIsawShLG4LYxpNP0gqdgK/K69UXCLt3AE3zp+T+/NDKZW0DtEdF +N8FZmjvmbE+AOQTyDtt0cjeyJK4k6TAKBggqhkjOPQQDAgNIADBFAiEAwU+JjD3a +xA8y5YvEuAx81/CrY+ioA7G0DwM4BdzpEEoCIHfKcVbJk4gLIABgVgulrmGhZZkU +/VnbQ/lGBN9qdwDg +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.key.pem new file mode 100644 index 000000000..ad9656beb --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICDbTYFnhiAEP1iipM9zKG+NoM4lbrteq54yz958Lc09oAoGCCqGSM49 +AwEHoUQDQgAExNKMMIsawShLG4LYxpNP0gqdgK/K69UXCLt3AE3zp+T+/NDKZW0D +tEdFN8FZmjvmbE+AOQTyDtt0cjeyJK4k6Q== +-----END EC PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.crt.pem new file mode 100644 index 000000000..2fc23101a --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.crt.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB9jCCAXsCCQDMm4Wfx8vVCjAKBggqhkjOPQQDAjBjMQswCQYDVQQGEwJVUzET +MBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYG +A1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIxMTAxNDAw +MjIwNVoYDzMwMjExMDIyMDAyMjA1WjBjMQswCQYDVQQGEwJVUzETMBEGA1UECAwK +Q2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwPanNv +bndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MHYwEAYHKoZIzj0CAQYFK4EEACID +YgAEmIdXgmx2I4IQDi23KUjX7xJ4O5tMRj8QUh0hpJ5toVooZPBe0yINQ+aJHrE7 +snWjsTBFqFtAqodyage8G+GtkC6dwsffE2CWOi5Z+EMYInZk2W+iWNO69cGZ/uyH +9IVhMAoGCCqGSM49BAMCA2kAMGYCMQCbd1/Un8u2Rzv7Gr4sQtGGZuaZdeiQpQ4f +FqZHrLLte130JJ3rRjkuI8hDN38wc68CMQCJuBvGEg+vk1syB4jMO+O92h15nGUG +908CdAjnd9gA0xw71euP0nUFDGkoO4kmrF0= +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.key.pem new file mode 100644 index 000000000..fcab036a8 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.key.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDAwj/7H2g+1fvBRQ598Zna6rOXYfN/Kic9HGUSgFZizjDnIDEoFYNX4 +8fwBawFwSk2gBwYFK4EEACKhZANiAASYh1eCbHYjghAOLbcpSNfvEng7m0xGPxBS +HSGknm2hWihk8F7TIg1D5okesTuydaOxMEWoW0Cqh3JqB7wb4a2QLp3Cx98TYJY6 +Lln4QxgidmTZb6JY07r1wZn+7If0hWE= +-----END EC PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.crt.pem new file mode 100644 index 000000000..ad7e7c394 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.crt.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICPzCCAaECCQCKEqPphDTnFzAKBggqhkjOPQQDAjBjMQswCQYDVQQGEwJVUzET +MBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYG +A1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIxMTAxNDAw +MjIxOFoYDzMwMjExMDIyMDAyMjE4WjBjMQswCQYDVQQGEwJVUzETMBEGA1UECAwK +Q2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwPanNv +bndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MIGbMBAGByqGSM49AgEGBSuBBAAj +A4GGAAQBS8o7xRLNRNsD5zlcw8fblmEkmHeNHHiuPDvRSLzx/CyaA5VACiMqg2Kx +9JnlbaUKa/R4y4lyuDMY9r922zp7tnEBWEiIKZqKaA0S9T+7/C6rEu2CL6p/DyWb +T/3CxAXZisNPwvqZei7bG/UoD3uCPRs8d4Rm9oJBJ9nfZJS/EuSWaC0wCgYIKoZI +zj0EAwIDgYsAMIGHAkEntHNUKUmgOamlKN5sqrXuJJnWClTpJ6XY/zIzmgRfOAyn +o//sToH5y019Fc3vkVVjRnWykdGwgK6fGYn7Q9oYMAJCAYhsiJGFYgzumbUNlADU +mMBAoF/g3e3QhFngu3i60lm7eiHmqGAIlGr36My2vJT1yfBQZR+54+7ZCZh3IClu +6wbY +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.key.pem new file mode 100644 index 000000000..8b69bcd14 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.key.pem @@ -0,0 +1,7 @@ +-----BEGIN EC PRIVATE KEY----- +MIHbAgEBBEFRWZiTr6EKKQyuNJrR6zXT9KDtqQFm2Mi89fQcHGBC0GykIM8cUSyg +89/Vct6uaBlSATT2y4kXJ4tiDBX4A4nLaKAHBgUrgQQAI6GBiQOBhgAEAUvKO8US +zUTbA+c5XMPH25ZhJJh3jRx4rjw70Ui88fwsmgOVQAojKoNisfSZ5W2lCmv0eMuJ +crgzGPa/dts6e7ZxAVhIiCmaimgNEvU/u/wuqxLtgi+qfw8lm0/9wsQF2YrDT8L6 +mXou2xv1KA97gj0bPHeEZvaCQSfZ32SUvxLklmgt +-----END EC PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/README.md b/impl/src/test/resources/io/jsonwebtoken/impl/security/README.md index a89ac9d4f..e85b33be0 100644 --- a/impl/src/test/resources/io/jsonwebtoken/impl/security/README.md +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/README.md @@ -1,14 +1,29 @@ -The `*.key.pem` and `*.crt.pem` files in this directory were created for testing as follows: +The RSA `*.key.pem` and `*.crt.pem` files in this directory were created for testing as follows: openssl req -x509 -newkey rsa:2048 -keyout rsa2048.key.pem -out rsa2048.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' openssl req -x509 -newkey rsa:3072 -keyout rsa3072.key.pem -out rsa3072.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' openssl req -x509 -newkey rsa:4096 -keyout rsa4096.key.pem -out rsa4096.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' The only difference is the key size and file names using sizes of `2048`, `3072`, and `4096`. + +The Elliptic Curve `*.key.pem` and `*.crt.pem` files in this directory were created for testing as follows: + + # prime256v1 is the ID that OpenSSL uses for secp256r1. It uses the other secp* IDs as expected: + openssl ecparam -name prime256v1 -genkey -noout -out ES256.key.pem + openssl ecparam -name secp384r1 -genkey -noout -out ES384.key.pem + openssl ecparam -name secp521r1 -genkey -noout -out ES512.key.pem -Each command creates a (non-password-protected) private key and a self-signed certificate for the associated key size, -valid for 1000 years. These files are intended for testing purposes only and shouldn't be used in a production system. + openssl req -new -x509 -key ES256.key.pem -out ES256.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' + openssl req -new -x509 -key ES384.key.pem -out ES384.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' + openssl req -new -x509 -key ES512.key.pem -out ES512.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' + +The above commands create a (non-password-protected) private key and a self-signed certificate for the associated key +size, valid for 1000 years. These files are intended for testing purposes only and shouldn't be used in a production +system. + +All `ES*`, `RS*`, and `PS*` file prefixes are equal to JWA standard `SignatureAlgorithm` IDs. This allows +easy file lookup based on the `SignatureAlgorithm` `getId()` value when authoring tests. -Finally, the `RS*` and `PS*` files in this directory are just are symlinks back to these files based on the JWT alg -names and their respective key sizes. This enables easy file lookup based on the `SignatureAlgorithm` `name()` value -when authoring tests. +Finally, the `RS*` and `PS*` files in this directory are just are symlinks back to `rsa*` files based on the JWT alg +names and their respective key sizes. This is so the `RS*` and `PS*` algorithms can use the same files since there +is no difference in keys between the two sets of algorithms. From 82abdaf450c492af76fce772b2e7974151e4ad69 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Thu, 14 Oct 2021 12:37:43 -0700 Subject: [PATCH 05/75] adding tests, removed unused class --- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 10 +- .../java/io/jsonwebtoken/impl/lang/Bytes.java | 10 +- .../jsonwebtoken/impl/lang/NoConverter.java | 4 +- .../security/EncryptionAlgorithmsBridge.java | 2 +- .../impl/security/JwkBuilders.java | 16 -- .../impl/security/KeysBridge.java | 2 +- .../security/SignatureAlgorithmsBridge.java | 2 +- .../jsonwebtoken/impl/lang/BytesTest.groovy | 163 ++++++++++++++++++ .../lang/EncodedObjectConverterTest.groovy | 23 +++ .../impl/lang/LocatorFunctionTest.groovy | 30 ++++ .../impl/lang/NoConverterTest.groovy | 31 ++++ .../impl/lang/NullSafeConverterTest.groovy | 25 +++ .../impl/lang/UriStringConverterTest.groovy | 28 +++ .../security/BridgeConstructorsTest.groovy | 14 ++ .../security/SignatureAlgorithmsTest.groovy | 4 +- 15 files changed, 325 insertions(+), 39 deletions(-) delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JwkBuilders.java create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/BytesTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/LocatorFunctionTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/NoConverterTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/NullSafeConverterTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/UriStringConverterTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/BridgeConstructorsTest.groovy diff --git a/api/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/api/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index f42793b13..ef0debb9e 100644 --- a/api/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -21,15 +21,9 @@ import org.junit.runner.RunWith import org.powermock.core.classloader.annotations.PrepareForTest import org.powermock.modules.junit4.PowerMockRunner -import static org.easymock.EasyMock.createMock -import static org.easymock.EasyMock.eq -import static org.easymock.EasyMock.expect -import static org.easymock.EasyMock.same +import static org.easymock.EasyMock.* import static org.junit.Assert.assertSame -import static org.powermock.api.easymock.PowerMock.mockStatic -import static org.powermock.api.easymock.PowerMock.replay -import static org.powermock.api.easymock.PowerMock.reset -import static org.powermock.api.easymock.PowerMock.verify +import static org.powermock.api.easymock.PowerMock.* @RunWith(PowerMockRunner.class) @PrepareForTest([Classes, Jwts]) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java index 050157404..86a09e668 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java @@ -58,14 +58,6 @@ public static int toInt(byte[] bytes) { (bytes[3] & 0xFF); } - public static int[] toInts(byte[] bytes) { - int[] ints = new int[bytes.length]; - for (int i = 0; i < bytes.length; i++) { - ints[i] = bytes[i] & 0xFF; - } - return ints; - } - public static byte[] concat(byte[]... arrays) { int len = 0; int count = Arrays.length(arrays); @@ -86,7 +78,7 @@ public static byte[] concat(byte[]... arrays) { return output; } - public static int byteLength(byte[] bytes) { + public static int length(byte[] bytes) { return bytes == null ? 0 : bytes.length; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/NoConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/NoConverter.java index 5327cbf16..e34009060 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/NoConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/NoConverter.java @@ -1,11 +1,13 @@ package io.jsonwebtoken.impl.lang; +import io.jsonwebtoken.lang.Assert; + class NoConverter implements Converter { private final Class type; public NoConverter(Class type) { - this.type = type; + this.type = Assert.notNull(type, "type argument cannot be null."); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java index 7fd1f3677..8a1a1255d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java @@ -9,7 +9,7 @@ import java.util.Collection; @SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.EncryptionAlgorithms implementation -public class EncryptionAlgorithmsBridge { +public final class EncryptionAlgorithmsBridge { // prevent instantiation private EncryptionAlgorithmsBridge() { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkBuilders.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkBuilders.java deleted file mode 100644 index a4854e42d..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkBuilders.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.ProtoJwkBuilder; - -// Implementation bridge to concrete implementations so the API module doesn't need to know their -// internals. The API module just needs to call this class via reflection, and the internal Classes/Subclasses -// can change without requiring an API module change. -public final class JwkBuilders { - - private JwkBuilders() { - } - - public static ProtoJwkBuilder builder() { - return new DefaultProtoJwkBuilder<>(); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java index 32c7cfd7d..9278c4d9f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java @@ -6,7 +6,7 @@ import javax.crypto.interfaces.PBEKey; @SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.Keys implementation -public class KeysBridge { +public final class KeysBridge { // prevent instantiation private KeysBridge() { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java index eb182b3ee..788de89b7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java @@ -9,7 +9,7 @@ import java.util.Collection; @SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.SignatureAlgorithms implementation -public class SignatureAlgorithmsBridge { +public final class SignatureAlgorithmsBridge { //prevent instantiation private SignatureAlgorithmsBridge() { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BytesTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BytesTest.groovy new file mode 100644 index 000000000..745dc4e2a --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BytesTest.groovy @@ -0,0 +1,163 @@ +package io.jsonwebtoken.impl.lang + +import io.jsonwebtoken.impl.security.Randoms +import org.junit.Test + +import java.security.MessageDigest + +import static org.junit.Assert.* + +class BytesTest { + + final static Random RANDOM = Randoms.secureRandom() + + @Test + void testPrivateCtor() { // for code coverage only + new Bytes() + } + + @Test + void testIntToBytesToInt() { + int iterations = 10000 + for(int i = 0; i < iterations; i++) { + int a = RANDOM.nextInt() + byte[] bytes = Bytes.toBytes(a); + int b = Bytes.toInt(bytes) + assertEquals a, b + } + } + + @Test + void testLongToBytesToLong() { + int iterations = 10000 + for(int i = 0; i < iterations; i++) { + long a = RANDOM.nextLong() + byte[] bytes = Bytes.toBytes(a); + long b = Bytes.toLong(bytes) + assertEquals a, b + } + } + + @Test + void testConcatNull() { + byte[] output = Bytes.concat(null) + assertNotNull output + assertEquals 0, output.length + } + + @Test + void testConcatSingle() { + byte[] bytes = new byte[32] + RANDOM.nextBytes(bytes) + byte[][] arg = [bytes] as byte[][] + byte[] output = Bytes.concat(arg) + assertTrue MessageDigest.isEqual(bytes, output) + } + + @Test + void testConcatSingleEmpty() { + byte[] bytes = new byte[0] + byte[][] arg = [bytes] as byte[][] + byte[] output = Bytes.concat(arg) + assertNotNull output + assertEquals 0, output.length + } + + @Test + void testConcatMultiple() { + byte[] a = new byte[32]; RANDOM.nextBytes(a) + byte[] b = new byte[16]; RANDOM.nextBytes(b) + + byte[] output = Bytes.concat(a, b) + + assertNotNull output + assertEquals a.length + b.length, output.length + + byte[] partA = new byte[a.length] + System.arraycopy(output, 0, partA, 0, a.length) + assertTrue MessageDigest.isEqual(a, partA) + + byte[] partB = new byte[b.length] + System.arraycopy(output, a.length, partB, 0, b.length) + assertTrue MessageDigest.isEqual(b, partB) + } + + @Test + void testConcatMultipleWithOneEmpty() { + + byte[] a = new byte[32]; RANDOM.nextBytes(a) + byte[] b = new byte[0] + + byte[] output = Bytes.concat(a, b) + + assertNotNull output + assertEquals a.length + b.length, output.length + + byte[] partA = new byte[a.length] + System.arraycopy(output, 0, partA, 0, a.length) + assertTrue MessageDigest.isEqual(a, partA) + + byte[] partB = new byte[b.length] + System.arraycopy(output, a.length, partB, 0, b.length) + assertTrue MessageDigest.isEqual(b, partB) + } + + @Test + void testLength() { + int len = 32 + assertEquals len, Bytes.length(new byte[len]) + } + + @Test + void testLengthZero() { + assertEquals 0, Bytes.length(new byte[0]) + } + + @Test + void testLengthNull() { + assertEquals 0, Bytes.length(null) + } + + @Test + void testBitLength() { + int len = 32 + byte[] a = new byte[len] + assertEquals len * Byte.SIZE, Bytes.bitLength(a) + } + + @Test + void testBitLengthZero() { + assertEquals 0, Bytes.bitLength(new byte[0]) + } + + @Test + void testBitLengthNull() { + assertEquals 0, Bytes.bitLength(null) + } + + @Test + void testIncrement() { + + byte[] counter = Bytes.toBytes(0) + for(int i = 0; i < 100; i++) { + assertEquals i, Bytes.toInt(counter) + Bytes.increment(counter) + } + + counter = Bytes.toBytes(Integer.MAX_VALUE - 1) + + Bytes.increment(counter) + assertEquals Integer.MAX_VALUE, Bytes.toInt(counter) + + //check correct integer overflow: + Bytes.increment(counter) + assertEquals Integer.MIN_VALUE, Bytes.toInt(counter) + } + + @Test + void testIncrementEmpty() { + byte[] counter = new byte[0] + Bytes.increment(counter) + assertTrue MessageDigest.isEqual(new byte[0], counter) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy new file mode 100644 index 000000000..7f3ea3626 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy @@ -0,0 +1,23 @@ +package io.jsonwebtoken.impl.lang + +import io.jsonwebtoken.impl.security.DefaultJwkContext +import org.junit.Test + +import static org.junit.Assert.* + +class EncodedObjectConverterTest { + + @Test + void testApplyFromWithInvalidType() { + def converter = DefaultJwkContext.URI_CONVERTER + assertTrue converter instanceof EncodedObjectConverter + int value = 42 + try { + converter.applyFrom(value) + fail("IllegalArgumentException should have been thrown.") + } catch (IllegalArgumentException expected) { + String msg = "Values must be either String or java.net.URI instances. Value type found: java.lang.Integer. Value: ${value}" + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/LocatorFunctionTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/LocatorFunctionTest.groovy new file mode 100644 index 000000000..e529828dd --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/LocatorFunctionTest.groovy @@ -0,0 +1,30 @@ +package io.jsonwebtoken.impl.lang + +import io.jsonwebtoken.Header +import io.jsonwebtoken.Locator +import io.jsonwebtoken.impl.DefaultJweHeader +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class LocatorFunctionTest { + + @Test + void testApply() { + final int value = 42 + def locator = new StaticLocator(value) + def fn = new LocatorFunction(locator) + assertEquals value, fn.apply(new DefaultJweHeader()) + } + + static class StaticLocator, R> implements Locator { + private final R o; + StaticLocator(R o) { + this.o = o; + } + @Override + R locate(H header) { + return o; + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/NoConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/NoConverterTest.groovy new file mode 100644 index 000000000..b3358c984 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/NoConverterTest.groovy @@ -0,0 +1,31 @@ +package io.jsonwebtoken.impl.lang + +import org.junit.Test + +import static org.junit.Assert.* + +class NoConverterTest { + + @Test + void testApplyTo() { + def converter = new NoConverter(Integer.class) + def val = 42 + assertSame val, converter.applyTo(val) + } + + @Test + void testApplyFromNull() { + def converter = new NoConverter(Integer.class) + assertNull converter.applyFrom(null) + } + + @Test + void testApplyFromInvalidType() { + def converter = new NoConverter(Integer.class) + try { + converter.applyFrom('hello' as String) + } catch (IllegalArgumentException expected) { + assertEquals 'Unsupported value type: java.lang.String', expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/NullSafeConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/NullSafeConverterTest.groovy new file mode 100644 index 000000000..626ad0965 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/NullSafeConverterTest.groovy @@ -0,0 +1,25 @@ +package io.jsonwebtoken.impl.lang + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNull + +class NullSafeConverterTest { + + @Test + void testNullArguments() { + def converter = new NullSafeConverter(new UriStringConverter()) + assertNull converter.applyTo(null) + assertNull converter.applyFrom(null) + } + + @Test + void testNonNullArguments() { + def converter = new NullSafeConverter(new UriStringConverter()) + String url = 'https://github.com/jwtk/jjwt' + URI uri = new URI(url) + assertEquals url, converter.applyTo(uri) + assertEquals uri, converter.applyFrom(url) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/UriStringConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/UriStringConverterTest.groovy new file mode 100644 index 000000000..19046b88d --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/UriStringConverterTest.groovy @@ -0,0 +1,28 @@ +package io.jsonwebtoken.impl.lang + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class UriStringConverterTest { + + @Test + void testApplyTo() { + String url = 'https://github.com/jwtk/jjwt' + URI uri = new URI(url) + def converter = new UriStringConverter() + assertEquals url, converter.applyTo(uri) + assertEquals uri, converter.applyFrom(url) + } + + @Test + void testApplyFromWithInvalidArgument() { + String val = '{}asdfasdfasd' + try { + new UriStringConverter().applyFrom(val) + } catch (IllegalArgumentException expected) { + String msg = "Unable to convert String value '${val}' to URI instance: Illegal character in path at index 0: ${val}" + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/BridgeConstructorsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/BridgeConstructorsTest.groovy new file mode 100644 index 000000000..3016d6ab7 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/BridgeConstructorsTest.groovy @@ -0,0 +1,14 @@ +package io.jsonwebtoken.impl.security + +import org.junit.Test + +class BridgeConstructorsTest { + + @Test + void testPrivateCtors() { // for code coverage only + new SignatureAlgorithmsBridge() + new EncryptionAlgorithmsBridge() + new KeyAlgorithmsBridge() + new KeysBridge() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy index a47f6a206..5c0e89aef 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy @@ -9,8 +9,8 @@ import static org.junit.Assert.assertSame class SignatureAlgorithmsTest { @Test - void testPrivateCtor() { - new SignatureAlgorithms() // for code coverage only + void testPrivateCtor() { // for code coverage only + new SignatureAlgorithms() } @Test From cee1a255c01850e586a30a7c4bb1f66ad99f5067 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Mon, 18 Oct 2021 17:30:42 -0700 Subject: [PATCH 06/75] implementation checkpoint (safety save) --- .../main/java/io/jsonwebtoken/JweHeader.java | 72 ++++- .../main/java/io/jsonwebtoken/JwsHeader.java | 30 +++ .../io/jsonwebtoken/JwtParserBuilder.java | 17 ++ .../io/jsonwebtoken/lang/Collections.java | 12 + .../jsonwebtoken/security/KeyAlgorithms.java | 4 + .../io/jsonwebtoken/impl/DefaultClaims.java | 112 ++++---- .../io/jsonwebtoken/impl/DefaultHeader.java | 145 +++++++++- .../jsonwebtoken/impl/DefaultJweBuilder.java | 4 +- .../jsonwebtoken/impl/DefaultJweHeader.java | 98 ++++++- .../jsonwebtoken/impl/DefaultJwsHeader.java | 20 +- .../jsonwebtoken/impl/DefaultJwtParser.java | 46 +++- .../impl/DefaultJwtParserBuilder.java | 9 + .../java/io/jsonwebtoken/impl/JwtMap.java | 249 ++++++++++-------- .../BigIntegerUnsignedBytesConverter.java | 41 +++ .../impl/lang/CompoundConverter.java | 26 ++ .../io/jsonwebtoken/impl/lang/Converters.java | 28 +- .../jsonwebtoken/impl/lang/DefaultField.java | 76 ++++++ .../impl/lang/DefaultFieldBuilder.java | 78 ++++++ .../java/io/jsonwebtoken/impl/lang/Field.java | 15 ++ .../jsonwebtoken/impl/lang/FieldBuilder.java | 24 ++ .../io/jsonwebtoken/impl/lang/Fields.java | 52 ++++ .../impl/lang/JwtDateConverter.java | 81 ++++++ .../impl/security/AbstractAsymmetricJwk.java | 17 +- .../AbstractAsymmetricJwkBuilder.java | 6 +- .../impl/security/AbstractEcJwkFactory.java | 24 +- .../security/AbstractFamilyJwkFactory.java | 30 +-- .../impl/security/AbstractJwk.java | 16 +- .../impl/security/DefaultEcPrivateJwk.java | 8 +- .../impl/security/DefaultEcPublicJwk.java | 12 +- ...efaultEllipticCurveSignatureAlgorithm.java | 12 - .../impl/security/DefaultJwkContext.java | 100 +++---- .../impl/security/DefaultProtoJwkBuilder.java | 38 +-- .../impl/security/DefaultRsaPrivateJwk.java | 49 ++-- .../impl/security/DefaultRsaPublicJwk.java | 10 +- .../impl/security/DefaultSecretJwk.java | 6 +- .../impl/security/DefaultValueGetter.java | 2 +- .../impl/security/DispatchingJwkFactory.java | 2 +- .../impl/security/EcPrivateJwkFactory.java | 8 +- .../impl/security/EcPublicJwkFactory.java | 12 +- .../impl/security/EcdhKeyAlgorithm.java | 97 +++++++ .../jsonwebtoken/impl/security/KeyPairs.java | 47 ++++ .../impl/security/RsaPrivateJwkFactory.java | 62 +++-- .../impl/security/RsaPublicJwkFactory.java | 8 +- .../impl/security/SecretJwkFactory.java | 4 +- .../impl/DefaultJweHeaderTest.groovy | 4 +- .../io/jsonwebtoken/impl/JwtMapTest.groovy | 14 - .../AbstractAsymmetricJwkBuilderTest.groovy | 4 +- 47 files changed, 1375 insertions(+), 456 deletions(-) create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUnsignedBytesConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/CompoundConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultField.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/Field.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/FieldBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyPairs.java diff --git a/api/src/main/java/io/jsonwebtoken/JweHeader.java b/api/src/main/java/io/jsonwebtoken/JweHeader.java index 1a408aa9f..6e7df09ba 100644 --- a/api/src/main/java/io/jsonwebtoken/JweHeader.java +++ b/api/src/main/java/io/jsonwebtoken/JweHeader.java @@ -15,6 +15,13 @@ */ package io.jsonwebtoken; +import io.jsonwebtoken.security.PublicJwk; + +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + /** * A JWE header. * @@ -89,15 +96,58 @@ public interface JweHeader extends Header { */ String getEncryptionAlgorithm(); - /** - * Sets the JWE enc (Encryption - * Algorithm) header value. A {@code null} value will remove the property from the JSON map. - *

    The JWE {@code enc} (encryption algorithm) Header Parameter identifies the content encryption algorithm - * used to perform authenticated encryption on the plaintext to produce the ciphertext and the JWE - * {@code Authentication Tag}.

    - * - * @param enc the encryption algorithm identifier - * @return this header for method chaining - */ - JweHeader setEncryptionAlgorithm(String enc); + //commented out on purpose - API users shouldn't call this method as it is always called by the Jwt/Jwe Builder +// /** +// * Sets the JWE enc (Encryption +// * Algorithm) header value. A {@code null} value will remove the property from the JSON map. +// *

    The JWE {@code enc} (encryption algorithm) Header Parameter identifies the content encryption algorithm +// * used to perform authenticated encryption on the plaintext to produce the ciphertext and the JWE +// * {@code Authentication Tag}.

    +// * +// * @param enc the encryption algorithm identifier +// * @return this header for method chaining +// */ +// JweHeader setEncryptionAlgorithm(String enc); + + URI getJwkSetUrl(); + JweHeader setJwkSetUrl(URI uri); + + PublicJwk getJwk(); + JweHeader setJwk(PublicJwk jwk); + + String getKeyId(); + JweHeader setKeyId(String kid); + + URI getX509Url(); + JweHeader setX509Url(URI uri); + + List getX509CertificateChain(); + JweHeader setX509CertificateChain(List chain); + + byte[] getX509CertificateSha1Thumbprint(); + JweHeader setX509CertificateSha1Thumbprint(byte[] thumbprint); + JweHeader computeX509CertificateSha1Thumbprint(); + + byte[] getX509CertificateSha256Thumbprint(); + JweHeader setX509CertificateSha256Thumbprint(byte[] thumbprint); + JweHeader computeX509CertificateSha256Thumbprint(); + + Set getCritical(); + JweHeader setCritical(Set crit); + + int getPbes2Count(); + JweHeader setPbes2Count(int count); + + byte[] getPbes2Salt(); + JweHeader setPbes2Salt(byte[] salt); + + byte[] getAgreementPartyUInfo(); + String getAgreementPartyUInfoString(); + JweHeader setAgreementPartyUInfo(byte[] info); + JweHeader setAgreementPartyUInfo(String info); + + byte[] getAgreementPartyVInfo(); + String getAgreementPartyVInfoString(); + JweHeader setAgreementPartyVInfo(byte[] info); + JweHeader setAgreementPartyVInfo(String info); } diff --git a/api/src/main/java/io/jsonwebtoken/JwsHeader.java b/api/src/main/java/io/jsonwebtoken/JwsHeader.java index 055f3d0a9..29ffc8394 100644 --- a/api/src/main/java/io/jsonwebtoken/JwsHeader.java +++ b/api/src/main/java/io/jsonwebtoken/JwsHeader.java @@ -15,6 +15,13 @@ */ package io.jsonwebtoken; +import io.jsonwebtoken.security.PublicJwk; + +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + /** * A JWS header. * @@ -91,4 +98,27 @@ public interface JwsHeader extends Header { * @return the {@code Header} instance for method chaining. */ JwsHeader setKeyId(String kid); + + URI getJwkSetUrl(); + JwsHeader setJwkSetUrl(URI uri); + + PublicJwk getJwk(); + JwsHeader setJwk(PublicJwk jwk); + + URI getX509Url(); + JwsHeader setX509Url(URI uri); + + List getX509CertificateChain(); + JwsHeader setX509CertificateChain(List chain); + + byte[] getX509CertificateSha1Thumbprint(); + JwsHeader setX509CertificateSha1Thumbprint(byte[] thumbprint); + JwsHeader computeX509CertificateSha1Thumbprint(); + + byte[] getX509CertificateSha256Thumbprint(); + JwsHeader setX509CertificateSha256Thumbprint(byte[] thumbprint); + JwsHeader computeX509CertificateSha256Thumbprint(); + + Set getCritical(); + JwsHeader setCritical(Set crit); } diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index 875086ea0..e41a6436b 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -40,6 +40,23 @@ */ public interface JwtParserBuilder { + /** + * Enables parsing of Unsecured JWSs (JWTs an 'alg' (Algorithm) header value of + * 'none'). Be careful when calling this method - one should fully understand + * Unsecured JWS Security Considerations + * before enabling this feature. + *

    If this method is not called, Unsecured JWSs are disabled by default as mandated by + * RFC 7518, Section + * 3.6.

    + * + * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION + * @see Unsecured JWS Security Considerations + * @see Using the Algorithm "none" + * @see io.jsonwebtoken.security.SignatureAlgorithms#NONE + */ + JwtParserBuilder enableUnsecuredJws(); + /** * Sets the JCA Provider to use during cryptographic signature and decryption operations, or {@code null} if the * JCA subsystem preferred provider should be used. diff --git a/api/src/main/java/io/jsonwebtoken/lang/Collections.java b/api/src/main/java/io/jsonwebtoken/lang/Collections.java index 439271b5a..9244c4ded 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Collections.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Collections.java @@ -57,6 +57,10 @@ public static Set setOf(T... elements) { return java.util.Collections.unmodifiableSet(set); } + public static Set immutable(Set s) { + return java.util.Collections.unmodifiableSet(s); + } + /** * Return true if the supplied Collection is null * or empty. Otherwise, return false. @@ -112,6 +116,14 @@ public static List arrayToList(Object source) { return Arrays.asList(Objects.toObjectArray(source)); } + public static Set concat(Set c, T... elements) { + int size = Math.max(1, Collections.size(c) + io.jsonwebtoken.lang.Arrays.length(elements)); + Set set = new LinkedHashSet<>(size); + set.addAll(c); + java.util.Collections.addAll(set, elements); + return set; + } + /** * Merge the given array into the given Collection. * @param array the array to merge (may be null) diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index 7d611a07c..249378a24 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -77,6 +77,10 @@ private static T forId0(String id) { public static final RsaKeyAlgorithm RSA1_5 = forId0("RSA1_5"); public static final RsaKeyAlgorithm RSA_OAEP = forId0("RSA-OAEP"); public static final RsaKeyAlgorithm RSA_OAEP_256 = forId0("RSA-OAEP-256"); + public static final EcKeyAlgorithm ECDH_ES = forId0("ECDH-ES"); + public static final EcKeyAlgorithm ECDH_ES_A128KW = forId0("ECDH-ES+A128KW"); + public static final EcKeyAlgorithm ECDH_ES_A192KW = forId0("ECDH-ES+A192KW"); + public static final EcKeyAlgorithm ECDH_ES_A256KW = forId0("ECDH-ES+A256KW"); public static int estimateIterations(KeyAlgorithm alg, long desiredMillis) { return Classes.invokeStatic(BRIDGE_CLASS, "estimateIterations", ESTIMATE_ITERATIONS_ARG_TYPES, alg, desiredMillis); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 32194da4a..670878458 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -17,135 +17,141 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.RequiredTypeException; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.impl.lang.JwtDateConverter; +import io.jsonwebtoken.lang.Collections; + import java.util.Date; import java.util.Map; +import java.util.Set; public class DefaultClaims extends JwtMap implements Claims { private static final String CONVERSION_ERROR_MSG = "Cannot convert existing claim value of type '%s' to desired type " + - "'%s'. JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. " + - "Anything more complex is expected to be already converted to your desired type by the JSON Deserializer " + - "implementation. You may specify a custom Deserializer for a JwtParser with the desired conversion " + - "configuration via the JwtParserBuilder.deserializeJsonWith() method. " + - "See https://github.com/jwtk/jjwt#custom-json-processor for more information. If using Jackson, you can " + - "specify custom claim POJO types as described in https://github.com/jwtk/jjwt#json-jackson-custom-types"; + "'%s'. JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. " + + "Anything more complex is expected to be already converted to your desired type by the JSON Deserializer " + + "implementation. You may specify a custom Deserializer for a JwtParser with the desired conversion " + + "configuration via the JwtParserBuilder.deserializeJsonWith() method. " + + "See https://github.com/jwtk/jjwt#custom-json-processor for more information. If using Jackson, you can " + + "specify custom claim POJO types as described in https://github.com/jwtk/jjwt#json-jackson-custom-types"; + + static final Field ISSUER = Fields.string(Claims.ISSUER, "Issuer"); + static final Field SUBJECT = Fields.string(Claims.SUBJECT, "Subject"); + static final Field AUDIENCE = Fields.string(Claims.AUDIENCE, "Audience"); + static final Field EXPIRATION = Fields.rfcDate(Claims.EXPIRATION, "Expiration Time"); + static final Field NOT_BEFORE = Fields.rfcDate(Claims.NOT_BEFORE, "Not Before"); + static final Field ISSUED_AT = Fields.rfcDate(Claims.ISSUED_AT, "Issued At"); + static final Field JTI = Fields.string(Claims.ID, "JWT ID"); + + static final Set> FIELDS = Collections.immutable(Collections.>setOf( + ISSUER, SUBJECT, AUDIENCE, EXPIRATION, NOT_BEFORE, ISSUED_AT, JTI + )); public DefaultClaims() { - super(); + super(FIELDS); } public DefaultClaims(Map map) { - super(map); + super(FIELDS, map); } @Override public String getIssuer() { - return getString(ISSUER); + return idiomaticGet(ISSUER); } @Override public Claims setIssuer(String iss) { - setValue(ISSUER, iss); + put(ISSUER.getId(), iss); return this; } @Override public String getSubject() { - return getString(SUBJECT); + return idiomaticGet(SUBJECT); } @Override public Claims setSubject(String sub) { - setValue(SUBJECT, sub); + put(SUBJECT.getId(), sub); return this; } @Override public String getAudience() { - return getString(AUDIENCE); + return idiomaticGet(AUDIENCE); } @Override public Claims setAudience(String aud) { - setValue(AUDIENCE, aud); + put(AUDIENCE.getId(), aud); return this; } @Override public Date getExpiration() { - return get(Claims.EXPIRATION, Date.class); + return idiomaticGet(EXPIRATION); } @Override public Claims setExpiration(Date exp) { - setDateAsSeconds(Claims.EXPIRATION, exp); + put(EXPIRATION.getId(), exp); return this; } @Override public Date getNotBefore() { - return get(Claims.NOT_BEFORE, Date.class); + return idiomaticGet(NOT_BEFORE); } @Override public Claims setNotBefore(Date nbf) { - setDateAsSeconds(Claims.NOT_BEFORE, nbf); + put(NOT_BEFORE.getId(), nbf); return this; } @Override public Date getIssuedAt() { - return get(Claims.ISSUED_AT, Date.class); + return idiomaticGet(ISSUED_AT); } @Override public Claims setIssuedAt(Date iat) { - setDateAsSeconds(Claims.ISSUED_AT, iat); + put(ISSUED_AT.getId(), iat); return this; } @Override public String getId() { - return getString(ID); + return idiomaticGet(JTI); } @Override public Claims setId(String jti) { - setValue(Claims.ID, jti); + put(JTI.getId(), jti); return this; } - /** - * @since 0.10.0 - */ - private static boolean isSpecDate(String claimName) { - return Claims.EXPIRATION.equals(claimName) || - Claims.ISSUED_AT.equals(claimName) || - Claims.NOT_BEFORE.equals(claimName); - } - - @Override - public Object put(String s, Object o) { - if (o instanceof Date && isSpecDate(s)) { //since 0.10.0 - Date date = (Date)o; - return setDateAsSeconds(s, date); - } - return super.put(s, o); - } - @Override public T get(String claimName, Class requiredType) { - Object value = get(claimName); + Object value = idiomaticGet(claimName); + if (requiredType.isInstance(value)) { + return requiredType.cast(value); + } + + value = get(claimName); if (value == null) { return null; } if (Date.class.equals(requiredType)) { - if (isSpecDate(claimName)) { - value = toSpecDate(value, claimName); - } else { - value = toDate(value, claimName); + try { + value = JwtDateConverter.toDate(value); // NOT specDate logic + } catch (Exception e) { + String msg = "Cannot create Date from '" + claimName + "' value: " + value + ". Cause: " + e.getMessage(); + throw new IllegalArgumentException(msg, e); } } @@ -154,14 +160,14 @@ public T get(String claimName, Class requiredType) { private T castClaimValue(Object value, Class requiredType) { - if (value instanceof Integer) { - int intValue = (Integer) value; - if (requiredType == Long.class) { - value = (long) intValue; - } else if (requiredType == Short.class && Short.MIN_VALUE <= intValue && intValue <= Short.MAX_VALUE) { - value = (short) intValue; - } else if (requiredType == Byte.class && Byte.MIN_VALUE <= intValue && intValue <= Byte.MAX_VALUE) { - value = (byte) intValue; + if (value instanceof Integer || value instanceof Long || value instanceof Short || value instanceof Byte) { + long longValue = ((Number)value).longValue(); + if (Integer.class.equals(requiredType) && Integer.MIN_VALUE <= longValue && longValue <= Integer.MAX_VALUE) { + value = (int)longValue; + } else if (requiredType == Short.class && Short.MIN_VALUE <= longValue && longValue <= Short.MAX_VALUE) { + value = (short) longValue; + } else if (requiredType == Byte.class && Byte.MIN_VALUE <= longValue && longValue <= Byte.MAX_VALUE) { + value = (byte) longValue; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java index cc9b59013..de30dba12 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java @@ -16,72 +16,189 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.Header; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.impl.security.AbstractAsymmetricJwk; +import io.jsonwebtoken.impl.security.AbstractJwk; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.PublicJwk; +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.List; import java.util.Map; +import java.util.Set; public class DefaultHeader> extends JwtMap implements Header { + static final Field TYPE = Fields.string(Header.TYPE, "Type"); + static final Field CONTENT_TYPE = Fields.string(Header.CONTENT_TYPE, "Content Type"); + static final Field ALGORITHM = Fields.string(Header.ALGORITHM, "Algorithm"); + static final Field COMPRESSION_ALGORITHM = Fields.string(Header.COMPRESSION_ALGORITHM, "Compression Algorithm"); + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated // TODO: remove for 1.0.0: + static final Field DEPRECATED_COMPRESSION_ALGORITHM = Fields.string(Header.DEPRECATED_COMPRESSION_ALGORITHM, "Deprecated Compression Algorithm"); + static final Field JKU = Fields.uri("jku", "JWK Set URL"); + @SuppressWarnings("rawtypes") + static final Field JWK = Fields.builder(PublicJwk.class).setId("jwk").setName("JSON Web Key").build(); + static final Field> CRIT = Fields.stringSet("crit", "Critical"); + + static final Set> FIELDS = Collections.immutable(Collections.>setOf( + TYPE, CONTENT_TYPE, ALGORITHM, COMPRESSION_ALGORITHM)); + + static final Set> CHILD_FIELDS = Collections.immutable(Collections.concat(FIELDS, + JKU, JWK, CRIT, + AbstractJwk.KID, + AbstractAsymmetricJwk.X5U, AbstractAsymmetricJwk.X5C, AbstractAsymmetricJwk.X5T, AbstractAsymmetricJwk.X5T_S256 + )); + + protected DefaultHeader(Set> fieldSet) { + super(fieldSet); + } + + protected DefaultHeader(Set> fieldSet, Map values) { + super(fieldSet, values); + } + public DefaultHeader() { - super(); + this(FIELDS); } public DefaultHeader(Map map) { - super(map); + this(FIELDS, map); } @SuppressWarnings("unchecked") protected T tthis() { - return (T)this; + return (T) this; } @Override public String getType() { - return getString(TYPE); + return idiomaticGet(TYPE); } @Override public T setType(String typ) { - setValue(TYPE, typ); + put(TYPE.getId(), typ); return tthis(); } @Override public String getContentType() { - return getString(CONTENT_TYPE); + return idiomaticGet(CONTENT_TYPE); } @Override public T setContentType(String cty) { - setValue(CONTENT_TYPE, cty); + put(CONTENT_TYPE.getId(), cty); return tthis(); } @Override public String getAlgorithm() { - return getString(ALGORITHM); + return idiomaticGet(ALGORITHM); } @Override public T setAlgorithm(String alg) { - setValue(ALGORITHM, alg); + put(ALGORITHM.getId(), alg); return tthis(); } - @SuppressWarnings("deprecation") @Override public String getCompressionAlgorithm() { - String s = getString(COMPRESSION_ALGORITHM); + String s = idiomaticGet(COMPRESSION_ALGORITHM); if (!Strings.hasText(s)) { - //backwards compatibility TODO: remove when releasing 1.0 - s = getString(DEPRECATED_COMPRESSION_ALGORITHM); + s = idiomaticGet(DEPRECATED_COMPRESSION_ALGORITHM); } return s; } @Override public T setCompressionAlgorithm(String compressionAlgorithm) { - setValue(COMPRESSION_ALGORITHM, compressionAlgorithm); + put(COMPRESSION_ALGORITHM.getId(), compressionAlgorithm); + return tthis(); + } + + public String getKeyId() { + return idiomaticGet(AbstractJwk.KID); + } + + public T setKeyId(String kid) { + put(AbstractJwk.KID.getId(), kid); + return tthis(); + } + + public URI getJwkSetUrl() { + return idiomaticGet(JKU); + } + + public T setJwkSetUrl(URI uri) { + put(JKU.getId(), uri); + return tthis(); + } + + public PublicJwk getJwk() { + return idiomaticGet(JWK); + } + + public T setJwk(PublicJwk jwk) { + put(JWK.getId(), jwk); + return tthis(); + } + + public URI getX509Url() { + return idiomaticGet(AbstractAsymmetricJwk.X5U); + } + + public T setX509Url(URI uri) { + put(AbstractAsymmetricJwk.X5U.getId(), uri); + return tthis(); + } + + public List getX509CertificateChain() { + return idiomaticGet(AbstractAsymmetricJwk.X5C); + } + + public T setX509CertificateChain(List chain) { + put(AbstractAsymmetricJwk.X5C.getId(), chain); + return tthis(); + } + + public byte[] getX509CertificateSha1Thumbprint() { + return idiomaticGet(AbstractAsymmetricJwk.X5T); + } + + public T setX509CertificateSha1Thumbprint(byte[] thumbprint) { + put(AbstractAsymmetricJwk.X5T.getId(), thumbprint); + return tthis(); + } + + public T computeX509CertificateSha1Thumbprint() { + throw new UnsupportedOperationException("Not yet implemented."); + } + + public byte[] getX509CertificateSha256Thumbprint() { + return idiomaticGet(AbstractAsymmetricJwk.X5T_S256); + } + + public T setX509CertificateSha256Thumbprint(byte[] thumbprint) { + put(AbstractAsymmetricJwk.X5T_S256.getId(), thumbprint); + return tthis(); + } + + public T computeX509CertificateSha256Thumbprint() { + throw new UnsupportedOperationException("Not yet implemented."); + } + + public Set getCritical() { + return idiomaticGet(CRIT); + } + + public T setCritical(Set crit) { + put(CRIT.getId(), crit); return tthis(); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java index 240046a9d..3acc253cf 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java @@ -152,8 +152,8 @@ public String compact() { SecretKey cek = Assert.notNull(keyResult.getKey(), "KeyResult must return a content encryption key."); byte[] encryptedCek = Assert.notNull(keyResult.getPayload(), "KeyResult must return an encrypted key byte array, even if empty."); - jweHeader.setAlgorithm(alg.getId()); - jweHeader.setEncryptionAlgorithm(enc.getId()); + jweHeader.put(DefaultHeader.ALGORITHM.getId(), alg.getId()); + jweHeader.put(DefaultJweHeader.ENCRYPTION_ALGORITHM.getId(), enc.getId()); byte[] headerBytes = this.headerSerializer.apply(jweHeader); final String base64UrlEncodedHeader = base64UrlEncoder.encode(headerBytes); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java index cd4cdf84d..61935710f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java @@ -1,30 +1,118 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Strings; +import java.nio.charset.StandardCharsets; import java.util.Map; +import java.util.Set; /** * @since JJWT_RELEASE_VERSION */ public class DefaultJweHeader extends DefaultHeader implements JweHeader { + static final Field ENCRYPTION_ALGORITHM = Fields.string("enc", "Encryption Algorithm"); + static final Field P2C = Fields.builder(Integer.class).setId("p2c").setName("PBES2 Count").build(); + static final Field P2S = Fields.bytes("p2s", "PBES2 Salt Input").build(); + static final Field APU = Fields.bytes("apu", "Agreement PartyUInfo").build(); + static final Field APV = Fields.bytes("apv", "Agreement PartyVInfo").build(); + + static final Set> FIELDS = Collections.immutable(Collections.concat(CHILD_FIELDS, + ENCRYPTION_ALGORITHM, P2C, P2S, APU, APV + )); + public DefaultJweHeader() { - super(); + super(FIELDS); } public DefaultJweHeader(Map map) { - super(map); + super(FIELDS, map); } @Override public String getEncryptionAlgorithm() { - return getString(ENCRYPTION_ALGORITHM); + return idiomaticGet(ENCRYPTION_ALGORITHM); + } + +// @Override +// public JweHeader setEncryptionAlgorithm(String enc) { +// put(ENCRYPTION_ALGORITHM.getId(), enc); +// return this; +// } + + @Override + public int getPbes2Count() { + return idiomaticGet(P2C); } @Override - public JweHeader setEncryptionAlgorithm(String enc) { - setValue(ENCRYPTION_ALGORITHM, enc); + public JweHeader setPbes2Count(int count) { + put(P2C.getId(), count); return this; } + + public byte[] getPbes2Salt() { + return idiomaticGet(P2S); + } + + public JweHeader setPbes2Salt(byte[] salt) { + put(P2S.getId(), salt); + return this; + } + + @Override + public byte[] getAgreementPartyUInfo() { + return idiomaticGet(APU); + } + + @Override + public String getAgreementPartyUInfoString() { + byte[] bytes = getAgreementPartyUInfo(); + if (bytes == null) { + return null; + } + return new String(bytes, StandardCharsets.UTF_8); + } + + @Override + public JweHeader setAgreementPartyUInfo(byte[] info) { + put(APU.getId(), info); + return this; + } + + @Override + public JweHeader setAgreementPartyUInfo(String info) { + byte[] bytes = Strings.hasText(info) ? info.getBytes(StandardCharsets.UTF_8) : null; + return setAgreementPartyUInfo(bytes); + } + + @Override + public byte[] getAgreementPartyVInfo() { + return idiomaticGet(APV); + } + + @Override + public String getAgreementPartyVInfoString() { + byte[] bytes = getAgreementPartyVInfo(); + if (bytes == null) { + return null; + } + return new String(bytes, StandardCharsets.UTF_8); + } + + @Override + public JweHeader setAgreementPartyVInfo(byte[] info) { + put(APV.getId(), info); + return this; + } + + @Override + public JweHeader setAgreementPartyVInfo(String info) { + byte[] bytes = Strings.hasText(info) ? info.getBytes(StandardCharsets.UTF_8) : null; + return setAgreementPartyVInfo(bytes); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java index e4a02fe09..b758ba411 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java @@ -16,28 +16,20 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.impl.lang.Field; import java.util.Map; +import java.util.Set; public class DefaultJwsHeader extends DefaultHeader implements JwsHeader { + static final Set> FIELDS = CHILD_FIELDS; + public DefaultJwsHeader() { - super(); + super(FIELDS); } public DefaultJwsHeader(Map map) { - super(map); - } - - @Override - public String getKeyId() { - return getString(KEY_ID); + super(FIELDS, map); } - - @Override - public JwsHeader setKeyId(String kid) { - setValue(KEY_ID, kid); - return this; - } - } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index b74b03e55..fce095d1b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -138,6 +138,8 @@ private static Function encFn(Collection compressionCodecLocator; + private final boolean enableUnsecuredJws; + private final Function> signatureAlgorithmLocator; private final Function encryptionAlgorithmLocator; @@ -171,12 +173,14 @@ public DefaultJwtParser() { this.keyAlgorithmLocator = keyFn(Collections.>emptyList()); this.encryptionAlgorithmLocator = encFn(Collections.emptyList()); this.compressionCodecLocator = new CompressionCodecLocator<>(new DefaultCompressionCodecResolver()); + this.enableUnsecuredJws = false; } @SuppressWarnings("deprecation") //SigningKeyResolver will be removed for 1.0 DefaultJwtParser(Provider provider, SigningKeyResolver signingKeyResolver, + boolean enableUnsecuredJws, Function keyLocator, Clock clock, long allowedClockSkewMillis, @@ -188,6 +192,7 @@ public DefaultJwtParser() { Collection> extraKeyAlgs, Collection extraEncAlgs) { this.provider = provider; + this.enableUnsecuredJws = enableUnsecuredJws; this.signingKeyResolver = Assert.notNull(signingKeyResolver, "SigningKeyResolver cannot be null."); this.keyLocator = Assert.notNull(keyLocator, "Key Locator cannot be null."); this.clock = clock; @@ -378,27 +383,44 @@ private static boolean hasContentType(Header header) { if (!Strings.hasText(alg)) { String msg = tokenized instanceof TokenizedJwe ? MISSING_JWE_ALG_MSG : MISSING_JWS_ALG_MSG; throw new MalformedJwtException(msg); - } else { - if (!SignatureAlgorithms.NONE.getId().equals(alg) && !Strings.hasText(tokenized.getDigest())) { + } + if (SignatureAlgorithms.NONE.getId().equalsIgnoreCase(alg)) { + if (!enableUnsecuredJws) { + String msg = "Unsecured JWTs (those with an " + DefaultHeader.ALGORITHM + + " header value of '" + SignatureAlgorithms.NONE.getId() + + "') are disallowed by default as mandated by " + + "https://datatracker.ietf.org/doc/html/rfc7518#section-3.6. If you wish to allow them to be " + + "parsed, call the JwtParserBuilder.enableUnsecuredJws() method (but please read the " + + "security considerations covered in that method's JavaDoc before doing so). Header: " + + header; + throw new UnsupportedJwtException(msg); + } + if (Strings.hasText(tokenized.getDigest())) { String type = tokenized instanceof TokenizedJwe ? "JWE" : "JWS"; String algType = tokenized instanceof TokenizedJwe ? "key management" : "signature"; String digestType = tokenized instanceof TokenizedJwe ? "an AAD authentication tag" : "a signature"; + String msg = "The " + type + " header references " + algType + " algorithm '" + alg + "' yet the " + + "compact " + type + " string has " + digestType + " token. This is not permitted per " + + "https://tools.ietf.org/html/rfc7518#section-3.6."; + throw new MalformedJwtException(msg); + } + } else { // something other than 'none'. Must have a digest component: + if (!Strings.hasText(tokenized.getDigest())) { + String type = tokenized instanceof TokenizedJwe ? "JWE" : "JWS"; + String algType = tokenized instanceof TokenizedJwe ? "key management" : "signature"; + String digestType = tokenized instanceof TokenizedJwe ? "AAD authentication tag" : "signature"; String msg = "The " + type + " header references " + algType + " algorithm '" + alg + "' but the " + - "compact " + type + " string does not have " + digestType + " token."; + "compact " + type + " string is missing the required " + digestType + " token."; throw new MalformedJwtException(msg); } } // =============== Body ================= - CompressionCodec compressionCodec = compressionCodecLocator.apply(header); - byte[] bytes = base64UrlDecode(tokenized.getBody(), "payload"); // Only JWS body can be empty per https://github.com/jwtk/jjwt/pull/540 - if (tokenized instanceof TokenizedJwe && Arrays.length(bytes) == 0) { + byte[] bytes = base64UrlDecode(tokenized.getBody(), "payload"); + if (tokenized instanceof TokenizedJwe && Arrays.length(bytes) == 0) { // Only JWS body can be empty per https://github.com/jwtk/jjwt/pull/540 String msg = "Compact JWE strings MUST always contain a payload (ciphertext)."; throw new MalformedJwtException(msg); } - if (compressionCodec != null) { - bytes = compressionCodec.decompress(bytes); - } byte[] iv = null; byte[] tag = null; @@ -477,6 +499,12 @@ private static boolean hasContentType(Header header) { bytes = result.getPayload(); } + //TODO: Only allow decompression after JWS signature verification: + CompressionCodec compressionCodec = compressionCodecLocator.apply(header); + if (compressionCodec != null) { + bytes = compressionCodec.decompress(bytes); + } + String payload = new String(bytes, Strings.UTF_8); Claims claims = null; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index 9976b9b81..eb2ce732b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -66,6 +66,8 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { private Provider provider; + private boolean enableUnsecuredJws = false; + @SuppressWarnings({"rawtypes"}) private Function keyLocator = ConstantFunction.forNull(); @@ -93,6 +95,12 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { private Key signatureVerificationKey; private Key decryptionKey; + @Override + public JwtParserBuilder enableUnsecuredJws() { + this.enableUnsecuredJws = true; + return this; + } + @Override public JwtParserBuilder setProvider(Provider provider) { this.provider = provider; @@ -290,6 +298,7 @@ public Key apply(Header header) { return new ImmutableJwtParser(new DefaultJwtParser( provider, signingKeyResolver, + enableUnsecuredJws, keyLocator, clock, allowedClockSkewMillis, diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 3c2ce03af..88ee9c5bb 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -15,14 +15,22 @@ */ package io.jsonwebtoken.impl; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.JwtDateConverter; +import io.jsonwebtoken.impl.security.JwkContext; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; -import io.jsonwebtoken.lang.DateFormats; +import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.MalformedKeyException; import java.lang.reflect.Array; -import java.text.ParseException; -import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.LinkedHashMap; @@ -31,151 +39,178 @@ public class JwtMap implements Map { - private final Map map; + static final String REDACTED_VALUE = ""; - public JwtMap() { - this.map = new LinkedHashMap<>(); - } + protected final Map values; // canonical values formatted per RFC requirements + protected final Map idiomaticValues; // the values map with any string/encoded values converted to Java type-safe values where possible + protected final Map redactedValues; // the values map with any sensitive/secret values redacted. Used in the toString implementation. + protected final Map> FIELDS; - public JwtMap(Map map) { - this(); - Assert.notNull(map, "Map argument cannot be null."); - putAll(map); + public JwtMap(Set> fieldSet) { + Assert.notEmpty(fieldSet, "Fields cannot be null or empty."); + Map> fields = new LinkedHashMap<>(); + for (Field field : fieldSet) { + fields.put(field.getId(), field); + } + this.FIELDS = java.util.Collections.unmodifiableMap(fields); + this.values = new LinkedHashMap<>(); + this.idiomaticValues = new LinkedHashMap<>(); + this.redactedValues = new LinkedHashMap<>(); } - protected String getString(String name) { - Object v = get(name); - return v != null ? String.valueOf(v) : null; + public JwtMap(Set> fieldSet, Map values) { + this(fieldSet); + Assert.notNull(values, "Map argument cannot be null."); + putAll(values); } - protected static Date toDate(Object v, String name) { - if (v == null) { - return null; - } else if (v instanceof Date) { - return (Date) v; - } else if (v instanceof Calendar) { //since 0.10.0 - return ((Calendar) v).getTime(); - } else if (v instanceof Number) { - //assume millis: - long millis = ((Number) v).longValue(); - return new Date(millis); - } else if (v instanceof String) { - return parseIso8601Date((String) v, name); //ISO-8601 parsing since 0.10.0 - } else { - throw new IllegalStateException("Cannot create Date from '" + name + "' value '" + v + "'."); - } + protected boolean isSecret(String id) { + Field field = FIELDS.get(id); + return field != null && field.isSecret(); } - /** - * @since 0.10.0 - */ - private static Date parseIso8601Date(String s, String name) throws IllegalArgumentException { + protected static Date toDate(Object v, String name) { try { - return DateFormats.parseIso8601Date(s); - } catch (ParseException e) { - String msg = "'" + name + "' value does not appear to be ISO-8601-formatted: " + s; + return JwtDateConverter.toDate(v); + } catch (Exception e) { + String msg = "Cannot create Date from '" + name + "' value: " + v + ". Cause: " + e.getMessage(); throw new IllegalArgumentException(msg, e); } } - /** - * @since 0.10.0 - */ - protected static Date toSpecDate(Object v, String name) { - if (v == null) { - return null; - } else if (v instanceof Number) { - // https://github.com/jwtk/jjwt/issues/122: - // The JWT RFC *mandates* NumericDate values are represented as seconds. - // Because java.util.Date requires milliseconds, we need to multiply by 1000: - long seconds = ((Number) v).longValue(); - v = seconds * 1000; - } else if (v instanceof String) { - // https://github.com/jwtk/jjwt/issues/122 - // The JWT RFC *mandates* NumericDate values are represented as seconds. - // Because java.util.Date requires milliseconds, we need to multiply by 1000: - try { - long seconds = Long.parseLong((String) v); - v = seconds * 1000; - } catch (NumberFormatException ignored) { - } - } - //v would have been normalized to milliseconds if it was a number value, so perform normal date conversion: - return toDate(v, name); - } - public static boolean isReduceableToNull(Object v) { return v == null || - (v instanceof String && !Strings.hasText((String)v)) || + (v instanceof String && !Strings.hasText((String) v)) || (v instanceof Collection && Collections.isEmpty((Collection) v)) || - (v instanceof Map && Collections.isEmpty((Map)v)) || + (v instanceof Map && Collections.isEmpty((Map) v)) || (v.getClass().isArray() && Array.getLength(v) == 0); } - protected void setValue(String name, Object v) { - if (isReduceableToNull(v)) { - map.remove(name); - } else { - map.put(name, v); - } - } - - @Deprecated //remove just before 1.0.0 - protected void setDate(String name, Date d) { - if (d == null) { - map.remove(name); - } else { - long seconds = d.getTime() / 1000; - map.put(name, seconds); - } + protected Object idiomaticGet(String key) { + return this.idiomaticValues.get(key); } - protected Object setDateAsSeconds(String name, Date d) { - if (d == null) { - return map.remove(name); - } else { - long seconds = d.getTime() / 1000; - return map.put(name, seconds); - } + @SuppressWarnings("unchecked") + protected T idiomaticGet(Field field) { + return (T)this.idiomaticValues.get(field.getId()); } @Override public int size() { - return map.size(); + return values.size(); } @Override public boolean isEmpty() { - return map.isEmpty(); + return values.isEmpty(); } @Override public boolean containsKey(Object o) { - return map.containsKey(o); + return values.containsKey(o); } @Override public boolean containsValue(Object o) { - return map.containsValue(o); + return values.containsValue(o); } @Override public Object get(Object o) { - return map.get(o); + return values.get(o); } @Override - public Object put(String s, Object o) { - if (o == null) { - return map.remove(s); + public Object put(String name, Object value) { + name = Assert.notNull(Strings.clean(name), "Member name cannot be null or empty."); + if (value instanceof String) { + value = Strings.clean((String) value); + } else if (Objects.isArray(value) && !value.getClass().getComponentType().isPrimitive()) { + value = Collections.arrayToList(value); + } + return idiomaticPut(name, value); + } + + // ensures that if a property name matches an RFC-specified name, that value can be represented + // as an idiomatic type-safe Java value in addition to the canonical RFC/encoded value. + private Object idiomaticPut(String name, Object value) { + assert name != null; //asserted by caller. + Field field = FIELDS.get(name); + if (field != null) { //Setting a JWA-standard property - let's ensure we can represent it idiomatically: + return apply(field, value); + } else { //non-standard/custom property: + return nullSafePut(name, value); + } + } + + protected Object nullSafePut(String name, Object value) { + if (JwtMap.isReduceableToNull(value)) { + return remove(name); + } else { + Object redactedValue = isSecret(name) ? REDACTED_VALUE : value; + this.redactedValues.put(name, redactedValue); + this.idiomaticValues.put(name, value); + return this.values.put(name, value); + } + } + + private JwtException malformed(String msg, Exception cause) { + if (this instanceof JwkContext || this instanceof Jwk) { + return new MalformedKeyException(msg, cause); + } else { + return new MalformedJwtException(msg, cause); + } + } + + protected Object apply(Field field, Object rawValue) { + + final String id = field.getId(); + final Object previousValue = get(id); + + if (isReduceableToNull(rawValue)) { + return remove(id); + } + + T idiomaticValue; // preferred Java format + Object canonicalValue; // as required by the RFC + try { + idiomaticValue = field.applyFrom(rawValue); + Assert.notNull(idiomaticValue, "Converted idiomaticValue cannot be null."); + canonicalValue = field.applyTo(idiomaticValue); + Assert.notNull(canonicalValue, "Converted canonicalValue cannot be null."); + } catch (Exception e) { + Object sval = field.isSecret() ? REDACTED_VALUE : rawValue; + String msg = "Invalid " + name() + field + " value [" + sval + "]: " + e.getMessage(); + throw malformed(msg, e); + } + nullSafePut(id, canonicalValue); + this.idiomaticValues.put(id, idiomaticValue); + return previousValue; + } + + private String name() { + if (this instanceof JweHeader) { + return "JWE header"; + } else if (this instanceof JwsHeader) { + return "JWS header"; + } else if (this instanceof Header) { + return "JWT header"; + } else if (this instanceof Jwk || this instanceof JwkContext) { + Object value = values.get("kty"); + if ("oct".equals(value)) { + value = "Secret"; + } + return value != null ? value + " JWK" : "JWK"; } else { - return map.put(s, o); + return "Map"; } } @Override - public Object remove(Object o) { - return map.remove(o); + public Object remove(Object key) { + this.redactedValues.remove(key); + this.idiomaticValues.remove(key); + return this.values.remove(key); } @Override @@ -190,36 +225,38 @@ public void putAll(Map m) { @Override public void clear() { - map.clear(); + this.values.clear(); + this.idiomaticValues.clear(); + this.redactedValues.clear(); } @Override public Set keySet() { - return map.keySet(); + return values.keySet(); } @Override public Collection values() { - return map.values(); + return values.values(); } @Override public Set> entrySet() { - return map.entrySet(); + return values.entrySet(); } @Override public String toString() { - return map.toString(); + return redactedValues.toString(); } @Override public int hashCode() { - return map.hashCode(); + return values.hashCode(); } @Override public boolean equals(Object obj) { - return map.equals(obj); + return values.equals(obj); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUnsignedBytesConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUnsignedBytesConverter.java new file mode 100644 index 000000000..9976dd134 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUnsignedBytesConverter.java @@ -0,0 +1,41 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; + +import java.math.BigInteger; + +public class BigIntegerUnsignedBytesConverter implements Converter { + + // Copied from Apache Commons Codec 1.14: + // https://github.com/apache/commons-codec/blob/af7b94750e2178b8437d9812b28e36ac87a455f2/src/main/java/org/apache/commons/codec/binary/Base64.java#L746-L775 + @Override + public byte[] applyTo(BigInteger bigInt) { + Assert.notNull(bigInt, "BigInteger argument cannot be null."); + final int bitlen = bigInt.bitLength(); + // round bitlen + final int roundedBitlen = ((bitlen + 7) >> 3) << 3; + final byte[] bigBytes = bigInt.toByteArray(); + + if (((bitlen % 8) != 0) && (((bitlen / 8) + 1) == (roundedBitlen / 8))) { + return bigBytes; + } + // set up params for copying everything but sign bit + int startSrc = 0; + int len = bigBytes.length; + + // if bigInt is exactly byte-aligned, just skip signbit in copy + if ((bitlen % 8) == 0) { + startSrc = 1; + len--; + } + final int startDst = roundedBitlen / 8 - len; // to pad w/ nulls as per spec + final byte[] resizedBytes = new byte[roundedBitlen / 8]; + System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len); + return resizedBytes; + } + + @Override + public BigInteger applyFrom(byte[] bytes) { + return new BigInteger(1, bytes); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/CompoundConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/CompoundConverter.java new file mode 100644 index 000000000..f219d6d5e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/CompoundConverter.java @@ -0,0 +1,26 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; + +public class CompoundConverter implements Converter { + + private final Converter first; + private final Converter second; + + public CompoundConverter(Converter first, Converter second) { + this.first = Assert.notNull(first, "First converter cannot be null."); + this.second = Assert.notNull(second, "Second converter cannot be null."); + } + + @Override + public C applyTo(A a) { + B b = first.applyTo(a); + return second.applyTo(b); + } + + @Override + public A applyFrom(C c) { + B b = second.applyFrom(c); + return first.applyFrom(b); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java index c850a7cff..a99566a46 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java @@ -1,10 +1,28 @@ package io.jsonwebtoken.impl.lang; +import io.jsonwebtoken.impl.io.CodecConverter; +import io.jsonwebtoken.impl.security.JwkX509StringConverter; + +import java.math.BigInteger; +import java.net.URI; +import java.security.cert.X509Certificate; import java.util.List; import java.util.Set; public final class Converters { + public static final Converter URI = Converters.forEncoded(URI.class, new UriStringConverter()); + + public static final Converter BYTES = Converters.forEncoded(byte[].class, CodecConverter.BASE64URL); + + public static final Converter X509_CERTIFICATE = + Converters.forEncoded(X509Certificate.class, new JwkX509StringConverter()); + + public static final Converter BIGINT_UNSIGNED_BYTES = new BigIntegerUnsignedBytesConverter(); + + public static final Converter BIGINT = Converters.forEncoded(BigInteger.class, + compound(BIGINT_UNSIGNED_BYTES, CodecConverter.BASE64URL)); + //prevent instantiation private Converters() { } @@ -17,15 +35,15 @@ public static Converter, Object> forSet(Converter elementC return CollectionConverter.forSet(elementConverter); } - public static Converter, Object> forSetOf(Class clazz) { - return forSet(none(clazz)); - } - public static Converter, Object> forList(Converter elementConverter) { return CollectionConverter.forList(elementConverter); } public static Converter forEncoded(Class elementType, Converter elementConverter) { - return new EncodedObjectConverter(elementType, elementConverter); + return new EncodedObjectConverter<>(elementType, elementConverter); + } + + public static Converter compound(final Converter aConv, final Converter bConv) { + return new CompoundConverter<>(aConv, bConv); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultField.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultField.java new file mode 100644 index 000000000..b3cdaf8f0 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultField.java @@ -0,0 +1,76 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; + +public class DefaultField implements Field { + + private final String ID; + private final String NAME; + private final boolean SECRET; + private final Class IDIOMATIC_TYPE; + private final Converter CONVERTER; + + public DefaultField(String id, String name, boolean secret, + Class idiomaticType, + Converter converter) { + this.ID = Strings.clean(Assert.hasText(id, "ID argument cannot be null or empty.")); + this.NAME = Strings.clean(Assert.hasText(name, "Name argument cannot be null or empty.")); + this.SECRET = secret; + this.IDIOMATIC_TYPE = Assert.notNull(idiomaticType, "idiomaticType argument cannot be null."); + this.CONVERTER = Assert.notNull(converter, "Converter argument cannot be null."); + } + + @Override + public String getId() { + return this.ID; + } + + @Override + public String getName() { + return this.NAME; + } + + @Override + public Class getIdiomaticType() { + return this.IDIOMATIC_TYPE; + } + + @Override + public boolean isSecret() { + return SECRET; + } + + @Override + public Converter getConverter() { + return this.CONVERTER; + } + + @Override + public int hashCode() { + return this.ID.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Field) { + return this.ID.equals(((Field) obj).getId()); + } + return false; + } + + @Override + public String toString() { + return "'" + this.ID + "' (" + this.NAME + ")"; + } + + @Override + public Object applyTo(T t) { + return CONVERTER.applyTo(t); + } + + @Override + public T applyFrom(Object o) { + return CONVERTER.applyFrom(o); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java new file mode 100644 index 000000000..fa2798a5b --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java @@ -0,0 +1,78 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; + +import java.util.List; +import java.util.Set; + +public class DefaultFieldBuilder implements FieldBuilder { + + private String id; + private String name; + private boolean secret; + private Class type; + private Converter converter; + private Boolean list = null; // True == List, False == Set, null == not a collection + + @Override + public FieldBuilder setId(String id) { + this.id = id; + return this; + } + + @Override + public FieldBuilder setName(String name) { + this.name = name; + return this; + } + + @Override + public FieldBuilder setSecret(boolean secret) { + this.secret = secret; + return this; + } + + @SuppressWarnings({"unchecked", "rawtypes", "UnnecessaryLocalVariable"}) + @Override + public FieldBuilder setType(Class type) { + Class clazz = type; + this.type = clazz; + return (FieldBuilder) this; + } + + @SuppressWarnings("unchecked") + @Override + public FieldBuilder> list() { + this.list = true; + return (FieldBuilder>) this; + } + + @SuppressWarnings("unchecked") + @Override + public FieldBuilder> set() { + this.list = false; + return (FieldBuilder>) this; + } + + @Override + public FieldBuilder setConverter(Converter converter) { + this.converter = converter; + return this; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public Field build() { + + Assert.notNull(this.type, "Type must be set."); + Converter converter = this.converter; + if (converter == null) { + converter = Converters.none(this.type); + } + if (this.list != null) { + converter = this.list ? Converters.forList(converter) : Converters.forSet(converter); + } + + return new DefaultField<>(this.id, this.name, this.secret, this.type, converter); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Field.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Field.java new file mode 100644 index 000000000..9ad8ce420 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Field.java @@ -0,0 +1,15 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.Identifiable; + +public interface Field extends Identifiable, Converter { + + String getName(); + + Class getIdiomaticType(); + + boolean isSecret(); + + Converter getConverter(); + +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldBuilder.java new file mode 100644 index 000000000..2489926fe --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldBuilder.java @@ -0,0 +1,24 @@ +package io.jsonwebtoken.impl.lang; + +import java.util.List; +import java.util.Set; + +public interface FieldBuilder { + + FieldBuilder setId(String id); + + FieldBuilder setName(String name); + + FieldBuilder setSecret(boolean secret); + + FieldBuilder setType(Class type); + + FieldBuilder> list(); + + FieldBuilder> set(); + + FieldBuilder setConverter(Converter converter); + + Field build(); + +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java new file mode 100644 index 000000000..8f358e2f1 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java @@ -0,0 +1,52 @@ +package io.jsonwebtoken.impl.lang; + +import java.math.BigInteger; +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.List; +import java.util.Set; + +public final class Fields { + + private Fields() { // prevent instantiation + } + + public static Field string(String id, String name) { + return builder(String.class).setConverter(Converters.none(String.class)).setId(id).setName(name).build(); + } + + public static Field rfcDate(String id, String name) { + return builder(Date.class).setConverter(JwtDateConverter.INSTANCE).setId(id).setName(name).build(); + } + + public static Field> x509Chain(String id, String name) { + return builder(X509Certificate.class) + .setConverter(Converters.X509_CERTIFICATE).list() + .setId(id).setName(name).build(); + } + + public static FieldBuilder builder(Class type) { + return new DefaultFieldBuilder<>().setType(type); + } + + public static Field> stringSet(String id, String name) { + return builder(String.class).set().setId(id).setName(name).build(); + } + + public static Field uri(String id, String name) { + return builder(URI.class).setConverter(Converters.URI).setId(id).setName(name).build(); + } + + public static FieldBuilder bytes(String id, String name) { + return builder(byte[].class).setConverter(Converters.BYTES).setId(id).setName(name); + } + + public static FieldBuilder bigInt(String id, String name) { + return builder(BigInteger.class).setConverter(Converters.BIGINT).setId(id).setName(name); + } + + public static Field secretBigInt(String id, String name) { + return bigInt(id, name).setSecret(true).build(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java new file mode 100644 index 000000000..12f68c3a7 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java @@ -0,0 +1,81 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.DateFormats; + +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; + +public class JwtDateConverter implements Converter { + + public static final JwtDateConverter INSTANCE = new JwtDateConverter(); + + @Override + public Object applyTo(Date date) { + if (date == null) { + return null; + } + // https://datatracker.ietf.org/doc/html/rfc7519#section-2, 'Numeric Date' definition: + return date.getTime() / 1000; + } + + @Override + public Date applyFrom(Object o) { + return toSpecDate(o); + } + + /** + * @since 0.10.0 + */ + public static Date toSpecDate(Object value) { + if (value == null) { + return null; + } + if (value instanceof String) { + try { + value = Long.parseLong((String) value); + } catch (NumberFormatException ignored) { // will try in the fallback toDate method call below + } + } + if (value instanceof Number) { + // https://github.com/jwtk/jjwt/issues/122: + // The JWT RFC *mandates* NumericDate values are represented as seconds. + // Because java.util.Date requires milliseconds, we need to multiply by 1000: + long seconds = ((Number) value).longValue(); + value = seconds * 1000; + } + //v would have been normalized to milliseconds if it was a number value, so perform normal date conversion: + return toDate(value); + } + + public static Date toDate(Object v) { + if (v == null) { + return null; + } else if (v instanceof Date) { + return (Date) v; + } else if (v instanceof Calendar) { //since 0.10.0 + return ((Calendar) v).getTime(); + } else if (v instanceof Number) { + //assume millis: + long millis = ((Number) v).longValue(); + return new Date(millis); + } else if (v instanceof String) { + return parseIso8601Date((String) v); //ISO-8601 parsing since 0.10.0 + } else { + String msg = "Cannot create Date from Object of type " + v.getClass().getName() + " with value: " + v; + throw new IllegalArgumentException(msg); + } + } + + /** + * @since 0.10.0 + */ + private static Date parseIso8601Date(String value) throws IllegalArgumentException { + try { + return DateFormats.parseIso8601Date(value); + } catch (ParseException e) { + String msg = "String value does not appear to be ISO-8601-formatted: " + value; + throw new IllegalArgumentException(msg, e); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java index f23327a8d..3a4508236 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java @@ -1,19 +1,24 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.AsymmetricJwk; import java.net.URI; import java.security.Key; import java.security.cert.X509Certificate; import java.util.List; +import java.util.Set; -abstract class AbstractAsymmetricJwk extends AbstractJwk implements AsymmetricJwk { +public abstract class AbstractAsymmetricJwk extends AbstractJwk implements AsymmetricJwk { - static final String PUBLIC_KEY_USE = "use"; - static final String X509_URL = "x5u"; - static final String X509_CERT_CHAIN = "x5c"; - static final String X509_SHA1_THUMBPRINT = "x5t"; - static final String X509_SHA256_THUMBPRINT = "x5t#S256"; + static final Field USE = Fields.string("use", "Public Key Use"); + public static final Field> X5C = Fields.x509Chain("x5c", "X.509 Certificate Chain"); + public static final Field X5T = Fields.bytes("x5t", "X.509 Certificate SHA-1 Thumbprint").build(); + public static final Field X5T_S256 = Fields.bytes("x5t#S256", "X.509 Certificate SHA-256 Thumbprint").build(); + public static final Field X5U = Fields.uri("x5u", "X.509 URL"); + static final Set> FIELDS = Collections.immutable(Collections.concat(AbstractJwk.FIELDS, USE, X5C, X5T, X5T_S256, X5U)); AbstractAsymmetricJwk(JwkContext ctx) { super(ctx); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java index 7ca63f3dc..a3d0fa1b1 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java @@ -61,7 +61,7 @@ public AbstractAsymmetricJwkBuilder(JwkContext ctx) { @Override public T setPublicKeyUse(String use) { Assert.hasText(use, "publicKeyUse cannot be null or empty."); - return put(AbstractAsymmetricJwk.PUBLIC_KEY_USE, use); + return put(AbstractAsymmetricJwk.USE.getId(), use); } public T setKeyUseStrategy(KeyUseStrategy strategy) { @@ -141,11 +141,11 @@ public J build() { } if (computeX509Sha1Thumbprint) { byte[] thumbprint = computeThumbprint(firstCert, "SHA-1"); - put(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT, thumbprint); + put(AbstractAsymmetricJwk.X5T.getId(), thumbprint); } if (computeX509Sha256Thumbprint) { byte[] thumbprint = computeThumbprint(firstCert, "SHA-256"); - put(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT, thumbprint); + put(AbstractAsymmetricJwk.X5T_S256.getId(), thumbprint); } } return super.build(); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java index 1825b5dee..babeea17d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java @@ -1,6 +1,7 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.Converters; import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.security.Jwk; import io.jsonwebtoken.security.UnsupportedKeyException; @@ -23,8 +24,8 @@ abstract class AbstractEcJwkFactory> extends AbstractFamilyJwkFactory { - private static final BigInteger TWO = new BigInteger("2"); - private static final BigInteger THREE = new BigInteger("3"); + private static final BigInteger TWO = BigInteger.valueOf(2); + private static final BigInteger THREE = BigInteger.valueOf(3); private static final Map EC_SPECS_BY_JWA_ID; private static final Map JWA_IDS_BY_CURVE; @@ -85,7 +86,7 @@ protected static String getJwaIdByCurve(EllipticCurve curve) { */ // Algorithm defined in http://www.secg.org/sec1-v2.pdf Section 2.3.5 static String toOctetString(int fieldSize, BigInteger coordinate) { - byte[] bytes = toUnsignedBytes(coordinate); + byte[] bytes = Converters.BIGINT_UNSIGNED_BYTES.applyTo(coordinate); int mlen = (int) Math.ceil(fieldSize / 8d); if (mlen > bytes.length) { byte[] m = new byte[mlen]; @@ -108,6 +109,11 @@ static String toOctetString(int fieldSize, BigInteger coordinate) { * @return {@code true} if a given elliptic curve contains the specified {@code point}, {@code false} otherwise. */ static boolean contains(EllipticCurve curve, ECPoint point) { + + if (ECPoint.POINT_INFINITY.equals(point)) { + return false; + } + final BigInteger a = curve.getA(); final BigInteger b = curve.getB(); final BigInteger x = point.getAffineX(); @@ -119,10 +125,18 @@ static boolean contains(EllipticCurve curve, ECPoint point) { // to the equation to account for the restricted field. For a nice overview of the math behind EC curves and // their application in cryptography, see // https://web.northeastern.edu/dummit/docs/cryptography_5_elliptic_curves_in_cryptography.pdf + final BigInteger p = ((ECFieldFp) curve.getField()).getP(); - final BigInteger lhs = y.pow(2).mod(p); //mod p to account for field prime - final BigInteger rhs = x.pow(3).add(a.multiply(x)).add(b).mod(p); //mod p to account for field prime + // Verify the point coordinates are in field range: + if (x.compareTo(BigInteger.ZERO) < 0 || x.compareTo(p) >= 0 || + y.compareTo(BigInteger.ZERO) < 0 || y.compareTo(p) >= 0) { + return false; + } + + // Finally, assert Weierstrass form equality: + final BigInteger lhs = y.modPow(TWO, p); //mod p to account for field prime + final BigInteger rhs = x.modPow(THREE, p).add(a.multiply(x)).add(b).mod(p); //mod p to account for field prime return lhs.equals(rhs); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java index 9fe607c92..f4dcd52b8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java @@ -1,6 +1,7 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.Converters; import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.InvalidKeyException; @@ -14,35 +15,8 @@ abstract class AbstractFamilyJwkFactory> implements FamilyJwkFactory { - // Copied from Apache Commons Codec 1.14: - // https://github.com/apache/commons-codec/blob/af7b94750e2178b8437d9812b28e36ac87a455f2/src/main/java/org/apache/commons/codec/binary/Base64.java#L746-L775 - static byte[] toUnsignedBytes(BigInteger bigInt) { - Assert.notNull(bigInt, "BigInteger argument cannot be null."); - final int bitlen = bigInt.bitLength(); - // round bitlen - final int roundedBitlen = ((bitlen + 7) >> 3) << 3; - final byte[] bigBytes = bigInt.toByteArray(); - - if (((bitlen % 8) != 0) && (((bitlen / 8) + 1) == (roundedBitlen / 8))) { - return bigBytes; - } - // set up params for copying everything but sign bit - int startSrc = 0; - int len = bigBytes.length; - - // if bigInt is exactly byte-aligned, just skip signbit in copy - if ((bitlen % 8) == 0) { - startSrc = 1; - len--; - } - final int startDst = roundedBitlen / 8 - len; // to pad w/ nulls as per spec - final byte[] resizedBytes = new byte[roundedBitlen / 8]; - System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len); - return resizedBytes; - } - protected static String encode(BigInteger bigInt) { - byte[] unsigned = toUnsignedBytes(bigInt); + byte[] unsigned = Converters.BIGINT_UNSIGNED_BYTES.applyTo(bigInt); return Encoders.BASE64URL.encode(unsigned); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java index ffb9290a9..f53301b88 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java @@ -1,6 +1,9 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.Jwk; import java.security.Key; @@ -8,12 +11,15 @@ import java.util.Map; import java.util.Set; -abstract class AbstractJwk implements Jwk { +public abstract class AbstractJwk implements Jwk { + + static final Field ALG = Fields.string("alg", "Algorithm"); + public static final Field KID = Fields.string("kid", "Key ID"); + static final Field> KEY_OPS = Fields.stringSet("key_ops", "Key Operations"); + static final Field KTY = Fields.string("kty", "Key Type"); + @SuppressWarnings("RedundantTypeArguments") + static final Set> FIELDS = Collections.immutable(Collections.>setOf(ALG, KID, KEY_OPS, KTY)); - static final String TYPE = "kty"; - static final String OPERATIONS = "key_ops"; - static final String ALGORITHM = "alg"; - static final String ID = "kid"; static final String REDACTED_VALUE = ""; public static final String IMMUTABLE_MSG = "JWKs are immutable may not be modified."; protected final JwkContext context; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java index 47dfa008e..88616ffc3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java @@ -1,17 +1,21 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.EcPrivateJwk; import io.jsonwebtoken.security.EcPublicJwk; +import java.math.BigInteger; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.util.Set; class DefaultEcPrivateJwk extends AbstractPrivateJwk implements EcPrivateJwk { - static final String D = "d"; - static final Set PRIVATE_NAMES = Collections.setOf(D); + static final Field D = Fields.secretBigInt("d", "ECC Private Key"); + static final Set> FIELDS = Collections.immutable(Collections.concat(DefaultEcPublicJwk.FIELDS, D)); + static final Set PRIVATE_NAMES = Collections.setOf(D.getId()); DefaultEcPrivateJwk(JwkContext ctx, EcPublicJwk pubJwk) { super(ctx, pubJwk); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java index 2970b519c..1bd893edb 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java @@ -1,15 +1,21 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.EcPublicJwk; +import java.math.BigInteger; import java.security.interfaces.ECPublicKey; +import java.util.Set; class DefaultEcPublicJwk extends AbstractPublicJwk implements EcPublicJwk { static final String TYPE_VALUE = "EC"; - static final String CURVE_ID = "crv"; - static final String X = "x"; - static final String Y = "y"; + static final Field CRV = Fields.string("crv", "Curve"); + static final Field X = Fields.bigInt("x", "X Coordinate").build(); + static final Field Y = Fields.bigInt("y", "Y Coordinate").build(); + static final Set> FIELDS = Collections.immutable(Collections.concat(AbstractAsymmetricJwk.FIELDS, CRV, X, Y)); DefaultEcPublicJwk(JwkContext ctx) { super(ctx); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java index ee7e61b5f..749db171a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java @@ -160,14 +160,12 @@ public static byte[] transcodeSignatureToConcat(final byte[] derSignature, int o } byte rLength = derSignature[offset + 1]; - int i = rLength; while ((i > 0) && (derSignature[(offset + 2 + rLength) - i] == 0)) { i--; } byte sLength = derSignature[offset + 2 + rLength + 1]; - int j = sLength; while ((j > 0) && (derSignature[(offset + 2 + rLength + 2 + sLength) - j] == 0)) { j--; @@ -184,7 +182,6 @@ public static byte[] transcodeSignatureToConcat(final byte[] derSignature, int o } final byte[] concatSignature = new byte[2 * rawLen]; - System.arraycopy(derSignature, (offset + 2 + rLength) - i, concatSignature, rawLen - i, i); System.arraycopy(derSignature, (offset + 2 + rLength + 2 + sLength) - j, concatSignature, 2 * rawLen - j, j); @@ -206,39 +203,32 @@ public static byte[] transcodeSignatureToDER(byte[] jwsSignature) throws JwtExce int rawLen = jwsSignature.length / 2; int i = rawLen; - while ((i > 0) && (jwsSignature[rawLen - i] == 0)) { i--; } int j = i; - if (jwsSignature[rawLen - i] < 0) { j += 1; } int k = rawLen; - while ((k > 0) && (jwsSignature[2 * rawLen - k] == 0)) { k--; } int l = k; - if (jwsSignature[2 * rawLen - k] < 0) { l += 1; } int len = 2 + j + 2 + l; - if (len > 255) { throw new JwtException("Invalid ECDSA signature format"); } int offset; - final byte[] derSignature; - if (len < 128) { derSignature = new byte[2 + 2 + j + 2 + l]; offset = 1; @@ -252,14 +242,12 @@ public static byte[] transcodeSignatureToDER(byte[] jwsSignature) throws JwtExce derSignature[offset++] = (byte) len; derSignature[offset++] = 2; derSignature[offset++] = (byte) j; - System.arraycopy(jwsSignature, rawLen - i, derSignature, (offset + j) - i, i); offset += j; derSignature[offset++] = 2; derSignature[offset++] = (byte) l; - System.arraycopy(jwsSignature, 2 * rawLen - k, derSignature, (offset + l) - k, k); return derSignature; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java index 9c3f7260e..174ca8a49 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java @@ -2,12 +2,9 @@ import io.jsonwebtoken.Identifiable; import io.jsonwebtoken.impl.JwtMap; -import io.jsonwebtoken.impl.io.CodecConverter; import io.jsonwebtoken.impl.lang.BiFunction; import io.jsonwebtoken.impl.lang.Converter; -import io.jsonwebtoken.impl.lang.Converters; -import io.jsonwebtoken.impl.lang.NullSafeConverter; -import io.jsonwebtoken.impl.lang.UriStringConverter; +import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Objects; @@ -28,15 +25,6 @@ public class DefaultJwkContext implements JwkContext { - private static final Converter THUMBPRINT_CONVERTER = - Converters.forEncoded(byte[].class, CodecConverter.BASE64URL); - - private static final Converter X509_CONVERTER = - Converters.forEncoded(X509Certificate.class, new JwkX509StringConverter()); - - private static final Converter URI_CONVERTER = - Converters.forEncoded(URI.class, new UriStringConverter()); - private static final Set DEFAULT_PRIVATE_NAMES; private static final Map> SETTERS; @@ -49,15 +37,15 @@ public class DefaultJwkContext implements JwkContext { @SuppressWarnings("RedundantTypeArguments") List> fns = Collections.>of( - Canonicalizer.forKey(AbstractJwk.ALGORITHM, "Algorithm"), - Canonicalizer.forKey(AbstractJwk.ID, "Key ID"), - Canonicalizer.forKey(AbstractJwk.OPERATIONS, "Key Operations", Converters.forSetOf(String.class)), - Canonicalizer.forKey(AbstractAsymmetricJwk.PUBLIC_KEY_USE, "Public Key Use"), - Canonicalizer.forKey(AbstractJwk.TYPE, "Key Type"), - Canonicalizer.forKey(AbstractAsymmetricJwk.X509_CERT_CHAIN, "X.509 Certificate Chain", Converters.forList(X509_CONVERTER)), - Canonicalizer.forKey(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT, "X.509 Certificate SHA-1 Thumbprint", THUMBPRINT_CONVERTER), - Canonicalizer.forKey(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT, "X.509 Certificate SHA-256 Thumbprint", THUMBPRINT_CONVERTER), - Canonicalizer.forKey(AbstractAsymmetricJwk.X509_URL, "X.509 URL", URI_CONVERTER) + Canonicalizer.forField(AbstractJwk.ALG), + Canonicalizer.forField(AbstractJwk.KID), + Canonicalizer.forField(AbstractJwk.KEY_OPS), + Canonicalizer.forField(AbstractJwk.KTY), + Canonicalizer.forField(AbstractAsymmetricJwk.USE), + Canonicalizer.forField(AbstractAsymmetricJwk.X5C), + Canonicalizer.forField(AbstractAsymmetricJwk.X5T), + Canonicalizer.forField(AbstractAsymmetricJwk.X5T_S256), + Canonicalizer.forField(AbstractAsymmetricJwk.X5U) ); Map> s = new LinkedHashMap<>(); for (Canonicalizer fn : fns) { @@ -88,11 +76,6 @@ public DefaultJwkContext(Set privateMemberNames) { this.redactedValues = new LinkedHashMap<>(); } - public DefaultJwkContext(Set privateMemberNames, K key) { - this(privateMemberNames); - this.key = Assert.notNull(key, "Key cannot be null."); - } - public DefaultJwkContext(Set privateMemberNames, JwkContext other) { this(privateMemberNames, other, true); } @@ -216,102 +199,102 @@ public Set> entrySet() { @Override public String getAlgorithm() { - return (String) this.values.get(AbstractJwk.ALGORITHM); + return (String) this.values.get(AbstractJwk.ALG.getId()); } @Override public JwkContext setAlgorithm(String algorithm) { - put(AbstractJwk.ALGORITHM, algorithm); + put(AbstractJwk.ALG.getId(), algorithm); return this; } @Override public String getId() { - return (String) this.values.get(AbstractJwk.ID); + return (String) this.values.get(AbstractJwk.KID.getId()); } @Override public JwkContext setId(String id) { - put(AbstractJwk.ID, id); + put(AbstractJwk.KID.getId(), id); return this; } @Override public Set getOperations() { //noinspection unchecked - return (Set) this.idiomaticValues.get(AbstractJwk.OPERATIONS); + return (Set) this.idiomaticValues.get(AbstractJwk.KEY_OPS.getId()); } @Override public JwkContext setOperations(Set ops) { - put(AbstractJwk.OPERATIONS, ops); + put(AbstractJwk.KEY_OPS.getId(), ops); return this; } @Override public String getType() { - return (String) this.values.get(AbstractJwk.TYPE); + return (String) this.values.get(AbstractJwk.KTY.getId()); } @Override public JwkContext setType(String type) { - put(AbstractJwk.TYPE, type); + put(AbstractJwk.KTY.getId(), type); return this; } @Override public String getPublicKeyUse() { - return (String) this.values.get(AbstractAsymmetricJwk.PUBLIC_KEY_USE); + return (String) this.values.get(AbstractAsymmetricJwk.USE.getId()); } @Override public JwkContext setPublicKeyUse(String use) { - put(AbstractAsymmetricJwk.PUBLIC_KEY_USE, use); + put(AbstractAsymmetricJwk.USE.getId(), use); return this; } @Override public List getX509CertificateChain() { //noinspection unchecked - return (List) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_CERT_CHAIN); + return (List) this.idiomaticValues.get(AbstractAsymmetricJwk.X5C.getId()); } @Override public JwkContext setX509CertificateChain(List x5c) { - put(AbstractAsymmetricJwk.X509_CERT_CHAIN, x5c); + put(AbstractAsymmetricJwk.X5C.getId(), x5c); return this; } @Override public byte[] getX509CertificateSha1Thumbprint() { - return (byte[]) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT); + return (byte[]) this.idiomaticValues.get(AbstractAsymmetricJwk.X5T.getId()); } @Override public JwkContext setX509CertificateSha1Thumbprint(byte[] x5t) { - put(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT, x5t); + put(AbstractAsymmetricJwk.X5T.getId(), x5t); return this; } @Override public byte[] getX509CertificateSha256Thumbprint() { - return (byte[]) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT); + return (byte[]) this.idiomaticValues.get(AbstractAsymmetricJwk.X5T_S256.getId()); } @Override public JwkContext setX509CertificateSha256Thumbprint(byte[] x5ts256) { - put(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT, x5ts256); + put(AbstractAsymmetricJwk.X5T_S256.getId(), x5ts256); return this; } @Override public URI getX509Url() { - return (URI) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_URL); + return (URI) this.idiomaticValues.get(AbstractAsymmetricJwk.X5U.getId()); } @Override public JwkContext setX509Url(URI url) { - put(AbstractAsymmetricJwk.X509_URL, url); + put(AbstractAsymmetricJwk.X5U.getId(), url); return this; } @@ -377,18 +360,14 @@ private static class Canonicalizer implements BiFunction private final String title; private final Converter converter; - public static Canonicalizer forKey(String id, String title) { - return forKey(id, title, Converters.none(String.class)); + public static Canonicalizer forField(Field field) { + return new Canonicalizer<>(field); } - public static Canonicalizer forKey(String id, String title, Converter converter) { - return new Canonicalizer<>(id, title, new NullSafeConverter<>(converter)); - } - - public Canonicalizer(String id, String title, Converter converter) { - this.id = id; - this.title = title; - this.converter = converter; + public Canonicalizer(Field field) { + this.id = field.getId(); + this.title = field.getName(); + this.converter = field.getConverter(); } @Override @@ -399,9 +378,12 @@ public String getId() { @Override public T apply(DefaultJwkContext ctx, Object rawValue) { + //noinspection unchecked + final T previousValue = (T)ctx.idiomaticValues.get(id); + if (JwtMap.isReduceableToNull(rawValue)) { ctx.remove(id); - return null; + return previousValue; } T idiomaticValue; // preferred Java format @@ -410,13 +392,13 @@ public T apply(DefaultJwkContext ctx, Object rawValue) { idiomaticValue = converter.applyFrom(rawValue); canonicalValue = converter.applyTo(idiomaticValue); } catch (Exception e) { - String msg = "Invalid JWK '" + id + "' (" + title + ") value [" + rawValue + "]: " + e.getMessage(); + Object sval = ctx.privateMemberNames.contains(id) ? AbstractJwk.REDACTED_VALUE : rawValue; + String msg = "Invalid JWK '" + id + "' (" + title + ") value [" + sval + "]: " + e.getMessage(); throw new MalformedKeyException(msg, e); } ctx.nullSafePut(id, canonicalValue); ctx.idiomaticValues.put(id, idiomaticValue); - //noinspection unchecked - return (T) canonicalValue; + return previousValue; } } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java index 1b142f259..a1c0e1b24 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java @@ -14,7 +14,6 @@ import javax.crypto.SecretKey; import java.security.Key; import java.security.KeyPair; -import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.X509Certificate; import java.security.interfaces.ECPrivateKey; @@ -23,6 +22,7 @@ import java.security.interfaces.RSAPublicKey; import java.util.List; +@SuppressWarnings("unused") //used via reflection by io.jsonwebtoken.security.Jwks public class DefaultProtoJwkBuilder, T extends JwkBuilder> extends AbstractJwkBuilder implements ProtoJwkBuilder { @@ -50,8 +50,8 @@ public RsaPublicJwkBuilder forRsaChain(X509Certificate... chain) { public RsaPublicJwkBuilder forRsaChain(List chain) { Assert.notEmpty(chain, "X509Certificate chain cannot be empty."); X509Certificate cert = chain.get(0); - PublicKey key = Assert.notNull(cert.getPublicKey(), "The first X509Certificate's PublicKey cannot be null."); - RSAPublicKey pubKey = assertChildKey(RSAPublicKey.class, key, "first X509Certificate's"); + PublicKey key = cert.getPublicKey(); + RSAPublicKey pubKey = KeyPairs.assertKey(key, RSAPublicKey.class, "The first X509Certificate's "); return setKey(pubKey).setX509CertificateChain(chain); } @@ -65,8 +65,8 @@ public EcPublicJwkBuilder forEcChain(X509Certificate... chain) { public EcPublicJwkBuilder forEcChain(List chain) { Assert.notEmpty(chain, "X509Certificate chain cannot be empty."); X509Certificate cert = chain.get(0); - PublicKey key = Assert.notNull(cert.getPublicKey(), "The first X509Certificate's PublicKey cannot be null."); - ECPublicKey pubKey = assertChildKey(ECPublicKey.class, key, "first X509Certificate's"); + PublicKey key = cert.getPublicKey(); + ECPublicKey pubKey = KeyPairs.assertKey(key, ECPublicKey.class, "The first X509Certificate's "); return setKey(pubKey).setX509CertificateChain(chain); } @@ -85,33 +85,17 @@ public EcPrivateJwkBuilder setKey(ECPrivateKey key) { return new AbstractAsymmetricJwkBuilder.DefaultEcPrivateJwkBuilder(this.jwkContext, key); } - private static T assertChildKey(Class clazz, Key key, String parentName) { - String type = PrivateKey.class.isAssignableFrom(clazz) ? "private" : "public"; - if (key == null) { - String msg = "The " + parentName + " " + type + " key cannot be null."; - throw new IllegalArgumentException(msg); - } - if (!clazz.isInstance(key)) { - String msg = "The " + parentName + " " + type + " key must be an instance of " + clazz.getName() + - ". Type found: " + key.getClass().getName(); - throw new IllegalArgumentException(msg); - } - return clazz.cast(key); - } - @Override - public RsaPrivateJwkBuilder setKeyPairRsa(KeyPair keyPair) { - Assert.notNull(keyPair, "KeyPair cannot be null."); - RSAPublicKey pub = assertChildKey(RSAPublicKey.class, keyPair.getPublic(), "KeyPair"); - RSAPrivateKey priv = assertChildKey(RSAPrivateKey.class, keyPair.getPrivate(), "KeyPair"); + public RsaPrivateJwkBuilder setKeyPairRsa(KeyPair pair) { + RSAPublicKey pub = KeyPairs.getKey(pair, RSAPublicKey.class); + RSAPrivateKey priv = KeyPairs.getKey(pair, RSAPrivateKey.class); return setKey(priv).setPublicKey(pub); } @Override - public EcPrivateJwkBuilder setKeyPairEc(KeyPair keyPair) { - Assert.notNull(keyPair, "KeyPair cannot be null."); - ECPublicKey pub = assertChildKey(ECPublicKey.class, keyPair.getPublic(), "KeyPair"); - ECPrivateKey priv = assertChildKey(ECPrivateKey.class, keyPair.getPrivate(), "KeyPair"); + public EcPrivateJwkBuilder setKeyPairEc(KeyPair pair) { + ECPublicKey pub = KeyPairs.getKey(pair, ECPublicKey.class); + ECPrivateKey priv = KeyPairs.getKey(pair, ECPrivateKey.class); return setKey(priv).setPublicKey(pub); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java index 72969f870..d20b82967 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java @@ -1,38 +1,47 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.RsaPrivateJwk; import io.jsonwebtoken.security.RsaPublicJwk; +import java.math.BigInteger; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAOtherPrimeInfo; import java.util.LinkedHashSet; import java.util.Set; class DefaultRsaPrivateJwk extends AbstractPrivateJwk implements RsaPrivateJwk { - static String PRIVATE_EXPONENT = "d"; - static String FIRST_PRIME = "p"; - static String SECOND_PRIME = "q"; - static String FIRST_CRT_EXPONENT = "dp"; - static String SECOND_CRT_EXPONENT = "dq"; - static String FIRST_CRT_COEFFICIENT = "qi"; - static String OTHER_PRIMES_INFO = "oth"; - static String PRIME_FACTOR = "r"; - static String FACTOR_CRT_EXPONENT = "d"; - static String FACTOR_CRT_COEFFICIENT = "t"; - - static final Set PRIVATE_NAMES = Collections.setOf( - PRIVATE_EXPONENT, FIRST_PRIME, SECOND_PRIME, - FIRST_CRT_EXPONENT, SECOND_CRT_EXPONENT, - FIRST_CRT_COEFFICIENT, OTHER_PRIMES_INFO); - - static final Set OPTIONAL_PRIVATE_NAMES; - + static final Field PRIVATE_EXPONENT = Fields.secretBigInt("d", "Private Exponent"); + static final Field FIRST_PRIME = Fields.secretBigInt("p", "First Prime Factor"); + static final Field SECOND_PRIME = Fields.secretBigInt("q", "Second Prime Factor"); + static final Field FIRST_CRT_EXPONENT = Fields.secretBigInt("dp", "First Factor CRT Exponent"); + static final Field SECOND_CRT_EXPONENT = Fields.secretBigInt("dq", "Second Factor CRT Exponent"); + static final Field FIRST_CRT_COEFFICIENT = Fields.secretBigInt("qi", "First CRT Coefficient"); + static final Field OTHER_PRIMES_INFO = Fields.builder(RSAOtherPrimeInfo.class).setSecret(true) + .setId("oth").setName("Other Primes Info") + .setConverter(new RsaPrivateJwkFactory.RSAOtherPrimeInfoConverter()) + .build(); + + static final Set> FIELDS = Collections.immutable(Collections.concat(DefaultRsaPublicJwk.FIELDS, + PRIVATE_EXPONENT, FIRST_PRIME, SECOND_PRIME, FIRST_CRT_EXPONENT, + SECOND_CRT_EXPONENT, FIRST_CRT_COEFFICIENT, OTHER_PRIMES_INFO + )); + + static final Set PRIVATE_NAMES; static { - OPTIONAL_PRIVATE_NAMES = new LinkedHashSet<>(PRIVATE_NAMES); - OPTIONAL_PRIVATE_NAMES.remove(PRIVATE_EXPONENT); + Set names = new LinkedHashSet<>(); + for (Field field : FIELDS) { + if (field.isSecret()) { + names.add(field.getId()); + } + } + PRIVATE_NAMES = java.util.Collections.unmodifiableSet(names); } + static final Set OPTIONAL_PRIVATE_NAMES = Collections.immutable(Collections.setOf(PRIVATE_EXPONENT.getId())); DefaultRsaPrivateJwk(JwkContext ctx, RsaPublicJwk pubJwk) { super(ctx, pubJwk); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java index 13e4fba51..3218c6540 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java @@ -1,14 +1,20 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.RsaPublicJwk; +import java.math.BigInteger; import java.security.interfaces.RSAPublicKey; +import java.util.Set; class DefaultRsaPublicJwk extends AbstractPublicJwk implements RsaPublicJwk { static final String TYPE_VALUE = "RSA"; - static final String MODULUS = "n"; - static final String PUBLIC_EXPONENT = "e"; + static final Field MODULUS = Fields.bigInt("n", "Modulus").build(); + static final Field PUBLIC_EXPONENT = Fields.bigInt("e", "Public Exponent").build(); + static final Set> FIELDS = Collections.immutable(Collections.concat(AbstractAsymmetricJwk.FIELDS, MODULUS, PUBLIC_EXPONENT)); DefaultRsaPublicJwk(JwkContext ctx) { super(ctx); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java index 60250bcd3..0c973befd 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java @@ -1,5 +1,7 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.SecretJwk; @@ -9,8 +11,8 @@ class DefaultSecretJwk extends AbstractJwk implements SecretJwk { static final String TYPE_VALUE = "oct"; - static final String K = "k"; - static final Set PRIVATE_NAMES = Collections.setOf(K); + static final Field K = Fields.bytes("k", "Key Value").setSecret(true).build(); + static final Set PRIVATE_NAMES = java.util.Collections.unmodifiableSet(Collections.setOf(K.getId())); DefaultSecretJwk(JwkContext ctx) { super(ctx); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java index c57b3a49b..9cf6b761e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java @@ -36,7 +36,7 @@ private String name() { } else if (values instanceof Header) { return "JWT header"; } else if (values instanceof Jwk || values instanceof JwkContext) { - Object value = values.get(AbstractJwk.TYPE); + Object value = values.get(AbstractJwk.KTY); if (DefaultSecretJwk.TYPE_VALUE.equals(value)) { value = "Secret"; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java index e14b49a29..9b9efb9f3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java @@ -52,7 +52,7 @@ public Jwk createJwk(JwkContext ctx) { final String kty = Strings.clean(ctx.getType()); if (key == null && kty == null) { - String msg = "Either a Key instance or a '" + AbstractJwk.TYPE + "' value is required to create a JWK."; + String msg = "Either a Key instance or a '" + AbstractJwk.KTY + "' value is required to create a JWK."; throw new IllegalArgumentException(msg); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java index 1932f0d5e..73f1bf6af 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java @@ -24,7 +24,7 @@ class EcPrivateJwkFactory extends AbstractEcJwkFactory ctx) { - return super.supportsKeyValues(ctx) && ctx.containsKey(DefaultEcPrivateJwk.D); + return super.supportsKeyValues(ctx) && ctx.containsKey(DefaultEcPrivateJwk.D.getId()); } @Override @@ -48,7 +48,7 @@ protected EcPrivateJwk createJwkFromKey(JwkContext ctx) { int fieldSize = key.getParams().getCurve().getField().getFieldSize(); String d = toOctetString(fieldSize, key.getS()); - ctx.put(DefaultEcPrivateJwk.D, d); + ctx.put(DefaultEcPrivateJwk.D.getId(), d); return new DefaultEcPrivateJwk(ctx, pubJwk); } @@ -57,8 +57,8 @@ protected EcPrivateJwk createJwkFromKey(JwkContext ctx) { protected EcPrivateJwk createJwkFromValues(final JwkContext ctx) { ValueGetter getter = new DefaultValueGetter(ctx); - String curveId = getter.getRequiredString(DefaultEcPublicJwk.CURVE_ID); - BigInteger d = getter.getRequiredBigInt(DefaultEcPrivateJwk.D, true); + String curveId = getter.getRequiredString(DefaultEcPublicJwk.CRV.getId()); + BigInteger d = getter.getRequiredBigInt(DefaultEcPrivateJwk.D.getId(), true); // We don't actually need the public x,y point coordinates for JVM lookup, but the // [JWA spec](https://tools.ietf.org/html/rfc7518#section-6.2.2) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java index a8e3d858a..fd0d9b96d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java @@ -31,14 +31,14 @@ protected EcPublicJwk createJwkFromKey(JwkContext ctx) { ECPoint point = key.getW(); String curveId = getJwaIdByCurve(curve); - ctx.put(DefaultEcPublicJwk.CURVE_ID, curveId); + ctx.put(DefaultEcPublicJwk.CRV.getId(), curveId); int fieldSize = curve.getField().getFieldSize(); String x = toOctetString(fieldSize, point.getAffineX()); - ctx.put(DefaultEcPublicJwk.X, x); + ctx.put(DefaultEcPublicJwk.X.getId(), x); String y = toOctetString(fieldSize, point.getAffineY()); - ctx.put(DefaultEcPublicJwk.Y, y); + ctx.put(DefaultEcPublicJwk.Y.getId(), y); return new DefaultEcPublicJwk(ctx); } @@ -47,9 +47,9 @@ protected EcPublicJwk createJwkFromKey(JwkContext ctx) { protected EcPublicJwk createJwkFromValues(final JwkContext ctx) { ValueGetter getter = new DefaultValueGetter(ctx); - String curveId = getter.getRequiredString(DefaultEcPublicJwk.CURVE_ID); - BigInteger x = getter.getRequiredBigInt(DefaultEcPublicJwk.X, false); - BigInteger y = getter.getRequiredBigInt(DefaultEcPublicJwk.Y, false); + String curveId = getter.getRequiredString(DefaultEcPublicJwk.CRV.getId()); + BigInteger x = getter.getRequiredBigInt(DefaultEcPublicJwk.X.getId(), false); + BigInteger y = getter.getRequiredBigInt(DefaultEcPublicJwk.Y.getId(), false); ECParameterSpec spec = getCurveByJwaId(curveId); ECPoint point = new ECPoint(x, y); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java new file mode 100644 index 000000000..de6f5521c --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java @@ -0,0 +1,97 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.DecryptionKeyRequest; +import io.jsonwebtoken.security.EcKeyAlgorithm; +import io.jsonwebtoken.security.EcPublicJwk; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.Jwks; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.SecurityException; + +import javax.crypto.KeyAgreement; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.ECKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.EllipticCurve; + +public class EcdhKeyAlgorithm extends CryptoAlgorithm implements EcKeyAlgorithm { + + protected static final String JCA_NAME = "ECDH"; + protected static final String EPHEMERAL_PUBLIC_KEY = "epk"; + + EcdhKeyAlgorithm(String id) { + super(id, JCA_NAME); + } + + private KeyPair generateKeyPair(final KeyRequest request, final ECParameterSpec spec) { + Assert.notNull(spec, "request key params cannot be null."); + return new JcaTemplate("EC", request.getProvider(), ensureSecureRandom(request)) + .execute(KeyPairGenerator.class, new CheckedFunction() { + @Override + public KeyPair apply(KeyPairGenerator keyPairGenerator) throws Exception { + keyPairGenerator.initialize(spec, ensureSecureRandom(request)); + return keyPairGenerator.generateKeyPair(); + } + }); + } + + protected SecretKey generateSecretKey(final KeyRequest request, final PublicKey pub, final PrivateKey priv) { + return execute(request, KeyAgreement.class, new CheckedFunction() { + @Override + public SecretKey apply(KeyAgreement keyAgreement) throws Exception { + keyAgreement.init(priv); + keyAgreement.doPhase(pub, true); + byte[] derived = keyAgreement.generateSecret(); + return new SecretKeySpec(derived, "AES"); + } + }); + } + + @Override + public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + Assert.notNull(request, "Request cannot be null."); + JweHeader header = Assert.notNull(request.getHeader(), "request JweHeader cannot be null."); + E publicKey = Assert.notNull(request.getKey(), "request key cannot be null."); + + // guarantee that the specified request key is on a supported curve + ECParameterSpec spec = Assert.notNull(publicKey.getParams(), "request key params cannot be null."); + EllipticCurve curve = spec.getCurve(); + String jwaCurveId = AbstractEcJwkFactory.getJwaIdByCurve(curve); + if (publicKey instanceof ECPublicKey && !AbstractEcJwkFactory.contains(curve, ((ECPublicKey) publicKey).getW())) { + String msg = "Specified ECPublicKey cannot be used with JWA standard curve " + jwaCurveId + ": " + + "The key's ECPoint does not exist on curve '" + jwaCurveId + "'."; + throw new InvalidKeyException(msg); + } + + KeyPair pair = generateKeyPair(request, spec); + ECPublicKey genPubKey = KeyPairs.getKey(pair, ECPublicKey.class); + ECPrivateKey genPrivKey = KeyPairs.getKey(pair, ECPrivateKey.class); + + SecretKey secretKey = generateSecretKey(request, publicKey, genPrivKey); + + EcPublicJwk jwk = Jwks.builder().setKey(genPubKey).build(); + header.put(EPHEMERAL_PUBLIC_KEY, jwk); + + byte[] apu = header.getAgreementPartyUInfo(); + byte[] apv = header.getAgreementPartyVInfo(); + + + return null; + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + return null; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyPairs.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyPairs.java new file mode 100644 index 000000000..04e9773d8 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyPairs.java @@ -0,0 +1,47 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; + +import java.security.Key; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.interfaces.RSAKey; + +public final class KeyPairs { + + private KeyPairs() { + } + + private static String family(Class clazz) { + return RSAKey.class.equals(clazz) ? "RSA" : "EC"; + } + + public static K getKey(KeyPair pair, Class clazz) { + if (pair == null) { + String msg = family(clazz) + " KeyPair cannot be null."; + throw new IllegalArgumentException(msg); + } + String prefix = family(clazz) + " KeyPair "; + boolean isPrivate = PrivateKey.class.isAssignableFrom(clazz); + Key key = isPrivate ? pair.getPrivate() : pair.getPublic(); + return assertKey(key, clazz, prefix); + } + + public static K assertKey(Key key, Class clazz) { + Assert.notNull(clazz, "Class argument cannot be null."); + String family = family(clazz); + return assertKey(key, clazz, family + " "); + } + + public static K assertKey(Key key, Class clazz, String msgPrefix) { + Assert.notNull(key, "Key argument cannot be null."); + Assert.notNull(clazz, "Class argument cannot be null."); + String type = key instanceof PrivateKey ? "private" : "public"; + if (!clazz.isInstance(key)) { + String msg = msgPrefix + type + " key must be an instance of " + clazz.getName() + + ". Type found: " + key.getClass().getName(); + throw new IllegalArgumentException(msg); + } + return clazz.cast(key); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java index 58f119208..02b38d43b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java @@ -3,6 +3,8 @@ import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.impl.lang.Converter; import io.jsonwebtoken.impl.lang.Converters; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.impl.lang.ValueGetter; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; @@ -42,7 +44,7 @@ class RsaPrivateJwkFactory extends AbstractFamilyJwkFactory ctx) { - return super.supportsKeyValues(ctx) && ctx.containsKey(DefaultRsaPrivateJwk.PRIVATE_EXPONENT); + return super.supportsKeyValues(ctx) && ctx.containsKey(DefaultRsaPrivateJwk.PRIVATE_EXPONENT.getId()); } private static BigInteger getPublicExponent(RSAPrivateKey key) { @@ -99,26 +101,28 @@ protected RsaPrivateJwk createJwkFromKey(JwkContext ctx) { RsaPublicJwk pubJwk = RsaPublicJwkFactory.DEFAULT_INSTANCE.createJwk(pubCtx); ctx.putAll(pubJwk); // add public values to private key context - ctx.put(DefaultRsaPrivateJwk.PRIVATE_EXPONENT, encode(key.getPrivateExponent())); + ctx.put(DefaultRsaPrivateJwk.PRIVATE_EXPONENT.getId(), encode(key.getPrivateExponent())); if (key instanceof RSAPrivateCrtKey) { RSAPrivateCrtKey ckey = (RSAPrivateCrtKey) key; - ctx.put(DefaultRsaPrivateJwk.FIRST_PRIME, encode(ckey.getPrimeP())); - ctx.put(DefaultRsaPrivateJwk.SECOND_PRIME, encode(ckey.getPrimeQ())); - ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, encode(ckey.getPrimeExponentP())); - ctx.put(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, encode(ckey.getPrimeExponentQ())); - ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, encode(ckey.getCrtCoefficient())); + //noinspection DuplicatedCode + ctx.put(DefaultRsaPrivateJwk.FIRST_PRIME.getId(), encode(ckey.getPrimeP())); + ctx.put(DefaultRsaPrivateJwk.SECOND_PRIME.getId(), encode(ckey.getPrimeQ())); + ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT.getId(), encode(ckey.getPrimeExponentP())); + ctx.put(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT.getId(), encode(ckey.getPrimeExponentQ())); + ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT.getId(), encode(ckey.getCrtCoefficient())); } else if (key instanceof RSAMultiPrimePrivateCrtKey) { RSAMultiPrimePrivateCrtKey ckey = (RSAMultiPrimePrivateCrtKey) key; - ctx.put(DefaultRsaPrivateJwk.FIRST_PRIME, encode(ckey.getPrimeP())); - ctx.put(DefaultRsaPrivateJwk.SECOND_PRIME, encode(ckey.getPrimeQ())); - ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, encode(ckey.getPrimeExponentP())); - ctx.put(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, encode(ckey.getPrimeExponentQ())); - ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, encode(ckey.getCrtCoefficient())); + //noinspection DuplicatedCode + ctx.put(DefaultRsaPrivateJwk.FIRST_PRIME.getId(), encode(ckey.getPrimeP())); + ctx.put(DefaultRsaPrivateJwk.SECOND_PRIME.getId(), encode(ckey.getPrimeQ())); + ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT.getId(), encode(ckey.getPrimeExponentP())); + ctx.put(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT.getId(), encode(ckey.getPrimeExponentQ())); + ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT.getId(), encode(ckey.getCrtCoefficient())); List infos = Arrays.asList(ckey.getOtherPrimeInfo()); if (!Collections.isEmpty(infos)) { Object val = RSA_OTHER_PRIMES_CONVERTER.applyTo(infos); - ctx.put(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO, val); + ctx.put(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO.getId(), val); } } @@ -129,7 +133,7 @@ protected RsaPrivateJwk createJwkFromKey(JwkContext ctx) { protected RsaPrivateJwk createJwkFromValues(JwkContext ctx) { final ValueGetter getter = new DefaultValueGetter(ctx); - final BigInteger privateExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.PRIVATE_EXPONENT, true); + final BigInteger privateExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.PRIVATE_EXPONENT.getId(), true); //The [JWA Spec, Section 6.3.2](https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.2) requires //RSA Private Keys to also encode the public key values, so we assert that we can acquire it successfully: @@ -157,16 +161,16 @@ protected RsaPrivateJwk createJwkFromValues(JwkContext ctx) { KeySpec spec; if (containsOptional) { //if any one optional field exists, they are all required per JWA Section 6.3.2: - BigInteger firstPrime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_PRIME, true); - BigInteger secondPrime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.SECOND_PRIME, true); - BigInteger firstCrtExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, true); - BigInteger secondCrtExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, true); - BigInteger firstCrtCoefficient = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, true); + BigInteger firstPrime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_PRIME.getId(), true); + BigInteger secondPrime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.SECOND_PRIME.getId(), true); + BigInteger firstCrtExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT.getId(), true); + BigInteger secondCrtExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT.getId(), true); + BigInteger firstCrtCoefficient = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT.getId(), true); // Other Primes Info is actually optional even if the above ones are required: - if (ctx.containsKey(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO)) { + if (ctx.containsKey(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO.getId())) { - Object value = ctx.get(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO); + Object value = ctx.get(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO.getId()); List otherPrimes = RSA_OTHER_PRIMES_CONVERTER.applyFrom(value); RSAOtherPrimeInfo[] arr = new RSAOtherPrimeInfo[otherPrimes.size()]; @@ -196,12 +200,16 @@ public RSAPrivateKey apply(KeyFactory kf) throws Exception { static class RSAOtherPrimeInfoConverter implements Converter { + static final Field PRIME_FACTOR = Fields.secretBigInt("r", "Prime Factor"); + static final Field FACTOR_CRT_EXPONENT = Fields.secretBigInt("d", "Factor CRT Exponent"); + static final Field FACTOR_CRT_COEFFICIENT = Fields.secretBigInt("t", "Factor CRT Coefficient"); + @Override public Object applyTo(RSAOtherPrimeInfo info) { Map m = new LinkedHashMap<>(3); - m.put(DefaultRsaPrivateJwk.PRIME_FACTOR, encode(info.getPrime())); - m.put(DefaultRsaPrivateJwk.FACTOR_CRT_EXPONENT, encode(info.getExponent())); - m.put(DefaultRsaPrivateJwk.FACTOR_CRT_COEFFICIENT, encode(info.getCrtCoefficient())); + m.put(PRIME_FACTOR.getId(), encode(info.getPrime())); + m.put(FACTOR_CRT_EXPONENT.getId(), encode(info.getExponent())); + m.put(FACTOR_CRT_COEFFICIENT.getId(), encode(info.getCrtCoefficient())); return m; } @@ -229,9 +237,9 @@ public RSAOtherPrimeInfo applyFrom(Object o) { } final ValueGetter getter = new DefaultValueGetter(ctx); - BigInteger prime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.PRIME_FACTOR, true); - BigInteger primeExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FACTOR_CRT_EXPONENT, true); - BigInteger crtCoefficient = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FACTOR_CRT_COEFFICIENT, true); + BigInteger prime = getter.getRequiredBigInt(PRIME_FACTOR.getId(), true); + BigInteger primeExponent = getter.getRequiredBigInt(FACTOR_CRT_EXPONENT.getId(), true); + BigInteger crtCoefficient = getter.getRequiredBigInt(FACTOR_CRT_COEFFICIENT.getId(), true); return new RSAOtherPrimeInfo(prime, primeExponent, crtCoefficient); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java index 7889857e5..7c202d747 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java @@ -20,16 +20,16 @@ class RsaPublicJwkFactory extends AbstractFamilyJwkFactory ctx) { RSAPublicKey key = ctx.getKey(); - ctx.put(DefaultRsaPublicJwk.MODULUS, encode(key.getModulus())); - ctx.put(DefaultRsaPublicJwk.PUBLIC_EXPONENT, encode(key.getPublicExponent())); + ctx.put(DefaultRsaPublicJwk.MODULUS.getId(), encode(key.getModulus())); + ctx.put(DefaultRsaPublicJwk.PUBLIC_EXPONENT.getId(), encode(key.getPublicExponent())); return new DefaultRsaPublicJwk(ctx); } @Override protected RsaPublicJwk createJwkFromValues(JwkContext ctx) { ValueGetter getter = new DefaultValueGetter(ctx); - BigInteger modulus = getter.getRequiredBigInt(DefaultRsaPublicJwk.MODULUS, false); - BigInteger publicExponent = getter.getRequiredBigInt(DefaultRsaPublicJwk.PUBLIC_EXPONENT, false); + BigInteger modulus = getter.getRequiredBigInt(DefaultRsaPublicJwk.MODULUS.getId(), false); + BigInteger publicExponent = getter.getRequiredBigInt(DefaultRsaPublicJwk.PUBLIC_EXPONENT.getId(), false); final RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent); RSAPublicKey key = generateKey(ctx, new CheckedFunction() { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java index 7cb524c0a..fd30998f6 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java @@ -50,7 +50,7 @@ protected SecretJwk createJwkFromKey(JwkContext ctx) { } assert k != null : "k value is mandatory."; - ctx.put(DefaultSecretJwk.K, k); + ctx.put(DefaultSecretJwk.K.getId(), k); return new DefaultSecretJwk(ctx); } @@ -58,7 +58,7 @@ protected SecretJwk createJwkFromKey(JwkContext ctx) { @Override protected SecretJwk createJwkFromValues(JwkContext ctx) { ValueGetter getter = new DefaultValueGetter(ctx); - byte[] bytes = getter.getRequiredBytes(DefaultSecretJwk.K); + byte[] bytes = getter.getRequiredBytes(DefaultSecretJwk.K.getId()); SecretKey key = new SecretKeySpec(bytes, "NONE"); //TODO: do we need a JCA-specific ID here? ctx.setKey(key); return new DefaultSecretJwk(ctx); diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy index 8d434ac97..f79384cd3 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy @@ -3,7 +3,7 @@ package io.jsonwebtoken.impl import io.jsonwebtoken.JweHeader import org.junit.Test -import static org.junit.Assert.* +import static org.junit.Assert.assertEquals /** * @since JJWT_RELEASE_VERSION @@ -23,7 +23,7 @@ class DefaultJweHeaderTest { @Test void testEncryptionAlgorithm() { JweHeader header = new DefaultJweHeader() - header.setEncryptionAlgorithm('foo') + header.put('enc', 'foo') assertEquals 'foo', header.getEncryptionAlgorithm() header = new DefaultJweHeader([enc: 'bar']) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy index 3db94003c..665f594b6 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy @@ -110,20 +110,6 @@ class JwtMapTest { assertSame d, date } - @Deprecated //remove just before 1.0.0 - @Test - void testSetDate() { - def m = new JwtMap() - m.put('foo', 'bar') - m.setDate('foo', null) - assertNull m.get('foo') - long millis = System.currentTimeMillis() - long seconds = (millis / 1000l) as long - Date date = new Date(millis) - m.setDate('foo', date) - assertEquals seconds, m.get('foo') - } - @Test void testToDateFromNonDateObject() { try { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy index 412b410ed..d2a0e9253 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy @@ -52,13 +52,13 @@ class AbstractAsymmetricJwkBuilderTest { void testX509CertificateSha1Thumbprint() { def jwk = builder().setX509CertificateChain(CHAIN).withX509Sha1Thumbprint(true).build() Assert.notEmpty(jwk.getX509CertificateSha1Thumbprint()) - Assert.hasText(jwk.get(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT) as String) + Assert.hasText(jwk.get(AbstractAsymmetricJwk.X5T) as String) } @Test void testX509CertificateSha256Thumbprint() { def jwk = builder().setX509CertificateChain(CHAIN).withX509Sha256Thumbprint(true).build() Assert.notEmpty(jwk.getX509CertificateSha256Thumbprint()) - Assert.hasText(jwk.get(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT) as String) + Assert.hasText(jwk.get(AbstractAsymmetricJwk.X5T_S256) as String) } } From ee73ba031b9a0f7e54399221819996e96f729da1 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Thu, 21 Oct 2021 17:08:43 -0700 Subject: [PATCH 07/75] clean build checkpoint --- .../io/jsonwebtoken/lang/Collections.java | 64 +++-- .../jsonwebtoken/security/KeyAlgorithms.java | 9 +- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 182 ------------- .../io/jsonwebtoken/impl/DefaultClaims.java | 12 +- .../io/jsonwebtoken/impl/DefaultHeader.java | 11 +- .../jsonwebtoken/impl/DefaultJweHeader.java | 4 +- .../jsonwebtoken/impl/DefaultJwtParser.java | 75 +++--- .../java/io/jsonwebtoken/impl/JwtMap.java | 25 +- .../io/jsonwebtoken/impl/lang/BiFunction.java | 6 - ...er.java => BigIntegerUBytesConverter.java} | 2 +- .../io/jsonwebtoken/impl/lang/Converters.java | 6 +- .../jsonwebtoken/impl/lang/DefaultField.java | 5 - .../impl/lang/DefaultFieldBuilder.java | 4 +- .../java/io/jsonwebtoken/impl/lang/Field.java | 3 - .../io/jsonwebtoken/impl/lang/Fields.java | 2 +- .../impl/lang/JwtDateConverter.java | 2 +- ...verter.java => RequiredTypeConverter.java} | 4 +- .../impl/security/AbstractAsymmetricJwk.java | 2 +- .../AbstractAsymmetricJwkBuilder.java | 21 +- .../impl/security/AbstractJwk.java | 4 +- .../impl/security/AbstractJwkBuilder.java | 5 +- .../impl/security/DefaultEcPrivateJwk.java | 3 +- .../impl/security/DefaultEcPublicJwk.java | 2 +- .../impl/security/DefaultJwkContext.java | 246 ++---------------- .../impl/security/DefaultRsaPrivateJwk.java | 17 +- .../impl/security/DefaultRsaPublicJwk.java | 2 +- .../impl/security/DefaultSecretJwk.java | 2 +- .../impl/security/EcPrivateJwkFactory.java | 4 +- .../impl/security/JwkContext.java | 2 - .../impl/security/RsaPrivateJwkFactory.java | 21 +- .../CustomObjectDeserializationTest.groovy | 8 +- .../DeprecatedJwtParserTest.groovy | 46 ++-- .../io/jsonwebtoken/DeprecatedJwtsTest.groovy | 42 +-- .../io/jsonwebtoken/JwtParserTest.groovy | 71 +++-- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 46 ++-- .../impl/DefaultClaimsTest.groovy | 189 +++++++++++++- .../impl/DefaultJweBuilderTest.groovy | 4 +- .../jsonwebtoken/impl/DefaultJwtTest.groovy | 2 +- .../io/jsonwebtoken/impl/JwtMapTest.groovy | 177 +++---------- .../DeflateCompressionCodecTest.groovy | 2 +- .../lang/EncodedObjectConverterTest.groovy | 4 +- ...roovy => RequiredTypeConverterTest.groovy} | 8 +- .../AbstractAsymmetricJwkBuilderTest.groovy | 4 +- .../security/DispatchingJwkFactoryTest.groovy | 2 +- 44 files changed, 538 insertions(+), 814 deletions(-) delete mode 100644 api/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/BiFunction.java rename impl/src/main/java/io/jsonwebtoken/impl/lang/{BigIntegerUnsignedBytesConverter.java => BigIntegerUBytesConverter.java} (94%) rename impl/src/main/java/io/jsonwebtoken/impl/lang/{NoConverter.java => RequiredTypeConverter.java} (84%) rename impl/src/test/groovy/io/jsonwebtoken/impl/lang/{NoConverterTest.groovy => RequiredTypeConverterTest.groovy} (71%) diff --git a/api/src/main/java/io/jsonwebtoken/lang/Collections.java b/api/src/main/java/io/jsonwebtoken/lang/Collections.java index 9244c4ded..d656a3348 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Collections.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Collections.java @@ -28,7 +28,8 @@ public final class Collections { - private Collections(){} //prevent instantiation + private Collections() { + } //prevent instantiation public static List emptyList() { return java.util.Collections.emptyList(); @@ -38,10 +39,11 @@ public static Set emptySet() { return java.util.Collections.emptySet(); } - public static Map emptyMap() { + public static Map emptyMap() { return java.util.Collections.emptyMap(); } + @SafeVarargs public static List of(T... elements) { if (elements == null || elements.length == 0) { return java.util.Collections.emptyList(); @@ -49,14 +51,24 @@ public static List of(T... elements) { return java.util.Collections.unmodifiableList(Arrays.asList(elements)); } + @SafeVarargs public static Set setOf(T... elements) { if (elements == null || elements.length == 0) { return java.util.Collections.emptySet(); } Set set = new LinkedHashSet<>(Arrays.asList(elements)); - return java.util.Collections.unmodifiableSet(set); + return immutable(set); } + /** + * Shorter convenience alias for {@link java.util.Collections#unmodifiableSet} so both classes + * don't need to be imported. + * + * @param s set to wrap in an immutable/unmodifiable collection + * @param type of elements in the set + * @return an immutable wrapper for {@code s}. + * @since JJWT_RELEASE_VERSION + */ public static Set immutable(Set s) { return java.util.Collections.unmodifiableSet(s); } @@ -64,6 +76,7 @@ public static Set immutable(Set s) { /** * Return true if the supplied Collection is null * or empty. Otherwise, return false. + * * @param collection the Collection to check * @return whether the given Collection is empty */ @@ -96,6 +109,7 @@ public static int size(Map map) { /** * Return true if the supplied Map is null * or empty. Otherwise, return false. + * * @param map the Map to check * @return whether the given Map is empty */ @@ -108,6 +122,7 @@ public static boolean isEmpty(Map map) { * converted into a List of the appropriate wrapper type. *

    A null source value will be converted to an * empty List. + * * @param source the (potentially primitive) array * @return the converted List result * @see Objects#toObjectArray(Object) @@ -116,17 +131,19 @@ public static List arrayToList(Object source) { return Arrays.asList(Objects.toObjectArray(source)); } + @SafeVarargs public static Set concat(Set c, T... elements) { int size = Math.max(1, Collections.size(c) + io.jsonwebtoken.lang.Arrays.length(elements)); Set set = new LinkedHashSet<>(size); set.addAll(c); java.util.Collections.addAll(set, elements); - return set; + return immutable(set); } /** * Merge the given array into the given Collection. - * @param array the array to merge (may be null) + * + * @param array the array to merge (may be null) * @param collection the target Collection to merge the array into */ @SuppressWarnings("unchecked") @@ -145,8 +162,9 @@ public static void mergeArrayIntoCollection(Object array, Collection collection) * copying all properties (key-value pairs) over. *

    Uses Properties.propertyNames() to even catch * default properties linked into the original Properties instance. + * * @param props the Properties instance to merge (may be null) - * @param map the target Map to merge the properties into + * @param map the target Map to merge the properties into */ @SuppressWarnings("unchecked") public static void mergePropertiesIntoMap(Properties props, Map map) { @@ -154,7 +172,7 @@ public static void mergePropertiesIntoMap(Properties props, Map map) { throw new IllegalArgumentException("Map must not be null"); } if (props != null) { - for (Enumeration en = props.propertyNames(); en.hasMoreElements();) { + for (Enumeration en = props.propertyNames(); en.hasMoreElements(); ) { String key = (String) en.nextElement(); Object value = props.getProperty(key); if (value == null) { @@ -169,8 +187,9 @@ public static void mergePropertiesIntoMap(Properties props, Map map) { /** * Check whether the given Iterator contains the given element. + * * @param iterator the Iterator to check - * @param element the element to look for + * @param element the element to look for * @return true if found, false else */ public static boolean contains(Iterator iterator, Object element) { @@ -187,8 +206,9 @@ public static boolean contains(Iterator iterator, Object element) { /** * Check whether the given Enumeration contains the given element. + * * @param enumeration the Enumeration to check - * @param element the element to look for + * @param element the element to look for * @return true if found, false else */ public static boolean contains(Enumeration enumeration, Object element) { @@ -207,8 +227,9 @@ public static boolean contains(Enumeration enumeration, Object element) { * Check whether the given Collection contains the given element instance. *

    Enforces the given instance to be present, rather than returning * true for an equal element as well. + * * @param collection the Collection to check - * @param element the element to look for + * @param element the element to look for * @return true if found, false else */ public static boolean containsInstance(Collection collection, Object element) { @@ -225,7 +246,8 @@ public static boolean containsInstance(Collection collection, Object element) { /** * Return true if any element in 'candidates' is * contained in 'source'; otherwise returns false. - * @param source the source Collection + * + * @param source the source Collection * @param candidates the candidates to search for * @return whether any of the candidates has been found */ @@ -246,7 +268,8 @@ public static boolean containsAny(Collection source, Collection candidates) { * 'source'. If no element in 'candidates' is present in * 'source' returns null. Iteration order is * {@link Collection} implementation specific. - * @param source the source Collection + * + * @param source the source Collection * @param candidates the candidates to search for * @return the first present object, or null if not found */ @@ -264,8 +287,9 @@ public static Object findFirstMatch(Collection source, Collection candidates) { /** * Find a single value of the given type in the given Collection. + * * @param collection the Collection to search - * @param type the type to look for + * @param type the type to look for * @return a value of the given type found if there is a clear match, * or null if none or more than one such value found */ @@ -291,8 +315,9 @@ public static T findValueOfType(Collection collection, Class type) { * Find a single value of one of the given types in the given Collection: * searching the Collection for a value of the first type, then * searching for a value of the second type, etc. + * * @param collection the collection to search - * @param types the types to look for, in prioritized order + * @param types the types to look for, in prioritized order * @return a value of one of the given types found if there is a clear match, * or null if none or more than one such value found */ @@ -311,6 +336,7 @@ public static Object findValueOfType(Collection collection, Class[] types) /** * Determine whether the given Collection only contains a single unique object. + * * @param collection the Collection to check * @return true if the collection contains a single reference or * multiple references to the same instance, false else @@ -325,8 +351,7 @@ public static boolean hasUniqueObject(Collection collection) { if (!hasCandidate) { hasCandidate = true; candidate = elem; - } - else if (candidate != elem) { + } else if (candidate != elem) { return false; } } @@ -335,6 +360,7 @@ else if (candidate != elem) { /** * Find the common element type of the given Collection, if any. + * * @param collection the Collection to check * @return the common element type, or null if no clear * common type has been found (or the collection was empty) @@ -348,8 +374,7 @@ public static Class findCommonElementType(Collection collection) { if (val != null) { if (candidate == null) { candidate = val.getClass(); - } - else if (candidate != val.getClass()) { + } else if (candidate != val.getClass()) { return null; } } @@ -362,7 +387,7 @@ else if (candidate != val.getClass()) { * Enumeration elements must be assignable to the type of the given array. The array * returned will be a different instance than the array given. */ - public static A[] toArray(Enumeration enumeration, A[] array) { + public static A[] toArray(Enumeration enumeration, A[] array) { ArrayList elements = new ArrayList(); while (enumeration.hasMoreElements()) { elements.add(enumeration.nextElement()); @@ -372,6 +397,7 @@ public static A[] toArray(Enumeration enumeration, A[] array) /** * Adapt an enumeration to an iterator. + * * @param enumeration the enumeration * @return the iterator */ diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index 249378a24..3b639edc4 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -77,10 +77,11 @@ private static T forId0(String id) { public static final RsaKeyAlgorithm RSA1_5 = forId0("RSA1_5"); public static final RsaKeyAlgorithm RSA_OAEP = forId0("RSA-OAEP"); public static final RsaKeyAlgorithm RSA_OAEP_256 = forId0("RSA-OAEP-256"); - public static final EcKeyAlgorithm ECDH_ES = forId0("ECDH-ES"); - public static final EcKeyAlgorithm ECDH_ES_A128KW = forId0("ECDH-ES+A128KW"); - public static final EcKeyAlgorithm ECDH_ES_A192KW = forId0("ECDH-ES+A192KW"); - public static final EcKeyAlgorithm ECDH_ES_A256KW = forId0("ECDH-ES+A256KW"); + + //public static final EcKeyAlgorithm ECDH_ES = forId0("ECDH-ES"); + //public static final EcKeyAlgorithm ECDH_ES_A128KW = forId0("ECDH-ES+A128KW"); + //public static final EcKeyAlgorithm ECDH_ES_A192KW = forId0("ECDH-ES+A192KW"); + //public static final EcKeyAlgorithm ECDH_ES_A256KW = forId0("ECDH-ES+A256KW"); public static int estimateIterations(KeyAlgorithm alg, long desiredMillis) { return Classes.invokeStatic(BRIDGE_CLASS, "estimateIterations", ESTIMATE_ITERATIONS_ARG_TYPES, alg, desiredMillis); diff --git a/api/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/api/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy deleted file mode 100644 index ef0debb9e..000000000 --- a/api/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken - -import io.jsonwebtoken.lang.Classes -import org.junit.Test -import org.junit.runner.RunWith -import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.modules.junit4.PowerMockRunner - -import static org.easymock.EasyMock.* -import static org.junit.Assert.assertSame -import static org.powermock.api.easymock.PowerMock.* - -@RunWith(PowerMockRunner.class) -@PrepareForTest([Classes, Jwts]) -class JwtsTest { - - @Test - void testPrivateCtor() { //for code coverage only - new Jwts() - } - - @Test - void testHeader() { - - mockStatic(Classes) - - def instance = createMock(Header) - - expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultHeader"))).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.header() - - verify Classes, instance - } - - @Test - void testHeaderFromMap() { - - mockStatic(Classes) - - def map = [:] - - def instance = createMock(Header) - - expect(Classes.newInstance( - eq("io.jsonwebtoken.impl.DefaultHeader"), - same(Jwts.MAP_ARG), - same(map)) - ).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.header(map) - - verify Classes, instance - } - - @Test - void testJwsHeader() { - - mockStatic(Classes) - - def instance = createMock(JwsHeader) - - expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultJwsHeader"))).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.jwsHeader() - - verify Classes, instance - } - - @Test - void testJwsHeaderFromMap() { - - mockStatic(Classes) - - def map = [:] - - def instance = createMock(JwsHeader) - - expect(Classes.newInstance( - eq("io.jsonwebtoken.impl.DefaultJwsHeader"), - same(Jwts.MAP_ARG), - same(map)) - ).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.jwsHeader(map) - - verify Classes, instance - } - - @Test - void testClaims() { - - mockStatic(Classes) - - def instance = createMock(Claims) - - expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultClaims"))).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.claims() - - verify Classes, instance - } - - @Test - void testClaimsFromMap() { - - mockStatic(Classes) - - def map = [:] - - def instance = createMock(Claims) - - expect(Classes.newInstance( - eq("io.jsonwebtoken.impl.DefaultClaims"), - same(Jwts.MAP_ARG), - same(map)) - ).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.claims(map) - - verify Classes, instance - } - - @Test - void testParser() { - - mockStatic(Classes) - - def instance = createMock(JwtParser) - - expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultJwtParser"))).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.parser() - - verify Classes, instance - } - - @Test - void testBuilder() { - - mockStatic(Classes) - - def instance = createMock(JwtBuilder) - - expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultJwtBuilder"))).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.builder() - - verify Classes, instance - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 670878458..049b22b60 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -44,9 +44,9 @@ public class DefaultClaims extends JwtMap implements Claims { static final Field ISSUED_AT = Fields.rfcDate(Claims.ISSUED_AT, "Issued At"); static final Field JTI = Fields.string(Claims.ID, "JWT ID"); - static final Set> FIELDS = Collections.immutable(Collections.>setOf( + static final Set> FIELDS = Collections.>setOf( ISSUER, SUBJECT, AUDIENCE, EXPIRATION, NOT_BEFORE, ISSUED_AT, JTI - )); + ); public DefaultClaims() { super(FIELDS); @@ -150,7 +150,7 @@ public T get(String claimName, Class requiredType) { try { value = JwtDateConverter.toDate(value); // NOT specDate logic } catch (Exception e) { - String msg = "Cannot create Date from '" + claimName + "' value: " + value + ". Cause: " + e.getMessage(); + String msg = "Cannot create Date from '" + claimName + "' value [" + value + "]. Cause: " + e.getMessage(); throw new IllegalArgumentException(msg, e); } } @@ -160,9 +160,11 @@ public T get(String claimName, Class requiredType) { private T castClaimValue(Object value, Class requiredType) { - if (value instanceof Integer || value instanceof Long || value instanceof Short || value instanceof Byte) { + if (value instanceof Long || value instanceof Integer || value instanceof Short || value instanceof Byte) { long longValue = ((Number)value).longValue(); - if (Integer.class.equals(requiredType) && Integer.MIN_VALUE <= longValue && longValue <= Integer.MAX_VALUE) { + if (Long.class.equals(requiredType)) { + value = longValue; + } else if (Integer.class.equals(requiredType) && Integer.MIN_VALUE <= longValue && longValue <= Integer.MAX_VALUE) { value = (int)longValue; } else if (requiredType == Short.class && Short.MIN_VALUE <= longValue && longValue <= Short.MAX_VALUE) { value = (short) longValue; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java index de30dba12..04fbad4fd 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java @@ -44,14 +44,9 @@ public class DefaultHeader> extends JwtMap implements Header static final Field JWK = Fields.builder(PublicJwk.class).setId("jwk").setName("JSON Web Key").build(); static final Field> CRIT = Fields.stringSet("crit", "Critical"); - static final Set> FIELDS = Collections.immutable(Collections.>setOf( - TYPE, CONTENT_TYPE, ALGORITHM, COMPRESSION_ALGORITHM)); - - static final Set> CHILD_FIELDS = Collections.immutable(Collections.concat(FIELDS, - JKU, JWK, CRIT, - AbstractJwk.KID, - AbstractAsymmetricJwk.X5U, AbstractAsymmetricJwk.X5C, AbstractAsymmetricJwk.X5T, AbstractAsymmetricJwk.X5T_S256 - )); + static final Set> FIELDS = Collections.>setOf(TYPE, CONTENT_TYPE, ALGORITHM, COMPRESSION_ALGORITHM); + static final Set> CHILD_FIELDS = Collections.concat(FIELDS, JKU, JWK, CRIT, AbstractJwk.KID, + AbstractAsymmetricJwk.X5U, AbstractAsymmetricJwk.X5C, AbstractAsymmetricJwk.X5T, AbstractAsymmetricJwk.X5T_S256); protected DefaultHeader(Set> fieldSet) { super(fieldSet); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java index 61935710f..43c7d072b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java @@ -21,9 +21,7 @@ public class DefaultJweHeader extends DefaultHeader implements JweHea static final Field APU = Fields.bytes("apu", "Agreement PartyUInfo").build(); static final Field APV = Fields.bytes("apv", "Agreement PartyVInfo").build(); - static final Set> FIELDS = Collections.immutable(Collections.concat(CHILD_FIELDS, - ENCRYPTION_ALGORITHM, P2C, P2S, APU, APV - )); + static final Set> FIELDS = Collections.concat(CHILD_FIELDS, ENCRYPTION_ALGORITHM, P2C, P2S, APU, APV); public DefaultJweHeader() { super(FIELDS); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index fce095d1b..3890190da 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -104,6 +104,22 @@ public class DefaultJwtParser implements JwtParser { "This header parameter is mandatory per the JWE Specification, Section 4.1.2. See " + "https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.2 for more information."; + private static final String UNSECURED_DISABLED_MSG_PREFIX = "Unsecured JWSs (those with an " + + DefaultHeader.ALGORITHM + " header value of '" + SignatureAlgorithms.NONE.getId() + + "') are disallowed by default as mandated by " + + "https://datatracker.ietf.org/doc/html/rfc7518#section-3.6. If you wish to allow them to be " + + "parsed, call the JwtParserBuilder.enableUnsecuredJws() method (but please read the " + + "security considerations covered in that method's JavaDoc before doing so). Header: "; + + private static final String JWE_NONE_MSG = + "JWEs do not support key management " + DefaultHeader.ALGORITHM + + " header value 'none' per https://datatracker.ietf.org/doc/html/rfc7518#section-4.1"; + + private static final String JWS_NONE_SIG_MISMATCH_MSG = + "The JWS header references signature algorithm 'none' yet the " + + "compact JWS string contains a signature. This is not permitted per " + + "https://tools.ietf.org/html/rfc7518#section-3.6."; + private static , R extends Identifiable> Function backup(String id, String msg, Collection extras) { if (Collections.isEmpty(extras)) { return ConstantFunction.forNull(); @@ -324,22 +340,12 @@ public boolean isSigned(String compact) { if (compact == null) { return false; } - - int delimiterCount = 0; - - for (int i = 0; i < compact.length(); i++) { - char c = compact.charAt(i); - - if (delimiterCount == 2) { - return !Character.isWhitespace(c) && c != SEPARATOR_CHAR; - } - - if (c == SEPARATOR_CHAR) { - delimiterCount++; - } + try { + final TokenizedJwt tokenized = jwtTokenizer.tokenize(compact); + return (!(tokenized instanceof TokenizedJwe)) && Strings.hasText(tokenized.getDigest()); + } catch (MalformedJwtException e) { + return false; } - - return false; } private static boolean hasContentType(Header header) { @@ -371,7 +377,13 @@ private static boolean hasContentType(Header header) { final byte[] headerBytes = base64UrlDecode(base64UrlHeader, "protected header"); String origValue = new String(headerBytes, Strings.UTF_8); Map m = readValue(origValue, "protected header"); - Header header = tokenized.createHeader(m); + Header header; + try { + header = tokenized.createHeader(m); + } catch (Exception e) { + String msg = "Invalid protected header: " + e.getMessage(); + throw new MalformedJwtException(msg, e); + } // https://tools.ietf.org/html/rfc7515#section-10.7 , second-to-last bullet point, note the use of 'always': // @@ -385,24 +397,16 @@ private static boolean hasContentType(Header header) { throw new MalformedJwtException(msg); } if (SignatureAlgorithms.NONE.getId().equalsIgnoreCase(alg)) { + if (tokenized instanceof TokenizedJwe) { + throw new MalformedJwtException(JWE_NONE_MSG); + } + // else it's a JWS: if (!enableUnsecuredJws) { - String msg = "Unsecured JWTs (those with an " + DefaultHeader.ALGORITHM + - " header value of '" + SignatureAlgorithms.NONE.getId() + - "') are disallowed by default as mandated by " + - "https://datatracker.ietf.org/doc/html/rfc7518#section-3.6. If you wish to allow them to be " + - "parsed, call the JwtParserBuilder.enableUnsecuredJws() method (but please read the " + - "security considerations covered in that method's JavaDoc before doing so). Header: " + - header; + String msg = UNSECURED_DISABLED_MSG_PREFIX + header; throw new UnsupportedJwtException(msg); } if (Strings.hasText(tokenized.getDigest())) { - String type = tokenized instanceof TokenizedJwe ? "JWE" : "JWS"; - String algType = tokenized instanceof TokenizedJwe ? "key management" : "signature"; - String digestType = tokenized instanceof TokenizedJwe ? "an AAD authentication tag" : "a signature"; - String msg = "The " + type + " header references " + algType + " algorithm '" + alg + "' yet the " + - "compact " + type + " string has " + digestType + " token. This is not permitted per " + - "https://tools.ietf.org/html/rfc7518#section-3.6."; - throw new MalformedJwtException(msg); + throw new MalformedJwtException(JWS_NONE_SIG_MISMATCH_MSG); } } else { // something other than 'none'. Must have a digest component: if (!Strings.hasText(tokenized.getDigest())) { @@ -410,7 +414,7 @@ private static boolean hasContentType(Header header) { String algType = tokenized instanceof TokenizedJwe ? "key management" : "signature"; String digestType = tokenized instanceof TokenizedJwe ? "AAD authentication tag" : "signature"; String msg = "The " + type + " header references " + algType + " algorithm '" + alg + "' but the " + - "compact " + type + " string is missing the required " + digestType + " token."; + "compact " + type + " string is missing the required " + digestType + "."; throw new MalformedJwtException(msg); } } @@ -510,7 +514,12 @@ private static boolean hasContentType(Header header) { Claims claims = null; if (!payload.isEmpty() && !hasContentType(header) && payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it: Map claimsMap = readValue(payload, "claims"); - claims = new DefaultClaims(claimsMap); + try { + claims = new DefaultClaims(claimsMap); + } catch (Exception e) { + String msg = "Invalid claims: " + e.getMessage(); + throw new MalformedJwtException(msg, e); + } } Jwt jwt; @@ -815,7 +824,7 @@ protected byte[] base64UrlDecode(String base64UrlEncoded, String name) { try { byte[] bytes = val.getBytes(Strings.UTF_8); return deserializer.deserialize(bytes); - } catch (DeserializationException e) { + } catch (MalformedJwtException | DeserializationException e) { throw new MalformedJwtException("Unable to read " + name + " JSON: " + val, e); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 88ee9c5bb..9ee12f81e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -40,9 +40,8 @@ public class JwtMap implements Map { static final String REDACTED_VALUE = ""; - protected final Map values; // canonical values formatted per RFC requirements - protected final Map idiomaticValues; // the values map with any string/encoded values converted to Java type-safe values where possible + protected final Map idiomaticValues; // the values map with any RFC values converted to Java type-safe values where possible protected final Map redactedValues; // the values map with any sensitive/secret values redacted. Used in the toString implementation. protected final Map> FIELDS; @@ -92,7 +91,7 @@ protected Object idiomaticGet(String key) { @SuppressWarnings("unchecked") protected T idiomaticGet(Field field) { - return (T)this.idiomaticValues.get(field.getId()); + return (T) this.idiomaticValues.get(field.getId()); } @Override @@ -144,7 +143,7 @@ private Object idiomaticPut(String name, Object value) { } protected Object nullSafePut(String name, Object value) { - if (JwtMap.isReduceableToNull(value)) { + if (isReduceableToNull(value)) { return remove(name); } else { Object redactedValue = isSecret(name) ? REDACTED_VALUE : value; @@ -165,7 +164,6 @@ private JwtException malformed(String msg, Exception cause) { protected Object apply(Field field, Object rawValue) { final String id = field.getId(); - final Object previousValue = get(id); if (isReduceableToNull(rawValue)) { return remove(id); @@ -175,20 +173,20 @@ protected Object apply(Field field, Object rawValue) { Object canonicalValue; // as required by the RFC try { idiomaticValue = field.applyFrom(rawValue); - Assert.notNull(idiomaticValue, "Converted idiomaticValue cannot be null."); + Assert.notNull(idiomaticValue, "Converter's resulting idiomaticValue cannot be null."); canonicalValue = field.applyTo(idiomaticValue); - Assert.notNull(canonicalValue, "Converted canonicalValue cannot be null."); - } catch (Exception e) { + Assert.notNull(canonicalValue, "Converter's resulting canonicalValue cannot be null."); + } catch (IllegalArgumentException e) { Object sval = field.isSecret() ? REDACTED_VALUE : rawValue; - String msg = "Invalid " + name() + field + " value [" + sval + "]: " + e.getMessage(); - throw malformed(msg, e); + String msg = "Invalid " + getName() + " " + field + " value [" + sval + "]. Cause: " + e.getMessage(); + throw new IllegalArgumentException(msg, e); } - nullSafePut(id, canonicalValue); + Object retval = nullSafePut(id, canonicalValue); this.idiomaticValues.put(id, idiomaticValue); - return previousValue; + return retval; } - private String name() { + private String getName() { if (this instanceof JweHeader) { return "JWE header"; } else if (this instanceof JwsHeader) { @@ -255,6 +253,7 @@ public int hashCode() { return values.hashCode(); } + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") @Override public boolean equals(Object obj) { return values.equals(obj); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/BiFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/BiFunction.java deleted file mode 100644 index dba519e74..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/BiFunction.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.jsonwebtoken.impl.lang; - -public interface BiFunction { - - R apply(T t, U u); -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUnsignedBytesConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverter.java similarity index 94% rename from impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUnsignedBytesConverter.java rename to impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverter.java index 9976dd134..124e20459 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUnsignedBytesConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverter.java @@ -4,7 +4,7 @@ import java.math.BigInteger; -public class BigIntegerUnsignedBytesConverter implements Converter { +public class BigIntegerUBytesConverter implements Converter { // Copied from Apache Commons Codec 1.14: // https://github.com/apache/commons-codec/blob/af7b94750e2178b8437d9812b28e36ac87a455f2/src/main/java/org/apache/commons/codec/binary/Base64.java#L746-L775 diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java index a99566a46..946ba5a15 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java @@ -18,7 +18,7 @@ public final class Converters { public static final Converter X509_CERTIFICATE = Converters.forEncoded(X509Certificate.class, new JwkX509StringConverter()); - public static final Converter BIGINT_UNSIGNED_BYTES = new BigIntegerUnsignedBytesConverter(); + public static final Converter BIGINT_UNSIGNED_BYTES = new BigIntegerUBytesConverter(); public static final Converter BIGINT = Converters.forEncoded(BigInteger.class, compound(BIGINT_UNSIGNED_BYTES, CodecConverter.BASE64URL)); @@ -27,8 +27,8 @@ public final class Converters { private Converters() { } - public static Converter none(Class clazz) { - return new NoConverter<>(clazz); + public static Converter forType(Class clazz) { + return new RequiredTypeConverter<>(clazz); } public static Converter, Object> forSet(Converter elementConverter) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultField.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultField.java index b3cdaf8f0..d33c8f2ad 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultField.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultField.java @@ -41,11 +41,6 @@ public boolean isSecret() { return SECRET; } - @Override - public Converter getConverter() { - return this.CONVERTER; - } - @Override public int hashCode() { return this.ID.hashCode(); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java index fa2798a5b..72faa2c71 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java @@ -63,16 +63,14 @@ public FieldBuilder setConverter(Converter converter) { @SuppressWarnings({"rawtypes", "unchecked"}) @Override public Field build() { - Assert.notNull(this.type, "Type must be set."); Converter converter = this.converter; if (converter == null) { - converter = Converters.none(this.type); + converter = Converters.forType(this.type); } if (this.list != null) { converter = this.list ? Converters.forList(converter) : Converters.forSet(converter); } - return new DefaultField<>(this.id, this.name, this.secret, this.type, converter); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Field.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Field.java index 9ad8ce420..476810e10 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Field.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Field.java @@ -9,7 +9,4 @@ public interface Field extends Identifiable, Converter { Class getIdiomaticType(); boolean isSecret(); - - Converter getConverter(); - } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java index 8f358e2f1..2059f978d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java @@ -13,7 +13,7 @@ private Fields() { // prevent instantiation } public static Field string(String id, String name) { - return builder(String.class).setConverter(Converters.none(String.class)).setId(id).setName(name).build(); + return builder(String.class).setId(id).setName(name).build(); } public static Field rfcDate(String id, String name) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java index 12f68c3a7..e86772624 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java @@ -16,7 +16,7 @@ public Object applyTo(Date date) { return null; } // https://datatracker.ietf.org/doc/html/rfc7519#section-2, 'Numeric Date' definition: - return date.getTime() / 1000; + return date.getTime() / 1000L; } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/NoConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredTypeConverter.java similarity index 84% rename from impl/src/main/java/io/jsonwebtoken/impl/lang/NoConverter.java rename to impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredTypeConverter.java index e34009060..38d035208 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/NoConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredTypeConverter.java @@ -2,11 +2,11 @@ import io.jsonwebtoken.lang.Assert; -class NoConverter implements Converter { +class RequiredTypeConverter implements Converter { private final Class type; - public NoConverter(Class type) { + public RequiredTypeConverter(Class type) { this.type = Assert.notNull(type, "type argument cannot be null."); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java index 3a4508236..740b45426 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java @@ -18,7 +18,7 @@ public abstract class AbstractAsymmetricJwk extends AbstractJwk X5T = Fields.bytes("x5t", "X.509 Certificate SHA-1 Thumbprint").build(); public static final Field X5T_S256 = Fields.bytes("x5t#S256", "X.509 Certificate SHA-256 Thumbprint").build(); public static final Field X5U = Fields.uri("x5u", "X.509 URL"); - static final Set> FIELDS = Collections.immutable(Collections.concat(AbstractJwk.FIELDS, USE, X5C, X5T, X5T_S256, X5U)); + static final Set> FIELDS = Collections.concat(AbstractJwk.FIELDS, USE, X5C, X5T, X5T_S256, X5U); AbstractAsymmetricJwk(JwkContext ctx) { super(ctx); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java index a3d0fa1b1..31922b01c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java @@ -1,5 +1,6 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; @@ -50,8 +51,8 @@ public AbstractAsymmetricJwkBuilder(JwkContext ctx) { super(ctx); } - AbstractAsymmetricJwkBuilder(AbstractAsymmetricJwkBuilder b, K key, Set privateNames) { - super(new DefaultJwkContext<>(privateNames, b.jwkContext, key)); + AbstractAsymmetricJwkBuilder(AbstractAsymmetricJwkBuilder b, K key, Set> fields) { + super(new DefaultJwkContext<>(fields, b.jwkContext, key)); this.computeX509Sha1Thumbprint = b.computeX509Sha1Thumbprint; this.computeX509Sha256Thumbprint = b.computeX509Sha256Thumbprint; this.applyX509KeyUse = b.applyX509KeyUse; @@ -181,8 +182,8 @@ private abstract static class DefaultPrivateJwkBuilder b, K key, Set privateNames) { - super(b, key, privateNames); + DefaultPrivateJwkBuilder(DefaultPublicJwkBuilder b, K key, Set> fields) { + super(b, key, fields); this.jwkContext.setPublicKey(b.jwkContext.getKey()); } @@ -198,7 +199,7 @@ static class DefaultEcPublicJwkBuilder implements EcPublicJwkBuilder { DefaultEcPublicJwkBuilder(JwkContext src, ECPublicKey key) { - super(new DefaultJwkContext<>(DefaultEcPrivateJwk.PRIVATE_NAMES, src, key)); + super(new DefaultJwkContext<>(DefaultEcPublicJwk.FIELDS, src, key)); } @Override @@ -212,7 +213,7 @@ static class DefaultRsaPublicJwkBuilder implements RsaPublicJwkBuilder { DefaultRsaPublicJwkBuilder(JwkContext ctx, RSAPublicKey key) { - super(new DefaultJwkContext<>(DefaultRsaPrivateJwk.PRIVATE_NAMES, ctx, key)); + super(new DefaultJwkContext<>(DefaultRsaPublicJwk.FIELDS, ctx, key)); } @Override @@ -226,11 +227,11 @@ static class DefaultEcPrivateJwkBuilder implements EcPrivateJwkBuilder { DefaultEcPrivateJwkBuilder(JwkContext src, ECPrivateKey key) { - super(new DefaultJwkContext<>(DefaultEcPrivateJwk.PRIVATE_NAMES, src, key)); + super(new DefaultJwkContext<>(DefaultEcPrivateJwk.FIELDS, src, key)); } DefaultEcPrivateJwkBuilder(DefaultEcPublicJwkBuilder b, ECPrivateKey key) { - super(b, key, DefaultEcPrivateJwk.PRIVATE_NAMES); + super(b, key, DefaultEcPrivateJwk.FIELDS); } } @@ -239,11 +240,11 @@ static class DefaultRsaPrivateJwkBuilder implements RsaPrivateJwkBuilder { DefaultRsaPrivateJwkBuilder(JwkContext src, RSAPrivateKey key) { - super(new DefaultJwkContext<>(DefaultRsaPrivateJwk.PRIVATE_NAMES, src, key)); + super(new DefaultJwkContext<>(DefaultRsaPrivateJwk.FIELDS, src, key)); } DefaultRsaPrivateJwkBuilder(DefaultRsaPublicJwkBuilder b, RSAPrivateKey key) { - super(b, key, DefaultRsaPrivateJwk.PRIVATE_NAMES); + super(b, key, DefaultRsaPrivateJwk.FIELDS); } } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java index f53301b88..e0c0e4d6d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java @@ -17,10 +17,8 @@ public abstract class AbstractJwk implements Jwk { public static final Field KID = Fields.string("kid", "Key ID"); static final Field> KEY_OPS = Fields.stringSet("key_ops", "Key Operations"); static final Field KTY = Fields.string("kty", "Key Type"); - @SuppressWarnings("RedundantTypeArguments") - static final Set> FIELDS = Collections.immutable(Collections.>setOf(ALG, KID, KEY_OPS, KTY)); + static final Set> FIELDS = Collections.setOf(ALG, KID, KEY_OPS, KTY); - static final String REDACTED_VALUE = ""; public static final String IMMUTABLE_MSG = "JWKs are immutable may not be modified."; protected final JwkContext context; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java index 532cf6831..8525bb71d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java @@ -3,6 +3,7 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.Jwk; import io.jsonwebtoken.security.JwkBuilder; +import io.jsonwebtoken.security.MalformedKeyException; import io.jsonwebtoken.security.SecretJwk; import io.jsonwebtoken.security.SecretJwkBuilder; @@ -83,14 +84,14 @@ public J build() { } catch (IllegalArgumentException iae) { //if we get an IAE, it means the builder state wasn't configured enough in order to create String msg = "Unable to create JWK: " + iae.getMessage(); - throw new IllegalStateException(msg, iae); + throw new MalformedKeyException(msg, iae); } } static class DefaultSecretJwkBuilder extends AbstractJwkBuilder implements SecretJwkBuilder { public DefaultSecretJwkBuilder(JwkContext ctx, SecretKey key) { - super(new DefaultJwkContext<>(DefaultSecretJwk.PRIVATE_NAMES, ctx, key)); + super(new DefaultJwkContext<>(DefaultSecretJwk.FIELDS, ctx, key)); } } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java index 88616ffc3..83874a4e8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java @@ -14,8 +14,7 @@ class DefaultEcPrivateJwk extends AbstractPrivateJwk implements EcPrivateJwk { static final Field D = Fields.secretBigInt("d", "ECC Private Key"); - static final Set> FIELDS = Collections.immutable(Collections.concat(DefaultEcPublicJwk.FIELDS, D)); - static final Set PRIVATE_NAMES = Collections.setOf(D.getId()); + static final Set> FIELDS = Collections.concat(DefaultEcPublicJwk.FIELDS, D); DefaultEcPrivateJwk(JwkContext ctx, EcPublicJwk pubJwk) { super(ctx, pubJwk); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java index 1bd893edb..42571870a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java @@ -15,7 +15,7 @@ class DefaultEcPublicJwk extends AbstractPublicJwk implements EcPub static final Field CRV = Fields.string("crv", "Curve"); static final Field X = Fields.bigInt("x", "X Coordinate").build(); static final Field Y = Fields.bigInt("y", "Y Coordinate").build(); - static final Set> FIELDS = Collections.immutable(Collections.concat(AbstractAsymmetricJwk.FIELDS, CRV, X, Y)); + static final Set> FIELDS = Collections.concat(AbstractAsymmetricJwk.FIELDS, CRV, X, Y); DefaultEcPublicJwk(JwkContext ctx) { super(ctx); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java index 174ca8a49..97ad6324f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java @@ -1,63 +1,31 @@ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.Identifiable; import io.jsonwebtoken.impl.JwtMap; -import io.jsonwebtoken.impl.lang.BiFunction; -import io.jsonwebtoken.impl.lang.Converter; import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; -import io.jsonwebtoken.lang.Objects; -import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.MalformedKeyException; import java.net.URI; import java.security.Key; import java.security.Provider; import java.security.PublicKey; import java.security.cert.X509Certificate; -import java.util.Collection; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -public class DefaultJwkContext implements JwkContext { - - private static final Set DEFAULT_PRIVATE_NAMES; - private static final Map> SETTERS; - - static { - Set set = new LinkedHashSet<>(); - set.addAll(DefaultRsaPrivateJwk.PRIVATE_NAMES); - set.addAll(DefaultEcPrivateJwk.PRIVATE_NAMES); - set.addAll(DefaultSecretJwk.PRIVATE_NAMES); - DEFAULT_PRIVATE_NAMES = java.util.Collections.unmodifiableSet(set); - - @SuppressWarnings("RedundantTypeArguments") - List> fns = Collections.>of( - Canonicalizer.forField(AbstractJwk.ALG), - Canonicalizer.forField(AbstractJwk.KID), - Canonicalizer.forField(AbstractJwk.KEY_OPS), - Canonicalizer.forField(AbstractJwk.KTY), - Canonicalizer.forField(AbstractAsymmetricJwk.USE), - Canonicalizer.forField(AbstractAsymmetricJwk.X5C), - Canonicalizer.forField(AbstractAsymmetricJwk.X5T), - Canonicalizer.forField(AbstractAsymmetricJwk.X5T_S256), - Canonicalizer.forField(AbstractAsymmetricJwk.X5U) - ); - Map> s = new LinkedHashMap<>(); - for (Canonicalizer fn : fns) { - s.put(fn.getId(), fn); - } - SETTERS = java.util.Collections.unmodifiableMap(s); +public class DefaultJwkContext extends JwtMap implements JwkContext { + + private static final Set> DEFAULT_FIELDS; + static { // assume all known fields: + Set> set = new LinkedHashSet<>(); + set.addAll(DefaultSecretJwk.FIELDS); // Private/Secret JWKs has both public and private fields + set.addAll(DefaultEcPrivateJwk.FIELDS); // Private JWKs have both public and private fields + set.addAll(DefaultRsaPrivateJwk.FIELDS); // Private JWKs have both public and private fields + DEFAULT_FIELDS = Collections.immutable(set); } - private final Map values; // canonical values formatted per RFC requirements - private final Map idiomaticValues; // the values map with any string/encoded values converted to Java type-safe values where possible - private final Map redactedValues; // the values map with any sensitive/secret values redacted. Used in the toString implementation. - private final Set privateMemberNames; // names of values that should be redacted for toString output private K key; private PublicKey publicKey; private Provider provider; @@ -65,136 +33,47 @@ public class DefaultJwkContext implements JwkContext { public DefaultJwkContext() { // For the default constructor case, we don't know how it will be used or what values will be populated, // so we can't know ahead of time what the sensitive data is. As such, for security reasons, we assume all - // the known private names for all supported algorithms in case it is used for any of them: - this(DEFAULT_PRIVATE_NAMES); + // the known fields for all supported keys/algorithms in case it is used for any of them: + this(DEFAULT_FIELDS); } - public DefaultJwkContext(Set privateMemberNames) { - this.privateMemberNames = Assert.notEmpty(privateMemberNames, "privateMemberNames cannot be null or empty."); - this.values = new LinkedHashMap<>(); - this.idiomaticValues = new LinkedHashMap<>(); - this.redactedValues = new LinkedHashMap<>(); + public DefaultJwkContext(Set> fields) { + super(fields); } - public DefaultJwkContext(Set privateMemberNames, JwkContext other) { - this(privateMemberNames, other, true); + public DefaultJwkContext(Set> fields, JwkContext other) { + this(fields, other, true); } - public DefaultJwkContext(Set privateMemberNames, JwkContext other, K key) { + public DefaultJwkContext(Set> fields, JwkContext other, K key) { //if the key is null or a PublicKey, we don't want to redact - we want to fully remove the items that are //private names (public JWKs should never contain any private key fields, even if redacted): - this(privateMemberNames, other, (key == null || key instanceof PublicKey)); + this(fields, other, (key == null || key instanceof PublicKey)); this.key = Assert.notNull(key, "Key cannot be null."); } - private DefaultJwkContext(Set privateMemberNames, JwkContext other, boolean removePrivate) { - this.privateMemberNames = Assert.notEmpty(privateMemberNames, "privateMemberNames cannot be null or empty."); + private DefaultJwkContext(Set> fields, JwkContext other, boolean removePrivate) { + super(Assert.notEmpty(fields, "Fields cannot be null or empty.")); Assert.notNull(other, "JwkContext cannot be null."); Assert.isInstanceOf(DefaultJwkContext.class, other, "JwkContext must be a DefaultJwkContext instance."); DefaultJwkContext src = (DefaultJwkContext) other; this.provider = other.getProvider(); - this.values = new LinkedHashMap<>(src.values); - this.idiomaticValues = new LinkedHashMap<>(src.idiomaticValues); - this.redactedValues = new LinkedHashMap<>(src.redactedValues); + this.values.putAll(src.values); + this.idiomaticValues.putAll(src.idiomaticValues); + this.redactedValues.putAll(src.redactedValues); if (removePrivate) { - for (String name : this.privateMemberNames) { - remove(name); + for(Field field : src.FIELDS.values()) { + if (field.isSecret()) { + remove(field.getId()); + } } } } - protected Object nullSafePut(String name, Object value) { - if (JwtMap.isReduceableToNull(value)) { - return remove(name); - } else { - Object redactedValue = this.privateMemberNames.contains(name) ? AbstractJwk.REDACTED_VALUE : value; - this.redactedValues.put(name, redactedValue); - this.idiomaticValues.put(name, value); - return this.values.put(name, value); - } - } - - @Override - public Object put(String name, Object value) { - name = Assert.notNull(Strings.clean(name), "JWK member name cannot be null or empty."); - if (value instanceof String) { - value = Strings.clean((String) value); - } else if (Objects.isArray(value) && !value.getClass().getComponentType().isPrimitive()) { - value = Collections.arrayToList(value); - } - return idiomaticPut(name, value); - } - - // ensures that if a property name matches an RFC-specified name, that value can be represented - // as an idiomatic type-safe Java value in addition to the canonical RFC/encoded value. - private Object idiomaticPut(String name, Object value) { - assert name != null; //asserted by caller. - Canonicalizer fn = SETTERS.get(name); - if (fn != null) { //Setting a JWA-standard property - let's ensure we can represent it idiomatically: - return fn.apply(this, value); - } else { //non-standard/custom property: - return nullSafePut(name, value); - } - } - @Override public void putAll(Map m) { Assert.notEmpty(m, "JWK values cannot be null or empty."); - for (Map.Entry entry : m.entrySet()) { - put(entry.getKey(), entry.getValue()); - } - } - - @Override - public Object remove(Object key) { - this.redactedValues.remove(key); - this.idiomaticValues.remove(key); - return this.values.remove(key); - } - - @Override - public int size() { - return this.values.size(); - } - - @Override - public boolean isEmpty() { - return this.values.isEmpty(); - } - - @Override - public boolean containsKey(Object key) { - return this.values.containsKey(key); - } - - @Override - public boolean containsValue(Object value) { - return this.values.containsValue(value); - } - - @Override - public Object get(Object key) { - return this.values.get(key); - } - - @Override - public void clear() { - throw new UnsupportedOperationException("Cannot clear JwkContext objects."); - } - - @Override - public Set keySet() { - return this.values.keySet(); - } - - @Override - public Collection values() { - return this.values.values(); - } - - @Override - public Set> entrySet() { - return this.values.entrySet(); + super.putAll(m); } @Override @@ -330,75 +209,4 @@ public JwkContext setProvider(Provider provider) { this.provider = provider; return this; } - - @Override - public Set getPrivateMemberNames() { - return this.privateMemberNames; - } - - @Override - public int hashCode() { - return this.values.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof Map) { - return this.values.equals(obj); - } - return false; - } - - @Override - public String toString() { - return this.redactedValues.toString(); - } - - private static class Canonicalizer implements BiFunction, Object, T>, Identifiable { - - private final String id; - private final String title; - private final Converter converter; - - public static Canonicalizer forField(Field field) { - return new Canonicalizer<>(field); - } - - public Canonicalizer(Field field) { - this.id = field.getId(); - this.title = field.getName(); - this.converter = field.getConverter(); - } - - @Override - public String getId() { - return this.id; - } - - @Override - public T apply(DefaultJwkContext ctx, Object rawValue) { - - //noinspection unchecked - final T previousValue = (T)ctx.idiomaticValues.get(id); - - if (JwtMap.isReduceableToNull(rawValue)) { - ctx.remove(id); - return previousValue; - } - - T idiomaticValue; // preferred Java format - Object canonicalValue; //as required by the RFC - try { - idiomaticValue = converter.applyFrom(rawValue); - canonicalValue = converter.applyTo(idiomaticValue); - } catch (Exception e) { - Object sval = ctx.privateMemberNames.contains(id) ? AbstractJwk.REDACTED_VALUE : rawValue; - String msg = "Invalid JWK '" + id + "' (" + title + ") value [" + sval + "]: " + e.getMessage(); - throw new MalformedKeyException(msg, e); - } - ctx.nullSafePut(id, canonicalValue); - ctx.idiomaticValues.put(id, idiomaticValue); - return previousValue; - } - } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java index d20b82967..4c0436f2f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java @@ -10,7 +10,6 @@ import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.RSAOtherPrimeInfo; -import java.util.LinkedHashSet; import java.util.Set; class DefaultRsaPrivateJwk extends AbstractPrivateJwk implements RsaPrivateJwk { @@ -26,22 +25,10 @@ class DefaultRsaPrivateJwk extends AbstractPrivateJwk> FIELDS = Collections.immutable(Collections.concat(DefaultRsaPublicJwk.FIELDS, + static final Set> FIELDS = Collections.concat(DefaultRsaPublicJwk.FIELDS, PRIVATE_EXPONENT, FIRST_PRIME, SECOND_PRIME, FIRST_CRT_EXPONENT, SECOND_CRT_EXPONENT, FIRST_CRT_COEFFICIENT, OTHER_PRIMES_INFO - )); - - static final Set PRIVATE_NAMES; - static { - Set names = new LinkedHashSet<>(); - for (Field field : FIELDS) { - if (field.isSecret()) { - names.add(field.getId()); - } - } - PRIVATE_NAMES = java.util.Collections.unmodifiableSet(names); - } - static final Set OPTIONAL_PRIVATE_NAMES = Collections.immutable(Collections.setOf(PRIVATE_EXPONENT.getId())); + ); DefaultRsaPrivateJwk(JwkContext ctx, RsaPublicJwk pubJwk) { super(ctx, pubJwk); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java index 3218c6540..53cd9699b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java @@ -14,7 +14,7 @@ class DefaultRsaPublicJwk extends AbstractPublicJwk implements Rsa static final String TYPE_VALUE = "RSA"; static final Field MODULUS = Fields.bigInt("n", "Modulus").build(); static final Field PUBLIC_EXPONENT = Fields.bigInt("e", "Public Exponent").build(); - static final Set> FIELDS = Collections.immutable(Collections.concat(AbstractAsymmetricJwk.FIELDS, MODULUS, PUBLIC_EXPONENT)); + static final Set> FIELDS = Collections.concat(AbstractAsymmetricJwk.FIELDS, MODULUS, PUBLIC_EXPONENT); DefaultRsaPublicJwk(JwkContext ctx) { super(ctx); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java index 0c973befd..6af4ab0d9 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java @@ -12,7 +12,7 @@ class DefaultSecretJwk extends AbstractJwk implements SecretJwk { static final String TYPE_VALUE = "oct"; static final Field K = Fields.bytes("k", "Key Value").setSecret(true).build(); - static final Set PRIVATE_NAMES = java.util.Collections.unmodifiableSet(Collections.setOf(K.getId())); + static final Set> FIELDS = Collections.concat(AbstractJwk.FIELDS, K); DefaultSecretJwk(JwkContext ctx) { super(ctx); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java index 73f1bf6af..247b1afe3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java @@ -42,7 +42,7 @@ protected EcPrivateJwk createJwkFromKey(JwkContext ctx) { // [JWA spec](https://tools.ietf.org/html/rfc7518#section-6.2.2) // requires public values to be present in private JWKs, so add them: - JwkContext pubCtx = new DefaultJwkContext<>(DefaultEcPrivateJwk.PRIVATE_NAMES, ctx, ecPublicKey); + JwkContext pubCtx = new DefaultJwkContext<>(DefaultEcPublicJwk.FIELDS, ctx, ecPublicKey); EcPublicJwk pubJwk = EcPublicJwkFactory.DEFAULT_INSTANCE.createJwk(pubCtx); ctx.putAll(pubJwk); // add public values to private key context @@ -63,7 +63,7 @@ protected EcPrivateJwk createJwkFromValues(final JwkContext ctx) { // We don't actually need the public x,y point coordinates for JVM lookup, but the // [JWA spec](https://tools.ietf.org/html/rfc7518#section-6.2.2) // requires them to be present and valid for the private key as well, so we assert that here: - JwkContext pubCtx = new DefaultJwkContext<>(DefaultEcPrivateJwk.PRIVATE_NAMES, ctx); + JwkContext pubCtx = new DefaultJwkContext<>(DefaultEcPublicJwk.FIELDS, ctx); EcPublicJwk pubJwk = EcPublicJwkFactory.DEFAULT_INSTANCE.createJwk(pubCtx); ECParameterSpec spec = getCurveByJwaId(curveId); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java index d6c84ac83..1724afe97 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java @@ -55,8 +55,6 @@ public interface JwkContext extends Identifiable, Map setPublicKey(PublicKey publicKey); - Set getPrivateMemberNames(); - Provider getProvider(); JwkContext setProvider(Provider provider); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java index 02b38d43b..4d1c1e42d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java @@ -30,9 +30,17 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; class RsaPrivateJwkFactory extends AbstractFamilyJwkFactory { + //All RSA Private fields _except_ for PRIVATE_EXPONENT. That is always required: + private static final Set> OPTIONAL_PRIVATE_FIELDS = Collections.setOf( + DefaultRsaPrivateJwk.FIRST_PRIME, DefaultRsaPrivateJwk.SECOND_PRIME, + DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, + DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT + ); + static final Converter, Object> RSA_OTHER_PRIMES_CONVERTER = Converters.forList(new RSAOtherPrimeInfoConverter()); @@ -97,7 +105,7 @@ protected RsaPrivateJwk createJwkFromKey(JwkContext ctx) { // The [JWA Spec](https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.1) // requires public values to be present in private JWKs, so add them: - JwkContext pubCtx = new DefaultJwkContext<>(DefaultRsaPrivateJwk.PRIVATE_NAMES, ctx, rsaPublicKey); + JwkContext pubCtx = new DefaultJwkContext<>(DefaultRsaPublicJwk.FIELDS, ctx, rsaPublicKey); RsaPublicJwk pubJwk = RsaPublicJwkFactory.DEFAULT_INSTANCE.createJwk(pubCtx); ctx.putAll(pubJwk); // add public values to private key context @@ -137,7 +145,7 @@ protected RsaPrivateJwk createJwkFromValues(JwkContext ctx) { //The [JWA Spec, Section 6.3.2](https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.2) requires //RSA Private Keys to also encode the public key values, so we assert that we can acquire it successfully: - JwkContext pubCtx = new DefaultJwkContext<>(DefaultRsaPrivateJwk.PRIVATE_NAMES, ctx); + JwkContext pubCtx = new DefaultJwkContext<>(DefaultRsaPublicJwk.FIELDS, ctx); RsaPublicJwk pubJwk = RsaPublicJwkFactory.DEFAULT_INSTANCE.createJwkFromValues(pubCtx); RSAPublicKey pubKey = pubJwk.toKey(); final BigInteger modulus = pubKey.getModulus(); @@ -151,8 +159,8 @@ protected RsaPrivateJwk createJwkFromValues(JwkContext ctx) { // factors were used // boolean containsOptional = false; - for (String optionalPrivateName : DefaultRsaPrivateJwk.OPTIONAL_PRIVATE_NAMES) { - if (ctx.containsKey(optionalPrivateName)) { + for (Field field : OPTIONAL_PRIVATE_FIELDS) { + if (ctx.containsKey(field.getId())) { containsOptional = true; break; } @@ -173,7 +181,7 @@ protected RsaPrivateJwk createJwkFromValues(JwkContext ctx) { Object value = ctx.get(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO.getId()); List otherPrimes = RSA_OTHER_PRIMES_CONVERTER.applyFrom(value); - RSAOtherPrimeInfo[] arr = new RSAOtherPrimeInfo[otherPrimes.size()]; + RSAOtherPrimeInfo[] arr = new RSAOtherPrimeInfo[Collections.size(otherPrimes)]; otherPrimes.toArray(arr); spec = new RSAMultiPrimePrivateCrtKeySpec(modulus, publicExponent, privateExponent, firstPrime, @@ -203,6 +211,7 @@ static class RSAOtherPrimeInfoConverter implements Converter PRIME_FACTOR = Fields.secretBigInt("r", "Prime Factor"); static final Field FACTOR_CRT_EXPONENT = Fields.secretBigInt("d", "Factor CRT Exponent"); static final Field FACTOR_CRT_COEFFICIENT = Fields.secretBigInt("t", "Factor CRT Coefficient"); + static final Set> FIELDS = Collections.>setOf(PRIME_FACTOR, FACTOR_CRT_EXPONENT, FACTOR_CRT_COEFFICIENT); @Override public Object applyTo(RSAOtherPrimeInfo info) { @@ -230,7 +239,7 @@ public RSAOtherPrimeInfo applyFrom(Object o) { // Need to add the values to a Context instance to satisfy the API contract of the getRequired* methods // called below. It's less than ideal, but it works: - JwkContext ctx = new DefaultJwkContext<>(DefaultRsaPrivateJwk.PRIVATE_NAMES); + JwkContext ctx = new DefaultJwkContext<>(FIELDS); for (Map.Entry entry : m.entrySet()) { String name = String.valueOf(entry.getKey()); ctx.put(name, entry.getValue()); diff --git a/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy index 5652bb00c..efe7b8d42 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy @@ -19,10 +19,8 @@ import io.jsonwebtoken.io.Deserializer import io.jsonwebtoken.jackson.io.JacksonDeserializer import org.junit.Test -import static org.hamcrest.CoreMatchers.is import static org.junit.Assert.assertEquals import static org.junit.Assert.assertNotNull -import static org.junit.Assert.assertThat class CustomObjectDeserializationTest { @@ -39,16 +37,16 @@ class CustomObjectDeserializationTest { String jwtString = Jwts.builder().claim("cust", customBean).compact() // no custom deserialization, object is a map - Jwt jwt = Jwts.parser().parseClaimsJwt(jwtString) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(jwtString) assertNotNull jwt assertEquals jwt.getBody().get('cust'), [key1: 'value1', key2: 42] // custom type for 'cust' claim Deserializer deserializer = new JacksonDeserializer([cust: CustomBean]) - jwt = Jwts.parser().deserializeJsonWith(deserializer).parseClaimsJwt(jwtString) + jwt = Jwts.parserBuilder().enableUnsecuredJws().deserializeJsonWith(deserializer).build().parseClaimsJwt(jwtString) assertNotNull jwt CustomBean result = jwt.getBody().get("cust", CustomBean) - assertThat result, is(customBean) + assertEquals customBean, result } static class CustomBean { diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy index 9bfcc529b..1993819b0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy @@ -63,7 +63,7 @@ class DeprecatedJwtParserTest { String bad = base64Url('{"alg":"none"}') + '.' + base64Url(junkPayload) + '.' try { - Jwts.parser().parse(bad) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(bad) fail() } catch (MalformedJwtException expected) { assertEquals 'Unable to read claims JSON: ' + junkPayload, expected.getMessage() @@ -123,10 +123,10 @@ class DeprecatedJwtParserTest { String bad = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(badSig) try { - Jwts.parserBuilder().setSigningKey(randomKey()).build().parse(bad) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(randomKey()).build().parse(bad) fail() } catch (MalformedJwtException se) { - assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string has a digest/signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.getMessage() + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string contains a signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.getMessage() } } @@ -158,7 +158,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { - Jwts.parser().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -176,7 +176,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact() try { - Jwts.parser().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact) fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') @@ -193,7 +193,7 @@ class DeprecatedJwtParserTest { String subject = 'Joe' String compact = Jwts.builder().setSubject(subject).setExpiration(exp).compact() - Jwt jwt = Jwts.parser().setAllowedClockSkewSeconds(10).parse(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(10).build().parse(compact) assertEquals jwt.getBody().getSubject(), subject } @@ -205,7 +205,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { - Jwts.parser().setAllowedClockSkewSeconds(1).parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(1).build().parse(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -219,7 +219,7 @@ class DeprecatedJwtParserTest { String subject = 'Joe' String compact = Jwts.builder().setSubject(subject).setNotBefore(exp).compact() - Jwt jwt = Jwts.parser().setAllowedClockSkewSeconds(10).parse(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(10).build().parse(compact) assertEquals jwt.getBody().getSubject(), subject } @@ -231,7 +231,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(exp).compact() try { - Jwts.parser().setAllowedClockSkewSeconds(1).parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(1).build().parse(compact) fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') @@ -249,7 +249,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() - Jwt jwt = Jwts.parser().parsePlaintextJwt(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parsePlaintextJwt(compact) assertEquals jwt.getBody(), payload } @@ -260,7 +260,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').compact() try { - Jwts.parser().parsePlaintextJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parsePlaintextJwt(compact) fail() } catch (UnsupportedJwtException e) { assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' @@ -306,7 +306,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() - Jwt jwt = Jwts.parser().parseClaimsJwt(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) assertEquals jwt.getBody().getSubject(), subject } @@ -319,7 +319,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parser().parseClaimsJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) fail() } catch (UnsupportedJwtException e) { assertEquals 'Unsigned plaintext JWTs are not supported.', e.getMessage() @@ -364,7 +364,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { - Jwts.parser().parseClaimsJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -379,7 +379,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact() try { - Jwts.parser().parseClaimsJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') @@ -414,7 +414,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parsePlaintextJws(compact) fail() } catch (UnsupportedJwtException e) { assertEquals 'Unsigned plaintext JWTs are not supported.', e.getMessage() @@ -431,7 +431,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() try { - Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parsePlaintextJws(compact) fail() } catch (UnsupportedJwtException e) { assertEquals 'Unsigned Claims JWTs are not supported.', e.getMessage() @@ -527,7 +527,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseClaimsJws(compact) fail() } catch (UnsupportedJwtException e) { assertEquals 'Unsigned plaintext JWTs are not supported.', e.getMessage() @@ -544,7 +544,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() try { - Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseClaimsJws(compact) fail() } catch (UnsupportedJwtException e) { assertEquals 'Unsigned Claims JWTs are not supported.', e.getMessage() @@ -1394,7 +1394,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact() - Jwts.parser().setClock(new FixedClock(beforeExpiry)).parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setClock(new FixedClock(beforeExpiry)).build().parse(compact) } @Test @@ -1414,7 +1414,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact() try { - Jwts.parser().setClock(new DefaultClock()).parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setClock(new DefaultClock()).build().parse(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -1470,10 +1470,10 @@ class DeprecatedJwtParserTest { String jwtStr = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(sig) try { - Jwts.parser().parse(jwtStr) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(jwtStr) fail() } catch (MalformedJwtException se) { - assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string has a digest/signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.message + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string contains a signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.message } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy index 94c47e723..d18d058b7 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy @@ -20,9 +20,9 @@ import io.jsonwebtoken.impl.DefaultJwsHeader import io.jsonwebtoken.impl.JwtTokenizer import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec +import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer -import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.WeakKeyException @@ -121,7 +121,7 @@ class DeprecatedJwtsTest { String jwt = Jwts.builder().setClaims(claims).compact(); - def token = Jwts.parser().parse(jwt); + def token = Jwts.parserBuilder().enableUnsecuredJws().build().parse(jwt); //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims @@ -172,7 +172,7 @@ class DeprecatedJwtsTest { @Test void testParseWithHeaderOnly() { String unsecuredJwt = base64Url("{\"alg\":\"none\"}") + ".." - Jwt jwt = Jwts.parser().parse(unsecuredJwt) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parse(unsecuredJwt) assertEquals("none", jwt.getHeader().get("alg")) } @@ -184,7 +184,7 @@ class DeprecatedJwtsTest { @Test void testConvenienceIssuer() { String compact = Jwts.builder().setIssuer("Me").compact(); - Claims claims = Jwts.parser().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getIssuer(), "Me" compact = Jwts.builder().setSubject("Joe") @@ -192,14 +192,14 @@ class DeprecatedJwtsTest { .setIssuer(null) //null should remove it .compact(); - claims = Jwts.parser().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getIssuer() } @Test void testConvenienceSubject() { String compact = Jwts.builder().setSubject("Joe").compact(); - Claims claims = Jwts.parser().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getSubject(), "Joe" compact = Jwts.builder().setIssuer("Me") @@ -207,14 +207,14 @@ class DeprecatedJwtsTest { .setSubject(null) //null should remove it .compact(); - claims = Jwts.parser().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getSubject() } @Test void testConvenienceAudience() { String compact = Jwts.builder().setAudience("You").compact(); - Claims claims = Jwts.parser().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getAudience(), "You" compact = Jwts.builder().setIssuer("Me") @@ -222,7 +222,7 @@ class DeprecatedJwtsTest { .setAudience(null) //null should remove it .compact(); - claims = Jwts.parser().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getAudience() } @@ -252,7 +252,7 @@ class DeprecatedJwtsTest { void testConvenienceExpiration() { Date then = laterDate(); String compact = Jwts.builder().setExpiration(then).compact(); - Claims claims = Jwts.parser().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getExpiration() assertEquals claimedDate, then @@ -261,7 +261,7 @@ class DeprecatedJwtsTest { .setExpiration(null) //null should remove it .compact(); - claims = Jwts.parser().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getExpiration() } @@ -269,7 +269,7 @@ class DeprecatedJwtsTest { void testConvenienceNotBefore() { Date now = now() //jwt exp only supports *seconds* since epoch: String compact = Jwts.builder().setNotBefore(now).compact(); - Claims claims = Jwts.parser().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getNotBefore() assertEquals claimedDate, now @@ -278,7 +278,7 @@ class DeprecatedJwtsTest { .setNotBefore(null) //null should remove it .compact(); - claims = Jwts.parser().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getNotBefore() } @@ -286,7 +286,7 @@ class DeprecatedJwtsTest { void testConvenienceIssuedAt() { Date now = now() //jwt exp only supports *seconds* since epoch: String compact = Jwts.builder().setIssuedAt(now).compact(); - Claims claims = Jwts.parser().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getIssuedAt() assertEquals claimedDate, now @@ -295,7 +295,7 @@ class DeprecatedJwtsTest { .setIssuedAt(null) //null should remove it .compact(); - claims = Jwts.parser().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getIssuedAt() } @@ -303,7 +303,7 @@ class DeprecatedJwtsTest { void testConvenienceId() { String id = UUID.randomUUID().toString(); String compact = Jwts.builder().setId(id).compact(); - Claims claims = Jwts.parser().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getId(), id compact = Jwts.builder().setIssuer("Me") @@ -311,7 +311,7 @@ class DeprecatedJwtsTest { .setId(null) //null should remove it .compact(); - claims = Jwts.parser().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getId() } @@ -570,7 +570,7 @@ class DeprecatedJwtsTest { String notSigned = Jwts.builder().setSubject("Foo").compact() try { - Jwts.parser().setSigningKey(key).parseClaimsJws(notSigned) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseClaimsJws(notSigned) fail('parseClaimsJws must fail for unsigned JWTs') } catch (UnsupportedJwtException expected) { assertEquals expected.message, 'Unsigned Claims JWTs are not supported.' @@ -596,17 +596,17 @@ class DeprecatedJwtsTest { String forged = Jwts.builder().setSubject("Not Joe").compact() //assert that our forged header has a 'NONE' algorithm: - assertEquals 'none', Jwts.parser().parseClaimsJwt(forged).getHeader().get('alg') + assertEquals 'none', Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(forged).getHeader().get('alg') //now let's forge it by appending the signature the server expects: forged += signature //now assert that, when the server tries to parse the forged token, parsing fails: try { - Jwts.parser().setSigningKey(key).parse(forged) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parse(forged) fail("Parsing must fail for a forged token.") } catch (MalformedJwtException expected) { - assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string has a digest/signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', expected.message + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string contains a signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', expected.message } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 38a72368b..cd25d73ab 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -16,6 +16,7 @@ package io.jsonwebtoken import io.jsonwebtoken.impl.DefaultClock +import io.jsonwebtoken.impl.DefaultJwtParser import io.jsonwebtoken.impl.FixedClock import io.jsonwebtoken.impl.JwtTokenizer import io.jsonwebtoken.io.Encoders @@ -67,10 +68,10 @@ class JwtParserTest { String bad = base64Url('{"alg":"none"}') + '.' + base64Url(junkPayload) + '.' try { - Jwts.parserBuilder().build().parse(bad) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(bad) fail() } catch (MalformedJwtException expected) { - assertEquals expected.getMessage(), 'Malformed JWT JSON: ' + junkPayload + assertEquals 'Unable to read claims JSON: ' + junkPayload, expected.getMessage() } } @@ -127,14 +128,32 @@ class JwtParserTest { String bad = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(badSig) try { - Jwts.parserBuilder().setSigningKey(randomKey()).build().parse(bad) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(randomKey()).build().parse(bad) fail() } catch (MalformedJwtException se) { - assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string has a digest/signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.getMessage() + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string contains a signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.getMessage() } } + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseUnsecuredJwsDefault() { + // not signed - unsecured by default. Parsing should be disabled automatically + def header = '{"alg":"none"}' + def payload = '{"subject":"Joe"}' + String unsecured = base64Url(header) + '.' + base64Url(payload) + '.' + try { + Jwts.parserBuilder().build().parse(unsecured) + fail() + } catch (UnsupportedJwtException expected) { + String msg = DefaultJwtParser.UNSECURED_DISABLED_MSG_PREFIX + '{alg=none}' + assertEquals msg, expected.getMessage() + } + } + @Test void testParseWithBase64EncodedSigningKey() { @@ -186,7 +205,7 @@ class JwtParserTest { void testParseNullPayloadWithoutKey() { String compact = Jwts.builder().compact() - Jwt jwt = Jwts.parserBuilder().build().parse(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact) assertEquals 'none', jwt.header.alg assertEquals '', jwt.body @@ -200,7 +219,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { - Jwts.parserBuilder().build().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -218,7 +237,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact() try { - Jwts.parserBuilder().build().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact) fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') @@ -235,7 +254,7 @@ class JwtParserTest { String subject = 'Joe' String compact = Jwts.builder().setSubject(subject).setExpiration(exp).compact() - Jwt jwt = Jwts.parserBuilder().setAllowedClockSkewSeconds(10).build().parse(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(10).build().parse(compact) assertEquals jwt.getBody().getSubject(), subject } @@ -247,7 +266,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { - Jwts.parserBuilder().setAllowedClockSkewSeconds(1).build().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(1).build().parse(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -261,7 +280,7 @@ class JwtParserTest { String subject = 'Joe' String compact = Jwts.builder().setSubject(subject).setNotBefore(exp).compact() - Jwt jwt = Jwts.parserBuilder().setAllowedClockSkewSeconds(10).build().parse(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(10).build().parse(compact) assertEquals jwt.getBody().getSubject(), subject } @@ -273,7 +292,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(exp).compact() try { - Jwts.parserBuilder().setAllowedClockSkewSeconds(1).build().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(1).build().parse(compact) fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') @@ -291,7 +310,7 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() - Jwt jwt = Jwts.parserBuilder().build().parsePlaintextJwt(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parsePlaintextJwt(compact) assertEquals jwt.getBody(), payload } @@ -302,7 +321,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').compact() try { - Jwts.parserBuilder().build().parsePlaintextJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parsePlaintextJwt(compact) fail() } catch (UnsupportedJwtException e) { assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' @@ -349,7 +368,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() - Jwt jwt = Jwts.parserBuilder().build().parseClaimsJwt(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) assertEquals jwt.getBody().getSubject(), subject } @@ -362,7 +381,7 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parserBuilder().build().parseClaimsJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) fail() } catch (UnsupportedJwtException e) { assertEquals 'Unsigned plaintext JWTs are not supported.', e.getMessage() @@ -408,7 +427,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { - Jwts.parserBuilder().build().parseClaimsJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -423,9 +442,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact() try { - Jwts.parserBuilder(). - build(). - parseClaimsJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') @@ -463,7 +480,7 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parserBuilder().setSigningKey(key).build().parsePlaintextJws(compact) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parsePlaintextJws(compact) fail() } catch (UnsupportedJwtException e) { assertEquals 'Unsigned plaintext JWTs are not supported.', e.getMessage() @@ -480,7 +497,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() try { - Jwts.parserBuilder().setSigningKey(key).build().parsePlaintextJws(compact) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parsePlaintextJws(compact) fail() } catch (UnsupportedJwtException e) { assertEquals 'Unsigned Claims JWTs are not supported.', e.getMessage() @@ -576,7 +593,7 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseClaimsJws(compact) fail() } catch (UnsupportedJwtException e) { assertEquals 'Unsigned plaintext JWTs are not supported.', e.getMessage() @@ -593,7 +610,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() try { - Jwts.parserBuilder().setSigningKey(key). + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key). build(). parseClaimsJws(compact) fail() @@ -1475,7 +1492,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact() - Jwts.parserBuilder().setClock(new FixedClock(beforeExpiry)).build().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setClock(new FixedClock(beforeExpiry)).build().parse(compact) } @Test @@ -1495,7 +1512,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact() try { - Jwts.parserBuilder().setClock(new DefaultClock()).build().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setClock(new DefaultClock()).build().parse(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -1568,10 +1585,10 @@ class JwtParserTest { String jwtStr = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(sig) try { - Jwts.parserBuilder().build().parse(jwtStr) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(jwtStr) fail() } catch (MalformedJwtException se) { - assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string has a digest/signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.message + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string contains a signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.message } } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 8da70d799..c0cf11250 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -54,7 +54,7 @@ class JwtsTest { } @Test - void testSubclass() { + void testPrivateCtor() { // for code coverage only new Jwts() } @@ -137,7 +137,7 @@ class JwtsTest { String jwt = Jwts.builder().setClaims(claims).compact(); - def token = Jwts.parserBuilder().build().parse(jwt); + def token = Jwts.parserBuilder().enableUnsecuredJws().build().parse(jwt); //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims @@ -193,7 +193,7 @@ class JwtsTest { @Test void testParseWithHeaderOnly() { String unsecuredJwt = base64Url("{\"alg\":\"none\"}") + ".." - Jwt jwt = Jwts.parserBuilder().build().parse(unsecuredJwt) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parse(unsecuredJwt) assertEquals("none", jwt.getHeader().get("alg")) } @@ -214,10 +214,10 @@ class JwtsTest { int i = compact.lastIndexOf('.') String missingSig = compact.substring(0, i + 1) try { - Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(missingSig) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseClaimsJws(missingSig) fail() } catch (MalformedJwtException expected) { - assertEquals 'The JWS header references signature algorithm \'HS256\' but the compact JWS string does not have a signature token.', expected.getMessage() + assertEquals 'The JWS header references signature algorithm \'HS256\' but the compact JWS string is missing the required signature.', expected.getMessage() } } @@ -234,7 +234,7 @@ class JwtsTest { @Test void testConvenienceIssuer() { String compact = Jwts.builder().setIssuer("Me").compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getIssuer(), "Me" compact = Jwts.builder().setSubject("Joe") @@ -242,14 +242,14 @@ class JwtsTest { .setIssuer(null) //null should remove it .compact(); - claims = Jwts.parserBuilder().build().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getIssuer() } @Test void testConvenienceSubject() { String compact = Jwts.builder().setSubject("Joe").compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getSubject(), "Joe" compact = Jwts.builder().setIssuer("Me") @@ -257,14 +257,14 @@ class JwtsTest { .setSubject(null) //null should remove it .compact(); - claims = Jwts.parserBuilder().build().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getSubject() } @Test void testConvenienceAudience() { String compact = Jwts.builder().setAudience("You").compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getAudience(), "You" compact = Jwts.builder().setIssuer("Me") @@ -272,7 +272,7 @@ class JwtsTest { .setAudience(null) //null should remove it .compact(); - claims = Jwts.parserBuilder().build().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getAudience() } @@ -302,7 +302,7 @@ class JwtsTest { void testConvenienceExpiration() { Date then = laterDate(); String compact = Jwts.builder().setExpiration(then).compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getExpiration() assertEquals claimedDate, then @@ -311,7 +311,7 @@ class JwtsTest { .setExpiration(null) //null should remove it .compact(); - claims = Jwts.parserBuilder().build().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getExpiration() } @@ -319,7 +319,7 @@ class JwtsTest { void testConvenienceNotBefore() { Date now = now() //jwt exp only supports *seconds* since epoch: String compact = Jwts.builder().setNotBefore(now).compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getNotBefore() assertEquals claimedDate, now @@ -328,7 +328,7 @@ class JwtsTest { .setNotBefore(null) //null should remove it .compact(); - claims = Jwts.parserBuilder().build().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getNotBefore() } @@ -336,7 +336,7 @@ class JwtsTest { void testConvenienceIssuedAt() { Date now = now() //jwt exp only supports *seconds* since epoch: String compact = Jwts.builder().setIssuedAt(now).compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getIssuedAt() assertEquals claimedDate, now @@ -345,7 +345,7 @@ class JwtsTest { .setIssuedAt(null) //null should remove it .compact(); - claims = Jwts.parserBuilder().build().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getIssuedAt() } @@ -353,7 +353,7 @@ class JwtsTest { void testConvenienceId() { String id = UUID.randomUUID().toString(); String compact = Jwts.builder().setId(id).compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getId(), id compact = Jwts.builder().setIssuer("Me") @@ -361,7 +361,7 @@ class JwtsTest { .setId(null) //null should remove it .compact(); - claims = Jwts.parserBuilder().build().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getId() } @@ -620,7 +620,7 @@ class JwtsTest { String notSigned = Jwts.builder().setSubject("Foo").compact() try { - Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(notSigned) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseClaimsJws(notSigned) fail('parseClaimsJws must fail for unsigned JWTs') } catch (UnsupportedJwtException expected) { assertEquals expected.message, 'Unsigned Claims JWTs are not supported.' @@ -646,17 +646,17 @@ class JwtsTest { String forged = Jwts.builder().setSubject("Not Joe").compact() //assert that our forged header has a 'NONE' algorithm: - assertEquals Jwts.parserBuilder().build().parseClaimsJwt(forged).getHeader().get('alg'), 'none' + assertEquals Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(forged).getHeader().get('alg'), 'none' //now let's forge it by appending the signature the server expects: forged += signature //now assert that, when the server tries to parse the forged token, parsing fails: try { - Jwts.parserBuilder().setSigningKey(key).build().parse(forged) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parse(forged) fail("Parsing must fail for a forged token.") } catch (MalformedJwtException expected) { - assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string has a digest/signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', expected.message + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string contains a signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', expected.message } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy index e5bc9b24b..3db8f45a3 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -17,8 +17,11 @@ package io.jsonwebtoken.impl import io.jsonwebtoken.Claims import io.jsonwebtoken.RequiredTypeException +import io.jsonwebtoken.impl.lang.Field +import io.jsonwebtoken.lang.DateFormats import org.junit.Before import org.junit.Test + import static org.junit.Assert.* class DefaultClaimsTest { @@ -152,20 +155,186 @@ class DefaultClaimsTest { } @Test - void testGetClaimWithRequiredType_Date_Success() { - def actual = new Date(); - claims.put("aDate", actual) - Date expected = claims.get("aDate", Date.class); - assertEquals(expected, actual) + void testGetRequiredDateFromNull() { + Date date = claims.get("aDate", Date.class) + assertNull date + } + + @Test + void testGetRequiredDateFromDate() { + def expected = new Date(); + claims.put("aDate", expected) + Date result = claims.get("aDate", Date.class) + assertEquals expected, result + } + + @Test + void testGetRequiredDateFromCalendar() { + def c = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + def expected = c.getTime() + claims.put("aDate", c) + Date result = claims.get('aDate', Date.class) + assertEquals expected, result } @Test - void testGetClaimWithRequiredType_DateWithLong_Success() { - def actual = new Date(); + void testGetRequiredDateFromLong() { + def expected = new Date() // note that Long is stored in claim - claims.put("aDate", actual.getTime()) - Date expected = claims.get("aDate", Date.class); - assertEquals(expected, actual) + claims.put("aDate", expected.getTime()) + Date result = claims.get("aDate", Date.class) + assertEquals expected, result + } + + @Test + void testGetRequiredDateFromIso8601String() { + def expected = new Date() + claims.put("aDate", DateFormats.formatIso8601(expected)) + Date result = claims.get("aDate", Date.class) + assertEquals expected, result + } + + @Test + void testGetRequiredDateFromIso8601MillisString() { + def expected = new Date() + claims.put("aDate", DateFormats.formatIso8601(expected, true)) + Date result = claims.get("aDate", Date.class) + assertEquals expected, result + } + + @Test + void testGetRequiredDateFromInvalidIso8601String() { + Date d = new Date() + String s = d.toString() + claims.put('aDate', s) + try { + claims.get('aDate', Date.class) + fail() + } catch (IllegalArgumentException expected) { + String expectedMsg = "Cannot create Date from 'aDate' value [$s]. Cause: String value does not appear to be ISO-8601-formatted: $s" as String + assertEquals expectedMsg, expected.getMessage() + } + } + + @Test + void testToSpecDateWithNull() { + assertNull claims.get(Claims.EXPIRATION) + assertNull claims.getExpiration() + assertNull claims.get(Claims.ISSUED_AT) + assertNull claims.getIssuedAt() + assertNull claims.get(Claims.NOT_BEFORE) + assertNull claims.getNotBefore() + } + + @Test + void testGetSpecDateWithLongString() { + Date orig = new Date() + long millis = orig.getTime() + long seconds = millis / 1000L as long + Date expected = new Date(seconds * 1000L) + String secondsString = '' + seconds + claims.put(Claims.EXPIRATION, secondsString) + claims.put(Claims.ISSUED_AT, secondsString) + claims.put(Claims.NOT_BEFORE, secondsString) + assertEquals expected, claims.getExpiration() + assertEquals expected, claims.getIssuedAt() + assertEquals expected, claims.getNotBefore() + assertEquals seconds, claims.get(Claims.EXPIRATION) + assertEquals seconds, claims.get(Claims.ISSUED_AT) + assertEquals seconds, claims.get(Claims.NOT_BEFORE) + } + + @Test + void testGetSpecDateWithLong() { + Date orig = new Date() + long millis = orig.getTime() + long seconds = millis / 1000L as long + Date expected = new Date(seconds * 1000L) + claims.put(Claims.EXPIRATION, seconds) + claims.put(Claims.ISSUED_AT, seconds) + claims.put(Claims.NOT_BEFORE, seconds) + assertEquals expected, claims.getExpiration() + assertEquals expected, claims.getIssuedAt() + assertEquals expected, claims.getNotBefore() + assertEquals seconds, claims.get(Claims.EXPIRATION) + assertEquals seconds, claims.get(Claims.ISSUED_AT) + assertEquals seconds, claims.get(Claims.NOT_BEFORE) + } + + @Test + void testGetSpecDateWithIso8601String() { + Date orig = new Date() + long millis = orig.getTime() + long seconds = millis / 1000L as long + String s = DateFormats.formatIso8601(orig) + claims.put(Claims.EXPIRATION, s) + claims.put(Claims.ISSUED_AT, s) + claims.put(Claims.NOT_BEFORE, s) + assertEquals orig, claims.getExpiration() + assertEquals orig, claims.getIssuedAt() + assertEquals orig, claims.getNotBefore() + assertEquals seconds, claims.get(Claims.EXPIRATION) + assertEquals seconds, claims.get(Claims.ISSUED_AT) + assertEquals seconds, claims.get(Claims.NOT_BEFORE) + } + + @Test + void testGetSpecDateWithDate() { + Date orig = new Date() + long millis = orig.getTime() + long seconds = millis / 1000L as long + claims.put(Claims.EXPIRATION, orig) + claims.put(Claims.ISSUED_AT, orig) + claims.put(Claims.NOT_BEFORE, orig) + assertEquals orig, claims.getExpiration() + assertEquals orig, claims.getIssuedAt() + assertEquals orig, claims.getNotBefore() + assertEquals seconds, claims.get(Claims.EXPIRATION) + assertEquals seconds, claims.get(Claims.ISSUED_AT) + assertEquals seconds, claims.get(Claims.NOT_BEFORE) + } + + @Test + void testGetSpecDateWithCalendar() { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + Date date = cal.getTime() + long millis = date.getTime() + long seconds = millis / 1000L as long + claims.put(Claims.EXPIRATION, cal) + claims.put(Claims.ISSUED_AT, cal) + claims.put(Claims.NOT_BEFORE, cal) + assertEquals date, claims.getExpiration() + assertEquals date, claims.getIssuedAt() + assertEquals date, claims.getNotBefore() + assertEquals seconds, claims.get(Claims.EXPIRATION) + assertEquals seconds, claims.get(Claims.ISSUED_AT) + assertEquals seconds, claims.get(Claims.NOT_BEFORE) + } + + @Test + void testToSpecDateWithDate() { + long millis = System.currentTimeMillis(); + Date d = new Date(millis) + claims.put('exp', d) + assertEquals d, claims.getExpiration() + } + + void trySpecDateNonDate(Field field) { + def val = new Object() { @Override public String toString() {return 'hi'} } + try { + claims.put(field.getId(), val) + fail() + } catch (IllegalArgumentException iae) { + String msg = "Invalid Map $field value [hi]. Cause: Cannot create Date from Object of type io.jsonwebtoken.impl.DefaultClaimsTest\$1 with value: hi" + assertEquals msg, iae.getMessage() + } + } + + @Test + void testSpecDateFromNonDateObject() { + trySpecDateNonDate(DefaultClaims.EXPIRATION) + trySpecDateNonDate(DefaultClaims.ISSUED_AT) + trySpecDateNonDate(DefaultClaims.NOT_BEFORE) } @Test diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy index f6b4be1c6..37b52d812 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy @@ -85,10 +85,10 @@ class DefaultJweBuilderTest { @Test void testBuild() { - def enc = EncryptionAlgorithms.A128GCM; + def enc = EncryptionAlgorithms.A128GCM def key = enc.generateKey() - String jwe = new DefaultJweBuilder() + new DefaultJweBuilder() .setSubject('joe') .encryptWith(enc) .withKey(key) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy index fed984aa0..013128016 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy @@ -26,7 +26,7 @@ class DefaultJwtTest { @Test void testToString() { String compact = Jwts.builder().setHeaderParam('foo', 'bar').setAudience('jsmith').compact(); - Jwt jwt = Jwts.parserBuilder().build().parseClaimsJwt(compact); + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact); assertEquals 'header={foo=bar, alg=none},body={aud=jsmith}', jwt.toString() } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy index 665f594b6..ef8f36a6c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy @@ -15,182 +15,90 @@ */ package io.jsonwebtoken.impl -import io.jsonwebtoken.lang.DateFormats +import io.jsonwebtoken.impl.lang.Field +import io.jsonwebtoken.impl.lang.Fields +import io.jsonwebtoken.impl.security.Randoms +import io.jsonwebtoken.lang.Collections +import org.junit.Before import org.junit.Test import static org.junit.Assert.* class JwtMapTest { - @Test - void testToDateFromNull() { - Date actual = JwtMap.toDate(null, 'foo') - assertNull actual - } - - @Test - void testToDateFromDate() { - def d = new Date() - Date date = JwtMap.toDate(d, 'foo') - assertSame date, d - } - - @Test - void testToDateFromCalendar() { - def c = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - def d = c.getTime() - Date date = JwtMap.toDate(c, 'foo') - assertEquals date, d - } - - @Test - void testToDateFromIso8601String() { - Date d = new Date(2015, 1, 1, 12, 0, 0) - String s = DateFormats.formatIso8601(d, false) - Date date = JwtMap.toDate(s, 'foo') - assertEquals date, d - } - - @Test - void testToDateFromInvalidIso8601String() { - Date d = new Date(2015, 1, 1, 12, 0, 0) - String s = d.toString() - try { - JwtMap.toDate(d.toString(), 'foo') - fail() - } catch (IllegalArgumentException iae) { - assertEquals "'foo' value does not appear to be ISO-8601-formatted: $s" as String, iae.getMessage() - } - } - - @Test - void testToDateFromIso8601MillisString() { - long millis = System.currentTimeMillis(); - Date d = new Date(millis) - String s = DateFormats.formatIso8601(d) - Date date = JwtMap.toDate(s, 'foo') - assertEquals date, d - } + private static final Field DUMMY = Fields.string('' + Randoms.secureRandom().nextInt(), "RANDOM") + private static final Set> FIELDS = Collections.setOf(DUMMY) + JwtMap jwtMap - @Test - void testToSpecDateWithNull() { - assertNull JwtMap.toSpecDate(null, 'exp') - } - - @Test - void testToSpecDateWithLong() { - long millis = System.currentTimeMillis() - long seconds = (millis / 1000l) as long - Date d = new Date(seconds * 1000) - assertEquals d, JwtMap.toSpecDate(seconds, 'exp') - } - - @Test - void testToSpecDateWithString() { - Date d = new Date(2015, 1, 1, 12, 0, 0) - String s = (d.getTime() / 1000) + '' //JWT timestamps are in seconds - need to strip millis - Date date = JwtMap.toSpecDate(s, 'exp') - assertEquals date, d - } - - @Test - void testToSpecDateWithIso8601String() { - long millis = System.currentTimeMillis(); - Date d = new Date(millis) - String s = DateFormats.formatIso8601(d) - Date date = JwtMap.toSpecDate(s, 'exp') - assertEquals date, d - } - - @Test - void testToSpecDateWithDate() { - long millis = System.currentTimeMillis(); - Date d = new Date(millis) - Date date = JwtMap.toSpecDate(d, 'exp') - assertSame d, date - } - - @Test - void testToDateFromNonDateObject() { - try { - JwtMap.toDate(new Object() { @Override public String toString() {return 'hi'} }, 'foo') - fail() - } catch (IllegalStateException iae) { - assertEquals iae.message, "Cannot create Date from 'foo' value 'hi'." - } + @Before + void setup() { + // dummy field to satisfy constructor: + jwtMap = new JwtMap(FIELDS) } @Test void testContainsKey() { - def m = new JwtMap() - m.put('foo', 'bar') - assertTrue m.containsKey('foo') + jwtMap.put('foo', 'bar') + assertTrue jwtMap.containsKey('foo') } @Test void testContainsValue() { - def m = new JwtMap() - m.put('foo', 'bar') - assertTrue m.containsValue('bar') + jwtMap.put('foo', 'bar') + assertTrue jwtMap.containsValue('bar') } @Test void testRemoveByPuttingNull() { - def m = new JwtMap() - m.put('foo', 'bar') - assertTrue m.containsKey('foo') - assertTrue m.containsValue('bar') - m.put('foo', null) - assertFalse m.containsKey('foo') - assertFalse m.containsValue('bar') + jwtMap.put('foo', 'bar') + assertTrue jwtMap.containsKey('foo') + assertTrue jwtMap.containsValue('bar') + jwtMap.put('foo', null) + assertFalse jwtMap.containsKey('foo') + assertFalse jwtMap.containsValue('bar') } @Test void testPutAll() { - def m = new JwtMap(); - m.putAll([a: 'b', c: 'd']) - assertEquals m.size(), 2 - assertEquals m.a, 'b' - assertEquals m.c, 'd' + jwtMap.putAll([a: 'b', c: 'd']) + assertEquals jwtMap.size(), 2 + assertEquals jwtMap.a, 'b' + assertEquals jwtMap.c, 'd' } @Test void testPutAllWithNullArgument() { - def m = new JwtMap(); - m.putAll((Map)null) - assertEquals m.size(), 0 + jwtMap.putAll((Map)null) + assertEquals jwtMap.size(), 0 } @Test void testClear() { - def m = new JwtMap(); - m.put('foo', 'bar') - assertEquals m.size(), 1 - m.clear() - assertEquals m.size(), 0 + jwtMap.put('foo', 'bar') + assertEquals jwtMap.size(), 1 + jwtMap.clear() + assertEquals jwtMap.size(), 0 } @Test void testKeySet() { - def m = new JwtMap() - m.putAll([a: 'b', c: 'd']) - assertEquals( m.keySet(), ['a', 'c'] as Set) + jwtMap.putAll([a: 'b', c: 'd']) + assertEquals( jwtMap.keySet(), ['a', 'c'] as Set) } @Test void testValues() { - def m = new JwtMap() - m.putAll([a: 'b', c: 'd']) + jwtMap.putAll([a: 'b', c: 'd']) def s = ['b', 'd'] - assertTrue m.values().containsAll(s) && s.containsAll(m.values()) + assertTrue jwtMap.values().containsAll(s) && s.containsAll(jwtMap.values()) } @Test void testEquals() throws Exception { - def m1 = new JwtMap(); + def m1 = new JwtMap(FIELDS); m1.put("a", "a"); - def m2 = new JwtMap(); + def m2 = new JwtMap(FIELDS); m2.put("a", "a"); assertEquals(m1, m2); @@ -198,14 +106,13 @@ class JwtMapTest { @Test void testHashcode() throws Exception { - def m = new JwtMap(); - def hashCodeEmpty = m.hashCode(); + def hashCodeEmpty = jwtMap.hashCode(); - m.put("a", "b"); - def hashCodeNonEmpty = m.hashCode(); + jwtMap.put("a", "b"); + def hashCodeNonEmpty = jwtMap.hashCode(); assertTrue(hashCodeEmpty != hashCodeNonEmpty); - def identityHash = System.identityHashCode(m); + def identityHash = System.identityHashCode(jwtMap); assertTrue(hashCodeNonEmpty != identityHash); } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DeflateCompressionCodecTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DeflateCompressionCodecTest.groovy index 52f2b4e1f..6451b0982 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DeflateCompressionCodecTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DeflateCompressionCodecTest.groovy @@ -18,7 +18,7 @@ class DeflateCompressionCodecTest { @Test void testBackwardsCompatibility_0_10_6() { final String jwtFrom0106 = 'eyJhbGciOiJub25lIiwiemlwIjoiREVGIn0.eNqqVsosLlayUspNVdJRKi5NAjJLi1OLgJzMxBIlK0sTMzMLEwsDAx2l1IoCJSsTQwMjExOQQC0AAAD__w.' - Jwts.parserBuilder().build().parseClaimsJwt(jwtFrom0106) // no exception should be thrown + Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(jwtFrom0106) // no exception should be thrown } /** diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy index 7f3ea3626..8c44593c5 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy @@ -1,6 +1,6 @@ package io.jsonwebtoken.impl.lang -import io.jsonwebtoken.impl.security.DefaultJwkContext + import org.junit.Test import static org.junit.Assert.* @@ -9,7 +9,7 @@ class EncodedObjectConverterTest { @Test void testApplyFromWithInvalidType() { - def converter = DefaultJwkContext.URI_CONVERTER + def converter = Converters.URI assertTrue converter instanceof EncodedObjectConverter int value = 42 try { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/NoConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RequiredTypeConverterTest.groovy similarity index 71% rename from impl/src/test/groovy/io/jsonwebtoken/impl/lang/NoConverterTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/lang/RequiredTypeConverterTest.groovy index b3358c984..7eca46465 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/NoConverterTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RequiredTypeConverterTest.groovy @@ -4,24 +4,24 @@ import org.junit.Test import static org.junit.Assert.* -class NoConverterTest { +class RequiredTypeConverterTest { @Test void testApplyTo() { - def converter = new NoConverter(Integer.class) + def converter = new RequiredTypeConverter(Integer.class) def val = 42 assertSame val, converter.applyTo(val) } @Test void testApplyFromNull() { - def converter = new NoConverter(Integer.class) + def converter = new RequiredTypeConverter(Integer.class) assertNull converter.applyFrom(null) } @Test void testApplyFromInvalidType() { - def converter = new NoConverter(Integer.class) + def converter = new RequiredTypeConverter(Integer.class) try { converter.applyFrom('hello' as String) } catch (IllegalArgumentException expected) { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy index d2a0e9253..09a77f2d5 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy @@ -52,13 +52,13 @@ class AbstractAsymmetricJwkBuilderTest { void testX509CertificateSha1Thumbprint() { def jwk = builder().setX509CertificateChain(CHAIN).withX509Sha1Thumbprint(true).build() Assert.notEmpty(jwk.getX509CertificateSha1Thumbprint()) - Assert.hasText(jwk.get(AbstractAsymmetricJwk.X5T) as String) + Assert.hasText(jwk.get(AbstractAsymmetricJwk.X5T.getId()) as String) } @Test void testX509CertificateSha256Thumbprint() { def jwk = builder().setX509CertificateChain(CHAIN).withX509Sha256Thumbprint(true).build() Assert.notEmpty(jwk.getX509CertificateSha256Thumbprint()) - Assert.hasText(jwk.get(AbstractAsymmetricJwk.X5T_S256) as String) + Assert.hasText(jwk.get(AbstractAsymmetricJwk.X5T_S256.getId()) as String) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy index 6aef4e7b8..8cf63e64b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy @@ -60,7 +60,7 @@ class DispatchingJwkFactoryTest { assertEquals jwk.d, d //remove the 'd' mapping to represent only a public key: - m.remove(DefaultEcPrivateJwk.D) + m.remove(DefaultEcPrivateJwk.D.getId()) ctx = new DefaultJwkContext() ctx.putAll(m) From 27d128ee5b969e075d5e04bd439d60a5037ce33f Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 23 Oct 2021 14:47:07 -0700 Subject: [PATCH 08/75] continued testing w/ more coverage. Replaced PbeKey concept with PasswordKey --- .../java/io/jsonwebtoken/lang/Assert.java | 109 +++++--- .../jsonwebtoken/security/KeyAlgorithms.java | 8 +- .../java/io/jsonwebtoken/security/Keys.java | 29 +- .../io/jsonwebtoken/security/PasswordKey.java | 51 ++++ .../java/io/jsonwebtoken/security/PbeKey.java | 41 --- .../jsonwebtoken/security/PbeKeyBuilder.java | 52 ---- .../jsonwebtoken/impl/DefaultJweBuilder.java | 11 +- .../jsonwebtoken/impl/DefaultJweHeader.java | 4 +- .../jsonwebtoken/impl/DefaultJwtParser.java | 3 +- .../impl/lang/BigIntegerUBytesConverter.java | 40 ++- .../AbstractAsymmetricJwkBuilder.java | 7 +- .../security/AbstractFamilyJwkFactory.java | 11 +- .../impl/security/AbstractJwkBuilder.java | 12 +- .../impl/security/DefaultPasswordKey.java | 81 ++++++ .../impl/security/DefaultPbeKey.java | 90 ------ .../impl/security/DefaultPbeKeyBuilder.java | 29 -- .../impl/security/DispatchingJwkFactory.java | 6 +- .../impl/security/EcPublicJwkFactory.java | 17 +- .../impl/security/EcdhKeyAlgorithm.java | 16 +- .../jsonwebtoken/impl/security/JcaPbeKey.java | 51 ---- .../impl/security/KeyAlgorithmsBridge.java | 23 +- .../impl/security/KeysBridge.java | 13 +- .../impl/security/Pbes2HsAkwAlgorithm.java | 81 ++---- .../io/jsonwebtoken/DeprecatedJwtsTest.groovy | 7 +- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 260 ++++++++---------- .../lang/BigIntegerUBytesConverterTest.groovy | 44 +++ .../jsonwebtoken/impl/lang/FieldsTest.groovy | 49 ++++ .../impl/lang/JwtDateConverterTest.groovy | 23 ++ .../AbstractFamilyJwkFactoryTest.groovy | 71 +++++ .../security/AbstractJwkBuilderTest.groovy | 28 +- .../security/ConstantKeyLocatorTest.groovy | 10 +- .../security/DefaultPasswordKeyTest.groovy | 100 +++++++ .../security/DispatchingJwkFactoryTest.groovy | 56 ++-- .../impl/security/JwksTest.groovy | 94 +++++++ .../security/Pbes2HsAkwAlgorithmTest.groovy | 47 +++- .../impl/security/RFC7517AppendixCTest.groovy | 29 +- .../io/jsonwebtoken/security/KeysTest.groovy | 23 +- 37 files changed, 954 insertions(+), 672 deletions(-) create mode 100644 api/src/main/java/io/jsonwebtoken/security/PasswordKey.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/PbeKey.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/PbeKeyBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPasswordKey.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKey.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKeyBuilder.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JcaPbeKey.java create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverterTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/FieldsTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/JwtDateConverterTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactoryTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultPasswordKeyTest.groovy diff --git a/api/src/main/java/io/jsonwebtoken/lang/Assert.java b/api/src/main/java/io/jsonwebtoken/lang/Assert.java index 2068d8d34..7af714b90 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Assert.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Assert.java @@ -20,14 +20,16 @@ public final class Assert { - private Assert(){} //prevent instantiation + private Assert() { + } //prevent instantiation /** * Assert a boolean expression, throwing IllegalArgumentException * if the test result is false. *
    Assert.isTrue(i > 0, "The value must be greater than zero");
    + * * @param expression a boolean expression - * @param message the exception message to use if the assertion fails + * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if expression is false */ public static void isTrue(boolean expression, String message) { @@ -40,6 +42,7 @@ public static void isTrue(boolean expression, String message) { * Assert a boolean expression, throwing IllegalArgumentException * if the test result is false. *
    Assert.isTrue(i > 0);
    + * * @param expression a boolean expression * @throws IllegalArgumentException if expression is false */ @@ -50,7 +53,8 @@ public static void isTrue(boolean expression) { /** * Assert that an object is null . *
    Assert.isNull(value, "The value must be null");
    - * @param object the object to check + * + * @param object the object to check * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object is not null */ @@ -63,6 +67,7 @@ public static void isNull(Object object, String message) { /** * Assert that an object is null . *
    Assert.isNull(value);
    + * * @param object the object to check * @throws IllegalArgumentException if the object is not null */ @@ -73,7 +78,8 @@ public static void isNull(Object object) { /** * Assert that an object is not null . *
    Assert.notNull(clazz, "The class must not be null");
    - * @param object the object to check + * + * @param object the object to check * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object is null */ @@ -87,6 +93,7 @@ public static T notNull(T object, String message) { /** * Assert that an object is not null . *
    Assert.notNull(clazz);
    + * * @param object the object to check * @throws IllegalArgumentException if the object is null */ @@ -98,7 +105,8 @@ public static void notNull(Object object) { * Assert that the given String is not empty; that is, * it must not be null and not the empty String. *
    Assert.hasLength(name, "Name must not be empty");
    - * @param text the String to check + * + * @param text the String to check * @param message the exception message to use if the assertion fails * @see Strings#hasLength */ @@ -112,19 +120,21 @@ public static void hasLength(String text, String message) { * Assert that the given String is not empty; that is, * it must not be null and not the empty String. *
    Assert.hasLength(name);
    + * * @param text the String to check * @see Strings#hasLength */ public static void hasLength(String text) { hasLength(text, - "[Assertion failed] - this String argument must have length; it must not be null or empty"); + "[Assertion failed] - this String argument must have length; it must not be null or empty"); } /** * Assert that the given String has valid text content; that is, it must not * be null and must contain at least one non-whitespace character. *
    Assert.hasText(name, "'name' must not be empty");
    - * @param text the String to check + * + * @param text the String to check * @param message the exception message to use if the assertion fails * @see Strings#hasText */ @@ -139,20 +149,22 @@ public static String hasText(String text, String message) { * Assert that the given String has valid text content; that is, it must not * be null and must contain at least one non-whitespace character. *
    Assert.hasText(name, "'name' must not be empty");
    + * * @param text the String to check * @see Strings#hasText */ public static void hasText(String text) { hasText(text, - "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank"); + "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank"); } /** * Assert that the given text does not contain the given substring. *
    Assert.doesNotContain(name, "rod", "Name must not contain 'rod'");
    + * * @param textToSearch the text to search - * @param substring the substring to find within the text - * @param message the exception message to use if the assertion fails + * @param substring the substring to find within the text + * @param message the exception message to use if the assertion fails */ public static void doesNotContain(String textToSearch, String substring, String message) { if (Strings.hasLength(textToSearch) && Strings.hasLength(substring) && @@ -164,12 +176,13 @@ public static void doesNotContain(String textToSearch, String substring, String /** * Assert that the given text does not contain the given substring. *
    Assert.doesNotContain(name, "rod");
    + * * @param textToSearch the text to search - * @param substring the substring to find within the text + * @param substring the substring to find within the text */ public static void doesNotContain(String textToSearch, String substring) { doesNotContain(textToSearch, substring, - "[Assertion failed] - this String argument must not contain the substring [" + substring + "]"); + "[Assertion failed] - this String argument must not contain the substring [" + substring + "]"); } @@ -177,7 +190,8 @@ public static void doesNotContain(String textToSearch, String substring) { * Assert that an array has elements; that is, it must not be * null and must have at least one element. *
    Assert.notEmpty(array, "The array must have elements");
    - * @param array the array to check + * + * @param array the array to check * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object array is null or has no elements */ @@ -191,6 +205,7 @@ public static void notEmpty(Object[] array, String message) { * Assert that an array has elements; that is, it must not be * null and must have at least one element. *
    Assert.notEmpty(array);
    + * * @param array the array to check * @throws IllegalArgumentException if the object array is null or has no elements */ @@ -216,7 +231,8 @@ public static char[] notEmpty(char[] chars, String msg) { * Assert that an array has no null elements. * Note: Does not complain if the array is empty! *
    Assert.noNullElements(array, "The array must have non-null elements");
    - * @param array the array to check + * + * @param array the array to check * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object array contains a null element */ @@ -234,6 +250,7 @@ public static void noNullElements(Object[] array, String message) { * Assert that an array has no null elements. * Note: Does not complain if the array is empty! *
    Assert.noNullElements(array);
    + * * @param array the array to check * @throws IllegalArgumentException if the object array contains a null element */ @@ -245,8 +262,9 @@ public static void noNullElements(Object[] array) { * Assert that a collection has elements; that is, it must not be * null and must have at least one element. *
    Assert.notEmpty(collection, "Collection must have elements");
    + * * @param collection the collection to check - * @param message the exception message to use if the assertion fails + * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the collection is null or has no elements */ public static > T notEmpty(T collection, String message) { @@ -260,23 +278,25 @@ public static > T notEmpty(T collection, String message) * Assert that a collection has elements; that is, it must not be * null and must have at least one element. *
    Assert.notEmpty(collection, "Collection must have elements");
    + * * @param collection the collection to check * @throws IllegalArgumentException if the collection is null or has no elements */ public static void notEmpty(Collection collection) { notEmpty(collection, - "[Assertion failed] - this collection must not be empty: it must contain at least 1 element"); + "[Assertion failed] - this collection must not be empty: it must contain at least 1 element"); } /** * Assert that a Map has entries; that is, it must not be null * and must have at least one entry. *
    Assert.notEmpty(map, "Map must have entries");
    - * @param map the map to check + * + * @param map the map to check * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the map is null or has no entries */ - public static > T notEmpty(T map, String message) { + public static > T notEmpty(T map, String message) { if (Collections.isEmpty(map)) { throw new IllegalArgumentException(message); } @@ -287,6 +307,7 @@ public static > T notEmpty(T map, String message) { * Assert that a Map has entries; that is, it must not be null * and must have at least one entry. *
    Assert.notEmpty(map);
    + * * @param map the map to check * @throws IllegalArgumentException if the map is null or has no entries */ @@ -298,8 +319,9 @@ public static void notEmpty(Map map) { /** * Assert that the provided object is an instance of the provided class. *
    Assert.instanceOf(Foo.class, foo);
    + * * @param clazz the required class - * @param obj the object to check + * @param obj the object to check * @throws IllegalArgumentException if the object is not an instance of clazz * @see Class#isInstance */ @@ -310,12 +332,13 @@ public static void isInstanceOf(Class clazz, Object obj) { /** * Assert that the provided object is an instance of the provided class. *
    Assert.instanceOf(Foo.class, foo);
    - * @param type the type to check against - * @param obj the object to check + * + * @param type the type to check against + * @param obj the object to check * @param message a message which will be prepended to the message produced by - * the function itself, and which may be used to provide context. It should - * normally end in a ": " or ". " so that the function generate message looks - * ok when prepended to it. + * the function itself, and which may be used to provide context. It should + * normally end in a ": " or ". " so that the function generate message looks + * ok when prepended to it. * @throws IllegalArgumentException if the object is not an instance of clazz * @see Class#isInstance */ @@ -323,8 +346,8 @@ public static T isInstanceOf(Class type, Object obj, String message) { notNull(type, "Type to check against must not be null"); if (!type.isInstance(obj)) { throw new IllegalArgumentException(message + - "Object of class [" + (obj != null ? obj.getClass().getName() : "null") + - "] must be an instance of " + type); + "Object of class [" + (obj != null ? obj.getClass().getName() : "null") + + "] must be an instance of " + type); } return type.cast(obj); } @@ -332,8 +355,9 @@ public static T isInstanceOf(Class type, Object obj, String message) { /** * Assert that superType.isAssignableFrom(subType) is true. *
    Assert.isAssignable(Number.class, myClass);
    + * * @param superType the super type to check - * @param subType the sub type to check + * @param subType the sub type to check * @throws IllegalArgumentException if the classes are not assignable */ public static void isAssignable(Class superType, Class subType) { @@ -343,12 +367,13 @@ public static void isAssignable(Class superType, Class subType) { /** * Assert that superType.isAssignableFrom(subType) is true. *
    Assert.isAssignable(Number.class, myClass);
    + * * @param superType the super type to check against - * @param subType the sub type to check - * @param message a message which will be prepended to the message produced by - * the function itself, and which may be used to provide context. It should - * normally end in a ": " or ". " so that the function generate message looks - * ok when prepended to it. + * @param subType the sub type to check + * @param message a message which will be prepended to the message produced by + * the function itself, and which may be used to provide context. It should + * normally end in a ": " or ". " so that the function generate message looks + * ok when prepended to it. * @throws IllegalArgumentException if the classes are not assignable */ public static void isAssignable(Class superType, Class subType, String message) { @@ -364,8 +389,9 @@ public static void isAssignable(Class superType, Class subType, String message) * if the test result is false. Call isTrue if you wish to * throw IllegalArgumentException on an assertion failure. *
    Assert.state(id == null, "The id property must not already be initialized");
    + * * @param expression a boolean expression - * @param message the exception message to use if the assertion fails + * @param message the exception message to use if the assertion fails * @throws IllegalStateException if expression is false */ public static void state(boolean expression, String message) { @@ -380,6 +406,7 @@ public static void state(boolean expression, String message) { *

    Call {@link #isTrue(boolean)} if you wish to * throw {@link IllegalArgumentException} on an assertion failure. *

    Assert.state(id == null);
    + * * @param expression a boolean expression * @throws IllegalStateException if the supplied expression is false */ @@ -387,4 +414,20 @@ public static void state(boolean expression) { state(expression, "[Assertion failed] - this state invariant must be true"); } + /** + * Asserts that the specified {@code value} is not null, otherwise throws an + * {@link IllegalStateException} with the specified {@code msg}. Intended to be used with + * code invariants (as opposed to method arguments, like {@link #notNull(Object)}). + * + * @param value value to assert is not null + * @param msg exception message to use if {@code value} is null + * @throws IllegalStateException with the specified {@code msg} if {@code value} is null. + * @since JJWT_RELEASE_VERSION + */ + public static void stateNotNull(Object value, String msg) throws IllegalStateException { + if (value == null) { + throw new IllegalStateException(msg); + } + } + } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index 3b639edc4..4c7895e12 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -71,9 +71,9 @@ private static T forId0(String id) { public static final KeyAlgorithm A128GCMKW = forId0("A128GCMKW"); public static final KeyAlgorithm A192GCMKW = forId0("A192GCMKW"); public static final KeyAlgorithm A256GCMKW = forId0("A256GCMKW"); - public static final KeyAlgorithm PBES2_HS256_A128KW = forId0("PBES2-HS256+A128KW"); - public static final KeyAlgorithm PBES2_HS384_A192KW = forId0("PBES2-HS384+A192KW"); - public static final KeyAlgorithm PBES2_HS512_A256KW = forId0("PBES2-HS512+A256KW"); + public static final KeyAlgorithm PBES2_HS256_A128KW = forId0("PBES2-HS256+A128KW"); + public static final KeyAlgorithm PBES2_HS384_A192KW = forId0("PBES2-HS384+A192KW"); + public static final KeyAlgorithm PBES2_HS512_A256KW = forId0("PBES2-HS512+A256KW"); public static final RsaKeyAlgorithm RSA1_5 = forId0("RSA1_5"); public static final RsaKeyAlgorithm RSA_OAEP = forId0("RSA-OAEP"); public static final RsaKeyAlgorithm RSA_OAEP_256 = forId0("RSA-OAEP-256"); @@ -83,7 +83,7 @@ private static T forId0(String id) { //public static final EcKeyAlgorithm ECDH_ES_A192KW = forId0("ECDH-ES+A192KW"); //public static final EcKeyAlgorithm ECDH_ES_A256KW = forId0("ECDH-ES+A256KW"); - public static int estimateIterations(KeyAlgorithm alg, long desiredMillis) { + public static int estimateIterations(KeyAlgorithm alg, long desiredMillis) { return Classes.invokeStatic(BRIDGE_CLASS, "estimateIterations", ESTIMATE_ITERATIONS_ARG_TYPES, alg, desiredMillis); } } diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index 038aeeb2b..e4e2f8acb 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -19,7 +19,6 @@ import io.jsonwebtoken.lang.Classes; import javax.crypto.SecretKey; -import javax.crypto.interfaces.PBEKey; import javax.crypto.spec.SecretKeySpec; import java.security.KeyPair; @@ -33,7 +32,7 @@ public final class Keys { private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.KeysBridge"; private static final Class BRIDGE_CLASS = Classes.forName(BRIDGE_CLASSNAME); @SuppressWarnings("rawtypes") - private static final Class[] TO_PBE_ARG_TYPES = new Class[]{PBEKey.class}; + private static final Class[] FOR_PASSWORD_ARG_TYPES = new Class[]{char[].class}; //prevent instantiation private Keys() { @@ -119,6 +118,7 @@ public static SecretKey hmacShaKeyFor(byte[] bytes) throws WeakKeyException { * @deprecated since JJWT_RELEASE_VERSION. Use your preferred {@link SecretKeySignatureAlgorithm} instance's * {@link SecretKeySignatureAlgorithm#generateKey() generateKey()} method directly. */ + @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); @@ -214,6 +214,7 @@ public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) thr * @deprecated since JJWT_RELEASE_VERSION. Use your preferred {@link AsymmetricKeySignatureAlgorithm} instance's * {@link AsymmetricKeySignatureAlgorithm#generateKeyPair() generateKeyPair()} method directly. */ + @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); @@ -227,24 +228,16 @@ public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws } /** - * Returns a JJWT {@link PbeKey} directly backed by the specified JCA {@link PBEKey}. The returned instance - * is directly linked to the specified {@code PBEKey} - a call to either key's {@link SecretKey#destroy() destroy} - * method will destroy the other to ensure correct/safe cleanup for both. + * Returns a new {@link PasswordKey} suitable for use with password-based key derivation algorithms. + * Usage Note: Using {@code PasswordKey}s outside of key derivation contexts will likely + * fail. See the {@link PasswordKey} JavaDoc for more, and also note the Password Safety section. * - * @param key the {@code PBEKey} to represent as a {@code PbeKey} instance. - * @return a JJWT {@link PbeKey} instance that wraps the specified JCA {@link PBEKey} + * @param password the raw password character array to use with password-based key derivation algorithms. + * @return a new {@link PasswordKey} that shares the specified {@code password} character array. + * @see PasswordKey#getPassword() * @since JJWT_RELEASE_VERSION */ - public static PbeKey toPbeKey(PBEKey key) { - return Classes.invokeStatic(BRIDGE_CLASS, "toPbeKey", TO_PBE_ARG_TYPES, new Object[]{key}); - } - - /** - * Returns a new {@link PbeKeyBuilder} to use to construct a {@link PbeKey} instance. - * - * @return a new {@link PbeKeyBuilder} to use to construct a {@link PbeKey} instance. - */ - public static PbeKeyBuilder forPbe() { - return Classes.invokeStatic(BRIDGE_CLASS, "forPbe", null, (Object[]) null); + public static PasswordKey forPassword(char[] password) { + return Classes.invokeStatic(BRIDGE_CLASS, "forPassword", FOR_PASSWORD_ARG_TYPES, new Object[]{password}); } } diff --git a/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java b/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java new file mode 100644 index 000000000..eb4e6df85 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java @@ -0,0 +1,51 @@ +package io.jsonwebtoken.security; + +import javax.crypto.SecretKey; + +/** + * A {@code Key} suitable for use with password-based key derivation algorithms. + * + *

    Usage Warning

    + *

    Because raw passwords should never be used as direct inputs for cryptographic operations (such as authenticated + * hashing or encryption) - and only for derivation algorithms (like password-based encryption) - {@code PasswordKey} + * instances will throw an exception when used in these invalid contexts. Specifically, calling a + * {@code PasswordKey}'s {@link PasswordKey#getEncoded() getEncoded()} method (as would be done automatically by the + * JCA subsystem during direct cryptographic operations) will throw an + * {@link UnsupportedOperationException UnsupportedOperationException}.

    + * + *

    Password Safety

    + *

    Instances returned by this method directly share the specified {@code password} character array argument - + * changes to that char array will be reflected in the returned key, and similarly, any call to the key's + * {@link PasswordKey#destroy() destroy()} method will clear/overwrite the shared char array. This is to ensure that + * any clearing of the source password char array for security/safety reasons also guarantees the key is also + * cleared and vice versa. However, as is standard for JCA keys, calling {@link #getPassword() getPassword()} will + * return a separate independent clone of the underlying character array.

    + * + * @see #getPassword() + * @since JJWT_RELEASE_VERSION + */ +public interface PasswordKey extends SecretKey { + + /** + * Returns a clone of the underlying password character array represented by this Key. Like all + * {@code SecretKey} implementations, if you wish to clear the backing password character array for + * safety/security reasons, call the Key's {@link #destroy()} method, ensuring that both the password is cleared + * and the key instance can no longer be used. + *

    Usage

    + *

    Because a clone is returned from this method, it is expected that callers will clear the resulting clone from + * memory as soon as possible to reduce password exposure. For example: + *

    
    +     * char[] clonedPassword = aPasswordKey.getPassword();
    +     * try {
    +     *     doSomethingWithPassword(clonedPassword);
    +     * } finally {
    +     *     // guarantee clone is cleared regardless of any Exception thrown:
    +     *     java.util.Arrays.fill(clonedPassword, '\u0000');
    +     * }
    +     * 
    + *

    + * + * @return a clone of the underlying password character array represented by this Key. + */ + char[] getPassword(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PbeKey.java b/api/src/main/java/io/jsonwebtoken/security/PbeKey.java deleted file mode 100644 index 258980a79..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/PbeKey.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2021 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.security; - -import javax.crypto.SecretKey; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface PbeKey extends SecretKey { - - /** - * Returns a clone of the underlying password character array represented by this Key. Like all - * {@code SecretKey} implementations, if you wish to clear the backing password character array for - * safety/security reasons, call the {@link #destroy()} method, ensuring the key instance can no longer - * be used. - * - * @return a clone of the underlying password character array represented by this Key. - */ - char[] getPassword(); - - /** - * Returns the number of hashing iterations to perform. - * - * @return the number of hashing iterations to perform. - */ - int getIterations(); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/PbeKeyBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PbeKeyBuilder.java deleted file mode 100644 index 4f1e8e8ef..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/PbeKeyBuilder.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2021 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.security; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface PbeKeyBuilder { - - /** - * Sets the password character array for the constructed key. This does not clone the argument - changes made - * to the backing array will be reflected by the constructed key and any {@link PbeKey#destroy()} call will do - * the same. This is to ensure that any clearing of the password argument for security/safety reasons also - * guarantees the resulting key is also cleared and vice versa. - * - * @param password password character array for the constructed key - * @return this builder for method chaining - */ - PbeKeyBuilder setPassword(char[] password); - - /** - * Sets the number of hashing iterations to perform when deriving an encryption key. - * - * @param iterations the number of hashing iterations to perform when deriving an encryption key. - * @return @return this builder for method chaining - */ - PbeKeyBuilder setIterations(int iterations); - - /** - * Constructs a new {@link PbeKey} that shares the {@link #setPassword(char[]) specified} password character array. - * Changes to that char array will be reflected in the returned key, and similarly, - * any call to the key's {@link PbeKey#destroy() destroy} method will clear/overwrite the shared char array. - * This is to ensure that any clearing of the password char array for security/safety reasons also - * guarantees the key is also cleared and vice versa. - * - * @return a new {@link PbeKey} that shares the {@link #setPassword(char[]) specified} password character array. - */ - K build(); -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java index 3acc253cf..7b02a2475 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java @@ -21,12 +21,10 @@ import io.jsonwebtoken.security.KeyAlgorithms; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; -import io.jsonwebtoken.security.Keys; -import io.jsonwebtoken.security.PbeKey; +import io.jsonwebtoken.security.PasswordKey; import io.jsonwebtoken.security.SecurityException; import javax.crypto.SecretKey; -import javax.crypto.interfaces.PBEKey; import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.Map; @@ -80,11 +78,8 @@ public AeadResult apply(AeadRequest request) { @Override public JweBuilder withKey(SecretKey key) { - if (key instanceof PBEKey) { - key = Keys.toPbeKey((PBEKey) key); - } - if (key instanceof PbeKey) { - return withKeyFrom((PbeKey) key, KeyAlgorithms.PBES2_HS512_A256KW); + if (key instanceof PasswordKey) { + return withKeyFrom((PasswordKey) key, KeyAlgorithms.PBES2_HS512_A256KW); } return withKeyFrom(key, KeyAlgorithms.DIRECT); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java index 43c7d072b..884acc4b7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java @@ -16,8 +16,8 @@ public class DefaultJweHeader extends DefaultHeader implements JweHeader { static final Field ENCRYPTION_ALGORITHM = Fields.string("enc", "Encryption Algorithm"); - static final Field P2C = Fields.builder(Integer.class).setId("p2c").setName("PBES2 Count").build(); - static final Field P2S = Fields.bytes("p2s", "PBES2 Salt Input").build(); + public static final Field P2C = Fields.builder(Integer.class).setId("p2c").setName("PBES2 Count").build(); + public static final Field P2S = Fields.bytes("p2s", "PBES2 Salt Input").build(); static final Field APU = Fields.bytes("apu", "Agreement PartyUInfo").build(); static final Field APV = Fields.bytes("apv", "Agreement PartyVInfo").build(); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 3890190da..baa98ab0f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -192,8 +192,7 @@ public DefaultJwtParser() { this.enableUnsecuredJws = false; } - @SuppressWarnings("deprecation") - //SigningKeyResolver will be removed for 1.0 + @SuppressWarnings("deprecation") //SigningKeyResolver will be removed for 1.0 DefaultJwtParser(Provider provider, SigningKeyResolver signingKeyResolver, boolean enableUnsecuredJws, diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverter.java index 124e20459..296894b58 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverter.java @@ -6,32 +6,30 @@ public class BigIntegerUBytesConverter implements Converter { - // Copied from Apache Commons Codec 1.14: - // https://github.com/apache/commons-codec/blob/af7b94750e2178b8437d9812b28e36ac87a455f2/src/main/java/org/apache/commons/codec/binary/Base64.java#L746-L775 + private static final String NEGATIVE_MSG = + "JWA Base64urlUInt values MUST be >= 0 (non-negative) per the " + + "[JWA RFC 7518, Section 2](https://datatracker.ietf.org/doc/html/rfc7518#section-2) " + + "'Base64urlUInt' definition."; + @Override public byte[] applyTo(BigInteger bigInt) { Assert.notNull(bigInt, "BigInteger argument cannot be null."); - final int bitlen = bigInt.bitLength(); - // round bitlen - final int roundedBitlen = ((bitlen + 7) >> 3) << 3; - final byte[] bigBytes = bigInt.toByteArray(); - - if (((bitlen % 8) != 0) && (((bitlen / 8) + 1) == (roundedBitlen / 8))) { - return bigBytes; + if (BigInteger.ZERO.compareTo(bigInt) > 0) { + throw new IllegalArgumentException(NEGATIVE_MSG); } - // set up params for copying everything but sign bit - int startSrc = 0; - int len = bigBytes.length; - - // if bigInt is exactly byte-aligned, just skip signbit in copy - if ((bitlen % 8) == 0) { - startSrc = 1; - len--; + + final int bitLen = bigInt.bitLength(); + final byte[] bytes = bigInt.toByteArray(); + // round bitLen. This gives the minimal number of bytes necessary to represent an unsigned byte array: + final int unsignedByteLen = Math.max(1, (bitLen + 7) / Byte.SIZE); + + if (bytes.length == unsignedByteLen) { // already in the form we need + return bytes; } - final int startDst = roundedBitlen / 8 - len; // to pad w/ nulls as per spec - final byte[] resizedBytes = new byte[roundedBitlen / 8]; - System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len); - return resizedBytes; + //otherwise, we need to strip the sign byte (start copying at index 1 instead of 0): + byte[] ubytes = new byte[unsignedByteLen]; + System.arraycopy(bytes, 1, ubytes, 0, unsignedByteLen); + return ubytes; } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java index 31922b01c..84aebda84 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java @@ -62,7 +62,8 @@ public AbstractAsymmetricJwkBuilder(JwkContext ctx) { @Override public T setPublicKeyUse(String use) { Assert.hasText(use, "publicKeyUse cannot be null or empty."); - return put(AbstractAsymmetricJwk.USE.getId(), use); + this.jwkContext.setPublicKeyUse(use); + return tthis(); } public T setKeyUseStrategy(KeyUseStrategy strategy) { @@ -142,11 +143,11 @@ public J build() { } if (computeX509Sha1Thumbprint) { byte[] thumbprint = computeThumbprint(firstCert, "SHA-1"); - put(AbstractAsymmetricJwk.X5T.getId(), thumbprint); + this.jwkContext.setX509CertificateSha1Thumbprint(thumbprint); } if (computeX509Sha256Thumbprint) { byte[] thumbprint = computeThumbprint(firstCert, "SHA-256"); - put(AbstractAsymmetricJwk.X5T_S256.getId(), thumbprint); + this.jwkContext.setX509CertificateSha256Thumbprint(thumbprint); } } return super.build(); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java index f4dcd52b8..4493101b4 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java @@ -6,12 +6,11 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.KeyException; import java.math.BigInteger; import java.security.Key; import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; abstract class AbstractFamilyJwkFactory> implements FamilyJwkFactory { @@ -53,11 +52,13 @@ protected K generateKey(final JwkContext ctx, final CheckedFunction T generateKey(final JwkContext ctx, final Class type, final CheckedFunction fn) { return new JcaTemplate(getId(), ctx.getProvider()).execute(KeyFactory.class, new CheckedFunction() { @Override - public T apply(KeyFactory instance) throws Exception { + public T apply(KeyFactory instance) { try { return fn.apply(instance); - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - String msg = "Unable to create " + type.getSimpleName() + " from JWK {" + ctx + "}: " + e.getMessage(); + } catch (KeyException keyException) { + throw keyException; // propagate + } catch (Exception e) { + String msg = "Unable to create " + type.getSimpleName() + " from JWK " + ctx + ": " + e.getMessage(); throw new InvalidKeyException(msg, e); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java index 8525bb71d..279dd7718 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java @@ -20,8 +20,13 @@ abstract class AbstractJwkBuilder, T extends Jwk @SuppressWarnings("unchecked") protected AbstractJwkBuilder(JwkContext jwkContext) { - this.jwkContext = Assert.notNull(jwkContext, "JwkContext cannot be null."); - this.jwkFactory = (JwkFactory) DispatchingJwkFactory.DEFAULT_INSTANCE; + this(jwkContext, (JwkFactory)DispatchingJwkFactory.DEFAULT_INSTANCE); + } + + // visible for testing + protected AbstractJwkBuilder(JwkContext context, JwkFactory factory) { + this.jwkContext = Assert.notNull(context, "JwkContext cannot be null."); + this.jwkFactory = Assert.notNull(factory, "JwkFactory cannot be null."); } @Override @@ -72,7 +77,8 @@ protected final T tthis() { @Override public J build() { - assert this.jwkContext != null; //should always exist as there isn't a way to set it outside the constructor + //should always exist as there isn't a way to set it outside the constructor: + Assert.stateNotNull(this.jwkContext, "JwkContext should always be non-null"); K key = this.jwkContext.getKey(); if (key == null && this.jwkContext.isEmpty()) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPasswordKey.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPasswordKey.java new file mode 100644 index 000000000..6a2a9eb65 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPasswordKey.java @@ -0,0 +1,81 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.security.PasswordKey; + +public class DefaultPasswordKey implements PasswordKey { + + private static final String RAW_FORMAT = "RAW"; + private static final String NONE_ALGORITHM = "NONE"; + private static final String DESTROYED_MSG = "PasswordKey has been destroyed. Password character array may not be obtained."; + private static final String ENCODED_DISABLED_MSG = + "getEncoded() is disabled for PasswordKeys as they are intended to be used " + + "with key derivation algorithms only. Passwords should never be used as direct inputs for " + + "cryptographic operations such as authenticated hashing or encryption; if you see this " + + "exception message, it is likely that the associated PasswordKey is being used incorrectly."; + + private volatile boolean destroyed; + private final char[] password; + + public DefaultPasswordKey(char[] password) { + this.password = Assert.notNull(password, "Password character array cannot be null or empty."); + } + + private void assertActive() { + if (destroyed) { + throw new IllegalStateException(DESTROYED_MSG); + } + } + + @Override + public char[] getPassword() { + assertActive(); + return this.password.clone(); + } + + @Override + public String getAlgorithm() { + return NONE_ALGORITHM; + } + + @Override + public String getFormat() { + return RAW_FORMAT; + } + + @Override + public byte[] getEncoded() { + throw new UnsupportedOperationException(ENCODED_DISABLED_MSG); + } + + @Override + public void destroy() { + java.util.Arrays.fill(password, '\u0000'); + this.destroyed = true; + } + + @Override + public boolean isDestroyed() { + return this.destroyed; + } + + @Override + public int hashCode() { + return Objects.nullSafeHashCode(this.password); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof DefaultPasswordKey) { + DefaultPasswordKey other = (DefaultPasswordKey) obj; + return Objects.nullSafeEquals(this.password, other.password); + } + return false; + } + + @Override + public final String toString() { + return "password="; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKey.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKey.java deleted file mode 100644 index 3d9be0f06..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKey.java +++ /dev/null @@ -1,90 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Objects; -import io.jsonwebtoken.security.PbeKey; - -public class DefaultPbeKey implements PbeKey { - - private static final String RAW_FORMAT = "RAW"; - private static final String NONE_ALGORITHM = "NONE"; - - private volatile boolean destroyed; - private final char[] chars; - private final int iterations; - - public DefaultPbeKey(char[] password, int iterations) { - if (iterations <= 0) { - String msg = "iterations must be a positive integer. Value: " + iterations; - throw new IllegalArgumentException(msg); - } - this.iterations = iterations; - this.chars = Assert.notEmpty(password, "Password character array cannot be null or empty."); - } - - private void assertActive() { - if (destroyed) { - String msg = "PBKey has been destroyed. Password characters or bytes may not be obtained."; - throw new IllegalStateException(msg); - } - } - - @Override - public char[] getPassword() { - assertActive(); - return this.chars.clone(); - } - - @Override - public int getIterations() { - return this.iterations; - } - - @Override - public String getAlgorithm() { - return NONE_ALGORITHM; - } - - @Override - public String getFormat() { - return RAW_FORMAT; - } - - @Override - public byte[] getEncoded() { - throw new UnsupportedOperationException("getEncoded is not supported for PbeKey instances."); - } - - @Override - public void destroy() { - if (!destroyed && chars != null) { - java.util.Arrays.fill(chars, '\u0000'); - } - this.destroyed = true; - } - - @Override - public boolean isDestroyed() { - return destroyed; - } - - @Override - public int hashCode() { - return Objects.nullSafeHashCode(this.chars); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof DefaultPbeKey) { - DefaultPbeKey other = (DefaultPbeKey) obj; - return this.iterations == other.iterations && - Objects.nullSafeEquals(this.chars, other.chars); - } - return false; - } - - @Override - public String toString() { - return "password=, iterations=" + this.iterations; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKeyBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKeyBuilder.java deleted file mode 100644 index af7ab4225..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKeyBuilder.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.PbeKey; -import io.jsonwebtoken.security.PbeKeyBuilder; - -public class DefaultPbeKeyBuilder implements PbeKeyBuilder { - - private char[] password; - private int iterations; - - @Override - public DefaultPbeKeyBuilder setPassword(final char[] password) { - this.password = Assert.notEmpty(password, "password cannot be null or empty."); - return this; - } - - @Override - public DefaultPbeKeyBuilder setIterations(final int iterations) { - Assert.isTrue(iterations > 0, "iterations must be a positive integer."); - this.iterations = iterations; - return this; - } - - @Override - public PbeKey build() { - return new DefaultPbeKey(this.password, this.iterations); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java index 9b9efb9f3..6ca33f01b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java @@ -34,11 +34,7 @@ class DispatchingJwkFactory implements JwkFactory> { Assert.notEmpty(factories, "FamilyJwkFactory collection cannot be null or empty."); this.factories = new ArrayList<>(factories.size()); for (FamilyJwkFactory factory : factories) { - if (!Strings.hasText(factory.getId())) { - String msg = "FamilyJwkFactory instance of type " + factory.getClass().getName() + " does not " + - "have a required algorithm family id (factory.getFactoryId() cannot be null or empty)."; - throw new IllegalArgumentException(msg); - } + Assert.hasText(factory.getId(), "FamilyJwkFactory.getFactoryId() cannot return null or empty."); this.factories.add((FamilyJwkFactory) factory); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java index fd0d9b96d..8044e81cc 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java @@ -17,6 +17,14 @@ class EcPublicJwkFactory extends AbstractEcJwkFactory static final EcPublicJwkFactory DEFAULT_INSTANCE = new EcPublicJwkFactory(); + private static final String KEY_CONTAINS_FORMAT_MSG = + "ECPublicKey's ECPoint does not exist on elliptic curve '%s' and may not be used to create '%s' JWKs."; + + private static final String JWK_CONTAINS_FORMAT_MSG = + "EC JWK x,y coordinates do not exist on elliptic curve '%s'. This " + + "could be due simply to an incorrectly-created JWK or possibly an attempted Invalid Curve Attack " + + "(see https://safecurves.cr.yp.to/twist.html for more information). JWK: %s"; + EcPublicJwkFactory() { super(ECPublicKey.class); } @@ -31,6 +39,11 @@ protected EcPublicJwk createJwkFromKey(JwkContext ctx) { ECPoint point = key.getW(); String curveId = getJwaIdByCurve(curve); + if (!contains(curve, point)) { + String msg = String.format(KEY_CONTAINS_FORMAT_MSG, curveId, curveId); + throw new InvalidKeyException(msg); + } + ctx.put(DefaultEcPublicJwk.CRV.getId(), curveId); int fieldSize = curve.getField().getFieldSize(); @@ -55,9 +68,7 @@ protected EcPublicJwk createJwkFromValues(final JwkContext ctx) { ECPoint point = new ECPoint(x, y); if (!contains(spec.getCurve(), point)) { - String msg = "EC JWK x,y coordinates do not match a point on the '" + curveId + "' elliptic curve. This " + - "could be due simply to an incorrectly-created JWK or possibly an attempted Invalid Curve Attack " + - "(see https://safecurves.cr.yp.to/twist.html for more information). JWK: {" + ctx + "}."; + String msg = String.format(JWK_CONTAINS_FORMAT_MSG, curveId, ctx); throw new InvalidKeyException(msg); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java index de6f5521c..aa2ba18a3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java @@ -6,7 +6,6 @@ import io.jsonwebtoken.security.DecryptionKeyRequest; import io.jsonwebtoken.security.EcKeyAlgorithm; import io.jsonwebtoken.security.EcPublicJwk; -import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.Jwks; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; @@ -23,7 +22,6 @@ import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.spec.ECParameterSpec; -import java.security.spec.EllipticCurve; public class EcdhKeyAlgorithm extends CryptoAlgorithm implements EcKeyAlgorithm { @@ -63,16 +61,10 @@ public KeyResult getEncryptionKey(KeyRequest request) throws SecurityExceptio Assert.notNull(request, "Request cannot be null."); JweHeader header = Assert.notNull(request.getHeader(), "request JweHeader cannot be null."); E publicKey = Assert.notNull(request.getKey(), "request key cannot be null."); - - // guarantee that the specified request key is on a supported curve ECParameterSpec spec = Assert.notNull(publicKey.getParams(), "request key params cannot be null."); - EllipticCurve curve = spec.getCurve(); - String jwaCurveId = AbstractEcJwkFactory.getJwaIdByCurve(curve); - if (publicKey instanceof ECPublicKey && !AbstractEcJwkFactory.contains(curve, ((ECPublicKey) publicKey).getW())) { - String msg = "Specified ECPublicKey cannot be used with JWA standard curve " + jwaCurveId + ": " + - "The key's ECPoint does not exist on curve '" + jwaCurveId + "'."; - throw new InvalidKeyException(msg); - } + + // note: we don't need to validate if specified key's point is on a supported curve here + // because that will automatically be asserted when using Jwks.builder().... below KeyPair pair = generateKeyPair(request, spec); ECPublicKey genPubKey = KeyPairs.getKey(pair, ECPublicKey.class); @@ -80,6 +72,8 @@ public KeyResult getEncryptionKey(KeyRequest request) throws SecurityExceptio SecretKey secretKey = generateSecretKey(request, publicKey, genPrivKey); + // This line will assert/guarantee that the generated public key (and therefore the request key) is on + // a JWK-supported curve: EcPublicJwk jwk = Jwks.builder().setKey(genPubKey).build(); header.put(EPHEMERAL_PUBLIC_KEY, jwk); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JcaPbeKey.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JcaPbeKey.java deleted file mode 100644 index aa2e147de..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JcaPbeKey.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.PbeKey; - -import javax.crypto.interfaces.PBEKey; -import javax.security.auth.DestroyFailedException; - -public class JcaPbeKey implements PbeKey { - - private final PBEKey jcaKey; - - public JcaPbeKey(PBEKey jcaKey) { - this.jcaKey = Assert.notNull(jcaKey, "PBEKey cannot be null."); - } - - @Override - public char[] getPassword() { - return this.jcaKey.getPassword(); - } - - @Override - public int getIterations() { - return this.jcaKey.getIterationCount(); - } - - @Override - public String getAlgorithm() { - return this.jcaKey.getAlgorithm(); - } - - @Override - public String getFormat() { - return this.jcaKey.getFormat(); - } - - @Override - public byte[] getEncoded() { - return this.jcaKey.getEncoded(); - } - - @Override - public void destroy() throws DestroyFailedException { - this.jcaKey.destroy(); - } - - @Override - public boolean isDestroyed() { - return this.jcaKey.isDestroyed(); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java index 96fb29087..af13d3d0f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java @@ -14,7 +14,7 @@ import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; import io.jsonwebtoken.security.Keys; -import io.jsonwebtoken.security.PbeKey; +import io.jsonwebtoken.security.PasswordKey; import io.jsonwebtoken.security.SecurityException; import javax.crypto.SecretKey; @@ -81,7 +81,7 @@ private KeyAlgorithmsBridge() { return instance; } - private static KeyAlgorithm lean(final Pbes2HsAkwAlgorithm alg) { + private static KeyAlgorithm lean(final Pbes2HsAkwAlgorithm alg) { // ensure we use the same key factory over and over so that time spent acquiring one is not repeated: JcaTemplate template = new JcaTemplate(alg.getJcaName(), null, Randoms.secureRandom()); @@ -98,10 +98,10 @@ public SecretKeyFactory apply(SecretKeyFactory secretKeyFactory) { // ensure that the bare minimum steps are performed to hash, ensuring our time sampling pertains only to // hashing and not ancillary steps needed to setup the hashing/derivation - return new KeyAlgorithm() { + return new KeyAlgorithm() { @Override - public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { - int iterations = request.getKey().getIterations(); + public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + int iterations = request.getHeader().getPbes2Count(); char[] password = request.getKey().getPassword(); try { alg.deriveKey(factory, password, rfcSalt, iterations); @@ -112,7 +112,7 @@ public KeyResult getEncryptionKey(KeyRequest request) throws SecurityExc } @Override - public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { throw new UnsupportedOperationException("Not intended to be called."); } @@ -127,7 +127,7 @@ private static char randomChar() { return (char) Randoms.secureRandom().nextInt(Character.MAX_VALUE); } - private static char[] randomChars(int length) { + private static char[] randomChars(@SuppressWarnings("SameParameterValue") int length) { char[] chars = new char[length]; for (int i = 0; i < length; i++) { chars[i] = randomChar(); @@ -135,7 +135,7 @@ private static char[] randomChars(int length) { return chars; } - public static int estimateIterations(KeyAlgorithm alg, long desiredMillis) { + public static int estimateIterations(KeyAlgorithm alg, long desiredMillis) { // The number of computational samples that land in our 'sweet spot' timing range matching desiredMillis. // These samples will be averaged and the final average will be the return value of this method @@ -147,7 +147,7 @@ public static int estimateIterations(KeyAlgorithm alg, long d // 8 characters is a commonly-found minimum required length in many systems circa 2021. final int PASSWORD_LENGTH = 8; - final JweHeader HEADER = new DefaultJweHeader(); // not used during execution, needed to satisfy API call. + final JweHeader HEADER = new DefaultJweHeader(); final AeadAlgorithm ENC_ALG = EncryptionAlgorithms.A128GCM; // not used, needed to satisfy API if (alg instanceof Pbes2HsAkwAlgorithm) { @@ -161,8 +161,9 @@ public static int estimateIterations(KeyAlgorithm alg, long d for (int i = 0; points.size() < NUM_SAMPLES; i++) { char[] password = randomChars(PASSWORD_LENGTH); - PbeKey pbeKey = Keys.forPbe().setPassword(password).setIterations(workFactor).build(); - KeyRequest request = new DefaultKeyRequest<>(null, null, pbeKey, HEADER, ENC_ALG); + PasswordKey key = Keys.forPassword(password); + HEADER.setPbes2Count(workFactor); + KeyRequest request = new DefaultKeyRequest<>(null, null, key, HEADER, ENC_ALG); long start = System.currentTimeMillis(); alg.getEncryptionKey(request); // <-- Computation occurs here. Don't need the result, just need to exec diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java index 9278c4d9f..d19545163 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java @@ -1,9 +1,6 @@ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.security.PbeKey; -import io.jsonwebtoken.security.PbeKeyBuilder; - -import javax.crypto.interfaces.PBEKey; +import io.jsonwebtoken.security.PasswordKey; @SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.Keys implementation public final class KeysBridge { @@ -12,11 +9,7 @@ public final class KeysBridge { private KeysBridge() { } - public static PbeKey toPbeKey(PBEKey key) { - return new JcaPbeKey(key); - } - - public static PbeKeyBuilder forPbe() { - return new DefaultPbeKeyBuilder(); + public static PasswordKey forPassword(char[] password) { + return new DefaultPasswordKey(password); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java index 4c93aa4fe..e954b0a42 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java @@ -1,31 +1,30 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.impl.DefaultJweHeader; import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.impl.lang.ValueGetter; -import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.DecryptionKeyRequest; import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; -import io.jsonwebtoken.security.PbeKey; +import io.jsonwebtoken.security.PasswordKey; import io.jsonwebtoken.security.SecurityException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; -import javax.crypto.interfaces.PBEKey; import javax.crypto.spec.PBEKeySpec; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; -public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm { +public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm { - private static final String SALT_HEADER_NAME = "p2s"; - private static final String ITERATION_HEADER_NAME = "p2c"; // iteration count private static final int MIN_RECOMMENDED_ITERATIONS = 1000; // https://datatracker.ietf.org/doc/html/rfc7518#section-4.8.1.2 + private static final String MIN_ITERATIONS_MSG_PREFIX = + "[JWA RFC 7518, Section 4.8.1.2](https://datatracker.ietf.org/doc/html/rfc7518#section-4.8.1.2) " + + "recommends password-based-encryption iterations be greater than or equal to " + + MIN_RECOMMENDED_ITERATIONS + ". Provided: "; private final int HASH_BYTE_LENGTH; private final int DERIVED_KEY_BIT_LENGTH; @@ -53,9 +52,7 @@ private static String idFor(int hashBitLength, KeyAlgorithm request, final char[] password, final byte[] salt, final int iterations) { + Assert.notEmpty(password, "Key password character array cannot be null or empty."); try { return execute(request, SecretKeyFactory.class, new CheckedFunction() { @Override @@ -103,9 +98,7 @@ public SecretKey apply(SecretKeyFactory factory) throws Exception { } }); } finally { - if (password != null) { - java.util.Arrays.fill(password, '\u0000'); - } + java.util.Arrays.fill(password, '\u0000'); } } @@ -121,16 +114,15 @@ protected byte[] toRfcSalt(byte[] inputSalt) { } @Override - public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { Assert.notNull(request, "request cannot be null."); - final PbeKey pbeKey = Assert.notNull(request.getKey(), "request.getKey() cannot be null."); + final PasswordKey key = Assert.notNull(request.getKey(), "request.getKey() cannot be null."); - final int iterations = assertIterations(pbeKey.getIterations()); + final int iterations = assertIterations(request.getHeader().getPbes2Count()); byte[] inputSalt = generateInputSalt(request); final byte[] rfcSalt = toRfcSalt(inputSalt); - final String p2s = Encoders.BASE64URL.encode(inputSalt); - char[] password = pbeKey.getPassword(); // will be safely cleaned/zeroed in deriveKey next: + char[] password = key.getPassword(); // password will be safely cleaned/zeroed in deriveKey next: final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations); // now get a new CEK that is encrypted ('wrapped') with the PBE-derived key: @@ -138,51 +130,22 @@ public KeyResult getEncryptionKey(KeyRequest request) throws SecurityExc request.getSecureRandom(), derivedKek, request.getHeader(), request.getEncryptionAlgorithm()); KeyResult result = wrapAlg.getEncryptionKey(wrapReq); - request.getHeader().put(SALT_HEADER_NAME, p2s); - request.getHeader().put(ITERATION_HEADER_NAME, iterations); + request.getHeader().setPbes2Salt(inputSalt); //retain for recipients return result; } - private static char[] toChars(byte[] bytes) { - // use bytebuffer/charbuffer so we don't create a String that remains in the JVM string memory table (heap) - // the respective byte and char arrays will be cleared by the caller - ByteBuffer buf = ByteBuffer.wrap(bytes); - CharBuffer cbuf = StandardCharsets.UTF_8.decode(buf); - return cbuf.compact().array(); - } - - private char[] toPasswordChars(SecretKey key) { - if (key instanceof PBEKey) { - return ((PBEKey) key).getPassword(); - } - if (key instanceof PbeKey) { - return ((PbeKey) key).getPassword(); - } - // convert bytes to UTF-8 characters: - byte[] keyBytes = null; - try { - keyBytes = key.getEncoded(); - return toChars(keyBytes); - } finally { - if (keyBytes != null) { - java.util.Arrays.fill(keyBytes, (byte) 0); - } - } - } - @Override - public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); - final SecretKey key = Assert.notNull(request.getKey(), "Request Key cannot be null."); + final PasswordKey key = Assert.notNull(request.getKey(), "Request Key cannot be null."); ValueGetter getter = new DefaultValueGetter(header); - final byte[] inputSalt = getter.getRequiredBytes(SALT_HEADER_NAME); + final byte[] inputSalt = getter.getRequiredBytes(DefaultJweHeader.P2S.getId()); final byte[] rfcSalt = Bytes.concat(SALT_PREFIX, inputSalt); - final int iterations = getter.getRequiredPositiveInteger(ITERATION_HEADER_NAME); - final char[] password = toPasswordChars(key); // will be safely cleaned/zeroed in deriveKey next: - + final int iterations = getter.getRequiredPositiveInteger(DefaultJweHeader.P2C.getId()); + final char[] password = key.getPassword(); // password will be safely cleaned/zeroed in deriveKey next: final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations); DecryptionKeyRequest unwrapReq = new DefaultDecryptionKeyRequest<>(request.getProvider(), diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy index d18d058b7..9ee283327 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy @@ -25,6 +25,7 @@ import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.SignatureAlgorithms import io.jsonwebtoken.security.WeakKeyException import org.junit.Test @@ -615,7 +616,7 @@ class DeprecatedJwtsTest { void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPrivateKey() { //Create a legitimate RSA public and private key pair: - KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.RS256) + KeyPair kp = SignatureAlgorithms.RS256.generateKeyPair() PublicKey publicKey = kp.getPublic() PrivateKey privateKey = kp.getPrivate() @@ -647,7 +648,7 @@ class DeprecatedJwtsTest { void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPublicKey() { //Create a legitimate RSA public and private key pair: - KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.RS256) + KeyPair kp = SignatureAlgorithms.RS256.generateKeyPair() PublicKey publicKey = kp.getPublic(); //PrivateKey privateKey = kp.getPrivate(); @@ -679,7 +680,7 @@ class DeprecatedJwtsTest { void testParseForgedEllipticCurvePublicKeyAsHmacToken() { //Create a legitimate RSA public and private key pair: - KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.ES256) + KeyPair kp = SignatureAlgorithms.ES256.generateKeyPair() PublicKey publicKey = kp.getPublic(); //PrivateKey privateKey = kp.getPrivate(); diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index c0cf11250..90a281391 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -15,14 +15,11 @@ */ package io.jsonwebtoken -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.impl.DefaultHeader -import io.jsonwebtoken.impl.DefaultJweHeader -import io.jsonwebtoken.impl.DefaultJwsHeader -import io.jsonwebtoken.impl.JwtTokenizer +import io.jsonwebtoken.impl.* import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec import io.jsonwebtoken.impl.lang.Services +import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.lang.Strings @@ -30,13 +27,14 @@ import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.Mac +import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec import java.nio.charset.Charset +import java.nio.charset.StandardCharsets import java.security.Key import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey -import java.security.interfaces.RSAPrivateKey import static org.junit.Assert.* @@ -55,6 +53,7 @@ class JwtsTest { @Test void testPrivateCtor() { // for code coverage only + //noinspection GroovyAccessibility new Jwts() } @@ -112,32 +111,22 @@ class JwtsTest { @Test void testPlaintextJwtString() { - - // Assert exact output per example at https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-6.1 - - // The base64url encoding of the example claims set in the spec shows that their original payload ends lines with - // carriage return + newline, so we have to include them in the test payload to assert our encoded output - // matches what is in the spec: - - def payload = '{"iss":"joe",\r\n' + - ' "exp":1300819380,\r\n' + - ' "http://example.com/is_root":true}' - - String val = Jwts.builder().setPayload(payload).compact(); - - def specOutput = 'eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.' - - assertEquals val, specOutput + // Assert exact output per example at https://datatracker.ietf.org/doc/html/rfc7519#section-6.1 + String encodedBody = 'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ' + String payload = new String(Decoders.BASE64URL.decode(encodedBody), StandardCharsets.UTF_8) + String val = Jwts.builder().setPayload(payload).compact() + String RFC_VALUE = 'eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.' + assertEquals val, RFC_VALUE } @Test void testParsePlaintextToken() { - def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root': true] + def claims = [iss: 'joe', exp: later(), 'https://example.com/is_root': true] - String jwt = Jwts.builder().setClaims(claims).compact(); + String jwt = Jwts.builder().setClaims(claims).compact() - def token = Jwts.parserBuilder().enableUnsecuredJws().build().parse(jwt); + def token = Jwts.parserBuilder().enableUnsecuredJws().build().parse(jwt) //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims @@ -164,6 +153,7 @@ class JwtsTest { Jwts.parserBuilder().build().parse('foo') fail() } catch (MalformedJwtException e) { + //noinspection GroovyAccessibility String expected = JwtTokenizer.DELIM_ERR_MSG_PREFIX + '0' assertEquals expected, e.message } @@ -175,6 +165,7 @@ class JwtsTest { Jwts.parserBuilder().build().parse('.') fail() } catch (MalformedJwtException e) { + //noinspection GroovyAccessibility String expected = JwtTokenizer.DELIM_ERR_MSG_PREFIX + '1' assertEquals expected, e.message } @@ -233,14 +224,14 @@ class JwtsTest { @Test void testConvenienceIssuer() { - String compact = Jwts.builder().setIssuer("Me").compact(); + String compact = Jwts.builder().setIssuer("Me").compact() Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getIssuer(), "Me" compact = Jwts.builder().setSubject("Joe") .setIssuer("Me") //set it .setIssuer(null) //null should remove it - .compact(); + .compact() claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getIssuer() @@ -248,14 +239,14 @@ class JwtsTest { @Test void testConvenienceSubject() { - String compact = Jwts.builder().setSubject("Joe").compact(); + String compact = Jwts.builder().setSubject("Joe").compact() Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getSubject(), "Joe" compact = Jwts.builder().setIssuer("Me") .setSubject("Joe") //set it .setSubject(null) //null should remove it - .compact(); + .compact() claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getSubject() @@ -263,45 +254,45 @@ class JwtsTest { @Test void testConvenienceAudience() { - String compact = Jwts.builder().setAudience("You").compact(); + String compact = Jwts.builder().setAudience("You").compact() Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getAudience(), "You" compact = Jwts.builder().setIssuer("Me") .setAudience("You") //set it .setAudience(null) //null should remove it - .compact(); + .compact() claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getAudience() } private static Date now() { - return dateWithOnlySecondPrecision(System.currentTimeMillis()); + return dateWithOnlySecondPrecision(System.currentTimeMillis()) } private static int later() { - return laterDate().getTime() / 1000; + return laterDate().getTime() / 1000 } private static Date laterDate(int seconds) { - return dateWithOnlySecondPrecision(System.currentTimeMillis() + (seconds * 1000)); + return dateWithOnlySecondPrecision(System.currentTimeMillis() + (seconds * 1000)) } private static Date laterDate() { - return laterDate(10000); + return laterDate(10000) } private static Date dateWithOnlySecondPrecision(long millis) { - long seconds = millis / 1000; - long secondOnlyPrecisionMillis = seconds * 1000; - return new Date(secondOnlyPrecisionMillis); + long seconds = (long) (millis / 1000) + long secondOnlyPrecisionMillis = seconds * 1000 + return new Date(secondOnlyPrecisionMillis) } @Test void testConvenienceExpiration() { - Date then = laterDate(); - String compact = Jwts.builder().setExpiration(then).compact(); + Date then = laterDate() + String compact = Jwts.builder().setExpiration(then).compact() Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getExpiration() assertEquals claimedDate, then @@ -309,7 +300,7 @@ class JwtsTest { compact = Jwts.builder().setIssuer("Me") .setExpiration(then) //set it .setExpiration(null) //null should remove it - .compact(); + .compact() claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getExpiration() @@ -318,7 +309,7 @@ class JwtsTest { @Test void testConvenienceNotBefore() { Date now = now() //jwt exp only supports *seconds* since epoch: - String compact = Jwts.builder().setNotBefore(now).compact(); + String compact = Jwts.builder().setNotBefore(now).compact() Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getNotBefore() assertEquals claimedDate, now @@ -326,7 +317,7 @@ class JwtsTest { compact = Jwts.builder().setIssuer("Me") .setNotBefore(now) //set it .setNotBefore(null) //null should remove it - .compact(); + .compact() claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getNotBefore() @@ -335,7 +326,7 @@ class JwtsTest { @Test void testConvenienceIssuedAt() { Date now = now() //jwt exp only supports *seconds* since epoch: - String compact = Jwts.builder().setIssuedAt(now).compact(); + String compact = Jwts.builder().setIssuedAt(now).compact() Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getIssuedAt() assertEquals claimedDate, now @@ -343,7 +334,7 @@ class JwtsTest { compact = Jwts.builder().setIssuer("Me") .setIssuedAt(now) //set it .setIssuedAt(null) //null should remove it - .compact(); + .compact() claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getIssuedAt() @@ -351,15 +342,15 @@ class JwtsTest { @Test void testConvenienceId() { - String id = UUID.randomUUID().toString(); - String compact = Jwts.builder().setId(id).compact(); + String id = UUID.randomUUID().toString() + String compact = Jwts.builder().setId(id).compact() Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getId(), id compact = Jwts.builder().setIssuer("Me") .setId(id) //set it .setId(null) //null should remove it - .compact(); + .compact() claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getId() @@ -368,12 +359,12 @@ class JwtsTest { @Test void testUncompressedJwt() { - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + SignatureAlgorithm alg = SignatureAlgorithms.HS256 + SecretKey key = alg.generateKey() String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compact() def jws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) @@ -390,12 +381,12 @@ class JwtsTest { @Test void testCompressedJwtWithDeflate() { - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + SignatureAlgorithm alg = SignatureAlgorithms.HS256 + SecretKey key = alg.generateKey() String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compressWith(CompressionCodecs.DEFLATE).compact() def jws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) @@ -412,12 +403,12 @@ class JwtsTest { @Test void testCompressedJwtWithGZIP() { - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + SignatureAlgorithm alg = SignatureAlgorithms.HS256 + SecretKey key = alg.generateKey() String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compressWith(CompressionCodecs.GZIP).compact() def jws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) @@ -434,12 +425,12 @@ class JwtsTest { @Test void testCompressedWithCustomResolver() { - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + SignatureAlgorithm alg = SignatureAlgorithms.HS256 + SecretKey key = alg.generateKey() String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { @Override String getAlgorithmName() { @@ -451,6 +442,7 @@ class JwtsTest { @Override CompressionCodec resolveCompressionCodec(Header header) { String algorithm = header.getCompressionAlgorithm() + //noinspection ChangeToOperator if ("CUSTOM".equals(algorithm)) { return CompressionCodecs.GZIP } else { @@ -472,12 +464,12 @@ class JwtsTest { @Test(expected = CompressionException.class) void testCompressedJwtWithUnrecognizedHeader() { - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + SignatureAlgorithm alg = SignatureAlgorithms.HS256 + SecretKey key = alg.generateKey() String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { @Override String getAlgorithmName() { @@ -491,12 +483,12 @@ class JwtsTest { @Test void testCompressStringPayloadWithDeflate() { - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + SignatureAlgorithm alg = SignatureAlgorithms.HS256 + SecretKey key = alg.generateKey() String payload = "this is my test for a payload" - String compact = Jwts.builder().setPayload(payload).signWith(alg, key) + String compact = Jwts.builder().setPayload(payload).signWith(key, alg) .compressWith(CompressionCodecs.DEFLATE).compact() def jws = Jwts.parserBuilder().setSigningKey(key).build().parsePlaintextJws(compact) @@ -510,103 +502,100 @@ class JwtsTest { @Test void testHS256() { - testHmac(SignatureAlgorithm.HS256) + testHmac(SignatureAlgorithms.HS256) } @Test void testHS384() { - testHmac(SignatureAlgorithm.HS384) + testHmac(SignatureAlgorithms.HS384) } @Test void testHS512() { - testHmac(SignatureAlgorithm.HS512) + testHmac(SignatureAlgorithms.HS512) } @Test void testRS256() { - testRsa(SignatureAlgorithm.RS256) + testRsa(SignatureAlgorithms.RS256) } @Test void testRS384() { - testRsa(SignatureAlgorithm.RS384) + testRsa(SignatureAlgorithms.RS384) } @Test void testRS512() { - testRsa(SignatureAlgorithm.RS512) + testRsa(SignatureAlgorithms.RS512) } @Test void testPS256() { - testRsa(SignatureAlgorithm.PS256) + testRsa(SignatureAlgorithms.PS256) } @Test void testPS384() { - testRsa(SignatureAlgorithm.PS384) + testRsa(SignatureAlgorithms.PS384) } @Test void testPS512() { - testRsa(SignatureAlgorithm.PS512) + testRsa(SignatureAlgorithms.PS512) } @Test void testRSA256WithPrivateKeyValidation() { - testRsa(SignatureAlgorithm.RS256, true) + testRsa(SignatureAlgorithms.RS256, true) } @Test void testRSA384WithPrivateKeyValidation() { - testRsa(SignatureAlgorithm.RS384, true) + testRsa(SignatureAlgorithms.RS384, true) } @Test void testRSA512WithPrivateKeyValidation() { - testRsa(SignatureAlgorithm.RS512, true) + testRsa(SignatureAlgorithms.RS512, true) } @Test void testES256() { - testEC(SignatureAlgorithm.ES256) + testEC(SignatureAlgorithms.ES256) } @Test void testES384() { - testEC(SignatureAlgorithm.ES384) + testEC(SignatureAlgorithms.ES384) } @Test void testES512() { - testEC(SignatureAlgorithm.ES512) + testEC(SignatureAlgorithms.ES512) } @Test void testES256WithPrivateKeyValidation() { try { - testEC(SignatureAlgorithm.ES256, true) + testEC(SignatureAlgorithms.ES256, true) fail("EC private keys cannot be used to validate EC signatures.") } catch (UnsupportedJwtException e) { assertEquals e.cause.message, "Elliptic Curve signature validation requires an ECPublicKey instance." } } - @Test + @Test(expected = WeakKeyException) void testParseClaimsJwsWithWeakHmacKey() { - SignatureAlgorithm alg = SignatureAlgorithm.HS384 - def key = Keys.secretKeyFor(alg) - def weakKey = Keys.secretKeyFor(SignatureAlgorithm.HS256) + SignatureAlgorithm alg = SignatureAlgorithms.HS384 + def key = alg.generateKey() + def weakKey = SignatureAlgorithms.HS256.generateKey() String jws = Jwts.builder().setSubject("Foo").signWith(key, alg).compact() - try { - Jwts.parserBuilder().setSigningKey(weakKey).build().parseClaimsJws(jws) - fail('parseClaimsJws must fail for weak keys') - } catch (WeakKeyException expected) { - } + Jwts.parserBuilder().setSigningKey(weakKey).build().parseClaimsJws(jws) + fail('parseClaimsJws must fail for weak keys') } //Asserts correct/expected behavior discussed in https://github.com/jwtk/jjwt/issues/20 @@ -614,8 +603,8 @@ class JwtsTest { void testParseClaimsJwsWithUnsignedJwt() { //create random signing key for testing: - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + SignatureAlgorithm alg = SignatureAlgorithms.HS256 + SecretKey key = alg.generateKey() String notSigned = Jwts.builder().setSubject("Foo").compact() @@ -632,11 +621,11 @@ class JwtsTest { void testForgedTokenWithSwappedHeaderUsingNoneAlgorithm() { //create random signing key for testing: - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + SignatureAlgorithm alg = SignatureAlgorithms.HS256 + SecretKey key = alg.generateKey() //this is a 'real', valid JWT: - String compact = Jwts.builder().setSubject("Joe").signWith(alg, key).compact() + String compact = Jwts.builder().setSubject("Joe").signWith(key, alg).compact() //Now strip off the signature so we can add it back in later on a forged token: int i = compact.lastIndexOf('.') @@ -665,7 +654,7 @@ class JwtsTest { void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPrivateKey() { //Create a legitimate RSA public and private key pair: - KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.RS256) + KeyPair kp = SignatureAlgorithms.RS256.generateKeyPair() PublicKey publicKey = kp.getPublic() PrivateKey privateKey = kp.getPrivate() @@ -675,17 +664,17 @@ class JwtsTest { // Now for the forgery: simulate an attacker using the RSA public key to sign a token, but // using it as an HMAC signing key instead of RSA: - Mac mac = Mac.getInstance('HmacSHA256'); - mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')); + Mac mac = Mac.getInstance('HmacSHA256') + mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')) byte[] signatureBytes = mac.doFinal(compact.getBytes(Charset.forName('US-ASCII'))) String encodedSignature = Encoders.BASE64URL.encode(signatureBytes) //Finally, the forged token is the header + body + forged signature: - String forged = compact + encodedSignature; + String forged = compact + encodedSignature // Assert that the server (that should always use the private key) does not recognized the forged token: try { - Jwts.parserBuilder().setSigningKey(privateKey).build().parse(forged); + Jwts.parserBuilder().setSigningKey(privateKey).build().parse(forged) fail("Forged token must not be successfully parsed.") } catch (UnsupportedJwtException expected) { assertTrue expected.getMessage().startsWith('The parsed JWT indicates it was signed with the') @@ -697,8 +686,8 @@ class JwtsTest { void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPublicKey() { //Create a legitimate RSA public and private key pair: - KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.RS256) - PublicKey publicKey = kp.getPublic(); + KeyPair kp = SignatureAlgorithms.RS256.generateKeyPair() + PublicKey publicKey = kp.getPublic() //PrivateKey privateKey = kp.getPrivate(); String header = base64Url(toJson(['alg': 'HS256'])) @@ -707,17 +696,17 @@ class JwtsTest { // Now for the forgery: simulate an attacker using the RSA public key to sign a token, but // using it as an HMAC signing key instead of RSA: - Mac mac = Mac.getInstance('HmacSHA256'); - mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')); + Mac mac = Mac.getInstance('HmacSHA256') + mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')) byte[] signatureBytes = mac.doFinal(compact.getBytes(Charset.forName('US-ASCII'))) - String encodedSignature = Encoders.BASE64URL.encode(signatureBytes); + String encodedSignature = Encoders.BASE64URL.encode(signatureBytes) //Finally, the forged token is the header + body + forged signature: - String forged = compact + encodedSignature; + String forged = compact + encodedSignature // Assert that the parser does not recognized the forged token: try { - Jwts.parserBuilder().setSigningKey(publicKey).build().parse(forged); + Jwts.parserBuilder().setSigningKey(publicKey).build().parse(forged) fail("Forged token must not be successfully parsed.") } catch (UnsupportedJwtException expected) { assertTrue expected.getMessage().startsWith('The parsed JWT indicates it was signed with the') @@ -729,8 +718,8 @@ class JwtsTest { void testParseForgedEllipticCurvePublicKeyAsHmacToken() { //Create a legitimate RSA public and private key pair: - KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.ES256) - PublicKey publicKey = kp.getPublic(); + KeyPair kp = SignatureAlgorithms.ES256.generateKeyPair() + PublicKey publicKey = kp.getPublic() //PrivateKey privateKey = kp.getPrivate(); String header = base64Url(toJson(['alg': 'HS256'])) @@ -739,13 +728,13 @@ class JwtsTest { // Now for the forgery: simulate an attacker using the Elliptic Curve public key to sign a token, but // using it as an HMAC signing key instead of Elliptic Curve: - Mac mac = Mac.getInstance('HmacSHA256'); - mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')); + Mac mac = Mac.getInstance('HmacSHA256') + mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')) byte[] signatureBytes = mac.doFinal(compact.getBytes(Charset.forName('US-ASCII'))) - String encodedSignature = Encoders.BASE64URL.encode(signatureBytes); + String encodedSignature = Encoders.BASE64URL.encode(signatureBytes) //Finally, the forged token is the header + body + forged signature: - String forged = compact + encodedSignature; + String forged = compact + encodedSignature // Assert that the parser does not recognized the forged token: try { @@ -756,13 +745,13 @@ class JwtsTest { } } - static void testRsa(SignatureAlgorithm alg, boolean verifyWithPrivateKey = false) { + static void testRsa(AsymmetricKeySignatureAlgorithm alg, boolean verifyWithPrivateKey = false) { - KeyPair kp = Keys.keyPairFor(alg) + KeyPair kp = alg.generateKeyPair() PublicKey publicKey = kp.getPublic() PrivateKey privateKey = kp.getPrivate() - def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root': true] + def claims = new DefaultClaims([iss: 'joe', exp: later(), 'https://example.com/is_root': true]) String jwt = Jwts.builder().setClaims(claims).signWith(privateKey, alg).compact() @@ -773,34 +762,32 @@ class JwtsTest { def token = Jwts.parserBuilder().setSigningKey(key).build().parse(jwt) - assert [alg: alg.name()] == token.header - //noinspection GrEqualsBetweenInconvertibleTypes - assert token.body == claims + assertEquals([alg: alg.getId()], token.header) + assertEquals(claims, token.body) } - static void testHmac(SignatureAlgorithm alg) { + static void testHmac(SecretKeySignatureAlgorithm alg) { //create random signing key for testing: - byte[] key = Keys.secretKeyFor(alg).encoded + SecretKey key = alg.generateKey() - def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root': true] + def claims = new DefaultClaims([iss: 'joe', exp: later(), 'https://example.com/is_root': true]) - String jwt = Jwts.builder().setClaims(claims).signWith(alg, key).compact() + String jwt = Jwts.builder().setClaims(claims).signWith(key, alg).compact() def token = Jwts.parserBuilder().setSigningKey(key).build().parse(jwt) - assert token.header == [alg: alg.name()] - //noinspection GrEqualsBetweenInconvertibleTypes - assert token.body == claims + assertEquals([alg: alg.getId()], token.header) + assertEquals(claims, token.body) } - static void testEC(SignatureAlgorithm alg, boolean verifyWithPrivateKey = false) { + static void testEC(AsymmetricKeySignatureAlgorithm alg, boolean verifyWithPrivateKey = false) { - KeyPair pair = Keys.keyPairFor(alg) + KeyPair pair = alg.generateKeyPair() PublicKey publicKey = pair.getPublic() PrivateKey privateKey = pair.getPrivate() - def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root': true] + def claims = new DefaultClaims([iss: 'joe', exp: later(), 'https://example.com/is_root': true]) String jwt = Jwts.builder().setClaims(claims).signWith(privateKey, alg).compact() @@ -811,17 +798,8 @@ class JwtsTest { def token = Jwts.parserBuilder().setSigningKey(key).build().parse(jwt) - assert token.header == [alg: alg.name()] - //noinspection GrEqualsBetweenInconvertibleTypes - assert token.body == claims - } - - void testFoo() { - RSAPrivateKey key; - Jwts.jweBuilder() - .encryptWith(EncryptionAlgorithms.A128GCM) - .usingKey(key) - .fromKeyAlgorithm(KeyAlgorithms.PBES2_HS256_A128KW.withIterations(1203023)) + assertEquals([alg: alg.getId()], token.header) + assertEquals(claims, token.body) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverterTest.groovy new file mode 100644 index 000000000..6374638fe --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverterTest.groovy @@ -0,0 +1,44 @@ +package io.jsonwebtoken.impl.lang + +import org.junit.Test + +import static org.junit.Assert.* + +class BigIntegerUBytesConverterTest { + + private BigIntegerUBytesConverter CONVERTER = Converters.BIGINT_UNSIGNED_BYTES + + @Test + void testNegative() { + try { + CONVERTER.applyTo(BigInteger.valueOf(-1)) + fail() + } catch (IllegalArgumentException expected) { + assertEquals BigIntegerUBytesConverter.NEGATIVE_MSG, expected.getMessage() + } + } + + @Test + void testZero() { + byte[] result = CONVERTER.applyTo(BigInteger.ZERO) + assertEquals 1, result.length + assertTrue result[0] == 0x00 as byte + } + + @Test + void testStripSignByte() { + BigInteger val = BigInteger.valueOf(128) + byte[] bytes = val.toByteArray() + byte[] result = CONVERTER.applyTo(val) + assertEquals bytes.length - 1, result.length + } + + /** + * Asserts https://datatracker.ietf.org/doc/html/rfc7518#section-2, 'Base64urlUInt' definition, last sentence: + *
    Zero is represented as BASE64URL(single zero-valued octet), which is "AA".
    + */ + @Test + void testZeroProducesAABase64Url() { + assertEquals 'AA', Converters.BIGINT.applyTo(BigInteger.ZERO) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FieldsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FieldsTest.groovy new file mode 100644 index 000000000..fa0304ae0 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FieldsTest.groovy @@ -0,0 +1,49 @@ +package io.jsonwebtoken.impl.lang + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertFalse + +class FieldsTest { + + @Test + void testPrivateCtor() { // for code coverage only + new Fields() + } + + @Test + void testString() { + def field = Fields.builder(String.class).setId('foo').setName("FooName").build() + assertEquals 'FooName', field.getName() + assertEquals 'foo', field.getId() + assertEquals String.class, field.getIdiomaticType() + } + + @Test + void testEquals() { + def a = Fields.string('foo', "NameA") + def b = Fields.builder(Object.class).setId('foo').setName("NameB").build() + //ensure equality only based on id: + assertEquals a, b + } + + @Test + void testHashCode() { + def a = Fields.string('foo', "NameA") + def b = Fields.builder(Object.class).setId('foo').setName("NameB").build() + //ensure only based on id: + assertEquals a.hashCode(), b.hashCode() + } + + @Test + void testToString() { + assertEquals "'foo' (FooName)", Fields.string('foo', 'FooName').toString() + } + + @Test + void testEqualsNonField() { + def field = Fields.builder(String.class).setId('foo').setName("FooName").build() + assertFalse field.equals(new Object()) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/JwtDateConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/JwtDateConverterTest.groovy new file mode 100644 index 000000000..d02a160a7 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/JwtDateConverterTest.groovy @@ -0,0 +1,23 @@ +package io.jsonwebtoken.impl.lang + +import org.junit.Test + +import static org.junit.Assert.assertNull + +class JwtDateConverterTest { + + @Test + void testApplyToNull() { + assertNull JwtDateConverter.INSTANCE.applyTo(null) + } + + @Test + void testApplyFromNull() { + assertNull JwtDateConverter.INSTANCE.applyFrom(null) + } + + @Test + void testToDateWithNull() { + assertNull JwtDateConverter.toDate(null) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactoryTest.groovy new file mode 100644 index 000000000..05d7dbaa8 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactoryTest.groovy @@ -0,0 +1,71 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.impl.lang.CheckedFunction +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.KeyException +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test + +import java.security.KeyFactory +import java.security.NoSuchAlgorithmException +import java.security.interfaces.ECPublicKey + +import static org.junit.Assert.* + +class AbstractFamilyJwkFactoryTest { + + @Test + void testGenerateKeyPropagatesKeyException() { + // any AbstractFamilyJwkFactory subclass will do: + def factory = new EcPublicJwkFactory() + def ctx = new DefaultJwkContext() + ctx.put('hello', 'world') + def ex = new MalformedKeyException('foo') + try { + factory.generateKey(ctx, new CheckedFunction() { + @Override + ECPublicKey apply(KeyFactory keyFactory) throws Exception { + throw ex + } + }) + fail() + } catch (KeyException expected) { + assertSame ex, expected + } + } + + @Test + void testGenerateKeyUnexpectedException() { + // any AbstractFamilyJwkFactory subclass will do: + def factory = new EcPublicJwkFactory() + def ctx = new DefaultJwkContext() + ctx.put('hello', 'world') + try { + factory.generateKey(ctx, new CheckedFunction() { + @Override + ECPublicKey apply(KeyFactory keyFactory) throws Exception { + throw new NoSuchAlgorithmException("foo") + } + }) + fail() + } catch (InvalidKeyException expected) { + assertEquals 'Unable to create ECPublicKey from JWK {hello=world}: foo', expected.getMessage() + } + } + + @Test + void testUnsupportedContext() { + def factory = new EcPublicJwkFactory() { + @Override + boolean supports(JwkContext ctx) { + return false + } + } + try { + factory.createJwk(new DefaultJwkContext()) + fail() + } catch (IllegalArgumentException iae) { + assertEquals 'Unsupported JwkContext.', iae.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy index 81a1a9785..d22f18f59 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy @@ -1,18 +1,17 @@ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.security.EncryptionAlgorithms -import io.jsonwebtoken.security.Jwks -import io.jsonwebtoken.security.SecretJwk +import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.SecretKey import java.security.Security -import static org.junit.Assert.* +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNotNull class AbstractJwkBuilderTest { - private static final SecretKey SKEY = EncryptionAlgorithms.A256GCM.generateKey(); + private static final SecretKey SKEY = EncryptionAlgorithms.A256GCM.generateKey() private static AbstractJwkBuilder builder() { return (AbstractJwkBuilder)Jwks.builder().setKey(SKEY) @@ -110,4 +109,23 @@ class AbstractJwkBuilderTest { def jwk = builder().setProvider(provider).build() assertEquals 'oct', jwk.getType() } + + @Test + void testFactoryThrowsIllegalArgumentException() { + def ctx = new DefaultJwkContext() + ctx.put('whatevs', 42) + //noinspection GroovyUnusedAssignment + JwkFactory factory = new JwkFactory() { + @Override + Jwk createJwk(JwkContext jwkContext) { + throw new IllegalArgumentException("foo") + } + } + def builder = new AbstractJwkBuilder(ctx, factory) {} + try { + builder.build() + } catch (MalformedKeyException expected) { + assertEquals 'Unable to create JWK: foo', expected.getMessage() + } + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConstantKeyLocatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConstantKeyLocatorTest.groovy index 647634b1e..3d2d8a967 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConstantKeyLocatorTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConstantKeyLocatorTest.groovy @@ -1,14 +1,14 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.UnsupportedJwtException +import io.jsonwebtoken.impl.DefaultHeader import io.jsonwebtoken.impl.DefaultJweHeader import io.jsonwebtoken.impl.DefaultJwsHeader import org.junit.Test import javax.crypto.spec.SecretKeySpec -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertSame +import static org.junit.Assert.* class ConstantKeyLocatorTest { @@ -49,4 +49,10 @@ class ConstantKeyLocatorTest { assertEquals msg, uje.getMessage() } } + + @Test + void testApply() { + def locator = new ConstantKeyLocator(null, null) + assertNull locator.apply(new DefaultHeader()) + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultPasswordKeyTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultPasswordKeyTest.groovy new file mode 100644 index 000000000..037d22d6c --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultPasswordKeyTest.groovy @@ -0,0 +1,100 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.PasswordKey +import org.junit.Before +import org.junit.Test + +import static org.junit.Assert.* + +@SuppressWarnings('GroovyAccessibility') +class DefaultPasswordKeyTest { + + private char[] PASSWORD + private DefaultPasswordKey KEY + + @Before + void setup() { + PASSWORD = "whatever".toCharArray() + KEY = new DefaultPasswordKey(PASSWORD) + } + + @Test + void testNewInstance() { + assertArrayEquals PASSWORD, KEY.getPassword() + assertEquals DefaultPasswordKey.NONE_ALGORITHM, KEY.getAlgorithm() + assertEquals DefaultPasswordKey.RAW_FORMAT, KEY.getFormat() + } + + @Test + void testGetEncodedUnsupported() { + try { + KEY.getEncoded() + fail() + } catch (UnsupportedOperationException expected) { + assertEquals DefaultPasswordKey.ENCODED_DISABLED_MSG, expected.getMessage() + } + } + + @Test + void testSymmetricChange() { + //assert change in backing array changes key as well: + PASSWORD[0] = 'b' + assertArrayEquals PASSWORD, KEY.getPassword() + } + + @Test + void testSymmetricDestroy() { + KEY.destroy() + assertTrue KEY.isDestroyed() + for(char c : PASSWORD) { //assert clearing key clears backing array: + assertTrue c == (char)'\u0000' + } + } + + @Test + void testDestroyIdempotent() { + testSymmetricDestroy() + //now do it again to assert idempotent result: + KEY.destroy() + assertTrue KEY.isDestroyed() + for(char c : PASSWORD) { + assertTrue c == (char)'\u0000' + } + } + + @Test + void testDestroyPreventsPassword() { + KEY.destroy() + try { + KEY.getPassword() + fail() + } catch (IllegalStateException expected) { + assertEquals DefaultPasswordKey.DESTROYED_MSG, expected.getMessage() + } + } + + @Test + void testEquals() { + PasswordKey key2 = Keys.forPassword(PASSWORD) + assertArrayEquals KEY.getPassword(), key2.getPassword() + assertEquals KEY, key2 + assertNotEquals KEY, new Object() + } + + @Test + void testHashCode() { + PasswordKey key2 = Keys.forPassword(PASSWORD) + assertArrayEquals KEY.getPassword(), key2.getPassword() + assertEquals KEY.hashCode(), key2.hashCode() + } + + @Test + void testToString() { + assertEquals 'password=', KEY.toString() + PasswordKey key2 = Keys.forPassword(PASSWORD) + assertArrayEquals KEY.getPassword(), key2.getPassword() + assertEquals KEY.toString(), key2.toString() + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy index 8cf63e64b..142aa95e0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy @@ -1,15 +1,12 @@ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.io.Encoders + import io.jsonwebtoken.security.EcPrivateJwk import io.jsonwebtoken.security.EcPublicJwk -import io.jsonwebtoken.security.SignatureAlgorithms import io.jsonwebtoken.security.UnsupportedKeyException -import org.junit.Ignore import org.junit.Test import java.security.Key -import java.security.KeyPair import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey @@ -28,12 +25,40 @@ class DispatchingJwkFactoryTest { } @Test(expected = UnsupportedKeyException) - void testUnknownKeyType() { + void testUnknownKtyValue() { def ctx = new DefaultJwkContext(); ctx.put('kty', 'foo') new DispatchingJwkFactory().createJwk(ctx) } + @Test + void testUnknownKeyType() { + def key = new Key() { + @Override + String getAlgorithm() { + return null + } + + @Override + String getFormat() { + return null + } + + @Override + byte[] getEncoded() { + return new byte[0] + } + } + def ctx = new DefaultJwkContext().setKey(key) + try { + new DispatchingJwkFactory().createJwk(ctx) + fail() + } catch (UnsupportedKeyException uke) { + String msg = 'Unable to create JWK for unrecognized key of type io.jsonwebtoken.impl.security.DispatchingJwkFactoryTest$1: there is no known JWK Factory capable of creating JWKs for this key type.' + assertEquals msg, uke.getMessage() + } + } + @Test void testEcKeyPairToKey() { @@ -71,25 +96,4 @@ class DispatchingJwkFactoryTest { assertEquals jwk.x, x assertEquals jwk.y, y } - - @Test - @Ignore - //TODO re-enable - void testEcKeyPairToJwk() { - - KeyPair pair = SignatureAlgorithms.ES256.generateKeyPair() - ECPublicKey pubKey = (ECPublicKey) pair.getPublic() - def ctx = new DefaultJwkContext() - ctx.setKey(pubKey) - - DispatchingJwkFactory factory = new DispatchingJwkFactory() - - def jwk = factory.createJwk(ctx) - - assertNotNull jwk - assertEquals "EC", jwk.kty - assertEquals Encoders.BASE64URL.encode(pubKey.w.affineX.toByteArray()), jwk.x - assertEquals Encoders.BASE64URL.encode(pubKey.w.affineY.toByteArray()), jwk.y - assertNull jwk.d //public keys should not populate the private key 'd' parameter - } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index 5092069b5..51ead82a8 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -1,6 +1,7 @@ package io.jsonwebtoken.impl.security +import io.jsonwebtoken.impl.lang.Converters import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.security.* @@ -13,7 +14,10 @@ import java.security.PrivateKey import java.security.PublicKey import java.security.cert.X509Certificate import java.security.interfaces.ECKey +import java.security.interfaces.ECPublicKey import java.security.interfaces.RSAPublicKey +import java.security.spec.ECParameterSpec +import java.security.spec.ECPoint import static org.junit.Assert.* @@ -84,6 +88,17 @@ class JwksTest { } } + @Test + void testBuilderWithoutState() { + try { + Jwks.builder().build() + fail() + } catch (IllegalStateException ise) { + String msg = 'A java.security.Key or one or more name/value pairs must be provided to create a JWK.' + assertEquals msg, ise.getMessage() + } + } + @Test void testBuilderWithSecretKey() { def jwk = Jwks.builder().setKey(SKEY).build() @@ -207,4 +222,83 @@ class JwksTest { assertEquals priv, jwkPair.getPrivate() } } + + @Test + void testInvalidCurvePoint() { + def algs = [SignatureAlgorithms.ES256, SignatureAlgorithms.ES384, SignatureAlgorithms.ES512] + + for(EllipticCurveSignatureAlgorithm alg : algs) { + + def pair = alg.generateKeyPair() + ECPublicKey pubKey = pair.getPublic() as ECPublicKey + + EcPublicJwk jwk = Jwks.builder().setKey(pubKey).build() + + //try creating a JWK with a bad point: + def badPubKey = new InvalidECPublicKey(pubKey) + try { + Jwks.builder().setKey(badPubKey).build() + } catch (InvalidKeyException ike) { + String curveId = jwk.get('crv') + String msg = String.format(EcPublicJwkFactory.KEY_CONTAINS_FORMAT_MSG, curveId, curveId) + assertEquals msg, ike.getMessage() + } + + BigInteger p = pubKey.getParams().getCurve().getField().getP() + def outOfFieldRange = [BigInteger.ZERO, BigInteger.ONE,p, p.add(BigInteger.valueOf(1))] + for(def x : outOfFieldRange) { + Map modified = new LinkedHashMap<>(jwk) + modified.put('x', Converters.BIGINT.applyTo(x)) + try { + Jwks.builder().putAll(modified).build() + } catch (InvalidKeyException ike) { + String expected = String.format(EcPublicJwkFactory.JWK_CONTAINS_FORMAT_MSG, jwk.get('crv'), modified) + assertEquals(expected, ike.getMessage()) + } + } + for(def y : outOfFieldRange) { + Map modified = new LinkedHashMap<>(jwk) + modified.put('y', Converters.BIGINT.applyTo(y)) + try { + Jwks.builder().putAll(modified).build() + } catch (InvalidKeyException ike) { + String expected = String.format(EcPublicJwkFactory.JWK_CONTAINS_FORMAT_MSG, jwk.get('crv'), modified) + assertEquals(expected, ike.getMessage()) + } + } + } + } + + private static class InvalidECPublicKey implements ECPublicKey { + + private final ECPublicKey good; + + InvalidECPublicKey(ECPublicKey good) { + this.good = good; + } + @Override + ECPoint getW() { + return ECPoint.POINT_INFINITY // bad value, should make all 'contains' validations fail + } + + @Override + String getAlgorithm() { + return good.getAlgorithm() + } + + @Override + String getFormat() { + return good.getFormat() + } + + @Override + byte[] getEncoded() { + return good.getEncoded() + } + + @Override + ECParameterSpec getParams() { + return good.getParams() + } + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy index 6e85d53b7..289eb5331 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy @@ -1,15 +1,41 @@ package io.jsonwebtoken.impl.security +import io.jsonwebtoken.JweHeader +import io.jsonwebtoken.Jwts import io.jsonwebtoken.impl.DefaultJweHeader -import io.jsonwebtoken.security.EncryptionAlgorithms -import io.jsonwebtoken.security.KeyAlgorithms -import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.* import org.junit.Ignore import org.junit.Test +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +@SuppressWarnings('SpellCheckingInspection') class Pbes2HsAkwAlgorithmTest { - @Ignore // for manual/developer testing only. Takes a long time and there is no deterministic output to assert + private static PasswordKey KEY = Keys.forPassword("12345678".toCharArray()) + private static List ALGS = [KeyAlgorithms.PBES2_HS256_A128KW, + KeyAlgorithms.PBES2_HS384_A192KW, + KeyAlgorithms.PBES2_HS512_A256KW] as List + + @Test + void testInsufficientIterations() { + for (Pbes2HsAkwAlgorithm alg : ALGS) { + int iterations = 50 // must be 1000 or more + JweHeader header = Jwts.jweHeader().setPbes2Count(iterations) + KeyRequest req = new DefaultKeyRequest<>(null, null, KEY, header, EncryptionAlgorithms.A256GCM) + try { + alg.getEncryptionKey(req) + fail() + } catch (IllegalArgumentException iae) { + assertEquals Pbes2HsAkwAlgorithm.MIN_ITERATIONS_MSG_PREFIX + iterations, iae.getMessage() + + } + } + } + + @Ignore + // for manual/developer testing only. Takes a long time and there is no deterministic output to assert @Test void test() { @@ -24,16 +50,17 @@ class Pbes2HsAkwAlgorithmTest { //double scale = 0.5035246727 def password = 'hellowor'.toCharArray() - def key = Keys.forPbe().setPassword(password).setIterations(iterations).build() - def req = new DefaultKeyRequest(null, null, key, new DefaultJweHeader(), EncryptionAlgorithms.A128GCM) - int sum = 0; - for(int i = 0; i < tries; i++) { + def header = new DefaultJweHeader().setPbes2Count(iterations) + def key = Keys.forPassword(password) + def req = new DefaultKeyRequest(null, null, key, header, EncryptionAlgorithms.A128GCM) + int sum = 0 + for (int i = 0; i < tries; i++) { long start = System.currentTimeMillis() alg.getEncryptionKey(req) long end = System.currentTimeMillis() - long duration = end - start; + long duration = end - start if (i >= skip) { - sum+= duration + sum += duration } println "Try $i: ${alg.id} took $duration millis" } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy index 9fdd8c44c..dfe5d5d93 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy @@ -8,19 +8,21 @@ import io.jsonwebtoken.io.SerializationException import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.security.KeyRequest import io.jsonwebtoken.security.Keys -import io.jsonwebtoken.security.PbeKey +import io.jsonwebtoken.security.PasswordKey import io.jsonwebtoken.security.SecurityRequest import org.junit.Test import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec import java.nio.charset.StandardCharsets +import java.security.Key import static org.junit.Assert.* /** * https://datatracker.ietf.org/doc/html/rfc7517#appendix-C */ +@SuppressWarnings('SpellCheckingInspection') class RFC7517AppendixCTest { private static final String rfcString(String s) { @@ -205,9 +207,11 @@ class RFC7517AppendixCTest { 101, 100, 46] as byte[] // "The Salt value (UTF8(Alg) || 0x00 || Salt Input) is": + @SuppressWarnings('unused') private static final byte[] RFC_SALT_VALUE = [80, 66, 69, 83, 50, 45, 72, 83, 50, 53, 54, 43, 65, 49, 50, 56, 75, 87, 0, 217, 96, 147, 112, 150, 117, 70, 247, 127, 8, 155, 137, 174, 42, 80, 215] as byte[] + @SuppressWarnings('unused') private static final byte[] RFC_PBKDF2_DERIVED_KEY_BYTES = [110, 171, 169, 92, 129, 92, 109, 117, 233, 242, 116, 233, 170, 14, 24, 75] @@ -270,18 +274,19 @@ class RFC7517AppendixCTest { def encAlg = new HmacAesAeadAlgorithm(128) { @Override SecretKey generateKey() { - return RFC_CEK; + return RFC_CEK } @Override protected byte[] ensureInitializationVector(SecurityRequest request) { - return RFC_IV; + return RFC_IV } } + //noinspection unused def keyAlg = new Pbes2HsAkwAlgorithm(128) { @Override - protected byte[] generateInputSalt(KeyRequest request) { - return RFC_P2S; + protected byte[] generateInputSalt(KeyRequest request) { + return RFC_P2S } } def serializer = new Serializer() { @@ -300,25 +305,27 @@ class RFC7517AppendixCTest { //JSON serialization order isn't guaranteed, so now that we've asserted the values are correct, //return the exact serialization order expected in the RFC test: - return RFC_JWE_PROTECTED_HEADER_JSON.getBytes(StandardCharsets.UTF_8); + return RFC_JWE_PROTECTED_HEADER_JSON.getBytes(StandardCharsets.UTF_8) } } - PbeKey pbeKey = Keys.forPbe().setPassword(RFC_SHARED_PASSPHRASE.toCharArray()).setIterations(RFC_P2C).build() + PasswordKey key = Keys.forPassword(RFC_SHARED_PASSPHRASE.toCharArray()) String compact = Jwts.jweBuilder() .setPayload(RFC_JWK_JSON) - .setHeaderParam('cty', 'jwk+json') + .setHeader(Jwts.jweHeader() + .setContentType('jwk+json') + .setPbes2Count(RFC_P2C)) .encryptWith(encAlg) - .withKeyFrom(pbeKey, keyAlg) + .withKeyFrom(key, keyAlg) .serializeToJsonWith(serializer) //ensure JJWT created the header as expected with an assertion serializer - .compact(); + .compact() assertEquals RFC_COMPACT_JWE, compact //ensure we can decrypt now: Jwe jwe = Jwts.parserBuilder() - .decryptWith(new SecretKeySpec(RFC_SHARED_PASSPHRASE_BYTES, "RAW")) + .decryptWith(key) .build() .parsePlaintextJwe(compact) diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy index bdc21e75e..06cc2f60a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy @@ -16,12 +16,12 @@ package io.jsonwebtoken.security import io.jsonwebtoken.impl.security.DefaultEllipticCurveSignatureAlgorithm +import io.jsonwebtoken.impl.security.DefaultPasswordKey import io.jsonwebtoken.impl.security.DefaultRsaSignatureAlgorithm -import io.jsonwebtoken.impl.security.JcaPbeKey +import io.jsonwebtoken.impl.security.KeysBridge import org.junit.Test import javax.crypto.SecretKey -import javax.crypto.interfaces.PBEKey import javax.crypto.spec.SecretKeySpec import java.security.KeyPair import java.security.PrivateKey @@ -32,9 +32,9 @@ import java.security.interfaces.ECPublicKey import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey -import static org.easymock.EasyMock.* import static org.junit.Assert.* +@SuppressWarnings('GroovyAccessibility') class KeysTest { private static final Random RANDOM = new SecureRandom() @@ -47,7 +47,9 @@ class KeysTest { @Test void testPrivateCtor() { //for code coverage purposes only + //noinspection GroovyResultOfObjectAllocationIgnored new Keys() + new KeysBridge() } @Test @@ -242,15 +244,10 @@ class KeysTest { } @Test - void testToPbeKey() { - - PBEKey jcaKey = createMock(PBEKey) - //asserts key is wrapped and no methods are called (i.e. we don't need to copy any values) - replay(jcaKey) - - PbeKey key = Keys.toPbeKey(jcaKey) - assertTrue key instanceof JcaPbeKey - - verify jcaKey + void testForPassword() { + def password = "whatever".toCharArray() + PasswordKey key = Keys.forPassword(password) + assertArrayEquals password, key.getPassword() + assertTrue key instanceof DefaultPasswordKey } } From f0801da65dd8aeac26c824cc9a0ea707597ac2aa Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 24 Oct 2021 11:02:07 -0700 Subject: [PATCH 09/75] fixed erroneous soptimize imports' --- impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 90a281391..9220cce21 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -15,7 +15,11 @@ */ package io.jsonwebtoken -import io.jsonwebtoken.impl.* +import io.jsonwebtoken.impl.DefaultClaims +import io.jsonwebtoken.impl.DefaultHeader +import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.impl.DefaultJwsHeader +import io.jsonwebtoken.impl.JwtTokenizer import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec import io.jsonwebtoken.impl.lang.Services @@ -23,7 +27,11 @@ import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.lang.Strings -import io.jsonwebtoken.security.* +import io.jsonwebtoken.security.AsymmetricKeySignatureAlgorithm +import io.jsonwebtoken.security.SecretKeySignatureAlgorithm +import io.jsonwebtoken.security.SignatureAlgorithm +import io.jsonwebtoken.security.SignatureAlgorithms +import io.jsonwebtoken.security.WeakKeyException import org.junit.Test import javax.crypto.Mac From 2d87821640b5294070f6203fc9fe336c7c87e1fa Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Mon, 1 Nov 2021 19:31:30 -0700 Subject: [PATCH 10/75] A bit of cleanup, more test cases... --- .../java/io/jsonwebtoken/lang/Arrays.java | 45 ++++- .../java/io/jsonwebtoken/lang/Assert.java | 2 +- .../io/jsonwebtoken/lang/Collections.java | 55 ++++-- .../java/io/jsonwebtoken/lang/Strings.java | 2 + .../java/io/jsonwebtoken/security/Keys.java | 9 +- .../io/jsonwebtoken/security/PasswordKey.java | 8 - .../java/io/jsonwebtoken/impl/io/Codec.java | 38 ++++ .../jsonwebtoken/impl/io/CodecConverter.java | 32 ---- .../impl/lang/BigIntegerUBytesConverter.java | 6 +- .../io/jsonwebtoken/impl/lang/Converters.java | 13 +- .../impl/lang/EncodedObjectConverter.java | 1 + .../io/jsonwebtoken/impl/lang/Fields.java | 2 +- .../impl/lang/UriStringConverter.java | 4 + .../impl/security/AbstractAsymmetricJwk.java | 8 +- .../impl/security/AbstractEcJwkFactory.java | 36 ++-- .../security/AbstractFamilyJwkFactory.java | 2 +- .../impl/security/AbstractJwk.java | 27 ++- .../impl/security/AesAlgorithm.java | 46 ++--- ...erter.java => JwtX509StringConverter.java} | 23 +-- .../jsonwebtoken/impl/security/KeyPairs.java | 25 ++- .../io/jsonwebtoken/impl/io/CodecTest.groovy | 23 +++ .../lang/BigIntegerUBytesConverterTest.groovy | 2 +- .../security/AbstractEcJwkFactoryTest.groovy | 122 +++++++++++++ .../impl/security/AesAlgorithmTest.groovy | 64 ++++++- .../impl/security/JwksTest.groovy | 13 +- .../impl/security/KeyPairsTest.groovy | 170 ++++++++++++++++++ .../security/Pbes2HsAkwAlgorithmTest.groovy | 10 +- 27 files changed, 630 insertions(+), 158 deletions(-) create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/io/Codec.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/io/CodecConverter.java rename impl/src/main/java/io/jsonwebtoken/impl/security/{JwkX509StringConverter.java => JwtX509StringConverter.java} (66%) create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/io/CodecTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy diff --git a/api/src/main/java/io/jsonwebtoken/lang/Arrays.java b/api/src/main/java/io/jsonwebtoken/lang/Arrays.java index 9c0cec8ad..f0db0dc2b 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Arrays.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Arrays.java @@ -15,6 +15,7 @@ */ package io.jsonwebtoken.lang; +import java.lang.reflect.Array; import java.util.List; /** @@ -22,7 +23,8 @@ */ public final class Arrays { - private Arrays(){} //prevent instantiation + private Arrays() { + } //prevent instantiation public static int length(T[] a) { return a == null ? 0 : a.length; @@ -39,4 +41,45 @@ public static int length(byte[] bytes) { public static byte[] clean(byte[] bytes) { return length(bytes) > 0 ? bytes : null; } + + public static Object copy(Object obj) { + if (obj == null) { + return null; + } + Assert.isTrue(Objects.isArray(obj), "Argument must be an array."); + if (obj instanceof Object[]) { + return ((Object[]) obj).clone(); + } + if (obj instanceof boolean[]) { + return ((boolean[]) obj).clone(); + } + if (obj instanceof byte[]) { + return ((byte[]) obj).clone(); + } + if (obj instanceof char[]) { + return ((char[]) obj).clone(); + } + if (obj instanceof double[]) { + return ((double[]) obj).clone(); + } + if (obj instanceof float[]) { + return ((float[]) obj).clone(); + } + if (obj instanceof int[]) { + return ((int[]) obj).clone(); + } + if (obj instanceof long[]) { + return ((long[]) obj).clone(); + } + if (obj instanceof short[]) { + return ((short[]) obj).clone(); + } + Class componentType = obj.getClass().getComponentType(); + int length = Array.getLength(obj); + Object[] copy = (Object[]) Array.newInstance(componentType, length); + for (int i = 0; i < length; i++) { + copy[i] = Array.get(obj, i); + } + return copy; + } } diff --git a/api/src/main/java/io/jsonwebtoken/lang/Assert.java b/api/src/main/java/io/jsonwebtoken/lang/Assert.java index 7af714b90..5cfa0ff0a 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Assert.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Assert.java @@ -282,7 +282,7 @@ public static > T notEmpty(T collection, String message) * @param collection the collection to check * @throws IllegalArgumentException if the collection is null or has no elements */ - public static void notEmpty(Collection collection) { + public static void notEmpty(Collection collection) { notEmpty(collection, "[Assertion failed] - this collection must not be empty: it must contain at least 1 element"); } diff --git a/api/src/main/java/io/jsonwebtoken/lang/Collections.java b/api/src/main/java/io/jsonwebtoken/lang/Collections.java index d656a3348..73749daba 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Collections.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Collections.java @@ -61,16 +61,47 @@ public static Set setOf(T... elements) { } /** - * Shorter convenience alias for {@link java.util.Collections#unmodifiableSet} so both classes + * Shorter null-safe convenience alias for {@link java.util.Collections#unmodifiableList(List)} so both classes * don't need to be imported. * - * @param s set to wrap in an immutable/unmodifiable collection - * @param type of elements in the set - * @return an immutable wrapper for {@code s}. + * @param m map to wrap in an immutable/unmodifiable collection + * @param map key type + * @param map value type + * @return an immutable wrapper for {@code m}. * @since JJWT_RELEASE_VERSION */ - public static Set immutable(Set s) { - return java.util.Collections.unmodifiableSet(s); + public static Map immutable(Map m) { + return m != null ? java.util.Collections.unmodifiableMap(m) : null; + } + + public static Set immutable(Set set) { + return set != null ? java.util.Collections.unmodifiableSet(set) : null; + } + + public static List immutable(List list) { + return list != null ? java.util.Collections.unmodifiableList(list) : null; + } + + /** + * Null-safe factory method that returns an immutable/unmodifiable view of the specified collection instance. + * Works for {@link List}, {@link Set} and {@link Collection} arguments. + * + * @param c collection to wrap in an immutable/unmodifiable collection + * @param type of elements in the collection + * @return an immutable wrapper for {@code l}. + * @since JJWT_RELEASE_VERSION + */ + @SuppressWarnings("unchecked") + public static > C immutable(C c) { + if (c == null) { + return null; + } else if (c instanceof Set) { + return (C) java.util.Collections.unmodifiableSet((Set) c); + } else if (c instanceof List) { + return (C) java.util.Collections.unmodifiableList((List) c); + } else { + return (C) java.util.Collections.unmodifiableCollection(c); + } } /** @@ -80,8 +111,8 @@ public static Set immutable(Set s) { * @param collection the Collection to check * @return whether the given Collection is empty */ - public static boolean isEmpty(Collection collection) { - return (collection == null || collection.isEmpty()); + public static boolean isEmpty(Collection collection) { + return size(collection) == 0; } /** @@ -91,7 +122,7 @@ public static boolean isEmpty(Collection collection) { * @return the collection's size or {@code 0} if the collection is {@code null}. * @since 0.9.2 */ - public static int size(Collection collection) { + public static int size(Collection collection) { return collection == null ? 0 : collection.size(); } @@ -102,7 +133,7 @@ public static int size(Collection collection) { * @return the map's size or {@code 0} if the map is {@code null}. * @since 0.9.2 */ - public static int size(Map map) { + public static int size(Map map) { return map == null ? 0 : map.size(); } @@ -113,8 +144,8 @@ public static int size(Map map) { * @param map the Map to check * @return whether the given Map is empty */ - public static boolean isEmpty(Map map) { - return (map == null || map.isEmpty()); + public static boolean isEmpty(Map map) { + return size(map) == 0; } /** diff --git a/api/src/main/java/io/jsonwebtoken/lang/Strings.java b/api/src/main/java/io/jsonwebtoken/lang/Strings.java index ec0d76c26..6db89639c 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Strings.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Strings.java @@ -32,6 +32,8 @@ public final class Strings { + public static final String EMPTY = ""; + private static final String FOLDER_SEPARATOR = "/"; private static final String WINDOWS_FOLDER_SEPARATOR = "\\"; diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index e4e2f8acb..a8f4e64ac 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -230,7 +230,14 @@ public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws /** * Returns a new {@link PasswordKey} suitable for use with password-based key derivation algorithms. * Usage Note: Using {@code PasswordKey}s outside of key derivation contexts will likely - * fail. See the {@link PasswordKey} JavaDoc for more, and also note the Password Safety section. + * fail. See the {@link PasswordKey} JavaDoc for more, and also note the Password Safety section below. + *

    Password Safety

    + *

    Instances returned by this method directly share the specified {@code password} character array argument - + * changes to that char array will be reflected in the returned key, and similarly, any call to the key's + * {@link PasswordKey#destroy()} method will clear/overwrite the shared char array. This is to ensure that + * any clearing of the source password char array for security/safety reasons also guarantees the key is also + * cleared and vice versa. However, as is standard for JCA keys, calling {@link PasswordKey#getPassword()} will + * return a separate independent clone of the underlying character array.

    * * @param password the raw password character array to use with password-based key derivation algorithms. * @return a new {@link PasswordKey} that shares the specified {@code password} character array. diff --git a/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java b/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java index eb4e6df85..26ec0fc95 100644 --- a/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java +++ b/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java @@ -13,14 +13,6 @@ * JCA subsystem during direct cryptographic operations) will throw an * {@link UnsupportedOperationException UnsupportedOperationException}.

    * - *

    Password Safety

    - *

    Instances returned by this method directly share the specified {@code password} character array argument - - * changes to that char array will be reflected in the returned key, and similarly, any call to the key's - * {@link PasswordKey#destroy() destroy()} method will clear/overwrite the shared char array. This is to ensure that - * any clearing of the source password char array for security/safety reasons also guarantees the key is also - * cleared and vice versa. However, as is standard for JCA keys, calling {@link #getPassword() getPassword()} will - * return a separate independent clone of the underlying character array.

    - * * @see #getPassword() * @since JJWT_RELEASE_VERSION */ diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/Codec.java b/impl/src/main/java/io/jsonwebtoken/impl/io/Codec.java new file mode 100644 index 000000000..73c6f0c67 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/Codec.java @@ -0,0 +1,38 @@ +package io.jsonwebtoken.impl.io; + +import io.jsonwebtoken.impl.lang.Converter; +import io.jsonwebtoken.io.Decoder; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.DecodingException; +import io.jsonwebtoken.io.Encoder; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Assert; + +public class Codec implements Converter { + + public static final Codec BASE64 = new Codec(Encoders.BASE64, Decoders.BASE64); + public static final Codec BASE64URL = new Codec(Encoders.BASE64URL, Decoders.BASE64URL); + + private final Encoder encoder; + private final Decoder decoder; + + public Codec(Encoder encoder, Decoder decoder) { + this.encoder = Assert.notNull(encoder, "Encoder cannot be null."); + this.decoder = Assert.notNull(decoder, "Decoder cannot be null."); + } + + @Override + public String applyTo(byte[] a) { + return this.encoder.encode(a); + } + + @Override + public byte[] applyFrom(String b) { + try { + return this.decoder.decode(b); + } catch (DecodingException e) { + String msg = "Cannot decode input String '" + b + "'. Cause: " + e.getMessage(); + throw new IllegalArgumentException(msg, e); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/CodecConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/io/CodecConverter.java deleted file mode 100644 index afb3dd33d..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/CodecConverter.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.jsonwebtoken.impl.io; - -import io.jsonwebtoken.impl.lang.Converter; -import io.jsonwebtoken.io.Decoder; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.io.Encoder; -import io.jsonwebtoken.io.Encoders; -import io.jsonwebtoken.lang.Assert; - -public class CodecConverter implements Converter { - - public static final CodecConverter BASE64 = new CodecConverter<>(Encoders.BASE64, Decoders.BASE64); - public static final CodecConverter BASE64URL = new CodecConverter<>(Encoders.BASE64URL, Decoders.BASE64URL); - - private final Encoder encoder; - private final Decoder decoder; - - public CodecConverter(Encoder encoder, Decoder decoder) { - this.encoder = Assert.notNull(encoder, "Encoder cannot be null."); - this.decoder = Assert.notNull(decoder, "Decoder cannot be null."); - } - - @Override - public B applyTo(A a) { - return this.encoder.encode(a); - } - - @Override - public A applyFrom(B b) { - return this.decoder.decode(b); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverter.java index 296894b58..0ddbf5433 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverter.java @@ -7,9 +7,8 @@ public class BigIntegerUBytesConverter implements Converter { private static final String NEGATIVE_MSG = - "JWA Base64urlUInt values MUST be >= 0 (non-negative) per the " + - "[JWA RFC 7518, Section 2](https://datatracker.ietf.org/doc/html/rfc7518#section-2) " + - "'Base64urlUInt' definition."; + "JWA Base64urlUInt values MUST be >= 0 (non-negative) per the 'Base64urlUInt' definition in " + + "[JWA RFC 7518, Section 2](https://datatracker.ietf.org/doc/html/rfc7518#section-2)"; @Override public byte[] applyTo(BigInteger bigInt) { @@ -34,6 +33,7 @@ public byte[] applyTo(BigInteger bigInt) { @Override public BigInteger applyFrom(byte[] bytes) { + Assert.notEmpty(bytes, "Byte array cannot be null or empty."); return new BigInteger(1, bytes); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java index 946ba5a15..c6859fdc2 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java @@ -1,7 +1,7 @@ package io.jsonwebtoken.impl.lang; -import io.jsonwebtoken.impl.io.CodecConverter; -import io.jsonwebtoken.impl.security.JwkX509StringConverter; +import io.jsonwebtoken.impl.io.Codec; +import io.jsonwebtoken.impl.security.JwtX509StringConverter; import java.math.BigInteger; import java.net.URI; @@ -13,15 +13,14 @@ public final class Converters { public static final Converter URI = Converters.forEncoded(URI.class, new UriStringConverter()); - public static final Converter BYTES = Converters.forEncoded(byte[].class, CodecConverter.BASE64URL); + public static final Converter BASE64URL_BYTES = Converters.forEncoded(byte[].class, Codec.BASE64URL); public static final Converter X509_CERTIFICATE = - Converters.forEncoded(X509Certificate.class, new JwkX509StringConverter()); - - public static final Converter BIGINT_UNSIGNED_BYTES = new BigIntegerUBytesConverter(); + Converters.forEncoded(X509Certificate.class, new JwtX509StringConverter()); + public static final Converter BIGINT_UBYTES = new BigIntegerUBytesConverter(); public static final Converter BIGINT = Converters.forEncoded(BigInteger.class, - compound(BIGINT_UNSIGNED_BYTES, CodecConverter.BASE64URL)); + compound(BIGINT_UBYTES, Codec.BASE64URL)); //prevent instantiation private Converters() { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java index 39250408d..f590eaea1 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java @@ -14,6 +14,7 @@ public EncodedObjectConverter(Class type, Converter converter) { @Override public Object applyTo(T t) { + Assert.notNull(t, "Value argument cannot be null."); return converter.applyTo(t); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java index 2059f978d..22fbd954e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java @@ -39,7 +39,7 @@ public static Field uri(String id, String name) { } public static FieldBuilder bytes(String id, String name) { - return builder(byte[].class).setConverter(Converters.BYTES).setId(id).setName(name); + return builder(byte[].class).setConverter(Converters.BASE64URL_BYTES).setId(id).setName(name); } public static FieldBuilder bigInt(String id, String name) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/UriStringConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/UriStringConverter.java index b7509daa1..cf97bf43d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/UriStringConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/UriStringConverter.java @@ -1,16 +1,20 @@ package io.jsonwebtoken.impl.lang; +import io.jsonwebtoken.lang.Assert; + import java.net.URI; public class UriStringConverter implements Converter { @Override public String applyTo(URI uri) { + Assert.notNull(uri, "URI cannot be null."); return uri.toString(); } @Override public URI applyFrom(String s) { + Assert.hasText(s, "URI string cannot be null or empty."); try { return URI.create(s); } catch (Exception e) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java index 740b45426..0647c8513 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java @@ -2,6 +2,7 @@ import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.AsymmetricJwk; @@ -36,17 +37,16 @@ public URI getX509Url() { @Override public List getX509CertificateChain() { - return this.context.getX509CertificateChain(); + return Collections.immutable(this.context.getX509CertificateChain()); } @Override public byte[] getX509CertificateSha1Thumbprint() { - return this.context.getX509CertificateSha1Thumbprint(); + return (byte[])Arrays.copy(this.context.getX509CertificateSha1Thumbprint()); } @Override public byte[] getX509CertificateSha256Thumbprint() { - return this.context.getX509CertificateSha256Thumbprint(); + return (byte[])Arrays.copy(this.context.getX509CertificateSha256Thumbprint()); } - } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java index babeea17d..1ae603b3b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java @@ -19,6 +19,7 @@ import java.security.spec.ECPoint; import java.security.spec.ECPublicKeySpec; import java.security.spec.EllipticCurve; +import java.security.spec.InvalidKeySpecException; import java.util.LinkedHashMap; import java.util.Map; @@ -28,16 +29,17 @@ abstract class AbstractEcJwkFactory> ext private static final BigInteger THREE = BigInteger.valueOf(3); private static final Map EC_SPECS_BY_JWA_ID; private static final Map JWA_IDS_BY_CURVE; + private static final String UNSUPPORTED_CURVE_MSG = "The specified ECKey curve does not match a JWA standard curve id."; - private static ECParameterSpec getJcaParameterSpec(String jcaAlgorithmName) throws IllegalStateException { - try { - AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC"); - parameters.init(new ECGenParameterSpec(jcaAlgorithmName)); - return parameters.getParameterSpec(ECParameterSpec.class); - } catch (Exception e) { - String msg = "Unable to obtain JVM ECParameterSpec for JCA algorithm name '" + jcaAlgorithmName + "'."; - throw new IllegalStateException(msg, e); - } + private static ECParameterSpec getJcaParameterSpec(final String jcaAlgorithmName) throws IllegalStateException { + JcaTemplate template = new JcaTemplate("EC", null); + return template.execute(AlgorithmParameters.class, new CheckedFunction() { + @Override + public ECParameterSpec apply(AlgorithmParameters params) throws Exception { + params.init(new ECGenParameterSpec(jcaAlgorithmName)); + return params.getParameterSpec(ECParameterSpec.class); + } + }); } static { @@ -70,8 +72,7 @@ protected static ECParameterSpec getCurveByJwaId(String jwaCurveId) { protected static String getJwaIdByCurve(EllipticCurve curve) { String jwaCurveId = JWA_IDS_BY_CURVE.get(curve); if (jwaCurveId == null) { - String msg = "The specified ECKey curve does not match a JWA standard curve id."; - throw new UnsupportedKeyException(msg); + throw new UnsupportedKeyException(UNSUPPORTED_CURVE_MSG); } return jwaCurveId; } @@ -86,7 +87,7 @@ protected static String getJwaIdByCurve(EllipticCurve curve) { */ // Algorithm defined in http://www.secg.org/sec1-v2.pdf Section 2.3.5 static String toOctetString(int fieldSize, BigInteger coordinate) { - byte[] bytes = Converters.BIGINT_UNSIGNED_BYTES.applyTo(coordinate); + byte[] bytes = Converters.BIGINT_UBYTES.applyTo(coordinate); int mlen = (int) Math.ceil(fieldSize / 8d); if (mlen > bytes.length) { byte[] m = new byte[mlen]; @@ -215,6 +216,11 @@ private static ECPoint doublePoint(ECPoint P, EllipticCurve curve) { super(DefaultEcPublicJwk.TYPE_VALUE, keyType); } + // visible for testing + protected ECPublicKey derivePublic(KeyFactory keyFactory, ECPublicKeySpec spec) throws InvalidKeySpecException { + return (ECPublicKey)keyFactory.generatePublic(spec); + } + protected ECPublicKey derivePublic(final JwkContext ctx) { final ECPrivateKey key = ctx.getKey(); final ECParameterSpec params = key.getParams(); @@ -224,10 +230,10 @@ protected ECPublicKey derivePublic(final JwkContext ctx) { @Override public ECPublicKey apply(KeyFactory kf) { try { - return (ECPublicKey) kf.generatePublic(spec); + return derivePublic(kf, spec); } catch (Exception e) { - String msg = "Unable to derive ECPublicKey from ECPrivateKey {" + ctx + "}."; - throw new UnsupportedKeyException(msg); + String msg = "Unable to derive ECPublicKey from ECPrivateKey: " + e.getMessage(); + throw new UnsupportedKeyException(msg, e); } } }); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java index 4493101b4..64d7b8f5b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java @@ -15,7 +15,7 @@ abstract class AbstractFamilyJwkFactory> implements FamilyJwkFactory { protected static String encode(BigInteger bigInt) { - byte[] unsigned = Converters.BIGINT_UNSIGNED_BYTES.applyTo(bigInt); + byte[] unsigned = Converters.BIGINT_UBYTES.applyTo(bigInt); return Encoders.BASE64URL.encode(unsigned); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java index e0c0e4d6d..ba8c0f067 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java @@ -2,8 +2,10 @@ import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.security.Jwk; import java.security.Key; @@ -36,7 +38,7 @@ public String getType() { @Override public Set getOperations() { - return this.context.getOperations(); + return Collections.immutable(this.context.getOperations()); } @Override @@ -76,22 +78,31 @@ public boolean containsValue(Object value) { @Override public Object get(Object key) { - return this.context.get(key); + Object val = this.context.get(key); + if (val instanceof Map) { + return Collections.immutable((Map) val); + } else if (val instanceof Collection) { + return Collections.immutable((Collection) val); + } else if (Objects.isArray(val)) { + return Arrays.copy(val); + } else { + return val; + } } @Override public Set keySet() { - return this.context.keySet(); + return Collections.immutable(this.context.keySet()); } @Override public Collection values() { - return this.context.values(); + return Collections.immutable(this.context.values()); } @Override public Set> entrySet() { - return this.context.entrySet(); + return Collections.immutable(this.context.entrySet()); } private static Object immutable() { @@ -128,11 +139,9 @@ public int hashCode() { return this.context.hashCode(); } + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") @Override public boolean equals(Object obj) { - if (obj instanceof Map) { - return this.context.equals(obj); - } - return false; + return this.context.equals(obj); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java index c2d52bbd7..4dfbc5a5c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java @@ -16,16 +16,13 @@ import java.security.SecureRandom; import java.security.spec.AlgorithmParameterSpec; -import static io.jsonwebtoken.lang.Arrays.*; - - abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerator { protected static final String KEY_ALG_NAME = "AES"; protected static final int BLOCK_SIZE = 128; protected static final int BLOCK_BYTE_SIZE = BLOCK_SIZE / Byte.SIZE; protected static final int GCM_IV_SIZE = 96; // https://tools.ietf.org/html/rfc7518#section-5.3 - protected static final int GCM_IV_BYTE_SIZE = GCM_IV_SIZE / Byte.SIZE; + //protected static final int GCM_IV_BYTE_SIZE = GCM_IV_SIZE / Byte.SIZE; protected static final String DECRYPT_NO_IV = "This algorithm implementation rejects decryption " + "requests that do not include initialization vectors. AES ciphertext without an IV is weak and " + "susceptible to attack."; @@ -61,14 +58,14 @@ private void validateLengthIfPossible(SecretKey key) { validateLength(key, this.keyBitLength, false); } - protected static String lengthMsg(String id, String type, int requiredLengthInBits, int actualLengthInBits) { + protected static String lengthMsg(String id, String type, int requiredLengthInBits, long actualLengthInBits) { return "The '" + id + "' algorithm requires " + type + " with a length of " + Bytes.bitsMsg(requiredLengthInBits) + ". The provided key has a length of " + Bytes.bitsMsg(actualLengthInBits) + "."; } protected byte[] validateLength(SecretKey key, int requiredBitLength, boolean propagate) { - byte[] keyBytes = null; + byte[] keyBytes; try { keyBytes = key.getEncoded(); @@ -77,9 +74,9 @@ protected byte[] validateLength(SecretKey key, int requiredBitLength, boolean pr throw re; } //can't get the bytes to validate, e.g. hardware security module or later Android, so just return: - return keyBytes; + return null; } - int keyBitLength = keyBytes.length * Byte.SIZE; + long keyBitLength = Bytes.bitLength(keyBytes); if (keyBitLength < requiredBitLength) { throw new WeakKeyException(lengthMsg(getId(), "keys", requiredBitLength, keyBitLength)); } @@ -87,22 +84,21 @@ protected byte[] validateLength(SecretKey key, int requiredBitLength, boolean pr return keyBytes; } - byte[] assertIvLength(final byte[] iv) { - int length = length(iv); - if ((this.ivBitLength / Byte.SIZE) != length) { - String msg = lengthMsg(getId(), "initialization vectors", this.ivBitLength, length * Byte.SIZE); + protected byte[] assertBytes(byte[] bytes, String type, int requiredBitLen) { + long bitLen = Bytes.bitLength(bytes); + if (requiredBitLen != bitLen) { + String msg = lengthMsg(getId(), type, requiredBitLen, bitLen); throw new IllegalArgumentException(msg); } - return iv; + return bytes; + } + + byte[] assertIvLength(final byte[] iv) { + return assertBytes(iv, "initialization vectors", this.ivBitLength); } byte[] assertTag(byte[] tag) { - int len = Arrays.length(tag) * Byte.SIZE; - if (this.tagBitLength != len) { - String msg = lengthMsg(getId(), "authentication tags", this.tagBitLength, len); - throw new IllegalArgumentException(msg); - } - return tag; + return assertBytes(tag, "authentication tags", this.tagBitLength); } byte[] assertDecryptionIv(InitializationVectorSupplier src) throws IllegalArgumentException { @@ -128,17 +124,11 @@ protected byte[] ensureInitializationVector(SecurityRequest request) { } protected AlgorithmParameterSpec getIvSpec(byte[] iv) { - if (Arrays.length(iv) == 0) { - return null; - } + Assert.notEmpty(iv, "Initialization Vector byte array cannot be null or empty."); return this.gcm ? new GCMParameterSpec(BLOCK_SIZE, iv) : new IvParameterSpec(iv); } - protected byte[] getAAD(SecurityRequest request) { - byte[] aad = null; - if (request instanceof AssociatedDataSupplier) { - aad = Arrays.clean(((AssociatedDataSupplier) request).getAssociatedData()); - } - return aad; + protected byte[] getAAD(AssociatedDataSupplier request) { + return Arrays.clean(request.getAssociatedData()); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkX509StringConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwtX509StringConverter.java similarity index 66% rename from impl/src/main/java/io/jsonwebtoken/impl/security/JwkX509StringConverter.java rename to impl/src/main/java/io/jsonwebtoken/impl/security/JwtX509StringConverter.java index 23602ecab..71c65fb51 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkX509StringConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwtX509StringConverter.java @@ -5,8 +5,6 @@ import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.InvalidKeyException; -import io.jsonwebtoken.security.MalformedKeyException; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -14,12 +12,14 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -public class JwkX509StringConverter implements Converter { +public class JwtX509StringConverter implements Converter { - static final JwkX509StringConverter INSTANCE = new JwkX509StringConverter(); + static final JwtX509StringConverter INSTANCE = new JwtX509StringConverter(); - // Returns a Base64 encoded (NOT Base64Url encoded) string of the cert's encoded byte array - // per https://datatracker.ietf.org/doc/html/rfc7517#section-4.7 + // Returns a Base64 encoded (NOT Base64Url encoded) string of the cert's encoded byte array per + // https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6 + // https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.8 + // https://datatracker.ietf.org/doc/html/rfc7517#section-4.7 @Override public String applyTo(X509Certificate cert) { Assert.notNull(cert, "X509Certificate cannot be null."); @@ -29,25 +29,26 @@ public String applyTo(X509Certificate cert) { } catch (CertificateEncodingException e) { String msg = "Unable to access X509Certificate encoded bytes necessary to perform DER " + "Base64-encoding. Certificate: {" + cert + "}. Cause: " + e.getMessage(); - throw new InvalidKeyException(msg, e); + throw new IllegalArgumentException(msg, e); } if (Arrays.length(der) == 0) { String msg = "X509Certificate encoded bytes cannot be null or empty. Certificate: {" + cert + "}."; - throw new InvalidKeyException(msg); + throw new IllegalArgumentException(msg); } return Encoders.BASE64.encode(der); } @Override public X509Certificate applyFrom(String s) { + Assert.hasText(s, "X.509 Certificate encoded string cannot be null or empty."); try { byte[] der = Decoders.BASE64.decode(s); //RFC requires Base64, not Base64Url CertificateFactory cf = CertificateFactory.getInstance("X.509"); InputStream stream = new ByteArrayInputStream(der); - return (X509Certificate)cf.generateCertificate(stream); + return (X509Certificate) cf.generateCertificate(stream); } catch (Exception e) { - String msg = "Unable to convert String value '" + s + "' to X509Certificate instance: " + e.getMessage(); - throw new MalformedKeyException(msg, e); + String msg = "Unable to convert Base64 String '" + s + "' to X509Certificate instance: " + e.getMessage(); + throw new IllegalArgumentException(msg, e); } } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyPairs.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyPairs.java index 04e9773d8..940aa5c8c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyPairs.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyPairs.java @@ -1,10 +1,12 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; import java.security.Key; import java.security.KeyPair; import java.security.PrivateKey; +import java.security.interfaces.ECKey; import java.security.interfaces.RSAKey; public final class KeyPairs { @@ -12,27 +14,24 @@ public final class KeyPairs { private KeyPairs() { } - private static String family(Class clazz) { - return RSAKey.class.equals(clazz) ? "RSA" : "EC"; + private static String familyPrefix(Class clazz) { + if (RSAKey.class.isAssignableFrom(clazz)) { + return "RSA "; + } else if (ECKey.class.isAssignableFrom(clazz)) { + return "EC "; + } else { + return Strings.EMPTY; + } } public static K getKey(KeyPair pair, Class clazz) { - if (pair == null) { - String msg = family(clazz) + " KeyPair cannot be null."; - throw new IllegalArgumentException(msg); - } - String prefix = family(clazz) + " KeyPair "; + Assert.notNull(pair, "KeyPair cannot be null."); + String prefix = familyPrefix(clazz) + "KeyPair "; boolean isPrivate = PrivateKey.class.isAssignableFrom(clazz); Key key = isPrivate ? pair.getPrivate() : pair.getPublic(); return assertKey(key, clazz, prefix); } - public static K assertKey(Key key, Class clazz) { - Assert.notNull(clazz, "Class argument cannot be null."); - String family = family(clazz); - return assertKey(key, clazz, family + " "); - } - public static K assertKey(Key key, Class clazz, String msgPrefix) { Assert.notNull(key, "Key argument cannot be null."); Assert.notNull(clazz, "Class argument cannot be null."); diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/io/CodecTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/io/CodecTest.groovy new file mode 100644 index 000000000..841caa202 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/io/CodecTest.groovy @@ -0,0 +1,23 @@ +package io.jsonwebtoken.impl.io + +import io.jsonwebtoken.io.DecodingException +import org.junit.Test + +import static org.junit.Assert.* + +class CodecTest { + + @Test + void testDecodingExceptionThrowsIAE() { + String s = 't#t' + try { + Codec.BASE64URL.applyFrom(s) + fail() + } catch (IllegalArgumentException expected) { + def cause = expected.getCause() + assertTrue cause instanceof DecodingException + String msg = "Cannot decode input String '$s'. Cause: ${cause.getMessage()}" + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverterTest.groovy index 6374638fe..510c1b955 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverterTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverterTest.groovy @@ -6,7 +6,7 @@ import static org.junit.Assert.* class BigIntegerUBytesConverterTest { - private BigIntegerUBytesConverter CONVERTER = Converters.BIGINT_UNSIGNED_BYTES + private BigIntegerUBytesConverter CONVERTER = Converters.BIGINT_UBYTES as BigIntegerUBytesConverter @Test void testNegative() { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy new file mode 100644 index 000000000..75fc2bb95 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy @@ -0,0 +1,122 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.Jwk +import io.jsonwebtoken.security.SignatureAlgorithms +import io.jsonwebtoken.security.UnsupportedKeyException +import org.junit.Test + +import java.security.KeyFactory +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.spec.ECParameterSpec +import java.security.spec.ECPoint +import java.security.spec.ECPublicKeySpec +import java.security.spec.EllipticCurve +import java.security.spec.InvalidKeySpecException + +import static org.junit.Assert.* + +class AbstractEcJwkFactoryTest { + + @Test + void testInvalidJwaCurveId() { + String id = 'foo' + try { + AbstractEcJwkFactory.getCurveByJwaId(id) + fail() + } catch (UnsupportedKeyException e) { + String msg = "Unrecognized JWA curve id '$id'" + assertEquals msg, e.getMessage() + } + } + + @Test + void testUnsupportedCurve() { + EllipticCurve curve = AbstractEcJwkFactory.getJcaParameterSpec('secp128r1').getCurve() + try { + AbstractEcJwkFactory.getJwaIdByCurve(curve) + fail() + } catch (UnsupportedKeyException e) { + assertEquals AbstractEcJwkFactory.UNSUPPORTED_CURVE_MSG, e.getMessage() + } + } + + @Test + void testMultiplyInfinity() { + ECParameterSpec spec = AbstractEcJwkFactory.getCurveByJwaId('P-256') + def result = AbstractEcJwkFactory.multiply(ECPoint.POINT_INFINITY, BigInteger.valueOf(1), spec) + assertEquals ECPoint.POINT_INFINITY, result + } + + @Test + void testDoubleInfinity() { + ECParameterSpec spec = AbstractEcJwkFactory.getCurveByJwaId('P-256') + def curve = spec.getCurve() + def result = AbstractEcJwkFactory.doublePoint(ECPoint.POINT_INFINITY, curve) + assertEquals ECPoint.POINT_INFINITY, result + } + + @Test + void testAddInfinity() { + ECParameterSpec spec = AbstractEcJwkFactory.getCurveByJwaId('P-256') + def curve = spec.getCurve() + ECPoint point = new ECPoint(BigInteger.valueOf(1), BigInteger.valueOf(2)) // any point is fine for this test + def result = AbstractEcJwkFactory.add(ECPoint.POINT_INFINITY, point, curve) + //adding infinity to a point should return the point: + assertEquals point, result + //adding a point to infinity should return the point: + result = AbstractEcJwkFactory.add(point, ECPoint.POINT_INFINITY, curve) + assertEquals point, result + } + + @Test + void testAddSamePointDoublesIt() { + def pair = SignatureAlgorithms.ES256.generateKeyPair(); + def pub = pair.getPublic() as ECPublicKey + + def spec = pub.getParams() + def curve = spec.getCurve() + def point = pub.getW() + + def doubled = AbstractEcJwkFactory.doublePoint(point, curve) + def added = AbstractEcJwkFactory.add(point, point, curve) + assertEquals doubled, added + } + + @Test + void testDerivePublicFails() { + + def pair = SignatureAlgorithms.ES256.generateKeyPair(); + def priv = pair.getPrivate() as ECPrivateKey + + final def context = new DefaultJwkContext(DefaultEcPrivateJwk.FIELDS) + context.setKey(priv) + + def ex = new InvalidKeySpecException("invalid") + + def factory = new AbstractEcJwkFactory(ECPrivateKey.class) { + @Override + protected Jwk createJwkFromKey(JwkContext ctx) { + return null + } + + @Override + protected Jwk createJwkFromValues(JwkContext ctx) { + return null + } + + @Override + protected ECPublicKey derivePublic(KeyFactory keyFactory, ECPublicKeySpec spec) throws InvalidKeySpecException { + throw ex + } + } + + try { + factory.derivePublic(context) + fail() + } catch (UnsupportedKeyException expected) { + String msg = 'Unable to derive ECPublicKey from ECPrivateKey: invalid' + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy index 667cd9d68..5b77faba8 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy @@ -1,12 +1,19 @@ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.security.* + +import io.jsonwebtoken.security.AeadAlgorithm +import io.jsonwebtoken.security.AeadRequest +import io.jsonwebtoken.security.AeadResult +import io.jsonwebtoken.security.DecryptAeadRequest +import io.jsonwebtoken.security.EncryptionAlgorithms +import io.jsonwebtoken.security.PayloadSupplier +import io.jsonwebtoken.security.SecurityException import org.junit.Test +import javax.crypto.spec.SecretKeySpec import java.security.SecureRandom -import static org.junit.Assert.assertSame -import static org.junit.Assert.fail +import static org.junit.Assert.* /** * @since JJWT_RELEASE_VERSION @@ -34,6 +41,57 @@ class AesAlgorithmTest { } } + @Test + void testValidateLengthKeyExceptionPropagated() { + + def alg = new TestAesAlgorithm('foo', 'foo', 192) + def ex = new java.lang.SecurityException("HSM: not allowed") + def key = new SecretKeySpec(new byte[1], 'AES') { + @Override + byte[] getEncoded() { + throw ex + } + } + + try { + alg.validateLength(key, 192, true) + fail() + } catch (java.lang.SecurityException expected) { + assertSame ex, expected + } + } + + @Test + void testValidateLengthKeyExceptionNotPropagated() { + + def alg = new TestAesAlgorithm('foo', 'foo', 192) + def ex = new java.lang.SecurityException("HSM: not allowed") + def key = new SecretKeySpec(new byte[1], 'AES') { + @Override + byte[] getEncoded() { + throw ex + } + } + + //exception thrown, but we don't propagate: + assertNull alg.validateLength(key, 192, false) + } + + @Test + void testAssertBytesWithLengthMismatch() { + int reqdBitLen = 192 + def alg = new TestAesAlgorithm('foo', 'foo', reqdBitLen) + byte[] bytes = new byte[(reqdBitLen - 8) / Byte.SIZE] + try { + alg.assertBytes(bytes, 'test arrays', reqdBitLen) + fail() + } catch (IllegalArgumentException iae) { + String msg = "The 'foo' algorithm requires test arrays with a length of 192 bits (24 bytes). " + + "The provided key has a length of 184 bits (23 bytes)." + assertEquals msg, iae.getMessage() + } + } + @Test void testGetSecureRandomWhenRequestHasSpecifiedASecureRandom() { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index 51ead82a8..59ddf6ffd 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -1,10 +1,17 @@ package io.jsonwebtoken.impl.security - import io.jsonwebtoken.impl.lang.Converters import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders -import io.jsonwebtoken.security.* +import io.jsonwebtoken.security.AsymmetricKeySignatureAlgorithm +import io.jsonwebtoken.security.EcPublicJwk +import io.jsonwebtoken.security.EllipticCurveSignatureAlgorithm +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.PrivateJwk +import io.jsonwebtoken.security.PublicJwk +import io.jsonwebtoken.security.SecretKeySignatureAlgorithm +import io.jsonwebtoken.security.SignatureAlgorithms import org.junit.Test import javax.crypto.SecretKey @@ -133,7 +140,7 @@ class JwksTest { void testX509CertChain() { //get a test cert: X509Certificate cert = CertUtils.readTestCertificate(SignatureAlgorithms.RS256) - def sval = JwkX509StringConverter.INSTANCE.applyTo(cert) + def sval = JwtX509StringConverter.INSTANCE.applyTo(cert) testProperty('x509CertificateChain', 'x5c', [cert], [sval]) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy new file mode 100644 index 000000000..db891d9f0 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy @@ -0,0 +1,170 @@ +package io.jsonwebtoken.impl.security + + +import io.jsonwebtoken.security.SignatureAlgorithms +import org.junit.Test + +import java.security.Key +import java.security.KeyPair +import java.security.PublicKey +import java.security.interfaces.DSAPublicKey +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.ECParameterSpec +import java.security.spec.ECPoint + +import static org.junit.Assert.* + +class KeyPairsTest { + + @Test + void testPrivateCtor() { // for code coverage only + new KeyPairs() + } + + @Test + void testGetKeyNullPair() { + try { + KeyPairs.getKey(null, ECPublicKey.class) + fail() + } catch (IllegalArgumentException iae) { + assertEquals 'KeyPair cannot be null.', iae.getMessage() + } + } + + @Test + void testUnrecognizedFamily() { + PublicKey pub = new TestECPublicKey() + KeyPair pair = new KeyPair(pub, new TestECPrivateKey()) + Class clazz = DSAPublicKey // unrecognized --> no 'family' prefix in message + try { + KeyPairs.getKey(pair, clazz) + fail() + } catch (IllegalArgumentException iae) { + String msg = "KeyPair public key must be an instance of ${clazz.name}. Type found: ${pub.class.name}" + assertEquals msg, iae.getMessage() + } + } + + @Test + void testGetKeyECMismatch() { + KeyPair pair = SignatureAlgorithms.RS256.generateKeyPair() + Class clazz = ECPublicKey + try { + KeyPairs.getKey(pair, clazz) + } catch (IllegalArgumentException iae) { + String msg = "EC KeyPair public key must be an instance of ${clazz.name}. Type found: ${pair.public.class.name}" + assertEquals msg, iae.getMessage() + } + } + + @Test + void testGetKeyRSAMismatch() { + KeyPair pair = new KeyPair(new TestECPublicKey(), new TestECPrivateKey()) + Class clazz = RSAPublicKey + try { + KeyPairs.getKey(pair, clazz) + } catch (IllegalArgumentException iae) { + String msg = "RSA KeyPair public key must be an instance of ${clazz.name}. Type found: ${pair.public.class.name}" + assertEquals msg, iae.getMessage() + } + } + + @Test + void testAssertPublicKeyTypeMismatch() { + Key key = new TestECPublicKey() + Class clazz = RSAPublicKey + String prefix = 'Foo ' + try { + KeyPairs.assertKey(key, clazz, prefix) + fail() + } catch (IllegalArgumentException iae) { + String msg = "${prefix}public key must be an instance of ${clazz.name}. Type found: ${key.class.name}" + assertEquals msg, iae.getMessage() + } + } + + @Test + void testAssertPrivateKeyTypeMismatch() { + Key key = new TestECPrivateKey() + Class clazz = RSAPrivateKey + String prefix = 'Foo ' + try { + KeyPairs.assertKey(key, clazz, prefix) + fail() + } catch (IllegalArgumentException iae) { + String msg = "${prefix}private key must be an instance of ${clazz.name}. Type found: ${key.class.name}" + assertEquals msg, iae.getMessage() + } + } + + private void printMap(Map m, int indentCount) { + for(def entry : m.entrySet()) { + indentCount.times {print("\t")} + print "${entry.key}: " + if (entry.value instanceof Map) { + println() + printMap(entry.value as Map, indentCount + 1) + } else { + println "${entry.value}" + } + } + } + + private static class TestECPublicKey implements ECPublicKey { + @Override + ECPoint getW() { + return null + } + + @Override + String getAlgorithm() { + return null + } + + @Override + String getFormat() { + return null + } + + @Override + byte[] getEncoded() { + return new byte[0] + } + + @Override + ECParameterSpec getParams() { + return null + } + } + + private static class TestECPrivateKey implements ECPrivateKey { + @Override + BigInteger getS() { + return null + } + + @Override + String getAlgorithm() { + return null + } + + @Override + String getFormat() { + return null + } + + @Override + byte[] getEncoded() { + return new byte[0] + } + + @Override + ECParameterSpec getParams() { + return null + } + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy index 289eb5331..de5bc8d54 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy @@ -3,12 +3,15 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.JweHeader import io.jsonwebtoken.Jwts import io.jsonwebtoken.impl.DefaultJweHeader -import io.jsonwebtoken.security.* +import io.jsonwebtoken.security.EncryptionAlgorithms +import io.jsonwebtoken.security.KeyAlgorithms +import io.jsonwebtoken.security.KeyRequest +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.PasswordKey import org.junit.Ignore import org.junit.Test -import static org.junit.Assert.assertEquals -import static org.junit.Assert.fail +import static org.junit.Assert.* @SuppressWarnings('SpellCheckingInspection') class Pbes2HsAkwAlgorithmTest { @@ -29,7 +32,6 @@ class Pbes2HsAkwAlgorithmTest { fail() } catch (IllegalArgumentException iae) { assertEquals Pbes2HsAkwAlgorithm.MIN_ITERATIONS_MSG_PREFIX + iterations, iae.getMessage() - } } } From ad5c8b12516c87211741d2c3615630a16965c25e Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 3 Nov 2021 22:13:12 -0700 Subject: [PATCH 11/75] Added ECDH-ES key algorithms + RFC tests --- .../jsonwebtoken/security/KeyAlgorithms.java | 9 +- .../jsonwebtoken/impl/DefaultJweHeader.java | 11 +- .../jsonwebtoken/impl/lang/ValueGetter.java | 3 + .../jsonwebtoken/impl/security/ConcatKDF.java | 12 +- .../impl/security/DefaultValueGetter.java | 11 ++ .../impl/security/EcdhKeyAlgorithm.java | 166 ++++++++++++++---- .../impl/security/KeyAlgorithmsBridge.java | 4 + .../impl/security/RFC7518AppendixCTest.groovy | 129 ++++++++++++++ .../security/KeyAlgorithmsTest.groovy | 12 +- 9 files changed, 302 insertions(+), 55 deletions(-) create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index 4c7895e12..aa995de0a 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -77,11 +77,10 @@ private static T forId0(String id) { public static final RsaKeyAlgorithm RSA1_5 = forId0("RSA1_5"); public static final RsaKeyAlgorithm RSA_OAEP = forId0("RSA-OAEP"); public static final RsaKeyAlgorithm RSA_OAEP_256 = forId0("RSA-OAEP-256"); - - //public static final EcKeyAlgorithm ECDH_ES = forId0("ECDH-ES"); - //public static final EcKeyAlgorithm ECDH_ES_A128KW = forId0("ECDH-ES+A128KW"); - //public static final EcKeyAlgorithm ECDH_ES_A192KW = forId0("ECDH-ES+A192KW"); - //public static final EcKeyAlgorithm ECDH_ES_A256KW = forId0("ECDH-ES+A256KW"); + public static final EcKeyAlgorithm ECDH_ES = forId0("ECDH-ES"); + public static final EcKeyAlgorithm ECDH_ES_A128KW = forId0("ECDH-ES+A128KW"); + public static final EcKeyAlgorithm ECDH_ES_A192KW = forId0("ECDH-ES+A192KW"); + public static final EcKeyAlgorithm ECDH_ES_A256KW = forId0("ECDH-ES+A256KW"); public static int estimateIterations(KeyAlgorithm alg, long desiredMillis) { return Classes.invokeStatic(BRIDGE_CLASS, "estimateIterations", ESTIMATE_ITERATIONS_ARG_TYPES, alg, desiredMillis); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java index 884acc4b7..150a7da35 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java @@ -3,6 +3,7 @@ import io.jsonwebtoken.JweHeader; import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; @@ -70,10 +71,7 @@ public byte[] getAgreementPartyUInfo() { @Override public String getAgreementPartyUInfoString() { byte[] bytes = getAgreementPartyUInfo(); - if (bytes == null) { - return null; - } - return new String(bytes, StandardCharsets.UTF_8); + return Arrays.length(bytes) == 0 ? null : new String(bytes, StandardCharsets.UTF_8); } @Override @@ -96,10 +94,7 @@ public byte[] getAgreementPartyVInfo() { @Override public String getAgreementPartyVInfoString() { byte[] bytes = getAgreementPartyVInfo(); - if (bytes == null) { - return null; - } - return new String(bytes, StandardCharsets.UTF_8); + return Arrays.length(bytes) == 0 ? null : new String(bytes, StandardCharsets.UTF_8); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/ValueGetter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/ValueGetter.java index 36075d7a2..423b6d1e4 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/ValueGetter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/ValueGetter.java @@ -1,6 +1,7 @@ package io.jsonwebtoken.impl.lang; import java.math.BigInteger; +import java.util.Map; public interface ValueGetter { @@ -15,4 +16,6 @@ public interface ValueGetter { byte[] getRequiredBytes(String key, int requiredByteLength); BigInteger getRequiredBigInt(String key, boolean sensitive); + + Map getRequiredMap(String key); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java index 47e5b6117..e034b5c10 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java @@ -46,7 +46,7 @@ public Integer apply(MessageDigest instance) { * NIST.800-56A, * Section 5.8.1.1. * - * @param sharedSecretKey shared secret key to use to seed the derived secret. key.getEncoded() must not be empty. + * @param Z shared secret key to use to seed the derived secret. Cannot be null or empty. * @param derivedKeyBitLength the total number of bits (not bytes) required in the returned derived * key. * @param OtherInfo any additional party info to be associated with the derived key. May be null/empty. @@ -56,16 +56,14 @@ public Integer apply(MessageDigest instance) { * @throws SecurityException if unable to perform the necessary {@link MessageDigest} computations to * generate the derived key. */ - public SecretKey deriveKey(SecretKey sharedSecretKey, final long derivedKeyBitLength, final byte[] OtherInfo) + public SecretKey deriveKey(final byte[] Z, final long derivedKeyBitLength, final byte[] OtherInfo) throws UnsupportedKeyException, SecurityException { // OtherInfo argument assertions: final int otherInfoByteLength = Arrays.length(OtherInfo); // sharedSecretKey argument assertions: - Assert.notNull(sharedSecretKey, "sharedSecretKey cannot be null."); - final byte[] Z = SecretJwkFactory.getRequiredEncoded(sharedSecretKey, - "use this key to create a Concat KDF derived key."); + Assert.notEmpty(Z, "Z cannot be null or empty."); // derivedKeyBitLength argument assertions: Assert.isTrue(derivedKeyBitLength > 0, "derivedKeyBitLength must be a positive number."); @@ -88,7 +86,7 @@ public SecretKey deriveKey(SecretKey sharedSecretKey, final long derivedKeyBitLe assert reps <= MAX_REP_COUNT : "derivedKeyBitLength is too large."; // Section 5.8.1.1, Process step #3: - final byte[] counter = new byte[]{0, 0, 0, 0, 0, 0, 0, 1}; // same as 0x01L, but no extra step to convert to byte[] + final byte[] counter = new byte[]{0, 0, 0, 1}; // same as 0x0001L, but no extra step to convert to byte[] // Section 5.8.1.1, Process step #4: long inputBitLength = bitLength(counter) + bitLength(Z) + bitLength(OtherInfo); @@ -104,12 +102,12 @@ public byte[] apply(MessageDigest md) throws Exception { // Section 5.8.1.1, Process step #5: for (long i = 0; i < reps; i++) { + // Section 5.8.1.1, Process step #5.1: md.update(counter); md.update(Z); if (otherInfoByteLength > 0) { md.update(OtherInfo); } - // Section 5.8.1.1, Process step #5.1: byte[] Ki = md.digest(); // Section 5.8.1.1, Process step #5.2: diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java index 9cf6b761e..64e461929 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java @@ -146,4 +146,15 @@ public BigInteger getRequiredBigInt(String key, boolean sensitive) { throw malformed(msg); } } + + @SuppressWarnings("unchecked") + @Override + public Map getRequiredMap(String key) { + Object value = getRequiredValue(key); + if (!(value instanceof Map)) { + String msg = name() + " '" + key + "' value must be a Map. Actual type: " + value.getClass().getName(); + throw malformed(msg); + } + return (Map)value; + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java index aa2ba18a3..0a61a1da2 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java @@ -1,19 +1,27 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.DecryptionKeyRequest; import io.jsonwebtoken.security.EcKeyAlgorithm; import io.jsonwebtoken.security.EcPublicJwk; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.Jwk; import io.jsonwebtoken.security.Jwks; +import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; import io.jsonwebtoken.security.SecurityException; import javax.crypto.KeyAgreement; import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; @@ -22,70 +30,168 @@ import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.spec.ECParameterSpec; +import java.util.Map; -public class EcdhKeyAlgorithm extends CryptoAlgorithm implements EcKeyAlgorithm { +class EcdhKeyAlgorithm extends CryptoAlgorithm implements EcKeyAlgorithm { protected static final String JCA_NAME = "ECDH"; + protected static final String DEFAULT_ID = JCA_NAME + "-ES"; protected static final String EPHEMERAL_PUBLIC_KEY = "epk"; - EcdhKeyAlgorithm(String id) { - super(id, JCA_NAME); + // Per https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.2, 2nd paragraph: + // Key derivation is performed using the Concat KDF, as defined in + // Section 5.8.1 of [NIST.800-56A](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf), + // where the Digest Method is SHA-256. + private static final String CONCAT_KDF_HASH_ALG_NAME = "SHA-256"; + private static final ConcatKDF CONCAT_KDF = new ConcatKDF(CONCAT_KDF_HASH_ALG_NAME); + + private final KeyAlgorithm WRAP_ALG; + + private static String idFor(KeyAlgorithm wrapAlg) { + return wrapAlg instanceof DirectKeyAlgorithm ? DEFAULT_ID : DEFAULT_ID + "+" + wrapAlg.getId(); + } + + EcdhKeyAlgorithm() { + // default ECDH-ES doesn't do a wrap, so we use DirectKeyAlgorithm which is a no-op. That is, we're using + // the Null Object Design Pattern so we don't have to check for null depending on if key wrapping is used or not + this(new DirectKeyAlgorithm()); } - private KeyPair generateKeyPair(final KeyRequest request, final ECParameterSpec spec) { + EcdhKeyAlgorithm(KeyAlgorithm wrapAlg) { + super(idFor(wrapAlg), JCA_NAME); + this.WRAP_ALG = Assert.notNull(wrapAlg, "Wrap algorithm cannot be null."); + } + + //visible for testing + protected KeyPair generateKeyPair(final KeyRequest request, final ECParameterSpec spec) { Assert.notNull(spec, "request key params cannot be null."); return new JcaTemplate("EC", request.getProvider(), ensureSecureRandom(request)) .execute(KeyPairGenerator.class, new CheckedFunction() { - @Override - public KeyPair apply(KeyPairGenerator keyPairGenerator) throws Exception { - keyPairGenerator.initialize(spec, ensureSecureRandom(request)); - return keyPairGenerator.generateKeyPair(); - } - }); + @Override + public KeyPair apply(KeyPairGenerator keyPairGenerator) throws Exception { + keyPairGenerator.initialize(spec, ensureSecureRandom(request)); + return keyPairGenerator.generateKeyPair(); + } + }); } - protected SecretKey generateSecretKey(final KeyRequest request, final PublicKey pub, final PrivateKey priv) { - return execute(request, KeyAgreement.class, new CheckedFunction() { + protected byte[] generateZ(final KeyRequest request, final PublicKey pub, final PrivateKey priv) { + return execute(request, KeyAgreement.class, new CheckedFunction() { @Override - public SecretKey apply(KeyAgreement keyAgreement) throws Exception { - keyAgreement.init(priv); + public byte[] apply(KeyAgreement keyAgreement) throws Exception { + keyAgreement.init(priv, ensureSecureRandom(request)); keyAgreement.doPhase(pub, true); - byte[] derived = keyAgreement.generateSecret(); - return new SecretKeySpec(derived, "AES"); + return keyAgreement.generateSecret(); } }); } + protected String getConcatKDFAlgorithmId(AeadAlgorithm enc) { + return this.WRAP_ALG instanceof DirectKeyAlgorithm ? + Assert.hasText(enc.getId(), "AeadAlgorithm id cannot be null or empty.") : + getId(); + } + + private byte[] createOtherInfo(int keydatalen, String AlgorithmID, byte[] PartyUInfo, byte[] PartyVInfo) { + + // https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.2 "AlgorithmID": + Assert.hasText(AlgorithmID, "AlgorithmId cannot be null or empty."); + byte[] algIdBytes = AlgorithmID.getBytes(StandardCharsets.US_ASCII); + + PartyUInfo = Arrays.length(PartyUInfo) == 0 ? Bytes.EMPTY : PartyUInfo; // ensure not null + PartyVInfo = Arrays.length(PartyVInfo) == 0 ? Bytes.EMPTY : PartyVInfo; // ensure not null + + // Values and order defined in https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.2 and + // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf section 5.8.1.2 : + return Bytes.concat( + Bytes.toBytes(algIdBytes.length), algIdBytes, // AlgorithmID + Bytes.toBytes(PartyUInfo.length), PartyUInfo, // PartyUInfo + Bytes.toBytes(PartyVInfo.length), PartyVInfo, // PartyVInfo + Bytes.toBytes(keydatalen), // SuppPubInfo per https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.2 + Bytes.EMPTY // SuppPrivInfo empty per https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.2 + ); + } + + private int getKeyBitLength(AeadAlgorithm enc) { + SecretKey forBitLen = Assert.notNull(enc.generateKey(), "EncryptionAlgorithm generated key cannot be null."); + byte[] toCount = Assert.notEmpty(forBitLen.getEncoded(), "EncryptionAlgorithm generated key encoded bytes cannot be null or empty."); + return (int)Bytes.bitLength(toCount); // MUST be an integer per the RFC + } + @Override public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { Assert.notNull(request, "Request cannot be null."); - JweHeader header = Assert.notNull(request.getHeader(), "request JweHeader cannot be null."); - E publicKey = Assert.notNull(request.getKey(), "request key cannot be null."); - ECParameterSpec spec = Assert.notNull(publicKey.getParams(), "request key params cannot be null."); + JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); + E publicKey = Assert.notNull(request.getKey(), "Request key cannot be null."); + ECParameterSpec spec = Assert.notNull(publicKey.getParams(), "Request key params cannot be null."); + AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); + + int requiredCekBitLen = getKeyBitLength(enc); + final String AlgorithmID = getConcatKDFAlgorithmId(enc); + byte[] apu = header.getAgreementPartyUInfo(); + byte[] apv = header.getAgreementPartyVInfo(); + byte[] OtherInfo = createOtherInfo(requiredCekBitLen, AlgorithmID, apu, apv); // note: we don't need to validate if specified key's point is on a supported curve here // because that will automatically be asserted when using Jwks.builder().... below - KeyPair pair = generateKeyPair(request, spec); ECPublicKey genPubKey = KeyPairs.getKey(pair, ECPublicKey.class); ECPrivateKey genPrivKey = KeyPairs.getKey(pair, ECPrivateKey.class); + // This asserts that the generated public key (and therefore the request key) is on a JWK-supported curve: + final EcPublicJwk jwk = Jwks.builder().setKey(genPubKey).build(); - SecretKey secretKey = generateSecretKey(request, publicKey, genPrivKey); + byte[] Z = generateZ(request, publicKey, genPrivKey); + SecretKey derived = CONCAT_KDF.deriveKey(Z, requiredCekBitLen, OtherInfo); + + DefaultKeyRequest wrapReq = new DefaultKeyRequest<>(request.getProvider(), request.getSecureRandom(), + derived, request.getHeader(), enc); + KeyResult result = WRAP_ALG.getEncryptionKey(wrapReq); - // This line will assert/guarantee that the generated public key (and therefore the request key) is on - // a JWK-supported curve: - EcPublicJwk jwk = Jwks.builder().setKey(genPubKey).build(); header.put(EPHEMERAL_PUBLIC_KEY, jwk); + return result; + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + + Assert.notNull(request, "Request cannot be null."); + JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); + D privateKey = Assert.notNull(request.getKey(), "Request key cannot be null."); + ECParameterSpec spec = Assert.notNull(privateKey.getParams(), "Request key params cannot be null."); + + ValueGetter getter = new DefaultValueGetter(header); + Map epkValues = getter.getRequiredMap(EPHEMERAL_PUBLIC_KEY); + // This call will assert the EPK, if valid, is also on a NIST curve: + Jwk jwk = Jwks.builder().putAll(epkValues).build(); + if (!(jwk instanceof EcPublicJwk)) { + String msg = "JWE Header '" + EPHEMERAL_PUBLIC_KEY + "' (Ephemeral Public Key) value is not an " + + "EllipticCurve Public JWK as required."; + throw new MalformedJwtException(msg); + } + EcPublicJwk epk = (EcPublicJwk)jwk; + // Now, while the EPK might be on a NIST curve, we need to ensure it's on the exact curve associted with the + // private key: + if (!EcPublicJwkFactory.contains(privateKey.getParams().getCurve(), epk.toKey().getW())) { + String msg = "JWE Header '" + EPHEMERAL_PUBLIC_KEY + "' (Ephemeral Public Key) value does not represent " + + "a point on the expected curve."; + throw new InvalidKeyException(msg); + } + + AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); + + int requiredCekBitLen = getKeyBitLength(enc); + final String AlgorithmID = getConcatKDFAlgorithmId(enc); byte[] apu = header.getAgreementPartyUInfo(); byte[] apv = header.getAgreementPartyVInfo(); + byte[] OtherInfo = createOtherInfo(requiredCekBitLen, AlgorithmID, apu, apv); + byte[] Z = generateZ(request, epk.toKey(), privateKey); + SecretKey derived = CONCAT_KDF.deriveKey(Z, requiredCekBitLen, OtherInfo); - return null; - } + DecryptionKeyRequest unwrapReq = new DefaultDecryptionKeyRequest<>(request.getProvider(), + request.getSecureRandom(), derived, header, enc, request.getPayload()); - @Override - public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { - return null; + return WRAP_ALG.getDecryptionKey(unwrapReq); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java index af13d3d0f..c5e415b3b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java @@ -58,6 +58,10 @@ private KeyAlgorithmsBridge() { new Pbes2HsAkwAlgorithm(128), new Pbes2HsAkwAlgorithm(192), new Pbes2HsAkwAlgorithm(256), + new EcdhKeyAlgorithm<>(), + new EcdhKeyAlgorithm<>(new AesWrapKeyAlgorithm(128)), + new EcdhKeyAlgorithm<>(new AesWrapKeyAlgorithm(192)), + new EcdhKeyAlgorithm<>(new AesWrapKeyAlgorithm(256)), new DefaultRsaKeyAlgorithm<>(RSA1_5_ID, RSA1_5_TRANSFORMATION), new DefaultRsaKeyAlgorithm<>(RSA_OAEP_ID, RSA_OAEP_TRANSFORMATION), new DefaultRsaKeyAlgorithm<>(RSA_OAEP_256_ID, RSA_OAEP_256_TRANSFORMATION, RSA_OAEP_256_SPEC) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy new file mode 100644 index 000000000..adf2b3c1d --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy @@ -0,0 +1,129 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.Claims +import io.jsonwebtoken.Jwe +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.lang.Services +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.security.EcPrivateJwk +import io.jsonwebtoken.security.EncryptionAlgorithms +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.KeyRequest +import io.jsonwebtoken.security.KeyResult +import io.jsonwebtoken.security.SecurityException +import org.junit.Test + +import java.nio.charset.StandardCharsets +import java.security.KeyPair +import java.security.spec.ECParameterSpec + +import static org.junit.Assert.* + +class RFC7518AppendixCTest { + + private static final String rfcString(String s) { + return s.replaceAll('[\\s]', '') + } + + private static final Map fromEncoded(String s) { + byte[] json = Decoders.BASE64URL.decode(s) + return Services.loadFirst(Deserializer.class).deserialize(json) as Map + } + + private static final Map fromJson(String s) { + byte[] bytes = s.getBytes(StandardCharsets.UTF_8) + return Services.loadFirst(Deserializer.class).deserialize(bytes) as Map + } + + private static EcPrivateJwk readJwk(String json) { + Map m = fromJson(json) + return Jwks.builder().putAll(m).build() as EcPrivateJwk + } + + // https://datatracker.ietf.org/doc/html/rfc7517#appendix-C.1 + private static final String ALICE_EPHEMERAL_JWK_STRING = rfcString(''' + {"kty":"EC", + "crv":"P-256", + "x":"gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", + "y":"SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps", + "d":"0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo" + }''') + + private static final String BOB_PRIVATE_JWK_STRING = rfcString(''' + {"kty":"EC", + "crv":"P-256", + "x":"weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "y":"e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", + "d":"VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw" + }''') + + private static final String RFC_HEADER_JSON_STRING = rfcString(''' + {"alg":"ECDH-ES", + "enc":"A128GCM", + "apu":"QWxpY2U", + "apv":"Qm9i", + "epk": + {"kty":"EC", + "crv":"P-256", + "x":"gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", + "y":"SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps" + } + } + ''') + + private static final byte[] RFC_DERIVED_KEY = + [86, 170, 141, 234, 248, 35, 109, 32, 92, 34, 40, 205, 113, 167, 16, 26] as byte[] + + @Test + void test() { + EcPrivateJwk aliceJwk = readJwk(ALICE_EPHEMERAL_JWK_STRING) + EcPrivateJwk bobJwk = readJwk(BOB_PRIVATE_JWK_STRING) + + Map RFC_HEADER = fromJson(RFC_HEADER_JSON_STRING) + + byte[] derivedKey = null + + def alg = new EcdhKeyAlgorithm() { + + //ensure keypair reflects required RFC test value: + @Override + protected KeyPair generateKeyPair(KeyRequest request, ECParameterSpec spec) { + return aliceJwk.toKeyPair() + } + + @Override + KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + KeyResult result = super.getEncryptionKey(request) + // save result derived key so we can compare with the RFC value: + derivedKey = result.getKey().getEncoded() + return result + } + } + + String jwe = Jwts.jweBuilder() + .setHeader(Jwts.jweHeader() + .setAgreementPartyUInfo("Alice") + .setAgreementPartyVInfo("Bob")) + .claim("Hello", "World") + .encryptWith(EncryptionAlgorithms.A128GCM) + .withKeyFrom(bobJwk.toPublicJwk().toKey(), alg) + .compact() + + // Ensure the protected header produced by JJWT is identical to the one in the RFC: + String encodedProtectedHeader = jwe.substring(0, jwe.indexOf('.')) + Map protectedHeader = fromEncoded(encodedProtectedHeader) + assertEquals RFC_HEADER, protectedHeader + + assertNotNull derivedKey + assertArrayEquals RFC_DERIVED_KEY, derivedKey + + // now reverse the process and ensure it all works: + Jwe claimsJwe = Jwts.parserBuilder() + .decryptWith(bobJwk.toKey()) + .build().parseClaimsJwe(jwe) + + assertEquals RFC_HEADER, claimsJwe.getHeader() + assertEquals "World", claimsJwe.getBody().get("Hello") + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy index c9fbb1864..fb48afc65 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy @@ -21,7 +21,7 @@ class KeyAlgorithmsTest { @Test void testValues() { - assertEquals 13, KeyAlgorithms.values().size() + assertEquals 17, KeyAlgorithms.values().size() assertTrue(contains(KeyAlgorithms.DIRECT) && contains(KeyAlgorithms.A128KW) && contains(KeyAlgorithms.A192KW) && @@ -34,7 +34,11 @@ class KeyAlgorithmsTest { contains(KeyAlgorithms.PBES2_HS512_A256KW) && contains(KeyAlgorithms.RSA1_5) && contains(KeyAlgorithms.RSA_OAEP) && - contains(KeyAlgorithms.RSA_OAEP_256) + contains(KeyAlgorithms.RSA_OAEP_256) && + contains(KeyAlgorithms.ECDH_ES) && + contains(KeyAlgorithms.ECDH_ES_A128KW) && + contains(KeyAlgorithms.ECDH_ES_A192KW) && + contains(KeyAlgorithms.ECDH_ES_A256KW) ) } @@ -81,10 +85,8 @@ class KeyAlgorithmsTest { @Test void testEstimateIterations() { // keep it super short so we don't hammer the test server or slow down the build too much: - long desiredMillis = 50; - + long desiredMillis = 50 int result = KeyAlgorithms.estimateIterations(KeyAlgorithms.PBES2_HS256_A128KW, desiredMillis) - assertTrue result > Pbes2HsAkwAlgorithm.MIN_RECOMMENDED_ITERATIONS } } From dc87c42361c3da830b96605f0042211d69f400fd Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 6 Nov 2021 20:41:16 -0700 Subject: [PATCH 12/75] 1. Enabled targeted/limited use of BouncyCastle only when required by eliminating use of RuntimeEnvironment in favor of new Providers implementation. JJWT will no longer modify the system providers list. 2. Changed SecretKeyGenerator.generateKey() to KeyBuilderSupplier.keyBuilder() and a new SecretKeyBuilder interface. This allows users to customize the Provider and SecureRandom used during key generation if desired. 3. Added KeyLengthSupplier to allow certain algorithms the ability to determine a key bit length without forcing a key generation. 4. Ensured Pbes2 algorithms defaulted to OWASP-recommended iteration counts if not specified by the user. --- .../main/java/io/jsonwebtoken/JweHeader.java | 2 +- .../io/jsonwebtoken/JwtParserBuilder.java | 3 +- .../java/io/jsonwebtoken/lang/Assert.java | 12 ++ .../java/io/jsonwebtoken/lang/Builder.java | 17 ++ .../main/java/io/jsonwebtoken/lang/Maps.java | 25 ++- .../jsonwebtoken/lang/RuntimeEnvironment.java | 16 +- .../jsonwebtoken/security/AeadAlgorithm.java | 4 +- .../io/jsonwebtoken/security/JwkBuilder.java | 6 +- .../jsonwebtoken/security/KeyAlgorithms.java | 12 +- .../io/jsonwebtoken/security/KeyBuilder.java | 9 + ...Generator.java => KeyBuilderSupplier.java} | 10 +- .../security/KeyLengthSupplier.java | 14 ++ .../java/io/jsonwebtoken/security/Keys.java | 4 +- .../security/SecretKeyAlgorithm.java | 9 + .../security/SecretKeyBuilder.java | 12 ++ .../security/SecretKeySignatureAlgorithm.java | 2 +- .../security/SecurityBuilder.java | 37 ++++ .../jsonwebtoken/impl/DefaultJweHeader.java | 2 +- .../impl/TokenizedJwtBuilder.java | 9 - .../impl/lang/CheckedSupplier.java | 9 + .../io/jsonwebtoken/impl/lang/Condition.java | 8 + .../io/jsonwebtoken/impl/lang/Conditions.java | 56 ++++++ .../jsonwebtoken/impl/lang/FieldBuilder.java | 10 +- .../impl/security/AesAlgorithm.java | 35 +++- .../impl/security/AesGcmKeyAlgorithm.java | 8 +- .../impl/security/AesWrapKeyAlgorithm.java | 9 +- .../impl/security/CryptoAlgorithm.java | 34 +++- .../impl/security/DefaultRsaKeyAlgorithm.java | 7 +- .../DefaultRsaSignatureAlgorithm.java | 47 ++--- .../security/DefaultSecretKeyBuilder.java | 47 +++++ .../impl/security/EcdhKeyAlgorithm.java | 18 +- .../impl/security/GcmAesAeadAlgorithm.java | 7 - .../impl/security/HmacAesAeadAlgorithm.java | 14 +- .../impl/security/MacSignatureAlgorithm.java | 33 ++-- .../impl/security/Pbes2HsAkwAlgorithm.java | 34 +++- .../jsonwebtoken/impl/security/Providers.java | 70 +++++++ .../impl/security/RandomSecretKeyBuilder.java | 21 +++ .../impl/security/SecretJwkFactory.java | 5 +- .../impl/security/WrappedSecretKey.java | 35 ++++ .../groovy/io/jsonwebtoken/JwtsTest.groovy | 177 ++++++++++++++++-- .../impl/DefaultJweBuilderTest.groovy | 11 +- .../impl/DefaultJwtBuilderTest.groovy | 12 +- .../impl/DefaultJwtParserBuilderTest.groovy | 9 +- .../security/AbstractJwkBuilderTest.groovy | 15 +- .../impl/security/AesAlgorithmTest.groovy | 4 +- .../security/AesGcmKeyAlgorithmTest.groovy | 17 +- .../DefaultRsaSignatureAlgorithmTest.groovy | 2 +- .../security/DirectKeyAlgorithmTest.groovy | 5 +- .../security/FixedSecretKeyBuilder.groovy | 31 +++ .../security/HmacAesAeadAlgorithmTest.groovy | 27 ++- .../impl/security/JwksTest.groovy | 4 +- .../security/MacSignatureAlgorithmTest.groovy | 6 +- ....groovy => PrivateConstructorsTest.groovy} | 4 +- .../security/RFC7516AppendixA3Test.groovy | 16 +- .../impl/security/RFC7517AppendixCTest.groovy | 5 +- .../security/EncryptionAlgorithmsTest.groovy | 4 +- .../io/jsonwebtoken/security/KeysTest.groovy | 5 +- 57 files changed, 868 insertions(+), 198 deletions(-) create mode 100644 api/src/main/java/io/jsonwebtoken/lang/Builder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java rename api/src/main/java/io/jsonwebtoken/security/{SecretKeyGenerator.java => KeyBuilderSupplier.java} (63%) create mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SecretKeyBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwtBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/CheckedSupplier.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/Condition.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/Conditions.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/Providers.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/RandomSecretKeyBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/WrappedSecretKey.java create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/FixedSecretKeyBuilder.groovy rename impl/src/test/groovy/io/jsonwebtoken/impl/security/{BridgeConstructorsTest.groovy => PrivateConstructorsTest.groovy} (73%) diff --git a/api/src/main/java/io/jsonwebtoken/JweHeader.java b/api/src/main/java/io/jsonwebtoken/JweHeader.java index 6e7df09ba..41844de48 100644 --- a/api/src/main/java/io/jsonwebtoken/JweHeader.java +++ b/api/src/main/java/io/jsonwebtoken/JweHeader.java @@ -135,7 +135,7 @@ public interface JweHeader extends Header { Set getCritical(); JweHeader setCritical(Set crit); - int getPbes2Count(); + Integer getPbes2Count(); JweHeader setPbes2Count(int count); byte[] getPbes2Salt(); diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index e41a6436b..a721fb881 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -17,6 +17,7 @@ import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.lang.Builder; import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.SignatureAlgorithm; @@ -38,7 +39,7 @@ * } * @since 0.11.0 */ -public interface JwtParserBuilder { +public interface JwtParserBuilder extends Builder { /** * Enables parsing of Unsecured JWSs (JWTs an 'alg' (Algorithm) header value of diff --git a/api/src/main/java/io/jsonwebtoken/lang/Assert.java b/api/src/main/java/io/jsonwebtoken/lang/Assert.java index 5cfa0ff0a..6664d370b 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Assert.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Assert.java @@ -383,6 +383,18 @@ public static void isAssignable(Class superType, Class subType, String message) } } + /** + * @since JJWT_RELEASE_VERSION + */ + public static Integer gt(Integer value, Integer requirement, String msg) { + notNull(value, "value cannot be null."); + notNull(requirement, "requirement cannot be null."); + if (!(value > requirement)) { + throw new IllegalArgumentException(msg); + } + return value; + } + /** * Assert a boolean expression, throwing IllegalStateException diff --git a/api/src/main/java/io/jsonwebtoken/lang/Builder.java b/api/src/main/java/io/jsonwebtoken/lang/Builder.java new file mode 100644 index 000000000..84dd2acf6 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/lang/Builder.java @@ -0,0 +1,17 @@ +package io.jsonwebtoken.lang; + +/** + * Type-safe interface that reflects the Builder pattern. + * + * @param The type of object that will be created each time {@link #build()} is invoked. + * @since JJWT_RELEASE_VERSION + */ +public interface Builder { + + /** + * Creates and returns a new instance of type {@code T}. + * + * @return a new instance of type {@code T}. + */ + T build(); +} diff --git a/api/src/main/java/io/jsonwebtoken/lang/Maps.java b/api/src/main/java/io/jsonwebtoken/lang/Maps.java index f96e4cb30..6fbca67af 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Maps.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Maps.java @@ -21,11 +21,13 @@ /** * Utility class to help with the manipulation of working with Maps. + * * @since 0.11.0 */ public final class Maps { - private Maps() {} //prevent instantiation + private Maps() { + } //prevent instantiation /** * Creates a new map builder with a single entry. @@ -35,11 +37,12 @@ private Maps() {} //prevent instantiation * // ... * .build(); * } - * @param key the key of an map entry to be added + * + * @param key the key of an map entry to be added * @param value the value of map entry to be added - * @param the maps key type - * @param the maps value type - * Creates a new map builder with a single entry. + * @param the maps key type + * @param the maps value type + * Creates a new map builder with a single entry. */ public static MapBuilder of(K key, V value) { return new HashMapBuilder().and(key, value); @@ -53,21 +56,24 @@ public static MapBuilder of(K key, V value) { * // ... * .build(); * } + * * @param the maps key type * @param the maps value type */ - public interface MapBuilder { + public interface MapBuilder extends Builder> { /** * Add a new entry to this map builder - * @param key the key of an map entry to be added + * + * @param key the key of an map entry to be added * @param value the value of map entry to be added * @return the current MapBuilder to allow for method chaining. */ MapBuilder and(K key, V value); /** - * Returns a the resulting Map object from this MapBuilder. - * @return Returns a the resulting Map object from this MapBuilder. + * Returns the resulting Map object from this MapBuilder. + * + * @return the resulting Map object from this MapBuilder. */ Map build(); } @@ -80,6 +86,7 @@ public MapBuilder and(K key, V value) { data.put(key, value); return this; } + public Map build() { return Collections.unmodifiableMap(data); } diff --git a/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java b/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java index df5b8c7c7..4667dd190 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java +++ b/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java @@ -19,9 +19,16 @@ import java.security.Security; import java.util.concurrent.atomic.AtomicBoolean; +/** + * No longer used by JJWT. Will be removed before the 1.0 final release. + * + * @deprecated will be removed before the 1.0 final release. + */ +@Deprecated public final class RuntimeEnvironment { - private RuntimeEnvironment(){} //prevent instantiation + private RuntimeEnvironment() { + } //prevent instantiation private static final String BC_PROVIDER_CLASS_NAME = "org.bouncycastle.jce.provider.BouncyCastleProvider"; @@ -36,13 +43,13 @@ public static void enableBouncyCastleIfPossible() { } try { - Class clazz = Classes.forName(BC_PROVIDER_CLASS_NAME); + Class clazz = Classes.forName(BC_PROVIDER_CLASS_NAME); //check to see if the user has already registered the BC provider: Provider[] providers = Security.getProviders(); - for(Provider provider : providers) { + for (Provider provider : providers) { if (clazz.isInstance(provider)) { bcLoaded.set(true); return; @@ -50,7 +57,8 @@ public static void enableBouncyCastleIfPossible() { } //bc provider not enabled - add it: - Security.addProvider((Provider)Classes.newInstance(clazz)); + Provider provider = Classes.newInstance(clazz); + Security.addProvider(provider); bcLoaded.set(true); } catch (UnknownClassException e) { diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java index 50aa77034..599c83c70 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java @@ -17,10 +17,12 @@ import io.jsonwebtoken.Identifiable; +import javax.crypto.SecretKey; + /** * @since JJWT_RELEASE_VERSION */ -public interface AeadAlgorithm extends Identifiable, SecretKeyGenerator { +public interface AeadAlgorithm extends Identifiable, KeyBuilderSupplier, KeyLengthSupplier { AeadResult encrypt(AeadRequest request) throws SecurityException; diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java index c7c5162f9..fa0f8b7d2 100644 --- a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java @@ -15,6 +15,8 @@ */ package io.jsonwebtoken.security; +import io.jsonwebtoken.lang.Builder; + import java.security.Key; import java.security.Provider; import java.util.Map; @@ -23,7 +25,7 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface JwkBuilder, T extends JwkBuilder> { +public interface JwkBuilder, T extends JwkBuilder> extends Builder { T put(String name, Object value); @@ -44,6 +46,4 @@ public interface JwkBuilder, T extends JwkBuilde * @return the builder for method chaining. */ T setProvider(Provider provider); - - J build(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index aa995de0a..2c05404b1 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -65,12 +65,12 @@ private static T forId0(String id) { } public static final KeyAlgorithm DIRECT = forId0("dir"); - public static final KeyAlgorithm A128KW = forId0("A128KW"); - public static final KeyAlgorithm A192KW = forId0("A192KW"); - public static final KeyAlgorithm A256KW = forId0("A256KW"); - public static final KeyAlgorithm A128GCMKW = forId0("A128GCMKW"); - public static final KeyAlgorithm A192GCMKW = forId0("A192GCMKW"); - public static final KeyAlgorithm A256GCMKW = forId0("A256GCMKW"); + public static final SecretKeyAlgorithm A128KW = forId0("A128KW"); + public static final SecretKeyAlgorithm A192KW = forId0("A192KW"); + public static final SecretKeyAlgorithm A256KW = forId0("A256KW"); + public static final SecretKeyAlgorithm A128GCMKW = forId0("A128GCMKW"); + public static final SecretKeyAlgorithm A192GCMKW = forId0("A192GCMKW"); + public static final SecretKeyAlgorithm A256GCMKW = forId0("A256GCMKW"); public static final KeyAlgorithm PBES2_HS256_A128KW = forId0("PBES2-HS256+A128KW"); public static final KeyAlgorithm PBES2_HS384_A192KW = forId0("PBES2-HS384+A192KW"); public static final KeyAlgorithm PBES2_HS512_A256KW = forId0("PBES2-HS512+A256KW"); diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java b/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java new file mode 100644 index 000000000..22e23bea7 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.security; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface KeyBuilder> extends SecurityBuilder { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretKeyGenerator.java b/api/src/main/java/io/jsonwebtoken/security/KeyBuilderSupplier.java similarity index 63% rename from api/src/main/java/io/jsonwebtoken/security/SecretKeyGenerator.java rename to api/src/main/java/io/jsonwebtoken/security/KeyBuilderSupplier.java index cfcbb3ef7..59dbc4a82 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecretKeyGenerator.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyBuilderSupplier.java @@ -20,12 +20,14 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface SecretKeyGenerator { +public interface KeyBuilderSupplier> { /** - * Creates and returns a new secure-random key with a length sufficient to be used by the associated Algorithm. + * Returns a new {@link KeyBuilder} instance that will produce new secure-random keys with a length sufficient + * to be used by the associated algorithm. * - * @return a new secure-random key with a length sufficient to be used by the associated Algorithm. + * @return a new {@link KeyBuilder} instance that will produce new secure-random keys with a length sufficient + * to be used by the associated algorithm. */ - SecretKey generateKey(); + B keyBuilder(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java new file mode 100644 index 000000000..1cc0b177f --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java @@ -0,0 +1,14 @@ +package io.jsonwebtoken.security; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface KeyLengthSupplier { + + /** + * Returns the required length in bits (not bytes) of keys usable with the associated algorithm. + * + * @return the required length in bits (not bytes) of keys usable with the associated algorithm. + */ + int getKeyBitLength(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index a8f4e64ac..1998c7bf1 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -116,7 +116,7 @@ public static SecretKey hmacShaKeyFor(byte[] bytes) throws WeakKeyException { * @throws IllegalArgumentException for any input value other than {@link io.jsonwebtoken.SignatureAlgorithm#HS256}, * {@link io.jsonwebtoken.SignatureAlgorithm#HS384}, or {@link io.jsonwebtoken.SignatureAlgorithm#HS512} * @deprecated since JJWT_RELEASE_VERSION. Use your preferred {@link SecretKeySignatureAlgorithm} instance's - * {@link SecretKeySignatureAlgorithm#generateKey() generateKey()} method directly. + * {@link SecretKeySignatureAlgorithm#keyBuilder() keyBuilder()} method directly. */ @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated @@ -127,7 +127,7 @@ public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) thr String msg = "The " + alg.name() + " algorithm does not support shared secret keys."; throw new IllegalArgumentException(msg); } - return ((SecretKeySignatureAlgorithm) salg).generateKey(); + return ((SecretKeySignatureAlgorithm) salg).keyBuilder().build(); } /** diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java new file mode 100644 index 000000000..efad25c9a --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.security; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface SecretKeyAlgorithm extends KeyAlgorithm, KeyBuilderSupplier { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretKeyBuilder.java b/api/src/main/java/io/jsonwebtoken/security/SecretKeyBuilder.java new file mode 100644 index 000000000..549453b5c --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecretKeyBuilder.java @@ -0,0 +1,12 @@ +package io.jsonwebtoken.security; + +import javax.crypto.SecretKey; + +/** + * A {@link KeyBuilder} that creates new secure-random {@link SecretKey}s with a length sufficient to be used by + * the security algorithm that produced this builder. + * + * @since JJWT_RELEASE_VERSION + */ +public interface SecretKeyBuilder extends KeyBuilder { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretKeySignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SecretKeySignatureAlgorithm.java index e14e22af0..c6f3a8956 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecretKeySignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecretKeySignatureAlgorithm.java @@ -20,5 +20,5 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface SecretKeySignatureAlgorithm extends SignatureAlgorithm, SecretKeyGenerator { +public interface SecretKeySignatureAlgorithm extends SignatureAlgorithm, KeyBuilderSupplier, KeyLengthSupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java b/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java new file mode 100644 index 000000000..62ea18596 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java @@ -0,0 +1,37 @@ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.lang.Builder; + +import java.security.Provider; +import java.security.SecureRandom; + + +/** + * A Security-specific {@link Builder} that allows configuration of common JCA API parameters, such as a + * {@link java.security.Provider} or {@link java.security.SecureRandom}. + * + * @param The type of object that will be created each time {@link #build()} is invoked. + * @see #setProvider(Provider) + * @see #setRandom(SecureRandom) + * @since JJWT_RELEASE_VERSION + */ +public interface SecurityBuilder> extends Builder { + + /** + * Sets the JCA Security {@link Provider} to use if necessary when calling {@link #build()}. This is an optional + * property - if not specified, the default JCA Provider will be used. + * + * @param provider the JCA Security Provider instance to use if necessary when building the new instance. + * @return the builder for method chaining. + */ + B setProvider(Provider provider); + + /** + * Sets the {@link SecureRandom} to use if necessary when calling {@link #build()}. This is an optional property + * - if not specified and one is required, a default {@code SecureRandom} will be used. + * + * @param random the {@link SecureRandom} instance to use if necessary when building the new instance. + * @return the builder for method chaining. + */ + B setRandom(SecureRandom random); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java index 150a7da35..23953ab0a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java @@ -44,7 +44,7 @@ public String getEncryptionAlgorithm() { // } @Override - public int getPbes2Count() { + public Integer getPbes2Count() { return idiomaticGet(P2C); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwtBuilder.java deleted file mode 100644 index cd4ad5edd..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwtBuilder.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jsonwebtoken.impl; - -public interface TokenizedJwtBuilder { - - TokenizedJwtBuilder append(String token); - - T build(); - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/CheckedSupplier.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/CheckedSupplier.java new file mode 100644 index 000000000..87b8431d3 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/CheckedSupplier.java @@ -0,0 +1,9 @@ +package io.jsonwebtoken.impl.lang; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface CheckedSupplier { + + T get() throws Exception; +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Condition.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Condition.java new file mode 100644 index 000000000..ecd0981a2 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Condition.java @@ -0,0 +1,8 @@ +package io.jsonwebtoken.impl.lang; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface Condition { + boolean test(); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Conditions.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Conditions.java new file mode 100644 index 000000000..924dc4dd7 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Conditions.java @@ -0,0 +1,56 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; + +/** + * @since JJWT_RELEASE_VERSION + */ +public final class Conditions { + + private Conditions() { + } + + public static Condition not(Condition c) { + return new NotCondition(c); + } + + public static Condition exists(CheckedSupplier s) { + return new ExistsCondition(s); + } + + public static Condition notExists(CheckedSupplier s) { + return not(exists(s)); + } + + private static final class NotCondition implements Condition { + + private final Condition c; + + private NotCondition(Condition c) { + this.c = Assert.notNull(c, "Condition cannot be null."); + } + + @Override + public boolean test() { + return !c.test(); + } + } + + private static final class ExistsCondition implements Condition { + private final CheckedSupplier supplier; + + ExistsCondition(CheckedSupplier supplier) { + this.supplier = Assert.notNull(supplier, "CheckedSupplier cannot be null."); + } + + @Override + public boolean test() { + Object value = null; + try { + value = supplier.get(); + } catch (Exception ignored) { + } + return value != null; + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldBuilder.java index 2489926fe..5888a254a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldBuilder.java @@ -1,9 +1,14 @@ package io.jsonwebtoken.impl.lang; +import io.jsonwebtoken.lang.Builder; + import java.util.List; import java.util.Set; -public interface FieldBuilder { +/** + * @since JJWT_RELEASE_VERSION + */ +public interface FieldBuilder extends Builder> { FieldBuilder setId(String id); @@ -18,7 +23,4 @@ public interface FieldBuilder { FieldBuilder> set(); FieldBuilder setConverter(Converter converter); - - Field build(); - } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java index 4dfbc5a5c..de5c038ac 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java @@ -1,22 +1,30 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedSupplier; +import io.jsonwebtoken.impl.lang.Conditions; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.AssociatedDataSupplier; import io.jsonwebtoken.security.InitializationVectorSupplier; +import io.jsonwebtoken.security.KeyBuilderSupplier; +import io.jsonwebtoken.security.KeyLengthSupplier; import io.jsonwebtoken.security.KeySupplier; -import io.jsonwebtoken.security.SecretKeyGenerator; +import io.jsonwebtoken.security.SecretKeyBuilder; import io.jsonwebtoken.security.SecurityRequest; import io.jsonwebtoken.security.WeakKeyException; +import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.IvParameterSpec; import java.security.SecureRandom; import java.security.spec.AlgorithmParameterSpec; -abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerator { +/** + * @since JJWT_RELEASE_VERSION + */ +abstract class AesAlgorithm extends CryptoAlgorithm implements KeyBuilderSupplier, KeyLengthSupplier { protected static final String KEY_ALG_NAME = "AES"; protected static final int BLOCK_SIZE = 128; @@ -32,7 +40,7 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato protected final int tagBitLength; protected final boolean gcm; - AesAlgorithm(String id, String jcaTransformation, int keyBitLength) { + AesAlgorithm(String id, final String jcaTransformation, int keyBitLength) { super(id, jcaTransformation); Assert.isTrue(keyBitLength == 128 || keyBitLength == 192 || keyBitLength == 256, "Invalid AES key length: it must equal 128, 192, or 256."); this.keyBitLength = keyBitLength; @@ -40,12 +48,27 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato this.ivBitLength = jcaTransformation.equals("AESWrap") ? 0 : (this.gcm ? GCM_IV_SIZE : BLOCK_SIZE); // https://tools.ietf.org/html/rfc7518#section-5.2.3 through https://tools.ietf.org/html/rfc7518#section-5.3 : this.tagBitLength = this.gcm ? BLOCK_SIZE : this.keyBitLength; + + // GCM mode only available on JDK 8 and later, so enable BC as a backup provider if necessary for <= JDK 7: + // TODO: remove when dropping JDK 7: + if (this.gcm) { + setProvider(Providers.getBouncyCastle(Conditions.notExists(new CheckedSupplier() { + @Override + public Cipher get() throws Exception { + return Cipher.getInstance(jcaTransformation); + } + }))); + } + } + + @Override + public int getKeyBitLength() { + return this.keyBitLength; } @Override - public SecretKey generateKey() { - return new JcaTemplate(KEY_ALG_NAME, null).generateSecretKey(this.keyBitLength); - //TODO: assert generated key length? + public SecretKeyBuilder keyBuilder() { + return new DefaultSecretKeyBuilder(KEY_ALG_NAME, getKeyBitLength()); } protected SecretKey assertKey(KeySupplier request) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java index 74c0afecc..f4faec84f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java @@ -6,11 +6,10 @@ import io.jsonwebtoken.impl.lang.ValueGetter; import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.DecryptionKeyRequest; -import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.SecretKeyAlgorithm; import io.jsonwebtoken.security.SecurityException; import javax.crypto.Cipher; @@ -21,7 +20,7 @@ /** * @since JJWT_RELEASE_VERSION */ -public class AesGcmKeyAlgorithm extends AesAlgorithm implements KeyAlgorithm { +public class AesGcmKeyAlgorithm extends AesAlgorithm implements SecretKeyAlgorithm { public static final String TRANSFORMATION = "AES/GCM/NoPadding"; @@ -34,8 +33,7 @@ public KeyResult getEncryptionKey(final KeyRequest request) throws Se Assert.notNull(request, "request cannot be null."); final SecretKey kek = assertKey(request); - AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); - final SecretKey cek = Assert.notNull(enc.generateKey(), "Request encryption algorithm cannot generate a null key."); + final SecretKey cek = generateKey(request); final byte[] iv = ensureInitializationVector(request); final AlgorithmParameterSpec ivSpec = getIvSpec(iv); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java index a70dfd1d5..87e7f83ee 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java @@ -2,11 +2,10 @@ import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.DecryptionKeyRequest; -import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.SecretKeyAlgorithm; import io.jsonwebtoken.security.SecurityException; import javax.crypto.Cipher; @@ -16,7 +15,7 @@ /** * @since JJWT_RELEASE_VERSION */ -public class AesWrapKeyAlgorithm extends AesAlgorithm implements KeyAlgorithm { +public class AesWrapKeyAlgorithm extends AesAlgorithm implements SecretKeyAlgorithm { private static final String TRANSFORMATION = "AESWrap"; @@ -28,9 +27,7 @@ public AesWrapKeyAlgorithm(int keyLen) { public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { Assert.notNull(request, "request cannot be null."); final SecretKey kek = assertKey(request); - AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); - final SecretKey cek = enc.generateKey(); - Assert.notNull(cek, "Request encryption algorithm cannot generate a null key."); + final SecretKey cek = generateKey(request); byte[] ciphertext = execute(request, Cipher.class, new CheckedFunction() { @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java index 50ffe6731..ccd0b100e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java @@ -3,17 +3,26 @@ import io.jsonwebtoken.Identifiable; import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.SecretKeyBuilder; import io.jsonwebtoken.security.SecurityRequest; +import javax.crypto.SecretKey; import java.security.Provider; import java.security.SecureRandom; +/** + * @since JJWT_RELEASE_VERSION + */ abstract class CryptoAlgorithm implements Identifiable { private final String ID; private final String jcaName; + private Provider provider; // default, if any + CryptoAlgorithm(String id, String jcaName) { Assert.hasText(id, "id cannot be null or empty."); this.ID = id; @@ -30,30 +39,49 @@ String getJcaName() { return this.jcaName; } + protected void setProvider(Provider provider) { // can be null + this.provider = provider; + } + SecureRandom ensureSecureRandom(SecurityRequest request) { SecureRandom random = request != null ? request.getSecureRandom() : null; return random != null ? random : Randoms.secureRandom(); } protected R execute(Class clazz, CheckedFunction fn) { - return new JcaTemplate(getJcaName(), null).execute(clazz, fn); + return new JcaTemplate(getJcaName(), this.provider).execute(clazz, fn); + } + + protected Provider getProvider(SecurityRequest request) { + Provider provider = request.getProvider(); + if (provider == null) { + provider = this.provider; // fallback, if any + } + return provider; } protected T execute(SecurityRequest request, Class clazz, CheckedFunction fn) { Assert.notNull(request, "request cannot be null."); - Provider provider = request.getProvider(); + Provider provider = getProvider(request); SecureRandom random = ensureSecureRandom(request); JcaTemplate template = new JcaTemplate(getJcaName(), provider, random); return template.execute(clazz, fn); } + public SecretKey generateKey(KeyRequest request) { + AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); + SecretKeyBuilder builder = Assert.notNull(enc.keyBuilder(), "Request encryptionAlgorithm cannot produce a null SecretKeyBuilder"); + SecretKey key = builder.setProvider(getProvider(request)).setRandom(request.getSecureRandom()).build(); + return Assert.notNull(key, "Request encryptionAlgorithm SecretKeyBuilder cannot produce null keys."); + } + @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof CryptoAlgorithm) { - CryptoAlgorithm other = (CryptoAlgorithm)obj; + CryptoAlgorithm other = (CryptoAlgorithm) obj; return this.ID.equals(other.getId()) && this.jcaName.equals(other.getJcaName()); } return false; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java index 782954e00..efc262cf7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java @@ -2,7 +2,6 @@ import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.DecryptionKeyRequest; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; @@ -17,6 +16,9 @@ import java.security.interfaces.RSAKey; import java.security.spec.AlgorithmParameterSpec; +/** + * @since JJWT_RELEASE_VERSION + */ public class DefaultRsaKeyAlgorithm extends CryptoAlgorithm implements RsaKeyAlgorithm { @@ -35,8 +37,7 @@ public DefaultRsaKeyAlgorithm(String id, String jcaTransformationString, Algorit public KeyResult getEncryptionKey(final KeyRequest request) throws SecurityException { Assert.notNull(request, "Request cannot be null."); final E kek = Assert.notNull(request.getKey(), "Request key encryption key cannot be null."); - AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); - final SecretKey cek = Assert.notNull(enc.generateKey(), "Request encryption algorithm cannot generate a null key."); + final SecretKey cek = generateKey(request); byte[] ciphertext = execute(request, Cipher.class, new CheckedFunction() { @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java index b061e4ca7..a61a39488 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java @@ -1,7 +1,8 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.CheckedFunction; -import io.jsonwebtoken.lang.RuntimeEnvironment; +import io.jsonwebtoken.impl.lang.CheckedSupplier; +import io.jsonwebtoken.impl.lang.Conditions; import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.RsaSignatureAlgorithm; import io.jsonwebtoken.security.SignatureRequest; @@ -18,13 +19,14 @@ import java.security.spec.MGF1ParameterSpec; import java.security.spec.PSSParameterSpec; -public class DefaultRsaSignatureAlgorithm extends AbstractSignatureAlgorithm implements RsaSignatureAlgorithm { +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultRsaSignatureAlgorithm + extends AbstractSignatureAlgorithm implements RsaSignatureAlgorithm { - static { - RuntimeEnvironment.enableBouncyCastleIfPossible(); //PS256, PS384, PS512 on <= JDK 10 require BC - } - - private static final int MIN_KEY_LENGTH_BITS = 2048; + private static final String PSS_JCA_NAME = "RSASSA-PSS"; + private static final int MIN_KEY_BIT_LENGTH = 2048; private static AlgorithmParameterSpec pssParamFromSaltBitLength(int saltBitLength) { MGF1ParameterSpec ps = new MGF1ParameterSpec("SHA-" + saltBitLength); @@ -36,30 +38,29 @@ private static AlgorithmParameterSpec pssParamFromSaltBitLength(int saltBitLengt private final AlgorithmParameterSpec algorithmParameterSpec; - public DefaultRsaSignatureAlgorithm(String name, String jcaName, int preferredKeyLengthBits, AlgorithmParameterSpec algParam) { + public DefaultRsaSignatureAlgorithm(String name, String jcaName, int preferredKeyBitLength, AlgorithmParameterSpec algParam) { super(name, jcaName); - if (preferredKeyLengthBits < MIN_KEY_LENGTH_BITS) { - String msg = "preferredKeyLengthBits must be greater than the JWA mandatory minimum key length of " + MIN_KEY_LENGTH_BITS; + if (preferredKeyBitLength < MIN_KEY_BIT_LENGTH) { + String msg = "preferredKeyLengthBits must be greater than the JWA mandatory minimum key length of " + MIN_KEY_BIT_LENGTH; throw new IllegalArgumentException(msg); } - this.preferredKeyLength = preferredKeyLengthBits; + this.preferredKeyLength = preferredKeyBitLength; this.algorithmParameterSpec = algParam; } public DefaultRsaSignatureAlgorithm(int digestBitLength, int preferredKeyBitLength) { - this("RS" + digestBitLength, "SHA" + digestBitLength + "withRSA", preferredKeyBitLength); + this("RS" + digestBitLength, "SHA" + digestBitLength + "withRSA", preferredKeyBitLength, null); } public DefaultRsaSignatureAlgorithm(int digestBitLength, int preferredKeyBitLength, int pssSaltBitLength) { - this("PS" + digestBitLength, "RSASSA-PSS", preferredKeyBitLength, pssSaltBitLength); - } - - public DefaultRsaSignatureAlgorithm(String name, String jcaName, int preferredKeyLengthBits) { - this(name, jcaName, preferredKeyLengthBits, null); - } - - public DefaultRsaSignatureAlgorithm(String name, String jcaName, int preferredKeyLengthBits, int pssSaltLengthBits) { - this(name, jcaName, preferredKeyLengthBits, pssParamFromSaltBitLength(pssSaltLengthBits)); + this("PS" + digestBitLength, PSS_JCA_NAME, preferredKeyBitLength, pssParamFromSaltBitLength(pssSaltBitLength)); + // PSS is not available natively until JDK 11, so try to load BC as a backup provider if possible on <= JDK 10: + setProvider(Providers.getBouncyCastle(Conditions.notExists(new CheckedSupplier() { + @Override + public Signature get() throws Exception { + return Signature.getInstance(PSS_JCA_NAME); + } + }))); } @Override @@ -86,7 +87,7 @@ protected void validateKey(Key key, boolean signing) { RSAKey rsaKey = (RSAKey) key; int size = rsaKey.getModulus().bitLength(); - if (size < MIN_KEY_LENGTH_BITS) { + if (size < MIN_KEY_BIT_LENGTH) { String id = getId(); @@ -95,7 +96,7 @@ protected void validateKey(Key key, boolean signing) { String msg = "The " + keyType(signing) + " key's size is " + size + " bits which is not secure " + "enough for the " + id + " algorithm. The JWT JWA Specification (RFC 7518, Section " + section + ") states that RSA keys MUST have a size >= " + - MIN_KEY_LENGTH_BITS + " bits. Consider using the SignatureAlgorithms." + id + ".generateKeyPair() " + + MIN_KEY_BIT_LENGTH + " bits. Consider using the SignatureAlgorithms." + id + ".generateKeyPair() " + "method to create a key pair guaranteed to be secure enough for " + id + ". See " + "https://tools.ietf.org/html/rfc7518#section-" + section + " for more information."; throw new WeakKeyException(msg); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java new file mode 100644 index 000000000..f90cdcd8d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java @@ -0,0 +1,47 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.SecretKeyBuilder; + +import javax.crypto.SecretKey; +import java.security.Provider; +import java.security.SecureRandom; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultSecretKeyBuilder implements SecretKeyBuilder { + + protected final String JCA_NAME; + protected final int BIT_LENGTH; + protected Provider provider; + protected SecureRandom random; + + public DefaultSecretKeyBuilder(String jcaName, int bitLength) { + this.JCA_NAME = Assert.hasText(jcaName, "jcaName cannot be null or empty."); + if (bitLength % Byte.SIZE != 0) { + String msg = "bitLength must be a multiple of 8"; + throw new IllegalArgumentException(msg); + } + this.BIT_LENGTH = Assert.gt(bitLength, 0, "bitLength must be > 0"); + setRandom(Randoms.secureRandom()); + } + + @Override + public SecretKeyBuilder setProvider(Provider provider) { + this.provider = provider; + return this; + } + + @Override + public SecretKeyBuilder setRandom(SecureRandom random) { + this.random = random != null ? random : Randoms.secureRandom(); + return this; + } + + @Override + public SecretKey build() { + JcaTemplate template = new JcaTemplate(JCA_NAME, this.provider, this.random); + return template.generateSecretKey(this.BIT_LENGTH); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java index 0a61a1da2..92e596d07 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java @@ -15,6 +15,7 @@ import io.jsonwebtoken.security.Jwk; import io.jsonwebtoken.security.Jwks; import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.KeyLengthSupplier; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; import io.jsonwebtoken.security.SecurityException; @@ -32,7 +33,11 @@ import java.security.spec.ECParameterSpec; import java.util.Map; -class EcdhKeyAlgorithm extends CryptoAlgorithm implements EcKeyAlgorithm { +/** + * @since JJWT_RELEASE_VERSION + */ +class EcdhKeyAlgorithm extends CryptoAlgorithm + implements EcKeyAlgorithm { protected static final String JCA_NAME = "ECDH"; protected static final String DEFAULT_ID = JCA_NAME + "-ES"; @@ -113,9 +118,9 @@ private byte[] createOtherInfo(int keydatalen, String AlgorithmID, byte[] PartyU } private int getKeyBitLength(AeadAlgorithm enc) { - SecretKey forBitLen = Assert.notNull(enc.generateKey(), "EncryptionAlgorithm generated key cannot be null."); - byte[] toCount = Assert.notEmpty(forBitLen.getEncoded(), "EncryptionAlgorithm generated key encoded bytes cannot be null or empty."); - return (int)Bytes.bitLength(toCount); // MUST be an integer per the RFC + int bitLength = this.WRAP_ALG instanceof KeyLengthSupplier ? + ((KeyLengthSupplier)this.WRAP_ALG).getKeyBitLength() : enc.getKeyBitLength(); + return Assert.gt(bitLength, 0, "Algorithm keyBitLength must be > 0"); } @Override @@ -158,10 +163,9 @@ public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws Securi Assert.notNull(request, "Request cannot be null."); JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); D privateKey = Assert.notNull(request.getKey(), "Request key cannot be null."); - ECParameterSpec spec = Assert.notNull(privateKey.getParams(), "Request key params cannot be null."); ValueGetter getter = new DefaultValueGetter(header); - Map epkValues = getter.getRequiredMap(EPHEMERAL_PUBLIC_KEY); + Map epkValues = getter.getRequiredMap(EPHEMERAL_PUBLIC_KEY); // This call will assert the EPK, if valid, is also on a NIST curve: Jwk jwk = Jwks.builder().putAll(epkValues).build(); if (!(jwk instanceof EcPublicJwk)) { @@ -169,7 +173,7 @@ public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws Securi "EllipticCurve Public JWK as required."; throw new MalformedJwtException(msg); } - EcPublicJwk epk = (EcPublicJwk)jwk; + EcPublicJwk epk = (EcPublicJwk) jwk; // Now, while the EPK might be on a NIST curve, we need to ensure it's on the exact curve associted with the // private key: if (!EcPublicJwkFactory.contains(privateKey.getParams().getCurve(), epk.toKey().getW())) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java index ae571e7ad..380014be4 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java @@ -4,7 +4,6 @@ import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.RuntimeEnvironment; import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.AeadRequest; import io.jsonwebtoken.security.AeadResult; @@ -20,12 +19,6 @@ */ public class GcmAesAeadAlgorithm extends AesAlgorithm implements AeadAlgorithm { - //TODO: Remove this static block when JDK 7 support is removed - // JDK <= 7 does not support AES GCM mode natively and so BouncyCastle is required - static { - RuntimeEnvironment.enableBouncyCastleIfPossible(); - } - private static final String TRANSFORMATION_STRING = "AES/GCM/NoPadding"; public GcmAesAeadAlgorithm(int keyLength) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java index 6f14555cb..7f0046ccd 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java @@ -9,6 +9,7 @@ import io.jsonwebtoken.security.CryptoRequest; import io.jsonwebtoken.security.DecryptAeadRequest; import io.jsonwebtoken.security.PayloadSupplier; +import io.jsonwebtoken.security.SecretKeyBuilder; import io.jsonwebtoken.security.SignatureException; import io.jsonwebtoken.security.SignatureRequest; @@ -37,7 +38,7 @@ private static String id(int keyLength) { } public HmacAesAeadAlgorithm(String id, MacSignatureAlgorithm sigAlg) { - super(id, TRANSFORMATION_STRING, sigAlg.getMinKeyLength()); + super(id, TRANSFORMATION_STRING, sigAlg.getKeyBitLength()); this.SIGALG = sigAlg; } @@ -46,8 +47,15 @@ public HmacAesAeadAlgorithm(int keyBitLength) { } @Override - public SecretKey generateKey() { - return new JcaTemplate("AES", null).generateSecretKey(this.keyBitLength * 2); + public int getKeyBitLength() { + return super.getKeyBitLength() * 2; + } + + @Override + public SecretKeyBuilder keyBuilder() { + // The Sun JCE KeyGenerator throws an exception if bitLengths are not standard AES 128, 192 or 256 values. + // Since the JWA HmacAes algorithms require double that, we use secure-random keys instead: + return new RandomSecretKeyBuilder(KEY_ALG_NAME, getKeyBitLength()); } byte[] assertKeyBytes(CryptoRequest request) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java index abcd949f3..3ab2219bc 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java @@ -1,11 +1,12 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.CheckedFunction; -import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.SecretKeyBuilder; import io.jsonwebtoken.security.SecretKeySignatureAlgorithm; import io.jsonwebtoken.security.SignatureRequest; import io.jsonwebtoken.security.WeakKeyException; @@ -17,9 +18,12 @@ import java.util.Locale; import java.util.Set; +/** + * @since JJWT_RELEASE_VERSION + */ public class MacSignatureAlgorithm extends AbstractSignatureAlgorithm implements SecretKeySignatureAlgorithm { - private final int minKeyLength; //in bits + private final int minKeyBitLength; //in bits private static final Set JWA_STANDARD_IDS = new LinkedHashSet<>(Collections.of("HS256", "HS384", "HS512")); @@ -42,14 +46,15 @@ public MacSignatureAlgorithm(int digestBitLength) { this("HS" + digestBitLength, "HmacSHA" + digestBitLength, digestBitLength); } - public MacSignatureAlgorithm(String id, String jcaName, int minKeyLength) { + public MacSignatureAlgorithm(String id, String jcaName, int minKeyBitLength) { super(id, jcaName); - Assert.isTrue(minKeyLength > 0, "minKeyLength must be greater than zero."); - this.minKeyLength = minKeyLength; + Assert.isTrue(minKeyBitLength > 0, "minKeyLength must be greater than zero."); + this.minKeyBitLength = minKeyBitLength; } - int getMinKeyLength() { - return this.minKeyLength; + @Override + public int getKeyBitLength() { + return this.minKeyBitLength; } private boolean isJwaStandard() { @@ -61,8 +66,8 @@ private boolean isJwaStandardJcaName(String jcaName) { } @Override - public SecretKey generateKey() { - return new JcaTemplate(getJcaName(), null).generateSecretKey(minKeyLength); + public SecretKeyBuilder keyBuilder() { + return new DefaultSecretKeyBuilder(getJcaName(), getKeyBitLength()); } @Override @@ -113,20 +118,20 @@ protected void validateKey(Key k, boolean signing) { // so return early if we can't: if (encoded == null) return; - int size = Arrays.length(encoded) * Byte.SIZE; - if (size < this.minKeyLength) { + int size = (int)Bytes.bitLength(encoded); + if (size < this.minKeyBitLength) { String msg = "The " + keyType + " key's size is " + size + " bits which " + "is not secure enough for the " + id + " algorithm."; if (isJwaStandard() && isJwaStandardJcaName(getJcaName())) { //JWA standard algorithm name - reference the spec: msg += " The JWT " + "JWA Specification (RFC 7518, Section 3.2) states that keys used with " + id + " MUST have a " + - "size >= " + minKeyLength + " bits (the key size must be greater than or equal to the hash " + + "size >= " + minKeyBitLength + " bits (the key size must be greater than or equal to the hash " + "output size). Consider using the SignatureAlgorithms." + id + ".generateKey() " + "method to create a key guaranteed to be secure enough for " + id + ". See " + "https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; } else { //custom algorithm - just indicate required key length: - msg += " The " + id + " algorithm requires keys to have a size >= " + minKeyLength + " bits."; + msg += " The " + id + " algorithm requires keys to have a size >= " + minKeyBitLength + " bits."; } throw new WeakKeyException(msg); @@ -134,7 +139,7 @@ protected void validateKey(Key k, boolean signing) { } @Override - public byte[] doSign(final SignatureRequest request) throws Exception { + public byte[] doSign(final SignatureRequest request) { return execute(request, Mac.class, new CheckedFunction() { @Override public byte[] apply(Mac mac) throws Exception { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java index e954b0a42..497106937 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java @@ -18,17 +18,26 @@ import javax.crypto.spec.PBEKeySpec; import java.nio.charset.StandardCharsets; +/** + * @since JJWT_RELEASE_VERSION + */ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm { + // See https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 : + private static final int DEFAULT_SHA256_ITERATIONS = 310000; + private static final int DEFAULT_SHA384_ITERATIONS = 210000; + private static final int DEFAULT_SHA512_ITERATIONS = 120000; + private static final int MIN_RECOMMENDED_ITERATIONS = 1000; // https://datatracker.ietf.org/doc/html/rfc7518#section-4.8.1.2 private static final String MIN_ITERATIONS_MSG_PREFIX = "[JWA RFC 7518, Section 4.8.1.2](https://datatracker.ietf.org/doc/html/rfc7518#section-4.8.1.2) " + - "recommends password-based-encryption iterations be greater than or equal to " + - MIN_RECOMMENDED_ITERATIONS + ". Provided: "; + "recommends password-based-encryption iterations be greater than or equal to " + + MIN_RECOMMENDED_ITERATIONS + ". Provided: "; private final int HASH_BYTE_LENGTH; private final int DERIVED_KEY_BIT_LENGTH; private final byte[] SALT_PREFIX; + private final int DEFAULT_ITERATIONS; private final KeyAlgorithm wrapAlg; private static byte[] toRfcSaltPrefix(byte[] bytes) { @@ -71,6 +80,16 @@ private Pbes2HsAkwAlgorithm(int hashBitLength, KeyAlgorithm= 512) { + DEFAULT_ITERATIONS = DEFAULT_SHA512_ITERATIONS; + } else if (hashBitLength >= 384) { + DEFAULT_ITERATIONS = DEFAULT_SHA384_ITERATIONS; + } else { + DEFAULT_ITERATIONS = DEFAULT_SHA256_ITERATIONS; + } + // https://datatracker.ietf.org/doc/html/rfc7518#section-4.8, 2nd paragraph, last sentence: // "Their derived-key lengths respectively are 16, 24, and 32 octets." : this.DERIVED_KEY_BIT_LENGTH = hashBitLength / 2; // results in 128, 192, or 256 @@ -82,7 +101,8 @@ private Pbes2HsAkwAlgorithm(int hashBitLength, KeyAlgorithm request) throws Securi Assert.notNull(request, "request cannot be null."); final PasswordKey key = Assert.notNull(request.getKey(), "request.getKey() cannot be null."); - - final int iterations = assertIterations(request.getHeader().getPbes2Count()); + Integer p2c = request.getHeader().getPbes2Count(); + if (p2c == null) { + p2c = DEFAULT_ITERATIONS; + request.getHeader().setPbes2Count(p2c); + } + final int iterations = assertIterations(p2c); byte[] inputSalt = generateInputSalt(request); final byte[] rfcSalt = toRfcSalt(inputSalt); char[] password = key.getPassword(); // password will be safely cleaned/zeroed in deriveKey next: diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Providers.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Providers.java new file mode 100644 index 000000000..90c1bda1f --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Providers.java @@ -0,0 +1,70 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.Condition; +import io.jsonwebtoken.lang.Classes; + +import java.security.Provider; +import java.security.Security; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @since JJWT_RELEASE_VERSION + */ +final class Providers { + + private static final String BC_PROVIDER_CLASS_NAME = "org.bouncycastle.jce.provider.BouncyCastleProvider"; + static final boolean BOUNCY_CASTLE_AVAILABLE = Classes.isAvailable(BC_PROVIDER_CLASS_NAME); + private static final AtomicReference BC_PROVIDER = new AtomicReference<>(); + + private Providers() { + } + + private static Provider getBouncyCastleProviderIfPossible() { + if (!BOUNCY_CASTLE_AVAILABLE) { + return null; + } + Provider provider = BC_PROVIDER.get(); + if (provider == null) { + + Class clazz = Classes.forName(BC_PROVIDER_CLASS_NAME); + + //check to see if the user has already registered the BC provider: + Provider[] providers = Security.getProviders(); + for (Provider aProvider : providers) { + if (clazz.isInstance(aProvider)) { + BC_PROVIDER.set(aProvider); + return aProvider; + } + } + + //user hasn't created the BC provider, so we'll create one just for JJWT's needs: + provider = Classes.newInstance(clazz); + BC_PROVIDER.set(provider); + } + return provider; + } + + /** + * Returns the BouncyCastle provider if and only if the specified Condition evaluates to {@code true} + * and BouncyCastle is available. Returns {@code null} otherwise. + *

    + * If the condition evaluates to true and the JVM runtime already has BouncyCastle registered + * (e.g. {@code Security.addProvider(bcProvider)}, that Provider instance will be found and returned. + * If an existing BC provider is not found, a new BC instance will be created, cached for future reference, + * and returned. + *

    + * If a new BC provider is created and returned, it is not registered in the JVM via + * {@code Security.addProvider} to ensure JJWT doesn't interfere with the application security provider + * configuration and/or expectations. + * + * @param c condition to evaluate + * @return any available BouncyCastle Provider if {@code c} evaluates to true, or {@code null} if either + * {@code c} evaluates to false, or BouncyCastle is not available. + */ + public static Provider getBouncyCastle(Condition c) { + if (c.test()) { + return getBouncyCastleProviderIfPossible(); + } + return null; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RandomSecretKeyBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RandomSecretKeyBuilder.java new file mode 100644 index 000000000..810463bfa --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RandomSecretKeyBuilder.java @@ -0,0 +1,21 @@ +package io.jsonwebtoken.impl.security; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class RandomSecretKeyBuilder extends DefaultSecretKeyBuilder { + + public RandomSecretKeyBuilder(String jcaName, int bitLength) { + super(jcaName, bitLength); + } + + @Override + public SecretKey build() { + byte[] bytes = new byte[this.BIT_LENGTH / Byte.SIZE]; + this.random.nextBytes(bytes); + return new SecretKeySpec(bytes, this.JCA_NAME); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java index fd30998f6..b7149730e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java @@ -10,6 +10,9 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +/** + * @since JJWT_RELEASE_VERSION + */ class SecretJwkFactory extends AbstractFamilyJwkFactory { SecretJwkFactory() { @@ -59,7 +62,7 @@ protected SecretJwk createJwkFromKey(JwkContext ctx) { protected SecretJwk createJwkFromValues(JwkContext ctx) { ValueGetter getter = new DefaultValueGetter(ctx); byte[] bytes = getter.getRequiredBytes(DefaultSecretJwk.K.getId()); - SecretKey key = new SecretKeySpec(bytes, "NONE"); //TODO: do we need a JCA-specific ID here? + SecretKey key = new SecretKeySpec(bytes, "AES"); ctx.setKey(key); return new DefaultSecretJwk(ctx); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/WrappedSecretKey.java b/impl/src/main/java/io/jsonwebtoken/impl/security/WrappedSecretKey.java new file mode 100644 index 000000000..a479f42c9 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/WrappedSecretKey.java @@ -0,0 +1,35 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class WrappedSecretKey implements SecretKey { + + private final String algorithm; + private final SecretKey key; + + public WrappedSecretKey(SecretKey key, String algorithm) { + this.key = Assert.notNull(key, "SecretKey cannot be null."); + this.algorithm = Strings.hasText(algorithm) ? algorithm : key.getAlgorithm(); + } + + @Override + public String getAlgorithm() { + return this.algorithm; + } + + @Override + public String getFormat() { + return this.key.getFormat(); + } + + @Override + public byte[] getEncoded() { + return this.key.getEncoded(); + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 9220cce21..816591320 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -23,11 +23,22 @@ import io.jsonwebtoken.impl.JwtTokenizer import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec import io.jsonwebtoken.impl.lang.Services +import io.jsonwebtoken.impl.security.DirectKeyAlgorithm +import io.jsonwebtoken.impl.security.Pbes2HsAkwAlgorithm import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.lang.Strings +import io.jsonwebtoken.security.AeadAlgorithm import io.jsonwebtoken.security.AsymmetricKeySignatureAlgorithm +import io.jsonwebtoken.security.EcKeyAlgorithm +import io.jsonwebtoken.security.EncryptionAlgorithms +import io.jsonwebtoken.security.KeyAlgorithm +import io.jsonwebtoken.security.KeyAlgorithms +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.PasswordKey +import io.jsonwebtoken.security.RsaKeyAlgorithm +import io.jsonwebtoken.security.SecretKeyAlgorithm import io.jsonwebtoken.security.SecretKeySignatureAlgorithm import io.jsonwebtoken.security.SignatureAlgorithm import io.jsonwebtoken.security.SignatureAlgorithms @@ -208,7 +219,7 @@ class JwtsTest { @Test void testParseWithMissingRequiredSignature() { - Key key = SignatureAlgorithms.HS256.generateKey() + Key key = SignatureAlgorithms.HS256.keyBuilder().build() String compact = Jwts.builder().setSubject('foo').signWith(key).compact() int i = compact.lastIndexOf('.') String missingSig = compact.substring(0, i + 1) @@ -368,7 +379,7 @@ class JwtsTest { void testUncompressedJwt() { SignatureAlgorithm alg = SignatureAlgorithms.HS256 - SecretKey key = alg.generateKey() + SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() @@ -390,7 +401,7 @@ class JwtsTest { void testCompressedJwtWithDeflate() { SignatureAlgorithm alg = SignatureAlgorithms.HS256 - SecretKey key = alg.generateKey() + SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() @@ -412,7 +423,7 @@ class JwtsTest { void testCompressedJwtWithGZIP() { SignatureAlgorithm alg = SignatureAlgorithms.HS256 - SecretKey key = alg.generateKey() + SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() @@ -434,7 +445,7 @@ class JwtsTest { void testCompressedWithCustomResolver() { SignatureAlgorithm alg = SignatureAlgorithms.HS256 - SecretKey key = alg.generateKey() + SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() @@ -473,7 +484,7 @@ class JwtsTest { void testCompressedJwtWithUnrecognizedHeader() { SignatureAlgorithm alg = SignatureAlgorithms.HS256 - SecretKey key = alg.generateKey() + SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() @@ -492,7 +503,7 @@ class JwtsTest { void testCompressStringPayloadWithDeflate() { SignatureAlgorithm alg = SignatureAlgorithms.HS256 - SecretKey key = alg.generateKey() + SecretKey key = alg.keyBuilder().build() String payload = "this is my test for a payload" @@ -597,8 +608,8 @@ class JwtsTest { void testParseClaimsJwsWithWeakHmacKey() { SignatureAlgorithm alg = SignatureAlgorithms.HS384 - def key = alg.generateKey() - def weakKey = SignatureAlgorithms.HS256.generateKey() + def key = alg.keyBuilder().build() + def weakKey = SignatureAlgorithms.HS256.keyBuilder().build() String jws = Jwts.builder().setSubject("Foo").signWith(key, alg).compact() @@ -612,7 +623,7 @@ class JwtsTest { //create random signing key for testing: SignatureAlgorithm alg = SignatureAlgorithms.HS256 - SecretKey key = alg.generateKey() + SecretKey key = alg.keyBuilder().build() String notSigned = Jwts.builder().setSubject("Foo").compact() @@ -630,7 +641,7 @@ class JwtsTest { //create random signing key for testing: SignatureAlgorithm alg = SignatureAlgorithms.HS256 - SecretKey key = alg.generateKey() + SecretKey key = alg.keyBuilder().build() //this is a 'real', valid JWT: String compact = Jwts.builder().setSubject("Joe").signWith(key, alg).compact() @@ -753,6 +764,148 @@ class JwtsTest { } } + @Test + void testSecretKeyJwes() { + + def algs = KeyAlgorithms.values().findAll({ it -> + it instanceof DirectKeyAlgorithm || it instanceof SecretKeyAlgorithm + })// as Collection> + + for (KeyAlgorithm alg : algs) { + + for (AeadAlgorithm enc : EncryptionAlgorithms.values()) { + + SecretKey key = alg instanceof SecretKeyAlgorithm ? + ((SecretKeyAlgorithm) alg).keyBuilder().build() : + enc.keyBuilder().build() + + // encrypt: + String jwe = Jwts.jweBuilder() + .claim('foo', 'bar') + .encryptWith(enc) + .withKeyFrom(key, alg) + .compact() + + //decrypt: + def jwt = Jwts.parserBuilder() + .decryptWith(key) + .build() + .parseClaimsJwe(jwe) + assertEquals 'bar', jwt.getBody().get('foo') + } + } + } + + @Test + void testPasswordJwes() { + + def algs = KeyAlgorithms.values().findAll({ it -> + it instanceof Pbes2HsAkwAlgorithm + })// as Collection> + + PasswordKey key = Keys.forPassword("12345678".toCharArray()) + + for (KeyAlgorithm alg : algs) { + + for (AeadAlgorithm enc : EncryptionAlgorithms.values()) { + + // encrypt: + String jwe = Jwts.jweBuilder() + .claim('foo', 'bar') + .encryptWith(enc) + .withKeyFrom(key, alg) + .compact() + + //decrypt: + def jwt = Jwts.parserBuilder() + .decryptWith(key) + .build() + .parseClaimsJwe(jwe) + assertEquals 'bar', jwt.getBody().get('foo') + } + } + } + + @Test + void testRsaJwes() { + + def pairs = [ + SignatureAlgorithms.RS256.generateKeyPair(), + SignatureAlgorithms.RS384.generateKeyPair(), + SignatureAlgorithms.RS512.generateKeyPair() + ] + + def algs = KeyAlgorithms.values().findAll({ it -> + it instanceof RsaKeyAlgorithm + })// as Collection> + + for (KeyPair pair : pairs) { + + def pubKey = pair.getPublic() + def privKey = pair.getPrivate() + + for (KeyAlgorithm alg : algs) { + + for (AeadAlgorithm enc : EncryptionAlgorithms.values()) { + + // encrypt: + String jwe = Jwts.jweBuilder() + .claim('foo', 'bar') + .encryptWith(enc) + .withKeyFrom(pubKey, alg) + .compact() + + //decrypt: + def jwt = Jwts.parserBuilder() + .decryptWith(privKey) + .build() + .parseClaimsJwe(jwe) + assertEquals 'bar', jwt.getBody().get('foo') + } + } + } + } + + @Test + void testEcJwes() { + + def pairs = [ + SignatureAlgorithms.ES256.generateKeyPair(), + SignatureAlgorithms.ES384.generateKeyPair(), + SignatureAlgorithms.ES512.generateKeyPair() + ] + + def algs = KeyAlgorithms.values().findAll({ it -> + it instanceof EcKeyAlgorithm + }) + + for (KeyPair pair : pairs) { + + def pubKey = pair.getPublic() + def privKey = pair.getPrivate() + + for (KeyAlgorithm alg : algs) { + + for (AeadAlgorithm enc : EncryptionAlgorithms.values()) { + + // encrypt: + String jwe = Jwts.jweBuilder() + .claim('foo', 'bar') + .encryptWith(enc) + .withKeyFrom(pubKey, alg) + .compact() + + //decrypt: + def jwt = Jwts.parserBuilder() + .decryptWith(privKey) + .build() + .parseClaimsJwe(jwe) + assertEquals 'bar', jwt.getBody().get('foo') + } + } + } + } + static void testRsa(AsymmetricKeySignatureAlgorithm alg, boolean verifyWithPrivateKey = false) { KeyPair kp = alg.generateKeyPair() @@ -777,7 +930,7 @@ class JwtsTest { static void testHmac(SecretKeySignatureAlgorithm alg) { //create random signing key for testing: - SecretKey key = alg.generateKey() + SecretKey key = alg.keyBuilder().build() def claims = new DefaultClaims([iss: 'joe', exp: later(), 'https://example.com/is_root': true]) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy index 37b52d812..3db9cd74a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy @@ -4,8 +4,7 @@ import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.EncryptionAlgorithms import org.junit.Test -import static org.junit.Assert.assertEquals -import static org.junit.Assert.fail +import static org.junit.Assert.* class DefaultJweBuilderTest { @@ -43,7 +42,7 @@ class DefaultJweBuilderTest { @Test void testCompactWithoutEncryptionAlgorithm() { - def key = EncryptionAlgorithms.A128GCM.generateKey() + def key = EncryptionAlgorithms.A128GCM.keyBuilder().build() try { builder().setIssuer("me").withKey(key).compact() } catch (IllegalStateException ise) { @@ -54,7 +53,7 @@ class DefaultJweBuilderTest { @Test void testCompactSimplestPayload() { def enc = EncryptionAlgorithms.A128GCM - def key = enc.generateKey() + def key = enc.keyBuilder().build() def jwe = builder().setPayload("me").encryptWith(enc).withKey(key).compact() def jwt = Jwts.parserBuilder().decryptWith(key).build().parsePlaintextJwe(jwe) assertEquals 'me', jwt.getBody() @@ -63,7 +62,7 @@ class DefaultJweBuilderTest { @Test void testCompactSimplestClaims() { def enc = EncryptionAlgorithms.A128GCM - def key = enc.generateKey() + def key = enc.keyBuilder().build() def jwe = builder().setSubject('joe').encryptWith(enc).withKey(key).compact() def jwt = Jwts.parserBuilder().decryptWith(key).build().parseClaimsJwe(jwe) assertEquals 'joe', jwt.getBody().getSubject() @@ -86,7 +85,7 @@ class DefaultJweBuilderTest { @Test void testBuild() { def enc = EncryptionAlgorithms.A128GCM - def key = enc.generateKey() + def key = enc.keyBuilder().build() new DefaultJweBuilder() .setSubject('joe') diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index bceed48bc..b1f498455 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -24,11 +24,15 @@ import io.jsonwebtoken.io.Encoder import io.jsonwebtoken.io.EncodingException import io.jsonwebtoken.io.SerializationException import io.jsonwebtoken.io.Serializer -import io.jsonwebtoken.security.* +import io.jsonwebtoken.security.KeyException +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.SignatureAlgorithms +import io.jsonwebtoken.security.SignatureException +import io.jsonwebtoken.security.SignatureRequest +import io.jsonwebtoken.security.VerifySignatureRequest import org.junit.Test import javax.crypto.KeyGenerator -import java.security.Key import java.security.Provider import java.security.SecureRandom @@ -70,7 +74,7 @@ class DefaultJwtBuilderTest { replay provider def b = new DefaultJwtBuilder().setProvider(provider) - .setSubject('me').signWith(SignatureAlgorithms.HS256.generateKey(), alg) + .setSubject('me').signWith(SignatureAlgorithms.HS256.keyBuilder().build(), alg) assertSame provider, b.provider b.compact() verify provider @@ -107,7 +111,7 @@ class DefaultJwtBuilderTest { } def b = new DefaultJwtBuilder().setSecureRandom(random) - .setSubject('me').signWith(SignatureAlgorithms.HS256.generateKey(), alg) + .setSubject('me').signWith(SignatureAlgorithms.HS256.keyBuilder().build(), alg) assertSame random, b.secureRandom b.compact() assertTrue called[0] diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy index d7cecd76b..c3ec502ba 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy @@ -30,8 +30,7 @@ import java.security.Provider import static org.easymock.EasyMock.* import static org.hamcrest.MatcherAssert.assertThat -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertSame +import static org.junit.Assert.* // NOTE to the casual reader: even though this test class appears mostly empty, the DefaultJwtParserBuilder // implementation is tested to 100% coverage. The vast majority of its tests are in the JwtsTest class. This class @@ -86,7 +85,7 @@ class DefaultJwtParserBuilderTest { assertSame deserializer, p.deserializer def alg = SignatureAlgorithms.HS256 - def key = alg.generateKey() + def key = alg.keyBuilder().build() String jws = Jwts.builder().claim('foo', 'bar').signWith(key, alg).compact() @@ -123,8 +122,8 @@ class DefaultJwtParserBuilderTest { void testUserSetDeserializerWrapped() { Deserializer deserializer = niceMock(Deserializer) JwtParser parser = new DefaultJwtParserBuilder() - .deserializeJsonWith(deserializer) - .build() + .deserializeJsonWith(deserializer) + .build() // TODO: When the ImmutableJwtParser replaces the default implementation this test will need updating assertThat parser.jwtParser.deserializer, CoreMatchers.instanceOf(JwtDeserializer) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy index d22f18f59..78531e574 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy @@ -1,17 +1,20 @@ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.security.* + +import io.jsonwebtoken.security.EncryptionAlgorithms +import io.jsonwebtoken.security.Jwk +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.MalformedKeyException +import io.jsonwebtoken.security.SecretJwk import org.junit.Test import javax.crypto.SecretKey -import java.security.Security -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertNotNull +import static org.junit.Assert.* class AbstractJwkBuilderTest { - private static final SecretKey SKEY = EncryptionAlgorithms.A256GCM.generateKey() + private static final SecretKey SKEY = EncryptionAlgorithms.A256GCM.keyBuilder().build() private static AbstractJwkBuilder builder() { return (AbstractJwkBuilder)Jwks.builder().setKey(SKEY) @@ -105,7 +108,7 @@ class AbstractJwkBuilderTest { @Test void testProvider() { - def provider = Security.getProvider("BC") + def provider = Providers.getBouncyCastleProviderIfPossible() def jwk = builder().setProvider(provider).build() assertEquals 'oct', jwk.getType() } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy index 5b77faba8..c57371b42 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy @@ -30,7 +30,7 @@ class AesAlgorithmTest { def alg = new TestAesAlgorithm('foo', 'foo', 192) - def key = EncryptionAlgorithms.A128GCM.generateKey() //weaker than required + def key = EncryptionAlgorithms.A128GCM.keyBuilder().build() //weaker than required def request = new DefaultCryptoRequest(null, null, new byte[1], key) @@ -99,7 +99,7 @@ class AesAlgorithmTest { def secureRandom = new SecureRandom() - def req = new DefaultAeadRequest(null, secureRandom, 'data'.getBytes(), alg.generateKey(), 'aad'.getBytes()) + def req = new DefaultAeadRequest(null, secureRandom, 'data'.getBytes(), alg.keyBuilder().build(), 'aad'.getBytes()) def returnedSecureRandom = alg.ensureSecureRandom(req) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy index 5230be4ca..d70cf907a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy @@ -7,10 +7,10 @@ import io.jsonwebtoken.impl.lang.CheckedFunction import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.lang.Arrays import io.jsonwebtoken.security.EncryptionAlgorithms +import io.jsonwebtoken.security.SecretKeyBuilder import org.junit.Test import javax.crypto.Cipher -import javax.crypto.SecretKey import javax.crypto.spec.GCMParameterSpec import java.nio.charset.StandardCharsets @@ -31,8 +31,8 @@ class AesGcmKeyAlgorithmTest { def iv = new byte[12]; Randoms.secureRandom().nextBytes(iv); - def kek = alg.generateKey(); - def cek = alg.generateKey(); + def kek = alg.keyBuilder().build() + def cek = alg.keyBuilder().build() JcaTemplate template = new JcaTemplate("AES/GCM/NoPadding", null) byte[] jcaResult = template.execute(Cipher.class, new CheckedFunction() { @@ -72,8 +72,8 @@ class AesGcmKeyAlgorithmTest { def cek = template.generateSecretKey(keyLength) def enc = new GcmAesAeadAlgorithm(keyLength) { @Override - SecretKey generateKey() { - return cek; + SecretKeyBuilder keyBuilder() { + return new FixedSecretKeyBuilder(cek) } } @@ -107,8 +107,8 @@ class AesGcmKeyAlgorithmTest { def cek = template.generateSecretKey(keyLength) def enc = new GcmAesAeadAlgorithm(keyLength) { @Override - SecretKey generateKey() { - return cek + SecretKeyBuilder keyBuilder() { + return new FixedSecretKeyBuilder(cek) } } def ereq = new DefaultKeyRequest(null, null, kek, header, enc) @@ -129,12 +129,15 @@ class AesGcmKeyAlgorithmTest { String missing(String name) { return "JWE header is missing required '${name}' value." as String } + String type(String name) { return "JWE header '${name}' value must be a String. Actual type: java.lang.Integer" as String } + String base64Url(String name) { return "JWE header '${name}' value is not a valid Base64URL String: Illegal base64url character: '#'" } + String length(String name, int requiredBitLength) { return "JWE header '${name}' decoded byte array must be ${Bytes.bitsMsg(requiredBitLength)} long. Actual length: ${Bytes.bitsMsg(16)}." } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy index dbad8e005..ea7bcd0b5 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy @@ -30,7 +30,7 @@ class DefaultRsaSignatureAlgorithmTest { @Test(expected = IllegalArgumentException) void testWeakPreferredKeyLength() { - new DefaultRsaSignatureAlgorithm('RS256', 'SHA256withRSA', 1024) //must be >= 2048 + new DefaultRsaSignatureAlgorithm(256, 1024) //must be >= 2048 } @Test diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy index e440b78db..47306a163 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy @@ -10,8 +10,7 @@ import javax.crypto.spec.SecretKeySpec import java.security.Key import static org.easymock.EasyMock.* -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertSame +import static org.junit.Assert.* class DirectKeyAlgorithmTest { @@ -51,7 +50,7 @@ class DirectKeyAlgorithmTest { void testGetDecryptionKey() { def alg = new DirectKeyAlgorithm() DecryptionKeyRequest req = createMock(DecryptionKeyRequest) - def key = EncryptionAlgorithms.A128GCM.generateKey() + def key = EncryptionAlgorithms.A128GCM.keyBuilder().build() expect(req.getKey()).andReturn(key) replay(req) def result = alg.getDecryptionKey(req) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/FixedSecretKeyBuilder.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/FixedSecretKeyBuilder.groovy new file mode 100644 index 000000000..a5a7cdc5a --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/FixedSecretKeyBuilder.groovy @@ -0,0 +1,31 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.SecretKeyBuilder + +import javax.crypto.SecretKey +import java.security.Provider +import java.security.SecureRandom + +class FixedSecretKeyBuilder implements SecretKeyBuilder { + + final SecretKey key + + FixedSecretKeyBuilder(SecretKey key) { + this.key = key + } + + @Override + SecretKey build() { + return this.key + } + + @Override + SecretKeyBuilder setProvider(Provider provider) { + return this + } + + @Override + SecretKeyBuilder setRandom(SecureRandom random) { + return this + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy index 9f4279b23..2e8e92b8e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy @@ -1,5 +1,7 @@ package io.jsonwebtoken.impl.security +import io.jsonwebtoken.impl.lang.Bytes +import io.jsonwebtoken.security.AeadAlgorithm import io.jsonwebtoken.security.EncryptionAlgorithms import io.jsonwebtoken.security.SignatureException import org.junit.Test @@ -13,12 +15,27 @@ import static org.junit.Assert.assertEquals */ class HmacAesAeadAlgorithmTest { + @Test + void testKeyBitLength() { + // asserts that key lengths are double than what is usually expected for AES + // due to the encrypt-then-mac scheme requiring two separate keys + // (encrypt key is half of the generated key, mac key is the 2nd half of the generated key): + assertEquals 256, EncryptionAlgorithms.A128CBC_HS256.getKeyBitLength() + assertEquals 384, EncryptionAlgorithms.A192CBC_HS384.getKeyBitLength() + assertEquals 512, EncryptionAlgorithms.A256CBC_HS512.getKeyBitLength() + } + @Test void testGenerateKey() { - def alg = EncryptionAlgorithms.A128CBC_HS256 - SecretKey key = alg.generateKey(); - int algKeyByteLength = (alg.keyBitLength * 2) / Byte.SIZE - assertEquals algKeyByteLength, key.getEncoded().length + def algs = [ + EncryptionAlgorithms.A128CBC_HS256, + EncryptionAlgorithms.A192CBC_HS384, + EncryptionAlgorithms.A256CBC_HS512 + ] + for(AeadAlgorithm alg : algs) { + SecretKey key = alg.keyBuilder().build() + assertEquals alg.getKeyBitLength(), Bytes.bitLength(key.getEncoded()) + } } @Test(expected = SignatureException) @@ -26,7 +43,7 @@ class HmacAesAeadAlgorithmTest { def alg = EncryptionAlgorithms.A128CBC_HS256; - SecretKey key = alg.generateKey() + SecretKey key = alg.keyBuilder().build() def plaintext = "Hello World! Nice to meet you!".getBytes("UTF-8") diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index 59ddf6ffd..5ca5536f6 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -30,7 +30,7 @@ import static org.junit.Assert.* class JwksTest { - private static final SecretKey SKEY = SignatureAlgorithms.HS256.generateKey(); + private static final SecretKey SKEY = SignatureAlgorithms.HS256.keyBuilder().build() private static final KeyPair EC_PAIR = SignatureAlgorithms.ES256.generateKeyPair(); private static String srandom() { @@ -184,7 +184,7 @@ class JwksTest { void testSecretJwks() { Collection algs = SignatureAlgorithms.values().findAll({it instanceof SecretKeySignatureAlgorithm}) as Collection for(def alg : algs) { - SecretKey secretKey = alg.generateKey() + SecretKey secretKey = alg.keyBuilder().build() def jwk = Jwks.builder().setKey(secretKey).setId('id').build() assertEquals 'oct', jwk.getType() assertTrue jwk.containsKey('k') diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy index 303878171..9690e78b9 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy @@ -19,16 +19,16 @@ class MacSignatureAlgorithmTest { @Test(expected = SecurityException) void testKeyGeneratorNoSuchAlgorithm() { MacSignatureAlgorithm alg = new MacSignatureAlgorithm('HS256', 'foo', 256); - alg.generateKey() + alg.keyBuilder().build() } @Test void testKeyGeneratorKeyLength() { MacSignatureAlgorithm alg = new MacSignatureAlgorithm('HS256', 'HmacSHA256', 256); - assertEquals 256, alg.generateKey().getEncoded().length * Byte.SIZE + assertEquals 256, alg.keyBuilder().build().getEncoded().length * Byte.SIZE alg = new MacSignatureAlgorithm('A128CBC-HS256', 'HmacSHA256', 128) - assertEquals 128, alg.generateKey().getEncoded().length * Byte.SIZE + assertEquals 128, alg.keyBuilder().build().getEncoded().length * Byte.SIZE } @Test(expected = IllegalArgumentException) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/BridgeConstructorsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateConstructorsTest.groovy similarity index 73% rename from impl/src/test/groovy/io/jsonwebtoken/impl/security/BridgeConstructorsTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateConstructorsTest.groovy index 3016d6ab7..1eed4e4a8 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/BridgeConstructorsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateConstructorsTest.groovy @@ -1,8 +1,9 @@ package io.jsonwebtoken.impl.security +import io.jsonwebtoken.impl.lang.Conditions import org.junit.Test -class BridgeConstructorsTest { +class PrivateConstructorsTest { @Test void testPrivateCtors() { // for code coverage only @@ -10,5 +11,6 @@ class BridgeConstructorsTest { new EncryptionAlgorithmsBridge() new KeyAlgorithmsBridge() new KeysBridge() + new Conditions() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy index 8ead52ed7..19efdbea9 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy @@ -4,15 +4,19 @@ import io.jsonwebtoken.Jwe import io.jsonwebtoken.Jwts import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders -import io.jsonwebtoken.security.* +import io.jsonwebtoken.security.AeadAlgorithm +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.KeyAlgorithms +import io.jsonwebtoken.security.SecretJwk +import io.jsonwebtoken.security.SecretKeyBuilder +import io.jsonwebtoken.security.SecurityRequest import org.junit.Test import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec import java.nio.charset.StandardCharsets -import static org.junit.Assert.assertArrayEquals -import static org.junit.Assert.assertEquals +import static org.junit.Assert.* class RFC7516AppendixA3Test { @@ -105,12 +109,12 @@ class RFC7516AppendixA3Test { AeadAlgorithm enc = new HmacAesAeadAlgorithm(128) { @Override protected byte[] ensureInitializationVector(SecurityRequest request) { - return IV; + return IV } @Override - SecretKey generateKey() { - return CEK; + SecretKeyBuilder keyBuilder() { + return new FixedSecretKeyBuilder(CEK) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy index dfe5d5d93..24c514092 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy @@ -9,6 +9,7 @@ import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.security.KeyRequest import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.PasswordKey +import io.jsonwebtoken.security.SecretKeyBuilder import io.jsonwebtoken.security.SecurityRequest import org.junit.Test @@ -273,8 +274,8 @@ class RFC7517AppendixCTest { //ensure that the KeyAlgorithm reflects test harness values: def encAlg = new HmacAesAeadAlgorithm(128) { @Override - SecretKey generateKey() { - return RFC_CEK + SecretKeyBuilder keyBuilder() { + return new FixedSecretKeyBuilder(RFC_CEK) } @Override diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy index 8af3d51b5..ff9ff081f 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy @@ -84,7 +84,7 @@ class EncryptionAlgorithmsTest { for (AeadAlgorithm alg : EncryptionAlgorithms.values()) { - def key = alg.generateKey() + def key = alg.keyBuilder().build() def request = new DefaultAeadRequest(PLAINTEXT_BYTES, key, null) @@ -114,7 +114,7 @@ class EncryptionAlgorithmsTest { for (AeadAlgorithm alg : EncryptionAlgorithms.values()) { - def key = alg.generateKey() + def key = alg.keyBuilder().build() def req = new DefaultAeadRequest(null, null, PLAINTEXT_BYTES, key, AAD_BYTES) diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy index 06cc2f60a..38e018662 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy @@ -15,6 +15,7 @@ */ package io.jsonwebtoken.security +import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.impl.security.DefaultEllipticCurveSignatureAlgorithm import io.jsonwebtoken.impl.security.DefaultPasswordKey import io.jsonwebtoken.impl.security.DefaultRsaSignatureAlgorithm @@ -130,8 +131,8 @@ class KeysTest { void testSecretKeyFor() { for (SignatureAlgorithm alg : SignatureAlgorithms.values()) { if (alg instanceof SecretKeySignatureAlgorithm) { - SecretKey key = alg.generateKey() - assertEquals alg.minKeyLength, key.getEncoded().length * 8 //convert byte count to bit count + SecretKey key = alg.keyBuilder().build() + assertEquals alg.getKeyBitLength(), Bytes.bitLength(key.getEncoded()) assertEquals alg.jcaName, key.algorithm assertEquals alg, SignatureAlgorithms.forSigningKey(key) // https://github.com/jwtk/jjwt/issues/381 } From be299073bbcec24b588b0a1ce40263945cf56a02 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 6 Nov 2021 21:25:06 -0700 Subject: [PATCH 13/75] 1. EcdhKeyAlgorithm: consolidated duplicate logic to a single private helper method 2. Updated RequiredTypeConverter exception message to represent the expected type as well as the found type --- .../impl/lang/RequiredTypeConverter.java | 5 +- .../impl/security/EcdhKeyAlgorithm.java | 46 ++++++++----------- .../lang/RequiredTypeConverterTest.groovy | 6 ++- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredTypeConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredTypeConverter.java index 38d035208..9c1a1c680 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredTypeConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredTypeConverter.java @@ -2,6 +2,9 @@ import io.jsonwebtoken.lang.Assert; +/** + * @since JJWT_RELEASE_VERSION + */ class RequiredTypeConverter implements Converter { private final Class type; @@ -22,7 +25,7 @@ public T applyFrom(Object o) { } Class clazz = o.getClass(); if (!type.isAssignableFrom(clazz)) { - String msg = "Unsupported value type: " + clazz.getName(); + String msg = "Unsupported value type. Expected: " + type.getName() + ", found: " + clazz.getName(); throw new IllegalArgumentException(msg); } return type.cast(o); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java index 92e596d07..fb755b5e4 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java @@ -1,7 +1,6 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.JweHeader; -import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.impl.lang.ValueGetter; @@ -123,20 +122,24 @@ private int getKeyBitLength(AeadAlgorithm enc) { return Assert.gt(bitLength, 0, "Algorithm keyBitLength must be > 0"); } + private SecretKey deriveKey(KeyRequest request, PublicKey publicKey, PrivateKey privateKey) { + AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); + int requiredCekBitLen = getKeyBitLength(enc); + final String AlgorithmID = getConcatKDFAlgorithmId(enc); + byte[] apu = request.getHeader().getAgreementPartyUInfo(); + byte[] apv = request.getHeader().getAgreementPartyVInfo(); + byte[] OtherInfo = createOtherInfo(requiredCekBitLen, AlgorithmID, apu, apv); + byte[] Z = generateZ(request, publicKey, privateKey); + return CONCAT_KDF.deriveKey(Z, requiredCekBitLen, OtherInfo); + } + @Override public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { Assert.notNull(request, "Request cannot be null."); JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); + E publicKey = Assert.notNull(request.getKey(), "Request key cannot be null."); ECParameterSpec spec = Assert.notNull(publicKey.getParams(), "Request key params cannot be null."); - AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); - - int requiredCekBitLen = getKeyBitLength(enc); - final String AlgorithmID = getConcatKDFAlgorithmId(enc); - byte[] apu = header.getAgreementPartyUInfo(); - byte[] apv = header.getAgreementPartyVInfo(); - byte[] OtherInfo = createOtherInfo(requiredCekBitLen, AlgorithmID, apu, apv); - // note: we don't need to validate if specified key's point is on a supported curve here // because that will automatically be asserted when using Jwks.builder().... below KeyPair pair = generateKeyPair(request, spec); @@ -145,11 +148,10 @@ public KeyResult getEncryptionKey(KeyRequest request) throws SecurityExceptio // This asserts that the generated public key (and therefore the request key) is on a JWK-supported curve: final EcPublicJwk jwk = Jwks.builder().setKey(genPubKey).build(); - byte[] Z = generateZ(request, publicKey, genPrivKey); - SecretKey derived = CONCAT_KDF.deriveKey(Z, requiredCekBitLen, OtherInfo); + final SecretKey derived = deriveKey(request, publicKey, genPrivKey); DefaultKeyRequest wrapReq = new DefaultKeyRequest<>(request.getProvider(), request.getSecureRandom(), - derived, request.getHeader(), enc); + derived, request.getHeader(), request.getEncryptionAlgorithm()); KeyResult result = WRAP_ALG.getEncryptionKey(wrapReq); header.put(EPHEMERAL_PUBLIC_KEY, jwk); @@ -166,35 +168,25 @@ public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws Securi ValueGetter getter = new DefaultValueGetter(header); Map epkValues = getter.getRequiredMap(EPHEMERAL_PUBLIC_KEY); - // This call will assert the EPK, if valid, is also on a NIST curve: + // This call will assert the EPK, if valid, is also on a JWA-supported NIST curve: Jwk jwk = Jwks.builder().putAll(epkValues).build(); if (!(jwk instanceof EcPublicJwk)) { String msg = "JWE Header '" + EPHEMERAL_PUBLIC_KEY + "' (Ephemeral Public Key) value is not an " + "EllipticCurve Public JWK as required."; - throw new MalformedJwtException(msg); + throw new InvalidKeyException(msg); } EcPublicJwk epk = (EcPublicJwk) jwk; - // Now, while the EPK might be on a NIST curve, we need to ensure it's on the exact curve associted with the - // private key: + // While the EPK might be on a JWA-supported NIST curve, it must be on the private key's exact curve: if (!EcPublicJwkFactory.contains(privateKey.getParams().getCurve(), epk.toKey().getW())) { String msg = "JWE Header '" + EPHEMERAL_PUBLIC_KEY + "' (Ephemeral Public Key) value does not represent " + "a point on the expected curve."; throw new InvalidKeyException(msg); } - AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); - - int requiredCekBitLen = getKeyBitLength(enc); - final String AlgorithmID = getConcatKDFAlgorithmId(enc); - byte[] apu = header.getAgreementPartyUInfo(); - byte[] apv = header.getAgreementPartyVInfo(); - byte[] OtherInfo = createOtherInfo(requiredCekBitLen, AlgorithmID, apu, apv); - - byte[] Z = generateZ(request, epk.toKey(), privateKey); - SecretKey derived = CONCAT_KDF.deriveKey(Z, requiredCekBitLen, OtherInfo); + final SecretKey derived = deriveKey(request, epk.toKey(), privateKey); DecryptionKeyRequest unwrapReq = new DefaultDecryptionKeyRequest<>(request.getProvider(), - request.getSecureRandom(), derived, header, enc, request.getPayload()); + request.getSecureRandom(), derived, header, request.getEncryptionAlgorithm(), request.getPayload()); return WRAP_ALG.getDecryptionKey(unwrapReq); } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RequiredTypeConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RequiredTypeConverterTest.groovy index 7eca46465..53ec86b8a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RequiredTypeConverterTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RequiredTypeConverterTest.groovy @@ -4,6 +4,9 @@ import org.junit.Test import static org.junit.Assert.* +/** + * @since JJWT_RELEASE_VERSION + */ class RequiredTypeConverterTest { @Test @@ -25,7 +28,8 @@ class RequiredTypeConverterTest { try { converter.applyFrom('hello' as String) } catch (IllegalArgumentException expected) { - assertEquals 'Unsupported value type: java.lang.String', expected.getMessage() + String msg = 'Unsupported value type. Expected: java.lang.Integer, found: java.lang.String' + assertEquals msg, expected.getMessage() } } } From eb4ee64b6d13b741aaff72c798fb6fe7168d217e Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 6 Nov 2021 21:28:04 -0700 Subject: [PATCH 14/75] Minor javadoc update --- api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java b/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java index 4667dd190..e98dc509a 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java +++ b/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java @@ -22,7 +22,7 @@ /** * No longer used by JJWT. Will be removed before the 1.0 final release. * - * @deprecated will be removed before the 1.0 final release. + * @deprecated since JJWT_RELEASE_VERSION. will be removed before the 1.0 final release. */ @Deprecated public final class RuntimeEnvironment { From 7a6b1a29b14a760493d64adf09a84d5867e31179 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 6 Nov 2021 22:21:27 -0700 Subject: [PATCH 15/75] 1. Javadoc cleanup. 2. Ensured CompressionException extends from io.jsonwebtoken.io.IOException 3. Deleted old POC unused JwkParser interface 4. Ensured NoneSignatureAlgorithm reflected the correct exception message for `sign` --- .../io/jsonwebtoken/CompressionException.java | 4 +++- .../java/io/jsonwebtoken/security/Keys.java | 12 ++++++------ .../jsonwebtoken/impl/security/JwkParser.java | 19 ------------------- .../impl/security/NoneSignatureAlgorithm.java | 2 +- .../impl/security/SecretJwkFactory.java | 2 +- 5 files changed, 11 insertions(+), 28 deletions(-) delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JwkParser.java diff --git a/api/src/main/java/io/jsonwebtoken/CompressionException.java b/api/src/main/java/io/jsonwebtoken/CompressionException.java index 287ccfb01..e0bdfe7ca 100644 --- a/api/src/main/java/io/jsonwebtoken/CompressionException.java +++ b/api/src/main/java/io/jsonwebtoken/CompressionException.java @@ -15,12 +15,14 @@ */ package io.jsonwebtoken; +import io.jsonwebtoken.io.IOException; + /** * Exception indicating that either compressing or decompressing an JWT body failed. * * @since 0.6.0 */ -public class CompressionException extends JwtException { +public class CompressionException extends IOException { public CompressionException(String message) { super(message); diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index 1998c7bf1..e3c393156 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -77,15 +77,15 @@ public static SecretKey hmacShaKeyFor(byte[] bytes) throws WeakKeyException { /** *

    Deprecation Notice

    *

    As of JJWT JJWT_RELEASE_VERSION, symmetric (secret) key algorithm instances can generate a key of suitable - * length for that specific algorithm by calling their {@code generateKey()} method directly. For example: + * length for that specific algorithm by calling their {@code keyBuilder()} method directly. For example: *

    -     * {@link SignatureAlgorithms#HS256}.generateKey();
    -     * {@link SignatureAlgorithms#HS384}.generateKey();
    -     * {@link SignatureAlgorithms#HS512}.generateKey();
    +     * {@link SignatureAlgorithms#HS256}.keyBuilder().build();
    +     * {@link SignatureAlgorithms#HS384}.keyBuilder().build();
    +     * {@link SignatureAlgorithms#HS512}.keyBuilder().build();
          * 
    * Call those methods as needed instead of this {@code secretKeyFor} helper method. This helper method will be * removed before the 1.0 final release.

    - *

    + *

    Previous Documentation

    * Returns a new {@link SecretKey} with a key length suitable for use with the specified {@link SignatureAlgorithm}. * *

    JWA Specification (RFC 7518), Section 3.2 @@ -143,7 +143,7 @@ public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) thr * * Call those methods as needed instead of this {@code keyPairFor} helper method. This helper method will be * removed before the 1.0 final release.

    - *

    + *

    Previous Documentation

    * Returns a new {@link KeyPair} suitable for use with the specified asymmetric algorithm. * *

    If the {@code alg} argument is an RSA algorithm, a KeyPair is generated based on the following:

    diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkParser.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkParser.java deleted file mode 100644 index 23ca38d32..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkParser.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.security.Jwk; -import io.jsonwebtoken.security.KeyException; - -import java.security.Key; -import java.util.Map; - -public interface JwkParser { - - Key parse(String json) throws KeyException; - - Key parse(Map jwkMap) throws KeyException; - - Jwk parseToJwk(String json) throws KeyException; - - Jwk parseToJwk(Map jwkMap) throws KeyException; - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java index 2b19f4b85..94f757910 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java @@ -19,7 +19,7 @@ public String getId() { @Override public byte[] sign(SignatureRequest request) throws SecurityException { - throw new SignatureException("The 'none' algorithm cannot be used to verify signatures."); + throw new SignatureException("The 'none' algorithm cannot be used to create signatures."); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java index b7149730e..3993d94ee 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java @@ -47,12 +47,12 @@ protected SecretJwk createJwkFromKey(JwkContext ctx) { try { byte[] encoded = getRequiredEncoded(key, "represent the SecretKey instance as a JWK"); k = Encoders.BASE64URL.encode(encoded); + Assert.hasText(k, "k value cannot be null or empty."); } catch (Exception e) { String msg = "Unable to encode SecretKey to JWK: " + e.getMessage(); throw new UnsupportedKeyException(msg, e); } - assert k != null : "k value is mandatory."; ctx.put(DefaultSecretJwk.K.getId(), k); return new DefaultSecretJwk(ctx); From 1144c64e56a3b59eca45c7617614aa6eb2784ed8 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 7 Nov 2021 11:34:30 -0800 Subject: [PATCH 16/75] Fixed erroneous JavaDoc, enhanced code coverage for DefaultClaims --- .../io/jsonwebtoken/impl/DefaultClaims.java | 13 +++++++++-- .../jsonwebtoken/impl/security/ConcatKDF.java | 2 +- .../impl/DefaultClaimsTest.groovy | 23 ++++++++++++++++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 049b22b60..f24b2ca15 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -20,6 +20,7 @@ import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.impl.lang.JwtDateConverter; +import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; import java.util.Date; @@ -135,6 +136,7 @@ public Claims setId(String jti) { @Override public T get(String claimName, Class requiredType) { + Assert.notNull(requiredType, "requiredType argument cannot be null."); Object value = idiomaticGet(claimName); if (requiredType.isInstance(value)) { @@ -155,10 +157,10 @@ public T get(String claimName, Class requiredType) { } } - return castClaimValue(value, requiredType); + return castClaimValue(claimName, value, requiredType); } - private T castClaimValue(Object value, Class requiredType) { + private T castClaimValue(String name, Object value, Class requiredType) { if (value instanceof Long || value instanceof Integer || value instanceof Short || value instanceof Byte) { long longValue = ((Number)value).longValue(); @@ -173,6 +175,13 @@ private T castClaimValue(Object value, Class requiredType) { } } + if (value instanceof Long && + (requiredType.equals(Integer.class) || requiredType.equals(Short.class) || requiredType.equals(Byte.class))) { + String msg = "Claim '" + name + "' value is too large or too small to be represented as a " + + requiredType.getName() + " instance (would cause numeric overflow)."; + throw new RequiredTypeException(msg); + } + if (!requiredType.isInstance(value)) { throw new RequiredTypeException(String.format(CONVERSION_ERROR_MSG, value.getClass(), requiredType)); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java index e034b5c10..dccac3cf7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java @@ -17,7 +17,7 @@ /** * 'Clean room' implementation of the Concat KDF algorithm based solely on * NIST.800-56A, - * Section 5.8.1.1. Call the {@link #deriveKey(SecretKey, long, byte[]) deriveKey} method. + * Section 5.8.1.1. Call the {@link #deriveKey(byte[], long, byte[]) deriveKey} method. */ final class ConcatKDF extends CryptoAlgorithm { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy index 3db8f45a3..3278a8b78 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -154,6 +154,23 @@ class DefaultClaimsTest { } } + @Test + void testGetRequiredIntegerFromLong() { + claims.put('foo', Long.valueOf(Integer.MAX_VALUE)) + assertEquals Integer.MAX_VALUE, claims.get('foo', Integer.class) as Integer + } + + @Test + void testGetRequiredIntegerWouldCauseOverflow() { + claims.put('foo', Long.MAX_VALUE) + try { + claims.get('foo', Integer.class) + } catch (RequiredTypeException expected) { + String msg = "Claim 'foo' value is too large or too small to be represented as a java.lang.Integer instance (would cause numeric overflow)." + assertEquals msg, expected.getMessage() + } + } + @Test void testGetRequiredDateFromNull() { Date date = claims.get("aDate", Date.class) @@ -162,7 +179,7 @@ class DefaultClaimsTest { @Test void testGetRequiredDateFromDate() { - def expected = new Date(); + def expected = new Date() claims.put("aDate", expected) Date result = claims.get("aDate", Date.class) assertEquals expected, result @@ -313,14 +330,14 @@ class DefaultClaimsTest { @Test void testToSpecDateWithDate() { - long millis = System.currentTimeMillis(); + long millis = System.currentTimeMillis() Date d = new Date(millis) claims.put('exp', d) assertEquals d, claims.getExpiration() } void trySpecDateNonDate(Field field) { - def val = new Object() { @Override public String toString() {return 'hi'} } + def val = new Object() { @Override String toString() {return 'hi'} } try { claims.put(field.getId(), val) fail() From 1e84170d9abb9a393bad5e82ef25b2e67c2e7d93 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 7 Nov 2021 15:07:53 -0800 Subject: [PATCH 17/75] Added jwe compression test --- .../jsonwebtoken/impl/DefaultJweBuilder.java | 2 +- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 52 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java index 7b02a2475..ed50475de 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java @@ -116,7 +116,7 @@ public String compact() { Assert.state(key != null, "Key is required."); Assert.state(enc != null, "Encryption algorithm is required."); - assert alg != null : "KeyAlgorithm is required."; //always set by withKey calling withKeyFrom + Assert.state(alg != null, "KeyAlgorithm is required."); //always set by withKey calling withKeyFrom if (this.serializer == null) { // try to find one based on the services available //noinspection unchecked diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 816591320..e360f4aa0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -796,6 +796,35 @@ class JwtsTest { } } + @Test + void testJweCompression() { + + def codecs = [CompressionCodecs.DEFLATE, CompressionCodecs.GZIP] + + for (CompressionCodec codec : codecs) { + + for (AeadAlgorithm enc : EncryptionAlgorithms.values()) { + + SecretKey key = enc.keyBuilder().build() + + // encrypt and compress: + String jwe = Jwts.jweBuilder() + .claim('foo', 'bar') + .compressWith(codec) + .encryptWith(enc) + .withKey(key) + .compact() + + //decompress and decrypt: + def jwt = Jwts.parserBuilder() + .decryptWith(key) + .build() + .parseClaimsJwe(jwe) + assertEquals 'bar', jwt.getBody().get('foo') + } + } + } + @Test void testPasswordJwes() { @@ -826,6 +855,27 @@ class JwtsTest { } } + @Test + void testPasswordJweWithoutSpecifyingAlg() { + + PasswordKey key = Keys.forPassword("12345678".toCharArray()) + + // encrypt: + String jwe = Jwts.jweBuilder() + .claim('foo', 'bar') + .encryptWith(EncryptionAlgorithms.A256GCM) + .withKey(key) // does not use 'withKeyFrom', should default to strongest PBES2_HS512_A256KW + .compact() + + //decrypt: + def jwt = Jwts.parserBuilder() + .decryptWith(key) + .build() + .parseClaimsJwe(jwe) + assertEquals 'bar', jwt.getBody().get('foo') + assertEquals KeyAlgorithms.PBES2_HS512_A256KW, KeyAlgorithms.forId(jwt.getHeader().getAlgorithm()) + } + @Test void testRsaJwes() { @@ -851,7 +901,7 @@ class JwtsTest { // encrypt: String jwe = Jwts.jweBuilder() .claim('foo', 'bar') - .encryptWith(enc) + .encryptWith(enc) // does not use 'withKeyFrom' .withKeyFrom(pubKey, alg) .compact() From 5db786cc7a04606e9e9abebab22901b2665f646e Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 7 Nov 2021 19:02:25 -0800 Subject: [PATCH 18/75] Added TestKeys concept for Groovy test authoring --- .../security/SecretKeyAlgorithm.java | 2 +- .../java/io/jsonwebtoken/impl/JwtMap.java | 2 +- .../impl/security/CryptoAlgorithm.java | 2 +- .../io/jsonwebtoken/DeprecatedJwtsTest.groovy | 8 +- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 25 +++--- .../impl/DefaultHeaderTest.groovy | 36 +++++---- .../impl/DefaultJweHeaderTest.groovy | 73 ++++++++++++++++- .../AbstractAsymmetricJwkBuilderTest.groovy | 12 ++- .../security/AbstractJwkBuilderTest.groovy | 3 +- .../impl/security/AesAlgorithmTest.groovy | 3 +- .../impl/security/Issue542Test.groovy | 4 +- .../impl/security/JwksTest.groovy | 4 +- ...rtUtils.groovy => TestCertificates.groovy} | 20 ++++- .../impl/security/TestKeys.groovy | 80 +++++++++++++++++++ 14 files changed, 212 insertions(+), 62 deletions(-) rename impl/src/test/groovy/io/jsonwebtoken/impl/security/{CertUtils.groovy => TestCertificates.groovy} (77%) create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java index efad25c9a..2d14c792e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java @@ -5,5 +5,5 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface SecretKeyAlgorithm extends KeyAlgorithm, KeyBuilderSupplier { +public interface SecretKeyAlgorithm extends KeyAlgorithm, KeyBuilderSupplier, KeyLengthSupplier { } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 9ee12f81e..4df57bdfa 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -133,7 +133,7 @@ public Object put(String name, Object value) { // ensures that if a property name matches an RFC-specified name, that value can be represented // as an idiomatic type-safe Java value in addition to the canonical RFC/encoded value. private Object idiomaticPut(String name, Object value) { - assert name != null; //asserted by caller. + Assert.stateNotNull(name, "Name cannot be null."); // asserted by caller Field field = FIELDS.get(name); if (field != null) { //Setting a JWA-standard property - let's ensure we can represent it idiomatically: return apply(field, value); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java index ccd0b100e..b7b51baa3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java @@ -70,7 +70,7 @@ protected T execute(SecurityRequest request, Class clazz, CheckedFunct public SecretKey generateKey(KeyRequest request) { AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); - SecretKeyBuilder builder = Assert.notNull(enc.keyBuilder(), "Request encryptionAlgorithm cannot produce a null SecretKeyBuilder"); + SecretKeyBuilder builder = Assert.notNull(enc.keyBuilder(), "Request encryptionAlgorithm keyBuilder cannot be null."); SecretKey key = builder.setProvider(getProvider(request)).setRandom(request.getSecureRandom()).build(); return Assert.notNull(key, "Request encryptionAlgorithm SecretKeyBuilder cannot produce null keys."); } diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy index 9ee283327..0919161f9 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy @@ -21,11 +21,11 @@ import io.jsonwebtoken.impl.JwtTokenizer import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec import io.jsonwebtoken.impl.lang.Services +import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.Keys -import io.jsonwebtoken.security.SignatureAlgorithms import io.jsonwebtoken.security.WeakKeyException import org.junit.Test @@ -616,7 +616,7 @@ class DeprecatedJwtsTest { void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPrivateKey() { //Create a legitimate RSA public and private key pair: - KeyPair kp = SignatureAlgorithms.RS256.generateKeyPair() + KeyPair kp = TestKeys.RS256.pair PublicKey publicKey = kp.getPublic() PrivateKey privateKey = kp.getPrivate() @@ -648,7 +648,7 @@ class DeprecatedJwtsTest { void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPublicKey() { //Create a legitimate RSA public and private key pair: - KeyPair kp = SignatureAlgorithms.RS256.generateKeyPair() + KeyPair kp = TestKeys.RS256.pair PublicKey publicKey = kp.getPublic(); //PrivateKey privateKey = kp.getPrivate(); @@ -680,7 +680,7 @@ class DeprecatedJwtsTest { void testParseForgedEllipticCurvePublicKeyAsHmacToken() { //Create a legitimate RSA public and private key pair: - KeyPair kp = SignatureAlgorithms.ES256.generateKeyPair() + KeyPair kp = TestKeys.ES256.pair PublicKey publicKey = kp.getPublic(); //PrivateKey privateKey = kp.getPrivate(); diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index e360f4aa0..7a68cada3 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -25,6 +25,7 @@ import io.jsonwebtoken.impl.compression.GzipCompressionCodec import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.impl.security.DirectKeyAlgorithm import io.jsonwebtoken.impl.security.Pbes2HsAkwAlgorithm +import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer @@ -673,7 +674,7 @@ class JwtsTest { void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPrivateKey() { //Create a legitimate RSA public and private key pair: - KeyPair kp = SignatureAlgorithms.RS256.generateKeyPair() + KeyPair kp = TestKeys.RS256.pair PublicKey publicKey = kp.getPublic() PrivateKey privateKey = kp.getPrivate() @@ -705,7 +706,7 @@ class JwtsTest { void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPublicKey() { //Create a legitimate RSA public and private key pair: - KeyPair kp = SignatureAlgorithms.RS256.generateKeyPair() + KeyPair kp = TestKeys.RS256.pair PublicKey publicKey = kp.getPublic() //PrivateKey privateKey = kp.getPrivate(); @@ -736,8 +737,8 @@ class JwtsTest { @Test void testParseForgedEllipticCurvePublicKeyAsHmacToken() { - //Create a legitimate RSA public and private key pair: - KeyPair kp = SignatureAlgorithms.ES256.generateKeyPair() + //Create a legitimate EC public and private key pair: + KeyPair kp = TestKeys.ES256.pair PublicKey publicKey = kp.getPublic() //PrivateKey privateKey = kp.getPrivate(); @@ -879,11 +880,7 @@ class JwtsTest { @Test void testRsaJwes() { - def pairs = [ - SignatureAlgorithms.RS256.generateKeyPair(), - SignatureAlgorithms.RS384.generateKeyPair(), - SignatureAlgorithms.RS512.generateKeyPair() - ] + def pairs = [TestKeys.RS256.pair, TestKeys.RS384.pair, TestKeys.RS512.pair] def algs = KeyAlgorithms.values().findAll({ it -> it instanceof RsaKeyAlgorithm @@ -919,11 +916,7 @@ class JwtsTest { @Test void testEcJwes() { - def pairs = [ - SignatureAlgorithms.ES256.generateKeyPair(), - SignatureAlgorithms.ES384.generateKeyPair(), - SignatureAlgorithms.ES512.generateKeyPair() - ] + def pairs = [TestKeys.ES256.pair, TestKeys.ES384.pair, TestKeys.ES512.pair] def algs = KeyAlgorithms.values().findAll({ it -> it instanceof EcKeyAlgorithm @@ -958,7 +951,7 @@ class JwtsTest { static void testRsa(AsymmetricKeySignatureAlgorithm alg, boolean verifyWithPrivateKey = false) { - KeyPair kp = alg.generateKeyPair() + KeyPair kp = TestKeys.forAlgorithm(alg).pair PublicKey publicKey = kp.getPublic() PrivateKey privateKey = kp.getPrivate() @@ -994,7 +987,7 @@ class JwtsTest { static void testEC(AsymmetricKeySignatureAlgorithm alg, boolean verifyWithPrivateKey = false) { - KeyPair pair = alg.generateKeyPair() + KeyPair pair = TestKeys.forAlgorithm(alg).pair PublicKey publicKey = pair.getPublic() PrivateKey privateKey = pair.getPrivate() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy index d10a756b3..e91c7b3b6 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy @@ -16,40 +16,42 @@ package io.jsonwebtoken.impl import io.jsonwebtoken.Header +import org.junit.Before import org.junit.Test -import static org.junit.Assert.* + +import static org.junit.Assert.assertEquals class DefaultHeaderTest { + + private DefaultHeader header + + @Before + void setUp() { + header = new DefaultHeader() + } @Test void testType() { - - def h = new DefaultHeader() - - h.setType('foo') - assertEquals h.getType(), 'foo' + header.setType('foo') + assertEquals header.getType(), 'foo' } @Test void testContentType() { - - def h = new DefaultHeader() - - h.setContentType('bar') - assertEquals h.getContentType(), 'bar' + header.setContentType('bar') + assertEquals header.getContentType(), 'bar' } @Test void testSetCompressionAlgorithm() { - def h = new DefaultHeader() - h.setCompressionAlgorithm("DEF") - assertEquals "DEF", h.getCompressionAlgorithm() + header.setCompressionAlgorithm("DEF") + assertEquals "DEF", header.getCompressionAlgorithm() } + @SuppressWarnings('GrDeprecatedAPIUsage') @Test void testBackwardsCompatibleCompressionHeader() { - def h = new DefaultHeader() - h.put(Header.DEPRECATED_COMPRESSION_ALGORITHM, "DEF") - assertEquals "DEF", h.getCompressionAlgorithm() + header.put(Header.DEPRECATED_COMPRESSION_ALGORITHM, "DEF") + assertEquals "DEF", header.getCompressionAlgorithm() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy index f79384cd3..c584b2e47 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy @@ -1,18 +1,32 @@ package io.jsonwebtoken.impl -import io.jsonwebtoken.JweHeader + +import io.jsonwebtoken.impl.security.Randoms +import io.jsonwebtoken.impl.security.TestKeys +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.lang.Collections +import io.jsonwebtoken.security.EcPrivateJwk +import io.jsonwebtoken.security.EcPublicJwk +import io.jsonwebtoken.security.Jwks +import org.junit.Before import org.junit.Test -import static org.junit.Assert.assertEquals +import static org.junit.Assert.* /** * @since JJWT_RELEASE_VERSION */ class DefaultJweHeaderTest { + private DefaultJweHeader header + + @Before + void setUp() { + header = new DefaultJweHeader() + } + @Test void testAlgorithm() { - JweHeader header = new DefaultJweHeader() header.setAlgorithm('foo') assertEquals 'foo', header.getAlgorithm() @@ -22,11 +36,62 @@ class DefaultJweHeaderTest { @Test void testEncryptionAlgorithm() { - JweHeader header = new DefaultJweHeader() header.put('enc', 'foo') assertEquals 'foo', header.getEncryptionAlgorithm() header = new DefaultJweHeader([enc: 'bar']) assertEquals 'bar', header.getEncryptionAlgorithm() } + + @Test + void testJwkSetUrl() { + URI uri = new URI('https://github.com/jwtk/jjwt') + header.setJwkSetUrl(uri) + assertEquals uri, header.getJwkSetUrl() + assert uri.toString(), header.get('jku') + } + + @Test + void testJwk() { + EcPrivateJwk jwk = Jwks.builder().setKeyPairEc(TestKeys.ES256.pair).build() + EcPublicJwk pubJwk = jwk.toPublicJwk() + header.setJwk(pubJwk) + assertEquals pubJwk, header.getJwk() + } + + @Test + void testX509CertChain() { + def bundle = TestKeys.RS256 + List encodedCerts = Collections.of(Encoders.BASE64.encode(bundle.cert.getEncoded())) + header.setX509CertificateChain(bundle.chain) + assertEquals bundle.chain, header.getX509CertificateChain() + assertEquals encodedCerts, header.get('x5c') + } + + @Test + void testX509CertSha1Thumbprint() { + byte[] thumbprint = new byte[16] // simulate + Randoms.secureRandom().nextBytes(thumbprint) + String encoded = Encoders.BASE64URL.encode(thumbprint) + header.setX509CertificateSha1Thumbprint(thumbprint) + assertArrayEquals thumbprint, header.getX509CertificateSha1Thumbprint() + assertEquals encoded, header.get('x5t') + } + + @Test + void testX509CertSha256Thumbprint() { + byte[] thumbprint = new byte[32] // simulate + Randoms.secureRandom().nextBytes(thumbprint) + String encoded = Encoders.BASE64URL.encode(thumbprint) + header.setX509CertificateSha256Thumbprint(thumbprint) + assertArrayEquals thumbprint, header.getX509CertificateSha256Thumbprint() + assertEquals encoded, header.get('x5t#S256') + } + + @Test + void testCritical() { + Set crits = Collections.setOf('foo', 'bar') + header.setCritical(crits) + assertEquals crits, header.getCritical() + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy index 09a77f2d5..2f23301bb 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy @@ -3,21 +3,19 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.lang.Assert import io.jsonwebtoken.security.Jwks import io.jsonwebtoken.security.RsaPublicJwkBuilder -import io.jsonwebtoken.security.SignatureAlgorithms import org.junit.Test import java.security.cert.X509Certificate import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertSame +import static org.junit.Assert.* class AbstractAsymmetricJwkBuilderTest { - private static final X509Certificate CERT = CertUtils.readTestCertificate(SignatureAlgorithms.RS256) + private static final X509Certificate CERT = TestKeys.RS256.cert private static final List CHAIN = [CERT] - private static final RSAPublicKey PUB_KEY = (RSAPublicKey) CERT.getPublicKey() + private static final RSAPublicKey PUB_KEY = CERT.getPublicKey() as RSAPublicKey private static RsaPublicJwkBuilder builder() { return Jwks.builder().setKey(PUB_KEY) @@ -30,9 +28,9 @@ class AbstractAsymmetricJwkBuilderTest { assertEquals val, jwk.getPublicKeyUse() assertEquals val, jwk.use - def privateKey = CertUtils.readTestPrivateKey(SignatureAlgorithms.RS256) + RSAPrivateKey privateKey = TestKeys.RS256.pair.private as RSAPrivateKey - jwk = builder().setPublicKeyUse(val).setPrivateKey((RSAPrivateKey) privateKey).build() + jwk = builder().setPublicKeyUse(val).setPrivateKey(privateKey).build() assertEquals val, jwk.getPublicKeyUse() assertEquals val, jwk.use } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy index 78531e574..34d0eb6b7 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy @@ -1,7 +1,6 @@ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.security.EncryptionAlgorithms import io.jsonwebtoken.security.Jwk import io.jsonwebtoken.security.Jwks import io.jsonwebtoken.security.MalformedKeyException @@ -14,7 +13,7 @@ import static org.junit.Assert.* class AbstractJwkBuilderTest { - private static final SecretKey SKEY = EncryptionAlgorithms.A256GCM.keyBuilder().build() + private static final SecretKey SKEY = TestKeys.A256GCM private static AbstractJwkBuilder builder() { return (AbstractJwkBuilder)Jwks.builder().setKey(SKEY) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy index c57371b42..71b77bf92 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy @@ -5,7 +5,6 @@ import io.jsonwebtoken.security.AeadAlgorithm import io.jsonwebtoken.security.AeadRequest import io.jsonwebtoken.security.AeadResult import io.jsonwebtoken.security.DecryptAeadRequest -import io.jsonwebtoken.security.EncryptionAlgorithms import io.jsonwebtoken.security.PayloadSupplier import io.jsonwebtoken.security.SecurityException import org.junit.Test @@ -30,7 +29,7 @@ class AesAlgorithmTest { def alg = new TestAesAlgorithm('foo', 'foo', 192) - def key = EncryptionAlgorithms.A128GCM.keyBuilder().build() //weaker than required + def key = TestKeys.A128GCM //weaker than required def request = new DefaultCryptoRequest(null, null, new byte[1], key) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Issue542Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Issue542Test.groovy index fd4adf38a..552478596 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Issue542Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Issue542Test.groovy @@ -40,7 +40,7 @@ class Issue542Test { def algs = [SignatureAlgorithms.PS256, SignatureAlgorithms.PS384, SignatureAlgorithms.PS512] for (alg in algs) { - PublicKey key = CertUtils.readTestPublicKey(alg) + PublicKey key = TestCertificates.readTestPublicKey(alg) String jws = JWS_0_10_7_VALUES[alg] def token = Jwts.parser().setSigningKey(key).parseClaimsJws(jws) assert 'joe' == token.body.getIssuer() @@ -54,7 +54,7 @@ class Issue542Test { static void main(String[] args) { def algs = [SignatureAlgorithms.PS256, SignatureAlgorithms.PS384, SignatureAlgorithms.PS512] for (alg in algs) { - PrivateKey privateKey = CertUtils.readTestPrivateKey(alg) + PrivateKey privateKey = TestCertificates.readTestPrivateKey(alg) String jws = Jwts.builder().setIssuer('joe').signWith(privateKey, alg).compact() println "private static String ${alg.id()}_0_10_7 = '$jws'" } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index 5ca5536f6..6d1817b2b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -139,7 +139,7 @@ class JwksTest { @Test void testX509CertChain() { //get a test cert: - X509Certificate cert = CertUtils.readTestCertificate(SignatureAlgorithms.RS256) + X509Certificate cert = TestCertificates.readTestCertificate(SignatureAlgorithms.RS256) def sval = JwtX509StringConverter.INSTANCE.applyTo(cert) testProperty('x509CertificateChain', 'x5c', [cert], [sval]) } @@ -159,7 +159,7 @@ class JwksTest { for(def alg : algs) { //get test cert: - X509Certificate cert = CertUtils.readTestCertificate(alg) + X509Certificate cert = TestCertificates.readTestCertificate(alg) def pubKey = cert.getPublicKey() def builder = pubKey instanceof RSAPublicKey ? diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CertUtils.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestCertificates.groovy similarity index 77% rename from impl/src/test/groovy/io/jsonwebtoken/impl/security/CertUtils.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/TestCertificates.groovy index 698683924..e9235b7a7 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CertUtils.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestCertificates.groovy @@ -1,10 +1,12 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.lang.Classes +import io.jsonwebtoken.security.AsymmetricKeySignatureAlgorithm import io.jsonwebtoken.security.SignatureAlgorithm import org.bouncycastle.asn1.pkcs.PrivateKeyInfo import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.openssl.PEMKeyPair import org.bouncycastle.openssl.PEMParser import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter @@ -28,7 +30,7 @@ import java.security.cert.X509Certificate * 1) be used in Test classes only, and * 2) encapsulate the BouncyCastle API so it is not exposed to other Test classes. */ -class CertUtils { +class TestCertificates { private static JcaX509CertificateConverter X509_CERT_CONVERTER = new JcaX509CertificateConverter() private static JcaPEMKeyConverter PEM_KEY_CONVERTER = new JcaPEMKeyConverter() @@ -49,16 +51,28 @@ class CertUtils { } static PublicKey readTestPublicKey(SignatureAlgorithm alg) { - return readTestCertificate(alg).getPublicKey(); + return readTestCertificate(alg).getPublicKey() } static PrivateKey readTestPrivateKey(SignatureAlgorithm alg) { PEMParser parser = getParser(alg.getId() + '.key.pem') try { - PrivateKeyInfo info = parser.readObject() as PrivateKeyInfo + PrivateKeyInfo info + Object object = parser.readObject() + if (object instanceof PEMKeyPair) { + info = ((PEMKeyPair)object).getPrivateKeyInfo() + } else { + info = (PrivateKeyInfo)object + } return PEM_KEY_CONVERTER.getPrivateKey(info) } finally { parser.close() } } + + static TestKeys.Bundle readAsymmetricBundle(AsymmetricKeySignatureAlgorithm alg) { + X509Certificate cert = readTestCertificate(alg) + PrivateKey priv = readTestPrivateKey(alg) + return new TestKeys.Bundle(cert, priv) + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy new file mode 100644 index 000000000..666b14025 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy @@ -0,0 +1,80 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.Identifiable +import io.jsonwebtoken.lang.Collections +import io.jsonwebtoken.security.AsymmetricKeySignatureAlgorithm +import io.jsonwebtoken.security.EncryptionAlgorithms +import io.jsonwebtoken.security.KeyBuilderSupplier +import io.jsonwebtoken.security.SecretKeyBuilder +import io.jsonwebtoken.security.SignatureAlgorithms + +import javax.crypto.SecretKey +import java.security.KeyPair +import java.security.PrivateKey +import java.security.cert.X509Certificate + +/** + * Test helper with cached keys to save time across tests (so we don't have to constantly dynamically generate keys) + */ +class TestKeys { + + // ======================================================= + // Secret Keys + // ======================================================= + static SecretKey HS256 = SignatureAlgorithms.HS256.keyBuilder().build() + static SecretKey HS384 = SignatureAlgorithms.HS384.keyBuilder().build() + static SecretKey HS512 = SignatureAlgorithms.HS512.keyBuilder().build() + + static SecretKey A128GCM, A192GCM, A256GCM, A128KW, A192KW, A256KW, A128GCMKW, A192GCMKW, A256GCMKW + static { + A128GCM = A128KW = A128GCMKW = EncryptionAlgorithms.A128GCM.keyBuilder().build() + A192GCM = A192KW = A192GCMKW = EncryptionAlgorithms.A192GCM.keyBuilder().build() + A256GCM = A256KW = A256GCMKW = EncryptionAlgorithms.A256GCM.keyBuilder().build() + } + static SecretKey A128CBC_HS256 = EncryptionAlgorithms.A128CBC_HS256.keyBuilder().build() + static SecretKey A192CBC_HS384 = EncryptionAlgorithms.A192CBC_HS384.keyBuilder().build() + static SecretKey A256CBC_HS512 = EncryptionAlgorithms.A256CBC_HS512.keyBuilder().build() + + // ======================================================= + // Elliptic Curve Keys & Certificates + // ======================================================= + static Bundle ES256 = TestCertificates.readAsymmetricBundle(SignatureAlgorithms.ES256) + static Bundle ES384 = TestCertificates.readAsymmetricBundle(SignatureAlgorithms.ES384) + static Bundle ES512 = TestCertificates.readAsymmetricBundle(SignatureAlgorithms.ES512) + static Set EC = Collections.setOf(ES256, ES384, ES512) + + // ======================================================= + // RSA Keys & Certificates + // ======================================================= + static Bundle RS256 = TestCertificates.readAsymmetricBundle(SignatureAlgorithms.RS256) + static Bundle RS384 = TestCertificates.readAsymmetricBundle(SignatureAlgorithms.RS384) + static Bundle RS512 = TestCertificates.readAsymmetricBundle(SignatureAlgorithms.RS512) + static Set RSA = Collections.setOf(RS256, RS384, RS512) + + static & Identifiable> SecretKey forAlgorithm(T alg) { + String id = alg.getId() + if (id.contains('-')) { + id = id.replace('-', '_') + } + return TestKeys.metaClass.getAttribute(TestKeys, id) as SecretKey + } + + static Bundle forAlgorithm(AsymmetricKeySignatureAlgorithm alg) { + String id = alg.getId() + if (id.startsWith('PS')) { + id = 'R' + id.substring(1) //keys for PS* algs are the same as RS algs + } + return TestKeys.metaClass.getAttribute(TestKeys, id) as Bundle + } + + static class Bundle { + X509Certificate cert + List chain + KeyPair pair + Bundle(X509Certificate cert, PrivateKey privateKey) { + this.cert = cert + this.chain = Collections.of(cert) + this.pair = new KeyPair(cert.getPublicKey(), privateKey) + } + } +} From 3ba22feb0ee1655ffe0f08aac88c996f15ba11c1 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 7 Nov 2021 20:10:42 -0800 Subject: [PATCH 19/75] Added tests, cleaned up state assertions for code coverage --- .../jsonwebtoken/impl/security/ConcatKDF.java | 27 +++++++----- .../security/DefaultSecretKeyBuilder.java | 2 +- .../impl/security/WrappedSecretKey.java | 3 +- .../impl/security/ConcatKDFTest.groovy | 44 +++++++++++++++++++ .../DefaultSecretKeyBuilderTest.groovy | 20 +++++++++ 5 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/ConcatKDFTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilderTest.groovy diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java index dccac3cf7..42b76ce8c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java @@ -37,8 +37,17 @@ public Integer apply(MessageDigest instance) { } }); this.hashBitLength = hashByteLength * Byte.SIZE; - assert this.hashBitLength > 0 : "MessageDigest length must be a positive value."; - MAX_DERIVED_KEY_BIT_LENGTH = this.hashBitLength * MAX_REP_COUNT; + Assert.state(this.hashBitLength > 0, "MessageDigest length must be a positive value."); + + // NIST.SP.800-56Ar2.pdf, Section 5.8.1.1, Input requirement #2 says that the maximum bit length of the + // derived key cannot be more than this: + // + // hashBitLength * (2^32 - 1) + // + // However, this number is always greater than Integer.MAX_VALUE * Byte.SIZE, which is the maximum number of + // bits that can be represented in a Java byte array. So our implementation must be limited to that size + // regardless of what the spec allows: + MAX_DERIVED_KEY_BIT_LENGTH = (long) Integer.MAX_VALUE * (long) Byte.SIZE; } /** @@ -67,30 +76,26 @@ public SecretKey deriveKey(final byte[] Z, final long derivedKeyBitLength, final // derivedKeyBitLength argument assertions: Assert.isTrue(derivedKeyBitLength > 0, "derivedKeyBitLength must be a positive number."); - final long derivedKeyByteLength = derivedKeyBitLength / Byte.SIZE; - if (derivedKeyByteLength > Integer.MAX_VALUE) { // Java byte arrays can't be bigger than this - throw new IllegalArgumentException("derivedKeyBitLength cannot reflect a byte array size greater than Integer.MAX_VALUE."); - } - // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf, Section 5.8.1.1, Input requirement #2: if (derivedKeyBitLength > MAX_DERIVED_KEY_BIT_LENGTH) { - String msg = "derivedKeyBitLength for " + getJcaName() + "-derived keys may not exceed " + - bitsMsg(MAX_DERIVED_KEY_BIT_LENGTH) + ". Specified size: " + bitsMsg(derivedKeyBitLength) + "."; + String msg = "derivedKeyBitLength may not exceed " + bitsMsg(MAX_DERIVED_KEY_BIT_LENGTH) + + ". Specified size: " + bitsMsg(derivedKeyBitLength) + "."; throw new IllegalArgumentException(msg); } + final long derivedKeyByteLength = derivedKeyBitLength / Byte.SIZE; // Section 5.8.1.1, Process step #1: final double repsd = derivedKeyBitLength / (double) this.hashBitLength; final long reps = (long) (Math.ceil(repsd)); // Section 5.8.1.1, Process step #2: - assert reps <= MAX_REP_COUNT : "derivedKeyBitLength is too large."; + Assert.state(reps <= MAX_REP_COUNT, "derivedKeyBitLength is too large."); // Section 5.8.1.1, Process step #3: final byte[] counter = new byte[]{0, 0, 0, 1}; // same as 0x0001L, but no extra step to convert to byte[] // Section 5.8.1.1, Process step #4: long inputBitLength = bitLength(counter) + bitLength(Z) + bitLength(OtherInfo); - assert inputBitLength <= MAX_HASH_INPUT_BIT_LENGTH : "Hash input is too large."; + Assert.state(inputBitLength <= MAX_HASH_INPUT_BIT_LENGTH, "Hash input is too large."); byte[] derivedKeyBytes = new JcaTemplate(getJcaName(), null).execute(MessageDigest.class, new CheckedFunction() { @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java index f90cdcd8d..f3cba74dc 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java @@ -20,7 +20,7 @@ public class DefaultSecretKeyBuilder implements SecretKeyBuilder { public DefaultSecretKeyBuilder(String jcaName, int bitLength) { this.JCA_NAME = Assert.hasText(jcaName, "jcaName cannot be null or empty."); if (bitLength % Byte.SIZE != 0) { - String msg = "bitLength must be a multiple of 8"; + String msg = "bitLength must be an even multiple of 8"; throw new IllegalArgumentException(msg); } this.BIT_LENGTH = Assert.gt(bitLength, 0, "bitLength must be > 0"); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/WrappedSecretKey.java b/impl/src/main/java/io/jsonwebtoken/impl/security/WrappedSecretKey.java index a479f42c9..b7f8fac76 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/WrappedSecretKey.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/WrappedSecretKey.java @@ -1,7 +1,6 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Strings; import javax.crypto.SecretKey; @@ -15,7 +14,7 @@ public class WrappedSecretKey implements SecretKey { public WrappedSecretKey(SecretKey key, String algorithm) { this.key = Assert.notNull(key, "SecretKey cannot be null."); - this.algorithm = Strings.hasText(algorithm) ? algorithm : key.getAlgorithm(); + this.algorithm = Assert.hasText(algorithm, "Algorithm cannot be null or empty."); } @Override diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConcatKDFTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConcatKDFTest.groovy new file mode 100644 index 000000000..dd033f6c9 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConcatKDFTest.groovy @@ -0,0 +1,44 @@ +package io.jsonwebtoken.impl.security + +import org.junit.Before +import org.junit.Test + +import static org.junit.Assert.* + +class ConcatKDFTest { + + ConcatKDF CONCAT_KDF = EcdhKeyAlgorithm.CONCAT_KDF + + private byte[] Z + + @Before + void setUp() { + Z = new byte[16] + Randoms.secureRandom().nextBytes(Z) + } + + @Test + void testNonPositiveBitLength() { + try { + CONCAT_KDF.deriveKey(Z, 0, null) + fail() + } catch (IllegalArgumentException expected) { + String msg = 'derivedKeyBitLength must be a positive number.' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testDerivedKeyBitLengthBiggerThanJdkMax() { + byte[] Z = new byte[16] + long bitLength = Long.valueOf(Integer.MAX_VALUE) * 8L + 8L // one byte more than java byte arrays can handle + try { + CONCAT_KDF.deriveKey(Z, bitLength, null) + fail() + } catch (IllegalArgumentException expected) { + String msg = 'derivedKeyBitLength may not exceed 17179869176 bits (2147483647 bytes). ' + + 'Specified size: 17179869184 bits (2147483648 bytes).' + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilderTest.groovy new file mode 100644 index 000000000..b483ba77c --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilderTest.groovy @@ -0,0 +1,20 @@ +package io.jsonwebtoken.impl.security + +import org.junit.Test + +import static org.junit.Assert.* + +class DefaultSecretKeyBuilderTest { + + @Test + void testInvalidBitLength() { + try { + //noinspection GroovyResultOfObjectAllocationIgnored + new DefaultSecretKeyBuilder("AES", 127) + fail() + } catch (IllegalArgumentException expected) { + String msg = "bitLength must be an even multiple of 8" + assertEquals msg, expected.getMessage() + } + } +} From 285c2aeed0fd7eb9853d4975f2cfdf7568ef941a Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 19 Mar 2022 23:54:26 -0700 Subject: [PATCH 20/75] Added tests, cleaned up state assertions for code coverage --- api/src/main/java/io/jsonwebtoken/Header.java | 2 +- .../io/jsonwebtoken/JwtHandlerAdapter.java | 4 +- .../io/jsonwebtoken/JwtParserBuilder.java | 2 +- .../main/java/io/jsonwebtoken/Locator.java | 4 +- .../java/io/jsonwebtoken/LocatorAdapter.java | 16 +- .../jsonwebtoken/CompressionCodecsTest.groovy | 57 ------- .../jsonwebtoken/JwtHandlerAdapterTest.groovy | 22 +++ .../io/AndroidOrgJsonSerializerTest.groovy | 7 +- .../impl/CompressionCodecLocator.java | 4 +- .../jsonwebtoken/impl/DefaultJwtParser.java | 152 ++++++------------ .../impl/DefaultJwtParserBuilder.java | 68 +++----- .../io/jsonwebtoken/impl/lang/Conditions.java | 9 ++ .../impl/lang/LocatorFunction.java | 8 +- .../jsonwebtoken/impl/security/ConcatKDF.java | 28 ++-- .../impl/security/ConstantKeyLocator.java | 11 +- .../impl/security/DefaultPasswordKey.java | 2 - .../impl/security/JwtX509StringConverter.java | 10 +- .../impl/security/SecretJwkFactory.java | 14 +- .../jsonwebtoken/CompressionCodecsTest.groovy | 37 +++++ .../io/jsonwebtoken/LocatorAdapterTest.groovy | 69 ++++++++ .../impl/lang/LocatorFunctionTest.groovy | 8 +- .../impl/security/ConcatKDFTest.groovy | 61 ++++++- .../impl/security/CryptoAlgorithmTest.groovy | 53 ++++++ .../impl/security/EcdhKeyAlgorithmTest.groovy | 91 +++++++++++ .../impl/security/JwksTest.groovy | 46 ++++-- .../JwtX509StringConverterTest.groovy | 77 +++++++++ .../impl/security/ProvidersTest.groovy | 84 ++++++++++ .../security/ProvidersWithoutBCTest.groovy | 35 ++++ .../impl/security/TestSecretKey.groovy | 25 +++ .../jsonwebtoken/impl/security/PS256.crt.pem | 21 ++- .../jsonwebtoken/impl/security/PS256.key.pem | 29 +++- .../jsonwebtoken/impl/security/PS384.crt.pem | 26 ++- .../jsonwebtoken/impl/security/PS384.key.pem | 41 ++++- .../jsonwebtoken/impl/security/PS512.crt.pem | 32 +++- .../jsonwebtoken/impl/security/PS512.key.pem | 53 +++++- .../jsonwebtoken/impl/security/RS256.crt.pem | 21 ++- .../jsonwebtoken/impl/security/RS256.key.pem | 29 +++- .../jsonwebtoken/impl/security/RS384.crt.pem | 26 ++- .../jsonwebtoken/impl/security/RS384.key.pem | 41 ++++- .../jsonwebtoken/impl/security/RS512.crt.pem | 32 +++- .../jsonwebtoken/impl/security/RS512.key.pem | 53 +++++- 41 files changed, 1129 insertions(+), 281 deletions(-) delete mode 100644 api/src/test/groovy/io/jsonwebtoken/CompressionCodecsTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/CompressionCodecsTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/LocatorAdapterTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/JwtX509StringConverterTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersWithoutBCTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestSecretKey.groovy mode change 120000 => 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.crt.pem mode change 120000 => 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.key.pem mode change 120000 => 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.crt.pem mode change 120000 => 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.key.pem mode change 120000 => 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.crt.pem mode change 120000 => 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.key.pem mode change 120000 => 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.crt.pem mode change 120000 => 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.key.pem mode change 120000 => 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.crt.pem mode change 120000 => 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.key.pem mode change 120000 => 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.crt.pem mode change 120000 => 100644 impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.key.pem diff --git a/api/src/main/java/io/jsonwebtoken/Header.java b/api/src/main/java/io/jsonwebtoken/Header.java index 49bdccb22..844315d06 100644 --- a/api/src/main/java/io/jsonwebtoken/Header.java +++ b/api/src/main/java/io/jsonwebtoken/Header.java @@ -37,7 +37,7 @@ * * @since 0.1 */ -public interface Header> extends Map { +public interface Header extends Map { /** JWT {@code Type} (typ) value: "JWT" */ String JWT_TYPE = "JWT"; diff --git a/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java b/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java index c21485a80..273c21c22 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java @@ -52,11 +52,11 @@ public T onClaimsJws(Jws jws) { @Override public T onPlaintextJwe(Jwe jwe) { - throw new UnsupportedJwtException("Encrypted plaintext JWEs are not supported."); + throw new UnsupportedJwtException("Encrypted plaintext JWTs are not supported."); } @Override public T onClaimsJwe(Jwe jwe) { - throw new UnsupportedJwtException("Encrypted Claims JWEs are not supported."); + throw new UnsupportedJwtException("Encrypted Claims JWTs are not supported."); } } diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index a721fb881..d16c90156 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -317,7 +317,7 @@ public interface JwtParserBuilder extends Builder { * @return the parser builder for method chaining. * @since JJWT_RELEASE_VERSION */ - JwtParserBuilder setKeyLocator(Locator, Key> keyLocator); + JwtParserBuilder setKeyLocator(Locator keyLocator); /** *

    Deprecation Notice

    diff --git a/api/src/main/java/io/jsonwebtoken/Locator.java b/api/src/main/java/io/jsonwebtoken/Locator.java index 38a70b60f..93e947ec0 100644 --- a/api/src/main/java/io/jsonwebtoken/Locator.java +++ b/api/src/main/java/io/jsonwebtoken/Locator.java @@ -18,7 +18,7 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface Locator, R> { +public interface Locator { - R locate(H header); + T locate(Header header); } diff --git a/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java b/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java index 753d1966a..87a662cb1 100644 --- a/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java @@ -20,29 +20,31 @@ /** * @since JJWT_RELEASE_VERSION */ -public abstract class LocatorAdapter, R> implements Locator { +public class LocatorAdapter implements Locator { @Override - public final R locate(H header) { + public final T locate(Header header) { Assert.notNull(header, "Header cannot be null."); if (header instanceof JwsHeader) { - return locate((JwsHeader) header); + JwsHeader jwsHeader = (JwsHeader) header; + return locate(jwsHeader); } else if (header instanceof JweHeader) { - return locate((JweHeader) header); + JweHeader jweHeader = (JweHeader) header; + return locate(jweHeader); } else { return doLocate(header); } } - protected R locate(JweHeader header) { + protected T locate(JweHeader header) { return null; } - protected R locate(JwsHeader header) { + protected T locate(JwsHeader header) { return null; } - protected R doLocate(Header header) { + protected T doLocate(Header header) { return null; } } diff --git a/api/src/test/groovy/io/jsonwebtoken/CompressionCodecsTest.groovy b/api/src/test/groovy/io/jsonwebtoken/CompressionCodecsTest.groovy deleted file mode 100644 index 33411d359..000000000 --- a/api/src/test/groovy/io/jsonwebtoken/CompressionCodecsTest.groovy +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2014 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken - -import io.jsonwebtoken.lang.Classes -import org.junit.Test -import org.junit.runner.RunWith -import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.modules.junit4.PowerMockRunner - -import static org.easymock.EasyMock.createMock -import static org.easymock.EasyMock.eq -import static org.easymock.EasyMock.expect -import static org.junit.Assert.assertSame -import static org.powermock.api.easymock.PowerMock.mockStatic -import static org.powermock.api.easymock.PowerMock.replay -import static org.powermock.api.easymock.PowerMock.verify - -@RunWith(PowerMockRunner.class) -@PrepareForTest([Classes, CompressionCodecs]) -class CompressionCodecsTest { - - @Test - void testStatics() { - - mockStatic(Classes) - - def deflate = createMock(CompressionCodec) - def gzip = createMock(CompressionCodec) - - expect(Classes.newInstance(eq("io.jsonwebtoken.impl.compression.DeflateCompressionCodec"))).andReturn(deflate) - expect(Classes.newInstance(eq("io.jsonwebtoken.impl.compression.GzipCompressionCodec"))).andReturn(gzip) - - replay Classes, deflate, gzip - - assertSame deflate, CompressionCodecs.DEFLATE - assertSame gzip, CompressionCodecs.GZIP - - verify Classes, deflate, gzip - - //test coverage for private constructor: - new CompressionCodecs() - } -} diff --git a/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy b/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy index e09ae37c3..749885727 100644 --- a/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy @@ -65,4 +65,26 @@ class JwtHandlerAdapterTest { assertEquals e.getMessage(), 'Signed Claims JWTs are not supported.' } } + + @Test + void testOnPlaintextJwe() { + def handler = new JwtHandlerAdapter(); + try { + handler.onPlaintextJwe(null) + fail() + } catch (UnsupportedJwtException e) { + assertEquals e.getMessage(), 'Encrypted plaintext JWTs are not supported.' + } + } + + @Test + void testOnClaimsJwe() { + def handler = new JwtHandlerAdapter(); + try { + handler.onClaimsJwe(null) + fail() + } catch (UnsupportedJwtException e) { + assertEquals e.getMessage(), 'Encrypted Claims JWTs are not supported.' + } + } } diff --git a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/AndroidOrgJsonSerializerTest.groovy b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/AndroidOrgJsonSerializerTest.groovy index 2cca2efb2..d47af2327 100644 --- a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/AndroidOrgJsonSerializerTest.groovy +++ b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/AndroidOrgJsonSerializerTest.groovy @@ -16,7 +16,6 @@ package io.jsonwebtoken.orgjson.io import io.jsonwebtoken.lang.Classes -import io.jsonwebtoken.orgjson.io.OrgJsonSerializer import org.junit.Test import org.junit.runner.RunWith import org.powermock.core.classloader.annotations.PrepareForTest @@ -24,10 +23,8 @@ import org.powermock.modules.junit4.PowerMockRunner import static org.easymock.EasyMock.eq import static org.easymock.EasyMock.expect -import static org.junit.Assert.* -import static org.powermock.api.easymock.PowerMock.mockStatic -import static org.powermock.api.easymock.PowerMock.replay -import static org.powermock.api.easymock.PowerMock.verify +import static org.junit.Assert.assertFalse +import static org.powermock.api.easymock.PowerMock.* @RunWith(PowerMockRunner.class) @PrepareForTest([Classes]) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java b/impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java index a413593ff..a52a64e5b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java @@ -6,7 +6,7 @@ import io.jsonwebtoken.impl.lang.Function; import io.jsonwebtoken.lang.Assert; -public class CompressionCodecLocator> implements Function { +public class CompressionCodecLocator implements Function, CompressionCodec> { private final CompressionCodecResolver resolver; @@ -15,7 +15,7 @@ public CompressionCodecLocator(CompressionCodecResolver resolver) { } @Override - public CompressionCodec apply(H header) { + public CompressionCodec apply(Header header) { return resolver.resolveCompressionCodec(header); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index baa98ab0f..a393f5a1b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -15,64 +15,18 @@ */ package io.jsonwebtoken.impl; -import io.jsonwebtoken.ClaimJwtException; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Clock; -import io.jsonwebtoken.CompressionCodec; -import io.jsonwebtoken.CompressionCodecResolver; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.Identifiable; -import io.jsonwebtoken.IncorrectClaimException; -import io.jsonwebtoken.InvalidClaimException; -import io.jsonwebtoken.Jwe; -import io.jsonwebtoken.JweHeader; -import io.jsonwebtoken.Jws; -import io.jsonwebtoken.JwsHeader; -import io.jsonwebtoken.Jwt; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.JwtHandler; -import io.jsonwebtoken.JwtHandlerAdapter; -import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.MissingClaimException; -import io.jsonwebtoken.PrematureJwtException; -import io.jsonwebtoken.SigningKeyResolver; -import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.*; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.ConstantFunction; import io.jsonwebtoken.impl.lang.Function; import io.jsonwebtoken.impl.lang.LegacyServices; -import io.jsonwebtoken.impl.security.ConstantKeyLocator; -import io.jsonwebtoken.impl.security.DefaultAeadResult; -import io.jsonwebtoken.impl.security.DefaultDecryptionKeyRequest; -import io.jsonwebtoken.impl.security.DefaultVerifySignatureRequest; -import io.jsonwebtoken.impl.security.EncryptionAlgorithmsBridge; -import io.jsonwebtoken.impl.security.KeyAlgorithmsBridge; -import io.jsonwebtoken.impl.security.SignatureAlgorithmsBridge; -import io.jsonwebtoken.io.Decoder; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.io.DecodingException; -import io.jsonwebtoken.io.DeserializationException; -import io.jsonwebtoken.io.Deserializer; -import io.jsonwebtoken.lang.Arrays; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Collections; -import io.jsonwebtoken.lang.DateFormats; -import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.AeadAlgorithm; -import io.jsonwebtoken.security.DecryptAeadRequest; -import io.jsonwebtoken.security.DecryptionKeyRequest; -import io.jsonwebtoken.security.InvalidKeyException; -import io.jsonwebtoken.security.KeyAlgorithm; -import io.jsonwebtoken.security.Keys; -import io.jsonwebtoken.security.PayloadSupplier; +import io.jsonwebtoken.impl.security.*; +import io.jsonwebtoken.io.*; +import io.jsonwebtoken.lang.*; import io.jsonwebtoken.security.SignatureAlgorithm; -import io.jsonwebtoken.security.SignatureAlgorithms; import io.jsonwebtoken.security.SignatureException; -import io.jsonwebtoken.security.VerifySignatureRequest; -import io.jsonwebtoken.security.WeakKeyException; +import io.jsonwebtoken.security.*; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; @@ -90,35 +44,35 @@ public class DefaultJwtParser implements JwtParser { private static final JwtTokenizer jwtTokenizer = new JwtTokenizer(); public static final String MISSING_JWS_ALG_MSG = - "JWS header does not contain a required 'alg' (Algorithm) header parameter. " + - "This header parameter is mandatory per the JWS Specification, Section 4.1.1. See " + - "https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.1 for more information."; + "JWS header does not contain a required 'alg' (Algorithm) header parameter. " + + "This header parameter is mandatory per the JWS Specification, Section 4.1.1. See " + + "https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.1 for more information."; public static final String MISSING_JWE_ALG_MSG = - "JWE header does not contain a required 'alg' (Algorithm) header parameter. " + - "This header parameter is mandatory per the JWE Specification, Section 4.1.1. See " + - "https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.1 for more information."; + "JWE header does not contain a required 'alg' (Algorithm) header parameter. " + + "This header parameter is mandatory per the JWE Specification, Section 4.1.1. See " + + "https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.1 for more information."; private static final String MISSING_ENC_MSG = - "JWE header does not contain a required 'enc' (Encryption Algorithm) header parameter. " + - "This header parameter is mandatory per the JWE Specification, Section 4.1.2. See " + - "https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.2 for more information."; + "JWE header does not contain a required 'enc' (Encryption Algorithm) header parameter. " + + "This header parameter is mandatory per the JWE Specification, Section 4.1.2. See " + + "https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.2 for more information."; private static final String UNSECURED_DISABLED_MSG_PREFIX = "Unsecured JWSs (those with an " + - DefaultHeader.ALGORITHM + " header value of '" + SignatureAlgorithms.NONE.getId() + - "') are disallowed by default as mandated by " + - "https://datatracker.ietf.org/doc/html/rfc7518#section-3.6. If you wish to allow them to be " + - "parsed, call the JwtParserBuilder.enableUnsecuredJws() method (but please read the " + - "security considerations covered in that method's JavaDoc before doing so). Header: "; + DefaultHeader.ALGORITHM + " header value of '" + SignatureAlgorithms.NONE.getId() + + "') are disallowed by default as mandated by " + + "https://datatracker.ietf.org/doc/html/rfc7518#section-3.6. If you wish to allow them to be " + + "parsed, call the JwtParserBuilder.enableUnsecuredJws() method (but please read the " + + "security considerations covered in that method's JavaDoc before doing so). Header: "; private static final String JWE_NONE_MSG = - "JWEs do not support key management " + DefaultHeader.ALGORITHM + - " header value 'none' per https://datatracker.ietf.org/doc/html/rfc7518#section-4.1"; + "JWEs do not support key management " + DefaultHeader.ALGORITHM + + " header value 'none' per https://datatracker.ietf.org/doc/html/rfc7518#section-4.1"; private static final String JWS_NONE_SIG_MISMATCH_MSG = - "The JWS header references signature algorithm 'none' yet the " + - "compact JWS string contains a signature. This is not permitted per " + - "https://tools.ietf.org/html/rfc7518#section-3.6."; + "The JWS header references signature algorithm 'none' yet the " + + "compact JWS string contains a signature. This is not permitted per " + + "https://tools.ietf.org/html/rfc7518#section-3.6."; private static , R extends Identifiable> Function backup(String id, String msg, Collection extras) { if (Collections.isEmpty(extras)) { @@ -151,8 +105,7 @@ private static Function encFn(Collection compressionCodecLocator; + private Function, CompressionCodec> compressionCodecLocator; private final boolean enableUnsecuredJws; @@ -182,17 +135,18 @@ private static Function encFn(Collection constantKeyLocator = new ConstantKeyLocator<>(null, null); + ConstantKeyLocator constantKeyLocator = new ConstantKeyLocator(null, null); this.keyLocator = constantKeyLocator; this.signingKeyResolver = constantKeyLocator; this.signatureAlgorithmLocator = sigFn(Collections.>emptyList()); this.keyAlgorithmLocator = keyFn(Collections.>emptyList()); this.encryptionAlgorithmLocator = encFn(Collections.emptyList()); - this.compressionCodecLocator = new CompressionCodecLocator<>(new DefaultCompressionCodecResolver()); + this.compressionCodecLocator = new CompressionCodecLocator(new DefaultCompressionCodecResolver()); this.enableUnsecuredJws = false; } - @SuppressWarnings("deprecation") //SigningKeyResolver will be removed for 1.0 + @SuppressWarnings("deprecation") + //SigningKeyResolver will be removed for 1.0 DefaultJwtParser(Provider provider, SigningKeyResolver signingKeyResolver, boolean enableUnsecuredJws, @@ -218,7 +172,7 @@ public DefaultJwtParser() { this.signatureAlgorithmLocator = sigFn(extraSigAlgs); this.keyAlgorithmLocator = keyFn(extraKeyAlgs); this.encryptionAlgorithmLocator = encFn(extraEncAlgs); - this.compressionCodecLocator = new CompressionCodecLocator<>(compressionCodecResolver); + this.compressionCodecLocator = new CompressionCodecLocator(compressionCodecResolver); } @Override @@ -315,7 +269,7 @@ public JwtParser setSigningKey(String base64EncodedSecretKey) { @Override public JwtParser setSigningKey(final Key key) { Assert.notNull(key, "signing key cannot be null."); - setSigningKeyResolver(new ConstantKeyLocator<>(key, null)); + setSigningKeyResolver(new ConstantKeyLocator(key, null)); return this; } @@ -330,7 +284,7 @@ public JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver) { @Override public JwtParser setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver) { Assert.notNull(compressionCodecResolver, "compressionCodecResolver cannot be null."); - this.compressionCodecLocator = new CompressionCodecLocator<>(compressionCodecResolver); + this.compressionCodecLocator = new CompressionCodecLocator(compressionCodecResolver); return this; } @@ -413,7 +367,7 @@ private static boolean hasContentType(Header header) { String algType = tokenized instanceof TokenizedJwe ? "key management" : "signature"; String digestType = tokenized instanceof TokenizedJwe ? "AAD authentication tag" : "signature"; String msg = "The " + type + " header references " + algType + " algorithm '" + alg + "' but the " + - "compact " + type + " string is missing the required " + digestType + "."; + "compact " + type + " string is missing the required " + digestType + "."; throw new MalformedJwtException(msg); } } @@ -488,16 +442,16 @@ private static boolean hasContentType(Header header) { } DecryptionKeyRequest request = - new DefaultDecryptionKeyRequest<>(this.provider, null, key, jweHeader, encAlg, cekBytes); + new DefaultDecryptionKeyRequest<>(this.provider, null, key, jweHeader, encAlg, cekBytes); final SecretKey cek = keyAlg.getDecryptionKey(request); if (cek == null) { String msg = "The '" + keyAlg.getId() + "' JWE key algorithm did not return a decryption key. " + - "Unable to perform '" + encAlg.getId() + "' decryption."; + "Unable to perform '" + encAlg.getId() + "' decryption."; throw new IllegalStateException(msg); } DecryptAeadRequest decryptRequest = - new DefaultAeadResult(this.provider, null, bytes, cek, aad, tag, iv); + new DefaultAeadResult(this.provider, null, bytes, cek, aad, tag, iv); PayloadSupplier result = encAlg.decrypt(decryptRequest); bytes = result.getPayload(); } @@ -587,11 +541,11 @@ private static boolean hasContentType(Header header) { try { VerifySignatureRequest request = - new DefaultVerifySignatureRequest<>(this.provider, null, data, key, signature); + new DefaultVerifySignatureRequest<>(this.provider, null, data, key, signature); if (!algorithm.verify(request)) { String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + - "asserted and should not be trusted."; + "asserted and should not be trusted."; throw new SignatureException(msg); } } catch (WeakKeyException e) { @@ -599,12 +553,12 @@ private static boolean hasContentType(Header header) { } catch (InvalidKeyException | IllegalArgumentException e) { String algId = algorithm.getId(); String msg = "The parsed JWT indicates it was signed with the " + algId + " signature " + - "algorithm, but the specified verification key of type " + key.getClass().getName() + - " may not be used to validate " + algId + " signatures. Because the verification " + - "key reflects a specific and expected algorithm, and the JWT does not reflect " + - "this algorithm, it is likely that the JWT was not expected and therefore should not be " + - "trusted. Another possibility is that the parser was supplied with the incorrect " + - "verification key, but this cannot be assumed for security reasons."; + "algorithm, but the specified verification key of type " + key.getClass().getName() + + " may not be used to validate " + algId + " signatures. Because the verification " + + "key reflects a specific and expected algorithm, and the JWT does not reflect " + + "this algorithm, it is likely that the JWT was not expected and therefore should not be " + + "trusted. Another possibility is that the parser was supplied with the incorrect " + + "verification key, but this cannot be assumed for security reasons."; throw new UnsupportedJwtException(msg, e); } } @@ -631,8 +585,8 @@ private static boolean hasContentType(Header header) { long differenceMillis = maxTime - exp.getTime(); String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " + - differenceMillis + " milliseconds. Allowed clock skew: " + - this.allowedClockSkewMillis + " milliseconds."; + differenceMillis + " milliseconds. Allowed clock skew: " + + this.allowedClockSkewMillis + " milliseconds."; throw new ExpiredJwtException(header, claims, msg); } } @@ -651,9 +605,9 @@ private static boolean hasContentType(Header header) { long differenceMillis = nbf.getTime() - minTime; String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal + - ", a difference of " + - differenceMillis + " milliseconds. Allowed clock skew: " + - this.allowedClockSkewMillis + " milliseconds."; + ", a difference of " + + differenceMillis + " milliseconds. Allowed clock skew: " + + this.allowedClockSkewMillis + " milliseconds."; throw new PrematureJwtException(header, claims, msg); } } @@ -686,7 +640,7 @@ private void validateExpectedClaims(Header header, Claims claims) { actualClaimValue = claims.get(expectedClaimName, Date.class); } catch (Exception e) { String msg = "JWT Claim '" + expectedClaimName + "' was expected to be a Date, but its value " + - "cannot be converted to a Date using current heuristics. Value: " + actualClaimValue; + "cannot be converted to a Date using current heuristics. Value: " + actualClaimValue; throw new IncorrectClaimException(header, claims, msg); } } @@ -696,14 +650,14 @@ private void validateExpectedClaims(Header header, Claims claims) { if (actualClaimValue == null) { String msg = String.format(ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, - expectedClaimName, expectedClaimValue); + expectedClaimName, expectedClaimValue); invalidClaimException = new MissingClaimException(header, claims, msg); } else if (!expectedClaimValue.equals(actualClaimValue)) { String msg = String.format(ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, - expectedClaimName, expectedClaimValue, actualClaimValue); + expectedClaimName, expectedClaimValue, actualClaimValue); invalidClaimException = new IncorrectClaimException(header, claims, msg); } @@ -718,7 +672,7 @@ private void validateExpectedClaims(Header header, Claims claims) { @Override public T parse(String compact, JwtHandler handler) - throws ExpiredJwtException, MalformedJwtException, SignatureException { + throws ExpiredJwtException, MalformedJwtException, SignatureException { Assert.notNull(handler, "JwtHandler argument cannot be null."); Assert.hasText(compact, "JWT String argument cannot be null or empty."); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index eb2ce732b..01d072cf3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -15,16 +15,7 @@ */ package io.jsonwebtoken.impl; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Clock; -import io.jsonwebtoken.CompressionCodecResolver; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.JweHeader; -import io.jsonwebtoken.JwsHeader; -import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.JwtParserBuilder; -import io.jsonwebtoken.Locator; -import io.jsonwebtoken.SigningKeyResolver; +import io.jsonwebtoken.*; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; import io.jsonwebtoken.impl.lang.ConstantFunction; import io.jsonwebtoken.impl.lang.Function; @@ -62,17 +53,16 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { */ static final long MAX_CLOCK_SKEW_MILLIS = Long.MAX_VALUE / MILLISECONDS_PER_SECOND; static final String MAX_CLOCK_SKEW_ILLEGAL_MSG = "Illegal allowedClockSkewMillis value: multiplying this " + - "value by 1000 to obtain the number of milliseconds would cause a numeric overflow."; + "value by 1000 to obtain the number of milliseconds would cause a numeric overflow."; private Provider provider; private boolean enableUnsecuredJws = false; - @SuppressWarnings({"rawtypes"}) - private Function keyLocator = ConstantFunction.forNull(); + private Function, Key> keyLocator = ConstantFunction.forNull(); @SuppressWarnings("deprecation") //TODO: remove for 1.0 - private SigningKeyResolver signingKeyResolver = new ConstantKeyLocator<>(null , null); + private SigningKeyResolver signingKeyResolver = new ConstantKeyLocator(null, null); private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); @@ -201,7 +191,7 @@ public JwtParserBuilder setSigningKey(String base64EncodedSecretKey) { @Override public JwtParserBuilder setSigningKey(final Key key) { this.signatureVerificationKey = Assert.notNull(key, "signing key cannot be null."); - return setSigningKeyResolver(new ConstantKeyLocator<>(key, null)); + return setSigningKeyResolver(new ConstantKeyLocator(key, null)); } @Override @@ -239,15 +229,10 @@ public JwtParserBuilder setSigningKeyResolver(SigningKeyResolver signingKeyResol return this; } - @SuppressWarnings({"unchecked", "rawtypes"}) - private static Function coerce(Function f) { - return (Function) f; - } - @Override - public JwtParserBuilder setKeyLocator(Locator, Key> keyLocator) { + public JwtParserBuilder setKeyLocator(Locator keyLocator) { Assert.notNull(keyLocator, "Key locator cannot be null."); - this.keyLocator = coerce(new LocatorFunction<>(keyLocator)); + this.keyLocator = new LocatorFunction<>(keyLocator); return this; } @@ -258,7 +243,6 @@ public JwtParserBuilder setCompressionCodecResolver(CompressionCodecResolver com return this; } - @SuppressWarnings("rawtypes") @Override public JwtParser build() { @@ -270,20 +254,20 @@ public JwtParser build() { this.deserializer = Services.loadFirst(Deserializer.class); } - final Function existing1 = this.keyLocator; + final Function, Key> existing1 = this.keyLocator; if (this.signatureVerificationKey != null) { - this.keyLocator = new Function() { + this.keyLocator = new Function, Key>() { @Override - public Key apply(Header header) { + public Key apply(Header header) { return header instanceof JwsHeader ? signatureVerificationKey : existing1.apply(header); } }; } - final Function existing2 = this.keyLocator; + final Function, Key> existing2 = this.keyLocator; if (this.decryptionKey != null) { - this.keyLocator = new Function() { + this.keyLocator = new Function, Key>() { @Override - public Key apply(Header header) { + public Key apply(Header header) { return header instanceof JweHeader ? decryptionKey : existing2.apply(header); } }; @@ -296,19 +280,19 @@ public Key apply(Header header) { assert this.compressionCodecResolver != null : "CompressionCodecResolver should never be null."; return new ImmutableJwtParser(new DefaultJwtParser( - provider, - signingKeyResolver, - enableUnsecuredJws, - keyLocator, - clock, - allowedClockSkewMillis, - expectedClaims, - base64UrlDecoder, - new JwtDeserializer<>(deserializer), - compressionCodecResolver, - extraSignatureAlgorithms, - extraKeyAlgorithms, - extraEncryptionAlgorithms + provider, + signingKeyResolver, + enableUnsecuredJws, + keyLocator, + clock, + allowedClockSkewMillis, + expectedClaims, + base64UrlDecoder, + new JwtDeserializer<>(deserializer), + compressionCodecResolver, + extraSignatureAlgorithms, + extraKeyAlgorithms, + extraEncryptionAlgorithms )); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Conditions.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Conditions.java index 924dc4dd7..370f54aaf 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Conditions.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Conditions.java @@ -10,6 +10,8 @@ public final class Conditions { private Conditions() { } + public static final Condition TRUE = new TrueCondition(); + public static Condition not(Condition c) { return new NotCondition(c); } @@ -36,6 +38,13 @@ public boolean test() { } } + private static final class TrueCondition implements Condition { + @Override + public boolean test() { + return true; + } + } + private static final class ExistsCondition implements Condition { private final CheckedSupplier supplier; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/LocatorFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/LocatorFunction.java index 98da49d46..3ba613f0f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/LocatorFunction.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/LocatorFunction.java @@ -4,16 +4,16 @@ import io.jsonwebtoken.Locator; import io.jsonwebtoken.lang.Assert; -public class LocatorFunction, R> implements Function { +public class LocatorFunction implements Function, T> { - private final Locator locator; + private final Locator locator; - public LocatorFunction(Locator locator) { + public LocatorFunction(Locator locator) { this.locator = Assert.notNull(locator, "Locator instance cannot be null."); } @Override - public R apply(H h) { + public T apply(Header h) { return this.locator.locate(h); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java index 42b76ce8c..e1c40967f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java @@ -1,7 +1,6 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.CheckedFunction; -import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.SecurityException; import io.jsonwebtoken.security.UnsupportedKeyException; @@ -58,24 +57,21 @@ public Integer apply(MessageDigest instance) { * @param Z shared secret key to use to seed the derived secret. Cannot be null or empty. * @param derivedKeyBitLength the total number of bits (not bytes) required in the returned derived * key. - * @param OtherInfo any additional party info to be associated with the derived key. May be null/empty. + * @param otherInfo any additional party info to be associated with the derived key. May be null/empty. * @return the derived key * @throws UnsupportedKeyException if unable to obtain {@code sharedSecretKey}'s * {@link Key#getEncoded() encoded byte array}. * @throws SecurityException if unable to perform the necessary {@link MessageDigest} computations to * generate the derived key. */ - public SecretKey deriveKey(final byte[] Z, final long derivedKeyBitLength, final byte[] OtherInfo) + public SecretKey deriveKey(final byte[] Z, final long derivedKeyBitLength, final byte[] otherInfo) throws UnsupportedKeyException, SecurityException { - // OtherInfo argument assertions: - final int otherInfoByteLength = Arrays.length(OtherInfo); - // sharedSecretKey argument assertions: Assert.notEmpty(Z, "Z cannot be null or empty."); // derivedKeyBitLength argument assertions: - Assert.isTrue(derivedKeyBitLength > 0, "derivedKeyBitLength must be a positive number."); + Assert.isTrue(derivedKeyBitLength > 0, "derivedKeyBitLength must be a positive integer."); if (derivedKeyBitLength > MAX_DERIVED_KEY_BIT_LENGTH) { String msg = "derivedKeyBitLength may not exceed " + bitsMsg(MAX_DERIVED_KEY_BIT_LENGTH) + ". Specified size: " + bitsMsg(derivedKeyBitLength) + "."; @@ -83,9 +79,14 @@ public SecretKey deriveKey(final byte[] Z, final long derivedKeyBitLength, final } final long derivedKeyByteLength = derivedKeyBitLength / Byte.SIZE; + final byte[] OtherInfo = otherInfo == null ? EMPTY : otherInfo; + // Section 5.8.1.1, Process step #1: final double repsd = derivedKeyBitLength / (double) this.hashBitLength; final long reps = (long) (Math.ceil(repsd)); + // If repsd didn't result in a whole number, the last derived key byte will be partially filled per + // Section 5.8.1.1, Process step #6: + final boolean kLastPartial = repsd != (double) reps; // Section 5.8.1.1, Process step #2: Assert.state(reps <= MAX_REP_COUNT, "derivedKeyBitLength is too large."); @@ -102,24 +103,23 @@ public SecretKey deriveKey(final byte[] Z, final long derivedKeyBitLength, final public byte[] apply(MessageDigest md) throws Exception { final ByteArrayOutputStream stream = new ByteArrayOutputStream((int) derivedKeyByteLength); - long kLastIndex = reps - 1; - // Section 5.8.1.1, Process step #5: - for (long i = 0; i < reps; i++) { + // Section 5.8.1.1, Process step #5. We depart from Java idioms here by starting iteration index at 1 + // (instead of 0) and continue to <= reps (instead of < reps) to match the NIST publication algorithm + // notation convention (so variables like Ki and kLast below match the NIST definitions). + for (long i = 1; i <= reps; i++) { // Section 5.8.1.1, Process step #5.1: md.update(counter); md.update(Z); - if (otherInfoByteLength > 0) { - md.update(OtherInfo); - } + md.update(OtherInfo); byte[] Ki = md.digest(); // Section 5.8.1.1, Process step #5.2: increment(counter); // Section 5.8.1.1, Process step #6: - if (i == kLastIndex && repsd != (double) reps) { //repsd calculation above didn't result in a whole number: + if (i == reps && kLastPartial) { long leftmostBitLength = derivedKeyBitLength % hashBitLength; int leftmostByteLength = (int) (leftmostBitLength / Byte.SIZE); byte[] kLast = new byte[leftmostByteLength]; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ConstantKeyLocator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ConstantKeyLocator.java index ccc5fa183..cebbaa69e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/ConstantKeyLocator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ConstantKeyLocator.java @@ -1,17 +1,12 @@ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.JweHeader; -import io.jsonwebtoken.JwsHeader; -import io.jsonwebtoken.LocatorAdapter; -import io.jsonwebtoken.SigningKeyResolver; +import io.jsonwebtoken.*; import io.jsonwebtoken.impl.lang.Function; import java.security.Key; @SuppressWarnings("deprecation") -public class ConstantKeyLocator> extends LocatorAdapter implements SigningKeyResolver, Function { +public class ConstantKeyLocator extends LocatorAdapter implements SigningKeyResolver, Function, Key> { private final Key jwsKey; private final Key jweKey; @@ -42,7 +37,7 @@ protected Key locate(JweHeader header) { } @Override - public Key apply(H header) { + public Key apply(Header header) { return locate(header); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPasswordKey.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPasswordKey.java index 6a2a9eb65..a0e3b3cd1 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPasswordKey.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPasswordKey.java @@ -49,13 +49,11 @@ public byte[] getEncoded() { throw new UnsupportedOperationException(ENCODED_DISABLED_MSG); } - @Override public void destroy() { java.util.Arrays.fill(password, '\u0000'); this.destroyed = true; } - @Override public boolean isDestroyed() { return this.destroyed; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwtX509StringConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwtX509StringConverter.java index 71c65fb51..a13382982 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwtX509StringConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwtX509StringConverter.java @@ -9,6 +9,7 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; @@ -38,16 +39,21 @@ public String applyTo(X509Certificate cert) { return Encoders.BASE64.encode(der); } + //visible for testing + protected CertificateFactory newCertificateFactory() throws CertificateException { + return CertificateFactory.getInstance("X.509"); + } + @Override public X509Certificate applyFrom(String s) { Assert.hasText(s, "X.509 Certificate encoded string cannot be null or empty."); try { byte[] der = Decoders.BASE64.decode(s); //RFC requires Base64, not Base64Url - CertificateFactory cf = CertificateFactory.getInstance("X.509"); + CertificateFactory cf = newCertificateFactory(); InputStream stream = new ByteArrayInputStream(der); return (X509Certificate) cf.generateCertificate(stream); } catch (Exception e) { - String msg = "Unable to convert Base64 String '" + s + "' to X509Certificate instance: " + e.getMessage(); + String msg = "Unable to convert Base64 String '" + s + "' to X509Certificate instance. Cause: " + e.getMessage(); throw new IllegalArgumentException(msg, e); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java index 3993d94ee..129e9b4b6 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java @@ -15,13 +15,16 @@ */ class SecretJwkFactory extends AbstractFamilyJwkFactory { + private static final String ENCODED_UNAVAILABLE_MSG = "SecretKey argument does not have any encoded bytes, or " + + "the key's backing JCA Provider is preventing key.getEncoded() from returning any bytes. It is not " + + "possible to represent the SecretKey instance as a JWK."; + SecretJwkFactory() { super(DefaultSecretJwk.TYPE_VALUE, SecretKey.class); } - static byte[] getRequiredEncoded(SecretKey key, String reason) { + static byte[] getRequiredEncoded(SecretKey key) { Assert.notNull(key, "SecretKey argument cannot be null."); - Assert.hasText(reason, "Reason string argument cannot be null or empty."); byte[] encoded = null; Exception cause = null; try { @@ -31,10 +34,7 @@ static byte[] getRequiredEncoded(SecretKey key, String reason) { } if (Arrays.length(encoded) == 0) { - String msg = "SecretKey argument does not have any encoded bytes, or the key's backing JCA Provider " + - "is preventing key.getEncoded() from returning any bytes. In either case, it is not possible to " + - reason + "."; - throw new UnsupportedKeyException(msg, cause); + throw new IllegalArgumentException(ENCODED_UNAVAILABLE_MSG, cause); } return encoded; @@ -45,7 +45,7 @@ protected SecretJwk createJwkFromKey(JwkContext ctx) { SecretKey key = Assert.notNull(ctx.getKey(), "JwkContext key cannot be null."); String k; try { - byte[] encoded = getRequiredEncoded(key, "represent the SecretKey instance as a JWK"); + byte[] encoded = getRequiredEncoded(key); k = Encoders.BASE64URL.encode(encoded); Assert.hasText(k, "k value cannot be null or empty."); } catch (Exception e) { diff --git a/impl/src/test/groovy/io/jsonwebtoken/CompressionCodecsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/CompressionCodecsTest.groovy new file mode 100644 index 000000000..b2ff67038 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/CompressionCodecsTest.groovy @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken + +import io.jsonwebtoken.impl.compression.DeflateCompressionCodec +import io.jsonwebtoken.impl.compression.GzipCompressionCodec +import org.junit.Test + +import static org.junit.Assert.assertTrue + +class CompressionCodecsTest { + + @Test + void testCtor() { + //test coverage for private constructor: + new CompressionCodecs() + } + + @Test + void testStatics() { + assertTrue CompressionCodecs.DEFLATE instanceof DeflateCompressionCodec + assertTrue CompressionCodecs.GZIP instanceof GzipCompressionCodec + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/LocatorAdapterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/LocatorAdapterTest.groovy new file mode 100644 index 000000000..ca336d98d --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/LocatorAdapterTest.groovy @@ -0,0 +1,69 @@ +package io.jsonwebtoken + +import io.jsonwebtoken.impl.DefaultHeader +import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.impl.DefaultJwsHeader +import org.junit.Test + +import static org.junit.Assert.assertNull +import static org.junit.Assert.assertSame + +class LocatorAdapterTest { + + @Test + void testJwtHeader() { + Header input = new DefaultHeader() + def locator = new LocatorAdapter() { + @Override + protected Object doLocate(Header header) { + return header + } + } + assertSame input, locator.locate(input as Header) + } + + @Test + void testJwtHeaderWithoutOverride() { + Header input = new DefaultHeader() + Locator locator = new LocatorAdapter() + assertNull locator.locate(input as Header) + } + + @Test + void testJwsHeader() { + Header input = new DefaultJwsHeader() + Locator locator = new LocatorAdapter() { + @Override + protected Object locate(JwsHeader header) { + return header + } + } + assertSame input, locator.locate(input as Header /* force Groovy to avoid signature erasure */) + } + + @Test + void testJwsHeaderWithoutOverride() { + Header input = new DefaultJwsHeader() + Locator locator = new LocatorAdapter() + assertNull locator.locate(input as Header) + } + + @Test + void testJweHeader() { + JweHeader input = new DefaultJweHeader() + def locator = new LocatorAdapter() { + @Override + protected Object locate(JweHeader header) { + return header + } + } + assertSame input, locator.locate(input as Header) + } + + @Test + void testJweHeaderWithoutOverride() { + JweHeader input = new DefaultJweHeader() + def locator = new LocatorAdapter() + assertNull locator.locate(input as Header) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/LocatorFunctionTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/LocatorFunctionTest.groovy index e529828dd..214ef4a67 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/LocatorFunctionTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/LocatorFunctionTest.groovy @@ -17,13 +17,13 @@ class LocatorFunctionTest { assertEquals value, fn.apply(new DefaultJweHeader()) } - static class StaticLocator, R> implements Locator { - private final R o; - StaticLocator(R o) { + static class StaticLocator implements Locator { + private final T o; + StaticLocator(T o) { this.o = o; } @Override - R locate(H header) { + T locate(Header header) { return o; } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConcatKDFTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConcatKDFTest.groovy index dd033f6c9..c0f916bc6 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConcatKDFTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConcatKDFTest.groovy @@ -1,8 +1,13 @@ package io.jsonwebtoken.impl.security +import io.jsonwebtoken.impl.lang.Bytes import org.junit.Before import org.junit.Test +import javax.crypto.SecretKey +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + import static org.junit.Assert.* class ConcatKDFTest { @@ -17,13 +22,67 @@ class ConcatKDFTest { Randoms.secureRandom().nextBytes(Z) } + @Test + void testNullOtherInfo() { + final int derivedKeyBitLength = 256 + final byte[] OtherInfo = null + + // exactly 1 Concat KDF iteration - derived key bit length of 256 is same as SHA-256 digest length: + def md = MessageDigest.getInstance("SHA-256") + md.update([0, 0, 0, 1] as byte[]) + md.update(Z) + md.update(Bytes.EMPTY) // null OtherInfo should equate to a Bytes.EMPTY argument here + byte[] digest = md.digest() + + SecretKey key = CONCAT_KDF.deriveKey(Z, derivedKeyBitLength, OtherInfo) + byte[] derived = key.getEncoded() + assertNotNull(key) + assertArrayEquals(digest, derived) + } + + @Test + void testEmptyOtherInfo() { + final int derivedKeyBitLength = 256 + final byte[] OtherInfo = Bytes.EMPTY + + // exactly 1 Concat KDF iteration - derived key bit length of 256 is same as SHA-256 digest length: + def md = MessageDigest.getInstance("SHA-256") + md.update([0, 0, 0, 1] as byte[]) + md.update(Z) + md.update(Bytes.EMPTY) // empty OtherInfo should equate to a Bytes.EMPTY argument here + byte[] digest = md.digest() + + SecretKey key = CONCAT_KDF.deriveKey(Z, derivedKeyBitLength, OtherInfo) + byte[] derived = key.getEncoded() + assertNotNull(key) + assertArrayEquals(digest, derived) + } + + @Test + void testPopulatedOtherInfo() { + final int derivedKeyBitLength = 256 + final byte[] OtherInfo = 'whatever'.getBytes(StandardCharsets.UTF_8) + + // exactly 1 Concat KDF iteration - derived key bit length of 256 is same as SHA-256 digest length: + def md = MessageDigest.getInstance("SHA-256") + md.update([0, 0, 0, 1] as byte[]) + md.update(Z) + md.update(OtherInfo) // ensure OtherInfo is included in the digest + byte[] digest = md.digest() + + SecretKey key = CONCAT_KDF.deriveKey(Z, derivedKeyBitLength, OtherInfo) + byte[] derived = key.getEncoded() + assertNotNull(key) + assertArrayEquals(digest, derived) + } + @Test void testNonPositiveBitLength() { try { CONCAT_KDF.deriveKey(Z, 0, null) fail() } catch (IllegalArgumentException expected) { - String msg = 'derivedKeyBitLength must be a positive number.' + String msg = 'derivedKeyBitLength must be a positive integer.' assertEquals msg, expected.getMessage() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy index 8f10e5779..de976527f 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy @@ -1,6 +1,11 @@ package io.jsonwebtoken.impl.security +import io.jsonwebtoken.security.SecurityRequest import org.junit.Test + +import java.security.Provider + +import static org.easymock.EasyMock.* import static org.junit.Assert.* class CryptoAlgorithmTest { @@ -43,6 +48,54 @@ class CryptoAlgorithmTest { assertEquals hash, new TestCryptoAlgorithm('name', 'jcaName').hashCode() } + @Test + void testRequestProviderPriorityOverDefaultProvider() { + + def alg = new TestCryptoAlgorithm('test', 'test') + + Provider defaultProvider = createMock(Provider) + Provider requestProvider = createMock(Provider) + SecurityRequest request = createMock(SecurityRequest) + alg.setProvider(defaultProvider) + + expect(request.getProvider()).andReturn(requestProvider) + + replay request, requestProvider, defaultProvider + + assertSame requestProvider, alg.getProvider(request) // assert we get back the request provider, not the default + + verify request, requestProvider, defaultProvider + } + + @Test + void testMissingRequestProviderUsesDefaultProvider() { + + def alg = new TestCryptoAlgorithm('test', 'test') + + Provider defaultProvider = createMock(Provider) + SecurityRequest request = createMock(SecurityRequest) + alg.setProvider(defaultProvider) + + expect(request.getProvider()).andReturn(null) + + replay request, defaultProvider + + assertSame defaultProvider, alg.getProvider(request) // assert we get back the default provider + + verify request, defaultProvider + } + + @Test + void testMissingRequestAndDefaultProviderReturnsNull() { + def alg = new TestCryptoAlgorithm('test', 'test') + SecurityRequest request = createMock(SecurityRequest) + expect(request.getProvider()).andReturn(null) + replay request + assertNull alg.getProvider(request) // null return value means use JCA internal default provider + verify request + } + + class TestCryptoAlgorithm extends CryptoAlgorithm { TestCryptoAlgorithm(String id, String jcaName) { super(id, jcaName) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy new file mode 100644 index 000000000..d36636336 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy @@ -0,0 +1,91 @@ +package io.jsonwebtoken.impl.security + + +import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.security.DecryptionKeyRequest +import io.jsonwebtoken.security.EncryptionAlgorithms +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.Jwks +import org.junit.Test + +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +/** + * The {@link EcdhKeyAlgorithm} class is mostly tested already in RFC Appendix tests, so this class + * adds in tests for assertions/conditionals that aren't as easily tested elsewhere. + */ +class EcdhKeyAlgorithmTest { + + @Test + void testDecryptionWithoutEcPublicJwk() { + + def alg = new EcdhKeyAlgorithm() + ECPrivateKey decryptionKey = TestKeys.ES256.pair.private as ECPrivateKey + + def header = new DefaultJweHeader() + def jwk = Jwks.builder().setKey(TestKeys.HS256).build() //something other than an EC public key + header.put('epk', jwk) + + DecryptionKeyRequest req = new DefaultDecryptionKeyRequest(null, null, decryptionKey, header, EncryptionAlgorithms.A128GCM, 'test'.getBytes()) + + try { + alg.getDecryptionKey(req) + fail() + } catch (InvalidKeyException expected) { + assertEquals("JWE Header 'epk' (Ephemeral Public Key) value is not an EllipticCurve Public JWK as required.", expected.getMessage()) + } + } + + @Test + void testDecryptionWithEcPublicJwkWithInvalidPoint() { + + def alg = new EcdhKeyAlgorithm() + ECPrivateKey decryptionKey = TestKeys.ES256.pair.private as ECPrivateKey // Expected curve for this is P-256 + + def header = new DefaultJweHeader() + def pubJwk = Jwks.builder().setKey(TestKeys.ES256.pair.public as ECPublicKey).build() + def jwk = new LinkedHashMap(pubJwk) // copy fields so we can mutate + // We have a public JWK for a point on the curve, now swap out the x coordinate for something invalid: + jwk.put('x', 'Kg') + + // now set the epk header as the invalid/manipulated jwk: + header.put('epk', jwk) + + DecryptionKeyRequest req = new DefaultDecryptionKeyRequest(null, null, decryptionKey, + header, EncryptionAlgorithms.A128GCM, 'test'.getBytes()) + + try { + alg.getDecryptionKey(req) + fail() + } catch (InvalidKeyException expected) { + String msg = expected.getMessage() + String expectedMsg = String.format(EcPublicJwkFactory.JWK_CONTAINS_FORMAT_MSG, pubJwk.crv, jwk) + assertEquals(expectedMsg, msg) + } + } + + @Test + void testDecryptionWithEcPublicJwkOnInvalidCurve() { + + def alg = new EcdhKeyAlgorithm() + ECPrivateKey decryptionKey = TestKeys.ES256.pair.private as ECPrivateKey // Expected curve for this is P-256 + + def header = new DefaultJweHeader() + // This uses curve P-384 instead, does not match private key, so it's unexpected: + def jwk = Jwks.builder().setKey(TestKeys.ES384.pair.public as ECPublicKey).build() + header.put('epk', jwk) + + DecryptionKeyRequest req = new DefaultDecryptionKeyRequest(null, null, decryptionKey, header, EncryptionAlgorithms.A128GCM, 'test'.getBytes()) + + try { + alg.getDecryptionKey(req) + fail() + } catch (InvalidKeyException expected) { + assertEquals("JWE Header 'epk' (Ephemeral Public Key) value does not represent a point on the expected curve.", expected.getMessage()) + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index 6d1817b2b..c56437926 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -3,15 +3,7 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.impl.lang.Converters import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders -import io.jsonwebtoken.security.AsymmetricKeySignatureAlgorithm -import io.jsonwebtoken.security.EcPublicJwk -import io.jsonwebtoken.security.EllipticCurveSignatureAlgorithm -import io.jsonwebtoken.security.InvalidKeyException -import io.jsonwebtoken.security.Jwks -import io.jsonwebtoken.security.PrivateJwk -import io.jsonwebtoken.security.PublicJwk -import io.jsonwebtoken.security.SecretKeySignatureAlgorithm -import io.jsonwebtoken.security.SignatureAlgorithms +import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.SecretKey @@ -193,6 +185,42 @@ class JwksTest { } } + @Test + void testSecretKeyGetEncodedReturnsNull() { + SecretKey key = new TestSecretKey(algorithm: "AES") + try { + Jwks.builder().setKey(key).build() + fail() + } catch (UnsupportedKeyException expected) { + String expectedMsg = 'Unable to encode SecretKey to JWK: ' + SecretJwkFactory.ENCODED_UNAVAILABLE_MSG + assertEquals expectedMsg, expected.getMessage() + assertTrue expected.getCause() instanceof IllegalArgumentException + assertEquals SecretJwkFactory.ENCODED_UNAVAILABLE_MSG, expected.getCause().getMessage() + } + } + + @Test + void testSecretKeyGetEncodedThrowsException() { + String encodedMsg = "not allowed" + def encodedEx = new UnsupportedOperationException(encodedMsg) + SecretKey key = new TestSecretKey() { + @Override + byte[] getEncoded() { + throw encodedEx + } + } + try { + Jwks.builder().setKey(key).build() + fail() + } catch (UnsupportedKeyException expected) { + String expectedMsg = 'Unable to encode SecretKey to JWK: ' + SecretJwkFactory.ENCODED_UNAVAILABLE_MSG + assertEquals expectedMsg, expected.getMessage() + assertTrue expected.getCause() instanceof IllegalArgumentException + assertEquals SecretJwkFactory.ENCODED_UNAVAILABLE_MSG, expected.getCause().getMessage() + assertSame encodedEx, expected.getCause().getCause() + } + } + @Test void testAsymmetricJwks() { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwtX509StringConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwtX509StringConverterTest.groovy new file mode 100644 index 000000000..b29518209 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwtX509StringConverterTest.groovy @@ -0,0 +1,77 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.impl.lang.Bytes +import org.junit.Test + +import java.security.cert.CertificateEncodingException +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + +import static org.easymock.EasyMock.* +import static org.junit.Assert.* + +class JwtX509StringConverterTest { + + @Test + void testApplyToThrowsEncodingException() { + + def ex = new CertificateEncodingException("foo") + + X509Certificate cert = createMock(X509Certificate) + expect(cert.getEncoded()).andThrow(ex) + replay cert + + try { + JwtX509StringConverter.INSTANCE.applyTo(cert) + fail() + } catch (IllegalArgumentException expected) { + String expectedMsg = 'Unable to access X509Certificate encoded bytes necessary to perform DER ' + + 'Base64-encoding. Certificate: {EasyMock for class java.security.cert.X509Certificate}. ' + + 'Cause: ' + ex.getMessage() + assertSame ex, expected.getCause() + assertEquals expectedMsg, expected.getMessage() + } + + verify cert + } + + @Test + void testApplyToWithEmptyEncoding() { + + X509Certificate cert = createMock(X509Certificate) + expect(cert.getEncoded()).andReturn(Bytes.EMPTY) + replay cert + + try { + JwtX509StringConverter.INSTANCE.applyTo(cert) + fail() + } catch (IllegalArgumentException expected) { + String expectedMsg = 'X509Certificate encoded bytes cannot be null or empty. Certificate: ' + + '{EasyMock for class java.security.cert.X509Certificate}.' + assertEquals expectedMsg, expected.getMessage() + } + + verify cert + } + + @Test + void testApplyFromThrowsCertificateException() { + + def converter = new JwtX509StringConverter() { + @Override + protected CertificateFactory newCertificateFactory() throws CertificateException { + throw new CertificateException("nope") + } + } + + String s = 'foo' + try { + converter.applyFrom(s) + fail() + } catch (IllegalArgumentException expected) { + String expectedMsg = "Unable to convert Base64 String '$s' to X509Certificate instance. Cause: nope" + assertEquals expectedMsg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy new file mode 100644 index 000000000..5c4174f3b --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy @@ -0,0 +1,84 @@ +package io.jsonwebtoken.impl.security + + +import io.jsonwebtoken.impl.lang.Conditions +import io.jsonwebtoken.lang.Classes +import org.junit.After +import org.junit.Before +import org.junit.Test + +import java.security.Provider +import java.security.Security + +import static org.junit.Assert.* + +class ProvidersTest { + + @Before + void before() { + cleanup() // ensure we start clean + } + + @After + void after() { + cleanup() // ensure we end clean + } + + static void cleanup() { + //ensure test environment is cleaned up: + Providers.BC_PROVIDER.set(null) + Security.removeProvider("BC") + assertFalse bcRegistered() // ensure clean + } + + static boolean bcRegistered() { + for (Provider p : Security.getProviders()) { + // do not reference the Providers class constant here - this is a utility method that could be used in + // other test classes that use static mocks and the `Provider` class might not be able to initialized + if (p.getClass().getName().equals("org.bouncycastle.jce.provider.BouncyCastleProvider")) { + return true + } + } + return false + } + + @Test + void testPrivateCtor() { // for code coverage only + new Providers() + } + + @Test + void testBouncyCastleAlreadyExists() { + + // ensure we don't have one yet: + assertNull Providers.BC_PROVIDER.get() + assertFalse bcRegistered() + + //now register one in the JVM provider list: + Provider bc = Classes.newInstance(Providers.BC_PROVIDER_CLASS_NAME) + assertEquals "BC", bc.getName() + Security.addProvider(bc) + assertTrue bcRegistered() // ensure it exists in the system as expected + + //now ensure that we find it and cache it: + def returned = Providers.getBouncyCastle(Conditions.TRUE) + assertSame bc, returned + assertSame bc, Providers.BC_PROVIDER.get() // ensure cached for future lookup + + //cleanup() method will remove the provider from the system + } + + @Test + void testBouncyCastleCreatedIfAvailable() { + // ensure we don't have one yet: + assertNull Providers.BC_PROVIDER.get() + assertFalse bcRegistered() + + // ensure we can create one and cache it, *without* modifying the system JVM: + //now ensure that we find it and cache it: + def returned = Providers.getBouncyCastle(Conditions.TRUE) + assertNotNull returned + assertSame Providers.BC_PROVIDER.get(), returned //ensure cached for future lookup + assertFalse bcRegistered() //ensure we don't alter the system environment + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersWithoutBCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersWithoutBCTest.groovy new file mode 100644 index 000000000..b3e060069 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersWithoutBCTest.groovy @@ -0,0 +1,35 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.impl.lang.Conditions +import io.jsonwebtoken.lang.Classes +import org.junit.After +import org.junit.Test +import org.junit.runner.RunWith +import org.powermock.core.classloader.annotations.PrepareForTest +import org.powermock.modules.junit4.PowerMockRunner + +import static org.easymock.EasyMock.eq +import static org.easymock.EasyMock.expect +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertNull +import static org.powermock.api.easymock.PowerMock.* + +@RunWith(PowerMockRunner.class) +@PrepareForTest([Classes]) +class ProvidersWithoutBCTest { + + @After + void after() { + ProvidersTest.cleanup() //ensure environment is clean + } + + @Test + void testBouncyCastleClassNotAvailable() { + mockStatic(Classes) + expect(Classes.isAvailable(eq("org.bouncycastle.jce.provider.BouncyCastleProvider"))).andReturn(Boolean.FALSE).anyTimes() + replay Classes + assertNull Providers.getBouncyCastle(Conditions.TRUE) // one should not be created/exist + verify Classes + assertFalse ProvidersTest.bcRegistered() // nothing should be in the environment + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestSecretKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestSecretKey.groovy new file mode 100644 index 000000000..991d2b7ce --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestSecretKey.groovy @@ -0,0 +1,25 @@ +package io.jsonwebtoken.impl.security + +import javax.crypto.SecretKey + +class TestSecretKey implements SecretKey { + + private String algorithm + private String format + private byte[] encoded + + @Override + String getAlgorithm() { + return this.algorithm + } + + @Override + String getFormat() { + return this.format + } + + @Override + byte[] getEncoded() { + return this.encoded + } +} diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.crt.pem deleted file mode 120000 index 9f0f221ce..000000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.crt.pem +++ /dev/null @@ -1 +0,0 @@ -rsa2048.crt.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.crt.pem new file mode 100644 index 000000000..11cbfe76b --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.crt.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDRDCCAiwCCQCgd9OzR40NCDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY +MBYGA1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIwMDIw +MzIzMDQzM1oYDzMwMjAwMjExMjMwNDMzWjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwP +anNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAzkH0MwxQ2cUFWsvOPVFqI/dk2EFTjQolCy97mI5/wYCb +aOoZ9Rm7c675mAeemRtNzgNVEz7m298ENqNGqPk2Nv3pBJ/XCaybBlp61CLez7dQ +2h5jUFEJ6FJcjeKHS+MwXr56t2ISdfLNMYtVIxjvXQcYx5VmS4mIqTxj5gVGtQVi +0GXdH6SvpdKV0fjE9KOhjsdBfKQzZfcQlusHg8pThwvjpMwCZnkxCS0RKa9y4+5+ +7MkC33+8+neZUzS7b6NdFxh6T/pMXpkf8d81fzVo4ZBMloweW0/l8MOdVxeX7M/7 +XSC1ank5i3IEZcotLmJYMwEo7rMpZVLevEQ118Eo8QIDAQABMA0GCSqGSIb3DQEB +CwUAA4IBAQBGbfmJumXEHMLko1ioY/eY5EYgrBRJAuuAMGqBZmK+1Iy2CqB90aEh +ve+jXjIBsrvXRuLxMdlzoP58Ia9C5M+78Vq0bEjuGJu3zxGev11Gt4E3V6bWfT7G +fhg66dbmjnqkhgSzpDzfYR7HHOQiDAGe5IH5FbvWehRzENoAODHHP1z3NdoGhsl9 +4DIjOTGYdhW0yUTSjGTWygo6OPU2L4M2k0gTA06FkvdLIS450GWRpgoVO/vfcPnO +h8KwZcWVwJVmG0Hv0fNhQk/tRuhYhCWGxc7gxkbLb7/xPpPKMD6EvgG0BSm27NxO +H5l3KYwtbdj5nYHU73cLqC1D6ki6F8+h +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.key.pem deleted file mode 120000 index dbed23bf3..000000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.key.pem +++ /dev/null @@ -1 +0,0 @@ -rsa2048.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.key.pem new file mode 100644 index 000000000..418581f2d --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDOQfQzDFDZxQVa +y849UWoj92TYQVONCiULL3uYjn/BgJto6hn1GbtzrvmYB56ZG03OA1UTPubb3wQ2 +o0ao+TY2/ekEn9cJrJsGWnrUIt7Pt1DaHmNQUQnoUlyN4odL4zBevnq3YhJ18s0x +i1UjGO9dBxjHlWZLiYipPGPmBUa1BWLQZd0fpK+l0pXR+MT0o6GOx0F8pDNl9xCW +6weDylOHC+OkzAJmeTEJLREpr3Lj7n7syQLff7z6d5lTNLtvo10XGHpP+kxemR/x +3zV/NWjhkEyWjB5bT+Xww51XF5fsz/tdILVqeTmLcgRlyi0uYlgzASjusyllUt68 +RDXXwSjxAgMBAAECggEAZ90ahaJMDH2ERsaeoo4e7uGjrKqo0jsrkEhm6tnHR7/l +gp1wWNaOaKDSG1aq7NqtAXL4Imroggv56TGrYWetf1+5OZTsCnkaz8Y8WBr/LIZZ +dp0a0dUdMhpXdTN/gh1zvCIbVcFTHoYYAjzxsGzcDHKIbeizzJIDeYVpoOlDQ9/9 +Bv6ft4mhaG5SHVnec9QdmbJnKDq5rI4aPXCCXOCzDjdTVfgntdH5TvoCH91ESSKw +kddciAbVsXoOWnBx3jKMj+hIA4F1p6nzZUbiVzmxhqfShQhDnCEvq8tF7KqRbUsS +Gx8MVtwSkEGaiJCDVjwSRGkghXlguNwZcfnWMtGMYQKBgQDmFWAApeXv4xXF2a/1 +HKumO5Z+w+XkKiM76YyTHTKO/KtDYRJiIlJMgx+hoRTBwlpYDrlbS9+Jnm7bZ9Ib +pxRyMAFRoV7eIhnoAn9KrxhS8xCYF2Km7U1lg/+m3pFKghjV4+K1GHbggmvoiIY1 +2t250zkZSslwTxu/2+jRKYOptQKBgQDlfYrzvuGqClJ9QClxlOV2UrWiGxq6eTgL +4V3l0HwPU9OW/hX0Hued8S70Dpb3o+AAyptbcAqFjSdyIPMbCfKLQQkKpfBUtOvb +Nm12z/VNKNZbu7kvaOJHunQNHzyMEHcjsB9daAVI0gJZKN+m6Qh4VF4jao7G9GNR +d7ge0KcXzQKBgQCqf8p9kHJ9OsVmsTMgK1fTvrJ+S8LvOn6TpjVCy08tAHYVXzjV +OePMyRpGluyfzNtQB9E5o1cKTzqNIjljvoN7PrGrgS6g45pZAIi9mlUnGvIAEsxL +MOy6vn9Tc/kswo2O6umUE4X8RwmZ7pmuDPtj+e+FG5N8w1Kn8VlsrhvgRQKBgAgz +clG/koTnFYeQUWrTrVeLIR6H5W6gglY6WYaq6qQJlNgigFpW+GP2iH0EQHTdEFY2 +51JfMKERKEW107o1ostDKbWNtIbyaDNPQJ4sVFHLkc15aea90shJa3hEk39V30wR +MS2/V+EAUEErasKmNT1Hlo2hczS86wewRY4kWrRJAoGAeYUG04cu55GwCgp50P3J +0NCNyiOkhnaj0wGPztMbDqNkaUAoaycoEsas5lhRAWT4YIVglz5pwR4uiI57w1cL +Mvjk5yDiQs7h3bV/qtm95YPBBC+y3mmZYlEA1lH0qktRNBlMVtfYkPztBh50UBOH +8qhIwqrpm3+JJ1p2p0XPl1c= +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.crt.pem deleted file mode 120000 index 2b21f33d2..000000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.crt.pem +++ /dev/null @@ -1 +0,0 @@ -rsa3072.crt.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.crt.pem new file mode 100644 index 000000000..a93fa8338 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.crt.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIERDCCAqwCCQDqxucO41yAmTANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY +MBYGA1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIwMDIw +MzIzMDQ1OFoYDzMwMjAwMjExMjMwNDU4WjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwP +anNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MIIBojANBgkqhkiG9w0BAQEF +AAOCAY8AMIIBigKCAYEA0DmQS4Xtgu5xtnQdxkuzB1j+W4OvNEOVOsg3Zcn9W1d5 +NowtngUh9K9vwBpl59M2j5PHj9dseEIuRqr++ZnZhBMlh/lLiIQ0oQ3cEa7wrwJO +i9ycZZbeNDHVNzAdQZEQR1DIMUhSTEMR96pVD4a9DzBKLQJaKxZRUOrMhf3QhAZ5 +9m0d/Kqu1Dm6YeWMLQQEewAaSQ9g22gty1EcLAvp/vnhR/DJSYIHCayd5mZwvk9q +WkXySfUmJGP70GFGG4GOnVLLkmCQgfT+OrzTtiIzNT26mtAsoUMnoD42TTBkR0aL +hULcj1MYUcB9uVCmJTvYuiCTODoNlL8T40r9L99HoHlTWVi9vf6I1vyNdHp3dXKB +MVL13+i5BeNIjADr0KKs0jtEEicWyuVhJz3rPLzBbPhz/DGQ/hTj/DRSdo9L9YA4 +WkqgD8uFUvIEKAJ/hXYx3QPEiIMU7hT4jk2Z2SIBiKKiNW4E/20EZOFmaeNWQaqq +mwX0cHtMygJtTYnEJ1IDAgMBAAEwDQYJKoZIhvcNAQELBQADggGBAGKkmv6d372z +Ujt1qjsjH7LHfIsPXdvnp7OhvujDEtY7dzwDCtR40zgB2qp+iXUO61FXErx8yDp9 +l7sDzk0AjY7RupANuo/3FyDuo0WoTUV3CJNnXf3Mrvu/DMjbaS6D4Jryz/HLE+2r +GYtdm165FZK/hQXuFfurkc4yqjrX90Wr+YHeen2y5Wk3jeUknmdp97F6+zkq6N5D +dKjy/ZOvy+1huNd5bzvJoiZLKqdSh/RQUoU6AP1p+83lo+7cPvS/zm/HvwxwMamA +1Cip1FypNxUxt5HR4bC5LwEvMTZ/+UTEelbyfjMdYU97aa58nPoMxf7DRBbr0tfj +GItI+mMoAw60eIaDbTncXvO1LVrFF5BfzVOTQ8ioPRwI7A5LMSC5JvxW8KsW2VX0 +vGwRbw8I6HXGRbBZ3zwmAK73q7go31+Dl/5VPFo+fVTL0P7/k/g0ZAtCu4/Wly9e +DLnYMoZbIF5lgp9cAzPOaWXiInsA6HSdgFUfXsBemRpholuw+Sacxg== +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.key.pem deleted file mode 120000 index 8e6d44890..000000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.key.pem +++ /dev/null @@ -1 +0,0 @@ -rsa3072.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.key.pem new file mode 100644 index 000000000..1b9b8708b --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.key.pem @@ -0,0 +1,40 @@ +-----BEGIN PRIVATE KEY----- +MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQDQOZBLhe2C7nG2 +dB3GS7MHWP5bg680Q5U6yDdlyf1bV3k2jC2eBSH0r2/AGmXn0zaPk8eP12x4Qi5G +qv75mdmEEyWH+UuIhDShDdwRrvCvAk6L3Jxllt40MdU3MB1BkRBHUMgxSFJMQxH3 +qlUPhr0PMEotAlorFlFQ6syF/dCEBnn2bR38qq7UObph5YwtBAR7ABpJD2DbaC3L +URwsC+n++eFH8MlJggcJrJ3mZnC+T2paRfJJ9SYkY/vQYUYbgY6dUsuSYJCB9P46 +vNO2IjM1Pbqa0CyhQyegPjZNMGRHRouFQtyPUxhRwH25UKYlO9i6IJM4Og2UvxPj +Sv0v30egeVNZWL29/ojW/I10end1coExUvXf6LkF40iMAOvQoqzSO0QSJxbK5WEn +Pes8vMFs+HP8MZD+FOP8NFJ2j0v1gDhaSqAPy4VS8gQoAn+FdjHdA8SIgxTuFPiO +TZnZIgGIoqI1bgT/bQRk4WZp41ZBqqqbBfRwe0zKAm1NicQnUgMCAwEAAQKCAYAg +ewo+LasKBIXqbxyB5ScNG126CsWWwoARxk+V6jdCO1fmIWGwR56vW3p0HeoNio31 +QZkcn/8El1Y+ocfaSZx7lL0DA+k7Z1wKT24nuAFFW3fDK2ueETWiMK/QxwmZQ7al +WT2RKnXj/YZc+s3/+QWey+qWMMq98+JFXAsBT8FqBtSZkxXdZwaUhljDkpoWH41P +Xom7IdH7B7o0//cEC+u5YWM55J6Rf933LV0IJqypkxvE7ypHTR1hCdOrArF78u5z +Jg61hZRDi9t+X2RNZZ027ysrVLU/gre6XzSZI1a7NygDOSWBmcycQBAf6ZYJDdeb +mLy5M62K0fNavaxiuspA/WD3k4BsXSsK/rGNU6DvpeuymEbWFzPIoD5uKWTwHdSa +5ZrJGcR+Q5D12EersJi3jm52tYqYE91sJ8x+q6Ko+u7kWSbUCssqJLITdCqBdoEL +tpZspCzfCShJ+7CqlC1jEAIRdYFWFgIk76eLyr1k8aYI+NBqwfQbTzNK9Okj07kC +gcEA6BSD2iW2KEHyPi10BsqiWLKWCS6e5UjVBZEgD7+c6pYABxvXrMCKgseyd4LY +FBJ15MLNp3KS1vozlQEYp7LFAhpNeYMADql39ZNc7FQPcv+QsyQfDLP26eypabhN +BDexMcBY4jhZNkEBXjdxU9l0rGCQw82qLO5mK4WuKfyj+IX0iQv6BOzfBTKYNsgt +JAb289KeyrsV7rAoxxxfmsjYqsQadeCaOMQfkATAKyVaMrs6aJfJokuz5ibv+PRz +p6JdAoHBAOWvnzNNUF5BAanmk6BeiYK1tf9xAjJ5tAOAdqqflbDVBlZVE8qGYOH2 +J7x2/LQVz+Dm3chC5AdUL0tu5qZ+rAr8Vc7lJDvGkbcfTaEv4/VbF1gyDwi+dwn1 +MV3WQMEuFrqLDa1G3zxYER6PsO1DcwMTMRiWWlF6F77FnRm1zmleIkeXEOPW4a3J +Id2W043od/tbNr0j4sU/Ha3M+Eb91XjkSVulsABL+98CP/BnqWEFQABgQmfBsXMD +Kla06hw/3wKBwHm7iQ28CjhDnxUOMnX9g/qScjCOy7no4hPxc6fPEjfaRll0OUTc +GctPhEU71Ktyo3RC2iyi5HLu+m+GC7CrDLt1oH3EQRtvuQSPL4am8ROZCgVtRPwc +yb8Z7CMQERXNQJygD/9ZHzJeFqGc40zgG1rvq/+IuWKoCd96V0iexENvwDzCk3pR +5QmM6FqT1Vm4bYCnUbN1PqPcswb90wgVodCw3FBIZ5yvAv8//qyjAxTpMFH8jD8d +BlgKxIUJdEDR4QKBwGZapfJBsN/fzjL9aqobluHlwg3sOVNvArZQyBDu/tEHjURp +s2EcEw5/GGQXDjPeSH3rw8ebb2yIqm7OJADsEBTxL/f8CvKMYaEeVQTQh6BuEHAg +Fq0J25hXaMFtWfv8YuqMTvL50z9b630YAXsqBJXJNqbDUcpfQzejbofnie1QoqwO +eNtfhcBhEjNiJDJn9xfPJQySclr97mbmIXnZYgj2im5J3q2zLrHJmd6zAzsWENha +DR2Zpk8fiP2Mr4sZNwKBwQDX2Y/Ycr/IQtNdP/YsFIDeNHJ+dBLDM4JJCetlbzsY +poIKM9+ZvC9EST0KoEumhUT4Fy75b75nbzRGRDmFDNyOxRcHixFVnbgZqWyAeCbw +xNCKrIbtrXk5JvFy5y0yjMdBeB2uB1KJZhesuwUS1JnhA8RDapQ7ZwpoleRd+iqg +3RJTtcvo5Ky6vdz74isxBL8WH+PMqQEm1el8Jwix5dHx5mKH5QM2XnkUm78V/NX9 +5I2wbxUhb3FO7gj9pxJbwX4= +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.crt.pem deleted file mode 120000 index aad9991b2..000000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.crt.pem +++ /dev/null @@ -1 +0,0 @@ -rsa4096.crt.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.crt.pem new file mode 100644 index 000000000..c63b1941d --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.crt.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFRDCCAywCCQC4g2isVGolKjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY +MBYGA1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIwMDIw +MzIzMDY1MloYDzMwMjAwMjExMjMwNjUyWjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwP +anNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAw7jTXeRwCRrDdYnrIwcLvSNfhpmJZ0ap1jzKVgUyCOYL +TaB9+naJRjHqx7B5wgx/ArRF2nluQ5tawZPMFw2I/iqXYrQEPTbq1XVugYC/C491 +bcXOTKx+DgEvnhNysm/KmzFsEcw78prB5sIAZSR+S5zZPuny4zww2UzE9TZ433RB +kyA+wVkd64bgXdkMrVc+gsRsOtvwPFbQ89zg8d/pNV0mDtjDsfiYw0pSAah11fJG +a+aRc46CqFu/6rHuN4uq6542LdtshPbHz29VKHxk8agtcx06+8F05Bg4LFm1rRhY +0g3KsT7s8XHMVdo9h2bIQuWOaFg3mehpH6ZYBV4ENo98V/jDaUPpBHsaUXw4fG/w +rnI+YwRjGlmp2QEr5VRfh0x8Addf6N64lmbQUpCPhJweJd54D3JvIpJr8HiG3GYW +eFsrmDzmhrozZHxE4P7UesW6lWwQzGfwYGXs7j6TEa2hZ8EB/t1jsYNjZ5UYY/Jb +KgFGSkMGje4Bi5Bv6kh4+pp3DT5QsG/AfLVlr5ineLDWkJ15uZjOxl12EOPXOCWV +iVqS6rayJfb95YJQ52rT4H83BsApHbzFj7q/CIaeJkUdv9GJ2SOADcXdgG7Xk7tH +qb1VIH4zRBo5mc0qN1cAwjopxHv2h3tGaTHKbptvfiLulH+AvvuWmLMEQo6ZDlsC +AwEAATANBgkqhkiG9w0BAQsFAAOCAgEApnHjMQwt5hm6UlEDvdWCYbh7ctkLbgwR +iBP1lvunm2oF0jGpipt8oDR/TT43usb6ieuU+ABksjxOROeoVZbK8bEpnzeo3nNE +41ERI3Byjp7tsja8QGG0uBk9QZ0+7MhJqhEDVAIbS0Lf4exkWiLZrW7ogAEFYKTN +DE6CxOcfR/kXj6ejuCnvN4xYqnw8G/OF/3tnHMfKnnnqtMmWdAKd3Y5S1EJZ5vtp +lZ3I9HA5Hx0sTH1ruCOIRzaC5En1c6zW1HjxmeAqLeG814gezlEhHzb4SCkabgQh +Bq15O8eQaW92f8xZoUQN25w7SNYszk9AdhroJz3+BOzG3+Y1EInLk5hDHT8oUNFz +e8EosJEwJDK3wq9YOhn8PUT/DacyNKONJVNly3fTBXoSR3oReW61p6T19z4AYsY9 +qMwSjIL2UcgAF8Kpsx2NdQrDfdveNMhul7AjIgz+e2DtRqCkZ6ypdhht2pmlpiXO +TiUG/1OBq2yTeJF9LjAUzsSNnsZ/F8pJbwSpr7VqDmTNGTfrh6x1ojHNFjJeTqK8 +MCTmQtJJTAbV4nuB+thFFWDx0IWvbG7ViYds9sdJNO4L3baXeAioJhHs5buBy3eb +ZWjLAwHpSCqNY3d6+ouGLwE1YVFsk8sV9UM+gl15VynKkunbYoKhiD82HGASNYtE +33eif1l5Nk0= +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.key.pem deleted file mode 120000 index 3ec40cc76..000000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.key.pem +++ /dev/null @@ -1 +0,0 @@ -rsa4096.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.key.pem new file mode 100644 index 000000000..6c3887052 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDDuNNd5HAJGsN1 +iesjBwu9I1+GmYlnRqnWPMpWBTII5gtNoH36dolGMerHsHnCDH8CtEXaeW5Dm1rB +k8wXDYj+KpditAQ9NurVdW6BgL8Lj3Vtxc5MrH4OAS+eE3Kyb8qbMWwRzDvymsHm +wgBlJH5LnNk+6fLjPDDZTMT1NnjfdEGTID7BWR3rhuBd2QytVz6CxGw62/A8VtDz +3ODx3+k1XSYO2MOx+JjDSlIBqHXV8kZr5pFzjoKoW7/qse43i6rrnjYt22yE9sfP +b1UofGTxqC1zHTr7wXTkGDgsWbWtGFjSDcqxPuzxccxV2j2HZshC5Y5oWDeZ6Gkf +plgFXgQ2j3xX+MNpQ+kEexpRfDh8b/Cucj5jBGMaWanZASvlVF+HTHwB11/o3riW +ZtBSkI+EnB4l3ngPcm8ikmvweIbcZhZ4WyuYPOaGujNkfETg/tR6xbqVbBDMZ/Bg +ZezuPpMRraFnwQH+3WOxg2NnlRhj8lsqAUZKQwaN7gGLkG/qSHj6mncNPlCwb8B8 +tWWvmKd4sNaQnXm5mM7GXXYQ49c4JZWJWpLqtrIl9v3lglDnatPgfzcGwCkdvMWP +ur8Ihp4mRR2/0YnZI4ANxd2AbteTu0epvVUgfjNEGjmZzSo3VwDCOinEe/aHe0Zp +Mcpum29+Iu6Uf4C++5aYswRCjpkOWwIDAQABAoICACPSgUUvGV5hOqMZsiLAGGLu +xX4iPebcJRukFrh1zPmZ+TmlBUnBRlDFtB4Ga9KbbOe2zQ42qXrQRWUmwvT5Mjiq +3Phg0GHP2l1lV+t2AAGCqVCFIsQf0haIGwoIrzZ/hYqwGgKL6fD2aETu/xmD+2Wl +eJGuShlTG/G5vlbPOIJVieb+wN2sjPBdyFUE8/AKBtPyVYjUVn0EusvXgohinhF5 +UgznmbHKOVONF8Nb7O1SoZcAJWEMFVfxKwguttYNxyPG2k28WnlfnaSW0PRPCD6+ +tErcb75CYz2YPTfI15qt2RvhEFcumDl8xZR1FEvjAQZVc6Ife1W9FviG/pdE5Oox +lzsdOtIVSsrkDgl+kPmQczTdl8qWndh1c5rnOWALX388I+CWEgNDY0cfD6W2Xg6i +IIYCZ3mm0ZBZVTh1qCTciPFs6eBLZ2r/N+/dT0tTYrtKPKE8FfUqes9eFI9yEMmp +XKRw7tZZ78olS8eii1xiPsTSwNOoCFclyRzIE/Wfml2oAWkRiuC9tQZwkw6mj55p +5g1kxz0OtG+KrVaFxronaB1LLuNKJ41vRvmxevD6LnvGm/PMMkbizXGm7VPpaT9G +ETfNnk0ZKGSVemEmr+zrV2cAlAX7ZR+9ULY8DwjBaKO2g7w0ONqBdzuAXbP2T9WA +Zhmc3YiIgx1IdvH286YxAoIBAQDj2kJLqD8MOhTTNLwYQAc2WA8C1IxJWObShPNx +N2n7RQl7wL7gdphNQ4jbkbGEKqu4eJUlBBHPNUpTcaaYXRD0mNcGRXloXVSaVhLU +vXt6/hEiSk9TX6gGT8XGmQ0xfDZfT+yfrO+z6cABfMh1da8xKDYxg8lAok79jJRr +OWwzKsMj94LUOP7Z8feh4rjvrR0nHoSC4Ds8mrXOZTt42xMVunPxgHEf39Hx9Ikx +qiKvOZHdqRdru11xcD5AW6nvwgYKcTfvcKDFYXwfi0WMFvecSY9nK8Qg5ZMba0wI +pOlccoyat1EOy8aDCYr1OSmzhoQrCJGVTqyHDnce53FZQyQDAoIBAQDb5nM7Df11 +4JMDM0zbU90nDf+Qm6BXiHtLpADiTwf6NFLs26+u026Lo/60LxjHKif1UJPidma4 +b37Yeuwvlg2BHg+A1guYSv75k/bGIViGP3zDAWQRJ2/LonxBNcEGQToP7bBd5UM4 +dKCeWgU9PMF7qa1/xs5rrEAsqGNYrKu45Ng0YOm61NKL4zf5rUfEx/gX7v9NW/er +q9p0Ms5k1UK/AXaeMGhzT75vhoiMObMv2BKM/IAR0Wrm/xYCvnuey7hva+NRGOJ/ +gm9KnUxteafEqllA/VHgYpqZFEXwoR4Ty4ByBXDYTcLG5m7LynAfo5NUIHI3HB+v +uKV7hX3egJjJAoIBAQC3PVKth4vUqG0RAcr28Z8bPCwuSYLchctzqAojlb38niOn +S3X2DEolcNeCRSPut2ZMP2UqVKCB9Ehm3PJufAHjw3rBh2PA47XjPK9+OTgxzFs5 +KWusEDSPht31/iYXEt6jPiJ8s1Y+aRDJ4XFQzSjsLnuOzH4wJZfC3qiJpq92YsB2 +j1m+lGuYGLjejvfNgHn+eNN2cSASeBUX/F+crQonIkCWCoZvbM9pdxBSSZIFOxYs +ngzAzfiy/uKBXXZH49B5211xiTEyK1joAVgX9myBWsMh5JehIR9yIJMQLJejile7 +IQvmC0kFHsqKtcLsppRqC0URPykOoDp6NwT4FT/DAoIBAQDXmhtgy1a3PHjnqmSw +pokuwYrRPcT4DdjVUPeM6+/mYWbs1Hhr8OFyCFiyUXr5y1tiKp7Ua0JLkwXLOrpX +7cdP0SliKHs11lIoYeqSWB9zgMvSZoq2RvRVs/of9ZRLjahf9av2Y9KEh9TzbU+1 +utv5Y2O45DN/XmONZYwCZUn4/mb89Ag2JnRIs38uTbcQOQAGd03Zi1JJ/zUwuJ+k +PXQz0jt63fuLE6SjtEQtOGV3g2Ks2OS4k5s84N2z0w9holwy4pT97mgknL6BabiF +ncHgESVxku20EvmBHV91joLu5ZgKM0twyM0wNr5rERDd9IN++FEDt49ZurCFa1z9 +yxgBAoIBAC3HJzGb7Cufqw1JNng8H1mkJ5+1ZCNo7jy/aUYd5OacGTCNTcvuPTj+ +2iGvn4G0JR7pukhU5dVtGMQGpmmp8zk6/xzmyqeeiQNi4wdEgMALq4I0nynXkxDv +utKsXpmPiwyxmwCg9EY7AokfGWbxI5Yf7HkrjxME7jHz31lt5OF7AKyE1veFYWRa +puP1KVjNH7UAoE3WHnPnj7xvfQspXVRpzPWXH86XVonqnjQgu3SDkclPbkjg4HVj +athb6h5RN5bYx1cbUvo3JssBYl92FlXPU9lLzgv4nALUdVSi8PjbjQ7WXdxaKPdf +lczRTJNTE/KNUE0pkC5P4c/e0A1OFu0= +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.crt.pem deleted file mode 120000 index 9f0f221ce..000000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.crt.pem +++ /dev/null @@ -1 +0,0 @@ -rsa2048.crt.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.crt.pem new file mode 100644 index 000000000..11cbfe76b --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.crt.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDRDCCAiwCCQCgd9OzR40NCDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY +MBYGA1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIwMDIw +MzIzMDQzM1oYDzMwMjAwMjExMjMwNDMzWjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwP +anNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAzkH0MwxQ2cUFWsvOPVFqI/dk2EFTjQolCy97mI5/wYCb +aOoZ9Rm7c675mAeemRtNzgNVEz7m298ENqNGqPk2Nv3pBJ/XCaybBlp61CLez7dQ +2h5jUFEJ6FJcjeKHS+MwXr56t2ISdfLNMYtVIxjvXQcYx5VmS4mIqTxj5gVGtQVi +0GXdH6SvpdKV0fjE9KOhjsdBfKQzZfcQlusHg8pThwvjpMwCZnkxCS0RKa9y4+5+ +7MkC33+8+neZUzS7b6NdFxh6T/pMXpkf8d81fzVo4ZBMloweW0/l8MOdVxeX7M/7 +XSC1ank5i3IEZcotLmJYMwEo7rMpZVLevEQ118Eo8QIDAQABMA0GCSqGSIb3DQEB +CwUAA4IBAQBGbfmJumXEHMLko1ioY/eY5EYgrBRJAuuAMGqBZmK+1Iy2CqB90aEh +ve+jXjIBsrvXRuLxMdlzoP58Ia9C5M+78Vq0bEjuGJu3zxGev11Gt4E3V6bWfT7G +fhg66dbmjnqkhgSzpDzfYR7HHOQiDAGe5IH5FbvWehRzENoAODHHP1z3NdoGhsl9 +4DIjOTGYdhW0yUTSjGTWygo6OPU2L4M2k0gTA06FkvdLIS450GWRpgoVO/vfcPnO +h8KwZcWVwJVmG0Hv0fNhQk/tRuhYhCWGxc7gxkbLb7/xPpPKMD6EvgG0BSm27NxO +H5l3KYwtbdj5nYHU73cLqC1D6ki6F8+h +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.key.pem deleted file mode 120000 index dbed23bf3..000000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.key.pem +++ /dev/null @@ -1 +0,0 @@ -rsa2048.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.key.pem new file mode 100644 index 000000000..418581f2d --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDOQfQzDFDZxQVa +y849UWoj92TYQVONCiULL3uYjn/BgJto6hn1GbtzrvmYB56ZG03OA1UTPubb3wQ2 +o0ao+TY2/ekEn9cJrJsGWnrUIt7Pt1DaHmNQUQnoUlyN4odL4zBevnq3YhJ18s0x +i1UjGO9dBxjHlWZLiYipPGPmBUa1BWLQZd0fpK+l0pXR+MT0o6GOx0F8pDNl9xCW +6weDylOHC+OkzAJmeTEJLREpr3Lj7n7syQLff7z6d5lTNLtvo10XGHpP+kxemR/x +3zV/NWjhkEyWjB5bT+Xww51XF5fsz/tdILVqeTmLcgRlyi0uYlgzASjusyllUt68 +RDXXwSjxAgMBAAECggEAZ90ahaJMDH2ERsaeoo4e7uGjrKqo0jsrkEhm6tnHR7/l +gp1wWNaOaKDSG1aq7NqtAXL4Imroggv56TGrYWetf1+5OZTsCnkaz8Y8WBr/LIZZ +dp0a0dUdMhpXdTN/gh1zvCIbVcFTHoYYAjzxsGzcDHKIbeizzJIDeYVpoOlDQ9/9 +Bv6ft4mhaG5SHVnec9QdmbJnKDq5rI4aPXCCXOCzDjdTVfgntdH5TvoCH91ESSKw +kddciAbVsXoOWnBx3jKMj+hIA4F1p6nzZUbiVzmxhqfShQhDnCEvq8tF7KqRbUsS +Gx8MVtwSkEGaiJCDVjwSRGkghXlguNwZcfnWMtGMYQKBgQDmFWAApeXv4xXF2a/1 +HKumO5Z+w+XkKiM76YyTHTKO/KtDYRJiIlJMgx+hoRTBwlpYDrlbS9+Jnm7bZ9Ib +pxRyMAFRoV7eIhnoAn9KrxhS8xCYF2Km7U1lg/+m3pFKghjV4+K1GHbggmvoiIY1 +2t250zkZSslwTxu/2+jRKYOptQKBgQDlfYrzvuGqClJ9QClxlOV2UrWiGxq6eTgL +4V3l0HwPU9OW/hX0Hued8S70Dpb3o+AAyptbcAqFjSdyIPMbCfKLQQkKpfBUtOvb +Nm12z/VNKNZbu7kvaOJHunQNHzyMEHcjsB9daAVI0gJZKN+m6Qh4VF4jao7G9GNR +d7ge0KcXzQKBgQCqf8p9kHJ9OsVmsTMgK1fTvrJ+S8LvOn6TpjVCy08tAHYVXzjV +OePMyRpGluyfzNtQB9E5o1cKTzqNIjljvoN7PrGrgS6g45pZAIi9mlUnGvIAEsxL +MOy6vn9Tc/kswo2O6umUE4X8RwmZ7pmuDPtj+e+FG5N8w1Kn8VlsrhvgRQKBgAgz +clG/koTnFYeQUWrTrVeLIR6H5W6gglY6WYaq6qQJlNgigFpW+GP2iH0EQHTdEFY2 +51JfMKERKEW107o1ostDKbWNtIbyaDNPQJ4sVFHLkc15aea90shJa3hEk39V30wR +MS2/V+EAUEErasKmNT1Hlo2hczS86wewRY4kWrRJAoGAeYUG04cu55GwCgp50P3J +0NCNyiOkhnaj0wGPztMbDqNkaUAoaycoEsas5lhRAWT4YIVglz5pwR4uiI57w1cL +Mvjk5yDiQs7h3bV/qtm95YPBBC+y3mmZYlEA1lH0qktRNBlMVtfYkPztBh50UBOH +8qhIwqrpm3+JJ1p2p0XPl1c= +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.crt.pem deleted file mode 120000 index 2b21f33d2..000000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.crt.pem +++ /dev/null @@ -1 +0,0 @@ -rsa3072.crt.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.crt.pem new file mode 100644 index 000000000..a93fa8338 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.crt.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIERDCCAqwCCQDqxucO41yAmTANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY +MBYGA1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIwMDIw +MzIzMDQ1OFoYDzMwMjAwMjExMjMwNDU4WjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwP +anNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MIIBojANBgkqhkiG9w0BAQEF +AAOCAY8AMIIBigKCAYEA0DmQS4Xtgu5xtnQdxkuzB1j+W4OvNEOVOsg3Zcn9W1d5 +NowtngUh9K9vwBpl59M2j5PHj9dseEIuRqr++ZnZhBMlh/lLiIQ0oQ3cEa7wrwJO +i9ycZZbeNDHVNzAdQZEQR1DIMUhSTEMR96pVD4a9DzBKLQJaKxZRUOrMhf3QhAZ5 +9m0d/Kqu1Dm6YeWMLQQEewAaSQ9g22gty1EcLAvp/vnhR/DJSYIHCayd5mZwvk9q +WkXySfUmJGP70GFGG4GOnVLLkmCQgfT+OrzTtiIzNT26mtAsoUMnoD42TTBkR0aL +hULcj1MYUcB9uVCmJTvYuiCTODoNlL8T40r9L99HoHlTWVi9vf6I1vyNdHp3dXKB +MVL13+i5BeNIjADr0KKs0jtEEicWyuVhJz3rPLzBbPhz/DGQ/hTj/DRSdo9L9YA4 +WkqgD8uFUvIEKAJ/hXYx3QPEiIMU7hT4jk2Z2SIBiKKiNW4E/20EZOFmaeNWQaqq +mwX0cHtMygJtTYnEJ1IDAgMBAAEwDQYJKoZIhvcNAQELBQADggGBAGKkmv6d372z +Ujt1qjsjH7LHfIsPXdvnp7OhvujDEtY7dzwDCtR40zgB2qp+iXUO61FXErx8yDp9 +l7sDzk0AjY7RupANuo/3FyDuo0WoTUV3CJNnXf3Mrvu/DMjbaS6D4Jryz/HLE+2r +GYtdm165FZK/hQXuFfurkc4yqjrX90Wr+YHeen2y5Wk3jeUknmdp97F6+zkq6N5D +dKjy/ZOvy+1huNd5bzvJoiZLKqdSh/RQUoU6AP1p+83lo+7cPvS/zm/HvwxwMamA +1Cip1FypNxUxt5HR4bC5LwEvMTZ/+UTEelbyfjMdYU97aa58nPoMxf7DRBbr0tfj +GItI+mMoAw60eIaDbTncXvO1LVrFF5BfzVOTQ8ioPRwI7A5LMSC5JvxW8KsW2VX0 +vGwRbw8I6HXGRbBZ3zwmAK73q7go31+Dl/5VPFo+fVTL0P7/k/g0ZAtCu4/Wly9e +DLnYMoZbIF5lgp9cAzPOaWXiInsA6HSdgFUfXsBemRpholuw+Sacxg== +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.key.pem deleted file mode 120000 index 8e6d44890..000000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.key.pem +++ /dev/null @@ -1 +0,0 @@ -rsa3072.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.key.pem new file mode 100644 index 000000000..1b9b8708b --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.key.pem @@ -0,0 +1,40 @@ +-----BEGIN PRIVATE KEY----- +MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQDQOZBLhe2C7nG2 +dB3GS7MHWP5bg680Q5U6yDdlyf1bV3k2jC2eBSH0r2/AGmXn0zaPk8eP12x4Qi5G +qv75mdmEEyWH+UuIhDShDdwRrvCvAk6L3Jxllt40MdU3MB1BkRBHUMgxSFJMQxH3 +qlUPhr0PMEotAlorFlFQ6syF/dCEBnn2bR38qq7UObph5YwtBAR7ABpJD2DbaC3L +URwsC+n++eFH8MlJggcJrJ3mZnC+T2paRfJJ9SYkY/vQYUYbgY6dUsuSYJCB9P46 +vNO2IjM1Pbqa0CyhQyegPjZNMGRHRouFQtyPUxhRwH25UKYlO9i6IJM4Og2UvxPj +Sv0v30egeVNZWL29/ojW/I10end1coExUvXf6LkF40iMAOvQoqzSO0QSJxbK5WEn +Pes8vMFs+HP8MZD+FOP8NFJ2j0v1gDhaSqAPy4VS8gQoAn+FdjHdA8SIgxTuFPiO +TZnZIgGIoqI1bgT/bQRk4WZp41ZBqqqbBfRwe0zKAm1NicQnUgMCAwEAAQKCAYAg +ewo+LasKBIXqbxyB5ScNG126CsWWwoARxk+V6jdCO1fmIWGwR56vW3p0HeoNio31 +QZkcn/8El1Y+ocfaSZx7lL0DA+k7Z1wKT24nuAFFW3fDK2ueETWiMK/QxwmZQ7al +WT2RKnXj/YZc+s3/+QWey+qWMMq98+JFXAsBT8FqBtSZkxXdZwaUhljDkpoWH41P +Xom7IdH7B7o0//cEC+u5YWM55J6Rf933LV0IJqypkxvE7ypHTR1hCdOrArF78u5z +Jg61hZRDi9t+X2RNZZ027ysrVLU/gre6XzSZI1a7NygDOSWBmcycQBAf6ZYJDdeb +mLy5M62K0fNavaxiuspA/WD3k4BsXSsK/rGNU6DvpeuymEbWFzPIoD5uKWTwHdSa +5ZrJGcR+Q5D12EersJi3jm52tYqYE91sJ8x+q6Ko+u7kWSbUCssqJLITdCqBdoEL +tpZspCzfCShJ+7CqlC1jEAIRdYFWFgIk76eLyr1k8aYI+NBqwfQbTzNK9Okj07kC +gcEA6BSD2iW2KEHyPi10BsqiWLKWCS6e5UjVBZEgD7+c6pYABxvXrMCKgseyd4LY +FBJ15MLNp3KS1vozlQEYp7LFAhpNeYMADql39ZNc7FQPcv+QsyQfDLP26eypabhN +BDexMcBY4jhZNkEBXjdxU9l0rGCQw82qLO5mK4WuKfyj+IX0iQv6BOzfBTKYNsgt +JAb289KeyrsV7rAoxxxfmsjYqsQadeCaOMQfkATAKyVaMrs6aJfJokuz5ibv+PRz +p6JdAoHBAOWvnzNNUF5BAanmk6BeiYK1tf9xAjJ5tAOAdqqflbDVBlZVE8qGYOH2 +J7x2/LQVz+Dm3chC5AdUL0tu5qZ+rAr8Vc7lJDvGkbcfTaEv4/VbF1gyDwi+dwn1 +MV3WQMEuFrqLDa1G3zxYER6PsO1DcwMTMRiWWlF6F77FnRm1zmleIkeXEOPW4a3J +Id2W043od/tbNr0j4sU/Ha3M+Eb91XjkSVulsABL+98CP/BnqWEFQABgQmfBsXMD +Kla06hw/3wKBwHm7iQ28CjhDnxUOMnX9g/qScjCOy7no4hPxc6fPEjfaRll0OUTc +GctPhEU71Ktyo3RC2iyi5HLu+m+GC7CrDLt1oH3EQRtvuQSPL4am8ROZCgVtRPwc +yb8Z7CMQERXNQJygD/9ZHzJeFqGc40zgG1rvq/+IuWKoCd96V0iexENvwDzCk3pR +5QmM6FqT1Vm4bYCnUbN1PqPcswb90wgVodCw3FBIZ5yvAv8//qyjAxTpMFH8jD8d +BlgKxIUJdEDR4QKBwGZapfJBsN/fzjL9aqobluHlwg3sOVNvArZQyBDu/tEHjURp +s2EcEw5/GGQXDjPeSH3rw8ebb2yIqm7OJADsEBTxL/f8CvKMYaEeVQTQh6BuEHAg +Fq0J25hXaMFtWfv8YuqMTvL50z9b630YAXsqBJXJNqbDUcpfQzejbofnie1QoqwO +eNtfhcBhEjNiJDJn9xfPJQySclr97mbmIXnZYgj2im5J3q2zLrHJmd6zAzsWENha +DR2Zpk8fiP2Mr4sZNwKBwQDX2Y/Ycr/IQtNdP/YsFIDeNHJ+dBLDM4JJCetlbzsY +poIKM9+ZvC9EST0KoEumhUT4Fy75b75nbzRGRDmFDNyOxRcHixFVnbgZqWyAeCbw +xNCKrIbtrXk5JvFy5y0yjMdBeB2uB1KJZhesuwUS1JnhA8RDapQ7ZwpoleRd+iqg +3RJTtcvo5Ky6vdz74isxBL8WH+PMqQEm1el8Jwix5dHx5mKH5QM2XnkUm78V/NX9 +5I2wbxUhb3FO7gj9pxJbwX4= +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.crt.pem deleted file mode 120000 index aad9991b2..000000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.crt.pem +++ /dev/null @@ -1 +0,0 @@ -rsa4096.crt.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.crt.pem new file mode 100644 index 000000000..c63b1941d --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.crt.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFRDCCAywCCQC4g2isVGolKjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY +MBYGA1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIwMDIw +MzIzMDY1MloYDzMwMjAwMjExMjMwNjUyWjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwP +anNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAw7jTXeRwCRrDdYnrIwcLvSNfhpmJZ0ap1jzKVgUyCOYL +TaB9+naJRjHqx7B5wgx/ArRF2nluQ5tawZPMFw2I/iqXYrQEPTbq1XVugYC/C491 +bcXOTKx+DgEvnhNysm/KmzFsEcw78prB5sIAZSR+S5zZPuny4zww2UzE9TZ433RB +kyA+wVkd64bgXdkMrVc+gsRsOtvwPFbQ89zg8d/pNV0mDtjDsfiYw0pSAah11fJG +a+aRc46CqFu/6rHuN4uq6542LdtshPbHz29VKHxk8agtcx06+8F05Bg4LFm1rRhY +0g3KsT7s8XHMVdo9h2bIQuWOaFg3mehpH6ZYBV4ENo98V/jDaUPpBHsaUXw4fG/w +rnI+YwRjGlmp2QEr5VRfh0x8Addf6N64lmbQUpCPhJweJd54D3JvIpJr8HiG3GYW +eFsrmDzmhrozZHxE4P7UesW6lWwQzGfwYGXs7j6TEa2hZ8EB/t1jsYNjZ5UYY/Jb +KgFGSkMGje4Bi5Bv6kh4+pp3DT5QsG/AfLVlr5ineLDWkJ15uZjOxl12EOPXOCWV +iVqS6rayJfb95YJQ52rT4H83BsApHbzFj7q/CIaeJkUdv9GJ2SOADcXdgG7Xk7tH +qb1VIH4zRBo5mc0qN1cAwjopxHv2h3tGaTHKbptvfiLulH+AvvuWmLMEQo6ZDlsC +AwEAATANBgkqhkiG9w0BAQsFAAOCAgEApnHjMQwt5hm6UlEDvdWCYbh7ctkLbgwR +iBP1lvunm2oF0jGpipt8oDR/TT43usb6ieuU+ABksjxOROeoVZbK8bEpnzeo3nNE +41ERI3Byjp7tsja8QGG0uBk9QZ0+7MhJqhEDVAIbS0Lf4exkWiLZrW7ogAEFYKTN +DE6CxOcfR/kXj6ejuCnvN4xYqnw8G/OF/3tnHMfKnnnqtMmWdAKd3Y5S1EJZ5vtp +lZ3I9HA5Hx0sTH1ruCOIRzaC5En1c6zW1HjxmeAqLeG814gezlEhHzb4SCkabgQh +Bq15O8eQaW92f8xZoUQN25w7SNYszk9AdhroJz3+BOzG3+Y1EInLk5hDHT8oUNFz +e8EosJEwJDK3wq9YOhn8PUT/DacyNKONJVNly3fTBXoSR3oReW61p6T19z4AYsY9 +qMwSjIL2UcgAF8Kpsx2NdQrDfdveNMhul7AjIgz+e2DtRqCkZ6ypdhht2pmlpiXO +TiUG/1OBq2yTeJF9LjAUzsSNnsZ/F8pJbwSpr7VqDmTNGTfrh6x1ojHNFjJeTqK8 +MCTmQtJJTAbV4nuB+thFFWDx0IWvbG7ViYds9sdJNO4L3baXeAioJhHs5buBy3eb +ZWjLAwHpSCqNY3d6+ouGLwE1YVFsk8sV9UM+gl15VynKkunbYoKhiD82HGASNYtE +33eif1l5Nk0= +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.key.pem deleted file mode 120000 index 3ec40cc76..000000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.key.pem +++ /dev/null @@ -1 +0,0 @@ -rsa4096.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.key.pem new file mode 100644 index 000000000..6c3887052 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDDuNNd5HAJGsN1 +iesjBwu9I1+GmYlnRqnWPMpWBTII5gtNoH36dolGMerHsHnCDH8CtEXaeW5Dm1rB +k8wXDYj+KpditAQ9NurVdW6BgL8Lj3Vtxc5MrH4OAS+eE3Kyb8qbMWwRzDvymsHm +wgBlJH5LnNk+6fLjPDDZTMT1NnjfdEGTID7BWR3rhuBd2QytVz6CxGw62/A8VtDz +3ODx3+k1XSYO2MOx+JjDSlIBqHXV8kZr5pFzjoKoW7/qse43i6rrnjYt22yE9sfP +b1UofGTxqC1zHTr7wXTkGDgsWbWtGFjSDcqxPuzxccxV2j2HZshC5Y5oWDeZ6Gkf +plgFXgQ2j3xX+MNpQ+kEexpRfDh8b/Cucj5jBGMaWanZASvlVF+HTHwB11/o3riW +ZtBSkI+EnB4l3ngPcm8ikmvweIbcZhZ4WyuYPOaGujNkfETg/tR6xbqVbBDMZ/Bg +ZezuPpMRraFnwQH+3WOxg2NnlRhj8lsqAUZKQwaN7gGLkG/qSHj6mncNPlCwb8B8 +tWWvmKd4sNaQnXm5mM7GXXYQ49c4JZWJWpLqtrIl9v3lglDnatPgfzcGwCkdvMWP +ur8Ihp4mRR2/0YnZI4ANxd2AbteTu0epvVUgfjNEGjmZzSo3VwDCOinEe/aHe0Zp +Mcpum29+Iu6Uf4C++5aYswRCjpkOWwIDAQABAoICACPSgUUvGV5hOqMZsiLAGGLu +xX4iPebcJRukFrh1zPmZ+TmlBUnBRlDFtB4Ga9KbbOe2zQ42qXrQRWUmwvT5Mjiq +3Phg0GHP2l1lV+t2AAGCqVCFIsQf0haIGwoIrzZ/hYqwGgKL6fD2aETu/xmD+2Wl +eJGuShlTG/G5vlbPOIJVieb+wN2sjPBdyFUE8/AKBtPyVYjUVn0EusvXgohinhF5 +UgznmbHKOVONF8Nb7O1SoZcAJWEMFVfxKwguttYNxyPG2k28WnlfnaSW0PRPCD6+ +tErcb75CYz2YPTfI15qt2RvhEFcumDl8xZR1FEvjAQZVc6Ife1W9FviG/pdE5Oox +lzsdOtIVSsrkDgl+kPmQczTdl8qWndh1c5rnOWALX388I+CWEgNDY0cfD6W2Xg6i +IIYCZ3mm0ZBZVTh1qCTciPFs6eBLZ2r/N+/dT0tTYrtKPKE8FfUqes9eFI9yEMmp +XKRw7tZZ78olS8eii1xiPsTSwNOoCFclyRzIE/Wfml2oAWkRiuC9tQZwkw6mj55p +5g1kxz0OtG+KrVaFxronaB1LLuNKJ41vRvmxevD6LnvGm/PMMkbizXGm7VPpaT9G +ETfNnk0ZKGSVemEmr+zrV2cAlAX7ZR+9ULY8DwjBaKO2g7w0ONqBdzuAXbP2T9WA +Zhmc3YiIgx1IdvH286YxAoIBAQDj2kJLqD8MOhTTNLwYQAc2WA8C1IxJWObShPNx +N2n7RQl7wL7gdphNQ4jbkbGEKqu4eJUlBBHPNUpTcaaYXRD0mNcGRXloXVSaVhLU +vXt6/hEiSk9TX6gGT8XGmQ0xfDZfT+yfrO+z6cABfMh1da8xKDYxg8lAok79jJRr +OWwzKsMj94LUOP7Z8feh4rjvrR0nHoSC4Ds8mrXOZTt42xMVunPxgHEf39Hx9Ikx +qiKvOZHdqRdru11xcD5AW6nvwgYKcTfvcKDFYXwfi0WMFvecSY9nK8Qg5ZMba0wI +pOlccoyat1EOy8aDCYr1OSmzhoQrCJGVTqyHDnce53FZQyQDAoIBAQDb5nM7Df11 +4JMDM0zbU90nDf+Qm6BXiHtLpADiTwf6NFLs26+u026Lo/60LxjHKif1UJPidma4 +b37Yeuwvlg2BHg+A1guYSv75k/bGIViGP3zDAWQRJ2/LonxBNcEGQToP7bBd5UM4 +dKCeWgU9PMF7qa1/xs5rrEAsqGNYrKu45Ng0YOm61NKL4zf5rUfEx/gX7v9NW/er +q9p0Ms5k1UK/AXaeMGhzT75vhoiMObMv2BKM/IAR0Wrm/xYCvnuey7hva+NRGOJ/ +gm9KnUxteafEqllA/VHgYpqZFEXwoR4Ty4ByBXDYTcLG5m7LynAfo5NUIHI3HB+v +uKV7hX3egJjJAoIBAQC3PVKth4vUqG0RAcr28Z8bPCwuSYLchctzqAojlb38niOn +S3X2DEolcNeCRSPut2ZMP2UqVKCB9Ehm3PJufAHjw3rBh2PA47XjPK9+OTgxzFs5 +KWusEDSPht31/iYXEt6jPiJ8s1Y+aRDJ4XFQzSjsLnuOzH4wJZfC3qiJpq92YsB2 +j1m+lGuYGLjejvfNgHn+eNN2cSASeBUX/F+crQonIkCWCoZvbM9pdxBSSZIFOxYs +ngzAzfiy/uKBXXZH49B5211xiTEyK1joAVgX9myBWsMh5JehIR9yIJMQLJejile7 +IQvmC0kFHsqKtcLsppRqC0URPykOoDp6NwT4FT/DAoIBAQDXmhtgy1a3PHjnqmSw +pokuwYrRPcT4DdjVUPeM6+/mYWbs1Hhr8OFyCFiyUXr5y1tiKp7Ua0JLkwXLOrpX +7cdP0SliKHs11lIoYeqSWB9zgMvSZoq2RvRVs/of9ZRLjahf9av2Y9KEh9TzbU+1 +utv5Y2O45DN/XmONZYwCZUn4/mb89Ag2JnRIs38uTbcQOQAGd03Zi1JJ/zUwuJ+k +PXQz0jt63fuLE6SjtEQtOGV3g2Ks2OS4k5s84N2z0w9holwy4pT97mgknL6BabiF +ncHgESVxku20EvmBHV91joLu5ZgKM0twyM0wNr5rERDd9IN++FEDt49ZurCFa1z9 +yxgBAoIBAC3HJzGb7Cufqw1JNng8H1mkJ5+1ZCNo7jy/aUYd5OacGTCNTcvuPTj+ +2iGvn4G0JR7pukhU5dVtGMQGpmmp8zk6/xzmyqeeiQNi4wdEgMALq4I0nynXkxDv +utKsXpmPiwyxmwCg9EY7AokfGWbxI5Yf7HkrjxME7jHz31lt5OF7AKyE1veFYWRa +puP1KVjNH7UAoE3WHnPnj7xvfQspXVRpzPWXH86XVonqnjQgu3SDkclPbkjg4HVj +athb6h5RN5bYx1cbUvo3JssBYl92FlXPU9lLzgv4nALUdVSi8PjbjQ7WXdxaKPdf +lczRTJNTE/KNUE0pkC5P4c/e0A1OFu0= +-----END PRIVATE KEY----- From 52839336f721a73d080cc8dfca4dbff8da3c6725 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 20 Apr 2022 16:58:14 -0400 Subject: [PATCH 21/75] Removed unused code --- .../java/io/jsonwebtoken/impl/JwtMap.java | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 4df57bdfa..a7b220c68 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -18,21 +18,15 @@ import io.jsonwebtoken.Header; import io.jsonwebtoken.JweHeader; import io.jsonwebtoken.JwsHeader; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.impl.lang.Field; -import io.jsonwebtoken.impl.lang.JwtDateConverter; import io.jsonwebtoken.impl.security.JwkContext; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; -import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.Jwk; -import io.jsonwebtoken.security.MalformedKeyException; import java.lang.reflect.Array; import java.util.Collection; -import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -68,15 +62,6 @@ protected boolean isSecret(String id) { return field != null && field.isSecret(); } - protected static Date toDate(Object v, String name) { - try { - return JwtDateConverter.toDate(v); - } catch (Exception e) { - String msg = "Cannot create Date from '" + name + "' value: " + v + ". Cause: " + e.getMessage(); - throw new IllegalArgumentException(msg, e); - } - } - public static boolean isReduceableToNull(Object v) { return v == null || (v instanceof String && !Strings.hasText((String) v)) || @@ -124,8 +109,6 @@ public Object put(String name, Object value) { name = Assert.notNull(Strings.clean(name), "Member name cannot be null or empty."); if (value instanceof String) { value = Strings.clean((String) value); - } else if (Objects.isArray(value) && !value.getClass().getComponentType().isPrimitive()) { - value = Collections.arrayToList(value); } return idiomaticPut(name, value); } @@ -153,14 +136,6 @@ protected Object nullSafePut(String name, Object value) { } } - private JwtException malformed(String msg, Exception cause) { - if (this instanceof JwkContext || this instanceof Jwk) { - return new MalformedKeyException(msg, cause); - } else { - return new MalformedJwtException(msg, cause); - } - } - protected Object apply(Field field, Object rawValue) { final String id = field.getId(); From 23e94afd5b24b441d12798f7073d1574f2d22d50 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 29 Apr 2022 16:50:23 -0400 Subject: [PATCH 22/75] Merge branch 'master' into jwe-merge # Conflicts: # .travis.yml # api/pom.xml # api/src/main/java/io/jsonwebtoken/JwtBuilder.java # api/src/main/java/io/jsonwebtoken/JwtParser.java # api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java # api/src/main/java/io/jsonwebtoken/lang/Collections.java # api/src/main/java/io/jsonwebtoken/lang/Maps.java # api/src/main/java/io/jsonwebtoken/lang/Strings.java # extensions/gson/pom.xml # extensions/jackson/pom.xml # extensions/orgjson/pom.xml # extensions/pom.xml # impl/pom.xml # impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java # impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java # impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java # impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java # impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java # impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java # impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy # impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy # impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy # impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy # impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy # impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy # pom.xml --- ...efaultEllipticCurveSignatureAlgorithm.java | 131 ++++++++++-------- .../security/SignatureAlgorithmsBridge.java | 6 +- .../io/jsonwebtoken/DeprecatedJwtsTest.groovy | 4 +- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 58 ++++---- ...EllipticCurveSignatureAlgorithmTest.groovy | 108 ++++++++------- .../io/jsonwebtoken/security/KeysTest.groovy | 6 +- 6 files changed, 167 insertions(+), 146 deletions(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java index 0b446d15b..5ae1f282b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java @@ -9,7 +9,6 @@ import io.jsonwebtoken.security.SignatureException; import io.jsonwebtoken.security.SignatureRequest; import io.jsonwebtoken.security.VerifySignatureRequest; -import io.jsonwebtoken.security.WeakKeyException; import java.math.BigInteger; import java.security.Key; @@ -20,45 +19,72 @@ import java.security.Signature; import java.security.interfaces.ECKey; import java.security.spec.ECGenParameterSpec; +import java.text.MessageFormat; import java.util.Arrays; public class DefaultEllipticCurveSignatureAlgorithm extends AbstractSignatureAlgorithm implements EllipticCurveSignatureAlgorithm { - private static final String EC_PUBLIC_KEY_REQD_MSG = - "Elliptic Curve signature validation requires an ECPublicKey instance."; + private static final String REQD_ORDER_BIT_LENGTH_MSG = "orderBitLength must equal 256, 384, or 512."; + private static final String KEY_TYPE_MSG_PATTERN = + "Elliptic Curve {0} keys must be {1}s (implement {2}). Provided key type: {3}."; private static final String DER_ENCODING_SYS_PROPERTY_NAME = "io.jsonwebtoken.impl.crypto.EllipticCurveSignatureValidator.derEncodingSupported"; - private static final int MIN_KEY_LENGTH_BITS = 256; - private final String curveName; - private final int minKeyBitLength; //in bits + private final int orderBitLength; + /** + * JWA EC (concat formatted) length in bytes for this instance's {@link #orderBitLength}. + */ private final int signatureByteLength; - private final int keyFieldByteLength; + private final int sigFieldByteLength; - private static int shaSize(int keyBitLength) { - return keyBitLength == 521 ? 512 : keyBitLength; + private static int shaSize(int orderBitLength) { + return orderBitLength == 521 ? 512 : orderBitLength; } - public DefaultEllipticCurveSignatureAlgorithm(int keyBitLength, int signatureByteLength) { - this("ES" + shaSize(keyBitLength), "SHA" + shaSize(keyBitLength) + "withECDSA", "secp" + keyBitLength + "r1", keyBitLength, signatureByteLength); + /** + * Returns the correct byte length of an R or S field in a concat signature for the given EC Key order bit length. + * + *

    Per RFC 7518, Section 3.4: + * + * the Integer-to-OctetString Conversion + * defined in Section 2.3.7 of SEC1 [SEC1] used to represent R and S as + * octet sequences adds zero-valued high-order padding bits when needed + * to round the size up to a multiple of 8 bits; thus, each 521-bit + * integer is represented using 528 bits in 66 octets. + * + *

    + * + * @param orderBitLength the EC Key order bit length (ecKey.getParams().getOrder().bitLength()) + * @return the correct byte length of an R or S field in a concat signature for the given EC Key order bit length. + */ + private static int fieldByteLength(int orderBitLength) { + return (orderBitLength + 7) / Byte.SIZE; } - public DefaultEllipticCurveSignatureAlgorithm(String name, String jcaName, String curveName, int minKeyBitLength, int signatureByteLength) { - super(name, jcaName); - Assert.hasText(curveName, "Curve name cannot be null or empty."); - this.curveName = curveName; - if (minKeyBitLength < MIN_KEY_LENGTH_BITS) { - String msg = "minKeyLength bits must be greater than the JWA mandatory minimum key length of " + MIN_KEY_LENGTH_BITS; - throw new IllegalArgumentException(msg); - } - this.minKeyBitLength = minKeyBitLength; - Assert.isTrue(signatureByteLength > 0, "signatureLength must be greater than zero."); - this.signatureByteLength = signatureByteLength; - this.keyFieldByteLength = this.signatureByteLength / 2; + /** + * Returns {@code true} for Order bit lengths defined in the JWA specification, {@code false} otherwise. + * Specifically, returns {@code true} only for values of {@code 256}, {@code 384} and {@code 521}. See + * RFC 7518, Section 3.4 for more. + * + * @param orderBitLength the EC key Order bit length to check + * @return {@code true} for Order bit lengths defined in the JWA specification, {@code false} otherwise. + */ + private static boolean isSupportedOrderBitLength(int orderBitLength) { + // This implementation supports only those defined in the JWA specification. + return orderBitLength == 256 || orderBitLength == 384 || orderBitLength == 521; + } + + public DefaultEllipticCurveSignatureAlgorithm(int orderBitLength) { + super("ES" + shaSize(orderBitLength), "SHA" + shaSize(orderBitLength) + "withECDSA"); + Assert.isTrue(isSupportedOrderBitLength(orderBitLength), REQD_ORDER_BIT_LENGTH_MSG); + this.curveName = "secp" + orderBitLength + "r1"; + this.orderBitLength = orderBitLength; + this.sigFieldByteLength = fieldByteLength(this.orderBitLength); + this.signatureByteLength = this.sigFieldByteLength * 2; // R bytes + S bytes = concat signature bytes } @Override @@ -74,52 +100,35 @@ public KeyPair apply(KeyPairGenerator generator) throws Exception { }); } - @Override - protected void validateKey(Key key, boolean signing) { - - if (!(key instanceof ECKey)) { - String msg = "EC " + keyType(signing) + " keys must be an ECKey. The specified key is of type: " + - key.getClass().getName(); + private static void assertKey(Key key, Class type, boolean signing) { + if (!type.isInstance(key)) { + String msg = MessageFormat.format(KEY_TYPE_MSG_PATTERN, + keyType(signing), type.getSimpleName(), type.getName(), key.getClass().getName()); throw new InvalidKeyException(msg); } + } - if (signing) { - // https://github.com/jwtk/jjwt/issues/68 - // Instead of checking for an instance of ECPrivateKey, check for PrivateKey (and ECKey assertion is above): - if (!(key instanceof PrivateKey)) { - String msg = "Asymmetric key signatures must be created with PrivateKeys. The specified key is of type: " + - key.getClass().getName(); - throw new InvalidKeyException(msg); - } - } else { //verification - if (!(key instanceof PublicKey)) { - throw new InvalidKeyException(EC_PUBLIC_KEY_REQD_MSG); - } - } + @Override + protected void validateKey(Key key, boolean signing) { + + assertKey(key, ECKey.class, signing); + // https://github.com/jwtk/jjwt/issues/68: + // Instead of checking for an instance of ECPrivateKey, check for PrivateKey (and ECKey assertion is above): + Class requiredType = signing ? PrivateKey.class : PublicKey.class; + assertKey(key, requiredType, signing); final String name = getId(); ECKey ecKey = (ECKey) key; BigInteger order = ecKey.getParams().getOrder(); int orderBitLength = order.bitLength(); - if (orderBitLength < this.minKeyBitLength) { - String msg = "The " + keyType(signing) + " key's size (ECParameterSpec order) is " + orderBitLength + - " bits which is not secure enough for the " + name + " algorithm. The JWT " + - "JWA Specification (RFC 7518, Section 3.4) states that keys used with " + - name + " MUST have a size >= " + this.minKeyBitLength + - " bits. Consider using the SignatureAlgorithms." + name + ".generateKeyPair() " + - "method to create a key pair guaranteed to be secure enough for " + name + ". See " + - "https://tools.ietf.org/html/rfc7518#section-3.4 for more information."; - throw new WeakKeyException(msg); - } - - int keyFieldByteLength = (orderBitLength + 7) / Byte.SIZE; //for ES512 (can be 65 or 66, this ensures 66) - int concatByteLength = keyFieldByteLength * 2; + int sigFieldByteLength = fieldByteLength(orderBitLength); + int concatByteLength = sigFieldByteLength * 2; if (concatByteLength != this.signatureByteLength) { - String msg = "EllipticCurve key has a field size of " + Bytes.bytesMsg(keyFieldByteLength) + ", but " + - getId() + " requires a field size of " + Bytes.bytesMsg(this.keyFieldByteLength) + - " per [RFC 7518, Section 3.4 (validation)]" + - "(https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)."; + String msg = "The provided Elliptic Curve " + keyType(signing) + " key's size (aka Order bit length) is " + + Bytes.bitsMsg(orderBitLength) + ", but the '" + name + "' algorithm requires EC Keys with " + + Bytes.bitsMsg(this.orderBitLength) + " per " + + "[RFC 7518, Section 3.4](https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)."; throw new InvalidKeyException(msg); } } @@ -169,8 +178,8 @@ public Boolean apply(Signature sig) { } else { //guard for JVM security bug CVE-2022-21449: BigInteger order = key.getParams().getOrder(); - BigInteger r = new BigInteger(1, Arrays.copyOfRange(concatSignature, 0, keyFieldByteLength)); - BigInteger s = new BigInteger(1, Arrays.copyOfRange(concatSignature, keyFieldByteLength, concatSignature.length)); + BigInteger r = new BigInteger(1, Arrays.copyOfRange(concatSignature, 0, sigFieldByteLength)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(concatSignature, sigFieldByteLength, concatSignature.length)); if (r.signum() < 1 || s.signum() < 1 || r.compareTo(order) >= 0 || s.compareTo(order) >= 0) { return false; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java index 788de89b7..fa48ed5a7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java @@ -31,9 +31,9 @@ private SignatureAlgorithmsBridge() { new DefaultRsaSignatureAlgorithm<>(256, 2048, 256), new DefaultRsaSignatureAlgorithm<>(384, 3072, 384), new DefaultRsaSignatureAlgorithm<>(512, 4096, 512), - new DefaultEllipticCurveSignatureAlgorithm<>(256, 64), - new DefaultEllipticCurveSignatureAlgorithm<>(384, 96), - new DefaultEllipticCurveSignatureAlgorithm<>(521, 132) + new DefaultEllipticCurveSignatureAlgorithm<>(256), + new DefaultEllipticCurveSignatureAlgorithm<>(384), + new DefaultEllipticCurveSignatureAlgorithm<>(521) )); } diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy index 9e4a77551..71ca06ab2 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy @@ -540,7 +540,9 @@ class DeprecatedJwtsTest { testEC(SignatureAlgorithm.ES256, true) fail("EC private keys cannot be used to validate EC signatures.") } catch (UnsupportedJwtException e) { - assertEquals e.cause.message, "Elliptic Curve signature validation requires an ECPublicKey instance." + String msg = "Elliptic Curve verification keys must be PublicKeys (implement java.security.PublicKey). " + + "Provided key type: sun.security.ec.ECPrivateKeyImpl." + assertEquals msg, e.cause.message } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 96ad70e4a..2ebf90c9c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -15,7 +15,6 @@ */ package io.jsonwebtoken -import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.impl.* import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec @@ -45,7 +44,7 @@ import static org.junit.Assert.* class JwtsTest { private static Date now() { - return dateWithOnlySecondPrecision(System.currentTimeMillis()); + return dateWithOnlySecondPrecision(System.currentTimeMillis()) } private static int later() { @@ -93,7 +92,7 @@ class JwtsTest { void testHeaderWithMapArg() { def header = Jwts.header([alg: "HS256"]) assertTrue header instanceof DefaultHeader - assertEquals header.alg, 'HS256' + assertEquals 'HS256', header.alg } @Test @@ -106,7 +105,7 @@ class JwtsTest { void testJwsHeaderWithMapArg() { def header = Jwts.jwsHeader([alg: "HS256"]) assertTrue header instanceof DefaultJwsHeader - assertEquals header.getAlgorithm(), 'HS256' + assertEquals 'HS256', header.getAlgorithm() } @Test @@ -119,7 +118,7 @@ class JwtsTest { void testJweHeaderWithMapArg() { def header = Jwts.jweHeader([enc: 'foo']) assertTrue header instanceof DefaultJweHeader - assertEquals header.getEncryptionAlgorithm(), 'foo' + assertEquals 'foo', header.getEncryptionAlgorithm() } @Test @@ -132,7 +131,7 @@ class JwtsTest { void testClaimsWithMapArg() { Claims claims = Jwts.claims([sub: 'Joe']) assertNotNull claims - assertEquals claims.getSubject(), 'Joe' + assertEquals 'Joe', claims.getSubject() } @Test @@ -142,7 +141,7 @@ class JwtsTest { String payload = new String(Decoders.BASE64URL.decode(encodedBody), StandardCharsets.UTF_8) String val = Jwts.builder().setPayload(payload).compact() String RFC_VALUE = 'eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.' - assertEquals val, RFC_VALUE + assertEquals RFC_VALUE, val } @Test @@ -203,7 +202,9 @@ class JwtsTest { Jwts.parserBuilder().build().parse('..') fail() } catch (MalformedJwtException e) { - assertEquals 'Compact JWT strings MUST always have a Base64Url protected header per https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4).', e.message + String msg = 'Compact JWT strings MUST always have a Base64Url protected header per ' + + 'https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4).' + assertEquals msg, e.message } } @@ -211,7 +212,7 @@ class JwtsTest { void testParseWithHeaderOnly() { String unsecuredJwt = base64Url("{\"alg\":\"none\"}") + ".." Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parse(unsecuredJwt) - assertEquals("none", jwt.getHeader().get("alg")) + assertEquals"none", jwt.getHeader().get("alg") } @Test @@ -252,7 +253,7 @@ class JwtsTest { void testConvenienceIssuer() { String compact = Jwts.builder().setIssuer("Me").compact() Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims - assertEquals claims.getIssuer(), "Me" + assertEquals 'Me', claims.getIssuer() compact = Jwts.builder().setSubject("Joe") .setIssuer("Me") //set it @@ -267,7 +268,7 @@ class JwtsTest { void testConvenienceSubject() { String compact = Jwts.builder().setSubject("Joe").compact() Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims - assertEquals claims.getSubject(), "Joe" + assertEquals 'Joe', claims.getSubject() compact = Jwts.builder().setIssuer("Me") .setSubject("Joe") //set it @@ -282,7 +283,7 @@ class JwtsTest { void testConvenienceAudience() { String compact = Jwts.builder().setAudience("You").compact() Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims - assertEquals claims.getAudience(), "You" + assertEquals 'You', claims.getAudience() compact = Jwts.builder().setIssuer("Me") .setAudience("You") //set it @@ -297,9 +298,9 @@ class JwtsTest { void testConvenienceExpiration() { Date then = laterDate(10000); String compact = Jwts.builder().setExpiration(then).compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getExpiration() - assertEquals claimedDate, then + assertEquals then, claimedDate compact = Jwts.builder().setIssuer("Me") .setExpiration(then) //set it @@ -316,7 +317,7 @@ class JwtsTest { String compact = Jwts.builder().setNotBefore(now).compact() Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getNotBefore() - assertEquals claimedDate, now + assertEquals now, claimedDate compact = Jwts.builder().setIssuer("Me") .setNotBefore(now) //set it @@ -333,7 +334,7 @@ class JwtsTest { String compact = Jwts.builder().setIssuedAt(now).compact() Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getIssuedAt() - assertEquals claimedDate, now + assertEquals now, claimedDate compact = Jwts.builder().setIssuer("Me") .setIssuedAt(now) //set it @@ -349,7 +350,7 @@ class JwtsTest { String id = UUID.randomUUID().toString() String compact = Jwts.builder().setId(id).compact() Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims - assertEquals claims.getId(), id + assertEquals id, claims.getId() compact = Jwts.builder().setIssuer("Me") .setId(id) //set it @@ -585,7 +586,9 @@ class JwtsTest { testEC(SignatureAlgorithms.ES256, true) fail("EC private keys cannot be used to validate EC signatures.") } catch (UnsupportedJwtException e) { - assertEquals e.cause.message, "Elliptic Curve signature validation requires an ECPublicKey instance." + String msg = "Elliptic Curve verification keys must be PublicKeys (implement java.security.PublicKey). " + + "Provided key type: sun.security.ec.ECPrivateKeyImpl." + assertEquals msg, e.cause.message } } @@ -608,9 +611,9 @@ class JwtsTest { @Test void testBuilderWithEcdsaPublicKey() { def builder = Jwts.builder().setSubject('foo') - def pair = Keys.keyPairFor(SignatureAlgorithm.ES256) + def pair = TestKeys.ES256.pair try { - builder.signWith(pair.public, SignatureAlgorithm.ES256) //public keys can't be used to create signatures + builder.signWith(pair.public, SignatureAlgorithms.ES256) //public keys can't be used to create signatures } catch (InvalidKeyException expected) { String msg = "ECDSA signing keys must be PrivateKey instances." assertEquals msg, expected.getMessage() @@ -623,9 +626,10 @@ class JwtsTest { @Test void testBuilderWithMismatchedEllipticCurveKeyAndAlgorithm() { def builder = Jwts.builder().setSubject('foo') - def pair = Keys.keyPairFor(SignatureAlgorithm.ES384) + def pair = TestKeys.ES384.pair try { - builder.signWith(pair.private, SignatureAlgorithm.ES256) //ES384 keys can't be used to create ES256 signatures + builder.signWith(pair.private, SignatureAlgorithms.ES256) + //ES384 keys can't be used to create ES256 signatures } catch (InvalidKeyException expected) { String msg = "EllipticCurve key has a field size of 48 bytes (384 bits), but ES256 requires a " + "field size of 32 bytes (256 bits) per [RFC 7518, Section 3.4 (validation)]" + @@ -639,9 +643,9 @@ class JwtsTest { */ @Test void testParserWithMismatchedEllipticCurveKeyAndAlgorithm() { - def pair = Keys.keyPairFor(SignatureAlgorithm.ES256) + def pair = TestKeys.ES256.pair def jws = Jwts.builder().setSubject('foo').signWith(pair.private).compact() - def parser = Jwts.parserBuilder().setSigningKey(Keys.keyPairFor(SignatureAlgorithm.ES384).public).build() + def parser = Jwts.parserBuilder().setSigningKey(TestKeys.ES384.pair.public).build() try { parser.parseClaimsJws(jws) } catch (UnsupportedJwtException expected) { @@ -658,7 +662,7 @@ class JwtsTest { /** * @since 0.11.5 as part of testing guards against JVM CVE-2022-21449 */ - @Test(expected=io.jsonwebtoken.security.SignatureException) + @Test(expected = io.jsonwebtoken.security.SignatureException) void testEcdsaInvalidSignatureValue() { def withoutSignature = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def invalidEncodedSignature = "_____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ" @@ -681,7 +685,7 @@ class JwtsTest { Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseClaimsJws(notSigned) fail('parseClaimsJws must fail for unsigned JWTs') } catch (UnsupportedJwtException expected) { - assertEquals expected.message, 'Unsigned Claims JWTs are not supported.' + assertEquals 'Unsigned Claims JWTs are not supported.', expected.message } } @@ -704,7 +708,7 @@ class JwtsTest { String forged = Jwts.builder().setSubject("Not Joe").compact() //assert that our forged header has a 'NONE' algorithm: - assertEquals Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(forged).getHeader().get('alg'), 'none' + assertEquals 'none', Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(forged).getHeader().get('alg') //now let's forge it by appending the signature the server expects: forged += signature diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy index f53059fe7..4d79032d2 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy @@ -3,7 +3,10 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.JwtException import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.io.Decoders -import io.jsonwebtoken.security.* +import io.jsonwebtoken.security.EllipticCurveSignatureAlgorithm +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.SignatureAlgorithms +import io.jsonwebtoken.security.SignatureException import org.junit.Test import javax.crypto.spec.SecretKeySpec @@ -13,7 +16,6 @@ import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey import java.security.spec.X509EncodedKeySpec -import static org.easymock.EasyMock.createMock import static org.junit.Assert.* class DefaultEllipticCurveSignatureAlgorithmTest { @@ -25,18 +27,13 @@ class DefaultEllipticCurveSignatureAlgorithmTest { @Test void testConstructorWithWeakKeyLength() { try { - new DefaultEllipticCurveSignatureAlgorithm('ES256', 'SHA256withECDSA', 'secp256r1', 128, 256) + new DefaultEllipticCurveSignatureAlgorithm(128) } catch (IllegalArgumentException iae) { - assertEquals 'minKeyLength bits must be greater than the JWA mandatory minimum key length of 256', iae.getMessage() + String msg = 'orderBitLength must equal 256, 384, or 512.' + assertEquals msg, iae.getMessage() } } - @Test(expected = SecurityException) - void testGenerateKeyPairInvalidCurveName() { - def alg = new DefaultEllipticCurveSignatureAlgorithm('ES256', 'SHA256withECDSA', 'notreal', 256, 256) - alg.generateKeyPair() - } - @Test void testSignWithoutEcKey() { def key = new SecretKeySpec(new byte[1], 'foo') @@ -46,8 +43,9 @@ class DefaultEllipticCurveSignatureAlgorithmTest { try { it.sign(req) } catch (InvalidKeyException expected) { - String msg = 'Elliptic Curve signatures require an ECKey. The provided key of type ' + - 'javax.crypto.spec.SecretKeySpec is not a java.security.interfaces.ECKey instance.' + String msg = "Elliptic Curve signing keys must be ECKeys " + + "(implement java.security.interfaces.ECKey). Provided key type: " + + "javax.crypto.spec.SecretKeySpec." assertEquals msg, expected.getMessage() } } @@ -55,12 +53,14 @@ class DefaultEllipticCurveSignatureAlgorithmTest { @Test void testSignWithPublicKey() { - ECPublicKey key = createMock(ECPublicKey) + ECPublicKey key = TestKeys.ES256.pair.public as ECPublicKey def request = new DefaultSignatureRequest(null, null, new byte[1], key) try { SignatureAlgorithms.ES256.sign(request) } catch (InvalidKeyException e) { - assertTrue e.getMessage().startsWith("Asymmetric key signatures must be created with PrivateKeys. The specified key is of type: ") + String msg = "Elliptic Curve signing keys must be PrivateKeys (implement ${PrivateKey.class.getName()}). " + + "Provided key type: ${key.getClass().getName()}." + assertEquals msg, e.getMessage() } } @@ -74,7 +74,13 @@ class DefaultEllipticCurveSignatureAlgorithmTest { algs().each { try { it.sign(request) - } catch (WeakKeyException expected) { + } catch (InvalidKeyException expected) { + def keyOrderBitLength = pair.getPublic().getParams().getOrder().bitLength() + String msg = "The provided Elliptic Curve signing key's size (aka Order bit length) is " + + "${Bytes.bitsMsg(keyOrderBitLength)}, but the '${it.getId()}' algorithm requires EC Keys with " + + "${Bytes.bitsMsg(it.orderBitLength)} per " + + "[RFC 7518, Section 3.4](https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)." as String + assertEquals msg, expected.getMessage() } } } @@ -87,9 +93,10 @@ class DefaultEllipticCurveSignatureAlgorithmTest { try { SignatureAlgorithms.ES384.sign(req) } catch (InvalidKeyException expected) { - String msg = "EllipticCurve key has a field size of 32 bytes (256 bits), but ES384 requires a " + - "field size of 48 bytes (384 bits) per [RFC 7518, Section 3.4 (validation)]" + - "(https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)." + String msg = "The provided Elliptic Curve signing key's size (aka Order bit length) is " + + "256 bits (32 bytes), but the 'ES384' algorithm requires EC Keys with " + + "384 bits (48 bytes) per " + + "[RFC 7518, Section 3.4](https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)." assertEquals msg, expected.getMessage() } } @@ -102,37 +109,49 @@ class DefaultEllipticCurveSignatureAlgorithmTest { try { it.verify(request) } catch (InvalidKeyException e) { - assertTrue e.getMessage().startsWith("Asymmetric key signatures must be created with PrivateKeys. The specified key is of type: ") + String msg = "Elliptic Curve verification keys must be ECKeys " + + "(implement java.security.interfaces.ECKey). Provided key type: " + + "javax.crypto.spec.SecretKeySpec." + assertEquals msg, e.getMessage() } } } @Test - void testVerifyWithWeakKey() { - def gen = KeyPairGenerator.getInstance("EC") - gen.initialize(192) //too week for any JWA EC algorithm - def pair = gen.generateKeyPair() - def request = new DefaultVerifySignatureRequest(null, null, new byte[1], pair.getPublic(), new byte[1]) + void testVerifyWithPrivateKey() { + byte[] data = 'foo'.getBytes(StandardCharsets.UTF_8) algs().each { + KeyPair pair = it.generateKeyPair() + def key = pair.getPrivate() + def signRequest = new DefaultSignatureRequest(null, null, data, key) + byte[] signature = it.sign(signRequest) + def verifyRequest = new DefaultVerifySignatureRequest(null, null, data, key, signature) try { - it.verify(request) - } catch (WeakKeyException expected) { + it.verify(verifyRequest) + } catch (InvalidKeyException e) { + String msg = "Elliptic Curve verification keys must be PublicKeys (implement " + + "${PublicKey.class.name}). Provided key type: ${key.class.name}." + assertEquals msg, e.getMessage() } } } @Test - void testVerifyWithPrivateKey() { - byte[] data = 'foo'.getBytes(StandardCharsets.UTF_8) + void testVerifyWithWeakKey() { + def gen = KeyPairGenerator.getInstance("EC") + gen.initialize(192) //too week for any JWA EC algorithm + def pair = gen.generateKeyPair() + def request = new DefaultVerifySignatureRequest(null, null, new byte[1], pair.getPublic(), new byte[1]) algs().each { - KeyPair pair = it.generateKeyPair() - def signRequest = new DefaultSignatureRequest(null, null, data, pair.getPrivate()) - byte[] signature = it.sign(signRequest) - def verifyRequest = new DefaultVerifySignatureRequest(null, null, data, pair.getPrivate(), signature) try { - it.verify(verifyRequest) - } catch (InvalidKeyException e) { - assertEquals 'Elliptic Curve signature validation requires an ECPublicKey instance.', e.getMessage() + it.verify(request) + } catch (InvalidKeyException expected) { + def keyOrderBitLength = pair.getPublic().getParams().getOrder().bitLength() + String msg = "The provided Elliptic Curve verification key's size (aka Order bit length) is " + + "${Bytes.bitsMsg(keyOrderBitLength)}, but the '${it.getId()}' algorithm requires EC Keys with " + + "${Bytes.bitsMsg(it.orderBitLength)} per " + + "[RFC 7518, Section 3.4](https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)." as String + assertEquals msg, expected.getMessage() } } } @@ -213,7 +232,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { DefaultEllipticCurveSignatureAlgorithm.transcodeConcatToDER(signature) fail() } catch (JwtException e) { - assertEquals e.message, 'Invalid ECDSA signature format' + assertEquals 'Invalid ECDSA signature format.', e.message } } @@ -250,19 +269,6 @@ class DefaultEllipticCurveSignatureAlgorithmTest { verifier("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJ0ZXN0IjoidGVzdCJ9.AV26tERbSEwcoDGshneZmhokg-tAKUk0uQBoHBohveEd51D5f6EIs6cskkgwtfzs4qAGfx2rYxqQXr7LTXCNquKiAJNkTIKVddbPfped3_TQtmHZTmMNiqmWjiFj7Y9eTPMMRRu26w4gD1a8EQcBF-7UGgeH4L_1CwHJWAXGbtu7uMUn") } - @Test - void legacySignatureCompatTest() { - def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def alg = SignatureAlgorithms.ES512 - def keypair = alg.generateKeyPair() - def signature = Signature.getInstance(alg.jcaName) - def data = withoutSignature.getBytes("US-ASCII") - signature.initSign(keypair.private) - signature.update(data) - def signed = signature.sign() - assertTrue alg.verify(new DefaultVerifySignatureRequest(null, null, data, keypair.public, signed)) - } - @Test void verifySwarmTest() { algs().each { alg -> @@ -440,8 +446,8 @@ class DefaultEllipticCurveSignatureAlgorithmTest { fail() } catch (SignatureException expected) { String signedBytesString = Bytes.bytesMsg(signed.length) - String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. Provided " + - "signature is $signedBytesString but ES512 signatures must be exactly 132 bytes (1056 bits) " + + String msg = "Unable to verify Elliptic Curve signature using provided ECPublicKey: Provided " + + "signature is $signedBytesString but ES512 signatures must be exactly 1056 bits (132 bytes) " + "per [RFC 7518, Section 3.4 (validation)]" + "(https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)." as String assertEquals msg, expected.getMessage() diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy index 411c44aec..005355f77 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy @@ -219,7 +219,7 @@ class KeysTest { KeyPair pair = alg.generateKeyPair() assertNotNull pair - int len = alg.minKeyBitLength + int len = alg.orderBitLength String asn1oid = "secp${len}r1" String suffix = len == 256 ? ", X9.62 prime${len}v1" : '' //the JDK only adds this extra suffix to the secp256r1 curve name and not secp384r1 or secp521r1 curve names @@ -229,13 +229,13 @@ class KeysTest { assert pub instanceof ECPublicKey assertEquals "EC", pub.algorithm assertEquals jdkParamName, pub.params.name - assertEquals alg.minKeyBitLength, pub.params.order.bitLength() + assertEquals alg.orderBitLength, pub.params.order.bitLength() PrivateKey priv = pair.getPrivate() assert priv instanceof ECPrivateKey assertEquals "EC", priv.algorithm assertEquals jdkParamName, priv.params.name - assertEquals alg.minKeyBitLength, priv.params.order.bitLength() + assertEquals alg.orderBitLength, priv.params.order.bitLength() } else { assertFalse alg instanceof AsymmetricKeySignatureAlgorithm From 700943bab6d3c47a68428bef11af769636b43d82 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 29 Apr 2022 17:04:59 -0400 Subject: [PATCH 23/75] using groovy syntax to avoid conflict with legacy SignatureAlgorithm type --- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 2ebf90c9c..565e40072 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -212,7 +212,7 @@ class JwtsTest { void testParseWithHeaderOnly() { String unsecuredJwt = base64Url("{\"alg\":\"none\"}") + ".." Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parse(unsecuredJwt) - assertEquals"none", jwt.getHeader().get("alg") + assertEquals "none", jwt.getHeader().get("alg") } @Test @@ -364,7 +364,7 @@ class JwtsTest { @Test void testUncompressedJwt() { - SignatureAlgorithm alg = SignatureAlgorithms.HS256 + def alg = SignatureAlgorithms.HS256 SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() @@ -386,7 +386,7 @@ class JwtsTest { @Test void testCompressedJwtWithDeflate() { - SignatureAlgorithm alg = SignatureAlgorithms.HS256 + def alg = SignatureAlgorithms.HS256 SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() @@ -408,7 +408,7 @@ class JwtsTest { @Test void testCompressedJwtWithGZIP() { - SignatureAlgorithm alg = SignatureAlgorithms.HS256 + def alg = SignatureAlgorithms.HS256 SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() @@ -430,7 +430,7 @@ class JwtsTest { @Test void testCompressedWithCustomResolver() { - SignatureAlgorithm alg = SignatureAlgorithms.HS256 + def alg = SignatureAlgorithms.HS256 SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() @@ -469,7 +469,7 @@ class JwtsTest { @Test(expected = CompressionException.class) void testCompressedJwtWithUnrecognizedHeader() { - SignatureAlgorithm alg = SignatureAlgorithms.HS256 + def alg = SignatureAlgorithms.HS256 SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() @@ -488,7 +488,7 @@ class JwtsTest { @Test void testCompressStringPayloadWithDeflate() { - SignatureAlgorithm alg = SignatureAlgorithms.HS256 + def alg = SignatureAlgorithms.HS256 SecretKey key = alg.keyBuilder().build() String payload = "this is my test for a payload" @@ -595,7 +595,7 @@ class JwtsTest { @Test(expected = WeakKeyException) void testParseClaimsJwsWithWeakHmacKey() { - SignatureAlgorithm alg = SignatureAlgorithms.HS384 + def alg = SignatureAlgorithms.HS384 def key = alg.keyBuilder().build() def weakKey = SignatureAlgorithms.HS256.keyBuilder().build() @@ -676,7 +676,7 @@ class JwtsTest { void testParseClaimsJwsWithUnsignedJwt() { //create random signing key for testing: - SignatureAlgorithm alg = SignatureAlgorithms.HS256 + def alg = SignatureAlgorithms.HS256 SecretKey key = alg.keyBuilder().build() String notSigned = Jwts.builder().setSubject("Foo").compact() @@ -694,7 +694,7 @@ class JwtsTest { void testForgedTokenWithSwappedHeaderUsingNoneAlgorithm() { //create random signing key for testing: - SignatureAlgorithm alg = SignatureAlgorithms.HS256 + def alg = SignatureAlgorithms.HS256 SecretKey key = alg.keyBuilder().build() //this is a 'real', valid JWT: From 8507ce45e81c9b5422fc3f8a5185ff4abf45ad83 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 29 Apr 2022 19:01:44 -0400 Subject: [PATCH 24/75] JavaDoc fixes --- api/src/main/java/io/jsonwebtoken/Header.java | 33 +++++--- .../main/java/io/jsonwebtoken/JwtBuilder.java | 2 + .../main/java/io/jsonwebtoken/JwtHandler.java | 15 ++++ .../main/java/io/jsonwebtoken/JwtParser.java | 77 ++++++++++++++++--- .../io/jsonwebtoken/JwtParserBuilder.java | 37 +++++---- api/src/main/java/io/jsonwebtoken/Jwts.java | 1 + .../io/jsonwebtoken/SigningKeyResolver.java | 2 +- .../SigningKeyResolverAdapter.java | 11 +-- .../java/io/jsonwebtoken/lang/Assert.java | 30 ++++++-- .../java/io/jsonwebtoken/lang/Classes.java | 9 +++ .../io/jsonwebtoken/lang/Collections.java | 1 + .../jsonwebtoken/security/KeyAlgorithm.java | 5 +- .../java/io/jsonwebtoken/security/Keys.java | 39 ++++++---- .../io/jsonwebtoken/security/PasswordKey.java | 12 ++- .../security/SignatureAlgorithms.java | 6 +- 15 files changed, 207 insertions(+), 73 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/Header.java b/api/src/main/java/io/jsonwebtoken/Header.java index d68ff95e6..808692c01 100644 --- a/api/src/main/java/io/jsonwebtoken/Header.java +++ b/api/src/main/java/io/jsonwebtoken/Header.java @@ -37,15 +37,21 @@ * * @since 0.1 */ -public interface Header extends Map { +public interface Header extends Map { - /** JWT {@code Type} (typ) value: "JWT" */ + /** + * JWT {@code Type} (typ) value: "JWT" + */ String JWT_TYPE = "JWT"; - /** JWT {@code Type} header parameter name: "typ" */ + /** + * JWT {@code Type} header parameter name: "typ" + */ String TYPE = "typ"; - /** JWT {@code Content Type} header parameter name: "cty" */ + /** + * JWT {@code Content Type} header parameter name: "cty" + */ String CONTENT_TYPE = "cty"; /** @@ -56,11 +62,16 @@ public interface Header extends Map { */ String ALGORITHM = "alg"; - /** JWT {@code Compression Algorithm} header parameter name: "zip" */ + /** + * JWT {@code Compression Algorithm} header parameter name: "zip" + */ String COMPRESSION_ALGORITHM = "zip"; - /** JJWT legacy/deprecated compression algorithm header parameter name: "calg" - * @deprecated use {@link #COMPRESSION_ALGORITHM} instead. */ + /** + * JJWT legacy/deprecated compression algorithm header parameter name: "calg" + * + * @deprecated use {@link #COMPRESSION_ALGORITHM} instead. + */ @Deprecated String DEPRECATED_COMPRESSION_ALGORITHM = "calg"; @@ -142,12 +153,12 @@ public interface Header extends Map { String getAlgorithm(); /** - * Sets the JWT alg (Algorithm) header value. A {@code null} value will remove the property + * Sets the JWT {@code alg} (Algorithm) header value. A {@code null} value will remove the property * from the JSON map. *
      - *
    • If the JWT is a Signed JWT (a JWS), the - * alg (Algorithm) header parameter identifies the cryptographic algorithm used to secure the - * JWS.
    • + *
    • If the JWT is a Signed JWT (a JWS), the + * {@code alg} (Algorithm) header + * parameter identifies the cryptographic algorithm used to secure the JWS.
    • *
    • If the JWT is an Encrypted JWT (a JWE), the * alg (Algorithm) header parameter * identifies the cryptographic key management algorithm used to encrypt or determine the value of the Content diff --git a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java index 41eba983b..4ee413a89 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -32,6 +32,7 @@ /** * A builder for constructing JWTs. * + * @param the type of builder returned from various builder configuration methods. * @since 0.1 */ public interface JwtBuilder> extends ClaimsMutator { @@ -509,6 +510,7 @@ public interface JwtBuilder> extends ClaimsMutator { * you want explicit control over the signature algorithm used with the specified key.

      * * @param key the signing key to use to digitally sign the JWT. + * @param The type of key accepted by the {@code SignatureAlgorithm}. * @param alg the JWS algorithm to use with the key to digitally sign the JWT, thereby producing a JWS. * @return the builder for method chaining. * @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification for diff --git a/api/src/main/java/io/jsonwebtoken/JwtHandler.java b/api/src/main/java/io/jsonwebtoken/JwtHandler.java index 621cdf2e8..610fce2b6 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtHandler.java +++ b/api/src/main/java/io/jsonwebtoken/JwtHandler.java @@ -66,11 +66,26 @@ public interface JwtHandler { T onClaimsJws(Jws jws); /** + * This method is invoked when a {@link io.jsonwebtoken.JwtParser JwtParser} determines that the parsed JWT is + * a plaintext JWE. A plaintext JWE is a JWE with a byte array (non-JSON) body (payload) that has been + * encrypted. + * + *

      This method will only be invoked if the plaintext JWE can be successfully decrypted.

      + * + * @param jwe the parsed plaintext jwe + * @return any object to be used after inspecting the JWE, or {@code null} if no return value is necessary. * @since JJWT_RELEASE_VERSION */ T onPlaintextJwe(Jwe jwe); /** + * This method is invoked when a {@link io.jsonwebtoken.JwtParser JwtParser} determines that the parsed JWT is + * a valid Claims JWE. A Claims JWE is a JWT with a {@link Claims} body that has been encrypted. + * + *

      This method will only be invoked if the Claims JWE can be successfully decrypted.

      + * + * @param jwe the parsed claims jwe + * @return any object to be used after inspecting the JWE, or {@code null} if no return value is necessary. * @since JJWT_RELEASE_VERSION */ T onClaimsJwe(Jwe jwe); diff --git a/api/src/main/java/io/jsonwebtoken/JwtParser.java b/api/src/main/java/io/jsonwebtoken/JwtParser.java index 9ef09adb8..6ce1388e8 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParser.java @@ -17,6 +17,7 @@ import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.security.SecurityException; import io.jsonwebtoken.security.SignatureException; import java.security.Key; @@ -320,7 +321,8 @@ public interface JwtParser { *

      NOTE: this method will be removed before version 1.0 */ @SuppressWarnings("DeprecatedIsStillUsed") - @Deprecated // TODO: remove for 1.0 + @Deprecated + // TODO: remove for 1.0 JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver); /** @@ -429,7 +431,7 @@ public interface JwtParser { * @see #parsePlaintextJws(String) * @see #parseClaimsJws(String) */ - Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; /** * Parses the specified compact serialized JWT string based on the builder's current configuration state and @@ -460,9 +462,9 @@ public interface JwtParser { *

    • {@link #parseClaimsJws(String)}
    • *
    * - * @param jwt the compact serialized JWT to parse + * @param jwt the compact serialized JWT to parse * @param handler the handler to invoke when encountering a specific type of JWT - * @param the type of object returned from the {@code handler} + * @param the type of object returned from the {@code handler} * @return the result returned by the {@code JwtHandler} * @throws MalformedJwtException if the specified JWT was incorrectly constructed (and therefore invalid). * Invalid JWTs should not be trusted and should be discarded. @@ -494,6 +496,7 @@ T parse(String jwt, JwtHandler handler) * an {@link UnsupportedJwtException} will be thrown.

    * * @param plaintextJwt a compact serialized unsigned plaintext JWT string. + * @param the type of {@link Header} expected to be contained within the JWT. * @return the {@link Jwt Jwt} instance that reflects the specified compact JWT string. * @throws UnsupportedJwtException if the {@code plaintextJwt} argument does not represent an unsigned plaintext * JWT @@ -509,7 +512,7 @@ T parse(String jwt, JwtHandler handler) * @since 0.2 */ > Jwt parsePlaintextJwt(String plaintextJwt) - throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; /** * Parses the specified compact serialized JWT string based on the builder's current configuration state and @@ -523,6 +526,7 @@ > Jwt parsePlaintextJwt(String plaintextJwt) * {@link UnsupportedJwtException} will be thrown.

    * * @param claimsJwt a compact serialized unsigned Claims JWT string. + * @param the type of {@link Header} expected to be contained within the JWT. * @return the {@link Jwt Jwt} instance that reflects the specified compact JWT string. * @throws UnsupportedJwtException if the {@code claimsJwt} argument does not represent an unsigned Claims JWT * @throws MalformedJwtException if the {@code claimsJwt} string is not a valid JWT @@ -539,7 +543,7 @@ > Jwt parsePlaintextJwt(String plaintextJwt) * @since 0.2 */ > Jwt parseClaimsJwt(String claimsJwt) - throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; /** * Parses the specified compact serialized JWS string based on the builder's current configuration state and @@ -559,8 +563,10 @@ > Jwt parseClaimsJwt(String claimsJwt) * @throws SignatureException if the {@code plaintextJws} JWS signature validation fails * @throws IllegalArgumentException if the {@code plaintextJws} string is {@code null} or empty or only whitespace * @see #parsePlaintextJwt(String) + * @see #parsePlaintextJwe(String) * @see #parseClaimsJwt(String) * @see #parseClaimsJws(String) + * @see #parseClaimsJwe(String) * @see #parse(String, JwtHandler) * @see #parse(String) * @since 0.2 @@ -587,22 +593,73 @@ Jws parsePlaintextJws(String plaintextJws) * before the time this method is invoked. * @throws IllegalArgumentException if the {@code claimsJws} string is {@code null} or empty or only whitespace * @see #parsePlaintextJwt(String) - * @see #parseClaimsJwt(String) * @see #parsePlaintextJws(String) + * @see #parsePlaintextJwe(String) + * @see #parseClaimsJwt(String) + * @see #parseClaimsJwe(String) * @see #parse(String, JwtHandler) * @see #parse(String) * @since 0.2 */ Jws parseClaimsJws(String claimsJws) - throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; /** + * Parses the specified compact serialized JWE string based on the builder's current configuration state and + * returns the resulting plaintext JWE instance. + * + *

    This is a convenience method that is usable if you are confident that the compact string argument reflects a + * plaintext JWE. A plaintext JWE is a JWT with a byte array (non-JSON) body (payload) that has been + * encrypted.

    + * + *

    If the compact string presented does not reflect a plaintext JWE, an {@link UnsupportedJwtException} + * will be thrown.

    + * + * @param plaintextJwe a compact serialized JWE string. + * @return the {@link Jwe Jwe} instance that reflects the specified compact JWS string. + * @throws UnsupportedJwtException if the {@code plaintextJwe} argument does not represent a plaintext JWE + * @throws MalformedJwtException if the {@code plaintextJwe} string is not a valid JWE + * @throws SecurityException if the {@code plaintextJwe} JWE decryption fails + * @throws IllegalArgumentException if the {@code plaintextJwe} string is {@code null} or empty or only whitespace + * @see #parsePlaintextJwt(String) + * @see #parsePlaintextJws(String) + * @see #parseClaimsJwt(String) + * @see #parseClaimsJws(String) + * @see #parseClaimsJwe(String) + * @see #parse(String, JwtHandler) + * @see #parse(String) * @since JJWT_RELEASE_VERSION */ - Jwe parsePlaintextJwe(String plaintextJwe) throws JwtException; + Jwe parsePlaintextJwe(String plaintextJwe) + throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SecurityException, IllegalArgumentException; /** + * Parses the specified compact serialized JWE string based on the builder's current configuration state and + * returns the resulting Claims JWE instance. + * + *

    This is a convenience method that is usable if you are confident that the compact string argument reflects a + * Claims JWE. A Claims JWE is a JWT with a {@link Claims} body that has been encrypted.

    + * + *

    If the compact string presented does not reflect a Claims JWE, an {@link UnsupportedJwtException} will be + * thrown.

    + * + * @param claimsJwe a compact serialized Claims JWE string. + * @return the {@link Jwe Jwe} instance that reflects the specified compact Claims JWE string. + * @throws UnsupportedJwtException if the {@code claimsJwe} argument does not represent a Claims JWE + * @throws MalformedJwtException if the {@code claimsJwe} string is not a valid JWE + * @throws SignatureException if the {@code claimsJwe} JWE decryption fails + * @throws ExpiredJwtException if the specified JWT is a Claims JWE and the Claims has an expiration time + * before the time this method is invoked. + * @throws IllegalArgumentException if the {@code claimsJwe} string is {@code null} or empty or only whitespace + * @see #parsePlaintextJwt(String) + * @see #parsePlaintextJws(String) + * @see #parsePlaintextJwe(String) + * @see #parseClaimsJwt(String) + * @see #parseClaimsJws(String) + * @see #parse(String, JwtHandler) + * @see #parse(String) * @since JJWT_RELEASE_VERSION */ - Jwe parseClaimsJwe(String claimsJwe) throws JwtException; + Jwe parseClaimsJwe(String claimsJwe) + throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SecurityException, IllegalArgumentException; } diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index 9683b0587..8bbfdae97 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -53,10 +53,10 @@ public interface JwtParserBuilder extends Builder { * 3.6.

    * * @return the builder for method chaining. - * @since JJWT_RELEASE_VERSION * @see Unsecured JWS Security Considerations * @see Using the Algorithm "none" * @see io.jsonwebtoken.security.SignatureAlgorithms#NONE + * @since JJWT_RELEASE_VERSION */ JwtParserBuilder enableUnsecuredJws(); @@ -249,7 +249,6 @@ public interface JwtParserBuilder extends Builder { JwtParserBuilder setSigningKey(String base64EncodedSecretKey); /** -<<<<<<< HEAD * Sets the signature verification key used to verify all encountered JWS signatures. If the encountered JWT * string is not a JWS (e.g. unsigned or a JWE), this key is not used. * @@ -259,7 +258,7 @@ public interface JwtParserBuilder extends Builder { * *

    If there is any chance that the parser will encounter JWSs * that need different signature verification keys based on the JWS being parsed, it is strongly - * recommended to configure your own {@link Locator Locator} via the + * recommended to configure your own {@link Locator} via the * {@link #setKeyLocator(Locator) setKeyLocator} method instead of using this one.

    * *

    Calling this method overrides any previously set signature verification key.

    @@ -273,18 +272,19 @@ public interface JwtParserBuilder extends Builder { /** * Sets the decryption key to be used to decrypt all encountered JWEs. If the encountered JWT string is not a * JWE (e.g. a JWS), this key is not used. - *

    + * *

    This is a convenience method to use in specific circumstances: when the parser will only ever encounter * JWEs that can always be decrypted by a single key. This also implies that this key MUST be a valid * key for both the key management algorithm ({@code alg} header) and the content encryption algorithm * ({@code enc} header) used for the JWE.

    - *

    + * *

    If there is any chance that the parser will encounter JWEs * that need different decryption keys based on the JWE being parsed, it is strongly recommended to configure - * your own {@link Locator Locator} via the {@link #setKeyLocator(Locator) setKeyLocator} method instead of + * your own {@link Locator Locator} via the {@link #setKeyLocator(Locator) setKeyLocator} method instead of * using this one.

    - *

    + * *

    Calling this method overrides any previously set decryption key.

    + * * @param key the algorithm-specific decryption key to use to decrypt all encountered JWEs. * @return the parser builder for method chaining. */ @@ -297,12 +297,12 @@ public interface JwtParserBuilder extends Builder { * necessary to verify the JWS signature. *
  • If the parsed String is a JWE, it will be called to find the appropriate decryption key.
  • * - *

    + * *

    Specifying a key {@code Locator} is necessary when the signing or decryption key is not already known before * parsing the JWT and the JWT header must be inspected first to determine how to * look up the verification or decryption key. Once returned by the locator, the JwtParser will then either * verify the JWS signature or decrypt the JWE payload with the returned key. For example:

    - *

    + * *

          * Jws<Claims> jws = Jwts.parser().setKeyLocator(new Locator<Header,Key>() {
          *         @Override
    @@ -315,7 +315,7 @@ public interface JwtParserBuilder extends Builder {
          *         }})
          *     .parseClaimsJws(compact);
          * 
    - *

    + * *

    A Key {@code Locator} is invoked once during parsing before performing decryption or signature verification.

    * * @param keyLocator the locator used to retrieve decryption or signature verification keys. @@ -325,14 +325,17 @@ public interface JwtParserBuilder extends Builder { JwtParserBuilder setKeyLocator(Locator keyLocator); /** - *

    Deprecation Notice

    + *

    Deprecation Notice

    + * *

    This method has been deprecated as of JJWT version JJWT_RELEASE_VERSION because it only supports key location * for JWSs (signed JWTs) instead of both signed (JWS) and encrypted (JWE) scenarios. Use the * {@link #setKeyLocator(Locator) setKeyLocator} method instead to ensure a locator that can work for both JWS and * JWE inputs. This method will be removed for the 1.0 release.

    - *

    Previous Documentation

    - * Sets the {@link SigningKeyResolver} used to acquire the signing key that should be used to verify - * a JWS's signature. If the parsed String is not a JWS (no signature), this resolver is not used. + * + *

    Previous Documentation

    + * + *

    Sets the {@link SigningKeyResolver} used to acquire the signing key that should be used to verify + * a JWS's signature. If the parsed String is not a JWS (no signature), this resolver is not used.

    * *

    Specifying a {@code SigningKeyResolver} is necessary when the signing key is not already known before parsing * the JWT and the JWT header or payload (plaintext body or Claims) must be inspected first to determine how to @@ -354,9 +357,9 @@ public interface JwtParserBuilder extends Builder { *

    This method should only be used if a signing key is not provided by the other {@code setSigningKey*} builder * methods.

    * - * @deprecated since JJWT_RELEASE_VERSION * @param signingKeyResolver the signing key resolver used to retrieve the signing key. * @return the parser builder for method chaining. + * @deprecated since JJWT_RELEASE_VERSION */ @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated @@ -364,9 +367,9 @@ public interface JwtParserBuilder extends Builder { JwtParserBuilder addEncryptionAlgorithms(Collection encAlgs); - JwtParserBuilder addSignatureAlgorithms(Collection> sigAlgs); + JwtParserBuilder addSignatureAlgorithms(Collection> sigAlgs); - JwtParserBuilder addKeyAlgorithms(Collection> keyAlgs); + JwtParserBuilder addKeyAlgorithms(Collection> keyAlgs); /** * Sets the {@link CompressionCodecResolver} used to acquire the {@link CompressionCodec} that should be used to diff --git a/api/src/main/java/io/jsonwebtoken/Jwts.java b/api/src/main/java/io/jsonwebtoken/Jwts.java index ff2a3bf41..07e979d0f 100644 --- a/api/src/main/java/io/jsonwebtoken/Jwts.java +++ b/api/src/main/java/io/jsonwebtoken/Jwts.java @@ -95,6 +95,7 @@ public static JweHeader jweHeader() { * Returns a new {@link JweHeader} instance suitable for encrypted JWTs (aka 'JWE's), populated with the * specified name/value pairs. * + * @param header initial name/value pairs to add to the created {@code Header} before returning. * @return a new {@link JweHeader} instance suitable for encrypted JWTs (aka 'JWE's), populated with the * specified name/value pairs. * @see JwtBuilder#setHeader(Header) diff --git a/api/src/main/java/io/jsonwebtoken/SigningKeyResolver.java b/api/src/main/java/io/jsonwebtoken/SigningKeyResolver.java index 9e4f09653..eddbcb838 100644 --- a/api/src/main/java/io/jsonwebtoken/SigningKeyResolver.java +++ b/api/src/main/java/io/jsonwebtoken/SigningKeyResolver.java @@ -45,7 +45,7 @@ * implementing this interface directly.

    * * @since 0.4 - * @deprecated since JJWT_RELEASE_VERSION. Implement {@link io.jsonwebtoken.Locator Locator} instead. + * @deprecated since JJWT_RELEASE_VERSION. Implement {@link Locator} instead. * @see io.jsonwebtoken.JwtParserBuilder#setKeyLocator(Locator) */ @Deprecated diff --git a/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java b/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java index 7142aa3a9..8931d5df5 100644 --- a/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java @@ -21,19 +21,20 @@ import java.security.Key; /** - *

    Deprecation Notice

    + *

    Deprecation Notice

    + * *

    As of JJWT JJWT_RELEASE_VERSION, various Resolver concepts (including the {@code SigningKeyResolver}) have been * unified into a single {@link Locator} interface. For key location, (for both signing and encryption keys), * use the {@link JwtParserBuilder#setKeyLocator(Locator)} to configure a parser with your desired Key locator instead * of using a {@code SigningKeyResolver}. Also see {@link LocatorAdapter} for the Adapter pattern parallel of this * class. This {@code SigningKeyResolverAdapter} class will be removed before the 1.0 release.

    * - *

    Previous Documentation

    - * An Adapter implementation of the + *

    Previous Documentation

    + * + *

    An Adapter implementation of the * {@link SigningKeyResolver} interface that allows subclasses to process only the type of JWS body that - * is known/expected for a particular case. + * is known/expected for a particular case.

    * - *

    Previous Documentation

    *

    The {@link #resolveSigningKey(JwsHeader, Claims)} and {@link #resolveSigningKey(JwsHeader, String)} method * implementations delegate to the * {@link #resolveSigningKeyBytes(JwsHeader, Claims)} and {@link #resolveSigningKeyBytes(JwsHeader, String)} methods diff --git a/api/src/main/java/io/jsonwebtoken/lang/Assert.java b/api/src/main/java/io/jsonwebtoken/lang/Assert.java index 6664d370b..6dd959c82 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Assert.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Assert.java @@ -80,7 +80,9 @@ public static void isNull(Object object) { *

    Assert.notNull(clazz, "The class must not be null");
    * * @param object the object to check + * @param the type of object * @param message the exception message to use if the assertion fails + * @return the non-null object * @throws IllegalArgumentException if the object is null */ public static T notNull(T object, String message) { @@ -126,7 +128,7 @@ public static void hasLength(String text, String message) { */ public static void hasLength(String text) { hasLength(text, - "[Assertion failed] - this String argument must have length; it must not be null or empty"); + "[Assertion failed] - this String argument must have length; it must not be null or empty"); } /** @@ -136,6 +138,7 @@ public static void hasLength(String text) { * * @param text the String to check * @param message the exception message to use if the assertion fails + * @return the string if it has text * @see Strings#hasText */ public static String hasText(String text, String message) { @@ -155,7 +158,7 @@ public static String hasText(String text, String message) { */ public static void hasText(String text) { hasText(text, - "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank"); + "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank"); } /** @@ -168,7 +171,7 @@ public static void hasText(String text) { */ public static void doesNotContain(String textToSearch, String substring, String message) { if (Strings.hasLength(textToSearch) && Strings.hasLength(substring) && - textToSearch.indexOf(substring) != -1) { + textToSearch.indexOf(substring) != -1) { throw new IllegalArgumentException(message); } } @@ -182,7 +185,7 @@ public static void doesNotContain(String textToSearch, String substring, String */ public static void doesNotContain(String textToSearch, String substring) { doesNotContain(textToSearch, substring, - "[Assertion failed] - this String argument must not contain the substring [" + substring + "]"); + "[Assertion failed] - this String argument must not contain the substring [" + substring + "]"); } @@ -264,7 +267,9 @@ public static void noNullElements(Object[] array) { *
    Assert.notEmpty(collection, "Collection must have elements");
    * * @param collection the collection to check + * @param the type of collection * @param message the exception message to use if the assertion fails + * @return the non-null, non-empty collection * @throws IllegalArgumentException if the collection is null or has no elements */ public static > T notEmpty(T collection, String message) { @@ -284,7 +289,7 @@ public static > T notEmpty(T collection, String message) */ public static void notEmpty(Collection collection) { notEmpty(collection, - "[Assertion failed] - this collection must not be empty: it must contain at least 1 element"); + "[Assertion failed] - this collection must not be empty: it must contain at least 1 element"); } /** @@ -293,7 +298,9 @@ public static void notEmpty(Collection collection) { *
    Assert.notEmpty(map, "Map must have entries");
    * * @param map the map to check + * @param the type of Map to check * @param message the exception message to use if the assertion fails + * @return the non-null, non-empty map * @throws IllegalArgumentException if the map is null or has no entries */ public static > T notEmpty(T map, String message) { @@ -334,11 +341,13 @@ public static void isInstanceOf(Class clazz, Object obj) { *
    Assert.instanceOf(Foo.class, foo);
    * * @param type the type to check against + * @param the object's expected type * @param obj the object to check * @param message a message which will be prepended to the message produced by * the function itself, and which may be used to provide context. It should * normally end in a ": " or ". " so that the function generate message looks * ok when prepended to it. + * @return the non-null object IFF it is an instance of the specified {@code type}. * @throws IllegalArgumentException if the object is not an instance of clazz * @see Class#isInstance */ @@ -346,8 +355,8 @@ public static T isInstanceOf(Class type, Object obj, String message) { notNull(type, "Type to check against must not be null"); if (!type.isInstance(obj)) { throw new IllegalArgumentException(message + - "Object of class [" + (obj != null ? obj.getClass().getName() : "null") + - "] must be an instance of " + type); + "Object of class [" + (obj != null ? obj.getClass().getName() : "null") + + "] must be an instance of " + type); } return type.cast(obj); } @@ -384,6 +393,13 @@ public static void isAssignable(Class superType, Class subType, String message) } /** + * Asserts that a specified {@code value} is greater than the given {@code requirement}, throwing + * an {@link IllegalArgumentException} with the given message if not. + * + * @param value the value to check + * @param requirement the integer that {@code value} must be greater than + * @param msg the message to use for the {@code IllegalArgumentException} if thrown. + * @return {@code value} if greater than the specified {@code requirement}. * @since JJWT_RELEASE_VERSION */ public static Integer gt(Integer value, Integer requirement, String msg) { diff --git a/api/src/main/java/io/jsonwebtoken/lang/Classes.java b/api/src/main/java/io/jsonwebtoken/lang/Classes.java index a211bfa3c..402e83059 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Classes.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Classes.java @@ -211,6 +211,15 @@ public static T invokeStatic(String fqcn, String methodName, Class[] argT } /** + * Invokes the {@code clazz}'s matching static method (named {@code methodName} with exact argument types + * of {@code argTypes}) with the given {@code args} arguments, and returns the method return value. + * + * @param clazz the class to invoke + * @param methodName the name of the static method on {@code clazz} to invoke + * @param argTypes the types of the arguments accepted by the method + * @param args the actual runtime arguments to use when invoking the method + * @param the type of object expected to be returned from the method + * @return the result returned by the invoked method. * @since JJWT_RELEASE_VERSION */ @SuppressWarnings("unchecked") diff --git a/api/src/main/java/io/jsonwebtoken/lang/Collections.java b/api/src/main/java/io/jsonwebtoken/lang/Collections.java index cf0d66a2b..98f1d5e52 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Collections.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Collections.java @@ -87,6 +87,7 @@ public static List immutable(List list) { * Works for {@link List}, {@link Set} and {@link Collection} arguments. * * @param c collection to wrap in an immutable/unmodifiable collection + * @param type of collection * @param type of elements in the collection * @return an immutable wrapper for {@code l}. * @since JJWT_RELEASE_VERSION diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java index 92be15d78..5dd2eb97b 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java @@ -24,13 +24,14 @@ * A {@code KeyAlgorithm} produces the {@link SecretKey} used to encrypt or decrypt a JWE. The {@code KeyAlgorithm} * used for a particular JWE is {@link #getId() identified} in the JWE's * {@code alg} header. - *

    + * *

    The {@code KeyAlgorithm} interface is JJWT's idiomatic approach to the JWE specification's - * {@code Key Management Mode} concept.

    + * {@code Key Management Mode} concept.

    * * @since JJWT_RELEASE_VERSION * @see RFC 7561, Section 2: JWE Key (Management) Algorithms */ +@SuppressWarnings("JavadocLinkAsPlainText") public interface KeyAlgorithm extends Identifiable { KeyResult getEncryptionKey(KeyRequest request) throws SecurityException; diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index 2e3f12c1d..e8eebaf80 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -76,17 +76,22 @@ public static SecretKey hmacShaKeyFor(byte[] bytes) throws WeakKeyException { /** *

    Deprecation Notice

    + * *

    As of JJWT JJWT_RELEASE_VERSION, symmetric (secret) key algorithm instances can generate a key of suitable * length for that specific algorithm by calling their {@code keyBuilder()} method directly. For example: - *

    +     *
    +     * 
    
          * {@link SignatureAlgorithms#HS256}.keyBuilder().build();
          * {@link SignatureAlgorithms#HS384}.keyBuilder().build();
          * {@link SignatureAlgorithms#HS512}.keyBuilder().build();
    -     * 
    - * Call those methods as needed instead of this {@code secretKeyFor} helper method. This helper method will be + *
    + * + *

    Call those methods as needed instead of this {@code secretKeyFor} helper method. This helper method will be * removed before the 1.0 final release.

    - *

    Previous Documentation

    - * Returns a new {@link SecretKey} with a key length suitable for use with the specified {@link SignatureAlgorithm}. + * + *

    Previous Documentation

    + * + *

    Returns a new {@link SecretKey} with a key length suitable for use with the specified {@link SignatureAlgorithm}.

    * *

    JWA Specification (RFC 7518), Section 3.2 * requires minimum key lengths to be used for each respective Signature Algorithm. This method returns a @@ -133,19 +138,24 @@ public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) thr /** *

    Deprecation Notice

    + * *

    As of JJWT JJWT_RELEASE_VERSION, asymmetric key algorithm instances can generate KeyPairs of suitable strength - * for that specific algorithm by calling their {@code generateKeyPair()} method directly. For example: - *

    +     * for that specific algorithm by calling their {@code generateKeyPair()} method directly. For example:

    + * + *
    
          * {@link SignatureAlgorithms#RS256}.generateKeyPair();
          * {@link SignatureAlgorithms#RS384}.generateKeyPair();
          * {@link SignatureAlgorithms#RS256}.generateKeyPair();
          * ... etc ...
          * {@link SignatureAlgorithms#ES512}.generateKeyPair();
    -     * 
    - * Call those methods as needed instead of this {@code keyPairFor} helper method. This helper method will be + *
    + * + *

    Call those methods as needed instead of this {@code keyPairFor} helper method. This helper method will be * removed before the 1.0 final release.

    + * *

    Previous Documentation

    - * Returns a new {@link KeyPair} suitable for use with the specified asymmetric algorithm. + * + *

    Returns a new {@link KeyPair} suitable for use with the specified asymmetric algorithm.

    * *

    If the {@code alg} argument is an RSA algorithm, a KeyPair is generated based on the following:

    * @@ -232,9 +242,12 @@ public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws /** * Returns a new {@link PasswordKey} suitable for use with password-based key derivation algorithms. - * Usage Note: Using {@code PasswordKey}s outside of key derivation contexts will likely - * fail. See the {@link PasswordKey} JavaDoc for more, and also note the Password Safety section below. - *

    Password Safety

    + * + *

    Usage Note: Using {@code PasswordKey}s outside of key derivation contexts will likely + * fail. See the {@link PasswordKey} JavaDoc for more, and also note the Password Safety section below.

    + * + *

    Password Safety

    + * *

    Instances returned by this method directly share the specified {@code password} character array argument - * changes to that char array will be reflected in the returned key, and similarly, any call to the key's * {@link PasswordKey#destroy()} method will clear/overwrite the shared char array. This is to ensure that diff --git a/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java b/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java index 26ec0fc95..3bb0ef8a4 100644 --- a/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java +++ b/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java @@ -1,11 +1,13 @@ package io.jsonwebtoken.security; import javax.crypto.SecretKey; +import javax.security.auth.Destroyable; /** * A {@code Key} suitable for use with password-based key derivation algorithms. * - *

    Usage Warning

    + *

    Usage Warning

    + * *

    Because raw passwords should never be used as direct inputs for cryptographic operations (such as authenticated * hashing or encryption) - and only for derivation algorithms (like password-based encryption) - {@code PasswordKey} * instances will throw an exception when used in these invalid contexts. Specifically, calling a @@ -16,16 +18,19 @@ * @see #getPassword() * @since JJWT_RELEASE_VERSION */ -public interface PasswordKey extends SecretKey { +public interface PasswordKey extends SecretKey, Destroyable { /** * Returns a clone of the underlying password character array represented by this Key. Like all * {@code SecretKey} implementations, if you wish to clear the backing password character array for * safety/security reasons, call the Key's {@link #destroy()} method, ensuring that both the password is cleared * and the key instance can no longer be used. - *

    Usage

    + * + *

    Usage

    + * *

    Because a clone is returned from this method, it is expected that callers will clear the resulting clone from * memory as soon as possible to reduce password exposure. For example: + * *

    
          * char[] clonedPassword = aPasswordKey.getPassword();
          * try {
    @@ -35,7 +40,6 @@ public interface PasswordKey extends SecretKey {
          *     java.util.Arrays.fill(clonedPassword, '\u0000');
          * }
          * 
    - *

    * * @return a clone of the underlying password character array represented by this Key. */ diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java index 5ff1e421e..8c4fb8565 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java @@ -28,7 +28,7 @@ /** * @since JJWT_RELEASE_VERSION */ -@SuppressWarnings("rawtypes") +@SuppressWarnings({"rawtypes", "JavadocLinkAsPlainText"}) public final class SignatureAlgorithms { // Prevent instantiation @@ -156,14 +156,14 @@ static T forId0(String id) { * {@code ECKey}s with key lengths less than 256 bits will be rejected with a * {@link WeakKeyException}. *
  • The ECDSA {@code P-521} curve does indeed use keys of 521 bits, not 512 as might be expected. ECDSA - * keys of 384 < size <= 520 are suitable for ES384, while ES512 requires keys >= 521 bits. The '512' part of the + * keys of 384 < size <= 520 are suitable for ES384, while ES512 requires keys >= 521 bits. The '512' part of the * ES512 name reflects the usage of the SHA-512 algorithm, not the ECDSA key length. ES512 with ECDSA keys less * than 521 bits will be rejected with a {@link WeakKeyException}.
  • *
  • The JWT JWA Specification (RFC 7518, * Section 3.3) mandates that RSA signing key lengths MUST be 2048 bits or greater. * {@code RSAKey}s with key lengths less than 2048 bits will be rejected with a * {@link WeakKeyException}.
  • - *
  • Technically any RSA key of length >= 2048 bits may be used with the {@link #RS256}, {@link #RS384}, and + *
  • Technically any RSA key of length >= 2048 bits may be used with the {@link #RS256}, {@link #RS384}, and * {@link #RS512} algorithms, so we assume an RSA signature algorithm based on the key length to * parallel similar decisions in the JWT specification for HMAC and ECDSA signature algorithms. * This is not required - just a convenience.
  • From 4f8b1d18e4b1f617b2fd540914f635360c2ed870 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 29 Apr 2022 22:42:10 -0400 Subject: [PATCH 25/75] JavaDoc fixes, test additions to work on JDK >= 15 --- api/src/main/java/io/jsonwebtoken/Header.java | 4 +- .../main/java/io/jsonwebtoken/JwtBuilder.java | 5 +- .../java/io/jsonwebtoken/security/Keys.java | 8 +-- ...EllipticCurveSignatureAlgorithmTest.groovy | 26 ++++---- .../impl/security/KeyPairsTest.groovy | 62 +------------------ .../impl/security/TestECField.groovy | 13 ++++ .../impl/security/TestECKey.groovy | 14 +++++ .../impl/security/TestECPrivateKey.groovy | 13 ++++ .../impl/security/TestECPublicKey.groovy | 14 +++++ .../jsonwebtoken/impl/security/TestKey.groovy | 25 ++++++++ .../impl/security/TestSecretKey.groovy | 21 +------ .../io/jsonwebtoken/security/KeysTest.groovy | 24 +++++-- 12 files changed, 124 insertions(+), 105 deletions(-) create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECField.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECKey.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECPrivateKey.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECPublicKey.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKey.groovy diff --git a/api/src/main/java/io/jsonwebtoken/Header.java b/api/src/main/java/io/jsonwebtoken/Header.java index 808692c01..46c8674ea 100644 --- a/api/src/main/java/io/jsonwebtoken/Header.java +++ b/api/src/main/java/io/jsonwebtoken/Header.java @@ -176,7 +176,7 @@ public interface Header extends Map { * Returns the JWT zip * (Compression Algorithm) header parameter value or {@code null} if not present. * - *

    Compatiblity Note

    + *

    Compatibility Note

    * *

    While the JWT family of specifications only defines the zip header in the JWE * (JSON Web Encryption) specification, JJWT will also support compression for JWS as well if you choose to use it. @@ -194,7 +194,7 @@ public interface Header extends Map { * (Compression Algorithm) header parameter value. A {@code null} value will remove * the property from the JSON map. * - *

    Compatibility Note

    + *

    Compatibility Note

    * *

    While the JWT family of specifications only defines the zip header in the JWE * (JSON Web Encryption) specification, JJWT will also support compression for JWS as well if you choose to use it. diff --git a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java index 4ee413a89..ad2f94059 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -478,13 +478,14 @@ public interface JwtBuilder> extends ClaimsMutator { T signWith(SignatureAlgorithm alg, Key key) throws InvalidKeyException; /** - *

    Deprecation Notice

    + *

    Deprecation Notice

    + * *

    This has been deprecated since JJWT_RELEASE_VERSION. Use * {@link #signWith(Key, io.jsonwebtoken.security.SignatureAlgorithm)} instead.. Standard JWA algorithms * are represented as instances of this new interface in the {@link io.jsonwebtoken.security.SignatureAlgorithms} * enum class.

    * - * Signs the constructed JWT with the specified key using the specified algorithm, producing a JWS. + *

    Signs the constructed JWT with the specified key using the specified algorithm, producing a JWS.

    * *

    It is typically recommended to call the {@link #signWith(Key)} instead for simplicity. * However, this method can be useful if the recommended algorithm heuristics do not meet your needs or if diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index e8eebaf80..785010b84 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -75,10 +75,10 @@ public static SecretKey hmacShaKeyFor(byte[] bytes) throws WeakKeyException { } /** - *

    Deprecation Notice

    + *

    Deprecation Notice

    * *

    As of JJWT JJWT_RELEASE_VERSION, symmetric (secret) key algorithm instances can generate a key of suitable - * length for that specific algorithm by calling their {@code keyBuilder()} method directly. For example: + * length for that specific algorithm by calling their {@code keyBuilder()} method directly. For example:

    * *
    
          * {@link SignatureAlgorithms#HS256}.keyBuilder().build();
    @@ -137,7 +137,7 @@ public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) thr
         }
     
         /**
    -     * 

    Deprecation Notice

    + *

    Deprecation Notice

    * *

    As of JJWT JJWT_RELEASE_VERSION, asymmetric key algorithm instances can generate KeyPairs of suitable strength * for that specific algorithm by calling their {@code generateKeyPair()} method directly. For example:

    @@ -153,7 +153,7 @@ public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) thr *

    Call those methods as needed instead of this {@code keyPairFor} helper method. This helper method will be * removed before the 1.0 final release.

    * - *

    Previous Documentation

    + *

    Previous Documentation

    * *

    Returns a new {@link KeyPair} suitable for use with the specified asymmetric algorithm.

    * diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy index 4d79032d2..2e5cb0e1f 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy @@ -14,6 +14,9 @@ import java.nio.charset.StandardCharsets import java.security.* import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey +import java.security.spec.ECParameterSpec +import java.security.spec.ECPoint +import java.security.spec.EllipticCurve import java.security.spec.X509EncodedKeySpec import static org.junit.Assert.* @@ -66,18 +69,16 @@ class DefaultEllipticCurveSignatureAlgorithmTest { @Test void testSignWithWeakKey() { - def gen = KeyPairGenerator.getInstance("EC") - gen.initialize(192) //too week for any JWA EC algorithm - def pair = gen.generateKeyPair() - - def request = new DefaultSignatureRequest(null, null, new byte[1], pair.getPrivate()) algs().each { + BigInteger order = BigInteger.ONE + ECParameterSpec spec = new ECParameterSpec(new EllipticCurve(new TestECField(), BigInteger.ONE, BigInteger.ONE), new ECPoint(BigInteger.ONE, BigInteger.ONE), order, 1) + ECPrivateKey priv = new TestECPrivateKey(params: spec) + def request = new DefaultSignatureRequest(null, null, new byte[1], priv) try { it.sign(request) } catch (InvalidKeyException expected) { - def keyOrderBitLength = pair.getPublic().getParams().getOrder().bitLength() String msg = "The provided Elliptic Curve signing key's size (aka Order bit length) is " + - "${Bytes.bitsMsg(keyOrderBitLength)}, but the '${it.getId()}' algorithm requires EC Keys with " + + "${Bytes.bitsMsg(order.bitLength())}, but the '${it.getId()}' algorithm requires EC Keys with " + "${Bytes.bitsMsg(it.orderBitLength)} per " + "[RFC 7518, Section 3.4](https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)." as String assertEquals msg, expected.getMessage() @@ -138,17 +139,16 @@ class DefaultEllipticCurveSignatureAlgorithmTest { @Test void testVerifyWithWeakKey() { - def gen = KeyPairGenerator.getInstance("EC") - gen.initialize(192) //too week for any JWA EC algorithm - def pair = gen.generateKeyPair() - def request = new DefaultVerifySignatureRequest(null, null, new byte[1], pair.getPublic(), new byte[1]) algs().each { + BigInteger order = BigInteger.ONE + ECParameterSpec spec = new ECParameterSpec(new EllipticCurve(new TestECField(), BigInteger.ONE, BigInteger.ONE), new ECPoint(BigInteger.ONE, BigInteger.ONE), order, 1) + ECPublicKey pub = new TestECPublicKey(params: spec) + def request = new DefaultVerifySignatureRequest(null, null, new byte[1], pub, new byte[1]) try { it.verify(request) } catch (InvalidKeyException expected) { - def keyOrderBitLength = pair.getPublic().getParams().getOrder().bitLength() String msg = "The provided Elliptic Curve verification key's size (aka Order bit length) is " + - "${Bytes.bitsMsg(keyOrderBitLength)}, but the '${it.getId()}' algorithm requires EC Keys with " + + "${Bytes.bitsMsg(order.bitLength())}, but the '${it.getId()}' algorithm requires EC Keys with " + "${Bytes.bitsMsg(it.orderBitLength)} per " + "[RFC 7518, Section 3.4](https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)." as String assertEquals msg, expected.getMessage() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy index db891d9f0..ab915b939 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy @@ -1,6 +1,5 @@ package io.jsonwebtoken.impl.security - import io.jsonwebtoken.security.SignatureAlgorithms import org.junit.Test @@ -8,14 +7,12 @@ import java.security.Key import java.security.KeyPair import java.security.PublicKey import java.security.interfaces.DSAPublicKey -import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey -import java.security.spec.ECParameterSpec -import java.security.spec.ECPoint -import static org.junit.Assert.* +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail class KeyPairsTest { @@ -112,59 +109,4 @@ class KeyPairsTest { } } } - - private static class TestECPublicKey implements ECPublicKey { - @Override - ECPoint getW() { - return null - } - - @Override - String getAlgorithm() { - return null - } - - @Override - String getFormat() { - return null - } - - @Override - byte[] getEncoded() { - return new byte[0] - } - - @Override - ECParameterSpec getParams() { - return null - } - } - - private static class TestECPrivateKey implements ECPrivateKey { - @Override - BigInteger getS() { - return null - } - - @Override - String getAlgorithm() { - return null - } - - @Override - String getFormat() { - return null - } - - @Override - byte[] getEncoded() { - return new byte[0] - } - - @Override - ECParameterSpec getParams() { - return null - } - } - } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECField.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECField.groovy new file mode 100644 index 000000000..e4626b41c --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECField.groovy @@ -0,0 +1,13 @@ +package io.jsonwebtoken.impl.security + +import java.security.spec.ECField + +class TestECField implements ECField { + + int fieldSize + + @Override + int getFieldSize() { + return fieldSize + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECKey.groovy new file mode 100644 index 000000000..113d44c07 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECKey.groovy @@ -0,0 +1,14 @@ +package io.jsonwebtoken.impl.security + +import java.security.interfaces.ECKey +import java.security.spec.ECParameterSpec + +class TestECKey extends TestKey implements ECKey { + + ECParameterSpec params + + @Override + ECParameterSpec getParams() { + return params + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECPrivateKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECPrivateKey.groovy new file mode 100644 index 000000000..f43739443 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECPrivateKey.groovy @@ -0,0 +1,13 @@ +package io.jsonwebtoken.impl.security + +import java.security.interfaces.ECPrivateKey + +class TestECPrivateKey extends TestECKey implements ECPrivateKey { + + BigInteger s + + @Override + BigInteger getS() { + return s + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECPublicKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECPublicKey.groovy new file mode 100644 index 000000000..5a6fd65dc --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECPublicKey.groovy @@ -0,0 +1,14 @@ +package io.jsonwebtoken.impl.security + +import java.security.interfaces.ECPublicKey +import java.security.spec.ECPoint + +class TestECPublicKey extends TestECKey implements ECPublicKey { + + ECPoint w + + @Override + ECPoint getW() { + return w + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKey.groovy new file mode 100644 index 000000000..bf1198a16 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKey.groovy @@ -0,0 +1,25 @@ +package io.jsonwebtoken.impl.security + +import java.security.Key + +class TestKey implements Key { + + String algorithm + String format + byte[] encoded + + @Override + String getAlgorithm() { + return algorithm + } + + @Override + String getFormat() { + return format + } + + @Override + byte[] getEncoded() { + return encoded + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestSecretKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestSecretKey.groovy index 991d2b7ce..255ce4a9c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestSecretKey.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestSecretKey.groovy @@ -2,24 +2,5 @@ package io.jsonwebtoken.impl.security import javax.crypto.SecretKey -class TestSecretKey implements SecretKey { - - private String algorithm - private String format - private byte[] encoded - - @Override - String getAlgorithm() { - return this.algorithm - } - - @Override - String getFormat() { - return this.format - } - - @Override - byte[] getEncoded() { - return this.encoded - } +class TestSecretKey extends TestKey implements SecretKey { } diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy index 005355f77..a311ec151 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy @@ -176,13 +176,21 @@ class KeysTest { PublicKey pub = pair.getPublic() assert pub instanceof ECPublicKey assertEquals "EC", pub.algorithm - assertEquals jdkParamName, pub.params.name + if (pub.params.hasProperty('name')) { // JDK <= 14 + assertEquals jdkParamName, pub.params.name + } else { // JDK >= 15 + assertEquals asn1oid, pub.params.nameAndAliases[0] + } assertEquals alg.minKeyLength, pub.params.order.bitLength() PrivateKey priv = pair.getPrivate() assert priv instanceof ECPrivateKey assertEquals "EC", priv.algorithm - assertEquals jdkParamName, priv.params.name + if (priv.params.hasProperty('name')) { // JDK <= 14 + assertEquals jdkParamName, priv.params.name + } else { // JDK >= 15 + assertEquals asn1oid, priv.params.nameAndAliases[0] + } assertEquals alg.minKeyLength, priv.params.order.bitLength() } else { @@ -228,13 +236,21 @@ class KeysTest { PublicKey pub = pair.getPublic() assert pub instanceof ECPublicKey assertEquals "EC", pub.algorithm - assertEquals jdkParamName, pub.params.name + if (pub.params.hasProperty('name')) { // JDK <= 14 + assertEquals jdkParamName, pub.params.name + } else { // JDK >= 15 + assertEquals asn1oid, pub.params.nameAndAliases[0] + } assertEquals alg.orderBitLength, pub.params.order.bitLength() PrivateKey priv = pair.getPrivate() assert priv instanceof ECPrivateKey assertEquals "EC", priv.algorithm - assertEquals jdkParamName, priv.params.name + if (priv.params.hasProperty('name')) { // JDK <= 14 + assertEquals jdkParamName, priv.params.name + } else { // JDK >= 15 + assertEquals asn1oid, priv.params.nameAndAliases[0] + } assertEquals alg.orderBitLength, priv.params.order.bitLength() } else { From 3928a7339dd9834299170c59496efdba36ac5d0e Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 29 Apr 2022 23:53:19 -0400 Subject: [PATCH 26/75] JavaDoc fixes, test additions to work on JDK 7 and JDK >= 15 --- README.md | 33 +++++++++++++++++-- .../security/AesGcmKeyAlgorithmTest.groovy | 13 ++++++++ .../impl/security/RFC7517AppendixCTest.groovy | 20 ++++++++--- .../security/KeyAlgorithmsTest.groovy | 2 ++ 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9e782ac79..b40da8021 100644 --- a/README.md +++ b/README.md @@ -107,11 +107,40 @@ enforcement. * PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-3841 * PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-5121 - 1. Requires JDK 11 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + 1. Requires Java 11 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + * Creating, parsing and decrypting encrypted compact JWTs (aka JWEs) with all standard JWE encryption algorithms: + * A128CBC-HS256: AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm, as defined in [RFC 7518, Section 5.2.3](https://datatracker.ietf.org/doc/html/rfc7518#section-5.2.3) + * A192CBC-HS384: AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm, as defined in [RFC 7518, Section 5.2.4](https://datatracker.ietf.org/doc/html/rfc7518#section-5.2.4) + * A256CBC-HS512: AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm, as defined in [RFC 7518, Section 5.2.5](https://datatracker.ietf.org/doc/html/rfc7518#section-5.2.5) + * A128GCM: AES GCM using 128-bit key2 + * A192GCM: AES GCM using 192-bit key2 + * A256GCM: AES GCM using 256-bit key2 + + 2. Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + * All Key Management Algorithms for obtaining JWE encryption and decryption keys: + * RSA1_5: RSAES-PKCS1-v1_5 + * RSA-OAEP: RSAES OAEP using default parameters + * RSA-OAEP-256: RSAES OAEP using SHA-256 and MGF1 with SHA-256 + * A128KW: AES Key Wrap with default initial value using 128-bit key + * A192KW: AES Key Wrap with default initial value using 192-bit key + * A256KW: AES Key Wrap with default initial value using 256-bit key + * dir: Direct use of a shared symmetric key as the CEK + * ECDH-ES: Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF + * ECDH-ES+A128KW: ECDH-ES using Concat KDF and CEK wrapped with "A128KW" + * ECDH-ES+A192KW: ECDH-ES using Concat KDF and CEK wrapped with "A192KW" + * ECDH-ES+A256KW: ECDH-ES using Concat KDF and CEK wrapped with "A256KW" + * A128GCMKW: Key wrapping with AES GCM using 128-bit key3 + * A192GCMKW: Key wrapping with AES GCM using 192-bit key3 + * A256GCMKW: Key wrapping with AES GCM using 256-bit key3 + * PBES2-HS256+A128KW: PBES2 with HMAC SHA-256 and "A128KW" wrapping3 + * PBES2-HS384+A192KW: PBES2 with HMAC SHA-384 and "A192KW" wrapping3 + * PBES2-HS512+A256KW: PBES2 with HMAC SHA-512 and "A256KW" wrapping3 + + 3. Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. * Convenience enhancements beyond the specification such as * Body compression for any large JWT, not just JWEs * Claims assertions (requiring specific values) - * Claim POJO marshaling and unmarshaling when using a compatible JSON parser (e.g. Jackson) + * Claim POJO marshaling and unmarshaling when using a compatible JSON parser (e.g. Jackson) * Secure Key generation based on desired JWA algorithms * and more... diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy index d70cf907a..b4e856418 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy @@ -6,6 +6,7 @@ import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.impl.lang.CheckedFunction import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.lang.Arrays +import io.jsonwebtoken.lang.RuntimeEnvironment import io.jsonwebtoken.security.EncryptionAlgorithms import io.jsonwebtoken.security.SecretKeyBuilder import org.junit.Test @@ -13,11 +14,23 @@ import org.junit.Test import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec import java.nio.charset.StandardCharsets +import java.security.NoSuchAlgorithmException import static org.junit.Assert.* class AesGcmKeyAlgorithmTest { + // TODO: remove when we stop supporting JDK 7: + static { + //test to see if the alg we need is available (e.g. see if we're on JDK 7). If it's not available + // and we're on JDK 7, enable BouncyCastle + try { + Cipher.getInstance('AES/GCM/NoPadding') + } catch (NoSuchAlgorithmException e) { + RuntimeEnvironment.enableBouncyCastleIfPossible(); + } + } + /** * This tests asserts that our AeadAlgorithm implementation and the JCA 'AES/GCM/NoPadding' wrap algorithm * produce the exact same values. This should be the case when the transformation is identical, even though diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy index 24c514092..66fc44b79 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy @@ -6,17 +6,16 @@ import io.jsonwebtoken.Jwts import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.SerializationException import io.jsonwebtoken.io.Serializer -import io.jsonwebtoken.security.KeyRequest -import io.jsonwebtoken.security.Keys -import io.jsonwebtoken.security.PasswordKey -import io.jsonwebtoken.security.SecretKeyBuilder -import io.jsonwebtoken.security.SecurityRequest +import io.jsonwebtoken.lang.RuntimeEnvironment +import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.SecretKey +import javax.crypto.SecretKeyFactory import javax.crypto.spec.SecretKeySpec import java.nio.charset.StandardCharsets import java.security.Key +import java.security.NoSuchAlgorithmException import static org.junit.Assert.* @@ -26,6 +25,17 @@ import static org.junit.Assert.* @SuppressWarnings('SpellCheckingInspection') class RFC7517AppendixCTest { + // TODO: remove when we stop supporting JDK 7: + static { + //test to see if the alg we need is available (e.g. see if we're on JDK 7). If it's not available + // and we're on JDK 7, enable BouncyCastle + try { + SecretKeyFactory.getInstance('PBKDF2WithHmacSHA256') + } catch (NoSuchAlgorithmException e) { + RuntimeEnvironment.enableBouncyCastleIfPossible(); + } + } + private static final String rfcString(String s) { return s.replaceAll('[\\s]', '') } diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy index fb48afc65..091a8b6ab 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy @@ -2,6 +2,7 @@ package io.jsonwebtoken.security import io.jsonwebtoken.UnsupportedJwtException import io.jsonwebtoken.impl.security.Pbes2HsAkwAlgorithm +import org.junit.Ignore import org.junit.Test import java.security.Key @@ -83,6 +84,7 @@ class KeyAlgorithmsTest { } @Test + @Ignore // temporarily until we decide if this API will remain void testEstimateIterations() { // keep it super short so we don't hammer the test server or slow down the build too much: long desiredMillis = 50 From f53af057e962ef305ccbbe40ac09109a2c4fb00b Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 30 Apr 2022 14:10:14 -0400 Subject: [PATCH 27/75] Test adjustment to work on Java 7 --- .../test/groovy/io/jsonwebtoken/JwtsTest.groovy | 17 +++++++++++++---- .../impl/security/AesGcmKeyAlgorithmTest.groovy | 3 +-- .../impl/security/RFC7517AppendixCTest.groovy | 3 +-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 565e40072..d387a7eb4 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -25,24 +25,33 @@ import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer +import io.jsonwebtoken.lang.RuntimeEnvironment import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.Mac import javax.crypto.SecretKey +import javax.crypto.SecretKeyFactory import javax.crypto.spec.SecretKeySpec import java.nio.charset.Charset import java.nio.charset.StandardCharsets -import java.security.Key -import java.security.KeyPair -import java.security.PrivateKey -import java.security.PublicKey +import java.security.* import static org.junit.Assert.* class JwtsTest { + // TODO: remove when we stop supporting JDK 7: + static { + // 'PBKDF2WithHmacSHA256' is available on Java 8 and later. If we're on Java 7, we need to enable BC: + try { + SecretKeyFactory.getInstance('PBKDF2WithHmacSHA256') + } catch (NoSuchAlgorithmException e) { + RuntimeEnvironment.enableBouncyCastleIfPossible(); + } + } + private static Date now() { return dateWithOnlySecondPrecision(System.currentTimeMillis()) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy index b4e856418..ab932eaa1 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy @@ -22,8 +22,7 @@ class AesGcmKeyAlgorithmTest { // TODO: remove when we stop supporting JDK 7: static { - //test to see if the alg we need is available (e.g. see if we're on JDK 7). If it's not available - // and we're on JDK 7, enable BouncyCastle + // 'GCM' is available on Java 8 and later. If we're on Java 7, we need to enable BC: try { Cipher.getInstance('AES/GCM/NoPadding') } catch (NoSuchAlgorithmException e) { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy index 66fc44b79..cdb0d4206 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy @@ -27,8 +27,7 @@ class RFC7517AppendixCTest { // TODO: remove when we stop supporting JDK 7: static { - //test to see if the alg we need is available (e.g. see if we're on JDK 7). If it's not available - // and we're on JDK 7, enable BouncyCastle + // 'PBKDF2WithHmacSHA256' is available on Java 8 and later. If we're on Java 7, we need to enable BC: try { SecretKeyFactory.getInstance('PBKDF2WithHmacSHA256') } catch (NoSuchAlgorithmException e) { From 89214c5b3790996869178f602fc3f615186b8901 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 30 Apr 2022 15:15:35 -0400 Subject: [PATCH 28/75] Test adjustment to work on Java 7 --- .../impl/security/AesAlgorithm.java | 2 +- .../DefaultRsaSignatureAlgorithm.java | 2 +- .../impl/security/Pbes2HsAkwAlgorithm.java | 12 ++++++++ .../jsonwebtoken/impl/security/Providers.java | 18 +++++------ .../groovy/io/jsonwebtoken/JwtsTest.groovy | 17 +++-------- .../security/AbstractJwkBuilderTest.groovy | 7 +++-- .../security/AesGcmKeyAlgorithmTest.groovy | 30 +++++++++++-------- .../impl/security/ProvidersTest.groovy | 4 +-- .../security/ProvidersWithoutBCTest.groovy | 2 +- .../impl/security/RFC7517AppendixCTest.groovy | 13 -------- 10 files changed, 51 insertions(+), 56 deletions(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java index de5c038ac..c88a5c753 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java @@ -52,7 +52,7 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements KeyBuilderSupplie // GCM mode only available on JDK 8 and later, so enable BC as a backup provider if necessary for <= JDK 7: // TODO: remove when dropping JDK 7: if (this.gcm) { - setProvider(Providers.getBouncyCastle(Conditions.notExists(new CheckedSupplier() { + setProvider(Providers.findBouncyCastle(Conditions.notExists(new CheckedSupplier() { @Override public Cipher get() throws Exception { return Cipher.getInstance(jcaTransformation); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java index a61a39488..5bbe59b44 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java @@ -55,7 +55,7 @@ public DefaultRsaSignatureAlgorithm(int digestBitLength, int preferredKeyBitLeng public DefaultRsaSignatureAlgorithm(int digestBitLength, int preferredKeyBitLength, int pssSaltBitLength) { this("PS" + digestBitLength, PSS_JCA_NAME, preferredKeyBitLength, pssParamFromSaltBitLength(pssSaltBitLength)); // PSS is not available natively until JDK 11, so try to load BC as a backup provider if possible on <= JDK 10: - setProvider(Providers.getBouncyCastle(Conditions.notExists(new CheckedSupplier() { + setProvider(Providers.findBouncyCastle(Conditions.notExists(new CheckedSupplier() { @Override public Signature get() throws Exception { return Signature.getInstance(PSS_JCA_NAME); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java index 497106937..d52e32eba 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java @@ -4,6 +4,8 @@ import io.jsonwebtoken.impl.DefaultJweHeader; import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.CheckedSupplier; +import io.jsonwebtoken.impl.lang.Conditions; import io.jsonwebtoken.impl.lang.ValueGetter; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.DecryptionKeyRequest; @@ -95,6 +97,16 @@ private Pbes2HsAkwAlgorithm(int hashBitLength, KeyAlgorithm() { + @Override + public SecretKeyFactory get() throws Exception { + return SecretKeyFactory.getInstance(getJcaName()); + } + }))); } // protected visibility for testing diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Providers.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Providers.java index 90c1bda1f..10925906b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/Providers.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Providers.java @@ -19,7 +19,7 @@ final class Providers { private Providers() { } - private static Provider getBouncyCastleProviderIfPossible() { + private static Provider findBouncyCastle() { if (!BOUNCY_CASTLE_AVAILABLE) { return null; } @@ -47,23 +47,23 @@ private static Provider getBouncyCastleProviderIfPossible() { /** * Returns the BouncyCastle provider if and only if the specified Condition evaluates to {@code true} * and BouncyCastle is available. Returns {@code null} otherwise. - *

    - * If the condition evaluates to true and the JVM runtime already has BouncyCastle registered + * + *

    If the condition evaluates to true and the JVM runtime already has BouncyCastle registered * (e.g. {@code Security.addProvider(bcProvider)}, that Provider instance will be found and returned. * If an existing BC provider is not found, a new BC instance will be created, cached for future reference, - * and returned. - *

    - * If a new BC provider is created and returned, it is not registered in the JVM via + * and returned.

    + * + *

    If a new BC provider is created and returned, it is not registered in the JVM via * {@code Security.addProvider} to ensure JJWT doesn't interfere with the application security provider - * configuration and/or expectations. + * configuration and/or expectations.

    * * @param c condition to evaluate * @return any available BouncyCastle Provider if {@code c} evaluates to true, or {@code null} if either * {@code c} evaluates to false, or BouncyCastle is not available. */ - public static Provider getBouncyCastle(Condition c) { + public static Provider findBouncyCastle(Condition c) { if (c.test()) { - return getBouncyCastleProviderIfPossible(); + return findBouncyCastle(); } return null; } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index d387a7eb4..565e40072 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -25,33 +25,24 @@ import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer -import io.jsonwebtoken.lang.RuntimeEnvironment import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.Mac import javax.crypto.SecretKey -import javax.crypto.SecretKeyFactory import javax.crypto.spec.SecretKeySpec import java.nio.charset.Charset import java.nio.charset.StandardCharsets -import java.security.* +import java.security.Key +import java.security.KeyPair +import java.security.PrivateKey +import java.security.PublicKey import static org.junit.Assert.* class JwtsTest { - // TODO: remove when we stop supporting JDK 7: - static { - // 'PBKDF2WithHmacSHA256' is available on Java 8 and later. If we're on Java 7, we need to enable BC: - try { - SecretKeyFactory.getInstance('PBKDF2WithHmacSHA256') - } catch (NoSuchAlgorithmException e) { - RuntimeEnvironment.enableBouncyCastleIfPossible(); - } - } - private static Date now() { return dateWithOnlySecondPrecision(System.currentTimeMillis()) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy index 34d0eb6b7..98d50366c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy @@ -1,6 +1,6 @@ package io.jsonwebtoken.impl.security - +import io.jsonwebtoken.impl.lang.Conditions import io.jsonwebtoken.security.Jwk import io.jsonwebtoken.security.Jwks import io.jsonwebtoken.security.MalformedKeyException @@ -9,7 +9,8 @@ import org.junit.Test import javax.crypto.SecretKey -import static org.junit.Assert.* +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNotNull class AbstractJwkBuilderTest { @@ -107,7 +108,7 @@ class AbstractJwkBuilderTest { @Test void testProvider() { - def provider = Providers.getBouncyCastleProviderIfPossible() + def provider = Providers.findBouncyCastle(Conditions.TRUE) def jwk = builder().setProvider(provider).build() assertEquals 'oct', jwk.getType() } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy index ab932eaa1..b58b315cf 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy @@ -4,32 +4,24 @@ import io.jsonwebtoken.MalformedJwtException import io.jsonwebtoken.impl.DefaultJweHeader import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.impl.lang.CheckedFunction +import io.jsonwebtoken.impl.lang.CheckedSupplier +import io.jsonwebtoken.impl.lang.Conditions import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.lang.Arrays -import io.jsonwebtoken.lang.RuntimeEnvironment import io.jsonwebtoken.security.EncryptionAlgorithms import io.jsonwebtoken.security.SecretKeyBuilder import org.junit.Test import javax.crypto.Cipher +import javax.crypto.SecretKeyFactory import javax.crypto.spec.GCMParameterSpec import java.nio.charset.StandardCharsets -import java.security.NoSuchAlgorithmException +import java.security.Provider import static org.junit.Assert.* class AesGcmKeyAlgorithmTest { - // TODO: remove when we stop supporting JDK 7: - static { - // 'GCM' is available on Java 8 and later. If we're on Java 7, we need to enable BC: - try { - Cipher.getInstance('AES/GCM/NoPadding') - } catch (NoSuchAlgorithmException e) { - RuntimeEnvironment.enableBouncyCastleIfPossible(); - } - } - /** * This tests asserts that our AeadAlgorithm implementation and the JCA 'AES/GCM/NoPadding' wrap algorithm * produce the exact same values. This should be the case when the transformation is identical, even though @@ -46,7 +38,19 @@ class AesGcmKeyAlgorithmTest { def kek = alg.keyBuilder().build() def cek = alg.keyBuilder().build() - JcaTemplate template = new JcaTemplate("AES/GCM/NoPadding", null) + final String jcaName = "AES/GCM/NoPadding" + + // AES/GCM/NoPadding is only available on JDK 8 and later, so enable BC as a backup provider if + // necessary for <= JDK 7: + // TODO: remove when dropping Java 7 support: + Provider provider = Providers.findBouncyCastle(Conditions.notExists(new CheckedSupplier() { + @Override + SecretKeyFactory get() throws Exception { + return SecretKeyFactory.getInstance(jcaName); + } + })) + + JcaTemplate template = new JcaTemplate(jcaName, provider) byte[] jcaResult = template.execute(Cipher.class, new CheckedFunction() { @Override byte[] apply(Cipher cipher) throws Exception { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy index 5c4174f3b..4171a27e5 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy @@ -61,7 +61,7 @@ class ProvidersTest { assertTrue bcRegistered() // ensure it exists in the system as expected //now ensure that we find it and cache it: - def returned = Providers.getBouncyCastle(Conditions.TRUE) + def returned = Providers.findBouncyCastle(Conditions.TRUE) assertSame bc, returned assertSame bc, Providers.BC_PROVIDER.get() // ensure cached for future lookup @@ -76,7 +76,7 @@ class ProvidersTest { // ensure we can create one and cache it, *without* modifying the system JVM: //now ensure that we find it and cache it: - def returned = Providers.getBouncyCastle(Conditions.TRUE) + def returned = Providers.findBouncyCastle(Conditions.TRUE) assertNotNull returned assertSame Providers.BC_PROVIDER.get(), returned //ensure cached for future lookup assertFalse bcRegistered() //ensure we don't alter the system environment diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersWithoutBCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersWithoutBCTest.groovy index b3e060069..e12664afe 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersWithoutBCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersWithoutBCTest.groovy @@ -28,7 +28,7 @@ class ProvidersWithoutBCTest { mockStatic(Classes) expect(Classes.isAvailable(eq("org.bouncycastle.jce.provider.BouncyCastleProvider"))).andReturn(Boolean.FALSE).anyTimes() replay Classes - assertNull Providers.getBouncyCastle(Conditions.TRUE) // one should not be created/exist + assertNull Providers.findBouncyCastle(Conditions.TRUE) // one should not be created/exist verify Classes assertFalse ProvidersTest.bcRegistered() // nothing should be in the environment } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy index cdb0d4206..14bc60ed3 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy @@ -6,16 +6,13 @@ import io.jsonwebtoken.Jwts import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.SerializationException import io.jsonwebtoken.io.Serializer -import io.jsonwebtoken.lang.RuntimeEnvironment import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.SecretKey -import javax.crypto.SecretKeyFactory import javax.crypto.spec.SecretKeySpec import java.nio.charset.StandardCharsets import java.security.Key -import java.security.NoSuchAlgorithmException import static org.junit.Assert.* @@ -25,16 +22,6 @@ import static org.junit.Assert.* @SuppressWarnings('SpellCheckingInspection') class RFC7517AppendixCTest { - // TODO: remove when we stop supporting JDK 7: - static { - // 'PBKDF2WithHmacSHA256' is available on Java 8 and later. If we're on Java 7, we need to enable BC: - try { - SecretKeyFactory.getInstance('PBKDF2WithHmacSHA256') - } catch (NoSuchAlgorithmException e) { - RuntimeEnvironment.enableBouncyCastleIfPossible(); - } - } - private static final String rfcString(String s) { return s.replaceAll('[\\s]', '') } From 46e5e9c5e9227d4419e22d97c76938ec6f6b1b39 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 1 May 2022 15:55:21 -0400 Subject: [PATCH 29/75] code coverage work cont'd --- .../main/java/io/jsonwebtoken/JweHeader.java | 103 ++-------------- .../main/java/io/jsonwebtoken/JwsHeader.java | 57 +-------- .../java/io/jsonwebtoken/ProtectedHeader.java | 70 +++++++++++ .../jsonwebtoken/security/KeyAlgorithms.java | 2 + .../impl/AbstractProtectedHeader.java | 113 ++++++++++++++++++ .../io/jsonwebtoken/impl/DefaultClaims.java | 36 +++--- .../io/jsonwebtoken/impl/DefaultHeader.java | 105 ++-------------- .../jsonwebtoken/impl/DefaultJweHeader.java | 21 ++-- .../jsonwebtoken/impl/DefaultJwsHeader.java | 9 +- .../jsonwebtoken/impl/DefaultJwtParser.java | 4 +- .../java/io/jsonwebtoken/impl/JwtMap.java | 45 +++---- .../java/io/jsonwebtoken/impl/io/Codec.java | 2 +- .../impl/lang/EncodedObjectConverter.java | 2 +- .../impl/security/DefaultJwkContext.java | 50 ++++---- .../impl/AbstractProtectedHeaderTest.groovy | 26 ++++ .../impl/DefaultHeaderTest.groovy | 6 + .../impl/DefaultJweHeaderTest.groovy | 10 +- .../impl/DefaultJwsHeaderTest.groovy | 9 +- .../io/jsonwebtoken/impl/IdLocatorTest.groovy | 79 ++++++++++++ .../io/jsonwebtoken/impl/JwtMapTest.groovy | 27 ++++- .../security/DefaultJwkContextTest.groovy | 22 ++++ 21 files changed, 472 insertions(+), 326 deletions(-) create mode 100644 api/src/main/java/io/jsonwebtoken/ProtectedHeader.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/IdLocatorTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy diff --git a/api/src/main/java/io/jsonwebtoken/JweHeader.java b/api/src/main/java/io/jsonwebtoken/JweHeader.java index 41844de48..f0c726e34 100644 --- a/api/src/main/java/io/jsonwebtoken/JweHeader.java +++ b/api/src/main/java/io/jsonwebtoken/JweHeader.java @@ -15,84 +15,23 @@ */ package io.jsonwebtoken; -import io.jsonwebtoken.security.PublicJwk; - -import java.net.URI; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Set; - /** * A JWE header. * * @since JJWT_RELEASE_VERSION */ -public interface JweHeader extends Header { - - /** - * JWE Algorithm Header name: the string literal alg - */ - String ALGORITHM = "alg"; - - /** - * JWE Encryption Algorithm Header name: the string literal enc - */ - String ENCRYPTION_ALGORITHM = "enc"; - - /** - * JWE Compression Algorithm Header name: the string literal zip - */ - String COMPRESSION_ALGORITHM = "zip"; - - /** - * JWE JWK Set URL Header name: the string literal jku - */ - String JWK_SET_URL = "jku"; - - /** - * JWE JSON Web Key Header name: the string literal jwk - */ - String JSON_WEB_KEY = "jwk"; - - /** - * JWE Key ID Header name: the string literal kid - */ - String KEY_ID = "kid"; - - /** - * JWE X.509 URL Header name: the string literal x5u - */ - String X509_URL = "x5u"; - - /** - * JWE X.509 Certificate Chain Header name: the string literal x5c - */ - String X509_CERT_CHAIN = "x5c"; +public interface JweHeader extends ProtectedHeader { /** - * JWE X.509 Certificate SHA-1 Thumbprint Header name: the string literal x5t - */ - String X509_CERT_SHA1_THUMBPRINT = "x5t"; - - /** - * JWE X.509 Certificate SHA-256 Thumbprint Header name: the string literal x5t#S256 - */ - String X509_CERT_SHA256_THUMBPRINT = "x5t#S256"; - - /** - * JWE Critical Header name: the string literal crit - */ - String CRITICAL = "crit"; - - /** - * Returns the JWE enc (Encryption + * Returns the JWE {@code enc} (Encryption * Algorithm) header value or {@code null} if not present. + * *

    The JWE {@code enc} (encryption algorithm) Header Parameter identifies the content encryption algorithm * used to perform authenticated encryption on the plaintext to produce the ciphertext and the JWE * {@code Authentication Tag}.

    * * @return the JWE {@code enc} (Encryption Algorithm) header value or {@code null} if not present. This will - * always be {@code non-null} on validly constructed JWE instances, but could be {@code null} during construction. + * always be {@code non-null} on validly-constructed JWE instances, but could be {@code null} during construction. */ String getEncryptionAlgorithm(); @@ -109,45 +48,27 @@ public interface JweHeader extends Header { // */ // JweHeader setEncryptionAlgorithm(String enc); - URI getJwkSetUrl(); - JweHeader setJwkSetUrl(URI uri); - - PublicJwk getJwk(); - JweHeader setJwk(PublicJwk jwk); - - String getKeyId(); - JweHeader setKeyId(String kid); - - URI getX509Url(); - JweHeader setX509Url(URI uri); - - List getX509CertificateChain(); - JweHeader setX509CertificateChain(List chain); - - byte[] getX509CertificateSha1Thumbprint(); - JweHeader setX509CertificateSha1Thumbprint(byte[] thumbprint); - JweHeader computeX509CertificateSha1Thumbprint(); - - byte[] getX509CertificateSha256Thumbprint(); - JweHeader setX509CertificateSha256Thumbprint(byte[] thumbprint); - JweHeader computeX509CertificateSha256Thumbprint(); - - Set getCritical(); - JweHeader setCritical(Set crit); - Integer getPbes2Count(); + JweHeader setPbes2Count(int count); byte[] getPbes2Salt(); + JweHeader setPbes2Salt(byte[] salt); byte[] getAgreementPartyUInfo(); + String getAgreementPartyUInfoString(); + JweHeader setAgreementPartyUInfo(byte[] info); + JweHeader setAgreementPartyUInfo(String info); byte[] getAgreementPartyVInfo(); + String getAgreementPartyVInfoString(); + JweHeader setAgreementPartyVInfo(byte[] info); + JweHeader setAgreementPartyVInfo(String info); } diff --git a/api/src/main/java/io/jsonwebtoken/JwsHeader.java b/api/src/main/java/io/jsonwebtoken/JwsHeader.java index 29ffc8394..2a0561cf7 100644 --- a/api/src/main/java/io/jsonwebtoken/JwsHeader.java +++ b/api/src/main/java/io/jsonwebtoken/JwsHeader.java @@ -15,19 +15,12 @@ */ package io.jsonwebtoken; -import io.jsonwebtoken.security.PublicJwk; - -import java.net.URI; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Set; - /** * A JWS header. * * @since 0.1 */ -public interface JwsHeader extends Header { +public interface JwsHeader extends ProtectedHeader { /** * JWS Algorithm Header name: the string literal alg @@ -73,52 +66,4 @@ public interface JwsHeader extends Header { * JWS Critical Header name: the string literal crit */ String CRITICAL = "crit"; - - /** - * Returns the JWS - * kid (Key ID) header value or {@code null} if not present. - *

    The keyId header parameter is a hint indicating which key was used to secure the JWS. This parameter allows - * originators to explicitly signal a change of key to recipients. The structure of the keyId value is - * unspecified.

    - *

    When used with a JWK, the keyId value is used to match a JWK {@code keyId} parameter value.

    - * - * @return the JWS {@code kid} header value or {@code null} if not present. - */ - String getKeyId(); - - /** - * Sets the JWT - * kid (Key ID) header value. A {@code null} value will remove the property from the JSON map. - *

    The keyId header parameter is a hint indicating which key was used to secure the JWS. This parameter allows - * originators to explicitly signal a change of key to recipients. The structure of the keyId value is - * unspecified.

    - *

    When used with a JWK, the keyId value is used to match a JWK {@code keyId} parameter value.

    - * - * @param kid the JWS {@code kid} header value or {@code null} to remove the property from the JSON map. - * @return the {@code Header} instance for method chaining. - */ - JwsHeader setKeyId(String kid); - - URI getJwkSetUrl(); - JwsHeader setJwkSetUrl(URI uri); - - PublicJwk getJwk(); - JwsHeader setJwk(PublicJwk jwk); - - URI getX509Url(); - JwsHeader setX509Url(URI uri); - - List getX509CertificateChain(); - JwsHeader setX509CertificateChain(List chain); - - byte[] getX509CertificateSha1Thumbprint(); - JwsHeader setX509CertificateSha1Thumbprint(byte[] thumbprint); - JwsHeader computeX509CertificateSha1Thumbprint(); - - byte[] getX509CertificateSha256Thumbprint(); - JwsHeader setX509CertificateSha256Thumbprint(byte[] thumbprint); - JwsHeader computeX509CertificateSha256Thumbprint(); - - Set getCritical(); - JwsHeader setCritical(Set crit); } diff --git a/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java new file mode 100644 index 000000000..c00851481 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java @@ -0,0 +1,70 @@ +package io.jsonwebtoken; + +import io.jsonwebtoken.security.PublicJwk; + +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +/** + * A JWT header that is integrity protected, either by JWS digital signature or JWE AEAD encryption. + * + * @param The exact header subtype returned during mutation (setter) operations. + * @see JwsHeader + * @see JweHeader + * @since JJWT_RELEASE_VERSION + */ +public interface ProtectedHeader> extends Header { + + URI getJwkSetUrl(); + T setJwkSetUrl(URI uri); + + PublicJwk getJwk(); + T setJwk(PublicJwk jwk); + + /** + * Returns the JWT case-sensitive {@code kid} (Key ID) header value or {@code null} if not present. + * + *

    The keyId header parameter is a hint indicating which key was used to secure a JWS or JWE. This + * parameter allows originators to explicitly signal a change of key to recipients. The structure of the keyId + * value is unspecified. Its value is a case-sensitive string.

    + * + *

    When used with a JWK, the keyId value is used to match a JWK {@code keyId} parameter value.

    + * + * @return the case-sensitive {@code kid} header value or {@code null} if not present. + * @see JWS Key ID + * @see JWE Key ID + */ + String getKeyId(); + + /** + * Sets the JWT case-sensitive {@code kid} (Key ID) header value. A {@code null} value will remove the property + * from the JSON map. + * + *

    The keyId header parameter is a hint indicating which key was used to secure a JWS or JWE. This parameter + * allows originators to explicitly signal a change of key to recipients. The structure of the keyId value is + * unspecified. Its value MUST be a case-sensitive string.

    + * + *

    When used with a JWK, the keyId value is used to match a JWK {@code keyId} parameter value.

    + * + * @param kid the case-sensitive JWS {@code kid} header value or {@code null} to remove the property from the JSON map. + * @return the header instance for method chaining. + */ + T setKeyId(String kid); + + URI getX509Url(); + T setX509Url(URI uri); + + List getX509CertificateChain(); + T setX509CertificateChain(List chain); + + byte[] getX509CertificateSha1Thumbprint(); + T setX509CertificateSha1Thumbprint(byte[] thumbprint); + + byte[] getX509CertificateSha256Thumbprint(); + T setX509CertificateSha256Thumbprint(byte[] thumbprint); + + Set getCritical(); + T setCritical(Set crit); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index 2c05404b1..e8ac8b673 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -82,7 +82,9 @@ private static T forId0(String id) { public static final EcKeyAlgorithm ECDH_ES_A192KW = forId0("ECDH-ES+A192KW"); public static final EcKeyAlgorithm ECDH_ES_A256KW = forId0("ECDH-ES+A256KW"); + /* public static int estimateIterations(KeyAlgorithm alg, long desiredMillis) { return Classes.invokeStatic(BRIDGE_CLASS, "estimateIterations", ESTIMATE_ITERATIONS_ARG_TYPES, alg, desiredMillis); } + */ } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java new file mode 100644 index 000000000..63bc31d95 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java @@ -0,0 +1,113 @@ +package io.jsonwebtoken.impl; + +import io.jsonwebtoken.ProtectedHeader; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.impl.security.AbstractAsymmetricJwk; +import io.jsonwebtoken.impl.security.AbstractJwk; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.PublicJwk; + +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Header implementation satisfying shared JWS and JWE header parameter requirements. Header parameters specific to + * either JWE or JWS will be defined in respective subclasses. + * + * @param specific header type to return from mutation/setter methods for method chaining + * @since JJWT_RELEASE_VERSION + */ +public abstract class AbstractProtectedHeader> extends DefaultHeader implements ProtectedHeader { + + static final Field JKU = Fields.uri("jku", "JWK Set URL"); + @SuppressWarnings("rawtypes") + static final Field JWK = Fields.builder(PublicJwk.class).setId("jwk").setName("JSON Web Key").build(); + static final Field> CRIT = Fields.stringSet("crit", "Critical"); + + static final Set> FIELDS = Collections.concat(DefaultHeader.FIELDS, CRIT, JKU, JWK, AbstractJwk.KID, + AbstractAsymmetricJwk.X5U, AbstractAsymmetricJwk.X5C, AbstractAsymmetricJwk.X5T, AbstractAsymmetricJwk.X5T_S256); + + protected AbstractProtectedHeader(Set> fieldSet) { + super(fieldSet); + } + + protected AbstractProtectedHeader(Set> fieldSet, Map values) { + super(fieldSet, values); + } + + public String getKeyId() { + return idiomaticGet(AbstractJwk.KID); + } + + public T setKeyId(String kid) { + put(AbstractJwk.KID, kid); + return tthis(); + } + + public URI getJwkSetUrl() { + return idiomaticGet(JKU); + } + + public T setJwkSetUrl(URI uri) { + put(JKU, uri); + return tthis(); + } + + public PublicJwk getJwk() { + return idiomaticGet(JWK); + } + + public T setJwk(PublicJwk jwk) { + put(JWK, jwk); + return tthis(); + } + + public URI getX509Url() { + return idiomaticGet(AbstractAsymmetricJwk.X5U); + } + + public T setX509Url(URI uri) { + put(AbstractAsymmetricJwk.X5U, uri); + return tthis(); + } + + public List getX509CertificateChain() { + return idiomaticGet(AbstractAsymmetricJwk.X5C); + } + + public T setX509CertificateChain(List chain) { + put(AbstractAsymmetricJwk.X5C, chain); + return tthis(); + } + + public byte[] getX509CertificateSha1Thumbprint() { + return idiomaticGet(AbstractAsymmetricJwk.X5T); + } + + public T setX509CertificateSha1Thumbprint(byte[] thumbprint) { + put(AbstractAsymmetricJwk.X5T, thumbprint); + return tthis(); + } + + public byte[] getX509CertificateSha256Thumbprint() { + return idiomaticGet(AbstractAsymmetricJwk.X5T_S256); + } + + public T setX509CertificateSha256Thumbprint(byte[] thumbprint) { + put(AbstractAsymmetricJwk.X5T_S256, thumbprint); + return tthis(); + } + + public Set getCritical() { + return idiomaticGet(CRIT); + } + + public T setCritical(Set crit) { + put(CRIT, crit); + return tthis(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index f24b2ca15..5e3b56711 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -30,12 +30,12 @@ public class DefaultClaims extends JwtMap implements Claims { private static final String CONVERSION_ERROR_MSG = "Cannot convert existing claim value of type '%s' to desired type " + - "'%s'. JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. " + - "Anything more complex is expected to be already converted to your desired type by the JSON Deserializer " + - "implementation. You may specify a custom Deserializer for a JwtParser with the desired conversion " + - "configuration via the JwtParserBuilder.deserializeJsonWith() method. " + - "See https://github.com/jwtk/jjwt#custom-json-processor for more information. If using Jackson, you can " + - "specify custom claim POJO types as described in https://github.com/jwtk/jjwt#json-jackson-custom-types"; + "'%s'. JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. " + + "Anything more complex is expected to be already converted to your desired type by the JSON Deserializer " + + "implementation. You may specify a custom Deserializer for a JwtParser with the desired conversion " + + "configuration via the JwtParserBuilder.deserializeJsonWith() method. " + + "See https://github.com/jwtk/jjwt#custom-json-processor for more information. If using Jackson, you can " + + "specify custom claim POJO types as described in https://github.com/jwtk/jjwt#json-jackson-custom-types"; static final Field ISSUER = Fields.string(Claims.ISSUER, "Issuer"); static final Field SUBJECT = Fields.string(Claims.SUBJECT, "Subject"); @@ -46,7 +46,7 @@ public class DefaultClaims extends JwtMap implements Claims { static final Field JTI = Fields.string(Claims.ID, "JWT ID"); static final Set> FIELDS = Collections.>setOf( - ISSUER, SUBJECT, AUDIENCE, EXPIRATION, NOT_BEFORE, ISSUED_AT, JTI + ISSUER, SUBJECT, AUDIENCE, EXPIRATION, NOT_BEFORE, ISSUED_AT, JTI ); public DefaultClaims() { @@ -64,7 +64,7 @@ public String getIssuer() { @Override public Claims setIssuer(String iss) { - put(ISSUER.getId(), iss); + put(ISSUER, iss); return this; } @@ -75,7 +75,7 @@ public String getSubject() { @Override public Claims setSubject(String sub) { - put(SUBJECT.getId(), sub); + put(SUBJECT, sub); return this; } @@ -86,7 +86,7 @@ public String getAudience() { @Override public Claims setAudience(String aud) { - put(AUDIENCE.getId(), aud); + put(AUDIENCE, aud); return this; } @@ -97,7 +97,7 @@ public Date getExpiration() { @Override public Claims setExpiration(Date exp) { - put(EXPIRATION.getId(), exp); + put(EXPIRATION, exp); return this; } @@ -108,7 +108,7 @@ public Date getNotBefore() { @Override public Claims setNotBefore(Date nbf) { - put(NOT_BEFORE.getId(), nbf); + put(NOT_BEFORE, nbf); return this; } @@ -119,7 +119,7 @@ public Date getIssuedAt() { @Override public Claims setIssuedAt(Date iat) { - put(ISSUED_AT.getId(), iat); + put(ISSUED_AT, iat); return this; } @@ -130,7 +130,7 @@ public String getId() { @Override public Claims setId(String jti) { - put(JTI.getId(), jti); + put(JTI, jti); return this; } @@ -163,11 +163,11 @@ public T get(String claimName, Class requiredType) { private T castClaimValue(String name, Object value, Class requiredType) { if (value instanceof Long || value instanceof Integer || value instanceof Short || value instanceof Byte) { - long longValue = ((Number)value).longValue(); + long longValue = ((Number) value).longValue(); if (Long.class.equals(requiredType)) { value = longValue; } else if (Integer.class.equals(requiredType) && Integer.MIN_VALUE <= longValue && longValue <= Integer.MAX_VALUE) { - value = (int)longValue; + value = (int) longValue; } else if (requiredType == Short.class && Short.MIN_VALUE <= longValue && longValue <= Short.MAX_VALUE) { value = (short) longValue; } else if (requiredType == Byte.class && Byte.MIN_VALUE <= longValue && longValue <= Byte.MAX_VALUE) { @@ -176,9 +176,9 @@ private T castClaimValue(String name, Object value, Class requiredType) { } if (value instanceof Long && - (requiredType.equals(Integer.class) || requiredType.equals(Short.class) || requiredType.equals(Byte.class))) { + (requiredType.equals(Integer.class) || requiredType.equals(Short.class) || requiredType.equals(Byte.class))) { String msg = "Claim '" + name + "' value is too large or too small to be represented as a " + - requiredType.getName() + " instance (would cause numeric overflow)."; + requiredType.getName() + " instance (would cause numeric overflow)."; throw new RequiredTypeException(msg); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java index 04fbad4fd..d40582afb 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java @@ -18,15 +18,9 @@ import io.jsonwebtoken.Header; import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.impl.lang.Fields; -import io.jsonwebtoken.impl.security.AbstractAsymmetricJwk; -import io.jsonwebtoken.impl.security.AbstractJwk; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.PublicJwk; -import java.net.URI; -import java.security.cert.X509Certificate; -import java.util.List; import java.util.Map; import java.util.Set; @@ -39,14 +33,8 @@ public class DefaultHeader> extends JwtMap implements Header @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated // TODO: remove for 1.0.0: static final Field DEPRECATED_COMPRESSION_ALGORITHM = Fields.string(Header.DEPRECATED_COMPRESSION_ALGORITHM, "Deprecated Compression Algorithm"); - static final Field JKU = Fields.uri("jku", "JWK Set URL"); - @SuppressWarnings("rawtypes") - static final Field JWK = Fields.builder(PublicJwk.class).setId("jwk").setName("JSON Web Key").build(); - static final Field> CRIT = Fields.stringSet("crit", "Critical"); static final Set> FIELDS = Collections.>setOf(TYPE, CONTENT_TYPE, ALGORITHM, COMPRESSION_ALGORITHM); - static final Set> CHILD_FIELDS = Collections.concat(FIELDS, JKU, JWK, CRIT, AbstractJwk.KID, - AbstractAsymmetricJwk.X5U, AbstractAsymmetricJwk.X5C, AbstractAsymmetricJwk.X5T, AbstractAsymmetricJwk.X5T_S256); protected DefaultHeader(Set> fieldSet) { super(fieldSet); @@ -64,6 +52,11 @@ public DefaultHeader(Map map) { this(FIELDS, map); } + @Override + protected String getName() { + return "JWT header"; + } + @SuppressWarnings("unchecked") protected T tthis() { return (T) this; @@ -76,7 +69,7 @@ public String getType() { @Override public T setType(String typ) { - put(TYPE.getId(), typ); + put(TYPE, typ); return tthis(); } @@ -87,7 +80,7 @@ public String getContentType() { @Override public T setContentType(String cty) { - put(CONTENT_TYPE.getId(), cty); + put(CONTENT_TYPE, cty); return tthis(); } @@ -98,7 +91,7 @@ public String getAlgorithm() { @Override public T setAlgorithm(String alg) { - put(ALGORITHM.getId(), alg); + put(ALGORITHM, alg); return tthis(); } @@ -113,87 +106,7 @@ public String getCompressionAlgorithm() { @Override public T setCompressionAlgorithm(String compressionAlgorithm) { - put(COMPRESSION_ALGORITHM.getId(), compressionAlgorithm); - return tthis(); - } - - public String getKeyId() { - return idiomaticGet(AbstractJwk.KID); - } - - public T setKeyId(String kid) { - put(AbstractJwk.KID.getId(), kid); - return tthis(); - } - - public URI getJwkSetUrl() { - return idiomaticGet(JKU); - } - - public T setJwkSetUrl(URI uri) { - put(JKU.getId(), uri); - return tthis(); - } - - public PublicJwk getJwk() { - return idiomaticGet(JWK); - } - - public T setJwk(PublicJwk jwk) { - put(JWK.getId(), jwk); - return tthis(); - } - - public URI getX509Url() { - return idiomaticGet(AbstractAsymmetricJwk.X5U); - } - - public T setX509Url(URI uri) { - put(AbstractAsymmetricJwk.X5U.getId(), uri); - return tthis(); - } - - public List getX509CertificateChain() { - return idiomaticGet(AbstractAsymmetricJwk.X5C); - } - - public T setX509CertificateChain(List chain) { - put(AbstractAsymmetricJwk.X5C.getId(), chain); - return tthis(); - } - - public byte[] getX509CertificateSha1Thumbprint() { - return idiomaticGet(AbstractAsymmetricJwk.X5T); - } - - public T setX509CertificateSha1Thumbprint(byte[] thumbprint) { - put(AbstractAsymmetricJwk.X5T.getId(), thumbprint); - return tthis(); - } - - public T computeX509CertificateSha1Thumbprint() { - throw new UnsupportedOperationException("Not yet implemented."); - } - - public byte[] getX509CertificateSha256Thumbprint() { - return idiomaticGet(AbstractAsymmetricJwk.X5T_S256); - } - - public T setX509CertificateSha256Thumbprint(byte[] thumbprint) { - put(AbstractAsymmetricJwk.X5T_S256.getId(), thumbprint); - return tthis(); - } - - public T computeX509CertificateSha256Thumbprint() { - throw new UnsupportedOperationException("Not yet implemented."); - } - - public Set getCritical() { - return idiomaticGet(CRIT); - } - - public T setCritical(Set crit) { - put(CRIT.getId(), crit); + put(COMPRESSION_ALGORITHM, compressionAlgorithm); return tthis(); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java index 23953ab0a..34941dd13 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java @@ -12,9 +12,11 @@ import java.util.Set; /** + * Header implementation satisfying JWE header parameter requirements. + * * @since JJWT_RELEASE_VERSION */ -public class DefaultJweHeader extends DefaultHeader implements JweHeader { +public class DefaultJweHeader extends AbstractProtectedHeader implements JweHeader { static final Field ENCRYPTION_ALGORITHM = Fields.string("enc", "Encryption Algorithm"); public static final Field P2C = Fields.builder(Integer.class).setId("p2c").setName("PBES2 Count").build(); @@ -22,7 +24,7 @@ public class DefaultJweHeader extends DefaultHeader implements JweHea static final Field APU = Fields.bytes("apu", "Agreement PartyUInfo").build(); static final Field APV = Fields.bytes("apv", "Agreement PartyVInfo").build(); - static final Set> FIELDS = Collections.concat(CHILD_FIELDS, ENCRYPTION_ALGORITHM, P2C, P2S, APU, APV); + static final Set> FIELDS = Collections.concat(AbstractProtectedHeader.FIELDS, ENCRYPTION_ALGORITHM, P2C, P2S, APU, APV); public DefaultJweHeader() { super(FIELDS); @@ -32,6 +34,11 @@ public DefaultJweHeader(Map map) { super(FIELDS, map); } + @Override + protected String getName() { + return "JWE header"; + } + @Override public String getEncryptionAlgorithm() { return idiomaticGet(ENCRYPTION_ALGORITHM); @@ -39,7 +46,7 @@ public String getEncryptionAlgorithm() { // @Override // public JweHeader setEncryptionAlgorithm(String enc) { -// put(ENCRYPTION_ALGORITHM.getId(), enc); +// put(ENCRYPTION_ALGORITHM, enc); // return this; // } @@ -50,7 +57,7 @@ public Integer getPbes2Count() { @Override public JweHeader setPbes2Count(int count) { - put(P2C.getId(), count); + put(P2C, count); return this; } @@ -59,7 +66,7 @@ public byte[] getPbes2Salt() { } public JweHeader setPbes2Salt(byte[] salt) { - put(P2S.getId(), salt); + put(P2S, salt); return this; } @@ -76,7 +83,7 @@ public String getAgreementPartyUInfoString() { @Override public JweHeader setAgreementPartyUInfo(byte[] info) { - put(APU.getId(), info); + put(APU, info); return this; } @@ -99,7 +106,7 @@ public String getAgreementPartyVInfoString() { @Override public JweHeader setAgreementPartyVInfo(byte[] info) { - put(APV.getId(), info); + put(APV, info); return this; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java index b758ba411..3a1762cc2 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java @@ -21,9 +21,9 @@ import java.util.Map; import java.util.Set; -public class DefaultJwsHeader extends DefaultHeader implements JwsHeader { +public class DefaultJwsHeader extends AbstractProtectedHeader implements JwsHeader { - static final Set> FIELDS = CHILD_FIELDS; + static final Set> FIELDS = AbstractProtectedHeader.FIELDS; //same public DefaultJwsHeader() { super(FIELDS); @@ -32,4 +32,9 @@ public DefaultJwsHeader() { public DefaultJwsHeader(Map map) { super(FIELDS, map); } + + @Override + protected String getName() { + return "JWS header"; + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index c86a307ad..88b693b98 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -134,11 +134,11 @@ private static , R extends Identifiable> Function locF } private static Function> sigFn(Collection> extras) { - return locFn(JwsHeader.ALGORITHM, MISSING_JWS_ALG_MSG, SignatureAlgorithmsBridge.REGISTRY, extras); + return locFn(DefaultHeader.ALGORITHM.getId(), MISSING_JWS_ALG_MSG, SignatureAlgorithmsBridge.REGISTRY, extras); } private static Function encFn(Collection extras) { - return locFn(JweHeader.ENCRYPTION_ALGORITHM, MISSING_ENC_MSG, EncryptionAlgorithmsBridge.REGISTRY, extras); + return locFn(DefaultJweHeader.ENCRYPTION_ALGORITHM.getId(), MISSING_ENC_MSG, EncryptionAlgorithmsBridge.REGISTRY, extras); } private static Function> keyFn(Collection> extras) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 04dee37e4..848a13492 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -15,15 +15,10 @@ */ package io.jsonwebtoken.impl; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.JweHeader; -import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.impl.lang.Field; -import io.jsonwebtoken.impl.security.JwkContext; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.Jwk; import java.lang.reflect.Array; import java.util.Collection; @@ -64,10 +59,10 @@ protected boolean isSecret(String id) { public static boolean isReduceableToNull(Object v) { return v == null || - (v instanceof String && !Strings.hasText((String) v)) || - (v instanceof Collection && Collections.isEmpty((Collection) v)) || - (v instanceof Map && Collections.isEmpty((Map) v)) || - (v.getClass().isArray() && Array.getLength(v) == 0); + (v instanceof String && !Strings.hasText((String) v)) || + (v instanceof Collection && Collections.isEmpty((Collection) v)) || + (v instanceof Map && Collections.isEmpty((Map) v)) || + (v.getClass().isArray() && Array.getLength(v) == 0); } protected Object idiomaticGet(String key) { @@ -104,6 +99,18 @@ public Object get(Object o) { return values.get(o); } + /** + * Convenience method to put a value for a canonical field. + * + * @param field the field representing the property name to set + * @param value the value to set + * @return the previous value for the field name, or {@code null} if there was no previous value + * @since JJWT_RELEASE_VERSION + */ + protected Object put(Field field, Object value) { + return put(field.getId(), value); + } + @Override public Object put(String name, Object value) { name = Assert.notNull(Strings.clean(name), "Member name cannot be null or empty."); @@ -161,22 +168,8 @@ protected Object apply(Field field, Object rawValue) { return retval; } - private String getName() { - if (this instanceof JweHeader) { - return "JWE header"; - } else if (this instanceof JwsHeader) { - return "JWS header"; - } else if (this instanceof Header) { - return "JWT header"; - } else if (this instanceof Jwk || this instanceof JwkContext) { - Object value = values.get("kty"); - if ("oct".equals(value)) { - value = "Secret"; - } - return value != null ? value + " JWK" : "JWK"; - } else { - return "Map"; - } + protected String getName() { + return "Map"; } @Override @@ -191,7 +184,7 @@ public void putAll(Map m) { if (m == null) { return; } - for (Map.Entry entry : m.entrySet()) { + for (Map.Entry entry : m.entrySet()) { String s = entry.getKey(); put(s, entry.getValue()); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/Codec.java b/impl/src/main/java/io/jsonwebtoken/impl/io/Codec.java index 73c6f0c67..38d60ad28 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/Codec.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/Codec.java @@ -31,7 +31,7 @@ public byte[] applyFrom(String b) { try { return this.decoder.decode(b); } catch (DecodingException e) { - String msg = "Cannot decode input String '" + b + "'. Cause: " + e.getMessage(); + String msg = "Cannot decode input String. Cause: " + e.getMessage(); throw new IllegalArgumentException(msg, e); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java index f590eaea1..247ba7b99 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java @@ -27,7 +27,7 @@ public T applyFrom(Object value) { return converter.applyFrom((String) value); } else { String msg = "Values must be either String or " + type.getName() + - " instances. Value type found: " + value.getClass().getName() + ". Value: " + value; + " instances. Value type found: " + value.getClass().getName() + "."; throw new IllegalArgumentException(msg); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java index 97ad6324f..cf0d6c10d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java @@ -18,6 +18,7 @@ public class DefaultJwkContext extends JwtMap implements JwkContext { private static final Set> DEFAULT_FIELDS; + static { // assume all known fields: Set> set = new LinkedHashSet<>(); set.addAll(DefaultSecretJwk.FIELDS); // Private/Secret JWKs has both public and private fields @@ -62,7 +63,7 @@ private DefaultJwkContext(Set> fields, JwkContext other, boolean rem this.idiomaticValues.putAll(src.idiomaticValues); this.redactedValues.putAll(src.redactedValues); if (removePrivate) { - for(Field field : src.FIELDS.values()) { + for (Field field : src.FIELDS.values()) { if (field.isSecret()) { remove(field.getId()); } @@ -70,6 +71,15 @@ private DefaultJwkContext(Set> fields, JwkContext other, boolean rem } } + @Override + protected String getName() { + Object value = values.get("kty"); + if ("oct".equals(value)) { + value = "Secret"; + } + return value != null ? value + " JWK" : "JWK"; + } + @Override public void putAll(Map m) { Assert.notEmpty(m, "JWK values cannot be null or empty."); @@ -78,102 +88,100 @@ public void putAll(Map m) { @Override public String getAlgorithm() { - return (String) this.values.get(AbstractJwk.ALG.getId()); + return idiomaticGet(AbstractJwk.ALG); } @Override public JwkContext setAlgorithm(String algorithm) { - put(AbstractJwk.ALG.getId(), algorithm); + put(AbstractJwk.ALG, algorithm); return this; } @Override public String getId() { - return (String) this.values.get(AbstractJwk.KID.getId()); + return idiomaticGet(AbstractJwk.KID); } @Override public JwkContext setId(String id) { - put(AbstractJwk.KID.getId(), id); + put(AbstractJwk.KID, id); return this; } @Override public Set getOperations() { - //noinspection unchecked - return (Set) this.idiomaticValues.get(AbstractJwk.KEY_OPS.getId()); + return idiomaticGet(AbstractJwk.KEY_OPS); } @Override public JwkContext setOperations(Set ops) { - put(AbstractJwk.KEY_OPS.getId(), ops); + put(AbstractJwk.KEY_OPS, ops); return this; } @Override public String getType() { - return (String) this.values.get(AbstractJwk.KTY.getId()); + return idiomaticGet(AbstractJwk.KTY); } @Override public JwkContext setType(String type) { - put(AbstractJwk.KTY.getId(), type); + put(AbstractJwk.KTY, type); return this; } @Override public String getPublicKeyUse() { - return (String) this.values.get(AbstractAsymmetricJwk.USE.getId()); + return idiomaticGet(AbstractAsymmetricJwk.USE); } @Override public JwkContext setPublicKeyUse(String use) { - put(AbstractAsymmetricJwk.USE.getId(), use); + put(AbstractAsymmetricJwk.USE, use); return this; } @Override public List getX509CertificateChain() { - //noinspection unchecked - return (List) this.idiomaticValues.get(AbstractAsymmetricJwk.X5C.getId()); + return idiomaticGet(AbstractAsymmetricJwk.X5C); } @Override public JwkContext setX509CertificateChain(List x5c) { - put(AbstractAsymmetricJwk.X5C.getId(), x5c); + put(AbstractAsymmetricJwk.X5C, x5c); return this; } @Override public byte[] getX509CertificateSha1Thumbprint() { - return (byte[]) this.idiomaticValues.get(AbstractAsymmetricJwk.X5T.getId()); + return idiomaticGet(AbstractAsymmetricJwk.X5T); } @Override public JwkContext setX509CertificateSha1Thumbprint(byte[] x5t) { - put(AbstractAsymmetricJwk.X5T.getId(), x5t); + put(AbstractAsymmetricJwk.X5T, x5t); return this; } @Override public byte[] getX509CertificateSha256Thumbprint() { - return (byte[]) this.idiomaticValues.get(AbstractAsymmetricJwk.X5T_S256.getId()); + return idiomaticGet(AbstractAsymmetricJwk.X5T_S256); } @Override public JwkContext setX509CertificateSha256Thumbprint(byte[] x5ts256) { - put(AbstractAsymmetricJwk.X5T_S256.getId(), x5ts256); + put(AbstractAsymmetricJwk.X5T_S256, x5ts256); return this; } @Override public URI getX509Url() { - return (URI) this.idiomaticValues.get(AbstractAsymmetricJwk.X5U.getId()); + return idiomaticGet(AbstractAsymmetricJwk.X5U); } @Override public JwkContext setX509Url(URI url) { - put(AbstractAsymmetricJwk.X5U.getId(), url); + put(AbstractAsymmetricJwk.X5U, url); return this; } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy new file mode 100644 index 000000000..8ed9a372b --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy @@ -0,0 +1,26 @@ +package io.jsonwebtoken.impl + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class AbstractProtectedHeaderTest { + + @Test + void x509UrlTest() { + def header = new DefaultJwsHeader() // extends AbstractProtectedHeader + URI uri = URI.create('https://google.com') + header.setX509Url(uri) + assertEquals uri, header.getX509Url() + } + + @Test + void x509UrlStringTest() { //test canonical/idiomatic conversion + def header = new DefaultJwsHeader() + String url = 'https://google.com' + URI uri = URI.create(url) + header.put('x5u', url) + assertEquals url, header.get('x5u') + assertEquals uri, header.getX509Url() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy index e91c7b3b6..707affcd2 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy @@ -54,4 +54,10 @@ class DefaultHeaderTest { header.put(Header.DEPRECATED_COMPRESSION_ALGORITHM, "DEF") assertEquals "DEF", header.getCompressionAlgorithm() } + + @Test + void testGetName() { + def header = new DefaultHeader() + assertEquals 'JWT header', header.getName() + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy index c584b2e47..a1e372cdd 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy @@ -1,6 +1,5 @@ package io.jsonwebtoken.impl - import io.jsonwebtoken.impl.security.Randoms import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.Encoders @@ -11,7 +10,8 @@ import io.jsonwebtoken.security.Jwks import org.junit.Before import org.junit.Test -import static org.junit.Assert.* +import static org.junit.Assert.assertArrayEquals +import static org.junit.Assert.assertEquals /** * @since JJWT_RELEASE_VERSION @@ -94,4 +94,10 @@ class DefaultJweHeaderTest { header.setCritical(crits) assertEquals crits, header.getCritical() } + + @Test + void testGetName() { + def header = new DefaultJweHeader() + assertEquals 'JWE header', header.getName() + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy index 9d43dfd9a..b77331053 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy @@ -16,7 +16,8 @@ package io.jsonwebtoken.impl import org.junit.Test -import static org.junit.Assert.* + +import static org.junit.Assert.assertEquals class DefaultJwsHeaderTest { @@ -28,4 +29,10 @@ class DefaultJwsHeaderTest { h.setKeyId('foo') assertEquals h.getKeyId(), 'foo' } + + @Test + void testGetName() { + def header = new DefaultJwsHeader() + assertEquals 'JWS header', header.getName() + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/IdLocatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/IdLocatorTest.groovy new file mode 100644 index 000000000..5f685bbca --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/IdLocatorTest.groovy @@ -0,0 +1,79 @@ +package io.jsonwebtoken.impl + +import io.jsonwebtoken.Header +import io.jsonwebtoken.MalformedJwtException +import io.jsonwebtoken.UnsupportedJwtException +import io.jsonwebtoken.impl.lang.Function +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +class IdLocatorTest { + + @Test + void missingRequiredHeaderValueTest() { + def msg = 'foo is required' + def loc = new IdLocator('foo', msg, DummyIdFn.INSTANCE, DummyHeaderFn.INSTANCE) + def header = new DefaultHeader() + try { + loc.apply(header) + fail() + } catch (MalformedJwtException expected) { + assertEquals msg, expected.getMessage() + } + } + + @Test + void unlocatableJwtHeaderInstanceTest() { + def loc = new IdLocator('foo', 'foo', DummyIdFn.INSTANCE, DummyHeaderFn.INSTANCE) + def header = new DefaultHeader([foo: 'foo']) + try { + loc.apply(header) + } catch (UnsupportedJwtException expected) { + String msg = 'Unrecognized JWT \'foo\' header value: foo' + assertEquals msg, expected.getMessage() + } + } + + @Test + void unlocatableJwsHeaderInstanceTest() { + def loc = new IdLocator('foo', 'foo', DummyIdFn.INSTANCE, DummyHeaderFn.INSTANCE) + def header = new DefaultJwsHeader([foo: 'foo']) + try { + loc.apply(header) + } catch (UnsupportedJwtException expected) { + String msg = 'Unrecognized JWS \'foo\' header value: foo' + assertEquals msg, expected.getMessage() + } + } + + @Test + void unlocatableJweHeaderInstanceTest() { + def loc = new IdLocator('foo', 'foo', DummyIdFn.INSTANCE, DummyHeaderFn.INSTANCE) + def header = new DefaultJweHeader([foo: 'foo']) + try { + loc.apply(header) + } catch (UnsupportedJwtException expected) { + String msg = 'Unrecognized JWE \'foo\' header value: foo' + assertEquals msg, expected.getMessage() + } + } + + private static class DummyIdFn implements Function { + static final DummyIdFn INSTANCE = new DummyIdFn() + @Override + String apply(String s) { + return null + } + } + + private static class DummyHeaderFn implements Function, String> { + static final DummyHeaderFn INSTANCE = new DummyHeaderFn() + + @Override + String apply(Header header) { + return null + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy index ef8f36a6c..1adb6ee79 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy @@ -27,6 +27,7 @@ import static org.junit.Assert.* class JwtMapTest { private static final Field DUMMY = Fields.string('' + Randoms.secureRandom().nextInt(), "RANDOM") + private static final Field SECRET = Fields.secretBigInt('foo', 'foo') private static final Set> FIELDS = Collections.setOf(DUMMY) JwtMap jwtMap @@ -68,7 +69,7 @@ class JwtMapTest { @Test void testPutAllWithNullArgument() { - jwtMap.putAll((Map)null) + jwtMap.putAll((Map) null) assertEquals jwtMap.size(), 0 } @@ -83,7 +84,7 @@ class JwtMapTest { @Test void testKeySet() { jwtMap.putAll([a: 'b', c: 'd']) - assertEquals( jwtMap.keySet(), ['a', 'c'] as Set) + assertEquals(jwtMap.keySet(), ['a', 'c'] as Set) } @Test @@ -115,4 +116,26 @@ class JwtMapTest { def identityHash = System.identityHashCode(jwtMap); assertTrue(hashCodeNonEmpty != identityHash); } + + @Test + void testGetName() { + def map = new JwtMap(FIELDS) + assertEquals 'Map', map.getName() + } + + @Test + void testSetSecretFieldWithInvalidTypeValue() { + def map = new JwtMap(Collections.setOf(SECRET)) + def invalidValue = URI.create('https://whatever.com') + try { + map.put('foo', invalidValue) + fail() + } catch (IllegalArgumentException expected) { + //Ensure message so we don't show any secret value: + String msg = 'Invalid Map \'foo\' (foo) value []. Cause: Values must be ' + + 'either String or java.math.BigInteger instances. Value type found: ' + + 'java.net.URI.' + assertEquals msg, expected.getMessage() + } + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy new file mode 100644 index 000000000..c350e7b93 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy @@ -0,0 +1,22 @@ +package io.jsonwebtoken.impl.security + + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class DefaultJwkContextTest { + + @Test + void testGetName() { + def header = new DefaultJwkContext() + assertEquals 'JWK', header.getName() + } + + @Test + void testGetNameWhenSecretKey() { + def header = new DefaultJwkContext(DefaultSecretJwk.FIELDS) + header.put('kty', 'oct') + assertEquals 'Secret JWK', header.getName() + } +} From a4ee957e1633f9746864a60e61f860614e300285 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 1 May 2022 22:31:37 -0400 Subject: [PATCH 30/75] code coverage work cont'd --- .../impl/DefaultJweHeaderTest.groovy | 92 ++++++++++++++++++- .../jsonwebtoken/impl/DefaultJweTest.groovy | 22 +++++ .../jsonwebtoken/impl/DefaultJwsTest.groovy | 21 ++++- .../jsonwebtoken/impl/DefaultJwtTest.groovy | 9 ++ .../io/jsonwebtoken/impl/io/CodecTest.groovy | 2 +- .../lang/EncodedObjectConverterTest.groovy | 3 +- 6 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy index a1e372cdd..cea35257e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy @@ -10,8 +10,9 @@ import io.jsonwebtoken.security.Jwks import org.junit.Before import org.junit.Test -import static org.junit.Assert.assertArrayEquals -import static org.junit.Assert.assertEquals +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.* /** * @since JJWT_RELEASE_VERSION @@ -97,7 +98,92 @@ class DefaultJweHeaderTest { @Test void testGetName() { - def header = new DefaultJweHeader() assertEquals 'JWE header', header.getName() } + + @Test + void pbe2SaltBytesTest() { + byte[] salt = new byte[32] + Randoms.secureRandom().nextBytes(salt) + header.setPbes2Salt(salt) + assertArrayEquals salt, header.getPbes2Salt() + } + + @Test + void pbe2SaltStringTest() { + byte[] salt = new byte[32] + Randoms.secureRandom().nextBytes(salt) + String val = Encoders.BASE64URL.encode(salt) + header.put('p2s', val) + //ensure that even though a Base64Url string was set, we get back a byte[]: + assertArrayEquals salt, header.getPbes2Salt() + } + + @Test + void testAgreementPartyUInfo() { + String val = "Party UInfo" + byte[] info = val.getBytes(StandardCharsets.UTF_8) + header.setAgreementPartyUInfo(info) + assertArrayEquals info, header.getAgreementPartyUInfo() + assertEquals val, header.getAgreementPartyUInfoString() + } + + @Test + void testAgreementPartyUInfoString() { + String val = "Party UInfo" + byte[] info = val.getBytes(StandardCharsets.UTF_8) + header.setAgreementPartyUInfo(val) + assertArrayEquals info, header.getAgreementPartyUInfo() + assertEquals val, header.getAgreementPartyUInfoString() + } + + @Test + void testEmptyAgreementPartyUInfo() { + byte[] info = new byte[0] + header.setAgreementPartyUInfo(info) + assertNull header.getAgreementPartyUInfo() + assertNull header.getAgreementPartyUInfoString() + } + + @Test + void testEmptyAgreementPartyUInfoString() { + String s = ' ' + header.setAgreementPartyUInfo(s) + assertNull header.getAgreementPartyUInfo() + assertNull header.getAgreementPartyUInfoString() + } + + @Test + void testAgreementPartyVInfo() { + String val = "Party VInfo" + byte[] info = val.getBytes(StandardCharsets.UTF_8) + header.setAgreementPartyVInfo(info) + assertArrayEquals info, header.getAgreementPartyVInfo() + assertEquals val, header.getAgreementPartyVInfoString() + } + + @Test + void testAgreementPartyVInfoString() { + String val = "Party VInfo" + byte[] info = val.getBytes(StandardCharsets.UTF_8) + header.setAgreementPartyVInfo(val) + assertArrayEquals info, header.getAgreementPartyVInfo() + assertEquals val, header.getAgreementPartyVInfoString() + } + + @Test + void testEmptyAgreementPartyVInfo() { + byte[] info = new byte[0] + header.setAgreementPartyVInfo(info) + assertNull header.getAgreementPartyVInfo() + assertNull header.getAgreementPartyVInfoString() + } + + @Test + void testEmptyAgreementPartyVInfoString() { + String s = ' ' + header.setAgreementPartyVInfo(s) + assertNull header.getAgreementPartyVInfo() + assertNull header.getAgreementPartyVInfoString() + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy new file mode 100644 index 000000000..ffad425c8 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy @@ -0,0 +1,22 @@ +package io.jsonwebtoken.impl + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.EncryptionAlgorithms +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class DefaultJweTest { + + @Test + void testEqualsAndHashCode() { + def alg = EncryptionAlgorithms.A128CBC_HS256 + def key = alg.keyBuilder().build() + String compact = Jwts.jweBuilder().claim('foo', 'bar').encryptWith(alg).withKey(key).compact() + def parser = Jwts.parserBuilder().decryptWith(key).build() + def jwe1 = parser.parseClaimsJwe(compact) + def jwe2 = parser.parseClaimsJwe(compact) + assertEquals jwe1, jwe2 + assertEquals jwe1.hashCode(), jwe2.hashCode() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy index b2fee93e9..08a05b2e5 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy @@ -17,8 +17,7 @@ package io.jsonwebtoken.impl import io.jsonwebtoken.JwsHeader import io.jsonwebtoken.Jwts -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.SignatureAlgorithms import org.junit.Test import static org.junit.Assert.assertEquals @@ -40,12 +39,24 @@ class DefaultJwsTest { @Test void testToString() { //create random signing key for testing: - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded - String compact = Jwts.builder().claim('foo', 'bar').signWith(alg, key).compact(); + def alg = SignatureAlgorithms.HS256 + def key = alg.keyBuilder().build() + String compact = Jwts.builder().claim('foo', 'bar').signWith(key, alg).compact(); int i = compact.lastIndexOf('.') String signature = compact.substring(i + 1) def jws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) assertEquals 'header={alg=HS256},body={foo=bar},signature=' + signature, jws.toString() } + + @Test + void testEqualsAndHashCode() { + def alg = SignatureAlgorithms.HS256 + def key = alg.keyBuilder().build() + String compact = Jwts.builder().claim('foo', 'bar').signWith(key, alg).compact() + def parser = Jwts.parserBuilder().setSigningKey(key).build() + def jws1 = parser.parseClaimsJws(compact) + def jws2 = parser.parseClaimsJws(compact) + assertEquals jws1, jws2 + assertEquals jws1.hashCode(), jws2.hashCode() + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy index 013128016..57c782c96 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy @@ -30,4 +30,13 @@ class DefaultJwtTest { assertEquals 'header={foo=bar, alg=none},body={aud=jsmith}', jwt.toString() } + @Test + void testEqualsAndHashCode() { + String compact = Jwts.builder().claim('foo', 'bar').compact() + def parser = Jwts.parserBuilder().enableUnsecuredJws().build() + def jwt1 = parser.parseClaimsJwt(compact) + def jwt2 = parser.parseClaimsJwt(compact) + assertEquals jwt1, jwt2 + assertEquals jwt1.hashCode(), jwt2.hashCode() + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/io/CodecTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/io/CodecTest.groovy index 841caa202..30976e99b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/io/CodecTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/io/CodecTest.groovy @@ -16,7 +16,7 @@ class CodecTest { } catch (IllegalArgumentException expected) { def cause = expected.getCause() assertTrue cause instanceof DecodingException - String msg = "Cannot decode input String '$s'. Cause: ${cause.getMessage()}" + String msg = "Cannot decode input String. Cause: ${cause.getMessage()}" assertEquals msg, expected.getMessage() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy index 8c44593c5..853986c68 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy @@ -16,7 +16,8 @@ class EncodedObjectConverterTest { converter.applyFrom(value) fail("IllegalArgumentException should have been thrown.") } catch (IllegalArgumentException expected) { - String msg = "Values must be either String or java.net.URI instances. Value type found: java.lang.Integer. Value: ${value}" + String msg = "Values must be either String or java.net.URI instances. " + + "Value type found: java.lang.Integer." assertEquals msg, expected.getMessage() } } From 98272715f354bb57461a42a3ec30ef85958e4024 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 1 May 2022 22:57:52 -0400 Subject: [PATCH 31/75] JavaDoc fix --- api/src/main/java/io/jsonwebtoken/ProtectedHeader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java index c00851481..8dd6f74ae 100644 --- a/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java +++ b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java @@ -24,7 +24,7 @@ public interface ProtectedHeader> extends Header T setJwk(PublicJwk jwk); /** - * Returns the JWT case-sensitive {@code kid} (Key ID) header value or {@code null} if not present. + * Returns the JWT case-sensitive {@code kid} (Key ID) header value or {@code null} if not present. * *

    The keyId header parameter is a hint indicating which key was used to secure a JWS or JWE. This * parameter allows originators to explicitly signal a change of key to recipients. The structure of the keyId From 6e787d2b1352b2b246eb86547207d3c0f83134d8 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Mon, 2 May 2022 10:33:42 -0400 Subject: [PATCH 32/75] Update impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java Co-authored-by: sonatype-lift[bot] <37194012+sonatype-lift[bot]@users.noreply.github.com> --- impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java index 467eda226..6285e20b8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java @@ -25,7 +25,7 @@ public T tokenize(String jwt) { StringBuilder sb = new StringBuilder(128); - for (char c : jwt.toCharArray()) { + for (int i = 0; i < jwt.length(); i++) { char c = jwt.charAt(i);if (Character.isWhitespace(c)) { if (Character.isWhitespace(c)) { String msg = "Compact JWT strings may not contain whitespace."; From aa501136520b3ef5d73e7a56e2f0c0ba9fea31f5 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Mon, 2 May 2022 10:45:25 -0400 Subject: [PATCH 33/75] lift edits --- .../java/io/jsonwebtoken/ProtectedHeader.java | 2 ++ impl/.lift.toml | 3 +++ .../java/io/jsonwebtoken/impl/DefaultJwe.java | 3 +++ .../io/jsonwebtoken/impl/DefaultJweBuilder.java | 6 +++--- .../java/io/jsonwebtoken/impl/DefaultJws.java | 3 +++ .../io/jsonwebtoken/impl/DefaultJwtParser.java | 4 ++-- .../impl/DefaultJwtParserBuilder.java | 17 +++++++++++++---- .../java/io/jsonwebtoken/impl/JwtTokenizer.java | 4 +++- .../impl/security/AbstractEcJwkFactory.java | 2 +- .../io/jsonwebtoken/impl/DefaultJweTest.groovy | 4 ++++ .../io/jsonwebtoken/impl/DefaultJwsTest.groovy | 6 ++++-- .../io/jsonwebtoken/impl/DefaultJwtTest.groovy | 4 ++++ .../impl/security/CryptoAlgorithmTest.groovy | 7 +++++++ .../impl/security/ProvidersTest.groovy | 6 ++++++ 14 files changed, 58 insertions(+), 13 deletions(-) create mode 100644 impl/.lift.toml diff --git a/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java index 8dd6f74ae..8ad0a1bcb 100644 --- a/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java +++ b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java @@ -50,6 +50,8 @@ public interface ProtectedHeader> extends Header * * @param kid the case-sensitive JWS {@code kid} header value or {@code null} to remove the property from the JSON map. * @return the header instance for method chaining. + * @see JWS Key ID + * @see JWE Key ID */ T setKeyId(String kid); diff --git a/impl/.lift.toml b/impl/.lift.toml new file mode 100644 index 000000000..192b4f812 --- /dev/null +++ b/impl/.lift.toml @@ -0,0 +1,3 @@ +# JavaDoc purity is not necessary in the impl module since it's never intended +# to be consumed by users: +ignoreRules = ["MissingSummary"] \ No newline at end of file diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwe.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwe.java index 667755466..f6c38aeb7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwe.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwe.java @@ -28,6 +28,9 @@ public byte[] getAadTag() { @Override public boolean equals(Object obj) { + if (obj == this) { + return true; + } if (obj instanceof Jwe) { Jwe jwe = (Jwe)obj; return super.equals(jwe) && diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java index ed50475de..ecc6a6147 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java @@ -114,9 +114,9 @@ public String compact() { throw new IllegalStateException("Both 'payload' and 'claims' cannot both be specified. Choose either one."); } - Assert.state(key != null, "Key is required."); - Assert.state(enc != null, "Encryption algorithm is required."); - Assert.state(alg != null, "KeyAlgorithm is required."); //always set by withKey calling withKeyFrom + Assert.stateNotNull(key, "Key is required."); + Assert.stateNotNull(enc, "Encryption algorithm is required."); + Assert.stateNotNull(alg, "KeyAlgorithm is required."); //always set by withKey calling withKeyFrom if (this.serializer == null) { // try to find one based on the services available //noinspection unchecked diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java index 19429c33b..9f46e1597 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java @@ -40,6 +40,9 @@ public String toString() { @Override public boolean equals(Object obj) { + if (obj == this) { + return true; + } if (obj instanceof Jws) { Jws jws = (Jws) obj; return super.equals(jws) && diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 88b693b98..d6e20d4ac 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -341,7 +341,7 @@ public boolean isSigned(String compact) { } try { final TokenizedJwt tokenized = jwtTokenizer.tokenize(compact); - return (!(tokenized instanceof TokenizedJwe)) && Strings.hasText(tokenized.getDigest()); + return !(tokenized instanceof TokenizedJwe) && Strings.hasText(tokenized.getDigest()); } catch (MalformedJwtException e) { return false; } @@ -565,7 +565,7 @@ private static boolean hasContentType(Header header) { throw new MalformedJwtException(msg); } - assert this.signingKeyResolver != null : "SigningKeyResolver cannot be null (invariant)."; + Assert.stateNotNull(this.signingKeyResolver, "SigningKeyResolver cannot be null (invariant)."); //digitally signed, let's assert the signature: Key key; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index 01d072cf3..5fbbb12af 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -15,7 +15,16 @@ */ package io.jsonwebtoken.impl; -import io.jsonwebtoken.*; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Clock; +import io.jsonwebtoken.CompressionCodecResolver; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.JwtParserBuilder; +import io.jsonwebtoken.Locator; +import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; import io.jsonwebtoken.impl.lang.ConstantFunction; import io.jsonwebtoken.impl.lang.Function; @@ -275,9 +284,9 @@ public Key apply(Header header) { // Invariants. If these are ever violated, it's an error in this class implementation // (we default to non-null instances, and the setters should never allow null): - assert this.keyLocator != null : "Key locator should never be null."; - assert this.signingKeyResolver != null : "SigningKeyResolver should never be null."; - assert this.compressionCodecResolver != null : "CompressionCodecResolver should never be null."; + Assert.stateNotNull(this.keyLocator, "Key locator should never be null."); + Assert.stateNotNull(this.signingKeyResolver, "SigningKeyResolver should never be null."); + Assert.stateNotNull(this.compressionCodecResolver, "CompressionCodecResolver should never be null."); return new ImmutableJwtParser(new DefaultJwtParser( provider, diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java index 6285e20b8..e7e3686e5 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java @@ -25,7 +25,9 @@ public T tokenize(String jwt) { StringBuilder sb = new StringBuilder(128); - for (int i = 0; i < jwt.length(); i++) { char c = jwt.charAt(i);if (Character.isWhitespace(c)) { + for (int i = 0; i < jwt.length(); i++) { + + char c = jwt.charAt(i); if (Character.isWhitespace(c)) { String msg = "Compact JWT strings may not contain whitespace."; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java index 1ae603b3b..5a8b1a2e9 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java @@ -190,7 +190,7 @@ private static ECPoint add(ECPoint P, ECPoint Q, EllipticCurve curve) { final BigInteger prime = ((ECFieldFp) curve.getField()).getP(); final BigInteger slope = Qy.subtract(Py).multiply(Qx.subtract(Px).modInverse(prime)).mod(prime); final BigInteger Rx = (slope.modPow(TWO, prime).subtract(Px)).subtract(Qx).mod(prime); - final BigInteger Ry = (Qy.negate().mod(prime)).add(slope.multiply(Qx.subtract(Rx))).mod(prime); + final BigInteger Ry = Qy.negate().mod(prime).add(slope.multiply(Qx.subtract(Rx))).mod(prime); return new ECPoint(Rx, Ry); } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy index ffad425c8..79dbb1b36 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy @@ -5,6 +5,7 @@ import io.jsonwebtoken.security.EncryptionAlgorithms import org.junit.Test import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNotEquals class DefaultJweTest { @@ -16,6 +17,9 @@ class DefaultJweTest { def parser = Jwts.parserBuilder().decryptWith(key).build() def jwe1 = parser.parseClaimsJwe(compact) def jwe2 = parser.parseClaimsJwe(compact) + assertNotEquals jwe1, 'hello' as String + assertEquals jwe1, jwe1 + assertEquals jwe2, jwe2 assertEquals jwe1, jwe2 assertEquals jwe1.hashCode(), jwe2.hashCode() } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy index 08a05b2e5..162d4c7cc 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy @@ -20,8 +20,7 @@ import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.SignatureAlgorithms import org.junit.Test -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertSame +import static org.junit.Assert.* class DefaultJwsTest { @@ -56,6 +55,9 @@ class DefaultJwsTest { def parser = Jwts.parserBuilder().setSigningKey(key).build() def jws1 = parser.parseClaimsJws(compact) def jws2 = parser.parseClaimsJws(compact) + assertNotEquals jws1, 'hello' as String + assertEquals jws1, jws1 + assertEquals jws2, jws2 assertEquals jws1, jws2 assertEquals jws1.hashCode(), jws2.hashCode() } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy index 57c782c96..fc25f1d2f 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy @@ -20,6 +20,7 @@ import io.jsonwebtoken.Jwts import org.junit.Test import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNotEquals class DefaultJwtTest { @@ -36,6 +37,9 @@ class DefaultJwtTest { def parser = Jwts.parserBuilder().enableUnsecuredJws().build() def jwt1 = parser.parseClaimsJwt(compact) def jwt2 = parser.parseClaimsJwt(compact) + assertNotEquals jwt1, 'hello' as String + assertEquals jwt1, jwt1 + assertEquals jwt2, jwt2 assertEquals jwt1, jwt2 assertEquals jwt1.hashCode(), jwt2.hashCode() } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy index de976527f..8c3782176 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy @@ -48,6 +48,13 @@ class CryptoAlgorithmTest { assertEquals hash, new TestCryptoAlgorithm('name', 'jcaName').hashCode() } + @Test + void testEnsureSecureRandomWorksWithNullRequest() { + def alg = new TestCryptoAlgorithm('test', 'test') + def random = alg.ensureSecureRandom(null) + assertSame Randoms.secureRandom(), random + } + @Test void testRequestProviderPriorityOverDefaultProvider() { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy index 4171a27e5..68f14accb 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy @@ -65,6 +65,9 @@ class ProvidersTest { assertSame bc, returned assertSame bc, Providers.BC_PROVIDER.get() // ensure cached for future lookup + //ensure cache hit works: + assertSame bc, Providers.findBouncyCastle(Conditions.TRUE) + //cleanup() method will remove the provider from the system } @@ -80,5 +83,8 @@ class ProvidersTest { assertNotNull returned assertSame Providers.BC_PROVIDER.get(), returned //ensure cached for future lookup assertFalse bcRegistered() //ensure we don't alter the system environment + + assertSame returned, Providers.findBouncyCastle(Conditions.TRUE) //ensure cache hit + assertFalse bcRegistered() //ensure we don't alter the system environment } } From 52c0044eb1580a1b7c7ceb0c2f88c28bd643147b Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Mon, 2 May 2022 10:53:16 -0400 Subject: [PATCH 34/75] lift edits --- impl/.lift.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/.lift.toml b/impl/.lift.toml index 192b4f812..1c779f4cc 100644 --- a/impl/.lift.toml +++ b/impl/.lift.toml @@ -1,3 +1,3 @@ # JavaDoc purity is not necessary in the impl module since it's never intended # to be consumed by users: -ignoreRules = ["MissingSummary"] \ No newline at end of file +ignoreRules = ["MissingSummary", "InconsistentCapitalization"] \ No newline at end of file From 2c76a7153ea3ad5bae4eaf60504098254058a143 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Mon, 2 May 2022 10:55:36 -0400 Subject: [PATCH 35/75] lift edits --- impl/.lift.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/.lift.toml b/impl/.lift.toml index 1c779f4cc..0ccd3d2ad 100644 --- a/impl/.lift.toml +++ b/impl/.lift.toml @@ -1,3 +1,3 @@ # JavaDoc purity is not necessary in the impl module since it's never intended # to be consumed by users: -ignoreRules = ["MissingSummary", "InconsistentCapitalization"] \ No newline at end of file +ignoreRules = ["MissingSummary", "InconsistentCapitalization", "JavaUtilDate"] \ No newline at end of file From 8afdc005602d050dea26ff80ab9f10cb0cd5361f Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Mon, 2 May 2022 23:07:18 -0400 Subject: [PATCH 36/75] code coverage testing cont'd --- .../java/io/jsonwebtoken/lang/Arrays.java | 2 +- .../security/AbstractFamilyJwkFactory.java | 9 +- .../impl/security/AbstractJwk.java | 2 +- .../impl/security/DefaultRsaPrivateJwk.java | 14 +- .../impl/security/EcPublicJwkFactory.java | 29 ++- .../impl/security/KeyAlgorithmsBridge.java | 19 +- .../security/RSAOtherPrimeInfoConverter.java | 64 +++++ .../impl/security/RsaPrivateJwkFactory.java | 93 ++----- .../impl/security/RsaPublicJwkFactory.java | 4 +- .../AbstractAsymmetricJwkBuilderTest.groovy | 25 ++ .../impl/security/AbstractJwkTest.groovy | 147 +++++++++++ .../impl/security/EcdhKeyAlgorithmTest.groovy | 2 +- .../impl/security/JwksTest.groovy | 6 +- .../security/RFC7517AppendixA2Test.groovy | 3 +- .../impl/security/RFC7517AppendixBTest.groovy | 5 +- .../RSAOtherPrimeInfoConverterTest.groovy | 44 ++++ .../security/RsaPrivateJwkFactoryTest.groovy | 240 ++++++++++++++++++ .../impl/security/TestRSAKey.groovy | 33 +++ .../TestRSAMultiPrimePrivateCrtKey.groovy | 50 ++++ .../impl/security/TestRSAPrivateKey.groovy | 15 ++ 20 files changed, 686 insertions(+), 120 deletions(-) create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverterTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAKey.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAMultiPrimePrivateCrtKey.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAPrivateKey.groovy diff --git a/api/src/main/java/io/jsonwebtoken/lang/Arrays.java b/api/src/main/java/io/jsonwebtoken/lang/Arrays.java index f0db0dc2b..024b06ac1 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Arrays.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Arrays.java @@ -31,7 +31,7 @@ public static int length(T[] a) { } public static List asList(T[] a) { - return a == null ? Collections.emptyList() : java.util.Arrays.asList(a); + return Objects.isEmpty(a) ? Collections.emptyList() : java.util.Arrays.asList(a); } public static int length(byte[] bytes) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java index 64d7b8f5b..65f74b816 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java @@ -1,22 +1,19 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.CheckedFunction; -import io.jsonwebtoken.impl.lang.Converters; -import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.Jwk; import io.jsonwebtoken.security.KeyException; -import java.math.BigInteger; import java.security.Key; import java.security.KeyFactory; abstract class AbstractFamilyJwkFactory> implements FamilyJwkFactory { - protected static String encode(BigInteger bigInt) { - byte[] unsigned = Converters.BIGINT_UBYTES.applyTo(bigInt); - return Encoders.BASE64URL.encode(unsigned); + protected static void put(JwkContext ctx, Field field, T value) { + ctx.put(field.getId(), field.applyTo(value)); } private final String ktyValue; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java index ba8c0f067..cc62d2bb2 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java @@ -21,7 +21,7 @@ public abstract class AbstractJwk implements Jwk { static final Field KTY = Fields.string("kty", "Key Type"); static final Set> FIELDS = Collections.setOf(ALG, KID, KEY_OPS, KTY); - public static final String IMMUTABLE_MSG = "JWKs are immutable may not be modified."; + public static final String IMMUTABLE_MSG = "JWKs are immutable and may not be modified."; protected final JwkContext context; AbstractJwk(JwkContext ctx) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java index 4c0436f2f..3f6af8791 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java @@ -10,6 +10,7 @@ import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.RSAOtherPrimeInfo; +import java.util.List; import java.util.Set; class DefaultRsaPrivateJwk extends AbstractPrivateJwk implements RsaPrivateJwk { @@ -20,14 +21,15 @@ class DefaultRsaPrivateJwk extends AbstractPrivateJwk FIRST_CRT_EXPONENT = Fields.secretBigInt("dp", "First Factor CRT Exponent"); static final Field SECOND_CRT_EXPONENT = Fields.secretBigInt("dq", "Second Factor CRT Exponent"); static final Field FIRST_CRT_COEFFICIENT = Fields.secretBigInt("qi", "First CRT Coefficient"); - static final Field OTHER_PRIMES_INFO = Fields.builder(RSAOtherPrimeInfo.class).setSecret(true) - .setId("oth").setName("Other Primes Info") - .setConverter(new RsaPrivateJwkFactory.RSAOtherPrimeInfoConverter()) - .build(); + static final Field> OTHER_PRIMES_INFO = + Fields.builder(RSAOtherPrimeInfo.class).setSecret(true) + .setId("oth").setName("Other Primes Info") + .setConverter(RSAOtherPrimeInfoConverter.INSTANCE).list() + .build(); static final Set> FIELDS = Collections.concat(DefaultRsaPublicJwk.FIELDS, - PRIVATE_EXPONENT, FIRST_PRIME, SECOND_PRIME, FIRST_CRT_EXPONENT, - SECOND_CRT_EXPONENT, FIRST_CRT_COEFFICIENT, OTHER_PRIMES_INFO + PRIVATE_EXPONENT, FIRST_PRIME, SECOND_PRIME, FIRST_CRT_EXPONENT, + SECOND_CRT_EXPONENT, FIRST_CRT_COEFFICIENT, OTHER_PRIMES_INFO ); DefaultRsaPrivateJwk(JwkContext ctx, RsaPublicJwk pubJwk) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java index 8044e81cc..add28b169 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java @@ -2,6 +2,7 @@ import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.EcPublicJwk; import io.jsonwebtoken.security.InvalidKeyException; @@ -12,23 +13,31 @@ import java.security.spec.ECPoint; import java.security.spec.ECPublicKeySpec; import java.security.spec.EllipticCurve; +import java.util.Map; class EcPublicJwkFactory extends AbstractEcJwkFactory { static final EcPublicJwkFactory DEFAULT_INSTANCE = new EcPublicJwkFactory(); - private static final String KEY_CONTAINS_FORMAT_MSG = - "ECPublicKey's ECPoint does not exist on elliptic curve '%s' and may not be used to create '%s' JWKs."; - - private static final String JWK_CONTAINS_FORMAT_MSG = - "EC JWK x,y coordinates do not exist on elliptic curve '%s'. This " + - "could be due simply to an incorrectly-created JWK or possibly an attempted Invalid Curve Attack " + - "(see https://safecurves.cr.yp.to/twist.html for more information). JWK: %s"; - EcPublicJwkFactory() { super(ECPublicKey.class); } + protected static String keyContainsErrorMessage(String curveId) { + Assert.hasText(curveId, "curveId cannot be null or empty."); + String fmt = "ECPublicKey's ECPoint does not exist on elliptic curve '%s' " + + "and may not be used to create '%s' JWKs."; + return String.format(fmt, curveId, curveId); + } + + protected static String jwkContainsErrorMessage(String curveId, Map jwk) { + Assert.hasText(curveId, "curveId cannot be null or empty."); + String fmt = "EC JWK x,y coordinates do not exist on elliptic curve '%s'. This " + + "could be due simply to an incorrectly-created JWK or possibly an attempted Invalid Curve Attack " + + "(see https://safecurves.cr.yp.to/twist.html for more information). JWK: %s"; + return String.format(fmt, curveId, jwk); + } + @Override protected EcPublicJwk createJwkFromKey(JwkContext ctx) { @@ -40,7 +49,7 @@ protected EcPublicJwk createJwkFromKey(JwkContext ctx) { String curveId = getJwaIdByCurve(curve); if (!contains(curve, point)) { - String msg = String.format(KEY_CONTAINS_FORMAT_MSG, curveId, curveId); + String msg = keyContainsErrorMessage(curveId); throw new InvalidKeyException(msg); } @@ -68,7 +77,7 @@ protected EcPublicJwk createJwkFromValues(final JwkContext ctx) { ECPoint point = new ECPoint(x, y); if (!contains(spec.getCurve(), point)) { - String msg = String.format(JWK_CONTAINS_FORMAT_MSG, curveId, ctx); + String msg = jwkContainsErrorMessage(curveId, ctx); throw new InvalidKeyException(msg); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java index c5e415b3b..1903ab164 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java @@ -1,31 +1,16 @@ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.JweHeader; import io.jsonwebtoken.UnsupportedJwtException; -import io.jsonwebtoken.impl.DefaultJweHeader; import io.jsonwebtoken.impl.IdRegistry; -import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.impl.lang.Registry; import io.jsonwebtoken.lang.Collections; -import io.jsonwebtoken.security.AeadAlgorithm; -import io.jsonwebtoken.security.DecryptionKeyRequest; -import io.jsonwebtoken.security.EncryptionAlgorithms; import io.jsonwebtoken.security.KeyAlgorithm; -import io.jsonwebtoken.security.KeyRequest; -import io.jsonwebtoken.security.KeyResult; -import io.jsonwebtoken.security.Keys; -import io.jsonwebtoken.security.PasswordKey; -import io.jsonwebtoken.security.SecurityException; - -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; + import javax.crypto.spec.OAEPParameterSpec; import javax.crypto.spec.PSource; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.MGF1ParameterSpec; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; @SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.KeyAlgorithms implementation public final class KeyAlgorithmsBridge { @@ -85,6 +70,7 @@ private KeyAlgorithmsBridge() { return instance; } + /* private static KeyAlgorithm lean(final Pbes2HsAkwAlgorithm alg) { // ensure we use the same key factory over and over so that time spent acquiring one is not repeated: @@ -246,4 +232,5 @@ public Point(long x, long y) { this.lnY = Math.log((double) y); } } + */ } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java new file mode 100644 index 000000000..188985058 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java @@ -0,0 +1,64 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.Converter; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.MalformedKeyException; + +import java.math.BigInteger; +import java.security.spec.RSAOtherPrimeInfo; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +class RSAOtherPrimeInfoConverter implements Converter { + + static final RSAOtherPrimeInfoConverter INSTANCE = new RSAOtherPrimeInfoConverter(); + + static final Field PRIME_FACTOR = Fields.secretBigInt("r", "Prime Factor"); + static final Field FACTOR_CRT_EXPONENT = Fields.secretBigInt("d", "Factor CRT Exponent"); + static final Field FACTOR_CRT_COEFFICIENT = Fields.secretBigInt("t", "Factor CRT Coefficient"); + static final Set> FIELDS = Collections.>setOf(PRIME_FACTOR, FACTOR_CRT_EXPONENT, FACTOR_CRT_COEFFICIENT); + + @Override + public Object applyTo(RSAOtherPrimeInfo info) { + Map m = new LinkedHashMap<>(3); + m.put(PRIME_FACTOR.getId(), (String)PRIME_FACTOR.applyTo(info.getPrime())); + m.put(FACTOR_CRT_EXPONENT.getId(), (String)FACTOR_CRT_EXPONENT.applyTo(info.getExponent())); + m.put(FACTOR_CRT_COEFFICIENT.getId(), (String)FACTOR_CRT_COEFFICIENT.applyTo(info.getCrtCoefficient())); + return m; + } + + @Override + public RSAOtherPrimeInfo applyFrom(Object o) { + if (o == null) { + throw new MalformedKeyException("RSA JWK 'oth' (Other Prime Info) element cannot be null."); + } + if (!(o instanceof Map)) { + String msg = "RSA JWK 'oth' (Other Prime Info) must contain map elements of name/value pairs. " + + "Element type found: " + o.getClass().getName(); + throw new MalformedKeyException(msg); + } + Map m = (Map) o; + if (Collections.isEmpty(m)) { + throw new MalformedKeyException("RSA JWK 'oth' (Other Prime Info) element map cannot be empty."); + } + + // Need to add the values to a Context instance to satisfy the API contract of the getRequired* methods + // called below. It's less than ideal, but it works: + JwkContext ctx = new DefaultJwkContext<>(FIELDS); + for (Map.Entry entry : m.entrySet()) { + String name = String.valueOf(entry.getKey()); + ctx.put(name, entry.getValue()); + } + + final ValueGetter getter = new DefaultValueGetter(ctx); + BigInteger prime = getter.getRequiredBigInt(PRIME_FACTOR.getId(), true); + BigInteger primeExponent = getter.getRequiredBigInt(FACTOR_CRT_EXPONENT.getId(), true); + BigInteger crtCoefficient = getter.getRequiredBigInt(FACTOR_CRT_COEFFICIENT.getId(), true); + + return new RSAOtherPrimeInfo(prime, primeExponent, crtCoefficient); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java index 4d1c1e42d..92742d8b8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java @@ -4,12 +4,10 @@ import io.jsonwebtoken.impl.lang.Converter; import io.jsonwebtoken.impl.lang.Converters; import io.jsonwebtoken.impl.lang.Field; -import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.impl.lang.ValueGetter; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; -import io.jsonwebtoken.security.MalformedKeyException; import io.jsonwebtoken.security.RsaPrivateJwk; import io.jsonwebtoken.security.RsaPublicJwk; import io.jsonwebtoken.security.UnsupportedKeyException; @@ -27,9 +25,7 @@ import java.security.spec.RSAPrivateCrtKeySpec; import java.security.spec.RSAPrivateKeySpec; import java.security.spec.RSAPublicKeySpec; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Set; class RsaPrivateJwkFactory extends AbstractFamilyJwkFactory { @@ -42,7 +38,7 @@ class RsaPrivateJwkFactory extends AbstractFamilyJwkFactory, Object> RSA_OTHER_PRIMES_CONVERTER = - Converters.forList(new RSAOtherPrimeInfoConverter()); + Converters.forList(RSAOtherPrimeInfoConverter.INSTANCE); private static final String PUBKEY_ERR_MSG = "JwkContext publicKey must be an " + RSAPublicKey.class.getName() + " instance."; @@ -83,7 +79,8 @@ public RSAPublicKey apply(KeyFactory kf) { try { return (RSAPublicKey) kf.generatePublic(spec); } catch (Exception e) { - String msg = "Unable to derive RSAPublicKey from RSAPrivateKey {" + ctx + "}."; + String msg = "Unable to derive RSAPublicKey from RSAPrivateKey " + ctx + + ". Cause: " + e.getMessage(); throw new UnsupportedKeyException(msg); } } @@ -109,28 +106,27 @@ protected RsaPrivateJwk createJwkFromKey(JwkContext ctx) { RsaPublicJwk pubJwk = RsaPublicJwkFactory.DEFAULT_INSTANCE.createJwk(pubCtx); ctx.putAll(pubJwk); // add public values to private key context - ctx.put(DefaultRsaPrivateJwk.PRIVATE_EXPONENT.getId(), encode(key.getPrivateExponent())); + put(ctx, DefaultRsaPrivateJwk.PRIVATE_EXPONENT, key.getPrivateExponent()); if (key instanceof RSAPrivateCrtKey) { RSAPrivateCrtKey ckey = (RSAPrivateCrtKey) key; //noinspection DuplicatedCode - ctx.put(DefaultRsaPrivateJwk.FIRST_PRIME.getId(), encode(ckey.getPrimeP())); - ctx.put(DefaultRsaPrivateJwk.SECOND_PRIME.getId(), encode(ckey.getPrimeQ())); - ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT.getId(), encode(ckey.getPrimeExponentP())); - ctx.put(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT.getId(), encode(ckey.getPrimeExponentQ())); - ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT.getId(), encode(ckey.getCrtCoefficient())); + put(ctx, DefaultRsaPrivateJwk.FIRST_PRIME, ckey.getPrimeP()); + put(ctx, DefaultRsaPrivateJwk.SECOND_PRIME, ckey.getPrimeQ()); + put(ctx, DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, ckey.getPrimeExponentP()); + put(ctx, DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, ckey.getPrimeExponentQ()); + put(ctx, DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, ckey.getCrtCoefficient()); } else if (key instanceof RSAMultiPrimePrivateCrtKey) { RSAMultiPrimePrivateCrtKey ckey = (RSAMultiPrimePrivateCrtKey) key; //noinspection DuplicatedCode - ctx.put(DefaultRsaPrivateJwk.FIRST_PRIME.getId(), encode(ckey.getPrimeP())); - ctx.put(DefaultRsaPrivateJwk.SECOND_PRIME.getId(), encode(ckey.getPrimeQ())); - ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT.getId(), encode(ckey.getPrimeExponentP())); - ctx.put(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT.getId(), encode(ckey.getPrimeExponentQ())); - ctx.put(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT.getId(), encode(ckey.getCrtCoefficient())); + put(ctx, DefaultRsaPrivateJwk.FIRST_PRIME, ckey.getPrimeP()); + put(ctx, DefaultRsaPrivateJwk.SECOND_PRIME, ckey.getPrimeQ()); + put(ctx, DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, ckey.getPrimeExponentP()); + put(ctx, DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, ckey.getPrimeExponentQ()); + put(ctx, DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, ckey.getCrtCoefficient()); List infos = Arrays.asList(ckey.getOtherPrimeInfo()); if (!Collections.isEmpty(infos)) { - Object val = RSA_OTHER_PRIMES_CONVERTER.applyTo(infos); - ctx.put(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO.getId(), val); + put(ctx,DefaultRsaPrivateJwk.OTHER_PRIMES_INFO, infos); } } @@ -194,63 +190,18 @@ protected RsaPrivateJwk createJwkFromValues(JwkContext ctx) { spec = new RSAPrivateKeySpec(modulus, privateExponent); } - final KeySpec keySpec = spec; - RSAPrivateKey key = generateKey(ctx, new CheckedFunction() { - @Override - public RSAPrivateKey apply(KeyFactory kf) throws Exception { - return (RSAPrivateKey) kf.generatePrivate(keySpec); - } - }); + RSAPrivateKey key = generateFromSpec(ctx, spec); ctx.setKey(key); return new DefaultRsaPrivateJwk(ctx, pubJwk); } - static class RSAOtherPrimeInfoConverter implements Converter { - - static final Field PRIME_FACTOR = Fields.secretBigInt("r", "Prime Factor"); - static final Field FACTOR_CRT_EXPONENT = Fields.secretBigInt("d", "Factor CRT Exponent"); - static final Field FACTOR_CRT_COEFFICIENT = Fields.secretBigInt("t", "Factor CRT Coefficient"); - static final Set> FIELDS = Collections.>setOf(PRIME_FACTOR, FACTOR_CRT_EXPONENT, FACTOR_CRT_COEFFICIENT); - - @Override - public Object applyTo(RSAOtherPrimeInfo info) { - Map m = new LinkedHashMap<>(3); - m.put(PRIME_FACTOR.getId(), encode(info.getPrime())); - m.put(FACTOR_CRT_EXPONENT.getId(), encode(info.getExponent())); - m.put(FACTOR_CRT_COEFFICIENT.getId(), encode(info.getCrtCoefficient())); - return m; - } - - @Override - public RSAOtherPrimeInfo applyFrom(Object o) { - if (o == null) { - throw new MalformedKeyException("RSA JWK 'oth' Other Prime Info element cannot be null."); - } - if (!(o instanceof Map)) { - String msg = "RSA JWK 'oth' Other Prime Info list must contain map elements of name/value pairs. " + - "Element type found: " + o.getClass().getName(); - throw new MalformedKeyException(msg); - } - Map m = (Map) o; - if (Collections.isEmpty(m)) { - throw new MalformedKeyException("RSA JWK 'oth' Other Prime Info element map cannot be empty."); - } - - // Need to add the values to a Context instance to satisfy the API contract of the getRequired* methods - // called below. It's less than ideal, but it works: - JwkContext ctx = new DefaultJwkContext<>(FIELDS); - for (Map.Entry entry : m.entrySet()) { - String name = String.valueOf(entry.getKey()); - ctx.put(name, entry.getValue()); + protected RSAPrivateKey generateFromSpec(JwkContext ctx, final KeySpec keySpec) { + return generateKey(ctx, new CheckedFunction() { + @Override + public RSAPrivateKey apply(KeyFactory kf) throws Exception { + return (RSAPrivateKey) kf.generatePrivate(keySpec); } - - final ValueGetter getter = new DefaultValueGetter(ctx); - BigInteger prime = getter.getRequiredBigInt(PRIME_FACTOR.getId(), true); - BigInteger primeExponent = getter.getRequiredBigInt(FACTOR_CRT_EXPONENT.getId(), true); - BigInteger crtCoefficient = getter.getRequiredBigInt(FACTOR_CRT_COEFFICIENT.getId(), true); - - return new RSAOtherPrimeInfo(prime, primeExponent, crtCoefficient); - } + }); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java index 7c202d747..f4d76f3c3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java @@ -20,8 +20,8 @@ class RsaPublicJwkFactory extends AbstractFamilyJwkFactory ctx) { RSAPublicKey key = ctx.getKey(); - ctx.put(DefaultRsaPublicJwk.MODULUS.getId(), encode(key.getModulus())); - ctx.put(DefaultRsaPublicJwk.PUBLIC_EXPONENT.getId(), encode(key.getPublicExponent())); + ctx.put(DefaultRsaPublicJwk.MODULUS.getId(), DefaultRsaPublicJwk.MODULUS.applyTo(key.getModulus())); + ctx.put(DefaultRsaPublicJwk.PUBLIC_EXPONENT.getId(), DefaultRsaPublicJwk.PUBLIC_EXPONENT.applyTo(key.getPublicExponent())); return new DefaultRsaPublicJwk(ctx); } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy index 2f23301bb..89e1d4b84 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy @@ -1,11 +1,15 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.lang.Assert +import io.jsonwebtoken.security.EcPrivateJwk +import io.jsonwebtoken.security.EcPublicJwk import io.jsonwebtoken.security.Jwks import io.jsonwebtoken.security.RsaPublicJwkBuilder import org.junit.Test import java.security.cert.X509Certificate +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey @@ -59,4 +63,25 @@ class AbstractAsymmetricJwkBuilderTest { Assert.notEmpty(jwk.getX509CertificateSha256Thumbprint()) Assert.hasText(jwk.get(AbstractAsymmetricJwk.X5T_S256.getId()) as String) } + + @Test + void testEcPrivateJwkFromPublicBuilder() { + def pair = TestKeys.ES256.pair + + //start with a public key builder + def builder = Jwks.builder().setKey(pair.public as ECPublicKey) + assertTrue builder instanceof AbstractAsymmetricJwkBuilder.DefaultEcPublicJwkBuilder + + //applying the private key turns it into a private key builder + builder = builder.setPrivateKey(pair.private as ECPrivateKey) + assertTrue builder instanceof AbstractAsymmetricJwkBuilder.DefaultEcPrivateJwkBuilder + + //building creates a private jwk: + def jwk = builder.build() + assertTrue jwk instanceof EcPrivateJwk + + //which also has information for the public key: + jwk = jwk.toPublicJwk() + assertTrue jwk instanceof EcPublicJwk + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy new file mode 100644 index 000000000..9c1700227 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy @@ -0,0 +1,147 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.lang.Collections +import io.jsonwebtoken.security.Jwks +import org.junit.Before +import org.junit.Test + +import javax.crypto.SecretKey +import java.security.Key + +import static org.junit.Assert.* + +class AbstractJwkTest { + + AbstractJwk jwk + + static JwkContext newCtx() { + return newCtx(null) + } + + static JwkContext newCtx(Map map) { + def ctx = new DefaultJwkContext(AbstractJwk.FIELDS) + ctx.put('kty', 'test') + if (!Collections.isEmpty(map as Map)) { + ctx.putAll(map) + } + ctx.setKey(TestKeys.HS256) + return ctx + } + + static AbstractJwk newJwk(JwkContext ctx) { + return new AbstractJwk(ctx) {} + } + + @Before + void setUp() { + jwk = newJwk(newCtx()) + } + + @Test + void testContainsValue() { + assertTrue jwk.containsValue('test') + assertFalse jwk.containsValue('bar') + } + + static void jwkImmutable(Closure c) { + try { + c.call() + fail() + } catch (UnsupportedOperationException expected) { + String msg = 'JWKs are immutable and may not be modified.' + assertEquals msg, expected.getMessage() + } + } + + static void jucImmutable(Closure c) { + try { + c.call() + fail() + } catch (UnsupportedOperationException expected) { + assertNull expected.getMessage() // java.util.Collections.unmodifiable* doesn't give a message + } + } + + @Test + void testImmutable() { + jwk = newJwk(newCtx()) + jwkImmutable { jwk.put('foo', 'bar') } + jwkImmutable { jwk.putAll([foo: 'bar']) } + jwkImmutable { jwk.remove('kty') } + jwkImmutable { jwk.clear() } + } + + @Test + // ensure that any map or collection returned from the JWK is immutable as well: + void testCollectionsAreImmutable() { + def vals = [ + map: [foo: 'bar'], + list: ['a'], + set: ['b'] as Set, + collection: ['c'] as Collection + ] + jwk = newJwk(newCtx(vals)) + jucImmutable { (jwk.get('map') as Map).remove('foo') } + jucImmutable { (jwk.get('list') as List).remove(0) } + jucImmutable { (jwk.get('set') as Set).remove('b') } + jucImmutable { (jwk.get('collection') as Collection).remove('c') } + jucImmutable { jwk.keySet().remove('map') } + jucImmutable { jwk.values().remove('a') } + } + + @Test + // ensure that any array value returned from the JWK is a copy, so modifying it won't modify the original array + void testArraysAreCopied() { + def vals = [ + array: ['a', 'b' ] as String[] + ] + jwk = newJwk(newCtx(vals)) + def returned = jwk.get('array') + assertTrue returned instanceof String[] + assertEquals 2, returned.length + + //now modify it: + returned[0] = 'x' + + //ensure the array structure hasn't changed: + def returned2 = jwk.get('array') + assertEquals 'a', returned2[0] + assertEquals 'b', returned2[1] + } + + @Test + void testPrivateJwkToStringHasRedactedValues() { + def secretJwk = Jwks.builder().setKey(TestKeys.HS256).build() + assertTrue secretJwk.toString().contains('k=') + + def ecPrivJwk = Jwks.builder().setKey(TestKeys.ES256.pair.private).build() + assertTrue ecPrivJwk.toString().contains('d=') + + def rsaPrivJwk = Jwks.builder().setKey(TestKeys.RS256.pair.private).build() + String s = 'd=, p=, q=, dp=, dq=, qi=' + assertTrue rsaPrivJwk.toString().contains(s) + } + + @Test + void testPrivateJwkHashCode() { + assertEquals jwk.hashCode(), jwk.@context.hashCode() + + def secretJwk1 = Jwks.builder().setKey(TestKeys.HS256).put('hello', 'world').build() + def secretJwk2 = Jwks.builder().setKey(TestKeys.HS256).put('hello', 'world').build() + assertEquals secretJwk1.hashCode(), secretJwk1.@context.hashCode() + assertEquals secretJwk2.hashCode(), secretJwk2.@context.hashCode() + assertEquals secretJwk1.hashCode(), secretJwk2.hashCode() + + def ecPrivJwk1 = Jwks.builder().setKey(TestKeys.ES256.pair.private).put('hello', 'ecworld').build() + def ecPrivJwk2 = Jwks.builder().setKey(TestKeys.ES256.pair.private).put('hello', 'ecworld').build() + assertEquals ecPrivJwk1.hashCode(), ecPrivJwk2.hashCode() + assertEquals ecPrivJwk1.hashCode(), ecPrivJwk1.@context.hashCode() + assertEquals ecPrivJwk2.hashCode(), ecPrivJwk2.@context.hashCode() + + def rsaPrivJwk1 = Jwks.builder().setKey(TestKeys.RS256.pair.private).put('hello', 'rsaworld').build() + def rsaPrivJwk2 = Jwks.builder().setKey(TestKeys.RS256.pair.private).put('hello', 'rsaworld').build() + assertEquals rsaPrivJwk1.hashCode(), rsaPrivJwk2.hashCode() + assertEquals rsaPrivJwk1.hashCode(), rsaPrivJwk1.@context.hashCode() + assertEquals rsaPrivJwk2.hashCode(), rsaPrivJwk2.@context.hashCode() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy index d36636336..b3dc8f85c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy @@ -63,7 +63,7 @@ class EcdhKeyAlgorithmTest { fail() } catch (InvalidKeyException expected) { String msg = expected.getMessage() - String expectedMsg = String.format(EcPublicJwkFactory.JWK_CONTAINS_FORMAT_MSG, pubJwk.crv, jwk) + String expectedMsg = EcPublicJwkFactory.jwkContainsErrorMessage(pubJwk.crv as String, jwk) assertEquals(expectedMsg, msg) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index c56437926..15a24ce9a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -275,7 +275,7 @@ class JwksTest { Jwks.builder().setKey(badPubKey).build() } catch (InvalidKeyException ike) { String curveId = jwk.get('crv') - String msg = String.format(EcPublicJwkFactory.KEY_CONTAINS_FORMAT_MSG, curveId, curveId) + String msg = EcPublicJwkFactory.keyContainsErrorMessage(curveId) assertEquals msg, ike.getMessage() } @@ -287,7 +287,7 @@ class JwksTest { try { Jwks.builder().putAll(modified).build() } catch (InvalidKeyException ike) { - String expected = String.format(EcPublicJwkFactory.JWK_CONTAINS_FORMAT_MSG, jwk.get('crv'), modified) + String expected = EcPublicJwkFactory.jwkContainsErrorMessage(jwk.crv as String, modified) assertEquals(expected, ike.getMessage()) } } @@ -297,7 +297,7 @@ class JwksTest { try { Jwks.builder().putAll(modified).build() } catch (InvalidKeyException ike) { - String expected = String.format(EcPublicJwkFactory.JWK_CONTAINS_FORMAT_MSG, jwk.get('crv'), modified) + String expected = EcPublicJwkFactory.jwkContainsErrorMessage(jwk.crv as String, modified) assertEquals(expected, ike.getMessage()) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy index 19c8afa02..2aad46dc7 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy @@ -1,5 +1,6 @@ package io.jsonwebtoken.impl.security +import io.jsonwebtoken.impl.lang.Converters import io.jsonwebtoken.security.EcPrivateJwk import io.jsonwebtoken.security.Jwks import io.jsonwebtoken.security.RsaPrivateJwk @@ -20,7 +21,7 @@ class RFC7517AppendixA2Test { } private static final String rsaEncode(BigInteger i) { - return AbstractFamilyJwkFactory.encode(i) + return Converters.BIGINT.applyTo(i) as String } private static final List> keys = [ diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixBTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixBTest.groovy index 60b69b890..f133c010e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixBTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixBTest.groovy @@ -1,5 +1,6 @@ package io.jsonwebtoken.impl.security +import io.jsonwebtoken.impl.lang.Converters import io.jsonwebtoken.security.Jwks import io.jsonwebtoken.security.RsaPublicJwk import org.junit.Test @@ -57,8 +58,8 @@ class RFC7517AppendixBTest { assertEquals m.kty, jwk.getType() assertEquals m.use, jwk.getPublicKeyUse() assertEquals m.kid, jwk.getId() - assertEquals m.n, AbstractFamilyJwkFactory.encode(key.getModulus()) - assertEquals m.e, AbstractFamilyJwkFactory.encode(key.getPublicExponent()) + assertEquals m.n, Converters.BIGINT.applyTo(key.getModulus()) + assertEquals m.e, Converters.BIGINT.applyTo(key.getPublicExponent()) def chain = jwk.getX509CertificateChain() assertNotNull chain assertFalse chain.isEmpty() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverterTest.groovy new file mode 100644 index 000000000..9e9c97248 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverterTest.groovy @@ -0,0 +1,44 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +class RSAOtherPrimeInfoConverterTest { + + @Test + void testApplyFromNull() { + try { + RSAOtherPrimeInfoConverter.INSTANCE.applyFrom(null) + fail() + } catch (MalformedKeyException expected) { + String msg = 'RSA JWK \'oth\' (Other Prime Info) element cannot be null.' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testApplyFromWithoutMap() { + try { + RSAOtherPrimeInfoConverter.INSTANCE.applyFrom(42) + fail() + } catch (MalformedKeyException expected) { + String msg = 'RSA JWK \'oth\' (Other Prime Info) must contain map elements of ' + + 'name/value pairs. Element type found: java.lang.Integer' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testApplyFromWithEmptyMap() { + try { + RSAOtherPrimeInfoConverter.INSTANCE.applyFrom([:]) + fail() + } catch (MalformedKeyException expected) { + String msg = 'RSA JWK \'oth\' (Other Prime Info) element map cannot be empty.' + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy new file mode 100644 index 000000000..5d3971f11 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy @@ -0,0 +1,240 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.impl.lang.Converters +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPrivateJwk +import io.jsonwebtoken.security.UnsupportedKeyException +import org.junit.Test + +import java.security.interfaces.RSAMultiPrimePrivateCrtKey +import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.KeySpec +import java.security.spec.RSAMultiPrimePrivateCrtKeySpec +import java.security.spec.RSAOtherPrimeInfo + +import static org.junit.Assert.* + +class RsaPrivateJwkFactoryTest { + + @Test + void testGetPublicExponentFailure() { + + def key = new TestRSAPrivateKey(null) { + @Override + BigInteger getModulus() { + return null + } + } + + try { + Jwks.builder().setKey(key).build() + fail() + } catch (UnsupportedKeyException expected) { + String msg = 'Unable to derive RSAPublicKey from RSAPrivateKey implementation ' + + '[io.jsonwebtoken.impl.security.RsaPrivateJwkFactoryTest$1]. Supported keys implement the ' + + 'java.security.interfaces.RSAPrivateCrtKey or ' + + 'java.security.interfaces.RSAMultiPrimePrivateCrtKey interfaces. If the specified RSAPrivateKey ' + + 'cannot be one of these two, you must explicitly provide an RSAPublicKey in addition to the ' + + 'RSAPrivateKey, as the [JWA RFC, Section 6.3.2]' + + '(https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.2) requires public values to be ' + + 'present in private RSA JWKs.' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testFailedPublicKeyDerivation() { + def key = new RSAPrivateCrtKey() { + @Override + BigInteger getPublicExponent() { + return BigInteger.ZERO + } + + @Override + BigInteger getPrimeP() { + return null + } + + @Override + BigInteger getPrimeQ() { + return null + } + + @Override + BigInteger getPrimeExponentP() { + return null + } + + @Override + BigInteger getPrimeExponentQ() { + return null + } + + @Override + BigInteger getCrtCoefficient() { + return null + } + + @Override + BigInteger getPrivateExponent() { + return null + } + + @Override + String getAlgorithm() { + return null + } + + @Override + String getFormat() { + return null + } + + @Override + byte[] getEncoded() { + return new byte[0] + } + + @Override + BigInteger getModulus() { + return BigInteger.ZERO + } + } + + try { + Jwks.builder().setKey(key).build() + fail() + } catch (UnsupportedKeyException expected) { + String prefix = 'Unable to derive RSAPublicKey from RSAPrivateKey {kty=RSA}. Cause: ' + assertTrue expected.getMessage().startsWith(prefix) + } + } + + @Test + void testMultiPrimePrivateKey() { + def pair = TestKeys.RS256.pair + RSAPrivateCrtKey priv = pair.private + RSAPublicKey pub = pair.public + + def info1 = new RSAOtherPrimeInfo(BigInteger.ONE, BigInteger.ONE, BigInteger.ONE) + def info2 = new RSAOtherPrimeInfo(BigInteger.TEN, BigInteger.TEN, BigInteger.TEN) + def infos = [ info1, info2 ] + + //build up test key: + RSAMultiPrimePrivateCrtKey key = new TestRSAMultiPrimePrivateCrtKey(priv, infos) + + RsaPrivateJwk jwk = Jwks.builder().setKey(key).build() + + List oth = jwk.get('oth') as List + assertTrue oth instanceof List + assertEquals 2, oth.size() + + Map one = oth.get(0) as Map + assertEquals one.r, RSAOtherPrimeInfoConverter.PRIME_FACTOR.applyTo(info1.prime) + assertEquals one.d, RSAOtherPrimeInfoConverter.FACTOR_CRT_EXPONENT.applyTo(info1.crtCoefficient) + assertEquals one.t, RSAOtherPrimeInfoConverter.FACTOR_CRT_COEFFICIENT .applyTo(info1.crtCoefficient) + + Map two = oth.get(1) as Map + assertEquals two.r, RSAOtherPrimeInfoConverter.PRIME_FACTOR.applyTo(info2.prime) + assertEquals two.d, RSAOtherPrimeInfoConverter.FACTOR_CRT_EXPONENT.applyTo(info2.crtCoefficient) + assertEquals two.t, RSAOtherPrimeInfoConverter.FACTOR_CRT_COEFFICIENT .applyTo(info2.crtCoefficient) + } + + @Test + void testMultiPrimePrivateKeyWithoutExtraInfo() { + def pair = TestKeys.RS256.pair + RSAPrivateCrtKey priv = pair.private + RSAPublicKey pub = pair.public + + RsaPrivateJwk jwk = Jwks.builder().setKey(priv).setPublicKey(pub).build() + // an RSAMultiPrimePrivateCrtKey without OtherInfo elements is treated the same as a normal RSAPrivateCrtKey, + // so ensure they are equal: + RSAMultiPrimePrivateCrtKey key = new TestRSAMultiPrimePrivateCrtKey(priv, null) + RsaPrivateJwk jwk2 = Jwks.builder().setKey(key).setPublicKey(pub).build() + assertEquals jwk, jwk2 + assertNull jwk.get(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO.getId()) + assertNull jwk2.get(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO.getId()) + } + + @Test + void testNonCrtPrivateKey() { //tests a standard RSAPrivateKey (not a RSAPrivateCrtKey or RSAMultiPrimePrivateCrtKey): + def pair = TestKeys.RS256.pair + RSAPrivateCrtKey privCrtKey = pair.private + RSAPublicKey pub = pair.public + + def priv = new TestRSAPrivateKey(pair.private) + + RsaPrivateJwk jwk = Jwks.builder().setKey(priv).setPublicKey(pub).build() + assertEquals 4, jwk.size() // kty, public exponent, modulus, private exponent + assertEquals 'RSA', jwk.getType() + assertEquals Converters.BIGINT.applyTo(pub.getModulus()), jwk.get(DefaultRsaPublicJwk.MODULUS.getId()) + assertEquals Converters.BIGINT.applyTo(pub.getPublicExponent()), jwk.get(DefaultRsaPublicJwk.PUBLIC_EXPONENT.getId()) + assertEquals Converters.BIGINT.applyTo(priv.getPrivateExponent()), jwk.get(DefaultRsaPrivateJwk.PRIVATE_EXPONENT.getId()) + } + + @Test + void testCreateJwkFromMinimalValues() { // no optional private values + def pair = TestKeys.RS256.pair + RSAPublicKey pub = pair.public + RSAPrivateKey priv = new TestRSAPrivateKey(pair.private) + def jwk = Jwks.builder().setKey(priv).setPublicKey(pub).build() + //minimal values: kty, modulus, public exponent, private exponent = 4 fields: + assertEquals 4, jwk.size() + def map = new LinkedHashMap(jwk) + assertEquals 4, map.size() + + def jwkFromValues = Jwks.builder().putAll(map).build() + + //ensure they're equal: + assertEquals jwk, jwkFromValues + } + + @Test + void testCreateJwkFromMultiPrimeValues() { + def pair = TestKeys.RS256.pair + RSAPrivateCrtKey priv = pair.private + RSAPublicKey pub = pair.public + + def info1 = new RSAOtherPrimeInfo(BigInteger.ONE, BigInteger.ONE, BigInteger.ONE) + def info2 = new RSAOtherPrimeInfo(BigInteger.TEN, BigInteger.TEN, BigInteger.TEN) + def infos = [ info1, info2 ] + RSAMultiPrimePrivateCrtKey key = new TestRSAMultiPrimePrivateCrtKey(priv, infos) + + final RsaPrivateJwk jwk = Jwks.builder().setKey(key).setPublicKey(pub).build() + + //we have to test the class directly and override, since the dummy MultiPrime values won't be accepted by the + //JVM: + def factory = new RsaPrivateJwkFactory() { + @Override + protected RSAPrivateKey generateFromSpec(JwkContext ctx, KeySpec keySpec) { + assertTrue keySpec instanceof RSAMultiPrimePrivateCrtKeySpec + RSAMultiPrimePrivateCrtKeySpec spec = (RSAMultiPrimePrivateCrtKeySpec)keySpec + assertEquals key.modulus, spec.modulus + assertEquals key.publicExponent, spec.publicExponent + assertEquals key.privateExponent, spec.privateExponent + assertEquals key.primeP, spec.primeP + assertEquals key.primeQ, spec.primeQ + assertEquals key.primeExponentP, spec.primeExponentP + assertEquals key.primeExponentQ, spec.primeExponentQ + assertEquals key.crtCoefficient, spec.crtCoefficient + + for(int i = 0; i < infos.size(); i++) { + RSAOtherPrimeInfo orig = infos.get(i) + RSAOtherPrimeInfo copy = spec.otherPrimeInfo[i] + assertEquals orig.prime, copy.prime + assertEquals orig.exponent, copy.exponent + assertEquals orig.crtCoefficient, copy.crtCoefficient + + } + return new TestRSAMultiPrimePrivateCrtKey(priv, infos) + } + } + + def returned = factory.createJwkFromValues(jwk.@context) + + assertEquals jwk, returned + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAKey.groovy new file mode 100644 index 000000000..02bd7df2c --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAKey.groovy @@ -0,0 +1,33 @@ +package io.jsonwebtoken.impl.security + +import java.security.Key +import java.security.interfaces.RSAKey + +class TestRSAKey implements RSAKey, Key { + + final T src + + TestRSAKey(T key) { + this.src = key + } + + @Override + String getAlgorithm() { + return src.getAlgorithm() + } + + @Override + String getFormat() { + return src.getFormat() + } + + @Override + byte[] getEncoded() { + return src.getEncoded() + } + + @Override + BigInteger getModulus() { + return src.getModulus() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAMultiPrimePrivateCrtKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAMultiPrimePrivateCrtKey.groovy new file mode 100644 index 000000000..5782444ae --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAMultiPrimePrivateCrtKey.groovy @@ -0,0 +1,50 @@ +package io.jsonwebtoken.impl.security + +import java.security.interfaces.RSAMultiPrimePrivateCrtKey +import java.security.interfaces.RSAPrivateCrtKey +import java.security.spec.RSAOtherPrimeInfo + +class TestRSAMultiPrimePrivateCrtKey extends TestRSAPrivateKey implements RSAMultiPrimePrivateCrtKey { + + private final List infos + + TestRSAMultiPrimePrivateCrtKey(RSAPrivateCrtKey src, List infos) { + super(src) + this.infos = infos + } + + @Override + BigInteger getPublicExponent() { + return src.publicExponent + } + + @Override + BigInteger getPrimeP() { + return src.primeP + } + + @Override + BigInteger getPrimeQ() { + return src.primeQ + } + + @Override + BigInteger getPrimeExponentP() { + return src.primeExponentP + } + + @Override + BigInteger getPrimeExponentQ() { + return src.primeExponentQ + } + + @Override + BigInteger getCrtCoefficient() { + return src.crtCoefficient + } + + @Override + RSAOtherPrimeInfo[] getOtherPrimeInfo() { + return infos as RSAOtherPrimeInfo[] + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAPrivateKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAPrivateKey.groovy new file mode 100644 index 000000000..7e949bb20 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAPrivateKey.groovy @@ -0,0 +1,15 @@ +package io.jsonwebtoken.impl.security + +import java.security.interfaces.RSAPrivateKey + +class TestRSAPrivateKey extends TestRSAKey implements RSAPrivateKey { + + TestRSAPrivateKey(T key) { + super(key) + } + + @Override + BigInteger getPrivateExponent() { + return src.privateExponent + } +} From bac861dff9fdfa763e46f5bae15a4275c8bc7427 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Tue, 3 May 2022 19:43:27 -0400 Subject: [PATCH 37/75] PayloadSupplier renamed to Message --- .../io/jsonwebtoken/CompressionCodec.java | 4 +-- .../jsonwebtoken/security/AeadAlgorithm.java | 2 +- .../io/jsonwebtoken/security/AeadRequest.java | 2 +- .../io/jsonwebtoken/security/AeadResult.java | 2 +- .../jsonwebtoken/security/CryptoRequest.java | 2 +- .../security/DecryptionKeyRequest.java | 2 +- .../io/jsonwebtoken/security/KeyResult.java | 2 +- .../{PayloadSupplier.java => Message.java} | 4 +-- .../security/SignatureRequest.java | 2 +- .../jsonwebtoken/impl/DefaultJweBuilder.java | 4 +-- .../jsonwebtoken/impl/DefaultJwtParser.java | 6 ++-- .../compression/AbstractCompressionCodec.java | 22 ++++++------- .../compression/DeflateCompressionCodec.java | 4 +-- .../compression/GzipCompressionCodec.java | 4 +-- .../security/AbstractSignatureAlgorithm.java | 4 +-- .../impl/security/AesGcmKeyAlgorithm.java | 2 +- .../impl/security/AesWrapKeyAlgorithm.java | 2 +- .../impl/security/DefaultAeadRequest.java | 2 +- .../impl/security/DefaultCryptoRequest.java | 6 ++-- .../security/DefaultDecryptionKeyRequest.java | 10 +++--- ...efaultEllipticCurveSignatureAlgorithm.java | 4 +-- .../impl/security/DefaultKeyResult.java | 8 ++--- .../impl/security/DefaultMessage.java | 19 ++++++++++++ .../impl/security/DefaultPayloadSupplier.java | 31 ------------------- .../impl/security/DefaultRsaKeyAlgorithm.java | 2 +- .../DefaultRsaSignatureAlgorithm.java | 4 +-- .../security/DefaultSignatureRequest.java | 2 +- .../impl/security/EcdhKeyAlgorithm.java | 2 +- .../impl/security/GcmAesAeadAlgorithm.java | 10 +++--- .../impl/security/HmacAesAeadAlgorithm.java | 12 +++---- .../impl/security/MacSignatureAlgorithm.java | 2 +- .../impl/security/Pbes2HsAkwAlgorithm.java | 2 +- .../compression/YagCompressionCodec.groovy | 2 +- .../impl/security/AesAlgorithmTest.groovy | 10 ++---- .../security/AesGcmKeyAlgorithmTest.groovy | 6 ++-- ...rTest.groovy => DefaultMessageTest.groovy} | 6 ++-- .../security/DirectKeyAlgorithmTest.groovy | 5 +-- .../security/GcmAesAeadAlgorithmTest.groovy | 4 +-- .../security/HmacAesAeadAlgorithmTest.groovy | 2 +- .../security/RFC7518AppendixB1Test.groovy | 4 +-- .../security/RFC7518AppendixB2Test.groovy | 4 +-- .../security/RFC7518AppendixB3Test.groovy | 4 +-- .../security/EncryptionAlgorithmsTest.groovy | 10 +++--- 43 files changed, 113 insertions(+), 130 deletions(-) rename api/src/main/java/io/jsonwebtoken/security/{PayloadSupplier.java => Message.java} (86%) create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMessage.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPayloadSupplier.java rename impl/src/test/groovy/io/jsonwebtoken/impl/security/{DefaultPayloadSupplierTest.groovy => DefaultMessageTest.groovy} (64%) diff --git a/api/src/main/java/io/jsonwebtoken/CompressionCodec.java b/api/src/main/java/io/jsonwebtoken/CompressionCodec.java index 1fbf38c14..41b8e4c34 100644 --- a/api/src/main/java/io/jsonwebtoken/CompressionCodec.java +++ b/api/src/main/java/io/jsonwebtoken/CompressionCodec.java @@ -36,12 +36,12 @@ public interface CompressionCodec { /** * Compresses the specified byte array according to the compression {@link #getAlgorithmName() algorithm}. * - * @param payload bytes to compress + * @param content bytes to compress * @return compressed bytes * @throws CompressionException if the specified byte array cannot be compressed according to the compression * {@link #getAlgorithmName() algorithm}. */ - byte[] compress(byte[] payload) throws CompressionException; + byte[] compress(byte[] content) throws CompressionException; /** * Decompresses the specified compressed byte array according to the compression diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java index 599c83c70..a6d48a484 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java @@ -26,5 +26,5 @@ public interface AeadAlgorithm extends Identifiable, KeyBuilderSupplier decrypt(DecryptAeadRequest request) throws SecurityException; + Message decrypt(DecryptAeadRequest request) throws SecurityException; } diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java b/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java index 6e4306020..7cb491234 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java @@ -20,5 +20,5 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface AeadRequest extends CryptoRequest, AssociatedDataSupplier { +public interface AeadRequest extends CryptoRequest, AssociatedDataSupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadResult.java b/api/src/main/java/io/jsonwebtoken/security/AeadResult.java index 999987a6a..558b51805 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AeadResult.java +++ b/api/src/main/java/io/jsonwebtoken/security/AeadResult.java @@ -18,5 +18,5 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface AeadResult extends PayloadSupplier, DigestSupplier, InitializationVectorSupplier { +public interface AeadResult extends Message, DigestSupplier, InitializationVectorSupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java b/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java index afa9476d8..78ff34515 100644 --- a/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java @@ -20,5 +20,5 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface CryptoRequest extends SecurityRequest, KeySupplier, PayloadSupplier { +public interface CryptoRequest extends Message, SecurityRequest, KeySupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java index 08040646a..f39b23167 100644 --- a/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java @@ -20,5 +20,5 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface DecryptionKeyRequest extends KeyRequest, PayloadSupplier { +public interface DecryptionKeyRequest extends KeyRequest, Message { } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyResult.java b/api/src/main/java/io/jsonwebtoken/security/KeyResult.java index 2426dfbc6..7557ba026 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyResult.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyResult.java @@ -20,5 +20,5 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface KeyResult extends PayloadSupplier, KeySupplier { +public interface KeyResult extends Message, KeySupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/PayloadSupplier.java b/api/src/main/java/io/jsonwebtoken/security/Message.java similarity index 86% rename from api/src/main/java/io/jsonwebtoken/security/PayloadSupplier.java rename to api/src/main/java/io/jsonwebtoken/security/Message.java index 49e1116c5..4b63af816 100644 --- a/api/src/main/java/io/jsonwebtoken/security/PayloadSupplier.java +++ b/api/src/main/java/io/jsonwebtoken/security/Message.java @@ -18,8 +18,8 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface PayloadSupplier { +public interface Message { - T getPayload(); //plaintext, ciphertext, or Key to be wrapped + byte[] getContent(); //plaintext or ciphertext } diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java b/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java index d053fff7e..2647d8807 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java @@ -20,5 +20,5 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface SignatureRequest extends CryptoRequest { +public interface SignatureRequest extends CryptoRequest { } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java index ecc6a6147..656bd81ad 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java @@ -145,7 +145,7 @@ public String compact() { Assert.state(keyResult != null, "KeyAlgorithm must return a KeyResult."); SecretKey cek = Assert.notNull(keyResult.getKey(), "KeyResult must return a content encryption key."); - byte[] encryptedCek = Assert.notNull(keyResult.getPayload(), "KeyResult must return an encrypted key byte array, even if empty."); + byte[] encryptedCek = Assert.notNull(keyResult.getContent(), "KeyResult must return an encrypted key byte array, even if empty."); jweHeader.put(DefaultHeader.ALGORITHM.getId(), alg.getId()); jweHeader.put(DefaultJweHeader.ENCRYPTION_ALGORITHM.getId(), enc.getId()); @@ -158,7 +158,7 @@ public String compact() { AeadResult encResult = encFunction.apply(encRequest); byte[] iv = Assert.notEmpty(encResult.getInitializationVector(), "Encryption result must have a non-empty initialization vector."); - byte[] ciphertext = Assert.notEmpty(encResult.getPayload(), "Encryption result must have non-empty ciphertext (result.getData())."); + byte[] ciphertext = Assert.notEmpty(encResult.getContent(), "Encryption result must have non-empty ciphertext (result.getData())."); byte[] tag = Assert.notEmpty(encResult.getDigest(), "Encryption result must have a non-empty authentication tag."); String base64UrlEncodedEncryptedCek = base64UrlEncoder.encode(encryptedCek); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index d6e20d4ac..18f73da6c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -67,7 +67,7 @@ import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.Keys; -import io.jsonwebtoken.security.PayloadSupplier; +import io.jsonwebtoken.security.Message; import io.jsonwebtoken.security.SignatureAlgorithm; import io.jsonwebtoken.security.SignatureAlgorithms; import io.jsonwebtoken.security.SignatureException; @@ -498,8 +498,8 @@ private static boolean hasContentType(Header header) { DecryptAeadRequest decryptRequest = new DefaultAeadResult(this.provider, null, bytes, cek, aad, tag, iv); - PayloadSupplier result = encAlg.decrypt(decryptRequest); - bytes = result.getPayload(); + Message result = encAlg.decrypt(decryptRequest); + bytes = result.getContent(); } //TODO: Only allow decompression after JWS signature verification: diff --git a/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java b/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java index b8b2b5771..163522061 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java @@ -58,11 +58,11 @@ byte[] readAndClose(InputStream input) throws IOException { //package-protected for a point release. This can be made protected on a minor release (0.11.0, 0.12.0, 1.0, etc). //TODO: make protected on a minor release - byte[] writeAndClose(byte[] payload, StreamWrapper wrapper) throws IOException { + byte[] writeAndClose(byte[] content, StreamWrapper wrapper) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(512); OutputStream compressionStream = wrapper.wrap(outputStream); try { - compressionStream.write(payload); + compressionStream.write(content); compressionStream.flush(); } finally { Objects.nullSafeClose(compressionStream); @@ -71,29 +71,29 @@ byte[] writeAndClose(byte[] payload, StreamWrapper wrapper) throws IOException { } /** - * Implement this method to do the actual work of compressing the payload + * Implement this method to do the actual work of compressing the content * - * @param payload the bytes to compress + * @param content the bytes to compress * @return the compressed bytes * @throws IOException if the compression causes an IOException */ - protected abstract byte[] doCompress(byte[] payload) throws IOException; + protected abstract byte[] doCompress(byte[] content) throws IOException; /** - * Asserts that payload is not null and calls {@link #doCompress(byte[]) doCompress} + * Asserts that content is not null and calls {@link #doCompress(byte[]) doCompress} * - * @param payload bytes to compress + * @param content bytes to compress * @return compressed bytes * @throws CompressionException if {@link #doCompress(byte[]) doCompress} throws an IOException */ @Override - public final byte[] compress(byte[] payload) { - Assert.notNull(payload, "payload cannot be null."); + public final byte[] compress(byte[] content) { + Assert.notNull(content, "content cannot be null."); try { - return doCompress(payload); + return doCompress(content); } catch (IOException e) { - throw new CompressionException("Unable to compress payload.", e); + throw new CompressionException("Unable to compress content.", e); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java b/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java index 978ced22e..db3cf0cab 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java @@ -48,8 +48,8 @@ public String getAlgorithmName() { } @Override - protected byte[] doCompress(byte[] payload) throws IOException { - return writeAndClose(payload, WRAPPER); + protected byte[] doCompress(byte[] content) throws IOException { + return writeAndClose(content, WRAPPER); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java b/impl/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java index 978f1dc01..a9d166d7e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java @@ -45,8 +45,8 @@ public String getAlgorithmName() { } @Override - protected byte[] doCompress(byte[] payload) throws IOException { - return writeAndClose(payload, WRAPPER); + protected byte[] doCompress(byte[] content) throws IOException { + return writeAndClose(content, WRAPPER); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java index baeb1e274..5ba64203c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java @@ -26,7 +26,7 @@ protected static String keyType(boolean signing) { @Override public byte[] sign(SignatureRequest request) throws SecurityException { final SK key = Assert.notNull(request.getKey(), "Request key cannot be null."); - Assert.notEmpty(request.getPayload(), "Request payload cannot be null or empty."); + Assert.notEmpty(request.getContent(), "Request content cannot be null or empty."); try { validateKey(key, true); return doSign(request); @@ -44,7 +44,7 @@ public byte[] sign(SignatureRequest request) throws SecurityException { @Override public boolean verify(VerifySignatureRequest request) throws SecurityException { final VK key = Assert.notNull(request.getKey(), "Request key cannot be null."); - Assert.notEmpty(request.getPayload(), "Request payload cannot be null or empty."); + Assert.notEmpty(request.getContent(), "Request content cannot be null or empty."); Assert.notEmpty(request.getDigest(), "Request signature byte array cannot be null or empty."); try { validateKey(key, false); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java index f4faec84f..218100640 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java @@ -65,7 +65,7 @@ public byte[] apply(Cipher cipher) throws Exception { public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { Assert.notNull(request, "request cannot be null."); final SecretKey kek = assertKey(request); - final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty."); + final byte[] cekBytes = Assert.notEmpty(request.getContent(), "Decryption request content (ciphertext) cannot be null or empty."); final JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); final ValueGetter getter = new DefaultValueGetter(header); final byte[] tag = getter.getRequiredBytes("tag", this.tagBitLength / Byte.SIZE); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java index 87e7f83ee..09f5b8712 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java @@ -44,7 +44,7 @@ public byte[] apply(Cipher cipher) throws Exception { public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { Assert.notNull(request, "request cannot be null."); final SecretKey kek = assertKey(request); - final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Request encrypted key (request.getPayload()) cannot be null or empty."); + final byte[] cekBytes = Assert.notEmpty(request.getContent(), "Request content (encrypted key) cannot be null or empty."); return execute(request, Cipher.class, new CheckedFunction() { @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadRequest.java index 586ea1e6c..5dedcafdc 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadRequest.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadRequest.java @@ -7,7 +7,7 @@ import java.security.Provider; import java.security.SecureRandom; -public class DefaultAeadRequest extends DefaultCryptoRequest implements AeadRequest, InitializationVectorSupplier { +public class DefaultAeadRequest extends DefaultCryptoRequest implements AeadRequest, InitializationVectorSupplier { private final byte[] IV; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java index f7e5f20fd..188c1c8f3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java @@ -7,14 +7,14 @@ import java.security.Provider; import java.security.SecureRandom; -public class DefaultCryptoRequest extends DefaultPayloadSupplier implements CryptoRequest{ +public class DefaultCryptoRequest extends DefaultMessage implements CryptoRequest{ private final Provider provider; private final SecureRandom secureRandom; private final K key; - public DefaultCryptoRequest(Provider provider, SecureRandom secureRandom, T payload, K key) { - super(payload); + public DefaultCryptoRequest(Provider provider, SecureRandom secureRandom, byte[] content, K key) { + super(content); this.provider = provider; this.secureRandom = secureRandom; this.key = Assert.notNull(key, "key cannot be null."); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultDecryptionKeyRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultDecryptionKeyRequest.java index ff5748ad3..f7a55810c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultDecryptionKeyRequest.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultDecryptionKeyRequest.java @@ -10,15 +10,15 @@ public class DefaultDecryptionKeyRequest extends DefaultKeyRequest implements DecryptionKeyRequest { - private final byte[] payload; + private final byte[] encryptedCek; - public DefaultDecryptionKeyRequest(Provider provider, SecureRandom secureRandom, K key, JweHeader header, AeadAlgorithm encryptionAlgorithm, byte[] payload) { + public DefaultDecryptionKeyRequest(Provider provider, SecureRandom secureRandom, K key, JweHeader header, AeadAlgorithm encryptionAlgorithm, byte[] encryptedCek) { super(provider, secureRandom, key, header, encryptionAlgorithm); - this.payload = payload; + this.encryptedCek = encryptedCek; } @Override - public byte[] getPayload() { - return this.payload; + public byte[] getContent() { + return this.encryptedCek; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java index 5ae1f282b..37b5abc92 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java @@ -139,7 +139,7 @@ protected byte[] doSign(final SignatureRequest request) { @Override public byte[] apply(Signature sig) throws Exception { sig.initSign(request.getKey()); - sig.update(request.getPayload()); + sig.update(request.getContent()); byte[] signature = sig.sign(); return transcodeDERToConcat(signature, signatureByteLength); } @@ -191,7 +191,7 @@ public Boolean apply(Signature sig) { } sig.initVerify(request.getKey()); - sig.update(request.getPayload()); + sig.update(request.getContent()); return sig.verify(derSignature); } catch (Exception e) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyResult.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyResult.java index 609520e4b..3f1054881 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyResult.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyResult.java @@ -8,7 +8,7 @@ public class DefaultKeyResult implements KeyResult { - private final byte[] payload; + private final byte[] encryptedKey; private final SecretKey key; public DefaultKeyResult(SecretKey key) { @@ -16,13 +16,13 @@ public DefaultKeyResult(SecretKey key) { } public DefaultKeyResult(SecretKey key, byte[] encryptedKey) { - this.payload = Assert.notNull(encryptedKey, "encryptedKey cannot be null (but can be empty)."); + this.encryptedKey = Assert.notNull(encryptedKey, "encryptedKey cannot be null (but can be empty)."); this.key = Assert.notNull(key, "Key argument cannot be null."); } @Override - public byte[] getPayload() { - return this.payload; + public byte[] getContent() { + return this.encryptedKey; } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMessage.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMessage.java new file mode 100644 index 000000000..71af24c9a --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMessage.java @@ -0,0 +1,19 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.Message; + +class DefaultMessage implements Message { + + private final byte[] content; + + DefaultMessage(byte[] content) { + Assert.notEmpty(content, "content byte array cannot be null or empty."); + this.content = content; + } + + @Override + public byte[] getContent() { + return content; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPayloadSupplier.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPayloadSupplier.java deleted file mode 100644 index efa777ae5..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPayloadSupplier.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.PayloadSupplier; - -import java.security.Key; - -class DefaultPayloadSupplier implements PayloadSupplier { - - private final T payload; - - DefaultPayloadSupplier(T payload) { - this.payload = assertValidPayload(payload); - } - - protected T assertValidPayload(T payload) throws IllegalArgumentException { - Assert.notNull(payload, "payload cannot be null."); - if (payload instanceof byte[]) { - Assert.notEmpty((byte[])payload, "payload byte array cannot be empty."); - } else if (!(payload instanceof Key)) { - String msg = "payload must be either a byte array or a java.security.Key instance."; - throw new IllegalArgumentException(msg); - } - return payload; - } - - @Override - public T getPayload() { - return payload; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java index efc262cf7..abb1d953c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java @@ -58,7 +58,7 @@ public byte[] apply(Cipher cipher) throws Exception { public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { Assert.notNull(request, "request cannot be null."); final D kek = Assert.notNull(request.getKey(), "Request key decryption key cannot be null."); - final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Request encrypted key (request.getPayload()) cannot be null or empty."); + final byte[] cekBytes = Assert.notEmpty(request.getContent(), "Request content (encrypted key) cannot be null or empty."); return execute(request, Cipher.class, new CheckedFunction() { @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java index 5bbe59b44..5a26f9c5e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java @@ -112,7 +112,7 @@ public byte[] apply(Signature sig) throws Exception { sig.setParameter(algorithmParameterSpec); } sig.initSign(request.getKey()); - sig.update(request.getPayload()); + sig.update(request.getContent()); return sig.sign(); } }); @@ -131,7 +131,7 @@ public Boolean apply(Signature sig) throws Exception { sig.setParameter(algorithmParameterSpec); } sig.initVerify(request.getKey()); - sig.update(request.getPayload()); + sig.update(request.getContent()); return sig.verify(request.getDigest()); } }); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSignatureRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSignatureRequest.java index 028601b33..1faaaf163 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSignatureRequest.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSignatureRequest.java @@ -9,7 +9,7 @@ /** * @since JJWT_RELEASE_VERSION */ -public class DefaultSignatureRequest extends DefaultCryptoRequest implements SignatureRequest { +public class DefaultSignatureRequest extends DefaultCryptoRequest implements SignatureRequest { public DefaultSignatureRequest(Provider provider, SecureRandom secureRandom, byte[] data, K key) { super(provider, secureRandom, data, key); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java index fb755b5e4..7856f7718 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java @@ -186,7 +186,7 @@ public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws Securi final SecretKey derived = deriveKey(request, epk.toKey(), privateKey); DecryptionKeyRequest unwrapReq = new DefaultDecryptionKeyRequest<>(request.getProvider(), - request.getSecureRandom(), derived, header, request.getEncryptionAlgorithm(), request.getPayload()); + request.getSecureRandom(), derived, header, request.getEncryptionAlgorithm(), request.getContent()); return WRAP_ALG.getDecryptionKey(unwrapReq); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java index 380014be4..e8ac7a435 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java @@ -8,7 +8,7 @@ import io.jsonwebtoken.security.AeadRequest; import io.jsonwebtoken.security.AeadResult; import io.jsonwebtoken.security.DecryptAeadRequest; -import io.jsonwebtoken.security.PayloadSupplier; +import io.jsonwebtoken.security.Message; import javax.crypto.Cipher; import javax.crypto.SecretKey; @@ -30,7 +30,7 @@ public AeadResult encrypt(final AeadRequest req) throws SecurityException { Assert.notNull(req, "Request cannot be null."); final SecretKey key = assertKey(req); - final byte[] plaintext = Assert.notEmpty(req.getPayload(), "Request payload (plaintext) cannot be null or empty."); + final byte[] plaintext = Assert.notEmpty(req.getContent(), "Request content (plaintext) cannot be null or empty."); final byte[] aad = getAAD(req); final byte[] iv = ensureInitializationVector(req); final AlgorithmParameterSpec ivSpec = getIvSpec(iv); @@ -58,11 +58,11 @@ public byte[] apply(Cipher cipher) throws Exception { } @Override - public PayloadSupplier decrypt(final DecryptAeadRequest req) throws SecurityException { + public Message decrypt(final DecryptAeadRequest req) throws SecurityException { Assert.notNull(req, "Request cannot be null."); final SecretKey key = assertKey(req); - final byte[] ciphertext = Assert.notEmpty(req.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty."); + final byte[] ciphertext = Assert.notEmpty(req.getContent(), "Decryption request content (ciphertext) cannot be null or empty."); final byte[] aad = getAAD(req); final byte[] tag = Assert.notEmpty(req.getDigest(), "Decryption request authentication tag cannot be null or empty."); final byte[] iv = assertDecryptionIv(req); @@ -82,6 +82,6 @@ public byte[] apply(Cipher cipher) throws Exception { } }); - return new DefaultPayloadSupplier<>(plaintext); + return new DefaultMessage(plaintext); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java index 7f0046ccd..fcf068ff8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java @@ -8,7 +8,7 @@ import io.jsonwebtoken.security.AeadResult; import io.jsonwebtoken.security.CryptoRequest; import io.jsonwebtoken.security.DecryptAeadRequest; -import io.jsonwebtoken.security.PayloadSupplier; +import io.jsonwebtoken.security.Message; import io.jsonwebtoken.security.SecretKeyBuilder; import io.jsonwebtoken.security.SignatureException; import io.jsonwebtoken.security.SignatureRequest; @@ -58,7 +58,7 @@ public SecretKeyBuilder keyBuilder() { return new RandomSecretKeyBuilder(KEY_ALG_NAME, getKeyBitLength()); } - byte[] assertKeyBytes(CryptoRequest request) { + byte[] assertKeyBytes(CryptoRequest request) { SecretKey key = Assert.notNull(request.getKey(), "Request key cannot be null."); return validateLength(key, this.keyBitLength * 2, true); } @@ -74,7 +74,7 @@ public AeadResult encrypt(final AeadRequest req) { byte[] encKeyBytes = Arrays.copyOfRange(compositeKeyBytes, halfCount, compositeKeyBytes.length); final SecretKey encryptionKey = new SecretKeySpec(encKeyBytes, "AES"); - final byte[] plaintext = Assert.notEmpty(req.getPayload(), "Request payload (plaintext) cannot be null or empty."); + final byte[] plaintext = Assert.notEmpty(req.getContent(), "Request content (plaintext) cannot be null or empty."); final byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty final byte[] iv = ensureInitializationVector(req); final AlgorithmParameterSpec ivSpec = getIvSpec(iv); @@ -122,7 +122,7 @@ private byte[] sign(byte[] aad, byte[] iv, byte[] ciphertext, byte[] macKeyBytes } @Override - public PayloadSupplier decrypt(final DecryptAeadRequest req) { + public Message decrypt(final DecryptAeadRequest req) { Assert.notNull(req, "Request cannot be null."); @@ -132,7 +132,7 @@ public PayloadSupplier decrypt(final DecryptAeadRequest req) { byte[] encKeyBytes = Arrays.copyOfRange(compositeKeyBytes, halfCount, compositeKeyBytes.length); final SecretKey decryptionKey = new SecretKeySpec(encKeyBytes, "AES"); - final byte[] ciphertext = Assert.notEmpty(req.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty."); + final byte[] ciphertext = Assert.notEmpty(req.getContent(), "Decryption request content (ciphertext) cannot be null or empty."); final byte[] aad = getAAD(req); final byte[] tag = assertTag(req.getDigest()); final byte[] iv = assertDecryptionIv(req); @@ -154,6 +154,6 @@ public byte[] apply(Cipher cipher) throws Exception { } }); - return new DefaultPayloadSupplier<>(plaintext); + return new DefaultMessage(plaintext); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java index 3ab2219bc..2a11bcb27 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java @@ -144,7 +144,7 @@ public byte[] doSign(final SignatureRequest request) { @Override public byte[] apply(Mac mac) throws Exception { mac.init(request.getKey()); - return mac.doFinal(request.getPayload()); + return mac.doFinal(request.getContent()); } }); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java index d52e32eba..3b55ba89e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java @@ -185,7 +185,7 @@ public SecretKey getDecryptionKey(DecryptionKeyRequest request) thr final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations); DecryptionKeyRequest unwrapReq = new DefaultDecryptionKeyRequest<>(request.getProvider(), - request.getSecureRandom(), derivedKek, header, request.getEncryptionAlgorithm(), request.getPayload()); + request.getSecureRandom(), derivedKek, header, request.getEncryptionAlgorithm(), request.getContent()); return wrapAlg.getDecryptionKey(unwrapReq); } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/YagCompressionCodec.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/YagCompressionCodec.groovy index 52ffe875f..5c76b4fa8 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/YagCompressionCodec.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/YagCompressionCodec.groovy @@ -29,7 +29,7 @@ class YagCompressionCodec implements CompressionCodec { } @Override - byte[] compress(byte[] payload) throws CompressionException { + byte[] compress(byte[] content) throws CompressionException { return new byte[0] } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy index 71b77bf92..97e727f1b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy @@ -1,12 +1,6 @@ package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.security.AeadAlgorithm -import io.jsonwebtoken.security.AeadRequest -import io.jsonwebtoken.security.AeadResult -import io.jsonwebtoken.security.DecryptAeadRequest -import io.jsonwebtoken.security.PayloadSupplier -import io.jsonwebtoken.security.SecurityException +import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.spec.SecretKeySpec @@ -117,7 +111,7 @@ class AesAlgorithmTest { } @Override - PayloadSupplier decrypt(DecryptAeadRequest symmetricAeadDecryptionRequest) { + Message decrypt(DecryptAeadRequest symmetricAeadDecryptionRequest) { return null } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy index b58b315cf..f08f22c2d 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy @@ -73,7 +73,7 @@ class AesGcmKeyAlgorithmTest { assertArrayEquals resultA.digest, encResult.digest assertArrayEquals resultA.initializationVector, encResult.initializationVector - assertArrayEquals resultA.payload, encResult.payload + assertArrayEquals resultA.getContent(), encResult.getContent() } static void assertAlgorithm(int keyLength) { @@ -97,7 +97,7 @@ class AesGcmKeyAlgorithmTest { def result = alg.getEncryptionKey(ereq) - byte[] encryptedKeyBytes = result.getPayload() + byte[] encryptedKeyBytes = result.getContent() assertFalse "encryptedKey must be populated", Arrays.length(encryptedKeyBytes) == 0 def dcek = alg.getDecryptionKey(new DefaultDecryptionKeyRequest(null, null, kek, header, enc, encryptedKeyBytes)) @@ -132,7 +132,7 @@ class AesGcmKeyAlgorithmTest { header.put(headerName, value) //null value will remove it - byte[] encryptedKeyBytes = result.getPayload() + byte[] encryptedKeyBytes = result.getContent() try { alg.getDecryptionKey(new DefaultDecryptionKeyRequest(null, null, kek, header, enc, encryptedKeyBytes)) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultPayloadSupplierTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultMessageTest.groovy similarity index 64% rename from impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultPayloadSupplierTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultMessageTest.groovy index 7a2199241..705756afb 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultPayloadSupplierTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultMessageTest.groovy @@ -2,15 +2,15 @@ package io.jsonwebtoken.impl.security import org.junit.Test -class DefaultPayloadSupplierTest { +class DefaultMessageTest { @Test(expected = IllegalArgumentException) void testNullData() { - new DefaultPayloadSupplier<>(null) + new DefaultMessage(null) } @Test(expected = IllegalArgumentException) void testEmptyByteArrayData() { - new DefaultPayloadSupplier<>(new byte[0]) + new DefaultMessage(new byte[0]) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy index 47306a163..c28a2a143 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy @@ -10,7 +10,8 @@ import javax.crypto.spec.SecretKeySpec import java.security.Key import static org.easymock.EasyMock.* -import static org.junit.Assert.* +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame class DirectKeyAlgorithmTest { @@ -26,7 +27,7 @@ class DirectKeyAlgorithmTest { def request = new DefaultKeyRequest(null, null, key, new DefaultJweHeader(), EncryptionAlgorithms.A128GCM) def result = alg.getEncryptionKey(request) assertSame key, result.getKey() - assertEquals 0, Arrays.length(result.getPayload()) //must not have an encrypted key + assertEquals 0, Arrays.length(result.getContent()) //must not have an encrypted key } @Test(expected = IllegalArgumentException) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithmTest.groovy index bcfe91277..87bc47400 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithmTest.groovy @@ -50,7 +50,7 @@ class GcmAesAeadAlgorithmTest { def result = alg.encrypt(req) - byte[] ciphertext = result.getPayload() + byte[] ciphertext = result.getContent() byte[] tag = result.getDigest() byte[] iv = result.getInitializationVector() @@ -60,7 +60,7 @@ class GcmAesAeadAlgorithmTest { // now test decryption: def dreq = new DefaultAeadResult(null, null, ciphertext, KEY, AAD, tag, iv) - byte[] decryptionResult = alg.decrypt(dreq).getPayload() + byte[] decryptionResult = alg.decrypt(dreq).getContent() assertArrayEquals(P, decryptionResult) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy index 2e8e92b8e..7f0875f8e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy @@ -56,7 +56,7 @@ class HmacAesAeadAlgorithmTest { def fakeTag = new byte[realTag.length] Randoms.secureRandom().nextBytes(fakeTag) - def dreq = new DefaultAeadResult(null, null, result.getPayload(), key, null, fakeTag, result.getInitializationVector()) + def dreq = new DefaultAeadResult(null, null, result.getContent(), key, null, fakeTag, result.getInitializationVector()) alg.decrypt(dreq) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB1Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB1Test.groovy index 3b8623712..7ac6a14bf 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB1Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB1Test.groovy @@ -73,7 +73,7 @@ class RFC7518AppendixB1Test { def request = new DefaultAeadRequest(null, null, P, KEY, A, IV) def result = alg.encrypt(request); - byte[] ciphertext = result.getPayload() + byte[] ciphertext = result.getContent() byte[] tag = result.getDigest() byte[] iv = result.getInitializationVector() @@ -83,7 +83,7 @@ class RFC7518AppendixB1Test { // now test decryption: def dreq = new DefaultAeadResult(null, null, ciphertext, KEY, A, tag, iv) - byte[] decryptionResult = alg.decrypt(dreq).getPayload() + byte[] decryptionResult = alg.decrypt(dreq).getContent() assertArrayEquals(P, decryptionResult) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB2Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB2Test.groovy index aebb8ab6f..2840ecabc 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB2Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB2Test.groovy @@ -73,7 +73,7 @@ class RFC7518AppendixB2Test { AeadRequest req = new DefaultAeadRequest(null, null, P, KEY, A, IV) AeadResult result = alg.encrypt(req) - byte[] resultCiphertext = result.getPayload() + byte[] resultCiphertext = result.getContent() byte[] resultTag = result.getDigest() byte[] resultIv = result.getInitializationVector() @@ -83,7 +83,7 @@ class RFC7518AppendixB2Test { // now test decryption: def dreq = new DefaultAeadResult(null, null, resultCiphertext, KEY, A, resultTag, resultIv) - byte[] decryptionResult = alg.decrypt(dreq).getPayload() + byte[] decryptionResult = alg.decrypt(dreq).getContent() assertArrayEquals(P, decryptionResult) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB3Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB3Test.groovy index 8d0e4500c..278afdbdc 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB3Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB3Test.groovy @@ -73,7 +73,7 @@ class RFC7518AppendixB3Test { AeadRequest req = new DefaultAeadRequest(null, null, P, KEY, A, IV) AeadResult result = alg.encrypt(req) - byte[] resultCiphertext = result.getPayload() + byte[] resultCiphertext = result.getContent() byte[] resultTag = result.getDigest(); byte[] resultIv = result.getInitializationVector(); @@ -83,7 +83,7 @@ class RFC7518AppendixB3Test { // now test decryption: def dreq = new DefaultAeadResult(null, null, resultCiphertext, KEY, A, resultTag, resultIv); - byte[] decryptionResult = alg.decrypt(dreq).getPayload() + byte[] decryptionResult = alg.decrypt(dreq).getContent() assertArrayEquals(P, decryptionResult); } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy index ff9ff081f..20ce7e5c8 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy @@ -93,7 +93,7 @@ class EncryptionAlgorithmsTest { byte[] tag = result.getDigest() //there is always a tag, even if there is no AAD assertNotNull tag - byte[] ciphertext = result.getPayload() + byte[] ciphertext = result.getContent() boolean gcm = alg instanceof GcmAesAeadAlgorithm @@ -103,7 +103,7 @@ class EncryptionAlgorithmsTest { def dreq = new DefaultAeadResult(null, null, ciphertext, key, null, tag, result.getInitializationVector()) - byte[] decryptedPlaintextBytes = alg.decrypt(dreq).payload + byte[] decryptedPlaintextBytes = alg.decrypt(dreq).getContent() assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes) } @@ -120,7 +120,7 @@ class EncryptionAlgorithmsTest { def result = alg.encrypt(req) - byte[] ciphertext = result.getPayload() + byte[] ciphertext = result.getContent() boolean gcm = alg instanceof GcmAesAeadAlgorithm @@ -128,8 +128,8 @@ class EncryptionAlgorithmsTest { assertEquals(ciphertext.length, PLAINTEXT_BYTES.length) } - def dreq = new DefaultAeadResult(null, null, result.getPayload(), key, AAD_BYTES, result.getDigest(), result.getInitializationVector()) - byte[] decryptedPlaintextBytes = alg.decrypt(dreq).getPayload() + def dreq = new DefaultAeadResult(null, null, result.getContent(), key, AAD_BYTES, result.getDigest(), result.getInitializationVector()) + byte[] decryptedPlaintextBytes = alg.decrypt(dreq).getContent() assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes) } } From 7d81efc21ef11bb4bb99c8bb361941f0584e36ef Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 7 May 2022 12:37:42 -0700 Subject: [PATCH 38/75] Added new KeyPairBuilder/KeyPairBuilderSupplier for parity with KeyBuilder/KeyBuilderSupplier. Switched all generate* calls to use the new API methods. --- CHANGELOG.md | 15 +++++ README.md | 35 +++++++---- .../security/AsymmetricKeyGenerator.java | 31 ---------- .../AsymmetricKeySignatureAlgorithm.java | 3 +- .../EllipticCurveSignatureAlgorithm.java | 3 +- .../io/jsonwebtoken/security/KeyBuilder.java | 12 +++- .../security/KeyBuilderSupplier.java | 14 +++-- .../io/jsonwebtoken/security/KeyPair.java | 16 +++++ .../jsonwebtoken/security/KeyPairBuilder.java | 20 ++++++ .../security/KeyPairBuilderSupplier.java | 26 ++++++++ .../java/io/jsonwebtoken/security/Keys.java | 24 ++++---- .../security/RsaSignatureAlgorithm.java | 3 +- .../security/SecurityBuilder.java | 1 - .../impl/security/CryptoAlgorithm.java | 4 ++ ...efaultEllipticCurveSignatureAlgorithm.java | 25 +++----- .../impl/security/DefaultKeyPair.java | 36 +++++++++++ .../impl/security/DefaultKeyPairBuilder.java | 61 +++++++++++++++++++ .../DefaultRsaSignatureAlgorithm.java | 29 ++++----- .../impl/security/EcdhKeyAlgorithm.java | 11 +--- .../impl/security/JcaTemplate.java | 21 +++++-- .../impl/security/MacSignatureAlgorithm.java | 2 +- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 2 +- .../security/AbstractEcJwkFactoryTest.groovy | 13 ++-- .../AbstractSignatureAlgorithmTest.groovy | 13 ++-- ...EllipticCurveSignatureAlgorithmTest.groovy | 18 +++--- .../DefaultRsaSignatureAlgorithmTest.groovy | 9 ++- .../impl/security/JwksTest.groovy | 10 +-- .../impl/security/KeyPairsTest.groovy | 2 +- .../security/MacSignatureAlgorithmTest.groovy | 2 +- .../io/jsonwebtoken/security/KeysTest.groovy | 12 ++-- 30 files changed, 325 insertions(+), 148 deletions(-) delete mode 100644 api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyGenerator.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyPair.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPair.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java diff --git a/CHANGELOG.md b/CHANGELOG.md index f3aabe546..d08a17790 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ ## Release Notes +### JJWT_RELEASE_VERSION + +* The `io.jsonwebtoken.SignatureAlgorithm` enum has been deprecated in favor of a new + `io.jsonwebtoken.security.SignatureAlgorithm` interface. Also, a new `io.jsonwebtoken.security.SignatureAlgorithms` + static helper class enumerates all the standard JWA algorithms as expected, exactly like the old enum. This change + was made because enums are a static concept by design and cannot support custom values: those who wanted to use custom + signature algorithms could not do so until now. The new interface now allows anyone to plug in and support custom + algorithms with JJWT as desired. + +* Similarly, as the `io.jsonwebtoken.security.Keys#secretKeyFor` and `io.jsonwebtoken.security.Keys#keyPairFor` methods + accepted the now-deprecated `io.jsonwebtoken.SignatureAlgorithm` enum, they have also been deprecated in favor of + calling new `keyBuilder()` or `keyPairBuilder()` methods on `SignatureAlgorithm` instances directly. The builders + allow for customization of the JCA `Provider` and `SecureRandom` during Key or KeyPair generation if desired, whereas + the old enum-based static utility methods did not. + ### 0.11.5 This patch release adds additional security guards against an ECDSA bug in Java SE versions 15-15.0.6, 17-17.0.2, and 18 diff --git a/README.md b/README.md index b40da8021..80f059046 100644 --- a/README.md +++ b/README.md @@ -406,7 +406,7 @@ import java.security.Key; // We need a signing key, so we'll create one just for this example. Usually // the key would be read from your application configuration instead. -Key key = SignatureAlgorithms.HS256.generateKey(); +SecretKey key = SignatureAlgorithms.HS256.keyBuilder().build(); String jws = Jwts.builder().setSubject("Joe").signWith(key).compact(); ``` @@ -624,21 +624,28 @@ JWT Elliptic Curve signature algorithms `ES256`, `ES384`, and `ES512` all requir #### Creating Safe Keys If you don't want to think about bit length requirements or just want to make your life easier, JJWT has -provided the `io.jsonwebtoken.security.Keys` utility class that can generate sufficiently secure keys for any given +provided convenient builder classes that can generate sufficiently secure keys for any given JWT signature algorithm you might want to use. ##### Secret Keys -If you want to generate a sufficiently strong `SecretKey` for use with the JWT HMAC-SHA algorithms, use the -`Keys.secretKeyFor(SignatureAlgorithm)` helper method: +If you want to generate a sufficiently strong `SecretKey` for use with the JWT HMAC-SHA algorithms, use the respective +algorithm's `keyBuilder()` method: ```java -SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //or HS384 or HS512 +SecretKey key = SignatureAlgorithms.HS256.keyBuilder().build(); //or HS384.keyBuilder() or HS512.keyBuilder() ``` -Under the hood, JJWT uses the JCA provider's `KeyGenerator` to create a secure-random key with the correct minimum -length for the given algorithm. +Under the hood, JJWT uses the JCA default provider's `KeyGenerator` to create a secure-random key with the correct +minimum length for the given algorithm. + +If you want to specify a specific JCA `Provider` or `SecureRandom` to use during key generation, you may specify those +as builder arguments. For example: + +```java +SecretKey key = SignatureAlgorithms.HS256.keyBuilder().setProvider(aProvider).setRandom(aSecureRandom).build(); +``` If you need to save this new `SecretKey`, you can Base64 (or Base64URL) encode it: @@ -654,14 +661,20 @@ further encrypt it, etc, before saving to disk (for example). ##### Asymmetric Keys If you want to generate sufficiently strong Elliptic Curve or RSA asymmetric key pairs for use with JWT ECDSA or RSA -algorithms, use the `Keys.keyPairFor(SignatureAlgorithm)` helper method: +algorithms, use an algorithm's respective `keyPairBuilder()` method: ```java -KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); //or RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512 +KeyPair keyPair = SignatureAlgorithms.RS256.keyPairBuilder().build(); //or RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512 ``` -You use the private key (`keyPair.getPrivate()`) to create a JWS and the public key (`keyPair.getPublic()`) to -parse/verify a JWS. +The `keyPair` instance returned is a `io.jsonwebtoken.security.KeyPair`, which is essentially the same thing +as the JDK's `java.security.KeyPair` class, except it provides generics type-safety, for example, +`KeyPair` or `KeyPair`. If you want to convert this type-safe +instance to the standard JDK type-erased instance, just call `keyPair.toJdkKeyPair()` and you'll get a +`java.security.KeyPair` as expected. + +Once you've generated a `KeyPair`, you can use the private key (`keyPair.getPrivate()`) to create a JWS and the +public key (`keyPair.getPublic()`) to parse/verify a JWS. **NOTE: The `PS256`, `PS384`, and `PS512` algorithms require JDK 11 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath.** If you are using JDK 10 or earlier and you want to use them, see diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyGenerator.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyGenerator.java deleted file mode 100644 index 96c00f82b..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeyGenerator.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2021 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.security; - -import java.security.KeyPair; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface AsymmetricKeyGenerator { - - /** - * Generates a new secure-random key pair with a key length suitable for the associated Algorithm. - * - * @return a new secure-random key pair with a key length suitable for the associated Algorithm. - */ - KeyPair generateKeyPair(); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java index 314acfef8..2e1b2cbb9 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java @@ -21,5 +21,6 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface AsymmetricKeySignatureAlgorithm extends SignatureAlgorithm, AsymmetricKeyGenerator { +public interface AsymmetricKeySignatureAlgorithm + extends SignatureAlgorithm, KeyPairBuilderSupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java index b8c5b44a2..db636524a 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java @@ -22,5 +22,6 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface EllipticCurveSignatureAlgorithm extends AsymmetricKeySignatureAlgorithm { +public interface EllipticCurveSignatureAlgorithm + extends AsymmetricKeySignatureAlgorithm { } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java b/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java index 22e23bea7..7912af50e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java @@ -1,9 +1,19 @@ package io.jsonwebtoken.security; import javax.crypto.SecretKey; +import java.security.Key; /** + * A {@code KeyBuilder} produces new {@link Key}s suitable for use with an associated cryptographic algorithm. + * A new {@link Key} is created each time the builder's {@link #build()} method is called. + * + *

    {@code KeyBuilder}s are provided by components that implement the {@link KeyBuilderSupplier} interface, + * ensuring the resulting {@link SecretKey}s are compatible with their associated cryptographic algorithm.

    + * + * @param the type of public key found within newly-created {@link KeyPair}s. + * @param the type of private key found within newly-created {@link KeyPair}s. + * @see KeyPairBuilderSupplier * @since JJWT_RELEASE_VERSION */ -public interface KeyBuilder> extends SecurityBuilder { +public interface KeyBuilder> extends SecurityBuilder { } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyBuilderSupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeyBuilderSupplier.java index 59dbc4a82..f47ae4c32 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyBuilderSupplier.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyBuilderSupplier.java @@ -15,19 +15,25 @@ */ package io.jsonwebtoken.security; -import javax.crypto.SecretKey; +import java.security.Key; /** + * Interface implemented by components that support building/creating new {@link Key}s suitable for use with + * their associated cryptographic algorithm implementation. + * + * @param type of {@link Key} created by the builder + * @param type of builder to create each time {@link #keyBuilder()} is called. + * @see #keyBuilder() * @since JJWT_RELEASE_VERSION */ -public interface KeyBuilderSupplier> { +public interface KeyBuilderSupplier> { /** * Returns a new {@link KeyBuilder} instance that will produce new secure-random keys with a length sufficient - * to be used by the associated algorithm. + * to be used by the component's associated cryptographic algorithm. * * @return a new {@link KeyBuilder} instance that will produce new secure-random keys with a length sufficient - * to be used by the associated algorithm. + * to be used by the component's associated cryptographic algorithm. */ B keyBuilder(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyPair.java b/api/src/main/java/io/jsonwebtoken/security/KeyPair.java new file mode 100644 index 000000000..0cd26e839 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyPair.java @@ -0,0 +1,16 @@ +package io.jsonwebtoken.security; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface KeyPair { + + A getPublic(); + + B getPrivate(); + + java.security.KeyPair toJdkKeyPair(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java new file mode 100644 index 000000000..e3ca7faa5 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java @@ -0,0 +1,20 @@ +package io.jsonwebtoken.security; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * A {@code KeyPairBuilder} produces new {@link KeyPair}s suitable for use with an associated cryptographic algorithm. + * A new {@link KeyPair} is created each time the builder's {@link #build()} method is called. + * + *

    {@code KeyPairBuilder}s are provided by components that implement the {@link KeyPairBuilderSupplier} interface, + * ensuring the resulting {@link KeyPair}s are compatible with their associated cryptographic algorithm.

    + * + * @param
    the type of public key found within newly-created {@link KeyPair}s. + * @param the type of private key found within newly-created {@link KeyPair}s. + * @see KeyPairBuilderSupplier + * @since JJWT_RELEASE_VERSION + */ +public interface KeyPairBuilder + extends SecurityBuilder, KeyPairBuilder> { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java new file mode 100644 index 000000000..adc57ee16 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java @@ -0,0 +1,26 @@ +package io.jsonwebtoken.security; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * Interface implemented by components that support building/creating new {@link KeyPair}s suitable for use with their + * associated cryptographic algorithm implementation. + * + * @param type of public key found in newly-created {@code KeyPair}s + * @param type of private key found in newly-created {@code KeyPair}s + * @see #keyPairBuilder() + * @see KeyPairBuilder + * @since JJWT_RELEASE_VERSION + */ +public interface KeyPairBuilderSupplier { + + /** + * Returns a new {@link KeyPairBuilder} that will create new secure-random {@link KeyPair}s with a length and + * parameters sufficient for use with the component's associated cryptographic algorithm. + * + * @return a new {@link KeyPairBuilder} that will create new secure-random {@link KeyPair}s with a length and + * parameters sufficient for use with the component's associated cryptographic algorithm. + */ + KeyPairBuilder keyPairBuilder(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index 785010b84..900a7c750 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -68,8 +68,8 @@ public static SecretKey hmacShaKeyFor(byte[] bytes) throws WeakKeyException { "is not secure enough for any JWT HMAC-SHA algorithm. The JWT " + "JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " + "size >= 256 bits (the key size must be greater than or equal to the hash " + - "output size). Consider using the SignatureAlgorithms.HS256.generateKey() method (or HS384.generateKey() " + - "or HS512.generateKey()) to create a key guaranteed to be secure enough for your preferred HMAC-SHA " + + "output size). Consider using the SignatureAlgorithms.HS256.keyBuilder() method (or HS384.keyBuilder() " + + "or HS512.keyBuilder()) to create a key guaranteed to be secure enough for your preferred HMAC-SHA " + "algorithm. See https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; throw new WeakKeyException(msg); } @@ -140,18 +140,20 @@ public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) thr *

    Deprecation Notice

    * *

    As of JJWT JJWT_RELEASE_VERSION, asymmetric key algorithm instances can generate KeyPairs of suitable strength - * for that specific algorithm by calling their {@code generateKeyPair()} method directly. For example:

    + * for that specific algorithm by calling their {@code keyPairBuilder()} method directly. For example:

    * *
    
    -     * {@link SignatureAlgorithms#RS256}.generateKeyPair();
    -     * {@link SignatureAlgorithms#RS384}.generateKeyPair();
    -     * {@link SignatureAlgorithms#RS256}.generateKeyPair();
    +     * {@link SignatureAlgorithms#RS256}.keyPairBuilder().build();
    +     * {@link SignatureAlgorithms#RS384}.keyPairBuilder().build();
    +     * {@link SignatureAlgorithms#RS256}.keyPairBuilder().build();
          * ... etc ...
    -     * {@link SignatureAlgorithms#ES512}.generateKeyPair();
    +     * {@link SignatureAlgorithms#ES512}.keyPairBuilder().build();
          * 
    * - *

    Call those methods as needed instead of this {@code keyPairFor} helper method. This helper method will be - * removed before the 1.0 final release.

    + *

    Call those methods as needed instead of this {@code keyPairFor} helper method - the returned + * {@link KeyPairBuilder} allows callers to specify a preferred Provider or SecureRandom on the builder if + * desired, whereas this {@code keyPairFor} method does not. Consequently this helper method will be removed + * before the 1.0 release.

    * *

    Previous Documentation

    * @@ -225,7 +227,7 @@ public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) thr * @return a new {@link KeyPair} suitable for use with the specified asymmetric algorithm. * @throws IllegalArgumentException if {@code alg} is not an asymmetric algorithm * @deprecated since JJWT_RELEASE_VERSION. Use your preferred {@link AsymmetricKeySignatureAlgorithm} instance's - * {@link AsymmetricKeySignatureAlgorithm#generateKeyPair() generateKeyPair()} method directly. + * {@link AsymmetricKeySignatureAlgorithm#keyPairBuilder() keyPairBuilder()} method directly. */ @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated @@ -237,7 +239,7 @@ public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws throw new IllegalArgumentException(msg); } AsymmetricKeySignatureAlgorithm asalg = ((AsymmetricKeySignatureAlgorithm) salg); - return asalg.generateKeyPair(); + return asalg.keyPairBuilder().build().toJdkKeyPair(); } /** diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java index 46450534e..52598546c 100644 --- a/api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java @@ -22,5 +22,6 @@ /** * @since JJWT_RELEASE_VERSION */ -public interface RsaSignatureAlgorithm extends AsymmetricKeySignatureAlgorithm { +public interface RsaSignatureAlgorithm + extends AsymmetricKeySignatureAlgorithm { } diff --git a/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java b/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java index 62ea18596..51f3beead 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java @@ -5,7 +5,6 @@ import java.security.Provider; import java.security.SecureRandom; - /** * A Security-specific {@link Builder} that allows configuration of common JCA API parameters, such as a * {@link java.security.Provider} or {@link java.security.SecureRandom}. diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java index b7b51baa3..17925df38 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java @@ -43,6 +43,10 @@ protected void setProvider(Provider provider) { // can be null this.provider = provider; } + protected Provider getProvider() { + return this.provider; + } + SecureRandom ensureSecureRandom(SecurityRequest request) { SecureRandom random = request != null ? request.getSecureRandom() : null; return random != null ? random : Randoms.secureRandom(); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java index 37b5abc92..328e3727a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java @@ -6,14 +6,13 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.EllipticCurveSignatureAlgorithm; import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.KeyPairBuilder; import io.jsonwebtoken.security.SignatureException; import io.jsonwebtoken.security.SignatureRequest; import io.jsonwebtoken.security.VerifySignatureRequest; import java.math.BigInteger; import java.security.Key; -import java.security.KeyPair; -import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; @@ -31,7 +30,7 @@ public class DefaultEllipticCurveSignatureAlgorithm() { - @Override - public KeyPair apply(KeyPairGenerator generator) throws Exception { - generator.initialize(spec, Randoms.secureRandom()); - return generator.generateKeyPair(); - } - }); + public KeyPairBuilder keyPairBuilder() { + return new DefaultKeyPairBuilder("EC", this.KEY_PAIR_GEN_PARAMS) + .setProvider(getProvider()).setRandom(Randoms.secureRandom()); } private static void assertKey(Key key, Class type, boolean signing) { @@ -111,9 +104,9 @@ private static void assertKey(Key key, Class type, boolean signing) { @Override protected void validateKey(Key key, boolean signing) { - assertKey(key, ECKey.class, signing); // https://github.com/jwtk/jjwt/issues/68: - // Instead of checking for an instance of ECPrivateKey, check for PrivateKey (and ECKey assertion is above): + // Instead of checking for an instance of ECPrivateKey, check for ECKey and PrivateKey separately: + assertKey(key, ECKey.class, signing); Class requiredType = signing ? PrivateKey.class : PublicKey.class; assertKey(key, requiredType, signing); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPair.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPair.java new file mode 100644 index 000000000..8841a3844 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPair.java @@ -0,0 +1,36 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.KeyPair; + +import java.security.PrivateKey; +import java.security.PublicKey; + +public class DefaultKeyPair
    implements KeyPair { + + private final A publicKey; + private final B privateKey; + + private final java.security.KeyPair jdkPair; + + public DefaultKeyPair(A publicKey, B privateKey) { + this.publicKey = Assert.notNull(publicKey, "PublicKey argument cannot be null."); + this.privateKey = Assert.notNull(privateKey, "PrivateKey argument cannot be null."); + this.jdkPair = new java.security.KeyPair(this.publicKey, this.privateKey); + } + + @Override + public A getPublic() { + return this.publicKey; + } + + @Override + public B getPrivate() { + return this.privateKey; + } + + @Override + public java.security.KeyPair toJdkKeyPair() { + return this.jdkPair; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java new file mode 100644 index 000000000..4b4cd1eab --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java @@ -0,0 +1,61 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.KeyPair; +import io.jsonwebtoken.security.KeyPairBuilder; + +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; + +public class DefaultKeyPairBuilder implements KeyPairBuilder { + + private final String jcaName; + private final int bitLength; + private final AlgorithmParameterSpec params; + private Provider provider; + private SecureRandom random; + + public DefaultKeyPairBuilder(String jcaName, int bitLength) { + this.jcaName = Assert.hasText(jcaName, "jcaName cannot be null or empty."); + this.bitLength = Assert.gt(bitLength, 0, "bitLength must be a positive integer greater than 0"); + this.params = null; + } + + public DefaultKeyPairBuilder(String jcaName, AlgorithmParameterSpec params) { + this.jcaName = Assert.hasText(jcaName, "jcaName cannot be null or empty."); + this.params = Assert.notNull(params, "AlgorithmParameterSpec params cannot be null."); + this.bitLength = 0; + } + + protected java.security.KeyPair generateJdkPair() throws io.jsonwebtoken.security.SecurityException { + JcaTemplate template = new JcaTemplate(this.jcaName, this.provider, this.random); + if (this.params != null) { + return template.generateKeyPair(this.params); + } else { + return template.generateKeyPair(this.bitLength); + } + } + + @Override + public KeyPair build() { + java.security.KeyPair pair = generateJdkPair(); + @SuppressWarnings("unchecked") A publicKey = (A) pair.getPublic(); + @SuppressWarnings("unchecked") B privateKey = (B) pair.getPrivate(); + return new DefaultKeyPair<>(publicKey, privateKey); + } + + @Override + public KeyPairBuilder setProvider(Provider provider) { + this.provider = provider; + return this; + } + + @Override + public KeyPairBuilder setRandom(SecureRandom random) { + this.random = random; + return this; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java index 5a26f9c5e..93b778b3f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java @@ -4,13 +4,13 @@ import io.jsonwebtoken.impl.lang.CheckedSupplier; import io.jsonwebtoken.impl.lang.Conditions; import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.KeyPairBuilder; import io.jsonwebtoken.security.RsaSignatureAlgorithm; import io.jsonwebtoken.security.SignatureRequest; import io.jsonwebtoken.security.VerifySignatureRequest; import io.jsonwebtoken.security.WeakKeyException; import java.security.Key; -import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; @@ -23,7 +23,7 @@ * @since JJWT_RELEASE_VERSION */ public class DefaultRsaSignatureAlgorithm - extends AbstractSignatureAlgorithm implements RsaSignatureAlgorithm { + extends AbstractSignatureAlgorithm implements RsaSignatureAlgorithm { private static final String PSS_JCA_NAME = "RSASSA-PSS"; private static final int MIN_KEY_BIT_LENGTH = 2048; @@ -34,17 +34,17 @@ private static AlgorithmParameterSpec pssParamFromSaltBitLength(int saltBitLengt return new PSSParameterSpec(ps.getDigestAlgorithm(), "MGF1", ps, saltByteLength, 1); } - private final int preferredKeyLength; + private final int preferredKeyBitLength; private final AlgorithmParameterSpec algorithmParameterSpec; public DefaultRsaSignatureAlgorithm(String name, String jcaName, int preferredKeyBitLength, AlgorithmParameterSpec algParam) { super(name, jcaName); if (preferredKeyBitLength < MIN_KEY_BIT_LENGTH) { - String msg = "preferredKeyLengthBits must be greater than the JWA mandatory minimum key length of " + MIN_KEY_BIT_LENGTH; + String msg = "preferredKeyBitLength must be greater than the JWA mandatory minimum key length of " + MIN_KEY_BIT_LENGTH; throw new IllegalArgumentException(msg); } - this.preferredKeyLength = preferredKeyBitLength; + this.preferredKeyBitLength = preferredKeyBitLength; this.algorithmParameterSpec = algParam; } @@ -64,8 +64,9 @@ public Signature get() throws Exception { } @Override - public KeyPair generateKeyPair() { - return new JcaTemplate("RSA", null).generateKeyPair(this.preferredKeyLength); + public KeyPairBuilder keyPairBuilder() { + return new DefaultKeyPairBuilder("RSA", this.preferredKeyBitLength) + .setProvider(getProvider()).setRandom(Randoms.secureRandom()); } @Override @@ -73,7 +74,7 @@ protected void validateKey(Key key, boolean signing) { if (!(key instanceof RSAKey)) { String msg = "RSA " + keyType(signing) + " keys must be an RSAKey. The specified key is of type: " + - key.getClass().getName(); + key.getClass().getName(); throw new InvalidKeyException(msg); } @@ -81,7 +82,7 @@ protected void validateKey(Key key, boolean signing) { // Instead of checking for an instance of RSAPrivateKey, check for PrivateKey (RSAKey assertion is above): if (signing && !(key instanceof PrivateKey)) { String msg = "Asymmetric key signatures must be created with PrivateKeys. The specified key is of type: " + - key.getClass().getName(); + key.getClass().getName(); throw new InvalidKeyException(msg); } @@ -94,11 +95,11 @@ protected void validateKey(Key key, boolean signing) { String section = id.startsWith("PS") ? "3.5" : "3.3"; String msg = "The " + keyType(signing) + " key's size is " + size + " bits which is not secure " + - "enough for the " + id + " algorithm. The JWT JWA Specification (RFC 7518, Section " + - section + ") states that RSA keys MUST have a size >= " + - MIN_KEY_BIT_LENGTH + " bits. Consider using the SignatureAlgorithms." + id + ".generateKeyPair() " + - "method to create a key pair guaranteed to be secure enough for " + id + ". See " + - "https://tools.ietf.org/html/rfc7518#section-" + section + " for more information."; + "enough for the " + id + " algorithm. The JWT JWA Specification (RFC 7518, Section " + + section + ") states that RSA keys MUST have a size >= " + + MIN_KEY_BIT_LENGTH + " bits. Consider using the SignatureAlgorithms." + id + ".generateKeyPair() " + + "method to create a key pair guaranteed to be secure enough for " + id + ". See " + + "https://tools.ietf.org/html/rfc7518#section-" + section + " for more information."; throw new WeakKeyException(msg); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java index 7856f7718..ccaa07b9c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java @@ -23,7 +23,6 @@ import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.security.KeyPair; -import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import java.security.interfaces.ECKey; @@ -69,14 +68,8 @@ private static String idFor(KeyAlgorithm wrapAlg) { //visible for testing protected KeyPair generateKeyPair(final KeyRequest request, final ECParameterSpec spec) { Assert.notNull(spec, "request key params cannot be null."); - return new JcaTemplate("EC", request.getProvider(), ensureSecureRandom(request)) - .execute(KeyPairGenerator.class, new CheckedFunction() { - @Override - public KeyPair apply(KeyPairGenerator keyPairGenerator) throws Exception { - keyPairGenerator.initialize(spec, ensureSecureRandom(request)); - return keyPairGenerator.generateKeyPair(); - } - }); + JcaTemplate template = new JcaTemplate("EC", getProvider(request), ensureSecureRandom(request)); + return template.generateKeyPair(spec); } protected byte[] generateZ(final KeyRequest request, final PublicKey pub, final PrivateKey priv) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java index cc4bc6a76..02ce229a6 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java @@ -9,11 +9,13 @@ import javax.crypto.KeyGenerator; import javax.crypto.Mac; import javax.crypto.SecretKey; +import java.security.InvalidAlgorithmParameterException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.Provider; import java.security.SecureRandom; import java.security.Signature; +import java.security.spec.AlgorithmParameterSpec; public class JcaTemplate { @@ -22,14 +24,13 @@ public class JcaTemplate { private final SecureRandom secureRandom; JcaTemplate(String jcaName, Provider provider) { - this(jcaName, provider, Randoms.secureRandom()); + this(jcaName, provider, null); } JcaTemplate(String jcaName, Provider provider, SecureRandom secureRandom) { - Assert.hasText(jcaName, "jcaName string cannot be null or empty."); - this.jcaName = jcaName; - this.provider = provider; - this.secureRandom = Assert.notNull(secureRandom, "SecureRandom cannot be null."); + this.jcaName = Assert.hasText(jcaName, "jcaName string cannot be null or empty."); + this.secureRandom = secureRandom != null ? secureRandom : Randoms.secureRandom(); + this.provider = provider; //may be null, meaning to use the JCA subsystem default provider } public R execute(Class clazz, CheckedFunction fn) throws SecurityException { @@ -56,6 +57,16 @@ public KeyPair apply(KeyPairGenerator generator) { }); } + public KeyPair generateKeyPair(final AlgorithmParameterSpec params) { + return execute(KeyPairGenerator.class, new CheckedFunction() { + @Override + public KeyPair apply(KeyPairGenerator generator) throws InvalidAlgorithmParameterException { + generator.initialize(params, secureRandom); + return generator.generateKeyPair(); + } + }); + } + private R execute(JcaInstanceSupplier supplier, CheckedFunction callback) throws SecurityException { try { T instance = supplier.getInstance(); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java index 2a11bcb27..daf15a115 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java @@ -127,7 +127,7 @@ protected void validateKey(Key k, boolean signing) { msg += " The JWT " + "JWA Specification (RFC 7518, Section 3.2) states that keys used with " + id + " MUST have a " + "size >= " + minKeyBitLength + " bits (the key size must be greater than or equal to the hash " + - "output size). Consider using the SignatureAlgorithms." + id + ".generateKey() " + + "output size). Consider using the SignatureAlgorithms." + id + ".keyBuilder() " + "method to create a key guaranteed to be secure enough for " + id + ". See " + "https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; } else { //custom algorithm - just indicate required key length: diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 565e40072..c2f0553c3 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -667,7 +667,7 @@ class JwtsTest { def withoutSignature = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def invalidEncodedSignature = "_____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ" String jws = withoutSignature + '.' + invalidEncodedSignature - def keypair = SignatureAlgorithms.ES256.generateKeyPair() + def keypair = SignatureAlgorithms.ES256.keyPairBuilder().build() Jwts.parserBuilder().setSigningKey(keypair.public).build().parseClaimsJws(jws) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy index 75fc2bb95..af98e80a0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy @@ -8,13 +8,10 @@ import org.junit.Test import java.security.KeyFactory import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey -import java.security.spec.ECParameterSpec -import java.security.spec.ECPoint -import java.security.spec.ECPublicKeySpec -import java.security.spec.EllipticCurve -import java.security.spec.InvalidKeySpecException +import java.security.spec.* -import static org.junit.Assert.* +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail class AbstractEcJwkFactoryTest { @@ -71,7 +68,7 @@ class AbstractEcJwkFactoryTest { @Test void testAddSamePointDoublesIt() { - def pair = SignatureAlgorithms.ES256.generateKeyPair(); + def pair = SignatureAlgorithms.ES256.keyPairBuilder().build() def pub = pair.getPublic() as ECPublicKey def spec = pub.getParams() @@ -86,7 +83,7 @@ class AbstractEcJwkFactoryTest { @Test void testDerivePublicFails() { - def pair = SignatureAlgorithms.ES256.generateKeyPair(); + def pair = SignatureAlgorithms.ES256.keyPairBuilder().build() def priv = pair.getPrivate() as ECPrivateKey final def context = new DefaultJwkContext(DefaultEcPrivateJwk.FIELDS) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy index 6cb252f82..f63982337 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy @@ -7,16 +7,19 @@ import io.jsonwebtoken.security.VerifySignatureRequest import org.junit.Test import java.nio.charset.StandardCharsets -import java.security.* +import java.security.Key +import java.security.Provider +import java.security.Security -import static org.junit.Assert.* +import static org.junit.Assert.assertSame +import static org.junit.Assert.assertTrue class AbstractSignatureAlgorithmTest { @Test void testSignAndVerifyWithExplicitProvider() { Provider provider = Security.getProvider('BC') - KeyPair pair = SignatureAlgorithms.RS256.generateKeyPair() + def pair = SignatureAlgorithms.RS256.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair byte[] data = 'foo'.getBytes(StandardCharsets.UTF_8) byte[] signature = SignatureAlgorithms.RS256.sign(new DefaultSignatureRequest(provider, null, data, pair.getPrivate())) assertTrue SignatureAlgorithms.RS256.verify(new DefaultVerifySignatureRequest(provider, null, data, pair.getPublic(), signature)) @@ -24,7 +27,7 @@ class AbstractSignatureAlgorithmTest { @Test void testSignFailsWithAnExternalException() { - KeyPair pair = SignatureAlgorithms.RS256.generateKeyPair() + def pair = SignatureAlgorithms.RS256.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair def ise = new IllegalStateException('foo') def alg = new TestAbstractSignatureAlgorithm() { @Override @@ -43,7 +46,7 @@ class AbstractSignatureAlgorithmTest { @Test void testVerifyFailsWithExternalException() { - KeyPair pair = SignatureAlgorithms.RS256.generateKeyPair() + def pair = SignatureAlgorithms.RS256.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair def ise = new IllegalStateException('foo') def alg = new TestAbstractSignatureAlgorithm() { @Override diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy index 2e5cb0e1f..bf7c43d7d 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy @@ -88,7 +88,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { @Test void testSignWithInvalidKeyFieldLength() { - def keypair = SignatureAlgorithms.ES256.generateKeyPair() + def keypair = SignatureAlgorithms.ES256.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair def data = "foo".getBytes(StandardCharsets.UTF_8) def req = new DefaultSignatureRequest(null, null, data, keypair.private) try { @@ -122,7 +122,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { void testVerifyWithPrivateKey() { byte[] data = 'foo'.getBytes(StandardCharsets.UTF_8) algs().each { - KeyPair pair = it.generateKeyPair() + def pair = it.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair def key = pair.getPrivate() def signRequest = new DefaultSignatureRequest(null, null, data, key) byte[] signature = it.sign(signRequest) @@ -273,7 +273,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { void verifySwarmTest() { algs().each { alg -> def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - KeyPair keypair = alg.generateKeyPair() + def keypair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair assertNotNull keypair assertTrue keypair.getPublic() instanceof ECPublicKey assertTrue keypair.getPrivate() instanceof ECPrivateKey @@ -434,7 +434,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { void legacySignatureCompatDefaultTest() { def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def alg = SignatureAlgorithms.ES512 - def keypair = alg.generateKeyPair() + def keypair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair def signature = Signature.getInstance(alg.jcaName) def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) signature.initSign(keypair.private) @@ -461,7 +461,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def alg = SignatureAlgorithms.ES512 - def keypair = alg.generateKeyPair() + def keypair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair def signature = Signature.getInstance(alg.jcaName) def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) signature.initSign(keypair.private) @@ -479,7 +479,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { byte[] forgedSig = new byte[64] def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def alg = SignatureAlgorithms.ES256 - def keypair = alg.generateKeyPair() + def keypair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) def request = new DefaultVerifySignatureRequest(null, null, data, keypair.public, forgedSig) assertFalse alg.verify(request) @@ -495,7 +495,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def alg = SignatureAlgorithms.ES256 - def keypair = alg.generateKeyPair() + def keypair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) def request = new DefaultVerifySignatureRequest(null, null, data, keypair.public, sig) assertFalse alg.verify(request) @@ -511,7 +511,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def alg = SignatureAlgorithms.ES256 - def keypair = alg.generateKeyPair() + def keypair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) def request = new DefaultVerifySignatureRequest(null, null, data, keypair.public, sig) assertFalse alg.verify(request) @@ -522,7 +522,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { def withoutSignature = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def invalidEncodedSignature = "_____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ" def alg = SignatureAlgorithms.ES256 - def keypair = alg.generateKeyPair() + def keypair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) def invalidSignature = Decoders.BASE64URL.decode(invalidEncodedSignature) def request = new DefaultVerifySignatureRequest(null, null, data, keypair.public, invalidSignature) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy index ea7bcd0b5..338c4c3e9 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy @@ -6,7 +6,6 @@ import io.jsonwebtoken.security.WeakKeyException import org.junit.Test import javax.crypto.spec.SecretKeySpec -import java.security.KeyPair import java.security.KeyPairGenerator import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey @@ -17,14 +16,14 @@ import static org.junit.Assert.* class DefaultRsaSignatureAlgorithmTest { @Test - void testGenerateKeyPair() { + void testKeyPairBuilder() { SignatureAlgorithms.values().findAll({it.id.startsWith("RS") || it.id.startsWith("PS")}).each { - KeyPair pair = it.generateKeyPair() + def pair = it.keyPairBuilder().build() assertNotNull pair.public assertTrue pair.public instanceof RSAPublicKey - assertEquals it.preferredKeyLength, pair.public.modulus.bitLength() + assertEquals it.preferredKeyBitLength, pair.public.modulus.bitLength() assertTrue pair.private instanceof RSAPrivateKey - assertEquals it.preferredKeyLength, pair.private.modulus.bitLength() + assertEquals it.preferredKeyBitLength, pair.private.modulus.bitLength() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index 15a24ce9a..6c55a2583 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -23,7 +23,7 @@ import static org.junit.Assert.* class JwksTest { private static final SecretKey SKEY = SignatureAlgorithms.HS256.keyBuilder().build() - private static final KeyPair EC_PAIR = SignatureAlgorithms.ES256.generateKeyPair(); + private static final KeyPair EC_PAIR = SignatureAlgorithms.ES256.keyPairBuilder().build().toJdkKeyPair() private static String srandom() { byte[] random = new byte[16]; @@ -228,7 +228,7 @@ class JwksTest { for(def alg : algs) { - def pair = alg.generateKeyPair() + def pair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair PublicKey pub = pair.getPublic() PrivateKey priv = pair.getPrivate() @@ -246,8 +246,8 @@ class JwksTest { // test pair privJwk = pub instanceof ECKey ? - Jwks.builder().setKeyPairEc(pair).setPublicKeyUse("sig").build() : - Jwks.builder().setKeyPairRsa(pair).setPublicKeyUse("sig").build() + Jwks.builder().setKeyPairEc(pair.toJdkKeyPair()).setPublicKeyUse("sig").build() : + Jwks.builder().setKeyPairRsa(pair.toJdkKeyPair()).setPublicKeyUse("sig").build() assertEquals priv, privJwk.toKey() privPubJwk = privJwk.toPublicJwk() assertEquals pubJwk, privPubJwk @@ -264,7 +264,7 @@ class JwksTest { for(EllipticCurveSignatureAlgorithm alg : algs) { - def pair = alg.generateKeyPair() + def pair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair ECPublicKey pubKey = pair.getPublic() as ECPublicKey EcPublicJwk jwk = Jwks.builder().setKey(pubKey).build() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy index ab915b939..b0b9c2c90 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy @@ -47,7 +47,7 @@ class KeyPairsTest { @Test void testGetKeyECMismatch() { - KeyPair pair = SignatureAlgorithms.RS256.generateKeyPair() + KeyPair pair = SignatureAlgorithms.RS256.keyPairBuilder().build().toJdkKeyPair() Class clazz = ECPublicKey try { KeyPairs.getKey(pair, clazz) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy index 9690e78b9..7ec4988e4 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/MacSignatureAlgorithmTest.groovy @@ -67,7 +67,7 @@ class MacSignatureAlgorithmTest { String msg = 'The signing key\'s size is 192 bits which is not secure enough for the HS256 algorithm. ' + 'The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HS256 MUST have a ' + 'size >= 256 bits (the key size must be greater than or equal to the hash output size). ' + - 'Consider using the SignatureAlgorithms.HS256.generateKey() method to create a key guaranteed ' + + 'Consider using the SignatureAlgorithms.HS256.keyBuilder() method to create a key guaranteed ' + 'to be secure enough for HS256. See https://tools.ietf.org/html/rfc7518#section-3.2 for more ' + 'information.' assertEquals msg, expected.getMessage() diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy index a311ec151..f89a8a0ae 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy @@ -73,8 +73,8 @@ class KeysTest { "is not secure enough for any JWT HMAC-SHA algorithm. The JWT " + "JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " + "size >= 256 bits (the key size must be greater than or equal to the hash " + - "output size). Consider using the SignatureAlgorithms.HS256.generateKey() method (or " + - "HS384.generateKey() or HS512.generateKey()) to create a key guaranteed to be secure enough " + + "output size). Consider using the SignatureAlgorithms.HS256.keyBuilder() method (or " + + "HS384.keyBuilder() or HS512.keyBuilder()) to create a key guaranteed to be secure enough " + "for your preferred HMAC-SHA algorithm. See " + "https://tools.ietf.org/html/rfc7518#section-3.2 for more information." as String, expected.message } @@ -211,20 +211,20 @@ class KeysTest { if (alg instanceof DefaultRsaSignatureAlgorithm) { - KeyPair pair = alg.generateKeyPair() + def pair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair assertNotNull pair PublicKey pub = pair.getPublic() assert pub instanceof RSAPublicKey - assertEquals alg.preferredKeyLength, pub.modulus.bitLength() + assertEquals alg.preferredKeyBitLength, pub.modulus.bitLength() PrivateKey priv = pair.getPrivate() assert priv instanceof RSAPrivateKey - assertEquals alg.preferredKeyLength, priv.modulus.bitLength() + assertEquals alg.preferredKeyBitLength, priv.modulus.bitLength() } else if (alg instanceof DefaultEllipticCurveSignatureAlgorithm) { - KeyPair pair = alg.generateKeyPair() + def pair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair assertNotNull pair int len = alg.orderBitLength From 414939a9e69139a24bb186fb1d9a03c5c2670734 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Tue, 10 May 2022 19:29:40 -0700 Subject: [PATCH 39/75] - Added lots of JavaDoc - JwtMap: Ensured Groovy GString implementations that invoke Groovy's InvokerHelper on a JwtMap implementation will print redacted values instead of the actual secret values. --- .../jsonwebtoken/security/AeadAlgorithm.java | 62 +++++- .../io/jsonwebtoken/security/AeadRequest.java | 5 + .../io/jsonwebtoken/security/AeadResult.java | 14 ++ .../security/AssociatedDataSupplier.java | 14 +- .../jsonwebtoken/security/AsymmetricJwk.java | 125 ++++++++++++ .../jsonwebtoken/security/CryptoRequest.java | 4 +- .../security/DecryptAeadRequest.java | 6 + .../security/DecryptionKeyRequest.java | 17 ++ .../jsonwebtoken/security/DigestSupplier.java | 10 + .../jsonwebtoken/security/EcKeyAlgorithm.java | 4 + .../jsonwebtoken/security/EcPrivateJwk.java | 15 ++ .../security/EncryptionAlgorithms.java | 51 ++++- .../java/io/jsonwebtoken/security/Jwk.java | 173 +++++++++++++++- .../io/jsonwebtoken/security/JwkBuilder.java | 15 +- .../jsonwebtoken/security/KeyAlgorithm.java | 50 ++++- .../jsonwebtoken/security/KeyAlgorithms.java | 193 ++++++++++++++++++ .../io/jsonwebtoken/security/KeyPair.java | 22 +- .../io/jsonwebtoken/security/KeyRequest.java | 48 ++++- .../java/io/jsonwebtoken/security/Keys.java | 10 +- .../io/jsonwebtoken/security/Message.java | 9 + .../io/jsonwebtoken/security/PrivateJwk.java | 33 ++- .../io/jsonwebtoken/security/PublicJwk.java | 2 + .../{SecurityRequest.java => Request.java} | 15 +- .../security/RsaKeyAlgorithm.java | 6 +- .../security/SecretKeyAlgorithm.java | 2 + .../java/io/jsonwebtoken/impl/IdRegistry.java | 2 +- .../java/io/jsonwebtoken/impl/JwtMap.java | 36 ++++ .../security/AbstractFamilyJwkFactory.java | 2 +- .../impl/security/AbstractJwkBuilder.java | 14 +- .../impl/security/AbstractPrivateJwk.java | 11 +- .../impl/security/AesAlgorithm.java | 4 +- .../impl/security/CryptoAlgorithm.java | 8 +- .../impl/security/DefaultJwkContext.java | 15 ++ .../impl/security/DefaultKeyPair.java | 2 +- .../impl/security/DefaultKeyedRequest.java | 2 +- ...curityRequest.java => DefaultRequest.java} | 6 +- .../security/EncryptionAlgorithmsBridge.java | 5 +- .../impl/security/JwkContext.java | 5 + .../impl/security/CryptoAlgorithmTest.groovy | 8 +- .../security/DefaultJwkContextTest.groovy | 23 ++- .../impl/security/JwksTest.groovy | 23 ++- .../impl/security/KeyPairsTest.groovy | 2 +- .../security/RFC7516AppendixA3Test.groovy | 12 +- .../impl/security/RFC7517AppendixCTest.groovy | 2 +- .../impl/security/RFC7518AppendixCTest.groovy | 9 +- .../security/EncryptionAlgorithmsTest.groovy | 4 +- 46 files changed, 1002 insertions(+), 98 deletions(-) rename api/src/main/java/io/jsonwebtoken/security/{SecurityRequest.java => Request.java} (61%) rename impl/src/main/java/io/jsonwebtoken/impl/security/{DefaultSecurityRequest.java => DefaultRequest.java} (70%) diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java index a6d48a484..b07bb13b2 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java @@ -20,11 +20,71 @@ import javax.crypto.SecretKey; /** + * A cryptographic algorithm that performs + * Authenticated encryption with additional data. + * Per JWE RFC 7516, Section 4.1.2, all JWEs + * MUST use an AEAD algorithm to encrypt or decrypt the JWE payload/content. Consequently, all + * JWA "enc" algorithms are AEAD + * algorithms, and they are accessible as concrete instances via the {@link EncryptionAlgorithms} utility class. + * + *

    "enc" identifier

    + * + *

    {@code AeadAlgorithm} extends {@code Identifiable}: the value returned from {@link Identifiable#getId() getId()} + * will be used as the JWE "enc" protected header value.

    + * + *

    Key Strength

    + * + *

    Encryption strength is in part attributed to how difficult it is to discover the encryption key. As such, + * cryptographic algorithms often require keys of a minimum length to ensure the keys are difficult to discover + * and the algorithm's security properties are maintained.

    + * + *

    The {@code AeadAlgorithm} interface extends the {@link KeyLengthSupplier} interface to represent the length + * in bits a key must have to be used with its implementation. If you do not want to worry about lengths and + * parameters of keys required for an algorithm, it is often easier to automatically generate a key that adheres + * to the algorithms requirements, as discussed below.

    + * + *

    Key Generation

    + * + *

    {@code AeadAlgorithm} extends {@link KeyBuilderSupplier} to enable {@link SecretKey} generation. Each AEAD + * algorithm instance will return a {@link KeyBuilder} that ensures any created keys will have a sufficient length + * and algorithm parameters required by that algorithm. For example:

    + * + *
    
    + *     SecretKey key = aeadAlgorithm.keyBuilder().build();
    + * 
    + * + *

    The resulting {@code key} is guaranteed to have the correct algorithm parameters and strength/length necessary for + * that exact {@code aeadAlgorithm} instance.

    + * + * @see EncryptionAlgorithms + * @see Identifiable#getId() + * @see KeyLengthSupplier + * @see KeyBuilderSupplier + * @see KeyBuilder * @since JJWT_RELEASE_VERSION */ -public interface AeadAlgorithm extends Identifiable, KeyBuilderSupplier, KeyLengthSupplier { +public interface AeadAlgorithm extends Identifiable, KeyLengthSupplier, KeyBuilderSupplier { + /** + * Perform AEAD encryption with the plaintext represented by the specified {@code request}, returning the + * integrity-protected encrypted ciphertext result. + * + * @param request the encryption request representing the plaintext to be encrypted, any additional + * integrity-protected data and the encryption key. + * @return the encryption result containing the ciphertext, and associated initialization vector and resulting + * authentication tag. + * @throws SecurityException if there is an encryption problem or authenticity cannot be guaranteed. + */ AeadResult encrypt(AeadRequest request) throws SecurityException; + /** + * Perform AEAD decryption with the ciphertext represented by the specific {@code request}, also verifying the + * integrity and authenticity of any associated data, returning the decrypted plaintext result. + * + * @param request the decryption request representing the ciphertext to be decrypted, any additional + * integrity-protected data, authentication tag, initialization vector, and the decryption key. + * @return the decryption result containing the plaintext + * @throws SecurityException if there is a decryption problem or authenticity assertions fail. + */ Message decrypt(DecryptAeadRequest request) throws SecurityException; } diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java b/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java index 7cb491234..5edef80cb 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java @@ -18,6 +18,11 @@ import javax.crypto.SecretKey; /** + * A request to an {@link AeadAlgorithm} to perform authenticated encryption with a supplied symmetric + * {@link SecretKey}, allowing for additional data to be authenticated and integrity-protected. + * + * @see CryptoRequest + * @see AssociatedDataSupplier * @since JJWT_RELEASE_VERSION */ public interface AeadRequest extends CryptoRequest, AssociatedDataSupplier { diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadResult.java b/api/src/main/java/io/jsonwebtoken/security/AeadResult.java index 558b51805..e469cd332 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AeadResult.java +++ b/api/src/main/java/io/jsonwebtoken/security/AeadResult.java @@ -16,6 +16,20 @@ package io.jsonwebtoken.security; /** + * The result of authenticated encryption, providing access to the resulting ciphertext, AAD tag, and initialization + * vector. The AAD tag and initialization vector must be supplied with the ciphertext to decrypt. + * + *

    AAD Tag

    + * + * {@code AeadResult} inherits {@link DigestSupplier} which is a generic concept for supplying any digest. The digest + * in the case of AEAD is called an AAD tag, and it must in turn be supplied for verification during decryption. + * + *

    Initialization Vector

    + * + * All JWE-standard AEAD algorithms use a secure-random Initialization Vector for safe ciphertext creation, so + * {@code AeadAlgorithm} inherits {@link InitializationVectorSupplier} to make the generated IV available after + * encryption. This IV must in turn be supplied during decryption. + * * @since JJWT_RELEASE_VERSION */ public interface AeadResult extends Message, DigestSupplier, InitializationVectorSupplier { diff --git a/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSupplier.java b/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSupplier.java index 9960988be..3d90e8b88 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSupplier.java +++ b/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSupplier.java @@ -16,10 +16,22 @@ package io.jsonwebtoken.security; /** + * Provides any "associated data" that must be integrity protected (but not encrypted) when performing + * AEAD encryption or decryption. + * + * @see #getAssociatedData() * @since JJWT_RELEASE_VERSION */ public interface AssociatedDataSupplier { + /** + * Returns any data that must be integrity protected (but not encrypted) when performing + * AEAD encryption or decryption, or + * {@code null} if no additional data must be integrity protected. + * + * @return any data that must be integrity protected (but not encrypted) when performing + * AEAD encryption or decryption, or + * {@code null} if no additional data must be integrity protected. + */ byte[] getAssociatedData(); - } diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java index 767dee4f5..ce67ea687 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java @@ -21,17 +21,142 @@ import java.util.List; /** + * A JWK that represents an asymmetric (public or private) cryptographic key. + * * @since JJWT_RELEASE_VERSION */ public interface AsymmetricJwk extends Jwk { + /** + * Returns the JWK + * {@code use} (Public Key Use) + * parameter value or {@code null} if not present. {@code use} values are CaSe-SeNsItIvE. + * + *

    The JWK specification defines the + * following {@code use} values:

    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    JWK Key Use Values
    ValueKey Use
    {@code sig}signature
    {@code enc}encryption
    + * + *

    Other values MAY be used. For best interoperability with other applications however, it is + * recommended to use only the values above.

    + * + *

    When a key is used to wrap another key and a public key use designation for the first key is desired, the + * {@code enc} (encryption) key use value is used, since key wrapping is a kind of encryption. The + * {@code enc} value is also to be used for public keys used for key agreement operations.

    + * + *

    Public Key Use vs Key Operations

    + * + *

    Per + * JWK RFC 7517, Section 4.3, last paragraph, + * the {@code use} (Public Key Use) and {@link #getOperations() key_ops (Key Operations)} members + * SHOULD NOT be used together; however, if both are used, the information they convey MUST be + * consistent. Applications should specify which of these members they use, if either is to be used by the + * application.

    + * + * @return the JWK {@code use} value or {@code null} if not present. + */ String getPublicKeyUse(); + /** + * Returns the JWK + * {@code x5u} (X.509 URL) + * parameter value as a {@link URI} instance, or {@code null} if not present. + * + *

    If present, the URI MUST refer to a + * resource for an X.509 public key certificate or certificate chain that conforms to + * RFC 5280 in PEM-encoded form, with each certificate + * delimited as specified in + * Section 6.1 of RFC 4945. + * The key in the first certificate MUST match the public key represented by other members of + * the JWK. The protocol used to acquire the resource MUST provide integrity protection; an HTTP GET + * request to retrieve the certificate MUST use + * HTTP over TLS; the identity of the server + * MUST be validated, as per + * Section 6 of RFC 6125. Use of this + * parameter is OPTIONAL.

    + * + *

    While there is no requirement that optional JWK members providing key usage, algorithm, or other + * information be present when the {@code x5u} member is used, doing so may improve interoperability for + * applications that do not handle + * PKIX certificates [RFC5280]. If other members + * are present, the contents of those members MUST be semantically consistent with the related fields + * in the first certificate. For instance, if the {@link #getPublicKeyUse() use (Public Key Use)} member is + * present, then it MUST correspond to the usage that is specified in the certificate, when it includes + * this information. Similarly, if the {@link #getAlgorithm() alg (Algorithm)} member is present, it + * MUST correspond to the algorithm specified in the certificate.

    + * + * @return the JWK {@code x5u} value as a {@link URI} instance or {@code null} if not present. + */ URI getX509Url(); + /** + * Returns the JWK + * {@code x5c} (X.509 Certificate Chain) + * parameter value as a type-safe List<{@link X509Certificate}>, or + * {@code null} if not present. + * + *

    The certificate chain is a {@code List} of {@link X509Certificate}s. The certificate containing the + * key value MUST be the first in the list (at index {@code 0}). This MAY be + * followed by additional certificates, with each subsequent certificate being the one used to certify the + * previous one. The key in the first certificate MUST match the public key represented by other + * members of the JWK.

    + * + * @return the JWK {@code x5u} value as a type-safe List<{@link X509Certificate}> or + * {@code null} if not present. + */ List getX509CertificateChain(); + /** + * Returns the JWK + * {@code x5t} (X.509 Certificate SHA-1 + * Thumbprint) parameter value (aka SHA-1 'fingerprint') as a type-safe {@code byte[]}, or {@code null} + * if not present. + * + *

    The key in the certificate MUST match the public key represented by other members of the JWK.

    + * + *

    As with the {@link #getX509Url()} method, optional JWK members providing key usage, algorithm, or other + * information MAY also be present when the {@code x5t} member is used. If other members are + * present, the contents of those members MUST be semantically consistent with the related fields in + * the referenced certificate. See the last paragraph of the {@link #getX509Url()} method JavaDoc for + * additional guidance on this.

    + * + * @return the JWK {@code x5t} value as a type-safe {@code byte[]} or {@code null} if not present. + */ byte[] getX509CertificateSha1Thumbprint(); + /** + * Returns the JWK + * {@code x5t#256} (X.509 Certificate SHA-256 + * Thumbprint) parameter value (aka SHA-256 'fingerprint') as a type-safe {@code byte[]}, or {@code null} + * if not present. + * + *

    The key in the certificate MUST match the public key represented by other members of the JWK.

    + * + *

    As with the {@link #getX509Url()} method, optional JWK members providing key usage, algorithm, or other + * information MAY also be present when the {@code x5t#256} member is used. If other members are + * present, the contents of those members MUST be semantically consistent with the related fields in + * the referenced certificate. See the last paragraph of the {@link #getX509Url()} method JavaDoc for + * additional guidance on this.

    + * + * @return the JWK {@code x5t#256} value as a type-safe {@code byte[]} or {@code null} if not present. + */ byte[] getX509CertificateSha256Thumbprint(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java b/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java index 78ff34515..eee6ea6b9 100644 --- a/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java @@ -18,7 +18,9 @@ import java.security.Key; /** + * A request to a cryptographic algorithm that requires a {@link Key}. + * * @since JJWT_RELEASE_VERSION */ -public interface CryptoRequest extends Message, SecurityRequest, KeySupplier { +public interface CryptoRequest extends Message, Request, KeySupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/DecryptAeadRequest.java b/api/src/main/java/io/jsonwebtoken/security/DecryptAeadRequest.java index c34e06b1a..accca6a06 100644 --- a/api/src/main/java/io/jsonwebtoken/security/DecryptAeadRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/DecryptAeadRequest.java @@ -15,7 +15,13 @@ */ package io.jsonwebtoken.security; +import javax.crypto.SecretKey; + /** + * A request to an {@link AeadAlgorithm} to decrypt ciphertext and perform integrity-protection with a supplied + * decryption {@link SecretKey}. Extends both {@link InitializationVectorSupplier} and {@link DigestSupplier} to + * ensure the respective required IV and AAD tag returned from an {@link AeadResult} are available for decryption. + * * @since JJWT_RELEASE_VERSION */ public interface DecryptAeadRequest extends AeadRequest, InitializationVectorSupplier, DigestSupplier { diff --git a/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java index f39b23167..92eac4823 100644 --- a/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java @@ -18,6 +18,23 @@ import java.security.Key; /** + * A {@link KeyRequest} to obtain a decryption key that will be used to decrypt a JWE using an {@link AeadAlgorithm}. + * The AEAD algorithm used for decryption is accessible via {@link #getEncryptionAlgorithm()}. + * + *

    The key used to perform cryptographic operations, for example a direct shared key, or a + * JWE "key decryption key" will be accessible via {@link #getKey()}. This is always required and + * never {@code null}.

    + * + *

    Any encrypted key material (what the JWE specification calls the + * JWE Encrypted Key) will + * be accessible via {@link #getContent()}. If present, the {@link KeyAlgorithm} will decrypt it to obtain the resulting + * Content Encryption Key (CEK). + * This may be empty however depending on which {@link KeyAlgorithm} was used during JWE encryption.

    + * + *

    Finally, any public information necessary by the called {@link KeyAlgorithm} to decrypt any + * {@code JWE Encrypted Key} (such as an initialization vector, authentication tag, ephemeral key, etc) is expected + * to be available in the JWE protected header, accessible via {@link #getHeader()}.

    + * * @since JJWT_RELEASE_VERSION */ public interface DecryptionKeyRequest extends KeyRequest, Message { diff --git a/api/src/main/java/io/jsonwebtoken/security/DigestSupplier.java b/api/src/main/java/io/jsonwebtoken/security/DigestSupplier.java index fdc71f619..60f0ec2b3 100644 --- a/api/src/main/java/io/jsonwebtoken/security/DigestSupplier.java +++ b/api/src/main/java/io/jsonwebtoken/security/DigestSupplier.java @@ -16,10 +16,20 @@ package io.jsonwebtoken.security; /** + * A {@code DigestSupplier} provides access to the result of a cryptographic digest algorithm, such as a + * Message Digest, MAC, Signature, or Authentication Tag. + * * @since JJWT_RELEASE_VERSION */ public interface DigestSupplier { + /** + * Returns a cryptographic digest result, such as a Message Digest, MAC, Signature, or Authentication Tag + * depending on the cryptographic algorithm that produced it. + * + * @return a cryptographic digest result, such as a Message Digest, MAC, Signature, or Authentication Tag + * * depending on the cryptographic algorithm that produced it. + */ byte[] getDigest(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/EcKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/EcKeyAlgorithm.java index ccb5567d4..d402e4a6c 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EcKeyAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/EcKeyAlgorithm.java @@ -20,6 +20,10 @@ import java.security.interfaces.ECKey; /** + * A {@link KeyAlgorithm} that produces JWE Encrypted Keys via Elliptic Curve cryptography. + * + * @param the type of Elliptic Curve public key used to obtain the AEAD encryption key + * @param the type of Elliptic Curve private key used to obtain the AEAD decryption key * @since JJWT_RELEASE_VERSION */ public interface EcKeyAlgorithm extends KeyAlgorithm { diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java index 7ed6da5fd..d95a99af0 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java @@ -19,6 +19,21 @@ import java.security.interfaces.ECPublicKey; /** + * The JWK parallel of a Java {@link ECPrivateKey}. + * + *

    Note that the various EC-specific properties are not available as separate dedicated getter methods, as most Java + * applications should rarely, if ever, need to access these individual key properties since they typically represent + * internal key material and/or implementation details.

    + * + *

    Even so, because they exist and are readable by nature of every JWK being a {@link java.util.Map Map}, the + * properties are still accessible in two different ways:

    + *
      + *
    • Via the standard {@code Map} {@link #get(Object) get} method using an appropriate JWK parameter id, + * e.g. {@code jwk.get("x")}, {@code jwk.get("y")}, etc.
    • + *
    • Via the various getter methods on the {@link ECPrivateKey} instance returned by {@link #toKey()}.
    • + *
    + * + * the {@link #get(Object) get} method

    * @since JJWT_RELEASE_VERSION */ public interface EcPrivateJwk extends PrivateJwk { diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java index 27907dc59..56ac36e99 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java @@ -21,6 +21,10 @@ import java.util.Collection; /** + * Constant definitions and utility methods for all + *
    JWA (RFC 7518) Encryption Algorithms. + * + * @see AeadAlgorithm * @since JJWT_RELEASE_VERSION */ public final class EncryptionAlgorithms { @@ -33,6 +37,11 @@ private EncryptionAlgorithms() { private static final Class BRIDGE_CLASS = Classes.forName(BRIDGE_CLASSNAME); private static final Class[] ID_ARG_TYPES = new Class[]{String.class}; + /** + * Returns all JWE-standard AEAD encryption algorithms as an unmodifiable collection. + * + * @return all JWE-standard AEAD encryption algorithms as an unmodifiable collection. + */ public static Collection values() { return Classes.invokeStatic(BRIDGE_CLASS, "values", null, (Object[]) null); } @@ -40,10 +49,12 @@ public static Collection values() { /** * Returns the JWE Encryption Algorithm with the specified * {@code enc} algorithm identifier or - * {@code null} if an algorithm for the specified {@code id} cannot be found. + * {@code null} if a JWE-standard algorithm for the specified {@code id} cannot be found. If a JWE-standard + * instance must be resolved, consider using the {@link #forId(String)} method instead. * * @param id a JWE standard {@code enc} algorithm identifier - * @return the associated Encryption Algorithm instance or {@code null} otherwise. + * @return the associated standard Encryption Algorithm instance or {@code null} otherwise. + * @see #forId(String) * @see RFC 7518, Section 5.1 */ public static AeadAlgorithm findById(String id) { @@ -51,7 +62,20 @@ public static AeadAlgorithm findById(String id) { return Classes.invokeStatic(BRIDGE_CLASS, "findById", ID_ARG_TYPES, id); } - public static AeadAlgorithm forId(String id) { + /** + * Returns the JWE Encryption Algorithm with the specified + * {@code enc} algorithm identifier or + * throws an {@link IllegalArgumentException} if there is no JWE-standard algorithm for the specified + * {@code id}. If a JWE-standard instance result is not mandatory, consider using the {@link #findById(String)} + * method instead. + * + * @param id a JWE standard {@code enc} algorithm identifier + * @return the associated Encryption Algorithm instance. + * @throws IllegalArgumentException if there is no JWE-standard algorithm for the specified identifier. + * @see #findById(String) + * @see RFC 7518, Section 5.1 + */ + public static AeadAlgorithm forId(String id) throws IllegalArgumentException { Assert.hasText(id, "id cannot be null or empty."); return Classes.invokeStatic(BRIDGE_CLASS, "forId", ID_ARG_TYPES, id); } @@ -79,22 +103,31 @@ public static AeadAlgorithm forId(String id) { /** * "AES GCM using 128-bit key" as defined by - * RFC 7518, Section 5.3. This algorithm requires - * a 128 bit (16 byte) key. + * RFC 7518, Section 5.31. This + * algorithm requires a 128 bit (16 byte) key. + * + *

    1 Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath.

    */ public static final AeadAlgorithm A128GCM = forId("A128GCM"); /** * "AES GCM using 192-bit key" as defined by - * RFC 7518, Section 5.3. This algorithm requires - * a 192 bit (24 byte) key. + * RFC 7518, Section 5.31. This + * algorithm requires a 192 bit (24 byte) key. + * + *

    1 Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath.

    */ public static final AeadAlgorithm A192GCM = forId("A192GCM"); /** * "AES GCM using 256-bit key" as defined by - * RFC 7518, Section 5.3. This algorithm requires - * a 256 bit (32 byte) key. + * RFC 7518, Section 5.31. This + * algorithm requires a 256 bit (32 byte) key. + * + *

    1 Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath.

    */ public static final AeadAlgorithm A256GCM = forId("A256GCM"); } diff --git a/api/src/main/java/io/jsonwebtoken/security/Jwk.java b/api/src/main/java/io/jsonwebtoken/security/Jwk.java index 4ab0c81a2..2a046118b 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Jwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/Jwk.java @@ -22,15 +22,186 @@ import java.util.Set; /** + * A JWK is an immutable set of name/value pairs that represent a cryptographic key as defined by + * RFC 7517: JSON Web Key (JWK). The {@code Jwk} + * interface represents JWK properties accessible for any JWK. Subtypes will have additional JWK properties + * specific to different types of cryptographic keys (e.g. Secret, Asymmetric, RSA, Elliptic Curve, etc).

    + * + *

    Immutability

    + * + *

    JWKs are immutable and cannot be changed after they are created. {@code Jwk} extends the + * {@link Map} interface purely out of convenience: to allow easy marshalling to JSON as well as name/value + * pair access and key/value iteration, and other conveniences provided by the Map interface. Attempting to call any of + * the {@link Map} interface's mutation methods however (such as {@link Map#put(Object, Object) put}, + * {@link Map#remove(Object) remove}, {@link Map#clear() clear}, etc) will result in an + * {@link UnsupportedOperationException} being thrown.

    + * + *

    Identification

    + * + *

    {@code Jwk} extends {@link Identifiable} to support the + * JWK {@code kid} parameter. Calling + * {@link #getId() aJwk.getId()} is the type-safe idiomatic approach to the alternative equivalent of + * {@code aJwk.get("kid")}. Either approach will return an id if one was originally set on the JWK, or {@code null} if + * an id does not exist.

    + * + *

    toString Safety

    + * + *

    JWKs often represent secret or private key data which should never be exposed publicly, nor mistakenly printed + * via application logs or {@code System.out.println} calls. As a result, all JJWT JWK {@link #toString()} + * implementations automatically print redacted values instead actual values for any private or secret fields.

    + * + *

    For example, a {@link SecretJwk} will have an internal "{@code k}" member whose value reflects raw + * key material that should always be kept secret. If {@code aSecretJwk.toString()} is called, the resulting string + * will contain the substring k=<redacted>, instead of the actual {@code k} value. The string + * literal <redacted> is printed everywhere a private or secret value would have otherwise.

    + * + *

    WARNING: Note however, certain JVM programming languages (like + * + * Groovy for example) when encountering a + * Map or Collection instance, will NOT always call an object's {@code toString()} method when rendering + * strings. Because all JJWT JWKs implement the {@link Map Map} interface, in these language environments, + * you must explicitly call {@code aJwk.toString()} method to override the language's built-in string rendering to + * ensure key safety. This is not a concern if using the Java language directly.

    + * + *

    For example, this is safe in Java:

    + *
    
    + *     String s = "My JWK is: " + aSecretJwk; //or String.format("My JWK is: %s", aSecretJwk)
    + *     System.out.println(s);
    + * 
    + * + *

    Whereas the same is NOT SAFE in Groovy:

    + *
    
    + *     println "My JWK is: ${aSecretJwk}" // or "My JWK is " + aSecretJwk
    + * 
    + * + *

    But the following IS safe in Groovy:

    + *
    
    + *     println "My JWK is: ${aSecretJwk.toString()}" // or "My JWK is " + aSecretJwk.toString()
    + * 
    + *

    Because Groovy's {@code GString} concept does not call {@code Map#toString()} directly and creates its own + * toString implementation with the raw name/value pairs, you must call {@link String#toString() toString()} + * explicitly.

    + * + *

    If you are using an alternative JVM programming language other than Java, understand your language + * environment's String rendering behavior and adjust for explicit {@code toString()} calls as necessary.

    + * * @since JJWT_RELEASE_VERSION */ -public interface Jwk extends Identifiable, Map { +public interface Jwk extends Identifiable, Map { + /** + * Returns the JWK + * {@code alg} (Algorithm) parameter value + * or {@code null} if not present. + * + * @return the JWK {@code alg} value or {@code null} if not present. + */ String getAlgorithm(); + /** + * Returns the JWK + * {@code key_ops} (Key Operations) + * parameter values or {@code null} if not present. Any values within the returned {@code Set} are + * CaSe-SeNsItIvE. + * + *

    The JWK specification defines the + * following values:

    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    JWK Key Operations
    ValueOperation
    {@code sign}compute digital signatures or MAC
    {@code verify}verify digital signatures or MAC
    {@code encrypt}encrypt content
    {@code decrypt}decrypt content and validate decryption, if applicable
    {@code wrapKey}encrypt key
    {@code unwrapKey}decrypt key and validate decryption, if applicable
    {@code deriveKey}derive key
    {@code deriveBits}derive bits not to be used as a key
    + * + *

    Other values MAY be used. For best interoperability with other applications however, it is + * recommended to use only the values above.

    + * + *

    Multiple unrelated key operations SHOULD NOT be specified for a key because of the potential + * vulnerabilities associated with using the same key with multiple algorithms. Thus, the combinations + * {@code sign} with {@code verify}, {@code encrypt} with {@code decrypt}, and {@code wrapKey} with + * {@code unwrapKey} are permitted, but other combinations SHOULD NOT be used.

    + * + * @return the JWK {@code alg} value or {@code null} if not present. + */ Set getOperations(); + /** + * Returns the required JWK + * {@code kty} (Key Type) + * parameter value. A value is required and may not be {@code null}. + * + *

    The JWA specification defines the + * following {@code kty} values:

    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    JWK Key Types
    ValueKey Type
    {@code EC}Elliptic Curve [DSS]
    {@code RSA}RSA [RFC 3447]
    {@code oct}Octet sequence (used to represent symmetric keys)
    + */ String getType(); + /** + * Converts the JWK to its corresponding Java {@link Key} instance for use with Java cryptographic + * APIs. + * + * @return the corresponding Java {@link Key} instance for use with Java cryptographic APIs. + */ K toKey(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java index fa0f8b7d2..7a235f27a 100644 --- a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java @@ -15,17 +15,14 @@ */ package io.jsonwebtoken.security; -import io.jsonwebtoken.lang.Builder; - import java.security.Key; -import java.security.Provider; import java.util.Map; import java.util.Set; /** * @since JJWT_RELEASE_VERSION */ -public interface JwkBuilder, T extends JwkBuilder> extends Builder { +public interface JwkBuilder, T extends JwkBuilder> extends SecurityBuilder { T put(String name, Object value); @@ -36,14 +33,4 @@ public interface JwkBuilder, T extends JwkBuilde T setId(String id); T setOperations(Set ops); - - /** - * Sets the JCA Provider to use during key operations, or {@code null} if the - * JCA subsystem preferred provider should be used. - * - * @param provider the JCA Provider to use during key operations, or {@code null} if the - * JCA subsystem preferred provider should be used. - * @return the builder for method chaining. - */ - T setProvider(Provider provider); } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java index 5dd2eb97b..84a40d50b 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java @@ -23,18 +23,60 @@ /** * A {@code KeyAlgorithm} produces the {@link SecretKey} used to encrypt or decrypt a JWE. The {@code KeyAlgorithm} * used for a particular JWE is {@link #getId() identified} in the JWE's - * {@code alg} header. + * {@code alg} header. The {@code KeyAlgorithm} + * interface is JJWT's idiomatic approach to the JWE specification's + * {@code Key Management Mode} concept. * - *

    The {@code KeyAlgorithm} interface is JJWT's idiomatic approach to the JWE specification's - * {@code Key Management Mode} concept.

    + *

    All standard Key Algorithms are defined in + * JWA (RFC 7518), Section 4.1, + * and they are all available as concrete instances via the {@link KeyAlgorithms} utility class.

    * - * @since JJWT_RELEASE_VERSION + *

    "alg" identifier

    + * + *

    {@code KeyAlgorithm} extends {@code Identifiable}: the value returned from + * {@link Identifiable#getId() keyAlgorithm.getId()} will be used as the + * JWE "alg" protected header value.

    + * + * @param The type of key to use to obtain the AEAD encryption key + * @param The type of key to use to obtain the AEAD decryption key * @see RFC 7561, Section 2: JWE Key (Management) Algorithms + * @since JJWT_RELEASE_VERSION */ @SuppressWarnings("JavadocLinkAsPlainText") public interface KeyAlgorithm extends Identifiable { + /** + * Return the {@link SecretKey} that should be used to encrypt a JWE via the request's specified + * {@link KeyRequest#getEncryptionAlgorithm() AeadAlgorithm}. The encryption key will + * be available via the result's {@link KeyResult#getKey() result.getKey()} method. + * + *

    If the key algorithm uses key encryption or key agreement to produce an encrypted key value that must be + * included in the JWE, the encrypted key ciphertext will be available via the result's + * {@link KeyResult#getContent() result.getContent()} method. If the key algorithm does not produce encrypted + * key ciphertext, {@link KeyResult#getContent() result.getContent()} will be a non-null empty byte array.

    + * + * @param request the {@code KeyRequest} containing information necessary to produce a {@code SecretKey} for + * {@link AeadAlgorithm AEAD} encryption. + * @return the {@link SecretKey} that should be used to encrypt a JWE via the request's specified + * {@link KeyRequest#getEncryptionAlgorithm() AeadAlgorithm}, along with any optional encrypted key ciphertext. + * @throws SecurityException if there is a problem obtaining or encrypting the AEAD {@code SecretKey}. + */ KeyResult getEncryptionKey(KeyRequest request) throws SecurityException; + /** + * Return the {@link SecretKey} that should be used to decrypt a JWE via the request's specified + * {@link DecryptionKeyRequest#getEncryptionAlgorithm() AeadAlgorithm}. + * + *

    If the key algorithm used key encryption or key agreement to produce an encrypted key value, the encrypted + * key ciphertext will be available via the request's {@link DecryptionKeyRequest#getContent() result.getContent()} + * method. If the key algorithm did not produce encrypted key ciphertext, + * {@link DecryptionKeyRequest#getContent() request.getContent()} will return a non-null empty byte array.

    + * + * @param request the {@code DecryptionKeyRequest} containing information necessary to obtain a + * {@code SecretKey} for {@link AeadAlgorithm AEAD} decryption. + * @return the {@link SecretKey} that should be used to decrypt a JWE via the request's specified + * {@link DecryptionKeyRequest#getEncryptionAlgorithm() AeadAlgorithm}. + * @throws SecurityException if there is a problem obtaining or decrypting the AEAD {@code SecretKey}. + */ SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException; } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index e8ac8b673..2a4bb27e6 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -64,16 +64,209 @@ private static T forId0(String id) { return Classes.invokeStatic(BRIDGE_CLASS, "forId", ID_ARG_TYPES, id); } + /** + * Key algorithm reflecting direct use of a shared symmetric key as the JWE AEAD encryption key, as defined + * by RFC 7518 (JWA), Section 4.5. This + * algorithm does not produce encrypted key ciphertext. + */ public static final KeyAlgorithm DIRECT = forId0("dir"); + + /** + * AES Key Wrap algorithm with default initial value using a 128-bit key, as defined by + * RFC 7518 (JWA), Section 4.4. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    2. + *
    3. Encrypts this newly-generated {@code SecretKey} with a 128-bit shared symmetric key using the + * AES Key Wrap algorithm, producing encrypted key ciphertext.
    4. + *
    5. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
    6. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Decrypts the encrypted key ciphertext with the 128-bit shared symmetric key, + * using the AES Key Unwrap algorithm, producing the decryption key plaintext.
    4. + *
    5. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    6. + *
    + */ public static final SecretKeyAlgorithm A128KW = forId0("A128KW"); + + /** + * AES Key Wrap algorithm with default initial value using a 192-bit key, as defined by + * RFC 7518 (JWA), Section 4.4. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    2. + *
    3. Encrypts this newly-generated {@code SecretKey} with a 192-bit shared symmetric key using the + * AES Key Wrap algorithm, producing encrypted key ciphertext.
    4. + *
    5. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
    6. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Decrypts the encrypted key ciphertext with the 192-bit shared symmetric key, + * using the AES Key Unwrap algorithm, producing the decryption key plaintext.
    4. + *
    5. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    6. + *
    + */ public static final SecretKeyAlgorithm A192KW = forId0("A192KW"); + + /** + * AES Key Wrap algorithm with default initial value using a 256-bit key, as defined by + * RFC 7518 (JWA), Section 4.4. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    2. + *
    3. Encrypts this newly-generated {@code SecretKey} with a 256-bit shared symmetric key using the + * AES Key Wrap algorithm, producing encrypted key ciphertext.
    4. + *
    5. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
    6. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Decrypts the encrypted key ciphertext with the 256-bit shared symmetric key, + * using the AES Key Unwrap algorithm, producing the decryption key plaintext.
    4. + *
    5. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    6. + *
    + */ public static final SecretKeyAlgorithm A256KW = forId0("A256KW"); + + /** + * Key wrap algorithm with AES GCM using a 128-bit key, as defined by + * RFC 7518 (JWA), Section 4.7. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    2. + *
    3. Generates a new secure-random 96-bit Initialization Vector to use during key wrap/encryption.
    4. + *
    5. Encrypts this newly-generated {@code SecretKey} with a 128-bit shared symmetric key using the + * AES GCM Key Wrap algorithm with the generated Initialization Vector, producing encrypted key ciphertext + * and GCM authentication tag.
    6. + *
    7. Sets the generated initialization vector as the required + * "iv" + * (Initialization Vector) Header Parameter
    8. + *
    9. Sets the resulting GCM authentication tag as the required + * "tag" + * (Authentication Tag) Header Parameter
    10. + *
    11. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
    12. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Obtains the required initialization vector from the + * "iv" + * (Initialization Vector) Header Parameter
    4. + *
    5. Obtains the required GCM authentication tag from the + * "tag" + * (Authentication Tag) Header Parameter
    6. + *
    7. Decrypts the encrypted key ciphertext with the 128-bit shared symmetric key, the initialization vector + * and GCM authentication tag using the AES GCM Key Unwrap algorithm, producing the decryption key + * plaintext.
    8. + *
    9. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    10. + *
    + */ public static final SecretKeyAlgorithm A128GCMKW = forId0("A128GCMKW"); + + /** + * Key wrap algorithm with AES GCM using a 192-bit key, as defined by + * RFC 7518 (JWA), Section 4.7. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    2. + *
    3. Generates a new secure-random 96-bit Initialization Vector to use during key wrap/encryption.
    4. + *
    5. Encrypts this newly-generated {@code SecretKey} with a 192-bit shared symmetric key using the + * AES GCM Key Wrap algorithm with the generated Initialization Vector, producing encrypted key ciphertext + * and GCM authentication tag.
    6. + *
    7. Sets the generated initialization vector as the required + * "iv" + * (Initialization Vector) Header Parameter
    8. + *
    9. Sets the resulting GCM authentication tag as the required + * "tag" + * (Authentication Tag) Header Parameter
    10. + *
    11. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
    12. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Obtains the required initialization vector from the + * "iv" + * (Initialization Vector) Header Parameter
    4. + *
    5. Obtains the required GCM authentication tag from the + * "tag" + * (Authentication Tag) Header Parameter
    6. + *
    7. Decrypts the encrypted key ciphertext with the 192-bit shared symmetric key, the initialization vector + * and GCM authentication tag using the AES GCM Key Unwrap algorithm, producing the decryption key \ + * plaintext.
    8. + *
    9. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    10. + *
    + */ public static final SecretKeyAlgorithm A192GCMKW = forId0("A192GCMKW"); + + /** + * Key wrap algorithm with AES GCM using a 256-bit key, as defined by + * RFC 7518 (JWA), Section 4.7. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    2. + *
    3. Generates a new secure-random 96-bit Initialization Vector to use during key wrap/encryption.
    4. + *
    5. Encrypts this newly-generated {@code SecretKey} with a 256-bit shared symmetric key using the + * AES GCM Key Wrap algorithm with the generated Initialization Vector, producing encrypted key ciphertext + * and GCM authentication tag.
    6. + *
    7. Sets the generated initialization vector as the required + * "iv" + * (Initialization Vector) Header Parameter
    8. + *
    9. Sets the resulting GCM authentication tag as the required + * "tag" + * (Authentication Tag) Header Parameter
    10. + *
    11. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
    12. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Obtains the required initialization vector from the + * "iv" + * (Initialization Vector) Header Parameter
    4. + *
    5. Obtains the required GCM authentication tag from the + * "tag" + * (Authentication Tag) Header Parameter
    6. + *
    7. Decrypts the encrypted key ciphertext with the 256-bit shared symmetric key, the initialization vector + * and GCM authentication tag using the AES GCM Key Unwrap algorithm, producing the decryption key \ + * plaintext.
    8. + *
    9. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    10. + *
    + */ public static final SecretKeyAlgorithm A256GCMKW = forId0("A256GCMKW"); public static final KeyAlgorithm PBES2_HS256_A128KW = forId0("PBES2-HS256+A128KW"); public static final KeyAlgorithm PBES2_HS384_A192KW = forId0("PBES2-HS384+A192KW"); public static final KeyAlgorithm PBES2_HS512_A256KW = forId0("PBES2-HS512+A256KW"); + + /** + * Key Encryption with RSAES-PKCS1-v1_5, as defined by + * RFC 7518 (JWA), Section 4.7. + */ public static final RsaKeyAlgorithm RSA1_5 = forId0("RSA1_5"); public static final RsaKeyAlgorithm RSA_OAEP = forId0("RSA-OAEP"); public static final RsaKeyAlgorithm RSA_OAEP_256 = forId0("RSA-OAEP-256"); diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyPair.java b/api/src/main/java/io/jsonwebtoken/security/KeyPair.java index 0cd26e839..fb2785967 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyPair.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyPair.java @@ -4,13 +4,33 @@ import java.security.PublicKey; /** + * Generics-capable and type-safe alternative to {@link java.security.KeyPair}. Instances may be + * converted to {@link java.security.KeyPair} if desired via {@link #toJavaKeyPair()}. + * + * @param The type of {@link PublicKey} in the key pair. + * @param The type of {@link PrivateKey} in the key pair. * @since JJWT_RELEASE_VERSION */ public interface KeyPair { + /** + * Returns the pair's public key. + * + * @return the pair's public key. + */ A getPublic(); + /** + * Returns the pair's private key. + * + * @return the pair's private key. + */ B getPrivate(); - java.security.KeyPair toJdkKeyPair(); + /** + * Returns this instance as a {@link java.security.KeyPair} instance. + * + * @return this instance as a {@link java.security.KeyPair} instance. + */ + java.security.KeyPair toJavaKeyPair(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java b/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java index c81eb5e06..58e08c7e5 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java @@ -20,11 +20,57 @@ import java.security.Key; /** + * A request to a {@link KeyAlgorithm} to obtain the key necessary for AEAD encryption or decryption. The exact + * {@link AeadAlgorithm} that will be used is accessible via {@link #getEncryptionAlgorithm()}. + * + *

    The key used to perform cryptographic operations, for example a direct shared key, or a + * JWE "key encryption key" will be accessible via {@link #getKey()}. This is always required and + * never {@code null}.

    + * + *

    For an encryption key request, any public information specific to the called {@link KeyAlgorithm} + * implementation that is required to be transmitted in the JWE (such as an initialization vector, + * authentication tag or ephemeral key, etc) may be added to the JWE protected header, accessible via + * {@link #getHeader()}. Although the JWE header is checked for authenticity and integrity, it itself is + * not encrypted, so {@link KeyAlgorithm}s should never place any secret or private information in the + * header.

    + * + *

    For a decryption request, any public information necessary by the called {@link KeyAlgorithm} + * (such as an initialization vector, authentication tag, ephemeral key, etc) is expected to be available in + * the JWE protected header, accessible via {@link #getHeader()}.

    + * * @since JJWT_RELEASE_VERSION */ -public interface KeyRequest extends SecurityRequest, KeySupplier { +public interface KeyRequest extends Request, KeySupplier { + /** + * Returns the {@link AeadAlgorithm} that will be called for encryption or decryption after processing the + * {@code KeyRequest}. {@link KeyAlgorithm} implementations that generate an ephemeral {@code SecretKey} to use + * as what the
    JWE specification calls a + * "Content Encryption Key (CEK)" should call the {@code AeadAlgorithm}'s + * {@link AeadAlgorithm#keyBuilder() keyBuilder()} to obtain a builder that will create a key suitable for that + * exact {@code AeadAlgorithm}. + * + * @return the {@link AeadAlgorithm} that will be called for encryption or decryption after processing the + * {@code KeyRequest}. + */ AeadAlgorithm getEncryptionAlgorithm(); + /** + * Returns the {@link JweHeader} that will be used to construct the final JWE, available for reading or writing + * any {@link KeyAlgorithm}-specific information. + * + *

    For an encryption key request, any public information specific to the called {@code KeyAlgorithm} + * implementation that is required to be transmitted in the JWE (such as an initialization vector, + * authentication tag or ephemeral key, etc) is expected to be added to this header. Although the header is + * checked for authenticity and integrity, it itself is not encrypted, so + * {@link KeyAlgorithm}s should never place any secret or private information in the header.

    + * + *

    For a decryption request, any public information necessary by the called {@link KeyAlgorithm} + * (such as an initialization vector, authentication tag, ephemeral key, etc) is expected to be available in + * this header.

    + * + * @return the {@link JweHeader} that will be used to construct the final JWE, available for reading or writing + * any {@link KeyAlgorithm}-specific information. + */ JweHeader getHeader(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index 900a7c750..74fa0d8a3 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -86,8 +86,10 @@ public static SecretKey hmacShaKeyFor(byte[] bytes) throws WeakKeyException { * {@link SignatureAlgorithms#HS512}.keyBuilder().build(); *
    * - *

    Call those methods as needed instead of this {@code secretKeyFor} helper method. This helper method will be - * removed before the 1.0 final release.

    + *

    Call those methods as needed instead of this static {@code secretKeyFor} helper method - the returned + * {@link KeyBuilder} allows callers to specify a preferred Provider or SecureRandom on the builder if + * desired, whereas this {@code secretKeyFor} method does not. Consequently this helper method will be removed + * before the 1.0 release.

    * *

    Previous Documentation

    * @@ -150,7 +152,7 @@ public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) thr * {@link SignatureAlgorithms#ES512}.keyPairBuilder().build(); * * - *

    Call those methods as needed instead of this {@code keyPairFor} helper method - the returned + *

    Call those methods as needed instead of this static {@code keyPairFor} helper method - the returned * {@link KeyPairBuilder} allows callers to specify a preferred Provider or SecureRandom on the builder if * desired, whereas this {@code keyPairFor} method does not. Consequently this helper method will be removed * before the 1.0 release.

    @@ -239,7 +241,7 @@ public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws throw new IllegalArgumentException(msg); } AsymmetricKeySignatureAlgorithm asalg = ((AsymmetricKeySignatureAlgorithm) salg); - return asalg.keyPairBuilder().build().toJdkKeyPair(); + return asalg.keyPairBuilder().build().toJavaKeyPair(); } /** diff --git a/api/src/main/java/io/jsonwebtoken/security/Message.java b/api/src/main/java/io/jsonwebtoken/security/Message.java index 4b63af816..d8b0a4339 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Message.java +++ b/api/src/main/java/io/jsonwebtoken/security/Message.java @@ -16,10 +16,19 @@ package io.jsonwebtoken.security; /** + * A message contains {@link #getContent() content} used as input to a cryptographic algorithm, such as + * signing, encryption or decryption. + * * @since JJWT_RELEASE_VERSION */ public interface Message { + /** + * Returns the message content to be used as input to a cryptographic algorithm. This is almost always + * plaintext used for cryptographic signatures or encryption, or ciphertext for decryption. + * + * @return the message content to be used as input to a cryptographic algorithm. + */ byte[] getContent(); //plaintext or ciphertext } diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java index a19148274..52d002cb6 100644 --- a/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java @@ -15,17 +15,44 @@ */ package io.jsonwebtoken.security; -import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; /** + * The JWK parallel of a Java {@link PrivateKey}. + * + *

    JWK Private Key vs Java {@code PrivateKey} differences

    + * + *

    Unlike the Java cryptography APIs, the JWK specification requires all public key and private key + * properties to be contained within every private JWK. As such, a {@code PrivateJwk} of course represents + * private key fields as its name implies, but it is probably more similar to the Java JCA concept of a + * {@link java.security.KeyPair} since it contains everything for both keys.

    + * + *

    Consequently a {@code PrivateJwk} is capable of providing two additional convenience methods:

    + *
      + *
    • {@link #toPublicJwk()} - a method to obtain a {@link PublicJwk} instance that contains only the JWK public + * key properties, and
    • + *
    • {@link #toKeyPair()} - a method to obtain both Java {@link PublicKey} and {@link PrivateKey}s in aggregate + * as a {@link KeyPair} instance if desired.
    • + *
    + * * @since JJWT_RELEASE_VERSION */ public interface PrivateJwk> extends AsymmetricJwk { + /** + * Returns the private JWK's corresponding {@link PublicJwk}, containing only the key's public properties. + * + * @return the private JWK's corresponding {@link PublicJwk}, containing only the key's public properties. + */ M toPublicJwk(); - KeyPair toKeyPair(); - + /** + * Returns the key's corresponding Java {@link PrivateKey} and {@link PublicKey} in aggregate as a + * type-safe {@link KeyPair} instance. + * + * @return the key's corresponding Java {@link PrivateKey} and {@link PublicKey} in aggregate as a + * type-safe {@link KeyPair} instance. + */ + KeyPair toKeyPair(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java b/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java index 22cfae9b4..425fb5749 100644 --- a/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java @@ -18,6 +18,8 @@ import java.security.PublicKey; /** + * The JWK parallel of a Java {@link PublicKey}. + * * @since JJWT_RELEASE_VERSION */ public interface PublicJwk extends AsymmetricJwk { diff --git a/api/src/main/java/io/jsonwebtoken/security/SecurityRequest.java b/api/src/main/java/io/jsonwebtoken/security/Request.java similarity index 61% rename from api/src/main/java/io/jsonwebtoken/security/SecurityRequest.java rename to api/src/main/java/io/jsonwebtoken/security/Request.java index 0da0b1d98..f618049e3 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecurityRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/Request.java @@ -19,9 +19,22 @@ import java.security.SecureRandom; /** + * A {@code Request} aggregates various parameters that may be used by a particular cryptographic algorithm. It and + * any of its subtypes implemented as a single object submitted to an algorithm effectively reflect the + * Parameter Object design pattern. This + * provides for a much cleaner request/result algorithm API instead of polluting the API with an excessive number of + * overloaded methods that would exist otherwise. + * + *

    The {@code Request} interface specifically allows for JCA {@link Provider} and {@link SecureRandom} instances + * to be used during request execution, which allows more flexibility than forcing a single {@code Provider} or + * {@code SecureRandom} for all executions. {@code Request} subtypes provide additional parameters as necessary + * depending on the type of cryptographic algorithm invoked.

    + * + * @see #getProvider() + * @see #getSecureRandom() * @since JJWT_RELEASE_VERSION */ -public interface SecurityRequest { +public interface Request { /** * Returns the JCA provider that should be used for cryptographic operations during the request or diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java index 8e2ac2727..a34fa7a51 100644 --- a/api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java @@ -20,7 +20,11 @@ import java.security.interfaces.RSAKey; /** + * A {@link KeyAlgorithm} that produces JWE Encrypted Keys via RSA cryptography. + * + * @param the type of RSA public key used to obtain the AEAD encryption key + * @param the type of RSA private key used to obtain the AEAD decryption key * @since JJWT_RELEASE_VERSION */ -public interface RsaKeyAlgorithm extends KeyAlgorithm { +public interface RsaKeyAlgorithm extends KeyAlgorithm { } diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java index 2d14c792e..c13b05cec 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java @@ -3,6 +3,8 @@ import javax.crypto.SecretKey; /** + * A {@link KeyAlgorithm} that uses symmetric {@link SecretKey}s to obtain AEAD encryption and decryption keys. + * * @since JJWT_RELEASE_VERSION */ public interface SecretKeyAlgorithm extends KeyAlgorithm, KeyBuilderSupplier, KeyLengthSupplier { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/IdRegistry.java b/impl/src/main/java/io/jsonwebtoken/impl/IdRegistry.java index b39072a87..df6820f12 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/IdRegistry.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/IdRegistry.java @@ -17,7 +17,7 @@ public IdRegistry(Collection instances) { Assert.notEmpty(instances, "Collection of Identifiable instances may not be null or empty."); Map m = new LinkedHashMap<>(instances.size()); for (T instance : instances) { - String id = Assert.hasText(Strings.clean(instance.getId()), "All Identifiable instances within the collection cannot be null or empty."); + String id = Assert.hasText(Strings.clean(instance.getId()), "All Identifiable instances within the collection cannot have a null or empty id."); m.put(id, instance); } this.INSTANCES = java.util.Collections.unmodifiableMap(m); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 848a13492..388980f63 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -16,7 +16,9 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Classes; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; @@ -28,18 +30,28 @@ public class JwtMap implements Map { + private static final String GROOVY_PRESENCE_CLASS_NAME = "org.codehaus.groovy.runtime.InvokerHelper"; + private static final String GROOVY_PRESENCE_CLASS_METHOD_NAME = "formatMap"; + private static final boolean GROOVY_PRESENT = Classes.isAvailable(GROOVY_PRESENCE_CLASS_NAME); + static final String REDACTED_VALUE = ""; protected final Map values; // canonical values formatted per RFC requirements protected final Map idiomaticValues; // the values map with any RFC values converted to Java type-safe values where possible protected final Map redactedValues; // the values map with any sensitive/secret values redacted. Used in the toString implementation. protected final Map> FIELDS; + private final boolean hasSecretFields; public JwtMap(Set> fieldSet) { Assert.notEmpty(fieldSet, "Fields cannot be null or empty."); Map> fields = new LinkedHashMap<>(); + boolean hasSecretFields = false; for (Field field : fieldSet) { fields.put(field.getId(), field); + if (field.isSecret()) { + hasSecretFields = true; + } } + this.hasSecretFields = hasSecretFields; this.FIELDS = java.util.Collections.unmodifiableMap(fields); this.values = new LinkedHashMap<>(); this.idiomaticValues = new LinkedHashMap<>(); @@ -207,8 +219,32 @@ public Collection values() { return values.values(); } + // MAINTAINER'S NOTE: + // + // BE VERY CAREFUL about moving this method - it's exact location in this + // file ties it to its implementation per StackTrace depth expectations. + // + // This behavior (and it's stack depth) is asserted in the + // DefaultJwkContextTest.testGStringPrintsRedactedValues() test case. If you + // change the location of this method, you must update that test as well. + protected boolean preferRedactedEntrySet() { + // For better performance, only execute the groovy stack count if this instance has secret fields + // (otherwise, we don't need to worry about redaction) and Groovy is detected: + if (this.hasSecretFields && GROOVY_PRESENT) { + Throwable t = new Throwable(); + StackTraceElement[] elements = t.getStackTrace(); + Assert.gt(Arrays.length(elements), 2, "StackTraceElement array must be greater than 2."); + return GROOVY_PRESENCE_CLASS_NAME.equals(elements[2].getClassName()) && + GROOVY_PRESENCE_CLASS_METHOD_NAME.equals(elements[2].getMethodName()); + } + return false; + } + @Override public Set> entrySet() { + if (preferRedactedEntrySet()) { + return this.redactedValues.entrySet(); + } return values.entrySet(); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java index 65f74b816..511deaf8c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java @@ -47,7 +47,7 @@ protected K generateKey(final JwkContext ctx, final CheckedFunction T generateKey(final JwkContext ctx, final Class type, final CheckedFunction fn) { - return new JcaTemplate(getId(), ctx.getProvider()).execute(KeyFactory.class, new CheckedFunction() { + return new JcaTemplate(getId(), ctx.getProvider(), ctx.getRandom()).execute(KeyFactory.class, new CheckedFunction() { @Override public T apply(KeyFactory instance) { try { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java index 279dd7718..c14f607f2 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java @@ -10,6 +10,7 @@ import javax.crypto.SecretKey; import java.security.Key; import java.security.Provider; +import java.security.SecureRandom; import java.util.Map; import java.util.Set; @@ -20,11 +21,11 @@ abstract class AbstractJwkBuilder, T extends Jwk @SuppressWarnings("unchecked") protected AbstractJwkBuilder(JwkContext jwkContext) { - this(jwkContext, (JwkFactory)DispatchingJwkFactory.DEFAULT_INSTANCE); + this(jwkContext, (JwkFactory) DispatchingJwkFactory.DEFAULT_INSTANCE); } // visible for testing - protected AbstractJwkBuilder(JwkContext context, JwkFactory factory) { + protected AbstractJwkBuilder(JwkContext context, JwkFactory factory) { this.jwkContext = Assert.notNull(context, "JwkContext cannot be null."); this.jwkFactory = Assert.notNull(factory, "JwkFactory cannot be null."); } @@ -36,6 +37,13 @@ public T setProvider(Provider provider) { return tthis(); } + @Override + public T setRandom(SecureRandom random) { + Assert.notNull(random, "SecureRandom cannot be null."); + jwkContext.setRandom(random); + return tthis(); + } + @Override public T put(String name, Object value) { jwkContext.put(name, value); @@ -95,7 +103,7 @@ public J build() { } static class DefaultSecretJwkBuilder extends AbstractJwkBuilder - implements SecretJwkBuilder { + implements SecretJwkBuilder { public DefaultSecretJwkBuilder(JwkContext ctx, SecretKey key) { super(new DefaultJwkContext<>(DefaultSecretJwk.FIELDS, ctx, key)); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java index 323ec2dbb..d7b6b032e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java @@ -1,25 +1,24 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.KeyPair; import io.jsonwebtoken.security.PrivateJwk; import io.jsonwebtoken.security.PublicJwk; -import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; -abstract class AbstractPrivateJwk> - extends AbstractAsymmetricJwk implements PrivateJwk { +abstract class AbstractPrivateJwk> extends AbstractAsymmetricJwk implements PrivateJwk { private final M publicJwk; - private final KeyPair keyPair; + private final KeyPair keyPair; AbstractPrivateJwk(JwkContext ctx, M pubJwk) { super(ctx); this.publicJwk = Assert.notNull(pubJwk, "PublicJwk instance cannot be null."); L publicKey = Assert.notNull(pubJwk.toKey(), "PublicJwk key instance cannot be null."); this.context.setPublicKey(publicKey); - this.keyPair = new KeyPair(publicKey, toKey()); + this.keyPair = new DefaultKeyPair<>(publicKey, toKey()); } @Override @@ -28,7 +27,7 @@ public M toPublicJwk() { } @Override - public KeyPair toKeyPair() { + public KeyPair toKeyPair() { return this.keyPair; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java index c88a5c753..178041966 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java @@ -10,8 +10,8 @@ import io.jsonwebtoken.security.KeyBuilderSupplier; import io.jsonwebtoken.security.KeyLengthSupplier; import io.jsonwebtoken.security.KeySupplier; +import io.jsonwebtoken.security.Request; import io.jsonwebtoken.security.SecretKeyBuilder; -import io.jsonwebtoken.security.SecurityRequest; import io.jsonwebtoken.security.WeakKeyException; import javax.crypto.Cipher; @@ -130,7 +130,7 @@ byte[] assertDecryptionIv(InitializationVectorSupplier src) throws IllegalArgume return assertIvLength(iv); } - protected byte[] ensureInitializationVector(SecurityRequest request) { + protected byte[] ensureInitializationVector(Request request) { byte[] iv = null; if (request instanceof InitializationVectorSupplier) { iv = Arrays.clean(((InitializationVectorSupplier) request).getInitializationVector()); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java index 17925df38..668a3eae7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java @@ -5,8 +5,8 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.Request; import io.jsonwebtoken.security.SecretKeyBuilder; -import io.jsonwebtoken.security.SecurityRequest; import javax.crypto.SecretKey; import java.security.Provider; @@ -47,7 +47,7 @@ protected Provider getProvider() { return this.provider; } - SecureRandom ensureSecureRandom(SecurityRequest request) { + SecureRandom ensureSecureRandom(Request request) { SecureRandom random = request != null ? request.getSecureRandom() : null; return random != null ? random : Randoms.secureRandom(); } @@ -56,7 +56,7 @@ protected R execute(Class clazz, CheckedFunction fn) { return new JcaTemplate(getJcaName(), this.provider).execute(clazz, fn); } - protected Provider getProvider(SecurityRequest request) { + protected Provider getProvider(Request request) { Provider provider = request.getProvider(); if (provider == null) { provider = this.provider; // fallback, if any @@ -64,7 +64,7 @@ protected Provider getProvider(SecurityRequest request) { return provider; } - protected T execute(SecurityRequest request, Class clazz, CheckedFunction fn) { + protected T execute(Request request, Class clazz, CheckedFunction fn) { Assert.notNull(request, "request cannot be null."); Provider provider = getProvider(request); SecureRandom random = ensureSecureRandom(request); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java index cf0d6c10d..94050eadf 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java @@ -9,6 +9,7 @@ import java.security.Key; import java.security.Provider; import java.security.PublicKey; +import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.util.LinkedHashSet; import java.util.List; @@ -31,6 +32,8 @@ public class DefaultJwkContext extends JwtMap implements JwkConte private PublicKey publicKey; private Provider provider; + private SecureRandom random; + public DefaultJwkContext() { // For the default constructor case, we don't know how it will be used or what values will be populated, // so we can't know ahead of time what the sensitive data is. As such, for security reasons, we assume all @@ -59,6 +62,7 @@ private DefaultJwkContext(Set> fields, JwkContext other, boolean rem Assert.isInstanceOf(DefaultJwkContext.class, other, "JwkContext must be a DefaultJwkContext instance."); DefaultJwkContext src = (DefaultJwkContext) other; this.provider = other.getProvider(); + this.random = other.getRandom(); this.values.putAll(src.values); this.idiomaticValues.putAll(src.idiomaticValues); this.redactedValues.putAll(src.redactedValues); @@ -217,4 +221,15 @@ public JwkContext setProvider(Provider provider) { this.provider = provider; return this; } + + @Override + public SecureRandom getRandom() { + return this.random; + } + + @Override + public JwkContext setRandom(SecureRandom random) { + this.random = random; + return this; + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPair.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPair.java index 8841a3844..48c31b1f7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPair.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPair.java @@ -30,7 +30,7 @@ public B getPrivate() { } @Override - public java.security.KeyPair toJdkKeyPair() { + public java.security.KeyPair toJavaKeyPair() { return this.jdkPair; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyedRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyedRequest.java index b45431db4..f9252f456 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyedRequest.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyedRequest.java @@ -7,7 +7,7 @@ import java.security.Provider; import java.security.SecureRandom; -public class DefaultKeyedRequest extends DefaultSecurityRequest implements KeySupplier { +public class DefaultKeyedRequest extends DefaultRequest implements KeySupplier { private final K key; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecurityRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRequest.java similarity index 70% rename from impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecurityRequest.java rename to impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRequest.java index 5efaa08d2..c8dc1c458 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecurityRequest.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRequest.java @@ -1,16 +1,16 @@ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.security.SecurityRequest; +import io.jsonwebtoken.security.Request; import java.security.Provider; import java.security.SecureRandom; -public class DefaultSecurityRequest implements SecurityRequest { +public class DefaultRequest implements Request { private final Provider provider; private final SecureRandom secureRandom; - public DefaultSecurityRequest(Provider provider, SecureRandom secureRandom) { + public DefaultRequest(Provider provider, SecureRandom secureRandom) { this.provider = provider; this.secureRandom = secureRandom; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java index 8a1a1255d..526f8cf86 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java @@ -1,6 +1,5 @@ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.impl.IdRegistry; import io.jsonwebtoken.impl.lang.Registry; import io.jsonwebtoken.lang.Collections; @@ -37,11 +36,11 @@ public static AeadAlgorithm findById(String id) { return REGISTRY.apply(id); } - public static AeadAlgorithm forId(String id) { + public static AeadAlgorithm forId(String id) throws IllegalArgumentException { AeadAlgorithm alg = findById(id); if (alg == null) { String msg = "Unrecognized JWA AeadAlgorithm identifier: " + id; - throw new UnsupportedJwtException(msg); + throw new IllegalArgumentException(msg); } return alg; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java index 1724afe97..dd47d0ae1 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java @@ -6,6 +6,7 @@ import java.security.Key; import java.security.Provider; import java.security.PublicKey; +import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.util.List; import java.util.Map; @@ -58,4 +59,8 @@ public interface JwkContext extends Identifiable, Map setProvider(Provider provider); + + SecureRandom getRandom(); + + JwkContext setRandom(SecureRandom random); } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy index 8c3782176..ec7e696de 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy @@ -1,6 +1,6 @@ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.security.SecurityRequest +import io.jsonwebtoken.security.Request import org.junit.Test import java.security.Provider @@ -62,7 +62,7 @@ class CryptoAlgorithmTest { Provider defaultProvider = createMock(Provider) Provider requestProvider = createMock(Provider) - SecurityRequest request = createMock(SecurityRequest) + Request request = createMock(Request) alg.setProvider(defaultProvider) expect(request.getProvider()).andReturn(requestProvider) @@ -80,7 +80,7 @@ class CryptoAlgorithmTest { def alg = new TestCryptoAlgorithm('test', 'test') Provider defaultProvider = createMock(Provider) - SecurityRequest request = createMock(SecurityRequest) + Request request = createMock(Request) alg.setProvider(defaultProvider) expect(request.getProvider()).andReturn(null) @@ -95,7 +95,7 @@ class CryptoAlgorithmTest { @Test void testMissingRequestAndDefaultProviderReturnsNull() { def alg = new TestCryptoAlgorithm('test', 'test') - SecurityRequest request = createMock(SecurityRequest) + Request request = createMock(Request) expect(request.getProvider()).andReturn(null) replay request assertNull alg.getProvider(request) // null return value means use JCA internal default provider diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy index c350e7b93..e3e116a10 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy @@ -1,6 +1,5 @@ package io.jsonwebtoken.impl.security - import org.junit.Test import static org.junit.Assert.assertEquals @@ -19,4 +18,26 @@ class DefaultJwkContextTest { header.put('kty', 'oct') assertEquals 'Secret JWK', header.getName() } + + @Test + void testGStringPrintsRedactedValues() { + + // DO NOT REMOVE THIS METHOD: IT IS CRITICAL TO ENSURE GROOVY STRINGS DO NOT LEAK SECRET/PRIVATE KEY MATERIAL + // If you still believe it should be removed, discuss with the JJWT dev team first. + + def header = new DefaultJwkContext(DefaultSecretJwk.FIELDS) + header.put('kty', 'oct') + header.put('k', 'test') + String s = '[kty:oct, k:]' + assertEquals "$s", "$header" + } + + @Test + void testGStringToStringPrintsRedactedValues() { + def header = new DefaultJwkContext(DefaultSecretJwk.FIELDS) + header.put('kty', 'oct') + header.put('k', 'test') + String s = '{kty=oct, k=}' + assertEquals "$s", "${header.toString()}" + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index 6c55a2583..cd9dfbcd3 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -7,10 +7,7 @@ import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.SecretKey -import java.security.KeyPair -import java.security.MessageDigest -import java.security.PrivateKey -import java.security.PublicKey +import java.security.* import java.security.cert.X509Certificate import java.security.interfaces.ECKey import java.security.interfaces.ECPublicKey @@ -23,7 +20,7 @@ import static org.junit.Assert.* class JwksTest { private static final SecretKey SKEY = SignatureAlgorithms.HS256.keyBuilder().build() - private static final KeyPair EC_PAIR = SignatureAlgorithms.ES256.keyPairBuilder().build().toJdkKeyPair() + private static final KeyPair EC_PAIR = SignatureAlgorithms.ES256.keyPairBuilder().build().toJavaKeyPair() private static String srandom() { byte[] random = new byte[16]; @@ -146,6 +143,18 @@ class JwksTest { testThumbprint(256) } + @Test + void testRandom() { + def random = new SecureRandom() + def jwk = Jwks.builder().setKey(SKEY).setRandom(random).build() + assertSame random, jwk.@context.getRandom() + } + + @Test(expected = IllegalArgumentException) + void testNullRandom() { + Jwks.builder().setKey(SKEY).setRandom(null).build() + } + static void testThumbprint(int number) { def algs = SignatureAlgorithms.values().findAll {it instanceof AsymmetricKeySignatureAlgorithm} @@ -246,8 +255,8 @@ class JwksTest { // test pair privJwk = pub instanceof ECKey ? - Jwks.builder().setKeyPairEc(pair.toJdkKeyPair()).setPublicKeyUse("sig").build() : - Jwks.builder().setKeyPairRsa(pair.toJdkKeyPair()).setPublicKeyUse("sig").build() + Jwks.builder().setKeyPairEc(pair.toJavaKeyPair()).setPublicKeyUse("sig").build() : + Jwks.builder().setKeyPairRsa(pair.toJavaKeyPair()).setPublicKeyUse("sig").build() assertEquals priv, privJwk.toKey() privPubJwk = privJwk.toPublicJwk() assertEquals pubJwk, privPubJwk diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy index b0b9c2c90..cf08ca8e1 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy @@ -47,7 +47,7 @@ class KeyPairsTest { @Test void testGetKeyECMismatch() { - KeyPair pair = SignatureAlgorithms.RS256.keyPairBuilder().build().toJdkKeyPair() + KeyPair pair = SignatureAlgorithms.RS256.keyPairBuilder().build().toJavaKeyPair() Class clazz = ECPublicKey try { KeyPairs.getKey(pair, clazz) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy index 19efdbea9..e7675bc0b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy @@ -4,19 +4,15 @@ import io.jsonwebtoken.Jwe import io.jsonwebtoken.Jwts import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders -import io.jsonwebtoken.security.AeadAlgorithm -import io.jsonwebtoken.security.Jwks -import io.jsonwebtoken.security.KeyAlgorithms -import io.jsonwebtoken.security.SecretJwk -import io.jsonwebtoken.security.SecretKeyBuilder -import io.jsonwebtoken.security.SecurityRequest +import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec import java.nio.charset.StandardCharsets -import static org.junit.Assert.* +import static org.junit.Assert.assertArrayEquals +import static org.junit.Assert.assertEquals class RFC7516AppendixA3Test { @@ -108,7 +104,7 @@ class RFC7516AppendixA3Test { //ensure that the algorithm reflects the test harness values: AeadAlgorithm enc = new HmacAesAeadAlgorithm(128) { @Override - protected byte[] ensureInitializationVector(SecurityRequest request) { + protected byte[] ensureInitializationVector(Request request) { return IV } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy index 14bc60ed3..0066831c0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy @@ -275,7 +275,7 @@ class RFC7517AppendixCTest { } @Override - protected byte[] ensureInitializationVector(SecurityRequest request) { + protected byte[] ensureInitializationVector(Request request) { return RFC_IV } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy index adf2b3c1d..4fe9508e2 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy @@ -6,12 +6,7 @@ import io.jsonwebtoken.Jwts import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Deserializer -import io.jsonwebtoken.security.EcPrivateJwk -import io.jsonwebtoken.security.EncryptionAlgorithms -import io.jsonwebtoken.security.Jwks -import io.jsonwebtoken.security.KeyRequest -import io.jsonwebtoken.security.KeyResult -import io.jsonwebtoken.security.SecurityException +import io.jsonwebtoken.security.* import org.junit.Test import java.nio.charset.StandardCharsets @@ -89,7 +84,7 @@ class RFC7518AppendixCTest { //ensure keypair reflects required RFC test value: @Override protected KeyPair generateKeyPair(KeyRequest request, ECParameterSpec spec) { - return aliceJwk.toKeyPair() + return aliceJwk.toKeyPair().toJavaKeyPair() } @Override diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy index 20ce7e5c8..cc59fe35e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy @@ -1,6 +1,6 @@ package io.jsonwebtoken.security -import io.jsonwebtoken.UnsupportedJwtException + import io.jsonwebtoken.impl.security.DefaultAeadRequest import io.jsonwebtoken.impl.security.DefaultAeadResult import io.jsonwebtoken.impl.security.GcmAesAeadAlgorithm @@ -53,7 +53,7 @@ class EncryptionAlgorithmsTest { } } - @Test(expected = UnsupportedJwtException) + @Test(expected = IllegalArgumentException) void testForIdWithInvalidId() { //unlike the 'find' paradigm, 'for' requires the value to exist EncryptionAlgorithms.forId('invalid') From fb50c3ef843111095c1f6b8e1b179f793ef8ec10 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 11 May 2022 22:19:35 -0700 Subject: [PATCH 40/75] - JavaDoc additions cont'd --- .../io/jsonwebtoken/security/AeadResult.java | 7 +- .../jsonwebtoken/security/AsymmetricJwk.java | 4 +- .../security/AsymmetricJwkBuilder.java | 109 +++++++++++++- .../AsymmetricKeySignatureAlgorithm.java | 20 ++- .../jsonwebtoken/security/CryptoRequest.java | 3 +- .../jsonwebtoken/security/EcPrivateJwk.java | 5 +- .../security/EcPrivateJwkBuilder.java | 2 + .../io/jsonwebtoken/security/EcPublicJwk.java | 16 ++ .../security/EcPublicJwkBuilder.java | 2 + .../EllipticCurveSignatureAlgorithm.java | 9 +- .../InitializationVectorSupplier.java | 13 +- .../security/InvalidKeyException.java | 3 + .../java/io/jsonwebtoken/security/Jwk.java | 2 +- .../io/jsonwebtoken/security/JwkBuilder.java | 139 +++++++++++++++++- .../java/io/jsonwebtoken/security/Jwks.java | 4 + .../jsonwebtoken/security/KeyAlgorithms.java | 8 +- .../io/jsonwebtoken/security/KeyBuilder.java | 6 +- .../security/KeyBuilderSupplier.java | 1 + .../io/jsonwebtoken/security/PrivateJwk.java | 2 +- .../security/PrivateJwkBuilder.java | 28 +++- .../security/ProtoJwkBuilder.java | 108 +++++++++++++- .../io/jsonwebtoken/security/PublicJwk.java | 2 +- .../security/PublicJwkBuilder.java | 22 ++- .../jsonwebtoken/security/RsaPrivateJwk.java | 16 ++ .../jsonwebtoken/security/RsaPublicJwk.java | 16 ++ .../security/RsaPublicJwkBuilder.java | 2 + .../security/RsaSignatureAlgorithm.java | 9 +- .../io/jsonwebtoken/security/SecretJwk.java | 15 ++ .../security/SecretJwkBuilder.java | 2 + .../security/SecurityBuilder.java | 4 +- .../security/SignatureAlgorithms.java | 7 +- ...efaultEllipticCurveSignatureAlgorithm.java | 12 +- .../DefaultRsaSignatureAlgorithm.java | 16 +- 33 files changed, 557 insertions(+), 57 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadResult.java b/api/src/main/java/io/jsonwebtoken/security/AeadResult.java index e469cd332..cbb5e91cc 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AeadResult.java +++ b/api/src/main/java/io/jsonwebtoken/security/AeadResult.java @@ -16,8 +16,9 @@ package io.jsonwebtoken.security; /** - * The result of authenticated encryption, providing access to the resulting ciphertext, AAD tag, and initialization - * vector. The AAD tag and initialization vector must be supplied with the ciphertext to decrypt. + * The result of authenticated encryption, providing access to the resulting {@link #getContent() ciphertext}, + * {@link #getDigest() AAD tag}, and {@link #getInitializationVector() initialization vector}. The AAD tag and + * initialization vector must be supplied with the ciphertext to decrypt. * *

    AAD Tag

    * @@ -27,7 +28,7 @@ *

    Initialization Vector

    * * All JWE-standard AEAD algorithms use a secure-random Initialization Vector for safe ciphertext creation, so - * {@code AeadAlgorithm} inherits {@link InitializationVectorSupplier} to make the generated IV available after + * {@code AeadResult} inherits {@link InitializationVectorSupplier} to make the generated IV available after * encryption. This IV must in turn be supplied during decryption. * * @since JJWT_RELEASE_VERSION diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java index ce67ea687..7546a8414 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java @@ -21,7 +21,7 @@ import java.util.List; /** - * A JWK that represents an asymmetric (public or private) cryptographic key. + * JWK representation of an asymmetric (public or private) cryptographic key. * * @since JJWT_RELEASE_VERSION */ @@ -119,7 +119,7 @@ public interface AsymmetricJwk extends Jwk { * previous one. The key in the first certificate MUST match the public key represented by other * members of the JWK.

    * - * @return the JWK {@code x5u} value as a type-safe List<{@link X509Certificate}> or + * @return the JWK {@code x5c} value as a type-safe List<{@link X509Certificate}> or * {@code null} if not present. */ List getX509CertificateChain(); diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java index bfa1f3521..62cc29c08 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java @@ -19,17 +19,118 @@ import java.security.Key; import java.security.cert.X509Certificate; import java.util.List; +import java.util.Set; /** + * A {@link JwkBuilder} that builds asymmetric (public or private) JWKs. + * + * @param the type of Java key provided by the JWK. + * @param the type of asymmetric JWK created + * @param the type of the builder, for subtype method chaining * @since JJWT_RELEASE_VERSION */ -public interface AsymmetricJwkBuilder, T extends AsymmetricJwkBuilder> extends JwkBuilder { +public interface AsymmetricJwkBuilder, T extends AsymmetricJwkBuilder> + extends JwkBuilder { - T setPublicKeyUse(String use); + /** + * Sets the JWK + * {@code use} (Public Key Use) + * parameter value. {@code use} values are CaSe-SeNsItIvE. A {@code null} value will remove the property + * from the JWK. + * + *

    The JWK specification defines the + * following {@code use} values:

    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    JWK Key Use Values
    ValueKey Use
    {@code sig}signature
    {@code enc}encryption
    + * + *

    Other values MAY be used. For best interoperability with other applications however, it is + * recommended to use only the values above.

    + * + *

    When a key is used to wrap another key and a public key use designation for the first key is desired, the + * {@code enc} (encryption) key use value is used, since key wrapping is a kind of encryption. The + * {@code enc} value is also to be used for public keys used for key agreement operations.

    + * + *

    Public Key Use vs Key Operations

    + * + *

    Per + * JWK RFC 7517, Section 4.3, last paragraph, + * the {@code use} (Public Key Use) and {@link #setOperations(Set) key_ops (Key Operations)} members + * SHOULD NOT be used together; however, if both are used, the information they convey MUST be + * consistent. Applications should specify which of these members they use, if either is to be used by the + * application.

    + * + * @param use the JWK {@code use} value. + * @return the builder for method chaining. + * @throws IllegalArgumentException if the {@code use} value is {@code null} or empty. + */ + T setPublicKeyUse(String use) throws IllegalArgumentException; - T setX509CertificateChain(List chain); + /** + * Sets the JWK + * {@code x5c} (X.509 Certificate Chain) + * parameter value as a type-safe List<{@link X509Certificate}>. + * + *

    The certificate chain is a {@code List} of {@link X509Certificate}s. The certificate containing the + * key value MUST be the first in the list (at list index {@code 0}). This MAY be + * followed by additional certificates, with each subsequent certificate being the one used to certify the + * previous one. The key in the first certificate MUST match the public key represented by other + * members of the JWK.

    + * + * @param chain the JWK {@code x5c} value as a type-safe List<{@link X509Certificate}>. + * @return the builder for method chaining. + * @throws IllegalArgumentException if the {@code chain} is null or empty. + */ + T setX509CertificateChain(List chain) throws IllegalArgumentException; - T setX509Url(URI uri); + /** + * Sets the JWK + * {@code x5u} (X.509 URL) + * parameter value as a {@link URI} instance. A {@code null} value will remove the property from the JWK. + * + *

    The URI MUST refer to a resource for an X.509 public key certificate or certificate chain that + * conforms to RFC 5280 in PEM-encoded form, with + * each certificate delimited as specified in + * Section 6.1 of RFC 4945. + * The key in the first certificate MUST match the public key represented by other members of + * the JWK. The protocol used to acquire the resource MUST provide integrity protection; an HTTP GET + * request to retrieve the certificate MUST use + * HTTP over TLS; the identity of the server + * MUST be validated, as per + * Section 6 of RFC 6125. + * + *

    While there is no requirement that optional JWK members providing key usage, algorithm, or other + * information be present when the {@code x5u} member is used, doing so may improve interoperability for + * applications that do not handle + * PKIX certificates [RFC5280]. If other members + * are present, the contents of those members MUST be semantically consistent with the related fields + * in the first certificate. For instance, if the {@link #setPublicKeyUse(String) use (Public Key Use)} value is + * set, then it MUST correspond to the usage that is specified in the certificate, when it includes + * this information. Similarly, if the {@link #setAlgorithm(String) alg (Algorithm)} value is present, it + * MUST correspond to the algorithm specified in the certificate.

    + * + * @param uri the JWK {@code x5u} X.509 URL value as a {@link URI}. + * @return the builder for method chaining. + * @throws IllegalArgumentException if {@code uri} is {@code null}. + */ + T setX509Url(URI uri) throws IllegalArgumentException; T withX509KeyUse(boolean enable); diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java index 2e1b2cbb9..d41a2c219 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java @@ -19,8 +19,24 @@ import java.security.PublicKey; /** + * A {@link SignatureAlgorithm} that works with asymmetric keys. A {@link PrivateKey} is used to create + * signatures, and a {@link PublicKey} is used to verify signatures. + * + *

    Key Pair Generation

    + * + *

    {@code AsymmetricKeySignatureAlgorithm} extends {@link KeyPairBuilderSupplier} to enable + * {@link KeyPair} generation. Each {@code AsymmetricKeySignatureAlgorithm} instance will return a + * {@link KeyPairBuilder} that ensures any created key pairs will have a sufficient length and algorithm parameters + * required by that algorithm. For example:

    + * + *
    + * KeyPair pair = anAsymmetricKeySignatureAlgorithm.keyPairBuilder().build();
    + * + *

    The resulting {@code pair} is guaranteed to have the correct algorithm parameters and length/strength necessary + * for that exact {@code anAsymmetricKeySignatureAlgorithm} instance.

    + * * @since JJWT_RELEASE_VERSION */ -public interface AsymmetricKeySignatureAlgorithm - extends SignatureAlgorithm, KeyPairBuilderSupplier { +public interface AsymmetricKeySignatureAlgorithm + extends SignatureAlgorithm, KeyPairBuilderSupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java b/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java index eee6ea6b9..05f648469 100644 --- a/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/CryptoRequest.java @@ -18,8 +18,9 @@ import java.security.Key; /** - * A request to a cryptographic algorithm that requires a {@link Key}. + * A request to a cryptographic algorithm requiring a {@link Key}. * + * @param they type of key used by the algorithm during the request * @since JJWT_RELEASE_VERSION */ public interface CryptoRequest extends Message, Request, KeySupplier { diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java index d95a99af0..10a0e185b 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java @@ -19,7 +19,9 @@ import java.security.interfaces.ECPublicKey; /** - * The JWK parallel of a Java {@link ECPrivateKey}. + * JWK representation of an {@link ECPrivateKey} as defined by the JWA (RFC 7518) specification sections on + * Parameters for Elliptic Curve Keys and + * Parameters for Elliptic Curve Private Keys. * *

    Note that the various EC-specific properties are not available as separate dedicated getter methods, as most Java * applications should rarely, if ever, need to access these individual key properties since they typically represent @@ -33,7 +35,6 @@ *

  • Via the various getter methods on the {@link ECPrivateKey} instance returned by {@link #toKey()}.
  • * * - * the {@link #get(Object) get} method

    * @since JJWT_RELEASE_VERSION */ public interface EcPrivateJwk extends PrivateJwk { diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwkBuilder.java index fd9f4d60a..07d91f448 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwkBuilder.java @@ -19,6 +19,8 @@ import java.security.interfaces.ECPublicKey; /** + * A {@link PrivateJwkBuilder} that creates {@link EcPrivateJwk}s. + * * @since JJWT_RELEASE_VERSION */ public interface EcPrivateJwkBuilder extends PrivateJwkBuilder { diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java b/api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java index fb51aac29..4806bd756 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java @@ -18,6 +18,22 @@ import java.security.interfaces.ECPublicKey; /** + * JWK representation of an {@link ECPublicKey} as defined by the JWA (RFC 7518) specification sections on + * Parameters for Elliptic Curve Keys and + * Parameters for Elliptic Curve Public Keys. + * + *

    Note that the various EC-specific properties are not available as separate dedicated getter methods, as most Java + * applications should rarely, if ever, need to access these individual key properties since they typically represent + * internal key material and/or implementation details.

    + * + *

    Even so, because they exist and are readable by nature of every JWK being a {@link java.util.Map Map}, the + * properties are still accessible in two different ways:

    + *
      + *
    • Via the standard {@code Map} {@link #get(Object) get} method using an appropriate JWK parameter id, + * e.g. {@code jwk.get("x")}, {@code jwk.get("y")}, etc.
    • + *
    • Via the various getter methods on the {@link ECPublicKey} instance returned by {@link #toKey()}.
    • + *
    + * * @since JJWT_RELEASE_VERSION */ public interface EcPublicJwk extends PublicJwk { diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPublicJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/EcPublicJwkBuilder.java index 30de09f38..4a0ce8872 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EcPublicJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/EcPublicJwkBuilder.java @@ -19,6 +19,8 @@ import java.security.interfaces.ECPublicKey; /** + * A {@link PublicJwkBuilder} that creates {@link EcPublicJwk}s. + * * @since JJWT_RELEASE_VERSION */ public interface EcPublicJwkBuilder extends PublicJwkBuilder { diff --git a/api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java index db636524a..69034ed08 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java @@ -20,8 +20,13 @@ import java.security.interfaces.ECKey; /** + * An {@link AsymmetricKeySignatureAlgorithm} that uses Elliptic Curve private keys to create signatures, and + * Elliptic Curve public keys to verify signatures. + * + * @param The type of Elliptic Curve private key used to create signatures + * @param The type of Elliptic Curve public key used to verify signatures * @since JJWT_RELEASE_VERSION */ -public interface EllipticCurveSignatureAlgorithm - extends AsymmetricKeySignatureAlgorithm { +public interface EllipticCurveSignatureAlgorithm + extends AsymmetricKeySignatureAlgorithm { } diff --git a/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSupplier.java b/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSupplier.java index b0b397e5a..22294e5a3 100644 --- a/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSupplier.java +++ b/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSupplier.java @@ -16,16 +16,21 @@ package io.jsonwebtoken.security; /** + * An {@code InitializationVectorSupplier} provides access to the secure-random Initialization Vector used during + * encryption, which must in turn be presented for use during decryption. To maintain the security integrity of cryptographic + * algorithms, a new secure-random Initialization Vector MUST be generated for every individual + * encryption attempt. + * * @since JJWT_RELEASE_VERSION */ public interface InitializationVectorSupplier { /** - * Returns the secure-random initialization vector used during encryption that must be presented in order - * to decrypt. + * Returns the secure-random Initialization Vector used during encryption, which must in turn be presented for + * use during decryption. * - * @return the secure-random initialization vector used during encryption that must be presented in order - * to decrypt. + * @return the secure-random Initialization Vector used during encryption, which must in turn be presented for + * use during decryption. */ byte[] getInitializationVector(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java b/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java index 6fd7a585d..ae001f896 100644 --- a/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java @@ -16,6 +16,9 @@ package io.jsonwebtoken.security; /** + * A {@code KeyException} thrown when encountering a key that is not suitable for the required functionality, or + * when attempting to use a Key in an incorrect or prohibited manner. + * * @since 0.10.0 */ public class InvalidKeyException extends KeyException { diff --git a/api/src/main/java/io/jsonwebtoken/security/Jwk.java b/api/src/main/java/io/jsonwebtoken/security/Jwk.java index 2a046118b..f6a2af252 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Jwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/Jwk.java @@ -159,7 +159,7 @@ public interface Jwk extends Identifiable, Map { * {@code sign} with {@code verify}, {@code encrypt} with {@code decrypt}, and {@code wrapKey} with * {@code unwrapKey} are permitted, but other combinations SHOULD NOT be used.

    * - * @return the JWK {@code alg} value or {@code null} if not present. + * @return the JWK {@code key_ops} value or {@code null} if not present. */ Set getOperations(); diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java index 7a235f27a..3a7b60641 100644 --- a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java @@ -20,17 +20,148 @@ import java.util.Set; /** + * A {@link SecurityBuilder} that produces a JWK. A JWK is an immutable set of name/value pairs that represent a + * cryptographic key as defined by + * RFC 7517: JSON Web Key (JWK). The {@code Jwk}. + * The {@code JwkBuilder} interface represents common JWK properties that may be specified for any type of JWK. + * Builder subtypes support additional JWK properties specific to different types of cryptographic keys + * (e.g. Secret, Asymmetric, RSA, Elliptic Curve, etc).

    + * + * @see SecretJwkBuilder + * @see RsaPublicJwkBuilder + * @see RsaPrivateJwkBuilder + * @see EcPublicJwkBuilder + * @see EcPrivateJwkBuilder * @since JJWT_RELEASE_VERSION */ public interface JwkBuilder, T extends JwkBuilder> extends SecurityBuilder { + /** + * Set a single JWK property by name. If the {@code value} is {@code null}, an empty + * {@link java.util.Collection}, or an empty {@link Map}, the property will be removed from the JWK. + * + * @param name the name of the JWK property + * @param value the value to set for the property name + * @return the builder for method chaining. + */ T put(String name, Object value); - T putAll(Map values); + /** + * Sets one or more JWK properties by name. If any {@code name} has a {@code value} that is {@code null}, + * an empty {@link java.util.Collection}, or an empty {@link Map}, the property will be removed from the JWK. + * + * @param values one or more name/value pairs to set on the JWK. + * @return the builder for method chaining. + * @throws IllegalArgumentException if {@code values} is {@code null} or empty. + */ + T putAll(Map values) throws IllegalArgumentException; - T setAlgorithm(String alg); + /** + * Sets the JWK {@code alg} (Algorithm) + * Parameter. + * + *

    The {@code alg} (algorithm) parameter identifies the algorithm intended for use with the key. The + * value specified should either be one of the values in the IANA + * JSON Web Signature and Encryption + * Algorithms registry or be a value that contains a {@code Collision-Resistant Name}. The {@code alg} + * must be a CaSe-SeNsItIvE ASCII string.

    + * + * @param alg the JWK {@code alg} value. + * @return the builder for method chaining. + * @throws IllegalArgumentException if {@code alg} is {@code null} or empty. + */ + T setAlgorithm(String alg) throws IllegalArgumentException; - T setId(String id); + /** + * Sets the JWK {@code kid} (Key ID) + * Parameter. + * + *

    The {@code kid} (key ID) parameter is used to match a specific key. This is used, for instance, + * to choose among a set of keys within a {@code JWK Set} during key rollover. The structure of the + * {@code kid} value is unspecified. When {@code kid} values are used within a JWK Set, different keys + * within the {@code JWK Set} SHOULD use distinct {@code kid} values. (One example in which + * different keys might use the same {@code kid} value is if they have different {@code kty} (key type) + * values but are considered to be equivalent alternatives by the application using them.)

    + * + *

    The {@code kid} value is a CaSe-SeNsItIvE string, and it is optional. When used with JWS or JWE, + * the {@code kid} value is used to match a JWS or JWE {@code kid} Header Parameter value.

    + * + * @param kid the JWK {@code kid} value. + * @return the builder for method chaining. + * @throws IllegalArgumentException if the argument is {@code null} or empty. + */ + T setId(String kid) throws IllegalArgumentException; - T setOperations(Set ops); + /** + * Sets the JWK {@code key_ops} + * (Key Operations) Parameter values. + * + *

    The {@code key_ops} (key operations) parameter identifies the operation(s) for which the key is + * intended to be used. The {@code key_ops} parameter is intended for use cases in which public, + * private, or symmetric keys may be present.

    + * + *

    The JWK specification defines the + * following values:

    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    JWK Key Operations
    ValueOperation
    {@code sign}compute digital signatures or MAC
    {@code verify}verify digital signatures or MAC
    {@code encrypt}encrypt content
    {@code decrypt}decrypt content and validate decryption, if applicable
    {@code wrapKey}encrypt key
    {@code unwrapKey}decrypt key and validate decryption, if applicable
    {@code deriveKey}derive key
    {@code deriveBits}derive bits not to be used as a key
    + * + *

    (Note that {@code key_ops} values intentionally match the {@code KeyUsage} values defined in the + * Web Cryptography API specification.)

    + * + *

    Other values MAY be used. For best interoperability with other applications however, it is + * recommended to use only the values above. Each value is a CaSe-SeNsItIvE string. Use of the + * {@code key_ops} member is OPTIONAL, unless the application requires its presence.

    + * + *

    Multiple unrelated key operations SHOULD NOT be specified for a key because of the potential + * vulnerabilities associated with using the same key with multiple algorithms. Thus, the combinations + * {@code sign} with {@code verify}, {@code encrypt} with {@code decrypt}, and {@code wrapKey} with + * {@code unwrapKey} are permitted, but other combinations SHOULD NOT be used.

    + * + * @param ops the JWK {@code key_ops} value set. + * @return the builder for method chaining. + * @throws IllegalArgumentException if {@code ops} is {@code null} or empty. + */ + T setOperations(Set ops) throws IllegalArgumentException; } diff --git a/api/src/main/java/io/jsonwebtoken/security/Jwks.java b/api/src/main/java/io/jsonwebtoken/security/Jwks.java index 1f14530f2..b6d4a308b 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Jwks.java +++ b/api/src/main/java/io/jsonwebtoken/security/Jwks.java @@ -18,6 +18,10 @@ import io.jsonwebtoken.lang.Classes; /** + * Utility methods for creating + * JWKs (JSON Web Keys) with a type-safe builder. + * + * @see #builder() * @since JJWT_RELEASE_VERSION */ public class Jwks { diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index 2a4bb27e6..f41f938c9 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -22,6 +22,12 @@ import java.util.Collection; /** + * Constant definitions and utility methods for all + * JWA (RFC 7518) Key Management Algorithms. + * + * @see #values() + * @see #findById(String) + * @see #forId(String) * @since JJWT_RELEASE_VERSION */ @SuppressWarnings("rawtypes") @@ -34,7 +40,7 @@ private KeyAlgorithms() { private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.KeyAlgorithmsBridge"; private static final Class BRIDGE_CLASS = Classes.forName(BRIDGE_CLASSNAME); private static final Class[] ID_ARG_TYPES = new Class[]{String.class}; - private static final Class[] ESTIMATE_ITERATIONS_ARG_TYPES = new Class[]{KeyAlgorithm.class, long.class}; + //private static final Class[] ESTIMATE_ITERATIONS_ARG_TYPES = new Class[]{KeyAlgorithm.class, long.class}; public static Collection> values() { return Classes.invokeStatic(BRIDGE_CLASS, "values", null, (Object[]) null); diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java b/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java index 7912af50e..1a53dadef 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java @@ -10,9 +10,9 @@ *

    {@code KeyBuilder}s are provided by components that implement the {@link KeyBuilderSupplier} interface, * ensuring the resulting {@link SecretKey}s are compatible with their associated cryptographic algorithm.

    * - * @param the type of public key found within newly-created {@link KeyPair}s. - * @param the type of private key found within newly-created {@link KeyPair}s. - * @see KeyPairBuilderSupplier + * @param the type of key to build + * @param the type of the builder, for subtype method chaining + * @see KeyBuilderSupplier * @since JJWT_RELEASE_VERSION */ public interface KeyBuilder> extends SecurityBuilder { diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyBuilderSupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeyBuilderSupplier.java index f47ae4c32..29c1e53d2 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyBuilderSupplier.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyBuilderSupplier.java @@ -24,6 +24,7 @@ * @param type of {@link Key} created by the builder * @param type of builder to create each time {@link #keyBuilder()} is called. * @see #keyBuilder() + * @see KeyBuilder * @since JJWT_RELEASE_VERSION */ public interface KeyBuilderSupplier> { diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java index 52d002cb6..6e936519e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java @@ -19,7 +19,7 @@ import java.security.PublicKey; /** - * The JWK parallel of a Java {@link PrivateKey}. + * JWK representation of a {@link PrivateKey}. * *

    JWK Private Key vs Java {@code PrivateKey} differences

    * diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PrivateJwkBuilder.java index 4b2646980..ebfd9d673 100644 --- a/api/src/main/java/io/jsonwebtoken/security/PrivateJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateJwkBuilder.java @@ -19,11 +19,35 @@ import java.security.PublicKey; /** + * An {@link AsymmetricJwkBuilder} that creates private JWKs. + * + * @param the type of Java {@link PrivateKey} provided by the created private JWK. + * @param the type of Java {@link PublicKey} paired with the private key. + * @param the type of {@link PrivateJwk} created + * @param the type of {@link PublicJwk} paired with the created private JWK. + * @param the type of the builder, for subtype method chaining + * @see #setPublicKey(PublicKey) * @since JJWT_RELEASE_VERSION */ public interface PrivateJwkBuilder, M extends PrivateJwk, - T extends PrivateJwkBuilder> extends AsymmetricJwkBuilder { + J extends PublicJwk, M extends PrivateJwk, + T extends PrivateJwkBuilder> extends AsymmetricJwkBuilder { + /** + * Allows specifying of the {@link PublicKey} associated with the builder's existing {@link PrivateKey}, + * offering a reasonable performance enhancement when building the final private JWK. Application developers + * should prefer to use this method when possible when building private JWKs. + * + *

    As discussed in the {@link PrivateJwk} documentation, the JWK and JWA specifications require private JWKs to + * contain both private key and public key data. If a public key is not provided via this + * {@code setPublicKey} method, the builder implementation must go through the work to derive the + * {@code PublicKey} instance based on the {@code PrivateKey} to obtain the necessary public key information.

    + * + *

    Calling this method with the {@code PrivateKey}'s matching {@code PublicKey} instance eliminates the need + * for the builder to do that work.

    + * + * @param publicKey the {@link PublicKey} that matches the builder's existing {@link PrivateKey}. + * @return the builder for method chaining. + */ T setPublicKey(L publicKey); } diff --git a/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java index 129aafb1e..d87de8d7a 100644 --- a/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java @@ -18,6 +18,7 @@ import javax.crypto.SecretKey; import java.security.Key; import java.security.KeyPair; +import java.security.PublicKey; import java.security.cert.X509Certificate; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; @@ -26,29 +27,130 @@ import java.util.List; /** + * A prototypical {@link JwkBuilder} that coerces to a more type-specific builder based on the {@link Key} that will + * be represented as a JWK. + * * @since JJWT_RELEASE_VERSION */ public interface ProtoJwkBuilder, T extends JwkBuilder> extends JwkBuilder { + /** + * Ensures the builder will create a {@link SecretJwk} for the specified Java {@link SecretKey}. + * + * @param key the {@link SecretKey} to represent as a {@link SecretJwk}. + * @return the builder coerced as a {@link SecretJwkBuilder}. + */ SecretJwkBuilder setKey(SecretKey key); + /** + * Ensures the builder will create an {@link RsaPublicJwk} for the specified Java {@link RSAPublicKey}. + * + * @param key the {@link RSAPublicKey} to represent as a {@link RsaPublicJwk}. + * @return the builder coerced as an {@link RsaPublicJwkBuilder}. + */ RsaPublicJwkBuilder setKey(RSAPublicKey key); + /** + * Ensures the builder will create an {@link RsaPublicJwk} for the specified Java {@link X509Certificate} chain. + * The first {@code X509Certificate} in the chain (at array index 0) MUST contain an {@link RSAPublicKey} + * instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method. + * + * @param chain the {@link X509Certificate} chain to inspect to find the {@link RSAPublicKey} to represent as a + * {@link RsaPublicJwk}. + * @return the builder coerced as an {@link RsaPublicJwkBuilder}. + */ RsaPublicJwkBuilder forRsaChain(X509Certificate... chain); - RsaPublicJwkBuilder forRsaChain(List x509CertificateChain); + /** + * Ensures the builder will create an {@link RsaPublicJwk} for the specified Java {@link X509Certificate} chain. + * The first {@code X509Certificate} in the chain (at list index 0) MUST contain an {@link RSAPublicKey} + * instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method. + * + * @param chain the {@link X509Certificate} chain to inspect to find the {@link RSAPublicKey} to represent as a + * {@link RsaPublicJwk}. + * @return the builder coerced as an {@link RsaPublicJwkBuilder}. + */ + RsaPublicJwkBuilder forRsaChain(List chain); + /** + * Ensures the builder will create an {@link RsaPrivateJwk} for the specified Java {@link RSAPrivateKey}. If + * possible, it is recommended to also call the resulting builder's + * {@link RsaPrivateJwkBuilder#setPublicKey(PublicKey) setPublicKey} method with the private key's matching + * {@link PublicKey} for better performance. See the + * {@link RsaPrivateJwkBuilder#setPublicKey(PublicKey) setPublicKey} and {@link PrivateJwk} JavaDoc for more + * information. + * + * @param key the {@link RSAPublicKey} to represent as a {@link RsaPublicJwk}. + * @return the builder coerced as an {@link RsaPrivateJwkBuilder}. + */ RsaPrivateJwkBuilder setKey(RSAPrivateKey key); + /** + * Ensures the builder will create an {@link EcPublicJwk} for the specified Java {@link ECPublicKey}. + * + * @param key the {@link ECPublicKey} to represent as a {@link EcPublicJwk}. + * @return the builder coerced as an {@link EcPublicJwkBuilder}. + */ EcPublicJwkBuilder setKey(ECPublicKey key); + /** + * Ensures the builder will create an {@link EcPublicJwk} for the specified Java {@link X509Certificate} chain. + * The first {@code X509Certificate} in the chain (at array index 0) MUST contain an {@link ECPublicKey} + * instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method. + * + * @param chain the {@link X509Certificate} chain to inspect to find the {@link ECPublicKey} to represent as a + * {@link EcPublicJwk}. + * @return the builder coerced as an {@link EcPublicJwkBuilder}. + */ EcPublicJwkBuilder forEcChain(X509Certificate... chain); + /** + * Ensures the builder will create an {@link EcPublicJwk} for the specified Java {@link X509Certificate} chain. + * The first {@code X509Certificate} in the chain (at list index 0) MUST contain an {@link ECPublicKey} + * instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method. + * + * @param chain the {@link X509Certificate} chain to inspect to find the {@link ECPublicKey} to represent as a + * {@link EcPublicJwk}. + * @return the builder coerced as an {@link EcPublicJwkBuilder}. + */ EcPublicJwkBuilder forEcChain(List chain); + /** + * Ensures the builder will create an {@link EcPrivateJwk} for the specified Java {@link ECPrivateKey}. If + * possible, it is recommended to also call the resulting builder's + * {@link EcPrivateJwkBuilder#setPublicKey(PublicKey) setPublicKey} method with the private key's matching + * {@link PublicKey} for better performance. See the + * {@link EcPrivateJwkBuilder#setPublicKey(PublicKey) setPublicKey} and {@link PrivateJwk} JavaDoc for more + * information. + * + * @param key the {@link ECPublicKey} to represent as an {@link EcPublicJwk}. + * @return the builder coerced as a {@link EcPrivateJwkBuilder}. + */ EcPrivateJwkBuilder setKey(ECPrivateKey key); - RsaPrivateJwkBuilder setKeyPairRsa(KeyPair keyPair); + /** + * Ensures the builder will create an {@link RsaPrivateJwk} for the specified Java RSA + * {@link KeyPair}. The pair's {@link KeyPair#getPublic() public key} MUST be an + * {@link RSAPublicKey} instance. The pair's {@link KeyPair#getPrivate() private key} MUST be an + * {@link RSAPrivateKey} instance. + * + * @param keyPair the RSA {@link KeyPair} to represent as an {@link RsaPrivateJwk}. + * @return the builder coerced as an {@link RsaPrivateJwkBuilder}. + * @throws IllegalArgumentException if the {@code keyPair} does not contain {@link RSAPublicKey} and + * {@link RSAPrivateKey} instances. + */ + RsaPrivateJwkBuilder setKeyPairRsa(KeyPair keyPair) throws IllegalArgumentException; - EcPrivateJwkBuilder setKeyPairEc(KeyPair keyPair); + /** + * Ensures the builder will create an {@link EcPrivateJwk} for the specified Java Elliptic Curve + * {@link KeyPair}. The pair's {@link KeyPair#getPublic() public key} MUST be an + * {@link ECPublicKey} instance. The pair's {@link KeyPair#getPrivate() private key} MUST be an + * {@link ECPrivateKey} instance. + * + * @param keyPair the EC {@link KeyPair} to represent as an {@link EcPrivateJwk}. + * @return the builder coerced as an {@link EcPrivateJwkBuilder}. + * @throws IllegalArgumentException if the {@code keyPair} does not contain {@link ECPublicKey} and + * {@link ECPrivateKey} instances. + */ + EcPrivateJwkBuilder setKeyPairEc(KeyPair keyPair) throws IllegalArgumentException; } diff --git a/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java b/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java index 425fb5749..5e8e086da 100644 --- a/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java @@ -18,7 +18,7 @@ import java.security.PublicKey; /** - * The JWK parallel of a Java {@link PublicKey}. + * JWK representation of a {@link PublicKey}. * * @since JJWT_RELEASE_VERSION */ diff --git a/api/src/main/java/io/jsonwebtoken/security/PublicJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PublicJwkBuilder.java index 69a2b84fb..ce9b34d5d 100644 --- a/api/src/main/java/io/jsonwebtoken/security/PublicJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/PublicJwkBuilder.java @@ -19,9 +19,29 @@ import java.security.PublicKey; /** + * An {@link AsymmetricJwkBuilder} that creates private JWKs. + * + * @param the type of {@link PublicKey} provided by the created public JWK. + * @param the type of {@link PrivateKey} that may be paired with the {@link PublicKey} to produce a {@link PrivateJwk} if desired. + * @param the type of {@link PublicJwk} created + * @param the type of {@link PrivateJwk} that matches the created {@link PublicJwk} + * @param

    Note that the various RSA-specific properties are not available as separate dedicated getter methods, as most Java + * applications should rarely, if ever, need to access these individual key properties since they typically represent + * internal key material and/or implementation details.

    + * + *

    Even so, because they exist and are readable by nature of every JWK being a {@link java.util.Map Map}, the + * properties are still accessible in two different ways:

    + *
      + *
    • Via the standard {@code Map} {@link #get(Object) get} method using an appropriate JWK parameter id, + * e.g. {@code jwk.get("n")}, {@code jwk.get("e")}, etc.
    • + *
    • Via the various getter methods on the {@link RSAPrivateKey} instance returned by {@link #toKey()}.
    • + *
    + * * @since JJWT_RELEASE_VERSION */ public interface RsaPrivateJwk extends PrivateJwk { diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java b/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java index 093a345f9..9fa9f100a 100644 --- a/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java @@ -18,6 +18,22 @@ import java.security.interfaces.RSAPublicKey; /** + * JWK representation of an {@link RSAPublicKey} as defined by the JWA (RFC 7518) specification sections on + * Parameters for RSA Keys and + * Parameters for RSA Public Keys. + * + *

    Note that the various RSA-specific properties are not available as separate dedicated getter methods, as most Java + * applications should rarely, if ever, need to access these individual key properties since they typically represent + * internal key material and/or implementation details.

    + * + *

    Even so, because they exist and are readable by nature of every JWK being a {@link java.util.Map Map}, the + * properties are still accessible in two different ways:

    + *
      + *
    • Via the standard {@code Map} {@link #get(Object) get} method using an appropriate JWK parameter id, + * e.g. {@code jwk.get("n")}, {@code jwk.get("e")}, etc.
    • + *
    • Via the various getter methods on the {@link RSAPublicKey} instance returned by {@link #toKey()}.
    • + *
    + * * @since JJWT_RELEASE_VERSION */ public interface RsaPublicJwk extends PublicJwk { diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwkBuilder.java index d850c5725..d0fbfddc5 100644 --- a/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwkBuilder.java @@ -19,6 +19,8 @@ import java.security.interfaces.RSAPublicKey; /** + * A {@link PublicJwkBuilder} that creates {@link RsaPublicJwk}s. + * * @since JJWT_RELEASE_VERSION */ public interface RsaPublicJwkBuilder extends PublicJwkBuilder { diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java index 52598546c..3c6ea64e8 100644 --- a/api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java @@ -20,8 +20,13 @@ import java.security.interfaces.RSAKey; /** + * An {@link AsymmetricKeySignatureAlgorithm} that uses RSA private keys to create signatures, and + * RSA public keys to verify signatures. + * + * @param The type of RSA private key used to create signatures + * @param The type of RSA public key used to verify signatures * @since JJWT_RELEASE_VERSION */ -public interface RsaSignatureAlgorithm - extends AsymmetricKeySignatureAlgorithm { +public interface RsaSignatureAlgorithm + extends AsymmetricKeySignatureAlgorithm { } diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretJwk.java b/api/src/main/java/io/jsonwebtoken/security/SecretJwk.java index 862d6c39a..019768b2c 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecretJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecretJwk.java @@ -18,6 +18,21 @@ import javax.crypto.SecretKey; /** + * JWK representation of a {@link SecretKey} as defined by the JWA (RFC 7518) specification section on + * Parameters for Symmetric Keys. + * + *

    Note that the {@code SecretKey}-specific properties are not available as separate dedicated getter methods, as + * most Java applications should rarely, if ever, need to access these individual key properties since they typically + * represent internal key material and/or implementation details.

    + * + *

    Even so, because they exist and are readable by nature of every JWK being a {@link java.util.Map Map}, the + * properties are still accessible in two different ways:

    + *
      + *
    • Via the standard {@code Map} {@link #get(Object) get} method using an appropriate JWK parameter id, + * e.g. {@code jwk.get("k")}.
    • + *
    • Via the various getter methods on the {@link SecretKey} instance returned by {@link #toKey()}.
    • + *
    + * * @since JJWT_RELEASE_VERSION */ public interface SecretJwk extends Jwk { diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/SecretJwkBuilder.java index a3d737ab8..e9ada6d9e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecretJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecretJwkBuilder.java @@ -18,6 +18,8 @@ import javax.crypto.SecretKey; /** + * A {@link JwkBuilder} that creates {@link SecretJwk}s. + * * @since JJWT_RELEASE_VERSION */ public interface SecretJwkBuilder extends JwkBuilder { diff --git a/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java b/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java index 51f3beead..17fdd9030 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java @@ -6,8 +6,8 @@ import java.security.SecureRandom; /** - * A Security-specific {@link Builder} that allows configuration of common JCA API parameters, such as a - * {@link java.security.Provider} or {@link java.security.SecureRandom}. + * A Security-specific {@link Builder} that allows configuration of common JCA API parameters that might be used + * during instance creation, such as a {@link java.security.Provider} or {@link java.security.SecureRandom}. * * @param The type of object that will be created each time {@link #build()} is invoked. * @see #setProvider(Provider) diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java index 8c4fb8565..5811af7fe 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java @@ -26,6 +26,9 @@ import java.util.Collection; /** + * Constant definitions and utility methods for all + * JWA (RFC 7518) Signature Algorithms. + * * @since JJWT_RELEASE_VERSION */ @SuppressWarnings({"rawtypes", "JavadocLinkAsPlainText"}) @@ -39,7 +42,7 @@ private SignatureAlgorithms() { private static final Class BRIDGE_CLASS = Classes.forName(BRIDGE_CLASSNAME); private static final Class[] ID_ARG_TYPES = new Class[]{String.class}; - public static Collection> values() { + public static Collection> values() { return Classes.invokeStatic(BRIDGE_CLASS, "values", null, (Object[]) null); } @@ -48,7 +51,7 @@ public static Collection> values() { return Classes.invokeStatic(BRIDGE_CLASS, "findById", ID_ARG_TYPES, id); } - public static SignatureAlgorithm forId(String id) { + public static SignatureAlgorithm forId(String id) { return forId0(id); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java index 328e3727a..374893bdb 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java @@ -21,7 +21,9 @@ import java.text.MessageFormat; import java.util.Arrays; -public class DefaultEllipticCurveSignatureAlgorithm extends AbstractSignatureAlgorithm implements EllipticCurveSignatureAlgorithm { +// @since JJWT_RELEASE_VERSION +public class DefaultEllipticCurveSignatureAlgorithm + extends AbstractSignatureAlgorithm implements EllipticCurveSignatureAlgorithm { private static final String REQD_ORDER_BIT_LENGTH_MSG = "orderBitLength must equal 256, 384, or 512."; private static final String KEY_TYPE_MSG_PATTERN = @@ -88,8 +90,8 @@ public DefaultEllipticCurveSignatureAlgorithm(int orderBitLength) { } @Override - public KeyPairBuilder keyPairBuilder() { - return new DefaultKeyPairBuilder("EC", this.KEY_PAIR_GEN_PARAMS) + public KeyPairBuilder keyPairBuilder() { + return new DefaultKeyPairBuilder("EC", this.KEY_PAIR_GEN_PARAMS) .setProvider(getProvider()).setRandom(Randoms.secureRandom()); } @@ -127,7 +129,7 @@ protected void validateKey(Key key, boolean signing) { } @Override - protected byte[] doSign(final SignatureRequest request) { + protected byte[] doSign(final SignatureRequest request) { return execute(request, Signature.class, new CheckedFunction() { @Override public byte[] apply(Signature sig) throws Exception { @@ -140,7 +142,7 @@ public byte[] apply(Signature sig) throws Exception { } @Override - protected boolean doVerify(final VerifySignatureRequest request) { + protected boolean doVerify(final VerifySignatureRequest request) { final ECKey key = request.getKey(); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java index 93b778b3f..a7aa6b40e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java @@ -19,11 +19,9 @@ import java.security.spec.MGF1ParameterSpec; import java.security.spec.PSSParameterSpec; -/** - * @since JJWT_RELEASE_VERSION - */ -public class DefaultRsaSignatureAlgorithm - extends AbstractSignatureAlgorithm implements RsaSignatureAlgorithm { +// @since JJWT_RELEASE_VERSION +public class DefaultRsaSignatureAlgorithm + extends AbstractSignatureAlgorithm implements RsaSignatureAlgorithm { private static final String PSS_JCA_NAME = "RSASSA-PSS"; private static final int MIN_KEY_BIT_LENGTH = 2048; @@ -64,8 +62,8 @@ public Signature get() throws Exception { } @Override - public KeyPairBuilder keyPairBuilder() { - return new DefaultKeyPairBuilder("RSA", this.preferredKeyBitLength) + public KeyPairBuilder keyPairBuilder() { + return new DefaultKeyPairBuilder("RSA", this.preferredKeyBitLength) .setProvider(getProvider()).setRandom(Randoms.secureRandom()); } @@ -105,7 +103,7 @@ protected void validateKey(Key key, boolean signing) { } @Override - protected byte[] doSign(final SignatureRequest request) { + protected byte[] doSign(final SignatureRequest request) { return execute(request, Signature.class, new CheckedFunction() { @Override public byte[] apply(Signature sig) throws Exception { @@ -120,7 +118,7 @@ public byte[] apply(Signature sig) throws Exception { } @Override - protected boolean doVerify(final VerifySignatureRequest request) throws Exception { + protected boolean doVerify(final VerifySignatureRequest request) throws Exception { final Key key = request.getKey(); if (key instanceof PrivateKey) { //legacy support only return super.doVerify(request); From 2da3c8110f3d6ae441c4e8105042e0a689226812 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Thu, 12 May 2022 19:03:22 -0700 Subject: [PATCH 41/75] - JavaDoc additions and syntax cleanup cont'd - Minor work to fix compilation errors on a few Groovy test classes --- .../io/jsonwebtoken/ClaimJwtException.java | 51 ++++++- .../io/jsonwebtoken/CompressionException.java | 13 +- .../io/jsonwebtoken/ExpiredJwtException.java | 21 ++- api/src/main/java/io/jsonwebtoken/Header.java | 3 + .../java/io/jsonwebtoken/Identifiable.java | 41 ++++++ .../jsonwebtoken/IncorrectClaimException.java | 19 ++- .../jsonwebtoken/InvalidClaimException.java | 40 ++++- api/src/main/java/io/jsonwebtoken/Jwe.java | 19 ++- .../main/java/io/jsonwebtoken/JweBuilder.java | 34 ++++- api/src/main/java/io/jsonwebtoken/Jws.java | 8 +- .../java/io/jsonwebtoken/JwtException.java | 11 ++ .../main/java/io/jsonwebtoken/Locator.java | 17 +++ .../java/io/jsonwebtoken/LocatorAdapter.java | 4 + .../java/io/jsonwebtoken/ProtectedHeader.java | 7 + .../io/jsonwebtoken/io/Base64Decoder.java | 3 + .../io/jsonwebtoken/io/Base64Encoder.java | 3 + .../io/jsonwebtoken/io/Base64Support.java | 2 + .../io/jsonwebtoken/io/Base64UrlDecoder.java | 3 + .../io/jsonwebtoken/io/Base64UrlEncoder.java | 3 + .../io/jsonwebtoken/io/CodecException.java | 13 ++ .../main/java/io/jsonwebtoken/io/Decoder.java | 9 ++ .../java/io/jsonwebtoken/io/Decoders.java | 13 ++ .../io/jsonwebtoken/io/DecodingException.java | 13 ++ .../io/DeserializationException.java | 13 ++ .../java/io/jsonwebtoken/io/Deserializer.java | 10 ++ .../main/java/io/jsonwebtoken/io/Encoder.java | 11 ++ .../java/io/jsonwebtoken/io/Encoders.java | 13 ++ .../io/jsonwebtoken/io/EncodingException.java | 8 + .../io/ExceptionPropagatingDecoder.java | 16 ++ .../io/ExceptionPropagatingEncoder.java | 16 ++ .../java/io/jsonwebtoken/io/IOException.java | 14 ++ .../io/jsonwebtoken/io/SerialException.java | 13 ++ .../io/SerializationException.java | 13 ++ .../java/io/jsonwebtoken/io/Serializer.java | 11 ++ .../java/io/jsonwebtoken/lang/Arrays.java | 34 +++++ .../java/io/jsonwebtoken/lang/Assert.java | 26 +++- .../java/io/jsonwebtoken/lang/Classes.java | 138 +++++++++++++----- .../io/jsonwebtoken/lang/Collections.java | 70 ++++++++- .../io/jsonwebtoken/lang/DateFormats.java | 30 +++- .../lang/InstantiationException.java | 12 +- .../jsonwebtoken/security/AsymmetricJwk.java | 2 +- .../security/AsymmetricJwkBuilder.java | 2 +- .../security/DecryptionKeyRequest.java | 2 +- .../security/InvalidKeyException.java | 15 +- .../java/io/jsonwebtoken/security/Jwk.java | 13 +- .../io/jsonwebtoken/security/JwkBuilder.java | 4 +- .../java/io/jsonwebtoken/security/Jwks.java | 10 +- .../jsonwebtoken/security/KeyException.java | 3 + .../security/KeyLengthSupplier.java | 2 + .../io/jsonwebtoken/security/KeyResult.java | 10 ++ .../io/jsonwebtoken/security/KeySupplier.java | 2 + .../security/MalformedKeyException.java | 3 + .../security/RsaPrivateJwkBuilder.java | 2 + .../security/SecretKeySignatureAlgorithm.java | 26 ++++ .../security/SecurityException.java | 14 ++ .../security/SignatureAlgorithm.java | 38 +++++ .../security/SignatureException.java | 2 + .../security/SignatureRequest.java | 7 + .../security/UnsupportedKeyException.java | 2 + .../security/VerifySignatureRequest.java | 8 + .../security/WeakKeyException.java | 3 + .../io/jsonwebtoken/io/DecodersTest.groovy | 8 +- .../io/jsonwebtoken/io/EncodersTest.groovy | 8 +- .../io/jsonwebtoken/lang/ArraysTest.groovy | 8 +- .../orgjson/io/OrgJsonDeserializer.java | 3 +- .../orgjson/io/OrgJsonSerializer.java | 29 ++-- .../io/jsonwebtoken/impl/DefaultHeader.java | 2 +- .../jsonwebtoken/impl/DefaultJwtParser.java | 8 +- .../impl/security/NoneSignatureAlgorithm.java | 7 +- .../DeprecatedJwtParserTest.groovy | 4 +- .../io/jsonwebtoken/JwtParserTest.groovy | 4 +- .../impl/security/JwksTest.groovy | 39 +++-- .../security/PrivateConstructorsTest.groovy | 2 + .../impl/security/TestRSAKey.groovy | 13 +- .../TestRSAMultiPrimePrivateCrtKey.groovy | 2 +- .../impl/security/TestRSAPrivateKey.groovy | 4 +- 76 files changed, 972 insertions(+), 137 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/ClaimJwtException.java b/api/src/main/java/io/jsonwebtoken/ClaimJwtException.java index bb7c81fd5..d01729fb1 100644 --- a/api/src/main/java/io/jsonwebtoken/ClaimJwtException.java +++ b/api/src/main/java/io/jsonwebtoken/ClaimJwtException.java @@ -16,36 +16,77 @@ package io.jsonwebtoken; /** - * ClaimJwtException is a subclass of the {@link JwtException} that is thrown after a validation of an JTW claim failed. + * ClaimJwtException is a subclass of the {@link JwtException} that is thrown after a validation of an JWT claim failed. * * @since 0.5 */ public abstract class ClaimJwtException extends JwtException { + /** + * Deprecated as this is an implementation detail accidentally exposed in the JJWT 0.5 public API. It is no + * longer referenced anywhere in JJWT's implementation and will be removed in a future release. + * + * @deprecated will be removed in a future release. + */ + @Deprecated public static final String INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was: %s."; + + /** + * Deprecated as this is an implementation detail accidentally exposed in the JJWT 0.5 public API. It is no + * longer referenced anywhere in JJWT's implementation and will be removed in a future release. + * + * @deprecated will be removed in a future release. + */ + @Deprecated public static final String MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was not present in the JWT claims."; - private final Header header; + private final Header header; private final Claims claims; - protected ClaimJwtException(Header header, Claims claims, String message) { + /** + * Creates a new instance with the specified header, claims and exception message. + * + * @param header the header inspected + * @param claims the claims obtained + * @param message the exception message + */ + protected ClaimJwtException(Header header, Claims claims, String message) { super(message); this.header = header; this.claims = claims; } - protected ClaimJwtException(Header header, Claims claims, String message, Throwable cause) { + /** + * Creates a new instance with the specified header, claims and exception message as a result of encountering + * the specified {@code cause}. + * + * @param header the header inspected + * @param claims the claims obtained + * @param message the exception message + * @param cause the exception that caused this ClaimJwtException to be thrown. + */ + protected ClaimJwtException(Header header, Claims claims, String message, Throwable cause) { super(message, cause); this.header = header; this.claims = claims; } + /** + * Returns the {@link Claims} that failed validation. + * + * @return the {@link Claims} that failed validation. + */ public Claims getClaims() { return claims; } - public Header getHeader() { + /** + * Returns the header associated with the {@link #getClaims() claims} that failed validation. + * + * @return the header associated with the {@link #getClaims() claims} that failed validation. + */ + public Header getHeader() { return header; } } diff --git a/api/src/main/java/io/jsonwebtoken/CompressionException.java b/api/src/main/java/io/jsonwebtoken/CompressionException.java index e0bdfe7ca..fd2c045c6 100644 --- a/api/src/main/java/io/jsonwebtoken/CompressionException.java +++ b/api/src/main/java/io/jsonwebtoken/CompressionException.java @@ -18,16 +18,27 @@ import io.jsonwebtoken.io.IOException; /** - * Exception indicating that either compressing or decompressing an JWT body failed. + * Exception indicating that either compressing or decompressing a JWT body failed. * * @since 0.6.0 */ public class CompressionException extends IOException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public CompressionException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public CompressionException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/ExpiredJwtException.java b/api/src/main/java/io/jsonwebtoken/ExpiredJwtException.java index 0748ca367..f3683db71 100644 --- a/api/src/main/java/io/jsonwebtoken/ExpiredJwtException.java +++ b/api/src/main/java/io/jsonwebtoken/ExpiredJwtException.java @@ -22,18 +22,27 @@ */ public class ExpiredJwtException extends ClaimJwtException { - public ExpiredJwtException(Header header, Claims claims, String message) { + /** + * Creates a new instance with the specified header, claims, and explanation message. + * + * @param header jwt header + * @param claims jwt claims (body) + * @param message the message explaining why the exception is thrown. + */ + public ExpiredJwtException(Header header, Claims claims, String message) { super(header, claims, message); } /** - * @param header jwt header - * @param claims jwt claims (body) - * @param message exception message - * @param cause cause + * Creates a new instance with the specified header, claims, explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + * @param header jwt header + * @param claims jwt claims (body) * @since 0.5 */ - public ExpiredJwtException(Header header, Claims claims, String message, Throwable cause) { + public ExpiredJwtException(Header header, Claims claims, String message, Throwable cause) { super(header, claims, message, cause); } } diff --git a/api/src/main/java/io/jsonwebtoken/Header.java b/api/src/main/java/io/jsonwebtoken/Header.java index 46c8674ea..fe5e35370 100644 --- a/api/src/main/java/io/jsonwebtoken/Header.java +++ b/api/src/main/java/io/jsonwebtoken/Header.java @@ -41,7 +41,9 @@ public interface Header extends Map { /** * JWT {@code Type} (typ) value: "JWT" + * @deprecated since JJWT_RELEASE_VERSION - this constant is never used within the JJWT codebase. */ + @Deprecated String JWT_TYPE = "JWT"; /** @@ -72,6 +74,7 @@ public interface Header extends Map { * * @deprecated use {@link #COMPRESSION_ALGORITHM} instead. */ + @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated String DEPRECATED_COMPRESSION_ALGORITHM = "calg"; diff --git a/api/src/main/java/io/jsonwebtoken/Identifiable.java b/api/src/main/java/io/jsonwebtoken/Identifiable.java index 082a50ddc..e9fb6a24c 100644 --- a/api/src/main/java/io/jsonwebtoken/Identifiable.java +++ b/api/src/main/java/io/jsonwebtoken/Identifiable.java @@ -16,6 +16,47 @@ package io.jsonwebtoken; /** + * An object that may be uniquely identified by an {@link #getId() id} relative to other instances of the same type. + * + *

    All JWT concepts that have a + * JWA identifier value implement this interface. + * Specifically, there are four JWT concepts that are {@code Identifiable}. The following table indicates how + * their {@link #getId() id} values are used.

    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    JWA Identifiable Concepts
    JJWT TypeHow {@link #getId()} is Used
    {@link io.jsonwebtoken.security.SignatureAlgorithm SignatureAlgorithm}JWS protected header's + * {@code alg} (Algorithm) parameter value.
    {@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm}JWE protected header's + * {@code alg} (Key Management Algorithm) + * parameter value.
    {@link io.jsonwebtoken.security.AeadAlgorithm AeadAlgorithm}JWE protected header's + * {@code enc} (Encryption Algorithm) + * parameter value.
    {@link io.jsonwebtoken.security.Jwk Jwk}JWK's {@code kid} (Key ID) + * parameter value.
    + * * @since JJWT_RELEASE_VERSION */ public interface Identifiable { diff --git a/api/src/main/java/io/jsonwebtoken/IncorrectClaimException.java b/api/src/main/java/io/jsonwebtoken/IncorrectClaimException.java index df68fe1e6..13e3f316f 100644 --- a/api/src/main/java/io/jsonwebtoken/IncorrectClaimException.java +++ b/api/src/main/java/io/jsonwebtoken/IncorrectClaimException.java @@ -23,11 +23,26 @@ */ public class IncorrectClaimException extends InvalidClaimException { - public IncorrectClaimException(Header header, Claims claims, String message) { + /** + * Creates a new instance with the specified header, claims and explanation message. + * + * @param header the header inspected + * @param claims the claims obtained + * @param message the exception message + */ + public IncorrectClaimException(Header header, Claims claims, String message) { super(header, claims, message); } - public IncorrectClaimException(Header header, Claims claims, String message, Throwable cause) { + /** + * Creates a new instance with the specified header, claims, explanation message and underlying cause. + * + * @param header the header inspected + * @param claims the claims obtained + * @param message the exception message + * @param cause the underlying cause that resulted in this exception being thrown + */ + public IncorrectClaimException(Header header, Claims claims, String message, Throwable cause) { super(header, claims, message, cause); } } diff --git a/api/src/main/java/io/jsonwebtoken/InvalidClaimException.java b/api/src/main/java/io/jsonwebtoken/InvalidClaimException.java index fbca6cabe..f3a397dbc 100644 --- a/api/src/main/java/io/jsonwebtoken/InvalidClaimException.java +++ b/api/src/main/java/io/jsonwebtoken/InvalidClaimException.java @@ -21,7 +21,6 @@ * * @see IncorrectClaimException * @see MissingClaimException - * * @since 0.6 */ public class InvalidClaimException extends ClaimJwtException { @@ -29,26 +28,61 @@ public class InvalidClaimException extends ClaimJwtException { private String claimName; private Object claimValue; - protected InvalidClaimException(Header header, Claims claims, String message) { + /** + * Creates a new instance with the specified header, claims and explanation message. + * + * @param header the header inspected + * @param claims the claims obtained + * @param message the exception message + */ + protected InvalidClaimException(Header header, Claims claims, String message) { super(header, claims, message); } - protected InvalidClaimException(Header header, Claims claims, String message, Throwable cause) { + /** + * Creates a new instance with the specified header, claims, explanation message and underlying cause. + * + * @param header the header inspected + * @param claims the claims obtained + * @param message the exception message + * @param cause the underlying cause that resulted in this exception being thrown + */ + protected InvalidClaimException(Header header, Claims claims, String message, Throwable cause) { super(header, claims, message, cause); } + /** + * Returns the name of the invalid claim. + * + * @return the name of the invalid claim. + */ public String getClaimName() { return claimName; } + /** + * Sets the name of the invalid claim. + * + * @param claimName the name of the invalid claim. + */ public void setClaimName(String claimName) { this.claimName = claimName; } + /** + * Returns the claim value that could not be validated. + * + * @return the claim value that could not be validated. + */ public Object getClaimValue() { return claimValue; } + /** + * Sets the claim value that could not be validated. + * + * @param claimValue the claim value that could not be validated. + */ public void setClaimValue(Object claimValue) { this.claimValue = claimValue; } diff --git a/api/src/main/java/io/jsonwebtoken/Jwe.java b/api/src/main/java/io/jsonwebtoken/Jwe.java index 9f8abf51b..af4ee6258 100644 --- a/api/src/main/java/io/jsonwebtoken/Jwe.java +++ b/api/src/main/java/io/jsonwebtoken/Jwe.java @@ -16,12 +16,27 @@ package io.jsonwebtoken; /** - * @param payload type + * An encrypted JWT, called a "JWE", per the + * JWE (RFC 7516) Specification. + * + * @param payload type, either {@link Claims} or {@code byte[]} content. * @since JJWT_RELEASE_VERSION */ -public interface Jwe extends Jwt { +public interface Jwe extends Jwt { + /** + * Returns the Initialization Vector used during JWE encryption and decryption. + * + * @return the Initialization Vector used during JWE encryption and decryption. + */ byte[] getInitializationVector(); + /** + * Returns the Additional Authenticated Data authentication Tag used for JWE header + * authenticity and integrity verification. + * + * @return the Additional Authenticated Data authentication Tag used for JWE header + * authenticity and integrity verification. + */ byte[] getAadTag(); } diff --git a/api/src/main/java/io/jsonwebtoken/JweBuilder.java b/api/src/main/java/io/jsonwebtoken/JweBuilder.java index 970877462..c1a91f3ff 100644 --- a/api/src/main/java/io/jsonwebtoken/JweBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JweBuilder.java @@ -22,13 +22,45 @@ import java.security.Key; /** + * A {@code JwtBuilder} that creates JWEs. + * * @since JJWT_RELEASE_VERSION */ public interface JweBuilder extends JwtBuilder { + /** + * Encrypt the resulting JWE with the specified {@link AeadAlgorithm} Content Encryption Algorithm. They + * key used to perform the encryption must be supplied by calling {@link #withKey(SecretKey)} or + * {@link #withKeyFrom(Key, KeyAlgorithm)}. + * + * @param enc the {@link AeadAlgorithm} algorithm used to encrypt the JWE. + * @return the builder for method chaining. + */ JweBuilder encryptWith(AeadAlgorithm enc); + /** + * Specifies the shared symmetric key to use to encrypt the JWE using the AEAD content encryption algorithm + * specified via the {@link #encryptWith(AeadAlgorithm)} builder method. + * + *

    This is a convenience method that is an alias for the following:

    + * + *
    +     * {@link #withKeyFrom(Key, KeyAlgorithm) withKeyFrom}(key, {@link io.jsonwebtoken.security.KeyAlgorithms KeyAlgorithms}.{@link io.jsonwebtoken.security.KeyAlgorithms#DIRECT DIRECT});
    + * + * @param key the shared symmetric key to use to encrypt the JWE. + * @return the builder for method chaining. + */ JweBuilder withKey(SecretKey key); - JweBuilder withKeyFrom(K key, KeyAlgorithm alg); + /** + * Use the specified {@code key} to invoke the specified {@link KeyAlgorithm} to obtain a + * {@code Content Encryption Key (CEK)}. The resulting CEK will be used to encrypt the JWE using the + * AEAD content encryption algorithm specified via the {@link #encryptWith(AeadAlgorithm)} builder method. + * + * @param key the key to use with the {@code keyAlg} to obtain a {@code Content Encryption Key (CEK)}. + * @param keyAlg the key algorithm that will provide a {@code Content Encryption Key (CEK)}. + * @param the type of key to use with {@code keyAlg} + * @return the builder for method chaining. + */ + JweBuilder withKeyFrom(K key, KeyAlgorithm keyAlg); } diff --git a/api/src/main/java/io/jsonwebtoken/Jws.java b/api/src/main/java/io/jsonwebtoken/Jws.java index 1be5fb390..4c4cb9786 100644 --- a/api/src/main/java/io/jsonwebtoken/Jws.java +++ b/api/src/main/java/io/jsonwebtoken/Jws.java @@ -19,10 +19,14 @@ * An expanded (not compact/serialized) Signed JSON Web Token. * * @param the type of the JWS body contents, either a String or a {@link Claims} instance. - * * @since 0.1 */ -public interface Jws extends Jwt { +public interface Jws extends Jwt { + /** + * Returns the verified JWS signature as a Base64Url string. + * + * @return the verified JWS signature as a Base64Url string. + */ String getSignature(); } diff --git a/api/src/main/java/io/jsonwebtoken/JwtException.java b/api/src/main/java/io/jsonwebtoken/JwtException.java index c25aa2239..e3990dabe 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtException.java +++ b/api/src/main/java/io/jsonwebtoken/JwtException.java @@ -22,10 +22,21 @@ */ public class JwtException extends RuntimeException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public JwtException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public JwtException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/Locator.java b/api/src/main/java/io/jsonwebtoken/Locator.java index 93e947ec0..ae64ba2d7 100644 --- a/api/src/main/java/io/jsonwebtoken/Locator.java +++ b/api/src/main/java/io/jsonwebtoken/Locator.java @@ -15,10 +15,27 @@ */ package io.jsonwebtoken; +import java.security.Key; + /** + * A {@link Locator} can return an object referenced in a JWT {@link Header} that is necessary to process + * the associated JWT. + * + *

    For example, a {@code Locator} implementation can inspect a header's {@code kid} (Key ID) parameter, and use the + * discovered {@code kid} value to lookup and return the associated {@link Key} instance. JJWT could then use this + * {@code key} to decrypt a JWE or verify a JWS signature.

    + * + * @param the type of object that may be returned from the {@link #locate(Header)} method * @since JJWT_RELEASE_VERSION */ public interface Locator { + /** + * Returns an object referenced in the specified {@code header}, or {@code null} if the object couldn't be found. + * + * @param header the JWT header to inspect; may be an instance of {@link Header}, {@link JwsHeader} or + * {@link JweHeader} depending on if the respective JWT is an unprotected JWT, JWS or JWE. + * @return an object referenced in the specified {@code header}, or {@code null} if the object couldn't be found. + */ T locate(Header header); } diff --git a/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java b/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java index 87a662cb1..11910ac61 100644 --- a/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java @@ -18,6 +18,10 @@ import io.jsonwebtoken.lang.Assert; /** + * Adapter pattern implementation for the {@link Locator} interface. Subclasses can override any of the + * {@link #locate(Header)}, {@link #locate(JwsHeader)}, or {@link #locate(JwsHeader)} methods for type-specific logic if + * desired when the encountered header is an unprotected JWT, JWS or JWE respectively. + * * @since JJWT_RELEASE_VERSION */ public class LocatorAdapter implements Locator { diff --git a/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java index 8ad0a1bcb..36e1b2676 100644 --- a/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java +++ b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java @@ -18,9 +18,11 @@ public interface ProtectedHeader> extends Header { URI getJwkSetUrl(); + T setJwkSetUrl(URI uri); PublicJwk getJwk(); + T setJwk(PublicJwk jwk); /** @@ -56,17 +58,22 @@ public interface ProtectedHeader> extends Header T setKeyId(String kid); URI getX509Url(); + T setX509Url(URI uri); List getX509CertificateChain(); + T setX509CertificateChain(List chain); byte[] getX509CertificateSha1Thumbprint(); + T setX509CertificateSha1Thumbprint(byte[] thumbprint); byte[] getX509CertificateSha256Thumbprint(); + T setX509CertificateSha256Thumbprint(byte[] thumbprint); Set getCritical(); + T setCritical(Set crit); } diff --git a/api/src/main/java/io/jsonwebtoken/io/Base64Decoder.java b/api/src/main/java/io/jsonwebtoken/io/Base64Decoder.java index 3c8bc817d..fa76d3aa5 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Base64Decoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/Base64Decoder.java @@ -18,6 +18,9 @@ import io.jsonwebtoken.lang.Assert; /** + * Very fast Base64 decoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + * * @since 0.10.0 */ class Base64Decoder extends Base64Support implements Decoder { diff --git a/api/src/main/java/io/jsonwebtoken/io/Base64Encoder.java b/api/src/main/java/io/jsonwebtoken/io/Base64Encoder.java index 963d8810c..cefad2f91 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Base64Encoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/Base64Encoder.java @@ -18,6 +18,9 @@ import io.jsonwebtoken.lang.Assert; /** + * Very fast Base64 encoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + * * @since 0.10.0 */ class Base64Encoder extends Base64Support implements Encoder { diff --git a/api/src/main/java/io/jsonwebtoken/io/Base64Support.java b/api/src/main/java/io/jsonwebtoken/io/Base64Support.java index eed404d58..8f8a4c130 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Base64Support.java +++ b/api/src/main/java/io/jsonwebtoken/io/Base64Support.java @@ -18,6 +18,8 @@ import io.jsonwebtoken.lang.Assert; /** + * Parent class for Base64 encoders and decoders. + * * @since 0.10.0 */ class Base64Support { diff --git a/api/src/main/java/io/jsonwebtoken/io/Base64UrlDecoder.java b/api/src/main/java/io/jsonwebtoken/io/Base64UrlDecoder.java index 0d26d948f..fcca4cba5 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Base64UrlDecoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/Base64UrlDecoder.java @@ -16,6 +16,9 @@ package io.jsonwebtoken.io; /** + * Very fast Base64Url decoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + * * @since 0.10.0 */ class Base64UrlDecoder extends Base64Decoder { diff --git a/api/src/main/java/io/jsonwebtoken/io/Base64UrlEncoder.java b/api/src/main/java/io/jsonwebtoken/io/Base64UrlEncoder.java index 867819222..1377d31e7 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Base64UrlEncoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/Base64UrlEncoder.java @@ -16,6 +16,9 @@ package io.jsonwebtoken.io; /** + * Very fast Base64Url encoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + * * @since 0.10.0 */ class Base64UrlEncoder extends Base64Encoder { diff --git a/api/src/main/java/io/jsonwebtoken/io/CodecException.java b/api/src/main/java/io/jsonwebtoken/io/CodecException.java index 5b449bd97..f25d8ca0d 100644 --- a/api/src/main/java/io/jsonwebtoken/io/CodecException.java +++ b/api/src/main/java/io/jsonwebtoken/io/CodecException.java @@ -16,14 +16,27 @@ package io.jsonwebtoken.io; /** + * An exception thrown when encountering a problem during encoding or decoding. + * * @since 0.10.0 */ public class CodecException extends IOException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public CodecException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public CodecException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/Decoder.java b/api/src/main/java/io/jsonwebtoken/io/Decoder.java index f9f2c1890..da02e805d 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Decoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/Decoder.java @@ -16,9 +16,18 @@ package io.jsonwebtoken.io; /** + * A decoder converts an already-encoded data value to a desired data type. + * * @since 0.10.0 */ public interface Decoder { + /** + * Convert the specified encoded data value into the desired data type. + * + * @param t the encoded data + * @return the resulting expected data + * @throws DecodingException if there is a problem during decoding. + */ R decode(T t) throws DecodingException; } diff --git a/api/src/main/java/io/jsonwebtoken/io/Decoders.java b/api/src/main/java/io/jsonwebtoken/io/Decoders.java index 3e95c28d7..57a14149a 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Decoders.java +++ b/api/src/main/java/io/jsonwebtoken/io/Decoders.java @@ -16,11 +16,24 @@ package io.jsonwebtoken.io; /** + * Constant definitions for various decoding algorithms. + * + * @see #BASE64 + * @see #BASE64URL * @since 0.10.0 */ public final class Decoders { + /** + * Very fast Base64 decoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + */ public static final Decoder BASE64 = new ExceptionPropagatingDecoder<>(new Base64Decoder()); + + /** + * Very fast Base64Url decoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + */ public static final Decoder BASE64URL = new ExceptionPropagatingDecoder<>(new Base64UrlDecoder()); private Decoders() { //prevent instantiation diff --git a/api/src/main/java/io/jsonwebtoken/io/DecodingException.java b/api/src/main/java/io/jsonwebtoken/io/DecodingException.java index 0bbe62a16..ab3df9217 100644 --- a/api/src/main/java/io/jsonwebtoken/io/DecodingException.java +++ b/api/src/main/java/io/jsonwebtoken/io/DecodingException.java @@ -16,14 +16,27 @@ package io.jsonwebtoken.io; /** + * An exception thrown when encountering a problem during decoding. + * * @since 0.10.0 */ public class DecodingException extends CodecException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public DecodingException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public DecodingException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/DeserializationException.java b/api/src/main/java/io/jsonwebtoken/io/DeserializationException.java index 59559666a..76c647ce7 100644 --- a/api/src/main/java/io/jsonwebtoken/io/DeserializationException.java +++ b/api/src/main/java/io/jsonwebtoken/io/DeserializationException.java @@ -16,14 +16,27 @@ package io.jsonwebtoken.io; /** + * Exception thrown when reconstituting a serialized byte array into a Java object. + * * @since 0.10.0 */ public class DeserializationException extends SerialException { + /** + * Creates a new instance with the specified explanation message. + * + * @param msg the message explaining why the exception is thrown. + */ public DeserializationException(String msg) { super(msg); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public DeserializationException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/Deserializer.java b/api/src/main/java/io/jsonwebtoken/io/Deserializer.java index f66a73edb..a83b3dc67 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Deserializer.java +++ b/api/src/main/java/io/jsonwebtoken/io/Deserializer.java @@ -16,9 +16,19 @@ package io.jsonwebtoken.io; /** + * A {@code Deserializer} is able to convert serialized data byte arrays into Java objects. + * + * @param the type of object to be returned as a result of deserialization. * @since 0.10.0 */ public interface Deserializer { + /** + * Convert the specified formatted data byte array into a Java object. + * + * @param bytes the formatted data byte array to convert + * @return the reconstituted Java object + * @throws DeserializationException if there is a problem converting the byte array to to an object. + */ T deserialize(byte[] bytes) throws DeserializationException; } diff --git a/api/src/main/java/io/jsonwebtoken/io/Encoder.java b/api/src/main/java/io/jsonwebtoken/io/Encoder.java index 6b28f31ec..f334ee8c2 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Encoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/Encoder.java @@ -16,9 +16,20 @@ package io.jsonwebtoken.io; /** + * An encoder converts data of one type into another formatted data value. + * + * @param the type of data to convert + * @param the type of the resulting formatted data * @since 0.10.0 */ public interface Encoder { + /** + * Convert the specified data into another formatted data value. + * + * @param t the data to convert + * @return the resulting formatted data value + * @throws EncodingException if there is a problem during encoding + */ R encode(T t) throws EncodingException; } diff --git a/api/src/main/java/io/jsonwebtoken/io/Encoders.java b/api/src/main/java/io/jsonwebtoken/io/Encoders.java index 3b7c060f6..17f03f250 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Encoders.java +++ b/api/src/main/java/io/jsonwebtoken/io/Encoders.java @@ -16,11 +16,24 @@ package io.jsonwebtoken.io; /** + * Constant definitions for various encoding algorithms. + * + * @see #BASE64 + * @see #BASE64URL * @since 0.10.0 */ public final class Encoders { + /** + * Very fast Base64 encoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + */ public static final Encoder BASE64 = new ExceptionPropagatingEncoder<>(new Base64Encoder()); + + /** + * Very fast Base64Url encoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + */ public static final Encoder BASE64URL = new ExceptionPropagatingEncoder<>(new Base64UrlEncoder()); private Encoders() { //prevent instantiation diff --git a/api/src/main/java/io/jsonwebtoken/io/EncodingException.java b/api/src/main/java/io/jsonwebtoken/io/EncodingException.java index 5b65389a1..c5ee9f90a 100644 --- a/api/src/main/java/io/jsonwebtoken/io/EncodingException.java +++ b/api/src/main/java/io/jsonwebtoken/io/EncodingException.java @@ -16,10 +16,18 @@ package io.jsonwebtoken.io; /** + * An exception thrown when encountering a problem during encoding. + * * @since 0.10.0 */ public class EncodingException extends CodecException { + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public EncodingException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingDecoder.java b/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingDecoder.java index a0d98134c..9e5bc78e2 100644 --- a/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingDecoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingDecoder.java @@ -18,17 +18,33 @@ import io.jsonwebtoken.lang.Assert; /** + * Decoder that ensures any exceptions thrown that are not {@link DecodingException}s are wrapped + * and re-thrown as a {@code DecodingException}. + * * @since 0.10.0 */ class ExceptionPropagatingDecoder implements Decoder { private final Decoder decoder; + /** + * Creates a new instance, wrapping the specified {@code decoder} to invoke during {@link #decode(Object)}. + * + * @param decoder the decoder to wrap and call during {@link #decode(Object)} + */ ExceptionPropagatingDecoder(Decoder decoder) { Assert.notNull(decoder, "Decoder cannot be null."); this.decoder = decoder; } + /** + * Decode the specified encoded data, delegating to the wrapped Decoder, wrapping any + * non-{@link DecodingException} as a {@code DecodingException}. + * + * @param t the encoded data + * @return the decoded data + * @throws DecodingException if there is an unexpected problem during decoding. + */ @Override public R decode(T t) throws DecodingException { Assert.notNull(t, "Decode argument cannot be null."); diff --git a/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingEncoder.java b/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingEncoder.java index c483c8cdd..8efca9576 100644 --- a/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingEncoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingEncoder.java @@ -18,17 +18,33 @@ import io.jsonwebtoken.lang.Assert; /** + * Encoder that ensures any exceptions thrown that are not {@link EncodingException}s are wrapped + * and re-thrown as a {@code EncodingException}. + * * @since 0.10.0 */ class ExceptionPropagatingEncoder implements Encoder { private final Encoder encoder; + /** + * Creates a new instance, wrapping the specified {@code encoder} to invoke during {@link #encode(Object)}. + * + * @param encoder the encoder to wrap and call during {@link #encode(Object)} + */ ExceptionPropagatingEncoder(Encoder encoder) { Assert.notNull(encoder, "Encoder cannot be null."); this.encoder = encoder; } + /** + * Encoded the specified data, delegating to the wrapped Encoder, wrapping any + * non-{@link EncodingException} as an {@code EncodingException}. + * + * @param t the data to encode + * @return the encoded data + * @throws EncodingException if there is an unexpected problem during encoding. + */ @Override public R encode(T t) throws EncodingException { Assert.notNull(t, "Encode argument cannot be null."); diff --git a/api/src/main/java/io/jsonwebtoken/io/IOException.java b/api/src/main/java/io/jsonwebtoken/io/IOException.java index 9ed1e0314..0ccd165c3 100644 --- a/api/src/main/java/io/jsonwebtoken/io/IOException.java +++ b/api/src/main/java/io/jsonwebtoken/io/IOException.java @@ -18,14 +18,28 @@ import io.jsonwebtoken.JwtException; /** + * JJWT's base exception for problems during data input or output operations, such as serialization, + * deserialization, marshalling, unmarshalling, etc. + * * @since 0.10.0 */ public class IOException extends JwtException { + /** + * Creates a new instance with the specified explanation message. + * + * @param msg the message explaining why the exception is thrown. + */ public IOException(String msg) { super(msg); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public IOException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/SerialException.java b/api/src/main/java/io/jsonwebtoken/io/SerialException.java index 86f70920c..0269c96ee 100644 --- a/api/src/main/java/io/jsonwebtoken/io/SerialException.java +++ b/api/src/main/java/io/jsonwebtoken/io/SerialException.java @@ -16,14 +16,27 @@ package io.jsonwebtoken.io; /** + * An exception thrown during serialization or deserialization. + * * @since 0.10.0 */ public class SerialException extends IOException { + /** + * Creates a new instance with the specified explanation message. + * + * @param msg the message explaining why the exception is thrown. + */ public SerialException(String msg) { super(msg); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public SerialException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/SerializationException.java b/api/src/main/java/io/jsonwebtoken/io/SerializationException.java index 514830def..d97892111 100644 --- a/api/src/main/java/io/jsonwebtoken/io/SerializationException.java +++ b/api/src/main/java/io/jsonwebtoken/io/SerializationException.java @@ -16,14 +16,27 @@ package io.jsonwebtoken.io; /** + * Exception thrown when converting a Java object to a formatted byte array. + * * @since 0.10.0 */ public class SerializationException extends SerialException { + /** + * Creates a new instance with the specified explanation message. + * + * @param msg the message explaining why the exception is thrown. + */ public SerializationException(String msg) { super(msg); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public SerializationException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/Serializer.java b/api/src/main/java/io/jsonwebtoken/io/Serializer.java index 5d6cd794a..630cfe89b 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Serializer.java +++ b/api/src/main/java/io/jsonwebtoken/io/Serializer.java @@ -16,10 +16,21 @@ package io.jsonwebtoken.io; /** + * A {@code Serializer} is able to convert a Java object into a formatted data byte array. It is expected this data + * can be reconstituted back into a Java object with a matching {@link Deserializer}. + * + * @param The type of object to serialize. * @since 0.10.0 */ public interface Serializer { + /** + * Convert the specified Java object into a formatted data byte array. + * + * @param t the object to serialize + * @return the serialized byte array representing the specified object. + * @throws SerializationException if there is a problem converting the object to a byte array. + */ byte[] serialize(T t) throws SerializationException; } diff --git a/api/src/main/java/io/jsonwebtoken/lang/Arrays.java b/api/src/main/java/io/jsonwebtoken/lang/Arrays.java index 024b06ac1..6c5b4e2ab 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Arrays.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Arrays.java @@ -19,6 +19,8 @@ import java.util.List; /** + * Utility methods to work with array instances. + * * @since 0.6 */ public final class Arrays { @@ -26,22 +28,54 @@ public final class Arrays { private Arrays() { } //prevent instantiation + /** + * Returns the length of the array, or {@code 0} if the array is {@code null}. + * + * @param a the possibly-null array + * @param the type of elements in the array + * @return the length of the array, or zero if the array is null. + */ public static int length(T[] a) { return a == null ? 0 : a.length; } + /** + * Converts the specified array to a {@link List}. If the array is empty, an empty list will be returned. + * + * @param a the array to represent as a list + * @param the type of elements in the array + * @return the array as a list, or an empty list if the array is empty. + */ public static List asList(T[] a) { return Objects.isEmpty(a) ? Collections.emptyList() : java.util.Arrays.asList(a); } + /** + * Returns the length of the specified byte array, or {@code 0} if the byte array is {@code null}. + * + * @param bytes the array to check + * @return the length of the specified byte array, or {@code 0} if the byte array is {@code null}. + */ public static int length(byte[] bytes) { return bytes != null ? bytes.length : 0; } + /** + * Returns the byte array unaltered if it is non-null and has a positive length, otherwise {@code null}. + * + * @param bytes the byte array to check. + * @return the byte array unaltered if it is non-null and has a positive length, otherwise {@code null}. + */ public static byte[] clean(byte[] bytes) { return length(bytes) > 0 ? bytes : null; } + /** + * Creates a shallow copy of the specified object or array. + * + * @param obj the object to copy + * @return a shallow copy of the specified object or array. + */ public static Object copy(Object obj) { if (obj == null) { return null; diff --git a/api/src/main/java/io/jsonwebtoken/lang/Assert.java b/api/src/main/java/io/jsonwebtoken/lang/Assert.java index 6dd959c82..6eabbd8e8 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Assert.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Assert.java @@ -18,6 +18,10 @@ import java.util.Collection; import java.util.Map; +/** + * Utility methods for providing argument and state assertions to reduce repeating these patterns and otherwise + * increasing cyclomatic complexity. + */ public final class Assert { private Assert() { @@ -216,6 +220,15 @@ public static void notEmpty(Object[] array) { notEmpty(array, "[Assertion failed] - this array must not be empty: it must contain at least 1 element"); } + /** + * Assert that the specified byte array is not null and has at least one byte element. + * + * @param array the byte array to check + * @param msg the exception message to use if the assertion fails + * @return the byte array if the assertion passes + * @throws IllegalArgumentException if the byte array is null or empty + * @since JJWT_RELEASE_VERSION + */ public static byte[] notEmpty(byte[] array, String msg) { if (Objects.isEmpty(array)) { throw new IllegalArgumentException(msg); @@ -223,6 +236,15 @@ public static byte[] notEmpty(byte[] array, String msg) { return array; } + /** + * Assert that the specified character array is not null and has at least one byte element. + * + * @param chars the character array to check + * @param msg the exception message to use if the assertion fails + * @return the character array if the assertion passes + * @throws IllegalArgumentException if the character array is null or empty + * @since JJWT_RELEASE_VERSION + */ public static char[] notEmpty(char[] chars, String msg) { if (Objects.isEmpty(chars)) { throw new IllegalArgumentException(msg); @@ -396,9 +418,9 @@ public static void isAssignable(Class superType, Class subType, String message) * Asserts that a specified {@code value} is greater than the given {@code requirement}, throwing * an {@link IllegalArgumentException} with the given message if not. * - * @param value the value to check + * @param value the value to check * @param requirement the integer that {@code value} must be greater than - * @param msg the message to use for the {@code IllegalArgumentException} if thrown. + * @param msg the message to use for the {@code IllegalArgumentException} if thrown. * @return {@code value} if greater than the specified {@code requirement}. * @since JJWT_RELEASE_VERSION */ diff --git a/api/src/main/java/io/jsonwebtoken/lang/Classes.java b/api/src/main/java/io/jsonwebtoken/lang/Classes.java index 402e83059..6ceca679e 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Classes.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Classes.java @@ -21,38 +21,32 @@ import java.lang.reflect.Method; /** + * Utility methods for working with {@link Class}es. + * * @since 0.1 */ public final class Classes { - private Classes() {} //prevent instantiation + private Classes() { + } //prevent instantiation - /** - * @since 0.1 - */ private static final ClassLoaderAccessor THREAD_CL_ACCESSOR = new ExceptionIgnoringAccessor() { @Override - protected ClassLoader doGetClassLoader() throws Throwable { + protected ClassLoader doGetClassLoader() { return Thread.currentThread().getContextClassLoader(); } }; - /** - * @since 0.1 - */ private static final ClassLoaderAccessor CLASS_CL_ACCESSOR = new ExceptionIgnoringAccessor() { @Override - protected ClassLoader doGetClassLoader() throws Throwable { + protected ClassLoader doGetClassLoader() { return Classes.class.getClassLoader(); } }; - /** - * @since 0.1 - */ private static final ClassLoaderAccessor SYSTEM_CL_ACCESSOR = new ExceptionIgnoringAccessor() { @Override - protected ClassLoader doGetClassLoader() throws Throwable { + protected ClassLoader doGetClassLoader() { return ClassLoader.getSystemClassLoader(); } }; @@ -66,14 +60,14 @@ protected ClassLoader doGetClassLoader() throws Throwable { * the JRE's ClassNotFoundException. * * @param fqcn the fully qualified class name to load - * @param The type of Class returned + * @param The type of Class returned * @return the located class * @throws UnknownClassException if the class cannot be found. */ @SuppressWarnings("unchecked") public static Class forName(String fqcn) throws UnknownClassException { - Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn); + Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn); if (clazz == null) { clazz = CLASS_CL_ACCESSOR.loadClass(fqcn); @@ -94,7 +88,7 @@ public static Class forName(String fqcn) throws UnknownClassException { throw new UnknownClassException(msg); } - return clazz; + return (Class) clazz; } /** @@ -124,6 +118,14 @@ public static InputStream getResourceAsStream(String name) { return is; } + /** + * Returns {@code true} if the specified {@code fullyQualifiedClassName} can be found in any of the thread + * context, class, or system classloaders, or {@code false} otherwise. + * + * @param fullyQualifiedClassName the fully qualified class name to check + * @return {@code true} if the specified {@code fullyQualifiedClassName} can be found in any of the thread + * context, class, or system classloaders, or {@code false} otherwise. + */ public static boolean isAvailable(String fullyQualifiedClassName) { try { forName(fullyQualifiedClassName); @@ -133,22 +135,56 @@ public static boolean isAvailable(String fullyQualifiedClassName) { } } + /** + * Creates and returns a new instance of the class with the specified fully qualified class name using the + * classes default no-argument constructor. + * + * @param fqcn the fully qualified class name + * @param the type of object created + * @return a new instance of the specified class name + */ @SuppressWarnings("unchecked") public static T newInstance(String fqcn) { - return (T)newInstance(forName(fqcn)); + return (T) newInstance(forName(fqcn)); } - public static T newInstance(String fqcn, Class[] ctorArgTypes, Object... args) { + /** + * Creates and returns a new instance of the specified fully qualified class name using the + * specified {@code args} arguments provided to the constructor with {@code ctorArgTypes} + * + * @param fqcn the fully qualified class name + * @param ctorArgTypes the argument types of the constructor to invoke + * @param args the arguments to supply when invoking the constructor + * @param the type of object created + * @return the newly created object + */ + public static T newInstance(String fqcn, Class[] ctorArgTypes, Object... args) { Class clazz = forName(fqcn); Constructor ctor = getConstructor(clazz, ctorArgTypes); return instantiate(ctor, args); } + /** + * Creates and returns a new instance of the specified fully qualified class name using a constructor that matches + * the specified {@code args} arguments. + * + * @param fqcn fully qualified class name + * @param args the arguments to supply to the constructor + * @param the type of the object created + * @return the newly created object + */ @SuppressWarnings("unchecked") public static T newInstance(String fqcn, Object... args) { - return (T)newInstance(forName(fqcn), args); + return (T) newInstance(forName(fqcn), args); } + /** + * Creates a new instance of the specified {@code clazz} via {@code clazz.newInstance()}. + * + * @param clazz the class to invoke + * @param the type of the object created + * @return the newly created object + */ public static T newInstance(Class clazz) { if (clazz == null) { String msg = "Class method parameter cannot be null."; @@ -161,8 +197,17 @@ public static T newInstance(Class clazz) { } } + /** + * Returns a new instance of the specified {@code clazz}, invoking the associated constructor with the specified + * {@code args} arguments. + * + * @param clazz the class to invoke + * @param args the arguments matching an associated class constructor + * @param the type of the created object + * @return the newly created object + */ public static T newInstance(Class clazz, Object... args) { - Class[] argTypes = new Class[args.length]; + Class[] argTypes = new Class[args.length]; for (int i = 0; i < args.length; i++) { argTypes[i] = args[i].getClass(); } @@ -170,7 +215,17 @@ public static T newInstance(Class clazz, Object... args) { return instantiate(ctor, args); } - public static Constructor getConstructor(Class clazz, Class... argTypes) { + /** + * Returns the {@link Constructor} for the specified {@code Class} with arguments matching the specified + * {@code argTypes}. + * + * @param clazz the class to inspect + * @param argTypes the argument types for the desired constructor + * @param the type of object to create + * @return the constructor matching the specified argument types + * @throws IllegalStateException if the constructor for the specified {@code argTypes} does not exist. + */ + public static Constructor getConstructor(Class clazz, Class... argTypes) throws IllegalStateException { try { return clazz.getConstructor(argTypes); } catch (NoSuchMethodException e) { @@ -179,6 +234,16 @@ public static Constructor getConstructor(Class clazz, Class... argType } + /** + * Creates a new object using the specified {@link Constructor}, invoking it with the specified constructor + * {@code args} arguments. + * + * @param ctor the constructor to invoke + * @param args the arguments to supply to the constructor + * @param the type of object to create + * @return the new object instance + * @throws InstantiationException if the constructor cannot be invoked successfully + */ public static T instantiate(Constructor ctor, Object... args) { try { return ctor.newInstance(args); @@ -191,11 +256,12 @@ public static T instantiate(Constructor ctor, Object... args) { /** * Invokes the fully qualified class name's method named {@code methodName} with parameters of type {@code argTypes} * using the {@code args} as the method arguments. - * @param fqcn fully qualified class name to locate + * + * @param fqcn fully qualified class name to locate * @param methodName name of the method to invoke on the class - * @param argTypes the method argument types supported by the {@code methodName} method - * @param args the runtime arguments to use when invoking the located class method - * @param the expected type of the object returned from the invoked method. + * @param argTypes the method argument types supported by the {@code methodName} method + * @param args the runtime arguments to use when invoking the located class method + * @param the expected type of the object returned from the invoked method. * @return the result returned by the invoked method * @since 0.10.0 */ @@ -205,7 +271,7 @@ public static T invokeStatic(String fqcn, String methodName, Class[] argT return invokeStatic(clazz, methodName, argTypes, args); } catch (Exception e) { String msg = "Unable to invoke class method " + fqcn + "#" + methodName + ". Ensure the necessary " + - "implementation is in the runtime classpath."; + "implementation is in the runtime classpath."; throw new IllegalStateException(msg, e); } } @@ -214,11 +280,11 @@ public static T invokeStatic(String fqcn, String methodName, Class[] argT * Invokes the {@code clazz}'s matching static method (named {@code methodName} with exact argument types * of {@code argTypes}) with the given {@code args} arguments, and returns the method return value. * - * @param clazz the class to invoke + * @param clazz the class to invoke * @param methodName the name of the static method on {@code clazz} to invoke - * @param argTypes the types of the arguments accepted by the method - * @param args the actual runtime arguments to use when invoking the method - * @param the type of object expected to be returned from the method + * @param argTypes the types of the arguments accepted by the method + * @param args the actual runtime arguments to use when invoking the method + * @param the type of object expected to be returned from the method * @return the result returned by the invoked method. * @since JJWT_RELEASE_VERSION */ @@ -227,14 +293,14 @@ public static T invokeStatic(Class clazz, String methodName, Class[] a try { Method method = clazz.getDeclaredMethod(methodName, argTypes); method.setAccessible(true); - return(T)method.invoke(null, args); + return (T) method.invoke(null, args); } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { Throwable cause = e.getCause(); if (cause instanceof RuntimeException) { throw ((RuntimeException) cause); //propagate } String msg = "Unable to invoke class method " + clazz.getName() + "#" + methodName + - ". Ensure the necessary implementation is in the runtime classpath."; + ". Ensure the necessary implementation is in the runtime classpath."; throw new IllegalStateException(msg, e); } } @@ -242,8 +308,8 @@ public static T invokeStatic(Class clazz, String methodName, Class[] a /** * @since 1.0 */ - private static interface ClassLoaderAccessor { - Class loadClass(String fqcn); + private interface ClassLoaderAccessor { + Class loadClass(String fqcn); InputStream getResourceStream(String name); } @@ -253,8 +319,8 @@ private static interface ClassLoaderAccessor { */ private static abstract class ExceptionIgnoringAccessor implements ClassLoaderAccessor { - public Class loadClass(String fqcn) { - Class clazz = null; + public Class loadClass(String fqcn) { + Class clazz = null; ClassLoader cl = getClassLoader(); if (cl != null) { try { diff --git a/api/src/main/java/io/jsonwebtoken/lang/Collections.java b/api/src/main/java/io/jsonwebtoken/lang/Collections.java index 98f1d5e52..2ca8f5e2f 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Collections.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Collections.java @@ -26,23 +26,55 @@ import java.util.Properties; import java.util.Set; +/** + * Utility methods for working with {@link Collection}s, {@link List}s, {@link Set}s, and {@link Maps}. + */ +@SuppressWarnings({"unused", "rawtypes"}) public final class Collections { private Collections() { } //prevent instantiation + /** + * Returns a type-safe immutable empty {@code List}. + * + * @param list element type + * @return a type-safe immutable empty {@code List}. + */ public static List emptyList() { return java.util.Collections.emptyList(); } + /** + * Returns a type-safe immutable empty {@code Set}. + * + * @param set element type + * @return a type-safe immutable empty {@code Set}. + */ + @SuppressWarnings("unused") public static Set emptySet() { return java.util.Collections.emptySet(); } + /** + * Returns a type-safe immutable empty {@code Map}. + * + * @param map key type + * @param map value type + * @return a type-safe immutable empty {@code Map}. + */ + @SuppressWarnings("unused") public static Map emptyMap() { return java.util.Collections.emptyMap(); } + /** + * Returns a type-safe immutable {@code List} containing the specified array elements. + * + * @param elements array elements to include in the list + * @param list element type + * @return a type-safe immutable {@code List} containing the specified array elements. + */ @SafeVarargs public static List of(T... elements) { if (elements == null || elements.length == 0) { @@ -51,6 +83,13 @@ public static List of(T... elements) { return java.util.Collections.unmodifiableList(Arrays.asList(elements)); } + /** + * Returns a type-safe immutable {@code Set} containing the specified array elements. + * + * @param elements array elements to include in the set + * @param set element type + * @return a type-safe immutable {@code Set} containing the specified array elements. + */ @SafeVarargs public static Set setOf(T... elements) { if (elements == null || elements.length == 0) { @@ -74,10 +113,26 @@ public static Map immutable(Map m) { return m != null ? java.util.Collections.unmodifiableMap(m) : null; } + /** + * Shorter null-safe convenience alias for {@link java.util.Collections#unmodifiableSet(Set)} so both classes don't + * need to be imported. + * + * @param set set to wrap in an immutable Set + * @param set element type + * @return an immutable wrapper for {@code set} + */ public static Set immutable(Set set) { return set != null ? java.util.Collections.unmodifiableSet(set) : null; } + /** + * Shorter null-safe convenience alias for {@link java.util.Collections#unmodifiableList(List)} so both classes + * don't need to be imported. + * + * @param list list to wrap in an immutable List + * @param list element type + * @return an immutable wrapper for {@code list} + */ public static List immutable(List list) { return list != null ? java.util.Collections.unmodifiableList(list) : null; } @@ -163,6 +218,15 @@ public static List arrayToList(Object source) { return Arrays.asList(Objects.toObjectArray(source)); } + /** + * Concatenate the specified set with the specified array elements, resulting in a new {@link LinkedHashSet} with + * the array elements appended to the end of the existing Set. + * + * @param c the set to append to + * @param elements the array elements to append to the end of the set + * @param set element type + * @return a new {@link LinkedHashSet} with the array elements appended to the end of the original set. + */ @SafeVarargs public static Set concat(Set c, T... elements) { int size = Math.max(1, Collections.size(c) + io.jsonwebtoken.lang.Arrays.length(elements)); @@ -425,7 +489,7 @@ public static Class findCommonElementType(Collection collection) { * @return a new array of type {@code A} that contains the elements in the specified {@code enumeration}. */ public static A[] toArray(Enumeration enumeration, A[] array) { - ArrayList elements = new ArrayList(); + ArrayList elements = new ArrayList<>(); while (enumeration.hasMoreElements()) { elements.add(enumeration.nextElement()); } @@ -440,7 +504,7 @@ public static A[] toArray(Enumeration enumeration, A[] array * @return the iterator */ public static Iterator toIterator(Enumeration enumeration) { - return new EnumerationIterator(enumeration); + return new EnumerationIterator<>(enumeration); } /** @@ -448,7 +512,7 @@ public static Iterator toIterator(Enumeration enumeration) { */ private static class EnumerationIterator implements Iterator { - private Enumeration enumeration; + private final Enumeration enumeration; public EnumerationIterator(Enumeration enumeration) { this.enumeration = enumeration; diff --git a/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java b/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java index 250d9892d..6a3b501b2 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java +++ b/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java @@ -22,9 +22,14 @@ import java.util.TimeZone; /** + * Utility methods to format and parse date strings. + * * @since 0.10.0 */ -public class DateFormats { +public final class DateFormats { + + private DateFormats() { + } // prevent instantiation private static final String ISO_8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; @@ -48,10 +53,25 @@ protected DateFormat initialValue() { } }; + /** + * Return an ISO-8601-formatted string with millisecond precision representing the + * specified {@code date}. + * + * @param date the date for which to create an ISO-8601-formatted string + * @return the date represented as an ISO-8601-formatted string with millisecond precision. + */ public static String formatIso8601(Date date) { return formatIso8601(date, true); } + /** + * Returns an ISO-8601-formatted string with optional millisecond precision for the specified + * {@code date}. + * + * @param date the date for which to create an ISO-8601-formatted string + * @param includeMillis whether to include millisecond notation within the string. + * @return the date represented as an ISO-8601-formatted string with optional millisecond precision. + */ public static String formatIso8601(Date date, boolean includeMillis) { if (includeMillis) { return ISO_8601_MILLIS.get().format(date); @@ -59,6 +79,14 @@ public static String formatIso8601(Date date, boolean includeMillis) { return ISO_8601.get().format(date); } + /** + * Parse the specified ISO-8601-formatted date string and return the corresponding {@link Date} instance. The + * date string may optionally contain millisecond notation, and those milliseconds will be represented accordingly. + * + * @param s the ISO-8601-formatted string to parse + * @return the string's corresponding {@link Date} instance. + * @throws ParseException if the specified date string is not a validly-formatted ISO-8601 string. + */ public static Date parseIso8601Date(String s) throws ParseException { Assert.notNull(s, "String argument cannot be null."); if (s.lastIndexOf('.') > -1) { //assume ISO-8601 with milliseconds diff --git a/api/src/main/java/io/jsonwebtoken/lang/InstantiationException.java b/api/src/main/java/io/jsonwebtoken/lang/InstantiationException.java index 0ee8418f5..d6b414d7e 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/InstantiationException.java +++ b/api/src/main/java/io/jsonwebtoken/lang/InstantiationException.java @@ -16,11 +16,19 @@ package io.jsonwebtoken.lang; /** + * {@link RuntimeException} equivalent of {@link java.lang.InstantiationException}. + * * @since 0.1 */ public class InstantiationException extends RuntimeException { - public InstantiationException(String s, Throwable t) { - super(s, t); + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ + public InstantiationException(String message, Throwable cause) { + super(message, cause); } } diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java index 7546a8414..f7b476e61 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java @@ -35,7 +35,7 @@ public interface AsymmetricJwk extends Jwk { *

    The JWK specification defines the * following {@code use} values:

    * - * + *
    * * * diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java index 62cc29c08..97a76aec8 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java @@ -41,7 +41,7 @@ public interface AsymmetricJwkBuilder, *

    The JWK specification defines the * following {@code use} values:

    * - *
    JWK Key Use Values
    + *
    * * * diff --git a/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java index 92eac4823..0066c3a86 100644 --- a/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java @@ -28,7 +28,7 @@ *

    Any encrypted key material (what the JWE specification calls the * JWE Encrypted Key) will * be accessible via {@link #getContent()}. If present, the {@link KeyAlgorithm} will decrypt it to obtain the resulting - * Content Encryption Key (CEK). + * Content Encryption Key (CEK). * This may be empty however depending on which {@link KeyAlgorithm} was used during JWE encryption.

    * *

    Finally, any public information necessary by the called {@link KeyAlgorithm} to decrypt any diff --git a/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java b/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java index ae001f896..caca71e47 100644 --- a/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java @@ -23,18 +23,23 @@ */ public class InvalidKeyException extends KeyException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public InvalidKeyException(String message) { super(message); } /** - * Creates a new {@code InvalidKeyException} with the specified message and cause. + * Creates a new instance with the specified explanation message and underlying cause. * - * @param msg exception message - * @param cause triggering cause for the InvalidKeyException + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. * @since JJWT_RELEASE_VERSION */ - public InvalidKeyException(String msg, Exception cause) { - super(msg, cause); + public InvalidKeyException(String message, Exception cause) { + super(message, cause); } } diff --git a/api/src/main/java/io/jsonwebtoken/security/Jwk.java b/api/src/main/java/io/jsonwebtoken/security/Jwk.java index f6a2af252..b9ced982e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Jwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/Jwk.java @@ -25,7 +25,7 @@ * A JWK is an immutable set of name/value pairs that represent a cryptographic key as defined by * RFC 7517: JSON Web Key (JWK). The {@code Jwk} * interface represents JWK properties accessible for any JWK. Subtypes will have additional JWK properties - * specific to different types of cryptographic keys (e.g. Secret, Asymmetric, RSA, Elliptic Curve, etc).

    + * specific to different types of cryptographic keys (e.g. Secret, Asymmetric, RSA, Elliptic Curve, etc). * *

    Immutability

    * @@ -47,8 +47,9 @@ *

    toString Safety

    * *

    JWKs often represent secret or private key data which should never be exposed publicly, nor mistakenly printed - * via application logs or {@code System.out.println} calls. As a result, all JJWT JWK {@link #toString()} - * implementations automatically print redacted values instead actual values for any private or secret fields.

    + * via application logs or {@code System.out.println} calls. As a result, all JJWT JWK + * {@link String#toString() toString()} implementations automatically print redacted values instead actual + * values for any private or secret fields.

    * *

    For example, a {@link SecretJwk} will have an internal "{@code k}" member whose value reflects raw * key material that should always be kept secret. If {@code aSecretJwk.toString()} is called, the resulting string @@ -107,7 +108,7 @@ public interface Jwk extends Identifiable, Map { *

    The JWK specification defines the * following values:

    * - *
    JWK Key Use Values
    + *
    * * * @@ -171,7 +172,7 @@ public interface Jwk extends Identifiable, Map { *

    The JWA specification defines the * following {@code kty} values:

    * - *
    JWK Key Operations
    + *
    * * * @@ -194,6 +195,8 @@ public interface Jwk extends Identifiable, Map { * * *
    JWK Key Types
    + * + * @return the JWK {@code kty} (Key Type) value. */ String getType(); diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java index 3a7b60641..b3ce73b08 100644 --- a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java @@ -25,7 +25,7 @@ * RFC 7517: JSON Web Key (JWK). The {@code Jwk}. * The {@code JwkBuilder} interface represents common JWK properties that may be specified for any type of JWK. * Builder subtypes support additional JWK properties specific to different types of cryptographic keys - * (e.g. Secret, Asymmetric, RSA, Elliptic Curve, etc).

    + * (e.g. Secret, Asymmetric, RSA, Elliptic Curve, etc). * * @see SecretJwkBuilder * @see RsaPublicJwkBuilder @@ -103,7 +103,7 @@ public interface JwkBuilder, T extends JwkBuilde *

    The JWK specification defines the * following values:

    * - * + *
    * * * diff --git a/api/src/main/java/io/jsonwebtoken/security/Jwks.java b/api/src/main/java/io/jsonwebtoken/security/Jwks.java index b6d4a308b..078b255e8 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Jwks.java +++ b/api/src/main/java/io/jsonwebtoken/security/Jwks.java @@ -24,10 +24,18 @@ * @see #builder() * @since JJWT_RELEASE_VERSION */ -public class Jwks { +public final class Jwks { + + private Jwks() { + } //prevent instantiation private static final String CNAME = "io.jsonwebtoken.impl.security.DefaultProtoJwkBuilder"; + /** + * Return a new JWK builder instance, allowing for type-safe JWK builder coercion based on a provided key or key pair. + * + * @return a new JWK builder instance, allowing for type-safe JWK builder coercion based on a provided key or key pair. + */ public static ProtoJwkBuilder builder() { return Classes.newInstance(CNAME); } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyException.java b/api/src/main/java/io/jsonwebtoken/security/KeyException.java index da44f9ad8..55a488029 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyException.java @@ -16,6 +16,9 @@ package io.jsonwebtoken.security; /** + * General-purpose exception when encountering a problem with a cryptographic {@link java.security.Key} + * or {@link Jwk}. + * * @since 0.10.0 */ public class KeyException extends SecurityException { diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java index 1cc0b177f..82263a86c 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java @@ -1,6 +1,8 @@ package io.jsonwebtoken.security; /** + * Provides access to the required length in bits (not bytes) of keys usable with the associated algorithm. + * * @since JJWT_RELEASE_VERSION */ public interface KeyLengthSupplier { diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyResult.java b/api/src/main/java/io/jsonwebtoken/security/KeyResult.java index 7557ba026..c461ebe9d 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyResult.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyResult.java @@ -18,6 +18,16 @@ import javax.crypto.SecretKey; /** + * The result of a {@link KeyAlgorithm} encryption key request, containing the resulting + * {@code JWE encrypted key} and {@code JWE Content Encryption Key (CEK)}, concepts defined in + * JWE Terminology. + * + *

    The result {@link #getContent() content} is the {@code JWE encrypted key}, which will be Base64URL-encoded + * and embedded in the resulting compact JWE string.

    + * + *

    The result {@link #getKey() key} is the {@code JWE Content Encryption Key (CEK)} which will be used to encrypt + * the JWE.

    + * * @since JJWT_RELEASE_VERSION */ public interface KeyResult extends Message, KeySupplier { diff --git a/api/src/main/java/io/jsonwebtoken/security/KeySupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeySupplier.java index 219a28264..ede8bc8de 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeySupplier.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeySupplier.java @@ -18,6 +18,8 @@ import java.security.Key; /** + * Provides access to a cryptographic {@link Key} necessary for signing, wrapping, encryption or decryption algorithms. + * * @since JJWT_RELEASE_VERSION */ public interface KeySupplier { diff --git a/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java b/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java index ea44962e6..87a556105 100644 --- a/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java @@ -16,6 +16,9 @@ package io.jsonwebtoken.security; /** + * Exception thrown when encountering a key or key material that is incomplete or improperly configured or + * formatted and cannot be used as expected. + * * @since JJWT_RELEASE_VERSION */ public class MalformedKeyException extends InvalidKeyException { diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwkBuilder.java index bda3a9417..c88b16ca4 100644 --- a/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwkBuilder.java @@ -19,6 +19,8 @@ import java.security.interfaces.RSAPublicKey; /** + * A {@link PrivateJwkBuilder} that creates {@link RsaPrivateJwk}s. + * * @since JJWT_RELEASE_VERSION */ public interface RsaPrivateJwkBuilder extends PrivateJwkBuilder { diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretKeySignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SecretKeySignatureAlgorithm.java index c6f3a8956..486441373 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecretKeySignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecretKeySignatureAlgorithm.java @@ -18,6 +18,32 @@ import javax.crypto.SecretKey; /** + * A {@link SignatureAlgorithm} that uses a symmetric {@link SecretKey} to both create and verify digital signatures and + * message authentication codes (MAC)s. + * + *

    Key Strength

    + * + *

    Signature algorithm strength is in part attributed to how difficult it is to discover the signing key. As such, + * signature algorithms often require keys of a minimum length to ensure the keys are difficult to discover + * and the algorithm's security properties are maintained.

    + * + *

    The {@code SecretKeySignatureAlgorithm} interface extends the {@link KeyLengthSupplier} interface to represent + * the length in bits (not bytes) a key must have to be used with its implementation. If you do not want to + * worry about lengths and parameters of keys required for an algorithm, it is often easier to automatically generate + * a key that adheres to the algorithms requirements, as discussed below.

    + * + *

    Key Generation

    + * + *

    {@code SecretKeySignatureAlgorithm} extends {@link KeyBuilderSupplier} to enable {@link SecretKey} generation. + * Each secret key signature algorithm instance will return a {@link KeyBuilder} that ensures any created keys will + * have a sufficient length and algorithm parameters required by that algorithm. For example:

    + * + *
    + * SecretKey key = secretKeySignatureAlgorithm.keyBuilder().build();
    + * + *

    The resulting {@code key} is guaranteed to have the correct algorithm parameters and strength/length necessary for + * that exact {@code secretKeySignatureAlgorithm} instance.

    + * * @since JJWT_RELEASE_VERSION */ public interface SecretKeySignatureAlgorithm extends SignatureAlgorithm, KeyBuilderSupplier, KeyLengthSupplier { diff --git a/api/src/main/java/io/jsonwebtoken/security/SecurityException.java b/api/src/main/java/io/jsonwebtoken/security/SecurityException.java index 8b5f8abd8..107600df7 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecurityException.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecurityException.java @@ -18,14 +18,28 @@ import io.jsonwebtoken.JwtException; /** + * A {@code JwtException} attributed to a problem with security-related elements, such as + * cryptographic keys, algorithms, or the underlying Java JCA API. + * * @since 0.10.0 */ public class SecurityException extends JwtException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public SecurityException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public SecurityException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java index 5a0eff67e..f1d78e00e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java @@ -20,11 +20,49 @@ import java.security.Key; /** + * A cryptographic algorithm that computes and verifies the authenticity of data via + * digital signatures or + * message + * authentication codes as defined by the + * JSON Web Signature (JWS) specification. + * + *

    Standard Implementations

    + * + *

    Constant definitions and utility methods for all standard + * JWA (RFC 7518) Signature Algorithms are + * available via the {@link SignatureAlgorithms} utility class.

    + * + *

    "alg" identifier

    + * + *

    {@code SignatureAlgorithm} extends {@link Identifiable}: the value returned from + * {@link Identifiable#getId() getId()} will be used as the JWS "alg" protected header value.

    + * * @since JJWT_RELEASE_VERSION */ public interface SignatureAlgorithm extends Identifiable { + /** + * Compute a digital signature or MAC for the request {@link SignatureRequest#getContent() content} using the + * request {@link SignatureRequest#getKey() key}, returning the digest result. + * + * @param request the signature request representing the plaintext data to be signed or MAC'd and the + * {@code key} used during execution. + * @return the resulting digital signature or MAC. + * @throws SecurityException if there is invalid key input or a problem during digest creation. + */ byte[] sign(SignatureRequest request) throws SecurityException; + /** + * Verify the authenticity of the previously computed digital signature or MAC + * {@link VerifySignatureRequest#getDigest() digest output} represented by the specified {@code request}. + * + * @param request the request representing the previously-computed digital signature or MAC + * {@link VerifySignatureRequest#getDigest() digest output}, original + * {@link VerifySignatureRequest#getContent() content} and + * {@link VerifySignatureRequest#getKey() verification key}. + * @return {@code true} if the authenticity and integrity of the previously-computed digital signature or MAC can + * be verified, {@code false} otherwise. + * @throws SecurityException if there is invalid key input or a problem that won't allow digest verification. + */ boolean verify(VerifySignatureRequest request) throws SecurityException; } diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureException.java b/api/src/main/java/io/jsonwebtoken/security/SignatureException.java index 93253a01b..242626bcb 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureException.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureException.java @@ -16,6 +16,8 @@ package io.jsonwebtoken.security; /** + * Exception thrown if there is problem calculating or verifying a digital signature or message authentication code. + * * @since 0.10.0 */ @SuppressWarnings("deprecation") diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java b/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java index 2647d8807..f2c34a67e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java @@ -18,6 +18,13 @@ import java.security.Key; /** + * A request to a {@link SignatureAlgorithm} to compute a digital signature or + * digital signature or + * message + * authentication code. + *

    The content for signature input will be available via {@link #getContent()}, and the key used to compute + * the signature will be available via {@link #getKey()}.

    + * * @since JJWT_RELEASE_VERSION */ public interface SignatureRequest extends CryptoRequest { diff --git a/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java b/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java index f12ab4efa..f24396a41 100644 --- a/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java @@ -16,6 +16,8 @@ package io.jsonwebtoken.security; /** + * Exception thrown when encountering a key or key material that is not supported or recognized. + * * @since JJWT_RELEASE_VERSION */ public class UnsupportedKeyException extends KeyException { diff --git a/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java b/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java index 2b6632da3..a3748be5e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java @@ -18,6 +18,14 @@ import java.security.Key; /** + * A request to a {@link SignatureAlgorithm} to verify a previously-computed digital signature or + * digital signature or + * message + * authentication code. + * + *

    The content to verify will be available via {@link #getContent()}, the previously-computed signature or MAC will + * be available via {@link #getDigest()}, and the verification key will be available via {@link #getKey()}.

    + * * @since JJWT_RELEASE_VERSION */ public interface VerifySignatureRequest extends SignatureRequest, DigestSupplier { diff --git a/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java b/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java index 8a7688fed..87a611988 100644 --- a/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java @@ -16,6 +16,9 @@ package io.jsonwebtoken.security; /** + * Exception thrown when encountering a key that is not strong enough (of sufficient length) to be used with + * a particular algorithm or in a particular security context. + * * @since 0.10.0 */ public class WeakKeyException extends InvalidKeyException { diff --git a/api/src/test/groovy/io/jsonwebtoken/io/DecodersTest.groovy b/api/src/test/groovy/io/jsonwebtoken/io/DecodersTest.groovy index a10acc74c..1a8ad1a69 100644 --- a/api/src/test/groovy/io/jsonwebtoken/io/DecodersTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/io/DecodersTest.groovy @@ -17,13 +17,17 @@ package io.jsonwebtoken.io import org.junit.Test -import static org.junit.Assert.* +import static org.junit.Assert.assertTrue class DecodersTest { @Test - void testBase64() { + void testPrivateCtor() { new Decoders() //not allowed in java, including here only to pass test coverage assertions + } + + @Test + void testBase64() { assertTrue Decoders.BASE64 instanceof ExceptionPropagatingDecoder assertTrue Decoders.BASE64.decoder instanceof Base64Decoder } diff --git a/api/src/test/groovy/io/jsonwebtoken/io/EncodersTest.groovy b/api/src/test/groovy/io/jsonwebtoken/io/EncodersTest.groovy index d4b323cb1..57d25c724 100644 --- a/api/src/test/groovy/io/jsonwebtoken/io/EncodersTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/io/EncodersTest.groovy @@ -17,13 +17,17 @@ package io.jsonwebtoken.io import org.junit.Test -import static org.junit.Assert.* +import static org.junit.Assert.assertTrue class EncodersTest { @Test - void testBase64() { + void testPrivateCtor() { new Encoders() //not allowed in java, including here only to pass test coverage assertions + } + + @Test + void testBase64() { assertTrue Encoders.BASE64 instanceof ExceptionPropagatingEncoder assertTrue Encoders.BASE64.encoder instanceof Base64Encoder } diff --git a/api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy b/api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy index df4fd5bad..df0d56674 100644 --- a/api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy @@ -1,5 +1,6 @@ package io.jsonwebtoken.lang + import org.junit.Test import java.nio.charset.StandardCharsets @@ -11,6 +12,11 @@ import static org.junit.Assert.* */ class ArraysTest { + @Test + void testPrivateCtor() { + new Arrays() //not allowed in java, including here only to pass test coverage assertions + } + @Test void testCleanWithNull() { assertNull Arrays.clean(null) @@ -29,7 +35,7 @@ class ArraysTest { @Test void testByteArrayLengthWithNull() { - assertEquals 0, Arrays.length((byte[])null) + assertEquals 0, Arrays.length((byte[]) null) } @Test diff --git a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java index 2b5c2b76b..e1b31f01a 100644 --- a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java +++ b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java @@ -35,7 +35,6 @@ */ public class OrgJsonDeserializer implements Deserializer { - @SuppressWarnings("unchecked") @Override public Object deserialize(byte[] bytes) throws DeserializationException { @@ -91,7 +90,7 @@ private List toList(JSONArray a) { int length = a.length(); List list = new ArrayList<>(length); // https://github.com/jwtk/jjwt/issues/380: use a.get(i) and *not* a.toList() for Android compatibility: - for( int i = 0; i < length; i++) { + for (int i = 0; i < length; i++) { Object value = a.get(i); value = convertIfNecessary(value); list.add(value); diff --git a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java index b6ea242cb..2f00d1311 100644 --- a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java +++ b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java @@ -40,9 +40,9 @@ public class OrgJsonSerializer implements Serializer { // we need reflection for these because of Android - see https://github.com/jwtk/jjwt/issues/388 private static final String JSON_WRITER_CLASS_NAME = "org.json.JSONWriter"; - private static final Class[] VALUE_TO_STRING_ARG_TYPES = new Class[]{Object.class}; + private static final Class[] VALUE_TO_STRING_ARG_TYPES = new Class[]{Object.class}; private static final String JSON_STRING_CLASS_NAME = "org.json.JSONString"; - private static final Class JSON_STRING_CLASS; + private static final Class JSON_STRING_CLASS; static { // see see https://github.com/jwtk/jjwt/issues/388 if (Classes.isAvailable(JSON_STRING_CLASS_NAME)) { @@ -83,13 +83,13 @@ private Object toJSONInstance(Object object) { } if (object instanceof JSONObject || object instanceof JSONArray - || JSONObject.NULL.equals(object) || isJSONString(object) - || object instanceof Byte || object instanceof Character - || object instanceof Short || object instanceof Integer - || object instanceof Long || object instanceof Boolean - || object instanceof Float || object instanceof Double - || object instanceof String || object instanceof BigInteger - || object instanceof BigDecimal || object instanceof Enum) { + || JSONObject.NULL.equals(object) || isJSONString(object) + || object instanceof Byte || object instanceof Character + || object instanceof Short || object instanceof Integer + || object instanceof Long || object instanceof Boolean + || object instanceof Float || object instanceof Double + || object instanceof String || object instanceof BigInteger + || object instanceof BigDecimal || object instanceof Enum) { return object; } @@ -114,14 +114,15 @@ private Object toJSONInstance(Object object) { Map map = (Map) object; return toJSONObject(map); } + + if (Objects.isArray(object)) { + object = Collections.arrayToList(object); //sets object to List, will be converted in next if-statement: + } + if (object instanceof Collection) { Collection coll = (Collection) object; return toJSONArray(coll); } - if (Objects.isArray(object)) { - Collection c = Collections.arrayToList(object); - return toJSONArray(c); - } //not an immediately JSON-compatible object and probably a JavaBean (or similar). We can't convert that //directly without using a marshaller of some sort: @@ -145,7 +146,7 @@ private JSONObject toJSONObject(Map m) { return obj; } - private JSONArray toJSONArray(Collection c) { + private JSONArray toJSONArray(Collection c) { JSONArray array = new JSONArray(); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java index d40582afb..682de6829 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java @@ -34,7 +34,7 @@ public class DefaultHeader> extends JwtMap implements Header @Deprecated // TODO: remove for 1.0.0: static final Field DEPRECATED_COMPRESSION_ALGORITHM = Fields.string(Header.DEPRECATED_COMPRESSION_ALGORITHM, "Deprecated Compression Algorithm"); - static final Set> FIELDS = Collections.>setOf(TYPE, CONTENT_TYPE, ALGORITHM, COMPRESSION_ALGORITHM); + static final Set> FIELDS = Collections.>setOf(TYPE, CONTENT_TYPE, ALGORITHM, COMPRESSION_ALGORITHM, DEPRECATED_COMPRESSION_ALGORITHM); protected DefaultHeader(Set> fieldSet) { super(fieldSet); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 18f73da6c..2a8c77877 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -15,7 +15,6 @@ */ package io.jsonwebtoken.impl; -import io.jsonwebtoken.ClaimJwtException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Clock; import io.jsonwebtoken.CompressionCodec; @@ -89,6 +88,9 @@ public class DefaultJwtParser implements JwtParser { private static final JwtTokenizer jwtTokenizer = new JwtTokenizer(); + public static final String INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was: %s."; + public static final String MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was not present in the JWT claims."; + public static final String MISSING_JWS_ALG_MSG = "JWS header does not contain a required 'alg' (Algorithm) header parameter. " + "This header parameter is mandatory per the JWS Specification, Section 4.1.1. See " + @@ -695,14 +697,14 @@ private void validateExpectedClaims(Header header, Claims claims) { if (actualClaimValue == null) { - String msg = String.format(ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, + String msg = String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, expectedClaimName, expectedClaimValue); invalidClaimException = new MissingClaimException(header, claims, msg); } else if (!expectedClaimValue.equals(actualClaimValue)) { - String msg = String.format(ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, + String msg = String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, expectedClaimName, expectedClaimValue, actualClaimValue); invalidClaimException = new IncorrectClaimException(header, claims, msg); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java index 94f757910..4ec1c5d81 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java @@ -30,11 +30,16 @@ public boolean verify(VerifySignatureRequest request) throws SignatureExcep @Override public boolean equals(Object obj) { return this == obj || - (obj instanceof SignatureAlgorithm && ID.equalsIgnoreCase(((SignatureAlgorithm) obj).getId())); + (obj instanceof SignatureAlgorithm && ID.equalsIgnoreCase(((SignatureAlgorithm) obj).getId())); } @Override public int hashCode() { return getId().hashCode(); } + + @Override + public String toString() { + return ID; + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy index 1993819b0..41a84acf0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy @@ -25,8 +25,8 @@ import org.junit.Test import java.security.SecureRandom -import static ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE -import static ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE +import static io.jsonwebtoken.impl.DefaultJwtParser.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE +import static io.jsonwebtoken.impl.DefaultJwtParser.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE import static org.junit.Assert.* class DeprecatedJwtParserTest { diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index cd25d73ab..3b847cd76 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -28,9 +28,9 @@ import org.junit.Test import javax.crypto.SecretKey import java.security.SecureRandom -import static ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE -import static ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE import static io.jsonwebtoken.DateTestUtils.truncateMillis +import static io.jsonwebtoken.impl.DefaultJwtParser.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE +import static io.jsonwebtoken.impl.DefaultJwtParser.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE import static org.junit.Assert.* @SuppressWarnings('GrDeprecatedAPIUsage') diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index cd9dfbcd3..bd60e682c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -7,7 +7,10 @@ import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.SecretKey -import java.security.* +import java.security.MessageDigest +import java.security.PrivateKey +import java.security.PublicKey +import java.security.SecureRandom import java.security.cert.X509Certificate import java.security.interfaces.ECKey import java.security.interfaces.ECPublicKey @@ -20,7 +23,7 @@ import static org.junit.Assert.* class JwksTest { private static final SecretKey SKEY = SignatureAlgorithms.HS256.keyBuilder().build() - private static final KeyPair EC_PAIR = SignatureAlgorithms.ES256.keyPairBuilder().build().toJavaKeyPair() + private static final KeyPair EC_PAIR = SignatureAlgorithms.ES256.keyPairBuilder().build() private static String srandom() { byte[] random = new byte[16]; @@ -28,7 +31,7 @@ class JwksTest { return Encoders.BASE64URL.encode(random); } - static void testProperty(String name, String id, def val, def expectedFieldValue=val) { + static void testProperty(String name, String id, def val, def expectedFieldValue = val) { String cap = "${name.capitalize()}" def key = name == 'publicKeyUse' || name == 'x509CertificateChain' ? EC_PAIR.public : SKEY @@ -84,6 +87,11 @@ class JwksTest { } } + @Test + void testPrivateCtor() { + new Jwks() // for code coverage only + } + @Test void testBuilderWithoutState() { try { @@ -156,9 +164,9 @@ class JwksTest { } static void testThumbprint(int number) { - def algs = SignatureAlgorithms.values().findAll {it instanceof AsymmetricKeySignatureAlgorithm} + def algs = SignatureAlgorithms.values().findAll { it instanceof AsymmetricKeySignatureAlgorithm } - for(def alg : algs) { + for (def alg : algs) { //get test cert: X509Certificate cert = TestCertificates.readTestCertificate(alg) def pubKey = cert.getPublicKey() @@ -183,8 +191,8 @@ class JwksTest { @Test void testSecretJwks() { - Collection algs = SignatureAlgorithms.values().findAll({it instanceof SecretKeySignatureAlgorithm}) as Collection - for(def alg : algs) { + Collection algs = SignatureAlgorithms.values().findAll({ it instanceof SecretKeySignatureAlgorithm }) as Collection + for (def alg : algs) { SecretKey secretKey = alg.keyBuilder().build() def jwk = Jwks.builder().setKey(secretKey).setId('id').build() assertEquals 'oct', jwk.getType() @@ -233,9 +241,9 @@ class JwksTest { @Test void testAsymmetricJwks() { - Collection algs = SignatureAlgorithms.values().findAll({it instanceof AsymmetricKeySignatureAlgorithm}) as Collection + Collection algs = SignatureAlgorithms.values().findAll({ it instanceof AsymmetricKeySignatureAlgorithm }) as Collection - for(def alg : algs) { + for (def alg : algs) { def pair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair PublicKey pub = pair.getPublic() @@ -271,7 +279,7 @@ class JwksTest { void testInvalidCurvePoint() { def algs = [SignatureAlgorithms.ES256, SignatureAlgorithms.ES384, SignatureAlgorithms.ES512] - for(EllipticCurveSignatureAlgorithm alg : algs) { + for (EllipticCurveSignatureAlgorithm alg : algs) { def pair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair ECPublicKey pubKey = pair.getPublic() as ECPublicKey @@ -289,9 +297,9 @@ class JwksTest { } BigInteger p = pubKey.getParams().getCurve().getField().getP() - def outOfFieldRange = [BigInteger.ZERO, BigInteger.ONE,p, p.add(BigInteger.valueOf(1))] - for(def x : outOfFieldRange) { - Map modified = new LinkedHashMap<>(jwk) + def outOfFieldRange = [BigInteger.ZERO, BigInteger.ONE, p, p.add(BigInteger.valueOf(1))] + for (def x : outOfFieldRange) { + Map modified = new LinkedHashMap<>(jwk) modified.put('x', Converters.BIGINT.applyTo(x)) try { Jwks.builder().putAll(modified).build() @@ -300,8 +308,8 @@ class JwksTest { assertEquals(expected, ike.getMessage()) } } - for(def y : outOfFieldRange) { - Map modified = new LinkedHashMap<>(jwk) + for (def y : outOfFieldRange) { + Map modified = new LinkedHashMap<>(jwk) modified.put('y', Converters.BIGINT.applyTo(y)) try { Jwks.builder().putAll(modified).build() @@ -320,6 +328,7 @@ class JwksTest { InvalidECPublicKey(ECPublicKey good) { this.good = good; } + @Override ECPoint getW() { return ECPoint.POINT_INFINITY // bad value, should make all 'contains' validations fail diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateConstructorsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateConstructorsTest.groovy index 1eed4e4a8..bb5ea175d 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateConstructorsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateConstructorsTest.groovy @@ -1,12 +1,14 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.impl.lang.Conditions +import io.jsonwebtoken.lang.Classes import org.junit.Test class PrivateConstructorsTest { @Test void testPrivateCtors() { // for code coverage only + new Classes() new SignatureAlgorithmsBridge() new EncryptionAlgorithmsBridge() new KeyAlgorithmsBridge() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAKey.groovy index 02bd7df2c..da360af64 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAKey.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAKey.groovy @@ -1,29 +1,28 @@ package io.jsonwebtoken.impl.security -import java.security.Key import java.security.interfaces.RSAKey -class TestRSAKey implements RSAKey, Key { +class TestRSAKey extends TestKey implements RSAKey { - final T src + final def src - TestRSAKey(T key) { + TestRSAKey(def key) { this.src = key } @Override String getAlgorithm() { - return src.getAlgorithm() + return src.algorithm } @Override String getFormat() { - return src.getFormat() + return src.format } @Override byte[] getEncoded() { - return src.getEncoded() + return src.encoded } @Override diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAMultiPrimePrivateCrtKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAMultiPrimePrivateCrtKey.groovy index 5782444ae..274cf4944 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAMultiPrimePrivateCrtKey.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAMultiPrimePrivateCrtKey.groovy @@ -4,7 +4,7 @@ import java.security.interfaces.RSAMultiPrimePrivateCrtKey import java.security.interfaces.RSAPrivateCrtKey import java.security.spec.RSAOtherPrimeInfo -class TestRSAMultiPrimePrivateCrtKey extends TestRSAPrivateKey implements RSAMultiPrimePrivateCrtKey { +class TestRSAMultiPrimePrivateCrtKey extends TestRSAPrivateKey implements RSAMultiPrimePrivateCrtKey { private final List infos diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAPrivateKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAPrivateKey.groovy index 7e949bb20..4a9ed4145 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAPrivateKey.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAPrivateKey.groovy @@ -2,9 +2,9 @@ package io.jsonwebtoken.impl.security import java.security.interfaces.RSAPrivateKey -class TestRSAPrivateKey extends TestRSAKey implements RSAPrivateKey { +class TestRSAPrivateKey extends TestRSAKey implements RSAPrivateKey { - TestRSAPrivateKey(T key) { + TestRSAPrivateKey(RSAPrivateKey key) { super(key) } From ca02283e523b745a0f4fb146d314cdf9f86be753 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 13 May 2022 13:33:34 -0700 Subject: [PATCH 42/75] - JavaDoc additions and syntax cleanup cont'd - Minor work to fix compilation errors on a few Groovy test classes --- .lift/config.toml | 2 +- CHANGELOG.md | 6 ++ .../io/jsonwebtoken/JwtHandlerAdapter.java | 2 +- .../main/java/io/jsonwebtoken/JwtParser.java | 11 ++- .../io/jsonwebtoken/JwtParserBuilder.java | 17 ++++ .../jsonwebtoken/JwtHandlerAdapterTest.groovy | 14 ++-- impl/.lift.toml | 3 - .../jsonwebtoken/impl/DefaultJweBuilder.java | 9 +-- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 81 +++++++++---------- .../jsonwebtoken/impl/DefaultJwtParser.java | 2 + .../impl/security/DefaultJwkContext.java | 6 +- .../impl/security/DefaultValueGetter.java | 2 +- 12 files changed, 92 insertions(+), 63 deletions(-) delete mode 100644 impl/.lift.toml diff --git a/.lift/config.toml b/.lift/config.toml index a833985ea..0e7a807ea 100644 --- a/.lift/config.toml +++ b/.lift/config.toml @@ -1,4 +1,4 @@ -ignoreRules = ["MissingOverride"] +ignoreRules = ["MissingOverride", "MissingSummary", "InconsistentCapitalization", "JavaUtilDate"] ignoreFiles = ''' **/test/** ''' diff --git a/CHANGELOG.md b/CHANGELOG.md index d08a17790..f32f243f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ allow for customization of the JCA `Provider` and `SecureRandom` during Key or KeyPair generation if desired, whereas the old enum-based static utility methods did not. +#### Backwards Compatibility Warning + +* `io.jsonwebtoken.JwtHandlerAdapter` has been changed to add the `abstract` modifier. This class was never intended + to be instantiated directly, and is provided for subclassing benefits. The missing modifier has been added to ensure + the class is used as it had always been intended. + ### 0.11.5 This patch release adds additional security guards against an ECDSA bug in Java SE versions 15-15.0.6, 17-17.0.2, and 18 diff --git a/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java b/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java index 273c21c22..e96d8b1e2 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java @@ -28,7 +28,7 @@ * @param the type of object to return to the parser caller after handling the parsed JWT. * @since 0.2 */ -public class JwtHandlerAdapter implements JwtHandler { +public abstract class JwtHandlerAdapter implements JwtHandler { @Override public T onPlaintextJwt(Jwt jwt) { diff --git a/api/src/main/java/io/jsonwebtoken/JwtParser.java b/api/src/main/java/io/jsonwebtoken/JwtParser.java index 6ce1388e8..cc7b86c82 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParser.java @@ -31,6 +31,13 @@ */ public interface JwtParser { + /** + * Deprecated - this was an implementation detail accidentally added to the public interface. This + * will be removed in a future release. + * + * @deprecated since JJWT_RELEASE_VERSION, to be removed in a future relase. + */ + @Deprecated char SEPARATOR_CHAR = '.'; /** @@ -594,7 +601,7 @@ Jws parsePlaintextJws(String plaintextJws) * @throws IllegalArgumentException if the {@code claimsJws} string is {@code null} or empty or only whitespace * @see #parsePlaintextJwt(String) * @see #parsePlaintextJws(String) - * @see #parsePlaintextJwe(String) + * @see #parsePlaintextJwe(String) * @see #parseClaimsJwt(String) * @see #parseClaimsJwe(String) * @see #parse(String, JwtHandler) @@ -622,7 +629,7 @@ Jws parseClaimsJws(String claimsJws) * @throws SecurityException if the {@code plaintextJwe} JWE decryption fails * @throws IllegalArgumentException if the {@code plaintextJwe} string is {@code null} or empty or only whitespace * @see #parsePlaintextJwt(String) - * @see #parsePlaintextJws(String) + * @see #parsePlaintextJws(String) * @see #parseClaimsJwt(String) * @see #parseClaimsJws(String) * @see #parseClaimsJwe(String) diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index 8bbfdae97..c2989c064 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -19,6 +19,7 @@ import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.lang.Builder; import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.EncryptionAlgorithms; import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.SignatureAlgorithm; @@ -365,6 +366,22 @@ public interface JwtParserBuilder extends Builder { @Deprecated JwtParserBuilder setSigningKeyResolver(SigningKeyResolver signingKeyResolver); + /** + * Adds the specified AEAD encryption algorithms to the parser's total set of supported encryption algorithms, + * overwriting any previously-added algorithms with the same {@link AeadAlgorithm#getId() id}s. + * + *

    There may be only one registered {@code AeadAlgorithm} per algorithm {@code id}, and the {@code encAlgs} + * collection is added in iteration order; if a duplicate id is found when iterating the {@code encAlgs} + * collection, the later element will evict any previously-added algorithm with the same {@code id}.

    + * + *

    Finally, the {@link EncryptionAlgorithms#values() JWA standard encryption algorithms} are added last, + * after those in the {@code encAlgs} collection, to ensure that JWA standard algorithms cannot be + * accidentally replaced.

    + * + * @param encAlgs collection of AEAD encryption algorithms to add to the parser's total set of supported + * encryption algorithms. + * @return the builder for method chaining. + */ JwtParserBuilder addEncryptionAlgorithms(Collection encAlgs); JwtParserBuilder addSignatureAlgorithms(Collection> sigAlgs); diff --git a/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy b/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy index 749885727..5915cb684 100644 --- a/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy @@ -15,6 +15,7 @@ */ package io.jsonwebtoken +import org.junit.Before import org.junit.Test import static org.junit.Assert.assertEquals @@ -22,9 +23,15 @@ import static org.junit.Assert.fail class JwtHandlerAdapterTest { + private JwtHandlerAdapter handler + + @Before + void setUp() { + handler = new JwtHandlerAdapter(){} + } + @Test void testOnPlaintextJwt() { - def handler = new JwtHandlerAdapter(); try { handler.onPlaintextJwt(null) fail() @@ -35,7 +42,6 @@ class JwtHandlerAdapterTest { @Test void testOnClaimsJwt() { - def handler = new JwtHandlerAdapter(); try { handler.onClaimsJwt(null) fail() @@ -46,7 +52,6 @@ class JwtHandlerAdapterTest { @Test void testOnPlaintextJws() { - def handler = new JwtHandlerAdapter(); try { handler.onPlaintextJws(null) fail() @@ -57,7 +62,6 @@ class JwtHandlerAdapterTest { @Test void testOnClaimsJws() { - def handler = new JwtHandlerAdapter(); try { handler.onClaimsJws(null) fail() @@ -68,7 +72,6 @@ class JwtHandlerAdapterTest { @Test void testOnPlaintextJwe() { - def handler = new JwtHandlerAdapter(); try { handler.onPlaintextJwe(null) fail() @@ -79,7 +82,6 @@ class JwtHandlerAdapterTest { @Test void testOnClaimsJwe() { - def handler = new JwtHandlerAdapter(); try { handler.onClaimsJwe(null) fail() diff --git a/impl/.lift.toml b/impl/.lift.toml deleted file mode 100644 index 0ccd3d2ad..000000000 --- a/impl/.lift.toml +++ /dev/null @@ -1,3 +0,0 @@ -# JavaDoc purity is not necessary in the impl module since it's never intended -# to be consumed by users: -ignoreRules = ["MissingSummary", "InconsistentCapitalization", "JavaUtilDate"] \ No newline at end of file diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java index 656bd81ad..82e00b7a3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java @@ -2,7 +2,6 @@ import io.jsonwebtoken.JweBuilder; import io.jsonwebtoken.JweHeader; -import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.impl.lang.Function; import io.jsonwebtoken.impl.lang.PropagatingExceptionFunction; import io.jsonwebtoken.impl.lang.Services; @@ -167,10 +166,10 @@ public String compact() { String base64UrlEncodedTag = base64UrlEncoder.encode(tag); return - base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + - base64UrlEncodedEncryptedCek + JwtParser.SEPARATOR_CHAR + - base64UrlEncodedIv + JwtParser.SEPARATOR_CHAR + - base64UrlEncodedCiphertext + JwtParser.SEPARATOR_CHAR + + base64UrlEncodedHeader + DefaultJwtParser.SEPARATOR_CHAR + + base64UrlEncodedEncryptedCek + DefaultJwtParser.SEPARATOR_CHAR + + base64UrlEncodedIv + DefaultJwtParser.SEPARATOR_CHAR + + base64UrlEncodedCiphertext + DefaultJwtParser.SEPARATOR_CHAR + base64UrlEncodedTag; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index 703168b66..5e2f721ed 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -20,7 +20,6 @@ import io.jsonwebtoken.Header; import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtBuilder; -import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.impl.lang.Function; import io.jsonwebtoken.impl.lang.LegacyServices; import io.jsonwebtoken.impl.lang.PropagatingExceptionFunction; @@ -59,14 +58,14 @@ public class DefaultJwtBuilder> implements JwtBuilder protected Claims claims; protected String payload; - private SignatureAlgorithm algorithm = SignatureAlgorithms.NONE; + private SignatureAlgorithm algorithm = SignatureAlgorithms.NONE; private Function, byte[]> signFunction; private Key key; protected Serializer> serializer; - protected Function, byte[]> headerSerializer; - protected Function, byte[]> claimsSerializer; + protected Function, byte[]> headerSerializer; + protected Function, byte[]> claimsSerializer; protected Encoder base64UrlEncoder = Encoders.BASE64URL; protected CompressionCodec compressionCodec; @@ -74,28 +73,28 @@ public class DefaultJwtBuilder> implements JwtBuilder @Override public T setProvider(Provider provider) { this.provider = provider; - return (T)this; + return (T) this; } @Override public T setSecureRandom(SecureRandom secureRandom) { this.secureRandom = secureRandom; - return (T)this; + return (T) this; } @SuppressWarnings("rawtypes") - protected Function, byte[]> wrap(final Serializer> serializer, String which) { + protected Function, byte[]> wrap(final Serializer> serializer, String which) { // TODO for 1.0 - these should throw SerializationException not IllegalArgumentException // IAE is being retained for backwards pre-1.0 behavior compatibility Class clazz = "header".equals(which) ? IllegalStateException.class : IllegalArgumentException.class; return new PropagatingExceptionFunction<>(clazz, - "Unable to serialize " + which + " to JSON.", - new Function, byte[]>() { - @Override - public byte[] apply(Map map) { - return serializer.serialize(map); + "Unable to serialize " + which + " to JSON.", + new Function, byte[]>() { + @Override + public byte[] apply(Map map) { + return serializer.serialize(map); + } } - } ); } @@ -105,26 +104,26 @@ public T serializeToJsonWith(final Serializer> serializer) { this.serializer = serializer; this.headerSerializer = wrap(serializer, "header"); this.claimsSerializer = wrap(serializer, "claims"); - return (T)this; + return (T) this; } @Override public T base64UrlEncodeWith(Encoder base64UrlEncoder) { Assert.notNull(base64UrlEncoder, "base64UrlEncoder cannot be null."); this.base64UrlEncoder = base64UrlEncoder; - return (T)this; + return (T) this; } @Override public T setHeader(Header header) { this.header = header; - return (T)this; + return (T) this; } @Override public T setHeader(Map header) { this.header = new DefaultHeader<>(header); - return (T)this; + return (T) this; } @Override @@ -133,7 +132,7 @@ public T setHeaderParams(Map params) { Header header = ensureHeader(); header.putAll(params); } - return (T)this; + return (T) this; } protected Header ensureHeader() { @@ -146,30 +145,30 @@ protected Header ensureHeader() { @Override public T setHeaderParam(String name, Object value) { ensureHeader().put(name, value); - return (T)this; + return (T) this; } @Override public T signWith(Key key) throws InvalidKeyException { Assert.notNull(key, "Key argument cannot be null."); - SignatureAlgorithm alg = (SignatureAlgorithm)SignatureAlgorithms.forSigningKey(key); + SignatureAlgorithm alg = (SignatureAlgorithm) SignatureAlgorithms.forSigningKey(key); return signWith(key, alg); } @Override - public T signWith(K key, final SignatureAlgorithm alg) throws InvalidKeyException { + public T signWith(K key, final SignatureAlgorithm alg) throws InvalidKeyException { Assert.notNull(key, "Key argument cannot be null."); Assert.notNull(alg, "SignatureAlgorithm cannot be null."); this.key = key; - this.algorithm = (SignatureAlgorithm)alg; + this.algorithm = (SignatureAlgorithm) alg; this.signFunction = new PropagatingExceptionFunction<>(SignatureException.class, - "Unable to compute " + alg.getId() + " signature.", new Function, byte[]>() { + "Unable to compute " + alg.getId() + " signature.", new Function, byte[]>() { @Override public byte[] apply(SignatureRequest request) { return algorithm.sign(request); } }); - return (T)this; + return (T) this; } @SuppressWarnings("deprecation") // TODO: remove method for 1.0 @@ -177,7 +176,7 @@ public byte[] apply(SignatureRequest request) { public T signWith(Key key, io.jsonwebtoken.SignatureAlgorithm alg) throws InvalidKeyException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); alg.assertValidSigningKey(key); //since 0.10.0 for https://github.com/jwtk/jjwt/issues/334 - return signWith(key, (SignatureAlgorithm)SignatureAlgorithmsBridge.forId(alg.getValue())); + return signWith(key, (SignatureAlgorithm) SignatureAlgorithmsBridge.forId(alg.getValue())); } @SuppressWarnings("deprecation") // TODO: remove method for 1.0 @@ -209,13 +208,13 @@ public T signWith(io.jsonwebtoken.SignatureAlgorithm alg, Key key) { public T compressWith(CompressionCodec compressionCodec) { Assert.notNull(compressionCodec, "compressionCodec cannot be null"); this.compressionCodec = compressionCodec; - return (T)this; + return (T) this; } @Override public T setPayload(String payload) { this.payload = payload; - return (T)this; + return (T) this; } protected Claims ensureClaims() { @@ -228,19 +227,19 @@ protected Claims ensureClaims() { @Override public T setClaims(Claims claims) { this.claims = claims; - return (T)this; + return (T) this; } @Override public T setClaims(Map claims) { this.claims = new DefaultClaims(claims); - return (T)this; + return (T) this; } @Override public T addClaims(Map claims) { ensureClaims().putAll(claims); - return (T)this; + return (T) this; } @Override @@ -252,7 +251,7 @@ public T setIssuer(String iss) { claims.setIssuer(iss); } } - return (T)this; + return (T) this; } @Override @@ -264,7 +263,7 @@ public T setSubject(String sub) { claims.setSubject(sub); } } - return (T)this; + return (T) this; } @Override @@ -276,7 +275,7 @@ public T setAudience(String aud) { claims.setAudience(aud); } } - return (T)this; + return (T) this; } @Override @@ -289,7 +288,7 @@ public T setExpiration(Date exp) { this.claims.setExpiration(exp); } } - return (T)this; + return (T) this; } @Override @@ -302,7 +301,7 @@ public T setNotBefore(Date nbf) { this.claims.setNotBefore(nbf); } } - return (T)this; + return (T) this; } @Override @@ -315,7 +314,7 @@ public T setIssuedAt(Date iat) { this.claims.setIssuedAt(iat); } } - return (T)this; + return (T) this; } @Override @@ -327,7 +326,7 @@ public T setId(String jti) { claims.setId(jti); } } - return (T)this; + return (T) this; } @Override @@ -345,7 +344,7 @@ public T claim(String name, Object value) { } } - return (T)this; + return (T) this; } @Override @@ -390,18 +389,18 @@ public String compact() { String base64UrlEncodedHeader = base64UrlEncoder.encode(headerBytes); String base64UrlEncodedBody = base64UrlEncoder.encode(bytes); - String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody; + String jwt = base64UrlEncodedHeader + DefaultJwtParser.SEPARATOR_CHAR + base64UrlEncodedBody; if (key != null) { //jwt must be signed: byte[] data = jwt.getBytes(StandardCharsets.US_ASCII); SignatureRequest request = new DefaultSignatureRequest<>(provider, secureRandom, data, key); byte[] signature = signFunction.apply(request); String base64UrlSignature = base64UrlEncoder.encode(signature); - jwt += JwtParser.SEPARATOR_CHAR + base64UrlSignature; + jwt += DefaultJwtParser.SEPARATOR_CHAR + base64UrlSignature; } else { // no signature (plaintext), but must terminate w/ a period, see // https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-6.1 - jwt += JwtParser.SEPARATOR_CHAR; + jwt += DefaultJwtParser.SEPARATOR_CHAR; } return jwt; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 2a8c77877..862fc370c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -84,6 +84,8 @@ @SuppressWarnings("unchecked") public class DefaultJwtParser implements JwtParser { + static final char SEPARATOR_CHAR = '.'; + private static final int MILLISECONDS_PER_SECOND = 1000; private static final JwtTokenizer jwtTokenizer = new JwtTokenizer(); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java index 94050eadf..66f0dd27d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java @@ -20,7 +20,7 @@ public class DefaultJwkContext extends JwtMap implements JwkConte private static final Set> DEFAULT_FIELDS; - static { // assume all known fields: + static { // assume all JWA fields: Set> set = new LinkedHashSet<>(); set.addAll(DefaultSecretJwk.FIELDS); // Private/Secret JWKs has both public and private fields set.addAll(DefaultEcPrivateJwk.FIELDS); // Private JWKs have both public and private fields @@ -77,8 +77,8 @@ private DefaultJwkContext(Set> fields, JwkContext other, boolean rem @Override protected String getName() { - Object value = values.get("kty"); - if ("oct".equals(value)) { + Object value = values.get(AbstractJwk.KTY.getId()); + if (DefaultSecretJwk.TYPE_VALUE.equals(value)) { value = "Secret"; } return value != null ? value + " JWK" : "JWK"; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java index 64e461929..3676fd071 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java @@ -36,7 +36,7 @@ private String name() { } else if (values instanceof Header) { return "JWT header"; } else if (values instanceof Jwk || values instanceof JwkContext) { - Object value = values.get(AbstractJwk.KTY); + Object value = values.get(AbstractJwk.KTY.getId()); if (DefaultSecretJwk.TYPE_VALUE.equals(value)) { value = "Secret"; } From 310aeebbf30e2edb592477d5c1d7a635534f4c02 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 13 May 2022 14:42:21 -0700 Subject: [PATCH 43/75] - JavaDoc additions and syntax cleanup cont'd - Minor work to fix compilation errors on a few Groovy test classes --- .lift/config.toml | 4 +++- api/src/main/java/io/jsonwebtoken/JweBuilder.java | 2 +- impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java | 2 +- .../main/java/io/jsonwebtoken/impl/security/ConcatKDF.java | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.lift/config.toml b/.lift/config.toml index 0e7a807ea..a8dd63277 100644 --- a/.lift/config.toml +++ b/.lift/config.toml @@ -1,4 +1,6 @@ -ignoreRules = ["MissingOverride", "MissingSummary", "InconsistentCapitalization", "JavaUtilDate"] +ignoreRules = ["MissingOverride", "MissingSummary", "InconsistentCapitalization", "JavaUtilDate", + "TypeParameterUnusedInFormals", "JavaLangClash", "InlineFormatString"] ignoreFiles = ''' +impl/** **/test/** ''' diff --git a/api/src/main/java/io/jsonwebtoken/JweBuilder.java b/api/src/main/java/io/jsonwebtoken/JweBuilder.java index c1a91f3ff..5f6c5c2b5 100644 --- a/api/src/main/java/io/jsonwebtoken/JweBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JweBuilder.java @@ -29,7 +29,7 @@ public interface JweBuilder extends JwtBuilder { /** - * Encrypt the resulting JWE with the specified {@link AeadAlgorithm} Content Encryption Algorithm. They + * Encrypt the resulting JWE with the specified {@link AeadAlgorithm} Content Encryption Algorithm. The * key used to perform the encryption must be supplied by calling {@link #withKey(SecretKey)} or * {@link #withKeyFrom(Key, KeyAlgorithm)}. * diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java index 86a09e668..4f9685dfc 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java @@ -47,7 +47,7 @@ public static long toLong(byte[] bytes) { ((bytes[4] & 0xFFL) << 24) | ((bytes[5] & 0xFFL) << 16) | ((bytes[6] & 0xFFL) << 8) | - ((bytes[7] & 0xFFL)); + (bytes[7] & 0xFFL); } public static int toInt(byte[] bytes) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java index e1c40967f..0f2280e05 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java @@ -83,7 +83,7 @@ public SecretKey deriveKey(final byte[] Z, final long derivedKeyBitLength, final // Section 5.8.1.1, Process step #1: final double repsd = derivedKeyBitLength / (double) this.hashBitLength; - final long reps = (long) (Math.ceil(repsd)); + final long reps = (long) Math.ceil(repsd); // If repsd didn't result in a whole number, the last derived key byte will be partially filled per // Section 5.8.1.1, Process step #6: final boolean kLastPartial = repsd != (double) reps; From eb5cfe763584c76444a1e90c86e0a1a985394f6f Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 13 May 2022 15:16:51 -0700 Subject: [PATCH 44/75] - JavaDoc additions and syntax cleanup cont'd - Minor work to fix compilation errors on a few Groovy test classes --- api/src/main/java/io/jsonwebtoken/JweBuilder.java | 2 +- api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/JweBuilder.java b/api/src/main/java/io/jsonwebtoken/JweBuilder.java index 5f6c5c2b5..891a10537 100644 --- a/api/src/main/java/io/jsonwebtoken/JweBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JweBuilder.java @@ -29,7 +29,7 @@ public interface JweBuilder extends JwtBuilder { /** - * Encrypt the resulting JWE with the specified {@link AeadAlgorithm} Content Encryption Algorithm. The + * Encrypts the constructed JWT with the specified {@link AeadAlgorithm} Content Encryption Algorithm. The * key used to perform the encryption must be supplied by calling {@link #withKey(SecretKey)} or * {@link #withKeyFrom(Key, KeyAlgorithm)}. * diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java index b3ce73b08..ce1620417 100644 --- a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java @@ -47,7 +47,7 @@ public interface JwkBuilder, T extends JwkBuilde T put(String name, Object value); /** - * Sets one or more JWK properties by name. If any {@code name} has a {@code value} that is {@code null}, + * Sets one or more JWK properties by name. If any {@code name} has a value that is {@code null}, * an empty {@link java.util.Collection}, or an empty {@link Map}, the property will be removed from the JWK. * * @param values one or more name/value pairs to set on the JWK. From 766f1345acf2e096defee7b74832af7c7c2f6324 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 13 May 2022 15:35:30 -0700 Subject: [PATCH 45/75] - JavaDoc cont'd --- api/src/main/java/io/jsonwebtoken/Claims.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/Claims.java b/api/src/main/java/io/jsonwebtoken/Claims.java index 8541d40db..9311e5902 100644 --- a/api/src/main/java/io/jsonwebtoken/Claims.java +++ b/api/src/main/java/io/jsonwebtoken/Claims.java @@ -24,12 +24,11 @@ *

    This is ultimately a JSON map and any values can be added to it, but JWT standard names are provided as * type-safe getters and setters for convenience.

    * - *

    Because this interface extends {@code Map<String, Object>}, if you would like to add your own properties, + *

    Because this interface extends Map<String, Object>, if you would like to add your own properties, * you simply use map methods, for example:

    * - *
    - * claims.{@link Map#put(Object, Object) put}("someKey", "someValue");
    - * 
    + *
    + * claims.{@link Map#put(Object, Object) put}("someKey", "someValue");
    * *

    Creation

    * From 4735217dce0ebcc6f3e4101c77166ec04e7df748 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 20 May 2022 14:33:31 -0700 Subject: [PATCH 46/75] Code coverage updates cont'd --- .../io/jsonwebtoken/JwtParserBuilder.java | 6 +- .../io/jsonwebtoken/impl/DefaultClaims.java | 7 +- .../jsonwebtoken/impl/DefaultJwtParser.java | 77 ++- .../impl/DefaultJwtParserBuilder.java | 33 +- .../java/io/jsonwebtoken/impl/JwtMap.java | 2 +- .../impl/lang/JwtDateConverter.java | 3 +- .../DeprecatedJwtParserTest.groovy | 28 ++ .../groovy/io/jsonwebtoken/JwtsTest.groovy | 444 +++++++++++++++++- .../impl/DefaultClaimsTest.groovy | 6 +- .../impl/DefaultJwtParserBuilderTest.groovy | 44 +- .../io/jsonwebtoken/impl/JwtMapTest.groovy | 2 +- 11 files changed, 562 insertions(+), 90 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index c2989c064..72bc3773e 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -258,7 +258,7 @@ public interface JwtParserBuilder extends Builder { * MUST be a valid key for the signature algorithm ({@code alg} header) used for the JWS.

    * *

    If there is any chance that the parser will encounter JWSs - * that need different signature verification keys based on the JWS being parsed, it is strongly + * that need different signature verification keys based on the JWS being parsed, or JWEs, it is strongly * recommended to configure your own {@link Locator} via the * {@link #setKeyLocator(Locator) setKeyLocator} method instead of using this one.

    * @@ -279,8 +279,8 @@ public interface JwtParserBuilder extends Builder { * key for both the key management algorithm ({@code alg} header) and the content encryption algorithm * ({@code enc} header) used for the JWE.

    * - *

    If there is any chance that the parser will encounter JWEs - * that need different decryption keys based on the JWE being parsed, it is strongly recommended to configure + *

    If there is any chance that the parser will encounter JWEs that need different decryption keys based on the + * JWE being parsed, or JWSs, it is strongly recommended to configure * your own {@link Locator Locator} via the {@link #setKeyLocator(Locator) setKeyLocator} method instead of * using this one.

    * diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 5e3b56711..ad5bd70e5 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -57,6 +57,11 @@ public DefaultClaims(Map map) { super(FIELDS, map); } + @Override + protected String getName() { + return "JWT Claim"; + } + @Override public String getIssuer() { return idiomaticGet(ISSUER); @@ -152,7 +157,7 @@ public T get(String claimName, Class requiredType) { try { value = JwtDateConverter.toDate(value); // NOT specDate logic } catch (Exception e) { - String msg = "Cannot create Date from '" + claimName + "' value [" + value + "]. Cause: " + e.getMessage(); + String msg = "Cannot create Date from '" + claimName + "' value '" + value + "'. Cause: " + e.getMessage(); throw new IllegalArgumentException(msg, e); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 862fc370c..086d1928c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -103,6 +103,14 @@ public class DefaultJwtParser implements JwtParser { "This header parameter is mandatory per the JWE Specification, Section 4.1.1. See " + "https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.1 for more information."; + public static final String MISSING_JWS_DIGEST_MSG_FMT = + "The JWS header references signature algorithm '%s' but the compact JWE string is missing the " + + "required signature."; + + public static final String MISSING_JWE_DIGEST_MSG_FMT = + "The JWE header references key management algorithm '%s' but the compact JWE string is missing the " + + "required AAD authentication tag."; + private static final String MISSING_ENC_MSG = "JWE header does not contain a required 'enc' (Encryption Algorithm) header parameter. " + "This header parameter is mandatory per the JWE Specification, Section 4.1.2. See " + @@ -165,7 +173,7 @@ private static Function encFn(Collection> keyAlgorithmLocator; - private final Function keyLocator; + private final Function, Key> keyLocator; private Decoder base64UrlDecoder = Decoders.BASE64URL; @@ -200,7 +208,7 @@ public DefaultJwtParser() { DefaultJwtParser(Provider provider, SigningKeyResolver signingKeyResolver, boolean enableUnsecuredJws, - Function keyLocator, + Function, Key> keyLocator, Clock clock, long allowedClockSkewMillis, Claims expectedClaims, @@ -399,6 +407,8 @@ private static boolean hasContentType(Header header) { String msg = tokenized instanceof TokenizedJwe ? MISSING_JWE_ALG_MSG : MISSING_JWS_ALG_MSG; throw new MalformedJwtException(msg); } + + final String base64UrlDigest = tokenized.getDigest(); if (SignatureAlgorithms.NONE.getId().equalsIgnoreCase(alg)) { if (tokenized instanceof TokenizedJwe) { throw new MalformedJwtException(JWE_NONE_MSG); @@ -408,16 +418,13 @@ private static boolean hasContentType(Header header) { String msg = UNSECURED_DISABLED_MSG_PREFIX + header; throw new UnsupportedJwtException(msg); } - if (Strings.hasText(tokenized.getDigest())) { + if (Strings.hasText(base64UrlDigest)) { throw new MalformedJwtException(JWS_NONE_SIG_MISMATCH_MSG); } } else { // something other than 'none'. Must have a digest component: - if (!Strings.hasText(tokenized.getDigest())) { - String type = tokenized instanceof TokenizedJwe ? "JWE" : "JWS"; - String algType = tokenized instanceof TokenizedJwe ? "key management" : "signature"; - String digestType = tokenized instanceof TokenizedJwe ? "AAD authentication tag" : "signature"; - String msg = "The " + type + " header references " + algType + " algorithm '" + alg + "' but the " + - "compact " + type + " string is missing the required " + digestType + "."; + if (!Strings.hasText(base64UrlDigest)) { + String fmt = tokenized instanceof TokenizedJwe ? MISSING_JWE_DIGEST_MSG_FMT : MISSING_JWS_DIGEST_MSG_FMT; + String msg = String.format(fmt, alg); throw new MalformedJwtException(msg); } } @@ -460,10 +467,10 @@ private static boolean hasContentType(Header header) { // https://datatracker.ietf.org/doc/html/rfc7516#section-5.1, Step 14. final byte[] aad = base64UrlHeader.getBytes(StandardCharsets.US_ASCII); - base64Url = tokenizedJwe.getDigest(); - if (Strings.hasText(base64Url)) { - tag = base64UrlDecode(base64Url, "JWE AAD Authentication Tag"); - } + base64Url = base64UrlDigest; + //guaranteed to be non-empty via the `alg` check (~ line 423) above: + Assert.hasText(base64Url, "JWE AAD Authentication Tag cannot be null or empty."); + tag = base64UrlDecode(base64Url, "JWE AAD Authentication Tag"); if (Arrays.length(tag) == 0) { String msg = "Compact JWE strings must always contain an AAD Authentication Tag."; throw new MalformedJwtException(msg); @@ -474,18 +481,12 @@ private static boolean hasContentType(Header header) { throw new MalformedJwtException(MISSING_ENC_MSG); } final AeadAlgorithm encAlg = this.encryptionAlgorithmLocator.apply(jweHeader); - if (encAlg == null) { - String msg = "Unrecognized JWE encryption algorithm '" + enc + "'."; - throw new UnsupportedJwtException(msg); - } + Assert.stateNotNull(encAlg, "JWE Encryption Algorithm cannot be null."); @SuppressWarnings("rawtypes") final KeyAlgorithm keyAlg = this.keyAlgorithmLocator.apply(jweHeader); - if (keyAlg == null) { - String msg = "Unrecognized JWE key algorithm '" + alg + "'."; - throw new UnsupportedJwtException(msg); - } + Assert.stateNotNull(keyAlg, "JWE Key Algorithm cannot be null."); - final Key key = ((Function) this.keyLocator).apply(jweHeader); + final Key key = this.keyLocator.apply(jweHeader); if (key == null) { String msg = "Cannot decrypt JWE payload: unable to locate key for JWE with header: " + jweHeader; throw new UnsupportedJwtException(msg); @@ -530,11 +531,11 @@ private static boolean hasContentType(Header header) { if (header instanceof JweHeader) { jwt = new DefaultJwe<>((JweHeader) header, body, iv, tag); } else { // JWS - if (!Strings.hasText(tokenized.getDigest()) && SignatureAlgorithms.NONE.getId().equalsIgnoreCase(alg)) { + if (!Strings.hasText(base64UrlDigest) && SignatureAlgorithms.NONE.getId().equalsIgnoreCase(alg)) { //noinspection rawtypes jwt = new DefaultJwt(header, body); } else { - jwt = new DefaultJws<>((JwsHeader) header, body, tokenized.getDigest()); + jwt = new DefaultJws<>((JwsHeader) header, body, base64UrlDigest); } } @@ -553,21 +554,7 @@ private static boolean hasContentType(Header header) { String msg = "Unsupported signature algorithm '" + alg + "'"; throw new SignatureException(msg, e); } - if (algorithm == null) { - String msg = "Unrecognized JWS signature algorithm '" + alg + "'."; - throw new UnsupportedJwtException(msg); - } - - String digest = tokenized.getDigest(); - - if (SignatureAlgorithms.NONE.equals(algorithm) && Strings.hasText(digest)) { - //'none' algorithm, but it has a signature. This is invalid: - String msg = "The JWS header references signature algorithm '" + alg + "' yet the compact JWS string has a digest/signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6."; - throw new MalformedJwtException(msg); - } else if (!Strings.hasText(digest)) { - String msg = "The JWS header references signature algorithm '" + alg + "' but the compact JWS string does not have a signature token."; - throw new MalformedJwtException(msg); - } + Assert.stateNotNull(algorithm, "JWS Signature Algorithm cannot be null."); Assert.stateNotNull(this.signingKeyResolver, "SigningKeyResolver cannot be null (invariant)."); @@ -603,12 +590,12 @@ private static boolean hasContentType(Header header) { } catch (InvalidKeyException | IllegalArgumentException e) { String algId = algorithm.getId(); String msg = "The parsed JWT indicates it was signed with the '" + algId + "' signature " + - "algorithm, but the provided " + key.getClass().getName() + " key may " + - "not be used to verify " + algId + " signatures. Because the specified " + - "key reflects a specific and expected algorithm, and the JWT does not reflect " + - "this algorithm, it is likely that the JWT was not expected and therefore should not be " + - "trusted. Another possibility is that the parser was provided the incorrect " + - "signature verification key, but this cannot be assumed for security reasons."; + "algorithm, but the provided " + key.getClass().getName() + " key may " + + "not be used to verify " + algId + " signatures. Because the specified " + + "key reflects a specific and expected algorithm, and the JWT does not reflect " + + "this algorithm, it is likely that the JWT was not expected and therefore should not be " + + "trusted. Another possibility is that the parser was provided the incorrect " + + "signature verification key, but this cannot be assumed for security reasons."; throw new UnsupportedJwtException(msg, e); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index 5fbbb12af..905f08241 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -19,14 +19,11 @@ import io.jsonwebtoken.Clock; import io.jsonwebtoken.CompressionCodecResolver; import io.jsonwebtoken.Header; -import io.jsonwebtoken.JweHeader; -import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.JwtParserBuilder; import io.jsonwebtoken.Locator; import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; -import io.jsonwebtoken.impl.lang.ConstantFunction; import io.jsonwebtoken.impl.lang.Function; import io.jsonwebtoken.impl.lang.LocatorFunction; import io.jsonwebtoken.impl.lang.Services; @@ -68,10 +65,10 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { private boolean enableUnsecuredJws = false; - private Function, Key> keyLocator = ConstantFunction.forNull(); + private Function, Key> keyLocator; @SuppressWarnings("deprecation") //TODO: remove for 1.0 - private SigningKeyResolver signingKeyResolver = new ConstantKeyLocator(null, null); + private SigningKeyResolver signingKeyResolver = null; private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); @@ -263,23 +260,17 @@ public JwtParser build() { this.deserializer = Services.loadFirst(Deserializer.class); } - final Function, Key> existing1 = this.keyLocator; - if (this.signatureVerificationKey != null) { - this.keyLocator = new Function, Key>() { - @Override - public Key apply(Header header) { - return header instanceof JwsHeader ? signatureVerificationKey : existing1.apply(header); - } - }; + if (this.keyLocator != null && this.decryptionKey != null) { + String msg = "Both 'keyLocator' and 'decryptionKey' cannot be configured. Prefer 'keyLocator' if possible."; + throw new IllegalStateException(msg); } - final Function, Key> existing2 = this.keyLocator; - if (this.decryptionKey != null) { - this.keyLocator = new Function, Key>() { - @Override - public Key apply(Header header) { - return header instanceof JweHeader ? decryptionKey : existing2.apply(header); - } - }; + + if (this.keyLocator == null) { + this.keyLocator = new ConstantKeyLocator(this.signatureVerificationKey, this.decryptionKey); + } + + if (this.signingKeyResolver == null) { + this.signingKeyResolver = new ConstantKeyLocator(this.signatureVerificationKey, this.decryptionKey); } // Invariants. If these are ever violated, it's an error in this class implementation diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 388980f63..bf7e66edd 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -172,7 +172,7 @@ protected Object apply(Field field, Object rawValue) { Assert.notNull(canonicalValue, "Converter's resulting canonicalValue cannot be null."); } catch (IllegalArgumentException e) { Object sval = field.isSecret() ? REDACTED_VALUE : rawValue; - String msg = "Invalid " + getName() + " " + field + " value [" + sval + "]. Cause: " + e.getMessage(); + String msg = "Invalid " + getName() + " " + field + " value: " + sval + ". Cause: " + e.getMessage(); throw new IllegalArgumentException(msg, e); } Object retval = nullSafePut(id, canonicalValue); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java index e86772624..849f11127 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java @@ -74,7 +74,8 @@ private static Date parseIso8601Date(String value) throws IllegalArgumentExcepti try { return DateFormats.parseIso8601Date(value); } catch (ParseException e) { - String msg = "String value does not appear to be ISO-8601-formatted: " + value; + String msg = "String value is not a JWT NumericDate, nor is it ISO-8601-formatted. " + + "All heuristics exhausted. Cause: " + e.getMessage(); throw new IllegalArgumentException(msg, e); } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy index 41a84acf0..ebbdf28b1 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy @@ -18,6 +18,7 @@ package io.jsonwebtoken import io.jsonwebtoken.impl.DefaultClock import io.jsonwebtoken.impl.FixedClock import io.jsonwebtoken.impl.JwtTokenizer +import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.SignatureException @@ -1407,10 +1408,37 @@ class DeprecatedJwtParserTest { } } + @Test + void testSetClock() { + def clock = new DefaultClock(); + def parser = Jwts.parser().setClock(clock) + assertSame clock, parser.@clock + assertFalse DefaultClock.INSTANCE.is(parser.@clock) + } + @Test void testParseClockManipulationWithDefaultClock() { Date expiry = new Date(System.currentTimeMillis() - 1000) + def key = TestKeys.HS256 + + String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry) + .signWith(key).compact() + + try { + def clock = new DefaultClock() + def parser = Jwts.parser().setSigningKey(key).setClock(clock) + parser.parseClaimsJws(compact) + fail() + } catch (ExpiredJwtException e) { + assertTrue e.getMessage().startsWith('JWT expired at ') + } + } + + @Test + void testBuilderParseClockManipulationWithDefaultClock() { + Date expiry = new Date(System.currentTimeMillis() - 1000) + String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact() try { diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index c2f0553c3..d182b1707 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -19,6 +19,7 @@ import io.jsonwebtoken.impl.* import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec import io.jsonwebtoken.impl.lang.Services +import io.jsonwebtoken.impl.security.ConstantKeyLocator import io.jsonwebtoken.impl.security.DirectKeyAlgorithm import io.jsonwebtoken.impl.security.Pbes2HsAkwAlgorithm import io.jsonwebtoken.impl.security.TestKeys @@ -134,6 +135,47 @@ class JwtsTest { assertEquals 'Joe', claims.getSubject() } + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseMalformedHeader() { + def headerString = '{"jku":42}' // cannot be parsed as a URI --> malformed header + def claimsString = '{"sub":"joe"}' + def encodedHeader = base64Url(headerString) + def encodedClaims = base64Url(claimsString) + def compact = encodedHeader + '.' + encodedClaims + '.AAD=' + try { + Jwts.parserBuilder().build().parseClaimsJws(compact) + fail() + } catch (MalformedJwtException e) { + String expected = 'Invalid protected header: Invalid JWS header \'jku\' (JWK Set URL) value: 42. ' + + 'Cause: Values must be either String or java.net.URI instances. ' + + 'Value type found: java.lang.Integer.' + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseMalformedClaims() { + def h = base64Url('{"alg":"HS256"}') + def c = base64Url('{"sub":"joe","exp":"-42-"}') + def sig = 'IA==' + def compact = "$h.$c.$sig" as String + try { + Jwts.parserBuilder().build().parseClaimsJws(compact) + fail() + } catch (MalformedJwtException e) { + String expected = 'Invalid claims: Invalid JWT Claim \'exp\' (Expiration Time) value: -42-. Cause: ' + + 'String value is not a JWT NumericDate, nor is it ISO-8601-formatted. All heuristics exhausted. ' + + 'Cause: Unparseable date: "-42-"' + assertEquals expected, e.getMessage() + } + } + @Test void testPlaintextJwtString() { // Assert exact output per example at https://datatracker.ietf.org/doc/html/rfc7519#section-6.1 @@ -235,7 +277,8 @@ class JwtsTest { Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseClaimsJws(missingSig) fail() } catch (MalformedJwtException expected) { - assertEquals 'The JWS header references signature algorithm \'HS256\' but the compact JWS string is missing the required signature.', expected.getMessage() + String s = String.format(DefaultJwtParser.MISSING_JWS_DIGEST_MSG_FMT, 'HS256') + assertEquals s, expected.getMessage() } } @@ -296,8 +339,8 @@ class JwtsTest { @Test void testConvenienceExpiration() { - Date then = laterDate(10000); - String compact = Jwts.builder().setExpiration(then).compact(); + Date then = laterDate(10000) + String compact = Jwts.builder().setExpiration(then).compact() Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getExpiration() assertEquals then, claimedDate @@ -667,7 +710,7 @@ class JwtsTest { def withoutSignature = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def invalidEncodedSignature = "_____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ" String jws = withoutSignature + '.' + invalidEncodedSignature - def keypair = SignatureAlgorithms.ES256.keyPairBuilder().build() + def keypair = SignatureAlgorithms.ES256.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair Jwts.parserBuilder().setSigningKey(keypair.public).build().parseClaimsJws(jws) } @@ -689,6 +732,399 @@ class JwtsTest { } } + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweMissingAlg() { + def h = base64Url('{"enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def compact = h + '.ecek.iv.' + c + '.tag' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + assertEquals DefaultJwtParser.MISSING_JWE_ALG_MSG, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweEmptyAlg() { + def h = base64Url('{"alg":"","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def compact = h + '.ecek.iv.' + c + '.tag' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + assertEquals DefaultJwtParser.MISSING_JWE_ALG_MSG, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWhitespaceAlg() { + def h = base64Url('{"alg":" ","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def compact = h + '.ecek.iv.' + c + '.tag' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + assertEquals DefaultJwtParser.MISSING_JWE_ALG_MSG, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithNoneAlg() { + def h = base64Url('{"alg":"none","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def compact = h + '.ecek.iv.' + c + '.tag' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + assertEquals DefaultJwtParser.JWE_NONE_MSG, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithMissingAadTag() { + def h = base64Url('{"alg":"dir","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def compact = h + '.ecek.iv.' + c + '.' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + String expected = String.format(DefaultJwtParser.MISSING_JWE_DIGEST_MSG_FMT, 'dir') + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithEmptyAadTag() { + def h = base64Url('{"alg":"dir","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + // our decoder skips invalid Base64Url characters, so this decodes to empty which is not allowed: + def tag = '&' + def compact = h + '.IA==.IA==.' + c + '.' + tag + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + String expected = 'Compact JWE strings must always contain an AAD Authentication Tag.' + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithMissingRequiredBody() { + def h = base64Url('{"alg":"dir","enc":"A128GCM"}') + def compact = h + '.ecek.iv..tag' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + String expected = 'Compact JWE strings MUST always contain a payload (ciphertext).' + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithEmptyEncryptedKey() { + def h = base64Url('{"alg":"dir","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + // our decoder skips invalid Base64Url characters, so this decodes to empty which is not allowed: + def encodedKey = '&' + def compact = h + '.' + encodedKey + '.iv.' + c + '.tag' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + String expected = 'Compact JWE string represents an encrypted key, but the key is empty.' + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithMissingInitializationVector() { + def h = base64Url('{"alg":"dir","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def compact = h + '.IA==..' + c + '.tag' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + String expected = 'Compact JWE strings must always contain an Initialization Vector.' + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithMissingEncHeader() { + def h = base64Url('{"alg":"dir"}') + def c = base64Url('{"sub":"joe"}') + def ekey = 'IA==' + def iv = 'IA==' + def tag = 'IA==' + def compact = "$h.$ekey.$iv.$c.$tag" as String + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + assertEquals DefaultJwtParser.MISSING_ENC_MSG, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithUnrecognizedEncValue() { + def h = base64Url('{"alg":"dir","enc":"foo"}') + def c = base64Url('{"sub":"joe"}') + def ekey = 'IA==' + def iv = 'IA==' + def tag = 'IA==' + def compact = "$h.$ekey.$iv.$c.$tag" as String + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (UnsupportedJwtException e) { + String expected = "Unrecognized JWE 'enc' header value: foo" + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithUnrecognizedAlgValue() { + def h = base64Url('{"alg":"bar","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def ekey = 'IA==' + def iv = 'IA==' + def tag = 'IA==' + def compact = "$h.$ekey.$iv.$c.$tag" as String + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (UnsupportedJwtException e) { + String expected = "Unrecognized JWE 'alg' header value: bar" + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJwsWithUnrecognizedAlgValue() { + def h = base64Url('{"alg":"bar"}') + def c = base64Url('{"sub":"joe"}') + def sig = 'IA==' + def compact = "$h.$c.$sig" as String + try { + Jwts.parserBuilder().build().parseClaimsJws(compact) + fail() + } catch (io.jsonwebtoken.security.SignatureException e) { + String expected = "Unsupported signature algorithm 'bar'" + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithUnlocatableKey() { + def h = base64Url('{"alg":"dir","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def ekey = 'IA==' + def iv = 'IA==' + def tag = 'IA==' + def compact = "$h.$ekey.$iv.$c.$tag" as String + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (UnsupportedJwtException e) { + String expected = "Cannot decrypt JWE payload: unable to locate key for JWE with header: {alg=dir, enc=A128GCM}" + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJwsWithCustomSignatureAlgorithm() { + def realAlg = SignatureAlgorithms.HS256 // any alg will do, we're going to wrap it + def key = TestKeys.HS256 + def id = realAlg.getId() + 'X' // custom id + def alg = new SecretKeySignatureAlgorithm() { + @Override + SecretKeyBuilder keyBuilder() { + return realAlg.keyBuilder() + } + + @Override + int getKeyBitLength() { + return realAlg.keyBitLength + } + + @Override + byte[] sign(SignatureRequest request) throws SecurityException { + return realAlg.sign(request) + } + + @Override + boolean verify(VerifySignatureRequest request) throws SecurityException { + return realAlg.verify(request) + } + + @Override + String getId() { + return id + } + } + + def jws = Jwts.builder().setSubject("joe").signWith(key, alg).compact() + + assertEquals 'joe', Jwts.parserBuilder() + .addSignatureAlgorithms([alg]) + .setSigningKey(key) + .build() + .parseClaimsJws(jws).body.getSubject() + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithCustomEncryptionAlgorithm() { + def realAlg = EncryptionAlgorithms.A128GCM // any alg will do, we're going to wrap it + def key = realAlg.keyBuilder().build() + def enc = realAlg.getId() + 'X' // custom id + def encAlg = new AeadAlgorithm() { + @Override + AeadResult encrypt(AeadRequest request) throws SecurityException { + return realAlg.encrypt(request) + } + + @Override + Message decrypt(DecryptAeadRequest request) throws SecurityException { + return realAlg.decrypt(request) + } + + @Override + String getId() { + return enc + } + + @Override + SecretKeyBuilder keyBuilder() { + return realAlg.keyBuilder() + } + + @Override + int getKeyBitLength() { + return realAlg.getKeyBitLength() + } + } + + def jwe = Jwts.jweBuilder().setSubject("joe").encryptWith(encAlg).withKey(key).compact() + + assertEquals 'joe', Jwts.parserBuilder() + .addEncryptionAlgorithms([encAlg]) + .decryptWith(key) + .build() + .parseClaimsJwe(jwe).body.getSubject() + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithBadKeyAlg() { + def alg = 'foo' + def h = base64Url('{"alg":"foo","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def ekey = 'IA==' + def iv = 'IA==' + def tag = 'IA==' + def compact = "$h.$ekey.$iv.$c.$tag" as String + + def badKeyAlg = new KeyAlgorithm() { + @Override + KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + return null + } + + @Override + SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + return null // bad implementation here - returns null, and that's not good + } + + @Override + String getId() { + return alg + } + } + + try { + Jwts.parserBuilder() + .setKeyLocator(new ConstantKeyLocator(TestKeys.HS256, TestKeys.A128GCM)) + .addKeyAlgorithms([badKeyAlg]) // <-- add bad alg here + .build() + .parseClaimsJwe(compact) + fail() + } catch (IllegalStateException e) { + String expected = "The 'foo' JWE key algorithm did not return a decryption key. " + + "Unable to perform 'A128GCM' decryption." + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseRequiredInt() { + def key = TestKeys.HS256 + def jws = Jwts.builder().signWith(key).claim("foo", 42).compact() + Jwts.parserBuilder().setSigningKey(key) + .require("foo", 42L) //require a long, but jws contains int, should still work + .build().parseClaimsJws(jws) + } + //Asserts correct/expected behavior discussed in https://github.com/jwtk/jjwt/issues/20 @Test void testForgedTokenWithSwappedHeaderUsingNoneAlgorithm() { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy index 3278a8b78..9b445ab2e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -228,7 +228,9 @@ class DefaultClaimsTest { claims.get('aDate', Date.class) fail() } catch (IllegalArgumentException expected) { - String expectedMsg = "Cannot create Date from 'aDate' value [$s]. Cause: String value does not appear to be ISO-8601-formatted: $s" as String + String expectedMsg = "Cannot create Date from 'aDate' value '$s'. Cause: " + + "String value is not a JWT NumericDate, nor is it ISO-8601-formatted. All heuristics " + + "exhausted. Cause: Unparseable date: \"$s\"" assertEquals expectedMsg, expected.getMessage() } } @@ -342,7 +344,7 @@ class DefaultClaimsTest { claims.put(field.getId(), val) fail() } catch (IllegalArgumentException iae) { - String msg = "Invalid Map $field value [hi]. Cause: Cannot create Date from Object of type io.jsonwebtoken.impl.DefaultClaimsTest\$1 with value: hi" + String msg = "Invalid JWT Claim $field value: hi. Cause: Cannot create Date from Object of type io.jsonwebtoken.impl.DefaultClaimsTest\$1 with value: hi" assertEquals msg, iae.getMessage() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy index c3ec502ba..f98cb5737 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy @@ -18,12 +18,15 @@ package io.jsonwebtoken.impl import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.JwtParser import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.security.ConstantKeyLocator +import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.Decoder import io.jsonwebtoken.io.DecodingException import io.jsonwebtoken.io.DeserializationException import io.jsonwebtoken.io.Deserializer import io.jsonwebtoken.security.SignatureAlgorithms import org.hamcrest.CoreMatchers +import org.junit.Before import org.junit.Test import java.security.Provider @@ -40,20 +43,41 @@ class DefaultJwtParserBuilderTest { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + private DefaultJwtParserBuilder builder + + @Before + void setUp() { + builder = new DefaultJwtParserBuilder() + } + @Test void testSetProvider() { Provider provider = createMock(Provider) replay provider - def parser = new DefaultJwtParserBuilder().setProvider(provider).build() + def parser = builder.setProvider(provider).build() assertSame provider, parser.jwtParser.provider verify provider } + @Test + void testKeyLocatorAndDecryptionKeyConfigured() { + try { + builder + .setKeyLocator(new ConstantKeyLocator(null, null)) + .decryptWith(TestKeys.A128GCM) + .build() + fail() + } catch (IllegalStateException e) { + String msg = "Both 'keyLocator' and 'decryptionKey' cannot be configured. Prefer 'keyLocator' if possible." + assertEquals msg, e.getMessage() + } + } + @Test(expected = IllegalArgumentException) void testBase64UrlDecodeWithNullArgument() { - new DefaultJwtParserBuilder().base64UrlDecodeWith(null) + builder.base64UrlDecodeWith(null) } @Test @@ -64,13 +88,13 @@ class DefaultJwtParserBuilderTest { return null } } - def b = new DefaultJwtParserBuilder().base64UrlDecodeWith(decoder) + def b = builder.base64UrlDecodeWith(decoder) assertSame decoder, b.base64UrlDecoder } @Test(expected = IllegalArgumentException) void testDeserializeJsonWithNullArgument() { - new DefaultJwtParserBuilder().deserializeJsonWith(null) + builder.deserializeJsonWith(null) } @Test @@ -81,7 +105,7 @@ class DefaultJwtParserBuilderTest { return OBJECT_MAPPER.readValue(bytes, Map.class) } } - def p = new DefaultJwtParserBuilder().deserializeJsonWith(deserializer) + def p = builder.deserializeJsonWith(deserializer) assertSame deserializer, p.deserializer def alg = SignatureAlgorithms.HS256 @@ -95,7 +119,7 @@ class DefaultJwtParserBuilderTest { @Test void testMaxAllowedClockSkewSeconds() { long max = Long.MAX_VALUE / 1000 as long - new DefaultJwtParserBuilder().setAllowedClockSkewSeconds(max) // no exception should be thrown + builder.setAllowedClockSkewSeconds(max) // no exception should be thrown } @Test @@ -103,7 +127,7 @@ class DefaultJwtParserBuilderTest { long value = Long.MAX_VALUE / 1000 as long value = value + 1L try { - new DefaultJwtParserBuilder().setAllowedClockSkewSeconds(value) + builder.setAllowedClockSkewSeconds(value) } catch (IllegalArgumentException expected) { assertEquals DefaultJwtParserBuilder.MAX_CLOCK_SKEW_ILLEGAL_MSG, expected.message } @@ -111,7 +135,7 @@ class DefaultJwtParserBuilderTest { @Test void testDefaultDeserializer() { - JwtParser parser = new DefaultJwtParserBuilder().build() + JwtParser parser = builder.build() assertThat parser.jwtParser.deserializer, CoreMatchers.instanceOf(JwtDeserializer) // TODO: When the ImmutableJwtParser replaces the default implementation this test will need updating, something like: @@ -121,9 +145,7 @@ class DefaultJwtParserBuilderTest { @Test void testUserSetDeserializerWrapped() { Deserializer deserializer = niceMock(Deserializer) - JwtParser parser = new DefaultJwtParserBuilder() - .deserializeJsonWith(deserializer) - .build() + JwtParser parser = builder.deserializeJsonWith(deserializer).build() // TODO: When the ImmutableJwtParser replaces the default implementation this test will need updating assertThat parser.jwtParser.deserializer, CoreMatchers.instanceOf(JwtDeserializer) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy index 1adb6ee79..1b7f7af6b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy @@ -132,7 +132,7 @@ class JwtMapTest { fail() } catch (IllegalArgumentException expected) { //Ensure message so we don't show any secret value: - String msg = 'Invalid Map \'foo\' (foo) value []. Cause: Values must be ' + + String msg = 'Invalid Map \'foo\' (foo) value: . Cause: Values must be ' + 'either String or java.math.BigInteger instances. Value type found: ' + 'java.net.URI.' assertEquals msg, expected.getMessage() From 92f959ebbf0f28732f1ad5e4c42576efac08d3d8 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 20 May 2022 23:00:16 -0700 Subject: [PATCH 47/75] Propagating exception wrapper function enhancements --- .../java/io/jsonwebtoken/lang/Assert.java | 3 +- .../jsonwebtoken/impl/DefaultJweBuilder.java | 41 ++++---- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 23 ++--- .../impl/lang/DelegatingCheckedFunction.java | 15 +++ .../impl/lang/FormattedStringFunction.java | 17 ++++ .../impl/lang/FormattedStringSupplier.java | 20 ++++ .../io/jsonwebtoken/impl/lang/Functions.java | 32 +++++++ .../lang/PropagatingExceptionFunction.java | 35 +++++-- .../io/jsonwebtoken/impl/lang/Supplier.java | 6 ++ .../AbstractAsymmetricJwkBuilder.java | 72 +++++++------- .../impl/security/ContentRequest.java | 10 ++ .../impl/security/DefaultContentRequest.java | 19 ++++ .../impl/security/DefaultHashAlgorithm.java | 25 +++++ .../impl/security/HashAlgorithm.java | 8 ++ .../impl/DefaultJwtBuilderTest.groovy | 9 +- .../impl/lang/FunctionsTest.groovy | 95 +++++++++++++++++++ .../PropagatingExceptionFunctionTest.groovy | 4 +- .../security/PrivateConstructorsTest.groovy | 2 + 18 files changed, 346 insertions(+), 90 deletions(-) create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/DelegatingCheckedFunction.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringFunction.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringSupplier.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/Functions.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/Supplier.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/ContentRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultContentRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultHashAlgorithm.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/HashAlgorithm.java create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/FunctionsTest.groovy diff --git a/api/src/main/java/io/jsonwebtoken/lang/Assert.java b/api/src/main/java/io/jsonwebtoken/lang/Assert.java index 6eabbd8e8..ba0521c5b 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Assert.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Assert.java @@ -202,10 +202,11 @@ public static void doesNotContain(String textToSearch, String substring) { * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object array is null or has no elements */ - public static void notEmpty(Object[] array, String message) { + public static Object[] notEmpty(Object[] array, String message) { if (Objects.isEmpty(array)) { throw new IllegalArgumentException(message); } + return array; } /** diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java index 82e00b7a3..8974a16f4 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java @@ -3,7 +3,7 @@ import io.jsonwebtoken.JweBuilder; import io.jsonwebtoken.JweHeader; import io.jsonwebtoken.impl.lang.Function; -import io.jsonwebtoken.impl.lang.PropagatingExceptionFunction; +import io.jsonwebtoken.impl.lang.Functions; import io.jsonwebtoken.impl.lang.Services; import io.jsonwebtoken.impl.security.DefaultAeadRequest; import io.jsonwebtoken.impl.security.DefaultKeyRequest; @@ -38,21 +38,19 @@ public class DefaultJweBuilder extends DefaultJwtBuilder implements private Key key; - protected Function wrap(String msg, Function fn) { - return new PropagatingExceptionFunction<>(SecurityException.class, msg, fn); + protected Function wrap(Function fn, String fmt, Object... args) { + return Functions.wrap(fn, SecurityException.class, fmt, args); } //TODO for 1.0: delete this method when the parent class's implementation has changed to SerializationException @Override protected Function, byte[]> wrap(final Serializer> serializer, String which) { - return new PropagatingExceptionFunction<>(SerializationException.class, - "Unable to serialize " + which + " to JSON.", new Function, byte[]>() { + return Functions.wrap(new Function, byte[]>() { @Override public byte[] apply(Map map) { return serializer.serialize(map); } - } - ); + }, SerializationException.class, "Unable to serialize %s to JSON.", which); } @Override @@ -64,14 +62,14 @@ public JweBuilder setPayload(String payload) { @Override public JweBuilder encryptWith(final AeadAlgorithm enc) { this.enc = Assert.notNull(enc, "Encryption algorithm cannot be null."); - Assert.hasText(enc.getId(), "Encryption algorithm id cannot be null or empty."); - String encMsg = enc.getId() + " encryption failed."; - this.encFunction = wrap(encMsg, new Function() { + final String id = enc.getId(); + Assert.hasText(id, "Encryption algorithm id cannot be null or empty."); + this.encFunction = wrap(new Function() { @Override public AeadResult apply(AeadRequest request) { return enc.encrypt(request); } - }); + }, "%s encryption failed.", id); return this; } @@ -89,16 +87,15 @@ public JweBuilder withKeyFrom(K key, final KeyAlgorithm al //noinspection unchecked this.alg = (KeyAlgorithm) Assert.notNull(alg, "KeyAlgorithm cannot be null."); final KeyAlgorithm keyAlg = this.alg; - Assert.hasText(alg.getId(), "KeyAlgorithm id cannot be null or empty."); - - String cekMsg = "Unable to obtain content encryption key from key management algorithm '" + alg.getId() + "'."; - this.algFunction = wrap(cekMsg, new Function, KeyResult>() { + final String id = alg.getId(); + Assert.hasText(id, "KeyAlgorithm id cannot be null or empty."); + String cekMsg = "Unable to obtain content encryption key from key management algorithm '%s'."; + this.algFunction = Functions.wrap(new Function, KeyResult>() { @Override public KeyResult apply(KeyRequest request) { return keyAlg.getEncryptionKey(request); } - }); - + }, SecurityException.class, cekMsg, id); return this; } @@ -166,10 +163,10 @@ public String compact() { String base64UrlEncodedTag = base64UrlEncoder.encode(tag); return - base64UrlEncodedHeader + DefaultJwtParser.SEPARATOR_CHAR + - base64UrlEncodedEncryptedCek + DefaultJwtParser.SEPARATOR_CHAR + - base64UrlEncodedIv + DefaultJwtParser.SEPARATOR_CHAR + - base64UrlEncodedCiphertext + DefaultJwtParser.SEPARATOR_CHAR + - base64UrlEncodedTag; + base64UrlEncodedHeader + DefaultJwtParser.SEPARATOR_CHAR + + base64UrlEncodedEncryptedCek + DefaultJwtParser.SEPARATOR_CHAR + + base64UrlEncodedIv + DefaultJwtParser.SEPARATOR_CHAR + + base64UrlEncodedCiphertext + DefaultJwtParser.SEPARATOR_CHAR + + base64UrlEncodedTag; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index 5e2f721ed..c4c6fd185 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -21,8 +21,8 @@ import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.impl.lang.Functions; import io.jsonwebtoken.impl.lang.LegacyServices; -import io.jsonwebtoken.impl.lang.PropagatingExceptionFunction; import io.jsonwebtoken.impl.security.DefaultSignatureRequest; import io.jsonwebtoken.impl.security.SignatureAlgorithmsBridge; import io.jsonwebtoken.io.Decoders; @@ -87,15 +87,12 @@ public T setSecureRandom(SecureRandom secureRandom) { // TODO for 1.0 - these should throw SerializationException not IllegalArgumentException // IAE is being retained for backwards pre-1.0 behavior compatibility Class clazz = "header".equals(which) ? IllegalStateException.class : IllegalArgumentException.class; - return new PropagatingExceptionFunction<>(clazz, - "Unable to serialize " + which + " to JSON.", - new Function, byte[]>() { - @Override - public byte[] apply(Map map) { - return serializer.serialize(map); - } - } - ); + return Functions.wrap(new Function, byte[]>() { + @Override + public byte[] apply(Map map) { + return serializer.serialize(map); + } + }, clazz, "Unable to serialize %s to JSON.", which); } @Override @@ -161,13 +158,13 @@ public T signWith(K key, final SignatureAlgorithm alg) thr Assert.notNull(alg, "SignatureAlgorithm cannot be null."); this.key = key; this.algorithm = (SignatureAlgorithm) alg; - this.signFunction = new PropagatingExceptionFunction<>(SignatureException.class, - "Unable to compute " + alg.getId() + " signature.", new Function, byte[]>() { + String id = Assert.hasText(this.algorithm.getId(), "SignatureAlgorithm id cannot be null or empty."); + this.signFunction = Functions.wrap(new Function, byte[]>() { @Override public byte[] apply(SignatureRequest request) { return algorithm.sign(request); } - }); + }, SignatureException.class, "Unable to compute %s signature.", id); return (T) this; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/DelegatingCheckedFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/DelegatingCheckedFunction.java new file mode 100644 index 000000000..8a8079369 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/DelegatingCheckedFunction.java @@ -0,0 +1,15 @@ +package io.jsonwebtoken.impl.lang; + +public class DelegatingCheckedFunction implements CheckedFunction { + + final Function delegate; + + public DelegatingCheckedFunction(Function delegate) { + this.delegate = delegate; + } + + @Override + public R apply(T t) throws Exception { + return delegate.apply(t); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringFunction.java new file mode 100644 index 000000000..f67951e79 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringFunction.java @@ -0,0 +1,17 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; + +public class FormattedStringFunction implements Function { + + private final String msg; + + public FormattedStringFunction(String msg) { + this.msg = Assert.hasText(msg, "msg argument cannot be null or empty."); + } + + @Override + public String apply(T arg) { + return String.format(msg, arg); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringSupplier.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringSupplier.java new file mode 100644 index 000000000..5b4b7e4b1 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringSupplier.java @@ -0,0 +1,20 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; + +public class FormattedStringSupplier implements Supplier { + + private final String msg; + + private final Object[] args; + + public FormattedStringSupplier(String msg, Object[] args) { + this.msg = Assert.hasText(msg, "Message cannot be null or empty."); + this.args = Assert.notEmpty(args, "Arguments cannot be null or empty."); + } + + @Override + public String get() { + return String.format(this.msg, this.args); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Functions.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Functions.java new file mode 100644 index 000000000..11e2fe0ca --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Functions.java @@ -0,0 +1,32 @@ +package io.jsonwebtoken.impl.lang; + +public final class Functions { + + private Functions() { + } + + /** + * Wraps the specified function to ensure that if any exception occurs, it is of the specified type and/or with + * the specified message. If no exception occurs, the function's return value is returned as expected. + * + *

    If {@code fn} throws an exception, its type is checked. If it is already of type {@code exClass}, that + * exception is immediately thrown. If it is not the expected exception type, a message is created with the + * specified {@code msg} template, and a new exception of the specified type is thrown with the formatted message, + * using the original exception as its cause.

    + * + * @param fn the function to execute + * @param exClass the exception type expected, if any + * @param msg the formatted message to use if throwing a new exception, used as the first argument to {@link String#format(String, Object...) String.format}. + * @param the function argument type + * @param the function's return type + * @param type of exception to ensure + * @return the wrapping function instance. + */ + public static Function wrapFmt(CheckedFunction fn, Class exClass, String msg) { + return new PropagatingExceptionFunction<>(fn, exClass, new FormattedStringFunction(msg)); + } + + public static Function wrap(Function fn, Class exClass, String fmt, Object... args) { + return new PropagatingExceptionFunction<>(new DelegatingCheckedFunction<>(fn), exClass, new FormattedStringSupplier(fmt, args)); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java index 59dabe4bc..94df8b90a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java @@ -5,17 +5,30 @@ import java.lang.reflect.Constructor; -public class PropagatingExceptionFunction implements Function { +public class PropagatingExceptionFunction implements Function { - private final Function function; + private final CheckedFunction function; + + private final Function msgFunction; private final Class clazz; - private final String msg; - public PropagatingExceptionFunction(Class exceptionClass, String msg, Function f) { - this.function = Assert.notNull(f, "Function cannot be null."); + public PropagatingExceptionFunction(Function f, Class exceptionClass, String msg) { + this(new DelegatingCheckedFunction<>(f), exceptionClass, new ConstantFunction(msg)); + } + + public PropagatingExceptionFunction(CheckedFunction fn, Class exceptionClass, final Supplier msgSupplier) { + this(fn, exceptionClass, new Function() { + @Override + public String apply(T t) { + return msgSupplier.get(); + } + }); + } + + public PropagatingExceptionFunction(CheckedFunction f, Class exceptionClass, Function msgFunction) { this.clazz = Assert.notNull(exceptionClass, "Exception class cannot be null."); - Assert.hasText(msg, "String message cannot be null or empty."); - this.msg = msg; + this.msgFunction = Assert.notNull(msgFunction, "msgFunction cannot be null."); + this.function = Assert.notNull(f, "Function cannot be null"); } @SuppressWarnings("unchecked") @@ -26,8 +39,12 @@ public R apply(T t) { if (clazz.isAssignableFrom(e.getClass())) { throw clazz.cast(e); } - String msg = this.msg + " Cause: " + e.getMessage(); - Class clazzz = (Class)clazz; + String msg = this.msgFunction.apply(t); + if (!msg.endsWith(".")) { + msg += "."; + } + msg += " Cause: " + e.getMessage(); + Class clazzz = (Class) clazz; Constructor ctor = Classes.getConstructor(clazzz, String.class, Throwable.class); throw Classes.instantiate(ctor, msg, e); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Supplier.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Supplier.java new file mode 100644 index 000000000..b26eb0261 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Supplier.java @@ -0,0 +1,6 @@ +package io.jsonwebtoken.impl.lang; + +public interface Supplier { + + T get(); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java index 84aebda84..3581f29ed 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java @@ -1,6 +1,9 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.impl.lang.Functions; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; @@ -22,11 +25,8 @@ import java.net.URI; import java.security.Key; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; @@ -36,8 +36,8 @@ import java.util.Set; abstract class AbstractAsymmetricJwkBuilder, - T extends AsymmetricJwkBuilder> - extends AbstractJwkBuilder implements AsymmetricJwkBuilder { + T extends AsymmetricJwkBuilder> + extends AbstractJwkBuilder implements AsymmetricJwkBuilder { protected boolean computeX509Sha1Thumbprint; /** @@ -47,6 +47,14 @@ abstract class AbstractAsymmetricJwkBuilder GET_X509_BYTES = + Functions.wrapFmt(new CheckedFunction() { + @Override + public byte[] apply(X509Certificate cert) throws Exception { + return cert.getEncoded(); + } + }, MalformedKeyException.class, "Unable to access X509Certificate encoded bytes necessary to compute thumbprint. Certificate: %s"); + public AbstractAsymmetricJwkBuilder(JwkContext ctx) { super(ctx); } @@ -103,19 +111,11 @@ public T withX509Sha256Thumbprint(boolean enable) { return tthis(); } - private byte[] computeThumbprint(final X509Certificate cert, final String jcaName) { - try { - byte[] encoded = cert.getEncoded(); - MessageDigest digest = MessageDigest.getInstance(jcaName); - return digest.digest(encoded); - } catch (CertificateEncodingException e) { - String msg = "Unable to access X509Certificate encoded bytes necessary to compute a " + jcaName + - " thumbprint. Certificate: {" + cert + "}. Cause: " + e.getMessage(); - throw new MalformedKeyException(msg, e); - } catch (NoSuchAlgorithmException e) { - String msg = "JCA Algorithm Name '" + jcaName + "' is not available: " + e.getMessage(); - throw new IllegalStateException(msg, e); - } + private byte[] computeThumbprint(final X509Certificate cert, HashAlgorithm alg) { + byte[] encoded = GET_X509_BYTES.apply(cert); + ContentRequest request = + new DefaultContentRequest(this.jwkContext.getProvider(), this.jwkContext.getRandom(), encoded); + return alg.hash(request); } @Override @@ -142,11 +142,11 @@ public J build() { } } if (computeX509Sha1Thumbprint) { - byte[] thumbprint = computeThumbprint(firstCert, "SHA-1"); + byte[] thumbprint = computeThumbprint(firstCert, DefaultHashAlgorithm.SHA1); this.jwkContext.setX509CertificateSha1Thumbprint(thumbprint); } if (computeX509Sha256Thumbprint) { - byte[] thumbprint = computeThumbprint(firstCert, "SHA-256"); + byte[] thumbprint = computeThumbprint(firstCert, DefaultHashAlgorithm.SHA256); this.jwkContext.setX509CertificateSha256Thumbprint(thumbprint); } } @@ -154,10 +154,10 @@ public J build() { } private abstract static class DefaultPublicJwkBuilder, M extends PrivateJwk, P extends PrivateJwkBuilder, - T extends PublicJwkBuilder> - extends AbstractAsymmetricJwkBuilder - implements PublicJwkBuilder { + J extends PublicJwk, M extends PrivateJwk, P extends PrivateJwkBuilder, + T extends PublicJwkBuilder> + extends AbstractAsymmetricJwkBuilder + implements PublicJwkBuilder { DefaultPublicJwkBuilder(JwkContext ctx) { super(ctx); @@ -174,10 +174,10 @@ public P setPrivateKey(L privateKey) { } private abstract static class DefaultPrivateJwkBuilder, M extends PrivateJwk, - T extends PrivateJwkBuilder> - extends AbstractAsymmetricJwkBuilder - implements PrivateJwkBuilder { + J extends PublicJwk, M extends PrivateJwk, + T extends PrivateJwkBuilder> + extends AbstractAsymmetricJwkBuilder + implements PrivateJwkBuilder { DefaultPrivateJwkBuilder(JwkContext ctx) { super(ctx); @@ -196,8 +196,8 @@ public T setPublicKey(L publicKey) { } static class DefaultEcPublicJwkBuilder - extends DefaultPublicJwkBuilder - implements EcPublicJwkBuilder { + extends DefaultPublicJwkBuilder + implements EcPublicJwkBuilder { DefaultEcPublicJwkBuilder(JwkContext src, ECPublicKey key) { super(new DefaultJwkContext<>(DefaultEcPublicJwk.FIELDS, src, key)); @@ -210,8 +210,8 @@ protected EcPrivateJwkBuilder newPrivateBuilder(ECPrivateKey key) { } static class DefaultRsaPublicJwkBuilder - extends DefaultPublicJwkBuilder - implements RsaPublicJwkBuilder { + extends DefaultPublicJwkBuilder + implements RsaPublicJwkBuilder { DefaultRsaPublicJwkBuilder(JwkContext ctx, RSAPublicKey key) { super(new DefaultJwkContext<>(DefaultRsaPublicJwk.FIELDS, ctx, key)); @@ -224,8 +224,8 @@ protected RsaPrivateJwkBuilder newPrivateBuilder(RSAPrivateKey key) { } static class DefaultEcPrivateJwkBuilder - extends DefaultPrivateJwkBuilder - implements EcPrivateJwkBuilder { + extends DefaultPrivateJwkBuilder + implements EcPrivateJwkBuilder { DefaultEcPrivateJwkBuilder(JwkContext src, ECPrivateKey key) { super(new DefaultJwkContext<>(DefaultEcPrivateJwk.FIELDS, src, key)); @@ -237,8 +237,8 @@ static class DefaultEcPrivateJwkBuilder } static class DefaultRsaPrivateJwkBuilder - extends DefaultPrivateJwkBuilder - implements RsaPrivateJwkBuilder { + extends DefaultPrivateJwkBuilder + implements RsaPrivateJwkBuilder { DefaultRsaPrivateJwkBuilder(JwkContext src, RSAPrivateKey key) { super(new DefaultJwkContext<>(DefaultRsaPrivateJwk.FIELDS, src, key)); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ContentRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ContentRequest.java new file mode 100644 index 000000000..e381f6300 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ContentRequest.java @@ -0,0 +1,10 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.Message; +import io.jsonwebtoken.security.Request; + +/** + * Request to perform a cryptographic operation on a byte array. + */ +public interface ContentRequest extends Message, Request { +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultContentRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultContentRequest.java new file mode 100644 index 000000000..ffeb78788 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultContentRequest.java @@ -0,0 +1,19 @@ +package io.jsonwebtoken.impl.security; + +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultContentRequest extends DefaultRequest implements ContentRequest { + + private final byte[] content; + + public DefaultContentRequest(Provider provider, SecureRandom secureRandom, byte[] content) { + super(provider, secureRandom); + this.content = content; + } + + @Override + public byte[] getContent() { + return this.content; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultHashAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultHashAlgorithm.java new file mode 100644 index 000000000..b8f0328fe --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultHashAlgorithm.java @@ -0,0 +1,25 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; + +import java.security.MessageDigest; + +class DefaultHashAlgorithm extends CryptoAlgorithm implements HashAlgorithm { + + static final HashAlgorithm SHA1 = new DefaultHashAlgorithm("SHA1", "SHA-1"); + static final HashAlgorithm SHA256 = new DefaultHashAlgorithm("SHA256", "SHA-256"); + + DefaultHashAlgorithm(String id, String jcaName) { + super(id, jcaName); + } + + @Override + public byte[] hash(final ContentRequest request) { + return execute(request, MessageDigest.class, new CheckedFunction() { + @Override + public byte[] apply(MessageDigest md) { + return md.digest(request.getContent()); + } + }); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/HashAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/HashAlgorithm.java new file mode 100644 index 000000000..290089831 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/HashAlgorithm.java @@ -0,0 +1,8 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.Identifiable; + +public interface HashAlgorithm extends Identifiable { + + byte[] hash(ContentRequest request); +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index b1f498455..873f503ab 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -24,12 +24,7 @@ import io.jsonwebtoken.io.Encoder import io.jsonwebtoken.io.EncodingException import io.jsonwebtoken.io.SerializationException import io.jsonwebtoken.io.Serializer -import io.jsonwebtoken.security.KeyException -import io.jsonwebtoken.security.Keys -import io.jsonwebtoken.security.SignatureAlgorithms -import io.jsonwebtoken.security.SignatureException -import io.jsonwebtoken.security.SignatureRequest -import io.jsonwebtoken.security.VerifySignatureRequest +import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.KeyGenerator @@ -286,7 +281,7 @@ class DefaultJwtBuilderTest { b.setClaims(c).compressWith(CompressionCodecs.DEFLATE).compact() fail() } catch (IllegalArgumentException iae) { - assertEquals iae.message, 'Unable to serialize claims to JSON. Cause: dummy text' + assertEquals 'Unable to serialize claims to JSON. Cause: dummy text', iae.message } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FunctionsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FunctionsTest.groovy new file mode 100644 index 000000000..d930aacda --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FunctionsTest.groovy @@ -0,0 +1,95 @@ +package io.jsonwebtoken.impl.lang + +import io.jsonwebtoken.MalformedJwtException +import org.junit.Test + +import static org.junit.Assert.* + +class FunctionsTest { + + @Test + void testWrapFmt() { + + def cause = new IllegalStateException("foo") + + def fn = Functions.wrapFmt(new CheckedFunction() { + @Override + Object apply(Object o) throws Exception { + throw cause + } + }, MalformedJwtException, "format me %s") + + try { + fn.apply('hi') + fail() + } catch (MalformedJwtException expected) { + String msg = "format me hi. Cause: foo" + assertEquals msg, expected.getMessage() + assertSame cause, expected.getCause() + } + } + + @Test + void testWrapFmtPropagatesExpectedExceptionTypeWithoutWrapping() { + + def cause = new MalformedJwtException("foo") + + def fn = Functions.wrapFmt(new CheckedFunction() { + @Override + Object apply(Object o) throws Exception { + throw cause + } + }, MalformedJwtException, "format me %s") + + try { + fn.apply('hi') + fail() + } catch (MalformedJwtException expected) { + assertEquals "foo", expected.getMessage() + assertSame cause, expected + } + } + + @Test + void testWrap() { + + def cause = new IllegalStateException("foo") + + def fn = Functions.wrap(new Function() { + @Override + Object apply(Object o) { + throw cause + } + }, MalformedJwtException, "format me %s", 'someArg') + + try { + fn.apply('hi') + fail() + } catch (MalformedJwtException expected) { + String msg = "format me someArg. Cause: foo" + assertEquals msg, expected.getMessage() + assertSame cause, expected.getCause() + } + } + + @Test + void testWrapPropagatesExpectedExceptionTypeWithoutWrapping() { + + def cause = new MalformedJwtException("foo") + + def fn = Functions.wrap(new Function() { + @Override + Object apply(Object o) { + throw cause + } + }, MalformedJwtException, "format me %s", 'someArg') + + try { + fn.apply('hi') + fail() + } catch (MalformedJwtException expected) { + assertEquals "foo", expected.getMessage() + assertSame cause, expected + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/PropagatingExceptionFunctionTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/PropagatingExceptionFunctionTest.groovy index 25759f66c..0339be5f3 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/PropagatingExceptionFunctionTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/PropagatingExceptionFunctionTest.groovy @@ -12,12 +12,12 @@ class PropagatingExceptionFunctionTest { def ex = new SecurityException("test") - def fn = new PropagatingExceptionFunction<>(SecurityException.class, "foo", new Function() { + def fn = new PropagatingExceptionFunction<>(new Function() { @Override Object apply(Object t) { throw ex } - }) + }, SecurityException.class, "foo") try { fn.apply("hi") diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateConstructorsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateConstructorsTest.groovy index bb5ea175d..701cd34e7 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateConstructorsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateConstructorsTest.groovy @@ -1,6 +1,7 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.impl.lang.Conditions +import io.jsonwebtoken.impl.lang.Functions import io.jsonwebtoken.lang.Classes import org.junit.Test @@ -14,5 +15,6 @@ class PrivateConstructorsTest { new KeyAlgorithmsBridge() new KeysBridge() new Conditions() + new Functions() } } From fc853235de85f16f3bf1ea94b13ce0042b1be397 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 21 May 2022 17:39:56 -0700 Subject: [PATCH 48/75] 100% code coverage! --- .../security/AsymmetricJwkBuilder.java | 2 +- .../io/jsonwebtoken/security/JwkBuilder.java | 4 +- .../jsonwebtoken/impl/DispatchingParser.java | 143 ------------- .../AbstractAsymmetricJwkBuilder.java | 25 ++- .../impl/security/DefaultValueGetter.java | 14 +- .../impl/DispatchingParserTest.groovy | 15 -- .../security/DefaultKeyUseStrategyTest.groovy | 62 ++++++ .../security/DefaultValueGetterTest.groovy | 193 ++++++++++++++++++ .../impl/security/KeyUsageTest.groovy | 105 ++++++++++ .../NoneSignatureAlgorithmTest.groovy | 32 ++- .../impl/security/TestX509Certificate.groovy | 139 +++++++++++++ 11 files changed, 546 insertions(+), 188 deletions(-) delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/DispatchingParser.java delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/DispatchingParserTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultKeyUseStrategyTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyUsageTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestX509Certificate.groovy diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java index 97a76aec8..61e300670 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java @@ -132,7 +132,7 @@ public interface AsymmetricJwkBuilder, */ T setX509Url(URI uri) throws IllegalArgumentException; - T withX509KeyUse(boolean enable); + //T withX509KeyUse(boolean enable); T withX509Sha1Thumbprint(boolean enable); diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java index ce1620417..1964adb46 100644 --- a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java @@ -37,7 +37,7 @@ public interface JwkBuilder, T extends JwkBuilder> extends SecurityBuilder { /** - * Set a single JWK property by name. If the {@code value} is {@code null}, an empty + * Set a single JWK property by name. If the {@code value} is {@code null}, an empty array, an empty * {@link java.util.Collection}, or an empty {@link Map}, the property will be removed from the JWK. * * @param name the name of the JWK property @@ -47,7 +47,7 @@ public interface JwkBuilder, T extends JwkBuilde T put(String name, Object value); /** - * Sets one or more JWK properties by name. If any {@code name} has a value that is {@code null}, + * Sets one or more JWK properties by name. If any {@code name} has a value that is {@code null}, an empty array, * an empty {@link java.util.Collection}, or an empty {@link Map}, the property will be removed from the JWK. * * @param values one or more name/value pairs to set on the JWK. diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DispatchingParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DispatchingParser.java deleted file mode 100644 index 3ef6dc6a1..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/DispatchingParser.java +++ /dev/null @@ -1,143 +0,0 @@ -package io.jsonwebtoken.impl; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class DispatchingParser { - - static final char DELIMITER = '.'; - - /* - - public void parse(String compactJwe) { - - //parse the constituent parts of the compact JWE: - - String base64UrlEncodedHeader = null; //JWT, JWS or JWE - - String base64UrlEncodedCek = null; //JWE only - String base64UrlEncodedPayload = null; //JWT or JWS - - String base64UrlEncodedIv = null; //JWE only - String base64UrlEncodedCiphertext = null; //JWE only - - String base64UrlEncodedTag = null; //JWE only - String base64UrlencodedDigest = null; //JWS only - - StringBuilder sb = new StringBuilder(); - - char[] chars = compactJwe.toCharArray(); - - int tokenIndex = 0; - - for (char c : chars) { - - Assert.isTrue(!Character.isWhitespace(c), "Compact JWT strings cannot contain whitespace."); - - if (c == DELIMITER) { - - String value = sb.length() > 0 ? sb.toString() : null; - - switch (tokenIndex) { - case 0: - base64UrlEncodedHeader = value; - break; - case 1: - //we'll figure out if we have a compact JWE or JWS after finishing inspecting the char array: - base64UrlEncodedCek = value; - base64UrlEncodedPayload = value; - case 2: - base64UrlEncodedIv = value; - break; - case 3: - base64UrlEncodedCiphertext = value; - break; - } - - sb = new StringBuilder(); - tokenIndex++; - } else { - sb.append(c); - } - } - - boolean jwe = false; - if (tokenIndex == 2) { // JWT or JWS - jwe = false; - } else if (tokenIndex == 4) { // JWE - jwe = true; - } else { - String msg = "Invalid compact JWT string - invalid number of period character delimiters: " + tokenIndex + - ". JWTs and JWSs must have exactly 2 periods, JWEs must have exactly 4 periods."; - throw new IllegalArgumentException(msg); - } - - if (sb.length() > 0) { - String value = sb.toString(); - if (jwe) { - base64UrlEncodedTag = value; - } else { - base64UrlencodedDigest = value; - } - } - - throw new UnsupportedOperationException("Not yet implemented."); - - /* - - - base64UrlEncodedTag = sb.toString(); - - Assert.notNull(base64UrlEncodedHeader, "Invalid compact JWE: base64Url JWE Protected Header is missing."); - Assert.notNull(base64UrlEncodedIv, "Invalid compact JWE: base64Url JWE Initialization Vector is missing."); - Assert.notNull(base64UrlEncodedCiphertext, "Invalid compact JWE: base64Url JWE Ciphertext is missing."); - Assert.notNull(base64UrlEncodedTag, "Invalid compact JWE: base64Url JWE Authentication Tag is missing."); - - //find which encryption key was used so we can decrypt: - final byte[] headerBytes = base64UrlDecode(base64UrlEncodedHeader); - final DefaultHeaders headers = serializationCodec.deserialize(headerBytes, DefaultHeaders.class); - - SecretKey secretKey = secretKeyResolver.getSecretKey(headers); - if (secretKey == null) { - String msg = "SecretKeyResolver did not return a secret key for headers " + headers + - ". This is required for message decryption."; - throw new SecurityException(msg); - } - - byte[] aad = base64UrlEncodedHeader.getBytes(StandardCharsets.US_ASCII); - byte[] iv = base64UrlDecode(base64UrlEncodedIv); - byte[] ciphertext = base64UrlDecode(base64UrlEncodedCiphertext); - byte[] tag = base64UrlDecode(base64UrlEncodedTag); - - DecryptionRequest dreq = DecryptionRequests.builder() - .setKey(secretKey.getEncoded()) - .setAdditionalAuthenticatedData(aad) - .setInitializationVector(iv) - .setCiphertext(ciphertext) - .setAuthenticationTag(tag) - .build(); - - byte[] plaintext = encryptionService.decrypt(dreq); - - CompressionAlgorithm calg = headers.getCompressionAlgorithm(); - if (calg != null) { - plaintext = calg.getCodec().decompress(plaintext); - } - - Object body = null; - - val = headers.get(JAVA_TYPE_HEADER_NAME); - if (val != null) { - String jtyp = val.toString(); - if (jtyp != null) { - Class bodyType = ClassUtils.forName(jtyp); - body = serializationCodec.deserialize(plaintext, bodyType); - } - } - - message.getHeaders().putAll(headers); - message.setBody(body); - - } - */ -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java index 3581f29ed..09f8d8457 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java @@ -6,7 +6,6 @@ import io.jsonwebtoken.impl.lang.Functions; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; -import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.AsymmetricJwk; import io.jsonwebtoken.security.AsymmetricJwkBuilder; import io.jsonwebtoken.security.EcPrivateJwk; @@ -74,10 +73,12 @@ public T setPublicKeyUse(String use) { return tthis(); } + /* public T setKeyUseStrategy(KeyUseStrategy strategy) { this.keyUseStrategy = Assert.notNull(strategy, "KeyUseStrategy cannot be null."); return tthis(); } + */ @Override public T setX509CertificateChain(List chain) { @@ -93,11 +94,13 @@ public T setX509Url(URI url) { return tthis(); } + /* @Override public T withX509KeyUse(boolean enable) { this.applyX509KeyUse = enable; return tthis(); } + */ @Override public T withX509Sha1Thumbprint(boolean enable) { @@ -126,21 +129,21 @@ public J build() { firstCert = chain.get(0); } - if (applyX509KeyUse == null) { //if not specified, enable by default if possible: - applyX509KeyUse = firstCert != null && !Strings.hasText(this.jwkContext.getPublicKeyUse()); - } +// if (applyX509KeyUse == null) { //if not specified, enable by default if possible: +// applyX509KeyUse = firstCert != null && !Strings.hasText(this.jwkContext.getPublicKeyUse()); +// } if (computeX509Sha256Thumbprint == null) { //if not specified, enable by default if possible: computeX509Sha256Thumbprint = firstCert != null && !computeX509Sha1Thumbprint; } if (firstCert != null) { - if (applyX509KeyUse) { - KeyUsage usage = new KeyUsage(firstCert); - String use = keyUseStrategy.toJwkValue(usage); - if (Strings.hasText(use)) { - setPublicKeyUse(use); - } - } +// if (applyX509KeyUse) { +// KeyUsage usage = new KeyUsage(firstCert); +// String use = keyUseStrategy.toJwkValue(usage); +// if (Strings.hasText(use)) { +// setPublicKeyUse(use); +// } +// } if (computeX509Sha1Thumbprint) { byte[] thumbprint = computeThumbprint(firstCert, DefaultHashAlgorithm.SHA1); this.jwkContext.setX509CertificateSha1Thumbprint(thumbprint); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java index 3676fd071..0502f8d1e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java @@ -100,23 +100,13 @@ public int getRequiredPositiveInteger(String key) { @Override public byte[] getRequiredBytes(String key) { - - String encoded = getRequiredString(key); - - byte[] decoded; + String encoded = getRequiredString(key); // guaranteed to be non-null and non-empty try { - decoded = Decoders.BASE64URL.decode(encoded); + return Decoders.BASE64URL.decode(encoded); } catch (Exception e) { String msg = name() + " '" + key + "' value is not a valid Base64URL String: " + e.getMessage(); throw malformed(msg); } - - if (Arrays.length(decoded) == 0) { - String msg = name() + " '" + key + "' decoded byte array cannot be empty."; - throw malformed(msg); - } - - return decoded; } @Override diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DispatchingParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DispatchingParserTest.groovy deleted file mode 100644 index 071594f1e..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DispatchingParserTest.groovy +++ /dev/null @@ -1,15 +0,0 @@ -package io.jsonwebtoken.impl - -import org.junit.Test - -/** - * @since JJWT_RELEASE_VERSION - */ -class DispatchingParserTest { - - @Test - void testCtor() { - new DispatchingParser() - } -} - diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultKeyUseStrategyTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultKeyUseStrategyTest.groovy new file mode 100644 index 000000000..806789096 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultKeyUseStrategyTest.groovy @@ -0,0 +1,62 @@ +package io.jsonwebtoken.impl.security + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNull + +class DefaultKeyUseStrategyTest { + + final KeyUseStrategy strat = DefaultKeyUseStrategy.INSTANCE + + private static KeyUsage usage(int trueIndex) { + boolean[] usage = new boolean[9] + usage[trueIndex] = true + return new KeyUsage(new TestX509Certificate(keyUsage: usage)) + } + + @Test + void testKeyEncipherment() { + assertEquals 'enc', strat.toJwkValue(usage(2)) + } + + @Test + void testDataEncipherment() { + assertEquals 'enc', strat.toJwkValue(usage(3)) + } + + @Test + void testKeyAgreement() { + assertEquals 'enc', strat.toJwkValue(usage(4)) + } + + @Test + void testDigitalSignature() { + assertEquals 'sig', strat.toJwkValue(usage(0)) + } + + @Test + void testNonRepudiation() { + assertEquals 'sig', strat.toJwkValue(usage(1)) + } + + @Test + void testKeyCertSign() { + assertEquals 'sig', strat.toJwkValue(usage(5)) + } + + @Test + void testCRLSign() { + assertEquals 'sig', strat.toJwkValue(usage(6)) + } + + @Test + void testEncipherOnly() { + assertNull strat.toJwkValue(usage(7)) + } + + @Test + void testDecipherOnly() { + assertNull strat.toJwkValue(usage(8)) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy new file mode 100644 index 000000000..56fb167a7 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy @@ -0,0 +1,193 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.MalformedJwtException +import io.jsonwebtoken.impl.DefaultHeader +import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.impl.DefaultJwsHeader +import io.jsonwebtoken.lang.Maps +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test + +import javax.crypto.SecretKey + +import static org.junit.Assert.* + +class DefaultValueGetterTest { + + @Test + void testMapName() { + def getter = new DefaultValueGetter(Maps.of('foo', 'bar').build()) + assertEquals 'Map', getter.name() + } + + @Test + void testJwtName() { + def getter = new DefaultValueGetter(new DefaultHeader().setAlgorithm('foo')) + assertEquals 'JWT header', getter.name() + } + + @Test + void testJwsName() { + def getter = new DefaultValueGetter(new DefaultJwsHeader().setAlgorithm('foo')) + assertEquals 'JWS header', getter.name() + } + + @Test + void testJweName() { + def getter = new DefaultValueGetter(new DefaultJweHeader().setAlgorithm('foo')) + assertEquals 'JWE header', getter.name() + } + + @Test + void testJwkName() { + def ctx = new DefaultJwkContext().setId('id') + def getter = new DefaultValueGetter(ctx) + assertEquals 'JWK', getter.name() + } + + @Test + void testSecretJwkName() { + def key = TestKeys.A128GCM + def jwk = new DefaultSecretJwk(new DefaultJwkContext().setType('oct').setKey(key)) + def getter = new DefaultValueGetter(jwk) + assertEquals 'Secret JWK', getter.name() + } + + @Test + void testJwkContextName() { + def ctx = new DefaultJwkContext<>().setId('id') + def getter = new DefaultValueGetter(ctx) + assertEquals 'JWK', getter.name() + } + + @Test + void testMalformedJwkContext() { + def ctx = new DefaultJwkContext<>().setId('id') + ctx.put('foo', 42) + def getter = new DefaultValueGetter(ctx) + try { + getter.getRequiredString('foo') + fail() + } catch (MalformedKeyException expected) { + String msg = "JWK 'foo' value must be a String. Actual type: java.lang.Integer" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testMalformedJwk() { + def jwk = Jwks.builder().setKey(TestKeys.A128GCM).put('foo', 42).build() + def getter = new DefaultValueGetter(jwk) + try { + getter.getRequiredString('foo') + fail() + } catch (MalformedKeyException expected) { + String msg = "Secret JWK 'foo' value must be a String. Actual type: java.lang.Integer" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testGetRequiredStringWhenEmpty() { + def getter = new DefaultValueGetter(Maps.of('foo', ' ').build()) + try { + getter.getRequiredString('foo') + fail() + } catch (MalformedJwtException expected) { + String msg = "Map 'foo' string value cannot be null or empty." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testGetRequiredIntegerWrongType() { + def getter = new DefaultValueGetter(Maps.of('foo', 'bar').build()) + try { + getter.getRequiredInteger('foo') + fail() + } catch (MalformedJwtException expected) { + String msg = "Map 'foo' value must be an Integer. Actual type: java.lang.String" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testGetRequiredPositiveIntegerWhenZero() { + def getter = new DefaultValueGetter(Maps.of('foo', 0 as int).build()) + try { + getter.getRequiredPositiveInteger('foo') + fail() + } catch (MalformedJwtException expected) { + String msg = "Map 'foo' value must be a positive Integer. Value: 0" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testGetRequiredPositiveIntegerWhenNegative() { + def getter = new DefaultValueGetter(Maps.of('foo', Integer.MIN_VALUE).build()) + try { + getter.getRequiredPositiveInteger('foo') + fail() + } catch (MalformedJwtException expected) { + String msg = "Map 'foo' value must be a positive Integer. Value: ${Integer.MIN_VALUE}" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testGetRequiredBytesInvalidData() { + def getter = new DefaultValueGetter(Maps.of('foo', '#@!').build()) + try { + getter.getRequiredBytes('foo') + fail() + } catch (MalformedJwtException expected) { + String msg = "Map 'foo' value is not a valid Base64URL String: Unable to decode input: null" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testGetRequiredBigIntNotSensitive() { + def getter = new DefaultValueGetter(Maps.of('foo', '#@!').build()) + try { + getter.getRequiredBigInt('foo', false) + fail() + } catch (MalformedJwtException expected) { + String msg = "Unable to decode Map 'foo' value '#@!' to BigInteger: Unable to decode input: null" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testGetRequiredBigIntSensitive() { + def getter = new DefaultValueGetter(Maps.of('foo', '#@!').build()) + try { + getter.getRequiredBigInt('foo', true) + fail() + } catch (MalformedJwtException expected) { + String msg = "Unable to decode Map 'foo' value to BigInteger: Unable to decode input: null" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testGetRequiredMap() { + def map = Maps.of('bar', 'baz').build() + def getter = new DefaultValueGetter(Maps.of('foo', map).build()) + assertSame map, getter.getRequiredMap('foo') + } + + @Test + void testGetRequiredMapWithInvalidValue() { + def getter = new DefaultValueGetter(Maps.of('foo', 'bar').build()) + try { + getter.getRequiredMap('foo') + fail() + } catch (MalformedJwtException expected) { + String msg = "Map 'foo' value must be a Map. Actual type: java.lang.String" + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyUsageTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyUsageTest.groovy new file mode 100644 index 000000000..0ba594520 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyUsageTest.groovy @@ -0,0 +1,105 @@ +package io.jsonwebtoken.impl.security + +import org.junit.Before +import org.junit.Test + +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertTrue + +class KeyUsageTest { + + private static KeyUsage usage(int trueIndex) { + boolean[] usage = new boolean[9] + usage[trueIndex] = true + return new KeyUsage(new TestX509Certificate(keyUsage: usage)) + } + + private KeyUsage ku + + @Before + void setUp() { + ku = new KeyUsage(new TestX509Certificate()) + } + + @Test + void testNullCert() { + ku = new KeyUsage(null) + assertFalse ku.isCRLSign() + assertFalse ku.isDataEncipherment() + assertFalse ku.isDecipherOnly() + assertFalse ku.isDigitalSignature() + assertFalse ku.isEncipherOnly() + assertFalse ku.isKeyAgreement() + assertFalse ku.isKeyCertSign() + assertFalse ku.isKeyEncipherment() + assertFalse ku.isNonRepudiation() + } + + @Test + void testCertWithNullKeyUsage() { + ku = new KeyUsage(new TestX509Certificate(keyUsage: null)) + assertFalse ku.isCRLSign() + assertFalse ku.isDataEncipherment() + assertFalse ku.isDecipherOnly() + assertFalse ku.isDigitalSignature() + assertFalse ku.isEncipherOnly() + assertFalse ku.isKeyAgreement() + assertFalse ku.isKeyCertSign() + assertFalse ku.isKeyEncipherment() + assertFalse ku.isNonRepudiation() + } + + @Test + void testDigitalSignature() { + assertFalse ku.isDigitalSignature() //default + assertTrue usage(0).isDigitalSignature() + } + + @Test + void testNonRepudiation() { + assertFalse ku.isNonRepudiation() + assertTrue usage(1).isNonRepudiation() + } + + @Test + void testKeyEncipherment() { + assertFalse ku.isKeyEncipherment() + assertTrue usage(2).isKeyEncipherment() + } + + @Test + void testDataEncipherment() { + assertFalse ku.isDataEncipherment() + assertTrue usage(3).isDataEncipherment() + } + + @Test + void testKeyAgreement() { + assertFalse ku.isKeyAgreement() + assertTrue usage(4).isKeyAgreement() + } + + @Test + void testKeyCertSign() { + assertFalse ku.isKeyCertSign() + assertTrue usage(5).isKeyCertSign() + } + + @Test + void testCRLSign() { + assertFalse ku.isCRLSign() + assertTrue usage(6).isCRLSign() + } + + @Test + void testEncipherOnly() { + assertFalse ku.isEncipherOnly() + assertTrue usage(7).isEncipherOnly() + } + + @Test + void testDecipherOnly() { + assertFalse ku.isDecipherOnly() + assertTrue usage(8).isDecipherOnly() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy index d9261b37d..9ad6b3a36 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy @@ -1,29 +1,53 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.security.SignatureException +import org.junit.Before import org.junit.Test import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertTrue class NoneSignatureAlgorithmTest { + private NoneSignatureAlgorithm alg + + @Before + void setUp() { + this.alg = new NoneSignatureAlgorithm() + } + @Test void testName() { - assertEquals "none", new NoneSignatureAlgorithm().getId(); + assertEquals "none", alg.getId(); } @Test(expected = SignatureException) void testSign() { - new NoneSignatureAlgorithm().sign(null) + alg.sign(null) } @Test(expected = SignatureException) void testVerify() { - new NoneSignatureAlgorithm().verify(null) + alg.verify(null) } @Test void testHashCode() { - assertEquals 'none'.hashCode(), new NoneSignatureAlgorithm().hashCode() + assertEquals 'none'.hashCode(), alg.hashCode() + } + + @Test + void testEquals() { + assertTrue alg.equals(new NoneSignatureAlgorithm()) + } + + @Test + void testIdentityEquals() { + assertTrue alg.equals(alg) + } + + @Test + void testToString() { + assertEquals alg.getId(), alg.toString() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestX509Certificate.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestX509Certificate.groovy new file mode 100644 index 000000000..8e5f64350 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestX509Certificate.groovy @@ -0,0 +1,139 @@ +package io.jsonwebtoken.impl.security + +import java.security.* +import java.security.cert.* + +class TestX509Certificate extends X509Certificate { + + private boolean[] keyUsage = new boolean[9] + + @Override + void checkValidity() throws CertificateExpiredException, CertificateNotYetValidException { + + } + + @Override + void checkValidity(Date date) throws CertificateExpiredException, CertificateNotYetValidException { + + } + + @Override + int getVersion() { + return 0 + } + + @Override + BigInteger getSerialNumber() { + return null + } + + @Override + Principal getIssuerDN() { + return null + } + + @Override + Principal getSubjectDN() { + return null + } + + @Override + Date getNotBefore() { + return null + } + + @Override + Date getNotAfter() { + return null + } + + @Override + byte[] getTBSCertificate() throws CertificateEncodingException { + return new byte[0] + } + + @Override + byte[] getSignature() { + return new byte[0] + } + + @Override + String getSigAlgName() { + return null + } + + @Override + String getSigAlgOID() { + return null + } + + @Override + byte[] getSigAlgParams() { + return new byte[0] + } + + @Override + boolean[] getIssuerUniqueID() { + return new boolean[0] + } + + @Override + boolean[] getSubjectUniqueID() { + return new boolean[0] + } + + @Override + boolean[] getKeyUsage() { + return this.keyUsage + } + + @Override + int getBasicConstraints() { + return 0 + } + + @Override + byte[] getEncoded() throws CertificateEncodingException { + return new byte[0] + } + + @Override + void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException { + + } + + @Override + void verify(PublicKey key, String sigProvider) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException { + + } + + @Override + String toString() { + return null + } + + @Override + PublicKey getPublicKey() { + return null + } + + @Override + boolean hasUnsupportedCriticalExtension() { + return false + } + + @Override + Set getCriticalExtensionOIDs() { + return null + } + + @Override + Set getNonCriticalExtensionOIDs() { + return null + } + + @Override + byte[] getExtensionValue(String oid) { + return new byte[0] + } +} From d72eaf649a33edd19bfbdccdc5db52c113848df2 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 21 May 2022 18:02:04 -0700 Subject: [PATCH 49/75] Minor test changes to work with JDK >= 11 --- .../security/DefaultValueGetterTest.groovy | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy index 56fb167a7..00cfecccc 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy @@ -143,8 +143,10 @@ class DefaultValueGetterTest { getter.getRequiredBytes('foo') fail() } catch (MalformedJwtException expected) { - String msg = "Map 'foo' value is not a valid Base64URL String: Unable to decode input: null" - assertEquals msg, expected.getMessage() + String msg = "Map 'foo' value is not a valid Base64URL String: Unable to decode input: " + // cannot do msg equality check here - the trailing value differs depending on the JDK < 11 or >= 11, + // so we do a 'starts with' check to ensure the parts of the message in our control are verified: + assertTrue expected.getMessage().startsWith(msg) } } @@ -155,8 +157,10 @@ class DefaultValueGetterTest { getter.getRequiredBigInt('foo', false) fail() } catch (MalformedJwtException expected) { - String msg = "Unable to decode Map 'foo' value '#@!' to BigInteger: Unable to decode input: null" - assertEquals msg, expected.getMessage() + String msg = "Unable to decode Map 'foo' value '#@!' to BigInteger: Unable to decode input: " + // cannot do msg equality check here - the trailing value differs depending on the JDK < 11 or >= 11, + // so we do a 'starts with' check to ensure the parts of the message in our control are verified: + assertTrue expected.getMessage().startsWith(msg) } } @@ -167,8 +171,10 @@ class DefaultValueGetterTest { getter.getRequiredBigInt('foo', true) fail() } catch (MalformedJwtException expected) { - String msg = "Unable to decode Map 'foo' value to BigInteger: Unable to decode input: null" - assertEquals msg, expected.getMessage() + String msg = "Unable to decode Map 'foo' value to BigInteger: Unable to decode input: " + // cannot do msg equality check here - the trailing value differs depending on the JDK < 11 or >= 11, + // so we do a 'starts with' check to ensure the parts of the message in our control are verified: + assertTrue expected.getMessage().startsWith(msg) } } From 2d6fa5e3741433487b037c3e45f324e631576f11 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 22 May 2022 18:30:24 -0700 Subject: [PATCH 50/75] Ensured all JWK secret or private values were wrapped in a RedactedSupplier instance to prevent accidental printing of secure values --- .../java/io/jsonwebtoken/lang/Builder.java | 15 ++ .../java/io/jsonwebtoken/lang/Supplier.java | 37 ++++ .../jsonwebtoken/gson/io/GsonSerializer.java | 33 ++- .../gson/io/GsonSupplierSerializer.java | 34 +++ .../gson/io/GsonSerializerTest.groovy | 36 +++- .../jackson/io/JacksonDeserializer.java | 11 +- .../jackson/io/JacksonSerializer.java | 15 +- .../jackson/io/JacksonSupplierSerializer.java | 47 +++++ .../jackson/io/JacksonSerializerTest.groovy | 11 +- .../io/JacksonSupplierSerializerTest.groovy | 39 ++++ .../orgjson/io/OrgJsonSerializer.java | 7 +- .../java/io/jsonwebtoken/impl/JwtMap.java | 55 +---- .../impl/lang/DefaultFieldBuilder.java | 18 ++ .../impl/lang/FormattedStringSupplier.java | 16 ++ .../lang/PropagatingExceptionFunction.java | 16 ++ .../impl/lang/RedactedSupplier.java | 58 +++++ .../impl/lang/RedactedValueConverter.java | 45 ++++ .../io/jsonwebtoken/impl/lang/Supplier.java | 6 - .../impl/security/DefaultJwkContext.java | 16 +- .../impl/security/DefaultRsaPrivateJwk.java | 17 +- .../impl/security/DefaultValueGetter.java | 23 +- .../security/RSAOtherPrimeInfoConverter.java | 23 +- .../impl/lang/RedactedSupplierTest.groovy | 46 ++++ .../lang/RedactedValueConverterTest.groovy | 57 +++++ .../security/DefaultJwkContextTest.groovy | 18 +- .../security/DispatchingJwkFactoryTest.groovy | 18 +- .../impl/security/JwkSerializationTest.groovy | 198 ++++++++++++++++++ .../impl/security/JwksTest.groovy | 20 +- .../security/RFC7517AppendixA2Test.groovy | 29 ++- .../security/RFC7517AppendixA3Test.groovy | 19 +- .../security/RsaPrivateJwkFactoryTest.groovy | 17 +- 31 files changed, 897 insertions(+), 103 deletions(-) create mode 100644 api/src/main/java/io/jsonwebtoken/lang/Supplier.java create mode 100644 extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSupplierSerializer.java create mode 100644 extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSupplierSerializer.java create mode 100644 extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSupplierSerializerTest.groovy create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/RedactedSupplier.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/RedactedValueConverter.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/Supplier.java create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedSupplierTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedValueConverterTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy diff --git a/api/src/main/java/io/jsonwebtoken/lang/Builder.java b/api/src/main/java/io/jsonwebtoken/lang/Builder.java index 84dd2acf6..b3a239e02 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Builder.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Builder.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.lang; /** diff --git a/api/src/main/java/io/jsonwebtoken/lang/Supplier.java b/api/src/main/java/io/jsonwebtoken/lang/Supplier.java new file mode 100644 index 000000000..839a86043 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/lang/Supplier.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.lang; + +/** + * Represents a supplier of results. + * + *

    There is no requirement that a new or distinct result be returned each time the supplier is invoked.

    + * + *

    This interface is the equivalent of a JDK 8 {@code java.util.function.Supplier}, backported for JJWT's use in + * JDK 7 environments.

    + * + * @param the type of object returned by this supplier + * @since JJWT_RELEASE_VERSION + */ +public interface Supplier { + + /** + * Returns a result. + * + * @return a result. + */ + T get(); +} diff --git a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java index bb0d76f5e..01099a506 100644 --- a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java +++ b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java @@ -22,11 +22,14 @@ import io.jsonwebtoken.io.Serializer; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.lang.Supplier; public class GsonSerializer implements Serializer { - static final Gson DEFAULT_GSON = new GsonBuilder().disableHtmlEscaping().create(); - private Gson gson; + static final Gson DEFAULT_GSON = new GsonBuilder() + .registerTypeHierarchyAdapter(Supplier.class, GsonSupplierSerializer.INSTANCE) + .disableHtmlEscaping().create(); + private final Gson gson; @SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator public GsonSerializer() { @@ -37,6 +40,17 @@ public GsonSerializer() { public GsonSerializer(Gson gson) { Assert.notNull(gson, "gson cannot be null."); this.gson = gson; + + //ensure the necessary type adapter has been registered, and if not, throw an error: + String json = this.gson.toJson(TestSupplier.INSTANCE); + if (json.contains("value")) { + String msg = "Invalid Gson instance - it has not been registered with the necessary " + + Supplier.class.getName() + " type adapter. When using the GsonBuilder, ensure this " + + "type adapter is registered by calling gsonBuilder.registerTypeHierarchyAdapter(" + + Supplier.class.getName() + ".class, " + + GsonSupplierSerializer.class.getName() + ".INSTANCE) before calling gsonBuilder.create()"; + throw new IllegalArgumentException(msg); + } } @Override @@ -62,4 +76,19 @@ protected byte[] writeValueAsBytes(T t) { } return this.gson.toJson(o).getBytes(Strings.UTF_8); } + + private static class TestSupplier implements Supplier { + + private static final TestSupplier INSTANCE = new TestSupplier<>("test"); + private final T value; + + private TestSupplier(T value) { + this.value = value; + } + + @Override + public T get() { + return value; + } + } } diff --git a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSupplierSerializer.java b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSupplierSerializer.java new file mode 100644 index 000000000..8ae511716 --- /dev/null +++ b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSupplierSerializer.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.gson.io; + +import com.google.gson.JsonElement; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import io.jsonwebtoken.lang.Supplier; + +import java.lang.reflect.Type; + +public final class GsonSupplierSerializer implements JsonSerializer> { + + public static final GsonSupplierSerializer INSTANCE = new GsonSupplierSerializer(); + + @Override + public JsonElement serialize(Supplier supplier, Type type, JsonSerializationContext ctx) { + Object value = supplier.get(); + return ctx.serialize(value); + } +} diff --git a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy index be2abe48e..4fd6dfb9a 100644 --- a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy +++ b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy @@ -15,23 +15,23 @@ */ package io.jsonwebtoken.gson.io -import io.jsonwebtoken.io.Deserializer +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import io.jsonwebtoken.io.SerializationException import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.lang.Strings +import io.jsonwebtoken.lang.Supplier import org.junit.Test import static org.easymock.EasyMock.* import static org.junit.Assert.* -import static org.hamcrest.CoreMatchers.instanceOf -import com.google.gson.Gson -import io.jsonwebtoken.io.SerializationException class GsonSerializerTest { @Test void loadService() { def serializer = ServiceLoader.load(Serializer).iterator().next() - assertThat(serializer, instanceOf(GsonSerializer)) + assertTrue serializer instanceof GsonSerializer } @Test @@ -41,14 +41,32 @@ class GsonSerializerTest { } @Test - void testObjectMapperConstructor() { - def customGSON = new Gson() + void testGsonConstructor() { + def customGSON = new GsonBuilder() + .registerTypeHierarchyAdapter(Supplier.class, GsonSupplierSerializer.INSTANCE) + .disableHtmlEscaping().create() def serializer = new GsonSerializer<>(customGSON) assertSame customGSON, serializer.gson } + @Test + void testGsonConstructorWithIncorrectlyConfiguredGson() { + try { + //noinspection GroovyResultOfObjectAllocationIgnored + new GsonSerializer<>(new Gson()) + fail() + } catch (IllegalArgumentException expected) { + String msg = 'Invalid Gson instance - it has not been registered with the necessary ' + + 'io.jsonwebtoken.lang.Supplier type adapter. When using the GsonBuilder, ensure this type ' + + 'adapter is registered by calling ' + + 'gsonBuilder.registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, ' + + 'io.jsonwebtoken.gson.io.GsonSupplierSerializer.INSTANCE) before calling gsonBuilder.create()' + assertEquals msg, expected.message + } + } + @Test(expected = IllegalArgumentException) - void testObjectMapperConstructorWithNullArgument() { + void testConstructorWithNullArgument() { new GsonSerializer<>(null) } @@ -94,7 +112,7 @@ class GsonSerializerTest { assertTrue Arrays.equals(expected, result) } - + @Test void testSerializeFailsWithJsonProcessingException() { diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java index c208ed7eb..7aa5acf8d 100644 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.ObjectMapper; - import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; import io.jsonwebtoken.io.DeserializationException; @@ -63,10 +62,10 @@ public JacksonDeserializer() { * If you would like to use your own {@code ObjectMapper} instance that also supports custom types for * JWT {@code Claims}, you will need to first customize your {@code ObjectMapper} instance by registering * your custom types and then use the {@link #JacksonDeserializer(ObjectMapper)} constructor instead. - * + * * @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type */ - public JacksonDeserializer(Map claimTypeMap) { + public JacksonDeserializer(Map> claimTypeMap) { // DO NOT reuse JacksonSerializer.DEFAULT_OBJECT_MAPPER as this could result in sharing the custom deserializer // between instances this(new ObjectMapper()); @@ -109,9 +108,9 @@ protected T readValue(byte[] bytes) throws IOException { */ private static class MappedTypeDeserializer extends UntypedObjectDeserializer { - private final Map claimTypeMap; + private final Map> claimTypeMap; - private MappedTypeDeserializer(Map claimTypeMap) { + private MappedTypeDeserializer(Map> claimTypeMap) { super(null, null); this.claimTypeMap = claimTypeMap; } @@ -121,7 +120,7 @@ public Object deserialize(JsonParser parser, DeserializationContext context) thr // check if the current claim key is mapped, if so traverse it's value String name = parser.currentName(); if (claimTypeMap != null && name != null && claimTypeMap.containsKey(name)) { - Class type = claimTypeMap.get(name); + Class type = claimTypeMap.get(name); return parser.readValueAsTree().traverse(parser.getCodec()).readValueAs(type); } // otherwise default to super diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java index 1445f92df..da05c5aea 100644 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java @@ -16,7 +16,9 @@ package io.jsonwebtoken.jackson.io; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; import io.jsonwebtoken.io.SerializationException; import io.jsonwebtoken.io.Serializer; import io.jsonwebtoken.lang.Assert; @@ -26,7 +28,16 @@ */ public class JacksonSerializer implements Serializer { - static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper(); + static final String MODULE_ID = "jjwt-jackson"; + static final Module MODULE; + + static { + SimpleModule module = new SimpleModule(MODULE_ID); + module.addSerializer(JacksonSupplierSerializer.INSTANCE); + MODULE = module; + } + + static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper().registerModule(MODULE); private final ObjectMapper objectMapper; @@ -38,7 +49,7 @@ public JacksonSerializer() { @SuppressWarnings("WeakerAccess") //intended for end-users to use when providing a custom ObjectMapper public JacksonSerializer(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper cannot be null."); - this.objectMapper = objectMapper; + this.objectMapper = objectMapper.registerModule(MODULE); } @Override diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSupplierSerializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSupplierSerializer.java new file mode 100644 index 000000000..a415bcf09 --- /dev/null +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSupplierSerializer.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.jackson.io; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import io.jsonwebtoken.lang.Supplier; + +import java.io.IOException; + +final class JacksonSupplierSerializer extends StdSerializer> { + + static final JacksonSupplierSerializer INSTANCE = new JacksonSupplierSerializer(); + + public JacksonSupplierSerializer() { + super(Supplier.class, false); + } + + @Override + public void serialize(Supplier supplier, JsonGenerator generator, SerializerProvider provider) throws IOException { + Object value = supplier.get(); + + if (value == null) { + provider.defaultSerializeNull(generator); + return; + } + + Class clazz = value.getClass(); + JsonSerializer ser = provider.findTypedValueSerializer(clazz, true, null); + ser.serialize(value, generator, provider); + } +} diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSerializerTest.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSerializerTest.groovy index 1febe76ed..6112ad27d 100644 --- a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSerializerTest.groovy +++ b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSerializerTest.groovy @@ -23,8 +23,8 @@ import io.jsonwebtoken.lang.Strings import org.junit.Test import static org.easymock.EasyMock.* -import static org.junit.Assert.* import static org.hamcrest.CoreMatchers.instanceOf +import static org.junit.Assert.* class JacksonSerializerTest { @@ -52,6 +52,15 @@ class JacksonSerializerTest { new JacksonSerializer<>(null) } + @Test + void testObjectMapperConstructorAutoRegistersModule() { + def om = createMock(ObjectMapper) + expect(om.registerModule(same(JacksonSerializer.MODULE))).andReturn(om) + replay om + def serializer = new JacksonSerializer<>(om) + verify om + } + @Test void testByte() { byte[] expected = "120".getBytes(Strings.UTF_8) //ascii("x") = 120 diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSupplierSerializerTest.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSupplierSerializerTest.groovy new file mode 100644 index 000000000..9c6cb03d6 --- /dev/null +++ b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSupplierSerializerTest.groovy @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.jackson.io + +import io.jsonwebtoken.lang.Supplier +import org.junit.Test + +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.assertEquals + +class JacksonSupplierSerializerTest { + + @Test + void testSupplierNullValue() { + def serializer = new JacksonSerializer() + def supplier = new Supplier() { + @Override + Object get() { + return null + } + } + byte[] bytes = serializer.serialize(supplier) + assertEquals 'null', new String(bytes, StandardCharsets.UTF_8) + } +} diff --git a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java index 2f00d1311..c46ee2507 100644 --- a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java +++ b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java @@ -23,6 +23,7 @@ import io.jsonwebtoken.lang.DateFormats; import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.lang.Supplier; import org.json.JSONArray; import org.json.JSONObject; @@ -82,6 +83,10 @@ private Object toJSONInstance(Object object) { return JSONObject.NULL; } + if (object instanceof Supplier) { + object = ((Supplier)object).get(); + } + if (object instanceof JSONObject || object instanceof JSONArray || JSONObject.NULL.equals(object) || isJSONString(object) || object instanceof Byte || object instanceof Character @@ -168,7 +173,7 @@ protected byte[] toBytes(Object o) { // // This is sufficient for all JJWT-supported scenarios on Android since Android users shouldn't ever use // JJWT's internal Serializer implementation for general JSON serialization. That is, its intended use - // is within the context of JwtBuilder execution and not for application use outside of that. + // is within the context of JwtBuilder execution and not for application use beyond that. if (o instanceof JSONObject) { s = o.toString(); } else { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java index bf7e66edd..37af49322 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -16,9 +16,8 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.impl.lang.Field; -import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.impl.lang.RedactedSupplier; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Classes; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; @@ -30,32 +29,19 @@ public class JwtMap implements Map { - private static final String GROOVY_PRESENCE_CLASS_NAME = "org.codehaus.groovy.runtime.InvokerHelper"; - private static final String GROOVY_PRESENCE_CLASS_METHOD_NAME = "formatMap"; - private static final boolean GROOVY_PRESENT = Classes.isAvailable(GROOVY_PRESENCE_CLASS_NAME); - - static final String REDACTED_VALUE = ""; + protected final Map> FIELDS; protected final Map values; // canonical values formatted per RFC requirements protected final Map idiomaticValues; // the values map with any RFC values converted to Java type-safe values where possible - protected final Map redactedValues; // the values map with any sensitive/secret values redacted. Used in the toString implementation. - protected final Map> FIELDS; - private final boolean hasSecretFields; public JwtMap(Set> fieldSet) { Assert.notEmpty(fieldSet, "Fields cannot be null or empty."); Map> fields = new LinkedHashMap<>(); - boolean hasSecretFields = false; for (Field field : fieldSet) { fields.put(field.getId(), field); - if (field.isSecret()) { - hasSecretFields = true; - } } - this.hasSecretFields = hasSecretFields; this.FIELDS = java.util.Collections.unmodifiableMap(fields); this.values = new LinkedHashMap<>(); this.idiomaticValues = new LinkedHashMap<>(); - this.redactedValues = new LinkedHashMap<>(); } public JwtMap(Set> fieldSet, Map values) { @@ -64,11 +50,6 @@ public JwtMap(Set> fieldSet, Map values) { putAll(values); } - protected boolean isSecret(String id) { - Field field = FIELDS.get(id); - return field != null && field.isSecret(); - } - public static boolean isReduceableToNull(Object v) { return v == null || (v instanceof String && !Strings.hasText((String) v)) || @@ -148,8 +129,6 @@ protected Object nullSafePut(String name, Object value) { if (isReduceableToNull(value)) { return remove(name); } else { - Object redactedValue = isSecret(name) ? REDACTED_VALUE : value; - this.redactedValues.put(name, redactedValue); this.idiomaticValues.put(name, value); return this.values.put(name, value); } @@ -171,7 +150,7 @@ protected Object apply(Field field, Object rawValue) { canonicalValue = field.applyTo(idiomaticValue); Assert.notNull(canonicalValue, "Converter's resulting canonicalValue cannot be null."); } catch (IllegalArgumentException e) { - Object sval = field.isSecret() ? REDACTED_VALUE : rawValue; + Object sval = field.isSecret() ? RedactedSupplier.REDACTED_VALUE : rawValue; String msg = "Invalid " + getName() + " " + field + " value: " + sval + ". Cause: " + e.getMessage(); throw new IllegalArgumentException(msg, e); } @@ -186,7 +165,6 @@ protected String getName() { @Override public Object remove(Object key) { - this.redactedValues.remove(key); this.idiomaticValues.remove(key); return this.values.remove(key); } @@ -206,7 +184,6 @@ public void putAll(Map m) { public void clear() { this.values.clear(); this.idiomaticValues.clear(); - this.redactedValues.clear(); } @Override @@ -219,38 +196,14 @@ public Collection values() { return values.values(); } - // MAINTAINER'S NOTE: - // - // BE VERY CAREFUL about moving this method - it's exact location in this - // file ties it to its implementation per StackTrace depth expectations. - // - // This behavior (and it's stack depth) is asserted in the - // DefaultJwkContextTest.testGStringPrintsRedactedValues() test case. If you - // change the location of this method, you must update that test as well. - protected boolean preferRedactedEntrySet() { - // For better performance, only execute the groovy stack count if this instance has secret fields - // (otherwise, we don't need to worry about redaction) and Groovy is detected: - if (this.hasSecretFields && GROOVY_PRESENT) { - Throwable t = new Throwable(); - StackTraceElement[] elements = t.getStackTrace(); - Assert.gt(Arrays.length(elements), 2, "StackTraceElement array must be greater than 2."); - return GROOVY_PRESENCE_CLASS_NAME.equals(elements[2].getClassName()) && - GROOVY_PRESENCE_CLASS_METHOD_NAME.equals(elements[2].getMethodName()); - } - return false; - } - @Override public Set> entrySet() { - if (preferRedactedEntrySet()) { - return this.redactedValues.entrySet(); - } return values.entrySet(); } @Override public String toString() { - return redactedValues.toString(); + return values.toString(); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java index 72faa2c71..7ca7965fb 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.impl.lang; import io.jsonwebtoken.lang.Assert; @@ -71,6 +86,9 @@ public Field build() { if (this.list != null) { converter = this.list ? Converters.forList(converter) : Converters.forSet(converter); } + if (this.secret) { + converter = new RedactedValueConverter(converter); + } return new DefaultField<>(this.id, this.name, this.secret, this.type, converter); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringSupplier.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringSupplier.java index 5b4b7e4b1..967a6cefb 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringSupplier.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringSupplier.java @@ -1,6 +1,22 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.impl.lang; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Supplier; public class FormattedStringSupplier implements Supplier { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java index 94df8b90a..d20735bd8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java @@ -1,7 +1,23 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.impl.lang; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Classes; +import io.jsonwebtoken.lang.Supplier; import java.lang.reflect.Constructor; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/RedactedSupplier.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/RedactedSupplier.java new file mode 100644 index 000000000..49d9c6b86 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/RedactedSupplier.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.lang.Supplier; + +public class RedactedSupplier implements Supplier { + + public static final String REDACTED_VALUE = ""; + + private final T value; + + public RedactedSupplier(T value) { + this.value = Assert.notNull(value, "value cannot be null."); + } + + @Override + public T get() { + return value; + } + + @Override + public int hashCode() { + return Objects.nullSafeHashCode(value); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof RedactedSupplier) { + return Objects.nullSafeEquals(this.value, ((RedactedSupplier) obj).value); + } else { + return Objects.nullSafeEquals(this.value, obj); + } + } + + @Override + public String toString() { + return REDACTED_VALUE; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/RedactedValueConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/RedactedValueConverter.java new file mode 100644 index 000000000..12b46aabf --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/RedactedValueConverter.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Supplier; + +public class RedactedValueConverter implements Converter { + + private final Converter delegate; + + public RedactedValueConverter(Converter delegate) { + this.delegate = Assert.notNull(delegate, "Delegate cannot be null."); + } + + @Override + public Object applyTo(T t) { + Object value = this.delegate.applyTo(t); + if (value != null && !(value instanceof RedactedSupplier)) { + value = new RedactedSupplier<>(value); + } + return value; + } + + @Override + public T applyFrom(Object o) { + if (o instanceof RedactedSupplier) { + o = ((Supplier) o).get(); + } + return this.delegate.applyFrom(o); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Supplier.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Supplier.java deleted file mode 100644 index b26eb0261..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Supplier.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.jsonwebtoken.impl.lang; - -public interface Supplier { - - T get(); -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java index 66f0dd27d..e5da918d3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.JwtMap; @@ -65,7 +80,6 @@ private DefaultJwkContext(Set> fields, JwkContext other, boolean rem this.random = other.getRandom(); this.values.putAll(src.values); this.idiomaticValues.putAll(src.idiomaticValues); - this.redactedValues.putAll(src.redactedValues); if (removePrivate) { for (Field field : src.FIELDS.values()) { if (field.isSecret()) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java index 3f6af8791..ebf160627 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.Field; @@ -22,7 +37,7 @@ class DefaultRsaPrivateJwk extends AbstractPrivateJwk SECOND_CRT_EXPONENT = Fields.secretBigInt("dq", "Second Factor CRT Exponent"); static final Field FIRST_CRT_COEFFICIENT = Fields.secretBigInt("qi", "First CRT Coefficient"); static final Field> OTHER_PRIMES_INFO = - Fields.builder(RSAOtherPrimeInfo.class).setSecret(true) + Fields.builder(RSAOtherPrimeInfo.class) .setId("oth").setName("Other Primes Info") .setConverter(RSAOtherPrimeInfoConverter.INSTANCE).list() .build(); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java index 0502f8d1e..126046d62 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.Header; @@ -6,6 +21,7 @@ import io.jsonwebtoken.JwtException; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.RedactedSupplier; import io.jsonwebtoken.impl.lang.ValueGetter; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.lang.Arrays; @@ -56,6 +72,9 @@ private JwtException malformed(String msg) { protected Object getRequiredValue(String key) { Object value = this.values.get(key); + if (value instanceof RedactedSupplier) { + value = ((RedactedSupplier) value).get(); + } if (value == null) { String msg = name() + " is missing required '" + key + "' value."; throw malformed(msg); @@ -115,7 +134,7 @@ public byte[] getRequiredBytes(String key, int requiredByteLength) { int len = Arrays.length(decoded); if (len != requiredByteLength) { String msg = name() + " '" + key + "' decoded byte array must be " + Bytes.bytesMsg(requiredByteLength) + - " long. Actual length: " + Bytes.bytesMsg(len) + "."; + " long. Actual length: " + Bytes.bytesMsg(len) + "."; throw malformed(msg); } return decoded; @@ -145,6 +164,6 @@ public BigInteger getRequiredBigInt(String key, boolean sensitive) { String msg = name() + " '" + key + "' value must be a Map. Actual type: " + value.getClass().getName(); throw malformed(msg); } - return (Map)value; + return (Map) value; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java index 188985058..82cddbd2b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.Converter; @@ -24,10 +39,10 @@ class RSAOtherPrimeInfoConverter implements Converter @Override public Object applyTo(RSAOtherPrimeInfo info) { - Map m = new LinkedHashMap<>(3); - m.put(PRIME_FACTOR.getId(), (String)PRIME_FACTOR.applyTo(info.getPrime())); - m.put(FACTOR_CRT_EXPONENT.getId(), (String)FACTOR_CRT_EXPONENT.applyTo(info.getExponent())); - m.put(FACTOR_CRT_COEFFICIENT.getId(), (String)FACTOR_CRT_COEFFICIENT.applyTo(info.getCrtCoefficient())); + Map m = new LinkedHashMap<>(3); + m.put(PRIME_FACTOR.getId(), PRIME_FACTOR.applyTo(info.getPrime())); + m.put(FACTOR_CRT_EXPONENT.getId(), FACTOR_CRT_EXPONENT.applyTo(info.getExponent())); + m.put(FACTOR_CRT_COEFFICIENT.getId(), FACTOR_CRT_COEFFICIENT.applyTo(info.getCrtCoefficient())); return m; } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedSupplierTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedSupplierTest.groovy new file mode 100644 index 000000000..a4686e517 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedSupplierTest.groovy @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.lang + +import org.junit.Test + +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertTrue + +class RedactedSupplierTest { + + @Test + void testEqualsWrappedSameValue() { + def value = 42 + assertTrue new RedactedSupplier<>(value).equals(value) + } + + @Test + void testEqualsWrappedDifferentValue() { + assertFalse new RedactedSupplier<>(42).equals(30) + } + + @Test + void testEquals() { + assertTrue new RedactedSupplier<>(42).equals(new RedactedSupplier(42)) + } + + @Test + void testEqualsSameTypeDifferentValue() { + assertFalse new RedactedSupplier<>(42).equals(new RedactedSupplier(30)) + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedValueConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedValueConverterTest.groovy new file mode 100644 index 000000000..65c776b68 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedValueConverterTest.groovy @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.lang + +import org.junit.Test + +import static org.junit.Assert.assertNull +import static org.junit.Assert.assertSame + +class RedactedValueConverterTest { + + @Test + void testApplyToWithNullValue() { + def c = new RedactedValueConverter(new NullSafeConverter(Converters.URI)) + assertNull c.applyTo(null) + } + + @Test + void testApplyFromWithNullValue() { + def c = new RedactedValueConverter(new NullSafeConverter(Converters.URI)) + assertNull c.applyFrom(null) + } + + @Test + void testDelegateReturnsRedactedSupplierValue() { + def suri = 'https://jsonwebtoken.io' + def supplier = new RedactedSupplier(suri) + def delegate = new Converter() { + @Override + Object applyTo(Object o) { + return supplier + } + + @Override + Object applyFrom(Object o) { + return null + } + } + def c = new RedactedValueConverter(delegate) + + // ensure applyTo doesn't change or wrap the delegate return value that is already of type RedactedSupplier: + assertSame supplier, c.applyTo(suri) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy index e3e116a10..de655ab63 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.impl.security import org.junit.Test @@ -21,10 +36,7 @@ class DefaultJwkContextTest { @Test void testGStringPrintsRedactedValues() { - // DO NOT REMOVE THIS METHOD: IT IS CRITICAL TO ENSURE GROOVY STRINGS DO NOT LEAK SECRET/PRIVATE KEY MATERIAL - // If you still believe it should be removed, discuss with the JJWT dev team first. - def header = new DefaultJwkContext(DefaultSecretJwk.FIELDS) header.put('kty', 'oct') header.put('k', 'test') diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy index 142aa95e0..7bac475d4 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy @@ -1,6 +1,20 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.impl.security - import io.jsonwebtoken.security.EcPrivateJwk import io.jsonwebtoken.security.EcPublicJwk import io.jsonwebtoken.security.UnsupportedKeyException @@ -82,7 +96,7 @@ class DispatchingJwkFactoryTest { String x = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, jwk.toPublicJwk().toKey().w.affineX) String y = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, jwk.toPublicJwk().toKey().w.affineY) String d = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, key.s) - assertEquals jwk.d, d + assertEquals jwk.d.get(), d //remove the 'd' mapping to represent only a public key: m.remove(DefaultEcPrivateJwk.D.getId()) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy new file mode 100644 index 000000000..d4569135c --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.gson.io.GsonDeserializer +import io.jsonwebtoken.gson.io.GsonSerializer +import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.io.Serializer +import io.jsonwebtoken.jackson.io.JacksonDeserializer +import io.jsonwebtoken.jackson.io.JacksonSerializer +import io.jsonwebtoken.lang.Supplier +import io.jsonwebtoken.orgjson.io.OrgJsonDeserializer +import io.jsonwebtoken.orgjson.io.OrgJsonSerializer +import io.jsonwebtoken.security.Jwk +import io.jsonwebtoken.security.Jwks +import org.junit.Test + +import java.nio.charset.StandardCharsets +import java.security.Key + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertTrue + +/** + * Asserts that serializing and deserializing private or secret key values works as expected without + * exposing raw strings in the JWKs themselves (should be wrapped with RedactedSupplier instances) for toString safety. + */ +class JwkSerializationTest { + + @Test + void testJacksonSecretJwk() { + testSecretJwk(new JacksonSerializer(), new JacksonDeserializer()) + } + + @Test + void testJacksonPrivateEcJwk() { + testPrivateEcJwk(new JacksonSerializer(), new JacksonDeserializer()) + } + + @Test + void testJacksonPrivateRsaJwk() { + testPrivateRsaJwk(new JacksonSerializer(), new JacksonDeserializer()) + } + + @Test + void testGsonSecretJwk() { + testSecretJwk(new GsonSerializer(), new GsonDeserializer()) + } + + @Test + void testGsonPrivateEcJwk() { + testPrivateEcJwk(new GsonSerializer(), new GsonDeserializer()) + } + + @Test + void testGsonPrivateRsaJwk() { + testPrivateRsaJwk(new GsonSerializer(), new GsonDeserializer()) + } + + @Test + void testOrgJsonSecretJwk() { + testSecretJwk(new OrgJsonSerializer(), new OrgJsonDeserializer()) + } + + @Test + void testOrgJsonPrivateEcJwk() { + testPrivateEcJwk(new OrgJsonSerializer(), new OrgJsonDeserializer()) + } + + @Test + void testOrgJsonPrivateRsaJwk() { + testPrivateRsaJwk(new OrgJsonSerializer(), new OrgJsonDeserializer()) + } + + static void testSecretJwk(Serializer serializer, Deserializer deserializer) { + + def key = TestKeys.A128GCM + def jwk = Jwks.builder().setKey(key).setId('id').build() + assertWrapped(jwk, ['k']) + + // Ensure no Groovy or Java toString prints out secret values: + assertEquals '[kid:id, kty:oct, k:]', "$jwk" as String // groovy gstring + println jwk.toString() + assertEquals '{kid=id, kty=oct, k=}', jwk.toString() // java toString + + //but serialization prints the real value: + byte[] data = serializer.serialize(jwk) + def result = new String(data, StandardCharsets.UTF_8) + // assert substrings here because JSON order is not guaranteed: + assertTrue result.contains('"kid":"id"') + assertTrue result.contains('"kty":"oct"') + assertTrue result.contains("\"k\":\"${jwk.k.get()}\"" as String) + + //now ensure it deserializes back to a JWK: + def map = deserializer.deserialize(data) as Map + def jwk2 = Jwks.builder().putAll(map).build() + assertTrue jwk.k instanceof Supplier + assertEquals jwk, jwk2 + assertEquals jwk.k, jwk2.k + assertEquals jwk.k.get(), jwk2.k.get() + } + + static void testPrivateEcJwk(Serializer serializer, Deserializer deserializer) { + + def jwk = Jwks.builder().setKeyPairEc(TestKeys.ES256.pair).setId('id').build() + assertWrapped(jwk, ['d']) + + // Ensure no Groovy or Java toString prints out secret values: + assertEquals '[kid:id, kty:EC, crv:P-256, x:xNKMMIsawShLG4LYxpNP0gqdgK_K69UXCLt3AE3zp-Q, y:_vzQymVtA7RHRTfBWZo75mxPgDkE8g7bdHI3siSuJOk, d:]', "$jwk" as String + // groovy gstring + assertEquals '{kid=id, kty=EC, crv=P-256, x=xNKMMIsawShLG4LYxpNP0gqdgK_K69UXCLt3AE3zp-Q, y=_vzQymVtA7RHRTfBWZo75mxPgDkE8g7bdHI3siSuJOk, d=}', jwk.toString() + // java toString + + //but serialization prints the real value: + byte[] data = serializer.serialize(jwk) + def result = new String(data, StandardCharsets.UTF_8) + // assert substrings here because JSON order is not guaranteed: + assertTrue result.contains('"kid":"id"') + assertTrue result.contains('"kty":"EC"') + assertTrue result.contains('"crv":"P-256"') + assertTrue result.contains('"x":"xNKMMIsawShLG4LYxpNP0gqdgK_K69UXCLt3AE3zp-Q"') + assertTrue result.contains('"y":"_vzQymVtA7RHRTfBWZo75mxPgDkE8g7bdHI3siSuJOk"') + assertTrue result.contains("\"d\":\"${jwk.d.get()}\"" as String) + + //now ensure it deserializes back to a JWK: + def map = deserializer.deserialize(data) as Map + def jwk2 = Jwks.builder().putAll(map).build() + assertTrue jwk.d instanceof Supplier + assertEquals jwk, jwk2 + assertEquals jwk.d, jwk2.d + assertEquals jwk.d.get(), jwk2.d.get() + } + + private static assertWrapped(Map map, List keys) { + for (String key : keys) { + def value = map.get(key) + assertTrue value instanceof Supplier + value = ((Supplier) value).get() + assertTrue value instanceof String + } + } + + private static assertEquals(Jwk jwk1, Jwk jwk2, List keys) { + assertEquals jwk1, jwk2 + for (String key : keys) { + assertTrue jwk1.get(key) instanceof Supplier + assertTrue jwk2.get(key) instanceof Supplier + assertEquals jwk1.get(key), jwk2.get(key) + assertEquals jwk1.get(key).get(), jwk2.get(key).get() + } + } + + static void testPrivateRsaJwk(Serializer serializer, Deserializer deserializer) { + + def jwk = Jwks.builder().setKeyPairRsa(TestKeys.RS256.pair).setId('id').build() + def privateFieldNames = ['d', 'p', 'q', 'dp', 'dq', 'qi'] + assertWrapped(jwk, privateFieldNames) + + // Ensure no Groovy or Java toString prints out secret values: + assertEquals '[kid:id, kty:RSA, n:zkH0MwxQ2cUFWsvOPVFqI_dk2EFTjQolCy97mI5_wYCbaOoZ9Rm7c675mAeemRtNzgNVEz7m298ENqNGqPk2Nv3pBJ_XCaybBlp61CLez7dQ2h5jUFEJ6FJcjeKHS-MwXr56t2ISdfLNMYtVIxjvXQcYx5VmS4mIqTxj5gVGtQVi0GXdH6SvpdKV0fjE9KOhjsdBfKQzZfcQlusHg8pThwvjpMwCZnkxCS0RKa9y4-5-7MkC33-8-neZUzS7b6NdFxh6T_pMXpkf8d81fzVo4ZBMloweW0_l8MOdVxeX7M_7XSC1ank5i3IEZcotLmJYMwEo7rMpZVLevEQ118Eo8Q, e:AQAB, d:, p:, q:, dp:, dq:, qi:]', "$jwk" as String + // groovy gstring + assertEquals '{kid=id, kty=RSA, n=zkH0MwxQ2cUFWsvOPVFqI_dk2EFTjQolCy97mI5_wYCbaOoZ9Rm7c675mAeemRtNzgNVEz7m298ENqNGqPk2Nv3pBJ_XCaybBlp61CLez7dQ2h5jUFEJ6FJcjeKHS-MwXr56t2ISdfLNMYtVIxjvXQcYx5VmS4mIqTxj5gVGtQVi0GXdH6SvpdKV0fjE9KOhjsdBfKQzZfcQlusHg8pThwvjpMwCZnkxCS0RKa9y4-5-7MkC33-8-neZUzS7b6NdFxh6T_pMXpkf8d81fzVo4ZBMloweW0_l8MOdVxeX7M_7XSC1ank5i3IEZcotLmJYMwEo7rMpZVLevEQ118Eo8Q, e=AQAB, d=, p=, q=, dp=, dq=, qi=}', jwk.toString() + // java toString + + //but serialization prints the real value: + byte[] data = serializer.serialize(jwk) + def result = new String(data, StandardCharsets.UTF_8) + // assert substrings here because JSON order is not guaranteed: + assertTrue result.contains('"kid":"id"') + assertTrue result.contains('"kty":"RSA"') + assertTrue result.contains('"e":"AQAB"') + assertTrue result.contains("\"n\":\"${jwk.n}\"" as String) //public property, not wrapped + assertTrue result.contains("\"d\":\"${jwk.d.get()}\"" as String) // all remaining should be wrapped + assertTrue result.contains("\"p\":\"${jwk.p.get()}\"" as String) + assertTrue result.contains("\"q\":\"${jwk.q.get()}\"" as String) + assertTrue result.contains("\"dp\":\"${jwk.dp.get()}\"" as String) + assertTrue result.contains("\"dq\":\"${jwk.dq.get()}\"" as String) + assertTrue result.contains("\"qi\":\"${jwk.qi.get()}\"" as String) + + //now ensure it deserializes back to a JWK: + def map = deserializer.deserialize(data) as Map + def jwk2 = Jwks.builder().putAll(map).build() + assertEquals(jwk, jwk2, privateFieldNames) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index bd60e682c..cdaa9f6f3 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -1,6 +1,22 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.impl.security import io.jsonwebtoken.impl.lang.Converters +import io.jsonwebtoken.impl.lang.RedactedSupplier import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.security.* @@ -109,8 +125,8 @@ class JwksTest { assertEquals 'oct', jwk.getType() assertEquals 'oct', jwk.kty assertNotNull jwk.k - assertTrue jwk.k instanceof String - assertTrue MessageDigest.isEqual(SKEY.encoded, Decoders.BASE64URL.decode(jwk.k as String)) + assertTrue jwk.k instanceof RedactedSupplier + assertTrue MessageDigest.isEqual(SKEY.encoded, Decoders.BASE64URL.decode(jwk.k.get())) } @Test diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy index 2aad46dc7..4b8d3d8c9 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.impl.security import io.jsonwebtoken.impl.lang.Converters @@ -84,7 +99,7 @@ class RFC7517AppendixA2Test { assertEquals m.x, ecEncode(fieldSize, jwk.toPublicJwk().toKey().w.affineX) assertEquals m.y, jwk.get('y') assertEquals m.y, ecEncode(fieldSize, jwk.toPublicJwk().toKey().w.affineY) - assertEquals m.d, jwk.get('d') + assertEquals m.d, jwk.get('d').get() assertEquals m.d, ecEncode(fieldSize, key.s) assertEquals m.use, jwk.getPublicKeyUse() assertEquals m.kid, jwk.getId() @@ -99,17 +114,17 @@ class RFC7517AppendixA2Test { assertEquals m.n, rsaEncode(key.modulus) assertEquals m.e, jwk.get('e') assertEquals m.e, rsaEncode(jwk.toPublicJwk().toKey().publicExponent) - assertEquals m.d, jwk.get('d') + assertEquals m.d, jwk.get('d').get() assertEquals m.d, rsaEncode(key.privateExponent) - assertEquals m.p, jwk.get('p') + assertEquals m.p, jwk.get('p').get() assertEquals m.p, rsaEncode(key.getPrimeP()) - assertEquals m.q, jwk.get('q') + assertEquals m.q, jwk.get('q').get() assertEquals m.q, rsaEncode(key.getPrimeQ()) - assertEquals m.dp, jwk.get('dp') + assertEquals m.dp, jwk.get('dp').get() assertEquals m.dp, rsaEncode(key.getPrimeExponentP()) - assertEquals m.dq, jwk.get('dq') + assertEquals m.dq, jwk.get('dq').get() assertEquals m.dq, rsaEncode(key.getPrimeExponentQ()) - assertEquals m.qi, jwk.get('qi') + assertEquals m.qi, jwk.get('qi').get() assertEquals m.qi, rsaEncode(key.getCrtCoefficient()) assertEquals m.alg, jwk.getAlgorithm() assertEquals m.kid, jwk.getId() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA3Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA3Test.groovy index 3bcf49840..18a41e0b8 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA3Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA3Test.groovy @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.impl.security import io.jsonwebtoken.io.Encoders @@ -42,7 +57,7 @@ class RFC7517AppendixA3Test { assertEquals m.size(), jwk.size() assertEquals m.kty, jwk.getType() assertEquals m.alg, jwk.getAlgorithm() - assertEquals m.k, jwk.get('k') + assertEquals m.k, jwk.get('k').get() assertEquals m.k, encode(key) m = keys[1] @@ -51,7 +66,7 @@ class RFC7517AppendixA3Test { assertNotNull key assertEquals m.size(), jwk.size() assertEquals m.kty, jwk.getType() - assertEquals m.k, jwk.get('k') + assertEquals m.k, jwk.get('k').get() assertEquals m.k, encode(key) assertEquals m.kid, jwk.getId() } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy index 5d3971f11..35056117b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.impl.security import io.jsonwebtoken.impl.lang.Converters @@ -171,7 +186,7 @@ class RsaPrivateJwkFactoryTest { assertEquals 'RSA', jwk.getType() assertEquals Converters.BIGINT.applyTo(pub.getModulus()), jwk.get(DefaultRsaPublicJwk.MODULUS.getId()) assertEquals Converters.BIGINT.applyTo(pub.getPublicExponent()), jwk.get(DefaultRsaPublicJwk.PUBLIC_EXPONENT.getId()) - assertEquals Converters.BIGINT.applyTo(priv.getPrivateExponent()), jwk.get(DefaultRsaPrivateJwk.PRIVATE_EXPONENT.getId()) + assertEquals Converters.BIGINT.applyTo(priv.getPrivateExponent()), jwk.get(DefaultRsaPrivateJwk.PRIVATE_EXPONENT.getId()).get() } @Test From c3d218a0548ef5f834bd49c29f74c34e0433ff4f Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Mon, 23 May 2022 20:19:00 -0700 Subject: [PATCH 51/75] - Updated JavaDoc to reflect JWK toString safety and property access - Updated README to reflect new GsonSerializer requirements for io.jsonwebtoken.lang.Supplier --- README.md | 21 +++++- .../jsonwebtoken/security/EcPrivateJwk.java | 18 ++--- .../io/jsonwebtoken/security/EcPublicJwk.java | 18 ++--- .../java/io/jsonwebtoken/security/Jwk.java | 70 +++++++++---------- .../jsonwebtoken/security/RsaPrivateJwk.java | 18 ++--- .../jsonwebtoken/security/RsaPublicJwk.java | 18 ++--- .../io/jsonwebtoken/security/SecretJwk.java | 12 +--- 7 files changed, 93 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 80f059046..117c60413 100644 --- a/README.md +++ b/README.md @@ -1467,11 +1467,15 @@ all that is required, no code or config is necessary. If you're curious, JJWT will automatically create an internal default Gson instance for its own needs as follows: ```java -new GsonBuilder().disableHtmlEscaping().create(); +new GsonBuilder() + .registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, GsonSupplierSerializer.INSTANCE) + .disableHtmlEscaping().create(); ``` +The `registerTypeHierarchyAdapter` builder call is required to serialize JWKs with secret or private values. + However, if you prefer to use a different Gson instance instead of JJWT's default, you can configure JJWT to use your -own. +own - just don't forget to register the necessary JJWT type hierarchy adapter. You do this by declaring the `io.jsonwebtoken:jjwt-gson` dependency with **compile** scope (not runtime scope which is the typical JJWT default). That is: @@ -1499,7 +1503,10 @@ And then you can specify the `GsonSerializer` using your own `Gson` instance on ```java -Gson gson = getGson(); //implement me +Gson gson = new GsonBuilder() + // don't forget this line!: + .registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, GsonSupplierSerializer.INSTANCE) + .disableHtmlEscaping().create(); String jws = Jwts.builder() @@ -1520,6 +1527,14 @@ Jwts.parser() // ... etc ... ``` +Again, as shown above, it is critical to create your `Gson` instance using the `GsonBuilder` and include the line: + +```java +.registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, GsonSupplierSerializer.INSTANCE) +``` + +to ensure JWK serialization works as expected. + ## Base64 Support diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java index 10a0e185b..368df8292 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java @@ -25,15 +25,17 @@ * *

    Note that the various EC-specific properties are not available as separate dedicated getter methods, as most Java * applications should rarely, if ever, need to access these individual key properties since they typically represent - * internal key material and/or implementation details.

    + * internal key material and/or serialization details. If you need to access these key properties, it is usually + * recommended to obtain the corresponding {@link ECPrivateKey} instance returned by {@link #toKey()} and + * query that instead.

    * - *

    Even so, because they exist and are readable by nature of every JWK being a {@link java.util.Map Map}, the - * properties are still accessible in two different ways:

    - *
      - *
    • Via the standard {@code Map} {@link #get(Object) get} method using an appropriate JWK parameter id, - * e.g. {@code jwk.get("x")}, {@code jwk.get("y")}, etc.
    • - *
    • Via the various getter methods on the {@link ECPrivateKey} instance returned by {@link #toKey()}.
    • - *
    + *

    Even so, because these properties exist and are readable by nature of every JWK being a + * {@link java.util.Map Map}, they are still accessible via the standard {@code Map} {@link #get(Object) get} method + * using an appropriate JWK parameter id, for example:

    + *
    + * jwk.get("x");
    + * jwk.get("y");
    + * // ... etc ...
    * * @since JJWT_RELEASE_VERSION */ diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java b/api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java index 4806bd756..0b55f57d1 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java @@ -24,15 +24,17 @@ * *

    Note that the various EC-specific properties are not available as separate dedicated getter methods, as most Java * applications should rarely, if ever, need to access these individual key properties since they typically represent - * internal key material and/or implementation details.

    + * internal key material and/or serialization details. If you need to access these key properties, it is usually + * recommended to obtain the corresponding {@link ECPublicKey} instance returned by {@link #toKey()} and + * query that instead.

    * - *

    Even so, because they exist and are readable by nature of every JWK being a {@link java.util.Map Map}, the - * properties are still accessible in two different ways:

    - *
      - *
    • Via the standard {@code Map} {@link #get(Object) get} method using an appropriate JWK parameter id, - * e.g. {@code jwk.get("x")}, {@code jwk.get("y")}, etc.
    • - *
    • Via the various getter methods on the {@link ECPublicKey} instance returned by {@link #toKey()}.
    • - *
    + *

    Even so, because these properties exist and are readable by nature of every JWK being a + * {@link java.util.Map Map}, they are still accessible via the standard {@code Map} {@link #get(Object) get} method + * using an appropriate JWK parameter id, for example:

    + *
    + * jwk.get("x");
    + * jwk.get("y");
    + * // ... etc ...
    * * @since JJWT_RELEASE_VERSION */ diff --git a/api/src/main/java/io/jsonwebtoken/security/Jwk.java b/api/src/main/java/io/jsonwebtoken/security/Jwk.java index b9ced982e..991feb402 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Jwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/Jwk.java @@ -16,6 +16,7 @@ package io.jsonwebtoken.security; import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.lang.Supplier; import java.security.Key; import java.util.Map; @@ -44,47 +45,40 @@ * {@code aJwk.get("kid")}. Either approach will return an id if one was originally set on the JWK, or {@code null} if * an id does not exist.

    * - *

    toString Safety

    + *

    Private and Secret Value Safety

    * *

    JWKs often represent secret or private key data which should never be exposed publicly, nor mistakenly printed * via application logs or {@code System.out.println} calls. As a result, all JJWT JWK - * {@link String#toString() toString()} implementations automatically print redacted values instead actual - * values for any private or secret fields.

    + * private or secret field values are 'wrapped' in a {@link io.jsonwebtoken.lang.Supplier Supplier} instance to ensure + * any attempt to call {@link String#toString() toString()} on the value will print a redacted value instead of an + * actual private or secret value.

    * *

    For example, a {@link SecretJwk} will have an internal "{@code k}" member whose value reflects raw - * key material that should always be kept secret. If {@code aSecretJwk.toString()} is called, the resulting string - * will contain the substring k=<redacted>, instead of the actual {@code k} value. The string - * literal <redacted> is printed everywhere a private or secret value would have otherwise.

    - * - *

    WARNING: Note however, certain JVM programming languages (like - * - * Groovy for example) when encountering a - * Map or Collection instance, will NOT always call an object's {@code toString()} method when rendering - * strings. Because all JJWT JWKs implement the {@link Map Map} interface, in these language environments, - * you must explicitly call {@code aJwk.toString()} method to override the language's built-in string rendering to - * ensure key safety. This is not a concern if using the Java language directly.

    - * - *

    For example, this is safe in Java:

    - *
    
    - *     String s = "My JWK is: " + aSecretJwk; //or String.format("My JWK is: %s", aSecretJwk)
    - *     System.out.println(s);
    - * 
    - * - *

    Whereas the same is NOT SAFE in Groovy:

    - *
    
    - *     println "My JWK is: ${aSecretJwk}" // or "My JWK is " + aSecretJwk
    - * 
    - * - *

    But the following IS safe in Groovy:

    - *
    
    - *     println "My JWK is: ${aSecretJwk.toString()}" // or "My JWK is " + aSecretJwk.toString()
    - * 
    - *

    Because Groovy's {@code GString} concept does not call {@code Map#toString()} directly and creates its own - * toString implementation with the raw name/value pairs, you must call {@link String#toString() toString()} - * explicitly.

    - * - *

    If you are using an alternative JVM programming language other than Java, understand your language - * environment's String rendering behavior and adjust for explicit {@code toString()} calls as necessary.

    + * key material that should always be kept secret. If the following is called:

    + *
    + * System.out.println(aSecretJwk.get("k"));
    + *

    You would see the following:

    + *
    + * <redacted>
    + *

    instead of the actual/raw {@code k} value.

    + * + *

    Similarly, if the attempting to print the entire JWK:

    + *
    + * System.out.println(aSecretJwk);
    + *

    You would see the following substring in the output:

    + *
    + * k=<redacted>
    + *

    instead of the actual/raw {@code k} value.

    + * + *

    Finally, because all private or secret field values are wrapped as {@link io.jsonwebtoken.lang.Supplier} + * instances, if you really wanted the real internal value, you could just call the supplier's + * {@link Supplier#get() get()} method:

    + *
    + * String k = ((Supplier<String>)aSecretJwk.get("k")).get();
    + *

    but BE CAREFUL: obtaining the raw value in your application code exposes greater security + * risk - you must ensure to keep that value safe and out of console or log output. It is almost always better to + * interact with the JWK's {@link #toKey() toKey()} instance directly instead of accessing + * JWK internal serialization fields.

    * * @since JJWT_RELEASE_VERSION */ @@ -201,10 +195,10 @@ public interface Jwk extends Identifiable, Map { String getType(); /** - * Converts the JWK to its corresponding Java {@link Key} instance for use with Java cryptographic + * Represents the JWK as its corresponding Java {@link Key} instance for use with Java cryptographic * APIs. * - * @return the corresponding Java {@link Key} instance for use with Java cryptographic APIs. + * @return the JWK's corresponding Java {@link Key} instance for use with Java cryptographic APIs. */ K toKey(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java index d345e480e..cbf74b2fd 100644 --- a/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java @@ -25,15 +25,17 @@ * *

    Note that the various RSA-specific properties are not available as separate dedicated getter methods, as most Java * applications should rarely, if ever, need to access these individual key properties since they typically represent - * internal key material and/or implementation details.

    + * internal key material and/or serialization details. If you need to access these key properties, it is usually + * recommended to obtain the corresponding {@link RSAPrivateKey} instance returned by {@link #toKey()} and + * query that instead.

    * - *

    Even so, because they exist and are readable by nature of every JWK being a {@link java.util.Map Map}, the - * properties are still accessible in two different ways:

    - *
      - *
    • Via the standard {@code Map} {@link #get(Object) get} method using an appropriate JWK parameter id, - * e.g. {@code jwk.get("n")}, {@code jwk.get("e")}, etc.
    • - *
    • Via the various getter methods on the {@link RSAPrivateKey} instance returned by {@link #toKey()}.
    • - *
    + *

    Even so, because these properties exist and are readable by nature of every JWK being a + * {@link java.util.Map Map}, they are still accessible via the standard {@code Map} {@link #get(Object) get} method + * using an appropriate JWK parameter id, for example:

    + *
    + * jwk.get("n");
    + * jwk.get("e");
    + * // ... etc ...
    * * @since JJWT_RELEASE_VERSION */ diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java b/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java index 9fa9f100a..3cb8dc28a 100644 --- a/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java @@ -24,15 +24,17 @@ * *

    Note that the various RSA-specific properties are not available as separate dedicated getter methods, as most Java * applications should rarely, if ever, need to access these individual key properties since they typically represent - * internal key material and/or implementation details.

    + * internal key material and/or serialization details. If you need to access these key properties, it is usually + * recommended to obtain the corresponding {@link RSAPublicKey} instance returned by {@link #toKey()} and + * query that instead.

    * - *

    Even so, because they exist and are readable by nature of every JWK being a {@link java.util.Map Map}, the - * properties are still accessible in two different ways:

    - *
      - *
    • Via the standard {@code Map} {@link #get(Object) get} method using an appropriate JWK parameter id, - * e.g. {@code jwk.get("n")}, {@code jwk.get("e")}, etc.
    • - *
    • Via the various getter methods on the {@link RSAPublicKey} instance returned by {@link #toKey()}.
    • - *
    + *

    Even so, because these properties exist and are readable by nature of every JWK being a + * {@link java.util.Map Map}, they are still accessible via the standard {@code Map} {@link #get(Object) get} method + * using an appropriate JWK parameter id, for example:

    + *
    + * jwk.get("n");
    + * jwk.get("e");
    + * // ... etc ...
    * * @since JJWT_RELEASE_VERSION */ diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretJwk.java b/api/src/main/java/io/jsonwebtoken/security/SecretJwk.java index 019768b2c..b112a4c92 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecretJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecretJwk.java @@ -23,15 +23,9 @@ * *

    Note that the {@code SecretKey}-specific properties are not available as separate dedicated getter methods, as * most Java applications should rarely, if ever, need to access these individual key properties since they typically - * represent internal key material and/or implementation details.

    - * - *

    Even so, because they exist and are readable by nature of every JWK being a {@link java.util.Map Map}, the - * properties are still accessible in two different ways:

    - *
      - *
    • Via the standard {@code Map} {@link #get(Object) get} method using an appropriate JWK parameter id, - * e.g. {@code jwk.get("k")}.
    • - *
    • Via the various getter methods on the {@link SecretKey} instance returned by {@link #toKey()}.
    • - *
    + * internal key material and/or serialization details. If you need to access these key properties, it is usually + * recommended to obtain the corresponding {@link SecretKey} instance returned by {@link #toKey()} and + * query that instead.

    * * @since JJWT_RELEASE_VERSION */ From a1f404f2cc94496f1debe0e2ae640d15999ebbf8 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Tue, 24 May 2022 20:31:15 -0700 Subject: [PATCH 52/75] Documentation enhancements --- CHANGELOG.md | 15 +- .../io/jsonwebtoken/ClaimJwtException.java | 6 + .../jsonwebtoken/InvalidClaimException.java | 7 + .../main/java/io/jsonwebtoken/JwtBuilder.java | 125 +++++++- .../io/jsonwebtoken/JwtHandlerAdapter.java | 6 + .../io/jsonwebtoken/JwtParserBuilder.java | 33 ++ .../java/io/jsonwebtoken/LocatorAdapter.java | 42 ++- .../jsonwebtoken/MalformedJwtException.java | 11 + .../jsonwebtoken/MissingClaimException.java | 21 +- .../jsonwebtoken/PrematureJwtException.java | 19 +- .../jsonwebtoken/RequiredTypeException.java | 11 + .../io/jsonwebtoken/SignatureException.java | 11 + .../SigningKeyResolverAdapter.java | 35 ++- .../jsonwebtoken/UnsupportedJwtException.java | 11 + .../java/io/jsonwebtoken/lang/Assert.java | 1 + .../java/io/jsonwebtoken/lang/Objects.java | 35 ++- .../jsonwebtoken/lang/RuntimeEnvironment.java | 13 + .../java/io/jsonwebtoken/lang/Strings.java | 245 ++++++++++----- .../jsonwebtoken/security/AsymmetricJwk.java | 1 + .../security/AsymmetricJwkBuilder.java | 26 ++ .../AsymmetricKeySignatureAlgorithm.java | 2 + .../security/DecryptionKeyRequest.java | 1 + .../security/EncryptionAlgorithms.java | 24 +- .../java/io/jsonwebtoken/security/Jwk.java | 1 + .../io/jsonwebtoken/security/JwkBuilder.java | 3 + .../jsonwebtoken/security/KeyAlgorithms.java | 287 +++++++++++++++++- .../jsonwebtoken/security/KeyException.java | 11 + .../io/jsonwebtoken/security/KeyRequest.java | 1 + .../io/jsonwebtoken/security/KeySupplier.java | 1 + .../security/MalformedKeyException.java | 11 + .../io/jsonwebtoken/security/PrivateJwk.java | 3 + .../security/ProtoJwkBuilder.java | 3 + .../io/jsonwebtoken/security/PublicJwk.java | 1 + .../security/SignatureAlgorithm.java | 2 + .../security/SignatureAlgorithms.java | 238 +++++++-------- .../security/SignatureException.java | 11 + .../security/SignatureRequest.java | 1 + .../security/UnsupportedKeyException.java | 11 + .../security/VerifySignatureRequest.java | 1 + .../security/WeakKeyException.java | 5 + .../jsonwebtoken/impl/DefaultJwtBuilder.java | 2 +- .../DefaultRsaSignatureAlgorithm.java | 2 +- .../security/SignatureAlgorithmsBridge.java | 10 +- .../io/jsonwebtoken/security/KeysTest.groovy | 7 +- .../security/SignatureAlgorithmsTest.groovy | 4 +- 45 files changed, 1056 insertions(+), 261 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f32f243f8..8cd1967a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,12 +15,25 @@ allow for customization of the JCA `Provider` and `SecureRandom` during Key or KeyPair generation if desired, whereas the old enum-based static utility methods did not. -#### Backwards Compatibility Warning +#### Backwards Compatibility Breaking Changes + +* Parsing of unsecured JWTs (`alg` header of `none`) are now disabled by default as mandated by + [RFC 7518, Section 3.6](https://datatracker.ietf.org/doc/html/rfc7518#section-3.6). If you require parsing of + unsecured JWTs, you may call the `enableUnsecuredJws` method on the `JwtParserBuilder`, but note the security + implications of doing so as mentioned in that method's JavaDoc before doing so. * `io.jsonwebtoken.JwtHandlerAdapter` has been changed to add the `abstract` modifier. This class was never intended to be instantiated directly, and is provided for subclassing benefits. The missing modifier has been added to ensure the class is used as it had always been intended. +* `io.jsonwebtokne.gson.io.GsonSerializer` now requires `Gson` instances that have a registered + `GsonSupplierSerializer` type adapter, for example: + ```java + new GsonBuilder() + .registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, GsonSupplierSerializer.INSTANCE) + .disableHtmlEscaping().create(); + ``` + ### 0.11.5 This patch release adds additional security guards against an ECDSA bug in Java SE versions 15-15.0.6, 17-17.0.2, and 18 diff --git a/api/src/main/java/io/jsonwebtoken/ClaimJwtException.java b/api/src/main/java/io/jsonwebtoken/ClaimJwtException.java index d01729fb1..420a093de 100644 --- a/api/src/main/java/io/jsonwebtoken/ClaimJwtException.java +++ b/api/src/main/java/io/jsonwebtoken/ClaimJwtException.java @@ -40,8 +40,14 @@ public abstract class ClaimJwtException extends JwtException { @Deprecated public static final String MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was not present in the JWT claims."; + /** + * The header associated with the Claims that failed validation. + */ private final Header header; + /** + * The Claims that failed validation. + */ private final Claims claims; /** diff --git a/api/src/main/java/io/jsonwebtoken/InvalidClaimException.java b/api/src/main/java/io/jsonwebtoken/InvalidClaimException.java index f3a397dbc..c7cf0283c 100644 --- a/api/src/main/java/io/jsonwebtoken/InvalidClaimException.java +++ b/api/src/main/java/io/jsonwebtoken/InvalidClaimException.java @@ -25,7 +25,14 @@ */ public class InvalidClaimException extends ClaimJwtException { + /** + * The name of the invalid claim. + */ private String claimName; + + /** + * The claim value that could not be validated. + */ private Object claimValue; /** diff --git a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java index ad2f94059..5ce4a5cfd 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -22,10 +22,15 @@ import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureAlgorithms; +import io.jsonwebtoken.security.WeakKeyException; +import javax.crypto.SecretKey; import java.security.Key; +import java.security.PrivateKey; import java.security.Provider; import java.security.SecureRandom; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAKey; import java.util.Date; import java.util.Map; @@ -369,19 +374,128 @@ public interface JwtBuilder> extends ClaimsMutator { T claim(String name, Object value); /** - * Signs the constructed JWT with the specified key using the key's - * {@link SignatureAlgorithms#forSigningKey(Key) recommended signature algorithm}, producing a JWS. If the - * recommended signature algorithm isn't sufficient for your needs, consider using - * {@link #signWith(Key, io.jsonwebtoken.security.SignatureAlgorithm)} instead. + * Signs the constructed JWT with the specified key using the key's recommended signature algorithm + * as defined below, producing a JWS. If the recommended signature algorithm isn't sufficient for your needs, + * consider using {@link #signWith(Key, io.jsonwebtoken.security.SignatureAlgorithm)} instead. * *

    If you are looking to invoke this method with a byte array that you are confident may be used for HMAC-SHA * algorithms, consider using {@link Keys Keys}.{@link Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor(bytes)} to * convert the byte array into a valid {@code Key}.

    * + *

    Recommended Signature Algorithm

    + * + *

    The recommended signature algorithm used with a given key is chosen based on the following:

    + *
    JWK Key Operations
    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Key Recommended Signature Algorithm
    If the Key is a:And:With a key size of:The SignatureAlgorithm used will be:
    {@link SecretKey}{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA256")1256 <= size <= 383 2{@link SignatureAlgorithms#HS256 HS256}
    {@link SecretKey}{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA384")1384 <= size <= 511{@link SignatureAlgorithms#HS384 HS384}
    {@link SecretKey}{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA512")1512 <= size{@link SignatureAlgorithms#HS512 HS512}
    {@link ECKey}instanceof {@link PrivateKey}256 <= size <= 383 3{@link SignatureAlgorithms#ES256 ES256}
    {@link ECKey}instanceof {@link PrivateKey}384 <= size <= 520 4{@link SignatureAlgorithms#ES384 ES384}
    {@link ECKey}instanceof {@link PrivateKey}521 <= size 4{@link SignatureAlgorithms#ES512 ES512}
    {@link RSAKey}instanceof {@link PrivateKey}2048 <= size <= 3071 5,6{@link SignatureAlgorithms#RS256 RS256}
    {@link RSAKey}instanceof {@link PrivateKey}3072 <= size <= 4095 6{@link SignatureAlgorithms#RS384 RS384}
    {@link RSAKey}instanceof {@link PrivateKey}4096 <= size 5{@link SignatureAlgorithms#RS512 RS512}
    + *

    Notes:

    + *
      + *
    1. {@code SecretKey} instances must have an {@link Key#getAlgorithm() algorithm} name equal + * to {@code HmacSHA256}, {@code HmacSHA384} or {@code HmacSHA512}. If not, the key bytes might not be + * suitable for HMAC signatures will be rejected with a {@link InvalidKeyException}.
    2. + *
    3. The JWT JWA Specification (RFC 7518, + * Section 3.2) mandates that HMAC-SHA-* signing keys MUST be 256 bits or greater. + * {@code SecretKey}s with key lengths less than 256 bits will be rejected with an + * {@link WeakKeyException}.
    4. + *
    5. The JWT JWA Specification (RFC 7518, + * Section 3.4) mandates that ECDSA signing key lengths MUST be 256 bits or greater. + * {@code ECKey}s with key lengths less than 256 bits will be rejected with a + * {@link WeakKeyException}.
    6. + *
    7. The ECDSA {@code P-521} curve does indeed use keys of 521 bits, not 512 as might be expected. ECDSA + * keys of 384 < size <= 520 are suitable for ES384, while ES512 requires keys >= 521 bits. The '512' part of the + * ES512 name reflects the usage of the SHA-512 algorithm, not the ECDSA key length. ES512 with ECDSA keys less + * than 521 bits will be rejected with a {@link WeakKeyException}.
    8. + *
    9. The JWT JWA Specification (RFC 7518, + * Section 3.3) mandates that RSA signing key lengths MUST be 2048 bits or greater. + * {@code RSAKey}s with key lengths less than 2048 bits will be rejected with a + * {@link WeakKeyException}.
    10. + *
    11. Technically any RSA key of length >= 2048 bits may be used with the + * {@link SignatureAlgorithms#RS256 RS256}, {@link SignatureAlgorithms#RS384 RS384}, and + * {@link SignatureAlgorithms#RS512 RS512} algorithms, so we assume an RSA signature algorithm based on the key + * length to parallel similar decisions in the JWT specification for HMAC and ECDSA signature algorithms. + * This is not required - just a convenience.
    12. + *
    + * + *

    This implementation does not use the {@link SignatureAlgorithms#PS256 PS256}, + * {@link SignatureAlgorithms#PS384 PS384}, or {@link SignatureAlgorithms#PS512 PS512} RSA variants for any + * specified {@link RSAKey} because the the {@link SignatureAlgorithms#RS256 RS256}, + * {@link SignatureAlgorithms#RS384 RS384}, and {@link SignatureAlgorithms#RS512 RS512} algorithms are + * available in the JDK by default while the {@code PS}* variants require either JDK 11 or an additional JCA + * Provider (like BouncyCastle). If you wish to use a {@code PS}* variant with your key, use the + * {@link #signWith(Key, io.jsonwebtoken.security.SignatureAlgorithm)} method instead.

    + * + *

    Finally, this method will throw an {@link InvalidKeyException} for any key that does not match the + * heuristics and requirements documented above, since that inevitably means the Key is either insufficient or + * explicitly disallowed by the JWT specification.

    + * * @param key the key to use for signing * @return the builder instance for method chaining. * @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification as - * described by {@link SignatureAlgorithms#forSigningKey(Key)}. + * described above in recommended signature algorithms. * @see #signWith(Key, io.jsonwebtoken.security.SignatureAlgorithm) * @since 0.10.0 */ @@ -517,7 +631,6 @@ public interface JwtBuilder> extends ClaimsMutator { * @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification for * the specified algorithm. * @see #signWith(Key) - * @see SignatureAlgorithms#forSigningKey(Key) * @since JJWT_RELEASE_VERSION */ T signWith(K key, io.jsonwebtoken.security.SignatureAlgorithm alg) throws InvalidKeyException; diff --git a/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java b/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java index e96d8b1e2..8a3e3db03 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java @@ -30,6 +30,12 @@ */ public abstract class JwtHandlerAdapter implements JwtHandler { + /** + * Constructs a new instance, where all default method implementations throw an {@link UnsupportedJwtException}. + */ + public JwtHandlerAdapter() { + } + @Override public T onPlaintextJwt(Jwt jwt) { throw new UnsupportedJwtException("Unsigned plaintext JWTs are not supported."); diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index 72bc3773e..30e211797 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -22,6 +22,7 @@ import io.jsonwebtoken.security.EncryptionAlgorithms; import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.SignatureAlgorithm; +import io.jsonwebtoken.security.SignatureAlgorithms; import java.security.Key; import java.security.Provider; @@ -384,8 +385,40 @@ public interface JwtParserBuilder extends Builder { */ JwtParserBuilder addEncryptionAlgorithms(Collection encAlgs); + /** + * Adds the specified signature algorithms to the parser's total set of supported signature algorithms, + * overwriting any previously-added algorithms with the same {@link SignatureAlgorithm#getId() id}s. + * + *

    There may be only one registered {@code SignatureAlgorithm} per algorithm {@code id}, and the {@code sigAlgs} + * collection is added in iteration order; if a duplicate id is found when iterating the {@code sigAlgs} + * collection, the later element will evict any previously-added algorithm with the same {@code id}.

    + * + *

    Finally, the {@link SignatureAlgorithms#values() JWA standard signature algorithms} are added last, + * after those in the {@code sigAlgs} collection, to ensure that JWA standard algorithms cannot be + * accidentally replaced.

    + * + * @param sigAlgs collection of signature algorithms to add to the parser's total set of supported signature + * algorithms. + * @return the builder for method chaining. + */ JwtParserBuilder addSignatureAlgorithms(Collection> sigAlgs); + /** + * Adds the specified key management algorithms to the parser's total set of supported key algorithms, + * overwriting any previously-added algorithms with the same {@link KeyAlgorithm#getId() id}s. + * + *

    There may be only one registered {@code KeyAlgorithm} per algorithm {@code id}, and the {@code keyAlgs} + * collection is added in iteration order; if a duplicate id is found when iterating the {@code keyAlgs} + * collection, the later element will evict any previously-added algorithm with the same {@code id}.

    + * + *

    Finally, the {@link io.jsonwebtoken.security.KeyAlgorithms#values() JWA standard key management algorithms} + * are added last, after those in the {@code keyAlgs} collection, to ensure that JWA standard algorithms + * cannot be accidentally replaced.

    + * + * @param keyAlgs collection of key management algorithms to add to the parser's total set of supported key + * management algorithms. + * @return the builder for method chaining. + */ JwtParserBuilder addKeyAlgorithms(Collection> keyAlgs); /** diff --git a/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java b/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java index 11910ac61..16ed475b4 100644 --- a/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java @@ -24,8 +24,24 @@ * * @since JJWT_RELEASE_VERSION */ -public class LocatorAdapter implements Locator { +public abstract class LocatorAdapter implements Locator { + /** + * Constructs a new instance, where all default method implementations return {@code null}. + */ + public LocatorAdapter() { + } + + /** + * Inspects the specified header, and delegates to the respective + * {@link #locate(JweHeader)}, {@link #locate(JwsHeader)} or {@link #doLocate(Header)} methods if the encountered + * header is a {@link JweHeader}, {@link JwsHeader} or Unprotected {@link Header}. + * + * @param header the JWT header to inspect; may be an instance of {@link Header}, {@link JwsHeader} or + * {@link JweHeader} depending on if the respective JWT is an unprotected JWT, JWS or JWE. + * @return an object referenced in the specified header, or {@code null} if the referenced object cannot be found + * or does not exist. + */ @Override public final T locate(Header header) { Assert.notNull(header, "Header cannot be null."); @@ -40,14 +56,38 @@ public final T locate(Header header) { } } + /** + * Returns an object referenced in the specified JWE header, or {@code null} if the referenced + * object cannot be found or does not exist. + * + * @param header the header of an encountered JWE. + * @return an object referenced in the specified JWE header, or {@code null} if the referenced + * object cannot be found or does not exist. + */ protected T locate(JweHeader header) { return null; } + /** + * Returns an object referenced in the specified JWS header, or {@code null} if the referenced + * object cannot be found or does not exist. + * + * @param header the header of an encountered JWS. + * @return an object referenced in the specified JWS header, or {@code null} if the referenced + * object cannot be found or does not exist. + */ protected T locate(JwsHeader header) { return null; } + /** + * Returns an object referenced in the specified Unprotected JWT header, or {@code null} if the referenced + * object cannot be found or does not exist. + * + * @param header the header of an encountered JWE. + * @return an object referenced in the specified Unprotected JWT header, or {@code null} if the referenced + * object cannot be found or does not exist. + */ protected T doLocate(Header header) { return null; } diff --git a/api/src/main/java/io/jsonwebtoken/MalformedJwtException.java b/api/src/main/java/io/jsonwebtoken/MalformedJwtException.java index 6490550aa..5729388d6 100644 --- a/api/src/main/java/io/jsonwebtoken/MalformedJwtException.java +++ b/api/src/main/java/io/jsonwebtoken/MalformedJwtException.java @@ -22,10 +22,21 @@ */ public class MalformedJwtException extends JwtException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public MalformedJwtException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public MalformedJwtException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/MissingClaimException.java b/api/src/main/java/io/jsonwebtoken/MissingClaimException.java index 030fe98d0..bbcc668ea 100644 --- a/api/src/main/java/io/jsonwebtoken/MissingClaimException.java +++ b/api/src/main/java/io/jsonwebtoken/MissingClaimException.java @@ -22,11 +22,28 @@ * @since 0.6 */ public class MissingClaimException extends InvalidClaimException { - public MissingClaimException(Header header, Claims claims, String message) { + + /** + * Creates a new instance with the specified explanation message. + * + * @param header the header associated with the claims that did not contain the required claim + * @param claims the claims that did not contain the required claim + * @param message the message explaining why the exception is thrown. + */ + public MissingClaimException(Header header, Claims claims, String message) { super(header, claims, message); } - public MissingClaimException(Header header, Claims claims, String message, Throwable cause) { + + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param header the header associated with the claims that did not contain the required claim + * @param claims the claims that did not contain the required claim + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ + public MissingClaimException(Header header, Claims claims, String message, Throwable cause) { super(header, claims, message, cause); } } diff --git a/api/src/main/java/io/jsonwebtoken/PrematureJwtException.java b/api/src/main/java/io/jsonwebtoken/PrematureJwtException.java index 8853832e8..f6045730b 100644 --- a/api/src/main/java/io/jsonwebtoken/PrematureJwtException.java +++ b/api/src/main/java/io/jsonwebtoken/PrematureJwtException.java @@ -22,18 +22,27 @@ */ public class PrematureJwtException extends ClaimJwtException { - public PrematureJwtException(Header header, Claims claims, String message) { + /** + * Creates a new instance with the specified explanation message. + * + * @param header jwt header + * @param claims jwt claims (body) + * @param message the message explaining why the exception is thrown. + */ + public PrematureJwtException(Header header, Claims claims, String message) { super(header, claims, message); } /** - * @param header jwt header - * @param claims jwt claims (body) + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param header jwt header + * @param claims jwt claims (body) * @param message exception message - * @param cause cause + * @param cause cause * @since 0.5 */ - public PrematureJwtException(Header header, Claims claims, String message, Throwable cause) { + public PrematureJwtException(Header header, Claims claims, String message, Throwable cause) { super(header, claims, message, cause); } } diff --git a/api/src/main/java/io/jsonwebtoken/RequiredTypeException.java b/api/src/main/java/io/jsonwebtoken/RequiredTypeException.java index 44124615d..77a0035da 100644 --- a/api/src/main/java/io/jsonwebtoken/RequiredTypeException.java +++ b/api/src/main/java/io/jsonwebtoken/RequiredTypeException.java @@ -23,10 +23,21 @@ */ public class RequiredTypeException extends JwtException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public RequiredTypeException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public RequiredTypeException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/SignatureException.java b/api/src/main/java/io/jsonwebtoken/SignatureException.java index 365ec34d7..7a54cda1d 100644 --- a/api/src/main/java/io/jsonwebtoken/SignatureException.java +++ b/api/src/main/java/io/jsonwebtoken/SignatureException.java @@ -26,10 +26,21 @@ @Deprecated public class SignatureException extends SecurityException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public SignatureException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public SignatureException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java b/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java index 8931d5df5..784ba2126 100644 --- a/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java @@ -55,13 +55,20 @@ @Deprecated public class SigningKeyResolverAdapter implements SigningKeyResolver { + /** + * Default constructor. + */ + public SigningKeyResolverAdapter() { + + } + @Override public Key resolveSigningKey(JwsHeader header, Claims claims) { SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); - Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, Claims) implementation cannot be " + - "used for asymmetric key algorithms (RSA, Elliptic Curve). " + - "Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " + - "Key instance appropriate for the " + alg.name() + " algorithm."); + Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, Claims) implementation cannot " + + "be used for asymmetric key algorithms (RSA, Elliptic Curve). " + + "Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " + + "Key instance appropriate for the " + alg.name() + " algorithm."); byte[] keyBytes = resolveSigningKeyBytes(header, claims); return new SecretKeySpec(keyBytes, alg.getJcaName()); } @@ -69,10 +76,10 @@ public Key resolveSigningKey(JwsHeader header, Claims claims) { @Override public Key resolveSigningKey(JwsHeader header, String plaintext) { SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); - Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, String) implementation cannot be " + - "used for asymmetric key algorithms (RSA, Elliptic Curve). " + - "Override the resolveSigningKey(JwsHeader, String) method instead and return a " + - "Key instance appropriate for the " + alg.name() + " algorithm."); + Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, String) implementation cannot " + + "be used for asymmetric key algorithms (RSA, Elliptic Curve). " + + "Override the resolveSigningKey(JwsHeader, String) method instead and return a " + + "Key instance appropriate for the " + alg.name() + " algorithm."); byte[] keyBytes = resolveSigningKeyBytes(header, plaintext); return new SecretKeySpec(keyBytes, alg.getJcaName()); } @@ -91,9 +98,9 @@ public Key resolveSigningKey(JwsHeader header, String plaintext) { */ public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + - "Claims JWS signing key resolution. Consider overriding either the " + - "resolveSigningKey(JwsHeader, Claims) method or, for HMAC algorithms, the " + - "resolveSigningKeyBytes(JwsHeader, Claims) method."); + "Claims JWS signing key resolution. Consider overriding either the " + + "resolveSigningKey(JwsHeader, Claims) method or, for HMAC algorithms, the " + + "resolveSigningKeyBytes(JwsHeader, Claims) method."); } /** @@ -107,8 +114,8 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { */ public byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + - "plaintext JWS signing key resolution. Consider overriding either the " + - "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + - "resolveSigningKeyBytes(JwsHeader, String) method."); + "plaintext JWS signing key resolution. Consider overriding either the " + + "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + + "resolveSigningKeyBytes(JwsHeader, String) method."); } } diff --git a/api/src/main/java/io/jsonwebtoken/UnsupportedJwtException.java b/api/src/main/java/io/jsonwebtoken/UnsupportedJwtException.java index 3735f7d42..4dbe43cc7 100644 --- a/api/src/main/java/io/jsonwebtoken/UnsupportedJwtException.java +++ b/api/src/main/java/io/jsonwebtoken/UnsupportedJwtException.java @@ -26,10 +26,21 @@ */ public class UnsupportedJwtException extends JwtException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public UnsupportedJwtException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public UnsupportedJwtException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/lang/Assert.java b/api/src/main/java/io/jsonwebtoken/lang/Assert.java index ba0521c5b..0375b91f6 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Assert.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Assert.java @@ -200,6 +200,7 @@ public static void doesNotContain(String textToSearch, String substring) { * * @param array the array to check * @param message the exception message to use if the assertion fails + * @return the non-empty array for immediate use * @throws IllegalArgumentException if the object array is null or has no elements */ public static Object[] notEmpty(Object[] array, String message) { diff --git a/api/src/main/java/io/jsonwebtoken/lang/Objects.java b/api/src/main/java/io/jsonwebtoken/lang/Objects.java index 4680a4b28..1f93fd275 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Objects.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Objects.java @@ -20,6 +20,10 @@ import java.lang.reflect.Array; import java.util.Arrays; +/** + * Utility methods for working with object instances to reduce pattern repetition and otherwise + * increased cyclomatic complexity. + */ public final class Objects { private Objects() { @@ -156,8 +160,8 @@ public static boolean containsConstant(Enum[] enumValues, String constant) { public static boolean containsConstant(Enum[] enumValues, String constant, boolean caseSensitive) { for (Enum candidate : enumValues) { if (caseSensitive ? - candidate.toString().equals(constant) : - candidate.toString().equalsIgnoreCase(constant)) { + candidate.toString().equals(constant) : + candidate.toString().equalsIgnoreCase(constant)) { return true; } } @@ -182,8 +186,8 @@ public static > E caseInsensitiveValueOf(E[] enumValues, Strin } } throw new IllegalArgumentException( - String.format("constant [%s] does not exist in enum type %s", - constant, enumValues.getClass().getComponentType().getName())); + String.format("constant [%s] does not exist in enum type %s", + constant, enumValues.getClass().getComponentType().getName())); } /** @@ -191,9 +195,9 @@ public static > E caseInsensitiveValueOf(E[] enumValues, Strin * consisting of the input array contents plus the given object. * * @param array the array to append to (can be null) - * @param the type of each element in the specified {@code array} + * @param the type of each element in the specified {@code array} * @param obj the object to append - * @param the type of the specified object, which must equal to or extend the {@code <A>} type. + * @param the type of the specified object, which must equal to or extend the {@code <A>} type. * @return the new array (of the same component type; never null) */ public static A[] addObjectToArray(A[] array, O obj) { @@ -311,6 +315,8 @@ public static boolean nullSafeEquals(Object o1, Object o2) { * methods for arrays in this class. If the object is null, * this method returns 0. * + * @param obj the object to use for obtaining a hashcode + * @return the object's hashcode, which could be 0 if the object is null. * @see #nullSafeHashCode(Object[]) * @see #nullSafeHashCode(boolean[]) * @see #nullSafeHashCode(byte[]) @@ -320,8 +326,6 @@ public static boolean nullSafeEquals(Object o1, Object o2) { * @see #nullSafeHashCode(int[]) * @see #nullSafeHashCode(long[]) * @see #nullSafeHashCode(short[]) - * @param obj the object to use for obtaining a hashcode - * @return the object's hashcode, which could be 0 if the object is null. */ public static int nullSafeHashCode(Object obj) { if (obj == null) { @@ -362,6 +366,7 @@ public static int nullSafeHashCode(Object obj) { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the array to obtain a hashcode * @return the array's hashcode, which could be 0 if the array is null. */ @@ -380,6 +385,7 @@ public static int nullSafeHashCode(Object... array) { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the boolean array to obtain a hashcode * @return the boolean array's hashcode, which could be 0 if the array is null. */ @@ -398,6 +404,7 @@ public static int nullSafeHashCode(boolean[] array) { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the byte array to obtain a hashcode * @return the byte array's hashcode, which could be 0 if the array is null. */ @@ -416,6 +423,7 @@ public static int nullSafeHashCode(byte[] array) { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the char array to obtain a hashcode * @return the char array's hashcode, which could be 0 if the array is null. */ @@ -434,6 +442,7 @@ public static int nullSafeHashCode(char[] array) { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the double array to obtain a hashcode * @return the double array's hashcode, which could be 0 if the array is null. */ @@ -452,6 +461,7 @@ public static int nullSafeHashCode(double[] array) { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the float array to obtain a hashcode * @return the float array's hashcode, which could be 0 if the array is null. */ @@ -470,6 +480,7 @@ public static int nullSafeHashCode(float[] array) { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the int array to obtain a hashcode * @return the int array's hashcode, which could be 0 if the array is null. */ @@ -488,6 +499,7 @@ public static int nullSafeHashCode(int[] array) { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the long array to obtain a hashcode * @return the long array's hashcode, which could be 0 if the array is null. */ @@ -506,6 +518,7 @@ public static int nullSafeHashCode(long[] array) { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the short array to obtain a hashcode * @return the short array's hashcode, which could be 0 if the array is null. */ @@ -950,6 +963,12 @@ public static String nullSafeToString(short[] array) { return sb.toString(); } + /** + * Iterate over the specified {@link Closeable} instances, invoking + * {@link Closeable#close()} on each one, ignoring any potential {@link IOException}s. + * + * @param closeables the closeables to close. + */ public static void nullSafeClose(Closeable... closeables) { if (closeables == null) { return; diff --git a/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java b/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java index e98dc509a..1bb301628 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java +++ b/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java @@ -34,8 +34,21 @@ private RuntimeEnvironment() { private static final AtomicBoolean bcLoaded = new AtomicBoolean(false); + /** + * {@code true} if BouncyCastle is in the runtime classpath, {@code false} otherwise. + * + * @deprecated since JJWT_RELEASE_VERSION. will be removed before the 1.0 final release. + */ + @Deprecated public static final boolean BOUNCY_CASTLE_AVAILABLE = Classes.isAvailable(BC_PROVIDER_CLASS_NAME); + /** + * Register BouncyCastle as a JCA provider in the system's {@link Security#getProviders() Security Providers} list + * if BouncyCastle is in the runtime classpath. + * + * @deprecated since JJWT_RELEASE_VERSION. will be removed before the 1.0 final release. + */ + @Deprecated public static void enableBouncyCastleIfPossible() { if (!BOUNCY_CASTLE_AVAILABLE || bcLoaded.get()) { diff --git a/api/src/main/java/io/jsonwebtoken/lang/Strings.java b/api/src/main/java/io/jsonwebtoken/lang/Strings.java index d46d55736..0dd31c227 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Strings.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Strings.java @@ -30,8 +30,15 @@ import java.util.StringTokenizer; import java.util.TreeSet; +/** + * Utility methods for working with Strings to reduce pattern repetition and otherwise + * increased cyclomatic complexity. + */ public final class Strings { + /** + * Empty String, equal to "". + */ public static final String EMPTY = ""; private static final String FOLDER_SEPARATOR = "/"; @@ -44,9 +51,13 @@ public final class Strings { private static final char EXTENSION_SEPARATOR = '.'; + /** + * Convenience alias for {@link StandardCharsets#UTF_8}. + */ public static final Charset UTF_8 = StandardCharsets.UTF_8; - private Strings(){} //prevent instantiation + private Strings() { + } //prevent instantiation //--------------------------------------------------------------------- // General convenience methods for working with Strings @@ -61,6 +72,7 @@ private Strings(){} //prevent instantiation * Strings.hasLength(" ") = true * Strings.hasLength("Hello") = true * + * * @param str the CharSequence to check (may be null) * @return true if the CharSequence is not null and has length * @see #hasText(String) @@ -72,6 +84,7 @@ public static boolean hasLength(CharSequence str) { /** * Check that the given String is neither null nor of length 0. * Note: Will return true for a String that purely consists of whitespace. + * * @param str the String to check (may be null) * @return true if the String is not null and has length * @see #hasLength(CharSequence) @@ -91,6 +104,7 @@ public static boolean hasLength(String str) { * Strings.hasText("12345") = true * Strings.hasText(" 12345 ") = true * + * * @param str the CharSequence to check (may be null) * @return true if the CharSequence is not null, * its length is greater than 0, and it does not contain whitespace only @@ -113,6 +127,7 @@ public static boolean hasText(CharSequence str) { * Check whether the given String has actual text. * More specifically, returns true if the string not null, * its length is greater than 0, and it contains at least one non-whitespace character. + * * @param str the String to check (may be null) * @return true if the String is not null, its length is * greater than 0, and it does not contain whitespace only @@ -124,6 +139,7 @@ public static boolean hasText(String str) { /** * Check whether the given CharSequence contains any whitespace characters. + * * @param str the CharSequence to check (may be null) * @return true if the CharSequence is not empty and * contains at least 1 whitespace character @@ -144,6 +160,7 @@ public static boolean containsWhitespace(CharSequence str) { /** * Check whether the given String contains any whitespace characters. + * * @param str the String to check (may be null) * @return true if the String is not empty and * contains at least 1 whitespace character @@ -155,15 +172,16 @@ public static boolean containsWhitespace(String str) { /** * Trim leading and trailing whitespace from the given String. + * * @param str the String to check * @return the trimmed String * @see java.lang.Character#isWhitespace */ public static String trimWhitespace(String str) { - return (String) trimWhitespace((CharSequence)str); + return (String) trimWhitespace((CharSequence) str); } - - + + private static CharSequence trimWhitespace(CharSequence str) { if (!hasLength(str)) { return str; @@ -171,24 +189,40 @@ private static CharSequence trimWhitespace(CharSequence str) { final int length = str.length(); int start = 0; - while (start < length && Character.isWhitespace(str.charAt(start))) { + while (start < length && Character.isWhitespace(str.charAt(start))) { start++; } - - int end = length; + + int end = length; while (start < length && Character.isWhitespace(str.charAt(end - 1))) { end--; } - + return ((start > 0) || (end < length)) ? str.subSequence(start, end) : str; } + /** + * Returns the specified string without leading or trailing whitespace, or {@code null} if there are no remaining + * characters. + * + * @param str the string to clean + * @return the specified string without leading or trailing whitespace, or {@code null} if there are no remaining + * characters. + */ public static String clean(String str) { - CharSequence result = clean((CharSequence) str); - - return result!=null?result.toString():null; + CharSequence result = clean((CharSequence) str); + + return result != null ? result.toString() : null; } - + + /** + * Returns the specified {@code CharSequence} without leading or trailing whitespace, or {@code null} if there are + * no remaining characters. + * + * @param str the {@code CharSequence} to clean + * @return the specified string without leading or trailing whitespace, or {@code null} if there are no remaining + * characters. + */ public static CharSequence clean(CharSequence str) { str = trimWhitespace(str); if (!hasLength(str)) { @@ -197,16 +231,28 @@ public static CharSequence clean(CharSequence str) { return str; } + /** + * Returns a String representation (1s and 0s) of the specified byte. + * + * @param b the byte to represent as 1s and 0s. + * @return a String representation (1s and 0s) of the specified byte. + */ public static String toBinary(byte b) { String bString = Integer.toBinaryString(b & 0xFF); - return String.format("%8s", bString).replace((char)Character.SPACE_SEPARATOR, '0'); + return String.format("%8s", bString).replace((char) Character.SPACE_SEPARATOR, '0'); } + /** + * Returns a String representation (1s and 0s) of the specified byte array. + * + * @param bytes the bytes to represent as 1s and 0s. + * @return a String representation (1s and 0s) of the specified byte array. + */ public static String toBinary(byte[] bytes) { StringBuilder sb = new StringBuilder(19); //16 characters + 3 space characters - for(byte b : bytes) { + for (byte b : bytes) { if (sb.length() > 0) { - sb.append((char)Character.SPACE_SEPARATOR); + sb.append((char) Character.SPACE_SEPARATOR); } String val = toBinary(b); sb.append(val); @@ -214,11 +260,17 @@ public static String toBinary(byte[] bytes) { return sb.toString(); } + /** + * Returns a hexadecimal String representation of the specified byte array. + * + * @param bytes the bytes to represent as a hexidecimal string. + * @return a hexadecimal String representation of the specified byte array. + */ public static String toHex(byte[] bytes) { StringBuilder result = new StringBuilder(); for (byte temp : bytes) { if (result.length() > 0) { - result.append((char)Character.SPACE_SEPARATOR); + result.append((char) Character.SPACE_SEPARATOR); } result.append(String.format("%02x", temp)); } @@ -228,6 +280,7 @@ public static String toHex(byte[] bytes) { /** * Trim all whitespace from the given String: * leading, trailing, and intermediate characters. + * * @param str the String to check * @return the trimmed String * @see java.lang.Character#isWhitespace @@ -241,8 +294,7 @@ public static String trimAllWhitespace(String str) { while (sb.length() > index) { if (Character.isWhitespace(sb.charAt(index))) { sb.deleteCharAt(index); - } - else { + } else { index++; } } @@ -251,6 +303,7 @@ public static String trimAllWhitespace(String str) { /** * Trim leading whitespace from the given String. + * * @param str the String to check * @return the trimmed String * @see java.lang.Character#isWhitespace @@ -268,6 +321,7 @@ public static String trimLeadingWhitespace(String str) { /** * Trim trailing whitespace from the given String. + * * @param str the String to check * @return the trimmed String * @see java.lang.Character#isWhitespace @@ -285,7 +339,8 @@ public static String trimTrailingWhitespace(String str) { /** * Trim all occurrences of the supplied leading character from the given String. - * @param str the String to check + * + * @param str the String to check * @param leadingCharacter the leading character to be trimmed * @return the trimmed String */ @@ -302,7 +357,8 @@ public static String trimLeadingCharacter(String str, char leadingCharacter) { /** * Trim all occurrences of the supplied trailing character from the given String. - * @param str the String to check + * + * @param str the String to check * @param trailingCharacter the trailing character to be trimmed * @return the trimmed String */ @@ -320,7 +376,8 @@ public static String trimTrailingCharacter(String str, char trailingCharacter) { /** * Returns {@code true} if the given string starts with the specified case-insensitive prefix, {@code false} otherwise. - * @param str the String to check + * + * @param str the String to check * @param prefix the prefix to look for * @return {@code true} if the given string starts with the specified case-insensitive prefix, {@code false} otherwise. * @see java.lang.String#startsWith @@ -342,7 +399,8 @@ public static boolean startsWithIgnoreCase(String str, String prefix) { /** * Returns {@code true} if the given string ends with the specified case-insensitive suffix, {@code false} otherwise. - * @param str the String to check + * + * @param str the String to check * @param suffix the suffix to look for * @return {@code true} if the given string ends with the specified case-insensitive suffix, {@code false} otherwise. * @see java.lang.String#endsWith @@ -365,8 +423,9 @@ public static boolean endsWithIgnoreCase(String str, String suffix) { /** * Returns {@code true} if the given string matches the given substring at the given index, {@code false} otherwise. - * @param str the original string (or StringBuilder) - * @param index the index in the original string to start matching against + * + * @param str the original string (or StringBuilder) + * @param index the index in the original string to start matching against * @param substring the substring to match at the given index * @return {@code true} if the given string matches the given substring at the given index, {@code false} otherwise. */ @@ -382,6 +441,7 @@ public static boolean substringMatch(CharSequence str, int index, CharSequence s /** * Returns the number of occurrences the substring {@code sub} appears in string {@code str}. + * * @param str string to search in. Return 0 if this is null. * @param sub string to search for. Return 0 if this is null. * @return the number of occurrences the substring {@code sub} appears in string {@code str}. @@ -403,7 +463,8 @@ public static int countOccurrencesOf(String str, String sub) { /** * Replace all occurrences of a substring within a string with * another string. - * @param inString String to examine + * + * @param inString String to examine * @param oldPattern String to replace * @param newPattern String to insert * @return a String with the replacements @@ -430,8 +491,9 @@ public static String replace(String inString, String oldPattern, String newPatte /** * Delete all occurrences of the given substring. + * * @param inString the original String - * @param pattern the pattern to delete all occurrences of + * @param pattern the pattern to delete all occurrences of * @return the resulting String */ public static String delete(String inString, String pattern) { @@ -440,9 +502,10 @@ public static String delete(String inString, String pattern) { /** * Delete any character in a given String. - * @param inString the original String + * + * @param inString the original String * @param charsToDelete a set of characters to delete. - * E.g. "az\n" will delete 'a's, 'z's and new lines. + * E.g. "az\n" will delete 'a's, 'z's and new lines. * @return the resulting String */ public static String deleteAny(String inString, String charsToDelete) { @@ -466,6 +529,7 @@ public static String deleteAny(String inString, String charsToDelete) { /** * Quote the given String with single quotes. + * * @param str the input String (e.g. "myString") * @return the quoted String (e.g. "'myString'"), * or null if the input was null @@ -477,6 +541,7 @@ public static String quote(String str) { /** * Turn the given Object into a String with single quotes * if it is a String; keeping the Object as-is else. + * * @param obj the input Object (e.g. "myString") * @return the quoted String (e.g. "'myString'"), * or the input object as-is if not a String @@ -488,6 +553,7 @@ public static Object quoteIfString(Object obj) { /** * Unqualify a string qualified by a '.' dot character. For example, * "this.name.is.qualified", returns "qualified". + * * @param qualifiedName the qualified name * @return an unqualified string by stripping all previous text before (and including) the last period character. */ @@ -498,8 +564,9 @@ public static String unqualify(String qualifiedName) { /** * Unqualify a string qualified by a separator character. For example, * "this:name:is:qualified" returns "qualified" if using a ':' separator. + * * @param qualifiedName the qualified name - * @param separator the separator + * @param separator the separator * @return an unqualified string by stripping all previous text before and including the last {@code separator} character. */ public static String unqualify(String qualifiedName, char separator) { @@ -510,6 +577,7 @@ public static String unqualify(String qualifiedName, char separator) { * Capitalize a String, changing the first letter to * upper case as per {@link Character#toUpperCase(char)}. * No other letters are changed. + * * @param str the String to capitalize, may be null * @return the capitalized String, null if null */ @@ -521,6 +589,7 @@ public static String capitalize(String str) { * Uncapitalize a String, changing the first letter to * lower case as per {@link Character#toLowerCase(char)}. * No other letters are changed. + * * @param str the String to uncapitalize, may be null * @return the uncapitalized String, null if null */ @@ -535,8 +604,7 @@ private static String changeFirstCharacterCase(String str, boolean capitalize) { StringBuilder sb = new StringBuilder(str.length()); if (capitalize) { sb.append(Character.toUpperCase(str.charAt(0))); - } - else { + } else { sb.append(Character.toLowerCase(str.charAt(0))); } sb.append(str.substring(1)); @@ -546,6 +614,7 @@ private static String changeFirstCharacterCase(String str, boolean capitalize) { /** * Extract the filename from the given path, * e.g. "mypath/myfile.txt" -> "myfile.txt". + * * @param path the file path (may be null) * @return the extracted filename, or null if none */ @@ -560,6 +629,7 @@ public static String getFilename(String path) { /** * Extract the filename extension from the given path, * e.g. "mypath/myfile.txt" -> "txt". + * * @param path the file path (may be null) * @return the extracted filename extension, or null if none */ @@ -581,6 +651,7 @@ public static String getFilenameExtension(String path) { /** * Strip the filename extension from the given path, * e.g. "mypath/myfile.txt" -> "mypath/myfile". + * * @param path the file path (may be null) * @return the path with stripped filename extension, * or null if none @@ -603,9 +674,10 @@ public static String stripFilenameExtension(String path) { /** * Apply the given relative path to the given path, * assuming standard Java folder separation (i.e. "/" separators). - * @param path the path to start from (usually a full file path) + * + * @param path the path to start from (usually a full file path) * @param relativePath the relative path to apply - * (relative to the full file path above) + * (relative to the full file path above) * @return the full file path that results from applying the relative path */ public static String applyRelativePath(String path, String relativePath) { @@ -616,8 +688,7 @@ public static String applyRelativePath(String path, String relativePath) { newPath += FOLDER_SEPARATOR; } return newPath + relativePath; - } - else { + } else { return relativePath; } } @@ -627,6 +698,7 @@ public static String applyRelativePath(String path, String relativePath) { * inner simple dots. *

    The result is convenient for path comparison. For other uses, * notice that Windows separators ("\") are replaced by simple slashes. + * * @param path the original path * @return the normalized path */ @@ -659,17 +731,14 @@ public static String cleanPath(String path) { String element = pathArray[i]; if (CURRENT_PATH.equals(element)) { // Points to current directory - drop it. - } - else if (TOP_PATH.equals(element)) { + } else if (TOP_PATH.equals(element)) { // Registering top path found. tops++; - } - else { + } else { if (tops > 0) { // Merging path element with element corresponding to top path. tops--; - } - else { + } else { // Normal path element found. pathElements.add(0, element); } @@ -686,6 +755,7 @@ else if (TOP_PATH.equals(element)) { /** * Compare two paths after normalization of them. + * * @param path1 first path for comparison * @param path2 second path for comparison * @return whether the two paths are equivalent after normalization @@ -697,9 +767,10 @@ public static boolean pathEquals(String path1, String path2) { /** * Parse the given localeString value into a {@link java.util.Locale}. *

    This is the inverse operation of {@link java.util.Locale#toString Locale's toString}. + * * @param localeString the locale string, following Locale's - * toString() format ("en", "en_UK", etc); - * also accepts spaces as separators, as an alternative to underscores + * toString() format ("en", "en_UK", etc); + * also accepts spaces as separators, as an alternative to underscores * @return a corresponding Locale instance */ public static Locale parseLocaleString(String localeString) { @@ -727,7 +798,7 @@ private static void validateLocalePart(String localePart) { char ch = localePart.charAt(i); if (ch != '_' && ch != ' ' && !Character.isLetterOrDigit(ch)) { throw new IllegalArgumentException( - "Locale part \"" + localePart + "\" contains invalid characters"); + "Locale part \"" + localePart + "\" contains invalid characters"); } } } @@ -735,6 +806,7 @@ private static void validateLocalePart(String localePart) { /** * Determine the RFC 3066 compliant language tag, * as used for the HTTP "Accept-Language" header. + * * @param locale the Locale to transform to a language tag * @return the RFC 3066 compliant language tag as String */ @@ -750,13 +822,14 @@ public static String toLanguageTag(Locale locale) { /** * Append the given String to the given String array, returning a new array * consisting of the input array contents plus the given String. + * * @param array the array to append to (can be null) - * @param str the String to append + * @param str the String to append * @return the new array (never null) */ public static String[] addStringToArray(String[] array, String str) { if (Objects.isEmpty(array)) { - return new String[] {str}; + return new String[]{str}; } String[] newArr = new String[array.length + 1]; System.arraycopy(array, 0, newArr, 0, array.length); @@ -768,6 +841,7 @@ public static String[] addStringToArray(String[] array, String str) { * Concatenate the given String arrays into one, * with overlapping array elements included twice. *

    The order of elements in the original arrays is preserved. + * * @param array1 the first array (can be null) * @param array2 the second array (can be null) * @return the new array (null if both given arrays were null) @@ -791,6 +865,7 @@ public static String[] concatenateStringArrays(String[] array1, String[] array2) *

    The order of elements in the original arrays is preserved * (with the exception of overlapping elements, which are only * included on their first occurrence). + * * @param array1 the first array (can be null) * @param array2 the second array (can be null) * @return the new array (null if both given arrays were null) @@ -814,6 +889,7 @@ public static String[] mergeStringArrays(String[] array1, String[] array2) { /** * Turn given source String array into sorted array. + * * @param array the source array * @return the sorted array (never null) */ @@ -828,6 +904,7 @@ public static String[] sortStringArray(String[] array) { /** * Copy the given Collection into a String array. * The Collection must contain String elements only. + * * @param collection the Collection to copy * @return the String array (null if the passed-in * Collection was null) @@ -842,6 +919,7 @@ public static String[] toStringArray(Collection collection) { /** * Copy the given Enumeration into a String array. * The Enumeration must contain String elements only. + * * @param enumeration the Enumeration to copy * @return the String array (null if the passed-in * Enumeration was null) @@ -857,6 +935,7 @@ public static String[] toStringArray(Enumeration enumeration) { /** * Trim the elements of the given String array, * calling String.trim() on each of them. + * * @param array the original String array * @return the resulting array (of the same size) with trimmed elements */ @@ -875,6 +954,7 @@ public static String[] trimArrayElements(String[] array) { /** * Remove duplicate Strings from the given array. * Also sorts the array, as it uses a TreeSet. + * * @param array the String array * @return an array without duplicates, in natural sort order */ @@ -892,7 +972,8 @@ public static String[] removeDuplicateStrings(String[] array) { /** * Split a String at the first occurrence of the delimiter. * Does not include the delimiter in the result. - * @param toSplit the string to split + * + * @param toSplit the string to split * @param delimiter to split the string up with * @return a two element array with index 0 being before the delimiter, and * index 1 being after the delimiter (neither element includes the delimiter); @@ -908,7 +989,7 @@ public static String[] split(String toSplit, String delimiter) { } String beforeDelimiter = toSplit.substring(0, offset); String afterDelimiter = toSplit.substring(offset + delimiter.length()); - return new String[] {beforeDelimiter, afterDelimiter}; + return new String[]{beforeDelimiter, afterDelimiter}; } /** @@ -917,7 +998,8 @@ public static String[] split(String toSplit, String delimiter) { * delimiter providing the key, and the right of the delimiter providing the value. *

    Will trim both the key and value before adding them to the * Properties instance. - * @param array the array to process + * + * @param array the array to process * @param delimiter to split each element using (typically the equals symbol) * @return a Properties instance representing the array contents, * or null if the array to process was null or empty @@ -932,16 +1014,17 @@ public static Properties splitArrayElementsIntoProperties(String[] array, String * delimiter providing the key, and the right of the delimiter providing the value. *

    Will trim both the key and value before adding them to the * Properties instance. - * @param array the array to process - * @param delimiter to split each element using (typically the equals symbol) + * + * @param array the array to process + * @param delimiter to split each element using (typically the equals symbol) * @param charsToDelete one or more characters to remove from each element - * prior to attempting the split operation (typically the quotation mark - * symbol), or null if no removal should occur + * prior to attempting the split operation (typically the quotation mark + * symbol), or null if no removal should occur * @return a Properties instance representing the array contents, * or null if the array to process was null or empty */ public static Properties splitArrayElementsIntoProperties( - String[] array, String delimiter, String charsToDelete) { + String[] array, String delimiter, String charsToDelete) { if (Objects.isEmpty(array)) { return null; @@ -967,9 +1050,10 @@ public static Properties splitArrayElementsIntoProperties( * delimiter characters. Each of those characters can be used to separate * tokens. A delimiter is always a single character; for multi-character * delimiters, consider using delimitedListToStringArray - * @param str the String to tokenize + * + * @param str the String to tokenize * @param delimiters the delimiter characters, assembled as String - * (each of those characters is individually considered as delimiter). + * (each of those characters is individually considered as delimiter). * @return an array of the tokens * @see java.util.StringTokenizer * @see java.lang.String#trim() @@ -985,13 +1069,14 @@ public static String[] tokenizeToStringArray(String str, String delimiters) { * delimiter characters. Each of those characters can be used to separate * tokens. A delimiter is always a single character; for multi-character * delimiters, consider using delimitedListToStringArray - * @param str the String to tokenize - * @param delimiters the delimiter characters, assembled as String - * (each of those characters is individually considered as delimiter) - * @param trimTokens trim the tokens via String's trim + * + * @param str the String to tokenize + * @param delimiters the delimiter characters, assembled as String + * (each of those characters is individually considered as delimiter) + * @param trimTokens trim the tokens via String's trim * @param ignoreEmptyTokens omit empty tokens from the result array - * (only applies to tokens that are empty after trimming; StringTokenizer - * will not consider subsequent delimiters as token in the first place). + * (only applies to tokens that are empty after trimming; StringTokenizer + * will not consider subsequent delimiters as token in the first place). * @return an array of the tokens (null if the input String * was null) * @see java.util.StringTokenizer @@ -999,7 +1084,7 @@ public static String[] tokenizeToStringArray(String str, String delimiters) { * @see #delimitedListToStringArray */ public static String[] tokenizeToStringArray( - String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { + String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { if (str == null) { return null; @@ -1023,9 +1108,10 @@ public static String[] tokenizeToStringArray( *

    A single delimiter can consists of more than one character: It will still * be considered as single delimiter string, rather than as bunch of potential * delimiter characters - in contrast to tokenizeToStringArray. - * @param str the input String + * + * @param str the input String * @param delimiter the delimiter between elements (this is a single delimiter, - * rather than a bunch individual delimiter characters) + * rather than a bunch individual delimiter characters) * @return an array of the tokens in the list * @see #tokenizeToStringArray */ @@ -1038,11 +1124,12 @@ public static String[] delimitedListToStringArray(String str, String delimiter) *

    A single delimiter can consists of more than one character: It will still * be considered as single delimiter string, rather than as bunch of potential * delimiter characters - in contrast to tokenizeToStringArray. - * @param str the input String - * @param delimiter the delimiter between elements (this is a single delimiter, - * rather than a bunch individual delimiter characters) + * + * @param str the input String + * @param delimiter the delimiter between elements (this is a single delimiter, + * rather than a bunch individual delimiter characters) * @param charsToDelete a set of characters to delete. Useful for deleting unwanted - * line breaks: e.g. "\r\n\f" will delete all new lines and line feeds in a String. + * line breaks: e.g. "\r\n\f" will delete all new lines and line feeds in a String. * @return an array of the tokens in the list * @see #tokenizeToStringArray */ @@ -1051,15 +1138,14 @@ public static String[] delimitedListToStringArray(String str, String delimiter, return new String[0]; } if (delimiter == null) { - return new String[] {str}; + return new String[]{str}; } List result = new ArrayList(); if ("".equals(delimiter)) { for (int i = 0; i < str.length(); i++) { result.add(deleteAny(str.substring(i, i + 1), charsToDelete)); } - } - else { + } else { int pos = 0; int delPos; while ((delPos = str.indexOf(delimiter, pos)) != -1) { @@ -1076,6 +1162,7 @@ public static String[] delimitedListToStringArray(String str, String delimiter, /** * Convert a CSV list into an array of Strings. + * * @param str the input String * @return an array of Strings, or the empty array in case of empty input */ @@ -1086,6 +1173,7 @@ public static String[] commaDelimitedListToStringArray(String str) { /** * Convenience method to convert a CSV string list to a set. * Note that this will suppress duplicates. + * * @param str the input String * @return a Set of String entries in the list */ @@ -1101,8 +1189,9 @@ public static Set commaDelimitedListToSet(String str) { /** * Convenience method to return a Collection as a delimited (e.g. CSV) * String. E.g. useful for toString() implementations. - * @param coll the Collection to display - * @param delim the delimiter to use (probably a ",") + * + * @param coll the Collection to display + * @param delim the delimiter to use (probably a ",") * @param prefix the String to start each element with * @param suffix the String to end each element with * @return the delimited String @@ -1125,7 +1214,8 @@ public static String collectionToDelimitedString(Collection coll, String deli /** * Convenience method to return a Collection as a delimited (e.g. CSV) * String. E.g. useful for toString() implementations. - * @param coll the Collection to display + * + * @param coll the Collection to display * @param delim the delimiter to use (probably a ",") * @return the delimited String */ @@ -1136,6 +1226,7 @@ public static String collectionToDelimitedString(Collection coll, String deli /** * Convenience method to return a Collection as a CSV String. * E.g. useful for toString() implementations. + * * @param coll the Collection to display * @return the delimited String */ @@ -1146,7 +1237,8 @@ public static String collectionToCommaDelimitedString(Collection coll) { /** * Convenience method to return a String array as a delimited (e.g. CSV) * String. E.g. useful for toString() implementations. - * @param arr the array to display + * + * @param arr the array to display * @param delim the delimiter to use (probably a ",") * @return the delimited String */ @@ -1170,6 +1262,7 @@ public static String arrayToDelimitedString(Object[] arr, String delim) { /** * Convenience method to return a String array as a CSV String. * E.g. useful for toString() implementations. + * * @param arr the array to display * @return the delimited String */ diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java index f7b476e61..410505765 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java @@ -23,6 +23,7 @@ /** * JWK representation of an asymmetric (public or private) cryptographic key. * + * @param the type of {@link java.security.PublicKey} or {@link java.security.PrivateKey} represented by this JWK. * @since JJWT_RELEASE_VERSION */ public interface AsymmetricJwk extends Jwk { diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java index 61e300670..a8af66cfc 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java @@ -134,7 +134,33 @@ public interface AsymmetricJwkBuilder, //T withX509KeyUse(boolean enable); + /** + * If the {@code enable} argument is {@code true}, compute the SHA-1 thumbprint of the first + * {@link X509Certificate} in the configured {@link #setX509CertificateChain(List) x509CertificateChain}, and set + * the resulting value as the JWK {@code x5t} + * (X.509 Certificate SHA-1 Thumbprint) parameter. + * + *

    If no chain has been configured, or {@code enable} is {@code false}, the builder will not compute nor add a + * {@code x5t} value.

    + * + * @param enable whether to compute the SHA-1 thumbprint on the first available X.509 Certificate and set + * the resulting value as the {@code x5t} value. + * @return the builder for method chaining. + */ T withX509Sha1Thumbprint(boolean enable); + /** + * If the {@code enable} argument is {@code true}, compute the SHA-256 thumbprint of the first + * {@link X509Certificate} in the configured {@link #setX509CertificateChain(List) x509CertificateChain}, and set + * the resulting value as the JWK {@code x5t#S256} + * (X.509 Certificate SHA-256 Thumbprint) parameter. + * + *

    If no chain has been configured, or {@code enable} is {@code false}, the builder will not compute nor add a + * {@code x5t#S256} value.

    + * + * @param enable whether to compute the SHA-1 thumbprint on the first available X.509 Certificate and set + * the resulting value as the {@code x5t} value. + * @return the builder for method chaining. + */ T withX509Sha256Thumbprint(boolean enable); } diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java index d41a2c219..e9266d0b3 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java @@ -35,6 +35,8 @@ *

    The resulting {@code pair} is guaranteed to have the correct algorithm parameters and length/strength necessary * for that exact {@code anAsymmetricKeySignatureAlgorithm} instance.

    * + * @param The type of {@link PrivateKey} used to create signatures + * @param the type of {@link PublicKey} used to verify signatures * @since JJWT_RELEASE_VERSION */ public interface AsymmetricKeySignatureAlgorithm diff --git a/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java index 0066c3a86..ebf638a62 100644 --- a/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java @@ -35,6 +35,7 @@ * {@code JWE Encrypted Key} (such as an initialization vector, authentication tag, ephemeral key, etc) is expected * to be available in the JWE protected header, accessible via {@link #getHeader()}.

    * + * @param the type of {@link Key} used during the request to obtain the resulting decryption key. * @since JJWT_RELEASE_VERSION */ public interface DecryptionKeyRequest extends KeyRequest, Message { diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java index 56ac36e99..e8a3b2eff 100644 --- a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithms.java @@ -25,6 +25,9 @@ * JWA (RFC 7518) Encryption Algorithms. * * @see AeadAlgorithm + * @see #values() + * @see #forId(String) + * @see #findById(String) * @since JJWT_RELEASE_VERSION */ public final class EncryptionAlgorithms { @@ -81,32 +84,33 @@ public static AeadAlgorithm forId(String id) throws IllegalArgumentException { } /** - * AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm, as defined by + * {@code AES_128_CBC_HMAC_SHA_256} authenticated encryption algorithm as defined by * RFC 7518, Section 5.2.3. This algorithm - * requires a 256 bit (32 byte) key. + * requires a 256-bit (32 byte) key. */ public static final AeadAlgorithm A128CBC_HS256 = forId("A128CBC-HS256"); /** - * AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm, as defined by + * {@code AES_192_CBC_HMAC_SHA_384} authenticated encryption algorithm, as defined by * RFC 7518, Section 5.2.4. This algorithm - * requires a 384 bit (48 byte) key. + * requires a 384-bit (48 byte) key. */ public static final AeadAlgorithm A192CBC_HS384 = forId("A192CBC-HS384"); /** - * AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm, as defined by + * {@code AES_256_CBC_HMAC_SHA_512} authenticated encryption algorithm, as defined by * RFC 7518, Section 5.2.5. This algorithm - * requires a 512 bit (64 byte) key. + * requires a 512-bit (64 byte) key. */ public static final AeadAlgorithm A256CBC_HS512 = forId("A256CBC-HS512"); /** * "AES GCM using 128-bit key" as defined by * RFC 7518, Section 5.31. This - * algorithm requires a 128 bit (16 byte) key. + * algorithm requires a 128-bit (16 byte) key. * *

    1 Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath. If on Java 7 or earlier, BouncyCastle will be used automatically if found in the runtime * classpath.

    */ public static final AeadAlgorithm A128GCM = forId("A128GCM"); @@ -114,9 +118,10 @@ public static AeadAlgorithm forId(String id) throws IllegalArgumentException { /** * "AES GCM using 192-bit key" as defined by * RFC 7518, Section 5.31. This - * algorithm requires a 192 bit (24 byte) key. + * algorithm requires a 192-bit (24 byte) key. * *

    1 Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath. If on Java 7 or earlier, BouncyCastle will be used automatically if found in the runtime * classpath.

    */ public static final AeadAlgorithm A192GCM = forId("A192GCM"); @@ -124,9 +129,10 @@ public static AeadAlgorithm forId(String id) throws IllegalArgumentException { /** * "AES GCM using 256-bit key" as defined by * RFC 7518, Section 5.31. This - * algorithm requires a 256 bit (32 byte) key. + * algorithm requires a 256-bit (32 byte) key. * *

    1 Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath. If on Java 7 or earlier, BouncyCastle will be used automatically if found in the runtime * classpath.

    */ public static final AeadAlgorithm A256GCM = forId("A256GCM"); diff --git a/api/src/main/java/io/jsonwebtoken/security/Jwk.java b/api/src/main/java/io/jsonwebtoken/security/Jwk.java index 991feb402..623dd325e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Jwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/Jwk.java @@ -80,6 +80,7 @@ * interact with the JWK's {@link #toKey() toKey()} instance directly instead of accessing * JWK internal serialization fields.

    * + * @param The type of Java {@link Key} represented by this JWK * @since JJWT_RELEASE_VERSION */ public interface Jwk extends Identifiable, Map { diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java index 1964adb46..c8da95916 100644 --- a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java @@ -27,6 +27,9 @@ * Builder subtypes support additional JWK properties specific to different types of cryptographic keys * (e.g. Secret, Asymmetric, RSA, Elliptic Curve, etc). * + * @param the type of Java {@link Key} represented by the constructed JWK. + * @param the type of {@link Jwk} created by the builder + * @param the type of the builder, for subtype method chaining * @see SecretJwkBuilder * @see RsaPublicJwkBuilder * @see RsaPrivateJwkBuilder diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index f41f938c9..11e7bc544 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -15,6 +15,7 @@ */ package io.jsonwebtoken.security; +import io.jsonwebtoken.JweHeader; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Classes; @@ -25,9 +26,9 @@ * Constant definitions and utility methods for all * JWA (RFC 7518) Key Management Algorithms. * - * @see #values() - * @see #findById(String) - * @see #forId(String) + * @see #values() + * @see #findById(String) + * @see #forId(String) * @since JJWT_RELEASE_VERSION */ @SuppressWarnings("rawtypes") @@ -42,24 +43,44 @@ private KeyAlgorithms() { private static final Class[] ID_ARG_TYPES = new Class[]{String.class}; //private static final Class[] ESTIMATE_ITERATIONS_ARG_TYPES = new Class[]{KeyAlgorithm.class, long.class}; + /** + * Returns all JWA-standard Key Management algorithms as an unmodifiable collection. + * + * @return all JWA-standard Key Management algorithms as an unmodifiable collection. + */ public static Collection> values() { return Classes.invokeStatic(BRIDGE_CLASS, "values", null, (Object[]) null); } /** - * Returns the JWE KeyAlgorithm with the specified + * Returns the JWE Key Management Algorithm with the specified * {@code alg} key algorithm identifier or - * {@code null} if an algorithm for the specified {@code id} cannot be found. + * {@code null} if an algorithm for the specified {@code id} cannot be found. If a JWA-standard + * instance must be resolved, consider using the {@link #forId(String)} method instead. * - * @param id a JWE standard {@code alg} key algorithm identifier + * @param id a JWA standard {@code alg} key algorithm identifier * @return the associated KeyAlgorithm instance or {@code null} otherwise. * @see RFC 7518, Section 4.1 + * @see #forId(String) */ public static KeyAlgorithm findById(String id) { Assert.hasText(id, "id cannot be null or empty."); return Classes.invokeStatic(BRIDGE_CLASS, "findById", ID_ARG_TYPES, id); } + /** + * Returns the JWE Key Management Algorithm with the specified + * {@code alg} key algorithm identifier or + * throws an {@link IllegalArgumentException} if there is no JWE-standard algorithm for the specified + * {@code id}. If a JWE-standard instance result is not mandatory, consider using the {@link #findById(String)} + * method instead. + * + * @param id a JWA standard {@code alg} key algorithm identifier + * @return the associated {@code KeyAlgorithm} instance. + * @throws IllegalArgumentException if there is no JWA-standard algorithm for the specified identifier. + * @see #findById(String) + * @see RFC 7518, Section 4.1 + */ public static KeyAlgorithm forId(String id) { return forId0(id); } @@ -270,15 +291,265 @@ private static T forId0(String id) { public static final KeyAlgorithm PBES2_HS512_A256KW = forId0("PBES2-HS512+A256KW"); /** - * Key Encryption with RSAES-PKCS1-v1_5, as defined by - * RFC 7518 (JWA), Section 4.7. + * Key Encryption with {@code RSAES-PKCS1-v1_5}, as defined by + * RFC 7518 (JWA), Section 4.2. + * This algorithm requires a 2048-bit key. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    2. + *
    3. Encrypts this newly-generated {@code SecretKey} with the RSA key wrap algorithm, using the JWE + * recipient's RSA Public Key, producing encrypted key ciphertext.
    4. + *
    5. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
    6. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Decrypts the encrypted key ciphertext with the RSA key unwrap algorithm, using the JWE recipient's + * RSA Private Key, producing the decryption key plaintext.
    4. + *
    5. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    6. + *
    */ public static final RsaKeyAlgorithm RSA1_5 = forId0("RSA1_5"); + + /** + * Key Encryption with {@code RSAES OAEP using default parameters}, as defined by + * RFC 7518 (JWA), Section 4.3. + * This algorithm requires a 2048-bit key. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    2. + *
    3. Encrypts this newly-generated {@code SecretKey} with the RSA OAEP with SHA-1 and MGF1 key wrap algorithm, + * using the JWE recipient's RSA Public Key, producing encrypted key ciphertext.
    4. + *
    5. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
    6. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Decrypts the encrypted key ciphertext with the RSA OAEP with SHA-1 and MGF1 key unwrap algorithm, + * using the JWE recipient's RSA Private Key, producing the decryption key plaintext.
    4. + *
    5. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    6. + *
    + */ public static final RsaKeyAlgorithm RSA_OAEP = forId0("RSA-OAEP"); + + /** + * Key Encryption with {@code RSAES OAEP using SHA-256 and MGF1 with SHA-256}, as defined by + * RFC 7518 (JWA), Section 4.3. + * This algorithm requires a 2048-bit key. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    2. + *
    3. Encrypts this newly-generated {@code SecretKey} with the RSA OAEP with SHA-256 and MGF1 key wrap + * algorithm, using the JWE recipient's RSA Public Key, producing encrypted key ciphertext.
    4. + *
    5. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
    6. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Decrypts the encrypted key ciphertext with the RSA OAEP with SHA-256 and MGF1 key unwrap algorithm, + * using the JWE recipient's RSA Private Key, producing the decryption key plaintext.
    4. + *
    5. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    6. + *
    + */ public static final RsaKeyAlgorithm RSA_OAEP_256 = forId0("RSA-OAEP-256"); + + /** + * Key Agreement with {@code ECDH-ES using Concat KDF} as defined by + * RFC J518 (JW), Section 4.6. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Generates a new secure-random Elliptic Curve public/private key pair on the same curve as the + * JWE recipient's EC Public Key.
    2. + *
    3. Generates a shared secret with the ECDH key agreement algorithm using the generated EC Private Key + * and the JWE recipient's EC Public Key.
    4. + *
    5. Derives a symmetric Content + * Encryption {@code SecretKey} with the Concat KDF algorithm using the + * generated shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
    6. + *
    7. Sets the generated EC key pair's Public Key as the required + * "epk" + * (Ephemeral Public Key) Header Parameter to be transmitted in the JWE.
    8. + *
    9. Returns the derived symmetric {@code SecretKey} for JJWT to use to encrypt the entire JWE with the + * associated {@link AeadAlgorithm}. Encrypted key ciphertext is not produced with this algorithm, so + * the resulting JWE will not contain any embedded key ciphertext.
    10. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Obtains the required ephemeral Elliptic Curve Public Key from the + * "epk" + * (Ephemeral Public Key) Header Parameter.
    2. + *
    3. Validates that the ephemeral Public Key is on the same curve as the recipient's EC Private Key.
    4. + *
    5. Obtains the shared secret with the ECDH key agreement algorithm using the obtained EC Public Key + * and the JWE recipient's EC Private Key.
    6. + *
    7. Derives the symmetric Content + * Encryption {@code SecretKey} with the Concat KDF algorithm using the + * obtained shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
    8. + *
    9. Returns the derived symmetric {@code SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    10. + *
    + */ public static final EcKeyAlgorithm ECDH_ES = forId0("ECDH-ES"); + + /** + * Key Agreement with Key Wrapping via + * ECDH-ES using Concat KDF and CEK wrapped with "A128KW" as defined by + * RFC J518 (JW), Section 4.6. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Generates a new secure-random Elliptic Curve public/private key pair on the same curve as the + * JWE recipient's EC Public Key.
    2. + *
    3. Generates a shared secret with the ECDH key agreement algorithm using the generated EC Private Key + * and the JWE recipient's EC Public Key.
    4. + *
    5. Derives a 128-bit symmetric Key + * Encryption {@code SecretKey} with the Concat KDF algorithm using the + * generated shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
    6. + *
    7. Sets the generated EC key pair's Public Key as the required + * "epk" + * (Ephemeral Public Key) Header Parameter to be transmitted in the JWE.
    8. + *
    9. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    10. + *
    11. Encrypts this newly-generated {@code SecretKey} with the {@code A128KW} key wrap + * algorithm using the derived symmetric Key Encryption Key from step {@code #3}, producing encrypted key ciphertext.
    12. + *
    13. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
    14. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Obtains the required ephemeral Elliptic Curve Public Key from the + * "epk" + * (Ephemeral Public Key) Header Parameter.
    2. + *
    3. Validates that the ephemeral Public Key is on the same curve as the recipient's EC Private Key.
    4. + *
    5. Obtains the shared secret with the ECDH key agreement algorithm using the obtained EC Public Key + * and the JWE recipient's EC Private Key.
    6. + *
    7. Derives the symmetric Key + * Encryption {@code SecretKey} with the Concat KDF algorithm using the + * obtained shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
    8. + *
    9. Obtains the encrypted key ciphertext embedded in the received JWE.
    10. + *
    11. Decrypts the encrypted key ciphertext with the AES Key Unwrap algorithm using the + * 128-bit derived symmetric key from step {@code #4}, producing the decryption key plaintext.
    12. + *
    13. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    14. + *
    + */ public static final EcKeyAlgorithm ECDH_ES_A128KW = forId0("ECDH-ES+A128KW"); + + /** + * Key Agreement with Key Wrapping via + * ECDH-ES using Concat KDF and CEK wrapped with "A192KW" as defined by + * RFC J518 (JW), Section 4.6. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Generates a new secure-random Elliptic Curve public/private key pair on the same curve as the + * JWE recipient's EC Public Key.
    2. + *
    3. Generates a shared secret with the ECDH key agreement algorithm using the generated EC Private Key + * and the JWE recipient's EC Public Key.
    4. + *
    5. Derives a 192-bit symmetric Key + * Encryption {@code SecretKey} with the Concat KDF algorithm using the + * generated shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
    6. + *
    7. Sets the generated EC key pair's Public Key as the required + * "epk" + * (Ephemeral Public Key) Header Parameter to be transmitted in the JWE.
    8. + *
    9. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    10. + *
    11. Encrypts this newly-generated {@code SecretKey} with the {@code A192KW} key wrap + * algorithm using the derived symmetric Key Encryption Key from step {@code #3}, producing encrypted key + * ciphertext.
    12. + *
    13. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
    14. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Obtains the required ephemeral Elliptic Curve Public Key from the + * "epk" + * (Ephemeral Public Key) Header Parameter.
    2. + *
    3. Validates that the ephemeral Public Key is on the same curve as the recipient's EC Private Key.
    4. + *
    5. Obtains the shared secret with the ECDH key agreement algorithm using the obtained EC Public Key + * and the JWE recipient's EC Private Key.
    6. + *
    7. Derives the 192-bit symmetric + * Key Encryption {@code SecretKey} with the Concat KDF algorithm using the + * obtained shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
    8. + *
    9. Obtains the encrypted key ciphertext embedded in the received JWE.
    10. + *
    11. Decrypts the encrypted key ciphertext with the AES Key Unwrap algorithm using the + * 192-bit derived symmetric key from step {@code #4}, producing the decryption key plaintext.
    12. + *
    13. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    14. + *
    + */ public static final EcKeyAlgorithm ECDH_ES_A192KW = forId0("ECDH-ES+A192KW"); + + /** + * Key Agreement with Key Wrapping via + * ECDH-ES using Concat KDF and CEK wrapped with "A256KW" as defined by + * RFC J518 (JW), Section 4.6. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Generates a new secure-random Elliptic Curve public/private key pair on the same curve as the + * JWE recipient's EC Public Key.
    2. + *
    3. Generates a shared secret with the ECDH key agreement algorithm using the generated EC Private Key + * and the JWE recipient's EC Public Key.
    4. + *
    5. Derives a 256-bit symmetric Key + * Encryption {@code SecretKey} with the Concat KDF algorithm using the + * generated shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
    6. + *
    7. Sets the generated EC key pair's Public Key as the required + * "epk" + * (Ephemeral Public Key) Header Parameter to be transmitted in the JWE.
    8. + *
    9. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    10. + *
    11. Encrypts this newly-generated {@code SecretKey} with the {@code A256KW} key wrap + * algorithm using the derived symmetric Key Encryption Key from step {@code #3}, producing encrypted key + * ciphertext.
    12. + *
    13. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
    14. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Obtains the required ephemeral Elliptic Curve Public Key from the + * "epk" + * (Ephemeral Public Key) Header Parameter.
    2. + *
    3. Validates that the ephemeral Public Key is on the same curve as the recipient's EC Private Key.
    4. + *
    5. Obtains the shared secret with the ECDH key agreement algorithm using the obtained EC Public Key + * and the JWE recipient's EC Private Key.
    6. + *
    7. Derives the 256-bit symmetric + * Key Encryption {@code SecretKey} with the Concat KDF algorithm using the + * obtained shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
    8. + *
    9. Obtains the encrypted key ciphertext embedded in the received JWE.
    10. + *
    11. Decrypts the encrypted key ciphertext with the AES Key Unwrap algorithm using the + * 256-bit derived symmetric key from step {@code #4}, producing the decryption key plaintext.
    12. + *
    13. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    14. + *
    + */ public static final EcKeyAlgorithm ECDH_ES_A256KW = forId0("ECDH-ES+A256KW"); /* diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyException.java b/api/src/main/java/io/jsonwebtoken/security/KeyException.java index 55a488029..924066fe4 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyException.java @@ -23,10 +23,21 @@ */ public class KeyException extends SecurityException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public KeyException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param msg the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public KeyException(String msg, Exception cause) { super(msg, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java b/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java index 58e08c7e5..910a391d4 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java @@ -38,6 +38,7 @@ * (such as an initialization vector, authentication tag, ephemeral key, etc) is expected to be available in * the JWE protected header, accessible via {@link #getHeader()}.

    * + * @param the type of key used to perform cryptographic operations during the request. * @since JJWT_RELEASE_VERSION */ public interface KeyRequest extends Request, KeySupplier { diff --git a/api/src/main/java/io/jsonwebtoken/security/KeySupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeySupplier.java index ede8bc8de..df582971a 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeySupplier.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeySupplier.java @@ -20,6 +20,7 @@ /** * Provides access to a cryptographic {@link Key} necessary for signing, wrapping, encryption or decryption algorithms. * + * @param the type of key provided by this supplier. * @since JJWT_RELEASE_VERSION */ public interface KeySupplier { diff --git a/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java b/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java index 87a556105..74ebdb2f4 100644 --- a/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java @@ -23,10 +23,21 @@ */ public class MalformedKeyException extends InvalidKeyException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public MalformedKeyException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param msg the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public MalformedKeyException(String msg, Exception cause) { super(msg, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java index 6e936519e..1c65142cd 100644 --- a/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java @@ -36,6 +36,9 @@ * as a {@link KeyPair} instance if desired. * * + * @param The type of {@link PrivateKey} represented by this JWK + * @param The type of {@link PublicKey} represented by the JWK's corresponding {@link #toPublicJwk() public JWK}. + * @param The type of {@link PublicJwk} reflected by the JWK's public properties. * @since JJWT_RELEASE_VERSION */ public interface PrivateJwk> extends AsymmetricJwk { diff --git a/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java index d87de8d7a..894b14227 100644 --- a/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java @@ -30,6 +30,9 @@ * A prototypical {@link JwkBuilder} that coerces to a more type-specific builder based on the {@link Key} that will * be represented as a JWK. * + * @param the type of Java {@link Key} represented by the created {@link Jwk}. + * @param the type of {@link Jwk} created by the builder + * @param the type of the builder, for subtype method chaining * @since JJWT_RELEASE_VERSION */ public interface ProtoJwkBuilder, T extends JwkBuilder> extends JwkBuilder { diff --git a/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java b/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java index 5e8e086da..f7aa6d3e1 100644 --- a/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java @@ -20,6 +20,7 @@ /** * JWK representation of a {@link PublicKey}. * + * @param The type of {@link PublicKey} represented by this JWK * @since JJWT_RELEASE_VERSION */ public interface PublicJwk extends AsymmetricJwk { diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java index f1d78e00e..d80143602 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java @@ -37,6 +37,8 @@ *

    {@code SignatureAlgorithm} extends {@link Identifiable}: the value returned from * {@link Identifiable#getId() getId()} will be used as the JWS "alg" protected header value.

    * + * @param the type of {@link Key} used to create digital signatures or message authentication codes + * @param the type of {@link Key} used to verify digital signatures or message authentication codes * @since JJWT_RELEASE_VERSION */ public interface SignatureAlgorithm extends Identifiable { diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java index 5811af7fe..014e0e087 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java @@ -18,11 +18,7 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Classes; -import javax.crypto.SecretKey; import java.security.Key; -import java.security.PrivateKey; -import java.security.interfaces.ECKey; -import java.security.interfaces.RSAKey; import java.util.Collection; /** @@ -42,16 +38,45 @@ private SignatureAlgorithms() { private static final Class BRIDGE_CLASS = Classes.forName(BRIDGE_CLASSNAME); private static final Class[] ID_ARG_TYPES = new Class[]{String.class}; + /** + * Returns all JWA-standard {@code SignatureAlgorithm}s as an unmodifiable collection. + * + * @return all JWA-standard {@code SignatureAlgorithm}s as an unmodifiable collection. + */ public static Collection> values() { return Classes.invokeStatic(BRIDGE_CLASS, "values", null, (Object[]) null); } + /** + * Returns the {@code SignatureAlgorithm} instance with the specified JWA-standard identifier, or + * {@code null} if no algorithm with that identifier exists. If a result is mandatory, consider using + * {@link #forId(String)} instead. + * + * @param id a JWA-standard identifier defined in + *
    JWA RFC 7518, Section 3.1 + * in the "alg" Param Value column. + * @return the {@code SignatureAlgorithm} instance with the specified JWA-standard identifier, or + * {@code null} if no algorithm with that identifier exists. + * @see #forId(String) + */ public static SignatureAlgorithm findById(String id) { Assert.hasText(id, "id cannot be null or empty."); return Classes.invokeStatic(BRIDGE_CLASS, "findById", ID_ARG_TYPES, id); } - public static SignatureAlgorithm forId(String id) { + /** + * Returns the {@code SignatureAlgorithm} instance with the specified JWA-standard identifier, or + * throws an {@link IllegalArgumentException} if there is no such JWA-standard signature algorithm identifier. + * If a result is not mandatory, consider using {@link #findById(String)} instead. + * + * @param id a JWA-standard identifier defined in + * JWA RFC 7518, Section 3.1 + * in the "alg" Param Value column. + * @return the {@code SignatureAlgorithm} instance with the specified JWA-standard identifier + * @throws IllegalArgumentException is {@code id} is not a JWA-standard signature algorithm identifier. + * @see #findById(String) + */ + public static SignatureAlgorithm forId(String id) throws IllegalArgumentException { return forId0(id); } @@ -60,133 +85,106 @@ static T forId0(String id) { return Classes.invokeStatic(BRIDGE_CLASS, "forId", ID_ARG_TYPES, id); } + /** + * The "none" signature algorithm as defined by + * RFC 7518, Section 3.6. This algorithm + * is used only when creating unsecured (not integrity protected) JWSs and is not usable in any other scenario. + */ public static final SignatureAlgorithm NONE = forId0("none"); + + /** + * {@code HMAC using SHA-256} message authentication algorithm as defined by + * RFC 7518, Section 3.2. This algorithm + * requires a 256-bit (32 byte) key. + */ public static final SecretKeySignatureAlgorithm HS256 = forId0("HS256"); + + /** + * {@code HMAC using SHA-384} message authentication algorithm as defined by + * RFC 7518, Section 3.2. This algorithm + * requires a 384-bit (48 byte) key. + */ public static final SecretKeySignatureAlgorithm HS384 = forId0("HS384"); + + /** + * {@code HMAC using SHA-512} message authentication algorithm as defined by + * RFC 7518, Section 3.2. This algorithm + * requires a 512-bit (64 byte) key. + */ public static final SecretKeySignatureAlgorithm HS512 = forId0("HS512"); + + /** + * {@code RSASSA-PKCS1-v1_5 using SHA-256} signature algorithm as defined by + * RFC 7518, Section 3.3. This algorithm + * requires a 2048-bit key. + */ public static final RsaSignatureAlgorithm RS256 = forId0("RS256"); + + /** + * {@code RSASSA-PKCS1-v1_5 using SHA-384} signature algorithm as defined by + * RFC 7518, Section 3.3. This algorithm + * requires a 2048-bit key, but the JJWT team recommends a 3072-bit key. + */ public static final RsaSignatureAlgorithm RS384 = forId0("RS384"); + + /** + * {@code RSASSA-PKCS1-v1_5 using SHA-512} signature algorithm as defined by + * RFC 7518, Section 3.3. This algorithm + * requires a 2048-bit key, but the JJWT team recommends a 4096-bit key. + */ public static final RsaSignatureAlgorithm RS512 = forId0("RS512"); + + /** + * {@code RSASSA-PSS using SHA-256 and MGF1 with SHA-256} signature algorithm as defined by + * RFC 7518, Section 3.51. + * This algorithm requires a 2048-bit key. + * + *

    1 Requires Java 11 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath. If on Java 10 or earlier, BouncyCastle will be used automatically if found in the runtime + * classpath.

    + */ public static final RsaSignatureAlgorithm PS256 = forId0("PS256"); + + /** + * {@code RSASSA-PSS using SHA-384 and MGF1 with SHA-384} signature algorithm as defined by + * RFC 7518, Section 3.51. + * This algorithm requires a 2048-bit key, but the JJWT team recommends a 3072-bit key. + * + *

    1 Requires Java 11 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath. If on Java 10 or earlier, BouncyCastle will be used automatically if found in the runtime + * classpath.

    + */ public static final RsaSignatureAlgorithm PS384 = forId0("PS384"); + + /** + * {@code RSASSA-PSS using SHA-512 and MGF1 with SHA-512} signature algorithm as defined by + * RFC 7518, Section 3.51. + * This algorithm requires a 2048-bit key, but the JJWT team recommends a 4096-bit key. + * + *

    1 Requires Java 11 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath. If on Java 10 or earlier, BouncyCastle will be used automatically if found in the runtime + * classpath.

    + */ public static final RsaSignatureAlgorithm PS512 = forId0("PS512"); + + /** + * {@code ECDSA using P-256 and SHA-256} signature algorithm as defined by + * RFC 7518, Section 3.4. This algorithm + * requires a 256-bit key. + */ public static final EllipticCurveSignatureAlgorithm ES256 = forId0("ES256"); + + /** + * {@code ECDSA using P-384 and SHA-384} signature algorithm as defined by + * RFC 7518, Section 3.4. This algorithm + * requires a 384-bit key. + */ public static final EllipticCurveSignatureAlgorithm ES384 = forId0("ES384"); - public static final EllipticCurveSignatureAlgorithm ES512 = forId0("ES512"); /** - * Returns the recommended signature algorithm to be used with the specified key according to the following - * heuristics: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
    Key Signature Algorithm
    If the Key is a:And:With a key size of:The returned SignatureAlgorithm will be:
    {@link SecretKey}{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA256")1256 <= size <= 383 2{@link SignatureAlgorithms#HS256 HS256}
    {@link SecretKey}{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA384")1384 <= size <= 511{@link SignatureAlgorithms#HS384 HS384}
    {@link SecretKey}{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA512")1512 <= size{@link SignatureAlgorithms#HS512 HS512}
    {@link ECKey}instanceof {@link PrivateKey}256 <= size <= 383 3{@link SignatureAlgorithms#ES256 ES256}
    {@link ECKey}instanceof {@link PrivateKey}384 <= size <= 520 4{@link SignatureAlgorithms#ES384 ES384}
    {@link ECKey}instanceof {@link PrivateKey}521 <= size 4{@link SignatureAlgorithms#ES512 ES512}
    {@link RSAKey}instanceof {@link PrivateKey}2048 <= size <= 3071 5,6{@link SignatureAlgorithms#RS256 RS256}
    {@link RSAKey}instanceof {@link PrivateKey}3072 <= size <= 4095 6{@link SignatureAlgorithms#RS384 RS384}
    {@link RSAKey}instanceof {@link PrivateKey}4096 <= size 5{@link SignatureAlgorithms#RS512 RS512}
    - *

    Notes:

    - *
      - *
    1. {@code SecretKey} instances must have an {@link Key#getAlgorithm() algorithm} name equal - * to {@code HmacSHA256}, {@code HmacSHA384} or {@code HmacSHA512}. If not, the key bytes might not be - * suitable for HMAC signatures will be rejected with a {@link InvalidKeyException}.
    2. - *
    3. The JWT JWA Specification (RFC 7518, - * Section 3.2) mandates that HMAC-SHA-* signing keys MUST be 256 bits or greater. - * {@code SecretKey}s with key lengths less than 256 bits will be rejected with an - * {@link WeakKeyException}.
    4. - *
    5. The JWT JWA Specification (RFC 7518, - * Section 3.4) mandates that ECDSA signing key lengths MUST be 256 bits or greater. - * {@code ECKey}s with key lengths less than 256 bits will be rejected with a - * {@link WeakKeyException}.
    6. - *
    7. The ECDSA {@code P-521} curve does indeed use keys of 521 bits, not 512 as might be expected. ECDSA - * keys of 384 < size <= 520 are suitable for ES384, while ES512 requires keys >= 521 bits. The '512' part of the - * ES512 name reflects the usage of the SHA-512 algorithm, not the ECDSA key length. ES512 with ECDSA keys less - * than 521 bits will be rejected with a {@link WeakKeyException}.
    8. - *
    9. The JWT JWA Specification (RFC 7518, - * Section 3.3) mandates that RSA signing key lengths MUST be 2048 bits or greater. - * {@code RSAKey}s with key lengths less than 2048 bits will be rejected with a - * {@link WeakKeyException}.
    10. - *
    11. Technically any RSA key of length >= 2048 bits may be used with the {@link #RS256}, {@link #RS384}, and - * {@link #RS512} algorithms, so we assume an RSA signature algorithm based on the key length to - * parallel similar decisions in the JWT specification for HMAC and ECDSA signature algorithms. - * This is not required - just a convenience.
    12. - *
    - *

    This implementation does not return the {@link #PS256}, {@link #PS256}, {@link #PS256} RSA variants for any - * specified {@link RSAKey} because the the {@link #RS256}, {@link #RS384}, and {@link #RS512} algorithms are - * available in the JDK by default while the {@code PS}* variants require either JDK 11 or an additional JCA - * Provider (like BouncyCastle).

    - *

    Finally, this method will throw an {@link InvalidKeyException} for any key that does not match the - * heuristics and requirements documented above, since that inevitably means the Key is either insufficient or - * explicitly disallowed by the JWT specification.

    - * - * @param key the key to inspect - * @return the recommended signature algorithm to be used with the specified key - * @throws InvalidKeyException for any key that does not match the heuristics and requirements documented above, - * since that inevitably means the Key is either insufficient or explicitly disallowed by the JWT specification. - */ - public static SignatureAlgorithm forSigningKey(Key key) { - @SuppressWarnings("deprecation") - io.jsonwebtoken.SignatureAlgorithm alg = io.jsonwebtoken.SignatureAlgorithm.forSigningKey(key); - return forId(alg.getValue()); - } + * {@code ECDSA using P-521 and SHA-512} signature algorithm as defined by + * RFC 7518, Section 3.4. This algorithm + * requires a 521-bit key. + */ + public static final EllipticCurveSignatureAlgorithm ES512 = forId0("ES512"); } diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureException.java b/api/src/main/java/io/jsonwebtoken/security/SignatureException.java index 242626bcb..ad8a167ff 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureException.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureException.java @@ -23,10 +23,21 @@ @SuppressWarnings("deprecation") public class SignatureException extends io.jsonwebtoken.SignatureException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public SignatureException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public SignatureException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java b/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java index f2c34a67e..011f32290 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java @@ -25,6 +25,7 @@ *

    The content for signature input will be available via {@link #getContent()}, and the key used to compute * the signature will be available via {@link #getKey()}.

    * + * @param the type of {@link Key} used to compute a digital signature or message authentication code * @since JJWT_RELEASE_VERSION */ public interface SignatureRequest extends CryptoRequest { diff --git a/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java b/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java index f24396a41..f21183c2f 100644 --- a/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java @@ -22,10 +22,21 @@ */ public class UnsupportedKeyException extends KeyException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public UnsupportedKeyException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param msg the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public UnsupportedKeyException(String msg, Exception cause) { super(msg, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java b/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java index a3748be5e..4ed8efc43 100644 --- a/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java @@ -26,6 +26,7 @@ *

    The content to verify will be available via {@link #getContent()}, the previously-computed signature or MAC will * be available via {@link #getDigest()}, and the verification key will be available via {@link #getKey()}.

    * + * @param the type of {@link Key} used to verify a digital siganture or message authentication code * @since JJWT_RELEASE_VERSION */ public interface VerifySignatureRequest extends SignatureRequest, DigestSupplier { diff --git a/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java b/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java index 87a611988..8b466d048 100644 --- a/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java @@ -23,6 +23,11 @@ */ public class WeakKeyException extends InvalidKeyException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public WeakKeyException(String message) { super(message); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index c4c6fd185..40af7de8b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -148,7 +148,7 @@ public T setHeaderParam(String name, Object value) { @Override public T signWith(Key key) throws InvalidKeyException { Assert.notNull(key, "Key argument cannot be null."); - SignatureAlgorithm alg = (SignatureAlgorithm) SignatureAlgorithms.forSigningKey(key); + SignatureAlgorithm alg = (SignatureAlgorithm) SignatureAlgorithmsBridge.forSigningKey(key); return signWith(key, alg); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java index a7aa6b40e..ccbf13124 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java @@ -120,7 +120,7 @@ public byte[] apply(Signature sig) throws Exception { @Override protected boolean doVerify(final VerifySignatureRequest request) throws Exception { final Key key = request.getKey(); - if (key instanceof PrivateKey) { //legacy support only + if (key instanceof PrivateKey) { //legacy support only TODO: remove for 1.0 return super.doVerify(request); } return execute(request, Signature.class, new CheckedFunction() { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java index fa48ed5a7..5837b67e6 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java @@ -1,11 +1,11 @@ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.impl.IdRegistry; import io.jsonwebtoken.impl.lang.Registry; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.SignatureAlgorithm; +import java.security.Key; import java.util.Collection; @SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.SignatureAlgorithms implementation @@ -49,8 +49,14 @@ private SignatureAlgorithmsBridge() { SignatureAlgorithm instance = findById(id); if (instance == null) { String msg = "Unrecognized JWA SignatureAlgorithm identifier: " + id; - throw new UnsupportedJwtException(msg); + throw new IllegalArgumentException(msg); } return instance; } + + public static SignatureAlgorithm forSigningKey(Key key) { + @SuppressWarnings("deprecation") + io.jsonwebtoken.SignatureAlgorithm alg = io.jsonwebtoken.SignatureAlgorithm.forSigningKey(key); + return forId(alg.getValue()); + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy index f89a8a0ae..555d40c14 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy @@ -16,10 +16,7 @@ package io.jsonwebtoken.security import io.jsonwebtoken.impl.lang.Bytes -import io.jsonwebtoken.impl.security.DefaultEllipticCurveSignatureAlgorithm -import io.jsonwebtoken.impl.security.DefaultPasswordKey -import io.jsonwebtoken.impl.security.DefaultRsaSignatureAlgorithm -import io.jsonwebtoken.impl.security.KeysBridge +import io.jsonwebtoken.impl.security.* import org.junit.Test import javax.crypto.SecretKey @@ -134,7 +131,7 @@ class KeysTest { SecretKey key = alg.keyBuilder().build() assertEquals alg.getKeyBitLength(), Bytes.bitLength(key.getEncoded()) assertEquals alg.jcaName, key.algorithm - assertEquals alg, SignatureAlgorithms.forSigningKey(key) // https://github.com/jwtk/jjwt/issues/381 + assertEquals alg, SignatureAlgorithmsBridge.forSigningKey(key) // https://github.com/jwtk/jjwt/issues/381 } } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy index 5c0e89aef..4c0fee275 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/SignatureAlgorithmsTest.groovy @@ -1,6 +1,6 @@ package io.jsonwebtoken.security -import io.jsonwebtoken.UnsupportedJwtException + import org.junit.Test import static org.junit.Assert.assertNull @@ -27,7 +27,7 @@ class SignatureAlgorithmsTest { } } - @Test(expected = UnsupportedJwtException) + @Test(expected = IllegalArgumentException) void testForIdWithInvalidId() { //unlike the 'find' paradigm, 'for' requires the value to exist SignatureAlgorithms.forId('invalid') From 5d66e376c42b46a93640aaf6b872c4b95b8d1dc2 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Tue, 24 May 2022 20:33:36 -0700 Subject: [PATCH 53/75] Fixed erroneous JavaDoc element --- .../io/jsonwebtoken/security/KeyAlgorithms.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index 11e7bc544..b2bbc1ea6 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -375,7 +375,7 @@ private static T forId0(String id) { * JWE recipient's EC Public Key. *
  • Generates a shared secret with the ECDH key agreement algorithm using the generated EC Private Key * and the JWE recipient's EC Public Key.
  • - *
  • Derives a symmetric Content + *
  • Derives a symmetric Content * Encryption {@code SecretKey} with the Concat KDF algorithm using the * generated shared secret and any available * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and @@ -395,7 +395,7 @@ private static T forId0(String id) { *
  • Validates that the ephemeral Public Key is on the same curve as the recipient's EC Private Key.
  • *
  • Obtains the shared secret with the ECDH key agreement algorithm using the obtained EC Public Key * and the JWE recipient's EC Private Key.
  • - *
  • Derives the symmetric Content + *
  • Derives the symmetric Content * Encryption {@code SecretKey} with the Concat KDF algorithm using the * obtained shared secret and any available * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and @@ -417,7 +417,7 @@ private static T forId0(String id) { * JWE recipient's EC Public Key.
  • *
  • Generates a shared secret with the ECDH key agreement algorithm using the generated EC Private Key * and the JWE recipient's EC Public Key.
  • - *
  • Derives a 128-bit symmetric Key + *
  • Derives a 128-bit symmetric Key * Encryption {@code SecretKey} with the Concat KDF algorithm using the * generated shared secret and any available * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and @@ -440,7 +440,7 @@ private static T forId0(String id) { *
  • Validates that the ephemeral Public Key is on the same curve as the recipient's EC Private Key.
  • *
  • Obtains the shared secret with the ECDH key agreement algorithm using the obtained EC Public Key * and the JWE recipient's EC Private Key.
  • - *
  • Derives the symmetric Key + *
  • Derives the symmetric Key * Encryption {@code SecretKey} with the Concat KDF algorithm using the * obtained shared secret and any available * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and @@ -465,7 +465,7 @@ private static T forId0(String id) { * JWE recipient's EC Public Key.
  • *
  • Generates a shared secret with the ECDH key agreement algorithm using the generated EC Private Key * and the JWE recipient's EC Public Key.
  • - *
  • Derives a 192-bit symmetric Key + *
  • Derives a 192-bit symmetric Key * Encryption {@code SecretKey} with the Concat KDF algorithm using the * generated shared secret and any available * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and @@ -489,7 +489,7 @@ private static T forId0(String id) { *
  • Validates that the ephemeral Public Key is on the same curve as the recipient's EC Private Key.
  • *
  • Obtains the shared secret with the ECDH key agreement algorithm using the obtained EC Public Key * and the JWE recipient's EC Private Key.
  • - *
  • Derives the 192-bit symmetric + *
  • Derives the 192-bit symmetric * Key Encryption {@code SecretKey} with the Concat KDF algorithm using the * obtained shared secret and any available * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and @@ -514,7 +514,7 @@ private static T forId0(String id) { * JWE recipient's EC Public Key.
  • *
  • Generates a shared secret with the ECDH key agreement algorithm using the generated EC Private Key * and the JWE recipient's EC Public Key.
  • - *
  • Derives a 256-bit symmetric Key + *
  • Derives a 256-bit symmetric Key * Encryption {@code SecretKey} with the Concat KDF algorithm using the * generated shared secret and any available * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and @@ -538,7 +538,7 @@ private static T forId0(String id) { *
  • Validates that the ephemeral Public Key is on the same curve as the recipient's EC Private Key.
  • *
  • Obtains the shared secret with the ECDH key agreement algorithm using the obtained EC Public Key * and the JWE recipient's EC Private Key.
  • - *
  • Derives the 256-bit symmetric + *
  • Derives the 256-bit symmetric * Key Encryption {@code SecretKey} with the Concat KDF algorithm using the * obtained shared secret and any available * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and From 32759edd332cc62f04f56ad91b474680dce3f438 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 25 May 2022 11:15:12 -0700 Subject: [PATCH 54/75] Fixed LocatorAdapter usage now that it's abstract --- .../test/groovy/io/jsonwebtoken/LocatorAdapterTest.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/impl/src/test/groovy/io/jsonwebtoken/LocatorAdapterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/LocatorAdapterTest.groovy index ca336d98d..a036d29de 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/LocatorAdapterTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/LocatorAdapterTest.groovy @@ -25,7 +25,7 @@ class LocatorAdapterTest { @Test void testJwtHeaderWithoutOverride() { Header input = new DefaultHeader() - Locator locator = new LocatorAdapter() + Locator locator = new LocatorAdapter() {} assertNull locator.locate(input as Header) } @@ -44,7 +44,7 @@ class LocatorAdapterTest { @Test void testJwsHeaderWithoutOverride() { Header input = new DefaultJwsHeader() - Locator locator = new LocatorAdapter() + Locator locator = new LocatorAdapter() {} assertNull locator.locate(input as Header) } @@ -63,7 +63,7 @@ class LocatorAdapterTest { @Test void testJweHeaderWithoutOverride() { JweHeader input = new DefaultJweHeader() - def locator = new LocatorAdapter() + def locator = new LocatorAdapter() {} assertNull locator.locate(input as Header) } } From a3e246090909669a9516ed614236f7641484599c Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 25 May 2022 11:46:11 -0700 Subject: [PATCH 55/75] Minor JavaDoc fix --- api/src/main/java/io/jsonwebtoken/lang/Objects.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/io/jsonwebtoken/lang/Objects.java b/api/src/main/java/io/jsonwebtoken/lang/Objects.java index 1f93fd275..3787c218f 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Objects.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Objects.java @@ -197,7 +197,7 @@ public static > E caseInsensitiveValueOf(E[] enumValues, Strin * @param array the array to append to (can be null) * @param the type of each element in the specified {@code array} * @param obj the object to append - * @param the type of the specified object, which must equal to or extend the {@code <A>} type. + * @param the type of the specified object, which must be equal to or extend the <A> type. * @return the new array (of the same component type; never null) */ public static A[] addObjectToArray(A[] array, O obj) { From b83798c1ba98b1d2744c6e7d0a568ef8eeae74f6 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 25 May 2022 15:47:16 -0700 Subject: [PATCH 56/75] JavaDoc is now complete (no warnings) for api module --- .../main/java/io/jsonwebtoken/JweHeader.java | 159 ++++++++++++++ .../io/jsonwebtoken/JwtParserBuilder.java | 3 +- .../java/io/jsonwebtoken/ProtectedHeader.java | 203 ++++++++++++++++++ .../jsonwebtoken/security/KeyAlgorithms.java | 156 ++++++++++++-- 4 files changed, 508 insertions(+), 13 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/JweHeader.java b/api/src/main/java/io/jsonwebtoken/JweHeader.java index f0c726e34..3000a1330 100644 --- a/api/src/main/java/io/jsonwebtoken/JweHeader.java +++ b/api/src/main/java/io/jsonwebtoken/JweHeader.java @@ -15,6 +15,8 @@ */ package io.jsonwebtoken; +import io.jsonwebtoken.security.KeyAlgorithms; + /** * A JWE header. * @@ -48,27 +50,184 @@ public interface JweHeader extends ProtectedHeader { // */ // JweHeader setEncryptionAlgorithm(String enc); + /** + * Returns the number of PBKDF2 iterations necessary to derive the key used to encrypt the JWE, or {@code null} + * if not present. Used with password-based {@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm}s. + * + * @return the number of PBKDF2 iterations necessary to derive the key used to encrypt the JWE, or {@code null} + * if not present. + * @see JWE p2c (PBES2 Count) Header Parameter + * @see KeyAlgorithms#PBES2_HS256_A128KW + * @see KeyAlgorithms#PBES2_HS384_A192KW + * @see KeyAlgorithms#PBES2_HS512_A256KW + */ Integer getPbes2Count(); + /** + * Sets the number of PBKDF2 iterations necessary to derive the key used to encrypt the JWE. A {@code null} value + * will remove the property from the JSON map. + * + * @param count the number of PBKDF2 iterations necessary to derive the key used to encrypt the JWE. + * @return the header for method chaining + * @see JWE p2c (PBES2 Count) Header Parameter + * @see KeyAlgorithms#PBES2_HS256_A128KW + * @see KeyAlgorithms#PBES2_HS384_A192KW + * @see KeyAlgorithms#PBES2_HS512_A256KW + */ JweHeader setPbes2Count(int count); + /** + * Returns the PBKDF2 {@code Salt Input} value necessary to derive the key used to encrypt the JWE, or {@code null} + * if not present. Used with password-based {@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm}s. + * + * @return the PBKDF2 {@code Salt Input} value necessary to derive the key used to encrypt the JWE, or {@code null} + * if not present. + * @see JWE p2s (PBES2 Salt Input) Header Parameter + * @see KeyAlgorithms#PBES2_HS256_A128KW + * @see KeyAlgorithms#PBES2_HS384_A192KW + * @see KeyAlgorithms#PBES2_HS512_A256KW + */ byte[] getPbes2Salt(); + /** + * Sets the PBKDF2 {@code Salt Input} value necessary to derive the key used to encrypt the JWE. This should + * almost never be used by JJWT users directly - it should be automatically generated and set within a PBKDF2-based + * {@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm} implementation. + * + * @param salt the PBKDF2 {@code Salt Input} value necessary to derive the key used to encrypt the JWE. + * @return the header for method chaining + * @see JWE p2s (PBES2 Salt Input) Header Parameter + * @see KeyAlgorithms#PBES2_HS256_A128KW + * @see KeyAlgorithms#PBES2_HS384_A192KW + * @see KeyAlgorithms#PBES2_HS512_A256KW + */ JweHeader setPbes2Salt(byte[] salt); + /** + * Returns any information about the JWE producer for use with key agreement algorithms, or {@code null} if not + * present. + * + * @return any information about the JWE producer for use with key agreement algorithms, or {@code null} if not + * present. + * JWE apu (Agreement PartyUInfo) Header Parameter + * @see KeyAlgorithms#ECDH_ES + * @see KeyAlgorithms#ECDH_ES_A128KW + * @see KeyAlgorithms#ECDH_ES_A192KW + * @see KeyAlgorithms#ECDH_ES_A256KW + */ byte[] getAgreementPartyUInfo(); + /** + * Returns any information about the JWE producer for use with key agreement algorithms as a UTF-8 String, + * or {@code null} if not present. + * + *

    If not {@code null}, this is a convenience method that returns the equivalent of the following:

    + *
    +     * new String({@link #getAgreementPartyUInfo() getAgreementPartyUInfo()}, StandardCharsets.UTF_8)
    + * + * @return any information about the JWE producer for use with key agreement algorithms, or {@code null} if not + * present. + * JWE apu (Agreement PartyUInfo) Header Parameter + * @see KeyAlgorithms#ECDH_ES + * @see KeyAlgorithms#ECDH_ES_A128KW + * @see KeyAlgorithms#ECDH_ES_A192KW + * @see KeyAlgorithms#ECDH_ES_A256KW + */ String getAgreementPartyUInfoString(); + /** + * Sets any information about the JWE producer for use with key agreement algorithms. A {@code null} value removes + * the property from the JSON map. + * + * @param info information about the JWE producer to use with key agreement algorithms. + * @return the header for method chaining. + * @see JWE apu (Agreement PartyUInfo) Header Parameter + * @see KeyAlgorithms#ECDH_ES + * @see KeyAlgorithms#ECDH_ES_A128KW + * @see KeyAlgorithms#ECDH_ES_A192KW + * @see KeyAlgorithms#ECDH_ES_A256KW + */ JweHeader setAgreementPartyUInfo(byte[] info); + /** + * Sets any information about the JWE producer for use with key agreement algorithms. A {@code null} value removes + * the property from the JSON map. + * + *

    If not {@code null}, this is a convenience method that calls the equivalent of the following:

    + *
    +     * {@link #setAgreementPartyUInfo(byte[]) setAgreementPartyUInfo}(info.getBytes(StandardCharsets.UTF_8))
    + * + * @param info information about the JWE producer to use with key agreement algorithms. + * @return the header for method chaining. + * @see JWE apu (Agreement PartyUInfo) Header Parameter + * @see KeyAlgorithms#ECDH_ES + * @see KeyAlgorithms#ECDH_ES_A128KW + * @see KeyAlgorithms#ECDH_ES_A192KW + * @see KeyAlgorithms#ECDH_ES_A256KW + */ JweHeader setAgreementPartyUInfo(String info); + /** + * Returns any information about the JWE recipient for use with key agreement algorithms, or {@code null} if not + * present. + * + * @return any information about the JWE recipient for use with key agreement algorithms, or {@code null} if not + * present. + * JWE apv (Agreement PartyVInfo) Header Parameter + * @see KeyAlgorithms#ECDH_ES + * @see KeyAlgorithms#ECDH_ES_A128KW + * @see KeyAlgorithms#ECDH_ES_A192KW + * @see KeyAlgorithms#ECDH_ES_A256KW + */ byte[] getAgreementPartyVInfo(); + /** + * Returns any information about the JWE recipient for use with key agreement algorithms as a UTF-8 String, + * or {@code null} if not present. + * + *

    If not {@code null}, this is a convenience method that returns the equivalent of the following:

    + *
    +     * new String({@link #getAgreementPartyVInfo() getAgreementPartyVInfo()}, StandardCharsets.UTF_8)
    + * + * @return any information about the JWE recipient for use with key agreement algorithms, or {@code null} if not + * present. + * JWE apv (Agreement PartyVInfo) Header Parameter + * @see KeyAlgorithms#ECDH_ES + * @see KeyAlgorithms#ECDH_ES_A128KW + * @see KeyAlgorithms#ECDH_ES_A192KW + * @see KeyAlgorithms#ECDH_ES_A256KW + */ String getAgreementPartyVInfoString(); + /** + * Sets any information about the JWE recipient for use with key agreement algorithms. A {@code null} value removes + * the property from the JSON map. + * + * @param info information about the JWE recipient to use with key agreement algorithms. + * @return the header for method chaining. + * @see JWE apv (Agreement PartyVInfo) Header Parameter + * @see KeyAlgorithms#ECDH_ES + * @see KeyAlgorithms#ECDH_ES_A128KW + * @see KeyAlgorithms#ECDH_ES_A192KW + * @see KeyAlgorithms#ECDH_ES_A256KW + */ JweHeader setAgreementPartyVInfo(byte[] info); + /** + * Sets any information about the JWE recipient for use with key agreement algorithms. A {@code null} value removes + * the property from the JSON map. + * + *

    If not {@code null}, this is a convenience method that calls the equivalent of the following:

    + *
    +     * {@link #setAgreementPartyVInfo(byte[]) setAgreementPartVUInfo}(info.getBytes(StandardCharsets.UTF_8))
    + * + * @param info information about the JWE recipient to use with key agreement algorithms. + * @return the header for method chaining. + * @see JWE apv (Agreement PartyVInfo) Header Parameter + * @see KeyAlgorithms#ECDH_ES + * @see KeyAlgorithms#ECDH_ES_A128KW + * @see KeyAlgorithms#ECDH_ES_A192KW + * @see KeyAlgorithms#ECDH_ES_A256KW + */ JweHeader setAgreementPartyVInfo(String info); } diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index 30e211797..555047f43 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -387,7 +387,8 @@ public interface JwtParserBuilder extends Builder { /** * Adds the specified signature algorithms to the parser's total set of supported signature algorithms, - * overwriting any previously-added algorithms with the same {@link SignatureAlgorithm#getId() id}s. + * overwriting any previously-added algorithms with the same + * {@link io.jsonwebtoken.security.SignatureAlgorithm#getId() id}s. * *

    There may be only one registered {@code SignatureAlgorithm} per algorithm {@code id}, and the {@code sigAlgs} * collection is added in iteration order; if a duplicate id is found when iterating the {@code sigAlgs} diff --git a/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java index 36e1b2676..23c1ad857 100644 --- a/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java +++ b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java @@ -17,12 +17,59 @@ */ public interface ProtectedHeader> extends Header { + /** + * Returns the {@code jku} (JWK Set URL) value that refers to a + * JWK Set + * resource containing JSON-encoded Public Keys, or {@code null} if not present. When present in a + * {@link JwsHeader}, the first public key in the JWK Set must be the public key used to sign the JWS. + * When present in a {@link JweHeader}, the first public key in the JWK Set must be the public key used + * during encryption. + * + * @return a URI that refers to a JWK Set + * resource for a set of JSON-encoded Public Keys, or {@code null} if not present. + * @see JWS JWK Set URL + * @see JWE JWK Set URL + */ URI getJwkSetUrl(); + /** + * Sets the {@code jku} (JWK Set URL) value that refers to a JWK Set + * resource containing JSON-encoded Public Keys, or {@code null} if not present. When set for a + * {@link JwsHeader}, the first public key in the JWK Set must be the public key used to sign the JWS. + * When set for a {@link JweHeader}, the first public key in the JWK Set must be the public key used + * during encryption. + * + * @param uri a URI that refers to a JWK Set + * resource containing JSON-encoded Public Keys + * @return the header for method chaining + * @see JWS JWK Set URL + * @see JWE JWK Set URL + */ T setJwkSetUrl(URI uri); + /** + * Returns the {@code jwk} (JSON Web Key) associated with the JWT. When present in a {@link JwsHeader}, the + * {@code jwk} corresponds to the public key used to digitally sign the JWS. When present in a {@link JweHeader}, + * the {@code jwk} is the public key to which the JWE was encrypted, and may be used to determine the private key + * needed to decrypt the JWE. + * + * @return the {@code jwk} (JSON Web Key) associated with the header. + * @see JWS {@code jwk} (JSON Web Key) Header Parameter + * @see JWE {@code jwk} (JSON Web Key) Header Parameter + */ PublicJwk getJwk(); + /** + * Sets the {@code jwk} (JSON Web Key) associated with the JWT. When set for a {@link JwsHeader}, the + * {@code jwk} corresponds to the public key used to digitally sign the JWS. When set for a {@link JweHeader}, + * the {@code jwk} is the public key to which the JWE was encrypted, and may be used to determine the private key + * needed to decrypt the JWE. + * + * @param jwk the {@code jwk} (JSON Web Key) associated with the header. + * @return the header for method chaining + * @see JWS {@code jwk} (JSON Web Key) Header Parameter + * @see JWE {@code jwk} (JSON Web Key) Header Parameter + */ T setJwk(PublicJwk jwk); /** @@ -57,23 +104,179 @@ public interface ProtectedHeader> extends Header */ T setKeyId(String kid); + /** + * Returns the {@code x5u} (X.509 URL) that refers to a resource for the X.509 public key certificate or certificate + * chain associated with the JWT, or {@code null} if not present. + * + *

    When present in a {@link JwsHeader}, the certificate or certificate chain + * corresponds to the public key used to digitally sign the JWS. When present in a {@link JweHeader}, the + * certificate or certificate chain corresponds to the public key to which the JWE was encrypted, and may be + * used to determine the private key needed to decrypt the JWE.

    + * + *

    Each certificate in the resource MUST be in PEM-encoded form, with each certificate delimited as + * specified in Section 6.1 of RFC 4945.

    + * + * @return the {@code x5u} (X.509 URL) that refers to a resource for the X.509 public key certificate or certificate + * chain associated with the JWT. + * @see JWS {@code x5u} (X.509 URL) Header Parameter + * @see JWE {@code x5u} (X.509 URL) Header Parameter + */ URI getX509Url(); + /** + * Sets the {@code x5u} (X.509 URL) that refers to a resource for the X.509 public key certificate or certificate + * chain associated with the JWT. A {@code null} value will remove the property from the JSON map. + * + *

    When set for a {@link JwsHeader}, the certificate or certificate chain + * corresponds to the public key used to digitally sign the JWS. When present in a {@link JweHeader}, the + * certificate or certificate chain corresponds to the public key to which the JWE was encrypted, and may be + * used to determine the private key needed to decrypt the JWE.

    + * + *

    Each certificate in the resource MUST be in PEM-encoded form, with each certificate delimited as + * specified in Section 6.1 of RFC 4945.

    + * + * @param uri the {@code x5u} (X.509 URL) that refers to a resource for the X.509 public key certificate or certificate + * chain associated with the JWT. + * @return the header for method chaining. + * @see JWS {@code x5u} (X.509 URL) Header Parameter + * @see JWE {@code x5u} (X.509 URL) Header Parameter + */ T setX509Url(URI uri); + /** + * Returns the {@code x5c} (X.509 Certificate Chain) associated with the JWT, or {@code null} if not present. + * + *

    When present in a {@link JwsHeader}, + * the first certificate (at list index 0) corresponds to the public key used to digitally sign the JWS. When + * present in a {@link JweHeader}, the first certificate (at list index 0) corresponds to the public key to which + * the JWE was encrypted, and may be used to determine the private key needed to decrypt the JWE.

    + * + *

    The initial certificate MAY be followed by additional certificates, with each subsequent + * certificate being the one used to certify the previous one.

    + * + * @return the {@code x5c} (X.509 Certificate Chain) associated with the JWT or {@code null} if not present. + * @see JWS {@code x5c} (X.509 Certificate Chain) Header Parameter + * @see JWE {@code x5c} (X.509 Certificate Chain) Header Parameter + */ List getX509CertificateChain(); + /** + * Sets the {@code x5c} (X.509 Certificate Chain) associated with the JWT. A {@code null} value will remove the + * property from the JSON map. + * + *

    When set for a {@link JwsHeader}, + * the first certificate (at list index 0) MUST correspond to the public key used to digitally sign the + * JWS. When set for a {@link JweHeader}, the first certificate (at list index 0) MUST correspond to the + * public key to which the JWE was encrypted, and may be used to determine the private key needed to decrypt the + * JWE.

    + * + *

    The initial certificate MAY be followed by additional certificates, with each subsequent + * certificate being the one used to certify the previous one.

    + * + * @param chain the {@code x5c} (X.509 Certificate Chain) associated with the JWT. + * @return the header for method chaining. + * @see JWS {@code x5c} (X.509 Certificate Chain) Header Parameter + * @see JWE {@code x5c} (X.509 Certificate Chain) Header Parameter + */ T setX509CertificateChain(List chain); + /** + * Returns the {@code x5t} (X.509 Certificate SHA-1 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * X.509 Certificate associated with the JWT, or {@code null} if not present. + * + *

    When present in a {@link JwsHeader}, it is the thumbprint of the X.509 certificate corresponding to the key + * used to digitally sign the JWS. When present in a {@link JweHeader}, it is the thumbprint of the X.509 + * Certificate corresponding to the public key to which the JWE was encrypted, and may be used to determine the + * private key needed to decrypt the JWE.

    + * + *

    Note that certificate thumbprints are also sometimes known as certificate fingerprints.

    + * + * @return the {@code x5t} (X.509 Certificate SHA-1 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * X.509 Certificate associated with the JWT, or {@code null} if not present. + * @see JWS {@code x5t} (X.509 Certificate SHA-1 Thumbprint) Header Parameter + * @see JWE {@code x5t} (X.509 Certificate SHA-1 Thumbprint) Header Parameter + */ byte[] getX509CertificateSha1Thumbprint(); + /** + * Sets the {@code x5t} (X.509 Certificate SHA-1 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * X.509 Certificate associated with the JWT. A {@code null} value will remove the + * property from the JSON map. + * + *

    When set for a {@link JwsHeader}, it is the thumbprint of the X.509 certificate corresponding to the key + * used to digitally sign the JWS. When set for a {@link JweHeader}, it is the thumbprint of the X.509 + * Certificate corresponding to the public key to which the JWE was encrypted, and may be used to determine the + * private key needed to decrypt the JWE.

    + * + *

    Note that certificate thumbprints are also sometimes known as certificate fingerprints.

    + * + * @param thumbprint the {@code x5t} (X.509 Certificate SHA-1 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * X.509 Certificate associated with the JWT + * @return the header for method chaining + * @see JWS {@code x5t} (X.509 Certificate SHA-1 Thumbprint) Header Parameter + * @see JWE {@code x5t} (X.509 Certificate SHA-1 Thumbprint) Header Parameter + */ T setX509CertificateSha1Thumbprint(byte[] thumbprint); + /** + * Returns the {@code x5t#S256} (X.509 Certificate SHA-256 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * X.509 Certificate associated with the JWT, or {@code null} if not present. + * + *

    When present in a {@link JwsHeader}, it is the thumbprint of the X.509 certificate corresponding to the key + * used to digitally sign the JWS. When present in a {@link JweHeader}, it is the thumbprint of the X.509 + * Certificate corresponding to the public key to which the JWE was encrypted, and may be used to determine the + * private key needed to decrypt the JWE.

    + * + *

    Note that certificate thumbprints are also sometimes known as certificate fingerprints.

    + * + * @return the {@code x5t#S256} (X.509 Certificate SHA-256 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * X.509 Certificate associated with the JWT, or {@code null} if not present. + * @see JWS {@code x5t#S256} (X.509 Certificate SHA-256 Thumbprint) Header Parameter + * @see JWE {@code x5t#S256} (X.509 Certificate SHA-256 Thumbprint) Header Parameter + */ byte[] getX509CertificateSha256Thumbprint(); + /** + * Sets the {@code x5t#S256} (X.509 Certificate SHA-256 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * X.509 Certificate associated with the JWT. A {@code null} value will remove the + * property from the JSON map. + * + *

    When set for a {@link JwsHeader}, it is the thumbprint of the X.509 certificate corresponding to the key + * used to digitally sign the JWS. When set for a {@link JweHeader}, it is the thumbprint of the X.509 + * Certificate corresponding to the public key to which the JWE was encrypted, and may be used to determine the + * private key needed to decrypt the JWE.

    + * + *

    Note that certificate thumbprints are also sometimes known as certificate fingerprints.

    + * + * @param thumbprint the {@code x5t#S256} (X.509 Certificate SHA-256 Thumbprint) (a.k.a. digest) of the + * DER-encoding of the X.509 Certificate associated with the JWT + * @return the header for method chaining + * @see JWS {@code x5t#S256} (X.509 Certificate SHA-256 Thumbprint) Header Parameter + * @see JWE {@code x5t#S256} (X.509 Certificate SHA-256 Thumbprint) Header Parameter + */ T setX509CertificateSha256Thumbprint(byte[] thumbprint); + /** + * Returns the header parameter names that use extensions to the JWT or JWA specification that MUST + * be understood and supported by the JWT recipient, or {@code null} if not present. + * + * @return the header parameter names that use extensions to the JWT or JWA specification that MUST + * be understood and supported by the JWT recipient, or {@code null} if not present. + * @see JWS {@code crit} (Critical) Header Parameter + * @see JWS {@code crit} (Critical) Header Parameter + */ Set getCritical(); + /** + * Sets the header parameter names that use extensions to the JWT or JWA specification that MUST + * be understood and supported by the JWT recipient. A {@code null} value will remove the + * property from the JSON map. + * + * @param crit the header parameter names that use extensions to the JWT or JWA specification that MUST + * be understood and supported by the JWT recipient. + * @return the header for method chaining. + * @see JWS {@code crit} (Critical) Header Parameter + * @see JWS {@code crit} (Critical) Header Parameter + */ T setCritical(Set crit); } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index b2bbc1ea6..cfb5569fc 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -113,11 +113,11 @@ private static T forId0(String id) { * *

    For JWE decryption, this algorithm:

    *
      - *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Obtains the encrypted key ciphertext embedded in the received JWE.
    4. *
    5. Decrypts the encrypted key ciphertext with the 128-bit shared symmetric key, * using the AES Key Unwrap algorithm, producing the decryption key plaintext.
    6. *
    7. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire - * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    8. + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}. *
    */ public static final SecretKeyAlgorithm A128KW = forId0("A128KW"); @@ -137,11 +137,11 @@ private static T forId0(String id) { * *

    For JWE decryption, this algorithm:

    *
      - *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Obtains the encrypted key ciphertext embedded in the received JWE.
    4. *
    5. Decrypts the encrypted key ciphertext with the 192-bit shared symmetric key, * using the AES Key Unwrap algorithm, producing the decryption key plaintext.
    6. *
    7. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire - * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    8. + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}. *
    */ public static final SecretKeyAlgorithm A192KW = forId0("A192KW"); @@ -161,11 +161,11 @@ private static T forId0(String id) { * *

    For JWE decryption, this algorithm:

    *
      - *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Obtains the encrypted key ciphertext embedded in the received JWE.
    4. *
    5. Decrypts the encrypted key ciphertext with the 256-bit shared symmetric key, * using the AES Key Unwrap algorithm, producing the decryption key plaintext.
    6. *
    7. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire - * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    8. + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}. *
    */ public static final SecretKeyAlgorithm A256KW = forId0("A256KW"); @@ -193,7 +193,7 @@ private static T forId0(String id) { * *

    For JWE decryption, this algorithm:

    *
      - *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Obtains the encrypted key ciphertext embedded in the received JWE.
    4. *
    5. Obtains the required initialization vector from the * "iv" * (Initialization Vector) Header Parameter
    6. @@ -204,7 +204,7 @@ private static T forId0(String id) { * and GCM authentication tag using the AES GCM Key Unwrap algorithm, producing the decryption key * plaintext. *
    7. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire - * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    8. + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}. *
    */ public static final SecretKeyAlgorithm A128GCMKW = forId0("A128GCMKW"); @@ -232,7 +232,7 @@ private static T forId0(String id) { * *

    For JWE decryption, this algorithm:

    *
      - *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Obtains the encrypted key ciphertext embedded in the received JWE.
    4. *
    5. Obtains the required initialization vector from the * "iv" * (Initialization Vector) Header Parameter
    6. @@ -243,7 +243,7 @@ private static T forId0(String id) { * and GCM authentication tag using the AES GCM Key Unwrap algorithm, producing the decryption key \ * plaintext. *
    7. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire - * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    8. + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}. *
    */ public static final SecretKeyAlgorithm A192GCMKW = forId0("A192GCMKW"); @@ -271,7 +271,7 @@ private static T forId0(String id) { * *

    For JWE decryption, this algorithm:

    *
      - *
    1. Receives the encrypted key ciphertext embedded in the received JWE.
    2. + *
    3. Obtains the encrypted key ciphertext embedded in the received JWE.
    4. *
    5. Obtains the required initialization vector from the * "iv" * (Initialization Vector) Header Parameter
    6. @@ -282,12 +282,144 @@ private static T forId0(String id) { * and GCM authentication tag using the AES GCM Key Unwrap algorithm, producing the decryption key \ * plaintext. *
    7. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire - * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    8. + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}. *
    */ public static final SecretKeyAlgorithm A256GCMKW = forId0("A256GCMKW"); + + /** + * Key encryption algorithm using PBES2 with HMAC SHA-256 and "A128KW" wrapping + * as defined by + * RFC 7518 (JWA), Section 4.8. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Determines the number of PBDKF2 iterations via the JWE header's + * {@link JweHeader#getPbes2Count() pbes2Count} value. If that value is not set, a suitable number of + * iterations will be chosen based on + * OWASP + * PBKDF2 recommendations and then that value is set as the JWE header {@code pbes2Count} value.
    2. + *
    3. Generates a new secure-random salt input and sets it as the JWE header + * {@link JweHeader#setPbes2Salt(byte[]) pbes2Salt} value.
    4. + *
    5. Derives a 128-bit Key Encryption Key with the PBES2-HS256 password-based key derivation algorithm, + * using the provided password, iteration count, and input salt as arguments.
    6. + *
    7. Generates a new secure-random Content Encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    8. + *
    9. Encrypts this newly-generated Content Encryption {@code SecretKey} with the {@code A128KW} key wrap + * algorithm using the 128-bit derived password-based Key Encryption Key from step {@code #3}, + * producing encrypted key ciphertext.
    10. + *
    11. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * Content Encryption {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated + * {@link AeadAlgorithm}.
    12. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Obtains the required PBKDF2 input salt from the + * "p2s" + * (PBES2 Salt Input) Header Parameter
    2. + *
    3. Obtains the required PBKDF2 iteration count from the + * "p2c" + * (PBES2 Count) Header Parameter
    4. + *
    5. Derives the 128-bit Key Encryption Key with the PBES2-HS256 password-based key derivation algorithm, + * using the provided password, obtained salt input, and obtained iteration count as arguments.
    6. + *
    7. Obtains the encrypted key ciphertext embedded in the received JWE.
    8. + *
    9. Decrypts the encrypted key ciphertext with with the {@code A128KW} key unwrap + * algorithm using the 128-bit derived password-based Key Encryption Key from step {@code #3}, + * producing the decryption key plaintext.
    10. + *
    11. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    12. + *
    + */ public static final KeyAlgorithm PBES2_HS256_A128KW = forId0("PBES2-HS256+A128KW"); + + /** + * Key encryption algorithm using PBES2 with HMAC SHA-384 and "A192KW" wrapping + * as defined by + * RFC 7518 (JWA), Section 4.8. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Determines the number of PBDKF2 iterations via the JWE header's + * {@link JweHeader#getPbes2Count() pbes2Count} value. If that value is not set, a suitable number of + * iterations will be chosen based on + * OWASP + * PBKDF2 recommendations and then that value is set as the JWE header {@code pbes2Count} value.
    2. + *
    3. Generates a new secure-random salt input and sets it as the JWE header + * {@link JweHeader#setPbes2Salt(byte[]) pbes2Salt} value.
    4. + *
    5. Derives a 192-bit Key Encryption Key with the PBES2-HS384 password-based key derivation algorithm, + * using the provided password, iteration count, and input salt as arguments.
    6. + *
    7. Generates a new secure-random Content Encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    8. + *
    9. Encrypts this newly-generated Content Encryption {@code SecretKey} with the {@code A192KW} key wrap + * algorithm using the 192-bit derived password-based Key Encryption Key from step {@code #3}, + * producing encrypted key ciphertext.
    10. + *
    11. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * Content Encryption {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated + * {@link AeadAlgorithm}.
    12. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Obtains the required PBKDF2 input salt from the + * "p2s" + * (PBES2 Salt Input) Header Parameter
    2. + *
    3. Obtains the required PBKDF2 iteration count from the + * "p2c" + * (PBES2 Count) Header Parameter
    4. + *
    5. Derives the 192-bit Key Encryption Key with the PBES2-HS384 password-based key derivation algorithm, + * using the provided password, obtained salt input, and obtained iteration count as arguments.
    6. + *
    7. Obtains the encrypted key ciphertext embedded in the received JWE.
    8. + *
    9. Decrypts the encrypted key ciphertext with with the {@code A192KW} key unwrap + * algorithm using the 192-bit derived password-based Key Encryption Key from step {@code #3}, + * producing the decryption key plaintext.
    10. + *
    11. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    12. + *
    + */ public static final KeyAlgorithm PBES2_HS384_A192KW = forId0("PBES2-HS384+A192KW"); + + /** + * Key encryption algorithm using PBES2 with HMAC SHA-512 and "A256KW" wrapping + * as defined by + * RFC 7518 (JWA), Section 4.8. + * + *

    During JWE creation, this algorithm:

    + *
      + *
    1. Determines the number of PBDKF2 iterations via the JWE header's + * {@link JweHeader#getPbes2Count() pbes2Count} value. If that value is not set, a suitable number of + * iterations will be chosen based on + * OWASP + * PBKDF2 recommendations and then that value is set as the JWE header {@code pbes2Count} value.
    2. + *
    3. Generates a new secure-random salt input and sets it as the JWE header + * {@link JweHeader#setPbes2Salt(byte[]) pbes2Salt} value.
    4. + *
    5. Derives a 256-bit Key Encryption Key with the PBES2-HS512 password-based key derivation algorithm, + * using the provided password, iteration count, and input salt as arguments.
    6. + *
    7. Generates a new secure-random Content Encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
    8. + *
    9. Encrypts this newly-generated Content Encryption {@code SecretKey} with the {@code A256KW} key wrap + * algorithm using the 256-bit derived password-based Key Encryption Key from step {@code #3}, + * producing encrypted key ciphertext.
    10. + *
    11. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * Content Encryption {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated + * {@link AeadAlgorithm}.
    12. + *
    + *

    For JWE decryption, this algorithm:

    + *
      + *
    1. Obtains the required PBKDF2 input salt from the + * "p2s" + * (PBES2 Salt Input) Header Parameter
    2. + *
    3. Obtains the required PBKDF2 iteration count from the + * "p2c" + * (PBES2 Count) Header Parameter
    4. + *
    5. Derives the 256-bit Key Encryption Key with the PBES2-HS512 password-based key derivation algorithm, + * using the provided password, obtained salt input, and obtained iteration count as arguments.
    6. + *
    7. Obtains the encrypted key ciphertext embedded in the received JWE.
    8. + *
    9. Decrypts the encrypted key ciphertext with with the {@code A256KW} key unwrap + * algorithm using the 256-bit derived password-based Key Encryption Key from step {@code #3}, + * producing the decryption key plaintext.
    10. + *
    11. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
    12. + *
    + */ public static final KeyAlgorithm PBES2_HS512_A256KW = forId0("PBES2-HS512+A256KW"); /** From cbcde30df2b034c026b5f5e6a881b825e4fc399e Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Thu, 26 May 2022 15:20:07 -0700 Subject: [PATCH 57/75] Ensured JWS signatures are computed first before deserializing the body if no SigningKeyResolver has been configured. --- api/src/main/java/io/jsonwebtoken/Jws.java | 2 +- .../main/java/io/jsonwebtoken/JwtBuilder.java | 2 +- .../jsonwebtoken/security/KeyAlgorithms.java | 10 +- .../jsonwebtoken/impl/DefaultJwtParser.java | 142 +++++++++--------- .../impl/DefaultJwtParserBuilder.java | 15 +- .../impl/security/LocatingKeyResolver.java | 29 ++++ .../groovy/io/jsonwebtoken/JwtsTest.groovy | 12 +- .../impl/DefaultJwtParserBuilderTest.groovy | 2 +- .../impl/security/JwkSerializationTest.groovy | 1 - .../security/LocatingKeyResolverTest.groovy | 33 ++++ 10 files changed, 156 insertions(+), 92 deletions(-) create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/LocatingKeyResolver.java create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/LocatingKeyResolverTest.groovy diff --git a/api/src/main/java/io/jsonwebtoken/Jws.java b/api/src/main/java/io/jsonwebtoken/Jws.java index 4c4cb9786..8264386e0 100644 --- a/api/src/main/java/io/jsonwebtoken/Jws.java +++ b/api/src/main/java/io/jsonwebtoken/Jws.java @@ -28,5 +28,5 @@ public interface Jws extends Jwt { * * @return the verified JWS signature as a Base64Url string. */ - String getSignature(); + String getSignature(); //TODO for 1.0: return a byte[] } diff --git a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java index 5ce4a5cfd..c43b2553e 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -612,7 +612,7 @@ public interface JwtBuilder> extends ClaimsMutator { * the specified algorithm. * @see #signWith(Key) * @since 0.10.0 - * @deprecated since JJWT_RELEASE_VERSION to use a more the more flexible {@link io.jsonwebtoken.security.SignatureAlgorithm}. + * @deprecated since JJWT_RELEASE_VERSION to use the more flexible {@link io.jsonwebtoken.security.SignatureAlgorithm}. */ @Deprecated T signWith(Key key, SignatureAlgorithm alg) throws InvalidKeyException; diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index cfb5569fc..0f2cdae3f 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -44,9 +44,13 @@ private KeyAlgorithms() { //private static final Class[] ESTIMATE_ITERATIONS_ARG_TYPES = new Class[]{KeyAlgorithm.class, long.class}; /** - * Returns all JWA-standard Key Management algorithms as an unmodifiable collection. + * Returns all JWA-standard + * Key Management Algorithms as an + * unmodifiable collection. * - * @return all JWA-standard Key Management algorithms as an unmodifiable collection. + * @return all JWA-standard + * Key Management Algorithms as an + * unmodifiable collection. */ public static Collection> values() { return Classes.invokeStatic(BRIDGE_CLASS, "values", null, (Object[]) null); @@ -81,7 +85,7 @@ private KeyAlgorithms() { * @see #findById(String) * @see RFC 7518, Section 4.1 */ - public static KeyAlgorithm forId(String id) { + public static KeyAlgorithm forId(String id) throws IllegalArgumentException { return forId0(id); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 086d1928c..bf723b01a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -33,6 +33,7 @@ import io.jsonwebtoken.JwtHandler; import io.jsonwebtoken.JwtHandlerAdapter; import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Locator; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.MissingClaimException; import io.jsonwebtoken.PrematureJwtException; @@ -49,6 +50,7 @@ import io.jsonwebtoken.impl.security.DefaultVerifySignatureRequest; import io.jsonwebtoken.impl.security.EncryptionAlgorithmsBridge; import io.jsonwebtoken.impl.security.KeyAlgorithmsBridge; +import io.jsonwebtoken.impl.security.LocatingKeyResolver; import io.jsonwebtoken.impl.security.SignatureAlgorithmsBridge; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Decoders; @@ -173,7 +175,7 @@ private static Function encFn(Collection> keyAlgorithmLocator; - private final Function, Key> keyLocator; + private final Locator keyLocator; private Decoder base64UrlDecoder = Decoders.BASE64URL; @@ -193,9 +195,7 @@ private static Function encFn(Collection>emptyList()); this.keyAlgorithmLocator = keyFn(Collections.>emptyList()); this.encryptionAlgorithmLocator = encFn(Collections.emptyList()); @@ -208,7 +208,7 @@ public DefaultJwtParser() { DefaultJwtParser(Provider provider, SigningKeyResolver signingKeyResolver, boolean enableUnsecuredJws, - Function, Key> keyLocator, + Locator keyLocator, Clock clock, long allowedClockSkewMillis, Claims expectedClaims, @@ -220,7 +220,7 @@ public DefaultJwtParser() { Collection extraEncAlgs) { this.provider = provider; this.enableUnsecuredJws = enableUnsecuredJws; - this.signingKeyResolver = Assert.notNull(signingKeyResolver, "SigningKeyResolver cannot be null."); + this.signingKeyResolver = signingKeyResolver; this.keyLocator = Assert.notNull(keyLocator, "Key Locator cannot be null."); this.clock = clock; this.allowedClockSkewMillis = allowedClockSkewMillis; @@ -363,6 +363,64 @@ private static boolean hasContentType(Header header) { return header != null && Strings.hasText(header.getContentType()); } + private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHeader, final String alg, + @SuppressWarnings("deprecation") SigningKeyResolver resolver, + Claims claims, String payload) { + + Assert.notNull(resolver, "SigningKeyResolver instance cannot be null."); + + SignatureAlgorithm algorithm; + try { + algorithm = (SignatureAlgorithm) signatureAlgorithmLocator.apply(jwsHeader); + } catch (UnsupportedJwtException e) { + //For backwards compatibility. TODO: remove this try/catch block for 1.0 and let UnsupportedJwtException propagate + String msg = "Unsupported signature algorithm '" + alg + "'"; + throw new SignatureException(msg, e); + } + Assert.stateNotNull(algorithm, "JWS Signature Algorithm cannot be null."); + + //digitally signed, let's assert the signature: + Key key; + if (claims != null) { + key = resolver.resolveSigningKey(jwsHeader, claims); + } else { + key = resolver.resolveSigningKey(jwsHeader, payload); + } + if (key == null) { + String msg = "Cannot verify JWS signature: unable to locate signature verification key for JWS with header: " + jwsHeader; + throw new UnsupportedJwtException(msg); + } + + //re-create the jwt part without the signature. This is what is needed for signature verification: + String jwtWithoutSignature = tokenized.getProtected() + SEPARATOR_CHAR + tokenized.getBody(); + + byte[] data = jwtWithoutSignature.getBytes(StandardCharsets.US_ASCII); + byte[] signature = base64UrlDecode(tokenized.getDigest(), "JWS signature"); + + try { + VerifySignatureRequest request = + new DefaultVerifySignatureRequest<>(this.provider, null, data, key, signature); + + if (!algorithm.verify(request)) { + String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + + "asserted and should not be trusted."; + throw new SignatureException(msg); + } + } catch (WeakKeyException e) { + throw e; + } catch (InvalidKeyException | IllegalArgumentException e) { + String algId = algorithm.getId(); + String msg = "The parsed JWT indicates it was signed with the '" + algId + "' signature " + + "algorithm, but the provided " + key.getClass().getName() + " key may " + + "not be used to verify " + algId + " signatures. Because the specified " + + "key reflects a specific and expected algorithm, and the JWT does not reflect " + + "this algorithm, it is likely that the JWT was not expected and therefore should not be " + + "trusted. Another possibility is that the parser was provided the incorrect " + + "signature verification key, but this cannot be assumed for security reasons."; + throw new UnsupportedJwtException(msg, e); + } + } + @Override public Jwt parse(String compact) throws ExpiredJwtException, MalformedJwtException, SignatureException { @@ -438,7 +496,7 @@ private static boolean hasContentType(Header header) { byte[] iv = null; byte[] tag = null; - if (tokenized instanceof TokenizedJwe) { //need to decrypt the ciphertext + if (tokenized instanceof TokenizedJwe) { TokenizedJwe tokenizedJwe = (TokenizedJwe) tokenized; JweHeader jweHeader = (JweHeader) header; @@ -486,7 +544,7 @@ private static boolean hasContentType(Header header) { @SuppressWarnings("rawtypes") final KeyAlgorithm keyAlg = this.keyAlgorithmLocator.apply(jweHeader); Assert.stateNotNull(keyAlg, "JWE Key Algorithm cannot be null."); - final Key key = this.keyLocator.apply(jweHeader); + final Key key = this.keyLocator.locate(jweHeader); if (key == null) { String msg = "Cannot decrypt JWE payload: unable to locate key for JWE with header: " + jweHeader; throw new UnsupportedJwtException(msg); @@ -505,9 +563,13 @@ private static boolean hasContentType(Header header) { new DefaultAeadResult(this.provider, null, bytes, cek, aad, tag, iv); Message result = encAlg.decrypt(decryptRequest); bytes = result.getContent(); + + } else if (Strings.hasText(base64UrlDigest) && this.signingKeyResolver == null) { //TODO: for 1.0, remove the == null check + // not using a signing key resolver, so we can verify the signature before reading the body, which is + // always safer: + verifySignature(tokenized, ((JwsHeader) header), alg, new LocatingKeyResolver(this.keyLocator), null, null); } - //TODO: Only allow decompression after JWS signature verification: CompressionCodec compressionCodec = compressionCodecLocator.apply(header); if (compressionCodec != null) { bytes = compressionCodec.decompress(bytes); @@ -540,64 +602,10 @@ private static boolean hasContentType(Header header) { } // =============== Signature ================= - if (jwt instanceof Jws) { // it's a JWS, validate the signature - - Jws jws = (Jws) jwt; - - final JwsHeader jwsHeader = jws.getHeader(); - - SignatureAlgorithm algorithm; - try { - algorithm = (SignatureAlgorithm) signatureAlgorithmLocator.apply(jwsHeader); - } catch (UnsupportedJwtException e) { - //For backwards compatibility. TODO: remove this try/catch block for 1.0 and let UnsupportedJwtException propagate - String msg = "Unsupported signature algorithm '" + alg + "'"; - throw new SignatureException(msg, e); - } - Assert.stateNotNull(algorithm, "JWS Signature Algorithm cannot be null."); - - Assert.stateNotNull(this.signingKeyResolver, "SigningKeyResolver cannot be null (invariant)."); - - //digitally signed, let's assert the signature: - Key key; - if (claims != null) { - key = signingKeyResolver.resolveSigningKey(jwsHeader, claims); - } else { - key = signingKeyResolver.resolveSigningKey(jwsHeader, payload); - } - if (key == null) { - String msg = "Cannot verify JWS signature: unable to locate signature verification key for JWS with header: " + jwsHeader; - throw new UnsupportedJwtException(msg); - } - - //re-create the jwt part without the signature. This is what is needed for signature verification: - String jwtWithoutSignature = tokenized.getProtected() + SEPARATOR_CHAR + tokenized.getBody(); - - byte[] data = jwtWithoutSignature.getBytes(StandardCharsets.US_ASCII); - byte[] signature = base64UrlDecode(tokenized.getDigest(), "JWS signature"); - - try { - VerifySignatureRequest request = - new DefaultVerifySignatureRequest<>(this.provider, null, data, key, signature); - - if (!algorithm.verify(request)) { - String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + - "asserted and should not be trusted."; - throw new SignatureException(msg); - } - } catch (WeakKeyException e) { - throw e; - } catch (InvalidKeyException | IllegalArgumentException e) { - String algId = algorithm.getId(); - String msg = "The parsed JWT indicates it was signed with the '" + algId + "' signature " + - "algorithm, but the provided " + key.getClass().getName() + " key may " + - "not be used to verify " + algId + " signatures. Because the specified " + - "key reflects a specific and expected algorithm, and the JWT does not reflect " + - "this algorithm, it is likely that the JWT was not expected and therefore should not be " + - "trusted. Another possibility is that the parser was provided the incorrect " + - "signature verification key, but this cannot be assumed for security reasons."; - throw new UnsupportedJwtException(msg, e); - } + if (Strings.hasText(base64UrlDigest) && signingKeyResolver != null) { // TODO: remove for 1.0 + // A SigningKeyResolver has been configured, and due to it's API, we have to verify the signature after + // parsing the body. This can be a security risk, so it needs to be removed before 1.0 + verifySignature(tokenized, ((JwsHeader) header), alg, this.signingKeyResolver, claims, payload); } final boolean allowSkew = this.allowedClockSkewMillis > 0; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index 905f08241..f92b9f686 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -18,14 +18,11 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.Clock; import io.jsonwebtoken.CompressionCodecResolver; -import io.jsonwebtoken.Header; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.JwtParserBuilder; import io.jsonwebtoken.Locator; import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; -import io.jsonwebtoken.impl.lang.Function; -import io.jsonwebtoken.impl.lang.LocatorFunction; import io.jsonwebtoken.impl.lang.Services; import io.jsonwebtoken.impl.security.ConstantKeyLocator; import io.jsonwebtoken.io.Decoder; @@ -65,7 +62,7 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { private boolean enableUnsecuredJws = false; - private Function, Key> keyLocator; + private Locator keyLocator; @SuppressWarnings("deprecation") //TODO: remove for 1.0 private SigningKeyResolver signingKeyResolver = null; @@ -237,8 +234,7 @@ public JwtParserBuilder setSigningKeyResolver(SigningKeyResolver signingKeyResol @Override public JwtParserBuilder setKeyLocator(Locator keyLocator) { - Assert.notNull(keyLocator, "Key locator cannot be null."); - this.keyLocator = new LocatorFunction<>(keyLocator); + this.keyLocator = Assert.notNull(keyLocator, "Key locator cannot be null."); return this; } @@ -261,7 +257,7 @@ public JwtParser build() { } if (this.keyLocator != null && this.decryptionKey != null) { - String msg = "Both 'keyLocator' and 'decryptionKey' cannot be configured. Prefer 'keyLocator' if possible."; + String msg = "Both 'keyLocator' and 'decryptWith' key cannot be configured. Prefer 'keyLocator' if possible."; throw new IllegalStateException(msg); } @@ -269,14 +265,9 @@ public JwtParser build() { this.keyLocator = new ConstantKeyLocator(this.signatureVerificationKey, this.decryptionKey); } - if (this.signingKeyResolver == null) { - this.signingKeyResolver = new ConstantKeyLocator(this.signatureVerificationKey, this.decryptionKey); - } - // Invariants. If these are ever violated, it's an error in this class implementation // (we default to non-null instances, and the setters should never allow null): Assert.stateNotNull(this.keyLocator, "Key locator should never be null."); - Assert.stateNotNull(this.signingKeyResolver, "SigningKeyResolver should never be null."); Assert.stateNotNull(this.compressionCodecResolver, "CompressionCodecResolver should never be null."); return new ImmutableJwtParser(new DefaultJwtParser( diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/LocatingKeyResolver.java b/impl/src/main/java/io/jsonwebtoken/impl/security/LocatingKeyResolver.java new file mode 100644 index 000000000..b03fb2e50 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/LocatingKeyResolver.java @@ -0,0 +1,29 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.Locator; +import io.jsonwebtoken.SigningKeyResolver; +import io.jsonwebtoken.lang.Assert; + +import java.security.Key; + +@SuppressWarnings("deprecation") // TODO: delete this class for 1.0 +public class LocatingKeyResolver implements SigningKeyResolver { + + private final Locator locator; + + public LocatingKeyResolver(Locator locator) { + this.locator = Assert.notNull(locator, "Locator cannot be null."); + } + + @Override + public Key resolveSigningKey(JwsHeader header, Claims claims) { + return this.locator.locate(header); + } + + @Override + public Key resolveSigningKey(JwsHeader header, String plaintext) { + return this.locator.locate(header); + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index d182b1707..36f8a0acc 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -19,10 +19,7 @@ import io.jsonwebtoken.impl.* import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec import io.jsonwebtoken.impl.lang.Services -import io.jsonwebtoken.impl.security.ConstantKeyLocator -import io.jsonwebtoken.impl.security.DirectKeyAlgorithm -import io.jsonwebtoken.impl.security.Pbes2HsAkwAlgorithm -import io.jsonwebtoken.impl.security.TestKeys +import io.jsonwebtoken.impl.security.* import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer @@ -161,12 +158,15 @@ class JwtsTest { */ @Test void testParseMalformedClaims() { + def key = TestKeys.HS256 def h = base64Url('{"alg":"HS256"}') def c = base64Url('{"sub":"joe","exp":"-42-"}') - def sig = 'IA==' + def payload = ("$h.$c" as String).getBytes(StandardCharsets.UTF_8) + def result = SignatureAlgorithms.HS256.sign(new DefaultSignatureRequest(null, null, payload, key)) + def sig = Encoders.BASE64URL.encode(result) def compact = "$h.$c.$sig" as String try { - Jwts.parserBuilder().build().parseClaimsJws(compact) + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) fail() } catch (MalformedJwtException e) { String expected = 'Invalid claims: Invalid JWT Claim \'exp\' (Expiration Time) value: -42-. Cause: ' + diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy index f98cb5737..31e4155e8 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy @@ -70,7 +70,7 @@ class DefaultJwtParserBuilderTest { .build() fail() } catch (IllegalStateException e) { - String msg = "Both 'keyLocator' and 'decryptionKey' cannot be configured. Prefer 'keyLocator' if possible." + String msg = "Both 'keyLocator' and 'decryptWith' key cannot be configured. Prefer 'keyLocator' if possible." assertEquals msg, e.getMessage() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy index d4569135c..62b3d8496 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy @@ -93,7 +93,6 @@ class JwkSerializationTest { // Ensure no Groovy or Java toString prints out secret values: assertEquals '[kid:id, kty:oct, k:]', "$jwk" as String // groovy gstring - println jwk.toString() assertEquals '{kid=id, kty=oct, k=}', jwk.toString() // java toString //but serialization prints the real value: diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/LocatingKeyResolverTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/LocatingKeyResolverTest.groovy new file mode 100644 index 000000000..a501158d9 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/LocatingKeyResolverTest.groovy @@ -0,0 +1,33 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.impl.DefaultClaims +import io.jsonwebtoken.impl.DefaultJwsHeader +import org.junit.Test + +import static org.junit.Assert.assertSame + +class LocatingKeyResolverTest { + + @Test(expected = IllegalArgumentException) + void testNullConstructor() { + new LocatingKeyResolver(null) + } + + @Test + void testResolveSigningKeyClaims() { + def key = TestKeys.HS256 + def locator = new ConstantKeyLocator(key, null) + def header = new DefaultJwsHeader() + def claims = new DefaultClaims() + assertSame key, new LocatingKeyResolver(locator).resolveSigningKey(header, claims) + } + + @Test + void testResolveSigningKeyPayload() { + def key = TestKeys.HS256 + def locator = new ConstantKeyLocator(key, null) + def header = new DefaultJwsHeader() + def payload = 'hello world' + assertSame key, new LocatingKeyResolver(locator).resolveSigningKey(header, payload) + } +} From e471bd0636a3f03e2542a814e0ec5665addb4ef3 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Thu, 26 May 2022 17:50:17 -0700 Subject: [PATCH 58/75] Removed EllipticCurveSignatureAlgorithm and RsaSignatureAlgorithm concepts due to some PKCS11 and HSM security providers that cannot provide keys that implement the ECKey or RSAKey interfaces. --- .../AsymmetricKeySignatureAlgorithm.java | 6 +- .../EllipticCurveSignatureAlgorithm.java | 32 --------- .../java/io/jsonwebtoken/security/Keys.java | 2 +- .../security/RsaSignatureAlgorithm.java | 32 --------- .../security/SignatureAlgorithms.java | 20 +++--- .../security/AbstractSignatureAlgorithm.java | 20 +++--- ...efaultEllipticCurveSignatureAlgorithm.java | 66 +++++++++++-------- .../DefaultRsaSignatureAlgorithm.java | 51 ++++++-------- .../security/SignatureAlgorithmsBridge.java | 18 ++--- ...EllipticCurveSignatureAlgorithmTest.groovy | 52 ++++++--------- .../DefaultRsaSignatureAlgorithmTest.groovy | 26 +++++--- .../impl/security/JwksTest.groovy | 6 +- 12 files changed, 128 insertions(+), 203 deletions(-) delete mode 100644 api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java index e9266d0b3..058e779d3 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java @@ -35,10 +35,8 @@ *

    The resulting {@code pair} is guaranteed to have the correct algorithm parameters and length/strength necessary * for that exact {@code anAsymmetricKeySignatureAlgorithm} instance.

    * - * @param The type of {@link PrivateKey} used to create signatures - * @param the type of {@link PublicKey} used to verify signatures * @since JJWT_RELEASE_VERSION */ -public interface AsymmetricKeySignatureAlgorithm - extends SignatureAlgorithm, KeyPairBuilderSupplier { +public interface AsymmetricKeySignatureAlgorithm + extends SignatureAlgorithm, KeyPairBuilderSupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java deleted file mode 100644 index 69034ed08..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/EllipticCurveSignatureAlgorithm.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2021 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.security; - -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.interfaces.ECKey; - -/** - * An {@link AsymmetricKeySignatureAlgorithm} that uses Elliptic Curve private keys to create signatures, and - * Elliptic Curve public keys to verify signatures. - * - * @param The type of Elliptic Curve private key used to create signatures - * @param The type of Elliptic Curve public key used to verify signatures - * @since JJWT_RELEASE_VERSION - */ -public interface EllipticCurveSignatureAlgorithm - extends AsymmetricKeySignatureAlgorithm { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index 74fa0d8a3..20c7852f5 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -240,7 +240,7 @@ public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws String msg = "The " + alg.name() + " algorithm does not support Key Pairs."; throw new IllegalArgumentException(msg); } - AsymmetricKeySignatureAlgorithm asalg = ((AsymmetricKeySignatureAlgorithm) salg); + AsymmetricKeySignatureAlgorithm asalg = ((AsymmetricKeySignatureAlgorithm) salg); return asalg.keyPairBuilder().build().toJavaKeyPair(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java deleted file mode 100644 index 3c6ea64e8..000000000 --- a/api/src/main/java/io/jsonwebtoken/security/RsaSignatureAlgorithm.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2021 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.security; - -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.interfaces.RSAKey; - -/** - * An {@link AsymmetricKeySignatureAlgorithm} that uses RSA private keys to create signatures, and - * RSA public keys to verify signatures. - * - * @param The type of RSA private key used to create signatures - * @param The type of RSA public key used to verify signatures - * @since JJWT_RELEASE_VERSION - */ -public interface RsaSignatureAlgorithm - extends AsymmetricKeySignatureAlgorithm { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java index 014e0e087..a88c1685a 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithms.java @@ -27,7 +27,7 @@ * * @since JJWT_RELEASE_VERSION */ -@SuppressWarnings({"rawtypes", "JavadocLinkAsPlainText"}) +@SuppressWarnings({"JavadocLinkAsPlainText"}) public final class SignatureAlgorithms { // Prevent instantiation @@ -118,21 +118,21 @@ static T forId0(String id) { * RFC 7518, Section 3.3. This algorithm * requires a 2048-bit key. */ - public static final RsaSignatureAlgorithm RS256 = forId0("RS256"); + public static final AsymmetricKeySignatureAlgorithm RS256 = forId0("RS256"); /** * {@code RSASSA-PKCS1-v1_5 using SHA-384} signature algorithm as defined by * RFC 7518, Section 3.3. This algorithm * requires a 2048-bit key, but the JJWT team recommends a 3072-bit key. */ - public static final RsaSignatureAlgorithm RS384 = forId0("RS384"); + public static final AsymmetricKeySignatureAlgorithm RS384 = forId0("RS384"); /** * {@code RSASSA-PKCS1-v1_5 using SHA-512} signature algorithm as defined by * RFC 7518, Section 3.3. This algorithm * requires a 2048-bit key, but the JJWT team recommends a 4096-bit key. */ - public static final RsaSignatureAlgorithm RS512 = forId0("RS512"); + public static final AsymmetricKeySignatureAlgorithm RS512 = forId0("RS512"); /** * {@code RSASSA-PSS using SHA-256 and MGF1 with SHA-256} signature algorithm as defined by @@ -143,7 +143,7 @@ static T forId0(String id) { * classpath. If on Java 10 or earlier, BouncyCastle will be used automatically if found in the runtime * classpath.

    */ - public static final RsaSignatureAlgorithm PS256 = forId0("PS256"); + public static final AsymmetricKeySignatureAlgorithm PS256 = forId0("PS256"); /** * {@code RSASSA-PSS using SHA-384 and MGF1 with SHA-384} signature algorithm as defined by @@ -154,7 +154,7 @@ static T forId0(String id) { * classpath. If on Java 10 or earlier, BouncyCastle will be used automatically if found in the runtime * classpath.

    */ - public static final RsaSignatureAlgorithm PS384 = forId0("PS384"); + public static final AsymmetricKeySignatureAlgorithm PS384 = forId0("PS384"); /** * {@code RSASSA-PSS using SHA-512 and MGF1 with SHA-512} signature algorithm as defined by @@ -165,26 +165,26 @@ static T forId0(String id) { * classpath. If on Java 10 or earlier, BouncyCastle will be used automatically if found in the runtime * classpath.

    */ - public static final RsaSignatureAlgorithm PS512 = forId0("PS512"); + public static final AsymmetricKeySignatureAlgorithm PS512 = forId0("PS512"); /** * {@code ECDSA using P-256 and SHA-256} signature algorithm as defined by * RFC 7518, Section 3.4. This algorithm * requires a 256-bit key. */ - public static final EllipticCurveSignatureAlgorithm ES256 = forId0("ES256"); + public static final AsymmetricKeySignatureAlgorithm ES256 = forId0("ES256"); /** * {@code ECDSA using P-384 and SHA-384} signature algorithm as defined by * RFC 7518, Section 3.4. This algorithm * requires a 384-bit key. */ - public static final EllipticCurveSignatureAlgorithm ES384 = forId0("ES384"); + public static final AsymmetricKeySignatureAlgorithm ES384 = forId0("ES384"); /** * {@code ECDSA using P-521 and SHA-512} signature algorithm as defined by * RFC 7518, Section 3.4. This algorithm * requires a 521-bit key. */ - public static final EllipticCurveSignatureAlgorithm ES512 = forId0("ES512"); + public static final AsymmetricKeySignatureAlgorithm ES512 = forId0("ES512"); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java index 5ba64203c..f972c8f34 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java @@ -11,7 +11,7 @@ import java.security.Key; import java.security.MessageDigest; -abstract class AbstractSignatureAlgorithm extends CryptoAlgorithm implements SignatureAlgorithm { +abstract class AbstractSignatureAlgorithm extends CryptoAlgorithm implements SignatureAlgorithm { AbstractSignatureAlgorithm(String id, String jcaName) { super(id, jcaName); @@ -24,8 +24,8 @@ protected static String keyType(boolean signing) { protected abstract void validateKey(Key key, boolean signing); @Override - public byte[] sign(SignatureRequest request) throws SecurityException { - final SK key = Assert.notNull(request.getKey(), "Request key cannot be null."); + public byte[] sign(SignatureRequest request) throws SecurityException { + final S key = Assert.notNull(request.getKey(), "Request key cannot be null."); Assert.notEmpty(request.getContent(), "Request content cannot be null or empty."); try { validateKey(key, true); @@ -34,16 +34,16 @@ public byte[] sign(SignatureRequest request) throws SecurityException { throw e; //propagate } catch (Exception e) { String msg = "Unable to compute " + getId() + " signature with JCA algorithm '" + getJcaName() + "' " + - "using key {" + key + "}: " + e.getMessage(); + "using key {" + key + "}: " + e.getMessage(); throw new SignatureException(msg, e); } } - protected abstract byte[] doSign(SignatureRequest request) throws Exception; + protected abstract byte[] doSign(SignatureRequest request) throws Exception; @Override - public boolean verify(VerifySignatureRequest request) throws SecurityException { - final VK key = Assert.notNull(request.getKey(), "Request key cannot be null."); + public boolean verify(VerifySignatureRequest request) throws SecurityException { + final V key = Assert.notNull(request.getKey(), "Request key cannot be null."); Assert.notEmpty(request.getContent(), "Request content cannot be null or empty."); Assert.notEmpty(request.getDigest(), "Request signature byte array cannot be null or empty."); try { @@ -53,15 +53,15 @@ public boolean verify(VerifySignatureRequest request) throws SecurityExcepti throw e; //propagate } catch (Exception e) { String msg = "Unable to verify " + getId() + " signature with JCA algorithm '" + getJcaName() + "' " + - "using key {" + key + "}: " + e.getMessage(); + "using key {" + key + "}: " + e.getMessage(); throw new SignatureException(msg, e); } } - protected boolean doVerify(VerifySignatureRequest request) throws Exception { + protected boolean doVerify(VerifySignatureRequest request) throws Exception { byte[] providedSignature = request.getDigest(); Assert.notEmpty(providedSignature, "Request signature byte array cannot be null or empty."); - @SuppressWarnings("unchecked") byte[] computedSignature = sign((SignatureRequest)request); + @SuppressWarnings("unchecked") byte[] computedSignature = sign((SignatureRequest) request); return MessageDigest.isEqual(providedSignature, computedSignature); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java index 374893bdb..8d4711e3c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java @@ -4,7 +4,7 @@ import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.EllipticCurveSignatureAlgorithm; +import io.jsonwebtoken.security.AsymmetricKeySignatureAlgorithm; import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.KeyPairBuilder; import io.jsonwebtoken.security.SignatureException; @@ -22,8 +22,8 @@ import java.util.Arrays; // @since JJWT_RELEASE_VERSION -public class DefaultEllipticCurveSignatureAlgorithm - extends AbstractSignatureAlgorithm implements EllipticCurveSignatureAlgorithm { +public class DefaultEllipticCurveSignatureAlgorithm + extends AbstractSignatureAlgorithm implements AsymmetricKeySignatureAlgorithm { private static final String REQD_ORDER_BIT_LENGTH_MSG = "orderBitLength must equal 256, 384, or 512."; private static final String KEY_TYPE_MSG_PATTERN = @@ -90,8 +90,8 @@ public DefaultEllipticCurveSignatureAlgorithm(int orderBitLength) { } @Override - public KeyPairBuilder keyPairBuilder() { - return new DefaultKeyPairBuilder("EC", this.KEY_PAIR_GEN_PARAMS) + public KeyPairBuilder keyPairBuilder() { + return new DefaultKeyPairBuilder<>("EC", this.KEY_PAIR_GEN_PARAMS) .setProvider(getProvider()).setRandom(Randoms.secureRandom()); } @@ -107,29 +107,31 @@ private static void assertKey(Key key, Class type, boolean signing) { protected void validateKey(Key key, boolean signing) { // https://github.com/jwtk/jjwt/issues/68: - // Instead of checking for an instance of ECPrivateKey, check for ECKey and PrivateKey separately: - assertKey(key, ECKey.class, signing); Class requiredType = signing ? PrivateKey.class : PublicKey.class; assertKey(key, requiredType, signing); - final String name = getId(); - ECKey ecKey = (ECKey) key; - BigInteger order = ecKey.getParams().getOrder(); - int orderBitLength = order.bitLength(); - int sigFieldByteLength = fieldByteLength(orderBitLength); - int concatByteLength = sigFieldByteLength * 2; - - if (concatByteLength != this.signatureByteLength) { - String msg = "The provided Elliptic Curve " + keyType(signing) + " key's size (aka Order bit length) is " + - Bytes.bitsMsg(orderBitLength) + ", but the '" + name + "' algorithm requires EC Keys with " + - Bytes.bitsMsg(this.orderBitLength) + " per " + - "[RFC 7518, Section 3.4](https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)."; - throw new InvalidKeyException(msg); + // Some PKCS11 providers and HSMs won't expose the ECKey interface, so we have to check to see if we can cast + // If so, we can provide the additional safety checks: + if (key instanceof ECKey) { + final String name = getId(); + ECKey ecKey = (ECKey) key; + BigInteger order = ecKey.getParams().getOrder(); + int orderBitLength = order.bitLength(); + int sigFieldByteLength = fieldByteLength(orderBitLength); + int concatByteLength = sigFieldByteLength * 2; + + if (concatByteLength != this.signatureByteLength) { + String msg = "The provided Elliptic Curve " + keyType(signing) + " key's size (aka Order bit length) is " + + Bytes.bitsMsg(orderBitLength) + ", but the '" + name + "' algorithm requires EC Keys with " + + Bytes.bitsMsg(this.orderBitLength) + " per " + + "[RFC 7518, Section 3.4](https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)."; + throw new InvalidKeyException(msg); + } } } @Override - protected byte[] doSign(final SignatureRequest request) { + protected byte[] doSign(final SignatureRequest request) { return execute(request, Signature.class, new CheckedFunction() { @Override public byte[] apply(Signature sig) throws Exception { @@ -141,10 +143,21 @@ public byte[] apply(Signature sig) throws Exception { }); } + protected boolean isValidRAndS(PublicKey key, byte[] concatSignature) { + if (key instanceof ECKey) { //Some PKCS11 providers and HSMs won't expose the ECKey interface, so we have to check first + ECKey ecKey = (ECKey) key; + BigInteger order = ecKey.getParams().getOrder(); + BigInteger r = new BigInteger(1, Arrays.copyOfRange(concatSignature, 0, sigFieldByteLength)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(concatSignature, sigFieldByteLength, concatSignature.length)); + return r.signum() >= 1 && s.signum() >= 1 && r.compareTo(order) < 0 && s.compareTo(order) < 0; + } + return true; + } + @Override - protected boolean doVerify(final VerifySignatureRequest request) { + protected boolean doVerify(final VerifySignatureRequest request) { - final ECKey key = request.getKey(); + final PublicKey key = request.getKey(); return execute(request, Signature.class, new CheckedFunction() { @Override @@ -172,10 +185,7 @@ public Boolean apply(Signature sig) { } } else { //guard for JVM security bug CVE-2022-21449: - BigInteger order = key.getParams().getOrder(); - BigInteger r = new BigInteger(1, Arrays.copyOfRange(concatSignature, 0, sigFieldByteLength)); - BigInteger s = new BigInteger(1, Arrays.copyOfRange(concatSignature, sigFieldByteLength, concatSignature.length)); - if (r.signum() < 1 || s.signum() < 1 || r.compareTo(order) >= 0 || s.compareTo(order) >= 0) { + if (!isValidRAndS(key, concatSignature)) { return false; } @@ -185,7 +195,7 @@ public Boolean apply(Signature sig) { derSignature = transcodeConcatToDER(concatSignature); } - sig.initVerify(request.getKey()); + sig.initVerify(key); sig.update(request.getContent()); return sig.verify(derSignature); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java index ccbf13124..cc8f5829a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java @@ -3,9 +3,9 @@ import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.impl.lang.CheckedSupplier; import io.jsonwebtoken.impl.lang.Conditions; +import io.jsonwebtoken.security.AsymmetricKeySignatureAlgorithm; import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.KeyPairBuilder; -import io.jsonwebtoken.security.RsaSignatureAlgorithm; import io.jsonwebtoken.security.SignatureRequest; import io.jsonwebtoken.security.VerifySignatureRequest; import io.jsonwebtoken.security.WeakKeyException; @@ -20,8 +20,8 @@ import java.security.spec.PSSParameterSpec; // @since JJWT_RELEASE_VERSION -public class DefaultRsaSignatureAlgorithm - extends AbstractSignatureAlgorithm implements RsaSignatureAlgorithm { +public class DefaultRsaSignatureAlgorithm extends AbstractSignatureAlgorithm + implements AsymmetricKeySignatureAlgorithm { private static final String PSS_JCA_NAME = "RSASSA-PSS"; private static final int MIN_KEY_BIT_LENGTH = 2048; @@ -62,48 +62,35 @@ public Signature get() throws Exception { } @Override - public KeyPairBuilder keyPairBuilder() { - return new DefaultKeyPairBuilder("RSA", this.preferredKeyBitLength) - .setProvider(getProvider()).setRandom(Randoms.secureRandom()); + public KeyPairBuilder keyPairBuilder() { + return new DefaultKeyPairBuilder<>("RSA", this.preferredKeyBitLength).setProvider(getProvider()).setRandom(Randoms.secureRandom()); } @Override protected void validateKey(Key key, boolean signing) { - if (!(key instanceof RSAKey)) { - String msg = "RSA " + keyType(signing) + " keys must be an RSAKey. The specified key is of type: " + - key.getClass().getName(); - throw new InvalidKeyException(msg); - } - // https://github.com/jwtk/jjwt/issues/68 - // Instead of checking for an instance of RSAPrivateKey, check for PrivateKey (RSAKey assertion is above): if (signing && !(key instanceof PrivateKey)) { - String msg = "Asymmetric key signatures must be created with PrivateKeys. The specified key is of type: " + - key.getClass().getName(); + String msg = "Asymmetric key signatures must be created with PrivateKeys. The specified key is of type: " + key.getClass().getName(); throw new InvalidKeyException(msg); } - RSAKey rsaKey = (RSAKey) key; - int size = rsaKey.getModulus().bitLength(); - if (size < MIN_KEY_BIT_LENGTH) { - - String id = getId(); - - String section = id.startsWith("PS") ? "3.5" : "3.3"; - - String msg = "The " + keyType(signing) + " key's size is " + size + " bits which is not secure " + - "enough for the " + id + " algorithm. The JWT JWA Specification (RFC 7518, Section " + - section + ") states that RSA keys MUST have a size >= " + - MIN_KEY_BIT_LENGTH + " bits. Consider using the SignatureAlgorithms." + id + ".generateKeyPair() " + - "method to create a key pair guaranteed to be secure enough for " + id + ". See " + - "https://tools.ietf.org/html/rfc7518#section-" + section + " for more information."; - throw new WeakKeyException(msg); + // Some PKCS11 providers and HSMs won't expose the RSAKey interface, so we have to check to see if we can cast + // If so, we can provide additional safety checks: + if (key instanceof RSAKey) { + RSAKey rsaKey = (RSAKey) key; + int size = rsaKey.getModulus().bitLength(); + if (size < MIN_KEY_BIT_LENGTH) { + String id = getId(); + String section = id.startsWith("PS") ? "3.5" : "3.3"; + String msg = "The " + keyType(signing) + " key's size is " + size + " bits which is not secure " + "enough for the " + id + " algorithm. The JWT JWA Specification (RFC 7518, Section " + section + ") states that RSA keys MUST have a size >= " + MIN_KEY_BIT_LENGTH + " bits. Consider using the SignatureAlgorithms." + id + ".generateKeyPair() " + "method to create a key pair guaranteed to be secure enough for " + id + ". See " + "https://tools.ietf.org/html/rfc7518#section-" + section + " for more information."; + throw new WeakKeyException(msg); + } } } @Override - protected byte[] doSign(final SignatureRequest request) { + protected byte[] doSign(final SignatureRequest request) { return execute(request, Signature.class, new CheckedFunction() { @Override public byte[] apply(Signature sig) throws Exception { @@ -118,7 +105,7 @@ public byte[] apply(Signature sig) throws Exception { } @Override - protected boolean doVerify(final VerifySignatureRequest request) throws Exception { + protected boolean doVerify(final VerifySignatureRequest request) throws Exception { final Key key = request.getKey(); if (key instanceof PrivateKey) { //legacy support only TODO: remove for 1.0 return super.doVerify(request); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java index 5837b67e6..24451cd03 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SignatureAlgorithmsBridge.java @@ -25,15 +25,15 @@ private SignatureAlgorithmsBridge() { new MacSignatureAlgorithm(256), new MacSignatureAlgorithm(384), new MacSignatureAlgorithm(512), - new DefaultRsaSignatureAlgorithm<>(256, 2048), - new DefaultRsaSignatureAlgorithm<>(384, 3072), - new DefaultRsaSignatureAlgorithm<>(512, 4096), - new DefaultRsaSignatureAlgorithm<>(256, 2048, 256), - new DefaultRsaSignatureAlgorithm<>(384, 3072, 384), - new DefaultRsaSignatureAlgorithm<>(512, 4096, 512), - new DefaultEllipticCurveSignatureAlgorithm<>(256), - new DefaultEllipticCurveSignatureAlgorithm<>(384), - new DefaultEllipticCurveSignatureAlgorithm<>(521) + new DefaultRsaSignatureAlgorithm(256, 2048), + new DefaultRsaSignatureAlgorithm(384, 3072), + new DefaultRsaSignatureAlgorithm(512, 4096), + new DefaultRsaSignatureAlgorithm(256, 2048, 256), + new DefaultRsaSignatureAlgorithm(384, 3072, 384), + new DefaultRsaSignatureAlgorithm(512, 4096, 512), + new DefaultEllipticCurveSignatureAlgorithm(256), + new DefaultEllipticCurveSignatureAlgorithm(384), + new DefaultEllipticCurveSignatureAlgorithm(521) )); } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy index bf7c43d7d..6827f5b10 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy @@ -3,13 +3,11 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.JwtException import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.io.Decoders -import io.jsonwebtoken.security.EllipticCurveSignatureAlgorithm import io.jsonwebtoken.security.InvalidKeyException import io.jsonwebtoken.security.SignatureAlgorithms import io.jsonwebtoken.security.SignatureException import org.junit.Test -import javax.crypto.spec.SecretKeySpec import java.nio.charset.StandardCharsets import java.security.* import java.security.interfaces.ECPrivateKey @@ -19,12 +17,13 @@ import java.security.spec.ECPoint import java.security.spec.EllipticCurve import java.security.spec.X509EncodedKeySpec +import static org.easymock.EasyMock.* import static org.junit.Assert.* class DefaultEllipticCurveSignatureAlgorithmTest { - static Collection algs() { - return SignatureAlgorithms.values().findAll({ it instanceof EllipticCurveSignatureAlgorithm }) + static Collection algs() { + return SignatureAlgorithms.values().findAll({ it instanceof DefaultEllipticCurveSignatureAlgorithm }) } @Test @@ -38,20 +37,25 @@ class DefaultEllipticCurveSignatureAlgorithmTest { } @Test - void testSignWithoutEcKey() { - def key = new SecretKeySpec(new byte[1], 'foo') - def data = "foo".getBytes(StandardCharsets.UTF_8) - def req = new DefaultSignatureRequest(null, null, data, key) + void testValidateKeyWithoutEcKey() { + def key = createMock(PublicKey) + replay key algs().each { - try { - it.sign(req) - } catch (InvalidKeyException expected) { - String msg = "Elliptic Curve signing keys must be ECKeys " + - "(implement java.security.interfaces.ECKey). Provided key type: " + - "javax.crypto.spec.SecretKeySpec." - assertEquals msg, expected.getMessage() - } + it.validateKey(key, false) + //no exception - can't check for ECKey fields (e.g. PKCS11 or HSM key) + } + verify key + } + + @Test + void testIsValidRAndSWithoutEcKey() { + def key = createMock(PublicKey) + replay key + algs().each { + it.isValidRAndS(key, Bytes.EMPTY) + //no exception - can't check for ECKey fields (e.g. PKCS11 or HSM key) } + verify key } @Test @@ -102,22 +106,6 @@ class DefaultEllipticCurveSignatureAlgorithmTest { } } - @Test - void testVerifyWithoutEcKey() { - def key = new SecretKeySpec(new byte[1], 'foo') - def request = new DefaultVerifySignatureRequest(null, null, new byte[1], key, new byte[1]) - algs().each { - try { - it.verify(request) - } catch (InvalidKeyException e) { - String msg = "Elliptic Curve verification keys must be ECKeys " + - "(implement java.security.interfaces.ECKey). Provided key type: " + - "javax.crypto.spec.SecretKeySpec." - assertEquals msg, e.getMessage() - } - } - } - @Test void testVerifyWithPrivateKey() { byte[] data = 'foo'.getBytes(StandardCharsets.UTF_8) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy index 338c4c3e9..428cfa284 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithmTest.groovy @@ -1,23 +1,28 @@ package io.jsonwebtoken.impl.security + import io.jsonwebtoken.security.InvalidKeyException import io.jsonwebtoken.security.SignatureAlgorithms import io.jsonwebtoken.security.WeakKeyException import org.junit.Test -import javax.crypto.spec.SecretKeySpec import java.security.KeyPairGenerator +import java.security.PublicKey import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey -import static org.easymock.EasyMock.createMock +import static org.easymock.EasyMock.* import static org.junit.Assert.* class DefaultRsaSignatureAlgorithmTest { + static Collection algs() { + return SignatureAlgorithms.values().findAll({ it.id.startsWith("RS") || it.id.startsWith("PS") }) + } + @Test void testKeyPairBuilder() { - SignatureAlgorithms.values().findAll({it.id.startsWith("RS") || it.id.startsWith("PS")}).each { + algs().each { def pair = it.keyPairBuilder().build() assertNotNull pair.public assertTrue pair.public instanceof RSAPublicKey @@ -33,13 +38,14 @@ class DefaultRsaSignatureAlgorithmTest { } @Test - void testValidateKeyRsaKey() { - def request = new DefaultSignatureRequest(null, null, new byte[1], new SecretKeySpec(new byte[1], 'foo')) - try { - SignatureAlgorithms.RS256.sign(request) - } catch (InvalidKeyException e) { - assertTrue e.getMessage().contains("must be an RSAKey") + void testValidateKeyWithoutRsaKey() { + def key = createMock(PublicKey) + replay key + algs().each { + it.validateKey(key, false) + //no exception - can't check for RSAKey fields (e.g. PKCS11 or HSM key) } + verify key } @Test @@ -60,7 +66,7 @@ class DefaultRsaSignatureAlgorithmTest { def pair = gen.generateKeyPair() def request = new DefaultSignatureRequest(null, null, new byte[1], pair.getPrivate()) - SignatureAlgorithms.values().findAll({it.id.startsWith('RS') || it.id.startsWith('PS')}).each { + SignatureAlgorithms.values().findAll({ it.id.startsWith('RS') || it.id.startsWith('PS') }).each { try { it.sign(request) } catch (WeakKeyException expected) { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index cdaa9f6f3..cf7820f9e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -42,9 +42,9 @@ class JwksTest { private static final KeyPair EC_PAIR = SignatureAlgorithms.ES256.keyPairBuilder().build() private static String srandom() { - byte[] random = new byte[16]; + byte[] random = new byte[16] Randoms.secureRandom().nextBytes(random) - return Encoders.BASE64URL.encode(random); + return Encoders.BASE64URL.encode(random) } static void testProperty(String name, String id, def val, def expectedFieldValue = val) { @@ -295,7 +295,7 @@ class JwksTest { void testInvalidCurvePoint() { def algs = [SignatureAlgorithms.ES256, SignatureAlgorithms.ES384, SignatureAlgorithms.ES512] - for (EllipticCurveSignatureAlgorithm alg : algs) { + for (AsymmetricKeySignatureAlgorithm alg : algs) { def pair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair ECPublicKey pubKey = pair.getPublic() as ECPublicKey From 1fb6cb89814291fb402b7749328844c9e84ad80a Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Thu, 26 May 2022 18:15:52 -0700 Subject: [PATCH 59/75] Removed reliance on io.jsonwebtoken.security.KeyPair now that KeyPairBuilder implementations cannot guarantee RSAKey or ECKey types --- README.md | 6 ------ .../AsymmetricKeySignatureAlgorithm.java | 1 + .../jsonwebtoken/security/KeyPairBuilder.java | 8 ++----- .../security/KeyPairBuilderSupplier.java | 3 ++- .../java/io/jsonwebtoken/security/Keys.java | 14 ++++++------- ...efaultEllipticCurveSignatureAlgorithm.java | 7 ++++--- .../impl/security/DefaultKeyPairBuilder.java | 21 ++++++------------- .../DefaultRsaSignatureAlgorithm.java | 6 ++++-- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 2 +- .../AbstractSignatureAlgorithmTest.groovy | 8 +++---- ...EllipticCurveSignatureAlgorithmTest.groovy | 18 ++++++++-------- .../impl/security/JwksTest.groovy | 10 ++++----- .../impl/security/KeyPairsTest.groovy | 2 +- .../io/jsonwebtoken/security/KeysTest.groovy | 4 ++-- 14 files changed, 48 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 117c60413..263908633 100644 --- a/README.md +++ b/README.md @@ -667,12 +667,6 @@ algorithms, use an algorithm's respective `keyPairBuilder()` method: KeyPair keyPair = SignatureAlgorithms.RS256.keyPairBuilder().build(); //or RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512 ``` -The `keyPair` instance returned is a `io.jsonwebtoken.security.KeyPair`, which is essentially the same thing -as the JDK's `java.security.KeyPair` class, except it provides generics type-safety, for example, -`KeyPair` or `KeyPair`. If you want to convert this type-safe -instance to the standard JDK type-erased instance, just call `keyPair.toJdkKeyPair()` and you'll get a -`java.security.KeyPair` as expected. - Once you've generated a `KeyPair`, you can use the private key (`keyPair.getPrivate()`) to create a JWS and the public key (`keyPair.getPublic()`) to parse/verify a JWS. diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java index 058e779d3..a8f3cb940 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricKeySignatureAlgorithm.java @@ -15,6 +15,7 @@ */ package io.jsonwebtoken.security; +import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java index e3ca7faa5..4b98e4beb 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java @@ -1,7 +1,6 @@ package io.jsonwebtoken.security; -import java.security.PrivateKey; -import java.security.PublicKey; +import java.security.KeyPair; /** * A {@code KeyPairBuilder} produces new {@link KeyPair}s suitable for use with an associated cryptographic algorithm. @@ -10,11 +9,8 @@ *

    {@code KeyPairBuilder}s are provided by components that implement the {@link KeyPairBuilderSupplier} interface, * ensuring the resulting {@link KeyPair}s are compatible with their associated cryptographic algorithm.

    * - * @param the type of public key found within newly-created {@link KeyPair}s. - * @param the type of private key found within newly-created {@link KeyPair}s. * @see KeyPairBuilderSupplier * @since JJWT_RELEASE_VERSION */ -public interface KeyPairBuilder - extends SecurityBuilder, KeyPairBuilder> { +public interface KeyPairBuilder extends SecurityBuilder { } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java index adc57ee16..3ad484992 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java @@ -1,5 +1,6 @@ package io.jsonwebtoken.security; +import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; @@ -22,5 +23,5 @@ public interface KeyPairBuilderSupplier keyPairBuilder(); + KeyPairBuilder keyPairBuilder(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index 20c7852f5..bfedd5c65 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -65,12 +65,12 @@ public static SecretKey hmacShaKeyFor(byte[] bytes) throws WeakKeyException { } String msg = "The specified key byte array is " + bitLength + " bits which " + - "is not secure enough for any JWT HMAC-SHA algorithm. The JWT " + - "JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " + - "size >= 256 bits (the key size must be greater than or equal to the hash " + - "output size). Consider using the SignatureAlgorithms.HS256.keyBuilder() method (or HS384.keyBuilder() " + - "or HS512.keyBuilder()) to create a key guaranteed to be secure enough for your preferred HMAC-SHA " + - "algorithm. See https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; + "is not secure enough for any JWT HMAC-SHA algorithm. The JWT " + + "JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " + + "size >= 256 bits (the key size must be greater than or equal to the hash " + + "output size). Consider using the SignatureAlgorithms.HS256.keyBuilder() method (or HS384.keyBuilder() " + + "or HS512.keyBuilder()) to create a key guaranteed to be secure enough for your preferred HMAC-SHA " + + "algorithm. See https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; throw new WeakKeyException(msg); } @@ -241,7 +241,7 @@ public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws throw new IllegalArgumentException(msg); } AsymmetricKeySignatureAlgorithm asalg = ((AsymmetricKeySignatureAlgorithm) salg); - return asalg.keyPairBuilder().build().toJavaKeyPair(); + return asalg.keyPairBuilder().build(); } /** diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java index 8d4711e3c..e0cc80609 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java @@ -90,9 +90,10 @@ public DefaultEllipticCurveSignatureAlgorithm(int orderBitLength) { } @Override - public KeyPairBuilder keyPairBuilder() { - return new DefaultKeyPairBuilder<>("EC", this.KEY_PAIR_GEN_PARAMS) - .setProvider(getProvider()).setRandom(Randoms.secureRandom()); + public KeyPairBuilder keyPairBuilder() { + return new DefaultKeyPairBuilder("EC", this.KEY_PAIR_GEN_PARAMS) + .setProvider(getProvider()) + .setRandom(Randoms.secureRandom()); } private static void assertKey(Key key, Class type, boolean signing) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java index 4b4cd1eab..5da3599dc 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java @@ -1,16 +1,14 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.KeyPair; import io.jsonwebtoken.security.KeyPairBuilder; -import java.security.PrivateKey; +import java.security.KeyPair; import java.security.Provider; -import java.security.PublicKey; import java.security.SecureRandom; import java.security.spec.AlgorithmParameterSpec; -public class DefaultKeyPairBuilder implements KeyPairBuilder { +public class DefaultKeyPairBuilder implements KeyPairBuilder { private final String jcaName; private final int bitLength; @@ -30,7 +28,8 @@ public DefaultKeyPairBuilder(String jcaName, AlgorithmParameterSpec params) { this.bitLength = 0; } - protected java.security.KeyPair generateJdkPair() throws io.jsonwebtoken.security.SecurityException { + @Override + public KeyPair build() { JcaTemplate template = new JcaTemplate(this.jcaName, this.provider, this.random); if (this.params != null) { return template.generateKeyPair(this.params); @@ -40,21 +39,13 @@ protected java.security.KeyPair generateJdkPair() throws io.jsonwebtoken.securit } @Override - public KeyPair build() { - java.security.KeyPair pair = generateJdkPair(); - @SuppressWarnings("unchecked") A publicKey = (A) pair.getPublic(); - @SuppressWarnings("unchecked") B privateKey = (B) pair.getPrivate(); - return new DefaultKeyPair<>(publicKey, privateKey); - } - - @Override - public KeyPairBuilder setProvider(Provider provider) { + public KeyPairBuilder setProvider(Provider provider) { this.provider = provider; return this; } @Override - public KeyPairBuilder setRandom(SecureRandom random) { + public KeyPairBuilder setRandom(SecureRandom random) { this.random = random; return this; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java index cc8f5829a..3f4d78d39 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java @@ -62,8 +62,10 @@ public Signature get() throws Exception { } @Override - public KeyPairBuilder keyPairBuilder() { - return new DefaultKeyPairBuilder<>("RSA", this.preferredKeyBitLength).setProvider(getProvider()).setRandom(Randoms.secureRandom()); + public KeyPairBuilder keyPairBuilder() { + return new DefaultKeyPairBuilder("RSA", this.preferredKeyBitLength) + .setProvider(getProvider()) + .setRandom(Randoms.secureRandom()); } @Override diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 36f8a0acc..f6a00eb68 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -710,7 +710,7 @@ class JwtsTest { def withoutSignature = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def invalidEncodedSignature = "_____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ" String jws = withoutSignature + '.' + invalidEncodedSignature - def keypair = SignatureAlgorithms.ES256.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def keypair = SignatureAlgorithms.ES256.keyPairBuilder().build() Jwts.parserBuilder().setSigningKey(keypair.public).build().parseClaimsJws(jws) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy index f63982337..cf47ecfda 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithmTest.groovy @@ -19,15 +19,15 @@ class AbstractSignatureAlgorithmTest { @Test void testSignAndVerifyWithExplicitProvider() { Provider provider = Security.getProvider('BC') - def pair = SignatureAlgorithms.RS256.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def pair = SignatureAlgorithms.RS256.keyPairBuilder().build() byte[] data = 'foo'.getBytes(StandardCharsets.UTF_8) - byte[] signature = SignatureAlgorithms.RS256.sign(new DefaultSignatureRequest(provider, null, data, pair.getPrivate())) + byte[] signature = SignatureAlgorithms.RS256.sign(new DefaultSignatureRequest<>(provider, null, data, pair.getPrivate())) assertTrue SignatureAlgorithms.RS256.verify(new DefaultVerifySignatureRequest(provider, null, data, pair.getPublic(), signature)) } @Test void testSignFailsWithAnExternalException() { - def pair = SignatureAlgorithms.RS256.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def pair = SignatureAlgorithms.RS256.keyPairBuilder().build() def ise = new IllegalStateException('foo') def alg = new TestAbstractSignatureAlgorithm() { @Override @@ -46,7 +46,7 @@ class AbstractSignatureAlgorithmTest { @Test void testVerifyFailsWithExternalException() { - def pair = SignatureAlgorithms.RS256.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def pair = SignatureAlgorithms.RS256.keyPairBuilder().build() def ise = new IllegalStateException('foo') def alg = new TestAbstractSignatureAlgorithm() { @Override diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy index 6827f5b10..0a67857e9 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithmTest.groovy @@ -92,7 +92,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { @Test void testSignWithInvalidKeyFieldLength() { - def keypair = SignatureAlgorithms.ES256.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def keypair = SignatureAlgorithms.ES256.keyPairBuilder().build() def data = "foo".getBytes(StandardCharsets.UTF_8) def req = new DefaultSignatureRequest(null, null, data, keypair.private) try { @@ -110,7 +110,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { void testVerifyWithPrivateKey() { byte[] data = 'foo'.getBytes(StandardCharsets.UTF_8) algs().each { - def pair = it.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def pair = it.keyPairBuilder().build() def key = pair.getPrivate() def signRequest = new DefaultSignatureRequest(null, null, data, key) byte[] signature = it.sign(signRequest) @@ -261,7 +261,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { void verifySwarmTest() { algs().each { alg -> def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def keypair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def keypair = alg.keyPairBuilder().build() assertNotNull keypair assertTrue keypair.getPublic() instanceof ECPublicKey assertTrue keypair.getPrivate() instanceof ECPrivateKey @@ -422,7 +422,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { void legacySignatureCompatDefaultTest() { def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def alg = SignatureAlgorithms.ES512 - def keypair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def keypair = alg.keyPairBuilder().build() def signature = Signature.getInstance(alg.jcaName) def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) signature.initSign(keypair.private) @@ -449,7 +449,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def alg = SignatureAlgorithms.ES512 - def keypair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def keypair = alg.keyPairBuilder().build() def signature = Signature.getInstance(alg.jcaName) def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) signature.initSign(keypair.private) @@ -467,7 +467,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { byte[] forgedSig = new byte[64] def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def alg = SignatureAlgorithms.ES256 - def keypair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def keypair = alg.keyPairBuilder().build() def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) def request = new DefaultVerifySignatureRequest(null, null, data, keypair.public, forgedSig) assertFalse alg.verify(request) @@ -483,7 +483,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def alg = SignatureAlgorithms.ES256 - def keypair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def keypair = alg.keyPairBuilder().build() def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) def request = new DefaultVerifySignatureRequest(null, null, data, keypair.public, sig) assertFalse alg.verify(request) @@ -499,7 +499,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def alg = SignatureAlgorithms.ES256 - def keypair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def keypair = alg.keyPairBuilder().build() def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) def request = new DefaultVerifySignatureRequest(null, null, data, keypair.public, sig) assertFalse alg.verify(request) @@ -510,7 +510,7 @@ class DefaultEllipticCurveSignatureAlgorithmTest { def withoutSignature = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def invalidEncodedSignature = "_____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ" def alg = SignatureAlgorithms.ES256 - def keypair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def keypair = alg.keyPairBuilder().build() def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) def invalidSignature = Decoders.BASE64URL.decode(invalidEncodedSignature) def request = new DefaultVerifySignatureRequest(null, null, data, keypair.public, invalidSignature) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index cf7820f9e..af8ee811c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -39,7 +39,7 @@ import static org.junit.Assert.* class JwksTest { private static final SecretKey SKEY = SignatureAlgorithms.HS256.keyBuilder().build() - private static final KeyPair EC_PAIR = SignatureAlgorithms.ES256.keyPairBuilder().build() + private static final java.security.KeyPair EC_PAIR = SignatureAlgorithms.ES256.keyPairBuilder().build() private static String srandom() { byte[] random = new byte[16] @@ -261,7 +261,7 @@ class JwksTest { for (def alg : algs) { - def pair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def pair = alg.keyPairBuilder().build() PublicKey pub = pair.getPublic() PrivateKey priv = pair.getPrivate() @@ -279,8 +279,8 @@ class JwksTest { // test pair privJwk = pub instanceof ECKey ? - Jwks.builder().setKeyPairEc(pair.toJavaKeyPair()).setPublicKeyUse("sig").build() : - Jwks.builder().setKeyPairRsa(pair.toJavaKeyPair()).setPublicKeyUse("sig").build() + Jwks.builder().setKeyPairEc(pair).setPublicKeyUse("sig").build() : + Jwks.builder().setKeyPairRsa(pair).setPublicKeyUse("sig").build() assertEquals priv, privJwk.toKey() privPubJwk = privJwk.toPublicJwk() assertEquals pubJwk, privPubJwk @@ -297,7 +297,7 @@ class JwksTest { for (AsymmetricKeySignatureAlgorithm alg : algs) { - def pair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def pair = alg.keyPairBuilder().build() ECPublicKey pubKey = pair.getPublic() as ECPublicKey EcPublicJwk jwk = Jwks.builder().setKey(pubKey).build() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy index cf08ca8e1..5be252e2a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy @@ -47,7 +47,7 @@ class KeyPairsTest { @Test void testGetKeyECMismatch() { - KeyPair pair = SignatureAlgorithms.RS256.keyPairBuilder().build().toJavaKeyPair() + KeyPair pair = SignatureAlgorithms.RS256.keyPairBuilder().build() Class clazz = ECPublicKey try { KeyPairs.getKey(pair, clazz) diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy index 555d40c14..b1d960ef1 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy @@ -208,7 +208,7 @@ class KeysTest { if (alg instanceof DefaultRsaSignatureAlgorithm) { - def pair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def pair = alg.keyPairBuilder().build() assertNotNull pair PublicKey pub = pair.getPublic() @@ -221,7 +221,7 @@ class KeysTest { } else if (alg instanceof DefaultEllipticCurveSignatureAlgorithm) { - def pair = alg.keyPairBuilder().build() as io.jsonwebtoken.security.KeyPair + def pair = alg.keyPairBuilder().build() assertNotNull pair int len = alg.orderBitLength From a40a02a52a376c28471f48d193b524f1620113fc Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Thu, 26 May 2022 19:25:18 -0700 Subject: [PATCH 60/75] Ensured RsaKeyAlgorithm used PublicKey and PrivateKey parameters due to PKCS11 and HSM key stores that may not expose the RSAKey interface on their RSA key implementations --- .../io/jsonwebtoken/security/RsaKeyAlgorithm.java | 8 +++----- .../impl/security/AbstractEcJwkFactory.java | 1 + .../impl/security/DefaultRsaKeyAlgorithm.java | 14 ++++++-------- .../impl/security/KeyAlgorithmsBridge.java | 8 ++++---- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java index a34fa7a51..4e3b1347d 100644 --- a/api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java @@ -17,14 +17,12 @@ import java.security.PrivateKey; import java.security.PublicKey; -import java.security.interfaces.RSAKey; /** - * A {@link KeyAlgorithm} that produces JWE Encrypted Keys via RSA cryptography. + * A {@link KeyAlgorithm} that uses RSA cryptography; an RSA {@link PublicKey} is used to wrap (encrypt) the + * AEAD encryption key, and an RSA {@link PrivateKey} is used to unwrap (decrypt) the AEAD decryption key. * - * @param the type of RSA public key used to obtain the AEAD encryption key - * @param the type of RSA private key used to obtain the AEAD decryption key * @since JJWT_RELEASE_VERSION */ -public interface RsaKeyAlgorithm extends KeyAlgorithm { +public interface RsaKeyAlgorithm extends KeyAlgorithm { } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java index 5a8b1a2e9..884ed2009 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java @@ -109,6 +109,7 @@ static String toOctetString(int fieldSize, BigInteger coordinate) { * @param point a point that may or may not be defined on the specified elliptic curve * @return {@code true} if a given elliptic curve contains the specified {@code point}, {@code false} otherwise. */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") static boolean contains(EllipticCurve curve, ECPoint point) { if (ECPoint.POINT_INFINITY.equals(point)) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java index abb1d953c..7cf78ceca 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java @@ -13,14 +13,12 @@ import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.interfaces.RSAKey; import java.security.spec.AlgorithmParameterSpec; /** * @since JJWT_RELEASE_VERSION */ -public class DefaultRsaKeyAlgorithm extends CryptoAlgorithm - implements RsaKeyAlgorithm { +public class DefaultRsaKeyAlgorithm extends CryptoAlgorithm implements RsaKeyAlgorithm { private final AlgorithmParameterSpec SPEC; //can be null @@ -34,9 +32,9 @@ public DefaultRsaKeyAlgorithm(String id, String jcaTransformationString, Algorit } @Override - public KeyResult getEncryptionKey(final KeyRequest request) throws SecurityException { + public KeyResult getEncryptionKey(final KeyRequest request) throws SecurityException { Assert.notNull(request, "Request cannot be null."); - final E kek = Assert.notNull(request.getKey(), "Request key encryption key cannot be null."); + final PublicKey kek = Assert.notNull(request.getKey(), "Request key encryption key cannot be null."); final SecretKey cek = generateKey(request); byte[] ciphertext = execute(request, Cipher.class, new CheckedFunction() { @@ -55,9 +53,9 @@ public byte[] apply(Cipher cipher) throws Exception { } @Override - public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { Assert.notNull(request, "request cannot be null."); - final D kek = Assert.notNull(request.getKey(), "Request key decryption key cannot be null."); + final PrivateKey kek = Assert.notNull(request.getKey(), "Request key decryption key cannot be null."); final byte[] cekBytes = Assert.notEmpty(request.getContent(), "Request content (encrypted key) cannot be null or empty."); return execute(request, Cipher.class, new CheckedFunction() { @@ -68,7 +66,7 @@ public SecretKey apply(Cipher cipher) throws Exception { } else { cipher.init(Cipher.UNWRAP_MODE, kek, SPEC); } - Key key = cipher.unwrap(cekBytes, "AES", Cipher.SECRET_KEY); + Key key = cipher.unwrap(cekBytes, AesAlgorithm.KEY_ALG_NAME, Cipher.SECRET_KEY); Assert.state(key instanceof SecretKey, "Cipher unwrap must return a SecretKey instance."); return (SecretKey) key; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java index 1903ab164..bc0d494b3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java @@ -32,7 +32,7 @@ private KeyAlgorithmsBridge() { public static final Registry> REGISTRY; static { - REGISTRY = new IdRegistry<>(Collections.>of( + REGISTRY = new IdRegistry<>(Collections.of( new DirectKeyAlgorithm(), new AesWrapKeyAlgorithm(128), new AesWrapKeyAlgorithm(192), @@ -47,9 +47,9 @@ private KeyAlgorithmsBridge() { new EcdhKeyAlgorithm<>(new AesWrapKeyAlgorithm(128)), new EcdhKeyAlgorithm<>(new AesWrapKeyAlgorithm(192)), new EcdhKeyAlgorithm<>(new AesWrapKeyAlgorithm(256)), - new DefaultRsaKeyAlgorithm<>(RSA1_5_ID, RSA1_5_TRANSFORMATION), - new DefaultRsaKeyAlgorithm<>(RSA_OAEP_ID, RSA_OAEP_TRANSFORMATION), - new DefaultRsaKeyAlgorithm<>(RSA_OAEP_256_ID, RSA_OAEP_256_TRANSFORMATION, RSA_OAEP_256_SPEC) + new DefaultRsaKeyAlgorithm(RSA1_5_ID, RSA1_5_TRANSFORMATION), + new DefaultRsaKeyAlgorithm(RSA_OAEP_ID, RSA_OAEP_TRANSFORMATION), + new DefaultRsaKeyAlgorithm(RSA_OAEP_256_ID, RSA_OAEP_256_TRANSFORMATION, RSA_OAEP_256_SPEC) )); } From 494b59411c21534981f8f82d4d9e5cc60f953049 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Thu, 26 May 2022 21:25:43 -0700 Subject: [PATCH 61/75] cleaned up EC point addition/doubling logic to be more readable and match equations in literature --- .../impl/security/AbstractEcJwkFactory.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java index 884ed2009..18376ab5a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java @@ -190,8 +190,8 @@ private static ECPoint add(ECPoint P, ECPoint Q, EllipticCurve curve) { final BigInteger Qy = Q.getAffineY(); final BigInteger prime = ((ECFieldFp) curve.getField()).getP(); final BigInteger slope = Qy.subtract(Py).multiply(Qx.subtract(Px).modInverse(prime)).mod(prime); - final BigInteger Rx = (slope.modPow(TWO, prime).subtract(Px)).subtract(Qx).mod(prime); - final BigInteger Ry = Qy.negate().mod(prime).add(slope.multiply(Qx.subtract(Rx))).mod(prime); + final BigInteger Rx = slope.pow(2).subtract(Px).subtract(Qx).mod(prime); + final BigInteger Ry = slope.multiply(Px.subtract(Rx)).subtract(Py).mod(prime); return new ECPoint(Rx, Ry); } @@ -206,9 +206,9 @@ private static ECPoint doublePoint(ECPoint P, EllipticCurve curve) { final BigInteger Py = P.getAffineY(); final BigInteger p = ((ECFieldFp) curve.getField()).getP(); final BigInteger a = curve.getA(); - final BigInteger s = ((Px.pow(2)).multiply(THREE).add(a)).multiply(Py.multiply(TWO).modInverse(p)); - final BigInteger x = s.pow(2).subtract(Px.multiply(TWO)).mod(p); - final BigInteger y = (Py.negate()).add(s.multiply(Px.subtract(x))).mod(p); + final BigInteger s = THREE.multiply(Px.pow(2)).add(a).mod(p).multiply(TWO.multiply(Py).modInverse(p)).mod(p); + final BigInteger x = s.pow(2).subtract(TWO.multiply(Px)).mod(p); + final BigInteger y = s.multiply(Px.subtract(x)).subtract(Py).mod(p); return new ECPoint(x, y); } @@ -219,7 +219,7 @@ private static ECPoint doublePoint(ECPoint P, EllipticCurve curve) { // visible for testing protected ECPublicKey derivePublic(KeyFactory keyFactory, ECPublicKeySpec spec) throws InvalidKeySpecException { - return (ECPublicKey)keyFactory.generatePublic(spec); + return (ECPublicKey) keyFactory.generatePublic(spec); } protected ECPublicKey derivePublic(final JwkContext ctx) { From a5d30a0ff3c8bbd8d1f8e1dad62834a0016fd8dc Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 27 May 2022 15:32:55 -0700 Subject: [PATCH 62/75] Deprecated JwtParserBuilder setSigningKey* methods in favor of verifyWith for name accuracy and congruence with decryptWith --- .../io/jsonwebtoken/JwtParserBuilder.java | 86 ++++++++++++------- .../impl/DefaultJwtParserBuilder.java | 11 ++- .../impl/security/KeyAlgorithmsBridge.java | 3 +- 3 files changed, 67 insertions(+), 33 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index 555047f43..1eec5d350 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -34,8 +34,8 @@ * A builder to construct a {@link JwtParser}. Example usage: *
    {@code
      *     Jwts.parserBuilder()
    - *         .setSigningKey(...)
      *         .requireIssuer("https://issuer.example.com")
    + *         .verifyWith(...)
      *         .build()
      *         .parse(jwtString)
      * }
    @@ -192,8 +192,17 @@ public interface JwtParserBuilder extends Builder { JwtParserBuilder setAllowedClockSkewSeconds(long seconds) throws IllegalArgumentException; /** - * Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not - * a JWS (no signature), this key is not used. + *

    Deprecation Notice

    + * + *

    This method has been deprecated since JJWT_RELEASE_VERSION and will be removed before 1.0. It was not + * readily obvious to many JJWT users that this method was for bytes that pertained only to HMAC + * {@code SecretKey}s, and could be confused with keys of other types. It is better to obtain a type-safe + * {@link Key} instance and call the {@link #verifyWith(Key)} instead.

    + * + *

    Previous Documentation

    + * + *

    Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not + * a JWS (no signature), this key is not used.

    * *

    Note that this key MUST be a valid key for the signature algorithm found in the JWT header * (as the {@code alg} header parameter).

    @@ -203,21 +212,13 @@ public interface JwtParserBuilder extends Builder { * @param key the algorithm-specific signature verification key used to validate any discovered JWS digital * signature. * @return the parser builder for method chaining. + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #verifyWith(Key)} for type safety and name congruence + * with the {@link #decryptWith(Key)} method. */ + @Deprecated JwtParserBuilder setSigningKey(byte[] key); /** - * Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not - * a JWS (no signature), this key is not used. - * - *

    Note that this key MUST be a valid key for the signature algorithm found in the JWT header - * (as the {@code alg} header parameter).

    - * - *

    This method overwrites any previously set key.

    - * - *

    This is a convenience method: the string argument is first BASE64-decoded to a byte array and this resulting - * byte array is used to invoke {@link #setSigningKey(byte[])}.

    - * *

    Deprecation Notice: Deprecated as of 0.10.0, will be removed in 1.0.0

    * *

    This method has been deprecated because the {@code key} argument for this method can be confusing: keys for @@ -238,23 +239,52 @@ public interface JwtParserBuilder extends Builder { * StackOverflow answer explaining why raw (non-base64-encoded) strings are almost always incorrect for * signature operations.

    * - *

    Finally, please use the {@link #setSigningKey(Key) setSigningKey(Key)} instead, as this method (and likely the - * {@code byte[]} variant) will be removed before the 1.0.0 release.

    + *

    Finally, please use the {@link #verifyWith(Key)} method instead, as this method (and likely + * {@link #setSigningKey(byte[])}) will be removed before the 1.0.0 release.

    + * + *

    Previous JavaDoc

    * - * @param base64EncodedSecretKey the BASE64-encoded algorithm-specific signature verification key to use to validate - * any discovered JWS digital signature. + *

    This is a convenience method that equates to the following:

    + * + *
    +     * byte[] bytes = Decoders.{@link io.jsonwebtoken.io.Decoders#BASE64 BASE64}.decode(base64EncodedSecretKey);
    +     * Key key = Keys.{@link io.jsonwebtoken.security.Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor}(bytes);
    +     * return {@link #verifyWith(Key) verifyWith}(key);
    + * + * @param base64EncodedSecretKey BASE64-encoded HMAC-SHA key bytes used to create a Key which will be used to + * verify all encountered JWS digital signatures. * @return the parser builder for method chaining. - * @deprecated in favor of {@link #setSigningKey(Key)} as explained in the above Deprecation Notice, + * @deprecated in favor of {@link #verifyWith(Key)} as explained in the above Deprecation Notice, * and will be removed in 1.0.0. */ @Deprecated JwtParserBuilder setSigningKey(String base64EncodedSecretKey); + /** + *

    Deprecation Notice

    + * + *

    This method is being renamed to accurately reflect its purpose - the key is not technically a signing key, + * it is a signature verification key, and the two concepts can be different, especially with asymmetric key + * cryptography. The method has been deprecated since JJWT_RELEASE_VERSION in favor of + * {@link #verifyWith(Key)} for type safety, to reflect accurate naming of the concept, and for name congruence + * with the {@link #decryptWith(Key)} method.

    + * + *

    This method merely delegates directly to {@link #verifyWith(Key)}.

    + * + * @param key the algorithm-specific signature verification key to use to verify all encountered JWS digital + * signatures. + * @return the parser builder for method chaining. + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #verifyWith(Key)} for naming congruence with the + * {@link #decryptWith(Key)} method. + */ + @Deprecated + JwtParserBuilder setSigningKey(Key key); + /** * Sets the signature verification key used to verify all encountered JWS signatures. If the encountered JWT * string is not a JWS (e.g. unsigned or a JWE), this key is not used. * - *

    This is a convenience method to use in specific circumstances: when the parser will only ever encounter + *

    This is a convenience method to use in a specific scenario: when the parser will only ever encounter * JWSs with signatures that can always be verified by a single key. This also implies that this key * MUST be a valid key for the signature algorithm ({@code alg} header) used for the JWS.

    * @@ -265,11 +295,11 @@ public interface JwtParserBuilder extends Builder { * *

    Calling this method overrides any previously set signature verification key.

    * - * @param key the algorithm-specific signature verification key to use to verify all encountered JWS digital - * signatures. + * @param key the signature verification key to use to verify all encountered JWS digital signatures. * @return the parser builder for method chaining. + * @since JJWT_RELEASE_VERSION */ - JwtParserBuilder setSigningKey(Key key); + JwtParserBuilder verifyWith(Key key); /** * Sets the decryption key to be used to decrypt all encountered JWEs. If the encountered JWT string is not a @@ -306,15 +336,16 @@ public interface JwtParserBuilder extends Builder { * verify the JWS signature or decrypt the JWE payload with the returned key. For example:

    * *
    -     * Jws<Claims> jws = Jwts.parser().setKeyLocator(new Locator<Header,Key>() {
    +     * Jws<Claims> jws = Jwts.parserBuilder().setKeyLocator(new Locator<Key>() {
          *         @Override
    -     *         public Key locate(Header header) {
    +     *         public Key locate(Header<?> header) {
          *             if (header instanceof JwsHeader) {
          *                 return getSignatureVerificationKey((JwsHeader)header); // implement me
          *             } else {
          *                 return getDecryptionKey((JweHeader)header); // implement me
          *             }
          *         }})
    +     *     .build()
          *     .parseClaimsJws(compact);
          * 
    * @@ -356,12 +387,9 @@ public interface JwtParserBuilder extends Builder { * *

    A {@code SigningKeyResolver} is invoked once during parsing before the signature is verified.

    * - *

    This method should only be used if a signing key is not provided by the other {@code setSigningKey*} builder - * methods.

    - * * @param signingKeyResolver the signing key resolver used to retrieve the signing key. * @return the parser builder for method chaining. - * @deprecated since JJWT_RELEASE_VERSION + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #setKeyLocator(Locator)} */ @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index f92b9f686..4a0e0d4c7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -180,20 +180,25 @@ public JwtParserBuilder setAllowedClockSkewSeconds(long seconds) throws IllegalA @Override public JwtParserBuilder setSigningKey(byte[] key) { - Assert.notEmpty(key, "signing key cannot be null or empty."); + Assert.notEmpty(key, "signature verification key cannot be null or empty."); return setSigningKey(Keys.hmacShaKeyFor(key)); } @Override public JwtParserBuilder setSigningKey(String base64EncodedSecretKey) { - Assert.hasText(base64EncodedSecretKey, "signing key cannot be null or empty."); + Assert.hasText(base64EncodedSecretKey, "signature verification key cannot be null or empty."); byte[] bytes = Decoders.BASE64.decode(base64EncodedSecretKey); return setSigningKey(bytes); } @Override public JwtParserBuilder setSigningKey(final Key key) { - this.signatureVerificationKey = Assert.notNull(key, "signing key cannot be null."); + return verifyWith(key); + } + + @Override + public JwtParserBuilder verifyWith(Key key) { + this.signatureVerificationKey = Assert.notNull(key, "signature verification key cannot be null."); return setSigningKeyResolver(new ConstantKeyLocator(key, null)); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java index bc0d494b3..79c6032e4 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java @@ -32,7 +32,8 @@ private KeyAlgorithmsBridge() { public static final Registry> REGISTRY; static { - REGISTRY = new IdRegistry<>(Collections.of( + //noinspection RedundantTypeArguments + REGISTRY = new IdRegistry<>(Collections.>of( new DirectKeyAlgorithm(), new AesWrapKeyAlgorithm(128), new AesWrapKeyAlgorithm(192), From 0ed890f0eb63bc2fc0ebe514ad220f84e4f3d7c9 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 28 May 2022 16:55:11 -0700 Subject: [PATCH 63/75] Added PositiveIntegerConverter and PublicJwkConverter for JWE Header "p2c" and "jwk" fields --- .../impl/AbstractProtectedHeader.java | 4 +- .../io/jsonwebtoken/impl/DefaultClaims.java | 2 +- .../io/jsonwebtoken/impl/DefaultHeader.java | 2 +- .../jsonwebtoken/impl/DefaultJweHeader.java | 6 +- .../jsonwebtoken/impl/DefaultJwsHeader.java | 2 +- .../java/io/jsonwebtoken/impl/JwtMap.java | 2 +- .../impl/lang/PositiveIntegerConverter.java | 40 +++++ .../impl/lang/PublicJwkConverter.java | 60 +++++++ .../impl/security/DefaultJwkContext.java | 2 +- .../impl/security/DefaultValueGetter.java | 22 +-- .../security/RSAOtherPrimeInfoConverter.java | 3 +- .../impl/AbstractProtectedHeaderTest.groovy | 155 +++++++++++++++++- .../impl/DefaultJweHeaderTest.groovy | 79 +++++++++ .../security/DefaultValueGetterTest.groovy | 21 ++- 14 files changed, 364 insertions(+), 36 deletions(-) create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/PositiveIntegerConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/PublicJwkConverter.java diff --git a/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java index 63bc31d95..db8cfdd3c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java @@ -3,6 +3,7 @@ import io.jsonwebtoken.ProtectedHeader; import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.impl.lang.PublicJwkConverter; import io.jsonwebtoken.impl.security.AbstractAsymmetricJwk; import io.jsonwebtoken.impl.security.AbstractJwk; import io.jsonwebtoken.lang.Collections; @@ -25,7 +26,8 @@ public abstract class AbstractProtectedHeader> exte static final Field JKU = Fields.uri("jku", "JWK Set URL"); @SuppressWarnings("rawtypes") - static final Field JWK = Fields.builder(PublicJwk.class).setId("jwk").setName("JSON Web Key").build(); + static final Field JWK = Fields.builder(PublicJwk.class).setId("jwk").setName("JSON Web Key") + .setConverter(new PublicJwkConverter()).build(); static final Field> CRIT = Fields.stringSet("crit", "Critical"); static final Set> FIELDS = Collections.concat(DefaultHeader.FIELDS, CRIT, JKU, JWK, AbstractJwk.KID, diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index ad5bd70e5..c0379cae1 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -58,7 +58,7 @@ public DefaultClaims(Map map) { } @Override - protected String getName() { + public String getName() { return "JWT Claim"; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java index 682de6829..174c26b1b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java @@ -53,7 +53,7 @@ public DefaultHeader(Map map) { } @Override - protected String getName() { + public String getName() { return "JWT header"; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java index 34941dd13..e32e8f7f1 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java @@ -3,6 +3,7 @@ import io.jsonwebtoken.JweHeader; import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.impl.lang.PositiveIntegerConverter; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; @@ -19,7 +20,8 @@ public class DefaultJweHeader extends AbstractProtectedHeader implements JweHeader { static final Field ENCRYPTION_ALGORITHM = Fields.string("enc", "Encryption Algorithm"); - public static final Field P2C = Fields.builder(Integer.class).setId("p2c").setName("PBES2 Count").build(); + public static final Field P2C = Fields.builder(Integer.class) + .setConverter(PositiveIntegerConverter.INSTANCE).setId("p2c").setName("PBES2 Count").build(); public static final Field P2S = Fields.bytes("p2s", "PBES2 Salt Input").build(); static final Field APU = Fields.bytes("apu", "Agreement PartyUInfo").build(); static final Field APV = Fields.bytes("apv", "Agreement PartyVInfo").build(); @@ -35,7 +37,7 @@ public DefaultJweHeader(Map map) { } @Override - protected String getName() { + public String getName() { return "JWE header"; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java index 3a1762cc2..0d4d3a7c7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java @@ -34,7 +34,7 @@ public DefaultJwsHeader(Map map) { } @Override - protected String getName() { + public String getName() { return "JWS header"; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 37af49322..13f89eafd 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -159,7 +159,7 @@ protected Object apply(Field field, Object rawValue) { return retval; } - protected String getName() { + public String getName() { return "Map"; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/PositiveIntegerConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/PositiveIntegerConverter.java new file mode 100644 index 000000000..13124bcac --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/PositiveIntegerConverter.java @@ -0,0 +1,40 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; + +import java.util.concurrent.atomic.AtomicInteger; + +public class PositiveIntegerConverter implements Converter { + + public static final PositiveIntegerConverter INSTANCE = new PositiveIntegerConverter(); + + @Override + public Object applyTo(Integer integer) { + return integer; + } + + @Override + public Integer applyFrom(Object o) { + Assert.notNull(o, "Argument cannot be null."); + int i; + if (o instanceof Byte || o instanceof Short || o instanceof Integer || o instanceof AtomicInteger) { + i = ((Number) o).intValue(); + } else { // could be Long, AtomicLong, Float, Decimal, BigInteger, BigDecimal, String, etc., all of which + // may not be accurately converted into an Integer, either due to overflow or fractional values. The + // easiest way to account for all of them is to parse the string value as an int instead of testing all + // the types: + String sval = String.valueOf(o); + try { + i = Integer.parseInt(sval); + } catch (NumberFormatException e) { + String msg = "Value cannot be represented as a java.lang.Integer."; + throw new IllegalArgumentException(msg, e); + } + } + if (i <= 0) { + String msg = "Value is not a positive integer."; + throw new IllegalArgumentException(msg); + } + return i; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/PublicJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/PublicJwkConverter.java new file mode 100644 index 000000000..d44181a3c --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/PublicJwkConverter.java @@ -0,0 +1,60 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.JwkBuilder; +import io.jsonwebtoken.security.Jwks; +import io.jsonwebtoken.security.PrivateJwk; +import io.jsonwebtoken.security.PublicJwk; +import io.jsonwebtoken.security.SecretJwk; + +import java.util.Map; + +@SuppressWarnings("rawtypes") +public class PublicJwkConverter implements Converter { + + @Override + public Object applyTo(PublicJwk publicJwk) { + return publicJwk; + } + + @Override + public PublicJwk applyFrom(Object o) { + Assert.notNull(o, "JWK argument cannot be null."); + if (o instanceof PublicJwk) { + return ((PublicJwk) o); + } + if (o instanceof Map) { + Map map = (Map) o; + JwkBuilder builder = Jwks.builder(); + for(Map.Entry entry : map.entrySet()) { + Object key = entry.getKey(); + Assert.notNull(key, "JWK map key cannot be null."); + if (!(key instanceof String)) { + String msg = "Unsupported 'jwk' map value - all JWK map keys must be Strings. Encountered key '" + + key + "' of type " + key.getClass().getName(); + throw new IllegalArgumentException(msg); + } + String skey = (String)key; + builder.put(skey, entry.getValue()); + } + Jwk jwk = builder.build(); + if (!(jwk instanceof PublicJwk)) { + String type; + if (jwk instanceof SecretJwk) { + type = "SecretJwk"; + } else { + // only other type remaining: + Assert.isInstanceOf(PrivateJwk.class, jwk, "Unexpected Jwk type - programming error. Please report this to the JJWT team."); + type = "PrivateJwk"; + } + String msg = "Unsupported JWK map - JWK values must represent a PublicJwk, not a " + type + "."; + throw new IllegalArgumentException(msg); + } + return ((PublicJwk) jwk); + } + String msg = "Unsupported value type - expected a Map or Jwk instance. Type found: " + + o.getClass().getName(); + throw new IllegalArgumentException(msg); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java index e5da918d3..a25d174a2 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java @@ -90,7 +90,7 @@ private DefaultJwkContext(Set> fields, JwkContext other, boolean rem } @Override - protected String getName() { + public String getName() { Object value = values.get(AbstractJwk.KTY.getId()); if (DefaultSecretJwk.TYPE_VALUE.equals(value)) { value = "Secret"; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java index 126046d62..165aca38e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java @@ -15,11 +15,9 @@ */ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.JweHeader; -import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.impl.JwtMap; import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.RedactedSupplier; import io.jsonwebtoken.impl.lang.ValueGetter; @@ -45,18 +43,12 @@ public DefaultValueGetter(Map values) { } private String name() { - if (values instanceof JweHeader) { - return "JWE header"; - } else if (values instanceof JwsHeader) { - return "JWS header"; - } else if (values instanceof Header) { - return "JWT header"; - } else if (values instanceof Jwk || values instanceof JwkContext) { - Object value = values.get(AbstractJwk.KTY.getId()); - if (DefaultSecretJwk.TYPE_VALUE.equals(value)) { - value = "Secret"; - } - return value instanceof String ? value + " JWK" : "JWK"; + Object nameable = values; + if (nameable instanceof AbstractJwk) { + nameable = ((AbstractJwk) values).context; + } + if (nameable instanceof JwtMap) { + return ((JwtMap) nameable).getName(); } else { return "Map"; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java index 82cddbd2b..3557512fe 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java @@ -61,8 +61,7 @@ public RSAOtherPrimeInfo applyFrom(Object o) { throw new MalformedKeyException("RSA JWK 'oth' (Other Prime Info) element map cannot be empty."); } - // Need to add the values to a Context instance to satisfy the API contract of the getRequired* methods - // called below. It's less than ideal, but it works: + // Need a Context instance to satisfy the API contract of the getRequired* methods below. JwkContext ctx = new DefaultJwkContext<>(FIELDS); for (Map.Entry entry : m.entrySet()) { String name = String.valueOf(entry.getKey()); diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy index 8ed9a372b..a0d57517b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy @@ -1,26 +1,171 @@ package io.jsonwebtoken.impl +import io.jsonwebtoken.impl.security.TestKeys +import io.jsonwebtoken.security.EcPrivateJwk +import io.jsonwebtoken.security.EcPublicJwk +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.SecretJwk +import org.junit.Before import org.junit.Test -import static org.junit.Assert.assertEquals +import static org.junit.Assert.* class AbstractProtectedHeaderTest { + private AbstractProtectedHeader header + + @Before + void setUp() { + header = new DefaultJwsHeader() // extends AbstractProtectedHeader + } + @Test - void x509UrlTest() { - def header = new DefaultJwsHeader() // extends AbstractProtectedHeader + void testJku() { + URI uri = URI.create('https://google.com') + header.put('jku', uri) + assertEquals uri.toString(), header.get('jku') + assertEquals uri, header.getJwkSetUrl() + } + + @Test + void testJkuString() { //test canonical/idiomatic conversion + String url = 'https://google.com' + URI uri = URI.create(url) + header.put('jku', url) + assertEquals url, header.get('jku') + assertEquals uri, header.getJwkSetUrl() + } + + @Test + void testX509Url() { URI uri = URI.create('https://google.com') header.setX509Url(uri) assertEquals uri, header.getX509Url() } @Test - void x509UrlStringTest() { //test canonical/idiomatic conversion - def header = new DefaultJwsHeader() + void testX509UrlString() { //test canonical/idiomatic conversion String url = 'https://google.com' URI uri = URI.create(url) header.put('x5u', url) assertEquals url, header.get('x5u') assertEquals uri, header.getX509Url() } + + @Test + void testJwkWithNull() { + header.put('jwk', null) + assertNull header.getJwk() + } + + @Test + void testJwkWithEmptyMap() { + header.put('jwk', [:]) + assertNull header.getJwk() + } + + @Test + void testJwkWithoutMap() { + try { + header.put('jwk', 42) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWS header 'jwk' (JSON Web Key) value: 42. Cause: Unsupported value type - " + + "expected a Map or Jwk instance. Type found: java.lang.Integer" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testJwkWithJwk() { + EcPrivateJwk jwk = Jwks.builder().setKeyPairEc(TestKeys.ES256.pair).build() + EcPublicJwk pubJwk = jwk.toPublicJwk() + header.setJwk(pubJwk) + assertEquals pubJwk, header.getJwk() + } + + @Test + void testJwkWithMap() { + EcPrivateJwk jwk = Jwks.builder().setKeyPairEc(TestKeys.ES256.pair).build() + EcPublicJwk pubJwk = jwk.toPublicJwk() + Map m = new LinkedHashMap<>(pubJwk) + header.put('jwk', m) + assertEquals pubJwk, header.getJwk() + } + + @Test + void testJwkWithBadMapKeys() { + def m = [42: "hello"] + try { + header.put('jwk', m) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWS header 'jwk' (JSON Web Key) value: {42=hello}. Cause: Unsupported 'jwk' map " + + "value - all JWK map keys must be Strings. Encountered key '42' of type java.lang.Integer" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testJwkWithSecretJwk() { + SecretJwk jwk = Jwks.builder().setKey(TestKeys.HS256).build() + try { + header.put('jwk', jwk) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWS header 'jwk' (JSON Web Key) value: {kty=oct, k=}. Cause: " + + "Unsupported JWK map - JWK values must represent a PublicJwk, not a SecretJwk." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testJwkWithPrivateJwk() { + EcPrivateJwk jwk = Jwks.builder().setKeyPairEc(TestKeys.ES256.pair).build() + try { + header.put('jwk', jwk) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWS header 'jwk' (JSON Web Key) value: {kty=EC, crv=P-256, " + + "x=xNKMMIsawShLG4LYxpNP0gqdgK_K69UXCLt3AE3zp-Q, y=_vzQymVtA7RHRTfBWZo75mxPgDkE8g7bdHI3siSuJOk, " + + "d=}. Cause: Unsupported JWK map - JWK values must represent a PublicJwk, " + + "not a PrivateJwk." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testCritNull() { + header.put('crit', null) + assertNull header.getCritical() + } + + @Test + void testCritEmpty() { + header.put('crit', []) + assertNull header.getCritical() + } + + @Test + void testCritSingleValue() { + header.put('crit', 'foo') + assertEquals(["foo"] as Set, header.get('crit')) + assertEquals(["foo"] as Set, header.getCritical()) + } + + @Test + void testCritArray() { + String[] crit = ["exp"] as String[] + header.put('crit', crit) + assertEquals(["exp"] as Set, header.get('crit')) + assertEquals(["exp"] as Set, header.getCritical()) + } + + @Test + void testCritList() { + List crit = ["exp"] as List + header.put('crit', crit) + assertEquals(["exp"] as Set, header.get('crit')) + assertEquals(["exp"] as Set, header.getCritical()) + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy index cea35257e..cd3549058 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy @@ -11,6 +11,7 @@ import org.junit.Before import org.junit.Test import java.nio.charset.StandardCharsets +import java.util.concurrent.atomic.AtomicInteger import static org.junit.Assert.* @@ -101,6 +102,84 @@ class DefaultJweHeaderTest { assertEquals 'JWE header', header.getName() } + @Test + void testP2cByte() { + header.put('p2c', Byte.MAX_VALUE) + assertEquals 127, header.getPbes2Count() + } + + @Test + void testP2cShort() { + header.put('p2c', Short.MAX_VALUE) + assertEquals 32767, header.getPbes2Count() + } + @Test + void testP2cInt() { + header.put('p2c', Integer.MAX_VALUE) + assertEquals 0x7fffffff as Integer, header.getPbes2Count() + } + + @Test + void testP2cAtomicInteger() { + header.put('p2c', new AtomicInteger(Integer.MAX_VALUE)) + assertEquals 0x7fffffff as Integer, header.getPbes2Count() + } + + @Test + void testP2cString() { + header.put('p2c', "100") + assertEquals 100, header.getPbes2Count() + } + + @Test + void testP2cZero() { + try { + header.put('p2c', 0) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'p2c' (PBES2 Count) value: 0. " + + "Cause: Value is not a positive integer." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testP2cNegative() { + try { + header.put('p2c', -1) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'p2c' (PBES2 Count) value: -1. " + + "Cause: Value is not a positive integer." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testP2cTooLarge() { + try { + header.put('p2c', Long.MAX_VALUE) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'p2c' (PBES2 Count) value: 9223372036854775807. " + + "Cause: Value cannot be represented as a java.lang.Integer." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testP2cDecimal() { + double d = 42.2348423d + try { + header.put('p2c', d) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'p2c' (PBES2 Count) value: $d. " + + "Cause: Value cannot be represented as a java.lang.Integer." + assertEquals msg, expected.getMessage() + } + } + @Test void pbe2SaltBytesTest() { byte[] salt = new byte[32] diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy index 00cfecccc..88c3e44bb 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy @@ -10,6 +10,8 @@ import io.jsonwebtoken.security.MalformedKeyException import org.junit.Test import javax.crypto.SecretKey +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPublicKey import static org.junit.Assert.* @@ -40,8 +42,8 @@ class DefaultValueGetterTest { } @Test - void testJwkName() { - def ctx = new DefaultJwkContext().setId('id') + void testJwkContextName() { + def ctx = new DefaultJwkContext<>().setId('id') def getter = new DefaultValueGetter(ctx) assertEquals 'JWK', getter.name() } @@ -55,10 +57,17 @@ class DefaultValueGetterTest { } @Test - void testJwkContextName() { - def ctx = new DefaultJwkContext<>().setId('id') - def getter = new DefaultValueGetter(ctx) - assertEquals 'JWK', getter.name() + void testEcJwkName() { + def jwk = Jwks.builder().setKey(TestKeys.ES256.pair.public as ECPublicKey).build() + def getter = new DefaultValueGetter(jwk) + assertEquals 'EC JWK', getter.name() + } + + @Test + void testRsaJwkName() { + def jwk = Jwks.builder().setKey(TestKeys.RS256.pair.public as RSAPublicKey).build() + def getter = new DefaultValueGetter(jwk) + assertEquals 'RSA JWK', getter.name() } @Test From 3a40fda36261a5c126745815f1eb07d85adfa379 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 28 May 2022 17:43:30 -0700 Subject: [PATCH 64/75] Ensured CompressionCodec inherited Identifiable for consistency w/ all other algorithms --- .../io/jsonwebtoken/CompressionCodec.java | 24 ++++++++++++------- .../jsonwebtoken/impl/DefaultJweBuilder.java | 2 +- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 2 +- .../compression/AbstractCompressionCodec.java | 17 +++++++++++++ .../DefaultCompressionCodecResolver.java | 6 ++--- .../compression/DeflateCompressionCodec.java | 5 ++-- .../compression/GzipCompressionCodec.java | 5 ++-- .../io/jsonwebtoken/DeprecatedJwtsTest.groovy | 11 +++++++-- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 4 ++-- .../AbstractCompressionCodecTest.groovy | 19 +++++++-------- ...DefaultCompressionCodecResolverTest.groovy | 9 +++---- .../compression/YagCompressionCodec.groovy | 7 +++++- 12 files changed, 70 insertions(+), 41 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/CompressionCodec.java b/api/src/main/java/io/jsonwebtoken/CompressionCodec.java index 41b8e4c34..0ffc648c6 100644 --- a/api/src/main/java/io/jsonwebtoken/CompressionCodec.java +++ b/api/src/main/java/io/jsonwebtoken/CompressionCodec.java @@ -18,11 +18,17 @@ /** * Compresses and decompresses byte arrays according to a compression algorithm. * + *

    "zip" identifier

    + * + *

    {@code CompressionCodec} extends {@code Identifiable}; the value returned from + * {@link Identifiable#getId() getId()} will be used as the JWT's + * zip header value.

    + * * @see CompressionCodecs#DEFLATE * @see CompressionCodecs#GZIP * @since 0.6.0 */ -public interface CompressionCodec { +public interface CompressionCodec extends Identifiable { /** * The algorithm name to use as the JWT's @@ -30,28 +36,28 @@ public interface CompressionCodec { * * @return the algorithm name to use as the JWT's * zip header value. + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link Identifiable#getId()} to ensure congruence with + * all other identifiable algorithms. */ + @Deprecated String getAlgorithmName(); /** - * Compresses the specified byte array according to the compression {@link #getAlgorithmName() algorithm}. + * Compresses the specified byte array, returning the compressed byte array result. * * @param content bytes to compress * @return compressed bytes - * @throws CompressionException if the specified byte array cannot be compressed according to the compression - * {@link #getAlgorithmName() algorithm}. + * @throws CompressionException if the specified byte array cannot be compressed. */ byte[] compress(byte[] content) throws CompressionException; /** - * Decompresses the specified compressed byte array according to the compression - * {@link #getAlgorithmName() algorithm}. The specified byte array must already be in compressed form - * according to the {@link #getAlgorithmName() algorithm}. + * Decompresses the specified compressed byte array, returning the decompressed byte array result. The + * specified byte array must already be in compressed form. * * @param compressed compressed bytes * @return decompressed bytes - * @throws CompressionException if the specified byte array cannot be decompressed according to the compression - * {@link #getAlgorithmName() algorithm}. + * @throws CompressionException if the specified byte array cannot be decompressed. */ byte[] decompress(byte[] compressed) throws CompressionException; } \ No newline at end of file diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java index 8974a16f4..d59c55fcb 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java @@ -133,7 +133,7 @@ public String compact() { if (compressionCodec != null) { plaintext = compressionCodec.compress(plaintext); - jweHeader.setCompressionAlgorithm(compressionCodec.getAlgorithmName()); + jweHeader.setCompressionAlgorithm(compressionCodec.getId()); } KeyRequest keyRequest = new DefaultKeyRequest<>(this.provider, this.secureRandom, this.key, jweHeader, enc); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index 40af7de8b..a220d1dc4 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -378,7 +378,7 @@ public String compact() { byte[] bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : claimsSerializer.apply(claims); if (Arrays.length(bytes) > 0 && compressionCodec != null) { - header.setCompressionAlgorithm(compressionCodec.getAlgorithmName()); + header.setCompressionAlgorithm(compressionCodec.getId()); bytes = compressionCodec.compress(bytes); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java b/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java index 163522061..64a6a0d8d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java @@ -19,6 +19,7 @@ import io.jsonwebtoken.CompressionException; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.lang.Strings; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -32,6 +33,22 @@ */ public abstract class AbstractCompressionCodec implements CompressionCodec { + protected final String id; + + protected AbstractCompressionCodec(String id) { + this.id = Assert.hasText(Strings.clean(id), "id argument cannot be null or empty."); + } + + @Override + public String getId() { + return this.id; + } + + @Override + public String getAlgorithmName() { + return getId(); + } + //package-protected for a point release. This can be made protected on a minor release (0.11.0, 0.12.0, 1.0, etc). //TODO: make protected on a minor release interface StreamWrapper { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java b/impl/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java index 6564e248e..52ae5e808 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java @@ -58,11 +58,11 @@ public class DefaultCompressionCodecResolver implements CompressionCodecResolver public DefaultCompressionCodecResolver() { Map codecMap = new HashMap<>(); for (CompressionCodec codec : Services.loadAll(CompressionCodec.class)) { - codecMap.put(codec.getAlgorithmName().toUpperCase(), codec); + codecMap.put(codec.getId().toUpperCase(), codec); } - codecMap.put(CompressionCodecs.DEFLATE.getAlgorithmName().toUpperCase(), CompressionCodecs.DEFLATE); - codecMap.put(CompressionCodecs.GZIP.getAlgorithmName().toUpperCase(), CompressionCodecs.GZIP); + codecMap.put(CompressionCodecs.DEFLATE.getId().toUpperCase(), CompressionCodecs.DEFLATE); + codecMap.put(CompressionCodecs.GZIP.getId().toUpperCase(), CompressionCodecs.GZIP); codecs = Collections.unmodifiableMap(codecMap); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java b/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java index db3cf0cab..2f1eb5436 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java @@ -42,9 +42,8 @@ public OutputStream wrap(OutputStream out) { } }; - @Override - public String getAlgorithmName() { - return DEFLATE; + public DeflateCompressionCodec() { + super(DEFLATE); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java b/impl/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java index a9d166d7e..2b19905dd 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java @@ -39,9 +39,8 @@ public OutputStream wrap(OutputStream out) throws IOException { } }; - @Override - public String getAlgorithmName() { - return GZIP; + public GzipCompressionCodec() { + super(GZIP); } @Override diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy index 71ca06ab2..a26356998 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy @@ -38,6 +38,7 @@ import java.security.PublicKey import static org.junit.Assert.* +@SuppressWarnings(['GrDeprecatedAPIUsage', 'GrUnnecessarySemicolon']) class DeprecatedJwtsTest { private static Date now() { @@ -126,6 +127,7 @@ class DeprecatedJwtsTest { // carriage return + newline, so we have to include them in the test payload to assert our encoded output // matches what is in the spec: + //noinspection HttpUrlsUsage def payload = '{"iss":"joe",\r\n' + ' "exp":1300819380,\r\n' + ' "http://example.com/is_root":true}' @@ -393,7 +395,7 @@ class DeprecatedJwtsTest { String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { @Override - String getAlgorithmName() { + String getId() { return "CUSTOM" } }).compact() @@ -402,6 +404,7 @@ class DeprecatedJwtsTest { @Override CompressionCodec resolveCompressionCodec(Header header) { String algorithm = header.getCompressionAlgorithm() + //noinspection ChangeToOperator if ("CUSTOM".equals(algorithm)) { return CompressionCodecs.GZIP } else { @@ -431,7 +434,7 @@ class DeprecatedJwtsTest { String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { @Override - String getAlgorithmName() { + String getId() { return "CUSTOM" } }).compact() @@ -555,6 +558,7 @@ class DeprecatedJwtsTest { String jws = Jwts.builder().setSubject("Foo").signWith(key, alg).compact() + //noinspection GroovyUnusedCatchParameter try { Jwts.parser().setSigningKey(weakKey).parseClaimsJws(jws) fail('parseClaimsJws must fail for weak keys') @@ -726,6 +730,7 @@ class DeprecatedJwtsTest { def token = Jwts.parser().setSigningKey(key).parse(jwt) + //noinspection GrEqualsBetweenInconvertibleTypes assert [alg: alg.name()] == token.header //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims @@ -742,6 +747,7 @@ class DeprecatedJwtsTest { def token = Jwts.parser().setSigningKey(key).parse(jwt) + //noinspection GrEqualsBetweenInconvertibleTypes assert token.header == [alg: alg.name()] //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims @@ -764,6 +770,7 @@ class DeprecatedJwtsTest { def token = Jwts.parser().setSigningKey(key).parse(jwt) + //noinspection GrEqualsBetweenInconvertibleTypes assert token.header == [alg: alg.name()] //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index f6a00eb68..42b42438e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -481,7 +481,7 @@ class JwtsTest { String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { @Override - String getAlgorithmName() { + String getId() { return "CUSTOM" } }).compact() @@ -520,7 +520,7 @@ class JwtsTest { String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { @Override - String getAlgorithmName() { + String getId() { return "CUSTOM" } }).compact() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy index a1e17436b..f3bb89128 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy @@ -25,31 +25,30 @@ import org.junit.Test class AbstractCompressionCodecTest { static class ExceptionThrowingCodec extends AbstractCompressionCodec { - @Override - protected byte[] doCompress(byte[] payload) throws IOException { - throw new IOException("Test Exception") + ExceptionThrowingCodec() { + super("Test") } @Override - String getAlgorithmName() { - return "Test" + protected byte[] doCompress(byte[] payload) throws IOException { + throw new IOException("Test Exception") } @Override protected byte[] doDecompress(byte[] payload) throws IOException { - throw new IOException("Test Decompress Exception"); + throw new IOException("Test Decompress Exception") } } @Test(expected = CompressionException.class) void testCompressWithException() { - CompressionCodec codecUT = new ExceptionThrowingCodec(); - codecUT.compress(new byte[0]); + CompressionCodec codecUT = new ExceptionThrowingCodec() + codecUT.compress(new byte[0]) } @Test(expected = CompressionException.class) void testDecompressWithException() { - CompressionCodec codecUT = new ExceptionThrowingCodec(); - codecUT.decompress(new byte[0]); + CompressionCodec codecUT = new ExceptionThrowingCodec() + codecUT.decompress(new byte[0]) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolverTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolverTest.groovy index 6115e2346..0ff91edf1 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolverTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolverTest.groovy @@ -16,6 +16,7 @@ package io.jsonwebtoken.impl.compression import io.jsonwebtoken.CompressionCodec +import io.jsonwebtoken.CompressionCodecs import io.jsonwebtoken.CompressionException import io.jsonwebtoken.impl.DefaultHeader import io.jsonwebtoken.impl.io.FakeServiceDescriptorClassLoader @@ -23,12 +24,7 @@ import io.jsonwebtoken.impl.lang.Services import org.junit.Assert import org.junit.Test -import io.jsonwebtoken.CompressionCodecs - -import static org.hamcrest.CoreMatchers.hasItem -import static org.hamcrest.CoreMatchers.instanceOf -import static org.hamcrest.CoreMatchers.is -import static org.hamcrest.CoreMatchers.nullValue +import static org.hamcrest.CoreMatchers.* import static org.hamcrest.MatcherAssert.assertThat class DefaultCompressionCodecResolverTest { @@ -68,6 +64,7 @@ class DefaultCompressionCodecResolverTest { @Test void emptyCompressionAlgInHeaderTest() { + //noinspection GroovyUnusedCatchParameter try { new DefaultCompressionCodecResolver().byName("") Assert.fail("Expected IllegalArgumentException to be thrown") diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/YagCompressionCodec.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/YagCompressionCodec.groovy index 5c76b4fa8..601226bf0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/YagCompressionCodec.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/YagCompressionCodec.groovy @@ -23,9 +23,14 @@ import io.jsonwebtoken.CompressionException */ class YagCompressionCodec implements CompressionCodec { + @Override + String getId() { + return GzipCompressionCodec.GZIP + } + @Override String getAlgorithmName() { - return new GzipCompressionCodec().getAlgorithmName(); + return GzipCompressionCodec.GZIP } @Override From e8b4fe0674c00ec5ae6d3e46be81299392922781 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 28 May 2022 17:45:11 -0700 Subject: [PATCH 65/75] Ensured CompressionCodec inherited Identifiable for consistency w/ all other algorithms --- .../impl/compression/AbstractCompressionCodecTest.groovy | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy index f3bb89128..88cd47322 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy @@ -19,6 +19,8 @@ import io.jsonwebtoken.CompressionCodec import io.jsonwebtoken.CompressionException import org.junit.Test +import static org.junit.Assert.assertEquals + /** * @since 0.6.0 */ @@ -51,4 +53,9 @@ class AbstractCompressionCodecTest { CompressionCodec codecUT = new ExceptionThrowingCodec() codecUT.decompress(new byte[0]) } + + @Test + void testAlgorithmName() { + assertEquals "Test", new ExceptionThrowingCodec().getAlgorithmName() + } } From 45b1ee21b3b0508a2f218068b02d3c1e4355b320 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 28 May 2022 17:47:56 -0700 Subject: [PATCH 66/75] Ensured CompressionCodec inherited Identifiable for consistency w/ all other algorithms --- .../impl/compression/AbstractCompressionCodec.java | 2 +- .../impl/compression/AbstractCompressionCodecTest.groovy | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java b/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java index 64a6a0d8d..efda1ccb8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java @@ -33,7 +33,7 @@ */ public abstract class AbstractCompressionCodec implements CompressionCodec { - protected final String id; + private final String id; protected AbstractCompressionCodec(String id) { this.id = Assert.hasText(Strings.clean(id), "id argument cannot be null or empty."); diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy index 88cd47322..1fff6dd4f 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy @@ -54,6 +54,11 @@ class AbstractCompressionCodecTest { codecUT.decompress(new byte[0]) } + @Test + void testGetId() { + assertEquals "Test", new ExceptionThrowingCodec().getId() + } + @Test void testAlgorithmName() { assertEquals "Test", new ExceptionThrowingCodec().getAlgorithmName() From aa544e1ff108b3851d495831a5e83165a5a0f4bc Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 28 May 2022 17:50:17 -0700 Subject: [PATCH 67/75] JavaDoc enhancement --- api/src/main/java/io/jsonwebtoken/CompressionCodec.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/CompressionCodec.java b/api/src/main/java/io/jsonwebtoken/CompressionCodec.java index 0ffc648c6..450d874d9 100644 --- a/api/src/main/java/io/jsonwebtoken/CompressionCodec.java +++ b/api/src/main/java/io/jsonwebtoken/CompressionCodec.java @@ -21,7 +21,7 @@ *

    "zip" identifier

    * *

    {@code CompressionCodec} extends {@code Identifiable}; the value returned from - * {@link Identifiable#getId() getId()} will be used as the JWT's + * {@link Identifiable#getId() getId()} will be used as the JWT * zip header value.

    * * @see CompressionCodecs#DEFLATE @@ -31,14 +31,15 @@ public interface CompressionCodec extends Identifiable { /** - * The algorithm name to use as the JWT's + * The algorithm name to use as the JWT * zip header value. * - * @return the algorithm name to use as the JWT's + * @return the algorithm name to use as the JWT * zip header value. * @deprecated since JJWT_RELEASE_VERSION in favor of {@link Identifiable#getId()} to ensure congruence with * all other identifiable algorithms. */ + @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated String getAlgorithmName(); From 954338bcb2a3a59ede0f9d4aaa46f47cbb2de1db Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 28 May 2022 19:19:31 -0700 Subject: [PATCH 68/75] Added Jwks.parserBuilder(), JwkParserBuilder and JwkParser concepts --- .../io/jsonwebtoken/security/JwkParser.java | 33 ++++++ .../security/JwkParserBuilder.java | 61 +++++++++++ .../java/io/jsonwebtoken/security/Jwks.java | 16 ++- .../impl/security/DefaultJwkParser.java | 67 ++++++++++++ .../security/DefaultJwkParserBuilder.java | 55 ++++++++++ .../DefaultJwkParserBuilderTest.groovy | 53 +++++++++ .../impl/security/DefaultJwkParserTest.groovy | 103 ++++++++++++++++++ .../impl/security/TestKeys.groovy | 7 +- 8 files changed, 388 insertions(+), 7 deletions(-) create mode 100644 api/src/main/java/io/jsonwebtoken/security/JwkParser.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/JwkParserBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParser.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParserBuilder.java create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserTest.groovy diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkParser.java b/api/src/main/java/io/jsonwebtoken/security/JwkParser.java new file mode 100644 index 000000000..f4d471abe --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/JwkParser.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +/** + * Parses a JWK JSON string and produces its resulting {@link Jwk} instance. + * + * @since JJWT_RELEASE_VERSION + */ +public interface JwkParser { + + /** + * Parses the specified JWK JSON string and returns the resulting {@link Jwk} instance. + * + * @param json the json string representing the JWK + * @return the {@link Jwk} instance corresponding to the specified JWK json string. + * @throws KeyException if the json string cannot be represented as a {@link Jwk}. + */ + Jwk parse(String json) throws KeyException; +} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkParserBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkParserBuilder.java new file mode 100644 index 000000000..568938764 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/JwkParserBuilder.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.lang.Builder; + +import java.security.Provider; +import java.util.Map; + +/** + * A builder to construct a {@link JwkParser}. Example usage: + *
    + * Jwk<?> jwk = Jwks.parserBuilder()
    + *         .setProvider(aJcaProvider)         // optional
    + *         .deserializeJsonWith(deserializer) // optional
    + *         .build()
    + *         .parse(jwkString);
    + * + * @since JJWT_RELEASE_VERSION + */ +public interface JwkParserBuilder extends Builder { + + /** + * Sets the JCA Provider to use during cryptographic key factory operations, or {@code null} if the + * JCA subsystem preferred provider should be used. + * + * @param provider the JCA Provider to use during cryptographic key factory operations, or {@code null} + * if the JCA subsystem preferred provider should be used. + * @return the builder for method chaining. + */ + JwkParserBuilder setProvider(Provider provider); + + /** + * Uses the specified deserializer to convert JSON Strings (UTF-8 byte arrays) into Java Map objects. The + * resulting Maps are then used to construct {@link Jwk} instances. + * + *

    If this method is not called, JJWT will use whatever deserializer it can find at runtime, checking for the + * presence of well-known implementations such Jackson, Gson, and org.json. If one of these is not found + * in the runtime classpath, an exception will be thrown when the resulting {@link JwkParser}'s + * {@link JwkParser#parse(String) parse(json)} method is called. + * + * @param deserializer the deserializer to use when converting JSON Strings (UTF-8 byte arrays) into Map objects. + * @return the builder for method chaining. + */ + JwkParserBuilder deserializeJsonWith(Deserializer> deserializer); + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/Jwks.java b/api/src/main/java/io/jsonwebtoken/security/Jwks.java index 078b255e8..0bffa119d 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Jwks.java +++ b/api/src/main/java/io/jsonwebtoken/security/Jwks.java @@ -29,7 +29,9 @@ public final class Jwks { private Jwks() { } //prevent instantiation - private static final String CNAME = "io.jsonwebtoken.impl.security.DefaultProtoJwkBuilder"; + private static final String BUILDER_CLASSNAME = "io.jsonwebtoken.impl.security.DefaultProtoJwkBuilder"; + + private static final String PARSERBUILDER_CLASSNAME = "io.jsonwebtoken.impl.security.DefaultJwkParserBuilder"; /** * Return a new JWK builder instance, allowing for type-safe JWK builder coercion based on a provided key or key pair. @@ -37,6 +39,16 @@ private Jwks() { * @return a new JWK builder instance, allowing for type-safe JWK builder coercion based on a provided key or key pair. */ public static ProtoJwkBuilder builder() { - return Classes.newInstance(CNAME); + return Classes.newInstance(BUILDER_CLASSNAME); } + + /** + * Return a new thread-safe {@link JwkParserBuilder} to parse JSON strings into {@link Jwk} instances. + * + * @return a new thread-safe {@link JwkParserBuilder} to parse JSON strings into {@link Jwk} instances. + */ + public static JwkParserBuilder parserBuilder() { + return Classes.newInstance(PARSERBUILDER_CLASSNAME); + } + } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParser.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParser.java new file mode 100644 index 000000000..1f4d3ecce --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParser.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.JwkBuilder; +import io.jsonwebtoken.security.JwkParser; +import io.jsonwebtoken.security.Jwks; +import io.jsonwebtoken.security.KeyException; +import io.jsonwebtoken.security.MalformedKeyException; + +import java.nio.charset.StandardCharsets; +import java.security.Provider; +import java.util.Map; + +public class DefaultJwkParser implements JwkParser { + + private final Provider provider; + + private final Deserializer> deserializer; + + public DefaultJwkParser(Provider provider, Deserializer> deserializer) { + this.provider = provider; + this.deserializer = Assert.notNull(deserializer, "Deserializer cannot be null."); + } + + // visible for testing + protected Map deserialize(String json) { + byte[] data = json.getBytes(StandardCharsets.UTF_8); + return this.deserializer.deserialize(data); + } + + @Override + public Jwk parse(String json) throws KeyException { + Assert.hasText(json, "JSON string argument cannot be null or empty."); + Map data; + try { + data = deserialize(json); + } catch (Exception e) { + String msg = "Unable to deserialize JSON string argument: " + e.getMessage(); + throw new MalformedKeyException(msg); + } + + JwkBuilder builder = Jwks.builder(); + + if (this.provider != null) { + builder.setProvider(this.provider); + } + + return builder.putAll(data).build(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParserBuilder.java new file mode 100644 index 000000000..e60ae1728 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParserBuilder.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.Services; +import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.security.JwkParser; +import io.jsonwebtoken.security.JwkParserBuilder; + +import java.security.Provider; +import java.util.Map; + +@SuppressWarnings("unused") //used via reflection by Jwks.parserBuilder() +public class DefaultJwkParserBuilder implements JwkParserBuilder { + + private Provider provider; + + private Deserializer> deserializer; + + @Override + public JwkParserBuilder setProvider(Provider provider) { + this.provider = provider; + return this; + } + + @Override + public JwkParserBuilder deserializeJsonWith(Deserializer> deserializer) { + this.deserializer = deserializer; + return this; + } + + @Override + public JwkParser build() { + if (this.deserializer == null) { + // try to find one based on the services available: + //noinspection unchecked + this.deserializer = Services.loadFirst(Deserializer.class); + } + + return new DefaultJwkParser(this.provider, this.deserializer); + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy new file mode 100644 index 000000000..c47aab2cb --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.security.Jwks +import org.junit.Test + +import java.security.Provider + +import static org.easymock.EasyMock.createMock +import static org.junit.Assert.* + +class DefaultJwkParserBuilderTest { + + @Test + void testDefault() { + def builder = Jwks.parserBuilder() as DefaultJwkParserBuilder + assertNotNull builder + assertNull builder.provider + assertNull builder.deserializer + def parser = builder.build() as DefaultJwkParser + assertNull parser.provider + assertNotNull parser.deserializer // Services.loadFirst should have picked one up + } + + @Test + void testProvider() { + def provider = createMock(Provider) + def parser = Jwks.parserBuilder().setProvider(provider).build() as DefaultJwkParser + assertSame provider, parser.provider + } + + @Test + void testDeserializer() { + def deserializer = createMock(Deserializer) + def parser = Jwks.parserBuilder().deserializeJsonWith(deserializer).build() as DefaultJwkParser + assertSame deserializer, parser.deserializer + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserTest.groovy new file mode 100644 index 000000000..5c7504798 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserTest.groovy @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2022 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.impl.lang.Conditions +import io.jsonwebtoken.impl.lang.Services +import io.jsonwebtoken.io.DeserializationException +import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.io.Serializer +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test + +import java.nio.charset.StandardCharsets +import java.security.Key + +import static org.junit.Assert.* + +class DefaultJwkParserTest { + + @Test + void testKeys() { + + Set keys = new LinkedHashSet<>() + TestKeys.HS.each { keys.add(it) } + TestKeys.RSA.each { + keys.add(it.pair.public) + keys.add(it.pair.private) + } + TestKeys.EC.each { + keys.add(it.pair.public) + keys.add(it.pair.private) + } + + def serializer = Services.loadFirst(Serializer) + for (Key key : keys) { + //noinspection GroovyAssignabilityCheck + def jwk = Jwks.builder().setKey(key).build() + def data = serializer.serialize(jwk) + String json = new String(data, StandardCharsets.UTF_8) + def parsed = Jwks.parserBuilder().build().parse(json) + assertEquals jwk, parsed + } + } + + @Test + void testKeysWithProvider() { + + Set keys = new LinkedHashSet<>() + TestKeys.HS.each { keys.add(it) } + TestKeys.RSA.each { + keys.add(it.pair.public) + keys.add(it.pair.private) + } + TestKeys.EC.each { + keys.add(it.pair.public) + keys.add(it.pair.private) + } + + def serializer = Services.loadFirst(Serializer) + def provider = Providers.findBouncyCastle(Conditions.TRUE) + + for (Key key : keys) { + //noinspection GroovyAssignabilityCheck + def jwk = Jwks.builder().setKey(key).build() + def data = serializer.serialize(jwk) + String json = new String(data, StandardCharsets.UTF_8) + def parsed = Jwks.parserBuilder().setProvider(provider).build().parse(json) + assertEquals jwk, parsed + assertSame provider, parsed.@context.@provider + } + } + + @Test + void testDeserializationFailure() { + def parser = new DefaultJwkParser(null, Services.loadFirst(Deserializer)) { + @Override + protected Map deserialize(String json) { + throw new DeserializationException("test") + } + } + try { + parser.parse('foo') + fail() + } catch (MalformedKeyException expected) { + String msg = "Unable to deserialize JSON string argument: test" + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy index 666b14025..29ba76ace 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy @@ -2,11 +2,7 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.Identifiable import io.jsonwebtoken.lang.Collections -import io.jsonwebtoken.security.AsymmetricKeySignatureAlgorithm -import io.jsonwebtoken.security.EncryptionAlgorithms -import io.jsonwebtoken.security.KeyBuilderSupplier -import io.jsonwebtoken.security.SecretKeyBuilder -import io.jsonwebtoken.security.SignatureAlgorithms +import io.jsonwebtoken.security.* import javax.crypto.SecretKey import java.security.KeyPair @@ -24,6 +20,7 @@ class TestKeys { static SecretKey HS256 = SignatureAlgorithms.HS256.keyBuilder().build() static SecretKey HS384 = SignatureAlgorithms.HS384.keyBuilder().build() static SecretKey HS512 = SignatureAlgorithms.HS512.keyBuilder().build() + static Collection HS = Collections.setOf(HS256, HS384, HS512) static SecretKey A128GCM, A192GCM, A256GCM, A128KW, A192KW, A256KW, A128GCMKW, A192GCMKW, A256GCMKW static { From 4b8121911507d4a0089d0a70e7b430832c85aead Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 29 May 2022 13:44:52 -0700 Subject: [PATCH 69/75] Ensured ProtoJwkBuilder method names were all congruent (remaining set* methods were renamed to for*) --- .../security/ProtoJwkBuilder.java | 14 ++++----- .../impl/security/DefaultProtoJwkBuilder.java | 22 +++++++------- .../impl/security/EcdhKeyAlgorithm.java | 2 +- .../impl/AbstractProtectedHeaderTest.groovy | 8 ++--- .../impl/DefaultJweHeaderTest.groovy | 2 +- .../AbstractAsymmetricJwkBuilderTest.groovy | 4 +-- .../security/AbstractJwkBuilderTest.groovy | 2 +- .../impl/security/AbstractJwkTest.groovy | 18 +++++------ .../impl/security/DefaultJwkParserTest.groovy | 4 +-- .../security/DefaultValueGetterTest.groovy | 6 ++-- .../impl/security/EcdhKeyAlgorithmTest.groovy | 6 ++-- .../impl/security/JwkSerializationTest.groovy | 6 ++-- .../impl/security/JwksTest.groovy | 30 +++++++++---------- .../security/RsaPrivateJwkFactoryTest.groovy | 16 +++++----- 14 files changed, 70 insertions(+), 70 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java index 894b14227..270c5980c 100644 --- a/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java @@ -43,7 +43,7 @@ public interface ProtoJwkBuilder, T extends JwkB * @param key the {@link SecretKey} to represent as a {@link SecretJwk}. * @return the builder coerced as a {@link SecretJwkBuilder}. */ - SecretJwkBuilder setKey(SecretKey key); + SecretJwkBuilder forKey(SecretKey key); /** * Ensures the builder will create an {@link RsaPublicJwk} for the specified Java {@link RSAPublicKey}. @@ -51,7 +51,7 @@ public interface ProtoJwkBuilder, T extends JwkB * @param key the {@link RSAPublicKey} to represent as a {@link RsaPublicJwk}. * @return the builder coerced as an {@link RsaPublicJwkBuilder}. */ - RsaPublicJwkBuilder setKey(RSAPublicKey key); + RsaPublicJwkBuilder forKey(RSAPublicKey key); /** * Ensures the builder will create an {@link RsaPublicJwk} for the specified Java {@link X509Certificate} chain. @@ -86,7 +86,7 @@ public interface ProtoJwkBuilder, T extends JwkB * @param key the {@link RSAPublicKey} to represent as a {@link RsaPublicJwk}. * @return the builder coerced as an {@link RsaPrivateJwkBuilder}. */ - RsaPrivateJwkBuilder setKey(RSAPrivateKey key); + RsaPrivateJwkBuilder forKey(RSAPrivateKey key); /** * Ensures the builder will create an {@link EcPublicJwk} for the specified Java {@link ECPublicKey}. @@ -94,7 +94,7 @@ public interface ProtoJwkBuilder, T extends JwkB * @param key the {@link ECPublicKey} to represent as a {@link EcPublicJwk}. * @return the builder coerced as an {@link EcPublicJwkBuilder}. */ - EcPublicJwkBuilder setKey(ECPublicKey key); + EcPublicJwkBuilder forKey(ECPublicKey key); /** * Ensures the builder will create an {@link EcPublicJwk} for the specified Java {@link X509Certificate} chain. @@ -129,7 +129,7 @@ public interface ProtoJwkBuilder, T extends JwkB * @param key the {@link ECPublicKey} to represent as an {@link EcPublicJwk}. * @return the builder coerced as a {@link EcPrivateJwkBuilder}. */ - EcPrivateJwkBuilder setKey(ECPrivateKey key); + EcPrivateJwkBuilder forKey(ECPrivateKey key); /** * Ensures the builder will create an {@link RsaPrivateJwk} for the specified Java RSA @@ -142,7 +142,7 @@ public interface ProtoJwkBuilder, T extends JwkB * @throws IllegalArgumentException if the {@code keyPair} does not contain {@link RSAPublicKey} and * {@link RSAPrivateKey} instances. */ - RsaPrivateJwkBuilder setKeyPairRsa(KeyPair keyPair) throws IllegalArgumentException; + RsaPrivateJwkBuilder forRsaKeyPair(KeyPair keyPair) throws IllegalArgumentException; /** * Ensures the builder will create an {@link EcPrivateJwk} for the specified Java Elliptic Curve @@ -155,5 +155,5 @@ public interface ProtoJwkBuilder, T extends JwkB * @throws IllegalArgumentException if the {@code keyPair} does not contain {@link ECPublicKey} and * {@link ECPrivateKey} instances. */ - EcPrivateJwkBuilder setKeyPairEc(KeyPair keyPair) throws IllegalArgumentException; + EcPrivateJwkBuilder forEcKeyPair(KeyPair keyPair) throws IllegalArgumentException; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java index a1c0e1b24..a64d9c390 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java @@ -31,12 +31,12 @@ public DefaultProtoJwkBuilder() { } @Override - public SecretJwkBuilder setKey(SecretKey key) { + public SecretJwkBuilder forKey(SecretKey key) { return new AbstractJwkBuilder.DefaultSecretJwkBuilder(this.jwkContext, key); } @Override - public RsaPublicJwkBuilder setKey(RSAPublicKey key) { + public RsaPublicJwkBuilder forKey(RSAPublicKey key) { return new AbstractAsymmetricJwkBuilder.DefaultRsaPublicJwkBuilder(this.jwkContext, key); } @@ -52,7 +52,7 @@ public RsaPublicJwkBuilder forRsaChain(List chain) { X509Certificate cert = chain.get(0); PublicKey key = cert.getPublicKey(); RSAPublicKey pubKey = KeyPairs.assertKey(key, RSAPublicKey.class, "The first X509Certificate's "); - return setKey(pubKey).setX509CertificateChain(chain); + return forKey(pubKey).setX509CertificateChain(chain); } @Override @@ -67,35 +67,35 @@ public EcPublicJwkBuilder forEcChain(List chain) { X509Certificate cert = chain.get(0); PublicKey key = cert.getPublicKey(); ECPublicKey pubKey = KeyPairs.assertKey(key, ECPublicKey.class, "The first X509Certificate's "); - return setKey(pubKey).setX509CertificateChain(chain); + return forKey(pubKey).setX509CertificateChain(chain); } @Override - public RsaPrivateJwkBuilder setKey(RSAPrivateKey key) { + public RsaPrivateJwkBuilder forKey(RSAPrivateKey key) { return new AbstractAsymmetricJwkBuilder.DefaultRsaPrivateJwkBuilder(this.jwkContext, key); } @Override - public EcPublicJwkBuilder setKey(ECPublicKey key) { + public EcPublicJwkBuilder forKey(ECPublicKey key) { return new AbstractAsymmetricJwkBuilder.DefaultEcPublicJwkBuilder(this.jwkContext, key); } @Override - public EcPrivateJwkBuilder setKey(ECPrivateKey key) { + public EcPrivateJwkBuilder forKey(ECPrivateKey key) { return new AbstractAsymmetricJwkBuilder.DefaultEcPrivateJwkBuilder(this.jwkContext, key); } @Override - public RsaPrivateJwkBuilder setKeyPairRsa(KeyPair pair) { + public RsaPrivateJwkBuilder forRsaKeyPair(KeyPair pair) { RSAPublicKey pub = KeyPairs.getKey(pair, RSAPublicKey.class); RSAPrivateKey priv = KeyPairs.getKey(pair, RSAPrivateKey.class); - return setKey(priv).setPublicKey(pub); + return forKey(priv).setPublicKey(pub); } @Override - public EcPrivateJwkBuilder setKeyPairEc(KeyPair pair) { + public EcPrivateJwkBuilder forEcKeyPair(KeyPair pair) { ECPublicKey pub = KeyPairs.getKey(pair, ECPublicKey.class); ECPrivateKey priv = KeyPairs.getKey(pair, ECPrivateKey.class); - return setKey(priv).setPublicKey(pub); + return forKey(priv).setPublicKey(pub); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java index ccaa07b9c..fc5b50a75 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java @@ -139,7 +139,7 @@ public KeyResult getEncryptionKey(KeyRequest request) throws SecurityExceptio ECPublicKey genPubKey = KeyPairs.getKey(pair, ECPublicKey.class); ECPrivateKey genPrivKey = KeyPairs.getKey(pair, ECPrivateKey.class); // This asserts that the generated public key (and therefore the request key) is on a JWK-supported curve: - final EcPublicJwk jwk = Jwks.builder().setKey(genPubKey).build(); + final EcPublicJwk jwk = Jwks.builder().forKey(genPubKey).build(); final SecretKey derived = deriveKey(request, publicKey, genPrivKey); diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy index a0d57517b..dd999e18e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy @@ -78,7 +78,7 @@ class AbstractProtectedHeaderTest { @Test void testJwkWithJwk() { - EcPrivateJwk jwk = Jwks.builder().setKeyPairEc(TestKeys.ES256.pair).build() + EcPrivateJwk jwk = Jwks.builder().forEcKeyPair(TestKeys.ES256.pair).build() EcPublicJwk pubJwk = jwk.toPublicJwk() header.setJwk(pubJwk) assertEquals pubJwk, header.getJwk() @@ -86,7 +86,7 @@ class AbstractProtectedHeaderTest { @Test void testJwkWithMap() { - EcPrivateJwk jwk = Jwks.builder().setKeyPairEc(TestKeys.ES256.pair).build() + EcPrivateJwk jwk = Jwks.builder().forEcKeyPair(TestKeys.ES256.pair).build() EcPublicJwk pubJwk = jwk.toPublicJwk() Map m = new LinkedHashMap<>(pubJwk) header.put('jwk', m) @@ -108,7 +108,7 @@ class AbstractProtectedHeaderTest { @Test void testJwkWithSecretJwk() { - SecretJwk jwk = Jwks.builder().setKey(TestKeys.HS256).build() + SecretJwk jwk = Jwks.builder().forKey(TestKeys.HS256).build() try { header.put('jwk', jwk) fail() @@ -121,7 +121,7 @@ class AbstractProtectedHeaderTest { @Test void testJwkWithPrivateJwk() { - EcPrivateJwk jwk = Jwks.builder().setKeyPairEc(TestKeys.ES256.pair).build() + EcPrivateJwk jwk = Jwks.builder().forEcKeyPair(TestKeys.ES256.pair).build() try { header.put('jwk', jwk) fail() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy index cd3549058..07c8aef4b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy @@ -55,7 +55,7 @@ class DefaultJweHeaderTest { @Test void testJwk() { - EcPrivateJwk jwk = Jwks.builder().setKeyPairEc(TestKeys.ES256.pair).build() + EcPrivateJwk jwk = Jwks.builder().forEcKeyPair(TestKeys.ES256.pair).build() EcPublicJwk pubJwk = jwk.toPublicJwk() header.setJwk(pubJwk) assertEquals pubJwk, header.getJwk() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy index 89e1d4b84..2e3541d34 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy @@ -22,7 +22,7 @@ class AbstractAsymmetricJwkBuilderTest { private static final RSAPublicKey PUB_KEY = CERT.getPublicKey() as RSAPublicKey private static RsaPublicJwkBuilder builder() { - return Jwks.builder().setKey(PUB_KEY) + return Jwks.builder().forKey(PUB_KEY) } @Test @@ -69,7 +69,7 @@ class AbstractAsymmetricJwkBuilderTest { def pair = TestKeys.ES256.pair //start with a public key builder - def builder = Jwks.builder().setKey(pair.public as ECPublicKey) + def builder = Jwks.builder().forKey(pair.public as ECPublicKey) assertTrue builder instanceof AbstractAsymmetricJwkBuilder.DefaultEcPublicJwkBuilder //applying the private key turns it into a private key builder diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy index 98d50366c..4c6c3761f 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy @@ -17,7 +17,7 @@ class AbstractJwkBuilderTest { private static final SecretKey SKEY = TestKeys.A256GCM private static AbstractJwkBuilder builder() { - return (AbstractJwkBuilder)Jwks.builder().setKey(SKEY) + return (AbstractJwkBuilder)Jwks.builder().forKey(SKEY) } @Test diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy index 9c1700227..4104cce1b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy @@ -111,13 +111,13 @@ class AbstractJwkTest { @Test void testPrivateJwkToStringHasRedactedValues() { - def secretJwk = Jwks.builder().setKey(TestKeys.HS256).build() + def secretJwk = Jwks.builder().forKey(TestKeys.HS256).build() assertTrue secretJwk.toString().contains('k=') - def ecPrivJwk = Jwks.builder().setKey(TestKeys.ES256.pair.private).build() + def ecPrivJwk = Jwks.builder().forKey(TestKeys.ES256.pair.private).build() assertTrue ecPrivJwk.toString().contains('d=') - def rsaPrivJwk = Jwks.builder().setKey(TestKeys.RS256.pair.private).build() + def rsaPrivJwk = Jwks.builder().forKey(TestKeys.RS256.pair.private).build() String s = 'd=, p=, q=, dp=, dq=, qi=' assertTrue rsaPrivJwk.toString().contains(s) } @@ -126,20 +126,20 @@ class AbstractJwkTest { void testPrivateJwkHashCode() { assertEquals jwk.hashCode(), jwk.@context.hashCode() - def secretJwk1 = Jwks.builder().setKey(TestKeys.HS256).put('hello', 'world').build() - def secretJwk2 = Jwks.builder().setKey(TestKeys.HS256).put('hello', 'world').build() + def secretJwk1 = Jwks.builder().forKey(TestKeys.HS256).put('hello', 'world').build() + def secretJwk2 = Jwks.builder().forKey(TestKeys.HS256).put('hello', 'world').build() assertEquals secretJwk1.hashCode(), secretJwk1.@context.hashCode() assertEquals secretJwk2.hashCode(), secretJwk2.@context.hashCode() assertEquals secretJwk1.hashCode(), secretJwk2.hashCode() - def ecPrivJwk1 = Jwks.builder().setKey(TestKeys.ES256.pair.private).put('hello', 'ecworld').build() - def ecPrivJwk2 = Jwks.builder().setKey(TestKeys.ES256.pair.private).put('hello', 'ecworld').build() + def ecPrivJwk1 = Jwks.builder().forKey(TestKeys.ES256.pair.private).put('hello', 'ecworld').build() + def ecPrivJwk2 = Jwks.builder().forKey(TestKeys.ES256.pair.private).put('hello', 'ecworld').build() assertEquals ecPrivJwk1.hashCode(), ecPrivJwk2.hashCode() assertEquals ecPrivJwk1.hashCode(), ecPrivJwk1.@context.hashCode() assertEquals ecPrivJwk2.hashCode(), ecPrivJwk2.@context.hashCode() - def rsaPrivJwk1 = Jwks.builder().setKey(TestKeys.RS256.pair.private).put('hello', 'rsaworld').build() - def rsaPrivJwk2 = Jwks.builder().setKey(TestKeys.RS256.pair.private).put('hello', 'rsaworld').build() + def rsaPrivJwk1 = Jwks.builder().forKey(TestKeys.RS256.pair.private).put('hello', 'rsaworld').build() + def rsaPrivJwk2 = Jwks.builder().forKey(TestKeys.RS256.pair.private).put('hello', 'rsaworld').build() assertEquals rsaPrivJwk1.hashCode(), rsaPrivJwk2.hashCode() assertEquals rsaPrivJwk1.hashCode(), rsaPrivJwk1.@context.hashCode() assertEquals rsaPrivJwk2.hashCode(), rsaPrivJwk2.@context.hashCode() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserTest.groovy index 5c7504798..533707461 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserTest.groovy @@ -48,7 +48,7 @@ class DefaultJwkParserTest { def serializer = Services.loadFirst(Serializer) for (Key key : keys) { //noinspection GroovyAssignabilityCheck - def jwk = Jwks.builder().setKey(key).build() + def jwk = Jwks.builder().forKey(key).build() def data = serializer.serialize(jwk) String json = new String(data, StandardCharsets.UTF_8) def parsed = Jwks.parserBuilder().build().parse(json) @@ -75,7 +75,7 @@ class DefaultJwkParserTest { for (Key key : keys) { //noinspection GroovyAssignabilityCheck - def jwk = Jwks.builder().setKey(key).build() + def jwk = Jwks.builder().forKey(key).build() def data = serializer.serialize(jwk) String json = new String(data, StandardCharsets.UTF_8) def parsed = Jwks.parserBuilder().setProvider(provider).build().parse(json) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy index 88c3e44bb..641ae84d6 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy @@ -58,14 +58,14 @@ class DefaultValueGetterTest { @Test void testEcJwkName() { - def jwk = Jwks.builder().setKey(TestKeys.ES256.pair.public as ECPublicKey).build() + def jwk = Jwks.builder().forKey(TestKeys.ES256.pair.public as ECPublicKey).build() def getter = new DefaultValueGetter(jwk) assertEquals 'EC JWK', getter.name() } @Test void testRsaJwkName() { - def jwk = Jwks.builder().setKey(TestKeys.RS256.pair.public as RSAPublicKey).build() + def jwk = Jwks.builder().forKey(TestKeys.RS256.pair.public as RSAPublicKey).build() def getter = new DefaultValueGetter(jwk) assertEquals 'RSA JWK', getter.name() } @@ -86,7 +86,7 @@ class DefaultValueGetterTest { @Test void testMalformedJwk() { - def jwk = Jwks.builder().setKey(TestKeys.A128GCM).put('foo', 42).build() + def jwk = Jwks.builder().forKey(TestKeys.A128GCM).put('foo', 42).build() def getter = new DefaultValueGetter(jwk) try { getter.getRequiredString('foo') diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy index b3dc8f85c..fd01754ec 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy @@ -27,7 +27,7 @@ class EcdhKeyAlgorithmTest { ECPrivateKey decryptionKey = TestKeys.ES256.pair.private as ECPrivateKey def header = new DefaultJweHeader() - def jwk = Jwks.builder().setKey(TestKeys.HS256).build() //something other than an EC public key + def jwk = Jwks.builder().forKey(TestKeys.HS256).build() //something other than an EC public key header.put('epk', jwk) DecryptionKeyRequest req = new DefaultDecryptionKeyRequest(null, null, decryptionKey, header, EncryptionAlgorithms.A128GCM, 'test'.getBytes()) @@ -47,7 +47,7 @@ class EcdhKeyAlgorithmTest { ECPrivateKey decryptionKey = TestKeys.ES256.pair.private as ECPrivateKey // Expected curve for this is P-256 def header = new DefaultJweHeader() - def pubJwk = Jwks.builder().setKey(TestKeys.ES256.pair.public as ECPublicKey).build() + def pubJwk = Jwks.builder().forKey(TestKeys.ES256.pair.public as ECPublicKey).build() def jwk = new LinkedHashMap(pubJwk) // copy fields so we can mutate // We have a public JWK for a point on the curve, now swap out the x coordinate for something invalid: jwk.put('x', 'Kg') @@ -76,7 +76,7 @@ class EcdhKeyAlgorithmTest { def header = new DefaultJweHeader() // This uses curve P-384 instead, does not match private key, so it's unexpected: - def jwk = Jwks.builder().setKey(TestKeys.ES384.pair.public as ECPublicKey).build() + def jwk = Jwks.builder().forKey(TestKeys.ES384.pair.public as ECPublicKey).build() header.put('epk', jwk) DecryptionKeyRequest req = new DefaultDecryptionKeyRequest(null, null, decryptionKey, header, EncryptionAlgorithms.A128GCM, 'test'.getBytes()) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy index 62b3d8496..e39bf7e5e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy @@ -88,7 +88,7 @@ class JwkSerializationTest { static void testSecretJwk(Serializer serializer, Deserializer deserializer) { def key = TestKeys.A128GCM - def jwk = Jwks.builder().setKey(key).setId('id').build() + def jwk = Jwks.builder().forKey(key).setId('id').build() assertWrapped(jwk, ['k']) // Ensure no Groovy or Java toString prints out secret values: @@ -114,7 +114,7 @@ class JwkSerializationTest { static void testPrivateEcJwk(Serializer serializer, Deserializer deserializer) { - def jwk = Jwks.builder().setKeyPairEc(TestKeys.ES256.pair).setId('id').build() + def jwk = Jwks.builder().forEcKeyPair(TestKeys.ES256.pair).setId('id').build() assertWrapped(jwk, ['d']) // Ensure no Groovy or Java toString prints out secret values: @@ -164,7 +164,7 @@ class JwkSerializationTest { static void testPrivateRsaJwk(Serializer serializer, Deserializer deserializer) { - def jwk = Jwks.builder().setKeyPairRsa(TestKeys.RS256.pair).setId('id').build() + def jwk = Jwks.builder().forRsaKeyPair(TestKeys.RS256.pair).setId('id').build() def privateFieldNames = ['d', 'p', 'q', 'dp', 'dq', 'qi'] assertWrapped(jwk, privateFieldNames) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index af8ee811c..d1e9c34b6 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -52,14 +52,14 @@ class JwksTest { def key = name == 'publicKeyUse' || name == 'x509CertificateChain' ? EC_PAIR.public : SKEY //test non-null value: - def builder = Jwks.builder().setKey(key) + def builder = Jwks.builder().forKey(key) builder."set${cap}"(val) def jwk = builder.build() assertEquals val, jwk."get${cap}"() assertEquals expectedFieldValue, jwk."${id}" //test null value: - builder = Jwks.builder().setKey(key) + builder = Jwks.builder().forKey(key) try { builder."set${cap}"(null) fail("IAE should have been thrown") @@ -71,7 +71,7 @@ class JwksTest { assertFalse jwk.containsKey(id) //test empty string value - builder = Jwks.builder().setKey(key) + builder = Jwks.builder().forKey(key) if (val instanceof String) { try { builder."set${cap}"(' ' as String) @@ -121,7 +121,7 @@ class JwksTest { @Test void testBuilderWithSecretKey() { - def jwk = Jwks.builder().setKey(SKEY).build() + def jwk = Jwks.builder().forKey(SKEY).build() assertEquals 'oct', jwk.getType() assertEquals 'oct', jwk.kty assertNotNull jwk.k @@ -170,13 +170,13 @@ class JwksTest { @Test void testRandom() { def random = new SecureRandom() - def jwk = Jwks.builder().setKey(SKEY).setRandom(random).build() + def jwk = Jwks.builder().forKey(SKEY).setRandom(random).build() assertSame random, jwk.@context.getRandom() } @Test(expected = IllegalArgumentException) void testNullRandom() { - Jwks.builder().setKey(SKEY).setRandom(null).build() + Jwks.builder().forKey(SKEY).setRandom(null).build() } static void testThumbprint(int number) { @@ -210,7 +210,7 @@ class JwksTest { Collection algs = SignatureAlgorithms.values().findAll({ it instanceof SecretKeySignatureAlgorithm }) as Collection for (def alg : algs) { SecretKey secretKey = alg.keyBuilder().build() - def jwk = Jwks.builder().setKey(secretKey).setId('id').build() + def jwk = Jwks.builder().forKey(secretKey).setId('id').build() assertEquals 'oct', jwk.getType() assertTrue jwk.containsKey('k') assertEquals 'id', jwk.getId() @@ -222,7 +222,7 @@ class JwksTest { void testSecretKeyGetEncodedReturnsNull() { SecretKey key = new TestSecretKey(algorithm: "AES") try { - Jwks.builder().setKey(key).build() + Jwks.builder().forKey(key).build() fail() } catch (UnsupportedKeyException expected) { String expectedMsg = 'Unable to encode SecretKey to JWK: ' + SecretJwkFactory.ENCODED_UNAVAILABLE_MSG @@ -243,7 +243,7 @@ class JwksTest { } } try { - Jwks.builder().setKey(key).build() + Jwks.builder().forKey(key).build() fail() } catch (UnsupportedKeyException expected) { String expectedMsg = 'Unable to encode SecretKey to JWK: ' + SecretJwkFactory.ENCODED_UNAVAILABLE_MSG @@ -266,9 +266,9 @@ class JwksTest { PrivateKey priv = pair.getPrivate() // test individual keys - PublicJwk pubJwk = Jwks.builder().setKey(pub).setPublicKeyUse("sig").build() + PublicJwk pubJwk = Jwks.builder().forKey(pub).setPublicKeyUse("sig").build() assertEquals pub, pubJwk.toKey() - PrivateJwk privJwk = Jwks.builder().setKey(priv).setPublicKeyUse("sig").build() + PrivateJwk privJwk = Jwks.builder().forKey(priv).setPublicKeyUse("sig").build() assertEquals priv, privJwk.toKey() PublicJwk privPubJwk = privJwk.toPublicJwk() assertEquals pubJwk, privPubJwk @@ -279,8 +279,8 @@ class JwksTest { // test pair privJwk = pub instanceof ECKey ? - Jwks.builder().setKeyPairEc(pair).setPublicKeyUse("sig").build() : - Jwks.builder().setKeyPairRsa(pair).setPublicKeyUse("sig").build() + Jwks.builder().forEcKeyPair(pair).setPublicKeyUse("sig").build() : + Jwks.builder().forRsaKeyPair(pair).setPublicKeyUse("sig").build() assertEquals priv, privJwk.toKey() privPubJwk = privJwk.toPublicJwk() assertEquals pubJwk, privPubJwk @@ -300,12 +300,12 @@ class JwksTest { def pair = alg.keyPairBuilder().build() ECPublicKey pubKey = pair.getPublic() as ECPublicKey - EcPublicJwk jwk = Jwks.builder().setKey(pubKey).build() + EcPublicJwk jwk = Jwks.builder().forKey(pubKey).build() //try creating a JWK with a bad point: def badPubKey = new InvalidECPublicKey(pubKey) try { - Jwks.builder().setKey(badPubKey).build() + Jwks.builder().forKey(badPubKey).build() } catch (InvalidKeyException ike) { String curveId = jwk.get('crv') String msg = EcPublicJwkFactory.keyContainsErrorMessage(curveId) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy index 35056117b..40107fdbb 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy @@ -44,7 +44,7 @@ class RsaPrivateJwkFactoryTest { } try { - Jwks.builder().setKey(key).build() + Jwks.builder().forKey(key).build() fail() } catch (UnsupportedKeyException expected) { String msg = 'Unable to derive RSAPublicKey from RSAPrivateKey implementation ' + @@ -119,7 +119,7 @@ class RsaPrivateJwkFactoryTest { } try { - Jwks.builder().setKey(key).build() + Jwks.builder().forKey(key).build() fail() } catch (UnsupportedKeyException expected) { String prefix = 'Unable to derive RSAPublicKey from RSAPrivateKey {kty=RSA}. Cause: ' @@ -140,7 +140,7 @@ class RsaPrivateJwkFactoryTest { //build up test key: RSAMultiPrimePrivateCrtKey key = new TestRSAMultiPrimePrivateCrtKey(priv, infos) - RsaPrivateJwk jwk = Jwks.builder().setKey(key).build() + RsaPrivateJwk jwk = Jwks.builder().forKey(key).build() List oth = jwk.get('oth') as List assertTrue oth instanceof List @@ -163,11 +163,11 @@ class RsaPrivateJwkFactoryTest { RSAPrivateCrtKey priv = pair.private RSAPublicKey pub = pair.public - RsaPrivateJwk jwk = Jwks.builder().setKey(priv).setPublicKey(pub).build() + RsaPrivateJwk jwk = Jwks.builder().forKey(priv).setPublicKey(pub).build() // an RSAMultiPrimePrivateCrtKey without OtherInfo elements is treated the same as a normal RSAPrivateCrtKey, // so ensure they are equal: RSAMultiPrimePrivateCrtKey key = new TestRSAMultiPrimePrivateCrtKey(priv, null) - RsaPrivateJwk jwk2 = Jwks.builder().setKey(key).setPublicKey(pub).build() + RsaPrivateJwk jwk2 = Jwks.builder().forKey(key).setPublicKey(pub).build() assertEquals jwk, jwk2 assertNull jwk.get(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO.getId()) assertNull jwk2.get(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO.getId()) @@ -181,7 +181,7 @@ class RsaPrivateJwkFactoryTest { def priv = new TestRSAPrivateKey(pair.private) - RsaPrivateJwk jwk = Jwks.builder().setKey(priv).setPublicKey(pub).build() + RsaPrivateJwk jwk = Jwks.builder().forKey(priv).setPublicKey(pub).build() assertEquals 4, jwk.size() // kty, public exponent, modulus, private exponent assertEquals 'RSA', jwk.getType() assertEquals Converters.BIGINT.applyTo(pub.getModulus()), jwk.get(DefaultRsaPublicJwk.MODULUS.getId()) @@ -194,7 +194,7 @@ class RsaPrivateJwkFactoryTest { def pair = TestKeys.RS256.pair RSAPublicKey pub = pair.public RSAPrivateKey priv = new TestRSAPrivateKey(pair.private) - def jwk = Jwks.builder().setKey(priv).setPublicKey(pub).build() + def jwk = Jwks.builder().forKey(priv).setPublicKey(pub).build() //minimal values: kty, modulus, public exponent, private exponent = 4 fields: assertEquals 4, jwk.size() def map = new LinkedHashMap(jwk) @@ -217,7 +217,7 @@ class RsaPrivateJwkFactoryTest { def infos = [ info1, info2 ] RSAMultiPrimePrivateCrtKey key = new TestRSAMultiPrimePrivateCrtKey(priv, infos) - final RsaPrivateJwk jwk = Jwks.builder().setKey(key).setPublicKey(pub).build() + final RsaPrivateJwk jwk = Jwks.builder().forKey(key).setPublicKey(pub).build() //we have to test the class directly and override, since the dummy MultiPrime values won't be accepted by the //JVM: From a2b47cd695356a281c8995ab29bab8e0d28acff2 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 29 May 2022 14:06:18 -0700 Subject: [PATCH 70/75] Minor JavaDoc organization change --- .../security/ProtoJwkBuilder.java | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java index 270c5980c..f39a622ce 100644 --- a/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java @@ -53,28 +53,6 @@ public interface ProtoJwkBuilder, T extends JwkB */ RsaPublicJwkBuilder forKey(RSAPublicKey key); - /** - * Ensures the builder will create an {@link RsaPublicJwk} for the specified Java {@link X509Certificate} chain. - * The first {@code X509Certificate} in the chain (at array index 0) MUST contain an {@link RSAPublicKey} - * instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method. - * - * @param chain the {@link X509Certificate} chain to inspect to find the {@link RSAPublicKey} to represent as a - * {@link RsaPublicJwk}. - * @return the builder coerced as an {@link RsaPublicJwkBuilder}. - */ - RsaPublicJwkBuilder forRsaChain(X509Certificate... chain); - - /** - * Ensures the builder will create an {@link RsaPublicJwk} for the specified Java {@link X509Certificate} chain. - * The first {@code X509Certificate} in the chain (at list index 0) MUST contain an {@link RSAPublicKey} - * instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method. - * - * @param chain the {@link X509Certificate} chain to inspect to find the {@link RSAPublicKey} to represent as a - * {@link RsaPublicJwk}. - * @return the builder coerced as an {@link RsaPublicJwkBuilder}. - */ - RsaPublicJwkBuilder forRsaChain(List chain); - /** * Ensures the builder will create an {@link RsaPrivateJwk} for the specified Java {@link RSAPrivateKey}. If * possible, it is recommended to also call the resulting builder's @@ -96,6 +74,19 @@ public interface ProtoJwkBuilder, T extends JwkB */ EcPublicJwkBuilder forKey(ECPublicKey key); + /** + * Ensures the builder will create an {@link EcPrivateJwk} for the specified Java {@link ECPrivateKey}. If + * possible, it is recommended to also call the resulting builder's + * {@link EcPrivateJwkBuilder#setPublicKey(PublicKey) setPublicKey} method with the private key's matching + * {@link PublicKey} for better performance. See the + * {@link EcPrivateJwkBuilder#setPublicKey(PublicKey) setPublicKey} and {@link PrivateJwk} JavaDoc for more + * information. + * + * @param key the {@link ECPublicKey} to represent as an {@link EcPublicJwk}. + * @return the builder coerced as a {@link EcPrivateJwkBuilder}. + */ + EcPrivateJwkBuilder forKey(ECPrivateKey key); + /** * Ensures the builder will create an {@link EcPublicJwk} for the specified Java {@link X509Certificate} chain. * The first {@code X509Certificate} in the chain (at array index 0) MUST contain an {@link ECPublicKey} @@ -119,17 +110,39 @@ public interface ProtoJwkBuilder, T extends JwkB EcPublicJwkBuilder forEcChain(List chain); /** - * Ensures the builder will create an {@link EcPrivateJwk} for the specified Java {@link ECPrivateKey}. If - * possible, it is recommended to also call the resulting builder's - * {@link EcPrivateJwkBuilder#setPublicKey(PublicKey) setPublicKey} method with the private key's matching - * {@link PublicKey} for better performance. See the - * {@link EcPrivateJwkBuilder#setPublicKey(PublicKey) setPublicKey} and {@link PrivateJwk} JavaDoc for more - * information. + * Ensures the builder will create an {@link EcPrivateJwk} for the specified Java Elliptic Curve + * {@link KeyPair}. The pair's {@link KeyPair#getPublic() public key} MUST be an + * {@link ECPublicKey} instance. The pair's {@link KeyPair#getPrivate() private key} MUST be an + * {@link ECPrivateKey} instance. * - * @param key the {@link ECPublicKey} to represent as an {@link EcPublicJwk}. - * @return the builder coerced as a {@link EcPrivateJwkBuilder}. + * @param keyPair the EC {@link KeyPair} to represent as an {@link EcPrivateJwk}. + * @return the builder coerced as an {@link EcPrivateJwkBuilder}. + * @throws IllegalArgumentException if the {@code keyPair} does not contain {@link ECPublicKey} and + * {@link ECPrivateKey} instances. */ - EcPrivateJwkBuilder forKey(ECPrivateKey key); + EcPrivateJwkBuilder forEcKeyPair(KeyPair keyPair) throws IllegalArgumentException; + + /** + * Ensures the builder will create an {@link RsaPublicJwk} for the specified Java {@link X509Certificate} chain. + * The first {@code X509Certificate} in the chain (at array index 0) MUST contain an {@link RSAPublicKey} + * instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method. + * + * @param chain the {@link X509Certificate} chain to inspect to find the {@link RSAPublicKey} to represent as a + * {@link RsaPublicJwk}. + * @return the builder coerced as an {@link RsaPublicJwkBuilder}. + */ + RsaPublicJwkBuilder forRsaChain(X509Certificate... chain); + + /** + * Ensures the builder will create an {@link RsaPublicJwk} for the specified Java {@link X509Certificate} chain. + * The first {@code X509Certificate} in the chain (at list index 0) MUST contain an {@link RSAPublicKey} + * instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method. + * + * @param chain the {@link X509Certificate} chain to inspect to find the {@link RSAPublicKey} to represent as a + * {@link RsaPublicJwk}. + * @return the builder coerced as an {@link RsaPublicJwkBuilder}. + */ + RsaPublicJwkBuilder forRsaChain(List chain); /** * Ensures the builder will create an {@link RsaPrivateJwk} for the specified Java RSA @@ -143,17 +156,4 @@ public interface ProtoJwkBuilder, T extends JwkB * {@link RSAPrivateKey} instances. */ RsaPrivateJwkBuilder forRsaKeyPair(KeyPair keyPair) throws IllegalArgumentException; - - /** - * Ensures the builder will create an {@link EcPrivateJwk} for the specified Java Elliptic Curve - * {@link KeyPair}. The pair's {@link KeyPair#getPublic() public key} MUST be an - * {@link ECPublicKey} instance. The pair's {@link KeyPair#getPrivate() private key} MUST be an - * {@link ECPrivateKey} instance. - * - * @param keyPair the EC {@link KeyPair} to represent as an {@link EcPrivateJwk}. - * @return the builder coerced as an {@link EcPrivateJwkBuilder}. - * @throws IllegalArgumentException if the {@code keyPair} does not contain {@link ECPublicKey} and - * {@link ECPrivateKey} instances. - */ - EcPrivateJwkBuilder forEcKeyPair(KeyPair keyPair) throws IllegalArgumentException; } From 2637661b857b736355effe76f77eac6f9ef8963d Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 29 May 2022 17:30:15 -0700 Subject: [PATCH 71/75] Changed JweBuilder to have only two encryptWith* methods for consistency with JwtBuilder signWith* methods. Also prevents incorrect configuration by forgetting to call follow-up methods. --- .../main/java/io/jsonwebtoken/JweBuilder.java | 64 +++++++++++-------- .../jsonwebtoken/impl/DefaultJweBuilder.java | 41 ++++++------ .../groovy/io/jsonwebtoken/JwtsTest.groovy | 20 ++---- .../impl/DefaultJweBuilderTest.groovy | 20 ++---- .../jsonwebtoken/impl/DefaultJweTest.groovy | 2 +- .../security/RFC7516AppendixA3Test.groovy | 3 +- .../impl/security/RFC7517AppendixCTest.groovy | 3 +- .../impl/security/RFC7518AppendixCTest.groovy | 3 +- 8 files changed, 73 insertions(+), 83 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/JweBuilder.java b/api/src/main/java/io/jsonwebtoken/JweBuilder.java index 891a10537..7378baafa 100644 --- a/api/src/main/java/io/jsonwebtoken/JweBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JweBuilder.java @@ -17,6 +17,7 @@ import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.KeyAlgorithms; import javax.crypto.SecretKey; import java.security.Key; @@ -29,38 +30,49 @@ public interface JweBuilder extends JwtBuilder { /** - * Encrypts the constructed JWT with the specified {@link AeadAlgorithm} Content Encryption Algorithm. The - * key used to perform the encryption must be supplied by calling {@link #withKey(SecretKey)} or - * {@link #withKeyFrom(Key, KeyAlgorithm)}. + * Encrypts the constructed JWT with the specified {@code enc}ryption algorithm using the provided + * symmetric {@code key}. Because it is a symmetric key, the party decrypting the resulting + * JWE must also have access to the same key. + * + *

    This method is a convenience method that delegates to + * {@link #encryptWith(AeadAlgorithm, Key, KeyAlgorithm) encryptWith(enc, key, KeyAlgorithm)} + * based on the {@code key} argument:

    + *
      + *
    • If the provided {@code key} is an instance of {@link io.jsonwebtoken.security.PasswordKey PasswordKey}, + * the {@code KeyAlgorithm} used will be one of the three JWA-standard password-based key algorithms + * ({@link KeyAlgorithms#PBES2_HS256_A128KW PBES2_HS256_A128KW}, + * {@link KeyAlgorithms#PBES2_HS384_A192KW PBES2_HS384_A192KW}, or + * {@link KeyAlgorithms#PBES2_HS512_A256KW PBES2_HS512_A256KW}) as determined by the {@code enc} algorithm's + * {@link AeadAlgorithm#getKeyBitLength() key length} requirement.
    • + *
    • If the {@code key} is otherwise a standard {@code SecretKey}, the {@code KeyAlgorithm} will be + * {@link KeyAlgorithms#DIRECT}, indicating that {@code key} should be used directly with the + * {@code enc} algorithm. In this case, the {@code key} argument MUST be of sufficient strength to + * use with the specified {@code enc} algorithm, otherwise an exception will be thrown during encryption. If + * desired, secure-random keys suitable for an {@link AeadAlgorithm} may be generated using the algorithm's + * {@link AeadAlgorithm#keyBuilder() keyBuilder}.
    • + *
    * * @param enc the {@link AeadAlgorithm} algorithm used to encrypt the JWE. - * @return the builder for method chaining. + * @param key the symmetric encryption key to use with the {@code enc} algorithm. + * @return the JWE builder for method chaining. */ - JweBuilder encryptWith(AeadAlgorithm enc); + JweBuilder encryptWith(AeadAlgorithm enc, SecretKey key); /** - * Specifies the shared symmetric key to use to encrypt the JWE using the AEAD content encryption algorithm - * specified via the {@link #encryptWith(AeadAlgorithm)} builder method. - * - *

    This is a convenience method that is an alias for the following:

    - * - *
    -     * {@link #withKeyFrom(Key, KeyAlgorithm) withKeyFrom}(key, {@link io.jsonwebtoken.security.KeyAlgorithms KeyAlgorithms}.{@link io.jsonwebtoken.security.KeyAlgorithms#DIRECT DIRECT});
    + * Encrypts the constructed JWT with the specified {@code enc} algorithm using the symmetric key produced by + * the {@code keyAlg} when invoked with the specified {@code key}. In other words, the {@code keyAlg} is first + * invoked with the specified {@code key}, and that produces a {@link SecretKey} result. This resulting + * {@code SecretKey} is then used with the {@code enc} algorithm to encrypt the JWE. * - * @param key the shared symmetric key to use to encrypt the JWE. - * @return the builder for method chaining. - */ - JweBuilder withKey(SecretKey key); - - /** - * Use the specified {@code key} to invoke the specified {@link KeyAlgorithm} to obtain a - * {@code Content Encryption Key (CEK)}. The resulting CEK will be used to encrypt the JWE using the - * AEAD content encryption algorithm specified via the {@link #encryptWith(AeadAlgorithm)} builder method. + *

    The {@link KeyAlgorithms} utility class makes available all Key Algorithms defined by the JWA + * specification.

    * - * @param key the key to use with the {@code keyAlg} to obtain a {@code Content Encryption Key (CEK)}. - * @param keyAlg the key algorithm that will provide a {@code Content Encryption Key (CEK)}. - * @param the type of key to use with {@code keyAlg} - * @return the builder for method chaining. + * @param enc the {@link AeadAlgorithm} used to encrypt the JWE. + * @param key the key used to call the provided {@code keyAlg} instance. + * @param keyAlg the key management algorithm that will produce the symmetric {@code SecretKey} to use with the + * {@code enc} algorithm. + * @param the type of key that must be used with the specified {@code keyAlg} instance. + * @return the JWE builder for method chaining. */ - JweBuilder withKeyFrom(K key, KeyAlgorithm keyAlg); + JweBuilder encryptWith(AeadAlgorithm enc, K key, KeyAlgorithm keyAlg); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java index d59c55fcb..8c316dae7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java @@ -7,6 +7,7 @@ import io.jsonwebtoken.impl.lang.Services; import io.jsonwebtoken.impl.security.DefaultAeadRequest; import io.jsonwebtoken.impl.security.DefaultKeyRequest; +import io.jsonwebtoken.impl.security.Pbes2HsAkwAlgorithm; import io.jsonwebtoken.io.SerializationException; import io.jsonwebtoken.io.Serializer; import io.jsonwebtoken.lang.Arrays; @@ -60,42 +61,38 @@ public JweBuilder setPayload(String payload) { } @Override - public JweBuilder encryptWith(final AeadAlgorithm enc) { + public JweBuilder encryptWith(AeadAlgorithm enc, SecretKey key) { + if (key instanceof PasswordKey) { + return encryptWith(enc, (PasswordKey) key, new Pbes2HsAkwAlgorithm(enc.getKeyBitLength())); + } + return encryptWith(enc, key, KeyAlgorithms.DIRECT); + } + + @Override + public JweBuilder encryptWith(final AeadAlgorithm enc, final K key, final KeyAlgorithm keyAlg) { this.enc = Assert.notNull(enc, "Encryption algorithm cannot be null."); - final String id = enc.getId(); - Assert.hasText(id, "Encryption algorithm id cannot be null or empty."); + final String encId = Assert.hasText(enc.getId(), "Encryption algorithm id cannot be null or empty."); this.encFunction = wrap(new Function() { @Override public AeadResult apply(AeadRequest request) { return enc.encrypt(request); } - }, "%s encryption failed.", id); - return this; - } + }, "%s encryption failed.", encId); - @Override - public JweBuilder withKey(SecretKey key) { - if (key instanceof PasswordKey) { - return withKeyFrom((PasswordKey) key, KeyAlgorithms.PBES2_HS512_A256KW); - } - return withKeyFrom(key, KeyAlgorithms.DIRECT); - } + this.key = Assert.notNull(key, "Key cannot be null."); - @Override - public JweBuilder withKeyFrom(K key, final KeyAlgorithm alg) { - this.key = Assert.notNull(key, "key cannot be null."); //noinspection unchecked - this.alg = (KeyAlgorithm) Assert.notNull(alg, "KeyAlgorithm cannot be null."); - final KeyAlgorithm keyAlg = this.alg; - final String id = alg.getId(); - Assert.hasText(id, "KeyAlgorithm id cannot be null or empty."); + this.alg = (KeyAlgorithm) Assert.notNull(keyAlg, "KeyAlgorithm cannot be null."); + final String algId = Assert.hasText(keyAlg.getId(), "KeyAlgorithm id cannot be null or empty."); + final KeyAlgorithm alg = this.alg; String cekMsg = "Unable to obtain content encryption key from key management algorithm '%s'."; this.algFunction = Functions.wrap(new Function, KeyResult>() { @Override public KeyResult apply(KeyRequest request) { - return keyAlg.getEncryptionKey(request); + return alg.getEncryptionKey(request); } - }, SecurityException.class, cekMsg, id); + }, SecurityException.class, cekMsg, algId); + return this; } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 42b42438e..6ba8669c3 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -1060,7 +1060,7 @@ class JwtsTest { } } - def jwe = Jwts.jweBuilder().setSubject("joe").encryptWith(encAlg).withKey(key).compact() + def jwe = Jwts.jweBuilder().setSubject("joe").encryptWith(encAlg, key).compact() assertEquals 'joe', Jwts.parserBuilder() .addEncryptionAlgorithms([encAlg]) @@ -1272,8 +1272,7 @@ class JwtsTest { // encrypt: String jwe = Jwts.jweBuilder() .claim('foo', 'bar') - .encryptWith(enc) - .withKeyFrom(key, alg) + .encryptWith(enc, key, alg) .compact() //decrypt: @@ -1301,8 +1300,7 @@ class JwtsTest { String jwe = Jwts.jweBuilder() .claim('foo', 'bar') .compressWith(codec) - .encryptWith(enc) - .withKey(key) + .encryptWith(enc, key) .compact() //decompress and decrypt: @@ -1331,8 +1329,7 @@ class JwtsTest { // encrypt: String jwe = Jwts.jweBuilder() .claim('foo', 'bar') - .encryptWith(enc) - .withKeyFrom(key, alg) + .encryptWith(enc, key, alg) .compact() //decrypt: @@ -1353,8 +1350,7 @@ class JwtsTest { // encrypt: String jwe = Jwts.jweBuilder() .claim('foo', 'bar') - .encryptWith(EncryptionAlgorithms.A256GCM) - .withKey(key) // does not use 'withKeyFrom', should default to strongest PBES2_HS512_A256KW + .encryptWith(EncryptionAlgorithms.A256GCM, key) // should auto choose KeyAlg PBES2_HS512_A256KW .compact() //decrypt: @@ -1387,8 +1383,7 @@ class JwtsTest { // encrypt: String jwe = Jwts.jweBuilder() .claim('foo', 'bar') - .encryptWith(enc) // does not use 'withKeyFrom' - .withKeyFrom(pubKey, alg) + .encryptWith(enc, pubKey, alg) .compact() //decrypt: @@ -1423,8 +1418,7 @@ class JwtsTest { // encrypt: String jwe = Jwts.jweBuilder() .claim('foo', 'bar') - .encryptWith(enc) - .withKeyFrom(pubKey, alg) + .encryptWith(enc, pubKey, alg) .compact() //decrypt: diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy index 3db9cd74a..5df978945 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy @@ -4,7 +4,8 @@ import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.EncryptionAlgorithms import org.junit.Test -import static org.junit.Assert.* +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail class DefaultJweBuilderTest { @@ -40,21 +41,11 @@ class DefaultJweBuilderTest { } } - @Test - void testCompactWithoutEncryptionAlgorithm() { - def key = EncryptionAlgorithms.A128GCM.keyBuilder().build() - try { - builder().setIssuer("me").withKey(key).compact() - } catch (IllegalStateException ise) { - assertEquals 'Encryption algorithm is required.', ise.message - } - } - @Test void testCompactSimplestPayload() { def enc = EncryptionAlgorithms.A128GCM def key = enc.keyBuilder().build() - def jwe = builder().setPayload("me").encryptWith(enc).withKey(key).compact() + def jwe = builder().setPayload("me").encryptWith(enc, key).compact() def jwt = Jwts.parserBuilder().decryptWith(key).build().parsePlaintextJwe(jwe) assertEquals 'me', jwt.getBody() } @@ -63,7 +54,7 @@ class DefaultJweBuilderTest { void testCompactSimplestClaims() { def enc = EncryptionAlgorithms.A128GCM def key = enc.keyBuilder().build() - def jwe = builder().setSubject('joe').encryptWith(enc).withKey(key).compact() + def jwe = builder().setSubject('joe').encryptWith(enc, key).compact() def jwt = Jwts.parserBuilder().decryptWith(key).build().parseClaimsJwe(jwe) assertEquals 'joe', jwt.getBody().getSubject() } @@ -89,8 +80,7 @@ class DefaultJweBuilderTest { new DefaultJweBuilder() .setSubject('joe') - .encryptWith(enc) - .withKey(key) + .encryptWith(enc, key) .compact() //TODO create assertions diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy index 79dbb1b36..8059d8484 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy @@ -13,7 +13,7 @@ class DefaultJweTest { void testEqualsAndHashCode() { def alg = EncryptionAlgorithms.A128CBC_HS256 def key = alg.keyBuilder().build() - String compact = Jwts.jweBuilder().claim('foo', 'bar').encryptWith(alg).withKey(key).compact() + String compact = Jwts.jweBuilder().claim('foo', 'bar').encryptWith(alg, key).compact() def parser = Jwts.parserBuilder().decryptWith(key).build() def jwe1 = parser.parseClaimsJwe(compact) def jwe2 = parser.parseClaimsJwe(compact) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy index e7675bc0b..537209fc6 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy @@ -116,8 +116,7 @@ class RFC7516AppendixA3Test { String compact = Jwts.jweBuilder() .setPayload(PLAINTEXT) - .encryptWith(enc) - .withKeyFrom(kek, KeyAlgorithms.A128KW) + .encryptWith(enc, kek, KeyAlgorithms.A128KW) .compact() assertEquals COMPLETE_JWE, compact diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy index 0066831c0..3e9874e66 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy @@ -313,8 +313,7 @@ class RFC7517AppendixCTest { .setHeader(Jwts.jweHeader() .setContentType('jwk+json') .setPbes2Count(RFC_P2C)) - .encryptWith(encAlg) - .withKeyFrom(key, keyAlg) + .encryptWith(encAlg, key, keyAlg) .serializeToJsonWith(serializer) //ensure JJWT created the header as expected with an assertion serializer .compact() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy index 4fe9508e2..fa37cc558 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy @@ -101,8 +101,7 @@ class RFC7518AppendixCTest { .setAgreementPartyUInfo("Alice") .setAgreementPartyVInfo("Bob")) .claim("Hello", "World") - .encryptWith(EncryptionAlgorithms.A128GCM) - .withKeyFrom(bobJwk.toPublicJwk().toKey(), alg) + .encryptWith(EncryptionAlgorithms.A128GCM, bobJwk.toPublicJwk().toKey(), alg) .compact() // Ensure the protected header produced by JJWT is identical to the one in the RFC: From b190da0e643df8f5bdd04d91ed67d70a91485541 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 1 Jun 2022 19:34:00 -0700 Subject: [PATCH 72/75] Removed DefaultValueGetter in favor of new FieldReadable concept to leverage Field instances instead of duplicating logic. --- api/src/main/java/io/jsonwebtoken/Header.java | 2 +- .../main/java/io/jsonwebtoken/JweHeader.java | 247 ++++++++------ .../java/io/jsonwebtoken/ProtectedHeader.java | 79 ++--- .../jsonwebtoken/security/KeyAlgorithms.java | 6 +- .../impl/AbstractProtectedHeader.java | 10 +- .../jsonwebtoken/impl/DefaultJweHeader.java | 88 +++-- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 6 +- .../impl/DefaultTokenizedJwt.java | 3 +- .../java/io/jsonwebtoken/impl/JwtMap.java | 42 ++- .../java/io/jsonwebtoken/impl/lang/Bytes.java | 10 +- .../jsonwebtoken/impl/lang/FieldReadable.java | 6 + .../impl/lang/JwtDateConverter.java | 2 +- .../io/jsonwebtoken/impl/lang/Nameable.java | 6 + .../impl/lang/PositiveIntegerConverter.java | 2 +- .../impl/lang/PublicJwkConverter.java | 60 ---- .../impl/lang/RequiredBitLengthConverter.java | 36 ++ .../impl/lang/RequiredFieldReader.java | 45 +++ .../impl/security/AbstractJwk.java | 14 +- .../impl/security/AesGcmKeyAlgorithm.java | 14 +- .../impl/security/DefaultJwkContext.java | 22 +- .../impl/security/DefaultValueGetter.java | 161 --------- .../impl/security/EcPrivateJwkFactory.java | 9 +- .../impl/security/EcPublicJwkFactory.java | 15 +- .../impl/security/EcdhKeyAlgorithm.java | 48 ++- .../impl/security/JwkContext.java | 4 +- .../impl/security/JwkConverter.java | 123 +++++++ .../impl/security/Pbes2HsAkwAlgorithm.java | 12 +- .../security/RSAOtherPrimeInfoConverter.java | 23 +- .../impl/security/RsaPrivateJwkFactory.java | 53 ++- .../impl/security/RsaPublicJwkFactory.java | 9 +- .../impl/security/SecretJwkFactory.java | 7 +- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 5 +- .../impl/AbstractProtectedHeaderTest.groovy | 128 ++++++-- .../impl/DefaultClaimsTest.groovy | 2 +- .../impl/DefaultHeaderTest.groovy | 9 + .../impl/DefaultJweHeaderTest.groovy | 308 +++++++++++------- .../impl/DefaultJwsHeaderTest.groovy | 9 - .../io/jsonwebtoken/impl/JwtMapTest.groovy | 2 +- .../impl/security/AbstractJwkTest.groovy | 5 + .../security/AesGcmKeyAlgorithmTest.groovy | 30 +- .../security/DefaultJwkContextTest.groovy | 42 ++- .../security/DefaultValueGetterTest.groovy | 208 ------------ .../security/EcPrivateJwkFactoryTest.groovy | 25 ++ .../security/EcPublicJwkFactoryTest.groovy | 58 ++++ .../impl/security/EcdhKeyAlgorithmTest.groovy | 39 +-- .../impl/security/JwkConverterTest.groovy | 70 ++++ .../RSAOtherPrimeInfoConverterTest.groovy | 12 + 47 files changed, 1166 insertions(+), 950 deletions(-) create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/FieldReadable.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/Nameable.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/PublicJwkConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredBitLengthConverter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredFieldReader.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPrivateJwkFactoryTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPublicJwkFactoryTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkConverterTest.groovy diff --git a/api/src/main/java/io/jsonwebtoken/Header.java b/api/src/main/java/io/jsonwebtoken/Header.java index fe5e35370..78bfb8d51 100644 --- a/api/src/main/java/io/jsonwebtoken/Header.java +++ b/api/src/main/java/io/jsonwebtoken/Header.java @@ -37,7 +37,7 @@ * * @since 0.1 */ -public interface Header extends Map { +public interface Header> extends Map { /** * JWT {@code Type} (typ) value: "JWT" diff --git a/api/src/main/java/io/jsonwebtoken/JweHeader.java b/api/src/main/java/io/jsonwebtoken/JweHeader.java index 3000a1330..93ec2663d 100644 --- a/api/src/main/java/io/jsonwebtoken/JweHeader.java +++ b/api/src/main/java/io/jsonwebtoken/JweHeader.java @@ -15,8 +15,14 @@ */ package io.jsonwebtoken; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.EcPublicJwk; +import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.KeyAlgorithms; +import javax.crypto.SecretKey; +import java.security.Key; + /** * A JWE header. * @@ -25,119 +31,79 @@ public interface JweHeader extends ProtectedHeader { /** - * Returns the JWE {@code enc} (Encryption - * Algorithm) header value or {@code null} if not present. + * Returns the JWE {@code enc} (Encryption + * Algorithm) header value or {@code null} if not present. * *

    The JWE {@code enc} (encryption algorithm) Header Parameter identifies the content encryption algorithm * used to perform authenticated encryption on the plaintext to produce the ciphertext and the JWE * {@code Authentication Tag}.

    * + *

    Note that there is no corresponding 'setter' method for this 'getter' because JJWT users set this value by + * supplying an {@link AeadAlgorithm} to a {@link JweBuilder} via one of its + * {@link JweBuilder#encryptWith(AeadAlgorithm, SecretKey) encryptWith(AeadAlgorithm, SecretKey)} or + * {@link JweBuilder#encryptWith(AeadAlgorithm, Key, KeyAlgorithm) encryptWith(AeadAlgorithm, Key, KeyAlgorithm)} + * methods. JJWT will then set this {@code enc} header value automatically to the {@code AeadAlgorithm}'s + * {@link AeadAlgorithm#getId() getId()} value during encryption.

    + * * @return the JWE {@code enc} (Encryption Algorithm) header value or {@code null} if not present. This will * always be {@code non-null} on validly-constructed JWE instances, but could be {@code null} during construction. + * @see JweBuilder#encryptWith(AeadAlgorithm, SecretKey) + * @see JweBuilder#encryptWith(AeadAlgorithm, Key, KeyAlgorithm) */ String getEncryptionAlgorithm(); - //commented out on purpose - API users shouldn't call this method as it is always called by the Jwt/Jwe Builder // /** -// * Sets the JWE enc (Encryption -// * Algorithm) header value. A {@code null} value will remove the property from the JSON map. -// *

    The JWE {@code enc} (encryption algorithm) Header Parameter identifies the content encryption algorithm -// * used to perform authenticated encryption on the plaintext to produce the ciphertext and the JWE -// * {@code Authentication Tag}.

    +// * Sets the JWE {@code enc} (Encryption +// * Algorithm) header value. A {@code null} value will remove the property from the JSON map. // * -// * @param enc the encryption algorithm identifier +// *

    This should almost never be set by JJWT users directly - JJWT will always set this value to the value +// * returned by {@link AeadAlgorithm#getId()} when performing encryption, overwriting any potential previous +// * value.

    +// * +// * @param enc the encryption algorithm identifier obtained from {@link AeadAlgorithm#getId()}. // * @return this header for method chaining // */ +// @SuppressWarnings("UnusedReturnValue") // JweHeader setEncryptionAlgorithm(String enc); /** - * Returns the number of PBKDF2 iterations necessary to derive the key used to encrypt the JWE, or {@code null} - * if not present. Used with password-based {@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm}s. + * Returns the {@code epk} (Ephemeral + * Public Key) header value created by the JWE originator for use with key agreement algorithms, or + * {@code null} if not present. * - * @return the number of PBKDF2 iterations necessary to derive the key used to encrypt the JWE, or {@code null} - * if not present. - * @see JWE p2c (PBES2 Count) Header Parameter - * @see KeyAlgorithms#PBES2_HS256_A128KW - * @see KeyAlgorithms#PBES2_HS384_A192KW - * @see KeyAlgorithms#PBES2_HS512_A256KW - */ - Integer getPbes2Count(); - - /** - * Sets the number of PBKDF2 iterations necessary to derive the key used to encrypt the JWE. A {@code null} value - * will remove the property from the JSON map. - * - * @param count the number of PBKDF2 iterations necessary to derive the key used to encrypt the JWE. - * @return the header for method chaining - * @see JWE p2c (PBES2 Count) Header Parameter - * @see KeyAlgorithms#PBES2_HS256_A128KW - * @see KeyAlgorithms#PBES2_HS384_A192KW - * @see KeyAlgorithms#PBES2_HS512_A256KW - */ - JweHeader setPbes2Count(int count); - - /** - * Returns the PBKDF2 {@code Salt Input} value necessary to derive the key used to encrypt the JWE, or {@code null} - * if not present. Used with password-based {@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm}s. + *

    Note that there is no corresponding 'setter' method for this 'getter' because JJWT users set this value by + * supplying an ECDH-ES {@link KeyAlgorithm} to a {@link JweBuilder} via its + * {@link JweBuilder#encryptWith(AeadAlgorithm, Key, KeyAlgorithm) encryptWith(AeadAlgorithm, Key, KeyAlgorithm)} + * method. The ECDH-ES {@code KeyAlgorithm} implementation will then set this {@code epk} header value + * automatically when producing the encryption key.

    * - * @return the PBKDF2 {@code Salt Input} value necessary to derive the key used to encrypt the JWE, or {@code null} - * if not present. - * @see JWE p2s (PBES2 Salt Input) Header Parameter - * @see KeyAlgorithms#PBES2_HS256_A128KW - * @see KeyAlgorithms#PBES2_HS384_A192KW - * @see KeyAlgorithms#PBES2_HS512_A256KW - */ - byte[] getPbes2Salt(); - - /** - * Sets the PBKDF2 {@code Salt Input} value necessary to derive the key used to encrypt the JWE. This should - * almost never be used by JJWT users directly - it should be automatically generated and set within a PBKDF2-based - * {@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm} implementation. - * - * @param salt the PBKDF2 {@code Salt Input} value necessary to derive the key used to encrypt the JWE. - * @return the header for method chaining - * @see JWE p2s (PBES2 Salt Input) Header Parameter - * @see KeyAlgorithms#PBES2_HS256_A128KW - * @see KeyAlgorithms#PBES2_HS384_A192KW - * @see KeyAlgorithms#PBES2_HS512_A256KW - */ - JweHeader setPbes2Salt(byte[] salt); - - /** - * Returns any information about the JWE producer for use with key agreement algorithms, or {@code null} if not - * present. - * - * @return any information about the JWE producer for use with key agreement algorithms, or {@code null} if not - * present. - * JWE apu (Agreement PartyUInfo) Header Parameter + * @return the {@code epk} (Ephemeral + * Public Key) header value created by the JWE originator for use with key agreement algorithms, or + * {@code null} if not present. * @see KeyAlgorithms#ECDH_ES * @see KeyAlgorithms#ECDH_ES_A128KW * @see KeyAlgorithms#ECDH_ES_A192KW * @see KeyAlgorithms#ECDH_ES_A256KW */ - byte[] getAgreementPartyUInfo(); + EcPublicJwk getEphemeralPublicKey(); /** - * Returns any information about the JWE producer for use with key agreement algorithms as a UTF-8 String, - * or {@code null} if not present. - * - *

    If not {@code null}, this is a convenience method that returns the equivalent of the following:

    - *
    -     * new String({@link #getAgreementPartyUInfo() getAgreementPartyUInfo()}, StandardCharsets.UTF_8)
    + * Returns any information about the JWE producer for use with key agreement algorithms, or {@code null} if not + * present. * * @return any information about the JWE producer for use with key agreement algorithms, or {@code null} if not * present. - * JWE apu (Agreement PartyUInfo) Header Parameter + * @see JWE apu (Agreement PartyUInfo) Header Parameter * @see KeyAlgorithms#ECDH_ES * @see KeyAlgorithms#ECDH_ES_A128KW * @see KeyAlgorithms#ECDH_ES_A192KW * @see KeyAlgorithms#ECDH_ES_A256KW */ - String getAgreementPartyUInfoString(); + byte[] getAgreementPartyUInfo(); /** - * Sets any information about the JWE producer for use with key agreement algorithms. A {@code null} value removes - * the property from the JSON map. + * Sets any information about the JWE producer for use with key agreement algorithms. A {@code null} or empty value + * removes the property from the JSON map. * * @param info information about the JWE producer to use with key agreement algorithms. * @return the header for method chaining. @@ -173,7 +139,7 @@ public interface JweHeader extends ProtectedHeader { * * @return any information about the JWE recipient for use with key agreement algorithms, or {@code null} if not * present. - * JWE apv (Agreement PartyVInfo) Header Parameter + * @see JWE apv (Agreement PartyVInfo) Header Parameter * @see KeyAlgorithms#ECDH_ES * @see KeyAlgorithms#ECDH_ES_A128KW * @see KeyAlgorithms#ECDH_ES_A192KW @@ -181,24 +147,6 @@ public interface JweHeader extends ProtectedHeader { */ byte[] getAgreementPartyVInfo(); - /** - * Returns any information about the JWE recipient for use with key agreement algorithms as a UTF-8 String, - * or {@code null} if not present. - * - *

    If not {@code null}, this is a convenience method that returns the equivalent of the following:

    - *
    -     * new String({@link #getAgreementPartyVInfo() getAgreementPartyVInfo()}, StandardCharsets.UTF_8)
    - * - * @return any information about the JWE recipient for use with key agreement algorithms, or {@code null} if not - * present. - * JWE apv (Agreement PartyVInfo) Header Parameter - * @see KeyAlgorithms#ECDH_ES - * @see KeyAlgorithms#ECDH_ES_A128KW - * @see KeyAlgorithms#ECDH_ES_A192KW - * @see KeyAlgorithms#ECDH_ES_A256KW - */ - String getAgreementPartyVInfoString(); - /** * Sets any information about the JWE recipient for use with key agreement algorithms. A {@code null} value removes * the property from the JSON map. @@ -230,4 +178,111 @@ public interface JweHeader extends ProtectedHeader { * @see KeyAlgorithms#ECDH_ES_A256KW */ JweHeader setAgreementPartyVInfo(String info); + + /** + * Returns the 96-bit "iv" + * (Initialization Vector) generated during key encryption, or {@code null} if not present. + * Set by AES GCM {@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm} implementations. + * + *

    Note that there is no corresponding 'setter' method for this 'getter' because JJWT users set this value by + * supplying an AES GCM Wrap {@link KeyAlgorithm} to a {@link JweBuilder} via its + * {@link JweBuilder#encryptWith(AeadAlgorithm, Key, KeyAlgorithm) encryptWith(AeadAlgorithm, Key, KeyAlgorithm)} + * method. The AES GCM Wrap {@code KeyAlgorithm} implementation will then set this {@code iv} header value + * automatically when producing the encryption key.

    + * + * @return the 96-bit initialization vector generated during key encryption, or {@code null} if not present. + * @see KeyAlgorithms#A128GCMKW + * @see KeyAlgorithms#A192GCMKW + * @see KeyAlgorithms#A256GCMKW + */ + byte[] getInitializationVector(); + + /** + * Returns the 128-bit "tag" + * (Authentication Tag) resulting from key encryption, or {@code null} if not present. + * + *

    Note that there is no corresponding 'setter' method for this 'getter' because JJWT users set this value by + * supplying an AES GCM Wrap {@link KeyAlgorithm} to a {@link JweBuilder} via its + * {@link JweBuilder#encryptWith(AeadAlgorithm, Key, KeyAlgorithm) encryptWith(AeadAlgorithm, Key, KeyAlgorithm)} + * method. The AES GCM Wrap {@code KeyAlgorithm} implementation will then set this {@code tag} header value + * automatically when producing the encryption key.

    + * + * @return the 128-bit authentication tag resulting from key encryption, or {@code null} if not present. + * @see KeyAlgorithms#A128GCMKW + * @see KeyAlgorithms#A192GCMKW + * @see KeyAlgorithms#A256GCMKW + */ + byte[] getAuthenticationTag(); + + /** + * Returns the number of PBKDF2 iterations necessary to derive the key used during JWE encryption, or {@code null} + * if not present. Used with password-based {@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm}s. + * + * @return the number of PBKDF2 iterations necessary to derive the key used during JWE encryption, or {@code null} + * if not present. + * @see JWE p2c (PBES2 Count) Header Parameter + * @see KeyAlgorithms#PBES2_HS256_A128KW + * @see KeyAlgorithms#PBES2_HS384_A192KW + * @see KeyAlgorithms#PBES2_HS512_A256KW + */ + Integer getPbes2Count(); + + /** + * Sets the number of PBKDF2 iterations necessary to derive the key used during JWE encryption. If this value + * is not set when a password-based {@link KeyAlgorithm} is used, JJWT will automatically choose a suitable + * number of iterations based on + * OWASP PBKDF2 Iteration Recommendations. + * + *

    Minimum Count

    + * + *

    {@code IllegalArgumentException} will be thrown during encryption if a specified {@code count} is + * less than 1000 (one thousand), which is the + * minimum number recommended by the + * JWA specification. Anything less is susceptible to security attacks so the default PBKDF2 + * {@code KeyAlgorithm} implementations reject such values.

    + * + * @param count the number of PBKDF2 iterations necessary to derive the key used during JWE encryption, must be + * greater than or equal to 1000 (one thousand). + * @return the header for method chaining + * + * @see JWE p2c (PBES2 Count) Header Parameter + * @see KeyAlgorithms#PBES2_HS256_A128KW + * @see KeyAlgorithms#PBES2_HS384_A192KW + * @see KeyAlgorithms#PBES2_HS512_A256KW + * @see OWASP PBKDF2 Iteration Recommendations + */ + JweHeader setPbes2Count(int count); + + /** + * Returns the PBKDF2 {@code Salt Input} value necessary to derive the key used during JWE encryption, or + * {@code null} if not present. + * + *

    Note that there is no corresponding 'setter' method for this 'getter' because JJWT users set this value by + * supplying a password-based {@link KeyAlgorithm} to a {@link JweBuilder} via its + * {@link JweBuilder#encryptWith(AeadAlgorithm, Key, KeyAlgorithm) encryptWith(AeadAlgorithm, Key, KeyAlgorithm)} + * method. The password-based {@code KeyAlgorithm} implementation will then set this {@code p2s} header value + * automatically when producing the encryption key.

    + * + * @return the PBKDF2 {@code Salt Input} value necessary to derive the key used during JWE encryption, or + * {@code null} if not present. + * @see JWE p2s (PBES2 Salt Input) Header Parameter + * @see KeyAlgorithms#PBES2_HS256_A128KW + * @see KeyAlgorithms#PBES2_HS384_A192KW + * @see KeyAlgorithms#PBES2_HS512_A256KW + */ + byte[] getPbes2Salt(); + +// /** +// * Sets the PBKDF2 {@code Salt Input} value necessary to derive the key used during JWE encryption. This should +// * almost never be used by JJWT users directly - it should instead be automatically generated and set within a +// * PBKDF2-based {@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm} implementation. +// * +// * @param salt the PBKDF2 {@code Salt Input} value necessary to derive the key used during JWE encryption. +// * @return the header for method chaining +// * @see JWE p2s (PBES2 Salt Input) Header Parameter +// * @see KeyAlgorithms#PBES2_HS256_A128KW +// * @see KeyAlgorithms#PBES2_HS384_A192KW +// * @see KeyAlgorithms#PBES2_HS512_A256KW +// */ +// JweHeader setPbes2Salt(byte[] salt); } diff --git a/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java index 23c1ad857..0f3017b3a 100644 --- a/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java +++ b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java @@ -21,9 +21,9 @@ public interface ProtectedHeader> extends Header * Returns the {@code jku} (JWK Set URL) value that refers to a * JWK Set * resource containing JSON-encoded Public Keys, or {@code null} if not present. When present in a - * {@link JwsHeader}, the first public key in the JWK Set must be the public key used to sign the JWS. - * When present in a {@link JweHeader}, the first public key in the JWK Set must be the public key used - * during encryption. + * {@link JwsHeader}, the first public key in the JWK Set must be the public key complement of the private + * key used to sign the JWS. When present in a {@link JweHeader}, the first public key in the JWK Set must + * be the public key used during encryption. * * @return a URI that refers to a JWK Set * resource for a set of JSON-encoded Public Keys, or {@code null} if not present. @@ -33,11 +33,12 @@ public interface ProtectedHeader> extends Header URI getJwkSetUrl(); /** - * Sets the {@code jku} (JWK Set URL) value that refers to a JWK Set + * Sets the {@code jku} (JWK Set URL) value that refers to a + * JWK Set * resource containing JSON-encoded Public Keys, or {@code null} if not present. When set for a - * {@link JwsHeader}, the first public key in the JWK Set must be the public key used to sign the JWS. - * When set for a {@link JweHeader}, the first public key in the JWK Set must be the public key used - * during encryption. + * {@link JwsHeader}, the first public key in the JWK Set must be the public key complement of the + * private key used to sign the JWS. When set for a {@link JweHeader}, the first public key in the JWK Set + * must be the public key used during encryption. * * @param uri a URI that refers to a JWK Set * resource containing JSON-encoded Public Keys @@ -49,9 +50,9 @@ public interface ProtectedHeader> extends Header /** * Returns the {@code jwk} (JSON Web Key) associated with the JWT. When present in a {@link JwsHeader}, the - * {@code jwk} corresponds to the public key used to digitally sign the JWS. When present in a {@link JweHeader}, - * the {@code jwk} is the public key to which the JWE was encrypted, and may be used to determine the private key - * needed to decrypt the JWE. + * {@code jwk} is the public key complement of the private key used to digitally sign the JWS. When present in a + * {@link JweHeader}, the {@code jwk} is the public key to which the JWE was encrypted, and may be used to + * determine the private key needed to decrypt the JWE. * * @return the {@code jwk} (JSON Web Key) associated with the header. * @see JWS {@code jwk} (JSON Web Key) Header Parameter @@ -61,9 +62,9 @@ public interface ProtectedHeader> extends Header /** * Sets the {@code jwk} (JSON Web Key) associated with the JWT. When set for a {@link JwsHeader}, the - * {@code jwk} corresponds to the public key used to digitally sign the JWS. When set for a {@link JweHeader}, - * the {@code jwk} is the public key to which the JWE was encrypted, and may be used to determine the private key - * needed to decrypt the JWE. + * {@code jwk} is the public key complement of the private key used to digitally sign the JWS. When set for a + * {@link JweHeader}, the {@code jwk} is the public key to which the JWE was encrypted, and may be used to + * determine the private key needed to decrypt the JWE. * * @param jwk the {@code jwk} (JSON Web Key) associated with the header. * @return the header for method chaining @@ -77,7 +78,7 @@ public interface ProtectedHeader> extends Header * *

    The keyId header parameter is a hint indicating which key was used to secure a JWS or JWE. This * parameter allows originators to explicitly signal a change of key to recipients. The structure of the keyId - * value is unspecified. Its value is a case-sensitive string.

    + * value is unspecified. Its value is a CaSe-SeNsItIvE string.

    * *

    When used with a JWK, the keyId value is used to match a JWK {@code keyId} parameter value.

    * @@ -109,9 +110,9 @@ public interface ProtectedHeader> extends Header * chain associated with the JWT, or {@code null} if not present. * *

    When present in a {@link JwsHeader}, the certificate or certificate chain - * corresponds to the public key used to digitally sign the JWS. When present in a {@link JweHeader}, the - * certificate or certificate chain corresponds to the public key to which the JWE was encrypted, and may be - * used to determine the private key needed to decrypt the JWE.

    + * corresponds to the public key complement of the private key used to digitally sign the JWS. When present in a + * {@link JweHeader}, the certificate or certificate chain corresponds to the public key to which the JWE was + * encrypted, and may be used to determine the private key needed to decrypt the JWE.

    * *

    Each certificate in the resource MUST be in PEM-encoded form, with each certificate delimited as * specified in Section 6.1 of RFC 4945.

    @@ -127,10 +128,10 @@ public interface ProtectedHeader> extends Header * Sets the {@code x5u} (X.509 URL) that refers to a resource for the X.509 public key certificate or certificate * chain associated with the JWT. A {@code null} value will remove the property from the JSON map. * - *

    When set for a {@link JwsHeader}, the certificate or certificate chain - * corresponds to the public key used to digitally sign the JWS. When present in a {@link JweHeader}, the - * certificate or certificate chain corresponds to the public key to which the JWE was encrypted, and may be - * used to determine the private key needed to decrypt the JWE.

    + *

    When set for a {@link JwsHeader}, the certificate or first certificate in the chain contains + * the public key complement of the private key used to digitally sign the JWS. When present in a + * {@link JweHeader}, the certificate or first certificate in the chain contains the public key to which the JWE was + * encrypted, and may be used to determine the private key needed to decrypt the JWE.

    * *

    Each certificate in the resource MUST be in PEM-encoded form, with each certificate delimited as * specified in Section 6.1 of RFC 4945.

    @@ -147,9 +148,10 @@ public interface ProtectedHeader> extends Header * Returns the {@code x5c} (X.509 Certificate Chain) associated with the JWT, or {@code null} if not present. * *

    When present in a {@link JwsHeader}, - * the first certificate (at list index 0) corresponds to the public key used to digitally sign the JWS. When - * present in a {@link JweHeader}, the first certificate (at list index 0) corresponds to the public key to which - * the JWE was encrypted, and may be used to determine the private key needed to decrypt the JWE.

    + * the first certificate (at list index 0) is the public key complement of the private key used to digitally sign + * the JWS. When present in a {@link JweHeader}, the first certificate (at list index 0) contains the + * public key to which the JWE was encrypted, and may be used to determine the private key needed to decrypt the + * JWE.

    * *

    The initial certificate MAY be followed by additional certificates, with each subsequent * certificate being the one used to certify the previous one.

    @@ -164,11 +166,10 @@ public interface ProtectedHeader> extends Header * Sets the {@code x5c} (X.509 Certificate Chain) associated with the JWT. A {@code null} value will remove the * property from the JSON map. * - *

    When set for a {@link JwsHeader}, - * the first certificate (at list index 0) MUST correspond to the public key used to digitally sign the - * JWS. When set for a {@link JweHeader}, the first certificate (at list index 0) MUST correspond to the - * public key to which the JWE was encrypted, and may be used to determine the private key needed to decrypt the - * JWE.

    + *

    When set for a {@link JwsHeader}, the first certificate (at list index 0) MUST contain the + * public key complement of the private key used to digitally sign the JWS. When set for a {@link JweHeader}, the + * first certificate (at list index 0) MUST contain the public key to which the JWE was encrypted, and + * may be used to determine the private key needed to decrypt the JWE.

    * *

    The initial certificate MAY be followed by additional certificates, with each subsequent * certificate being the one used to certify the previous one.

    @@ -184,9 +185,9 @@ public interface ProtectedHeader> extends Header * Returns the {@code x5t} (X.509 Certificate SHA-1 Thumbprint) (a.k.a. digest) of the DER-encoding of the * X.509 Certificate associated with the JWT, or {@code null} if not present. * - *

    When present in a {@link JwsHeader}, it is the thumbprint of the X.509 certificate corresponding to the key - * used to digitally sign the JWS. When present in a {@link JweHeader}, it is the thumbprint of the X.509 - * Certificate corresponding to the public key to which the JWE was encrypted, and may be used to determine the + *

    When present in a {@link JwsHeader}, it is the thumbprint of the X.509 certificate complement of the private + * key used to digitally sign the JWS. When present in a {@link JweHeader}, it is the thumbprint of the X.509 + * Certificate containing the public key to which the JWE was encrypted, and may be used to determine the * private key needed to decrypt the JWE.

    * *

    Note that certificate thumbprints are also sometimes known as certificate fingerprints.

    @@ -203,9 +204,9 @@ public interface ProtectedHeader> extends Header * X.509 Certificate associated with the JWT. A {@code null} value will remove the * property from the JSON map. * - *

    When set for a {@link JwsHeader}, it is the thumbprint of the X.509 certificate corresponding to the key + *

    When set for a {@link JwsHeader}, it is the thumbprint of the X.509 certificate complement of the private key * used to digitally sign the JWS. When set for a {@link JweHeader}, it is the thumbprint of the X.509 - * Certificate corresponding to the public key to which the JWE was encrypted, and may be used to determine the + * Certificate containing the public key to which the JWE was encrypted, and may be used to determine the * private key needed to decrypt the JWE.

    * *

    Note that certificate thumbprints are also sometimes known as certificate fingerprints.

    @@ -222,9 +223,9 @@ public interface ProtectedHeader> extends Header * Returns the {@code x5t#S256} (X.509 Certificate SHA-256 Thumbprint) (a.k.a. digest) of the DER-encoding of the * X.509 Certificate associated with the JWT, or {@code null} if not present. * - *

    When present in a {@link JwsHeader}, it is the thumbprint of the X.509 certificate corresponding to the key - * used to digitally sign the JWS. When present in a {@link JweHeader}, it is the thumbprint of the X.509 - * Certificate corresponding to the public key to which the JWE was encrypted, and may be used to determine the + *

    When present in a {@link JwsHeader}, it is the thumbprint of the X.509 certificate complement of the private + * key used to digitally sign the JWS. When present in a {@link JweHeader}, it is the thumbprint of the X.509 + * Certificate containing to the public key to which the JWE was encrypted, and may be used to determine the * private key needed to decrypt the JWE.

    * *

    Note that certificate thumbprints are also sometimes known as certificate fingerprints.

    @@ -241,9 +242,9 @@ public interface ProtectedHeader> extends Header * X.509 Certificate associated with the JWT. A {@code null} value will remove the * property from the JSON map. * - *

    When set for a {@link JwsHeader}, it is the thumbprint of the X.509 certificate corresponding to the key + *

    When set for a {@link JwsHeader}, it is the thumbprint of the X.509 certificate complement of the private key * used to digitally sign the JWS. When set for a {@link JweHeader}, it is the thumbprint of the X.509 - * Certificate corresponding to the public key to which the JWE was encrypted, and may be used to determine the + * Certificate containing the public key to which the JWE was encrypted, and may be used to determine the * private key needed to decrypt the JWE.

    * *

    Note that certificate thumbprints are also sometimes known as certificate fingerprints.

    diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index 0f2cdae3f..c2384218a 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -304,7 +304,7 @@ private static T forId0(String id) { * OWASP * PBKDF2 recommendations and then that value is set as the JWE header {@code pbes2Count} value.
  • *
  • Generates a new secure-random salt input and sets it as the JWE header - * {@link JweHeader#setPbes2Salt(byte[]) pbes2Salt} value.
  • + * {@link JweHeader#getPbes2Salt() pbes2Salt} value. *
  • Derives a 128-bit Key Encryption Key with the PBES2-HS256 password-based key derivation algorithm, * using the provided password, iteration count, and input salt as arguments.
  • *
  • Generates a new secure-random Content Encryption {@link SecretKey} suitable for use with a @@ -349,7 +349,7 @@ private static T forId0(String id) { * OWASP * PBKDF2 recommendations and then that value is set as the JWE header {@code pbes2Count} value.
  • *
  • Generates a new secure-random salt input and sets it as the JWE header - * {@link JweHeader#setPbes2Salt(byte[]) pbes2Salt} value.
  • + * {@link JweHeader#getPbes2Salt() pbes2Salt} value. *
  • Derives a 192-bit Key Encryption Key with the PBES2-HS384 password-based key derivation algorithm, * using the provided password, iteration count, and input salt as arguments.
  • *
  • Generates a new secure-random Content Encryption {@link SecretKey} suitable for use with a @@ -394,7 +394,7 @@ private static T forId0(String id) { * OWASP * PBKDF2 recommendations and then that value is set as the JWE header {@code pbes2Count} value.
  • *
  • Generates a new secure-random salt input and sets it as the JWE header - * {@link JweHeader#setPbes2Salt(byte[]) pbes2Salt} value.
  • + * {@link JweHeader#getPbes2Salt() pbes2Salt} value. *
  • Derives a 256-bit Key Encryption Key with the PBES2-HS512 password-based key derivation algorithm, * using the provided password, iteration count, and input salt as arguments.
  • *
  • Generates a new secure-random Content Encryption {@link SecretKey} suitable for use with a diff --git a/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java index db8cfdd3c..13e216d04 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java @@ -3,9 +3,9 @@ import io.jsonwebtoken.ProtectedHeader; import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.impl.lang.Fields; -import io.jsonwebtoken.impl.lang.PublicJwkConverter; import io.jsonwebtoken.impl.security.AbstractAsymmetricJwk; import io.jsonwebtoken.impl.security.AbstractJwk; +import io.jsonwebtoken.impl.security.JwkConverter; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.PublicJwk; @@ -25,9 +25,11 @@ public abstract class AbstractProtectedHeader> extends DefaultHeader implements ProtectedHeader { static final Field JKU = Fields.uri("jku", "JWK Set URL"); - @SuppressWarnings("rawtypes") - static final Field JWK = Fields.builder(PublicJwk.class).setId("jwk").setName("JSON Web Key") - .setConverter(new PublicJwkConverter()).build(); + + @SuppressWarnings("unchecked") + static final Field> JWK = Fields.builder((Class>) (Class) PublicJwk.class) + .setId("jwk").setName("JSON Web Key") + .setConverter(JwkConverter.PUBLIC_JWK).build(); static final Field> CRIT = Fields.stringSet("crit", "Critical"); static final Set> FIELDS = Collections.concat(DefaultHeader.FIELDS, CRIT, JKU, JWK, AbstractJwk.KID, diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java index e32e8f7f1..a16a18cd2 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java @@ -1,12 +1,15 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.impl.lang.Converters; import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.impl.lang.PositiveIntegerConverter; -import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.impl.lang.RequiredBitLengthConverter; +import io.jsonwebtoken.impl.security.JwkConverter; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.EcPublicJwk; import java.nio.charset.StandardCharsets; import java.util.Map; @@ -20,13 +23,26 @@ public class DefaultJweHeader extends AbstractProtectedHeader implements JweHeader { static final Field ENCRYPTION_ALGORITHM = Fields.string("enc", "Encryption Algorithm"); - public static final Field P2C = Fields.builder(Integer.class) - .setConverter(PositiveIntegerConverter.INSTANCE).setId("p2c").setName("PBES2 Count").build(); - public static final Field P2S = Fields.bytes("p2s", "PBES2 Salt Input").build(); + + public static final Field EPK = Fields.builder(EcPublicJwk.class) + .setId("epk").setName("Ephemeral Public Key") + .setConverter(JwkConverter.EC_PUBLIC_JWK).build(); static final Field APU = Fields.bytes("apu", "Agreement PartyUInfo").build(); static final Field APV = Fields.bytes("apv", "Agreement PartyVInfo").build(); - static final Set> FIELDS = Collections.concat(AbstractProtectedHeader.FIELDS, ENCRYPTION_ALGORITHM, P2C, P2S, APU, APV); + // https://datatracker.ietf.org/doc/html/rfc7518#section-4.7.1.1 says 96 bits required: + public static final Field IV = Fields.bytes("iv", "Initialization Vector") + .setConverter(new RequiredBitLengthConverter(Converters.BASE64URL_BYTES, 96)).build(); + + // https://datatracker.ietf.org/doc/html/rfc7518#section-4.7.1.2 says 128 bits required: + public static final Field TAG = Fields.bytes("tag", "Authentication Tag") + .setConverter(new RequiredBitLengthConverter(Converters.BASE64URL_BYTES, 128)).build(); + + public static final Field P2S = Fields.bytes("p2s", "PBES2 Salt Input").build(); + public static final Field P2C = Fields.builder(Integer.class) + .setConverter(PositiveIntegerConverter.INSTANCE).setId("p2c").setName("PBES2 Count").build(); + + static final Set> FIELDS = Collections.concat(AbstractProtectedHeader.FIELDS, ENCRYPTION_ALGORITHM, EPK, APU, APV, IV, TAG, P2S, P2C); public DefaultJweHeader() { super(FIELDS); @@ -53,23 +69,8 @@ public String getEncryptionAlgorithm() { // } @Override - public Integer getPbes2Count() { - return idiomaticGet(P2C); - } - - @Override - public JweHeader setPbes2Count(int count) { - put(P2C, count); - return this; - } - - public byte[] getPbes2Salt() { - return idiomaticGet(P2S); - } - - public JweHeader setPbes2Salt(byte[] salt) { - put(P2S, salt); - return this; + public EcPublicJwk getEphemeralPublicKey() { + return idiomaticGet(EPK); } @Override @@ -77,12 +78,6 @@ public byte[] getAgreementPartyUInfo() { return idiomaticGet(APU); } - @Override - public String getAgreementPartyUInfoString() { - byte[] bytes = getAgreementPartyUInfo(); - return Arrays.length(bytes) == 0 ? null : new String(bytes, StandardCharsets.UTF_8); - } - @Override public JweHeader setAgreementPartyUInfo(byte[] info) { put(APU, info); @@ -100,12 +95,6 @@ public byte[] getAgreementPartyVInfo() { return idiomaticGet(APV); } - @Override - public String getAgreementPartyVInfoString() { - byte[] bytes = getAgreementPartyVInfo(); - return Arrays.length(bytes) == 0 ? null : new String(bytes, StandardCharsets.UTF_8); - } - @Override public JweHeader setAgreementPartyVInfo(byte[] info) { put(APV, info); @@ -117,4 +106,35 @@ public JweHeader setAgreementPartyVInfo(String info) { byte[] bytes = Strings.hasText(info) ? info.getBytes(StandardCharsets.UTF_8) : null; return setAgreementPartyVInfo(bytes); } + + @Override + public byte[] getInitializationVector() { + return idiomaticGet(IV); + } + + @Override + public byte[] getAuthenticationTag() { + return idiomaticGet(TAG); + } + + public byte[] getPbes2Salt() { + return idiomaticGet(P2S); + } + +// @Override +// public JweHeader setPbes2Salt(byte[] salt) { +// put(P2S, salt); +// return this; +// } + + @Override + public Integer getPbes2Count() { + return idiomaticGet(P2C); + } + + @Override + public JweHeader setPbes2Count(int count) { + put(P2C, count); + return this; + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index a220d1dc4..40e0aeadf 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -119,7 +119,8 @@ public T setHeader(Header header) { @Override public T setHeader(Map header) { - this.header = new DefaultHeader<>(header); + //noinspection rawtypes + this.header = new DefaultHeader(header); return (T) this; } @@ -134,7 +135,8 @@ public T setHeaderParams(Map params) { protected Header ensureHeader() { if (this.header == null) { - this.header = new DefaultHeader<>(); + //noinspection rawtypes + this.header = new DefaultHeader(); } return this.header; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java index 2489b09e6..93993c3a7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java @@ -38,6 +38,7 @@ public Header createHeader(Map m) { if (Strings.hasText(getDigest())) { return new DefaultJwsHeader(m); } - return new DefaultHeader<>(m); + //noinspection unchecked + return new DefaultHeader(m); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 13f89eafd..318b45aa8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -16,9 +16,12 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.Nameable; import io.jsonwebtoken.impl.lang.RedactedSupplier; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Strings; import java.lang.reflect.Array; @@ -27,7 +30,7 @@ import java.util.Map; import java.util.Set; -public class JwtMap implements Map { +public class JwtMap implements Map, FieldReadable, Nameable { protected final Map> FIELDS; protected final Map values; // canonical values formatted per RFC requirements @@ -50,6 +53,11 @@ public JwtMap(Set> fieldSet, Map values) { putAll(values); } + @Override + public String getName() { + return "Map"; + } + public static boolean isReduceableToNull(Object v) { return v == null || (v instanceof String && !Strings.hasText((String) v)) || @@ -67,6 +75,18 @@ protected T idiomaticGet(Field field) { return (T) this.idiomaticValues.get(field.getId()); } + @SuppressWarnings("unchecked") + @Override + public T get(Field field) { + Assert.notNull(field, "Field cannot be null."); + final String id = Assert.hasText(field.getId(), "Field id cannot be null or empty."); + Object value = idiomaticValues.get(id); + if (value == null) { + return null; + } + return (T) value; // should always be the field type - if not, it's a misuse of the API + } + @Override public int size() { return values.size(); @@ -149,9 +169,19 @@ protected Object apply(Field field, Object rawValue) { Assert.notNull(idiomaticValue, "Converter's resulting idiomaticValue cannot be null."); canonicalValue = field.applyTo(idiomaticValue); Assert.notNull(canonicalValue, "Converter's resulting canonicalValue cannot be null."); - } catch (IllegalArgumentException e) { - Object sval = field.isSecret() ? RedactedSupplier.REDACTED_VALUE : rawValue; - String msg = "Invalid " + getName() + " " + field + " value: " + sval + ". Cause: " + e.getMessage(); + } catch (Exception e) { + StringBuilder sb = new StringBuilder(100); + sb.append("Invalid ").append(getName()).append(" ").append(field).append(" value"); + if (field.isSecret()) { + sb.append(" ").append(RedactedSupplier.REDACTED_VALUE); + } else //noinspection StatementWithEmptyBody + if (rawValue instanceof byte[]) { + // don't do anything + } else { + sb.append(": ").append(Objects.nullSafeToString(rawValue)); + } + sb.append(". ").append(e.getMessage()); + String msg = sb.toString(); throw new IllegalArgumentException(msg, e); } Object retval = nullSafePut(id, canonicalValue); @@ -159,10 +189,6 @@ protected Object apply(Field field, Object rawValue) { return retval; } - public String getName() { - return "Map"; - } - @Override public Object remove(Object key) { this.idiomaticValues.remove(key); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java index 4f9685dfc..289b7f760 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java @@ -60,15 +60,15 @@ public static int toInt(byte[] bytes) { public static byte[] concat(byte[]... arrays) { int len = 0; - int count = Arrays.length(arrays); - for (int i = 0; i < count; i++) { - len += arrays[i].length; + int numArrays = Arrays.length(arrays); + for (int i = 0; i < numArrays; i++) { + len += length(arrays[i]); } byte[] output = new byte[len]; int position = 0; if (len > 0) { for (byte[] array : arrays) { - int alen = Arrays.length(array); + int alen = length(array); if (alen > 0) { System.arraycopy(array, 0, output, position, alen); position += alen; @@ -83,7 +83,7 @@ public static int length(byte[] bytes) { } public static long bitLength(byte[] bytes) { - return bytes == null ? 0 : bytes.length * (long) Byte.SIZE; + return length(bytes) * (long) Byte.SIZE; } public static String bitsMsg(long bitLength) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldReadable.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldReadable.java new file mode 100644 index 000000000..42020d015 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldReadable.java @@ -0,0 +1,6 @@ +package io.jsonwebtoken.impl.lang; + +public interface FieldReadable { + + T get(Field field); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java index 849f11127..5a1a327d3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java @@ -62,7 +62,7 @@ public static Date toDate(Object v) { } else if (v instanceof String) { return parseIso8601Date((String) v); //ISO-8601 parsing since 0.10.0 } else { - String msg = "Cannot create Date from Object of type " + v.getClass().getName() + " with value: " + v; + String msg = "Cannot create Date from object of type " + v.getClass().getName() + "."; throw new IllegalArgumentException(msg); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Nameable.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Nameable.java new file mode 100644 index 000000000..2f1718033 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Nameable.java @@ -0,0 +1,6 @@ +package io.jsonwebtoken.impl.lang; + +public interface Nameable { + + String getName(); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/PositiveIntegerConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/PositiveIntegerConverter.java index 13124bcac..3c8695be6 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/PositiveIntegerConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/PositiveIntegerConverter.java @@ -32,7 +32,7 @@ public Integer applyFrom(Object o) { } } if (i <= 0) { - String msg = "Value is not a positive integer."; + String msg = "Value must be a positive integer."; throw new IllegalArgumentException(msg); } return i; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/PublicJwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/PublicJwkConverter.java deleted file mode 100644 index d44181a3c..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/PublicJwkConverter.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.jsonwebtoken.impl.lang; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.Jwk; -import io.jsonwebtoken.security.JwkBuilder; -import io.jsonwebtoken.security.Jwks; -import io.jsonwebtoken.security.PrivateJwk; -import io.jsonwebtoken.security.PublicJwk; -import io.jsonwebtoken.security.SecretJwk; - -import java.util.Map; - -@SuppressWarnings("rawtypes") -public class PublicJwkConverter implements Converter { - - @Override - public Object applyTo(PublicJwk publicJwk) { - return publicJwk; - } - - @Override - public PublicJwk applyFrom(Object o) { - Assert.notNull(o, "JWK argument cannot be null."); - if (o instanceof PublicJwk) { - return ((PublicJwk) o); - } - if (o instanceof Map) { - Map map = (Map) o; - JwkBuilder builder = Jwks.builder(); - for(Map.Entry entry : map.entrySet()) { - Object key = entry.getKey(); - Assert.notNull(key, "JWK map key cannot be null."); - if (!(key instanceof String)) { - String msg = "Unsupported 'jwk' map value - all JWK map keys must be Strings. Encountered key '" + - key + "' of type " + key.getClass().getName(); - throw new IllegalArgumentException(msg); - } - String skey = (String)key; - builder.put(skey, entry.getValue()); - } - Jwk jwk = builder.build(); - if (!(jwk instanceof PublicJwk)) { - String type; - if (jwk instanceof SecretJwk) { - type = "SecretJwk"; - } else { - // only other type remaining: - Assert.isInstanceOf(PrivateJwk.class, jwk, "Unexpected Jwk type - programming error. Please report this to the JJWT team."); - type = "PrivateJwk"; - } - String msg = "Unsupported JWK map - JWK values must represent a PublicJwk, not a " + type + "."; - throw new IllegalArgumentException(msg); - } - return ((PublicJwk) jwk); - } - String msg = "Unsupported value type - expected a Map or Jwk instance. Type found: " + - o.getClass().getName(); - throw new IllegalArgumentException(msg); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredBitLengthConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredBitLengthConverter.java new file mode 100644 index 000000000..ea8f3166e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredBitLengthConverter.java @@ -0,0 +1,36 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.lang.Assert; + +public class RequiredBitLengthConverter implements Converter { + + private final Converter converter; + + private final int bitLength; + + public RequiredBitLengthConverter(Converter converter, int bitLength) { + this.converter = Assert.notNull(converter, "Converter cannot be null."); + this.bitLength = Assert.gt(bitLength, 0, "bitLength must be greater than 0"); + } + + private byte[] assertLength(byte[] bytes) { + long len = Bytes.bitLength(bytes); + if (len != this.bitLength) { + String msg = "Byte array must be exactly " + Bytes.bitsMsg(this.bitLength) + ". Found " + Bytes.bitsMsg(len); + throw new IllegalArgumentException(msg); + } + return bytes; + } + + @Override + public Object applyTo(byte[] bytes) { + assertLength(bytes); + return this.converter.applyTo(bytes); + } + + @Override + public byte[] applyFrom(Object o) { + byte[] result = this.converter.applyFrom(o); + return assertLength(result); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredFieldReader.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredFieldReader.java new file mode 100644 index 000000000..cb5faffbd --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredFieldReader.java @@ -0,0 +1,45 @@ +package io.jsonwebtoken.impl.lang; + +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.impl.security.JwkContext; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.MalformedKeyException; + +public class RequiredFieldReader implements FieldReadable { + + private final FieldReadable src; + + public RequiredFieldReader(Header header) { + this(Assert.isInstanceOf(FieldReadable.class, header, "Header implementations must implement FieldReadable.")); + } + + public RequiredFieldReader(FieldReadable src) { + this.src = Assert.notNull(src, "Source FieldReadable cannot be null."); + Assert.isInstanceOf(Nameable.class, src, "FieldReadable implementations must implement Nameable."); + } + + private String name() { + return ((Nameable) this.src).getName(); + } + + private JwtException malformed(String msg) { + if (this.src instanceof JwkContext || this.src instanceof Jwk) { + return new MalformedKeyException(msg); + } else { + return new MalformedJwtException(msg); + } + } + + @Override + public T get(Field field) { + T value = this.src.get(field); + if (value == null) { + String msg = name() + " is missing required " + field + " value."; + throw malformed(msg); + } + return value; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java index cc62d2bb2..326cded48 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java @@ -1,7 +1,9 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.FieldReadable; import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.impl.lang.Nameable; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; @@ -13,7 +15,7 @@ import java.util.Map; import java.util.Set; -public abstract class AbstractJwk implements Jwk { +public abstract class AbstractJwk implements Jwk, FieldReadable, Nameable { static final Field ALG = Fields.string("alg", "Algorithm"); public static final Field KID = Fields.string("kid", "Key ID"); @@ -36,6 +38,11 @@ public String getType() { return this.context.getType(); } + @Override + public String getName() { + return this.context.getName(); + } + @Override public Set getOperations() { return Collections.immutable(this.context.getOperations()); @@ -90,6 +97,11 @@ public Object get(Object key) { } } + @Override + public T get(Field field) { + return this.context.get(field); + } + @Override public Set keySet() { return Collections.immutable(this.context.keySet()); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java index 218100640..54699a209 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java @@ -1,9 +1,11 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.impl.DefaultJweHeader; import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.CheckedFunction; -import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.DecryptionKeyRequest; @@ -55,8 +57,8 @@ public byte[] apply(Cipher cipher) throws Exception { String encodedIv = Encoders.BASE64URL.encode(iv); String encodedTag = Encoders.BASE64URL.encode(tag); - request.getHeader().put("iv", encodedIv); - request.getHeader().put("tag", encodedTag); + request.getHeader().put(DefaultJweHeader.IV.getId(), encodedIv); + request.getHeader().put(DefaultJweHeader.TAG.getId(), encodedTag); return new DefaultKeyResult(cek, ciphertext); } @@ -67,9 +69,9 @@ public SecretKey getDecryptionKey(DecryptionKeyRequest request) throw final SecretKey kek = assertKey(request); final byte[] cekBytes = Assert.notEmpty(request.getContent(), "Decryption request content (ciphertext) cannot be null or empty."); final JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); - final ValueGetter getter = new DefaultValueGetter(header); - final byte[] tag = getter.getRequiredBytes("tag", this.tagBitLength / Byte.SIZE); - final byte[] iv = getter.getRequiredBytes("iv", this.ivBitLength / Byte.SIZE); + final FieldReadable reader = new RequiredFieldReader(header); + final byte[] tag = reader.get(DefaultJweHeader.TAG); + final byte[] iv = reader.get(DefaultJweHeader.IV); final AlgorithmParameterSpec ivSpec = getIvSpec(iv); //for tagged GCM, the JCA spec requires that the tag be appended to the end of the ciphertext byte array: diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java index a25d174a2..6ad2def26 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java @@ -22,6 +22,7 @@ import java.net.URI; import java.security.Key; +import java.security.PrivateKey; import java.security.Provider; import java.security.PublicKey; import java.security.SecureRandom; @@ -91,11 +92,28 @@ private DefaultJwkContext(Set> fields, JwkContext other, boolean rem @Override public String getName() { - Object value = values.get(AbstractJwk.KTY.getId()); + String value = get(AbstractJwk.KTY); if (DefaultSecretJwk.TYPE_VALUE.equals(value)) { value = "Secret"; } - return value != null ? value + " JWK" : "JWK"; + StringBuilder sb = value != null ? new StringBuilder(value) : new StringBuilder(); + K key = getKey(); + if (key instanceof PublicKey) { + if (sb.length() != 0) { + sb.append(' '); + } + sb.append("Public"); + } else if (key instanceof PrivateKey) { + if (sb.length() != 0) { + sb.append(' '); + } + sb.append("Private"); + } + if (sb.length() != 0) { + sb.append(' '); + } + sb.append("JWK"); + return sb.toString(); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java deleted file mode 100644 index 165aca38e..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2022 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.impl.JwtMap; -import io.jsonwebtoken.impl.lang.Bytes; -import io.jsonwebtoken.impl.lang.RedactedSupplier; -import io.jsonwebtoken.impl.lang.ValueGetter; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.lang.Arrays; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.Jwk; -import io.jsonwebtoken.security.MalformedKeyException; - -import java.math.BigInteger; -import java.util.Map; - -/** - * Allows use af shared assertions across codebase, regardless of inheritance hierarchy. - */ -public class DefaultValueGetter implements ValueGetter { - - private final Map values; - - public DefaultValueGetter(Map values) { - this.values = Assert.notEmpty(values, "Values cannot be null or empty."); - } - - private String name() { - Object nameable = values; - if (nameable instanceof AbstractJwk) { - nameable = ((AbstractJwk) values).context; - } - if (nameable instanceof JwtMap) { - return ((JwtMap) nameable).getName(); - } else { - return "Map"; - } - } - - private JwtException malformed(String msg) { - if (values instanceof JwkContext || values instanceof Jwk) { - return new MalformedKeyException(msg); - } else { - return new MalformedJwtException(msg); - } - } - - protected Object getRequiredValue(String key) { - Object value = this.values.get(key); - if (value instanceof RedactedSupplier) { - value = ((RedactedSupplier) value).get(); - } - if (value == null) { - String msg = name() + " is missing required '" + key + "' value."; - throw malformed(msg); - } - return value; - } - - @Override - public String getRequiredString(String key) { - Object value = getRequiredValue(key); - if (!(value instanceof String)) { - String msg = name() + " '" + key + "' value must be a String. Actual type: " + value.getClass().getName(); - throw malformed(msg); - } - String sval = Strings.clean((String) value); - if (!Strings.hasText(sval)) { - String msg = name() + " '" + key + "' string value cannot be null or empty."; - throw malformed(msg); - } - return (String) value; - } - - @Override - public int getRequiredInteger(String key) { - Object value = getRequiredValue(key); - if (!(value instanceof Integer)) { - String msg = name() + " '" + key + "' value must be an Integer. Actual type: " + value.getClass().getName(); - throw malformed(msg); - } - return (Integer) value; - } - - @Override - public int getRequiredPositiveInteger(String key) { - int value = getRequiredInteger(key); - if (value <= 0) { - String msg = name() + " '" + key + "' value must be a positive Integer. Value: " + value; - throw malformed(msg); - } - return value; - } - - @Override - public byte[] getRequiredBytes(String key) { - String encoded = getRequiredString(key); // guaranteed to be non-null and non-empty - try { - return Decoders.BASE64URL.decode(encoded); - } catch (Exception e) { - String msg = name() + " '" + key + "' value is not a valid Base64URL String: " + e.getMessage(); - throw malformed(msg); - } - } - - @Override - public byte[] getRequiredBytes(String key, int requiredByteLength) { - byte[] decoded = getRequiredBytes(key); - int len = Arrays.length(decoded); - if (len != requiredByteLength) { - String msg = name() + " '" + key + "' decoded byte array must be " + Bytes.bytesMsg(requiredByteLength) + - " long. Actual length: " + Bytes.bytesMsg(len) + "."; - throw malformed(msg); - } - return decoded; - } - - @Override - public BigInteger getRequiredBigInt(String key, boolean sensitive) { - String s = getRequiredString(key); - try { - byte[] bytes = Decoders.BASE64URL.decode(s); - return new BigInteger(1, bytes); - } catch (Exception e) { - String msg = "Unable to decode " + name() + " '" + key + "' value"; - if (!sensitive) { - msg += " '" + s + "'"; - } - msg += " to BigInteger: " + e.getMessage(); - throw malformed(msg); - } - } - - @SuppressWarnings("unchecked") - @Override - public Map getRequiredMap(String key) { - Object value = getRequiredValue(key); - if (!(value instanceof Map)) { - String msg = name() + " '" + key + "' value must be a Map. Actual type: " + value.getClass().getName(); - throw malformed(msg); - } - return (Map) value; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java index 247b1afe3..b4ec02e51 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java @@ -1,7 +1,8 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.CheckedFunction; -import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.EcPrivateJwk; import io.jsonwebtoken.security.EcPublicJwk; @@ -56,9 +57,9 @@ protected EcPrivateJwk createJwkFromKey(JwkContext ctx) { @Override protected EcPrivateJwk createJwkFromValues(final JwkContext ctx) { - ValueGetter getter = new DefaultValueGetter(ctx); - String curveId = getter.getRequiredString(DefaultEcPublicJwk.CRV.getId()); - BigInteger d = getter.getRequiredBigInt(DefaultEcPrivateJwk.D.getId(), true); + FieldReadable reader = new RequiredFieldReader(ctx); + String curveId = reader.get(DefaultEcPublicJwk.CRV); + BigInteger d = reader.get(DefaultEcPrivateJwk.D); // We don't actually need the public x,y point coordinates for JVM lookup, but the // [JWA spec](https://tools.ietf.org/html/rfc7518#section-6.2.2) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java index add28b169..a2355bbe0 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java @@ -1,7 +1,8 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.CheckedFunction; -import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.EcPublicJwk; import io.jsonwebtoken.security.InvalidKeyException; @@ -30,11 +31,11 @@ protected static String keyContainsErrorMessage(String curveId) { return String.format(fmt, curveId, curveId); } - protected static String jwkContainsErrorMessage(String curveId, Map jwk) { + protected static String jwkContainsErrorMessage(String curveId, Map jwk) { Assert.hasText(curveId, "curveId cannot be null or empty."); String fmt = "EC JWK x,y coordinates do not exist on elliptic curve '%s'. This " + "could be due simply to an incorrectly-created JWK or possibly an attempted Invalid Curve Attack " + - "(see https://safecurves.cr.yp.to/twist.html for more information). JWK: %s"; + "(see https://safecurves.cr.yp.to/twist.html for more information)."; return String.format(fmt, curveId, jwk); } @@ -68,10 +69,10 @@ protected EcPublicJwk createJwkFromKey(JwkContext ctx) { @Override protected EcPublicJwk createJwkFromValues(final JwkContext ctx) { - ValueGetter getter = new DefaultValueGetter(ctx); - String curveId = getter.getRequiredString(DefaultEcPublicJwk.CRV.getId()); - BigInteger x = getter.getRequiredBigInt(DefaultEcPublicJwk.X.getId(), false); - BigInteger y = getter.getRequiredBigInt(DefaultEcPublicJwk.Y.getId(), false); + FieldReadable reader = new RequiredFieldReader(ctx); + String curveId = reader.get(DefaultEcPublicJwk.CRV); + BigInteger x = reader.get(DefaultEcPublicJwk.X); + BigInteger y = reader.get(DefaultEcPublicJwk.Y); ECParameterSpec spec = getCurveByJwaId(curveId); ECPoint point = new ECPoint(x, y); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java index fc5b50a75..9822053bd 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java @@ -1,9 +1,11 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.impl.DefaultJweHeader; import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.CheckedFunction; -import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.AeadAlgorithm; @@ -11,7 +13,6 @@ import io.jsonwebtoken.security.EcKeyAlgorithm; import io.jsonwebtoken.security.EcPublicJwk; import io.jsonwebtoken.security.InvalidKeyException; -import io.jsonwebtoken.security.Jwk; import io.jsonwebtoken.security.Jwks; import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.KeyLengthSupplier; @@ -29,17 +30,15 @@ import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.spec.ECParameterSpec; -import java.util.Map; /** * @since JJWT_RELEASE_VERSION */ class EcdhKeyAlgorithm extends CryptoAlgorithm - implements EcKeyAlgorithm { + implements EcKeyAlgorithm { protected static final String JCA_NAME = "ECDH"; protected static final String DEFAULT_ID = JCA_NAME + "-ES"; - protected static final String EPHEMERAL_PUBLIC_KEY = "epk"; // Per https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.2, 2nd paragraph: // Key derivation is performed using the Concat KDF, as defined in @@ -85,8 +84,8 @@ public byte[] apply(KeyAgreement keyAgreement) throws Exception { protected String getConcatKDFAlgorithmId(AeadAlgorithm enc) { return this.WRAP_ALG instanceof DirectKeyAlgorithm ? - Assert.hasText(enc.getId(), "AeadAlgorithm id cannot be null or empty.") : - getId(); + Assert.hasText(enc.getId(), "AeadAlgorithm id cannot be null or empty.") : + getId(); } private byte[] createOtherInfo(int keydatalen, String AlgorithmID, byte[] PartyUInfo, byte[] PartyVInfo) { @@ -101,17 +100,17 @@ private byte[] createOtherInfo(int keydatalen, String AlgorithmID, byte[] PartyU // Values and order defined in https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.2 and // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf section 5.8.1.2 : return Bytes.concat( - Bytes.toBytes(algIdBytes.length), algIdBytes, // AlgorithmID - Bytes.toBytes(PartyUInfo.length), PartyUInfo, // PartyUInfo - Bytes.toBytes(PartyVInfo.length), PartyVInfo, // PartyVInfo - Bytes.toBytes(keydatalen), // SuppPubInfo per https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.2 - Bytes.EMPTY // SuppPrivInfo empty per https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.2 + Bytes.toBytes(algIdBytes.length), algIdBytes, // AlgorithmID + Bytes.toBytes(PartyUInfo.length), PartyUInfo, // PartyUInfo + Bytes.toBytes(PartyVInfo.length), PartyVInfo, // PartyVInfo + Bytes.toBytes(keydatalen), // SuppPubInfo per https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.2 + Bytes.EMPTY // SuppPrivInfo empty per https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.2 ); } private int getKeyBitLength(AeadAlgorithm enc) { int bitLength = this.WRAP_ALG instanceof KeyLengthSupplier ? - ((KeyLengthSupplier)this.WRAP_ALG).getKeyBitLength() : enc.getKeyBitLength(); + ((KeyLengthSupplier) this.WRAP_ALG).getKeyBitLength() : enc.getKeyBitLength(); return Assert.gt(bitLength, 0, "Algorithm keyBitLength must be > 0"); } @@ -144,10 +143,10 @@ public KeyResult getEncryptionKey(KeyRequest request) throws SecurityExceptio final SecretKey derived = deriveKey(request, publicKey, genPrivKey); DefaultKeyRequest wrapReq = new DefaultKeyRequest<>(request.getProvider(), request.getSecureRandom(), - derived, request.getHeader(), request.getEncryptionAlgorithm()); + derived, request.getHeader(), request.getEncryptionAlgorithm()); KeyResult result = WRAP_ALG.getEncryptionKey(wrapReq); - header.put(EPHEMERAL_PUBLIC_KEY, jwk); + header.put(DefaultJweHeader.EPK.getId(), jwk); return result; } @@ -159,27 +158,20 @@ public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws Securi JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); D privateKey = Assert.notNull(request.getKey(), "Request key cannot be null."); - ValueGetter getter = new DefaultValueGetter(header); - Map epkValues = getter.getRequiredMap(EPHEMERAL_PUBLIC_KEY); - // This call will assert the EPK, if valid, is also on a JWA-supported NIST curve: - Jwk jwk = Jwks.builder().putAll(epkValues).build(); - if (!(jwk instanceof EcPublicJwk)) { - String msg = "JWE Header '" + EPHEMERAL_PUBLIC_KEY + "' (Ephemeral Public Key) value is not an " + - "EllipticCurve Public JWK as required."; - throw new InvalidKeyException(msg); - } - EcPublicJwk epk = (EcPublicJwk) jwk; + FieldReadable reader = new RequiredFieldReader(header); + EcPublicJwk epk = reader.get(DefaultJweHeader.EPK); + // While the EPK might be on a JWA-supported NIST curve, it must be on the private key's exact curve: if (!EcPublicJwkFactory.contains(privateKey.getParams().getCurve(), epk.toKey().getW())) { - String msg = "JWE Header '" + EPHEMERAL_PUBLIC_KEY + "' (Ephemeral Public Key) value does not represent " + - "a point on the expected curve."; + String msg = "JWE Header " + DefaultJweHeader.EPK + " value does not represent " + + "a point on the expected curve."; throw new InvalidKeyException(msg); } final SecretKey derived = deriveKey(request, epk.toKey(), privateKey); DecryptionKeyRequest unwrapReq = new DefaultDecryptionKeyRequest<>(request.getProvider(), - request.getSecureRandom(), derived, header, request.getEncryptionAlgorithm(), request.getContent()); + request.getSecureRandom(), derived, header, request.getEncryptionAlgorithm(), request.getContent()); return WRAP_ALG.getDecryptionKey(unwrapReq); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java index dd47d0ae1..0362e5fb6 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java @@ -1,6 +1,8 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.Nameable; import java.net.URI; import java.security.Key; @@ -12,7 +14,7 @@ import java.util.Map; import java.util.Set; -public interface JwkContext extends Identifiable, Map { +public interface JwkContext extends Identifiable, Map, FieldReadable, Nameable { JwkContext setId(String id); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java new file mode 100644 index 000000000..9d7c07ccd --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java @@ -0,0 +1,123 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.lang.Converter; +import io.jsonwebtoken.impl.lang.Nameable; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.EcPrivateJwk; +import io.jsonwebtoken.security.EcPublicJwk; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.JwkBuilder; +import io.jsonwebtoken.security.Jwks; +import io.jsonwebtoken.security.PrivateJwk; +import io.jsonwebtoken.security.PublicJwk; +import io.jsonwebtoken.security.RsaPrivateJwk; +import io.jsonwebtoken.security.RsaPublicJwk; +import io.jsonwebtoken.security.SecretJwk; + +import java.util.Map; + +public final class JwkConverter> implements Converter { + + @SuppressWarnings("unchecked") + public static final JwkConverter> PUBLIC_JWK = new JwkConverter<>((Class>) (Class) PublicJwk.class); + + public static final JwkConverter EC_PUBLIC_JWK = new JwkConverter<>(EcPublicJwk.class); + + private final Class desiredType; + + public JwkConverter(Class desiredType) { + this.desiredType = Assert.notNull(desiredType, "desiredType cannot be null."); + } + + @Override + public Object applyTo(T jwk) { + return desiredType.cast(jwk); + } + + private static String articleFor(String s) { + switch (s.charAt(0)) { + case 'E': // for Elliptic Curve + case 'R': // for RSA + return "an"; + default: + return "a"; + } + } + + private static String typeString(Jwk jwk) { + Assert.isInstanceOf(Nameable.class, jwk, "All JWK implementations must implement Nameable."); + return ((Nameable)jwk).getName(); + } + + private static String typeString(Class clazz) { + StringBuilder sb = new StringBuilder(); + if (SecretJwk.class.isAssignableFrom(clazz)) { + sb.append("Secret"); + } else if (RsaPublicJwk.class.isAssignableFrom(clazz) || RsaPrivateJwk.class.isAssignableFrom(clazz)) { + sb.append("RSA"); + } else if (EcPublicJwk.class.isAssignableFrom(clazz) || EcPrivateJwk.class.isAssignableFrom(clazz)) { + sb.append("EC"); + } + return typeString(sb, clazz); + } + + private static String typeString(StringBuilder sb, Class clazz) { + if (PublicJwk.class.isAssignableFrom(clazz)) { + if (sb.length() != 0) { + sb.append(' '); + } + sb.append("Public"); + } else if (PrivateJwk.class.isAssignableFrom(clazz)) { + if (sb.length() != 0) { + sb.append(' '); + } + sb.append("Private"); + } + if (sb.length() != 0) { + sb.append(' '); + } + sb.append("JWK"); + return sb.toString(); + } + + private IllegalArgumentException unexpectedIAE(Jwk jwk) { + String desired = typeString(this.desiredType); + String jwkType = typeString(jwk); + String msg = "Value must be " + articleFor(desired) + " " + desired + ", not " + + articleFor(jwkType) + " " + jwkType + "."; + return new IllegalArgumentException(msg); + } + + @Override + public T applyFrom(Object o) { + Assert.notNull(o, "JWK argument cannot be null."); + if (desiredType.isInstance(o)) { + return desiredType.cast(o); + } else if (o instanceof Jwk) { + throw unexpectedIAE((Jwk) o); + } + if (!(o instanceof Map)) { + String msg = "Value must be a Jwk or Map. Type found: " + o.getClass().getName() + "."; + throw new IllegalArgumentException(msg); + } + Map map = (Map) o; + JwkBuilder builder = Jwks.builder(); + for (Map.Entry entry : map.entrySet()) { + Object key = entry.getKey(); + Assert.notNull(key, "JWK map key cannot be null."); + if (!(key instanceof String)) { + String msg = "JWK map keys must be Strings. Encountered key '" + key + "' of type " + + key.getClass().getName() + "."; + throw new IllegalArgumentException(msg); + } + String skey = (String) key; + builder.put(skey, entry.getValue()); + } + + Jwk jwk = builder.build(); + if (desiredType.isInstance(jwk)) { + return desiredType.cast(jwk); + } + throw unexpectedIAE(jwk); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java index 3b55ba89e..4d3d5b887 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java @@ -6,7 +6,8 @@ import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.impl.lang.CheckedSupplier; import io.jsonwebtoken.impl.lang.Conditions; -import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.DecryptionKeyRequest; import io.jsonwebtoken.security.KeyAlgorithm; @@ -166,7 +167,7 @@ public KeyResult getEncryptionKey(KeyRequest request) throws Securi request.getSecureRandom(), derivedKek, request.getHeader(), request.getEncryptionAlgorithm()); KeyResult result = wrapAlg.getEncryptionKey(wrapReq); - request.getHeader().setPbes2Salt(inputSalt); //retain for recipients + request.getHeader().put(DefaultJweHeader.P2S.getId(), inputSalt); //retain for recipients return result; } @@ -176,11 +177,10 @@ public SecretKey getDecryptionKey(DecryptionKeyRequest request) thr JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); final PasswordKey key = Assert.notNull(request.getKey(), "Request Key cannot be null."); - - ValueGetter getter = new DefaultValueGetter(header); - final byte[] inputSalt = getter.getRequiredBytes(DefaultJweHeader.P2S.getId()); + FieldReadable reader = new RequiredFieldReader(header); + final byte[] inputSalt = reader.get(DefaultJweHeader.P2S); + final int iterations = reader.get(DefaultJweHeader.P2C); final byte[] rfcSalt = Bytes.concat(SALT_PREFIX, inputSalt); - final int iterations = getter.getRequiredPositiveInteger(DefaultJweHeader.P2C.getId()); final char[] password = key.getPassword(); // password will be safely cleaned/zeroed in deriveKey next: final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java index 3557512fe..1d35109e4 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java @@ -17,8 +17,9 @@ import io.jsonwebtoken.impl.lang.Converter; import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.FieldReadable; import io.jsonwebtoken.impl.lang.Fields; -import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.MalformedKeyException; @@ -61,17 +62,21 @@ public RSAOtherPrimeInfo applyFrom(Object o) { throw new MalformedKeyException("RSA JWK 'oth' (Other Prime Info) element map cannot be empty."); } - // Need a Context instance to satisfy the API contract of the getRequired* methods below. + // Need a Context instance to satisfy the API contract of the reader.get* methods below. JwkContext ctx = new DefaultJwkContext<>(FIELDS); - for (Map.Entry entry : m.entrySet()) { - String name = String.valueOf(entry.getKey()); - ctx.put(name, entry.getValue()); + try { + for (Map.Entry entry : m.entrySet()) { + String name = String.valueOf(entry.getKey()); + ctx.put(name, entry.getValue()); + } + } catch (Exception e) { + throw new MalformedKeyException(e.getMessage(), e); } - final ValueGetter getter = new DefaultValueGetter(ctx); - BigInteger prime = getter.getRequiredBigInt(PRIME_FACTOR.getId(), true); - BigInteger primeExponent = getter.getRequiredBigInt(FACTOR_CRT_EXPONENT.getId(), true); - BigInteger crtCoefficient = getter.getRequiredBigInt(FACTOR_CRT_COEFFICIENT.getId(), true); + FieldReadable reader = new RequiredFieldReader(ctx); + BigInteger prime = reader.get(PRIME_FACTOR); + BigInteger primeExponent = reader.get(FACTOR_CRT_EXPONENT); + BigInteger crtCoefficient = reader.get(FACTOR_CRT_COEFFICIENT); return new RSAOtherPrimeInfo(prime, primeExponent, crtCoefficient); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java index 92742d8b8..72d40faf8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java @@ -1,10 +1,9 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.CheckedFunction; -import io.jsonwebtoken.impl.lang.Converter; -import io.jsonwebtoken.impl.lang.Converters; import io.jsonwebtoken.impl.lang.Field; -import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; @@ -32,14 +31,11 @@ class RsaPrivateJwkFactory extends AbstractFamilyJwkFactory> OPTIONAL_PRIVATE_FIELDS = Collections.setOf( - DefaultRsaPrivateJwk.FIRST_PRIME, DefaultRsaPrivateJwk.SECOND_PRIME, - DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, - DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT + DefaultRsaPrivateJwk.FIRST_PRIME, DefaultRsaPrivateJwk.SECOND_PRIME, + DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, + DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT ); - static final Converter, Object> RSA_OTHER_PRIMES_CONVERTER = - Converters.forList(RSAOtherPrimeInfoConverter.INSTANCE); - private static final String PUBKEY_ERR_MSG = "JwkContext publicKey must be an " + RSAPublicKey.class.getName() + " instance."; RsaPrivateJwkFactory() { @@ -59,12 +55,12 @@ private static BigInteger getPublicExponent(RSAPrivateKey key) { } String msg = "Unable to derive RSAPublicKey from RSAPrivateKey implementation [" + - key.getClass().getName() + "]. Supported keys implement the " + - RSAPrivateCrtKey.class.getName() + " or " + RSAMultiPrimePrivateCrtKey.class.getName() + - " interfaces. If the specified RSAPrivateKey cannot be one of these two, you must explicitly " + - "provide an RSAPublicKey in addition to the RSAPrivateKey, as the " + - "[JWA RFC, Section 6.3.2](https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.2) " + - "requires public values to be present in private RSA JWKs."; + key.getClass().getName() + "]. Supported keys implement the " + + RSAPrivateCrtKey.class.getName() + " or " + RSAMultiPrimePrivateCrtKey.class.getName() + + " interfaces. If the specified RSAPrivateKey cannot be one of these two, you must explicitly " + + "provide an RSAPublicKey in addition to the RSAPrivateKey, as the " + + "[JWA RFC, Section 6.3.2](https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.2) " + + "requires public values to be present in private RSA JWKs."; throw new UnsupportedKeyException(msg); } @@ -126,7 +122,7 @@ protected RsaPrivateJwk createJwkFromKey(JwkContext ctx) { put(ctx, DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, ckey.getCrtCoefficient()); List infos = Arrays.asList(ckey.getOtherPrimeInfo()); if (!Collections.isEmpty(infos)) { - put(ctx,DefaultRsaPrivateJwk.OTHER_PRIMES_INFO, infos); + put(ctx, DefaultRsaPrivateJwk.OTHER_PRIMES_INFO, infos); } } @@ -136,8 +132,9 @@ protected RsaPrivateJwk createJwkFromKey(JwkContext ctx) { @Override protected RsaPrivateJwk createJwkFromValues(JwkContext ctx) { - final ValueGetter getter = new DefaultValueGetter(ctx); - final BigInteger privateExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.PRIVATE_EXPONENT.getId(), true); + final FieldReadable reader = new RequiredFieldReader(ctx); + + final BigInteger privateExponent = reader.get(DefaultRsaPrivateJwk.PRIVATE_EXPONENT); //The [JWA Spec, Section 6.3.2](https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.2) requires //RSA Private Keys to also encode the public key values, so we assert that we can acquire it successfully: @@ -165,26 +162,22 @@ protected RsaPrivateJwk createJwkFromValues(JwkContext ctx) { KeySpec spec; if (containsOptional) { //if any one optional field exists, they are all required per JWA Section 6.3.2: - BigInteger firstPrime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_PRIME.getId(), true); - BigInteger secondPrime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.SECOND_PRIME.getId(), true); - BigInteger firstCrtExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT.getId(), true); - BigInteger secondCrtExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT.getId(), true); - BigInteger firstCrtCoefficient = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT.getId(), true); + BigInteger firstPrime = reader.get(DefaultRsaPrivateJwk.FIRST_PRIME); + BigInteger secondPrime = reader.get(DefaultRsaPrivateJwk.SECOND_PRIME); + BigInteger firstCrtExponent = reader.get(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT); + BigInteger secondCrtExponent = reader.get(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT); + BigInteger firstCrtCoefficient = reader.get(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT); // Other Primes Info is actually optional even if the above ones are required: if (ctx.containsKey(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO.getId())) { - - Object value = ctx.get(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO.getId()); - List otherPrimes = RSA_OTHER_PRIMES_CONVERTER.applyFrom(value); - + List otherPrimes = reader.get(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO); RSAOtherPrimeInfo[] arr = new RSAOtherPrimeInfo[Collections.size(otherPrimes)]; otherPrimes.toArray(arr); - spec = new RSAMultiPrimePrivateCrtKeySpec(modulus, publicExponent, privateExponent, firstPrime, - secondPrime, firstCrtExponent, secondCrtExponent, firstCrtCoefficient, arr); + secondPrime, firstCrtExponent, secondCrtExponent, firstCrtCoefficient, arr); } else { spec = new RSAPrivateCrtKeySpec(modulus, publicExponent, privateExponent, firstPrime, secondPrime, - firstCrtExponent, secondCrtExponent, firstCrtCoefficient); + firstCrtExponent, secondCrtExponent, firstCrtCoefficient); } } else { spec = new RSAPrivateKeySpec(modulus, privateExponent); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java index f4d76f3c3..7048afcaf 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java @@ -1,7 +1,8 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.CheckedFunction; -import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; import io.jsonwebtoken.security.RsaPublicJwk; import java.math.BigInteger; @@ -27,9 +28,9 @@ protected RsaPublicJwk createJwkFromKey(JwkContext ctx) { @Override protected RsaPublicJwk createJwkFromValues(JwkContext ctx) { - ValueGetter getter = new DefaultValueGetter(ctx); - BigInteger modulus = getter.getRequiredBigInt(DefaultRsaPublicJwk.MODULUS.getId(), false); - BigInteger publicExponent = getter.getRequiredBigInt(DefaultRsaPublicJwk.PUBLIC_EXPONENT.getId(), false); + FieldReadable reader = new RequiredFieldReader(ctx); + BigInteger modulus = reader.get(DefaultRsaPublicJwk.MODULUS); + BigInteger publicExponent = reader.get(DefaultRsaPublicJwk.PUBLIC_EXPONENT); final RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent); RSAPublicKey key = generateKey(ctx, new CheckedFunction() { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java index 129e9b4b6..4dcb288e7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java @@ -1,6 +1,7 @@ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; @@ -60,8 +61,8 @@ protected SecretJwk createJwkFromKey(JwkContext ctx) { @Override protected SecretJwk createJwkFromValues(JwkContext ctx) { - ValueGetter getter = new DefaultValueGetter(ctx); - byte[] bytes = getter.getRequiredBytes(DefaultSecretJwk.K.getId()); + FieldReadable reader = new RequiredFieldReader(ctx); + byte[] bytes = reader.get(DefaultSecretJwk.K); SecretKey key = new SecretKeySpec(bytes, "AES"); ctx.setKey(key); return new DefaultSecretJwk(ctx); diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 6ba8669c3..e9c3f09f9 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -147,8 +147,7 @@ class JwtsTest { fail() } catch (MalformedJwtException e) { String expected = 'Invalid protected header: Invalid JWS header \'jku\' (JWK Set URL) value: 42. ' + - 'Cause: Values must be either String or java.net.URI instances. ' + - 'Value type found: java.lang.Integer.' + 'Values must be either String or java.net.URI instances. Value type found: java.lang.Integer.' assertEquals expected, e.getMessage() } } @@ -169,7 +168,7 @@ class JwtsTest { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) fail() } catch (MalformedJwtException e) { - String expected = 'Invalid claims: Invalid JWT Claim \'exp\' (Expiration Time) value: -42-. Cause: ' + + String expected = 'Invalid claims: Invalid JWT Claim \'exp\' (Expiration Time) value: -42-. ' + 'String value is not a JWT NumericDate, nor is it ISO-8601-formatted. All heuristics exhausted. ' + 'Cause: Unparseable date: "-42-"' assertEquals expected, e.getMessage() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy index dd999e18e..8373d162e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy @@ -1,6 +1,9 @@ package io.jsonwebtoken.impl +import io.jsonwebtoken.impl.security.Randoms import io.jsonwebtoken.impl.security.TestKeys +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.lang.Collections import io.jsonwebtoken.security.EcPrivateJwk import io.jsonwebtoken.security.EcPublicJwk import io.jsonwebtoken.security.Jwks @@ -16,11 +19,39 @@ class AbstractProtectedHeaderTest { @Before void setUp() { - header = new DefaultJwsHeader() // extends AbstractProtectedHeader + header = new AbstractProtectedHeader(AbstractProtectedHeader.FIELDS) {} } @Test - void testJku() { + void testKeyId() { + def kid = 'foo' + header.setKeyId(kid) + assertEquals kid, header.get('kid') + assertEquals kid, header.getKeyId() + } + + @Test + void testKeyIdNonString() { + try { + header.put('kid', 42) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWT header 'kid' (Key ID) value: 42. Unsupported value type. " + + "Expected: java.lang.String, found: java.lang.Integer" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testSetJku() { + URI uri = URI.create('https://github.com') + header.setJwkSetUrl(uri) + assertEquals uri.toString(), header.get('jku') + assertEquals uri, header.getJwkSetUrl() + } + + @Test + void testPutJkuUri() { URI uri = URI.create('https://google.com') header.put('jku', uri) assertEquals uri.toString(), header.get('jku') @@ -28,7 +59,7 @@ class AbstractProtectedHeaderTest { } @Test - void testJkuString() { //test canonical/idiomatic conversion + void testPutJkuString() { String url = 'https://google.com' URI uri = URI.create(url) header.put('jku', url) @@ -37,19 +68,15 @@ class AbstractProtectedHeaderTest { } @Test - void testX509Url() { - URI uri = URI.create('https://google.com') - header.setX509Url(uri) - assertEquals uri, header.getX509Url() - } - - @Test - void testX509UrlString() { //test canonical/idiomatic conversion - String url = 'https://google.com' - URI uri = URI.create(url) - header.put('x5u', url) - assertEquals url, header.get('x5u') - assertEquals uri, header.getX509Url() + void testPutJkuNonString() { + try { + header.put('jku', 42) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWT header 'jku' (JWK Set URL) value: 42. Values must be either String or " + + "java.net.URI instances. Value type found: java.lang.Integer." + assertEquals msg, expected.getMessage() + } } @Test @@ -70,8 +97,8 @@ class AbstractProtectedHeaderTest { header.put('jwk', 42) fail() } catch (IllegalArgumentException expected) { - String msg = "Invalid JWS header 'jwk' (JSON Web Key) value: 42. Cause: Unsupported value type - " + - "expected a Map or Jwk instance. Type found: java.lang.Integer" + String msg = "Invalid JWT header 'jwk' (JSON Web Key) value: 42. " + + "Value must be a Jwk or Map. Type found: java.lang.Integer." assertEquals msg, expected.getMessage() } } @@ -100,8 +127,8 @@ class AbstractProtectedHeaderTest { header.put('jwk', m) fail() } catch (IllegalArgumentException expected) { - String msg = "Invalid JWS header 'jwk' (JSON Web Key) value: {42=hello}. Cause: Unsupported 'jwk' map " + - "value - all JWK map keys must be Strings. Encountered key '42' of type java.lang.Integer" + String msg = "Invalid JWT header 'jwk' (JSON Web Key) value: {42=hello}. JWK map keys must be Strings. " + + "Encountered key '42' of type java.lang.Integer." assertEquals msg, expected.getMessage() } } @@ -113,8 +140,8 @@ class AbstractProtectedHeaderTest { header.put('jwk', jwk) fail() } catch (IllegalArgumentException expected) { - String msg = "Invalid JWS header 'jwk' (JSON Web Key) value: {kty=oct, k=}. Cause: " + - "Unsupported JWK map - JWK values must represent a PublicJwk, not a SecretJwk." + String msg = "Invalid JWT header 'jwk' (JSON Web Key) value: {kty=oct, k=}. " + + "Value must be a Public JWK, not a Secret JWK." assertEquals msg, expected.getMessage() } } @@ -126,14 +153,65 @@ class AbstractProtectedHeaderTest { header.put('jwk', jwk) fail() } catch (IllegalArgumentException expected) { - String msg = "Invalid JWS header 'jwk' (JSON Web Key) value: {kty=EC, crv=P-256, " + + String msg = "Invalid JWT header 'jwk' (JSON Web Key) value: {kty=EC, crv=P-256, " + "x=xNKMMIsawShLG4LYxpNP0gqdgK_K69UXCLt3AE3zp-Q, y=_vzQymVtA7RHRTfBWZo75mxPgDkE8g7bdHI3siSuJOk, " + - "d=}. Cause: Unsupported JWK map - JWK values must represent a PublicJwk, " + - "not a PrivateJwk." + "d=}. Value must be a Public JWK, not an EC Private JWK." assertEquals msg, expected.getMessage() } } + @Test + void testX509Url() { + URI uri = URI.create('https://google.com') + header.setX509Url(uri) + assertEquals uri, header.getX509Url() + } + + @Test + void testX509UrlString() { //test canonical/idiomatic conversion + String url = 'https://google.com' + URI uri = URI.create(url) + header.put('x5u', url) + assertEquals url, header.get('x5u') + assertEquals uri, header.getX509Url() + } + + @Test + void testX509CertChain() { + def bundle = TestKeys.RS256 + List encodedCerts = Collections.of(Encoders.BASE64.encode(bundle.cert.getEncoded())) + header.setX509CertificateChain(bundle.chain) + assertEquals bundle.chain, header.getX509CertificateChain() + assertEquals encodedCerts, header.get('x5c') + } + + @Test + void testX509CertSha1Thumbprint() { + byte[] thumbprint = new byte[16] // simulate + Randoms.secureRandom().nextBytes(thumbprint) + String encoded = Encoders.BASE64URL.encode(thumbprint) + header.setX509CertificateSha1Thumbprint(thumbprint) + assertArrayEquals thumbprint, header.getX509CertificateSha1Thumbprint() + assertEquals encoded, header.get('x5t') + } + + @Test + void testX509CertSha256Thumbprint() { + byte[] thumbprint = new byte[32] // simulate + Randoms.secureRandom().nextBytes(thumbprint) + String encoded = Encoders.BASE64URL.encode(thumbprint) + header.setX509CertificateSha256Thumbprint(thumbprint) + assertArrayEquals thumbprint, header.getX509CertificateSha256Thumbprint() + assertEquals encoded, header.get('x5t#S256') + } + + @Test + void testCritical() { + Set crits = Collections.setOf('foo', 'bar') + header.setCritical(crits) + assertEquals crits, header.getCritical() + } + @Test void testCritNull() { header.put('crit', null) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy index 9b445ab2e..8a61b009c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -344,7 +344,7 @@ class DefaultClaimsTest { claims.put(field.getId(), val) fail() } catch (IllegalArgumentException iae) { - String msg = "Invalid JWT Claim $field value: hi. Cause: Cannot create Date from Object of type io.jsonwebtoken.impl.DefaultClaimsTest\$1 with value: hi" + String msg = "Invalid JWT Claim $field value: hi. Cannot create Date from object of type io.jsonwebtoken.impl.DefaultClaimsTest\$1." assertEquals msg, iae.getMessage() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy index 707affcd2..421a1e171 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy @@ -42,6 +42,15 @@ class DefaultHeaderTest { assertEquals header.getContentType(), 'bar' } + @Test + void testAlgorithm() { + header.setAlgorithm('foo') + assertEquals 'foo', header.getAlgorithm() + + header = new DefaultHeader([alg: 'bar']) + assertEquals 'bar', header.getAlgorithm() + } + @Test void testSetCompressionAlgorithm() { header.setCompressionAlgorithm("DEF") diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy index 07c8aef4b..0f42675cf 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy @@ -3,14 +3,15 @@ package io.jsonwebtoken.impl import io.jsonwebtoken.impl.security.Randoms import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.Encoders -import io.jsonwebtoken.lang.Collections -import io.jsonwebtoken.security.EcPrivateJwk -import io.jsonwebtoken.security.EcPublicJwk import io.jsonwebtoken.security.Jwks import org.junit.Before import org.junit.Test import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPublicKey import java.util.concurrent.atomic.AtomicInteger import static org.junit.Assert.* @@ -27,15 +28,6 @@ class DefaultJweHeaderTest { header = new DefaultJweHeader() } - @Test - void testAlgorithm() { - header.setAlgorithm('foo') - assertEquals 'foo', header.getAlgorithm() - - header = new DefaultJweHeader([alg: 'bar']) - assertEquals 'bar', header.getAlgorithm() - } - @Test void testEncryptionAlgorithm() { header.put('enc', 'foo') @@ -46,60 +38,196 @@ class DefaultJweHeaderTest { } @Test - void testJwkSetUrl() { - URI uri = new URI('https://github.com/jwtk/jjwt') - header.setJwkSetUrl(uri) - assertEquals uri, header.getJwkSetUrl() - assert uri.toString(), header.get('jku') + void testGetName() { + assertEquals 'JWE header', header.getName() } @Test - void testJwk() { - EcPrivateJwk jwk = Jwks.builder().forEcKeyPair(TestKeys.ES256.pair).build() - EcPublicJwk pubJwk = jwk.toPublicJwk() - header.setJwk(pubJwk) - assertEquals pubJwk, header.getJwk() + void testEpkWithSecretJwk() { + def jwk = Jwks.builder().forKey(TestKeys.HS256).build() + def values = new LinkedHashMap(jwk) //extract values to remove JWK type + try { + header.put('epk', values) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'epk' (Ephemeral Public Key) value: {kty=oct, k=}. " + + "Value must be an EC Public JWK, not a Secret JWK." + assertEquals msg, expected.getMessage() + } } @Test - void testX509CertChain() { - def bundle = TestKeys.RS256 - List encodedCerts = Collections.of(Encoders.BASE64.encode(bundle.cert.getEncoded())) - header.setX509CertificateChain(bundle.chain) - assertEquals bundle.chain, header.getX509CertificateChain() - assertEquals encodedCerts, header.get('x5c') + void testEpkWithPrivateJwk() { + def jwk = Jwks.builder().forKey(TestKeys.ES256.pair.private as ECPrivateKey).build() + def values = new LinkedHashMap(jwk) //extract values to remove JWK type + try { + header.put('epk', values) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'epk' (Ephemeral Public Key) value: {kty=EC, crv=P-256, " + + "x=xNKMMIsawShLG4LYxpNP0gqdgK_K69UXCLt3AE3zp-Q, y=_vzQymVtA7RHRTfBWZo75mxPgDkE8g7bdHI3siSuJOk, " + + "d=}. Value must be an EC Public JWK, not an EC Private JWK." + assertEquals msg, expected.getMessage() + } } @Test - void testX509CertSha1Thumbprint() { - byte[] thumbprint = new byte[16] // simulate - Randoms.secureRandom().nextBytes(thumbprint) - String encoded = Encoders.BASE64URL.encode(thumbprint) - header.setX509CertificateSha1Thumbprint(thumbprint) - assertArrayEquals thumbprint, header.getX509CertificateSha1Thumbprint() - assertEquals encoded, header.get('x5t') + void testEpkWithRsaPublicJwk() { + def jwk = Jwks.builder().forKey(TestKeys.RS256.pair.public as RSAPublicKey).build() + def values = new LinkedHashMap(jwk) //extract values to remove JWK type + try { + header.put('epk', values) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'epk' (Ephemeral Public Key) value: {kty=RSA, " + + "n=zkH0MwxQ2cUFWsvOPVFqI_dk2EFTjQolCy97mI5_wYCbaOoZ9Rm7c675mAeemRtNzgNVEz7m298ENqNGqPk2Nv3pBJ_" + + "XCaybBlp61CLez7dQ2h5jUFEJ6FJcjeKHS-MwXr56t2ISdfLNMYtVIxjvXQcYx5VmS4mIqTxj5gVGtQVi0GXdH6SvpdKV" + + "0fjE9KOhjsdBfKQzZfcQlusHg8pThwvjpMwCZnkxCS0RKa9y4-5-7MkC33-8-neZUzS7b6NdFxh6T_pMXpkf8d81fzVo4" + + "ZBMloweW0_l8MOdVxeX7M_7XSC1ank5i3IEZcotLmJYMwEo7rMpZVLevEQ118Eo8Q, " + + "e=AQAB}. Value must be an EC Public JWK, not an RSA Public JWK." + assertEquals msg, expected.getMessage() + } } @Test - void testX509CertSha256Thumbprint() { - byte[] thumbprint = new byte[32] // simulate - Randoms.secureRandom().nextBytes(thumbprint) - String encoded = Encoders.BASE64URL.encode(thumbprint) - header.setX509CertificateSha256Thumbprint(thumbprint) - assertArrayEquals thumbprint, header.getX509CertificateSha256Thumbprint() - assertEquals encoded, header.get('x5t#S256') + void testEpkWithEcPublicJwkValues() { + def jwk = Jwks.builder().forKey(TestKeys.ES256.pair.public as ECPublicKey).build() + def values = new LinkedHashMap(jwk) //extract values to remove JWK type + header.put('epk', values) + assertEquals jwk, header.get('epk') } @Test - void testCritical() { - Set crits = Collections.setOf('foo', 'bar') - header.setCritical(crits) - assertEquals crits, header.getCritical() + void testEpkWithInvalidEcPublicJwk() { + def jwk = Jwks.builder().forKey(TestKeys.ES256.pair.public as ECPublicKey).build() + def values = new LinkedHashMap(jwk) // copy fields so we can mutate + // We have a public JWK for a point on the curve, now swap out the x coordinate for something invalid: + values.put('x', 'Kg') + try { + header.put('epk', values) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'epk' (Ephemeral Public Key) value: {kty=EC, crv=P-256, x=Kg, " + + "y=_vzQymVtA7RHRTfBWZo75mxPgDkE8g7bdHI3siSuJOk}. EC JWK x,y coordinates do not exist on " + + "elliptic curve 'P-256'. This could be due simply to an incorrectly-created JWK or possibly an " + + "attempted Invalid Curve Attack (see https://safecurves.cr.yp.to/twist.html for more " + + "information)." + assertEquals msg, expected.getMessage() + } } @Test - void testGetName() { - assertEquals 'JWE header', header.getName() + void testEpkWithEcPublicJwk() { + def jwk = Jwks.builder().forKey(TestKeys.ES256.pair.public as ECPublicKey).build() + header.put('epk', jwk) + assertEquals jwk, header.get('epk') + assertEquals jwk, header.getEphemeralPublicKey() + } + + @Test + void testAgreementPartyUInfo() { + String val = "Party UInfo" + byte[] info = val.getBytes(StandardCharsets.UTF_8) + header.setAgreementPartyUInfo(info) + assertArrayEquals info, header.getAgreementPartyUInfo() + } + + @Test + void testAgreementPartyUInfoString() { + String val = "Party UInfo" + byte[] info = val.getBytes(StandardCharsets.UTF_8) + header.setAgreementPartyUInfo(val) + assertArrayEquals info, header.getAgreementPartyUInfo() + } + + @Test + void testEmptyAgreementPartyUInfo() { + byte[] info = new byte[0] + header.setAgreementPartyUInfo(info) + assertNull header.getAgreementPartyUInfo() + } + + @Test + void testEmptyAgreementPartyUInfoString() { + String s = ' ' + header.setAgreementPartyUInfo(s) + assertNull header.getAgreementPartyUInfo() + } + + @Test + void testAgreementPartyVInfo() { + String val = "Party VInfo" + byte[] info = val.getBytes(StandardCharsets.UTF_8) + header.setAgreementPartyVInfo(info) + assertArrayEquals info, header.getAgreementPartyVInfo() + } + + @Test + void testAgreementPartyVInfoString() { + String val = "Party VInfo" + byte[] info = val.getBytes(StandardCharsets.UTF_8) + header.setAgreementPartyVInfo(val) + assertArrayEquals info, header.getAgreementPartyVInfo() + } + + @Test + void testEmptyAgreementPartyVInfo() { + byte[] info = new byte[0] + header.setAgreementPartyVInfo(info) + assertNull header.getAgreementPartyVInfo() + } + + @Test + void testEmptyAgreementPartyVInfoString() { + String s = ' ' + header.setAgreementPartyVInfo(s) + assertNull header.getAgreementPartyVInfo() + } + + @Test + void testIv() { + byte[] bytes = new byte[12] + Randoms.secureRandom().nextBytes(bytes) + header.put('iv', bytes) + assertEquals Encoders.BASE64URL.encode(bytes), header.get('iv') + assertTrue MessageDigest.isEqual(bytes, header.getInitializationVector()) + } + + @Test + void testIvWithIncorrectSize() { + byte[] bytes = new byte[7] + Randoms.secureRandom().nextBytes(bytes) + try { + header.put('iv', bytes) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'iv' (Initialization Vector) value. " + + "Byte array must be exactly 96 bits (12 bytes). Found 56 bits (7 bytes)" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testTag() { + byte[] bytes = new byte[16] + Randoms.secureRandom().nextBytes(bytes) + header.put('tag', bytes) + assertEquals Encoders.BASE64URL.encode(bytes), header.get('tag') + assertTrue MessageDigest.isEqual(bytes, header.getAuthenticationTag()) + } + + @Test + void testTagWithIncorrectSize() { + byte[] bytes = new byte[15] + Randoms.secureRandom().nextBytes(bytes) + try { + header.put('tag', bytes) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'tag' (Authentication Tag) value. " + + "Byte array must be exactly 128 bits (16 bytes). Found 120 bits (15 bytes)" + assertEquals msg, expected.getMessage() + } } @Test @@ -113,6 +241,7 @@ class DefaultJweHeaderTest { header.put('p2c', Short.MAX_VALUE) assertEquals 32767, header.getPbes2Count() } + @Test void testP2cInt() { header.put('p2c', Integer.MAX_VALUE) @@ -137,8 +266,7 @@ class DefaultJweHeaderTest { header.put('p2c', 0) fail() } catch (IllegalArgumentException expected) { - String msg = "Invalid JWE header 'p2c' (PBES2 Count) value: 0. " + - "Cause: Value is not a positive integer." + String msg = "Invalid JWE header 'p2c' (PBES2 Count) value: 0. Value must be a positive integer." assertEquals msg, expected.getMessage() } } @@ -149,8 +277,7 @@ class DefaultJweHeaderTest { header.put('p2c', -1) fail() } catch (IllegalArgumentException expected) { - String msg = "Invalid JWE header 'p2c' (PBES2 Count) value: -1. " + - "Cause: Value is not a positive integer." + String msg = "Invalid JWE header 'p2c' (PBES2 Count) value: -1. Value must be a positive integer." assertEquals msg, expected.getMessage() } } @@ -162,7 +289,7 @@ class DefaultJweHeaderTest { fail() } catch (IllegalArgumentException expected) { String msg = "Invalid JWE header 'p2c' (PBES2 Count) value: 9223372036854775807. " + - "Cause: Value cannot be represented as a java.lang.Integer." + "Value cannot be represented as a java.lang.Integer." assertEquals msg, expected.getMessage() } } @@ -175,16 +302,17 @@ class DefaultJweHeaderTest { fail() } catch (IllegalArgumentException expected) { String msg = "Invalid JWE header 'p2c' (PBES2 Count) value: $d. " + - "Cause: Value cannot be represented as a java.lang.Integer." + "Value cannot be represented as a java.lang.Integer." assertEquals msg, expected.getMessage() } } @Test - void pbe2SaltBytesTest() { + void testPbe2SaltBytes() { byte[] salt = new byte[32] Randoms.secureRandom().nextBytes(salt) - header.setPbes2Salt(salt) + header.put('p2s', salt) + assertEquals Encoders.BASE64URL.encode(salt), header.get('p2s') assertArrayEquals salt, header.getPbes2Salt() } @@ -197,72 +325,4 @@ class DefaultJweHeaderTest { //ensure that even though a Base64Url string was set, we get back a byte[]: assertArrayEquals salt, header.getPbes2Salt() } - - @Test - void testAgreementPartyUInfo() { - String val = "Party UInfo" - byte[] info = val.getBytes(StandardCharsets.UTF_8) - header.setAgreementPartyUInfo(info) - assertArrayEquals info, header.getAgreementPartyUInfo() - assertEquals val, header.getAgreementPartyUInfoString() - } - - @Test - void testAgreementPartyUInfoString() { - String val = "Party UInfo" - byte[] info = val.getBytes(StandardCharsets.UTF_8) - header.setAgreementPartyUInfo(val) - assertArrayEquals info, header.getAgreementPartyUInfo() - assertEquals val, header.getAgreementPartyUInfoString() - } - - @Test - void testEmptyAgreementPartyUInfo() { - byte[] info = new byte[0] - header.setAgreementPartyUInfo(info) - assertNull header.getAgreementPartyUInfo() - assertNull header.getAgreementPartyUInfoString() - } - - @Test - void testEmptyAgreementPartyUInfoString() { - String s = ' ' - header.setAgreementPartyUInfo(s) - assertNull header.getAgreementPartyUInfo() - assertNull header.getAgreementPartyUInfoString() - } - - @Test - void testAgreementPartyVInfo() { - String val = "Party VInfo" - byte[] info = val.getBytes(StandardCharsets.UTF_8) - header.setAgreementPartyVInfo(info) - assertArrayEquals info, header.getAgreementPartyVInfo() - assertEquals val, header.getAgreementPartyVInfoString() - } - - @Test - void testAgreementPartyVInfoString() { - String val = "Party VInfo" - byte[] info = val.getBytes(StandardCharsets.UTF_8) - header.setAgreementPartyVInfo(val) - assertArrayEquals info, header.getAgreementPartyVInfo() - assertEquals val, header.getAgreementPartyVInfoString() - } - - @Test - void testEmptyAgreementPartyVInfo() { - byte[] info = new byte[0] - header.setAgreementPartyVInfo(info) - assertNull header.getAgreementPartyVInfo() - assertNull header.getAgreementPartyVInfoString() - } - - @Test - void testEmptyAgreementPartyVInfoString() { - String s = ' ' - header.setAgreementPartyVInfo(s) - assertNull header.getAgreementPartyVInfo() - assertNull header.getAgreementPartyVInfoString() - } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy index b77331053..6e28ef6e9 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy @@ -21,15 +21,6 @@ import static org.junit.Assert.assertEquals class DefaultJwsHeaderTest { - @Test - void testKeyId() { - - def h = new DefaultJwsHeader() - - h.setKeyId('foo') - assertEquals h.getKeyId(), 'foo' - } - @Test void testGetName() { def header = new DefaultJwsHeader() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy index 1b7f7af6b..e9de3b2af 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy @@ -132,7 +132,7 @@ class JwtMapTest { fail() } catch (IllegalArgumentException expected) { //Ensure message so we don't show any secret value: - String msg = 'Invalid Map \'foo\' (foo) value: . Cause: Values must be ' + + String msg = 'Invalid Map \'foo\' (foo) value . Values must be ' + 'either String or java.math.BigInteger instances. Value type found: ' + 'java.net.URI.' assertEquals msg, expected.getMessage() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy index 4104cce1b..091f3cd8b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy @@ -37,6 +37,11 @@ class AbstractJwkTest { jwk = newJwk(newCtx()) } + @Test + void testGetFieldValue() { + assertEquals 'test', jwk.get(AbstractJwk.KTY) + } + @Test void testContainsValue() { assertTrue jwk.containsValue('test') diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy index f08f22c2d..6a7c43692 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy @@ -6,7 +6,6 @@ import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.impl.lang.CheckedFunction import io.jsonwebtoken.impl.lang.CheckedSupplier import io.jsonwebtoken.impl.lang.Conditions -import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.lang.Arrays import io.jsonwebtoken.security.EncryptionAlgorithms import io.jsonwebtoken.security.SecretKeyBuilder @@ -15,7 +14,6 @@ import org.junit.Test import javax.crypto.Cipher import javax.crypto.SecretKeyFactory import javax.crypto.spec.GCMParameterSpec -import java.nio.charset.StandardCharsets import java.security.Provider import static org.junit.Assert.* @@ -32,8 +30,8 @@ class AesGcmKeyAlgorithmTest { def alg = new GcmAesAeadAlgorithm(256) - def iv = new byte[12]; - Randoms.secureRandom().nextBytes(iv); + def iv = new byte[12] + Randoms.secureRandom().nextBytes(iv) def kek = alg.keyBuilder().build() def cek = alg.keyBuilder().build() @@ -46,7 +44,7 @@ class AesGcmKeyAlgorithmTest { Provider provider = Providers.findBouncyCastle(Conditions.notExists(new CheckedSupplier() { @Override SecretKeyFactory get() throws Exception { - return SecretKeyFactory.getInstance(jcaName); + return SecretKeyFactory.getInstance(jcaName) } })) @@ -60,7 +58,7 @@ class AesGcmKeyAlgorithmTest { }) //separate tag from jca ciphertext: - int ciphertextLength = jcaResult.length - 16; //AES block size in bytes (128 bits) + int ciphertextLength = jcaResult.length - 16 //AES block size in bytes (128 bits) byte[] ciphertext = new byte[ciphertextLength] System.arraycopy(jcaResult, 0, ciphertext, 0, ciphertextLength) @@ -130,7 +128,9 @@ class AesGcmKeyAlgorithmTest { def ereq = new DefaultKeyRequest(null, null, kek, header, enc) def result = alg.getEncryptionKey(ereq) - header.put(headerName, value) //null value will remove it + header.remove(headerName) + + header.put(headerName, value) byte[] encryptedKeyBytes = result.getContent() @@ -142,28 +142,29 @@ class AesGcmKeyAlgorithmTest { } } - String missing(String name) { - return "JWE header is missing required '${name}' value." as String + static String missing(String id, String name) { + return "JWE header is missing required '$id' ($name) value." as String } - String type(String name) { + static String type(String name) { return "JWE header '${name}' value must be a String. Actual type: java.lang.Integer" as String } - String base64Url(String name) { + static String base64Url(String name) { return "JWE header '${name}' value is not a valid Base64URL String: Illegal base64url character: '#'" } - String length(String name, int requiredBitLength) { + static String length(String name, int requiredBitLength) { return "JWE header '${name}' decoded byte array must be ${Bytes.bitsMsg(requiredBitLength)} long. Actual length: ${Bytes.bitsMsg(16)}." } @Test void testMissingHeaders() { - testDecryptionHeader('iv', null, missing('iv')) - testDecryptionHeader('tag', null, missing('tag')) + testDecryptionHeader('iv', null, missing('iv', 'Initialization Vector')) + testDecryptionHeader('tag', null, missing('tag', 'Authentication Tag')) } + /* @Test void testIncorrectTypeHeaders() { testDecryptionHeader('iv', 14, type('iv')) @@ -182,4 +183,5 @@ class AesGcmKeyAlgorithmTest { testDecryptionHeader('iv', value, length('iv', 96)) testDecryptionHeader('tag', value, length('tag', 128)) } + */ } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy index de655ab63..ba53a5e2e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy @@ -23,33 +23,47 @@ class DefaultJwkContextTest { @Test void testGetName() { - def header = new DefaultJwkContext() - assertEquals 'JWK', header.getName() + def ctx = new DefaultJwkContext() + assertEquals 'JWK', ctx.getName() } @Test - void testGetNameWhenSecretKey() { - def header = new DefaultJwkContext(DefaultSecretJwk.FIELDS) - header.put('kty', 'oct') - assertEquals 'Secret JWK', header.getName() + void testGetNameWhenSecretJwk() { + def ctx = new DefaultJwkContext(DefaultSecretJwk.FIELDS) + ctx.put('kty', 'oct') + assertEquals 'Secret JWK', ctx.getName() + } + + @Test + void testGetNameWithGenericPublicKey() { + def ctx = new DefaultJwkContext() + ctx.setKey(TestKeys.ES256.pair.public) + assertEquals 'Public JWK', ctx.getName() + } + + @Test + void testGetNameWithGenericPrivateKey() { + def ctx = new DefaultJwkContext() + ctx.setKey(TestKeys.ES256.pair.private) + assertEquals 'Private JWK', ctx.getName() } @Test void testGStringPrintsRedactedValues() { // DO NOT REMOVE THIS METHOD: IT IS CRITICAL TO ENSURE GROOVY STRINGS DO NOT LEAK SECRET/PRIVATE KEY MATERIAL - def header = new DefaultJwkContext(DefaultSecretJwk.FIELDS) - header.put('kty', 'oct') - header.put('k', 'test') + def ctx = new DefaultJwkContext(DefaultSecretJwk.FIELDS) + ctx.put('kty', 'oct') + ctx.put('k', 'test') String s = '[kty:oct, k:]' - assertEquals "$s", "$header" + assertEquals "$s", "$ctx" } @Test void testGStringToStringPrintsRedactedValues() { - def header = new DefaultJwkContext(DefaultSecretJwk.FIELDS) - header.put('kty', 'oct') - header.put('k', 'test') + def ctx = new DefaultJwkContext(DefaultSecretJwk.FIELDS) + ctx.put('kty', 'oct') + ctx.put('k', 'test') String s = '{kty=oct, k=}' - assertEquals "$s", "${header.toString()}" + assertEquals "$s", "${ctx.toString()}" } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy deleted file mode 100644 index 641ae84d6..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultValueGetterTest.groovy +++ /dev/null @@ -1,208 +0,0 @@ -package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.MalformedJwtException -import io.jsonwebtoken.impl.DefaultHeader -import io.jsonwebtoken.impl.DefaultJweHeader -import io.jsonwebtoken.impl.DefaultJwsHeader -import io.jsonwebtoken.lang.Maps -import io.jsonwebtoken.security.Jwks -import io.jsonwebtoken.security.MalformedKeyException -import org.junit.Test - -import javax.crypto.SecretKey -import java.security.interfaces.ECPublicKey -import java.security.interfaces.RSAPublicKey - -import static org.junit.Assert.* - -class DefaultValueGetterTest { - - @Test - void testMapName() { - def getter = new DefaultValueGetter(Maps.of('foo', 'bar').build()) - assertEquals 'Map', getter.name() - } - - @Test - void testJwtName() { - def getter = new DefaultValueGetter(new DefaultHeader().setAlgorithm('foo')) - assertEquals 'JWT header', getter.name() - } - - @Test - void testJwsName() { - def getter = new DefaultValueGetter(new DefaultJwsHeader().setAlgorithm('foo')) - assertEquals 'JWS header', getter.name() - } - - @Test - void testJweName() { - def getter = new DefaultValueGetter(new DefaultJweHeader().setAlgorithm('foo')) - assertEquals 'JWE header', getter.name() - } - - @Test - void testJwkContextName() { - def ctx = new DefaultJwkContext<>().setId('id') - def getter = new DefaultValueGetter(ctx) - assertEquals 'JWK', getter.name() - } - - @Test - void testSecretJwkName() { - def key = TestKeys.A128GCM - def jwk = new DefaultSecretJwk(new DefaultJwkContext().setType('oct').setKey(key)) - def getter = new DefaultValueGetter(jwk) - assertEquals 'Secret JWK', getter.name() - } - - @Test - void testEcJwkName() { - def jwk = Jwks.builder().forKey(TestKeys.ES256.pair.public as ECPublicKey).build() - def getter = new DefaultValueGetter(jwk) - assertEquals 'EC JWK', getter.name() - } - - @Test - void testRsaJwkName() { - def jwk = Jwks.builder().forKey(TestKeys.RS256.pair.public as RSAPublicKey).build() - def getter = new DefaultValueGetter(jwk) - assertEquals 'RSA JWK', getter.name() - } - - @Test - void testMalformedJwkContext() { - def ctx = new DefaultJwkContext<>().setId('id') - ctx.put('foo', 42) - def getter = new DefaultValueGetter(ctx) - try { - getter.getRequiredString('foo') - fail() - } catch (MalformedKeyException expected) { - String msg = "JWK 'foo' value must be a String. Actual type: java.lang.Integer" - assertEquals msg, expected.getMessage() - } - } - - @Test - void testMalformedJwk() { - def jwk = Jwks.builder().forKey(TestKeys.A128GCM).put('foo', 42).build() - def getter = new DefaultValueGetter(jwk) - try { - getter.getRequiredString('foo') - fail() - } catch (MalformedKeyException expected) { - String msg = "Secret JWK 'foo' value must be a String. Actual type: java.lang.Integer" - assertEquals msg, expected.getMessage() - } - } - - @Test - void testGetRequiredStringWhenEmpty() { - def getter = new DefaultValueGetter(Maps.of('foo', ' ').build()) - try { - getter.getRequiredString('foo') - fail() - } catch (MalformedJwtException expected) { - String msg = "Map 'foo' string value cannot be null or empty." - assertEquals msg, expected.getMessage() - } - } - - @Test - void testGetRequiredIntegerWrongType() { - def getter = new DefaultValueGetter(Maps.of('foo', 'bar').build()) - try { - getter.getRequiredInteger('foo') - fail() - } catch (MalformedJwtException expected) { - String msg = "Map 'foo' value must be an Integer. Actual type: java.lang.String" - assertEquals msg, expected.getMessage() - } - } - - @Test - void testGetRequiredPositiveIntegerWhenZero() { - def getter = new DefaultValueGetter(Maps.of('foo', 0 as int).build()) - try { - getter.getRequiredPositiveInteger('foo') - fail() - } catch (MalformedJwtException expected) { - String msg = "Map 'foo' value must be a positive Integer. Value: 0" - assertEquals msg, expected.getMessage() - } - } - - @Test - void testGetRequiredPositiveIntegerWhenNegative() { - def getter = new DefaultValueGetter(Maps.of('foo', Integer.MIN_VALUE).build()) - try { - getter.getRequiredPositiveInteger('foo') - fail() - } catch (MalformedJwtException expected) { - String msg = "Map 'foo' value must be a positive Integer. Value: ${Integer.MIN_VALUE}" - assertEquals msg, expected.getMessage() - } - } - - @Test - void testGetRequiredBytesInvalidData() { - def getter = new DefaultValueGetter(Maps.of('foo', '#@!').build()) - try { - getter.getRequiredBytes('foo') - fail() - } catch (MalformedJwtException expected) { - String msg = "Map 'foo' value is not a valid Base64URL String: Unable to decode input: " - // cannot do msg equality check here - the trailing value differs depending on the JDK < 11 or >= 11, - // so we do a 'starts with' check to ensure the parts of the message in our control are verified: - assertTrue expected.getMessage().startsWith(msg) - } - } - - @Test - void testGetRequiredBigIntNotSensitive() { - def getter = new DefaultValueGetter(Maps.of('foo', '#@!').build()) - try { - getter.getRequiredBigInt('foo', false) - fail() - } catch (MalformedJwtException expected) { - String msg = "Unable to decode Map 'foo' value '#@!' to BigInteger: Unable to decode input: " - // cannot do msg equality check here - the trailing value differs depending on the JDK < 11 or >= 11, - // so we do a 'starts with' check to ensure the parts of the message in our control are verified: - assertTrue expected.getMessage().startsWith(msg) - } - } - - @Test - void testGetRequiredBigIntSensitive() { - def getter = new DefaultValueGetter(Maps.of('foo', '#@!').build()) - try { - getter.getRequiredBigInt('foo', true) - fail() - } catch (MalformedJwtException expected) { - String msg = "Unable to decode Map 'foo' value to BigInteger: Unable to decode input: " - // cannot do msg equality check here - the trailing value differs depending on the JDK < 11 or >= 11, - // so we do a 'starts with' check to ensure the parts of the message in our control are verified: - assertTrue expected.getMessage().startsWith(msg) - } - } - - @Test - void testGetRequiredMap() { - def map = Maps.of('bar', 'baz').build() - def getter = new DefaultValueGetter(Maps.of('foo', map).build()) - assertSame map, getter.getRequiredMap('foo') - } - - @Test - void testGetRequiredMapWithInvalidValue() { - def getter = new DefaultValueGetter(Maps.of('foo', 'bar').build()) - try { - getter.getRequiredMap('foo') - fail() - } catch (MalformedJwtException expected) { - String msg = "Map 'foo' value must be a Map. Actual type: java.lang.String" - assertEquals msg, expected.getMessage() - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPrivateJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPrivateJwkFactoryTest.groovy new file mode 100644 index 000000000..f0f8e1195 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPrivateJwkFactoryTest.groovy @@ -0,0 +1,25 @@ +package io.jsonwebtoken.impl.security + + +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +class EcPrivateJwkFactoryTest { + + @Test + void testDMissing() { + def values = ['kty': 'EC', 'crv': 'P-256', 'x': BigInteger.ONE, 'y': BigInteger.ONE] + try { + def ctx = new DefaultJwkContext(DefaultEcPrivateJwk.FIELDS) + ctx.putAll(values) + new EcPrivateJwkFactory().createJwkFromValues(ctx) + fail() + } catch (MalformedKeyException expected) { + String msg = "EC JWK is missing required 'd' (ECC Private Key) value." + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPublicJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPublicJwkFactoryTest.groovy new file mode 100644 index 000000000..91870697c --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPublicJwkFactoryTest.groovy @@ -0,0 +1,58 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +class EcPublicJwkFactoryTest { + + @Test + void testCurveMissing() { + try { + Jwks.builder().putAll(['kty': 'EC']).build() + fail() + } catch (MalformedKeyException expected) { + String msg = "EC JWK is missing required 'crv' (Curve) value." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testXMissing() { + try { + Jwks.builder().putAll(['kty': 'EC', 'crv': 'P-256']).build() + fail() + } catch (MalformedKeyException expected) { + String msg = "EC JWK is missing required 'x' (X Coordinate) value." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testYMissing() { + try { + Jwks.builder().putAll(['kty': 'EC', 'crv': 'P-256', 'x': BigInteger.ONE]).build() + fail() + } catch (MalformedKeyException expected) { + String msg = "EC JWK is missing required 'y' (Y Coordinate) value." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testPointNotOnCurve() { + try { + Jwks.builder().putAll(['kty': 'EC', 'crv': 'P-256', 'x': BigInteger.ONE, 'y': BigInteger.ONE]).build() + fail() + } catch (InvalidKeyException expected) { + String msg = "EC JWK x,y coordinates do not exist on elliptic curve 'P-256'. " + + "This could be due simply to an incorrectly-created JWK or possibly an attempted " + + "Invalid Curve Attack (see https://safecurves.cr.yp.to/twist.html for more information)." + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy index fd01754ec..6c2a2f786 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy @@ -1,6 +1,6 @@ package io.jsonwebtoken.impl.security - +import io.jsonwebtoken.MalformedJwtException import io.jsonwebtoken.impl.DefaultJweHeader import io.jsonwebtoken.security.DecryptionKeyRequest import io.jsonwebtoken.security.EncryptionAlgorithms @@ -21,50 +21,21 @@ import static org.junit.Assert.fail class EcdhKeyAlgorithmTest { @Test - void testDecryptionWithoutEcPublicJwk() { + void testDecryptionWithMissingEcPublicJwk() { def alg = new EcdhKeyAlgorithm() ECPrivateKey decryptionKey = TestKeys.ES256.pair.private as ECPrivateKey def header = new DefaultJweHeader() - def jwk = Jwks.builder().forKey(TestKeys.HS256).build() //something other than an EC public key - header.put('epk', jwk) DecryptionKeyRequest req = new DefaultDecryptionKeyRequest(null, null, decryptionKey, header, EncryptionAlgorithms.A128GCM, 'test'.getBytes()) try { alg.getDecryptionKey(req) fail() - } catch (InvalidKeyException expected) { - assertEquals("JWE Header 'epk' (Ephemeral Public Key) value is not an EllipticCurve Public JWK as required.", expected.getMessage()) - } - } - - @Test - void testDecryptionWithEcPublicJwkWithInvalidPoint() { - - def alg = new EcdhKeyAlgorithm() - ECPrivateKey decryptionKey = TestKeys.ES256.pair.private as ECPrivateKey // Expected curve for this is P-256 - - def header = new DefaultJweHeader() - def pubJwk = Jwks.builder().forKey(TestKeys.ES256.pair.public as ECPublicKey).build() - def jwk = new LinkedHashMap(pubJwk) // copy fields so we can mutate - // We have a public JWK for a point on the curve, now swap out the x coordinate for something invalid: - jwk.put('x', 'Kg') - - // now set the epk header as the invalid/manipulated jwk: - header.put('epk', jwk) - - DecryptionKeyRequest req = new DefaultDecryptionKeyRequest(null, null, decryptionKey, - header, EncryptionAlgorithms.A128GCM, 'test'.getBytes()) - - try { - alg.getDecryptionKey(req) - fail() - } catch (InvalidKeyException expected) { - String msg = expected.getMessage() - String expectedMsg = EcPublicJwkFactory.jwkContainsErrorMessage(pubJwk.crv as String, jwk) - assertEquals(expectedMsg, msg) + } catch (MalformedJwtException expected) { + String msg = "JWE header is missing required 'epk' (Ephemeral Public Key) value." + assertEquals msg, expected.getMessage() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkConverterTest.groovy new file mode 100644 index 000000000..df14784c7 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkConverterTest.groovy @@ -0,0 +1,70 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.* +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +class JwkConverterTest { + + @Test + void testJwkClassTypeString() { + assertEquals 'JWK', JwkConverter.typeString(Jwk.class) + } + + @Test + void testSecretJwkClassTypeString() { + assertEquals 'Secret JWK', JwkConverter.typeString(SecretJwk.class) + } + + @Test + void testSecretJwkTypeString() { + def jwk = Jwks.builder().forKey(TestKeys.HS256).build() + assertEquals 'Secret JWK', JwkConverter.typeString(jwk) + } + + @Test + void testPublicJwkClassTypeString() { + assertEquals 'Public JWK', JwkConverter.typeString(PublicJwk.class) + } + + @Test + void testEcPublicJwkClassTypeString() { + assertEquals 'EC Public JWK', JwkConverter.typeString(EcPublicJwk.class) + } + + @Test + void testRsaPublicJwkClassTypeString() { + assertEquals 'RSA Public JWK', JwkConverter.typeString(RsaPublicJwk.class) + } + + @Test + void testPrivateJwkClassTypeString() { + assertEquals 'Private JWK', JwkConverter.typeString(PrivateJwk.class) + } + + @Test + void testEcPrivateJwkClassTypeString() { + assertEquals 'EC Private JWK', JwkConverter.typeString(EcPrivateJwk.class) + } + + @Test + void testRsaPrivateJwkClassTypeString() { + assertEquals 'RSA Private JWK', JwkConverter.typeString(RsaPrivateJwk.class) + } + + @Test + void testPrivateJwk() { + JwkConverter converter = new JwkConverter<>(PrivateJwk.class) + def jwk = Jwks.builder().forKey(TestKeys.HS256).build() + try { + converter.applyFrom(jwk) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Value must be a Private JWK, not a Secret JWK." + assertEquals msg, expected.getMessage() + } + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverterTest.groovy index 9e9c97248..ed9d1a633 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverterTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverterTest.groovy @@ -41,4 +41,16 @@ class RSAOtherPrimeInfoConverterTest { assertEquals msg, expected.getMessage() } } + + @Test + void testApplyFromWithMalformedMap() { + try { + RSAOtherPrimeInfoConverter.INSTANCE.applyFrom(['r':2]) + fail() + } catch (MalformedKeyException expected) { + String msg = "Invalid JWK 'r' (Prime Factor) value . Values must be either String or " + + "java.math.BigInteger instances. Value type found: java.lang.Integer." + assertEquals msg, expected.getMessage() + } + } } From 5307aa10ffe9aca8316efa6348ff712383048dea Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 1 Jun 2022 19:40:03 -0700 Subject: [PATCH 73/75] Adding copyright headers --- .../java/io/jsonwebtoken/ProtectedHeader.java | 15 +++++++++++++++ .../java/io/jsonwebtoken/security/KeyBuilder.java | 15 +++++++++++++++ .../jsonwebtoken/security/KeyLengthSupplier.java | 15 +++++++++++++++ .../java/io/jsonwebtoken/security/KeyPair.java | 15 +++++++++++++++ .../io/jsonwebtoken/security/KeyPairBuilder.java | 15 +++++++++++++++ .../security/KeyPairBuilderSupplier.java | 15 +++++++++++++++ .../io/jsonwebtoken/security/PasswordKey.java | 15 +++++++++++++++ .../jsonwebtoken/security/SecretKeyAlgorithm.java | 15 +++++++++++++++ .../jsonwebtoken/security/SecretKeyBuilder.java | 15 +++++++++++++++ .../io/jsonwebtoken/security/SecurityBuilder.java | 15 +++++++++++++++ 10 files changed, 150 insertions(+) diff --git a/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java index 0f3017b3a..74e158cc3 100644 --- a/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java +++ b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken; import io.jsonwebtoken.security.PublicJwk; diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java b/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java index 1a53dadef..dd50239a9 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; import javax.crypto.SecretKey; diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java index 82263a86c..407230288 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; /** diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyPair.java b/api/src/main/java/io/jsonwebtoken/security/KeyPair.java index fb2785967..f680534b7 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyPair.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyPair.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; import java.security.PrivateKey; diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java index 4b98e4beb..2b240609d 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; import java.security.KeyPair; diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java index 3ad484992..73de81aae 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; import java.security.KeyPair; diff --git a/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java b/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java index 3bb0ef8a4..0caa53d6c 100644 --- a/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java +++ b/api/src/main/java/io/jsonwebtoken/security/PasswordKey.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; import javax.crypto.SecretKey; diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java index c13b05cec..76ea6656d 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; import javax.crypto.SecretKey; diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretKeyBuilder.java b/api/src/main/java/io/jsonwebtoken/security/SecretKeyBuilder.java index 549453b5c..6665eb541 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecretKeyBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecretKeyBuilder.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; import javax.crypto.SecretKey; diff --git a/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java b/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java index 17fdd9030..80e22c55e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jsonwebtoken.security; import io.jsonwebtoken.lang.Builder; From 2937114d5e7776bc2684e378692f0d844b736d4e Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Thu, 2 Jun 2022 15:34:39 -0700 Subject: [PATCH 74/75] Deprecated CompressionCodecResolver in favor of Locator --- CHANGELOG.md | 4 ++ .../CompressionCodecResolver.java | 2 +- .../main/java/io/jsonwebtoken/JwtParser.java | 18 +++--- .../io/jsonwebtoken/JwtParserBuilder.java | 40 ++++++++++-- .../impl/CompressionCodecLocator.java | 8 ++- .../jsonwebtoken/impl/DefaultJwtParser.java | 8 +-- .../impl/DefaultJwtParserBuilder.java | 20 ++++-- .../java/io/jsonwebtoken/impl/IdRegistry.java | 5 +- .../DefaultCompressionCodecResolver.java | 64 ++++++++----------- .../UnavailableImplementationException.java | 7 +- .../impl/DefaultJwtParserBuilderTest.groovy | 15 ++++- .../impl/ImmutableJwtParserTest.groovy | 13 +--- ...DefaultCompressionCodecResolverTest.groovy | 17 ++--- 13 files changed, 128 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cd1967a7..332a93a51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ allow for customization of the JCA `Provider` and `SecureRandom` during Key or KeyPair generation if desired, whereas the old enum-based static utility methods did not. +* `io.jsonwebtoken.CompressionCodec` now inherits `io.jsonwebtoken.Identifiable` and `getId()` is preferred over + the now-deprecated `getAlgorithmName()` method. This was to guarantee API congruence with all other JWT-identifiable + algorithm names that can be set as a header value. + #### Backwards Compatibility Breaking Changes * Parsing of unsecured JWTs (`alg` header of `none`) are now disabled by default as mandated by diff --git a/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java b/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java index 50ebe08fc..829defa9a 100644 --- a/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java +++ b/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java @@ -41,6 +41,6 @@ public interface CompressionCodecResolver { * @return CompressionCodec matching the {@code zip} header, or null if there is no {@code zip} header. * @throws CompressionException if a {@code zip} header value is found and not supported. */ - CompressionCodec resolveCompressionCodec(Header header) throws CompressionException; + CompressionCodec resolveCompressionCodec(Header header) throws CompressionException; } diff --git a/api/src/main/java/io/jsonwebtoken/JwtParser.java b/api/src/main/java/io/jsonwebtoken/JwtParser.java index cc7b86c82..16e943313 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParser.java @@ -221,7 +221,7 @@ public interface JwtParser { * @param key the algorithm-specific signature verification key used to validate any discovered JWS digital * signature. * @return the parser for method chaining. - * @deprecated see {@link JwtParserBuilder#setSigningKey(byte[])}. + * @deprecated in favor of {@link JwtParserBuilder#verifyWith(Key)}. * To construct a JwtParser use the corresponding builder via {@link Jwts#parserBuilder()}. This will construct an * immutable JwtParser. *

    NOTE: this method will be removed before version 1.0 @@ -267,7 +267,7 @@ public interface JwtParser { * @param base64EncodedSecretKey the BASE64-encoded algorithm-specific signature verification key to use to validate * any discovered JWS digital signature. * @return the parser for method chaining. - * @deprecated see {@link JwtParserBuilder#setSigningKey(String)}. + * @deprecated in favor of {@link JwtParserBuilder#verifyWith(Key)}. * To construct a JwtParser use the corresponding builder via {@link Jwts#parserBuilder()}. This will construct an * immutable JwtParser. *

    NOTE: this method will be removed before version 1.0 @@ -287,7 +287,7 @@ public interface JwtParser { * @param key the algorithm-specific signature verification key to use to validate any discovered JWS digital * signature. * @return the parser for method chaining. - * @deprecated see {@link JwtParserBuilder#setSigningKey(Key)}. + * @deprecated in favor of {@link JwtParserBuilder#verifyWith(Key)}. * To construct a JwtParser use the corresponding builder via {@link Jwts#parserBuilder()}. This will construct an * immutable JwtParser. *

    NOTE: this method will be removed before version 1.0 @@ -322,7 +322,7 @@ public interface JwtParser { * @param signingKeyResolver the signing key resolver used to retrieve the signing key. * @return the parser for method chaining. * @since 0.4 - * @deprecated see {@link JwtParserBuilder#setSigningKeyResolver(SigningKeyResolver)}. + * @deprecated in favor of {@link JwtParserBuilder#setKeyLocator(Locator)}. * To construct a JwtParser use the corresponding builder via {@link Jwts#parserBuilder()}. This will construct an * immutable JwtParser. *

    NOTE: this method will be removed before version 1.0 @@ -336,10 +336,10 @@ public interface JwtParser { * Sets the {@link CompressionCodecResolver} used to acquire the {@link CompressionCodec} that should be used to * decompress the JWT body. If the parsed JWT is not compressed, this resolver is not used. * - *

    NOTE: Compression is not defined by the JWT Specification, and it is not expected that other libraries - * (including JJWT versions < 0.6.0) are able to consume a compressed JWT body correctly. This method is only - * useful if the compact JWT was compressed with JJWT >= 0.6.0 or another library that you know implements - * the same behavior.

    + *

    NOTE: Compression is not defined by the JWS Specification - only the JWE Specification - and it is + * not expected that other libraries (including JJWT versions < 0.6.0) are able to consume a compressed JWS + * body correctly. This method is only useful if the compact JWT was compressed with JJWT >= 0.6.0 or another + * library that you know implements the same behavior.

    * *

    Default Support

    * @@ -354,7 +354,7 @@ public interface JwtParser { * @param compressionCodecResolver the compression codec resolver used to decompress the JWT body. * @return the parser for method chaining. * @since 0.6.0 - * @deprecated see {@link JwtParserBuilder#setCompressionCodecResolver(CompressionCodecResolver)}. + * @deprecated in favor of {@link JwtParserBuilder#setCompressionCodecLocator(Locator)}. * To construct a JwtParser use the corresponding builder via {@link Jwts#parserBuilder()}. This will construct an * immutable JwtParser. *

    NOTE: this method will be removed before version 1.0 diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index 1eec5d350..52c2c2bb1 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -454,10 +454,10 @@ public interface JwtParserBuilder extends Builder { * Sets the {@link CompressionCodecResolver} used to acquire the {@link CompressionCodec} that should be used to * decompress the JWT body. If the parsed JWT is not compressed, this resolver is not used. * - *

    NOTE: Compression is not defined by the JWT Specification, and it is not expected that other libraries - * (including JJWT versions < 0.6.0) are able to consume a compressed JWT body correctly. This method is only - * useful if the compact JWT was compressed with JJWT >= 0.6.0 or another library that you know implements - * the same behavior.

    + *

    NOTE: Compression is not defined by the JWS Specification - only the JWE Specification - and it is + * not expected that other libraries (including JJWT versions < 0.6.0) are able to consume a compressed JWS + * body correctly. This method is only useful if the compact JWS was compressed with JJWT >= 0.6.0 or + * another library that you know implements the same behavior.

    * *

    Default Support

    * @@ -466,15 +466,43 @@ public interface JwtParserBuilder extends Builder { * and {@link CompressionCodecs#GZIP GZIP} algorithms by default - you do not need to * specify a {@code CompressionCodecResolver} in these cases.

    * - *

    However, if you want to use a compression algorithm other than {@code DEF} or {@code GZIP}, you must implement - * your own {@link CompressionCodecResolver} and specify that via this method and also when + *

    However, if you want to use a compression algorithm other than {@code DEF} or {@code GZIP}, you must + * implement your own {@link CompressionCodecResolver} and specify that via this method and also when * {@link io.jsonwebtoken.JwtBuilder#compressWith(CompressionCodec) building} JWTs.

    * * @param compressionCodecResolver the compression codec resolver used to decompress the JWT body. * @return the parser builder for method chaining. + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #setCompressionCodecLocator(Locator)} to use the + * congruent {@code Locator} concept used elsewhere (such as {@link #setKeyLocator(Locator)}). */ + @Deprecated JwtParserBuilder setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver); + /** + * Sets the {@link CompressionCodec} {@code Locator} used to acquire the {@code CompressionCodec} that should be + * used to decompress the JWT body. + * + *

    NOTE: Compression is not defined by the JWS Specification - only the JWE Specification - and it is + * not expected that other libraries (including JJWT versions < 0.6.0) are able to consume a compressed JWS + * body correctly. This method is only useful if the compact JWS was compressed with JJWT >= 0.6.0 or + * another library that you know implements the same behavior.

    + * + *

    Default Support

    + * + *

    JJWT's default {@link JwtParser} implementation supports both the + * {@link CompressionCodecs#DEFLATE DEFLATE} + * and {@link CompressionCodecs#GZIP GZIP} algorithms by default - you do not need to + * specify a {@code CompressionCodec} {@link Locator} in these cases.

    + * + *

    However, if you want to use a compression algorithm other than {@code DEF} or {@code GZIP}, you must + * implement your own {@code CompressionCodec} {@link Locator} and specify that via this method and also when + * {@link io.jsonwebtoken.JwtBuilder#compressWith(CompressionCodec) building} JWTs.

    + * + * @param locator the compression codec locator used to decompress the JWT body. + * @return the parser builder for method chaining. + */ + JwtParserBuilder setCompressionCodecLocator(Locator locator); + /** * Perform Base64Url decoding with the specified Decoder * diff --git a/impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java b/impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java index a52a64e5b..245102de2 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java @@ -3,10 +3,11 @@ import io.jsonwebtoken.CompressionCodec; import io.jsonwebtoken.CompressionCodecResolver; import io.jsonwebtoken.Header; +import io.jsonwebtoken.Locator; import io.jsonwebtoken.impl.lang.Function; import io.jsonwebtoken.lang.Assert; -public class CompressionCodecLocator implements Function, CompressionCodec> { +public class CompressionCodecLocator implements Function, CompressionCodec>, Locator { private final CompressionCodecResolver resolver; @@ -18,4 +19,9 @@ public CompressionCodecLocator(CompressionCodecResolver resolver) { public CompressionCodec apply(Header header) { return resolver.resolveCompressionCodec(header); } + + @Override + public CompressionCodec locate(Header header) { + return apply(header); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index bf723b01a..302a47256 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -165,7 +165,7 @@ private static Function encFn(Collection, CompressionCodec> compressionCodecLocator; + private Locator compressionCodecLocator; private final boolean enableUnsecuredJws; @@ -214,7 +214,7 @@ public DefaultJwtParser() { Claims expectedClaims, Decoder base64UrlDecoder, Deserializer> deserializer, - CompressionCodecResolver compressionCodecResolver, + Locator compressionCodecLocator, Collection> extraSigAlgs, Collection> extraKeyAlgs, Collection extraEncAlgs) { @@ -230,7 +230,7 @@ public DefaultJwtParser() { this.signatureAlgorithmLocator = sigFn(extraSigAlgs); this.keyAlgorithmLocator = keyFn(extraKeyAlgs); this.encryptionAlgorithmLocator = encFn(extraEncAlgs); - this.compressionCodecLocator = new CompressionCodecLocator(compressionCodecResolver); + this.compressionCodecLocator = Assert.notNull(compressionCodecLocator, "CompressionCodec locator cannot be null."); } @Override @@ -570,7 +570,7 @@ private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHe verifySignature(tokenized, ((JwsHeader) header), alg, new LocatingKeyResolver(this.keyLocator), null, null); } - CompressionCodec compressionCodec = compressionCodecLocator.apply(header); + CompressionCodec compressionCodec = compressionCodecLocator.locate(header); if (compressionCodec != null) { bytes = compressionCodec.decompress(bytes); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index 4a0e0d4c7..56ee9f0a5 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -17,6 +17,7 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.Clock; +import io.jsonwebtoken.CompressionCodec; import io.jsonwebtoken.CompressionCodecResolver; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.JwtParserBuilder; @@ -67,7 +68,8 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { @SuppressWarnings("deprecation") //TODO: remove for 1.0 private SigningKeyResolver signingKeyResolver = null; - private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); + private Locator compressionCodecLocator = + new CompressionCodecLocator(new DefaultCompressionCodecResolver()); private final Collection extraEncryptionAlgorithms = new LinkedHashSet<>(); @@ -244,9 +246,15 @@ public JwtParserBuilder setKeyLocator(Locator keyLocator) { } @Override - public JwtParserBuilder setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver) { - Assert.notNull(compressionCodecResolver, "compressionCodecResolver cannot be null."); - this.compressionCodecResolver = compressionCodecResolver; + public JwtParserBuilder setCompressionCodecLocator(Locator locator) { + this.compressionCodecLocator = Assert.notNull(locator, "CompressionCodec locator cannot be null."); + return this; + } + + @Override + public JwtParserBuilder setCompressionCodecResolver(CompressionCodecResolver resolver) { + Assert.notNull(resolver, "compressionCodecResolver cannot be null."); + this.compressionCodecLocator = new CompressionCodecLocator(resolver); return this; } @@ -273,7 +281,7 @@ public JwtParser build() { // Invariants. If these are ever violated, it's an error in this class implementation // (we default to non-null instances, and the setters should never allow null): Assert.stateNotNull(this.keyLocator, "Key locator should never be null."); - Assert.stateNotNull(this.compressionCodecResolver, "CompressionCodecResolver should never be null."); + Assert.stateNotNull(this.compressionCodecLocator, "CompressionCodec Locator should never be null."); return new ImmutableJwtParser(new DefaultJwtParser( provider, @@ -285,7 +293,7 @@ public JwtParser build() { expectedClaims, base64UrlDecoder, new JwtDeserializer<>(deserializer), - compressionCodecResolver, + compressionCodecLocator, extraSignatureAlgorithms, extraKeyAlgorithms, extraEncryptionAlgorithms diff --git a/impl/src/main/java/io/jsonwebtoken/impl/IdRegistry.java b/impl/src/main/java/io/jsonwebtoken/impl/IdRegistry.java index df6820f12..1e0078600 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/IdRegistry.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/IdRegistry.java @@ -3,6 +3,7 @@ import io.jsonwebtoken.Identifiable; import io.jsonwebtoken.impl.lang.Registry; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; import java.util.Collection; @@ -20,13 +21,13 @@ public IdRegistry(Collection instances) { String id = Assert.hasText(Strings.clean(instance.getId()), "All Identifiable instances within the collection cannot have a null or empty id."); m.put(id, instance); } - this.INSTANCES = java.util.Collections.unmodifiableMap(m); + this.INSTANCES = Collections.immutable(m); } @Override public T apply(String id) { Assert.hasText(id, "id argument cannot be null or empty."); - //try constant time lookup first. This will satisfy 99% of invocations: + //try constant-time lookup first. This will satisfy 99% of invocations: T instance = INSTANCES.get(id); if (instance != null) { return instance; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java b/impl/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java index 52ae5e808..255baf8e6 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java @@ -20,13 +20,15 @@ import io.jsonwebtoken.CompressionCodecs; import io.jsonwebtoken.CompressionException; import io.jsonwebtoken.Header; +import io.jsonwebtoken.Locator; +import io.jsonwebtoken.impl.IdRegistry; +import io.jsonwebtoken.impl.lang.Registry; import io.jsonwebtoken.impl.lang.Services; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Strings; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import java.util.LinkedHashSet; +import java.util.Set; /** * Default implementation of {@link CompressionCodecResolver} that supports the following: @@ -43,56 +45,44 @@ *

    If you want to use a compression algorithm other than {@code DEF} or {@code GZIP}, you must implement your own * {@link CompressionCodecResolver} and specify that when * {@link io.jsonwebtoken.JwtBuilder#compressWith(CompressionCodec) building} and - * {@link io.jsonwebtoken.JwtParser#setCompressionCodecResolver(CompressionCodecResolver) parsing} JWTs.

    + * {@link io.jsonwebtoken.JwtParserBuilder#setCompressionCodecResolver(CompressionCodecResolver) parsing} JWTs.

    * * @see DeflateCompressionCodec * @see GzipCompressionCodec * @since 0.6.0 */ -public class DefaultCompressionCodecResolver implements CompressionCodecResolver { +public class DefaultCompressionCodecResolver implements CompressionCodecResolver, Locator { - private static final String MISSING_COMPRESSION_MESSAGE = "Unable to find an implementation for compression algorithm [%s] using java.util.ServiceLoader. Ensure you include a backing implementation .jar in the classpath, for example jjwt-impl.jar, or your own .jar for custom implementations."; + private static final String MISSING_COMPRESSION_MESSAGE = "Unable to find an implementation for compression " + + "algorithm [%s] using java.util.ServiceLoader. Ensure you include a backing implementation .jar in " + + "the classpath, for example jjwt-impl.jar, or your own .jar for custom implementations."; - private final Map codecs; + private final Registry codecs; public DefaultCompressionCodecResolver() { - Map codecMap = new HashMap<>(); - for (CompressionCodec codec : Services.loadAll(CompressionCodec.class)) { - codecMap.put(codec.getId().toUpperCase(), codec); - } - - codecMap.put(CompressionCodecs.DEFLATE.getId().toUpperCase(), CompressionCodecs.DEFLATE); - codecMap.put(CompressionCodecs.GZIP.getId().toUpperCase(), CompressionCodecs.GZIP); - - codecs = Collections.unmodifiableMap(codecMap); + Set codecs = new LinkedHashSet<>(Services.loadAll(CompressionCodec.class)); + codecs.add(CompressionCodecs.DEFLATE); // standard ones are added last so they can't be accidentally replaced + codecs.add(CompressionCodecs.GZIP); + this.codecs = new IdRegistry<>(codecs); } @Override - public CompressionCodec resolveCompressionCodec(Header header) { - String cmpAlg = getAlgorithmFromHeader(header); - - final boolean hasCompressionAlgorithm = Strings.hasText(cmpAlg); - - if (!hasCompressionAlgorithm) { + public CompressionCodec locate(Header header) { + Assert.notNull(header, "Header cannot be null."); + String id = header.getCompressionAlgorithm(); + if (!Strings.hasText(id)) { return null; } - return byName(cmpAlg); - } - - private String getAlgorithmFromHeader(Header header) { - Assert.notNull(header, "header cannot be null."); - - return header.getCompressionAlgorithm(); - } - - private CompressionCodec byName(String name) { - Assert.hasText(name, "'name' must not be empty"); - - CompressionCodec codec = codecs.get(name.toUpperCase()); + CompressionCodec codec = codecs.apply(id); if (codec == null) { - throw new CompressionException(String.format(MISSING_COMPRESSION_MESSAGE, name)); + String msg = String.format(MISSING_COMPRESSION_MESSAGE, id); + throw new CompressionException(msg); } - return codec; } + + @Override + public CompressionCodec resolveCompressionCodec(Header header) { + return locate(header); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/UnavailableImplementationException.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/UnavailableImplementationException.java index 3fe9eaa4f..098d3b096 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/UnavailableImplementationException.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/UnavailableImplementationException.java @@ -17,13 +17,16 @@ /** * Exception indicating that no implementation of an jjwt-api SPI was found on the classpath. + * * @since 0.11.0 */ public final class UnavailableImplementationException extends RuntimeException { - private static final String DEFAULT_NOT_FOUND_MESSAGE = "Unable to find an implementation for %s using java.util.ServiceLoader. Ensure you include a backing implementation .jar in the classpath, for example jjwt-impl.jar, or your own .jar for custom implementations."; + private static final String DEFAULT_NOT_FOUND_MESSAGE = "Unable to find an implementation for %s using " + + "java.util.ServiceLoader. Ensure you include a backing implementation .jar in the classpath, " + + "for example jjwt-impl.jar, or your own .jar for custom implementations."; - UnavailableImplementationException(final Class klass) { + UnavailableImplementationException(final Class klass) { super(String.format(DEFAULT_NOT_FOUND_MESSAGE, klass)); } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy index 31e4155e8..b99accc20 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy @@ -16,8 +16,7 @@ package io.jsonwebtoken.impl import com.fasterxml.jackson.databind.ObjectMapper -import io.jsonwebtoken.JwtParser -import io.jsonwebtoken.Jwts +import io.jsonwebtoken.* import io.jsonwebtoken.impl.security.ConstantKeyLocator import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.Decoder @@ -133,6 +132,18 @@ class DefaultJwtParserBuilderTest { } } + @Test + void testCompressionCodecLocator() { + Locator locator = new Locator() { + @Override + CompressionCodec locate(Header header) { + return null; + } + } + def parser = Jwts.parserBuilder().setCompressionCodecLocator(locator).build() + assertSame locator, parser.jwtParser.compressionCodecLocator + } + @Test void testDefaultDeserializer() { JwtParser parser = builder.build() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/ImmutableJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/ImmutableJwtParserTest.groovy index e95f3f3ea..0ae6987e8 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/ImmutableJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/ImmutableJwtParserTest.groovy @@ -15,23 +15,16 @@ */ package io.jsonwebtoken.impl -import io.jsonwebtoken.Clock -import io.jsonwebtoken.CompressionCodecResolver -import io.jsonwebtoken.JwtHandler -import io.jsonwebtoken.JwtParser -import io.jsonwebtoken.SigningKeyResolver +import io.jsonwebtoken.* import io.jsonwebtoken.io.Decoder import io.jsonwebtoken.io.Deserializer import org.junit.Test import java.security.Key -import static org.easymock.EasyMock.expect -import static org.easymock.EasyMock.mock -import static org.easymock.EasyMock.replay -import static org.easymock.EasyMock.verify -import static org.hamcrest.MatcherAssert.assertThat +import static org.easymock.EasyMock.* import static org.hamcrest.CoreMatchers.is +import static org.hamcrest.MatcherAssert.assertThat /** * TODO: These mutable methods will be removed pre 1.0, and ImmutableJwtParser will be replaced with the default diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolverTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolverTest.groovy index 0ff91edf1..fdc8add06 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolverTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolverTest.groovy @@ -19,6 +19,7 @@ import io.jsonwebtoken.CompressionCodec import io.jsonwebtoken.CompressionCodecs import io.jsonwebtoken.CompressionException import io.jsonwebtoken.impl.DefaultHeader +import io.jsonwebtoken.impl.DefaultJwsHeader import io.jsonwebtoken.impl.io.FakeServiceDescriptorClassLoader import io.jsonwebtoken.impl.lang.Services import org.junit.Assert @@ -51,25 +52,15 @@ class DefaultCompressionCodecResolverTest { } @Test - void overrideDefaultCompressionImplTest() { + void testCustomCompressionCodecServiceDoesNotOverrideStandardCodecs() { FakeServiceDescriptorClassLoader.runWithFake "io.jsonwebtoken.io.compression.CompressionCodec.test.override", { // first make sure the service loader actually resolves the test class assertThat Services.loadAll(CompressionCodec), hasItem(instanceOf(YagCompressionCodec)) + def header = new DefaultJwsHeader(['zip': 'gzip']) // now we know the class is loadable, make sure we ALWAYS return the GZIP impl - assertThat new DefaultCompressionCodecResolver().byName("gzip"), instanceOf(GzipCompressionCodec) - } - } - - @Test - void emptyCompressionAlgInHeaderTest() { - //noinspection GroovyUnusedCatchParameter - try { - new DefaultCompressionCodecResolver().byName("") - Assert.fail("Expected IllegalArgumentException to be thrown") - } catch (IllegalArgumentException e) { - // expected + assertThat new DefaultCompressionCodecResolver().locate(header), instanceOf(GzipCompressionCodec) } } } From 18fbc6910ed77ac426a1c60e0e9fda562e733894 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Thu, 2 Jun 2022 15:51:45 -0700 Subject: [PATCH 75/75] Deprecated CompressionCodecResolver in favor of Locator --- .../main/java/io/jsonwebtoken/CompressionCodecResolver.java | 4 ++++ api/src/main/java/io/jsonwebtoken/JwtBuilder.java | 2 +- api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java | 5 +++++ api/src/main/java/io/jsonwebtoken/security/Keys.java | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java b/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java index 829defa9a..ba61ff55c 100644 --- a/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java +++ b/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java @@ -30,7 +30,11 @@ * {@link io.jsonwebtoken.JwtParser#setCompressionCodecResolver(CompressionCodecResolver) parsing} JWTs.

    * * @since 0.6.0 + * @deprecated in favor of {@link Locator} + * @see JwtParserBuilder#setCompressionCodecLocator(Locator) */ +@SuppressWarnings("DeprecatedIsStillUsed") +@Deprecated public interface CompressionCodecResolver { /** diff --git a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java index c43b2553e..8e166d8e1 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -612,7 +612,7 @@ public interface JwtBuilder> extends ClaimsMutator { * the specified algorithm. * @see #signWith(Key) * @since 0.10.0 - * @deprecated since JJWT_RELEASE_VERSION to use the more flexible {@link io.jsonwebtoken.security.SignatureAlgorithm}. + * @deprecated since JJWT_RELEASE_VERSION to use the more flexible {@link #signWith(Key, io.jsonwebtoken.security.SignatureAlgorithm)}. */ @Deprecated T signWith(Key key, SignatureAlgorithm alg) throws InvalidKeyException; diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index 52c2c2bb1..88c5f81da 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -319,6 +319,7 @@ public interface JwtParserBuilder extends Builder { * * @param key the algorithm-specific decryption key to use to decrypt all encountered JWEs. * @return the parser builder for method chaining. + * @since JJWT_RELEASE_VERSION */ JwtParserBuilder decryptWith(Key key); @@ -410,6 +411,7 @@ public interface JwtParserBuilder extends Builder { * @param encAlgs collection of AEAD encryption algorithms to add to the parser's total set of supported * encryption algorithms. * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION */ JwtParserBuilder addEncryptionAlgorithms(Collection encAlgs); @@ -429,6 +431,7 @@ public interface JwtParserBuilder extends Builder { * @param sigAlgs collection of signature algorithms to add to the parser's total set of supported signature * algorithms. * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION */ JwtParserBuilder addSignatureAlgorithms(Collection> sigAlgs); @@ -447,6 +450,7 @@ public interface JwtParserBuilder extends Builder { * @param keyAlgs collection of key management algorithms to add to the parser's total set of supported key * management algorithms. * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION */ JwtParserBuilder addKeyAlgorithms(Collection> keyAlgs); @@ -500,6 +504,7 @@ public interface JwtParserBuilder extends Builder { * * @param locator the compression codec locator used to decompress the JWT body. * @return the parser builder for method chaining. + * @since JJWT_RELEASE_VERSION */ JwtParserBuilder setCompressionCodecLocator(Locator locator); diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index bfedd5c65..f65428c3b 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -228,7 +228,7 @@ public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) thr * @param alg the {@code SignatureAlgorithm} to inspect to determine which asymmetric algorithm to use. * @return a new {@link KeyPair} suitable for use with the specified asymmetric algorithm. * @throws IllegalArgumentException if {@code alg} is not an asymmetric algorithm - * @deprecated since JJWT_RELEASE_VERSION. Use your preferred {@link AsymmetricKeySignatureAlgorithm} instance's + * @deprecated since JJWT_RELEASE_VERSION in favor of your preferred {@link AsymmetricKeySignatureAlgorithm} instance's * {@link AsymmetricKeySignatureAlgorithm#keyPairBuilder() keyPairBuilder()} method directly. */ @SuppressWarnings("DeprecatedIsStillUsed")
  • the type of {@link PrivateJwkBuilder} that matches this builder if a {@link PrivateJwk} is desired. + * @param the type of the builder, for subtype method chaining + * @see #setPrivateKey(PrivateKey) * @since JJWT_RELEASE_VERSION */ -public interface PublicJwkBuilder, M extends PrivateJwk, P extends PrivateJwkBuilder, T extends PublicJwkBuilder> extends AsymmetricJwkBuilder { +public interface PublicJwkBuilder, M extends PrivateJwk, + P extends PrivateJwkBuilder, + T extends PublicJwkBuilder> extends AsymmetricJwkBuilder { + /** + * Sets the {@link PrivateKey} that pairs with the builder's existing {@link PublicKey}, converting this builder + * into a {@link PrivateJwkBuilder} which will produce a corresponding {@link PrivateJwk} instance. The + * specified {@code privateKey} MUST be the exact private key paired with the builder's public key. + * + * @param privateKey the {@link PrivateKey} that pairs with the builder's existing {@link PublicKey} + * @return the builder coerced as a {@link PrivateJwkBuilder} which will produce a corresponding {@link PrivateJwk}. + */ P setPrivateKey(L privateKey); } diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java index ab7dd5025..d345e480e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java +++ b/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java @@ -19,6 +19,22 @@ import java.security.interfaces.RSAPublicKey; /** + * JWK representation of an {@link RSAPrivateKey} as defined by the JWA (RFC 7518) specification sections on + * Parameters for RSA Keys and + * Parameters for RSA Private Keys. + * + *