mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 09:08:02 +01:00
Support for versioned profiles
Includes support for issuing zkgroup auth credentials
This commit is contained in:
@@ -166,6 +166,11 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private SecureBackupServiceConfiguration backupService;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private ZkConfig zkConfig;
|
||||
|
||||
private Map<String, String> transparentDataIndex = new HashMap<>();
|
||||
|
||||
public RecaptchaConfiguration getRecaptchaConfiguration() {
|
||||
@@ -289,4 +294,9 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
public SecureBackupServiceConfiguration getSecureBackupServiceConfiguration() {
|
||||
return backupService;
|
||||
}
|
||||
|
||||
public ZkConfig getZkConfig() {
|
||||
return zkConfig;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm;
|
||||
|
||||
import com.amazonaws.auth.AWSCredentials;
|
||||
import com.amazonaws.auth.AWSCredentialsProvider;
|
||||
import com.amazonaws.auth.AWSStaticCredentialsProvider;
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.services.s3.AmazonS3;
|
||||
import com.amazonaws.services.s3.AmazonS3Client;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.jdbi3.strategies.DefaultNameStrategy;
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
@@ -26,6 +32,9 @@ import com.google.common.collect.ImmutableSet;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||
import org.jdbi.v3.core.Jdbi;
|
||||
import org.signal.zkgroup.ServerSecretParams;
|
||||
import org.signal.zkgroup.auth.ServerZkAuthOperations;
|
||||
import org.signal.zkgroup.profiles.ServerZkProfileOperations;
|
||||
import org.whispersystems.dispatch.DispatchManager;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||
@@ -55,6 +64,8 @@ import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
import org.whispersystems.textsecuregcm.push.WebsocketSender;
|
||||
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
||||
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
|
||||
@@ -67,6 +78,7 @@ import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.workers.CertificateCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.VacuumCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.ZkParamsCommand;
|
||||
import org.whispersystems.websocket.WebSocketResourceProviderFactory;
|
||||
import org.whispersystems.websocket.setup.WebSocketEnvironment;
|
||||
|
||||
@@ -102,6 +114,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
bootstrap.addCommand(new VacuumCommand());
|
||||
bootstrap.addCommand(new DeleteUserCommand());
|
||||
bootstrap.addCommand(new CertificateCommand());
|
||||
bootstrap.addCommand(new ZkParamsCommand());
|
||||
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("keysdb", "keysdb.xml") {
|
||||
@Override
|
||||
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
|
||||
@@ -162,6 +175,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
PendingDevices pendingDevices = new PendingDevices (accountDatabase);
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
Keys keys = new Keys(keysDatabase);
|
||||
Messages messages = new Messages(messageDatabase);
|
||||
AbusiveHostRules abusiveHostRules = new AbusiveHostRules(abuseDatabase);
|
||||
@@ -182,6 +196,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, cacheClient );
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
|
||||
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheClient);
|
||||
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheClient);
|
||||
MessagesCache messagesCache = new MessagesCache(messagesClient, messages, accountsManager, config.getMessageCacheConfiguration().getPersistDelayMinutes());
|
||||
MessagesManager messagesManager = new MessagesManager(messages, messagesCache);
|
||||
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(messagesManager);
|
||||
@@ -231,11 +246,21 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
environment.lifecycle().manage(messagesCache);
|
||||
environment.lifecycle().manage(accountDatabaseCrawler);
|
||||
|
||||
AWSCredentials credentials = new BasicAWSCredentials(config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret());
|
||||
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
|
||||
AmazonS3 cdnS3Client = AmazonS3Client.builder().withCredentials(credentialsProvider).withRegion(config.getCdnConfiguration().getRegion()).build();
|
||||
PostPolicyGenerator cdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket(), config.getCdnConfiguration().getAccessKey());
|
||||
PolicySigner cdnPolicySigner = new PolicySigner(config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion());
|
||||
|
||||
ServerSecretParams zkSecretParams = new ServerSecretParams(config.getZkConfig().getServerSecret());
|
||||
ServerZkProfileOperations zkProfileOperations = new ServerZkProfileOperations(zkSecretParams);
|
||||
ServerZkAuthOperations zkAuthOperations = new ServerZkAuthOperations(zkSecretParams);
|
||||
|
||||
AttachmentControllerV1 attachmentControllerV1 = new AttachmentControllerV1(rateLimiters, config.getAttachmentsConfiguration().getAccessKey(), config.getAttachmentsConfiguration().getAccessSecret(), config.getAttachmentsConfiguration().getBucket() );
|
||||
AttachmentControllerV2 attachmentControllerV2 = new AttachmentControllerV2(rateLimiters, config.getAttachmentsConfiguration().getAccessKey(), config.getAttachmentsConfiguration().getAccessSecret(), config.getAttachmentsConfiguration().getRegion(), config.getAttachmentsConfiguration().getBucket());
|
||||
KeysController keysController = new KeysController(rateLimiters, keys, accountsManager, directoryQueue);
|
||||
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, apnFallbackManager);
|
||||
ProfileController profileController = new ProfileController(rateLimiters, accountsManager, usernamesManager, config.getCdnConfiguration());
|
||||
ProfileController profileController = new ProfileController(rateLimiters, accountsManager, profilesManager, usernamesManager, cdnS3Client, cdnPolicyGenerator, cdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations);
|
||||
StickerController stickerController = new StickerController(rateLimiters, config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket());
|
||||
|
||||
AuthFilter<BasicCredentials, Account> accountAuthFilter = new BasicCredentialAuthFilter.Builder<Account>().setAuthenticator(accountAuthenticator).buildAuthFilter ();
|
||||
@@ -249,7 +274,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, directoryQueue, rateLimiters, config.getMaxDevices()));
|
||||
environment.jersey().register(new DirectoryController(rateLimiters, directory, directoryCredentialsGenerator));
|
||||
environment.jersey().register(new ProvisioningController(rateLimiters, pushSender));
|
||||
environment.jersey().register(new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays())));
|
||||
environment.jersey().register(new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays()), zkAuthOperations));
|
||||
environment.jersey().register(new VoiceVerificationController(config.getVoiceVerificationConfiguration().getUrl(), config.getVoiceVerificationConfiguration().getLocales()));
|
||||
environment.jersey().register(new SecureStorageController(storageCredentialsGenerator));
|
||||
environment.jersey().register(new SecureBackupController(backupCredentialsGenerator));
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class ZkConfig {
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
@NotNull
|
||||
private byte[] serverSecret;
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
@NotNull
|
||||
private byte[] serverPublic;
|
||||
|
||||
public byte[] getServerSecret() {
|
||||
return serverSecret;
|
||||
}
|
||||
|
||||
public byte[] getServerPublic() {
|
||||
return serverPublic;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import org.signal.zkgroup.auth.ServerZkAuthOperations;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
|
||||
import org.whispersystems.textsecuregcm.entities.GroupCredentials;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
@@ -17,6 +20,8 @@ import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
@@ -27,10 +32,12 @@ public class CertificateController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(CertificateController.class);
|
||||
|
||||
private final CertificateGenerator certificateGenerator;
|
||||
private final CertificateGenerator certificateGenerator;
|
||||
private final ServerZkAuthOperations serverZkAuthOperations;
|
||||
|
||||
public CertificateController(CertificateGenerator certificateGenerator) {
|
||||
this.certificateGenerator = certificateGenerator;
|
||||
public CertificateController(CertificateGenerator certificateGenerator, ServerZkAuthOperations serverZkAuthOperations) {
|
||||
this.certificateGenerator = certificateGenerator;
|
||||
this.serverZkAuthOperations = serverZkAuthOperations;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -50,4 +57,27 @@ public class CertificateController {
|
||||
return new DeliveryCertificate(certificateGenerator.createFor(account, account.getAuthenticatedDevice().get(), includeUuid.orElse(false)));
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/group/{startRedemptionTime}/{endRedemptionTime}")
|
||||
public GroupCredentials getAuthenticationCredentials(@Auth Account account,
|
||||
@PathParam("startRedemptionTime") int startRedemptionTime,
|
||||
@PathParam("endRedemptionTime") int endRedemptionTime)
|
||||
{
|
||||
if (startRedemptionTime > endRedemptionTime) throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
if (endRedemptionTime > Util.currentDaysSinceEpoch() + 7) throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
if (startRedemptionTime < Util.currentDaysSinceEpoch()) throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
|
||||
List<GroupCredentials.GroupCredential> credentials = new LinkedList<>();
|
||||
|
||||
for (int i=startRedemptionTime;i<=endRedemptionTime;i++) {
|
||||
credentials.add(new GroupCredentials.GroupCredential(serverZkAuthOperations.issueAuthCredential(account.getUuid(), i)
|
||||
.serialize(),
|
||||
i));
|
||||
}
|
||||
|
||||
return new GroupCredentials(credentials);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.amazonaws.auth.AWSCredentials;
|
||||
import com.amazonaws.auth.AWSCredentialsProvider;
|
||||
import com.amazonaws.auth.AWSStaticCredentialsProvider;
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.services.s3.AmazonS3;
|
||||
import com.amazonaws.services.s3.AmazonS3Client;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import org.hibernate.validator.valuehandling.UnwrapValidatedValue;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
||||
import org.signal.zkgroup.profiles.ServerZkProfileOperations;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
|
||||
import org.whispersystems.textsecuregcm.auth.Anonymous;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum;
|
||||
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.CreateProfileRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.Profile;
|
||||
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.UserCapabilities;
|
||||
@@ -23,10 +28,14 @@ import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.PUT;
|
||||
@@ -49,41 +58,212 @@ import io.dropwizard.auth.Auth;
|
||||
@Path("/v1/profile")
|
||||
public class ProfileController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ProfileController.class);
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final ProfilesManager profilesManager;
|
||||
private final AccountsManager accountsManager;
|
||||
private final UsernamesManager usernamesManager;
|
||||
|
||||
private final PolicySigner policySigner;
|
||||
private final PostPolicyGenerator policyGenerator;
|
||||
private final PolicySigner policySigner;
|
||||
private final PostPolicyGenerator policyGenerator;
|
||||
private final ServerZkProfileOperations zkProfileOperations;
|
||||
|
||||
private final AmazonS3 s3client;
|
||||
private final String bucket;
|
||||
|
||||
public ProfileController(RateLimiters rateLimiters,
|
||||
AccountsManager accountsManager,
|
||||
ProfilesManager profilesManager,
|
||||
UsernamesManager usernamesManager,
|
||||
CdnConfiguration profilesConfiguration)
|
||||
AmazonS3 s3client,
|
||||
PostPolicyGenerator policyGenerator,
|
||||
PolicySigner policySigner,
|
||||
String bucket,
|
||||
ServerZkProfileOperations zkProfileOperations)
|
||||
{
|
||||
AWSCredentials credentials = new BasicAWSCredentials(profilesConfiguration.getAccessKey(), profilesConfiguration.getAccessSecret());
|
||||
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
|
||||
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.accountsManager = accountsManager;
|
||||
this.usernamesManager = usernamesManager;
|
||||
this.bucket = profilesConfiguration.getBucket();
|
||||
this.s3client = AmazonS3Client.builder()
|
||||
.withCredentials(credentialsProvider)
|
||||
.withRegion(profilesConfiguration.getRegion())
|
||||
.build();
|
||||
|
||||
this.policyGenerator = new PostPolicyGenerator(profilesConfiguration.getRegion(),
|
||||
profilesConfiguration.getBucket(),
|
||||
profilesConfiguration.getAccessKey());
|
||||
|
||||
this.policySigner = new PolicySigner(profilesConfiguration.getAccessSecret(),
|
||||
profilesConfiguration.getRegion());
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.accountsManager = accountsManager;
|
||||
this.profilesManager = profilesManager;
|
||||
this.usernamesManager = usernamesManager;
|
||||
this.zkProfileOperations = zkProfileOperations;
|
||||
this.bucket = bucket;
|
||||
this.s3client = s3client;
|
||||
this.policyGenerator = policyGenerator;
|
||||
this.policySigner = policySigner;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response setProfile(@Auth Account account, @Valid CreateProfileRequest request) {
|
||||
Optional<VersionedProfile> currentProfile = profilesManager.get(account.getUuid(), request.getVersion());
|
||||
String avatar = request.isAvatar() ? generateAvatarObjectName() : null;
|
||||
Optional<ProfileAvatarUploadAttributes> response = Optional.empty();
|
||||
|
||||
profilesManager.set(account.getUuid(), new VersionedProfile(request.getVersion(), request.getName(), avatar, request.getCommitment().serialize()));
|
||||
|
||||
if (request.isAvatar()) {
|
||||
Optional<String> currentAvatar = Optional.empty();
|
||||
|
||||
if (currentProfile.isPresent() && currentProfile.get().getAvatar() != null && currentProfile.get().getAvatar().startsWith("profiles/")) {
|
||||
currentAvatar = Optional.of(currentProfile.get().getAvatar());
|
||||
}
|
||||
|
||||
if (currentAvatar.isEmpty() && account.getAvatar() != null && account.getAvatar().startsWith("profiles/")) {
|
||||
currentAvatar = Optional.of(account.getAvatar());
|
||||
}
|
||||
|
||||
currentAvatar.ifPresent(s -> s3client.deleteObject(bucket, s));
|
||||
|
||||
response = Optional.of(generateAvatarUploadForm(avatar));
|
||||
}
|
||||
|
||||
account.setProfileName(request.getName());
|
||||
if (avatar != null) account.setAvatar(avatar);
|
||||
accountsManager.update(account);
|
||||
|
||||
if (response.isPresent()) return Response.ok(response).build();
|
||||
else return Response.ok().build();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{uuid}/{version}")
|
||||
public Optional<Profile> getProfile(@Auth Optional<Account> requestAccount,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@PathParam("uuid") UUID uuid,
|
||||
@PathParam("version") String version)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
return getVersionedProfile(requestAccount, accessKey, uuid, version, Optional.empty());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{uuid}/{version}/{credentialRequest}")
|
||||
public Optional<Profile> getProfile(@Auth Optional<Account> requestAccount,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@PathParam("uuid") UUID uuid,
|
||||
@PathParam("version") String version,
|
||||
@PathParam("credentialRequest") String credentialRequest)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
return getVersionedProfile(requestAccount, accessKey, uuid, version, Optional.of(credentialRequest));
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalIsPresent")
|
||||
private Optional<Profile> getVersionedProfile(Optional<Account> requestAccount,
|
||||
Optional<Anonymous> accessKey,
|
||||
UUID uuid,
|
||||
String version,
|
||||
Optional<String> credentialRequest)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
try {
|
||||
if (!requestAccount.isPresent() && !accessKey.isPresent()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (requestAccount.isPresent()) {
|
||||
rateLimiters.getProfileLimiter().validate(requestAccount.get().getNumber());
|
||||
}
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(uuid);
|
||||
OptionalAccess.verify(requestAccount, accessKey, accountProfile);
|
||||
|
||||
assert(accountProfile.isPresent());
|
||||
|
||||
Optional<String> username = usernamesManager.get(accountProfile.get().getUuid());
|
||||
Optional<VersionedProfile> profile = profilesManager.get(uuid, version);
|
||||
|
||||
String name = profile.map(VersionedProfile::getName).orElse(accountProfile.get().getProfileName());
|
||||
String avatar = profile.map(VersionedProfile::getAvatar).orElse(accountProfile.get().getAvatar());
|
||||
|
||||
Optional<ProfileKeyCredentialResponse> credential = getProfileCredential(credentialRequest, profile, uuid);
|
||||
|
||||
return Optional.of(new Profile(name,
|
||||
avatar,
|
||||
accountProfile.get().getIdentityKey(),
|
||||
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
|
||||
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
|
||||
new UserCapabilities(accountProfile.get().isUuidAddressingSupported()),
|
||||
username.orElse(null),
|
||||
null, credential.orElse(null)));
|
||||
} catch (InvalidInputException e) {
|
||||
logger.info("Bad profile request", e);
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/username/{username}")
|
||||
public Profile getProfileByUsername(@Auth Account account, @PathParam("username") String username) throws RateLimitExceededException {
|
||||
rateLimiters.getUsernameLookupLimiter().validate(account.getUuid().toString());
|
||||
|
||||
username = username.toLowerCase();
|
||||
|
||||
Optional<UUID> uuid = usernamesManager.get(username);
|
||||
|
||||
if (!uuid.isPresent()) {
|
||||
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
||||
}
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(uuid.get());
|
||||
|
||||
if (!accountProfile.isPresent()) {
|
||||
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
||||
}
|
||||
|
||||
return new Profile(accountProfile.get().getProfileName(),
|
||||
accountProfile.get().getAvatar(),
|
||||
accountProfile.get().getIdentityKey(),
|
||||
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
|
||||
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
|
||||
new UserCapabilities(accountProfile.get().isUuidAddressingSupported()),
|
||||
username,
|
||||
accountProfile.get().getUuid(), null);
|
||||
}
|
||||
|
||||
private Optional<ProfileKeyCredentialResponse> getProfileCredential(Optional<String> encodedProfileCredentialRequest,
|
||||
Optional<VersionedProfile> profile,
|
||||
UUID uuid)
|
||||
throws InvalidInputException
|
||||
{
|
||||
if (!encodedProfileCredentialRequest.isPresent()) return Optional.empty();
|
||||
if (!profile.isPresent()) return Optional.empty();
|
||||
|
||||
try {
|
||||
ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.get().getCommitment());
|
||||
ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(Hex.decodeHex(encodedProfileCredentialRequest.get()));
|
||||
ProfileKeyCredentialResponse response = zkProfileOperations.issueProfileKeyCredential(request, uuid, commitment);
|
||||
|
||||
return Optional.of(response);
|
||||
} catch (DecoderException | VerificationFailedException e) {
|
||||
throw new WebApplicationException(e, Response.status(Response.Status.BAD_REQUEST).build());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Old profile endpoints. Replaced by versioned profile endpoints (above)
|
||||
|
||||
@Deprecated
|
||||
@Timed
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/name/{name}")
|
||||
public void setProfile(@Auth Account account, @PathParam("name") @UnwrapValidatedValue(true) @ExactlySize({72, 108}) Optional<String> name) {
|
||||
account.setProfileName(name.orElse(null));
|
||||
accountsManager.update(account);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@@ -119,60 +299,19 @@ public class ProfileController {
|
||||
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
|
||||
new UserCapabilities(accountProfile.get().isUuidAddressingSupported()),
|
||||
username.orElse(null),
|
||||
null);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/username/{username}")
|
||||
public Profile getProfileByUsername(@Auth Account account, @PathParam("username") String username) throws RateLimitExceededException {
|
||||
rateLimiters.getUsernameLookupLimiter().validate(account.getUuid().toString());
|
||||
|
||||
username = username.toLowerCase();
|
||||
|
||||
Optional<UUID> uuid = usernamesManager.get(username);
|
||||
|
||||
if (!uuid.isPresent()) {
|
||||
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
||||
}
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(uuid.get());
|
||||
|
||||
if (!accountProfile.isPresent()) {
|
||||
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
||||
}
|
||||
|
||||
return new Profile(accountProfile.get().getProfileName(),
|
||||
accountProfile.get().getAvatar(),
|
||||
accountProfile.get().getIdentityKey(),
|
||||
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
|
||||
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
|
||||
new UserCapabilities(accountProfile.get().isUuidAddressingSupported()),
|
||||
username,
|
||||
accountProfile.get().getUuid());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/name/{name}")
|
||||
public void setProfile(@Auth Account account, @PathParam("name") @UnwrapValidatedValue(true) @ExactlySize({72, 108}) Optional<String> name) {
|
||||
account.setProfileName(name.orElse(null));
|
||||
accountsManager.update(account);
|
||||
null, null);
|
||||
}
|
||||
|
||||
|
||||
@Deprecated
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/form/avatar")
|
||||
public ProfileAvatarUploadAttributes getAvatarUploadForm(@Auth Account account) {
|
||||
String previousAvatar = account.getAvatar();
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
String objectName = generateAvatarObjectName();
|
||||
Pair<String, String> policy = policyGenerator.createFor(now, objectName, 10 * 1024 * 1024);
|
||||
String signature = policySigner.getSignature(now, policy.second());
|
||||
String previousAvatar = account.getAvatar();
|
||||
String objectName = generateAvatarObjectName();
|
||||
ProfileAvatarUploadAttributes profileAvatarUploadAttributes = generateAvatarUploadForm(objectName);
|
||||
|
||||
if (previousAvatar != null && previousAvatar.startsWith("profiles/")) {
|
||||
s3client.deleteObject(bucket, previousAvatar);
|
||||
@@ -181,8 +320,19 @@ public class ProfileController {
|
||||
account.setAvatar(objectName);
|
||||
accountsManager.update(account);
|
||||
|
||||
return profileAvatarUploadAttributes;
|
||||
}
|
||||
|
||||
////
|
||||
|
||||
private ProfileAvatarUploadAttributes generateAvatarUploadForm(String objectName) {
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
Pair<String, String> policy = policyGenerator.createFor(now, objectName, 10 * 1024 * 1024);
|
||||
String signature = policySigner.getSignature(now, policy.second());
|
||||
|
||||
return new ProfileAvatarUploadAttributes(objectName, policy.first(), "private", "AWS4-HMAC-SHA256",
|
||||
now.format(PostPolicyGenerator.AWS_DATE_TIME), policy.second(), signature);
|
||||
|
||||
}
|
||||
|
||||
private String generateAvatarObjectName() {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class CreateProfileRequest {
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String version;
|
||||
|
||||
@JsonProperty
|
||||
@ExactlySize({108})
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private boolean avatar;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@JsonDeserialize(using = ProfileKeyCommitmentAdapter.Deserializing.class)
|
||||
@JsonSerialize(using = ProfileKeyCommitmentAdapter.Serializing.class)
|
||||
private ProfileKeyCommitment commitment;
|
||||
|
||||
public CreateProfileRequest() {}
|
||||
|
||||
public CreateProfileRequest(ProfileKeyCommitment commitment, String version, String name, boolean wantsAvatar) {
|
||||
this.commitment = commitment;
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.avatar = wantsAvatar;
|
||||
}
|
||||
|
||||
public ProfileKeyCommitment getCommitment() {
|
||||
return commitment;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public boolean isAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class GroupCredentials {
|
||||
|
||||
@JsonProperty
|
||||
private List<GroupCredential> credentials;
|
||||
|
||||
public GroupCredentials() {}
|
||||
|
||||
public GroupCredentials(List<GroupCredential> credentials) {
|
||||
this.credentials = credentials;
|
||||
}
|
||||
|
||||
public List<GroupCredential> getCredentials() {
|
||||
return credentials;
|
||||
}
|
||||
|
||||
public static class GroupCredential {
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArraySerializer.class)
|
||||
@JsonDeserialize(using = ByteArrayDeserializer.class)
|
||||
private byte[] credential;
|
||||
|
||||
@JsonProperty
|
||||
private int redemptionTime;
|
||||
|
||||
public GroupCredential() {}
|
||||
|
||||
public GroupCredential(byte[] credential, int redemptionTime) {
|
||||
this.credential = credential;
|
||||
this.redemptionTime = redemptionTime;
|
||||
}
|
||||
|
||||
public byte[] getCredential() {
|
||||
return credential;
|
||||
}
|
||||
|
||||
public int getRedemptionTime() {
|
||||
return redemptionTime;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ByteArraySerializer extends JsonSerializer<byte[]> {
|
||||
@Override
|
||||
public void serialize(byte[] bytes, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
|
||||
jsonGenerator.writeString(Base64.encodeBytes(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
public static class ByteArrayDeserializer extends JsonDeserializer<byte[]> {
|
||||
@Override
|
||||
public byte[] deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
|
||||
return Base64.decode(jsonParser.getValueAsString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class Profile {
|
||||
@@ -31,11 +35,17 @@ public class Profile {
|
||||
@JsonProperty
|
||||
private UUID uuid;
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ProfileKeyCredentialResponseAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ProfileKeyCredentialResponseAdapter.Deserializing.class)
|
||||
private ProfileKeyCredentialResponse credential;
|
||||
|
||||
public Profile() {}
|
||||
|
||||
public Profile(String name, String avatar, String identityKey,
|
||||
String unidentifiedAccess, boolean unrestrictedUnidentifiedAccess,
|
||||
UserCapabilities capabilities, String username, UUID uuid)
|
||||
UserCapabilities capabilities, String username, UUID uuid,
|
||||
ProfileKeyCredentialResponse credential)
|
||||
{
|
||||
this.name = name;
|
||||
this.avatar = avatar;
|
||||
@@ -45,6 +55,7 @@ public class Profile {
|
||||
this.capabilities = capabilities;
|
||||
this.username = username;
|
||||
this.uuid = uuid;
|
||||
this.credential = credential;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
||||
@@ -41,4 +41,8 @@ public class ProfileAvatarUploadAttributes {
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ProfileKeyCommitmentAdapter {
|
||||
|
||||
public static class Serializing extends JsonSerializer<ProfileKeyCommitment> {
|
||||
@Override
|
||||
public void serialize(ProfileKeyCommitment value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
gen.writeString(Base64.encodeBytes(value.serialize()));
|
||||
}
|
||||
}
|
||||
|
||||
public static class Deserializing extends JsonDeserializer<ProfileKeyCommitment> {
|
||||
|
||||
@Override
|
||||
public ProfileKeyCommitment deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
|
||||
try {
|
||||
return new ProfileKeyCommitment(Base64.decode(p.getValueAsString()));
|
||||
} catch (InvalidInputException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ProfileKeyCredentialResponseAdapter {
|
||||
|
||||
public static class Serializing extends JsonSerializer<ProfileKeyCredentialResponse> {
|
||||
@Override
|
||||
public void serialize(ProfileKeyCredentialResponse response, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
|
||||
throws IOException, JsonProcessingException
|
||||
{
|
||||
if (response == null) jsonGenerator.writeNull();
|
||||
else jsonGenerator.writeString(Base64.encodeBytes(response.serialize()));
|
||||
}
|
||||
}
|
||||
|
||||
public static class Deserializing extends JsonDeserializer<ProfileKeyCredentialResponse> {
|
||||
@Override
|
||||
public ProfileKeyCredentialResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
|
||||
throws IOException, JsonProcessingException
|
||||
{
|
||||
try {
|
||||
return new ProfileKeyCredentialResponse(Base64.decode(jsonParser.getValueAsString()));
|
||||
} catch (InvalidInputException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.Timer;
|
||||
import org.whispersystems.textsecuregcm.storage.mappers.VersionedProfileMapper;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
public class Profiles {
|
||||
|
||||
public static final String ID = "id";
|
||||
public static final String UID = "uuid";
|
||||
public static final String VERSION = "version";
|
||||
public static final String NAME = "name";
|
||||
public static final String AVATAR = "avatar";
|
||||
public static final String COMMITMENT = "commitment";
|
||||
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
|
||||
private final Timer setTimer = metricRegistry.timer(name(Profiles.class, "set" ));
|
||||
private final Timer getTimer = metricRegistry.timer(name(Profiles.class, "get" ));
|
||||
private final Timer deleteTimer = metricRegistry.timer(name(Profiles.class, "delete"));
|
||||
|
||||
private final FaultTolerantDatabase database;
|
||||
|
||||
public Profiles(FaultTolerantDatabase database) {
|
||||
this.database = database;
|
||||
this.database.getDatabase().registerRowMapper(new VersionedProfileMapper());
|
||||
}
|
||||
|
||||
public void set(UUID uuid, VersionedProfile profile) {
|
||||
database.use(jdbi -> jdbi.useHandle(handle -> {
|
||||
try (Timer.Context ignored = setTimer.time()) {
|
||||
handle.createUpdate("INSERT INTO profiles (" + UID + ", " + VERSION + ", " + NAME + ", " + AVATAR + ", " + COMMITMENT + ") VALUES (:uuid, :version, :name, :avatar, :commitment) ON CONFLICT (" + UID + ", " + VERSION + ") DO UPDATE SET " + NAME + " = EXCLUDED." + NAME + ", " + AVATAR + " = EXCLUDED." + AVATAR)
|
||||
.bind("uuid", uuid)
|
||||
.bind("version", profile.getVersion())
|
||||
.bind("name", profile.getName())
|
||||
.bind("avatar", profile.getAvatar())
|
||||
.bind("commitment", profile.getCommitment())
|
||||
.execute();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public Optional<VersionedProfile> get(UUID uuid, String version) {
|
||||
return database.with(jdbi -> jdbi.withHandle(handle -> {
|
||||
try (Timer.Context ignored = getTimer.time()) {
|
||||
return handle.createQuery("SELECT * FROM profiles WHERE " + UID + " = :uuid AND " + VERSION + " = :version")
|
||||
.bind("uuid", uuid)
|
||||
.bind("version", version)
|
||||
.mapTo(VersionedProfile.class)
|
||||
.findFirst();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public void deleteAll(UUID uuid) {
|
||||
database.use(jdbi -> jdbi.useHandle(handle -> {
|
||||
try (Timer.Context ignored = deleteTimer.time()) {
|
||||
handle.createUpdate("DELETE FROM profiles WHERE " + UID + " = :uuid")
|
||||
.bind("uuid", uuid)
|
||||
.execute();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.exceptions.JedisException;
|
||||
|
||||
public class ProfilesManager {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PendingAccountsManager.class);
|
||||
|
||||
private static final String CACHE_PREFIX = "profiles::";
|
||||
|
||||
private final Profiles profiles;
|
||||
private final ReplicatedJedisPool cacheClient;
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
public ProfilesManager(Profiles profiles, ReplicatedJedisPool cacheClient) {
|
||||
this.profiles = profiles;
|
||||
this.cacheClient = cacheClient;
|
||||
this.mapper = SystemMapper.getMapper();
|
||||
}
|
||||
|
||||
public void set(UUID uuid, VersionedProfile versionedProfile) {
|
||||
memcacheSet(uuid, versionedProfile);
|
||||
profiles.set(uuid, versionedProfile);
|
||||
}
|
||||
|
||||
public void deleteAll(UUID uuid) {
|
||||
memcacheDelete(uuid);
|
||||
profiles.deleteAll(uuid);
|
||||
}
|
||||
|
||||
public Optional<VersionedProfile> get(UUID uuid, String version) {
|
||||
Optional<VersionedProfile> profile = memcacheGet(uuid, version);
|
||||
|
||||
if (!profile.isPresent()) {
|
||||
profile = profiles.get(uuid, version);
|
||||
profile.ifPresent(versionedProfile -> memcacheSet(uuid, versionedProfile));
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
private void memcacheSet(UUID uuid, VersionedProfile profile) {
|
||||
try (Jedis jedis = cacheClient.getWriteResource()) {
|
||||
jedis.hset(CACHE_PREFIX + uuid.toString(), profile.getVersion(), mapper.writeValueAsString(profile));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<VersionedProfile> memcacheGet(UUID uuid, String version) {
|
||||
try (Jedis jedis = cacheClient.getReadResource()) {
|
||||
String json = jedis.hget(CACHE_PREFIX + uuid.toString(), version);
|
||||
|
||||
if (json == null) return Optional.empty();
|
||||
else return Optional.of(mapper.readValue(json, VersionedProfile.class));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error deserializing value...", e);
|
||||
return Optional.empty();
|
||||
} catch (JedisException e) {
|
||||
logger.warn("Redis exception", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void memcacheDelete(UUID uuid) {
|
||||
try (Jedis jedis = cacheClient.getWriteResource()) {
|
||||
jedis.del(CACHE_PREFIX + uuid.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
|
||||
public class VersionedProfile {
|
||||
|
||||
@JsonProperty
|
||||
private String version;
|
||||
|
||||
@JsonProperty
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private String avatar;
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
private byte[] commitment;
|
||||
|
||||
public VersionedProfile() {}
|
||||
|
||||
public VersionedProfile(String version, String name, String avatar, byte[] commitment) {
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.avatar = avatar;
|
||||
this.commitment = commitment;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
public byte[] getCommitment() {
|
||||
return commitment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.whispersystems.textsecuregcm.storage.mappers;
|
||||
|
||||
import org.jdbi.v3.core.mapper.RowMapper;
|
||||
import org.jdbi.v3.core.statement.StatementContext;
|
||||
import org.whispersystems.textsecuregcm.storage.Profiles;
|
||||
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class VersionedProfileMapper implements RowMapper<VersionedProfile> {
|
||||
|
||||
@Override
|
||||
public VersionedProfile map(ResultSet resultSet, StatementContext ctx) throws SQLException {
|
||||
return new VersionedProfile(resultSet.getString(Profiles.VERSION),
|
||||
resultSet.getString(Profiles.NAME),
|
||||
resultSet.getString(Profiles.AVATAR),
|
||||
resultSet.getBytes(Profiles.COMMITMENT));
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,17 @@ public class Util {
|
||||
return data;
|
||||
}
|
||||
|
||||
public static int toIntExact(long value) {
|
||||
if ((int)value != value) {
|
||||
throw new ArithmeticException("integer overflow");
|
||||
}
|
||||
return (int)value;
|
||||
}
|
||||
|
||||
public static int currentDaysSinceEpoch() {
|
||||
return Util.toIntExact(System.currentTimeMillis() / 1000 / 60 / 60 / 24);
|
||||
}
|
||||
|
||||
public static void sleep(long i) {
|
||||
try {
|
||||
Thread.sleep(i);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.whispersystems.textsecuregcm.workers;
|
||||
|
||||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
import org.signal.zkgroup.ServerPublicParams;
|
||||
import org.signal.zkgroup.ServerSecretParams;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import io.dropwizard.cli.Command;
|
||||
import io.dropwizard.setup.Bootstrap;
|
||||
|
||||
public class ZkParamsCommand extends Command {
|
||||
|
||||
public ZkParamsCommand() {
|
||||
super("zkparams", "Generates server zkparams");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(Subparser subparser) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(Bootstrap<?> bootstrap, Namespace namespace) throws Exception {
|
||||
ServerSecretParams serverSecretParams = ServerSecretParams.generate();
|
||||
ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams();
|
||||
|
||||
System.out.println("Public: " + Base64.encodeBytesWithoutPadding(serverPublicParams.serialize()));
|
||||
System.out.println("Private: " + Base64.encodeBytesWithoutPadding(serverSecretParams.serialize()));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user