From 4e22da3293c59bf6d9c226312cd41d18b63b5217 Mon Sep 17 00:00:00 2001 From: Jordan Rose Date: Wed, 23 Oct 2024 13:03:53 -0700 Subject: [PATCH] Add BackupCredentialType to BackupAuthCredential Rename BackupLevel::{Messages, Media} to {Free, Paid}, then add BackupCredentialType::{Messages, Media}. This is a breaking change for apps and the server, both because of the new names, and because the credential attributes have changed (both what's in a serialized credential, and what's included in the signature). --- .../libsignal/zkgroup/BackupAuthTest.java | 24 +++- .../org/signal/libsignal/internal/Native.java | 4 +- .../zkgroup/backups/BackupAuthCredential.java | 5 + .../BackupAuthCredentialPresentation.java | 5 + .../backups/BackupAuthCredentialRequest.java | 11 +- .../zkgroup/backups/BackupCredentialType.java | 32 +++++ .../zkgroup/backups/BackupLevel.java | 23 ++-- node/Native.d.ts | 4 +- node/ts/test/ZKGroup-test.ts | 18 ++- .../zkgroup/backups/BackupAuthCredential.ts | 9 ++ .../BackupAuthCredentialPresentation.ts | 11 ++ .../backups/BackupAuthCredentialRequest.ts | 5 + .../zkgroup/backups/BackupCredentialType.ts | 11 ++ node/ts/zkgroup/backups/BackupLevel.ts | 4 +- rust/bridge/shared/src/zkgroup.rs | 19 ++- rust/zkgroup/src/api/backups.rs | 3 +- .../src/api/backups/auth_credential.rs | 122 +++++++++++++----- rust/zkgroup/tests/backup_auth_flow.rs | 5 +- .../zkgroup/BackupAuthCredential.swift | 14 ++ .../zkgroup/BackupAuthCredentialRequest.swift | 8 +- .../LibSignalClient/zkgroup/BackupLevel.swift | 7 +- swift/Sources/SignalFfi/signal_ffi.h | 4 +- .../LibSignalClientTests/ZKGroupTests.swift | 29 +++-- 23 files changed, 297 insertions(+), 80 deletions(-) create mode 100644 java/shared/java/org/signal/libsignal/zkgroup/backups/BackupCredentialType.java create mode 100644 node/ts/zkgroup/backups/BackupCredentialType.ts diff --git a/java/server/src/test/java/org/signal/libsignal/zkgroup/BackupAuthTest.java b/java/server/src/test/java/org/signal/libsignal/zkgroup/BackupAuthTest.java index da8c7c8cf..cc1f8a0d1 100644 --- a/java/server/src/test/java/org/signal/libsignal/zkgroup/BackupAuthTest.java +++ b/java/server/src/test/java/org/signal/libsignal/zkgroup/BackupAuthTest.java @@ -23,6 +23,7 @@ import org.signal.libsignal.zkgroup.backups.*; */ public final class BackupAuthTest extends SecureRandomTest { + // Chosen randomly private static final UUID TEST_USER_ID = UUID.fromString("e74beed0-e70f-4cfd-abbb-7e3eb333bbac"); private static final byte[] BACKUP_KEY = @@ -41,12 +42,14 @@ public final class BackupAuthTest extends SecureRandomTest { Hex.fromStringCondensedAssert( "8e3f24cb0a7e7614c7b4ab04ba8a145f108c53c4b10a096aa4503ae1e0c9f661"); + // These are expectations; if the contents of a credential or derivation of a backup ID changes, + // they will need to be updated. private static final byte[] SERIALIZED_BACKUP_ID = - Hex.fromStringCondensedAssert("e3926f11ddd143e6dd0f20bfcb08349e"); + Hex.fromStringCondensedAssert("52b899ef83125719d3daa9a4edcc0aff"); private static final byte[] SERIALIZED_REQUEST_CREDENTIAL = Base64.getDecoder() .decode( - "AISCxQa8OsFqphsQPxqtzJk5+jndpE3SJG6bfazQB3994Aersq2yNRgcARBoedBeoEfKIXdty6X7l6+TiPFAqDvojRSO8xaZOpKJOvWSDJIGn6EeMl2jOjx+IQg8d8M0AQ==" + "AISCxQa8OsFqphsQPxqtzJk5+jndpE3SJG6bfazQB3999KZFdtnpcIjx/0DPYbLJRbLQmz1ZXnueq5HPo9ewpEjojRSO8xaZOpKJOvWSDJIGn6EeMl2jOjx+IQg8d8M0AQ==" .getBytes(StandardCharsets.UTF_8)); @Test @@ -60,7 +63,8 @@ public final class BackupAuthTest extends SecureRandomTest { GenericServerSecretParams.generate(createSecureRandom(SERVER_SECRET_RANDOM)); Instant timestamp = Instant.now().truncatedTo(ChronoUnit.DAYS); BackupAuthCredentialResponse response = - request.issueCredential(timestamp, BackupLevel.MESSAGES, serverSecretParams); + request.issueCredential( + timestamp, BackupLevel.FREE, BackupCredentialType.MESSAGES, serverSecretParams); BackupAuthCredential credential = context.receiveResponse(response, timestamp, serverSecretParams.getPublicParams()); @@ -68,12 +72,14 @@ public final class BackupAuthTest extends SecureRandomTest { Assert.assertArrayEquals( SERIALIZED_BACKUP_ID, credential.present(serverSecretParams.getPublicParams()).getBackupId()); - Assert.assertEquals(BackupLevel.MESSAGES, credential.getBackupLevel()); + Assert.assertEquals(BackupLevel.FREE, credential.getBackupLevel()); + Assert.assertEquals(BackupCredentialType.MESSAGES, credential.getType()); } @Test public void testBackupAuthCredentialIntegration() throws VerificationFailedException { - final BackupLevel backupLevel = BackupLevel.MESSAGES; + final BackupLevel backupLevel = BackupLevel.FREE; + final BackupCredentialType credentialType = BackupCredentialType.MESSAGES; // SERVER // Generate keys @@ -91,13 +97,18 @@ public final class BackupAuthTest extends SecureRandomTest { Instant timestamp = Instant.now().truncatedTo(ChronoUnit.DAYS); BackupAuthCredentialResponse response = request.issueCredential( - timestamp, backupLevel, serverSecretParams, createSecureRandom(TEST_ARRAY_32_1)); + timestamp, + backupLevel, + credentialType, + serverSecretParams, + createSecureRandom(TEST_ARRAY_32_1)); // CLIENT // Gets stored credential BackupAuthCredential credential = context.receiveResponse(response, timestamp, serverPublicParams); Assert.assertEquals(backupLevel, credential.getBackupLevel()); + Assert.assertEquals(credentialType, credential.getType()); // CLIENT // Generates a presentation @@ -110,6 +121,7 @@ public final class BackupAuthTest extends SecureRandomTest { presentation.verify(timestamp.plus(1, ChronoUnit.DAYS), serverSecretParams); Assert.assertArrayEquals(credential.getBackupId(), presentation.getBackupId()); Assert.assertEquals(backupLevel, presentation.getBackupLevel()); + Assert.assertEquals(credentialType, presentation.getType()); Assert.assertThrows( "Credential should be expired after 2 days", diff --git a/java/shared/java/org/signal/libsignal/internal/Native.java b/java/shared/java/org/signal/libsignal/internal/Native.java index ea20ed02d..5f793a36b 100644 --- a/java/shared/java/org/signal/libsignal/internal/Native.java +++ b/java/shared/java/org/signal/libsignal/internal/Native.java @@ -152,6 +152,7 @@ public final class Native { public static native void BackupAuthCredentialPresentation_CheckValidContents(byte[] presentationBytes) throws Exception; public static native byte[] BackupAuthCredentialPresentation_GetBackupId(byte[] presentationBytes); public static native int BackupAuthCredentialPresentation_GetBackupLevel(byte[] presentationBytes); + public static native int BackupAuthCredentialPresentation_GetType(byte[] presentationBytes); public static native void BackupAuthCredentialPresentation_Verify(byte[] presentationBytes, long now, byte[] serverParamsBytes) throws Exception; public static native void BackupAuthCredentialRequestContext_CheckValidContents(byte[] contextBytes) throws Exception; @@ -160,13 +161,14 @@ public final class Native { public static native byte[] BackupAuthCredentialRequestContext_ReceiveResponse(byte[] contextBytes, byte[] responseBytes, long expectedRedemptionTime, byte[] paramsBytes) throws Exception; public static native void BackupAuthCredentialRequest_CheckValidContents(byte[] requestBytes) throws Exception; - public static native byte[] BackupAuthCredentialRequest_IssueDeterministic(byte[] requestBytes, long redemptionTime, int backupLevel, byte[] paramsBytes, byte[] randomness); + public static native byte[] BackupAuthCredentialRequest_IssueDeterministic(byte[] requestBytes, long redemptionTime, int backupLevel, int credentialType, byte[] paramsBytes, byte[] randomness); public static native void BackupAuthCredentialResponse_CheckValidContents(byte[] responseBytes) throws Exception; public static native void BackupAuthCredential_CheckValidContents(byte[] paramsBytes) throws Exception; public static native byte[] BackupAuthCredential_GetBackupId(byte[] credentialBytes); public static native int BackupAuthCredential_GetBackupLevel(byte[] credentialBytes); + 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 void CallLinkAuthCredentialPresentation_CheckValidContents(byte[] presentationBytes) throws Exception; diff --git a/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupAuthCredential.java b/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupAuthCredential.java index 143786c06..a58803e58 100644 --- a/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupAuthCredential.java +++ b/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupAuthCredential.java @@ -53,4 +53,9 @@ public final class BackupAuthCredential extends ByteArray { return BackupLevel.fromValue( Native.BackupAuthCredential_GetBackupLevel(getInternalContentsForJNI())); } + + public BackupCredentialType getType() { + return BackupCredentialType.fromValue( + Native.BackupAuthCredential_GetType(getInternalContentsForJNI())); + } } diff --git a/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupAuthCredentialPresentation.java b/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupAuthCredentialPresentation.java index 03517c966..692ac41f1 100644 --- a/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupAuthCredentialPresentation.java +++ b/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupAuthCredentialPresentation.java @@ -46,4 +46,9 @@ public final class BackupAuthCredentialPresentation extends ByteArray { return BackupLevel.fromValue( Native.BackupAuthCredentialPresentation_GetBackupLevel(getInternalContentsForJNI())); } + + public BackupCredentialType getType() { + return BackupCredentialType.fromValue( + Native.BackupAuthCredentialPresentation_GetType(getInternalContentsForJNI())); + } } diff --git a/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupAuthCredentialRequest.java b/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupAuthCredentialRequest.java index a3ef559a9..acf651bb1 100644 --- a/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupAuthCredentialRequest.java +++ b/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupAuthCredentialRequest.java @@ -30,11 +30,15 @@ public final class BackupAuthCredentialRequest extends ByteArray { * @param timestamp Must be a round number of days. Use {@link Instant#truncatedTo} to ensure * this. * @param backupLevel The {@link BackupLevel} that this credential is authorized for + * @param type The type of upload the credential will be used for * @param params The params that will be used by the verifying server to verify this credential. */ public BackupAuthCredentialResponse issueCredential( - Instant timestamp, BackupLevel backupLevel, GenericServerSecretParams params) { - return issueCredential(timestamp, backupLevel, params, new SecureRandom()); + Instant timestamp, + BackupLevel backupLevel, + BackupCredentialType type, + GenericServerSecretParams params) { + return issueCredential(timestamp, backupLevel, type, params, new SecureRandom()); } /** @@ -46,12 +50,14 @@ public final class BackupAuthCredentialRequest extends ByteArray { * @param timestamp Must be a round number of days. Use {@link Instant#truncatedTo} to ensure * this. * @param backupLevel The {@link BackupLevel} that this credential is authorized for + * @param type The type of upload the credential will be used for * @param params The params that will be used by the verifying server to verify this credential. * @param secureRandom Used to hide the server's secrets and make the issued credential unique. */ public BackupAuthCredentialResponse issueCredential( Instant timestamp, BackupLevel backupLevel, + BackupCredentialType type, GenericServerSecretParams params, SecureRandom secureRandom) { byte[] random = new byte[RANDOM_LENGTH]; @@ -62,6 +68,7 @@ public final class BackupAuthCredentialRequest extends ByteArray { getInternalContentsForJNI(), timestamp.getEpochSecond(), backupLevel.getValue(), + type.getValue(), params.getInternalContentsForJNI(), random); diff --git a/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupCredentialType.java b/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupCredentialType.java new file mode 100644 index 000000000..0638b2bdc --- /dev/null +++ b/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupCredentialType.java @@ -0,0 +1,32 @@ +// +// Copyright 2024 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +package org.signal.libsignal.zkgroup.backups; + +public enum BackupCredentialType { + // This must match the Rust version of the enum. + MESSAGES(1), + MEDIA(2); + + private final int value; + + BackupCredentialType(int value) { + this.value = value; + } + + int getValue() { + return this.value; + } + + public static BackupCredentialType fromValue(int value) { + // A linear scan is simpler than a hash lookup for a set of values this small. + for (final var credentialType : BackupCredentialType.values()) { + if (credentialType.getValue() == value) { + return credentialType; + } + } + throw new IllegalArgumentException("Invalid backup credential type: " + value); + } +} diff --git a/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupLevel.java b/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupLevel.java index b7bad3db4..3e3b3bfcf 100644 --- a/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupLevel.java +++ b/java/shared/java/org/signal/libsignal/zkgroup/backups/BackupLevel.java @@ -5,19 +5,10 @@ package org.signal.libsignal.zkgroup.backups; -import java.util.Arrays; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - public enum BackupLevel { // This must match the Rust version of the enum. - MESSAGES(200), - MEDIA(201); - - private static final Map LOOKUP = - Arrays.stream(BackupLevel.values()) - .collect(Collectors.toMap(BackupLevel::getValue, Function.identity())); + FREE(200), + PAID(201); private final int value; @@ -30,10 +21,12 @@ public enum BackupLevel { } public static BackupLevel fromValue(int value) { - BackupLevel backupLevel = LOOKUP.get(value); - if (backupLevel == null) { - throw new IllegalArgumentException("Invalid backup level: " + value); + // A linear scan is simpler than a hash lookup for a set of values this small. + for (final var backupLevel : BackupLevel.values()) { + if (backupLevel.getValue() == value) { + return backupLevel; + } } - return backupLevel; + throw new IllegalArgumentException("Invalid backup level: " + value); } } diff --git a/node/Native.d.ts b/node/Native.d.ts index d1ac7295d..83b17639f 100644 --- a/node/Native.d.ts +++ b/node/Native.d.ts @@ -156,17 +156,19 @@ export function AuthCredentialWithPni_CheckValidContents(bytes: Buffer): void; export function BackupAuthCredentialPresentation_CheckValidContents(presentationBytes: Buffer): void; export function BackupAuthCredentialPresentation_GetBackupId(presentationBytes: Buffer): Buffer; export function BackupAuthCredentialPresentation_GetBackupLevel(presentationBytes: Buffer): number; +export function BackupAuthCredentialPresentation_GetType(presentationBytes: Buffer): number; export function BackupAuthCredentialPresentation_Verify(presentationBytes: Buffer, now: Timestamp, serverParamsBytes: Buffer): void; export function BackupAuthCredentialRequestContext_CheckValidContents(contextBytes: Buffer): void; export function BackupAuthCredentialRequestContext_GetRequest(contextBytes: Buffer): Buffer; export function BackupAuthCredentialRequestContext_New(backupKey: Buffer, uuid: Uuid): Buffer; export function BackupAuthCredentialRequestContext_ReceiveResponse(contextBytes: Buffer, responseBytes: Buffer, expectedRedemptionTime: Timestamp, paramsBytes: Buffer): Buffer; export function BackupAuthCredentialRequest_CheckValidContents(requestBytes: Buffer): void; -export function BackupAuthCredentialRequest_IssueDeterministic(requestBytes: Buffer, redemptionTime: Timestamp, backupLevel: number, paramsBytes: Buffer, randomness: Buffer): Buffer; +export function BackupAuthCredentialRequest_IssueDeterministic(requestBytes: Buffer, redemptionTime: Timestamp, backupLevel: number, credentialType: number, paramsBytes: Buffer, randomness: Buffer): Buffer; export function BackupAuthCredentialResponse_CheckValidContents(responseBytes: Buffer): void; export function BackupAuthCredential_CheckValidContents(paramsBytes: Buffer): void; export function BackupAuthCredential_GetBackupId(credentialBytes: Buffer): Buffer; 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 CallLinkAuthCredentialPresentation_CheckValidContents(presentationBytes: Buffer): void; export function CallLinkAuthCredentialPresentation_GetUserId(presentationBytes: Buffer): Serialized; diff --git a/node/ts/test/ZKGroup-test.ts b/node/ts/test/ZKGroup-test.ts index 5b894dbdd..184b97f75 100644 --- a/node/ts/test/ZKGroup-test.ts +++ b/node/ts/test/ZKGroup-test.ts @@ -61,6 +61,7 @@ import { } from '../zkgroup/'; import { Aci, Pni } from '../Address'; import { LibSignalErrorBase, Uuid } from '..'; +import BackupCredentialType from '../zkgroup/backups/BackupCredentialType'; const SECONDS_PER_DAY = 86400; @@ -707,6 +708,7 @@ describe('ZKGroup', () => { }); describe('BackupAuthCredential', () => { + // Chosen randomly const SERVER_SECRET_RANDOM = hexToBuffer( '6987b92bdea075d3f8b42b39d780a5be0bc264874a18e11cac694e4fe28f6cca' ); @@ -715,16 +717,19 @@ describe('ZKGroup', () => { ); const TEST_USER_ID: Uuid = 'e74beed0-e70f-4cfd-abbb-7e3eb333bbac'; + // These are expectations; if the contents of a credential or derivation of a backup ID changes, + // they will need to be updated. const SERIALIZED_BACKUP_ID = hexToBuffer( - 'e3926f11ddd143e6dd0f20bfcb08349e' + '52b899ef83125719d3daa9a4edcc0aff' ); const SERIALIZED_REQUEST_CREDENTIAL = Buffer.from( - 'AISCxQa8OsFqphsQPxqtzJk5+jndpE3SJG6bfazQB3994Aersq2yNRgcARBoedBeoEfKIXdty6X7l6+TiPFAqDvojRSO8xaZOpKJOvWSDJIGn6EeMl2jOjx+IQg8d8M0AQ==', + 'AISCxQa8OsFqphsQPxqtzJk5+jndpE3SJG6bfazQB3999KZFdtnpcIjx/0DPYbLJRbLQmz1ZXnueq5HPo9ewpEjojRSO8xaZOpKJOvWSDJIGn6EeMl2jOjx+IQg8d8M0AQ==', 'base64' ); it('testDeterministic', () => { - const backupLevel = BackupLevel.Messages; + const backupLevel = BackupLevel.Free; + const credentialType = BackupCredentialType.Messages; const context = BackupAuthCredentialRequestContext.create( BACKUP_KEY, TEST_USER_ID @@ -740,6 +745,7 @@ describe('ZKGroup', () => { const response = request.issueCredential( startOfDay, backupLevel, + credentialType, serverSecretParams ); const credential = context.receive( @@ -748,6 +754,7 @@ describe('ZKGroup', () => { serverSecretParams.getPublicParams() ); assert.equal(backupLevel, credential.getBackupLevel()); + assert.equal(credentialType, credential.getType()); assertArrayEquals(SERIALIZED_BACKUP_ID, credential.getBackupId()); const presentation = credential.present( @@ -758,7 +765,8 @@ describe('ZKGroup', () => { }); it('testIntegration', () => { - const backupLevel = BackupLevel.Messages; + const backupLevel = BackupLevel.Free; + const credentialType = BackupCredentialType.Messages; const serverSecretParams = GenericServerSecretParams.generateWithRandom(SERVER_SECRET_RANDOM); @@ -777,6 +785,7 @@ describe('ZKGroup', () => { const response = request.issueCredentialWithRandom( startOfDay, backupLevel, + credentialType, serverSecretParams, TEST_ARRAY_32_1 ); @@ -788,6 +797,7 @@ describe('ZKGroup', () => { serverPublicParams ); assert.equal(backupLevel, credential.getBackupLevel()); + assert.equal(credentialType, credential.getType()); const presentation = credential.presentWithRandom( serverPublicParams, TEST_ARRAY_32_2 diff --git a/node/ts/zkgroup/backups/BackupAuthCredential.ts b/node/ts/zkgroup/backups/BackupAuthCredential.ts index 4716c334f..836a1f7ee 100644 --- a/node/ts/zkgroup/backups/BackupAuthCredential.ts +++ b/node/ts/zkgroup/backups/BackupAuthCredential.ts @@ -12,6 +12,7 @@ import { RANDOM_LENGTH } from '../internal/Constants'; import GenericServerPublicParams from '../GenericServerPublicParams'; import BackupAuthCredentialPresentation from './BackupAuthCredentialPresentation'; import BackupLevel from './BackupLevel'; +import BackupCredentialType from './BackupCredentialType'; export default class BackupAuthCredential extends ByteArray { private readonly __type?: never; @@ -51,4 +52,12 @@ export default class BackupAuthCredential extends ByteArray { } return n; } + + getType(): BackupCredentialType { + const n: number = Native.BackupAuthCredential_GetType(this.contents); + if (!(n in BackupCredentialType)) { + throw new TypeError(`Invalid BackupCredentialType ${n}`); + } + return n; + } } diff --git a/node/ts/zkgroup/backups/BackupAuthCredentialPresentation.ts b/node/ts/zkgroup/backups/BackupAuthCredentialPresentation.ts index 883cf7263..c515db8d6 100644 --- a/node/ts/zkgroup/backups/BackupAuthCredentialPresentation.ts +++ b/node/ts/zkgroup/backups/BackupAuthCredentialPresentation.ts @@ -8,6 +8,7 @@ import * as Native from '../../../Native'; import GenericServerSecretParams from '../GenericServerSecretParams'; import BackupLevel from './BackupLevel'; +import BackupCredentialType from './BackupCredentialType'; export default class BackupAuthCredentialPresentation extends ByteArray { private readonly __type?: never; @@ -40,4 +41,14 @@ export default class BackupAuthCredentialPresentation extends ByteArray { } return n; } + + getType(): BackupCredentialType { + const n: number = Native.BackupAuthCredentialPresentation_GetType( + this.contents + ); + if (!(n in BackupCredentialType)) { + throw new TypeError(`Invalid BackupCredentialType ${n}`); + } + return n; + } } diff --git a/node/ts/zkgroup/backups/BackupAuthCredentialRequest.ts b/node/ts/zkgroup/backups/BackupAuthCredentialRequest.ts index 38316f730..1ab8d7171 100644 --- a/node/ts/zkgroup/backups/BackupAuthCredentialRequest.ts +++ b/node/ts/zkgroup/backups/BackupAuthCredentialRequest.ts @@ -12,6 +12,7 @@ import { RANDOM_LENGTH } from '../internal/Constants'; import GenericServerSecretParams from '../GenericServerSecretParams'; import BackupAuthCredentialResponse from './BackupAuthCredentialResponse'; import BackupLevel from './BackupLevel'; +import BackupCredentialType from './BackupCredentialType'; export default class BackupAuthCredentialRequest extends ByteArray { private readonly __type?: never; @@ -23,12 +24,14 @@ export default class BackupAuthCredentialRequest extends ByteArray { issueCredential( timestamp: number, backupLevel: BackupLevel, + type: BackupCredentialType, params: GenericServerSecretParams ): BackupAuthCredentialResponse { const random = randomBytes(RANDOM_LENGTH); return this.issueCredentialWithRandom( timestamp, backupLevel, + type, params, random ); @@ -37,6 +40,7 @@ export default class BackupAuthCredentialRequest extends ByteArray { issueCredentialWithRandom( timestamp: number, backupLevel: BackupLevel, + type: BackupCredentialType, params: GenericServerSecretParams, random: Buffer ): BackupAuthCredentialResponse { @@ -45,6 +49,7 @@ export default class BackupAuthCredentialRequest extends ByteArray { this.contents, timestamp, backupLevel, + type, params.contents, random ) diff --git a/node/ts/zkgroup/backups/BackupCredentialType.ts b/node/ts/zkgroup/backups/BackupCredentialType.ts new file mode 100644 index 000000000..8e4cfc4a3 --- /dev/null +++ b/node/ts/zkgroup/backups/BackupCredentialType.ts @@ -0,0 +1,11 @@ +// +// Copyright 2024 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +// This must match the Rust version of the enum. +enum BackupCredentialType { + Messages = 1, + Media = 2, +} +export default BackupCredentialType; diff --git a/node/ts/zkgroup/backups/BackupLevel.ts b/node/ts/zkgroup/backups/BackupLevel.ts index 6ae632d42..511b211b4 100644 --- a/node/ts/zkgroup/backups/BackupLevel.ts +++ b/node/ts/zkgroup/backups/BackupLevel.ts @@ -5,7 +5,7 @@ // This must match the Rust version of the enum. enum BackupLevel { - Messages = 200, - Media = 201, + Free = 200, + Paid = 201, } export default BackupLevel; diff --git a/rust/bridge/shared/src/zkgroup.rs b/rust/bridge/shared/src/zkgroup.rs index b8bcbede4..d4ca56b24 100644 --- a/rust/bridge/shared/src/zkgroup.rs +++ b/rust/bridge/shared/src/zkgroup.rs @@ -4,6 +4,7 @@ // use ::zkgroup; +use backups::BackupCredentialType; use libsignal_bridge_macros::*; use libsignal_bridge_types::zkgroup::validate_serialization; use libsignal_protocol::{Aci, Pni, ServiceId}; @@ -893,7 +894,7 @@ fn CallLinkAuthCredentialPresentation_GetUserId( #[bridge_fn] fn BackupAuthCredentialRequestContext_New(backup_key: &[u8; 32], uuid: Uuid) -> Vec { - let backup_key: libsignal_account_keys::BackupKeyV0 = + let backup_key: libsignal_account_keys::BackupKey = libsignal_account_keys::BackupKey(*backup_key); let context = BackupAuthCredentialRequestContext::new(&backup_key, uuid.into()); zkgroup::serialize(&context) @@ -927,6 +928,7 @@ fn BackupAuthCredentialRequest_IssueDeterministic( request_bytes: &[u8], redemption_time: Timestamp, backup_level: AsType, + credential_type: AsType, params_bytes: &[u8], randomness: &[u8; RANDOMNESS_LEN], ) -> Vec { @@ -938,6 +940,7 @@ fn BackupAuthCredentialRequest_IssueDeterministic( let response = request.issue( redemption_time, backup_level.into_inner(), + credential_type.into_inner(), ¶ms, *randomness, ); @@ -990,6 +993,13 @@ fn BackupAuthCredential_GetBackupLevel(credential_bytes: &[u8]) -> u8 { credential.backup_level() as u8 } +#[bridge_fn] +fn BackupAuthCredential_GetType(credential_bytes: &[u8]) -> u8 { + let credential = bincode::deserialize::(credential_bytes) + .expect("should have been parsed previously"); + credential.credential_type() as u8 +} + #[bridge_fn] fn BackupAuthCredential_PresentDeterministic( credential_bytes: &[u8], @@ -1040,6 +1050,13 @@ fn BackupAuthCredentialPresentation_GetBackupLevel(presentation_bytes: &[u8]) -> presentation.backup_level() as u8 } +#[bridge_fn(ffi = false)] +fn BackupAuthCredentialPresentation_GetType(presentation_bytes: &[u8]) -> u8 { + let presentation = bincode::deserialize::(presentation_bytes) + .expect("should have been parsed previously"); + presentation.credential_type() as u8 +} + #[bridge_fn] fn GroupSendDerivedKeyPair_CheckValidContents( bytes: &[u8], diff --git a/rust/zkgroup/src/api/backups.rs b/rust/zkgroup/src/api/backups.rs index 19aad08a7..785c4508c 100644 --- a/rust/zkgroup/src/api/backups.rs +++ b/rust/zkgroup/src/api/backups.rs @@ -7,5 +7,6 @@ mod auth_credential; pub use auth_credential::{ BackupAuthCredential, BackupAuthCredentialPresentation, BackupAuthCredentialRequest, - BackupAuthCredentialRequestContext, BackupAuthCredentialResponse, BackupLevel, + BackupAuthCredentialRequestContext, BackupAuthCredentialResponse, BackupCredentialType, + BackupLevel, }; diff --git a/rust/zkgroup/src/api/backups/auth_credential.rs b/rust/zkgroup/src/api/backups/auth_credential.rs index 658e7e154..8db7891b7 100644 --- a/rust/zkgroup/src/api/backups/auth_credential.rs +++ b/rust/zkgroup/src/api/backups/auth_credential.rs @@ -43,9 +43,10 @@ impl zkcredential::attributes::RevealedAttribute for BackupIdPoint { const CREDENTIAL_LABEL: &[u8] = b"20231003_Signal_BackupAuthCredential"; -// We make sure we serialize BackupLevel with plenty of room to expand to other -// u64 values later. But since it fits in a byte today, we stick to just a u8 -// in the in-memory representation. +// We make sure we serialize BackupLevel and BackupType with plenty of room to expand to other u64 +// values later. But since they fit in a byte today, we stick to just a u8 in the in-memory and +// bridge representation. + #[derive( Copy, Clone, @@ -61,8 +62,8 @@ const CREDENTIAL_LABEL: &[u8] = b"20231003_Signal_BackupAuthCredential"; #[repr(u8)] pub enum BackupLevel { #[partial_default] - Messages = 200, - Media = 201, + Free = 200, + Paid = 201, } impl From for u64 { @@ -83,6 +84,43 @@ impl TryFrom for BackupLevel { } } +#[derive( + Copy, + Clone, + Serialize, + Deserialize, + PartialEq, + Eq, + PartialDefault, + Debug, + num_enum::TryFromPrimitive, +)] +#[serde(into = "u64", try_from = "u64")] +#[repr(u8)] +pub enum BackupCredentialType { + #[partial_default] + Messages = 1, + Media = 2, +} + +impl From for u64 { + fn from(credential_type: BackupCredentialType) -> Self { + credential_type as u64 + } +} + +impl TryFrom for BackupCredentialType { + // Unfortunately u8::try_from and TryFromPrimitive have different Error types. + // But we shouldn't be passing invalid BackupTypes anyway. + type Error = ZkGroupDeserializationFailure; + fn try_from(value: u64) -> Result { + u8::try_from(value) + .ok() + .and_then(|v| BackupCredentialType::try_from(v).ok()) + .ok_or(ZkGroupDeserializationFailure::new::()) + } +} + #[derive(Serialize, Deserialize, PartialDefault)] pub struct BackupAuthCredentialRequestContext { reserved: ReservedByte, @@ -138,6 +176,7 @@ impl BackupAuthCredentialRequest { &self, redemption_time: Timestamp, backup_level: BackupLevel, + credential_type: BackupCredentialType, params: &GenericServerSecretParams, randomness: RandomnessBytes, ) -> BackupAuthCredentialResponse { @@ -145,9 +184,11 @@ impl BackupAuthCredentialRequest { reserved: Default::default(), redemption_time, backup_level, + credential_type, blinded_credential: zkcredential::issuance::IssuanceProofBuilder::new(CREDENTIAL_LABEL) .add_public_attribute(&redemption_time) - .add_public_attribute(&(backup_level as u64)) + .add_public_attribute(&u64::from(backup_level)) + .add_public_attribute(&u64::from(credential_type)) .add_blinded_revealed_attribute(&self.blinded_backup_id) .issue(¶ms.credential_key, &self.public_key, randomness), } @@ -162,6 +203,7 @@ pub struct BackupAuthCredentialResponse { // But that would change the format. redemption_time: Timestamp, backup_level: BackupLevel, + credential_type: BackupCredentialType, blinded_credential: zkcredential::issuance::blind::BlindedIssuanceProof, } @@ -182,9 +224,11 @@ impl BackupAuthCredentialRequestContext { reserved: Default::default(), redemption_time: response.redemption_time, backup_level: response.backup_level, + credential_type: response.credential_type, credential: zkcredential::issuance::IssuanceProofBuilder::new(CREDENTIAL_LABEL) .add_public_attribute(&response.redemption_time) - .add_public_attribute(&(response.backup_level as u64)) + .add_public_attribute(&u64::from(response.backup_level)) + .add_public_attribute(&u64::from(response.credential_type)) .add_blinded_revealed_attribute(&self.blinded_backup_id) .verify( ¶ms.credential_key, @@ -202,6 +246,7 @@ pub struct BackupAuthCredential { reserved: ReservedByte, redemption_time: Timestamp, backup_level: BackupLevel, + credential_type: BackupCredentialType, credential: zkcredential::credentials::Credential, backup_id: libsignal_account_keys::BackupId, } @@ -216,6 +261,7 @@ impl BackupAuthCredential { version: Default::default(), redemption_time: self.redemption_time, backup_level: self.backup_level, + credential_type: self.credential_type, backup_id: self.backup_id, proof: zkcredential::presentation::PresentationProofBuilder::new(CREDENTIAL_LABEL) .add_revealed_attribute(&BackupIdPoint::new(&self.backup_id)) @@ -230,12 +276,17 @@ impl BackupAuthCredential { pub fn backup_level(&self) -> BackupLevel { self.backup_level } + + pub fn credential_type(&self) -> BackupCredentialType { + self.credential_type + } } #[derive(Serialize, Deserialize, PartialDefault)] pub struct BackupAuthCredentialPresentation { version: ReservedByte, backup_level: BackupLevel, + credential_type: BackupCredentialType, redemption_time: Timestamp, proof: zkcredential::presentation::PresentationProof, backup_id: libsignal_account_keys::BackupId, @@ -262,7 +313,8 @@ impl BackupAuthCredentialPresentation { zkcredential::presentation::PresentationProofVerifier::new(CREDENTIAL_LABEL) .add_public_attribute(&self.redemption_time) - .add_public_attribute(&(self.backup_level as u64)) + .add_public_attribute(&u64::from(self.backup_level)) + .add_public_attribute(&u64::from(self.credential_type)) .add_revealed_attribute(&BackupIdPoint::new(&self.backup_id)) .verify(&server_params.credential_key, &self.proof) .map_err(|_| ZkGroupVerificationFailure) @@ -272,6 +324,10 @@ impl BackupAuthCredentialPresentation { self.backup_level } + pub fn credential_type(&self) -> BackupCredentialType { + self.credential_type + } + pub fn backup_id(&self) -> libsignal_account_keys::BackupId { self.backup_id } @@ -281,10 +337,7 @@ impl BackupAuthCredentialPresentation { mod tests { use assert_matches::assert_matches; - use crate::backups::auth_credential::{BackupLevel, GenericServerSecretParams}; - use crate::backups::{ - BackupAuthCredential, BackupAuthCredentialPresentation, BackupAuthCredentialRequestContext, - }; + use super::*; use crate::{common, RandomnessBytes, Timestamp, RANDOMNESS_LEN, SECONDS_PER_DAY}; const DAY_ALIGNED_TIMESTAMP: Timestamp = Timestamp::from_epoch_seconds(1681344000); // 2023-04-13 00:00:00 UTC @@ -306,7 +359,8 @@ mod tests { // server generated materials; issuance request -> issuance response let blinded_credential = request.issue( redemption_time, - BackupLevel::Messages, + BackupLevel::Free, + BackupCredentialType::Messages, &server_secret_params(), ISSUE_RAND, ); @@ -377,7 +431,7 @@ mod tests { credential.present(&server_secret_params().get_public_params(), PRESENT_RAND); let invalid_presentation = BackupAuthCredentialPresentation { // Credential was for BackupLevel::Messages - backup_level: BackupLevel::Media, + backup_level: BackupLevel::Paid, ..valid_presentation }; invalid_presentation @@ -393,7 +447,8 @@ mod tests { let request = request_context.get_request(); let blinded_credential = request.issue( redemption_time, - BackupLevel::Messages, + BackupLevel::Free, + BackupCredentialType::Messages, &server_secret_params(), ISSUE_RAND, ); @@ -417,7 +472,8 @@ mod tests { let request = request_context.get_request(); let blinded_credential = request.issue( redemption_time, - BackupLevel::Messages, + BackupLevel::Free, + BackupCredentialType::Messages, &server_secret_params(), ISSUE_RAND, ); @@ -435,28 +491,30 @@ mod tests { #[test] fn test_backup_level_serialization() { - let messages_bytes = common::serialization::serialize(&BackupLevel::Messages); - let media_byte = common::serialization::serialize(&BackupLevel::Media); - assert_eq!(messages_bytes.len(), 8); - assert_eq!(media_byte.len(), 8); + let free_bytes = common::serialization::serialize(&BackupLevel::Free); + let paid_bytes = common::serialization::serialize(&BackupLevel::Paid); + assert_eq!(free_bytes.len(), 8); + assert_eq!(paid_bytes.len(), 8); - let messages_num: u64 = - common::serialization::deserialize(&messages_bytes).expect("valid u64"); - let media_num: u64 = common::serialization::deserialize(&media_byte).expect("valid u64"); - assert_eq!(messages_num, 200); - assert_eq!(media_num, 201); + let free_num: u64 = common::serialization::deserialize(&free_bytes).expect("valid u64"); + let paid_num: u64 = common::serialization::deserialize(&paid_bytes).expect("valid u64"); + assert_eq!(free_num, 200); + assert_eq!(paid_num, 201); - let messages: BackupLevel = - common::serialization::deserialize(&messages_bytes).expect("valid level"); - let media: BackupLevel = - common::serialization::deserialize(&media_byte).expect("valid level"); - assert_eq!(messages, BackupLevel::Messages); - assert_eq!(media, BackupLevel::Media); + let free: BackupLevel = + common::serialization::deserialize(&free_bytes).expect("valid level"); + let paid: BackupLevel = + common::serialization::deserialize(&paid_bytes).expect("valid level"); + assert_eq!(free, BackupLevel::Free); + assert_eq!(paid, BackupLevel::Paid); } #[test] fn test_backup_level_validation() { // Check that the u64 level isn't just truncated to u8. - assert_matches!(BackupLevel::try_from(0x100000000000u64 + 200u64), Err(_)); + assert_matches!( + BackupLevel::try_from(0x100000000000u64 + u64::from(BackupLevel::Free)), + Err(_) + ); } } diff --git a/rust/zkgroup/tests/backup_auth_flow.rs b/rust/zkgroup/tests/backup_auth_flow.rs index 3082d8f0d..ebc81ee1d 100644 --- a/rust/zkgroup/tests/backup_auth_flow.rs +++ b/rust/zkgroup/tests/backup_auth_flow.rs @@ -24,7 +24,8 @@ fn test_backup_auth_request_response() { // client receives in response to initial request let redemption_time: Timestamp = DAY_ALIGNED_TIMESTAMP; // client validates it's day-aligned - let backup_level = zkgroup::backups::BackupLevel::Messages; // client validates it's a valid backup level + let backup_level = zkgroup::backups::BackupLevel::Free; // client validates it's a valid backup level + let credential_type = zkgroup::backups::BackupCredentialType::Messages; // client validates it's for the right set of files // client generated materials; issuance request let request_context = @@ -37,6 +38,7 @@ fn test_backup_auth_request_response() { let blinded_credential = request.issue( redemption_time, backup_level, + credential_type, &server_secret_params, randomness2, ); @@ -48,6 +50,7 @@ fn test_backup_auth_request_response() { .expect("credential should be valid"); assert_eq!(credential.backup_level(), backup_level); + assert_eq!(credential.credential_type(), credential_type); let presentation = credential.present(&server_public_params, randomness3); diff --git a/swift/Sources/LibSignalClient/zkgroup/BackupAuthCredential.swift b/swift/Sources/LibSignalClient/zkgroup/BackupAuthCredential.swift index 25c4c8dda..0e872ccc8 100644 --- a/swift/Sources/LibSignalClient/zkgroup/BackupAuthCredential.swift +++ b/swift/Sources/LibSignalClient/zkgroup/BackupAuthCredential.swift @@ -54,4 +54,18 @@ public class BackupAuthCredential: ByteArray { return backupLevel } } + + public var type: BackupCredentialType { + return failOnError { + let rawValue = try withUnsafeBorrowedBuffer { contents in + try invokeFnReturningInteger { + signal_backup_auth_credential_get_type($0, contents) + } + } + guard let type = BackupCredentialType(rawValue: rawValue) else { + throw SignalError.internalError("Invalid BackupCredentialType \(rawValue)") + } + return type + } + } } diff --git a/swift/Sources/LibSignalClient/zkgroup/BackupAuthCredentialRequest.swift b/swift/Sources/LibSignalClient/zkgroup/BackupAuthCredentialRequest.swift index 709a6234b..a79a8687c 100644 --- a/swift/Sources/LibSignalClient/zkgroup/BackupAuthCredentialRequest.swift +++ b/swift/Sources/LibSignalClient/zkgroup/BackupAuthCredentialRequest.swift @@ -11,19 +11,19 @@ public class BackupAuthCredentialRequest: ByteArray { try super.init(contents, checkValid: signal_backup_auth_credential_request_check_valid_contents) } - public func issueCredential(timestamp: Date, backupLevel: BackupLevel, params: GenericServerSecretParams) -> BackupAuthCredentialResponse { + public func issueCredential(timestamp: Date, backupLevel: BackupLevel, type: BackupCredentialType, params: GenericServerSecretParams) -> BackupAuthCredentialResponse { return failOnError { - self.issueCredential(timestamp: timestamp, backupLevel: backupLevel, params: params, randomness: try .generate()) + self.issueCredential(timestamp: timestamp, backupLevel: backupLevel, type: type, params: params, randomness: try .generate()) } } - public func issueCredential(timestamp: Date, backupLevel: BackupLevel, params: GenericServerSecretParams, randomness: Randomness) -> BackupAuthCredentialResponse { + public func issueCredential(timestamp: Date, backupLevel: BackupLevel, type: BackupCredentialType, params: GenericServerSecretParams, randomness: Randomness) -> BackupAuthCredentialResponse { return failOnError { try withUnsafeBorrowedBuffer { contents in try params.withUnsafeBorrowedBuffer { params in try randomness.withUnsafePointerToBytes { randomness in try invokeFnReturningVariableLengthSerialized { - signal_backup_auth_credential_request_issue_deterministic($0, contents, UInt64(timestamp.timeIntervalSince1970), backupLevel.rawValue, params, randomness) + signal_backup_auth_credential_request_issue_deterministic($0, contents, UInt64(timestamp.timeIntervalSince1970), backupLevel.rawValue, type.rawValue, params, randomness) } } } diff --git a/swift/Sources/LibSignalClient/zkgroup/BackupLevel.swift b/swift/Sources/LibSignalClient/zkgroup/BackupLevel.swift index abc9cfa18..f11dfc567 100644 --- a/swift/Sources/LibSignalClient/zkgroup/BackupLevel.swift +++ b/swift/Sources/LibSignalClient/zkgroup/BackupLevel.swift @@ -8,5 +8,10 @@ import SignalFfi public enum BackupLevel: UInt8 { // This must match the Rust version of the enum. - case messages = 200, media = 201 + case free = 200, paid = 201 +} + +public enum BackupCredentialType: UInt8 { + // This must match the Rust version of the enum. + case messages = 1, media = 2 } diff --git a/swift/Sources/SignalFfi/signal_ffi.h b/swift/Sources/SignalFfi/signal_ffi.h index 56fd98b73..c07acce1d 100644 --- a/swift/Sources/SignalFfi/signal_ffi.h +++ b/swift/Sources/SignalFfi/signal_ffi.h @@ -1403,7 +1403,7 @@ SignalFfiError *signal_backup_auth_credential_request_context_get_request(Signal SignalFfiError *signal_backup_auth_credential_request_check_valid_contents(SignalBorrowedBuffer request_bytes); -SignalFfiError *signal_backup_auth_credential_request_issue_deterministic(SignalOwnedBuffer *out, SignalBorrowedBuffer request_bytes, uint64_t redemption_time, uint8_t backup_level, SignalBorrowedBuffer params_bytes, const uint8_t (*randomness)[SignalRANDOMNESS_LEN]); +SignalFfiError *signal_backup_auth_credential_request_issue_deterministic(SignalOwnedBuffer *out, SignalBorrowedBuffer request_bytes, uint64_t redemption_time, uint8_t backup_level, uint8_t credential_type, SignalBorrowedBuffer params_bytes, const uint8_t (*randomness)[SignalRANDOMNESS_LEN]); SignalFfiError *signal_backup_auth_credential_response_check_valid_contents(SignalBorrowedBuffer response_bytes); @@ -1415,6 +1415,8 @@ SignalFfiError *signal_backup_auth_credential_get_backup_id(uint8_t (*out)[16], SignalFfiError *signal_backup_auth_credential_get_backup_level(uint8_t *out, SignalBorrowedBuffer credential_bytes); +SignalFfiError *signal_backup_auth_credential_get_type(uint8_t *out, SignalBorrowedBuffer credential_bytes); + SignalFfiError *signal_backup_auth_credential_present_deterministic(SignalOwnedBuffer *out, SignalBorrowedBuffer credential_bytes, SignalBorrowedBuffer server_params_bytes, const uint8_t (*randomness)[SignalRANDOMNESS_LEN]); SignalFfiError *signal_backup_auth_credential_presentation_check_valid_contents(SignalBorrowedBuffer presentation_bytes); diff --git a/swift/Tests/LibSignalClientTests/ZKGroupTests.swift b/swift/Tests/LibSignalClientTests/ZKGroupTests.swift index 0b8602e58..6f3b5ab35 100644 --- a/swift/Tests/LibSignalClientTests/ZKGroupTests.swift +++ b/swift/Tests/LibSignalClientTests/ZKGroupTests.swift @@ -420,6 +420,7 @@ class ZKGroupTests: TestCaseBase { } func testBackupAuthCredentialDeterministic() throws { + // Chosen randomly let backupKey: [UInt8] = [ 0xF9, 0xAB, 0xBB, 0xFF, 0xA7, 0xD4, 0x24, 0x92, 0x97, 0x65, 0xAE, 0xCC, 0x84, 0xB6, 0x04, 0x63, @@ -427,27 +428,38 @@ class ZKGroupTests: TestCaseBase { 0x06, 0xB7, 0x9B, 0xC9, 0xA5, 0x62, 0x93, 0x38, ] let aci = UUID(uuidString: "e74beed0-e70f-4cfd-abbb-7e3eb333bbac")! - let serializedBackupID: [UInt8] = [0xE3, 0x92, 0x6F, 0x11, 0xDD, 0xD1, 0x43, 0xE6, 0xDD, 0x0F, 0x20, 0xBF, 0xCB, 0x08, 0x34, 0x9E] - let serializedRequestCredential = Data(base64Encoded: "AISCxQa8OsFqphsQPxqtzJk5+jndpE3SJG6bfazQB3994Aersq2yNRgcARBoedBeoEfKIXdty6X7l6+TiPFAqDvojRSO8xaZOpKJOvWSDJIGn6EeMl2jOjx+IQg8d8M0AQ==")! - let backupLevel = BackupLevel.messages + + // These are expectations; if the contents of a credential or derivation of a backup ID + // changes, they will need to be updated. + let serializedBackupID: [UInt8] = [0x52, 0xB8, 0x99, 0xEF, 0x83, 0x12, 0x57, 0x19, 0xD3, 0xDA, 0xA9, 0xA4, 0xED, 0xCC, 0x0A, 0xFF] + let serializedRequestCredential = Data(base64Encoded: "AISCxQa8OsFqphsQPxqtzJk5+jndpE3SJG6bfazQB3999KZFdtnpcIjx/0DPYbLJRbLQmz1ZXnueq5HPo9ewpEjojRSO8xaZOpKJOvWSDJIGn6EeMl2jOjx+IQg8d8M0AQ==")! + + let backupLevel = BackupLevel.free + let credentialType = BackupCredentialType.messages let context = BackupAuthCredentialRequestContext.create(backupKey: backupKey, aci: aci) let request = context.getRequest() let serverSecretParams = GenericServerSecretParams.generate(randomness: self.TEST_ARRAY_32) let serverPublicParams = serverSecretParams.getPublicParams() - XCTAssertEqual(request.serialize(), Array(serializedRequestCredential)) + XCTAssertEqual( + request.serialize(), + Array(serializedRequestCredential), + Data(request.serialize()).base64EncodedString() + ) let now = UInt64(Date().timeIntervalSince1970) let startOfDay = now - (now % SECONDS_PER_DAY) let redemptionTime = Date(timeIntervalSince1970: TimeInterval(startOfDay)) - let response = request.issueCredential(timestamp: redemptionTime, backupLevel: backupLevel, params: serverSecretParams, randomness: self.TEST_ARRAY_32_2) + let response = request.issueCredential(timestamp: redemptionTime, backupLevel: backupLevel, type: credentialType, params: serverSecretParams, randomness: self.TEST_ARRAY_32_2) let credential = try context.receive(response, timestamp: redemptionTime, params: serverPublicParams) - XCTAssertEqual(credential.backupID, serializedBackupID) + XCTAssertEqual(credential.backupID, serializedBackupID, credential.backupID.hexString) XCTAssertEqual(credential.backupLevel, backupLevel) + XCTAssertEqual(credential.type, credentialType) } func testBackupAuthCredential() throws { - let backupLevel = BackupLevel.messages + let backupLevel = BackupLevel.free + let credentialType = BackupCredentialType.messages let serverSecretParams = GenericServerSecretParams.generate(randomness: self.TEST_ARRAY_32) let serverPublicParams = serverSecretParams.getPublicParams() @@ -462,11 +474,12 @@ class ZKGroupTests: TestCaseBase { let now = UInt64(Date().timeIntervalSince1970) let startOfDay = now - (now % SECONDS_PER_DAY) let redemptionTime = Date(timeIntervalSince1970: TimeInterval(startOfDay)) - let response = request.issueCredential(timestamp: redemptionTime, backupLevel: backupLevel, params: serverSecretParams, randomness: self.TEST_ARRAY_32_2) + let response = request.issueCredential(timestamp: redemptionTime, backupLevel: backupLevel, type: credentialType, params: serverSecretParams, randomness: self.TEST_ARRAY_32_2) // Client let credential = try context.receive(response, timestamp: redemptionTime, params: serverPublicParams) XCTAssertEqual(backupLevel, credential.backupLevel) + XCTAssertEqual(credentialType, credential.type) let presentation = credential.present(serverParams: serverPublicParams, randomness: self.TEST_ARRAY_32_3)