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:
+ *
+ * - org.cryptomator.automaticAccessGrant (upstream)
+ * - cloud.katta.storage
+ *
+ * It has at two recipients:
+ *
+ * - org.cryptomator.hub.memberkey shared with vault members (having access to the member key)
+ * - org.cryptomator.hub.recoverykey. shared with vault owners (having access to the recovery key)
+ *
*/
@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(