import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.IncorrectPinException;
-import org.asamk.signal.manager.api.InvalidDeviceLinkException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException;
+import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import org.asamk.signal.manager.internal.SignalDependencies;
+import org.asamk.signal.manager.jobs.SyncStorageJob;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.NumberVerificationUtils;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
+import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
+import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
+import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
import org.whispersystems.signalservice.internal.push.SyncMessage;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
-import java.util.Optional;
+import java.util.UUID;
import java.util.concurrent.TimeUnit;
import okio.ByteString;
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
+import static org.asamk.signal.manager.util.Utils.handleResponseException;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
public class AccountHelper {
}
try {
updateAccountAttributes();
- context.getPreKeyHelper().refreshPreKeysIfNecessary();
+ if (account.getPreviousStorageVersion() < 9) {
+ context.getPreKeyHelper().forceRefreshPreKeys();
+ } else {
+ context.getPreKeyHelper().refreshPreKeysIfNecessary();
+ }
if (account.getAci() == null || account.getPni() == null) {
checkWhoAmiI();
}
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
- context.getSyncHelper().requestSyncPniIdentity();
+ throw new IOException("Missing PNI identity key, relinking required");
}
if (account.getPreviousStorageVersion() < 4
&& account.isPrimaryDevice()
account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
}
account.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
- // TODO check and update remote storage
context.getUnidentifiedAccessHelper().rotateSenderCertificates();
dependencies.resetAfterAddressChange();
context.getGroupV2Helper().clearAuthCredentialCache();
context.getAccountFileUpdater().updateAccountIdentifiers(account.getNumber(), account.getAci());
+ context.getJobExecutor().enqueueJob(new SyncStorageJob());
}
public void setPni(
}
public void startChangeNumber(
- String newNumber, boolean voiceVerification, String captcha
- ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
+ String newNumber,
+ boolean voiceVerification,
+ String captcha
+ ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
- String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
+ final var registrationApi = accountManager.getRegistrationApi();
+ String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi,
account.getSessionId(newNumber),
id -> account.setSessionId(newNumber, id),
voiceVerification,
captcha);
- NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification);
+ NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification);
}
public void finishChangeNumber(
- String newNumber, String verificationCode, String pin
+ String newNumber,
+ String verificationCode,
+ String pin
) throws IncorrectPinException, PinLockedException, IOException {
for (var attempts = 0; attempts < 5; attempts++) {
try {
}
private void finishChangeNumberInternal(
- String newNumber, String verificationCode, String pin
+ String newNumber,
+ String verificationCode,
+ String pin
) throws IncorrectPinException, PinLockedException, IOException {
final var pniIdentity = KeyUtils.generateIdentityKeyPair();
final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
final var messageSender = dependencies.getMessageSender();
for (final var deviceId : deviceIds) {
// Signed Prekey
- final var signedPreKeyRecord = KeyUtils.generateSignedPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
- pniIdentity.getPrivateKey());
- final var signedPreKeyEntity = new SignedPreKeyEntity(signedPreKeyRecord.getId(),
- signedPreKeyRecord.getKeyPair().getPublicKey(),
- signedPreKeyRecord.getSignature());
- devicePniSignedPreKeys.put(deviceId, signedPreKeyEntity);
+ final SignedPreKeyRecord signedPreKeyRecord;
+ try {
+ signedPreKeyRecord = KeyUtils.generateSignedPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
+ pniIdentity.getPrivateKey());
+ final var signedPreKeyEntity = new SignedPreKeyEntity(signedPreKeyRecord.getId(),
+ signedPreKeyRecord.getKeyPair().getPublicKey(),
+ signedPreKeyRecord.getSignature());
+ devicePniSignedPreKeys.put(deviceId, signedPreKeyEntity);
+ } catch (InvalidKeyException e) {
+ throw new AssertionError("unexpected invalid key", e);
+ }
// Last-resort kyber prekey
- final var lastResortKyberPreKeyRecord = KeyUtils.generateKyberPreKeyRecord(KeyUtils.getRandomInt(
- PREKEY_MAXIMUM_ID), pniIdentity.getPrivateKey());
- final var kyberPreKeyEntity = new KyberPreKeyEntity(lastResortKyberPreKeyRecord.getId(),
- lastResortKyberPreKeyRecord.getKeyPair().getPublicKey(),
- lastResortKyberPreKeyRecord.getSignature());
- devicePniLastResortKyberPreKeys.put(deviceId, kyberPreKeyEntity);
+ final KyberPreKeyRecord lastResortKyberPreKeyRecord;
+ try {
+ lastResortKyberPreKeyRecord = KeyUtils.generateKyberPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
+ pniIdentity.getPrivateKey());
+ final var kyberPreKeyEntity = new KyberPreKeyEntity(lastResortKyberPreKeyRecord.getId(),
+ lastResortKyberPreKeyRecord.getKeyPair().getPublicKey(),
+ lastResortKyberPreKeyRecord.getSignature());
+ devicePniLastResortKyberPreKeys.put(deviceId, kyberPreKeyEntity);
+ } catch (InvalidKeyException e) {
+ throw new AssertionError("unexpected invalid key", e);
+ }
// Registration Id
var pniRegistrationId = -1;
pin,
context.getPinHelper(),
(sessionId1, verificationCode1, registrationLock) -> {
- final var accountManager = dependencies.getAccountManager();
+ final var registrationApi = dependencies.getRegistrationApi();
+ final var accountApi = dependencies.getAccountApi();
try {
- Utils.handleResponseException(accountManager.verifyAccount(verificationCode1, sessionId1));
+ handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
} catch (AlreadyVerifiedException e) {
// Already verified so can continue changing number
}
- return Utils.handleResponseException(accountManager.changeNumber(new ChangePhoneNumberRequest(
- sessionId1,
+ return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
null,
newNumber,
registrationLock,
handlePniChangeNumberMessage(selfChangeNumber, updatePni);
}
- public void handlePniChangeNumberMessage(
- final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni
- ) {
+ public void handlePniChangeNumberMessage(final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni) {
if (pniChangeNumber.identityKeyPair != null
&& pniChangeNumber.registrationId != null
&& pniChangeNumber.signedPreKey != null) {
public static final int USERNAME_MIN_LENGTH = 3;
public static final int USERNAME_MAX_LENGTH = 32;
- public void reserveUsername(String nickname) throws IOException, BaseUsernameException {
+ public void reserveUsernameFromNickname(String nickname) throws IOException, BaseUsernameException {
final var currentUsername = account.getUsername();
if (currentUsername != null) {
final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.'));
if (currentNickname.equals(nickname)) {
- refreshCurrentUsername();
- return;
+ try {
+ refreshCurrentUsername();
+ return;
+ } catch (IOException | BaseUsernameException e) {
+ logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
+ }
}
}
final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH);
+ reserveUsername(candidates);
+ }
+
+ public void reserveExactUsername(String username) throws IOException, BaseUsernameException {
+ final var currentUsername = account.getUsername();
+ if (currentUsername != null) {
+ if (currentUsername.equals(username)) {
+ try {
+ refreshCurrentUsername();
+ return;
+ } catch (IOException | BaseUsernameException e) {
+ logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
+ }
+ }
+ }
+
+ final var candidates = List.of(new Username(username));
+ reserveUsername(candidates);
+ }
+
+ private void reserveUsername(final List<Username> candidates) throws IOException {
final var candidateHashes = new ArrayList<String>();
for (final var candidate : candidates) {
candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
}
- final var response = dependencies.getAccountManager().reserveUsername(candidateHashes);
+ final var response = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
if (hashIndex == -1) {
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
logger.debug("[reserveUsername] Successfully reserved username.");
final var username = candidates.get(hashIndex);
- dependencies.getAccountManager().confirmUsername(username.getUsername(), response);
+ final var linkComponents = confirmUsernameAndCreateNewLink(username);
account.setUsername(username.getUsername());
+ account.setUsernameLink(linkComponents);
account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
+ account.getRecipientStore().rotateSelfStorageId();
logger.debug("[confirmUsername] Successfully confirmed username.");
- tryToSetUsernameLink(username);
+ }
+
+ public UsernameLinkComponents createUsernameLink(Username username) throws IOException {
+ try {
+ Username.UsernameLink link = username.generateLink();
+ return handleResponseException(dependencies.getAccountApi().createUsernameLink(link));
+ } catch (BaseUsernameException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private UsernameLinkComponents confirmUsernameAndCreateNewLink(Username username) throws IOException {
+ try {
+ Username.UsernameLink link = username.generateLink();
+ UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
+
+ return new UsernameLinkComponents(link.getEntropy(), serverId);
+ } catch (BaseUsernameException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private UsernameLinkComponents reclaimUsernameAndLink(
+ Username username,
+ UsernameLinkComponents linkComponents
+ ) throws IOException {
+ try {
+ Username.UsernameLink link = username.generateLink(linkComponents.getEntropy());
+ UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
+
+ return new UsernameLinkComponents(link.getEntropy(), serverId);
+ } catch (BaseUsernameException e) {
+ throw new AssertionError(e);
+ }
}
public void refreshCurrentUsername() throws IOException, BaseUsernameException {
e.getClass().getSimpleName());
account.setUsername(null);
account.setUsernameLink(null);
+ account.getRecipientStore().rotateSelfStorageId();
throw e;
}
} else {
}
private void tryReserveConfirmUsername(final Username username) throws IOException {
- final var response = dependencies.getAccountManager()
- .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash())));
- logger.debug("[reserveUsername] Successfully reserved existing username.");
- dependencies.getAccountManager().confirmUsername(username.getUsername(), response);
- logger.debug("[confirmUsername] Successfully confirmed existing username.");
- tryToSetUsernameLink(username);
+ final var usernameLink = account.getUsernameLink();
+
+ if (usernameLink == null) {
+ handleResponseException(dependencies.getAccountApi()
+ .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))));
+ logger.debug("[reserveUsername] Successfully reserved existing username.");
+ final var linkComponents = confirmUsernameAndCreateNewLink(username);
+ account.setUsernameLink(linkComponents);
+ logger.debug("[confirmUsername] Successfully confirmed existing username.");
+ } else {
+ final var linkComponents = reclaimUsernameAndLink(username, usernameLink);
+ account.setUsernameLink(linkComponents);
+ logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
+ }
+ account.getRecipientStore().rotateSelfStorageId();
}
private void tryToSetUsernameLink(Username username) {
for (var i = 1; i < 4; i++) {
try {
- final var linkComponents = dependencies.getAccountManager().createUsernameLink(username);
+ final var linkComponents = createUsernameLink(username);
account.setUsernameLink(linkComponents);
break;
} catch (IOException e) {
}
public void deleteUsername() throws IOException {
- dependencies.getAccountManager().deleteUsername();
+ handleResponseException(dependencies.getAccountApi().deleteUsername());
+ account.setUsernameLink(null);
account.setUsername(null);
logger.debug("[deleteUsername] Successfully deleted the username.");
}
}
public void updateAccountAttributes() throws IOException {
- dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
+ handleResponseException(dependencies.getAccountApi().setAccountAttributes(account.getAccountAttributes(null)));
}
- public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException {
- var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
-
+ public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException {
+ final var linkDeviceApi = dependencies.getLinkDeviceApi();
+ final LinkedDeviceVerificationCodeResponse verificationCode;
try {
- dependencies.getAccountManager()
- .addDevice(deviceLinkInfo.deviceIdentifier(),
- deviceLinkInfo.deviceKey(),
- account.getAciIdentityKeyPair(),
- account.getPniIdentityKeyPair(),
- account.getProfileKey(),
- account.getOrCreatePinMasterKey(),
- verificationCode);
- } catch (InvalidKeyException e) {
- throw new InvalidDeviceLinkException("Invalid device link", e);
+ verificationCode = handleResponseException(linkDeviceApi.getDeviceVerificationCode());
+ } catch (DeviceLimitExceededException e) {
+ throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e);
}
+
+ handleResponseException(dependencies.getLinkDeviceApi()
+ .linkDevice(account.getNumber(),
+ account.getAci(),
+ account.getPni(),
+ deviceLinkInfo.deviceIdentifier(),
+ deviceLinkInfo.deviceKey(),
+ account.getAciIdentityKeyPair(),
+ account.getPniIdentityKeyPair(),
+ account.getProfileKey(),
+ account.getOrCreatePinMasterKey(),
+ account.getOrCreateMediaRootBackupKey(),
+ account.getOrCreateAccountEntropyPool(),
+ verificationCode.getVerificationCode(),
+ null));
account.setMultiDevice(true);
+ context.getJobExecutor().enqueueJob(new SyncStorageJob());
}
public void removeLinkedDevices(int deviceId) throws IOException {
- dependencies.getAccountManager().removeDevice(deviceId);
- var devices = dependencies.getAccountManager().getDevices();
+ handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId));
+ var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
account.setMultiDevice(devices.size() > 1);
}
var masterKey = account.getOrCreatePinMasterKey();
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
- dependencies.getAccountManager().enableRegistrationLock(masterKey);
+ handleResponseException(dependencies.getAccountApi()
+ .enableRegistrationLock(masterKey.deriveRegistrationLock()));
}
public void setRegistrationPin(String pin) throws IOException {
var masterKey = account.getOrCreatePinMasterKey();
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
- dependencies.getAccountManager().enableRegistrationLock(masterKey);
+ handleResponseException(dependencies.getAccountApi()
+ .enableRegistrationLock(masterKey.deriveRegistrationLock()));
account.setRegistrationLockPin(pin);
+ updateAccountAttributes();
}
public void removeRegistrationPin() throws IOException {
// Remove KBS Pin
context.getPinHelper().removeRegistrationLockPin();
- dependencies.getAccountManager().disableRegistrationLock();
+ handleResponseException(dependencies.getAccountApi().disableRegistrationLock());
account.setRegistrationLockPin(null);
}
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
// If this is the primary device, other users can't send messages to this number anymore.
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
- dependencies.getAccountManager().setGcmId(Optional.empty());
+ handleResponseException(dependencies.getAccountApi().clearFcmToken());
account.setRegistered(false);
unregisteredListener.call();