mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 18:40: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();
|
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…
Add table
Add a link
Reference in a new issue