Skip to content
Draft
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
33 changes: 33 additions & 0 deletions hub/src/main/java/cloud/katta/crypto/uvf/UvfJWKCallback.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 shift7 GmbH. All rights reserved.
*/

package cloud.katta.crypto.uvf;

import ch.cyberduck.core.Host;
import ch.cyberduck.core.LoginOptions;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.core.vault.JWKCallback;
import ch.cyberduck.core.vault.JWKCredentials;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.nimbusds.jose.jwk.JWK;

public class UvfJWKCallback implements JWKCallback {

private final JWK key;

public UvfJWKCallback(final JWK key) throws JsonProcessingException {
this.key = key;
}

@Override
public void close(final String input) {
//
}

@Override
public JWKCredentials prompt(final Host bookmark, final String title, final String reason, final LoginOptions options) throws LoginCanceledException {
return new JWKCredentials(key);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2025 shift7 GmbH. All rights reserved.
*/

package cloud.katta.crypto.uvf;

import ch.cyberduck.core.Host;
import ch.cyberduck.core.HostUrlProvider;
import ch.cyberduck.core.cryptomator.impl.uvf.VaultMetadataUVFProvider;
import ch.cyberduck.core.vault.VaultMetadataProvider;

import java.nio.charset.StandardCharsets;
import java.util.UUID;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.nimbusds.jose.JOSEException;

public class VaultIdMetadataUVFProvider implements VaultMetadataUVFProvider {

private final UUID vaultId;
private final UvfMetadataPayload.UniversalVaultFormatJWKS jwks;
private final byte[] vaultMetadata;
private final byte[] rootDirectoryMetadata;
private final String hashedRootDirId;

public VaultIdMetadataUVFProvider(final Host storage, final UUID vaultId,
final UvfMetadataPayload.UniversalVaultFormatJWKS jwks, final UvfMetadataPayload vaultMetadata) throws JOSEException, JsonProcessingException {
this(vaultId, jwks, vaultMetadata.encrypt(
String.format("%s/api", new HostUrlProvider(false, true).get(storage)),
vaultId,
jwks.toJWKSet()
).getBytes(StandardCharsets.US_ASCII),
vaultMetadata.computeRootDirUvf(),
vaultMetadata.computeRootDirIdHash()
);
}

public VaultIdMetadataUVFProvider(final UUID vaultId, final UvfMetadataPayload.UniversalVaultFormatJWKS jwks, final byte[] vaultMetadata, final byte[] rootDirectoryMetadata, final String hashedRootDirId) {
this.vaultId = vaultId;
this.jwks = jwks;
this.vaultMetadata = vaultMetadata;
this.rootDirectoryMetadata = rootDirectoryMetadata;
this.hashedRootDirId = hashedRootDirId;
}

public static VaultIdMetadataUVFProvider cast(VaultMetadataProvider provider) {
if(provider instanceof VaultIdMetadataUVFProvider) {
return (VaultIdMetadataUVFProvider) provider;
}
else {
throw new IllegalArgumentException("Unsupported metadata type " + provider.getClass());
}
}

public UUID getVaultId() {
return vaultId;
}

public UvfMetadataPayload.UniversalVaultFormatJWKS getJwks() {
return jwks;
}

@Override
public byte[] getMetadata() {
return vaultMetadata;
}

@Override
public byte[] getRootDirectoryMetadata() {
return rootDirectoryMetadata;
}

@Override
public String getDirPath() {
return hashedRootDirId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public UvfMetadataPayload toUvfMetadataPayload(final Path bucket) {
.provider(this.getProfile())
.defaultPath(bucket.getAbsolute())
.region(this.getRegion())
.nickname(null != bucket.attributes().getDisplayname() ? bucket.attributes().getDisplayname() : "Vault"))
.nickname(bucket.attributes().getDisplayname()))
.withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto()
.enabled(true)
.maxWotDepth(null));
Expand Down
123 changes: 33 additions & 90 deletions hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,19 @@
import ch.cyberduck.core.DisabledCancelCallback;
import ch.cyberduck.core.DisabledHostKeyCallback;
import ch.cyberduck.core.Host;
import ch.cyberduck.core.HostUrlProvider;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.PasswordCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.PathAttributes;
import ch.cyberduck.core.Session;
import ch.cyberduck.core.cryptomator.ContentWriter;
import ch.cyberduck.core.cryptomator.UVFVault;
import ch.cyberduck.core.cryptomator.AbstractVault;
import ch.cyberduck.core.cryptomator.impl.uvf.CryptoVault;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.InteroperabilityException;
import ch.cyberduck.core.exception.UnsupportedException;
import ch.cyberduck.core.features.AttributesFinder;
import ch.cyberduck.core.features.Directory;
import ch.cyberduck.core.features.Write;
import ch.cyberduck.core.preferences.HostPreferencesFactory;
import ch.cyberduck.core.preferences.PreferencesFactory;
import ch.cyberduck.core.proxy.ProxyFactory;
import ch.cyberduck.core.s3.S3Session;
import ch.cyberduck.core.transfer.TransferStatus;
import ch.cyberduck.core.vault.VaultCredentials;
import ch.cyberduck.core.vault.VaultMetadataProvider;
import ch.cyberduck.core.vault.VaultUnlockCancelException;

import org.apache.logging.log4j.LogManager;
Expand All @@ -35,7 +28,6 @@

import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.EnumSet;
import java.util.UUID;

import cloud.katta.client.ApiException;
Expand All @@ -44,8 +36,7 @@
import cloud.katta.client.model.VaultDto;
import cloud.katta.core.DeviceSetupCallback;
import cloud.katta.crypto.UserKeys;
import cloud.katta.crypto.uvf.UvfMetadataPayload;
import cloud.katta.crypto.uvf.UvfMetadataPayloadPasswordCallback;
import cloud.katta.crypto.uvf.VaultIdMetadataUVFProvider;
import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService;
import cloud.katta.protocols.s3.S3AssumeRoleProtocol;
import com.fasterxml.jackson.core.JsonProcessingException;
Expand All @@ -54,12 +45,9 @@
/**
* Unified vault format (UVF) implementation for Katta
*/
public class HubUVFVault extends UVFVault {
public class HubUVFVault extends CryptoVault {
private static final Logger log = LogManager.getLogger(HubUVFVault.class);

private final UUID vaultId;
private final UvfMetadataPayload vaultMetadata;

/**
* Storage connection only available after loading vault
*/
Expand All @@ -68,17 +56,13 @@ public class HubUVFVault extends UVFVault {

/**
*
* @param storage Storage connection
* @param vaultId Vault Id
* @param vaultMetadata Vault UVF metadata
* @param prompt Login prompt to access storage
* @param storage Storage connection
* @param bucket Vault UVF metadata
* @param prompt Login prompt to access storage
*/
public HubUVFVault(final Session<?> storage, final UUID vaultId, final UvfMetadataPayload vaultMetadata, final LoginCallback prompt) {
super(new Path(vaultMetadata.storage().getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume),
new PathAttributes().setDisplayname(vaultMetadata.storage().getNickname())));
public HubUVFVault(final Session<?> storage, final Path bucket, final LoginCallback prompt) {
super(bucket);
this.storage = storage;
this.vaultId = vaultId;
this.vaultMetadata = vaultMetadata;
this.login = prompt;
}

Expand Down Expand Up @@ -115,36 +99,32 @@ public synchronized void close() {
}

@Override
public Path create(final Session<?> session, final String region, final VaultCredentials noop) throws BackgroundException {
public AbstractVault create(final Session<?> session, final String region, final VaultMetadataProvider metadata) throws BackgroundException {
try {
final HubSession hub = HubSession.coerce(session);
log.debug("Created metadata JWE {}", vaultMetadata);
final UvfMetadataPayload.UniversalVaultFormatJWKS jwks = UvfMetadataPayload.createKeys();
final Path home = this.getHome();
final UUID vaultId = VaultIdMetadataUVFProvider.cast(metadata).getVaultId();
final VaultDto vaultDto = new VaultDto()
.id(vaultId)
.name(vaultMetadata.storage().getNickname())
.name(home.attributes().getDisplayname())
.description(null)
.archived(false)
.creationTime(DateTime.now())
.uvfMetadataFile(vaultMetadata.encrypt(
String.format("%s/api", new HostUrlProvider(false, true).get(session.getHost())),
vaultId,
jwks.toJWKSet()
))
.uvfKeySet(jwks.serializePublicRecoverykey());
.uvfMetadataFile(new String(VaultIdMetadataUVFProvider.cast(metadata).getMetadata(), StandardCharsets.US_ASCII))
.uvfKeySet(VaultIdMetadataUVFProvider.cast(metadata).getJwks().serializePublicRecoverykey());
// Create vault in Hub
final VaultResourceApi vaultResourceApi = new VaultResourceApi(hub.getClient());
log.debug("Create vault {}", vaultDto);
vaultResourceApi.apiVaultsVaultIdPut(vaultDto.getId(), vaultDto,
vaultResourceApi.apiVaultsVaultIdPut(vaultId, vaultDto,
storage.getHost().getProtocol().isRoleConfigurable() && !S3Session.isAwsHostname(storage.getHost().getHostname()),
storage.getHost().getProtocol().isRoleConfigurable() && S3Session.isAwsHostname(storage.getHost().getHostname()));
// Upload JWE
log.debug("Grant access to vault {}", vaultDto);
final UserDto userDto = hub.getMe();
final DeviceSetupCallback setup = login.getFeature(DeviceSetupCallback.class);
final UserKeys userKeys = hub.getUserKeys(setup);
vaultResourceApi.apiVaultsVaultIdAccessTokensPost(vaultDto.getId(),
Collections.singletonMap(userDto.getId(), jwks.toOwnerAccessToken().encryptForUser(userKeys.ecdhKeyPair().getPublic())));
vaultResourceApi.apiVaultsVaultIdAccessTokensPost(vaultId,
Collections.singletonMap(userDto.getId(), VaultIdMetadataUVFProvider.cast(metadata).getJwks().toOwnerAccessToken().encryptForUser(userKeys.ecdhKeyPair().getPublic())));
// Upload vault template to storage
log.debug("Connect to {}", storage);
final Host configuration = storage.getHost();
Expand All @@ -156,36 +136,8 @@ public Path create(final Session<?> session, final String region, final VaultCre
// No role chaining when creating vault
configuration.setProperty(S3AssumeRoleProtocol.S3_ASSUMEROLE_ROLEARN_TAG, null);
storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), login, new DisabledCancelCallback());
final Path vault;
if(false) {
log.debug("Upload vault template to {}", storage);
return super.create(storage,
HubStorageLocationService.StorageLocation.fromIdentifier(region).getRegion(), noop);
}
else { // Obsolete when implemented in super
final Directory<?> directory = (Directory<?>) storage._getFeature(Directory.class);
final Path home = this.getHome();
log.debug("Create vault root directory at {}", home);
final TransferStatus status = (new TransferStatus()).setRegion(HubStorageLocationService.StorageLocation.fromIdentifier(region).getRegion());
vault = directory.mkdir(storage._getFeature(Write.class), home, status);

final String hashedRootDirId = vaultMetadata.computeRootDirIdHash();
final Path dataDir = new Path(vault, "d", EnumSet.of(Path.Type.directory));
final Path firstLevel = new Path(dataDir, hashedRootDirId.substring(0, 2), EnumSet.of(Path.Type.directory));
final Path secondLevel = new Path(firstLevel, hashedRootDirId.substring(2), EnumSet.of(Path.Type.directory));

directory.mkdir(storage._getFeature(Write.class), dataDir, status);
directory.mkdir(storage._getFeature(Write.class), firstLevel, status);
directory.mkdir(storage._getFeature(Write.class), secondLevel, status);

// vault.uvf
new ContentWriter(storage).write(new Path(home, PreferencesFactory.get().getProperty("cryptomator.vault.config.filename"),
EnumSet.of(Path.Type.file, Path.Type.vault)), vaultDto.getUvfMetadataFile().getBytes(StandardCharsets.US_ASCII));
// dir.uvf
new ContentWriter(storage).write(new Path(secondLevel, "dir.uvf", EnumSet.of(Path.Type.file)),
vaultMetadata.computeRootDirUvf());
}
return vault;
log.debug("Upload vault template to {}", storage);
return super.create(storage, HubStorageLocationService.StorageLocation.fromIdentifier(region).getRegion(), metadata);
}
catch(JOSEException | JsonProcessingException e) {
throw new InteroperabilityException(e.getMessage(), e);
Expand All @@ -199,38 +151,29 @@ public Path create(final Session<?> session, final String region, final VaultCre
*
* @param session Hub Connection
* @param prompt Return user keys
* @param metadata metadata
* @return Vault configuration with storage connection
*/
@Override
public HubUVFVault load(final Session<?> session, final PasswordCallback prompt) throws BackgroundException {
public HubUVFVault load(final Session<?> session, final PasswordCallback prompt, final VaultMetadataProvider metadata) throws BackgroundException {
log.debug("Connect to {}", storage);
try {
log.debug("Connect to {}", storage);
try {
storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), login, new DisabledCancelCallback());
}
catch(BackgroundException e) {
log.warn("Skip loading vault with failure {} connecting to storage", e.toString());
throw new VaultUnlockCancelException(this, e);
}
final Path home = this.getHome();
home.setAttributes(storage.getFeature(AttributesFinder.class).find(home)
.setDisplayname(vaultMetadata.storage().getNickname()));
log.debug("Initialize vault {} with metadata {}", this, vaultMetadata);
// Initialize cryptors
super.load(storage, new UvfMetadataPayloadPasswordCallback(vaultMetadata.toJSON()));
return this;
storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), login, new DisabledCancelCallback());
}
catch(JsonProcessingException e) {
throw new InteroperabilityException(e.getMessage(), e);
catch(BackgroundException e) {
log.warn("Skip loading vault with failure {} connecting to storage", e.toString());
throw new VaultUnlockCancelException(this, e);
}
log.debug("Initialize vault {}", this);
// Initialize cryptors
super.load(storage, prompt, metadata);
return this;
}

@Override
public String toString() {
final StringBuilder sb = new StringBuilder("HubUVFVault{");
sb.append("vaultId=").append(vaultId);
sb.append(", vaultMetadata=").append(vaultMetadata);
sb.append(", storage=").append(storage);
sb.append("storage=").append(storage);
sb.append('}');
return sb.toString();
}
Expand Down
Loading
Loading