Allow relinking an account if it's no longer authorized

This commit is contained in:
AsamK 2021-05-09 12:22:44 +02:00
parent 0bc2141245
commit ab95e635ce
7 changed files with 137 additions and 22 deletions

View 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();

View 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;
}
}
}

View 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);
}

View 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 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.");

View 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);

View 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);

View 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("Dont download attachments of received messages.")