Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
java-version: 21
cache: maven
- name: Build with Maven
run: mvn --no-transfer-progress --batch-mode verify
run: mvn --no-transfer-progress --batch-mode verify -U
env:
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}
TESTCONTAINERS_RYUK_DISABLED: true
48 changes: 23 additions & 25 deletions hub/src/main/java/ch/iterate/hub/crypto/uvf/UvfMetadataPayload.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@
import ch.cyberduck.core.AlphanumericRandomStringService;
import ch.cyberduck.core.cryptomator.random.FastSecureRandomProvider;

import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.util.encoders.Base32;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.UVFMasterkey;
import org.cryptomator.cryptolib.common.P384KeyPair;
import org.openapitools.jackson.nullable.JsonNullableModule;

Expand All @@ -26,8 +25,8 @@
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;

import at.favre.lib.hkdf.HKDF;
import ch.iterate.hub.crypto.exceptions.NotECKeyException;
import ch.iterate.hub.model.JWEPayload;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
Expand Down Expand Up @@ -93,8 +92,14 @@ public static UvfMetadataPayload fromJWE(final String jwe) throws JsonProcessing
return mapper.readValue(jwe, UvfMetadataPayload.class);
}

public String toJSON() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JsonNullableModule());
return mapper.writeValueAsString(this);
}

