diff --git a/java/client/src/test/java/org/signal/libsignal/zkgroup/integrationtests/ZkGroupTest.java b/java/client/src/test/java/org/signal/libsignal/zkgroup/integrationtests/ZkGroupTest.java index ae31cdd4f..74ab4f3dc 100644 --- a/java/client/src/test/java/org/signal/libsignal/zkgroup/integrationtests/ZkGroupTest.java +++ b/java/client/src/test/java/org/signal/libsignal/zkgroup/integrationtests/ZkGroupTest.java @@ -784,6 +784,16 @@ public final class ZkGroupTest extends SecureRandomTest { assertArrayEquals(plaintext, plaintext257); } + @Test + public void testDeriveAccessKey() throws Exception { + byte[] expectedAccessKey = Hex.fromStringCondensedAssert("5a723acee52c5ea02b92a3a360c09595"); + byte[] profileKey = new byte[32]; + Arrays.fill(profileKey, (byte)0x02); + + byte[] result = new ProfileKey(profileKey).deriveAccessKey(); + assertArrayEquals(result, expectedAccessKey); + } + private void assertByteArray(String expectedAsHex, byte[] actual) { byte[] expectedBytes = Hex.fromStringCondensedAssert(expectedAsHex); diff --git a/java/shared/java/org/signal/libsignal/internal/Native.java b/java/shared/java/org/signal/libsignal/internal/Native.java index 8c4e9a943..96865b90c 100644 --- a/java/shared/java/org/signal/libsignal/internal/Native.java +++ b/java/shared/java/org/signal/libsignal/internal/Native.java @@ -311,6 +311,7 @@ public final class Native { public static native void ProfileKeyCredentialRequest_CheckValidContents(byte[] buffer); public static native void ProfileKey_CheckValidContents(byte[] buffer); + public static native byte[] ProfileKey_DeriveAccessKey(byte[] profileKey); public static native byte[] ProfileKey_GetCommitment(byte[] profileKey, UUID uuid); public static native byte[] ProfileKey_GetProfileKeyVersion(byte[] profileKey, UUID uuid); diff --git a/java/shared/java/org/signal/libsignal/zkgroup/profiles/ProfileKey.java b/java/shared/java/org/signal/libsignal/zkgroup/profiles/ProfileKey.java index 1b58b4b99..f8d7fee19 100644 --- a/java/shared/java/org/signal/libsignal/zkgroup/profiles/ProfileKey.java +++ b/java/shared/java/org/signal/libsignal/zkgroup/profiles/ProfileKey.java @@ -38,4 +38,8 @@ public final class ProfileKey extends ByteArray { } } + public byte[] deriveAccessKey() { + return Native.ProfileKey_DeriveAccessKey(contents); + } + } diff --git a/node/Native.d.ts b/node/Native.d.ts index 258e52028..bb3d93653 100644 --- a/node/Native.d.ts +++ b/node/Native.d.ts @@ -173,6 +173,7 @@ export function ProfileKeyCredentialRequestContext_CheckValidContents(buffer: Bu export function ProfileKeyCredentialRequestContext_GetRequest(context: Serialized): Serialized; export function ProfileKeyCredentialRequest_CheckValidContents(buffer: Buffer): void; export function ProfileKey_CheckValidContents(buffer: Buffer): void; +export function ProfileKey_DeriveAccessKey(profileKey: Serialized): Buffer; export function ProfileKey_GetCommitment(profileKey: Serialized, uuid: Uuid): Serialized; export function ProfileKey_GetProfileKeyVersion(profileKey: Serialized, uuid: Uuid): Buffer; export function ProtocolAddress_DeviceId(obj: Wrapper): number; diff --git a/node/ts/test/ZKGroup-test.ts b/node/ts/test/ZKGroup-test.ts index fe8a3770e..a6f55d176 100644 --- a/node/ts/test/ZKGroup-test.ts +++ b/node/ts/test/ZKGroup-test.ts @@ -580,4 +580,12 @@ describe('ZKGroup', () => { clientSecretParams.decryptUserId(presentation.getUserId()) ); }); + + it('testDeriveProfileKey', () => { + const expectedAccessKey = hexToBuffer('5a723acee52c5ea02b92a3a360c09595'); + const profileKey = Buffer.alloc(32, 0x02); + + const result = new ProfileKey(profileKey).deriveAccessKey(); + assertArrayEquals(expectedAccessKey, result); + }); }); diff --git a/node/ts/zkgroup/profiles/ProfileKey.ts b/node/ts/zkgroup/profiles/ProfileKey.ts index a6cccfe65..1fc54dae1 100644 --- a/node/ts/zkgroup/profiles/ProfileKey.ts +++ b/node/ts/zkgroup/profiles/ProfileKey.ts @@ -28,4 +28,8 @@ export default class ProfileKey extends ByteArray { Native.ProfileKey_GetProfileKeyVersion(this.contents, fromUUID(uuid)) ); } + + deriveAccessKey(): Buffer { + return Native.ProfileKey_DeriveAccessKey(this.contents); + } } diff --git a/rust/bridge/shared/src/zkgroup.rs b/rust/bridge/shared/src/zkgroup.rs index 23d3f8f70..1f4d59a79 100644 --- a/rust/bridge/shared/src/zkgroup.rs +++ b/rust/bridge/shared/src/zkgroup.rs @@ -122,6 +122,11 @@ fn ProfileKey_GetProfileKeyVersion( serialized.try_into().expect("right length") } +#[bridge_fn] +fn ProfileKey_DeriveAccessKey(profile_key: Serialized) -> [u8; ACCESS_KEY_LEN] { + profile_key.derive_access_key() +} + #[bridge_fn] fn GroupSecretParams_GenerateDeterministic( randomness: &[u8; RANDOMNESS_LEN], diff --git a/rust/zkgroup/src/api/profiles/profile_key.rs b/rust/zkgroup/src/api/profiles/profile_key.rs index 094ce86ce..4e9ff2359 100644 --- a/rust/zkgroup/src/api/profiles/profile_key.rs +++ b/rust/zkgroup/src/api/profiles/profile_key.rs @@ -62,10 +62,10 @@ impl ProfileKey { } } - pub fn derive_access_key(&self) -> [u8; 16] { - let nonce = &[0u8; 12]; + pub fn derive_access_key(&self) -> [u8; ACCESS_KEY_LEN] { + let nonce = &[0u8; AESGCM_NONCE_LEN]; let mut cipher = Aes256GcmEncryption::new(&self.bytes, nonce, &[]).unwrap(); - let mut buf = [0u8; 16]; + let mut buf = [0u8; ACCESS_KEY_LEN]; cipher.encrypt(&mut buf[..]).unwrap(); buf } diff --git a/rust/zkgroup/src/common/constants.rs b/rust/zkgroup/src/common/constants.rs index 2940caf5f..476c88c5f 100644 --- a/rust/zkgroup/src/common/constants.rs +++ b/rust/zkgroup/src/common/constants.rs @@ -47,6 +47,7 @@ pub const UUID_CIPHERTEXT_LEN: usize = 65; pub const RANDOMNESS_LEN: usize = 32; pub const SIGNATURE_LEN: usize = 64; pub const UUID_LEN: usize = 16; +pub const ACCESS_KEY_LEN: usize = 16; /// Seconds in a 24-hour cycle (ignoring leap seconds). pub const SECONDS_PER_DAY: u64 = 86400; diff --git a/swift/Sources/LibSignalClient/zkgroup/ProfileKey.swift b/swift/Sources/LibSignalClient/zkgroup/ProfileKey.swift index b75b5dee9..c68700745 100644 --- a/swift/Sources/LibSignalClient/zkgroup/ProfileKey.swift +++ b/swift/Sources/LibSignalClient/zkgroup/ProfileKey.swift @@ -34,4 +34,14 @@ public class ProfileKey: ByteArray { } } + public func deriveAccessKey() -> [UInt8] { + return failOnError { + try withUnsafePointerToSerialized { contents in + try invokeFnReturningFixedLengthArray { + signal_profile_key_derive_access_key($0, contents) + } + } + } + } + } diff --git a/swift/Sources/SignalFfi/signal_ffi.h b/swift/Sources/SignalFfi/signal_ffi.h index 1958dbe00..b5c8408a3 100644 --- a/swift/Sources/SignalFfi/signal_ffi.h +++ b/swift/Sources/SignalFfi/signal_ffi.h @@ -96,6 +96,8 @@ SPDX-License-Identifier: AGPL-3.0-only #define SignalUUID_LEN 16 +#define SignalACCESS_KEY_LEN 16 + /** * Seconds in a 24-hour cycle (ignoring leap seconds). */ @@ -1132,6 +1134,9 @@ SignalFfiError *signal_profile_key_get_profile_key_version(uint8_t (*out)[Signal const unsigned char (*profile_key)[SignalPROFILE_KEY_LEN], const uint8_t (*uuid)[16]); +SignalFfiError *signal_profile_key_derive_access_key(uint8_t (*out)[SignalACCESS_KEY_LEN], + const unsigned char (*profile_key)[SignalPROFILE_KEY_LEN]); + SignalFfiError *signal_group_secret_params_generate_deterministic(unsigned char (*out)[SignalGROUP_SECRET_PARAMS_LEN], const uint8_t (*randomness)[SignalRANDOMNESS_LEN]); diff --git a/swift/Tests/LibSignalClientTests/ZKGroupTests.swift b/swift/Tests/LibSignalClientTests/ZKGroupTests.swift index cf9a58f4c..1b0ce3297 100644 --- a/swift/Tests/LibSignalClientTests/ZKGroupTests.swift +++ b/swift/Tests/LibSignalClientTests/ZKGroupTests.swift @@ -395,4 +395,12 @@ class ZKGroupTests: TestCaseBase { // Client XCTAssertEqual(TEST_ARRAY_16, try clientSecretParams.decryptUserId(presentation.userId)) } + + func testDeriveProfileKey() throws { + let expectedAccessKey: [UInt8] = [0x5a, 0x72, 0x3a, 0xce, 0xe5, 0x2c, 0x5e, 0xa0, 0x2b, 0x92, 0xa3, 0xa3, 0x60, 0xc0, 0x95, 0x95] + let profileKeyBytes: [UInt8] = Array(repeating: 0x02, count: 32) + + let result = try ProfileKey(contents: profileKeyBytes).deriveAccessKey() + XCTAssertEqual(expectedAccessKey, result) + } }