mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 02:20:39 +00:00
Allow relinking an account if it's no longer authorized
This commit is contained in:
parent
0bc2141245
commit
ab95e635ce
7 changed files with 137 additions and 22 deletions
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 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());
|
||||
|
||||
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.");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue