Skip to content

Commit 7293551

Browse files
Immediately after a restore, use the same enc_salt/pw_salt for the next backup.
Co-authored-by: Jordan Rose <[email protected]>
1 parent 7b6bcfd commit 7293551

File tree

24 files changed

+597
-246
lines changed

24 files changed

+597
-246
lines changed

Cargo.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ default-members = [
3737
resolver = "2" # so that our dev-dependency features don't leak into products
3838

3939
[workspace.package]
40-
version = "0.77.2"
40+
version = "0.78.0"
4141
authors = ["Signal Messenger LLC"]
4242
license = "AGPL-3.0-only"
4343
rust-version = "1.83.0"

LibSignalClient.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
Pod::Spec.new do |s|
77
s.name = 'LibSignalClient'
8-
s.version = '0.77.2'
8+
s.version = '0.78.0'
99
s.summary = 'A Swift wrapper library for communicating with the Signal messaging service.'
1010

1111
s.homepage = 'https://github.com/signalapp/libsignal'

RELEASE_NOTES.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
1-
v0.77.2
1+
v0.78.0
22

3+
## SVR-B
4+
5+
- Operations have been consistently renamed to `store` and `restore`.
6+
- `restore` now returns an object containing both the BackupForwardSecrecyToken for decryption, and "secret data" to be used in the first `store` after restoration.
7+
8+
## Other changes

java/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ plugins {
1616
}
1717

1818
allprojects {
19-
version = "0.77.2"
19+
version = "0.78.0"
2020
group = "org.signal"
2121

2222
tasks.withType(KotlinCompile).configureEach {

java/client/src/main/java/org/signal/libsignal/net/SvrB.kt

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import org.signal.libsignal.messagebackup.BackupKey
3131
*
3232
* 1. Create a [Network] instance and get the [SvrB] service via [Network.svrB]
3333
* 2. Call [SvrB.store]
34-
* - Pass the secret data from the last **successful** [SvrB.store] call
34+
* - Pass the `nextBackupSecretData` from the last **successful** [SvrB.store] or [SvrB.restore] call
3535
* - If no previous backup exists or the secret data is unavailable, pass `null`
3636
* 3. Use the returned forward secrecy token to derive encryption keys
3737
* 4. Encrypt and upload the backup data to the user's remote, off-device storage location, including the
@@ -42,8 +42,8 @@ import org.signal.libsignal.messagebackup.BackupKey
4242
* ## Secret handling
4343
*
4444
* When calling [SvrB.store], the `previousSecretData` parameter
45-
* must be from the last call to [SvrB.store] that
46-
* succeeded. The returned secret from a successful `store()` call should
45+
* must be from the last call to [SvrB.store] or [SvrB.restore] that
46+
* succeeded. The returned secret from a successful `store()` or `restore()` call should
4747
* be persisted until it is overwritten by the value from a subsequent
4848
* successful call. The caller should pass `null` as `previousSecretData`
4949
* only for the very first backup from a device.
@@ -52,9 +52,10 @@ import org.signal.libsignal.messagebackup.BackupKey
5252
*
5353
* 1. Create a [Network] instance and get the [SvrB] service via [Network.svrB]
5454
* 2. Fetch the backup metadata from storage
55-
* 3. Call [SvrB.fetchForwardSecrecyTokenFromServer] to get the forward secrecy token
55+
* 3. Call [SvrB.restore] to get the forward secrecy token
5656
* 4. Use the token to derive decryption keys
5757
* 5. Decrypt and restore the backup data
58+
* 6. Store the [SvrBRestoreResponse.nextBackupSecretData] locally.
5859
*
5960
* ## Usage
6061
* ```kotlin
@@ -85,9 +86,8 @@ public class SvrB internal constructor(
8586
*
8687
* @param backupKey The backup key derived from the Account Entropy Pool (AEP).
8788
* @param previousSecretData Optional secret data from the most recent previous backup.
88-
* **Critical**: This MUST be the [SvrBStoreResponse.nextBackupSecretData] data
89-
* from the last [store] whose returned [SvrBStoreResponse.metadata] was
90-
* successfully uploaded, and whose `nextBackupSecretData` was persisted.
89+
* **Critical**: This MUST be the secret data from the last [store] or [restore]
90+
* whose returned `metadata` was successfully uploaded, and whose `nextBackupSecretData` was persisted.
9191
* If `null`, starts a new chain and renders any prior backups unretrievable; this should
9292
* only be used for the very first backup from a device.
9393
* @return a [CompletableFuture] that completes with:
@@ -96,7 +96,6 @@ public class SvrB internal constructor(
9696
* - [Result.failure] containing [NetworkException] if the network operation fails (connection, service, or timeout errors)
9797
* - [Result.failure] containing [NetworkProtocolException] if there is a protocol error
9898
* - [Result.failure] containing [AttestationFailedException] if enclave attestation fails
99-
* - [Result.failure] containing [DataMissingException] if the request fails with MISSING status
10099
* - [Result.failure] containing [SvrException] for other SVR request failures
101100
*/
102101
public fun store(
@@ -117,14 +116,14 @@ public class SvrB internal constructor(
117116
}
118117

119118
return nativeFuture.thenApply { backupResponseHandle ->
120-
val response = BackupResponse(backupResponseHandle)
119+
val response = BackupStoreResponse(backupResponseHandle)
121120
response.guardedMap { _ ->
122121
SvrBStoreResponse(
123122
forwardSecrecyToken = BackupForwardSecrecyToken(
124-
response.guardedMapChecked(Native::BackupResponse_GetForwardSecrecyToken),
123+
response.guardedMapChecked(Native::BackupStoreResponse_GetForwardSecrecyToken),
125124
),
126-
nextBackupSecretData = response.guardedMapChecked(Native::BackupResponse_GetNextBackupSecretData),
127-
metadata = response.guardedMapChecked(Native::BackupResponse_GetOpaqueMetadata),
125+
nextBackupSecretData = response.guardedMapChecked(Native::BackupStoreResponse_GetNextBackupSecretData),
126+
metadata = response.guardedMapChecked(Native::BackupStoreResponse_GetOpaqueMetadata),
128127
)
129128
}
130129
}.toResultFuture()
@@ -142,6 +141,7 @@ public class SvrB internal constructor(
142141
* 2. Call this function to retrieve the forward secrecy token from SVR-B
143142
* 3. Use the token to derive message backup keys
144143
* 4. Decrypt and restore the backup data
144+
* 5. Store the returned [SvrBRestoreResponse.nextBackupSecretData] locally.
145145
*
146146
* @param backupKey The backup key derived from the Account Entropy Pool (AEP).
147147
* @param metadata The metadata that was stored in a header in the backup file during backup creation.
@@ -155,10 +155,10 @@ public class SvrB internal constructor(
155155
* - [Result.failure] containing [AttestationFailedException] if enclave attestation fails
156156
* - [Result.failure] containing [SvrException] for other SVR request failures
157157
*/
158-
public fun fetchForwardSecrecyTokenFromServer(
158+
public fun restore(
159159
backupKey: BackupKey,
160160
metadata: ByteArray,
161-
): CompletableFuture<Result<BackupForwardSecrecyToken>> {
161+
): CompletableFuture<Result<SvrBRestoreResponse>> {
162162
val nativeFuture = network.asyncContext.guardedMap { asyncContextHandle ->
163163
network.connectionManager.guardedMap { connectionManagerHandle ->
164164
Native.SecureValueRecoveryForBackups_RestoreBackupFromServer(
@@ -172,20 +172,39 @@ public class SvrB internal constructor(
172172
}
173173
}
174174

175-
return nativeFuture.thenApply { bytes ->
176-
BackupForwardSecrecyToken(bytes)
175+
return nativeFuture.thenApply { backupResponseHandle ->
176+
val response = BackupRestoreResponse(backupResponseHandle)
177+
response.guardedMap { _ ->
178+
SvrBRestoreResponse(
179+
forwardSecrecyToken = BackupForwardSecrecyToken(
180+
response.guardedMapChecked(Native::BackupRestoreResponse_GetForwardSecrecyToken),
181+
),
182+
nextBackupSecretData = response.guardedMapChecked(Native::BackupRestoreResponse_GetNextBackupSecretData),
183+
)
184+
}
177185
}.toResultFuture()
178186
}
179187
}
180188

181189
/**
182190
* Native handle wrapper for backup response from the store operation.
183191
*/
184-
private class BackupResponse internal constructor(
192+
private class BackupStoreResponse internal constructor(
193+
nativeHandle: Long,
194+
) : NativeHandleGuard.SimpleOwner(nativeHandle) {
195+
override fun release(nativeHandle: Long) {
196+
Native.BackupStoreResponse_Destroy(nativeHandle)
197+
}
198+
}
199+
200+
/**
201+
* Native handle wrapper for backup response from the restore operation.
202+
*/
203+
private class BackupRestoreResponse internal constructor(
185204
nativeHandle: Long,
186205
) : NativeHandleGuard.SimpleOwner(nativeHandle) {
187206
override fun release(nativeHandle: Long) {
188-
Native.BackupResponse_Destroy(nativeHandle)
207+
Native.BackupRestoreResponse_Destroy(nativeHandle)
189208
}
190209
}
191210

@@ -226,3 +245,31 @@ public data class SvrBStoreResponse(
226245
*/
227246
public val metadata: ByteArray,
228247
)
248+
249+
/**
250+
* The result of restoring a backup.
251+
*
252+
* This context contains all the necessary components to decrypt a backup using a
253+
* key derived from both the user's Account Entropy Pool and the SVR-B-protected
254+
* Forward Secrecy Token.
255+
*
256+
* @see [BackupForwardSecrecyToken]
257+
*/
258+
public data class SvrBRestoreResponse(
259+
/**
260+
* The forward secrecy token used to derive [MessageBackupKey] instances.
261+
*
262+
* This token provides forward secrecy guarantees by ensuring that compromise of the backup key
263+
* alone is insufficient to decrypt backups. Each backup is protected by a value stored on
264+
* the SVR-B server that must be retrieved during restoration.
265+
*/
266+
public val forwardSecrecyToken: BackupForwardSecrecyToken,
267+
268+
/**
269+
* Opaque value that must be persisted and provided to the next call to [SvrB.store].
270+
*
271+
* See the [SvrB] documentation for lifecycle and persistence handling
272+
* for this value.
273+
*/
274+
public val nextBackupSecretData: ByteArray,
275+
)

java/client/src/test/java/org/signal/libsignal/net/SecureValueRecoveryBackupTest.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,17 +119,17 @@ class SecureValueRecoveryBackupTest {
119119
val firstSecretData = firstResponse.nextBackupSecretData
120120
assertFalse(firstSecretData.isEmpty())
121121

122-
val firstRestoreResult = svrB.fetchForwardSecrecyTokenFromServer(
122+
val firstRestoreResult = svrB.restore(
123123
testBackupKey,
124124
firstResponse.metadata,
125125
).get(ASYNC_TIMEOUT_SECONDS, TimeUnit.SECONDS)
126126

127127
assertTrue("First restore should succeed", firstRestoreResult.isSuccess)
128-
val restoredFirstToken = firstRestoreResult.getOrThrow()
129-
assertNotNull("Restored first token should not be null", restoredFirstToken)
128+
val restoredFirst = firstRestoreResult.getOrThrow()
129+
assertNotNull("Restored first token should not be null", restoredFirst)
130130

131131
val firstTokenBytes = firstToken.serialize()
132-
val restoredFirstTokenBytes = restoredFirstToken.serialize()
132+
val restoredFirstTokenBytes = restoredFirst.forwardSecrecyToken.serialize()
133133
assertTrue(
134134
"Restored first token should match stored token",
135135
firstTokenBytes.contentEquals(restoredFirstTokenBytes),
@@ -149,17 +149,17 @@ class SecureValueRecoveryBackupTest {
149149
val secondSecretData = secondResponse.nextBackupSecretData
150150
assertFalse(secondSecretData.isEmpty())
151151

152-
val secondRestoreResult = svrB.fetchForwardSecrecyTokenFromServer(
152+
val secondRestoreResult = svrB.restore(
153153
testBackupKey,
154154
secondResponse.metadata,
155155
).get(ASYNC_TIMEOUT_SECONDS, TimeUnit.SECONDS)
156156

157157
assertTrue("Second restore should succeed", secondRestoreResult.isSuccess)
158-
val restoredSecondToken = secondRestoreResult.getOrThrow()
159-
assertNotNull("Restored second token should not be null", restoredSecondToken)
158+
val restoredSecond = secondRestoreResult.getOrThrow()
159+
assertNotNull("Restored second token should not be null", restoredSecond)
160160

161161
val secondTokenBytes = secondToken.serialize()
162-
val restoredSecondTokenBytes = restoredSecondToken.serialize()
162+
val restoredSecondTokenBytes = restoredSecond.forwardSecrecyToken.serialize()
163163
assertTrue(
164164
"Restored second token should match stored token",
165165
secondTokenBytes.contentEquals(restoredSecondTokenBytes),

java/shared/java/org/signal/libsignal/internal/Native.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,14 @@ private Native() {}
206206
public static native byte[] BackupKey_DeriveMediaId(byte[] backupKey, String mediaName);
207207
public static native byte[] BackupKey_DeriveThumbnailTransitEncryptionKey(byte[] backupKey, byte[] mediaId);
208208

209-
public static native void BackupResponse_Destroy(long handle);
210-
public static native byte[] BackupResponse_GetForwardSecrecyToken(long response) throws Exception;
211-
public static native byte[] BackupResponse_GetNextBackupSecretData(long response);
212-
public static native byte[] BackupResponse_GetOpaqueMetadata(long response) throws Exception;
209+
public static native void BackupRestoreResponse_Destroy(long handle);
210+
public static native byte[] BackupRestoreResponse_GetForwardSecrecyToken(long response) throws Exception;
211+
public static native byte[] BackupRestoreResponse_GetNextBackupSecretData(long response);
212+
213+
public static native void BackupStoreResponse_Destroy(long handle);
214+
public static native byte[] BackupStoreResponse_GetForwardSecrecyToken(long response) throws Exception;
215+
public static native byte[] BackupStoreResponse_GetNextBackupSecretData(long response);
216+
public static native byte[] BackupStoreResponse_GetOpaqueMetadata(long response) throws Exception;
213217

214218
public static native void BridgedStringMap_Destroy(long handle);
215219
public static native void BridgedStringMap_insert(long map, String key, String value);
@@ -611,7 +615,7 @@ private Native() {}
611615
public static native byte[] SealedSessionCipher_MultiRecipientEncrypt(long[] recipients, long[] recipientSessions, byte[] excludedRecipients, long content, IdentityKeyStore identityKeyStore) throws Exception;
612616
public static native byte[] SealedSessionCipher_MultiRecipientMessageForSingleRecipient(byte[] encodedMultiRecipientMessage) throws Exception;
613617

614-
public static native CompletableFuture<byte[]> SecureValueRecoveryForBackups_RestoreBackupFromServer(long asyncRuntime, byte[] backupKey, byte[] metadata, long connectionManager, String username, String password);
618+
public static native CompletableFuture<Long> SecureValueRecoveryForBackups_RestoreBackupFromServer(long asyncRuntime, byte[] backupKey, byte[] metadata, long connectionManager, String username, String password);
615619
public static native CompletableFuture<Long> SecureValueRecoveryForBackups_StoreBackup(long asyncRuntime, byte[] backupKey, byte[] previousSecretData, long connectionManager, String username, String password);
616620

617621
public static native long SenderCertificate_Deserialize(byte[] data) throws Exception;

node/Native.d.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,11 @@ export function BackupKey_DeriveLocalBackupMetadataKey(backupKey: Uint8Array): U
228228
export function BackupKey_DeriveMediaEncryptionKey(backupKey: Uint8Array, mediaId: Uint8Array): Uint8Array;
229229
export function BackupKey_DeriveMediaId(backupKey: Uint8Array, mediaName: string): Uint8Array;
230230
export function BackupKey_DeriveThumbnailTransitEncryptionKey(backupKey: Uint8Array, mediaId: Uint8Array): Uint8Array;
231-
export function BackupResponse_GetForwardSecrecyToken(response: Wrapper<BackupResponse>): Uint8Array;
232-
export function BackupResponse_GetNextBackupSecretData(response: Wrapper<BackupResponse>): Uint8Array;
233-
export function BackupResponse_GetOpaqueMetadata(response: Wrapper<BackupResponse>): Uint8Array;
231+
export function BackupRestoreResponse_GetForwardSecrecyToken(response: Wrapper<BackupRestoreResponse>): Uint8Array;
232+
export function BackupRestoreResponse_GetNextBackupSecretData(response: Wrapper<BackupRestoreResponse>): Uint8Array;
233+
export function BackupStoreResponse_GetForwardSecrecyToken(response: Wrapper<BackupStoreResponse>): Uint8Array;
234+
export function BackupStoreResponse_GetNextBackupSecretData(response: Wrapper<BackupStoreResponse>): Uint8Array;
235+
export function BackupStoreResponse_GetOpaqueMetadata(response: Wrapper<BackupStoreResponse>): Uint8Array;
234236
export function BridgedStringMap_insert(map: Wrapper<BridgedStringMap>, key: string, value: string): void;
235237
export function BridgedStringMap_new(initialCapacity: number): BridgedStringMap;
236238
export function CallLinkAuthCredentialPresentation_CheckValidContents(presentationBytes: Uint8Array): void;
@@ -501,8 +503,8 @@ export function SealedSender_DecryptToUsmc(ctext: Uint8Array, identityStore: Ide
501503
export function SealedSender_Encrypt(destination: Wrapper<ProtocolAddress>, content: Wrapper<UnidentifiedSenderMessageContent>, identityKeyStore: IdentityKeyStore): Promise<Uint8Array>;
502504
export function SealedSender_MultiRecipientEncrypt(recipients: Wrapper<ProtocolAddress>[], recipientSessions: Wrapper<SessionRecord>[], excludedRecipients: Uint8Array, content: Wrapper<UnidentifiedSenderMessageContent>, identityKeyStore: IdentityKeyStore): Promise<Uint8Array>;
503505
export function SealedSender_MultiRecipientMessageForSingleRecipient(encodedMultiRecipientMessage: Uint8Array): Uint8Array;
504-
export function SecureValueRecoveryForBackups_RestoreBackupFromServer(asyncRuntime: Wrapper<TokioAsyncContext>, backupKey: Uint8Array, metadata: Uint8Array, connectionManager: Wrapper<ConnectionManager>, username: string, password: string): CancellablePromise<Uint8Array>;
505-
export function SecureValueRecoveryForBackups_StoreBackup(asyncRuntime: Wrapper<TokioAsyncContext>, backupKey: Uint8Array, previousSecretData: Uint8Array, connectionManager: Wrapper<ConnectionManager>, username: string, password: string): CancellablePromise<BackupResponse>;
506+
export function SecureValueRecoveryForBackups_RestoreBackupFromServer(asyncRuntime: Wrapper<TokioAsyncContext>, backupKey: Uint8Array, metadata: Uint8Array, connectionManager: Wrapper<ConnectionManager>, username: string, password: string): CancellablePromise<BackupRestoreResponse>;
507+
export function SecureValueRecoveryForBackups_StoreBackup(asyncRuntime: Wrapper<TokioAsyncContext>, backupKey: Uint8Array, previousSecretData: Uint8Array, connectionManager: Wrapper<ConnectionManager>, username: string, password: string): CancellablePromise<BackupStoreResponse>;
506508
export function SenderCertificate_Deserialize(data: Uint8Array): SenderCertificate;
507509
export function SenderCertificate_GetCertificate(obj: Wrapper<SenderCertificate>): Uint8Array;
508510
export function SenderCertificate_GetDeviceId(obj: Wrapper<SenderCertificate>): number;
@@ -719,7 +721,8 @@ export function initLogger(maxLevel: LogLevel, callback: (level: LogLevel, targe
719721
export function test_only_fn_returns_123(): number;
720722
interface Aes256GcmSiv { readonly __type: unique symbol; }
721723
interface AuthenticatedChatConnection { readonly __type: unique symbol; }
722-
interface BackupResponse { readonly __type: unique symbol; }
724+
interface BackupRestoreResponse { readonly __type: unique symbol; }
725+
interface BackupStoreResponse { readonly __type: unique symbol; }
723726
interface BridgedStringMap { readonly __type: unique symbol; }
724727
interface CdsiLookup { readonly __type: unique symbol; }
725728
interface ChatConnectionInfo { readonly __type: unique symbol; }

node/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)