import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.helper.GroupHelper;
+import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.helper.ProfileHelper;
import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
import org.asamk.signal.manager.storage.SignalAccount;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
-import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.libsignal.util.Medium;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
+import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
-import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
private final SignalServiceConfiguration serviceConfiguration;
private final String userAgent;
- private final boolean discoverableByPhoneNumber = true;
- private final boolean unrestrictedUnidentifiedAccess = false;
- private final SignalAccount account;
+ private SignalAccount account;
private final PathConfig pathConfig;
- private SignalServiceAccountManager accountManager;
- private GroupsV2Api groupsV2Api;
+ private final SignalServiceAccountManager accountManager;
+ private final GroupsV2Api groupsV2Api;
private final GroupsV2Operations groupsV2Operations;
+ private final SignalServiceMessageReceiver messageReceiver;
+ private final ClientZkProfileOperations clientZkProfileOperations;
- private SignalServiceMessageReceiver messageReceiver = null;
private SignalServiceMessagePipe messagePipe = null;
private SignalServiceMessagePipe unidentifiedMessagePipe = null;
private final UnidentifiedAccessHelper unidentifiedAccessHelper;
private final ProfileHelper profileHelper;
private final GroupHelper groupHelper;
+ private final PinHelper pinHelper;
Manager(
SignalAccount account,
this.userAgent = userAgent;
this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create(
serviceConfiguration)) : null;
- this.accountManager = createSignalServiceAccountManager();
+ this.accountManager = new SignalServiceAccountManager(serviceConfiguration,
+ new DynamicCredentialsProvider(account.getUuid(),
+ account.getUsername(),
+ account.getPassword(),
+ account.getSignalingKey(),
+ account.getDeviceId()),
+ userAgent,
+ groupsV2Operations,
+ timer);
this.groupsV2Api = accountManager.getGroupsV2Api();
+ final KeyBackupService keyBackupService = ServiceConfig.createKeyBackupService(accountManager);
+ this.pinHelper = new PinHelper(keyBackupService);
+ this.clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create(serviceConfiguration)
+ .getProfileOperations() : null;
+ this.messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration,
+ account.getUuid(),
+ account.getUsername(),
+ account.getPassword(),
+ account.getDeviceId(),
+ account.getSignalingKey(),
+ userAgent,
+ null,
+ timer,
+ clientZkProfileOperations);
this.account.setResolver(this::resolveSignalServiceAddress);
this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey,
unidentifiedAccessHelper::getAccessFor,
unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(),
- this::getOrCreateMessageReceiver);
+ () -> messageReceiver);
this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
this::getRecipientProfile,
account::getSelfAddress,
return account.getSelfAddress();
}
- private SignalServiceAccountManager createSignalServiceAccountManager() {
- return new SignalServiceAccountManager(serviceConfiguration,
- new DynamicCredentialsProvider(account.getUuid(),
- account.getUsername(),
- account.getPassword(),
- null,
- account.getDeviceId()),
- userAgent,
- groupsV2Operations,
- timer);
- }
-
private IdentityKeyPair getIdentityKeyPair() {
return account.getSignalProtocolStore().getIdentityKeyPair();
}
public static Manager init(
String username, File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent
- ) throws IOException {
+ ) throws IOException, NotRegisteredException {
PathConfig pathConfig = PathConfig.createDefault(settingsPath);
if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) {
- IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
- int registrationId = KeyHelper.generateRegistrationId(false);
-
- ProfileKey profileKey = KeyUtils.createProfileKey();
- SignalAccount account = SignalAccount.create(pathConfig.getDataPath(),
- username,
- identityKey,
- registrationId,
- profileKey);
- account.save();
-
- return new Manager(account, pathConfig, serviceConfiguration, userAgent);
+ throw new NotRegisteredException();
}
SignalAccount account = SignalAccount.load(pathConfig.getDataPath(), username);
- Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent);
-
- m.migrateLegacyConfigs();
-
- return m;
- }
-
- private void migrateLegacyConfigs() {
- if (account.getProfileKey() == null && isRegistered()) {
- // Old config file, creating new profile key
- account.setProfileKey(KeyUtils.createProfileKey());
- account.save();
- }
- // Store profile keys only in profile store
- for (ContactInfo contact : account.getContactStore().getContacts()) {
- String profileKeyString = contact.profileKey;
- if (profileKeyString == null) {
- continue;
- }
- final ProfileKey profileKey;
- try {
- profileKey = new ProfileKey(Base64.decode(profileKeyString));
- } catch (InvalidInputException | IOException e) {
- continue;
- }
- contact.profileKey = null;
- account.getProfileStore().storeProfileKey(contact.getAddress(), profileKey);
+ if (!account.isRegistered()) {
+ throw new NotRegisteredException();
}
- // Ensure our profile key is stored in profile store
- account.getProfileStore().storeProfileKey(getSelfAddress(), account.getProfileKey());
+
+ return new Manager(account, pathConfig, serviceConfiguration, userAgent);
}
public void checkAccountState() throws IOException {
return account.isRegistered();
}
- public void register(boolean voiceVerification, String captcha) throws IOException {
- account.setPassword(KeyUtils.createPassword());
-
- // Resetting UUID, because registering doesn't work otherwise
- account.setUuid(null);
- accountManager = createSignalServiceAccountManager();
- this.groupsV2Api = accountManager.getGroupsV2Api();
+ /**
+ * This is used for checking a set of phone numbers for registration on Signal
+ *
+ * @param numbers The set of phone number in question
+ * @return A map of numbers to booleans. True if registered, false otherwise. Should never be null
+ * @throws IOException if its unable to check if the users are registered
+ */
+ public Map<String, Boolean> areUsersRegistered(Set<String> numbers) throws IOException {
+ // Note "contactDetails" has no optionals. It only gives us info on users who are registered
+ List<ContactTokenDetails> contactDetails = this.accountManager.getContacts(numbers);
- if (voiceVerification) {
- accountManager.requestVoiceVerificationCode(Locale.getDefault(),
- Optional.fromNullable(captcha),
- Optional.absent());
- } else {
- accountManager.requestSmsVerificationCode(false, Optional.fromNullable(captcha), Optional.absent());
- }
+ Set<String> registeredUsers = contactDetails.stream()
+ .map(ContactTokenDetails::getNumber)
+ .collect(Collectors.toSet());
- account.setRegistered(false);
- account.save();
+ return numbers.stream().collect(Collectors.toMap(x -> x, registeredUsers::contains));
}
public void updateAccountAttributes() throws IOException {
accountManager.setAccountAttributes(account.getSignalingKey(),
account.getSignalProtocolStore().getLocalRegistrationId(),
true,
- account.getRegistrationLockPin(),
- account.getRegistrationLock(),
- unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(),
- unrestrictedUnidentifiedAccess,
+ // set legacy pin only if no KBS master key is set
+ account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null,
+ account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
+ account.getSelfUnidentifiedAccessKey(),
+ account.isUnrestrictedUnidentifiedAccess(),
capabilities,
- discoverableByPhoneNumber);
+ account.isDiscoverableByPhoneNumber());
}
public void setProfile(String name, File avatar) throws IOException {
}
}
- public void verifyAccount(String verificationCode, String pin) throws IOException {
- verificationCode = verificationCode.replace("-", "");
- account.setSignalingKey(KeyUtils.createSignalingKey());
- // TODO make unrestricted unidentified access configurable
- VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode,
- account.getSignalingKey(),
- account.getSignalProtocolStore().getLocalRegistrationId(),
- true,
- pin,
- null,
- unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(),
- unrestrictedUnidentifiedAccess,
- capabilities,
- discoverableByPhoneNumber);
-
- UUID uuid = UuidUtil.parseOrNull(response.getUuid());
- // TODO response.isStorageCapable()
- //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
- account.setRegistered(true);
- account.setUuid(uuid);
- account.setRegistrationLockPin(pin);
- account.getSignalProtocolStore()
- .saveIdentity(account.getSelfAddress(),
- getIdentityKeyPair().getPublicKey(),
- TrustLevel.TRUSTED_VERIFIED);
-
- refreshPreKeys();
- account.save();
- }
-
- public void setRegistrationLockPin(Optional<String> pin) throws IOException {
+ public void setRegistrationLockPin(Optional<String> pin) throws IOException, UnauthenticatedResponseException {
if (pin.isPresent()) {
+ final MasterKey masterKey = account.getPinMasterKey() != null
+ ? account.getPinMasterKey()
+ : KeyUtils.createMasterKey();
+
+ pinHelper.setRegistrationLockPin(pin.get(), masterKey);
+
account.setRegistrationLockPin(pin.get());
- throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
+ account.setPinMasterKey(masterKey);
} else {
- account.setRegistrationLockPin(null);
+ // Remove legacy registration lock
accountManager.removeRegistrationLockV1();
+
+ // Remove KBS Pin
+ pinHelper.removeRegistrationLockPin();
+
+ account.setRegistrationLockPin(null);
+ account.setPinMasterKey(null);
}
account.save();
}
accountManager.setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys);
}
- private SignalServiceMessageReceiver createMessageReceiver() {
- final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create(
- serviceConfiguration).getProfileOperations() : null;
- return new SignalServiceMessageReceiver(serviceConfiguration,
- account.getUuid(),
- account.getUsername(),
- account.getPassword(),
- account.getDeviceId(),
- account.getSignalingKey(),
- userAgent,
- null,
- timer,
- clientZkProfileOperations);
- }
-
- private SignalServiceMessageReceiver getOrCreateMessageReceiver() {
- if (messageReceiver == null) {
- messageReceiver = createMessageReceiver();
- }
- return messageReceiver;
- }
-
private SignalServiceMessagePipe getOrCreateMessagePipe() {
if (messagePipe == null) {
- messagePipe = getOrCreateMessageReceiver().createMessagePipe();
+ messagePipe = messageReceiver.createMessagePipe();
}
return messagePipe;
}
private SignalServiceMessagePipe getOrCreateUnidentifiedMessagePipe() {
if (unidentifiedMessagePipe == null) {
- unidentifiedMessagePipe = getOrCreateMessageReceiver().createUnidentifiedMessagePipe();
+ unidentifiedMessagePipe = messageReceiver.createUnidentifiedMessagePipe();
}
return unidentifiedMessagePipe;
}
private SignalServiceMessageSender createMessageSender() {
- final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create(
- serviceConfiguration).getProfileOperations() : null;
final ExecutorService executor = null;
return new SignalServiceMessageSender(serviceConfiguration,
account.getUuid(),
}
private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
- GroupInfo g = account.getGroupStore().getGroup(groupId);
+ GroupInfo g = getGroup(groupId);
if (g == null) {
throw new GroupNotFoundException(groupId);
}
}
private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
- GroupInfo g = account.getGroupStore().getGroup(groupId);
+ GroupInfo g = getGroup(groupId);
if (g == null) {
throw new GroupNotFoundException(groupId);
}
* Change the expiration timer for a group
*/
public void setExpirationTimer(GroupId groupId, int messageExpirationTimer) {
- GroupInfo g = account.getGroupStore().getGroup(groupId);
+ GroupInfo g = getGroup(groupId);
if (g instanceof GroupInfoV1) {
GroupInfoV1 groupInfoV1 = (GroupInfoV1) g;
groupInfoV1.messageExpirationTime = messageExpirationTimer;
if (message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
GroupIdV1 groupId = GroupId.v1(groupInfo.getGroupId());
- GroupInfo group = account.getGroupStore().getGroup(groupId);
+ GroupInfo group = getGroup(groupId);
if (group == null || group instanceof GroupInfoV1) {
GroupInfoV1 groupV1 = (GroupInfoV1) group;
switch (groupInfo.getType()) {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams);
- GroupInfo groupInfo = account.getGroupStore().getGroup(groupId);
+ GroupInfo groupInfo = getGroup(groupId);
final GroupInfoV2 groupInfoV2;
if (groupInfo instanceof GroupInfoV1) {
// Received a v2 group message for a v1 group, we need to locally migrate the group
}
}
GroupId groupId = GroupUtils.getGroupId(message.getGroupContext().get());
- GroupInfo group = account.getGroupStore().getGroup(groupId);
+ GroupInfo group = getGroup(groupId);
if (group != null && group.isBlocked()) {
return true;
}
GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey
) throws IOException {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
- SignalServiceMessageReceiver receiver = getOrCreateMessageReceiver();
File outputFile = getGroupAvatarFile(groupId);
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
File tmpFile = IOUtils.createTempFile();
tmpFile.deleteOnExit();
- try (InputStream input = receiver.retrieveGroupsV2ProfileAvatar(cdnKey,
+ try (InputStream input = messageReceiver.retrieveGroupsV2ProfileAvatar(cdnKey,
tmpFile,
ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
byte[] encryptedData = IOUtils.readFully(input);
SignalServiceAddress address, String avatarPath, ProfileKey profileKey
) throws IOException {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
- SignalServiceMessageReceiver receiver = getOrCreateMessageReceiver();
File outputFile = getProfileAvatarFile(address);
File tmpFile = IOUtils.createTempFile();
- try (InputStream input = receiver.retrieveProfileAvatar(avatarPath,
+ try (InputStream input = messageReceiver.retrieveProfileAvatar(avatarPath,
tmpFile,
profileKey,
ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
}
}
- final SignalServiceMessageReceiver messageReceiver = getOrCreateMessageReceiver();
-
File tmpFile = IOUtils.createTempFile();
try (InputStream input = messageReceiver.retrieveAttachment(pointer,
tmpFile,
private InputStream retrieveAttachmentAsStream(
SignalServiceAttachmentPointer pointer, File tmpFile
) throws IOException, InvalidMessageException, MissingConfigurationException {
- final SignalServiceMessageReceiver messageReceiver = getOrCreateMessageReceiver();
return messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE);
}
try {
try (OutputStream fos = new FileOutputStream(groupsFile)) {
DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos);
- for (GroupInfo record : account.getGroupStore().getGroups()) {
+ for (GroupInfo record : getGroups()) {
if (record instanceof GroupInfoV1) {
GroupInfoV1 groupInfo = (GroupInfoV1) record;
out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
}
}
List<byte[]> groupIds = new ArrayList<>();
- for (GroupInfo record : account.getGroupStore().getGroups()) {
+ for (GroupInfo record : getGroups()) {
if (record.isBlocked()) {
groupIds.add(record.getGroupId().serialize());
}
}
public GroupInfo getGroup(GroupId groupId) {
- return account.getGroupStore().getGroup(groupId);
+ final GroupInfo group = account.getGroupStore().getGroup(groupId);
+ if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
+ final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
+ ((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams));
+ account.getGroupStore().updateGroup(group);
+ }
+ return group;
}
public List<IdentityInfo> getIdentities() {
@Override
public void close() throws IOException {
+ close(true);
+ }
+
+ void close(boolean closeAccount) throws IOException {
if (messagePipe != null) {
messagePipe.shutdown();
messagePipe = null;
unidentifiedMessagePipe = null;
}
- account.close();
+ if (closeAccount && account != null) {
+ account.close();
+ }
+ account = null;
}
public interface ReceiveMessageHandler {