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

Web of Trust #281

Merged
merged 39 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6ec7f71
add `wot` and `effective_wot` tables
overheadhunter May 24, 2024
2843dea
add JPA entities for WoT
overheadhunter May 24, 2024
f11886f
add `PUT /api/users/trusted/{userId}`
overheadhunter May 24, 2024
7ce9db2
add `GET /api/users/trusted`
overheadhunter May 24, 2024
92e470c
updated ERM
overheadhunter May 24, 2024
c71cfbe
replace `String[]` with `List<String>`
overheadhunter May 24, 2024
5215e41
restore image size
overheadhunter May 24, 2024
3c6718b
move and simplify join condition, add explanation
overheadhunter May 24, 2024
02d251d
unused import
overheadhunter May 26, 2024
4798994
add `GET /api/users/trusted/{userId}`
overheadhunter May 26, 2024
19bcf6e
added TrustService
overheadhunter May 26, 2024
f5785ca
display Trust Icon alongside user details
overheadhunter May 26, 2024
0996eac
began implementing signatures
overheadhunter May 28, 2024
2ea94f0
Merge branch 'develop' into feature/wot
overheadhunter Jun 6, 2024
eac923b
missed one reference while renaming method
overheadhunter Jun 7, 2024
bfb1a92
explained deprecation
overheadhunter Jun 7, 2024
0bb3478
cherry-picked MemberDto from UVF branch
overheadhunter Jun 7, 2024
9d49a05
store signature
overheadhunter Jun 7, 2024
7e96abf
parse ES384-signed JWT
overheadhunter Jun 11, 2024
988d639
verify signature chain
overheadhunter Jun 11, 2024
216e2f9
moved WoT-related logic to its own .ts file
overheadhunter Jun 12, 2024
a2f8d7e
sign both ECDH as well as ECDSA keys
overheadhunter Jun 12, 2024
8207484
cherry picked JWK thumbprint function from b0790aa
overheadhunter Jun 12, 2024
3f88f0c
encoding the thumbprint is not part of the RFC
overheadhunter Jun 12, 2024
7efc6a1
prove identity using `wot.computeFingerprint()`
overheadhunter Jun 12, 2024
ef60c3e
improve efficiency by reusing trust data
overheadhunter Jun 12, 2024
9148042
parameterize WoT depth
overheadhunter Jun 21, 2024
f50d6fb
specify how much of a key to verify before signing
overheadhunter Jun 21, 2024
c25fd98
reintroduce `/settings` resource
overheadhunter Jun 21, 2024
b1ea583
make better use of type inference
overheadhunter Jun 21, 2024
f6ce0c0
log audit event when signing user identity
overheadhunter Jun 21, 2024
7dce8d2
move signing process to separate dialog
overheadhunter Jun 21, 2024
233da78
configure number formatting
overheadhunter Jun 22, 2024
99a527c
localize labels
overheadhunter Jun 22, 2024
819dcce
Merge branch 'develop' into feature/wot
overheadhunter Jun 28, 2024
5bbcacb
applied suggestions from code review
overheadhunter Jun 28, 2024
4199d38
addressed remarks 1, 2, 3, 4, 5 and 7 of PR review
overheadhunter Jun 28, 2024
6e10cc0
local signature validation
overheadhunter Jun 28, 2024
03cfd71
use a subset of type UserDto for fingerprint calc
overheadhunter Jun 29, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.cryptomator.hub.entities.events.AuditEvent;
import org.cryptomator.hub.entities.events.DeviceRegisteredEvent;
import org.cryptomator.hub.entities.events.DeviceRemovedEvent;
import org.cryptomator.hub.entities.events.SignedWotIdEvent;
import org.cryptomator.hub.entities.events.VaultAccessGrantedEvent;
import org.cryptomator.hub.entities.events.VaultCreatedEvent;
import org.cryptomator.hub.entities.events.VaultKeyRetrievedEvent;
Expand Down Expand Up @@ -80,6 +81,7 @@ public List<AuditEventDto> getAllEvents(@QueryParam("startDate") Instant startDa
@JsonSubTypes({ //
@JsonSubTypes.Type(value = DeviceRegisteredEventDto.class, name = DeviceRegisteredEvent.TYPE), //
@JsonSubTypes.Type(value = DeviceRemovedEventDto.class, name = DeviceRemovedEvent.TYPE), //
@JsonSubTypes.Type(value = SignedWotIdEvent.class, name = SignedWotIdEvent.TYPE), //
@JsonSubTypes.Type(value = VaultCreatedEventDto.class, name = VaultCreatedEvent.TYPE), //
@JsonSubTypes.Type(value = VaultUpdatedEventDto.class, name = VaultUpdatedEvent.TYPE), //
@JsonSubTypes.Type(value = VaultAccessGrantedEventDto.class, name = VaultAccessGrantedEvent.TYPE), //
Expand All @@ -101,6 +103,7 @@ static AuditEventDto fromEntity(AuditEvent entity) {
return switch (entity) {
case DeviceRegisteredEvent evt -> new DeviceRegisteredEventDto(evt.getId(), evt.getTimestamp(), DeviceRegisteredEvent.TYPE, evt.getRegisteredBy(), evt.getDeviceId(), evt.getDeviceName(), evt.getDeviceType());
case DeviceRemovedEvent evt -> new DeviceRemovedEventDto(evt.getId(), evt.getTimestamp(), DeviceRemovedEvent.TYPE, evt.getRemovedBy(), evt.getDeviceId());
case SignedWotIdEvent evt -> new SignedWotIdEventDto(evt.getId(), evt.getTimestamp(), SignedWotIdEvent.TYPE, evt.getUserId(), evt.getSignerId(), evt.getSignerKey(), evt.getSignature());
case VaultCreatedEvent evt -> new VaultCreatedEventDto(evt.getId(), evt.getTimestamp(), VaultCreatedEvent.TYPE, evt.getCreatedBy(), evt.getVaultId(), evt.getVaultName(), evt.getVaultDescription());
case VaultUpdatedEvent evt -> new VaultUpdatedEventDto(evt.getId(), evt.getTimestamp(), VaultUpdatedEvent.TYPE, evt.getUpdatedBy(), evt.getVaultId(), evt.getVaultName(), evt.getVaultDescription(), evt.isVaultArchived());
case VaultAccessGrantedEvent evt -> new VaultAccessGrantedEventDto(evt.getId(), evt.getTimestamp(), VaultAccessGrantedEvent.TYPE, evt.getGrantedBy(), evt.getVaultId(), evt.getAuthorityId());
Expand All @@ -121,6 +124,9 @@ record DeviceRegisteredEventDto(long id, Instant timestamp, String type, @JsonPr
record DeviceRemovedEventDto(long id, Instant timestamp, String type, @JsonProperty("removedBy") String removedBy, @JsonProperty("deviceId") String deviceId) implements AuditEventDto {
}

record SignedWotIdEventDto(long id, Instant timestamp, String type, @JsonProperty("userId") String userId, @JsonProperty("signerId") String signerId, @JsonProperty("signerKey") String signerKey, @JsonProperty("signature") String signature) implements AuditEventDto {
}

record VaultCreatedEventDto(long id, Instant timestamp, String type, @JsonProperty("createdBy") String createdBy, @JsonProperty("vaultId") UUID vaultId, @JsonProperty("vaultName") String vaultName,
@JsonProperty("vaultDescription") String vaultDescription) implements AuditEventDto {
}
Expand Down
14 changes: 10 additions & 4 deletions backend/src/main/java/org/cryptomator/hub/api/MemberDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,26 @@

public final class MemberDto extends AuthorityDto {

@JsonProperty("ecdhPublicKey")
public final String ecdhPublicKey;
@JsonProperty("ecdsaPublicKey")
public final String ecdsaPublicKey;
@JsonProperty("role")
public final VaultAccess.Role role;

MemberDto(@JsonProperty("id") String id, @JsonProperty("type") AuthorityDto.Type type, @JsonProperty("name") String name, @JsonProperty("pictureUrl") String pictureUrl, @JsonProperty("role") VaultAccess.Role role) {
MemberDto(@JsonProperty("id") String id, @JsonProperty("type") Type type, @JsonProperty("name") String name, @JsonProperty("pictureUrl") String pictureUrl, @JsonProperty("ecdhPublicKey") String ecdhPublicKey, @JsonProperty("ecdsaPublicKey") String ecdsaPublicKey, @JsonProperty("role") VaultAccess.Role role) {
super(id, type, name, pictureUrl);
this.ecdhPublicKey = ecdhPublicKey;
this.ecdsaPublicKey = ecdsaPublicKey;
this.role = role;
}

public static MemberDto fromEntity(User user, VaultAccess.Role role) {
return new MemberDto(user.getId(), Type.USER, user.getName(), user.getPictureUrl(), role);
return new MemberDto(user.getId(), Type.USER, user.getName(), user.getPictureUrl(), user.getEcdhPublicKey(), user.getEcdsaPublicKey(), role);
}

public static MemberDto fromEntity(Group group, VaultAccess.Role role) {
return new MemberDto(group.getId(), Type.GROUP, group.getName(), null, role);
return new MemberDto(group.getId(), Type.GROUP, group.getName(), null, null, null, role);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.cryptomator.hub.api;

import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.cryptomator.hub.entities.Settings;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;

@Path("/settings")
public class SettingsResource {

@Inject
Settings.Repository settingsRepo;

@GET
@RolesAllowed("user")
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "get the billing information")
@APIResponse(responseCode = "200")
@Transactional
public SettingsDto get() {
return SettingsDto.fromEntity(settingsRepo.get());
}

@PUT
@RolesAllowed("admin")
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "update settings")
@APIResponse(responseCode = "204", description = "token set")
@APIResponse(responseCode = "400", description = "invalid settings")
@APIResponse(responseCode = "403", description = "only admins are allowed to update settings")
@Transactional
public Response put(@NotNull @Valid SettingsDto dto) {
var settings = settingsRepo.get();
settings.setWotMaxDepth(dto.wotMaxDepth);
settings.setWotIdVerifyLen(dto.wotIdVerifyLen);
settingsRepo.persist(settings);
return Response.status(Response.Status.NO_CONTENT).build();
}

public record SettingsDto(@JsonProperty("hubId") String hubId, @JsonProperty("wotMaxDepth") @Min(0) @Max(9) int wotMaxDepth, @JsonProperty("wotIdVerifyLen") @Min(0) int wotIdVerifyLen) {

public static SettingsDto fromEntity(Settings entity) {
return new SettingsDto(entity.getHubId(), entity.getWotMaxDepth(), entity.getWotIdVerifyLen());
}

}

}
6 changes: 5 additions & 1 deletion backend/src/main/java/org/cryptomator/hub/api/UserDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ public final class UserDto extends AuthorityDto {
@JsonProperty("setupCode")
public final String setupCode;

@Deprecated
/**
* Same as {@link #ecdhPublicKey}, kept for compatibility purposes
* @deprecated to be removed when all clients moved to the new DTO field names
*/
@Deprecated(forRemoval = true)
@JsonProperty("publicKey")
public final String legacyEcdhPublicKey;

Expand Down
67 changes: 66 additions & 1 deletion backend/src/main/java/org/cryptomator/hub/api/UsersResource.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.JsonProperty;
import jakarta.annotation.Nullable;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
Expand All @@ -8,17 +9,21 @@
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.cryptomator.hub.entities.AccessToken;
import org.cryptomator.hub.entities.Device;
import org.cryptomator.hub.entities.EffectiveWot;
import org.cryptomator.hub.entities.User;
import org.cryptomator.hub.entities.Vault;
import org.cryptomator.hub.entities.WotEntry;
import org.cryptomator.hub.entities.events.EventLogger;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.eclipse.microprofile.openapi.annotations.Operation;
Expand Down Expand Up @@ -48,6 +53,10 @@ public class UsersResource {
Device.Repository deviceRepo;
@Inject
Vault.Repository vaultRepo;
@Inject
WotEntry.Repository wotRepo;
@Inject
EffectiveWot.Repository effectiveWotRepo;

@Inject
JsonWebToken jwt;
Expand Down Expand Up @@ -168,7 +177,63 @@ public Response resetMe() {
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "list all users")
public List<UserDto> getAll() {
return userRepo.findAll().<User>stream().map(UserDto::justPublicInfo).toList();
return userRepo.findAll().stream().map(UserDto::justPublicInfo).toList();
}

@PUT
@Path("/trusted/{userId}")
@RolesAllowed("user")
@Transactional
@Consumes(MediaType.TEXT_PLAIN)
@Operation(summary = "adds/updates trust", description = "Stores a signature for the given user.")
@APIResponse(responseCode = "204", description = "signature stored")
public Response putSignature(@PathParam("userId") String userId, @NotNull String signature) {
var signer = userRepo.findById(jwt.getSubject());
var id = new WotEntry.Id();
id.setUserId(userId);
id.setSignerId(signer.getId());
var entry = wotRepo.findById(id);
if (entry == null) {
entry = new WotEntry();
entry.setId(id);
}
entry.setSignature(signature);
SailReal marked this conversation as resolved.
Show resolved Hide resolved
wotRepo.persist(entry);
SailReal marked this conversation as resolved.
Show resolved Hide resolved
eventLogger.logWotIdSigned(userId, signer.getId(), signer.getEcdsaPublicKey(), signature);
return Response.status(Response.Status.NO_CONTENT).build();
}

@GET
@Path("/trusted/{userId}")
@RolesAllowed("user")
@NoCache
@Transactional
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "get trust detail for given user", description = "returns the shortest found signature chain for the given user")
@APIResponse(responseCode = "200")
@APIResponse(responseCode = "404", description = "if no sufficiently short trust chain between the invoking user and the user with the given id has been found")
public TrustedUserDto getTrustedUser(@PathParam("userId") String trustedUserId) {
var trustingUserId = jwt.getSubject();
return effectiveWotRepo.findTrusted(trustingUserId, trustedUserId).singleResultOptional().map(TrustedUserDto::fromEntity).orElseThrow(NotFoundException::new);
}

@GET
@Path("/trusted")
@RolesAllowed("user")
@NoCache
@Transactional
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "get trusted users", description = "returns a list of users trusted by the currently logged-in user")
@APIResponse(responseCode = "200")
SailReal marked this conversation as resolved.
Show resolved Hide resolved
public List<TrustedUserDto> getTrustedUsers() {
var trustingUserId = jwt.getSubject();
return effectiveWotRepo.findTrusted(trustingUserId).stream().map(TrustedUserDto::fromEntity).toList();
}

public record TrustedUserDto(@JsonProperty("trustedUserId") String trustedUserId, @JsonProperty("signatureChain") List<String> signatureChain) {

public static TrustedUserDto fromEntity(EffectiveWot entity) {
return new TrustedUserDto(entity.getId().getTrustedUserId(), List.of(entity.getSignatureChain()));
}
}
}
108 changes: 108 additions & 0 deletions backend/src/main/java/org/cryptomator/hub/entities/EffectiveWot.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.cryptomator.hub.entities;

import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import io.quarkus.panache.common.Parameters;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.Table;
import org.hibernate.annotations.Immutable;
import org.hibernate.annotations.Type;

import java.io.Serializable;
import java.util.Objects;

@Entity
@Immutable
@Table(name = "effective_wot")
@NamedQuery(name = "EffectiveWot.findTrustedUsers", query = """
SELECT wot
FROM EffectiveWot wot
WHERE wot.id.trustingUserId = :trustingUserId
""")
@NamedQuery(name = "EffectiveWot.findTrustedUser", query = """
SELECT wot
FROM EffectiveWot wot
WHERE wot.id.trustingUserId = :trustingUserId AND wot.id.trustedUserId = :trustedUserId
""")
public class EffectiveWot {

@EmbeddedId
private Id id;

@Column(name = "signature_chain")
@Type(StringArrayType.class)
private String[] signatureChain;

public Id getId() {
return id;
}

public void setId(Id id) {
this.id = id;
}

public String[] getSignatureChain() {
return signatureChain;
}

public void setSignatureChain(String[] signatureChain) {
this.signatureChain = signatureChain;
}
overheadhunter marked this conversation as resolved.
Show resolved Hide resolved

@Embeddable
public static class Id implements Serializable {

@Column(name = "trusting_user_id")
private String trustingUserId;

@Column(name = "trusted_user_id")
private String trustedUserId;

public String getTrustingUserId() {
return trustingUserId;
}

public String getTrustedUserId() {
return trustedUserId;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof Id other) {
return Objects.equals(trustingUserId, other.trustingUserId) //
&& Objects.equals(trustedUserId, other.trustedUserId);
}
return false;
}

@Override
public int hashCode() {
return Objects.hash(trustingUserId, trustedUserId);
}

@Override
public String toString() {
return "EffectiveWotId{" +
"trustingUserId='" + trustingUserId + '\'' +
", trustedUserId='" + trustedUserId + '\'' +
'}';
}
}
overheadhunter marked this conversation as resolved.
Show resolved Hide resolved

@ApplicationScoped
public static class Repository implements PanacheRepositoryBase<EffectiveWot, Id> {
public PanacheQuery<EffectiveWot> findTrusted(String trustingUserId) {
return find("#EffectiveWot.findTrustedUsers", Parameters.with("trustingUserId", trustingUserId));
}

public PanacheQuery<EffectiveWot> findTrusted(String trustingUserId, String trustedUserId) {
return find("#EffectiveWot.findTrustedUser", Parameters.with("trustingUserId", trustingUserId).and("trustedUserId", trustedUserId));
}
}
}
Loading