public static UvfMetadataPayload create() {
final String kid = new AlphanumericRandomStringService(4).random();
final String kid = Base64URL.encode(new AlphanumericRandomStringService(4).random()).toString();
final byte[] rawSeed = new byte[32];
FastSecureRandomProvider.get().provide().nextBytes(rawSeed);
final byte[] kdfSalt = new byte[32];
Expand All @@ -103,28 +108,21 @@ public static UvfMetadataPayload create() {
.withFileFormat("AES-256-GCM-32k")
.withNameFormat("AES-SIV-512-B64URL")
.withSeeds(new HashMap<String, String>() {{
put(kid, Base64URL.encode(rawSeed).toString());
put(kid, Base64.getEncoder().encodeToString(rawSeed));
}})
.withLatestSeed(kid)
.withinitialSeed(kid)
.withKdf("HKDF-SHA512")
.withKdfSalt(Base64URL.encode(kdfSalt).toString());
}

public byte[] computeRootDirId() {
return HKDF.fromHmacSha512().extractAndExpand(Base64URL.from(kdfSalt()).decode(), Base64URL.from(seeds().get(initialSeed())).decode(), "rootDirId".getBytes(), 256 / 8);
.withKdfSalt(Base64.getEncoder().encodeToString(kdfSalt));
}

public String computeRootDirIdHash(final byte[] rootDirId) {
final byte[] hmacKey = HKDF.fromHmacSha512()
.extractAndExpand(Base64URL.from(kdfSalt()).decode(), Base64URL.from(seeds().get(initialSeed())).decode(), "hmac".getBytes(), 512 / 8);
final Digest digest = new SHA256Digest();
final HMac hMac = new HMac(digest);
hMac.init(new KeyParameter(hmacKey));
hMac.update(rootDirId, 0, rootDirId.length);
final byte[] hmacOut = new byte[hMac.getMacSize()];
hMac.doFinal(hmacOut, 0);
return Base32.toBase32String(Arrays.copyOfRange(hmacOut, 0, 20));
public String computeRootDirIdHash() throws JsonProcessingException {
final UVFMasterkey masterKey = UVFMasterkey.fromDecryptedPayload(this.toJSON());
final CryptorProvider provider = CryptorProvider.forScheme(CryptorProvider.Scheme.UVF_DRAFT);
final Cryptor cryptor = provider.provide(masterKey, FastSecureRandomProvider.get().provide());
final byte[] rootDirId = masterKey.rootDirId();
final String hashedRootDirId = cryptor.fileNameCryptor(masterKey.firstRevision()).hashDirectoryId(rootDirId);
return hashedRootDirId;
}


Expand Down Expand Up @@ -328,11 +326,11 @@ public String toString() {
return "UvfMetadataPayload{" +
"fileFormat='" + fileFormat + '\'' +
", nameFormat='" + nameFormat + '\'' +
", seeds=" + seeds +
", seeds={" + seeds.entrySet().stream().map(e -> e.getKey() + "=" + StringUtils.repeat("*", Integer.min(8, StringUtils.length(e.getValue())))).collect(Collectors.joining(", ")) + "}" +
", initialSeed='" + initialSeed + '\'' +
", latestSeed='" + latestSeed + '\'' +
", kdf='" + kdf + '\'' +
", kdfSalt='" + kdfSalt + '\'' +
", kdfSalt='" + StringUtils.repeat("*", Integer.min(8, StringUtils.length(kdf))) + '\'' +
", automaticAccessGrant=" + automaticAccessGrant +
", storage=" + storage +
'}';
Expand Down
68 changes: 28 additions & 40 deletions hub/src/main/java/ch/iterate/hub/protocols/hub/HubCryptoVault.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@
import ch.cyberduck.core.AbstractPath;
import ch.cyberduck.core.DisabledListProgressListener;
import ch.cyberduck.core.ListService;
import ch.cyberduck.core.LoginOptions;
import ch.cyberduck.core.PasswordCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.Session;
import ch.cyberduck.core.cryptomator.ContentWriter;
import ch.cyberduck.core.cryptomator.CryptoVault;
import ch.cyberduck.core.cryptomator.UVFVault;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.features.Directory;
import ch.cyberduck.core.preferences.PreferencesFactory;
Expand All @@ -22,47 +20,50 @@

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.UVFMasterkey;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.EnumSet;

/**
* Cryptomator vault implementation for Cipherduck (without masterkey file).
*/
public class HubCryptoVault extends CryptoVault {
public class HubCryptoVault extends UVFVault {
private static final Logger log = LogManager.getLogger(HubCryptoVault.class);
private final String decryptedPayload;

// See https://github.com/cryptomator/hub/blob/develop/frontend/src/common/vaultconfig.ts
//const jwtPayload: VaultConfigPayload = {
// jti: vaultId,
// format: 8,
// cipherCombo: 'SIV_GCM',
// shorteningThreshold: 220
//};
//const header = JSON.stringify({
// kid: kid,
// typ: 'jwt',
// alg: 'HS256',
// hub: hubConfig
//});
private static final VaultConfig VAULT_CONFIG = new VaultConfig(8, 220, CryptorProvider.Scheme.SIV_GCM, "HS256", null);

public HubCryptoVault(final Path home) {
super(home);
this(home, null, null, null); // TODO cleanup
}

public HubCryptoVault(final Path home, final String masterkey, final String config, final byte[] pepper) {
super(home);
public HubCryptoVault(final Path home, final String decryptedPayload, final String config, final byte[] pepper) {
super(home, decryptedPayload, config, pepper);
this.decryptedPayload = decryptedPayload;
}

public Path encrypt(Session<?> session, Path file, byte[] directoryId, boolean metadata) throws BackgroundException {
log.debug("HubCryptoVault.encrypt. Use directory ID '{}' for folder {}", directoryId, file);
return super.encrypt(session, file, directoryId, metadata);
}


@Override
public Path getHome() {
final Path home = super.getHome();
final UVFMasterkey masterKey = UVFMasterkey.fromDecryptedPayload(this.decryptedPayload);
byte[] directoryId = masterKey.rootDirId();
assert directoryId != null;
home.attributes().setDirectoryId(directoryId);
return home;
}


/**
* Upload vault template into existing bucket (permanent credentials)
*/
// TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 review @dko check method signature?
public synchronized Path create(final Session<?> session, final String region, final VaultCredentials credentials, final int version, final String metadata, final String rootDirHash) throws BackgroundException {
public synchronized Path create(final Session<?> session, final String region, final VaultCredentials credentials, final int version, final String metadata, final String hashedRootDirId) throws BackgroundException {
final Path home = new Path(session.getHost().getDefaultPath(), EnumSet.of(AbstractPath.Type.directory));
log.debug("Uploading vault template {} in {} ", home, session.getHost());

Expand All @@ -75,11 +76,11 @@ public synchronized Path create(final Session<?> session, final String region, f
// zip.file('vault.cryptomator', this.vaultConfigToken);
// zip.folder('d')?.folder(this.rootDirHash.substring(0, 2))?.folder(this.rootDirHash.substring(2));
(new ContentWriter(session)).write(new Path(home, PreferencesFactory.get().getProperty("cryptomator.vault.config.filename"), EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.vault)), metadata.getBytes(StandardCharsets.US_ASCII));
Directory<?> directory = (Directory) session._getFeature(Directory.class);
Directory<?> directory = (Directory<?>) session._getFeature(Directory.class);

// TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 implement CryptoDirectory for uvf
// Path secondLevel = this.directoryProvider.toEncrypted(session, this.home.attributes().getDirectoryId(), this.home);
final Path secondLevel = new Path(String.format("/%s/d/%s/%s/", session.getHost().getDefaultPath(), rootDirHash.substring(0, 2), rootDirHash.substring(2)), EnumSet.of(AbstractPath.Type.directory));
final Path secondLevel = new Path(String.format("/%s/d/%s/%s/", session.getHost().getDefaultPath(), hashedRootDirId.substring(0, 2), hashedRootDirId.substring(2)), EnumSet.of(AbstractPath.Type.directory));
final Path firstLevel = secondLevel.getParent();
final Path dataDir = firstLevel.getParent();
log.debug("Create vault root directory at {}", secondLevel);
Expand All @@ -91,19 +92,6 @@ public synchronized Path create(final Session<?> session, final String region, f
return home;
}

@Override
public HubCryptoVault load(final Session<?> session, final PasswordCallback prompt) throws BackgroundException {
// no-interactive prompt in Cipherduck
final String masterkey = prompt.prompt(session.getHost(), "", "", new LoginOptions()).getPassword();
try {
this.open(VAULT_CONFIG, new Masterkey(Base64.getDecoder().decode(masterkey)));
}
catch(IllegalArgumentException e) {
throw new BackgroundException(e);
}
return this;
}

public Path getMasterkey() {
// No master key in vault
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@
import ch.iterate.hub.workflows.VaultServiceImpl;
import ch.iterate.hub.workflows.exceptions.AccessException;
import ch.iterate.hub.workflows.exceptions.SecurityFailure;
import com.google.common.primitives.Bytes;
import com.nimbusds.jose.util.Base64URL;
import com.fasterxml.jackson.core.JsonProcessingException;

public class S3AutoLoadVaultSession extends S3AssumeRoleSession {
private static final Logger log = LogManager.getLogger(S3AutoLoadVaultSession.class);
Expand Down Expand Up @@ -84,25 +83,21 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw
super.login(prompt, cancel);
final Path home = new DelegatingHomeFeature(new DefaultPathHomeFeature(host)).find();
log.debug("Attempting to locate vault in {}", home);
final Vault vault = VaultFactory.get(home);
// TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! MUST NEVER BE RELEASED LIKE THIS
// TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 use rawFileKey,rawNameKey as vault key for now (going into cryptolib's Masterkey)
// Retrieve from keychain
final DeviceKeys deviceKeys = new DeviceKeysServiceImpl(keychain).getDeviceKeys(backend.getHost());
final UvfMetadataPayload vaultMetadata = new VaultServiceImpl(backend).getVaultMetadataJWE(
UUID.fromString(host.getUuid()), new UserKeysServiceImpl(backend).getUserKeys(backend.getHost(), backend.getMe(), deviceKeys));
final byte[] rawFileKey = Base64URL.from(vaultMetadata.seeds().get(vaultMetadata.latestSeed())).decode();
final byte[] rawNameKey = Base64URL.from(vaultMetadata.seeds().get(vaultMetadata.latestSeed())).decode();
final byte[] vaultKey = Bytes.concat(rawFileKey, rawNameKey);
final String decryptedPayload = vaultMetadata.toJSON();
final Vault vault = VaultFactory.get(home, decryptedPayload, "", new byte[0]);
registry.add(vault.load(this, new DisabledPasswordCallback() {
@Override
public Credentials prompt(final Host bookmark, final String title, final String reason, final LoginOptions options) {
return new VaultCredentials(Base64.getEncoder().encodeToString(vaultKey));
return new VaultCredentials(decryptedPayload);
}
}));
backend.close();
}
catch(ApiException | SecurityFailure | AccessException e) {
catch(ApiException | SecurityFailure | AccessException | JsonProcessingException e) {
throw new LoginFailureException(LocaleFactory.localizedString("Login failed", "Credentials"), e);
}
}
Expand Down
Loading
Loading