Skip to content

Commit aa89937

Browse files
committed
Refactor to load vaults only once with short-lived user keys.
1 parent c231282 commit aa89937

File tree

5 files changed

+205
-204
lines changed

5 files changed

+205
-204
lines changed

hub/src/main/java/cloud/katta/protocols/hub/HubSession.java

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,13 @@
3030
import ch.cyberduck.core.oauth.OAuth2AuthorizationService;
3131
import ch.cyberduck.core.oauth.OAuth2ErrorResponseInterceptor;
3232
import ch.cyberduck.core.oauth.OAuth2RequestInterceptor;
33-
import ch.cyberduck.core.preferences.PreferencesFactory;
33+
import ch.cyberduck.core.preferences.HostPreferencesFactory;
3434
import ch.cyberduck.core.proxy.ProxyFinder;
3535
import ch.cyberduck.core.ssl.X509KeyManager;
3636
import ch.cyberduck.core.ssl.X509TrustManager;
3737
import ch.cyberduck.core.synchronization.ComparisonService;
3838
import ch.cyberduck.core.threading.CancelCallback;
3939
import ch.cyberduck.core.transfer.TransferStatus;
40-
import ch.cyberduck.core.vault.VaultRegistry;
4140

4241
import org.apache.http.impl.client.HttpClientBuilder;
4342
import org.apache.logging.log4j.LogManager;
@@ -54,6 +53,7 @@
5453
import cloud.katta.client.model.ConfigDto;
5554
import cloud.katta.client.model.UserDto;
5655
import cloud.katta.core.DeviceSetupCallback;
56+
import cloud.katta.crypto.DeviceKeys;
5757
import cloud.katta.crypto.UserKeys;
5858
import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService;
5959
import cloud.katta.protocols.hub.serializer.HubConfigDtoDeserializer;
@@ -77,18 +77,24 @@ public class HubSession extends HttpSession<HubApiClient> {
7777
*/
7878
private final Scheduler<?> access = new HubGrantAccessSchedulerService(this, keychain);
7979

80-
private HubVaultListService vaults;
81-
8280
/**
8381
* Interceptor for OpenID connect flow
8482
*/
8583
private OAuth2RequestInterceptor authorizationService;
84+
8685
private UserDto me;
86+
private ConfigDto config;
87+
private UserKeys userKeys;
88+
private AttributedList<Path> vaults;
8789

8890
public HubSession(final Host host, final X509TrustManager trust, final X509KeyManager key) {
8991
super(host, trust, key);
9092
}
9193

94+
public static HubSession coerce(final Session<?> session) {
95+
return (HubSession) session;
96+
}
97+
9298
@Override
9399
protected HubApiClient connect(final ProxyFinder proxy, final HostKeyCallback key,
94100
final LoginCallback prompt, final CancelCallback cancel) throws BackgroundException {
@@ -99,20 +105,19 @@ protected HubApiClient connect(final ProxyFinder proxy, final HostKeyCallback ke
99105
final HubApiClient client = new HubApiClient(host, configuration.build());
100106
try {
101107
// Obtain OAuth configuration
102-
final ConfigDto configDto = new ConfigResourceApi(client).apiConfigGet();
103-
104-
int minHubApiLevel = PreferencesFactory.get().getInteger("cloud.katta.min_api_level");
105-
final Integer apiLevel = configDto.getApiLevel();
108+
config = new ConfigResourceApi(client).apiConfigGet();
109+
final int minHubApiLevel = HostPreferencesFactory.get(host).getInteger("cloud.katta.min_api_level");
110+
final Integer apiLevel = config.getApiLevel();
106111
if(apiLevel == null || apiLevel < minHubApiLevel) {
107112
final String detail = String.format("Client requires API level at least %s, found %s, for hub %s", minHubApiLevel, apiLevel, host);
108113
log.error(detail);
109114
throw new InteroperabilityException(LocaleFactory.localizedString("Login failed", "Credentials"), detail);
110115
}
111116

112-
final String hubId = configDto.getUuid();
117+
final String hubId = config.getUuid();
113118
log.debug("Configure bookmark with id {}", hubId);
114119
host.setUuid(hubId);
115-
final Profile profile = new Profile(bundled, new HubConfigDtoDeserializer(configDto));
120+
final Profile profile = new Profile(bundled, new HubConfigDtoDeserializer(config));
116121
log.debug("Apply profile {} to bookmark {}", profile, host);
117122
host.setProtocol(profile);
118123
}
@@ -156,27 +161,32 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw
156161
// Ensure device key is available
157162
final DeviceSetupCallback setup = prompt.getFeature(DeviceSetupCallback.class);
158163
log.debug("Configured with setup prompt {}", setup);
159-
this.pair(setup);
164+
userKeys = this.pair(setup);
160165
// Ensure vaults are registered
161-
final OAuthTokens tokens = new OAuthTokens(credentials.getOauth().getAccessToken(), credentials.getOauth().getRefreshToken(), credentials.getOauth().getExpiryInMilliseconds(),
162-
credentials.getOauth().getIdToken());
163-
vaults = new HubVaultListService(this, tokens);
164-
vaults.list(Home.root(), new DisabledListProgressListener());
166+
try {
167+
vaults = new HubVaultListService(this, prompt).list(Home.root(), new DisabledListProgressListener());
168+
}
169+
finally {
170+
// Short-lived
171+
userKeys.destroy();
172+
}
165173
}
166174
catch(ApiException e) {
167175
throw new HubExceptionMappingService().map(e);
168176
}
169177
}
170178

171-
private void pair(final DeviceSetupCallback setup) throws BackgroundException {
179+
private UserKeys pair(final DeviceSetupCallback setup) throws BackgroundException {
172180
try {
173-
final UserKeys userKeys = new UserKeysServiceImpl(this, keychain).getOrCreateUserKeys(host, me,
174-
new DeviceKeysServiceImpl(keychain).getOrCreateDeviceKeys(host, setup), setup);
181+
final DeviceKeys deviceKeys = new DeviceKeysServiceImpl(keychain).getOrCreateDeviceKeys(host, setup);
182+
log.debug("Retrieved device keys {}", deviceKeys);
183+
final UserKeys userKeys = new UserKeysServiceImpl(this, keychain).getOrCreateUserKeys(host, me, deviceKeys, setup);
175184
log.debug("Retrieved user keys {}", userKeys);
185+
return userKeys;
176186
}
177187
catch(SecurityFailure e) {
178188
// Repeat until canceled by user
179-
this.pair(setup);
189+
return this.pair(setup);
180190
}
181191
catch(AccessException e) {
182192
throw new ConnectionCanceledException(e);
@@ -192,15 +202,35 @@ protected void logout() {
192202
client.getHttpClient().close();
193203
}
194204

205+
/**
206+
*
207+
* @return Null prior login
208+
*/
195209
public UserDto getMe() {
196210
return me;
197211
}
198212

213+
/**
214+
*
215+
* @return Null when not connected
216+
*/
217+
public ConfigDto getConfig() {
218+
return config;
219+
}
220+
221+
/**
222+
*
223+
* @return Destroyed keys after login
224+
*/
225+
public UserKeys getUserKeys() {
226+
return userKeys;
227+
}
228+
199229
@Override
200230
@SuppressWarnings("unchecked")
201231
public <T> T _getFeature(final Class<T> type) {
202232
if(type == ListService.class) {
203-
return (T) vaults;
233+
return (T) (ListService) (directory, listener) -> vaults;
204234
}
205235
if(type == Scheduler.class) {
206236
return (T) access;

hub/src/main/java/cloud/katta/protocols/hub/HubUVFVault.java

Lines changed: 105 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,79 @@
44

55
package cloud.katta.protocols.hub;
66

7-
import ch.cyberduck.core.AbstractPath;
7+
import ch.cyberduck.core.Credentials;
88
import ch.cyberduck.core.DisabledCancelCallback;
99
import ch.cyberduck.core.DisabledHostKeyCallback;
10-
import ch.cyberduck.core.DisabledListProgressListener;
1110
import ch.cyberduck.core.DisabledLoginCallback;
12-
import ch.cyberduck.core.ListService;
11+
import ch.cyberduck.core.Host;
1312
import ch.cyberduck.core.PasswordCallback;
1413
import ch.cyberduck.core.Path;
14+
import ch.cyberduck.core.PathAttributes;
15+
import ch.cyberduck.core.Protocol;
16+
import ch.cyberduck.core.ProtocolFactory;
1517
import ch.cyberduck.core.Session;
16-
import ch.cyberduck.core.cryptomator.ContentWriter;
18+
import ch.cyberduck.core.SessionFactory;
1719
import ch.cyberduck.core.cryptomator.UVFVault;
1820
import ch.cyberduck.core.exception.BackgroundException;
21+
import ch.cyberduck.core.exception.InteroperabilityException;
1922
import ch.cyberduck.core.exception.UnsupportedException;
20-
import ch.cyberduck.core.features.Directory;
21-
import ch.cyberduck.core.features.Write;
22-
import ch.cyberduck.core.preferences.PreferencesFactory;
23+
import ch.cyberduck.core.features.AttributesFinder;
2324
import ch.cyberduck.core.proxy.ProxyFactory;
24-
import ch.cyberduck.core.transfer.TransferStatus;
25+
import ch.cyberduck.core.ssl.X509KeyManager;
26+
import ch.cyberduck.core.ssl.X509TrustManager;
27+
import ch.cyberduck.core.vault.VaultUnlockCancelException;
2528

29+
import org.apache.http.HttpStatus;
2630
import org.apache.logging.log4j.LogManager;
2731
import org.apache.logging.log4j.Logger;
2832

29-
import java.nio.charset.StandardCharsets;
3033
import java.util.EnumSet;
34+
import java.util.UUID;
35+
36+
import cloud.katta.client.ApiException;
37+
import cloud.katta.crypto.uvf.UvfMetadataPayload;
38+
import cloud.katta.crypto.uvf.UvfMetadataPayloadPasswordCallback;
39+
import cloud.katta.crypto.uvf.VaultMetadataJWEBackendDto;
40+
import cloud.katta.protocols.hub.exceptions.HubExceptionMappingService;
41+
import cloud.katta.workflows.VaultServiceImpl;
42+
import cloud.katta.workflows.exceptions.AccessException;
43+
import cloud.katta.workflows.exceptions.SecurityFailure;
44+
import com.fasterxml.jackson.core.JsonProcessingException;
45+
46+
import static cloud.katta.protocols.s3.S3AssumeRoleProtocol.OAUTH_TOKENEXCHANGE_VAULT;
3147

3248
/**
3349
* Unified vault format (UVF) implementation for Katta
3450
*/
3551
public class HubUVFVault extends UVFVault {
3652
private static final Logger log = LogManager.getLogger(HubUVFVault.class);
3753

38-
private final Session<?> storage;
39-
private final Path home;
54+
private final UUID vaultId;
55+
56+
/**
57+
* Storage connection only available after loading vault
58+
*/
59+
private Session<?> storage;
4060

41-
public HubUVFVault(final Session<?> storage, final Path home) {
61+
/**
62+
* Constructor for factory creating new vault
63+
*
64+
* @param home Bucket
65+
*/
66+
public HubUVFVault(final Path home) {
4267
super(home);
43-
this.storage = storage;
44-
this.home = home;
68+
this.vaultId = UUID.fromString(home.getName());
69+
}
70+
71+
/**
72+
* Open from existing metadata
73+
*
74+
* @param vaultId Vault ID Used to lookup profile
75+
* @param bucket Bucket name
76+
*/
77+
public HubUVFVault(final UUID vaultId, final String bucket) {
78+
super(new Path(bucket, EnumSet.of(Path.Type.directory, Path.Type.volume)));
79+
this.vaultId = vaultId;
4580
}
4681

4782
public Session<?> getStorage() {
@@ -73,46 +108,66 @@ public synchronized void close() {
73108
}
74109

75110
/**
76-
* Upload vault template into existing bucket (permanent credentials)
111+
*
112+
* @param session Hub Connection
113+
* @param prompt Return user keys
114+
* @return Vault configuration with storage connection
77115
*/
78-
// TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 review @dko check method signature?
79-
public synchronized Path create(final Session<?> session, final String region, final String metadata, final String hashedRootDirId, final byte[] rootDirUvf) throws BackgroundException {
80-
log.debug("Uploading vault template {} in {} ", home, session.getHost());
81-
82-
// N.B. there seems to be no API to check write permissions without actually writing.
83-
if(!session.getFeature(ListService.class).list(home, new DisabledListProgressListener()).isEmpty()) {
84-
throw new BackgroundException("Bucket not empty", String.format("Cannot upload bucket %s in %s is not empty.", home, session.getHost()));
116+
@Override
117+
public HubUVFVault load(final Session<?> session, final PasswordCallback prompt) throws BackgroundException {
118+
try {
119+
final HubSession hub = HubSession.coerce(session);
120+
// Find storage configuration in vault metadata
121+
final VaultServiceImpl vaultService = new VaultServiceImpl(hub);
122+
final UvfMetadataPayload vaultMetadata = vaultService.getVaultMetadataJWE(vaultId, hub.getUserKeys());
123+
final Protocol profile = ProtocolFactory.get().forName(vaultId.toString());
124+
log.debug("Loaded profile {} for vault {}", profile, this);
125+
final Credentials credentials = new Credentials(hub.getHost().getCredentials());
126+
log.debug("Copy credentials {}", credentials);
127+
final VaultMetadataJWEBackendDto vaultStorageMetadata = vaultMetadata.storage();
128+
if(vaultStorageMetadata.getUsername() != null) {
129+
credentials.setUsername(vaultStorageMetadata.getUsername());
130+
}
131+
if(vaultStorageMetadata.getPassword() != null) {
132+
credentials.setPassword(vaultStorageMetadata.getPassword());
133+
}
134+
final Host bookmark = new Host(profile, credentials);
135+
log.debug("Configure bookmark for vault {}", vaultStorageMetadata);
136+
bookmark.setNickname(vaultStorageMetadata.getNickname());
137+
bookmark.setDefaultPath(vaultStorageMetadata.getDefaultPath());
138+
bookmark.setProperty(OAUTH_TOKENEXCHANGE_VAULT, vaultId.toString());
139+
// region as chosen by user upon vault creation (STS) or as retrieved from bucket (permanent)
140+
bookmark.setRegion(vaultStorageMetadata.getRegion());
141+
log.debug("Configured {} for vault {}", bookmark, this);
142+
storage = SessionFactory.create(bookmark, session.getFeature(X509TrustManager.class), session.getFeature(X509KeyManager.class));
143+
log.debug("Connect to {}", storage);
144+
storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback());
145+
storage.login(new DisabledLoginCallback(), new DisabledCancelCallback());
146+
final PathAttributes attr = storage.getFeature(AttributesFinder.class).find(home);
147+
attr.setDisplayname(vaultMetadata.storage().getNickname());
148+
home.setAttributes(attr);
149+
log.debug("Initialize vault {} with metadata {}", this, vaultMetadata);
150+
// Initialize cryptors
151+
super.load(storage, new UvfMetadataPayloadPasswordCallback(vaultMetadata.toJSON()));
152+
return this;
153+
}
154+
catch(ApiException e) {
155+
if(HttpStatus.SC_FORBIDDEN == e.getCode()) {
156+
throw new VaultUnlockCancelException(this, e);
157+
}
158+
throw new HubExceptionMappingService().map(e);
159+
}
160+
catch(JsonProcessingException | SecurityFailure | AccessException e) {
161+
throw new InteroperabilityException(e.getMessage(), e);
85162
}
86-
87-
// See https://github.com/cryptomator/hub/blob/develop/frontend/src/common/vaultconfig.ts
88-
// zip.file('vault.cryptomator', this.vaultConfigToken);
89-
// zip.folder('d')?.folder(this.rootDirHash.substring(0, 2))?.folder(this.rootDirHash.substring(2));
90-
91-
// /vault.uvf
92-
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));
93-
Directory<?> directory = (Directory<?>) session._getFeature(Directory.class);
94-
95-
// TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 implement CryptoDirectory for uvf
96-
// Path secondLevel = this.directoryProvider.toEncrypted(session, this.home.attributes().getDirectoryId(), this.home);
97-
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));
98-
final Path firstLevel = secondLevel.getParent();
99-
final Path dataDir = firstLevel.getParent();
100-
log.debug("Create vault root directory at {}", secondLevel);
101-
final TransferStatus status = (new TransferStatus()).setRegion(region);
102-
103-
directory.mkdir(session._getFeature(Write.class), dataDir, status);
104-
directory.mkdir(session._getFeature(Write.class), firstLevel, status);
105-
directory.mkdir(session._getFeature(Write.class), secondLevel, status);
106-
new ContentWriter(session).write(new Path(secondLevel, "dir.uvf", EnumSet.of(AbstractPath.Type.file)), rootDirUvf);
107-
return home;
108163
}
109164

110165
@Override
111-
public HubUVFVault load(final Session<?> ignore, final PasswordCallback prompt) throws BackgroundException {
112-
log.debug("Connect to {}", storage);
113-
storage.open(ProxyFactory.get(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback());
114-
storage.login(new DisabledLoginCallback(), new DisabledCancelCallback());
115-
super.load(storage, prompt);
116-
return this;
166+
public String toString() {
167+
final StringBuilder sb = new StringBuilder("HubUVFVault{");
168+
sb.append("storage=").append(storage);
169+
sb.append(", vaultId=").append(vaultId);
170+
sb.append('}');
171+
return sb.toString();
117172
}
118173
}

0 commit comments

Comments
 (0)