Allow relinking an account if it's no longer authorized
This commit is contained in:
		
							parent
							
								
									0bc2141245
								
							
						
					
					
						commit
						ab95e635ce
					
				@ -288,7 +288,7 @@ public class Manager implements Closeable {
 | 
				
			|||||||
            throw new NotRegisteredException();
 | 
					            throw new NotRegisteredException();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var account = SignalAccount.load(pathConfig.getDataPath(), username);
 | 
					        var account = SignalAccount.load(pathConfig.getDataPath(), username, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!account.isRegistered()) {
 | 
					        if (!account.isRegistered()) {
 | 
				
			||||||
            throw new NotRegisteredException();
 | 
					            throw new NotRegisteredException();
 | 
				
			||||||
 | 
				
			|||||||
@ -29,6 +29,7 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager;
 | 
				
			|||||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
 | 
					import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
 | 
				
			||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
 | 
					import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
 | 
				
			||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 | 
					import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 | 
				
			||||||
 | 
					import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
 | 
				
			||||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
 | 
					import org.whispersystems.signalservice.api.util.DeviceNameUtil;
 | 
				
			||||||
import org.whispersystems.signalservice.api.util.SleepTimer;
 | 
					import org.whispersystems.signalservice.api.util.SleepTimer;
 | 
				
			||||||
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
 | 
					import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
 | 
				
			||||||
@ -97,7 +98,7 @@ public class ProvisioningManager {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        logger.info("Received link information from {}, linking in progress ...", number);
 | 
					        logger.info("Received link information from {}, linking in progress ...", number);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (SignalAccount.userExists(pathConfig.getDataPath(), number)) {
 | 
					        if (SignalAccount.userExists(pathConfig.getDataPath(), number) && !canRelinkExistingAccount(number)) {
 | 
				
			||||||
            throw new UserAlreadyExists(number, SignalAccount.getFileName(pathConfig.getDataPath(), number));
 | 
					            throw new UserAlreadyExists(number, SignalAccount.getFileName(pathConfig.getDataPath(), number));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -116,7 +117,7 @@ public class ProvisioningManager {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        SignalAccount account = null;
 | 
					        SignalAccount account = null;
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            account = SignalAccount.createLinkedAccount(pathConfig.getDataPath(),
 | 
					            account = SignalAccount.createOrUpdateLinkedAccount(pathConfig.getDataPath(),
 | 
				
			||||||
                    number,
 | 
					                    number,
 | 
				
			||||||
                    ret.getUuid(),
 | 
					                    ret.getUuid(),
 | 
				
			||||||
                    password,
 | 
					                    password,
 | 
				
			||||||
@ -133,7 +134,7 @@ public class ProvisioningManager {
 | 
				
			|||||||
                try {
 | 
					                try {
 | 
				
			||||||
                    m.refreshPreKeys();
 | 
					                    m.refreshPreKeys();
 | 
				
			||||||
                } catch (Exception e) {
 | 
					                } catch (Exception e) {
 | 
				
			||||||
                    logger.error("Failed to refresh prekeys.");
 | 
					                    logger.error("Failed to check new account state.");
 | 
				
			||||||
                    throw e;
 | 
					                    throw e;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -160,4 +161,31 @@ public class ProvisioningManager {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private boolean canRelinkExistingAccount(final String number) throws UserAlreadyExists, IOException {
 | 
				
			||||||
 | 
					        final SignalAccount signalAccount;
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            signalAccount = SignalAccount.load(pathConfig.getDataPath(), number, false);
 | 
				
			||||||
 | 
					        } catch (IOException e) {
 | 
				
			||||||
 | 
					            logger.debug("Account in use or failed to load.", e);
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try (signalAccount) {
 | 
				
			||||||
 | 
					            if (signalAccount.isMasterDevice()) {
 | 
				
			||||||
 | 
					                logger.debug("Account is a master device.");
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final var m = new Manager(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent);
 | 
				
			||||||
 | 
					            try (m) {
 | 
				
			||||||
 | 
					                m.checkAccountState();
 | 
				
			||||||
 | 
					            } catch (AuthorizationFailedException ignored) {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            logger.debug("Account is still successfully linked.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -107,7 +107,7 @@ public class RegistrationManager implements Closeable {
 | 
				
			|||||||
            return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
 | 
					            return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var account = SignalAccount.load(pathConfig.getDataPath(), username);
 | 
					        var account = SignalAccount.load(pathConfig.getDataPath(), username, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
 | 
					        return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -105,14 +105,19 @@ public class SignalAccount implements Closeable {
 | 
				
			|||||||
        this.lock = lock;
 | 
					        this.lock = lock;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static SignalAccount load(File dataPath, String username) throws IOException {
 | 
					    public static SignalAccount load(File dataPath, String username, boolean waitForLock) throws IOException {
 | 
				
			||||||
        final var fileName = getFileName(dataPath, username);
 | 
					        final var fileName = getFileName(dataPath, username);
 | 
				
			||||||
        final var pair = openFileChannel(fileName);
 | 
					        final var pair = openFileChannel(fileName, waitForLock);
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            var account = new SignalAccount(pair.first(), pair.second());
 | 
					            var account = new SignalAccount(pair.first(), pair.second());
 | 
				
			||||||
            account.load(dataPath);
 | 
					            account.load(dataPath);
 | 
				
			||||||
            account.migrateLegacyConfigs();
 | 
					            account.migrateLegacyConfigs();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!username.equals(account.getUsername())) {
 | 
				
			||||||
 | 
					                throw new IOException("Username in account file doesn't match expected number: "
 | 
				
			||||||
 | 
					                        + account.getUsername());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return account;
 | 
					            return account;
 | 
				
			||||||
        } catch (Throwable e) {
 | 
					        } catch (Throwable e) {
 | 
				
			||||||
            pair.second().close();
 | 
					            pair.second().close();
 | 
				
			||||||
@ -130,7 +135,7 @@ public class SignalAccount implements Closeable {
 | 
				
			|||||||
            IOUtils.createPrivateFile(fileName);
 | 
					            IOUtils.createPrivateFile(fileName);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final var pair = openFileChannel(fileName);
 | 
					        final var pair = openFileChannel(fileName, true);
 | 
				
			||||||
        var account = new SignalAccount(pair.first(), pair.second());
 | 
					        var account = new SignalAccount(pair.first(), pair.second());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        account.username = username;
 | 
					        account.username = username;
 | 
				
			||||||
@ -167,7 +172,7 @@ public class SignalAccount implements Closeable {
 | 
				
			|||||||
        messageCache = new MessageCache(getMessageCachePath(dataPath, username));
 | 
					        messageCache = new MessageCache(getMessageCachePath(dataPath, username));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static SignalAccount createLinkedAccount(
 | 
					    public static SignalAccount createOrUpdateLinkedAccount(
 | 
				
			||||||
            File dataPath,
 | 
					            File dataPath,
 | 
				
			||||||
            String username,
 | 
					            String username,
 | 
				
			||||||
            UUID uuid,
 | 
					            UUID uuid,
 | 
				
			||||||
@ -181,18 +186,51 @@ public class SignalAccount implements Closeable {
 | 
				
			|||||||
        IOUtils.createPrivateDirectories(dataPath);
 | 
					        IOUtils.createPrivateDirectories(dataPath);
 | 
				
			||||||
        var fileName = getFileName(dataPath, username);
 | 
					        var fileName = getFileName(dataPath, username);
 | 
				
			||||||
        if (!fileName.exists()) {
 | 
					        if (!fileName.exists()) {
 | 
				
			||||||
            IOUtils.createPrivateFile(fileName);
 | 
					            return createLinkedAccount(dataPath,
 | 
				
			||||||
 | 
					                    username,
 | 
				
			||||||
 | 
					                    uuid,
 | 
				
			||||||
 | 
					                    password,
 | 
				
			||||||
 | 
					                    encryptedDeviceName,
 | 
				
			||||||
 | 
					                    deviceId,
 | 
				
			||||||
 | 
					                    identityKey,
 | 
				
			||||||
 | 
					                    registrationId,
 | 
				
			||||||
 | 
					                    profileKey);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final var pair = openFileChannel(fileName);
 | 
					        final var account = load(dataPath, username, true);
 | 
				
			||||||
 | 
					        account.setProvisioningData(username, uuid, password, encryptedDeviceName, deviceId, profileKey);
 | 
				
			||||||
 | 
					        account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
 | 
				
			||||||
 | 
					        account.sessionStore.archiveAllSessions();
 | 
				
			||||||
 | 
					        account.clearAllPreKeys();
 | 
				
			||||||
 | 
					        return account;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void clearAllPreKeys() {
 | 
				
			||||||
 | 
					        this.preKeyIdOffset = 0;
 | 
				
			||||||
 | 
					        this.nextSignedPreKeyId = 0;
 | 
				
			||||||
 | 
					        this.preKeyStore.removeAllPreKeys();
 | 
				
			||||||
 | 
					        this.signedPreKeyStore.removeAllSignedPreKeys();
 | 
				
			||||||
 | 
					        save();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static SignalAccount createLinkedAccount(
 | 
				
			||||||
 | 
					            File dataPath,
 | 
				
			||||||
 | 
					            String username,
 | 
				
			||||||
 | 
					            UUID uuid,
 | 
				
			||||||
 | 
					            String password,
 | 
				
			||||||
 | 
					            String encryptedDeviceName,
 | 
				
			||||||
 | 
					            int deviceId,
 | 
				
			||||||
 | 
					            IdentityKeyPair identityKey,
 | 
				
			||||||
 | 
					            int registrationId,
 | 
				
			||||||
 | 
					            ProfileKey profileKey
 | 
				
			||||||
 | 
					    ) throws IOException {
 | 
				
			||||||
 | 
					        var fileName = getFileName(dataPath, username);
 | 
				
			||||||
 | 
					        IOUtils.createPrivateFile(fileName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        final var pair = openFileChannel(fileName, true);
 | 
				
			||||||
        var account = new SignalAccount(pair.first(), pair.second());
 | 
					        var account = new SignalAccount(pair.first(), pair.second());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        account.username = username;
 | 
					        account.setProvisioningData(username, uuid, password, encryptedDeviceName, deviceId, profileKey);
 | 
				
			||||||
        account.uuid = uuid;
 | 
					 | 
				
			||||||
        account.password = password;
 | 
					 | 
				
			||||||
        account.profileKey = profileKey;
 | 
					 | 
				
			||||||
        account.encryptedDeviceName = encryptedDeviceName;
 | 
					 | 
				
			||||||
        account.deviceId = deviceId;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        account.initStores(dataPath, identityKey, registrationId);
 | 
					        account.initStores(dataPath, identityKey, registrationId);
 | 
				
			||||||
        account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
 | 
					        account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
 | 
				
			||||||
@ -200,9 +238,6 @@ public class SignalAccount implements Closeable {
 | 
				
			|||||||
                account::saveGroupStore);
 | 
					                account::saveGroupStore);
 | 
				
			||||||
        account.stickerStore = new StickerStore(account::saveStickerStore);
 | 
					        account.stickerStore = new StickerStore(account::saveStickerStore);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        account.registered = true;
 | 
					 | 
				
			||||||
        account.isMultiDevice = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
 | 
					        account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
 | 
				
			||||||
        account.migrateLegacyConfigs();
 | 
					        account.migrateLegacyConfigs();
 | 
				
			||||||
        account.save();
 | 
					        account.save();
 | 
				
			||||||
@ -210,6 +245,24 @@ public class SignalAccount implements Closeable {
 | 
				
			|||||||
        return account;
 | 
					        return account;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void setProvisioningData(
 | 
				
			||||||
 | 
					            final String username,
 | 
				
			||||||
 | 
					            final UUID uuid,
 | 
				
			||||||
 | 
					            final String password,
 | 
				
			||||||
 | 
					            final String encryptedDeviceName,
 | 
				
			||||||
 | 
					            final int deviceId,
 | 
				
			||||||
 | 
					            final ProfileKey profileKey
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        this.username = username;
 | 
				
			||||||
 | 
					        this.uuid = uuid;
 | 
				
			||||||
 | 
					        this.password = password;
 | 
				
			||||||
 | 
					        this.profileKey = profileKey;
 | 
				
			||||||
 | 
					        this.encryptedDeviceName = encryptedDeviceName;
 | 
				
			||||||
 | 
					        this.deviceId = deviceId;
 | 
				
			||||||
 | 
					        this.registered = true;
 | 
				
			||||||
 | 
					        this.isMultiDevice = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private void migrateLegacyConfigs() {
 | 
					    private void migrateLegacyConfigs() {
 | 
				
			||||||
        if (getPassword() == null) {
 | 
					        if (getPassword() == null) {
 | 
				
			||||||
            setPassword(KeyUtils.createPassword());
 | 
					            setPassword(KeyUtils.createPassword());
 | 
				
			||||||
@ -618,10 +671,14 @@ public class SignalAccount implements Closeable {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static Pair<FileChannel, FileLock> openFileChannel(File fileName) throws IOException {
 | 
					    private static Pair<FileChannel, FileLock> openFileChannel(File fileName, boolean waitForLock) throws IOException {
 | 
				
			||||||
        var fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
 | 
					        var fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
 | 
				
			||||||
        var lock = fileChannel.tryLock();
 | 
					        var lock = fileChannel.tryLock();
 | 
				
			||||||
        if (lock == null) {
 | 
					        if (lock == null) {
 | 
				
			||||||
 | 
					            if (!waitForLock) {
 | 
				
			||||||
 | 
					                logger.debug("Config file is in use by another instance.");
 | 
				
			||||||
 | 
					                throw new IOException("Config file is in use by another instance.");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            logger.info("Config file is in use by another instance, waiting…");
 | 
					            logger.info("Config file is in use by another instance, waiting…");
 | 
				
			||||||
            lock = fileChannel.lock();
 | 
					            lock = fileChannel.lock();
 | 
				
			||||||
            logger.info("Config file lock acquired.");
 | 
					            logger.info("Config file lock acquired.");
 | 
				
			||||||
 | 
				
			|||||||
@ -78,6 +78,21 @@ public class PreKeyStore implements org.whispersystems.libsignal.state.PreKeySto
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void removeAllPreKeys() {
 | 
				
			||||||
 | 
					        final var files = preKeysPath.listFiles();
 | 
				
			||||||
 | 
					        if (files == null) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (var file : files) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                Files.delete(file.toPath());
 | 
				
			||||||
 | 
					            } catch (IOException e) {
 | 
				
			||||||
 | 
					                logger.error("Failed to delete pre key file {}: {}", file, e.getMessage());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private File getPreKeyFile(int preKeyId) {
 | 
					    private File getPreKeyFile(int preKeyId) {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            IOUtils.createPrivateDirectories(preKeysPath);
 | 
					            IOUtils.createPrivateDirectories(preKeysPath);
 | 
				
			||||||
 | 
				
			|||||||
@ -91,6 +91,21 @@ public class SignedPreKeyStore implements org.whispersystems.libsignal.state.Sig
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void removeAllSignedPreKeys() {
 | 
				
			||||||
 | 
					        final var files = signedPreKeysPath.listFiles();
 | 
				
			||||||
 | 
					        if (files == null) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (var file : files) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                Files.delete(file.toPath());
 | 
				
			||||||
 | 
					            } catch (IOException e) {
 | 
				
			||||||
 | 
					                logger.error("Failed to delete signed pre key file {}: {}", file, e.getMessage());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private File getSignedPreKeyFile(int signedPreKeyId) {
 | 
					    private File getSignedPreKeyFile(int signedPreKeyId) {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            IOUtils.createPrivateDirectories(signedPreKeysPath);
 | 
					            IOUtils.createPrivateDirectories(signedPreKeysPath);
 | 
				
			||||||
 | 
				
			|||||||
@ -35,7 +35,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
 | 
				
			|||||||
    public void attachToSubparser(final Subparser subparser) {
 | 
					    public void attachToSubparser(final Subparser subparser) {
 | 
				
			||||||
        subparser.addArgument("-t", "--timeout")
 | 
					        subparser.addArgument("-t", "--timeout")
 | 
				
			||||||
                .type(double.class)
 | 
					                .type(double.class)
 | 
				
			||||||
                .setDefault(1.0)
 | 
					                .setDefault(3.0)
 | 
				
			||||||
                .help("Number of seconds to wait for new messages (negative values disable timeout)");
 | 
					                .help("Number of seconds to wait for new messages (negative values disable timeout)");
 | 
				
			||||||
        subparser.addArgument("--ignore-attachments")
 | 
					        subparser.addArgument("--ignore-attachments")
 | 
				
			||||||
                .help("Don’t download attachments of received messages.")
 | 
					                .help("Don’t download attachments of received messages.")
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user