Implement registration pin lock with KBS

Fixes #323
Fixes #268
This commit is contained in:
AsamK 2020-03-22 16:02:31 +01:00
parent a52f6a6657
commit 425626ef94
10 changed files with 254 additions and 44 deletions

View file

@ -18,7 +18,7 @@ repositories {
dependencies { dependencies {
implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_15' 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 'net.sourceforge.argparse4j:argparse4j:0.8.1'
implementation 'com.github.hypfvieh:dbus-java:3.2.4' implementation 'com.github.hypfvieh:dbus-java:3.2.4'
implementation 'org.slf4j:slf4j-simple:1.7.30' implementation 'org.slf4j:slf4j-simple:1.7.30'

View file

@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException; import java.io.IOException;
@ -23,7 +24,7 @@ public class RemovePinCommand implements LocalCommand {
try { try {
m.setRegistrationLockPin(Optional.absent()); m.setRegistrationLockPin(Optional.absent());
return 0; return 0;
} catch (IOException e) { } catch (IOException | UnauthenticatedResponseException e) {
System.err.println("Remove pin error: " + e.getMessage()); System.err.println("Remove pin error: " + e.getMessage());
return 3; return 3;
} }

View file

@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException; import java.io.IOException;
@ -26,7 +27,7 @@ public class SetPinCommand implements LocalCommand {
String registrationLockPin = ns.getString("registrationLockPin"); String registrationLockPin = ns.getString("registrationLockPin");
m.setRegistrationLockPin(Optional.of(registrationLockPin)); m.setRegistrationLockPin(Optional.of(registrationLockPin));
return 0; return 0;
} catch (IOException e) { } catch (IOException | UnauthenticatedResponseException e) {
System.err.println("Set pin error: " + e.getMessage()); System.err.println("Set pin error: " + e.getMessage());
return 3; return 3;
} }

View file

@ -4,6 +4,8 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager; 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 org.whispersystems.signalservice.internal.push.LockedException;
import java.io.IOException; import java.io.IOException;
@ -31,6 +33,12 @@ public class VerifyCommand implements LocalCommand {
System.err.println("Verification failed! This number is locked with a pin. Hours remaining until reset: " System.err.println("Verification failed! This number is locked with a pin. Hours remaining until reset: "
+ (e.getTimeRemaining() / 1000 / 60 / 60)); + (e.getTimeRemaining() / 1000 / 60 / 60));
System.err.println("Use '--pin PIN_CODE' to specify the registration lock PIN"); 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; return 3;
} catch (IOException e) { } catch (IOException e) {
System.err.println("Verify error: " + e.getMessage()); System.err.println("Verify error: " + e.getMessage());

View file

@ -26,6 +26,7 @@ import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.helper.GroupHelper; 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.ProfileHelper;
import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
@ -82,6 +83,10 @@ import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.libsignal.util.Medium; import org.whispersystems.libsignal.util.Medium;
import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional; 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.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe; import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
@ -96,6 +101,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; 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.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
@ -138,6 +144,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
import org.whispersystems.signalservice.internal.contacts.crypto.Quote; import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; 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.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
@ -160,6 +167,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.security.KeyStore;
import java.security.SignatureException; import java.security.SignatureException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -193,6 +201,8 @@ public class Manager implements Closeable {
private final SignalServiceConfiguration serviceConfiguration; private final SignalServiceConfiguration serviceConfiguration;
private final String userAgent; private final String userAgent;
// TODO make configurable
private final boolean discoverableByPhoneNumber = true; private final boolean discoverableByPhoneNumber = true;
private final boolean unrestrictedUnidentifiedAccess = false; private final boolean unrestrictedUnidentifiedAccess = false;
@ -209,6 +219,7 @@ public class Manager implements Closeable {
private final UnidentifiedAccessHelper unidentifiedAccessHelper; private final UnidentifiedAccessHelper unidentifiedAccessHelper;
private final ProfileHelper profileHelper; private final ProfileHelper profileHelper;
private final GroupHelper groupHelper; private final GroupHelper groupHelper;
private PinHelper pinHelper;
Manager( Manager(
SignalAccount account, SignalAccount account,
@ -222,8 +233,7 @@ public class Manager implements Closeable {
this.userAgent = userAgent; this.userAgent = userAgent;
this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create( this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create(
serviceConfiguration)) : null; serviceConfiguration)) : null;
this.accountManager = createSignalServiceAccountManager(); createSignalServiceAccountManager();
this.groupsV2Api = accountManager.getGroupsV2Api();
this.account.setResolver(this::resolveSignalServiceAddress); this.account.setResolver(this::resolveSignalServiceAddress);
@ -251,8 +261,8 @@ public class Manager implements Closeable {
return account.getSelfAddress(); return account.getSelfAddress();
} }
private SignalServiceAccountManager createSignalServiceAccountManager() { private void createSignalServiceAccountManager() {
return new SignalServiceAccountManager(serviceConfiguration, this.accountManager = new SignalServiceAccountManager(serviceConfiguration,
new DynamicCredentialsProvider(account.getUuid(), new DynamicCredentialsProvider(account.getUuid(),
account.getUsername(), account.getUsername(),
account.getPassword(), account.getPassword(),
@ -261,6 +271,18 @@ public class Manager implements Closeable {
userAgent, userAgent,
groupsV2Operations, groupsV2Operations,
timer); 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() { private IdentityKeyPair getIdentityKeyPair() {
@ -366,8 +388,7 @@ public class Manager implements Closeable {
// Resetting UUID, because registering doesn't work otherwise // Resetting UUID, because registering doesn't work otherwise
account.setUuid(null); account.setUuid(null);
accountManager = createSignalServiceAccountManager(); createSignalServiceAccountManager();
this.groupsV2Api = accountManager.getGroupsV2Api();
if (voiceVerification) { if (voiceVerification) {
accountManager.requestVoiceVerificationCode(Locale.getDefault(), accountManager.requestVoiceVerificationCode(Locale.getDefault(),
@ -385,8 +406,9 @@ public class Manager implements Closeable {
accountManager.setAccountAttributes(account.getSignalingKey(), accountManager.setAccountAttributes(account.getSignalingKey(),
account.getSignalProtocolStore().getLocalRegistrationId(), account.getSignalProtocolStore().getLocalRegistrationId(),
true, true,
account.getRegistrationLockPin(), // set legacy pin only if no KBS master key is set
account.getRegistrationLock(), account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null,
account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(), unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(),
unrestrictedUnidentifiedAccess, unrestrictedUnidentifiedAccess,
capabilities, capabilities,
@ -479,26 +501,39 @@ public class Manager implements Closeable {
} }
} }
public void verifyAccount(String verificationCode, String pin) throws IOException { public void verifyAccount(
String verificationCode,
String pin
) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException {
verificationCode = verificationCode.replace("-", ""); verificationCode = verificationCode.replace("-", "");
account.setSignalingKey(KeyUtils.createSignalingKey()); account.setSignalingKey(KeyUtils.createSignalingKey());
// TODO make unrestricted unidentified access configurable VerifyAccountResponse response;
VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, try {
account.getSignalingKey(), response = verifyAccountWithCode(verificationCode, pin, null);
account.getSignalProtocolStore().getLocalRegistrationId(), } catch (LockedException e) {
true, if (pin == null) {
pin, throw e;
null, }
unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(),
unrestrictedUnidentifiedAccess, KbsPinData registrationLockData = pinHelper.getRegistrationLockData(pin, e);
capabilities, if (registrationLockData == null) {
discoverableByPhoneNumber); 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() // TODO response.isStorageCapable()
//accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
account.setRegistered(true); account.setRegistered(true);
account.setUuid(uuid); account.setUuid(UuidUtil.parseOrNull(response.getUuid()));
account.setRegistrationLockPin(pin); account.setRegistrationLockPin(pin);
account.getSignalProtocolStore() account.getSignalProtocolStore()
.saveIdentity(account.getSelfAddress(), .saveIdentity(account.getSelfAddress(),
@ -509,13 +544,40 @@ public class Manager implements Closeable {
account.save(); 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()) { if (pin.isPresent()) {
final MasterKey masterKey = account.getPinMasterKey() != null
? account.getPinMasterKey()
: KeyUtils.createMasterKey();
pinHelper.setRegistrationLockPin(pin.get(), masterKey);
account.setRegistrationLockPin(pin.get()); account.setRegistrationLockPin(pin.get());
throw new RuntimeException("Not implemented anymore, will be replaced with KBS"); account.setPinMasterKey(masterKey);
} else { } else {
account.setRegistrationLockPin(null); // Remove legacy registration lock
accountManager.removeRegistrationLockV1(); accountManager.removeRegistrationLockV1();
// Remove KBS Pin
pinHelper.removeRegistrationLockPin();
account.setRegistrationLockPin(null);
account.setPinMasterKey(null);
} }
account.save(); account.save();
} }

View file

@ -1,5 +1,6 @@
package org.asamk.signal.manager; package org.asamk.signal.manager;
import org.bouncycastle.util.encoders.Hex;
import org.signal.zkgroup.ServerPublicParams; import org.signal.zkgroup.ServerPublicParams;
import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.Curve;
@ -13,13 +14,13 @@ import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupSe
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl; import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
import org.whispersystems.util.Base64;
import java.io.IOException; import java.io.IOException;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.KeyStoreException; import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -28,7 +29,8 @@ import okhttp3.Interceptor;
public class ServiceConfig { 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_MINIMUM_COUNT = 20;
final static int PREKEY_BATCH_SIZE = 100; final static int PREKEY_BATCH_SIZE = 100;
final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
@ -37,6 +39,11 @@ public class ServiceConfig {
final static String CDS_MRENCLAVE = "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15"; 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 URL = "https://textsecure-service.whispersystems.org";
private final static String CDN_URL = "https://cdn.signal.org"; private final static String CDN_URL = "https://cdn.signal.org";
private final static String CDN2_URL = "https://cdn2.signal.org"; private final static String CDN2_URL = "https://cdn2.signal.org";
@ -48,18 +55,12 @@ public class ServiceConfig {
private final static Optional<Dns> dns = Optional.absent(); 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 = Base64.getDecoder()
private final static byte[] zkGroupServerPublicParams; .decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=");
static final AccountAttributes.Capabilities capabilities; static final AccountAttributes.Capabilities capabilities;
static { static {
try {
zkGroupServerPublicParams = Base64.decode(zkGroupServerPublicParamsHex);
} catch (IOException e) {
throw new AssertionError(e);
}
boolean zkGroupAvailable; boolean zkGroupAvailable;
try { try {
new ServerPublicParams(zkGroupServerPublicParams); new ServerPublicParams(zkGroupServerPublicParams);
@ -110,8 +111,8 @@ public class ServiceConfig {
static ECPublicKey getUnidentifiedSenderTrustRoot() { static ECPublicKey getUnidentifiedSenderTrustRoot() {
try { try {
return Curve.decodePoint(Base64.decode(UNIDENTIFIED_SENDER_TRUST_ROOT), 0); return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
} catch (InvalidKeyException | IOException e) { } catch (InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }

View file

@ -0,0 +1,90 @@
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);
}
}
}

View file

@ -36,6 +36,7 @@ import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Medium; import org.whispersystems.libsignal.util.Medium;
import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64; import org.whispersystems.util.Base64;
@ -66,6 +67,7 @@ public class SignalAccount implements Closeable {
private boolean isMultiDevice = false; private boolean isMultiDevice = false;
private String password; private String password;
private String registrationLockPin; private String registrationLockPin;
private MasterKey pinMasterKey;
private String signalingKey; private String signalingKey;
private ProfileKey profileKey; private ProfileKey profileKey;
private int preKeyIdOffset; private int preKeyIdOffset;
@ -217,6 +219,10 @@ public class SignalAccount implements Closeable {
password = Utils.getNotNullNode(rootNode, "password").asText(); password = Utils.getNotNullNode(rootNode, "password").asText();
JsonNode pinNode = rootNode.get("registrationLockPin"); JsonNode pinNode = rootNode.get("registrationLockPin");
registrationLockPin = pinNode == null || pinNode.isNull() ? null : pinNode.asText(); 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")) { if (rootNode.has("signalingKey")) {
signalingKey = Utils.getNotNullNode(rootNode, "signalingKey").asText(); signalingKey = Utils.getNotNullNode(rootNode, "signalingKey").asText();
} }
@ -345,6 +351,7 @@ public class SignalAccount implements Closeable {
.put("isMultiDevice", isMultiDevice) .put("isMultiDevice", isMultiDevice)
.put("password", password) .put("password", password)
.put("registrationLockPin", registrationLockPin) .put("registrationLockPin", registrationLockPin)
.put("pinMasterKey", pinMasterKey == null ? null : Base64.encodeBytes(pinMasterKey.serialize()))
.put("signalingKey", signalingKey) .put("signalingKey", signalingKey)
.put("preKeyIdOffset", preKeyIdOffset) .put("preKeyIdOffset", preKeyIdOffset)
.put("nextSignedPreKeyId", nextSignedPreKeyId) .put("nextSignedPreKeyId", nextSignedPreKeyId)
@ -456,14 +463,18 @@ public class SignalAccount implements Closeable {
return registrationLockPin; return registrationLockPin;
} }
public String getRegistrationLock() {
return null; // TODO implement KBS
}
public void setRegistrationLockPin(final String registrationLockPin) { public void setRegistrationLockPin(final String registrationLockPin) {
this.registrationLockPin = registrationLockPin; this.registrationLockPin = registrationLockPin;
} }
public MasterKey getPinMasterKey() {
return pinMasterKey;
}
public void setPinMasterKey(final MasterKey pinMasterKey) {
this.pinMasterKey = pinMasterKey;
}
public String getSignalingKey() { public String getSignalingKey() {
return signalingKey; return signalingKey;
} }

View file

@ -3,6 +3,7 @@ package org.asamk.signal.manager.util;
import org.asamk.signal.util.RandomUtils; import org.asamk.signal.util.RandomUtils;
import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.util.Base64; import org.whispersystems.util.Base64;
public class KeyUtils { public class KeyUtils {
@ -30,6 +31,10 @@ public class KeyUtils {
return getSecretBytes(32); return getSecretBytes(32);
} }
public static MasterKey createMasterKey() {
return MasterKey.createNew(RandomUtils.getSecureRandom());
}
private static String getSecret(int size) { private static String getSecret(int size) {
byte[] secret = getSecretBytes(size); byte[] secret = getSecretBytes(size);
return Base64.encodeBytes(secret); return Base64.encodeBytes(secret);

View file

@ -0,0 +1,31 @@
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;
});
}
}