Skip to content

Commit

Permalink
Expose additional key derivations to apps via new BackupKey class
Browse files Browse the repository at this point in the history
  • Loading branch information
jrose-signal committed Oct 29, 2024
1 parent 4036ec0 commit 22252be
Show file tree
Hide file tree
Showing 19 changed files with 644 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,42 @@

import org.signal.libsignal.internal.Native;

/**
* The randomly-generated user-memorized entropy used to derive the backup key, with other possible
* future uses.
*/
public class AccountEntropyPool {
/**
* Generate a new entropy pool and return the canonical string representation.
*
* <p>This pool contains log_2(36^64) = ~330 bits of cryptographic quality randomness.
*
* @return A 64 character string containing randomly chosen digits from [a-z0-9].
*/
public static String generate() {
return filterExceptions(() -> (Native.AccountEntropyPool_Generate()));
return filterExceptions(() -> Native.AccountEntropyPool_Generate());
}

/**
* Derives an SVR key from the given account entropy pool.
*
* <p>{@code accountEntropyPool} must be a **validated** account entropy pool; passing an
* arbitrary string here is considered a programmer error.
*/
public static byte[] deriveSvrKey(String accountEntropyPool) {
return Native.AccountEntropyPool_DeriveSvrKey(accountEntropyPool);
}

/**
* Derives a backup key from the given account entropy pool.
*
* <p>{@code accountEntropyPool} must be a **validated** account entropy pool; passing an
* arbitrary string here is considered a programmer error.
*
* @see BackupKey#generateRandom
*/
public static BackupKey deriveBackupKey(String accountEntropyPool) {
return filterExceptions(
() -> new BackupKey(Native.AccountEntropyPool_DeriveBackupKey(accountEntropyPool)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//

package org.signal.libsignal.messagebackup;

import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;

import java.security.SecureRandom;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.protocol.ServiceId.Aci;
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.internal.ByteArray;

/**
* The randomly-generated user-memorized entropy used to derive the backup key, with other possible
* future uses.
*/
public class BackupKey extends ByteArray {
public static final int SIZE = 32;

public BackupKey(byte[] contents) throws InvalidInputException {
super(contents, SIZE);
}

/**
* Generates a random backup key.
*
* <p>Useful for tests and for the media root backup key, which is not derived from anything else.
*
* @see AccountEntropyPool#deriveBackupKey
*/
public static BackupKey generateRandom() {
SecureRandom secureRandom = new SecureRandom();
byte[] bytes = new byte[BackupKey.SIZE];
secureRandom.nextBytes(bytes);
return filterExceptions(() -> new BackupKey(bytes));
}

/** Derives the backup ID to use given the current device's ACI. */
public byte[] deriveBackupId(Aci aci) {
return Native.BackupKey_DeriveBackupId(
this.getInternalContentsForJNI(), aci.toServiceIdFixedWidthBinary());
}

/** Derives the backup EC key to use given the current device's ACI. */
public ECPrivateKey deriveEcKey(Aci aci) {
return new ECPrivateKey(
Native.BackupKey_DeriveEcKey(
this.getInternalContentsForJNI(), aci.toServiceIdFixedWidthBinary()));
}

/** Derives the AES key used for encrypted fields in local backup metadata. */
public byte[] deriveLocalBackupMetadataKey() {
return Native.BackupKey_DeriveLocalBackupMetadataKey(this.getInternalContentsForJNI());
}

/** Derives the ID for uploading media with the name {@code mediaName}. */
public byte[] deriveMediaId(String mediaName) {
return Native.BackupKey_DeriveMediaId(this.getInternalContentsForJNI(), mediaName);
}

/**
* Derives the composite encryption key for uploading media with the given ID.
*
* <p>This is a concatenation of an HMAC key (32 bytes) and an AES-CBC key (also 32 bytes).
*
* <p>Throws {@link IllegalArgumentException} if the media ID is invalid.
*/
public byte[] deriveMediaEncryptionKey(byte[] mediaId) {
return Native.BackupKey_DeriveMediaEncryptionKey(this.getInternalContentsForJNI(), mediaId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

package org.signal.libsignal.messagebackup;

import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;

import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.ServiceId.Aci;
Expand Down Expand Up @@ -40,8 +42,18 @@ public MessageBackupKey(String accountEntropy, Aci aci) {
* <p>This uses AccountEntropyPool-based key derivation rules; it cannot be used to read a backup
* created from a master key.
*/
public MessageBackupKey(BackupKey backupKey, byte[] backupId) {
this.nativeHandle =
Native.MessageBackupKey_FromBackupKeyAndBackupId(
backupKey.getInternalContentsForJNI(), backupId);
}

/**
* @deprecated Use the overload that takes a strongly-typed BackupKey instead.
*/
@Deprecated
public MessageBackupKey(byte[] backupKey, byte[] backupId) {
this.nativeHandle = Native.MessageBackupKey_FromBackupKeyAndBackupId(backupKey, backupId);
this(filterExceptions(() -> new BackupKey(backupKey)), backupId);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@

package org.signal.libsignal.messagebackup;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.junit.Test;
import org.signal.libsignal.protocol.ServiceId.Aci;

public class AccountEntropyPoolTest {
@Test
Expand All @@ -23,4 +30,44 @@ public void accountEntropyStringMeetsSpecifications() {
assertTrue("Duplicate pool generated: " + pool, generatedEntropyPools.add(pool));
}
}

@Test
public void testKeyDerivations() throws Exception {
var pool = AccountEntropyPool.generate();

var svrKey = AccountEntropyPool.deriveSvrKey(pool);
assertEquals(32, svrKey.length);

var backupKey = AccountEntropyPool.deriveBackupKey(pool);
assertEquals(32, backupKey.serialize().length);

var randomKey = BackupKey.generateRandom();
assertNotEquals(backupKey, randomKey);

var aci = new Aci(new UUID(0x1111111111111111L, 0x1111111111111111L));
var otherAci = new Aci(UUID.randomUUID());

var backupId = backupKey.deriveBackupId(aci);
assertEquals(16, backupId.length);
assertFalse(Arrays.equals(backupId, randomKey.deriveBackupId(aci)));
assertFalse(Arrays.equals(backupId, backupKey.deriveBackupId(otherAci)));

var ecKey = backupKey.deriveEcKey(aci);
assertFalse(Arrays.equals(ecKey.serialize(), randomKey.deriveEcKey(aci).serialize()));
assertFalse(Arrays.equals(ecKey.serialize(), backupKey.deriveEcKey(otherAci).serialize()));

var localMetadataKey = backupKey.deriveLocalBackupMetadataKey();
assertEquals(32, localMetadataKey.length);

var mediaId = backupKey.deriveMediaId("example.jpg");
assertEquals(15, mediaId.length);

var mediaKey = backupKey.deriveMediaEncryptionKey(mediaId);
assertEquals(32 + 32, mediaKey.length);

assertThrows(
"invalid media ID",
IllegalArgumentException.class,
() -> backupKey.deriveMediaEncryptionKey(new byte[1]));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ static MessageBackupKey makeMessageBackupKeyFromBackupId() {
"20241024_SIGNAL_BACKUP_ID:".getBytes(StandardCharsets.UTF_8),
aci.toServiceIdBinary()),
16);
return new MessageBackupKey(backupKey, backupId);
try {
return new MessageBackupKey(new BackupKey(backupKey), backupId);
} catch (Exception e) {
throw new AssertionError(e);
}
}

static final String VALID_BACKUP_RESOURCE_NAME = "encryptedbackup.binproto.encrypted";
Expand Down
8 changes: 8 additions & 0 deletions java/shared/java/org/signal/libsignal/internal/Native.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ private Native() {}
*/
public static native void keepAlive(Object obj);

public static native byte[] AccountEntropyPool_DeriveBackupKey(String accountEntropy);
public static native byte[] AccountEntropyPool_DeriveSvrKey(String accountEntropy);
public static native String AccountEntropyPool_Generate();

public static native void Aes256Ctr32_Destroy(long handle);
Expand Down Expand Up @@ -171,6 +173,12 @@ private Native() {}
public static native int BackupAuthCredential_GetType(byte[] credentialBytes);
public static native byte[] BackupAuthCredential_PresentDeterministic(byte[] credentialBytes, byte[] serverParamsBytes, byte[] randomness) throws Exception;

public static native byte[] BackupKey_DeriveBackupId(byte[] backupKey, byte[] aci);
public static native long BackupKey_DeriveEcKey(byte[] backupKey, byte[] aci);
public static native byte[] BackupKey_DeriveLocalBackupMetadataKey(byte[] backupKey);
public static native byte[] BackupKey_DeriveMediaEncryptionKey(byte[] backupKey, byte[] mediaId);
public static native byte[] BackupKey_DeriveMediaId(byte[] backupKey, String mediaName);

public static native void CallLinkAuthCredentialPresentation_CheckValidContents(byte[] presentationBytes) throws Exception;
public static native byte[] CallLinkAuthCredentialPresentation_GetUserId(byte[] presentationBytes);
public static native void CallLinkAuthCredentialPresentation_Verify(byte[] presentationBytes, long now, byte[] serverParamsBytes, byte[] callLinkParamsBytes) throws Exception;
Expand Down
7 changes: 7 additions & 0 deletions node/Native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ type Serialized<T> = Buffer;
export function registerErrors(errorsModule: Record<string, unknown>): void;

export const enum LogLevel { Error = 1, Warn, Info, Debug, Trace }
export function AccountEntropyPool_DeriveBackupKey(accountEntropy: string): Buffer;
export function AccountEntropyPool_DeriveSvrKey(accountEntropy: string): Buffer;
export function AccountEntropyPool_Generate(): string;
export function Aes256GcmSiv_Decrypt(aesGcmSiv: Wrapper<Aes256GcmSiv>, ctext: Buffer, nonce: Buffer, associatedData: Buffer): Buffer;
export function Aes256GcmSiv_Encrypt(aesGcmSivObj: Wrapper<Aes256GcmSiv>, ptext: Buffer, nonce: Buffer, associatedData: Buffer): Buffer;
Expand Down Expand Up @@ -170,6 +172,11 @@ export function BackupAuthCredential_GetBackupId(credentialBytes: Buffer): Buffe
export function BackupAuthCredential_GetBackupLevel(credentialBytes: Buffer): number;
export function BackupAuthCredential_GetType(credentialBytes: Buffer): number;
export function BackupAuthCredential_PresentDeterministic(credentialBytes: Buffer, serverParamsBytes: Buffer, randomness: Buffer): Buffer;
export function BackupKey_DeriveBackupId(backupKey: Buffer, aci: Buffer): Buffer;
export function BackupKey_DeriveEcKey(backupKey: Buffer, aci: Buffer): PrivateKey;
export function BackupKey_DeriveLocalBackupMetadataKey(backupKey: Buffer): Buffer;
export function BackupKey_DeriveMediaEncryptionKey(backupKey: Buffer, mediaId: Buffer): Buffer;
export function BackupKey_DeriveMediaId(backupKey: Buffer, mediaName: string): Buffer;
export function CallLinkAuthCredentialPresentation_CheckValidContents(presentationBytes: Buffer): void;
export function CallLinkAuthCredentialPresentation_GetUserId(presentationBytes: Buffer): Serialized<UuidCiphertext>;
export function CallLinkAuthCredentialPresentation_Verify(presentationBytes: Buffer, now: Timestamp, serverParamsBytes: Buffer, callLinkParamsBytes: Buffer): void;
Expand Down
89 changes: 88 additions & 1 deletion node/ts/AccountKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
* @module AccountKeys
*/

import * as crypto from 'node:crypto';
import * as Native from '../Native';
import ByteArray from './zkgroup/internal/ByteArray';
import { Aci } from './Address';
import { PrivateKey } from './EcKeys';

/**
* The randomly-generated user-memorized entropy used to derive the backup key,
Expand All @@ -21,12 +25,95 @@ import * as Native from '../Native';
*/
export class AccountEntropyPool {
/**
* Randomly generates an Account Entropy Pool and returns the cannonical string
* Randomly generates an Account Entropy Pool and returns the canonical string
* representation of that pool.
*
* @returns cryptographically random 64 character string of characters a-z, 0-9
*/
public static generate(): string {
return Native.AccountEntropyPool_Generate();
}

/**
* Derives an SVR key from the given account entropy pool.
*
* `accountEntropyPool` must be a **validated** account entropy pool;
* passing an arbitrary string here is considered a programmer error.
*/
public static deriveSvrKey(accountEntropyPool: string): Buffer {
return Native.AccountEntropyPool_DeriveSvrKey(accountEntropyPool);
}

/**
* Derives a backup key from the given account entropy pool.
*
* `accountEntropyPool` must be a **validated** account entropy pool;
* passing an arbitrary string here is considered a programmer error.
*
* @see {@link BackupKey.generateRandom}
*/
public static deriveBackupKey(accountEntropyPool: string): BackupKey {
return new BackupKey(
Native.AccountEntropyPool_DeriveBackupKey(accountEntropyPool)
);
}
}

/** A key used for many aspects of backups. */
export class BackupKey extends ByteArray {
private readonly __type?: never;
static SIZE = 32;

constructor(contents: Buffer) {
super(contents, BackupKey.checkLength(BackupKey.SIZE));
}

/**
* Generates a random backup key.
*
* Useful for tests and for the media root backup key, which is not derived from anything else.
*
* @see {@link AccountEntropyPool.deriveBackupKey}
*/
public static generateRandom(): BackupKey {
const bytes = crypto.randomBytes(BackupKey.SIZE);
return new BackupKey(bytes);
}

/** Derives the backup ID to use given the current device's ACI. */
public deriveBackupId(aci: Aci): Buffer {
return Native.BackupKey_DeriveBackupId(
this.contents,
aci.getServiceIdFixedWidthBinary()
);
}

/** Derives the backup EC key to use given the current device's ACI. */
public deriveEcKey(aci: Aci): PrivateKey {
return PrivateKey._fromNativeHandle(
Native.BackupKey_DeriveEcKey(
this.contents,
aci.getServiceIdFixedWidthBinary()
)
);
}

/** Derives the AES key used for encrypted fields in local backup metadata. */
public deriveLocalBackupMetadataKey(): Buffer {
return Native.BackupKey_DeriveLocalBackupMetadataKey(this.contents);
}

/** Derives the ID for uploading media with the name `mediaName`. */
public deriveMediaId(mediaName: string): Buffer {
return Native.BackupKey_DeriveMediaId(this.contents, mediaName);
}

/**
* Derives the composite encryption key for uploading media with the given ID.
*
* This is a concatenation of an HMAC key (32 bytes) and an AES-CBC key (also 32 bytes).
*/
public deriveMediaEncryptionKey(mediaId: Buffer): Buffer {
return Native.BackupKey_DeriveMediaEncryptionKey(this.contents, mediaId);
}
}
Loading

0 comments on commit 22252be

Please sign in to comment.