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

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

View file

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

View file

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

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) { private File getPreKeyFile(int preKeyId) {
try { try {
IOUtils.createPrivateDirectories(preKeysPath); 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) { private File getSignedPreKeyFile(int signedPreKeyId) {
try { try {
IOUtils.createPrivateDirectories(signedPreKeysPath); IOUtils.createPrivateDirectories(signedPreKeysPath);

View file

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