dependencies {
implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_15'
- implementation 'org.bouncycastle:bcprov-jdk15on:1.67'
+ implementation 'org.bouncycastle:bcprov-jdk15on:1.68'
implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1'
implementation 'com.github.hypfvieh:dbus-java:3.2.4'
implementation 'org.slf4j:slf4j-simple:1.7.30'
import org.asamk.signal.manager.Manager;
import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException;
try {
m.setRegistrationLockPin(Optional.absent());
return 0;
- } catch (IOException e) {
+ } catch (IOException | UnauthenticatedResponseException e) {
System.err.println("Remove pin error: " + e.getMessage());
return 3;
}
import org.asamk.signal.manager.Manager;
import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException;
String registrationLockPin = ns.getString("registrationLockPin");
m.setRegistrationLockPin(Optional.of(registrationLockPin));
return 0;
- } catch (IOException e) {
+ } catch (IOException | UnauthenticatedResponseException e) {
System.err.println("Set pin error: " + e.getMessage());
return 3;
}
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
+import org.whispersystems.signalservice.api.KeyBackupServicePinException;
+import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.internal.push.LockedException;
import java.io.IOException;
System.err.println("Verification failed! This number is locked with a pin. Hours remaining until reset: "
+ (e.getTimeRemaining() / 1000 / 60 / 60));
System.err.println("Use '--pin PIN_CODE' to specify the registration lock PIN");
+ return 1;
+ } catch (KeyBackupServicePinException e) {
+ System.err.println("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining());
+ return 1;
+ } catch (KeyBackupSystemNoDataException e) {
+ System.err.println("Verification failed! No KBS data.");
return 3;
} catch (IOException e) {
System.err.println("Verify error: " + e.getMessage());
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.util.Medium;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.KbsPinData;
+import org.whispersystems.signalservice.api.KeyBackupService;
+import org.whispersystems.signalservice.api.KeyBackupServicePinException;
+import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
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.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
+import org.whispersystems.signalservice.internal.push.LockedException;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
+import java.security.KeyStore;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.Arrays;
private final SignalServiceConfiguration serviceConfiguration;
private final String userAgent;
+
+ // TODO make configurable
private final boolean discoverableByPhoneNumber = true;
private final boolean unrestrictedUnidentifiedAccess = false;
private final UnidentifiedAccessHelper unidentifiedAccessHelper;
private final ProfileHelper profileHelper;
private final GroupHelper groupHelper;
+ private PinHelper pinHelper;
Manager(
SignalAccount account,
this.userAgent = userAgent;
this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create(
serviceConfiguration)) : null;
- this.accountManager = createSignalServiceAccountManager();
- this.groupsV2Api = accountManager.getGroupsV2Api();
+ createSignalServiceAccountManager();
this.account.setResolver(this::resolveSignalServiceAddress);
return account.getSelfAddress();
}
- private SignalServiceAccountManager createSignalServiceAccountManager() {
- return new SignalServiceAccountManager(serviceConfiguration,
+ private void createSignalServiceAccountManager() {
+ this.accountManager = new SignalServiceAccountManager(serviceConfiguration,
new DynamicCredentialsProvider(account.getUuid(),
account.getUsername(),
account.getPassword(),
userAgent,
groupsV2Operations,
timer);
+ this.groupsV2Api = accountManager.getGroupsV2Api();
+ this.pinHelper = new PinHelper(createKeyBackupService());
+ }
+
+ private KeyBackupService createKeyBackupService() {
+ KeyStore keyStore = ServiceConfig.getIasKeyStore();
+
+ return accountManager.getKeyBackupService(keyStore,
+ ServiceConfig.KEY_BACKUP_ENCLAVE_NAME,
+ ServiceConfig.KEY_BACKUP_SERVICE_ID,
+ ServiceConfig.KEY_BACKUP_MRENCLAVE,
+ 10);
}
private IdentityKeyPair getIdentityKeyPair() {
// Resetting UUID, because registering doesn't work otherwise
account.setUuid(null);
- accountManager = createSignalServiceAccountManager();
- this.groupsV2Api = accountManager.getGroupsV2Api();
+ createSignalServiceAccountManager();
if (voiceVerification) {
accountManager.requestVoiceVerificationCode(Locale.getDefault(),
accountManager.setAccountAttributes(account.getSignalingKey(),
account.getSignalProtocolStore().getLocalRegistrationId(),
true,
- account.getRegistrationLockPin(),
- account.getRegistrationLock(),
+ // set legacy pin only if no KBS master key is set
+ account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null,
+ account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(),
unrestrictedUnidentifiedAccess,
capabilities,
}
}
- public void verifyAccount(String verificationCode, String pin) throws IOException {
+ public void verifyAccount(
+ String verificationCode,
+ String pin
+ ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException {
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);
+ VerifyAccountResponse response;
+ try {
+ response = verifyAccountWithCode(verificationCode, pin, null);
+ } catch (LockedException e) {
+ if (pin == null) {
+ throw e;
+ }
+
+ KbsPinData registrationLockData = pinHelper.getRegistrationLockData(pin, e);
+ if (registrationLockData == null) {
+ throw e;
+ }
+
+ String registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
+ try {
+ response = verifyAccountWithCode(verificationCode, null, registrationLock);
+ } catch (LockedException _e) {
+ throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
+ }
+ account.setPinMasterKey(registrationLockData.getMasterKey());
+ }
- 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.setUuid(UuidUtil.parseOrNull(response.getUuid()));
account.setRegistrationLockPin(pin);
account.getSignalProtocolStore()
.saveIdentity(account.getSelfAddress(),
account.save();
}
- public void setRegistrationLockPin(Optional<String> pin) throws IOException {
+ private VerifyAccountResponse verifyAccountWithCode(
+ final String verificationCode, final String legacyPin, final String registrationLock
+ ) throws IOException {
+ return accountManager.verifyAccountWithCode(verificationCode,
+ account.getSignalingKey(),
+ account.getSignalProtocolStore().getLocalRegistrationId(),
+ true,
+ legacyPin,
+ registrationLock,
+ unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(),
+ unrestrictedUnidentifiedAccess,
+ capabilities,
+ discoverableByPhoneNumber);
+ }
+
+ 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();
}
package org.asamk.signal.manager;
+import org.bouncycastle.util.encoders.Hex;
import org.signal.zkgroup.ServerPublicParams;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
-import org.whispersystems.util.Base64;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
+import java.util.Base64;
import java.util.List;
import java.util.Map;
public class ServiceConfig {
- final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF";
+ final static byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
+ .decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
final static int PREKEY_MINIMUM_COUNT = 20;
final static int PREKEY_BATCH_SIZE = 100;
final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
final static String CDS_MRENCLAVE = "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15";
+ final static String KEY_BACKUP_ENCLAVE_NAME = "fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe";
+ final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode(
+ "fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe");
+ final static String KEY_BACKUP_MRENCLAVE = "a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87";
+
private final static String URL = "https://textsecure-service.whispersystems.org";
private final static String CDN_URL = "https://cdn.signal.org";
private final static String CDN2_URL = "https://cdn2.signal.org";
private final static Optional<Dns> dns = Optional.absent();
- private final static String zkGroupServerPublicParamsHex = "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=";
- private final static byte[] zkGroupServerPublicParams;
+ private final static byte[] zkGroupServerPublicParams = Base64.getDecoder()
+ .decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=");
static final AccountAttributes.Capabilities capabilities;
static {
- try {
- zkGroupServerPublicParams = Base64.decode(zkGroupServerPublicParamsHex);
- } catch (IOException e) {
- throw new AssertionError(e);
- }
-
boolean zkGroupAvailable;
try {
new ServerPublicParams(zkGroupServerPublicParams);
static ECPublicKey getUnidentifiedSenderTrustRoot() {
try {
- return Curve.decodePoint(Base64.decode(UNIDENTIFIED_SENDER_TRUST_ROOT), 0);
- } catch (InvalidKeyException | IOException e) {
+ return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
+ } catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
--- /dev/null
+package org.asamk.signal.manager.helper;
+
+import org.asamk.signal.manager.util.PinHashing;
+import org.whispersystems.signalservice.api.KbsPinData;
+import org.whispersystems.signalservice.api.KeyBackupService;
+import org.whispersystems.signalservice.api.KeyBackupServicePinException;
+import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
+import org.whispersystems.signalservice.api.kbs.HashedPin;
+import org.whispersystems.signalservice.api.kbs.MasterKey;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
+import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
+import org.whispersystems.signalservice.internal.push.LockedException;
+
+import java.io.IOException;
+
+public class PinHelper {
+
+ private final KeyBackupService keyBackupService;
+
+ public PinHelper(final KeyBackupService keyBackupService) {
+ this.keyBackupService = keyBackupService;
+ }
+
+ public void setRegistrationLockPin(
+ String pin, MasterKey masterKey
+ ) throws IOException, UnauthenticatedResponseException {
+ final KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
+ final HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession);
+
+ pinChangeSession.setPin(hashedPin, masterKey);
+ pinChangeSession.enableRegistrationLock(masterKey);
+ }
+
+ public void removeRegistrationLockPin() throws IOException, UnauthenticatedResponseException {
+ final KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
+ pinChangeSession.removePin();
+ }
+
+ public KbsPinData getRegistrationLockData(
+ String pin,
+ LockedException e
+ ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException {
+ String basicStorageCredentials = e.getBasicStorageCredentials();
+ if (basicStorageCredentials == null) {
+ return null;
+ }
+
+ return getRegistrationLockData(pin, basicStorageCredentials);
+ }
+
+ private KbsPinData getRegistrationLockData(
+ String pin,
+ String basicStorageCredentials
+ ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException {
+ TokenResponse tokenResponse = keyBackupService.getToken(basicStorageCredentials);
+ if (tokenResponse == null || tokenResponse.getTries() == 0) {
+ throw new IOException("KBS Account locked");
+ }
+
+ KbsPinData registrationLockData = restoreMasterKey(pin, basicStorageCredentials, tokenResponse);
+ if (registrationLockData == null) {
+ throw new AssertionError("Failed to restore master key");
+ }
+ return registrationLockData;
+ }
+
+ private KbsPinData restoreMasterKey(
+ String pin, String basicStorageCredentials, TokenResponse tokenResponse
+ ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException {
+ if (pin == null) return null;
+
+ if (basicStorageCredentials == null) {
+ throw new AssertionError("Cannot restore KBS key, no storage credentials supplied");
+ }
+
+ KeyBackupService.RestoreSession session = keyBackupService.newRegistrationSession(basicStorageCredentials,
+ tokenResponse);
+
+ try {
+ HashedPin hashedPin = PinHashing.hashPin(pin, session);
+ KbsPinData kbsData = session.restorePin(hashedPin);
+ if (kbsData == null) {
+ throw new AssertionError("Null not expected");
+ }
+ return kbsData;
+ } catch (UnauthenticatedResponseException e) {
+ throw new IOException(e);
+ }
+ }
+}
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Medium;
import org.whispersystems.libsignal.util.Pair;
+import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64;
private boolean isMultiDevice = false;
private String password;
private String registrationLockPin;
+ private MasterKey pinMasterKey;
private String signalingKey;
private ProfileKey profileKey;
private int preKeyIdOffset;
password = Utils.getNotNullNode(rootNode, "password").asText();
JsonNode pinNode = rootNode.get("registrationLockPin");
registrationLockPin = pinNode == null || pinNode.isNull() ? null : pinNode.asText();
+ JsonNode pinMasterKeyNode = rootNode.get("pinMasterKey");
+ pinMasterKey = pinMasterKeyNode == null || pinMasterKeyNode.isNull()
+ ? null
+ : new MasterKey(Base64.decode(pinMasterKeyNode.asText()));
if (rootNode.has("signalingKey")) {
signalingKey = Utils.getNotNullNode(rootNode, "signalingKey").asText();
}
.put("isMultiDevice", isMultiDevice)
.put("password", password)
.put("registrationLockPin", registrationLockPin)
+ .put("pinMasterKey", pinMasterKey == null ? null : Base64.encodeBytes(pinMasterKey.serialize()))
.put("signalingKey", signalingKey)
.put("preKeyIdOffset", preKeyIdOffset)
.put("nextSignedPreKeyId", nextSignedPreKeyId)
return registrationLockPin;
}
- public String getRegistrationLock() {
- return null; // TODO implement KBS
- }
-
public void setRegistrationLockPin(final String registrationLockPin) {
this.registrationLockPin = registrationLockPin;
}
+ public MasterKey getPinMasterKey() {
+ return pinMasterKey;
+ }
+
+ public void setPinMasterKey(final MasterKey pinMasterKey) {
+ this.pinMasterKey = pinMasterKey;
+ }
+
public String getSignalingKey() {
return signalingKey;
}
import org.asamk.signal.util.RandomUtils;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
+import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.util.Base64;
public class KeyUtils {
return getSecretBytes(32);
}
+ public static MasterKey createMasterKey() {
+ return MasterKey.createNew(RandomUtils.getSecureRandom());
+ }
+
private static String getSecret(int size) {
byte[] secret = getSecretBytes(size);
return Base64.encodeBytes(secret);
--- /dev/null
+package org.asamk.signal.manager.util;
+
+import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
+import org.bouncycastle.crypto.params.Argon2Parameters;
+import org.whispersystems.signalservice.api.KeyBackupService;
+import org.whispersystems.signalservice.api.kbs.HashedPin;
+import org.whispersystems.signalservice.internal.registrationpin.PinHasher;
+
+public final class PinHashing {
+
+ private PinHashing() {
+ }
+
+ public static HashedPin hashPin(String pin, KeyBackupService.HashSession hashSession) {
+ final Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id).withParallelism(1)
+ .withIterations(32)
+ .withVersion(13)
+ .withMemoryAsKB(16 * 1024)
+ .withSalt(hashSession.hashSalt())
+ .build();
+
+ final Argon2BytesGenerator generator = new Argon2BytesGenerator();
+ generator.init(params);
+
+ return PinHasher.hashPin(PinHasher.normalize(pin), password -> {
+ byte[] output = new byte[64];
+ generator.generateBytes(password, output);
+ return output;
+ });
+ }
+}