]> nmode's Git Repositories - signal-cli/commitdiff
Allow relinking an account if it's no longer authorized
authorAsamK <asamk@gmx.de>
Sun, 9 May 2021 10:22:44 +0000 (12:22 +0200)
committerAsamK <asamk@gmx.de>
Sun, 9 May 2021 10:22:44 +0000 (12:22 +0200)
lib/src/main/java/org/asamk/signal/manager/Manager.java
lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java
lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java
lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
lib/src/main/java/org/asamk/signal/manager/storage/prekeys/PreKeyStore.java
lib/src/main/java/org/asamk/signal/manager/storage/prekeys/SignedPreKeyStore.java
src/main/java/org/asamk/signal/commands/ReceiveCommand.java

index acd48c34e3153281697737cac41944f3950a37e0..8632129c44d2d1216656330003000817da739c94 100644 (file)
@@ -288,7 +288,7 @@ public class Manager implements Closeable {
             throw new NotRegisteredException();
         }
 
-        var account = SignalAccount.load(pathConfig.getDataPath(), username);
+        var account = SignalAccount.load(pathConfig.getDataPath(), username, true);
 
         if (!account.isRegistered()) {
             throw new NotRegisteredException();
index e6fe2a60a3c1045f456acdf5e7b921137f4d0bdd..b0bfebf819cf64f053879a78d9799749f7421c23 100644 (file)
@@ -29,6 +29,7 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager;
 import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
 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.SleepTimer;
 import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
@@ -97,7 +98,7 @@ public class ProvisioningManager {
 
         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));
         }
 
@@ -116,7 +117,7 @@ public class ProvisioningManager {
 
         SignalAccount account = null;
         try {
-            account = SignalAccount.createLinkedAccount(pathConfig.getDataPath(),
+            account = SignalAccount.createOrUpdateLinkedAccount(pathConfig.getDataPath(),
                     number,
                     ret.getUuid(),
                     password,
@@ -133,7 +134,7 @@ public class ProvisioningManager {
                 try {
                     m.refreshPreKeys();
                 } catch (Exception e) {
-                    logger.error("Failed to refresh prekeys.");
+                    logger.error("Failed to check new account state.");
                     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;
+        }
+    }
 }
index 612984edbdd78f50e047eb44b931bb922277b3fb..9dae3f41376fc043c8c906962f2b4ce761983c0d 100644 (file)
@@ -107,7 +107,7 @@ public class RegistrationManager implements Closeable {
             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);
     }
index 93178a91766430df6027c6ce28604547dddf3442..1620dbf258c313ae24055eb7e9b29d74c310a8ea 100644 (file)
@@ -105,14 +105,19 @@ public class SignalAccount implements Closeable {
         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 pair = openFileChannel(fileName);
+        final var pair = openFileChannel(fileName, waitForLock);
         try {
             var account = new SignalAccount(pair.first(), pair.second());
             account.load(dataPath);
             account.migrateLegacyConfigs();
 
+            if (!username.equals(account.getUsername())) {
+                throw new IOException("Username in account file doesn't match expected number: "
+                        + account.getUsername());
+            }
+
             return account;
         } catch (Throwable e) {
             pair.second().close();
@@ -130,7 +135,7 @@ public class SignalAccount implements Closeable {
             IOUtils.createPrivateFile(fileName);
         }
 
-        final var pair = openFileChannel(fileName);
+        final var pair = openFileChannel(fileName, true);
         var account = new SignalAccount(pair.first(), pair.second());
 
         account.username = username;
@@ -167,7 +172,7 @@ public class SignalAccount implements Closeable {
         messageCache = new MessageCache(getMessageCachePath(dataPath, username));
     }
 
-    public static SignalAccount createLinkedAccount(
+    public static SignalAccount createOrUpdateLinkedAccount(
             File dataPath,
             String username,
             UUID uuid,
@@ -181,18 +186,51 @@ public class SignalAccount implements Closeable {
         IOUtils.createPrivateDirectories(dataPath);
         var fileName = getFileName(dataPath, username);
         if (!fileName.exists()) {
-            IOUtils.createPrivateFile(fileName);
-        }
+            return createLinkedAccount(dataPath,
+                    username,
+                    uuid,
+                    password,
+                    encryptedDeviceName,
+                    deviceId,
+                    identityKey,
+                    registrationId,
+                    profileKey);
+        }
+
+        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;
+    }
 
-        final var pair = openFileChannel(fileName);
+    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());
 
-        account.username = username;
-        account.uuid = uuid;
-        account.password = password;
-        account.profileKey = profileKey;
-        account.encryptedDeviceName = encryptedDeviceName;
-        account.deviceId = deviceId;
+        account.setProvisioningData(username, uuid, password, encryptedDeviceName, deviceId, profileKey);
 
         account.initStores(dataPath, identityKey, registrationId);
         account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
@@ -200,9 +238,6 @@ public class SignalAccount implements Closeable {
                 account::saveGroupStore);
         account.stickerStore = new StickerStore(account::saveStickerStore);
 
-        account.registered = true;
-        account.isMultiDevice = true;
-
         account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
         account.migrateLegacyConfigs();
         account.save();
@@ -210,6 +245,24 @@ public class SignalAccount implements Closeable {
         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() {
         if (getPassword() == null) {
             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 lock = fileChannel.tryLock();
         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…");
             lock = fileChannel.lock();
             logger.info("Config file lock acquired.");
index c8153c563dc81db91f2b5936e24376be87ef61ed..ccdea42246b3ca0f79e3c3941c6baf3f62fba993 100644 (file)
@@ -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) {
         try {
             IOUtils.createPrivateDirectories(preKeysPath);
index 83176557f0cc59ef931c34b0c29a2c41a69c3038..bc85e3e8273f983a26aef83127931f8298f7ec9b 100644 (file)
@@ -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) {
         try {
             IOUtils.createPrivateDirectories(signedPreKeysPath);
index a6ef8484ef52315d1c225666a9cdce6c9a3b01e3..f3f77347b490c9ef20cb6f501453d8e39131eee0 100644 (file)
@@ -35,7 +35,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
     public void attachToSubparser(final Subparser subparser) {
         subparser.addArgument("-t", "--timeout")
                 .type(double.class)
-                .setDefault(1.0)
+                .setDefault(3.0)
                 .help("Number of seconds to wait for new messages (negative values disable timeout)");
         subparser.addArgument("--ignore-attachments")
                 .help("Don’t download attachments of received messages.")