diff --git a/pom.xml b/pom.xml index 50a90b72..5e5387e1 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.cryptomator cryptofs - 2.1.0 + 2.1.1 Cryptomator Crypto Filesystem This library provides the Java filesystem provider used by Cryptomator. https://github.com/cryptomator/cryptofs diff --git a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java index 0aeb37af..4a1392ac 100644 --- a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java +++ b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java @@ -6,7 +6,6 @@ import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.exceptions.SignatureVerificationException; -import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptolib.api.CryptorProvider; @@ -84,12 +83,12 @@ public String toToken(String keyId, byte[] rawKey) { /** * Convenience wrapper for {@link #decode(String)} and {@link UnverifiedVaultConfig#verify(byte[], int)} * - * @param token The token - * @param keyLoader A key loader capable of providing a key for this token + * @param token The token + * @param keyLoader A key loader capable of providing a key for this token * @param expectedVaultVersion The vault version this token should contain * @return The decoded configuration * @throws MasterkeyLoadingFailedException If the key loader was unable to provide a key for this vault configuration - * @throws VaultConfigLoadException When loading the configuration fails + * @throws VaultConfigLoadException When loading the configuration fails */ public static VaultConfig load(String token, MasterkeyLoader keyLoader, int expectedVaultVersion) throws MasterkeyLoadingFailedException, VaultConfigLoadException { var configLoader = decode(token); @@ -143,6 +142,7 @@ public URI getKeyId() { /** * Gets a value from the tokens header + * * @param key Which key to read * @param clazz Type of the value * @param Type of the value @@ -171,19 +171,30 @@ public int allegedShorteningThreshold() { return unverifiedConfig.getClaim(JSON_KEY_SHORTENING_THRESHOLD).asInt(); } + private Algorithm initAlgorithm(byte[] rawKey) throws VaultConfigLoadException { + var algo = unverifiedConfig.getAlgorithm(); + return switch (algo) { + case "HS256" -> Algorithm.HMAC256(rawKey); + case "HS384" -> Algorithm.HMAC384(rawKey); + case "HS512" -> Algorithm.HMAC512(rawKey); + default -> throw new VaultConfigLoadException("Unsupported signature algorithm: " + algo); + }; + } + /** * Decodes a vault configuration stored in JWT format. * - * @param rawKey The key matching the id in {@link #getKeyId()} + * @param rawKey The key matching the id in {@link #getKeyId()} * @param expectedVaultVersion The vault version this token should contain * @return The decoded configuration - * @throws VaultKeyInvalidException If the provided key was invalid + * @throws VaultKeyInvalidException If the provided key was invalid * @throws VaultVersionMismatchException If the token did not match the expected vault version - * @throws VaultConfigLoadException Generic parse error + * @throws VaultConfigLoadException Generic parse error */ public VaultConfig verify(byte[] rawKey, int expectedVaultVersion) throws VaultKeyInvalidException, VaultVersionMismatchException, VaultConfigLoadException { try { - var verifier = JWT.require(Algorithm.HMAC256(rawKey)) // + unverifiedConfig.getAlgorithm(); + var verifier = JWT.require(initAlgorithm(rawKey)) // .withClaim(JSON_KEY_VAULTVERSION, expectedVaultVersion) // .build(); var verifiedConfig = verifier.verify(unverifiedConfig); diff --git a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java index a7abbda7..0f6068d2 100644 --- a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java +++ b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java @@ -11,10 +11,13 @@ import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; @@ -34,6 +37,7 @@ public void setup() throws MasterkeyLoadingFailedException { } @Test + @DisplayName("test VaultConfig.load() with invalid token") public void testLoadMalformedToken() { Assertions.assertThrows(VaultConfigLoadException.class, () -> { VaultConfig.load("hello world", masterkeyLoader, 42); @@ -41,41 +45,73 @@ public void testLoadMalformedToken() { } @Nested - public class WithValidToken { + @DisplayName("VaultConfig ...") + public class WithExistingConfig { private VaultConfig originalConfig; - private String token; @BeforeEach public void setup() throws MasterkeyLoadingFailedException { originalConfig = VaultConfig.createNew().cipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).shorteningThreshold(220).build(); - token = originalConfig.toToken("TEST_KEY", rawKey); } @Test - public void testSuccessfulLoad() throws VaultConfigLoadException, MasterkeyLoadingFailedException { - var loaded = VaultConfig.load(token, masterkeyLoader, originalConfig.getVaultVersion()); + @DisplayName("toToken() is HS256-signed") + public void testToToken() { + var token = originalConfig.toToken("TEST_KEY", rawKey); + + Assertions.assertNotNull(token); + var decoded = JWT.decode(token); + Assertions.assertEquals("HS256", decoded.getAlgorithm()); + } + + } + + @Nested + @DisplayName("Using valid tokens...") + public class WithValidToken { + + private static final String TOKEN_NONE = "eyJraWQiOiJURVNUX0tFWSIsInR5cCI6IkpXVCIsImFsZyI6Im5vbmUifQ.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMCwianRpIjoiZjRiMjlmM2EtNDdkNi00NjlmLTk2NGMtZjRjMmRhZWU4ZWI2IiwiY2lwaGVyQ29tYm8iOiJTSVZfQ1RSTUFDIn0."; + private static final String TOKEN_HS256 = "eyJraWQiOiJURVNUX0tFWSIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMCwianRpIjoiZjRiMjlmM2EtNDdkNi00NjlmLTk2NGMtZjRjMmRhZWU4ZWI2IiwiY2lwaGVyQ29tYm8iOiJTSVZfQ1RSTUFDIn0.V7pqSXX1tBRgmntL1sXovnhNR4Z1_7z3Jzrq7NMqPO8"; + private static final String TOKEN_HS384 = "eyJraWQiOiJURVNUX0tFWSIsInR5cCI6IkpXVCIsImFsZyI6IkhTMzg0In0.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMCwianRpIjoiZjRiMjlmM2EtNDdkNi00NjlmLTk2NGMtZjRjMmRhZWU4ZWI2IiwiY2lwaGVyQ29tYm8iOiJTSVZfQ1RSTUFDIn0.rx03sCVAyrCmT6halPaFU46lu-DOd03iwDgvdw362hfgJj782q6xPXjAxdKeVKxG"; + private static final String TOKEN_HS512 = "eyJraWQiOiJURVNUX0tFWSIsInR5cCI6IkpXVCIsImFsZyI6IkhTNTEyIn0.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMCwianRpIjoiZjRiMjlmM2EtNDdkNi00NjlmLTk2NGMtZjRjMmRhZWU4ZWI2IiwiY2lwaGVyQ29tYm8iOiJTSVZfQ1RSTUFDIn0.fzkVI34Ou3z7RaFarS9VPCaA0NX9z7My14gAISTXJGKGNSID7xEcoaY56SBdWbU7Ta17KhxcHhbXffxk3Mzing"; + + @Test + public void testUnsupportedSignature() { + VaultConfigLoadException thrown = Assertions.assertThrows(VaultConfigLoadException.class, () -> { + VaultConfig.load(TOKEN_NONE, masterkeyLoader, 8); + }); + Assertions.assertEquals("Unsupported signature algorithm: none", thrown.getMessage()); + } + + @DisplayName("load token") + @ParameterizedTest(name = "signed with {0}") + @CsvSource({"HS256," + TOKEN_HS256, "HS384," + TOKEN_HS384, "HS512," + TOKEN_HS512}) + public void testSuccessfulLoad(String algo, String token) throws VaultConfigLoadException, MasterkeyLoadingFailedException { + Assumptions.assumeTrue(JWT.decode(token).getAlgorithm().equals(algo)); + + var loaded = VaultConfig.load(token, masterkeyLoader, 8); - Assertions.assertEquals(originalConfig.getId(), loaded.getId()); - Assertions.assertEquals(originalConfig.getVaultVersion(), loaded.getVaultVersion()); - Assertions.assertEquals(originalConfig.getCipherCombo(), loaded.getCipherCombo()); - Assertions.assertEquals(originalConfig.getShorteningThreshold(), loaded.getShorteningThreshold()); + Assertions.assertEquals(8, loaded.getVaultVersion()); + Assertions.assertEquals(CryptorProvider.Scheme.SIV_CTRMAC, loaded.getCipherCombo()); + Assertions.assertEquals(220, loaded.getShorteningThreshold()); } - @ParameterizedTest + @DisplayName("load using key with...") + @ParameterizedTest(name = "invalid byte at position {0}") @ValueSource(ints = {0, 1, 2, 3, 10, 20, 30, 63}) public void testLoadWithInvalidKey(int pos) { rawKey[pos] = (byte) 0x77; - Mockito.when(key.getEncoded()).thenReturn(rawKey); Assertions.assertThrows(VaultKeyInvalidException.class, () -> { - VaultConfig.load(token, masterkeyLoader, originalConfig.getVaultVersion()); + VaultConfig.load(TOKEN_HS256, masterkeyLoader, 8); }); } } @Test + @DisplayName("test VaultConfig.createNew()...") public void testCreateNew() { var config = VaultConfig.createNew().cipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).shorteningThreshold(220).build(); @@ -86,6 +122,7 @@ public void testCreateNew() { } @Test + @DisplayName("test VaultConfig.load(...)") public void testLoadExisting() throws VaultConfigLoadException, MasterkeyLoadingFailedException { var decodedJwt = Mockito.mock(DecodedJWT.class); var formatClaim = Mockito.mock(Claim.class); @@ -95,6 +132,7 @@ public void testLoadExisting() throws VaultConfigLoadException, MasterkeyLoading var verification = Mockito.mock(Verification.class); var verifier = Mockito.mock(JWTVerifier.class); Mockito.when(decodedJwt.getKeyId()).thenReturn("test:key"); + Mockito.when(decodedJwt.getAlgorithm()).thenReturn("HS256"); Mockito.when(decodedJwt.getClaim("format")).thenReturn(formatClaim); Mockito.when(decodedJwt.getClaim("cipherCombo")).thenReturn(cipherComboClaim); Mockito.when(decodedJwt.getClaim("shorteningThreshold")).thenReturn(maxFilenameLenClaim);