Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix legacy hub compatibility #315

Merged
merged 9 commits into from
Feb 13, 2025
128 changes: 103 additions & 25 deletions backend/src/main/java/org/cryptomator/hub/api/UserDto.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.cryptomator.hub.api;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.annotation.Nullable;
import org.cryptomator.hub.entities.User;
Expand All @@ -10,45 +11,122 @@

public final class UserDto extends AuthorityDto {

private final String email;
private final String language;
private final Set<DeviceResource.DeviceDto> devices;
private final String ecdhPublicKey;
private final String ecdsaPublicKey;
private final String privateKeys;
private final String setupCode;

@JsonCreator
public UserDto(
@JsonProperty("id") String id,
@JsonProperty("name") String name,
@JsonProperty("pictureUrl") String pictureUrl,
@JsonProperty("email") String email,
@JsonProperty("language") String language,
@JsonProperty("devices") Set<DeviceResource.DeviceDto> devices,
// Accept either "ecdhPublicKey" or the legacy "publicKey" on input
@Nullable
@JsonProperty("ecdhPublicKey") @OnlyBase64Chars String ecdhPublicKey,
@Nullable
@JsonProperty("publicKey") @OnlyBase64Chars String publicKey,
overheadhunter marked this conversation as resolved.
Show resolved Hide resolved
@Nullable
@JsonProperty("ecdsaPublicKey") @OnlyBase64Chars String ecdsaPublicKey,
// Accept either "privateKeys" or the legacy "privateKey" on input
@Nullable
@JsonProperty("privateKeys") @ValidJWE String privateKeys,
@Nullable
@JsonProperty("privateKey") @ValidJWE String privateKey,
@Nullable
@JsonProperty("setupCode") @ValidJWE String setupCode) {
super(id, Type.USER, name, pictureUrl);
this.email = email;
this.language = language;
this.devices = devices;
if (ecdhPublicKey != null) {
this.ecdhPublicKey = ecdhPublicKey;
} else {
this.ecdhPublicKey = publicKey;
}
overheadhunter marked this conversation as resolved.
Show resolved Hide resolved
this.ecdsaPublicKey = ecdsaPublicKey;
if (privateKeys != null) {
this.privateKeys = privateKeys;
} else {
this.privateKeys = privateKey;
}
overheadhunter marked this conversation as resolved.
Show resolved Hide resolved
this.setupCode = setupCode;
}

@JsonProperty("email")
public final String email;
public String getEmail() {
return email;
}

@JsonProperty("language")
public final String language;
public String getLanguage() {
return language;
}

@JsonProperty("devices")
public final Set<DeviceResource.DeviceDto> devices;
public Set<DeviceResource.DeviceDto> getDevices() {
return devices;
}

@JsonProperty("ecdhPublicKey")
public final String ecdhPublicKey;
@JsonProperty("ecdsaPublicKey")
public final String ecdsaPublicKey;
@JsonProperty("privateKey") // singular name for history reasons (don't break client compatibility)
public final String privateKeys;
@JsonProperty("setupCode")
public final String setupCode;
public String getEcdhPublicKey() {
return ecdhPublicKey;
}

/**
* Same as {@link #ecdhPublicKey}, kept for compatibility purposes
* @deprecated to be removed when all clients moved to the new DTO field names
* @deprecated to be removed in Hub 2.0.0, tracked in <a href="https://github.com/cryptomator/hub/issues/316">#316</a>
*/
@Deprecated(forRemoval = true)
@JsonProperty("publicKey")
public final String legacyEcdhPublicKey;
public String getPublicKey() {
return ecdhPublicKey;
}

UserDto(@JsonProperty("id") String id, @JsonProperty("name") String name, @JsonProperty("pictureUrl") String pictureUrl, @JsonProperty("email") String email, @JsonProperty("language") String language, @JsonProperty("devices") Set<DeviceResource.DeviceDto> devices,
@Nullable @JsonProperty("ecdhPublicKey") @OnlyBase64Chars String ecdhPublicKey, @Nullable @JsonProperty("ecdsaPublicKey") @OnlyBase64Chars String ecdsaPublicKey, @Nullable @JsonProperty("privateKeys") @ValidJWE String privateKeys, @Nullable @JsonProperty("setupCode") @ValidJWE String setupCode) {
super(id, Type.USER, name, pictureUrl);
this.email = email;
this.language = language;
this.devices = devices;
this.ecdhPublicKey = ecdhPublicKey;
this.ecdsaPublicKey = ecdsaPublicKey;
this.privateKeys = privateKeys;
this.setupCode = setupCode;
@JsonProperty("ecdsaPublicKey")
public String getEcdsaPublicKey() {
return ecdsaPublicKey;
}

// duplicate fields to maintain backwards compatibility:
this.legacyEcdhPublicKey = ecdhPublicKey;
@JsonProperty("privateKeys")
public String getPrivateKeys() {
return privateKeys;
}

/**
* Same as {@link #privateKeys}, kept for compatibility purposes
* @deprecated to be removed in Hub 2.0.0, tracked in <a href="https://github.com/cryptomator/hub/issues/316">#316</a>
*/
@Deprecated(forRemoval = true)
@JsonProperty("privateKey")
public String getPrivateKey() {
return privateKeys;
}

@JsonProperty("setupCode")
public String getSetupCode() {
return setupCode;
}

public static UserDto justPublicInfo(User user) {
return new UserDto(user.getId(), user.getName(), user.getPictureUrl(), user.getEmail(), user.getLanguage(), Set.of(), user.getEcdhPublicKey(), user.getEcdsaPublicKey(),null, null);
return new UserDto(
user.getId(),
user.getName(),
user.getPictureUrl(),
user.getEmail(),
user.getLanguage(),
Set.of(),
user.getEcdhPublicKey(),
user.getEcdhPublicKey(),
user.getEcdsaPublicKey(),
null,
null,
null);
}
SailReal marked this conversation as resolved.
Show resolved Hide resolved
}
42 changes: 22 additions & 20 deletions backend/src/main/java/org/cryptomator/hub/api/UsersResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,18 @@ public Response putMe(@Nullable @Valid UserDto dto) {
user.setPictureUrl(jwt.getClaim("picture"));
user.setEmail(jwt.getClaim("email"));
if (dto != null) {
if (!Objects.equals(user.getSetupCode(), dto.setupCode)) {
user.setSetupCode(dto.setupCode);
if (!Objects.equals(user.getSetupCode(), dto.getSetupCode())) {
user.setSetupCode(dto.getSetupCode());
eventLogger.logUserSetupCodeChanged(jwt.getSubject());
}
if (!Objects.equals(user.getEcdhPublicKey(), dto.ecdhPublicKey) || !Objects.equals(user.getEcdsaPublicKey(), dto.ecdsaPublicKey) || !Objects.equals(user.getPrivateKeys(), dto.privateKeys)) {
user.setEcdhPublicKey(dto.ecdhPublicKey);
user.setEcdsaPublicKey(dto.ecdsaPublicKey);
user.setPrivateKeys(dto.privateKeys);
if (!Objects.equals(user.getEcdhPublicKey(), dto.getEcdhPublicKey()) || !Objects.equals(user.getEcdsaPublicKey(), dto.getEcdsaPublicKey()) || !Objects.equals(user.getPrivateKeys(), dto.getPrivateKeys())) {
user.setEcdhPublicKey(dto.getEcdhPublicKey());
user.setEcdsaPublicKey(dto.getEcdsaPublicKey());
user.setPrivateKeys(dto.getPrivateKeys());
eventLogger.logUserKeysChanged(jwt.getSubject(), jwt.getName());
}
updateDevices(user, dto);
user.setLanguage(dto.language);
user.setLanguage(dto.getLanguage());
}
userRepo.persist(user);
return Response.created(URI.create(".")).build();
Expand All @@ -104,18 +104,20 @@ public Response putMe(@Nullable @Valid UserDto dto) {
* @param userDto The DTO
*/
private void updateDevices(User userEntity, UserDto userDto) {
var devices = userEntity.devices.stream().collect(Collectors.toUnmodifiableMap(Device::getId, Function.identity()));
var updatedDevices = userDto.devices.stream()
.filter(d -> devices.containsKey(d.id())) // only look at DTOs for which we find a matching existing entity
.map(dto -> {
var device = devices.get(dto.id());
device.setType(dto.type());
device.setName(dto.name());
device.setPublickey(dto.publicKey());
device.setUserPrivateKeys(dto.userPrivateKeys());
return device;
});
deviceRepo.persist(updatedDevices);
if (userDto.getDevices() != null) {
var devices = userEntity.devices.stream().collect(Collectors.toUnmodifiableMap(Device::getId, Function.identity()));
var updatedDevices = userDto.getDevices().stream()
.filter(d -> devices.containsKey(d.id())) // only look at DTOs for which we find a matching existing entity
.map(dto -> {
var device = devices.get(dto.id());
device.setType(dto.type());
device.setName(dto.name());
device.setPublickey(dto.publicKey());
device.setUserPrivateKeys(dto.userPrivateKeys());
return device;
});
deviceRepo.persist(updatedDevices);
}
}

@POST
Expand Down Expand Up @@ -158,7 +160,7 @@ public UserDto getMe(@QueryParam("withDevices") boolean withDevices) {
User user = userRepo.findById(jwt.getSubject());
Function<Device, DeviceResource.DeviceDto> mapDevices = d -> new DeviceResource.DeviceDto(d.getId(), d.getName(), d.getType(), d.getPublickey(), d.getUserPrivateKeys(), d.getOwner().getId(), d.getCreationTime().truncatedTo(ChronoUnit.MILLIS));
var devices = withDevices ? user.devices.stream().map(mapDevices).collect(Collectors.toSet()) : Set.<DeviceResource.DeviceDto>of();
return new UserDto(user.getId(), user.getName(), user.getPictureUrl(), user.getEmail(), user.getLanguage(), devices, user.getEcdhPublicKey(), user.getEcdsaPublicKey(), user.getPrivateKeys(), user.getSetupCode());
return new UserDto(user.getId(), user.getName(), user.getPictureUrl(), user.getEmail(), user.getLanguage(), devices, user.getEcdhPublicKey(), user.getEcdhPublicKey(), user.getEcdsaPublicKey(), user.getPrivateKeys(), user.getPrivateKeys(), user.getSetupCode());
overheadhunter marked this conversation as resolved.
Show resolved Hide resolved
}

@POST
Expand Down
3 changes: 2 additions & 1 deletion backend/src/main/resources/dev-realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@
"enabled": true,
"serviceAccountClientId": "cryptomatorhub-cli",
"realmRoles": [
"user"
"user",
"create-vaults"
],
"clientRoles" : {
"realm-management" : [ "manage-users", "view-users" ]
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/common/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export type UserDto = {
accessibleVaults: VaultDto[];
ecdhPublicKey?: string;
ecdsaPublicKey?: string;
privateKey?: string;
privateKeys?: string;
setupCode?: string;
}

Expand Down
6 changes: 3 additions & 3 deletions frontend/src/common/userdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ class UserData {
*/
public async decryptUserKeysWithSetupCode(setupCode: string): Promise<UserKeys> {
const me = await this.me;
if (!me.privateKey) {
if (!me.privateKeys) {
throw new Error('User not initialized.');
}
const userKeys = await UserKeys.recover(me.privateKey, setupCode, await this.ecdhPublicKey, await this.ecdsaPublicKey);
const userKeys = await UserKeys.recover(me.privateKeys, setupCode, await this.ecdhPublicKey, await this.ecdsaPublicKey);
await this.addEcdsaKeyIfMissing(userKeys);
return userKeys;
}
Expand Down Expand Up @@ -128,7 +128,7 @@ class UserData {
if (me.setupCode && !me.ecdsaPublicKey) {
const payload: { setupCode: string } = await JWEParser.parse(me.setupCode).decryptEcdhEs(userKeys.ecdhKeyPair.privateKey);
me.ecdsaPublicKey = await userKeys.encodedEcdsaPublicKey();
me.privateKey = await userKeys.encryptWithSetupCode(payload.setupCode);
me.privateKeys = await userKeys.encryptWithSetupCode(payload.setupCode);
for (const device of me.devices) {
device.userPrivateKey = await userKeys.encryptForDevice(base64.parse(device.publicKey));
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/InitialSetup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ async function createUserKey() {
const userKeys = await UserKeys.create();
me.ecdhPublicKey = await userKeys.encodedEcdhPublicKey();
me.ecdsaPublicKey = await userKeys.encodedEcdsaPublicKey();
me.privateKey = await userKeys.encryptWithSetupCode(setupCode.value);
me.privateKeys = await userKeys.encryptWithSetupCode(setupCode.value);
me.setupCode = await JWEBuilder.ecdhEs(userKeys.ecdhKeyPair.publicKey).encrypt({ setupCode: setupCode.value });
const browserKeys = await userdata.createBrowserKeys();
await submitBrowserKeys(browserKeys, me, userKeys);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/RegenerateSetupCodeDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ async function regenerateSetupCode() {
const me = await userdata.me;
const newCode = crypto.randomUUID();
const userKeys = await userdata.decryptUserKeysWithBrowser();
me.privateKey = await userKeys.encryptWithSetupCode(newCode);
me.privateKeys = await userKeys.encryptWithSetupCode(newCode);
me.setupCode = await JWEBuilder.ecdhEs(userKeys.ecdhKeyPair.publicKey).encrypt({ setupCode: newCode });
await backend.users.putMe(me);
setupCode.value = newCode;
Expand Down