Skip to content

Commit 417da32

Browse files
johnthecatclaude
andauthored
feat(server,platform): retain and expose session identity material (#403)
* feat(server,platform): retain and expose session identity material The v2 pairing handshake carries three values the core consumed and then discarded: the identity chat X25519 private key, the answering device's encryption public key, and the peer's statement account id. A host adopting core pairing had no way to reach them, so it had to keep a second duplicate pairing alive purely to harvest them. Retain rather than derive. The wallet chooses the chat key and every peer already addresses that identity by the matching public key, so a value derived host-side would name an identity nobody can reach. The pairing host provably cannot recompute it either: the derivation needs raw BIP-39 entropy, which only a signing host holds. Split the read paths by sensitivity. Public material rides SessionUiInfo (chat_public_key, device_enc_public_key, peer_statement_account_id) so a host that only addresses the identity never touches a secret. The secrets sit behind explicit CoreAdmin calls, because SessionUiInfo rides every AuthState broadcast to all registered AuthPresenters and would push them to hosts that never asked. Both secrets are reachable over wasm and UniFFI. Give the responder its own device encryption key. It previously advertised the SSO channel key as device_enc_pub_key, which makes every device sharing an identity indistinguishable. The key is now random and persisted under CoreStorageKey::DeviceEncryptionKey, matching the provider, and the CLI host anchors that slot to its bootstrap directory so a user switch cannot regenerate the identity peers address this install by. Verified field-for-field against the Android provider and the host-papp consumer: the handshake layout, the X25519 basepoint derivation of the chat public key, and the statement-signer origin of the peer account id all match. SessionInfo gains fields mid-struct, so its SCALE layout changes. The blob is unversioned and decode rejects short input, so sessions persisted by an earlier build no longer decode and those users re-pair once. Refs #384 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(server,host): close the device-key race and the empty-key fallback Move the device-key serialization guard from SigningHost onto RuntimeServices and bundle it with the read. The key is reachable from both roles now, so a guard owned by one of them left the CoreAdmin and pairing-runtime paths free to race a first-time read against pairing, generate a second secret, and overwrite the one peers were told to address. `device_encryption_secret` is the only way to reach the key, so no caller can forget to hold it. Reject instead of resolving with an empty key when the worker host runtime is disposed. Callers encrypt with whatever comes back, so a zero-length secret is the one answer that must never be handed out. * fix(server,host): address PR review nits Move the execution-kind doc comment onto execution_kind(), warn on a malformed stored device encryption key, and cover the provider's getDeviceEncryptionKey hex/rejection paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 00ab14e commit 417da32

27 files changed

Lines changed: 918 additions & 29 deletions

File tree

ios/truapi-host/Sources/TrUAPIHost/truapi_platform.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,23 @@ public struct SessionUiInfo: Equatable, Hashable {
11211121
* Wallet identity account id used for People-chain username lookup.
11221122
*/
11231123
public var identityAccountId: Bytes32?
1124+
/**
1125+
* X25519 public key addressing this identity in chat. Public counterpart
1126+
* of the key [`CoreAdmin::get_session_chat_identity_key`] serves.
1127+
*/
1128+
public var chatPublicKey: Bytes32?
1129+
/**
1130+
* X25519 public key of the wallet device that answered pairing. Hosts
1131+
* running their own encrypted device-sync channel key it against this.
1132+
*/
1133+
public var deviceEncPublicKey: Bytes32?
1134+
/**
1135+
* Statement-store account id the paired wallet signs every session-channel
1136+
* statement with. Whether it is scoped to the wallet device or to the
1137+
* wallet identity is the wallet's choice, so hosts must not treat it as a
1138+
* device discriminator; use [`Self::device_enc_public_key`] for that.
1139+
*/
1140+
public var peerStatementAccountId: Bytes32?
11241141
/**
11251142
* Short username from the People-chain identity record.
11261143
*/
@@ -1139,6 +1156,20 @@ public struct SessionUiInfo: Equatable, Hashable {
11391156
/**
11401157
* Wallet identity account id used for People-chain username lookup.
11411158
*/identityAccountId: Bytes32?,
1159+
/**
1160+
* X25519 public key addressing this identity in chat. Public counterpart
1161+
* of the key [`CoreAdmin::get_session_chat_identity_key`] serves.
1162+
*/chatPublicKey: Bytes32?,
1163+
/**
1164+
* X25519 public key of the wallet device that answered pairing. Hosts
1165+
* running their own encrypted device-sync channel key it against this.
1166+
*/deviceEncPublicKey: Bytes32?,
1167+
/**
1168+
* Statement-store account id the paired wallet signs every session-channel
1169+
* statement with. Whether it is scoped to the wallet device or to the
1170+
* wallet identity is the wallet's choice, so hosts must not treat it as a
1171+
* device discriminator; use [`Self::device_enc_public_key`] for that.
1172+
*/peerStatementAccountId: Bytes32?,
11421173
/**
11431174
* Short username from the People-chain identity record.
11441175
*/liteUsername: String?,
@@ -1147,6 +1178,9 @@ public struct SessionUiInfo: Equatable, Hashable {
11471178
*/fullUsername: String?) {
11481179
self.publicKey = publicKey
11491180
self.identityAccountId = identityAccountId
1181+
self.chatPublicKey = chatPublicKey
1182+
self.deviceEncPublicKey = deviceEncPublicKey
1183+
self.peerStatementAccountId = peerStatementAccountId
11501184
self.liteUsername = liteUsername
11511185
self.fullUsername = fullUsername
11521186
}
@@ -1169,6 +1203,9 @@ public struct FfiConverterTypeSessionUiInfo: FfiConverterRustBuffer {
11691203
try SessionUiInfo(
11701204
publicKey: FfiConverterTypeBytes32.read(from: &buf),
11711205
identityAccountId: FfiConverterOptionTypeBytes32.read(from: &buf),
1206+
chatPublicKey: FfiConverterOptionTypeBytes32.read(from: &buf),
1207+
deviceEncPublicKey: FfiConverterOptionTypeBytes32.read(from: &buf),
1208+
peerStatementAccountId: FfiConverterOptionTypeBytes32.read(from: &buf),
11721209
liteUsername: FfiConverterOptionString.read(from: &buf),
11731210
fullUsername: FfiConverterOptionString.read(from: &buf)
11741211
)
@@ -1177,6 +1214,9 @@ public struct FfiConverterTypeSessionUiInfo: FfiConverterRustBuffer {
11771214
public static func write(_ value: SessionUiInfo, into buf: inout [UInt8]) {
11781215
FfiConverterTypeBytes32.write(value.publicKey, into: &buf)
11791216
FfiConverterOptionTypeBytes32.write(value.identityAccountId, into: &buf)
1217+
FfiConverterOptionTypeBytes32.write(value.chatPublicKey, into: &buf)
1218+
FfiConverterOptionTypeBytes32.write(value.deviceEncPublicKey, into: &buf)
1219+
FfiConverterOptionTypeBytes32.write(value.peerStatementAccountId, into: &buf)
11801220
FfiConverterOptionString.write(value.liteUsername, into: &buf)
11811221
FfiConverterOptionString.write(value.fullUsername, into: &buf)
11821222
}

ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2432,6 +2432,12 @@ public func FfiConverterTypeNativeCustomRendererSubscription_lower(_ value: Nati
24322432
*/
24332433
public protocol NativeProductExecutionProtocol: AnyObject, Sendable {
24342434

2435+
/**
2436+
* Read this device's X25519 encryption secret, for device sync against a
2437+
* peer's `deviceEncPublicKey`. Generated and persisted on first read.
2438+
*/
2439+
func deviceEncryptionKey() throws -> Bytes32
2440+
24352441
/**
24362442
* Notify this execution's chain adapter that a connection closed.
24372443
*/
@@ -2473,6 +2479,12 @@ public protocol NativeProductExecutionProtocol: AnyObject, Sendable {
24732479
*/
24742480
func renderCustomMessage(messageId: String, messageType: String, payload: Data, observer: NativeCustomRendererObserver) throws -> NativeCustomRendererSubscription
24752481

2482+
/**
2483+
* Read the active session's X25519 chat identity private key, or `None`
2484+
* when no session is active.
2485+
*/
2486+
func sessionChatIdentityKey() throws -> Bytes32?
2487+
24762488
/**
24772489
* Update a product-scoped permission authorization.
24782490
*/
@@ -2554,6 +2566,19 @@ open class NativeProductExecution: NativeProductExecutionProtocol, @unchecked Se
25542566

25552567

25562568

2569+
/**
2570+
* Read this device's X25519 encryption secret, for device sync against a
2571+
* peer's `deviceEncPublicKey`. Generated and persisted on first read.
2572+
*/
2573+
open func deviceEncryptionKey()throws -> Bytes32 {
2574+
return try FfiConverterTypeBytes32_lift(try rustCallWithError(FfiConverterTypeHostRejection_lift) {
2575+
uniffiCallStatus in
2576+
uniffi_truapi_server_fn_method_nativeproductexecution_device_encryption_key(
2577+
self.uniffiCloneHandle(),uniffiCallStatus
2578+
)
2579+
})
2580+
}
2581+
25572582
/**
25582583
* Notify this execution's chain adapter that a connection closed.
25592584
*/
@@ -2656,6 +2681,19 @@ open func renderCustomMessage(messageId: String, messageType: String, payload: D
26562681
FfiConverterCallbackInterfaceNativeCustomRendererObserver_lower(observer),uniffiCallStatus
26572682
)
26582683
})
2684+
}
2685+
2686+
/**
2687+
* Read the active session's X25519 chat identity private key, or `None`
2688+
* when no session is active.
2689+
*/
2690+
open func sessionChatIdentityKey()throws -> Bytes32? {
2691+
return try FfiConverterOptionTypeBytes32.lift(try rustCallWithError(FfiConverterTypeHostRejection_lift) {
2692+
uniffiCallStatus in
2693+
uniffi_truapi_server_fn_method_nativeproductexecution_session_chat_identity_key(
2694+
self.uniffiCloneHandle(),uniffiCallStatus
2695+
)
2696+
})
26592697
}
26602698

26612699
/**
@@ -5831,6 +5869,30 @@ fileprivate struct FfiConverterOptionTypeProductAccountId: FfiConverterRustBuffe
58315869
}
58325870
}
58335871

5872+
#if swift(>=5.8)
5873+
@_documentation(visibility: private)
5874+
#endif
5875+
fileprivate struct FfiConverterOptionTypeBytes32: FfiConverterRustBuffer {
5876+
typealias SwiftType = Bytes32?
5877+
5878+
public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
5879+
guard let value = value else {
5880+
writeInt(&buf, Int8(0))
5881+
return
5882+
}
5883+
writeInt(&buf, Int8(1))
5884+
FfiConverterTypeBytes32.write(value, into: &buf)
5885+
}
5886+
5887+
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
5888+
switch try readInt(&buf) as Int8 {
5889+
case 0: return nil
5890+
case 1: return try FfiConverterTypeBytes32.read(from: &buf)
5891+
default: throw UniffiInternalError.unexpectedOptionalTag
5892+
}
5893+
}
5894+
}
5895+
58345896
#if swift(>=5.8)
58355897
@_documentation(visibility: private)
58365898
#endif
@@ -6217,6 +6279,9 @@ private let initializationResult: InitializationResult = {
62176279
if (uniffi_truapi_server_checksum_method_nativechatcallbacks_list_rooms() != 21374) {
62186280
return InitializationResult.apiChecksumMismatch
62196281
}
6282+
if (uniffi_truapi_server_checksum_method_nativeproductexecution_device_encryption_key() != 18707) {
6283+
return InitializationResult.apiChecksumMismatch
6284+
}
62206285
if (uniffi_truapi_server_checksum_method_nativeproductexecution_notify_chain_closed() != 59343) {
62216286
return InitializationResult.apiChecksumMismatch
62226287
}
@@ -6241,6 +6306,9 @@ private let initializationResult: InitializationResult = {
62416306
if (uniffi_truapi_server_checksum_method_nativeproductexecution_render_custom_message() != 17716) {
62426307
return InitializationResult.apiChecksumMismatch
62436308
}
6309+
if (uniffi_truapi_server_checksum_method_nativeproductexecution_session_chat_identity_key() != 3903) {
6310+
return InitializationResult.apiChecksumMismatch
6311+
}
62446312
if (uniffi_truapi_server_checksum_method_nativeproductexecution_set_permission_authorization_status() != 14164) {
62456313
return InitializationResult.apiChecksumMismatch
62466314
}

ios/truapi-host/Sources/truapi_serverFFI/include/truapi_serverFFI.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,11 @@ uint64_t uniffi_truapi_server_fn_clone_nativeproductexecution(uint64_t handle, R
641641
void uniffi_truapi_server_fn_free_nativeproductexecution(uint64_t handle, RustCallStatus *_Nonnull out_status
642642
);
643643
#endif
644+
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVEPRODUCTEXECUTION_DEVICE_ENCRYPTION_KEY
645+
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVEPRODUCTEXECUTION_DEVICE_ENCRYPTION_KEY
646+
RustBuffer uniffi_truapi_server_fn_method_nativeproductexecution_device_encryption_key(uint64_t ptr, RustCallStatus *_Nonnull out_status
647+
);
648+
#endif
644649
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVEPRODUCTEXECUTION_NOTIFY_CHAIN_CLOSED
645650
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVEPRODUCTEXECUTION_NOTIFY_CHAIN_CLOSED
646651
void uniffi_truapi_server_fn_method_nativeproductexecution_notify_chain_closed(uint64_t ptr, uint32_t connection_id, RustCallStatus *_Nonnull out_status
@@ -681,6 +686,11 @@ void uniffi_truapi_server_fn_method_nativeproductexecution_publish_chat_action(u
681686
uint64_t uniffi_truapi_server_fn_method_nativeproductexecution_render_custom_message(uint64_t ptr, RustBuffer message_id, RustBuffer message_type, RustBuffer payload, uint64_t observer, RustCallStatus *_Nonnull out_status
682687
);
683688
#endif
689+
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVEPRODUCTEXECUTION_SESSION_CHAT_IDENTITY_KEY
690+
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVEPRODUCTEXECUTION_SESSION_CHAT_IDENTITY_KEY
691+
RustBuffer uniffi_truapi_server_fn_method_nativeproductexecution_session_chat_identity_key(uint64_t ptr, RustCallStatus *_Nonnull out_status
692+
);
693+
#endif
684694
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVEPRODUCTEXECUTION_SET_PERMISSION_AUTHORIZATION_STATUS
685695
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVEPRODUCTEXECUTION_SET_PERMISSION_AUTHORIZATION_STATUS
686696
void uniffi_truapi_server_fn_method_nativeproductexecution_set_permission_authorization_status(uint64_t ptr, RustBuffer request, RustBuffer status, RustCallStatus *_Nonnull out_status
@@ -1321,6 +1331,12 @@ uint16_t uniffi_truapi_server_checksum_method_nativechatcallbacks_post_custom_me
13211331
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVECHATCALLBACKS_LIST_ROOMS
13221332
uint16_t uniffi_truapi_server_checksum_method_nativechatcallbacks_list_rooms(void
13231333

1334+
);
1335+
#endif
1336+
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVEPRODUCTEXECUTION_DEVICE_ENCRYPTION_KEY
1337+
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVEPRODUCTEXECUTION_DEVICE_ENCRYPTION_KEY
1338+
uint16_t uniffi_truapi_server_checksum_method_nativeproductexecution_device_encryption_key(void
1339+
13241340
);
13251341
#endif
13261342
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVEPRODUCTEXECUTION_NOTIFY_CHAIN_CLOSED
@@ -1369,6 +1385,12 @@ uint16_t uniffi_truapi_server_checksum_method_nativeproductexecution_publish_cha
13691385
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVEPRODUCTEXECUTION_RENDER_CUSTOM_MESSAGE
13701386
uint16_t uniffi_truapi_server_checksum_method_nativeproductexecution_render_custom_message(void
13711387

1388+
);
1389+
#endif
1390+
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVEPRODUCTEXECUTION_SESSION_CHAT_IDENTITY_KEY
1391+
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVEPRODUCTEXECUTION_SESSION_CHAT_IDENTITY_KEY
1392+
uint16_t uniffi_truapi_server_checksum_method_nativeproductexecution_session_chat_identity_key(void
1393+
13721394
);
13731395
#endif
13741396
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVEPRODUCTEXECUTION_SET_PERMISSION_AUTHORIZATION_STATUS

js/packages/truapi-host/src/wasm-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export interface WorkerPairingHostRuntime extends PermissionAuthorizationRuntime
2121
disconnectSession(): Promise<void>;
2222
cancelPairing(): void;
2323
notifySessionStoreChanged(): void;
24+
sessionChatIdentityKey(): Uint8Array | undefined;
25+
deviceEncryptionKey(): Promise<Uint8Array>;
2426
activateStoredSession(): Promise<void>;
2527
activateExternalSession(blob: Uint8Array): Promise<void>;
2628
resetSessionState(): Promise<void>;

0 commit comments

Comments
 (0)