]> nmode's Git Repositories - signal-cli/commitdiff
Implement registration pin lock with KBS
authorAsamK <asamk@gmx.de>
Sun, 22 Mar 2020 15:02:31 +0000 (16:02 +0100)
committerAsamK <asamk@gmx.de>
Thu, 31 Dec 2020 14:41:00 +0000 (15:41 +0100)
Fixes #323
Fixes #268

build.gradle
src/main/java/org/asamk/signal/commands/RemovePinCommand.java
src/main/java/org/asamk/signal/commands/SetPinCommand.java
src/main/java/org/asamk/signal/commands/VerifyCommand.java
src/main/java/org/asamk/signal/manager/Manager.java
src/main/java/org/asamk/signal/manager/ServiceConfig.java
src/main/java/org/asamk/signal/manager/helper/PinHelper.java [new file with mode: 0644]
src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
src/main/java/org/asamk/signal/manager/util/KeyUtils.java
src/main/java/org/asamk/signal/manager/util/PinHashing.java [new file with mode: 0644]

index 1fbf5948d01b4b2e44eb55d569b6e7dcc241f7c0..1c42834b56d74c5441415c44f47487a10c5a70b3 100644 (file)
@@ -18,7 +18,7 @@ repositories {
 
 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'
index b7de5402bbb86697ec0fce3ed61dad5fe1933b06..95531249663eeaff55d1d4a8eff16f414eee3dec 100644 (file)
@@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser;
 
 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;
 
@@ -23,7 +24,7 @@ public class RemovePinCommand implements LocalCommand {
         try {
             m.setRegistrationLockPin(Optional.absent());
             return 0;
-        } catch (IOException e) {
+        } catch (IOException | UnauthenticatedResponseException e) {
             System.err.println("Remove pin error: " + e.getMessage());
             return 3;
         }
index 9351dad0d74584caa440f5e16abb901bd65e7a7f..c68ea3a989817279b29be217dabd5b88d8d436b7 100644 (file)
@@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser;
 
 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;
 
@@ -26,7 +27,7 @@ public class SetPinCommand implements LocalCommand {
             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;
         }
index b6ad100bf0b4a83f0e0aaa65dd7a9c05291fd0ad..d4b0a7cb3169f0bfc806c9a1fabafbfac9169f52 100644 (file)
@@ -4,6 +4,8 @@ import net.sourceforge.argparse4j.inf.Namespace;
 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;
@@ -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: "
                     + (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());
index 25b2ec54080e5e3d753c4b9c565715860ed04e10..7ad7b88803ffe3a2fc55f7f6105beac8b2e57833 100644 (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.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;
@@ -82,6 +83,10 @@ 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.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;
@@ -96,6 +101,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException
 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;
@@ -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.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;
@@ -160,6 +167,7 @@ import java.nio.charset.StandardCharsets;
 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;
@@ -193,6 +201,8 @@ public class Manager implements Closeable {
 
     private final SignalServiceConfiguration serviceConfiguration;
     private final String userAgent;
+
+    // TODO make configurable
     private final boolean discoverableByPhoneNumber = true;
     private final boolean unrestrictedUnidentifiedAccess = false;
 
@@ -209,6 +219,7 @@ public class Manager implements Closeable {
     private final UnidentifiedAccessHelper unidentifiedAccessHelper;
     private final ProfileHelper profileHelper;
     private final GroupHelper groupHelper;
+    private PinHelper pinHelper;
 
     Manager(
             SignalAccount account,
@@ -222,8 +233,7 @@ public class Manager implements Closeable {
         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);
 
@@ -251,8 +261,8 @@ public class Manager implements Closeable {
         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(),
@@ -261,6 +271,18 @@ public class Manager implements Closeable {
                 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() {
@@ -366,8 +388,7 @@ public class Manager implements Closeable {
 
         // Resetting UUID, because registering doesn't work otherwise
         account.setUuid(null);
-        accountManager = createSignalServiceAccountManager();
-        this.groupsV2Api = accountManager.getGroupsV2Api();
+        createSignalServiceAccountManager();
 
         if (voiceVerification) {
             accountManager.requestVoiceVerificationCode(Locale.getDefault(),
@@ -385,8 +406,9 @@ public class Manager implements Closeable {
         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,
@@ -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("-", "");
         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(),
@@ -509,13 +544,40 @@ public class Manager implements Closeable {
         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();
     }
index 0ccd826a36662464b4174a0858ad21f7d3f50e12..55935ced840266dbf0e159561561d9f9336889d6 100644 (file)
@@ -1,5 +1,6 @@
 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;
@@ -13,13 +14,13 @@ import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupSe
 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;
 
@@ -28,7 +29,8 @@ import okhttp3.Interceptor;
 
 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;
@@ -37,6 +39,11 @@ public class ServiceConfig {
 
     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";
@@ -48,18 +55,12 @@ public class ServiceConfig {
 
     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);
@@ -110,8 +111,8 @@ public class ServiceConfig {
 
     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);
         }
     }
diff --git a/src/main/java/org/asamk/signal/manager/helper/PinHelper.java b/src/main/java/org/asamk/signal/manager/helper/PinHelper.java
new file mode 100644 (file)
index 0000000..47ee6b4
--- /dev/null
@@ -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);
+        }
+    }
+}
index c787471fc44f1c3571fa47ae9b29d04cc41f6a08..1c35b1fb65b14fc50190fb304534f9d6cc1ced96 100644 (file)
@@ -36,6 +36,7 @@ import org.whispersystems.libsignal.state.PreKeyRecord;
 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;
 
@@ -66,6 +67,7 @@ public class SignalAccount implements Closeable {
     private boolean isMultiDevice = false;
     private String password;
     private String registrationLockPin;
+    private MasterKey pinMasterKey;
     private String signalingKey;
     private ProfileKey profileKey;
     private int preKeyIdOffset;
@@ -217,6 +219,10 @@ public class SignalAccount implements Closeable {
         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();
         }
@@ -345,6 +351,7 @@ public class SignalAccount implements Closeable {
                 .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)
@@ -456,14 +463,18 @@ public class SignalAccount implements Closeable {
         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;
     }
index 2b4bc3716192c267abec1369e465cf3d454f3c2c..3f9ec08fd55d92f91d7a99a4dc37605d917cf8c1 100644 (file)
@@ -3,6 +3,7 @@ package org.asamk.signal.manager.util;
 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 {
@@ -30,6 +31,10 @@ 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);
diff --git a/src/main/java/org/asamk/signal/manager/util/PinHashing.java b/src/main/java/org/asamk/signal/manager/util/PinHashing.java
new file mode 100644 (file)
index 0000000..2adf814
--- /dev/null
@@ -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;
+        });
+    }
+}