diff --git a/hub/src/main/java/cloud/katta/crypto/uvf/UvfMetadataPayload.java b/hub/src/main/java/cloud/katta/crypto/uvf/UvfMetadataPayload.java index 5cae5546..088d66ab 100644 --- a/hub/src/main/java/cloud/katta/crypto/uvf/UvfMetadataPayload.java +++ b/hub/src/main/java/cloud/katta/crypto/uvf/UvfMetadataPayload.java @@ -24,6 +24,7 @@ import java.text.ParseException; import java.util.Arrays; import java.util.Base64; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -32,6 +33,7 @@ import cloud.katta.crypto.exceptions.NotECKeyException; import cloud.katta.model.JWEPayload; +import cloud.katta.workflows.exceptions.SecurityFailure; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; @@ -56,9 +58,27 @@ /** * Represents payload of vault.uvf metadata. * Counterpart of MetadataPayload. + *

+ * It has two custom fields: + *

+ * It has at two recipients: + * */ @JsonIgnoreProperties(ignoreUnknown = true) public class UvfMetadataPayload extends JWEPayload { + private static final String UVF_SPEC_VERSION_KEY_PARAM = "uvf.spec.version"; + + private static final String KID_MEMBERKEY = "org.cryptomator.hub.memberkey"; + private static final String KID_RECOVERYKEY_PREFIX = "org.cryptomator.hub.recoverykey.%s"; + + private static final String UVF_FILEFORMAT = "AES-256-GCM-32k"; + private static final String UVF_NAME_FORMAT = "AES-SIV-512-B64URL"; @JsonProperty(value = "fileFormat", required = true) String fileFormat; @@ -106,9 +126,11 @@ public static UvfMetadataPayload create() { FastSecureRandomProvider.get().provide().nextBytes(rawSeed); final byte[] kdfSalt = new byte[32]; FastSecureRandomProvider.get().provide().nextBytes(kdfSalt); + + return new UvfMetadataPayload() - .withFileFormat("AES-256-GCM-32k") - .withNameFormat("AES-SIV-512-B64URL") + .withFileFormat(UVF_FILEFORMAT) + .withNameFormat(UVF_NAME_FORMAT) .withSeeds(new HashMap() {{ put(kid, Base64.getUrlEncoder().encodeToString(rawSeed)); }}) @@ -151,7 +173,7 @@ public OctetSequenceKey memberKey() { private UniversalVaultFormatJWKS() throws JOSEException { memberKey = new OctetSequenceKeyGenerator(256) - .keyID("org.cryptomator.hub.memberkey") + .keyID(KID_MEMBERKEY) .algorithm(JWEAlgorithm.A256KW) .generate(); @@ -161,10 +183,11 @@ private UniversalVaultFormatJWKS() throws JOSEException { recoveryKey.getPublic()) .build(); + recoveryKeyJWK = new ECKey.Builder(Curve.P_384, recoveryKey.getPublic()) .algorithm(JWEAlgorithm.ECDH_ES_A256KW) - .keyID(String.format("org.cryptomator.hub.recoverykey.%s", recoveryKeyJWKWithoutThumbprint.computeThumbprint())) + .keyID(String.format("%s%s", KID_RECOVERYKEY_PREFIX, recoveryKeyJWKWithoutThumbprint.computeThumbprint())) .privateKey(recoveryKey.getPrivate()) .build(); } @@ -188,8 +211,9 @@ public UvfAccessTokenPayload toOwnerAccessToken() { } public static OctetSequenceKey memberKeyFromRawKey(final byte[] raw) { + return new OctetSequenceKey.Builder(raw) - .keyID("org.cryptomator.hub.memberkey") + .keyID(KID_MEMBERKEY) .algorithm(JWEAlgorithm.A256KW) .build(); } @@ -294,9 +318,19 @@ public UvfMetadataPayload withStorage(final VaultMetadataJWEBackendDto backend) * @param jwe The jwe * @param jwk The jwk */ - public static UvfMetadataPayload decryptWithJWK(final String jwe, final JWK jwk) throws ParseException, JOSEException, JsonProcessingException { + public static UvfMetadataPayload decryptWithJWK(final String jwe, final JWK jwk) throws ParseException, JOSEException, JsonProcessingException, SecurityFailure { final JWEObjectJSON jweObject = JWEObjectJSON.parse(jwe); - jweObject.decrypt(new MultiDecrypter(jwk)); + jweObject.decrypt(new MultiDecrypter(jwk, Collections.singleton(UVF_SPEC_VERSION_KEY_PARAM))); + + // https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.11 + // Recipients MAY consider the JWS to be invalid if the critical + // list contains any Header Parameter names defined by this + // specification or [JWA] for use with JWS or if any other constraints on its use are violated. + final Object uvfSpecVersion = jweObject.getHeader().getCustomParams().get(UVF_SPEC_VERSION_KEY_PARAM); + if(!"1".equals(uvfSpecVersion)) { + throw new SecurityFailure(String.format("Unexpected value for critical header %s: found %s, expected \"1\"", UVF_SPEC_VERSION_KEY_PARAM, uvfSpecVersion)); + } + final Payload payload = jweObject.getPayload(); return UvfMetadataPayload.fromJWE(payload.toString()); } @@ -309,10 +343,16 @@ public static UvfMetadataPayload decryptWithJWK(final String jwe, final JWK jwk) * @param keys recipient keys for whom to encrypt */ public String encrypt(final String apiURL, final UUID vaultId, final JWKSet keys) throws JOSEException { + // spec: https://github.com/encryption-alliance/unified-vault-format/tree/develop/vault%20metadata#jose-header + // web frontend implementation: https://github.com/shift7-ch/katta-server/blob/feature/cipherduck-uvf/frontend/src/common/universalVaultFormat.ts#L343-L346 final JWEObjectJSON builder = new JWEObjectJSON( new JWEHeader.Builder(EncryptionMethod.A256GCM) - .customParam("origin", String.format("%s/vaults/%s/uvf/vault.uvf", apiURL, vaultId.toString())) + // kid goes into recipient-specific header + .customParam("origin", URI.create(String.format("%s/vaults/%s/uvf/vault.uvf", apiURL, vaultId.toString())).normalize().toString()) .jwkURL(URI.create("jwks.json")) + .contentType("json") + .criticalParams(Collections.singleton(UVF_SPEC_VERSION_KEY_PARAM)) + .customParam(UVF_SPEC_VERSION_KEY_PARAM, "1") .build(), new Payload(new HashMap() {{ put("fileFormat", fileFormat); diff --git a/hub/src/test/java/cloud/katta/crypto/uvf/UvfMetadataPayloadTest.java b/hub/src/test/java/cloud/katta/crypto/uvf/UvfMetadataPayloadTest.java index 5f05a158..9f49b2ec 100644 --- a/hub/src/test/java/cloud/katta/crypto/uvf/UvfMetadataPayloadTest.java +++ b/hub/src/test/java/cloud/katta/crypto/uvf/UvfMetadataPayloadTest.java @@ -15,6 +15,7 @@ import ch.cyberduck.core.ssl.DisabledX509TrustManager; import org.cryptomator.cryptolib.api.UVFMasterkey; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; @@ -30,8 +31,10 @@ import cloud.katta.crypto.exceptions.NotECKeyException; import cloud.katta.protocols.hub.HubProtocol; import cloud.katta.protocols.hub.HubSession; +import cloud.katta.workflows.exceptions.SecurityFailure; import com.fasterxml.jackson.core.JsonProcessingException; import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWEObjectJSON; import com.nimbusds.jose.jwk.Curve; import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jose.jwk.JWKSet; @@ -75,7 +78,7 @@ void recoveryKeyToOwnerAccessTokenAndBack() throws JOSEException, ParseException } @Test - void encryptDecrypt() throws JOSEException, JsonProcessingException, ParseException { + void encryptDecrypt() throws JOSEException, JsonProcessingException, ParseException, SecurityFailure { final byte[] rawMasterKey = new byte[32]; FastSecureRandomProvider.get().provide().nextBytes(rawMasterKey); final HashMap keys = new HashMap() {{ @@ -103,17 +106,20 @@ void encryptDecrypt() throws JOSEException, JsonProcessingException, ParseExcept final OctetSequenceKey memberKey = jwks.memberKey(); final ECKey recoveryKey = jwks.recoveryKey(); - final String encrypted = orig.encrypt("https://example.com/api/", UUID.randomUUID(), jwks.toJWKSet()); + final UUID vaultId = UUID.randomUUID(); + final String encrypted = orig.encrypt("https://example.com/api/", vaultId, jwks.toJWKSet()); // decrypt with memberKey { final UvfMetadataPayload decrypted = UvfMetadataPayload.decryptWithJWK(encrypted, memberKey); + assertEquals(String.format("https://example.com/api/vaults/%s/uvf/vault.uvf", vaultId), JWEObjectJSON.parse(encrypted).getHeader().getCustomParams().get("origin")); assertEquals(orig, decrypted); } // decrypt with recoveryKey { final UvfMetadataPayload decrypted = UvfMetadataPayload.decryptWithJWK(encrypted, recoveryKey); + assertEquals(String.format("https://example.com/api/vaults/%s/uvf/vault.uvf", vaultId), JWEObjectJSON.parse(encrypted).getHeader().getCustomParams().get("origin")); assertEquals(orig, decrypted); } @@ -126,7 +132,8 @@ void encryptDecrypt() throws JOSEException, JsonProcessingException, ParseExcept } @Test - void decryptWithRecoveryKey() throws ParseException, JOSEException, NoSuchAlgorithmException, InvalidKeySpecException, NotECKeyException, JsonProcessingException { + @Disabled("TODO uvf.spec.version missing in protected.") + void decryptWithRecoveryKey() throws ParseException, JOSEException, NoSuchAlgorithmException, InvalidKeySpecException, NotECKeyException, JsonProcessingException, SecurityFailure { // https://datatracker.ietf.org/doc/html/rfc7516#section-7.2.1 final String jwe = "{\"protected\":\"eyJvcmlnaW4iOiJodHRwczovL2V4YW1wbGUuY29tL2FwaS92YXVsdHMvVE9ETy91dmYvdmF1bHQudXZmIiwiamt1Ijoiandrcy5qc29uIiwiZW5jIjoiQTI1NkdDTSJ9\",\"recipients\":[{\"header\":{\"kid\":\"org.cryptomator.hub.memberkey\",\"alg\":\"A256KW\"},\"encrypted_key\":\"XLoNIWvDKQqaDurrGt7VK9s2aggSMir7fS4ZdBUxdTxceCOHndo4kA\"},{\"header\":{\"kid\":\"org.cryptomator.hub.recoverykey.v2nb-mGX4POKMWCQKOogMWTlAn7DDqEOjjEGCsPEeco\",\"alg\":\"ECDH-ES+A256KW\",\"epk\":{\"key_ops\":[],\"ext\":true,\"kty\":\"EC\",\"x\":\"j6Retxx-L-rURQ4WNc8LvoqjbdPtGS6n9pCJgcm1U-NAWuWEvwJ_qi2tlrv_4w4p\",\"y\":\"wS-Emo-Q9qdtkHMJiDfVDAaxhF2-nSkDRn2Eg9CbG0pVwGEpaDybx_YYJwIaYooO\",\"crv\":\"P-384\"},\"apu\":\"\",\"apv\":\"\"},\"encrypted_key\":\"iNGgybMqmiXn_lbKLMMTpg38i1f00O6Zj65d5nzsLw3hyzuylGWpvA\"}],\"iv\":\"Pfy90C9SSq2gJr6B\",\"ciphertext\":\"ogYR1pZN9k97zEgO9Fj3ePQramtaUdHWq95geXD7FH1oB6T7fEOvdU2AEGWOcbIbQihn-eOqG2_5oTol16O_nQ4HcDOJ9w4R9EdpByuWG-kVNh_fpWeQjIuH4kO-Rtbf05JRVG2jexWopbIA8uHuoiOXSNpSYPTzTKirp2hU7w3sE01zycsu06HiasUX-tKZH_hbyiUEdTlFFLcvKpRwnYOQf6QMw0uY1IbUTX1cJY9LO5SpD8bZFZOd6hg_Qnsdcq52I8KkZyxocgqdW7P5OSUrv5z8DCLMPdByEpaz9cCOzQQvtZwHxJy82O4vDAh89QA_AzfK8J7TI5zJRlTGQgrNhiaVBC85fN3tMSv8sLfJs7rC_5LiVW5ZeqbQ52sAZQw0lfwgGpMmxsdMzPoVOLD8OxvX\",\"tag\":\"3Jiv6kI4Qoso60T0dRv9vIlca-P4UFyHqh-TEZvargM\"}"; final ECKey key = new ECKey.Builder(