@novasamatech/host-papp 0.8.5 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sso/auth/impl.d.ts +2 -7
- package/dist/sso/auth/impl.js +9 -4
- package/dist/sso/auth/scale/handshakeV2.d.ts +12 -113
- package/dist/sso/auth/scale/handshakeV2.js +15 -132
- package/dist/sso/auth/v2/service.d.ts +4 -0
- package/dist/sso/auth/v2/service.js +4 -5
- package/dist/sso/auth/v2/state.d.ts +7 -37
- package/dist/sso/auth/v2/state.js +4 -3
- package/dist/sso/userSecretRepository.d.ts +0 -1
- package/dist/sso/userSecretRepository.js +2 -11
- package/dist/sso/userSessionRepository.d.ts +53 -40
- package/dist/sso/userSessionRepository.js +15 -22
- package/package.json +5 -5
package/dist/sso/auth/impl.d.ts
CHANGED
|
@@ -17,13 +17,8 @@ export type AuthComponent = ReturnType<typeof createAuth>;
|
|
|
17
17
|
export type OnAuthSuccess = (event: {
|
|
18
18
|
session: StoredUserSession;
|
|
19
19
|
identityChatPrivateKey: Uint8Array;
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
* shipped a pre-v0.2.2 `HandshakeSuccessV2` body (no `sso_encr_pub_key`
|
|
23
|
-
* field on the wire). The host's SSO session transport stays inactive
|
|
24
|
-
* while null; chat is unaffected since it uses `identityChatPrivateKey`.
|
|
25
|
-
*/
|
|
26
|
-
ssoEncPubKey: Uint8Array | null;
|
|
20
|
+
/** `papp_encr_pub` — PApp's P-256 SSO ECDH public key. */
|
|
21
|
+
ssoEncPubKey: Uint8Array;
|
|
27
22
|
}) => Promise<void> | void;
|
|
28
23
|
type Params = {
|
|
29
24
|
hostMetadata?: HostMetadata;
|
package/dist/sso/auth/impl.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createAccountId, createLocalSessionAccount, createRemoteSessionAccount } from '@novasamatech/statement-store';
|
|
2
2
|
import { ResultAsync, errAsync, okAsync } from 'neverthrow';
|
|
3
|
+
import { createSharedSecret } from '../../crypto.js';
|
|
3
4
|
import { createFlowId, emitHostPappDebugMessage } from '../../debugBus.js';
|
|
4
5
|
import { AbortError } from '../../helpers/abortError.js';
|
|
5
6
|
import { createState, readonly } from '../../helpers/state.js';
|
|
@@ -148,17 +149,21 @@ export function createAuth({ hostMetadata, deviceIdentity, deviceIdentityStore,
|
|
|
148
149
|
};
|
|
149
150
|
function persistAndNotify(identity, success, _flowId) {
|
|
150
151
|
const localAccount = createLocalSessionAccount(createAccountId(identity.statementAccountPublicKey));
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
if (!success.peerStatementAccountId) {
|
|
153
|
+
return errAsync(new Error("Can't derive peerStatementAccountId from statement proof signer"));
|
|
154
|
+
}
|
|
155
|
+
const sharedSecret = createSharedSecret(identity.encryptionPrivateKey, success.ssoEncPubKey);
|
|
156
|
+
const remoteAccount = createRemoteSessionAccount(createAccountId(success.peerStatementAccountId), sharedSecret);
|
|
157
|
+
const session = createStoredUserSession(localAccount, remoteAccount, createAccountId(success.rootAccountId), {
|
|
153
158
|
identityAccountId: createAccountId(success.identityAccountId),
|
|
154
159
|
identityChatPublicKey: success.identityChatPublicKey,
|
|
155
|
-
ssoEncPubKey: success.ssoEncPubKey
|
|
160
|
+
ssoEncPubKey: success.ssoEncPubKey,
|
|
161
|
+
rootEntropySource: success.rootEntropySource,
|
|
156
162
|
});
|
|
157
163
|
return userSecretRepository
|
|
158
164
|
.write(session.id, {
|
|
159
165
|
ssSecret: identity.statementAccountSecret,
|
|
160
166
|
encrSecret: identity.encryptionPrivateKey,
|
|
161
|
-
entropy: new Uint8Array(0),
|
|
162
167
|
identityChatPrivateKey: success.identityChatPrivateKey,
|
|
163
168
|
})
|
|
164
169
|
.andThen(() => ssoSessionRepository.add(session))
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* `Success` carries:
|
|
12
12
|
* - `identityAccountId` — user identity sr25519 accountId (32 bytes).
|
|
13
|
-
*
|
|
13
|
+
* Addressing for chat / username lookup / session
|
|
14
14
|
* topic derivation.
|
|
15
15
|
* - `rootAccountId` — user root sr25519 accountId (32 bytes). Parent
|
|
16
16
|
* for soft-derivation of product accounts; PApp
|
|
@@ -23,11 +23,13 @@
|
|
|
23
23
|
* P-256 uncompressed). Tells the host which key to
|
|
24
24
|
* use when addressing chat envelopes back to the
|
|
25
25
|
* authorising PApp device.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* - `ssoEncPubKey` — `papp_encr_pub` (65 bytes, P-256 uncompressed); see
|
|
27
|
+
* the `HandshakeSuccessV2` codec doc below.
|
|
28
|
+
* - `rootEntropySource` — 32 bytes; `blake2b256_keyed(rootAccountSecret,
|
|
29
|
+
* "product-entropy-derivation")` per RFC-0007 (layer 1).
|
|
30
|
+
* Lets the host derive product entropy deterministically
|
|
31
|
+
* (`host_derive_entropy`) without ever holding the raw
|
|
32
|
+
* root account secret. See the codec doc below.
|
|
31
33
|
*/
|
|
32
34
|
export declare const MetadataKey: import("scale-ts").Codec<{
|
|
33
35
|
tag: "Custom";
|
|
@@ -96,10 +98,6 @@ export declare const HandshakeProposalV2: import("scale-ts").Codec<{
|
|
|
96
98
|
value: undefined;
|
|
97
99
|
}, string][];
|
|
98
100
|
}>;
|
|
99
|
-
/**
|
|
100
|
-
* V2 lives at SCALE discriminant 1; index 0 is reserved for legacy V1 which
|
|
101
|
-
* neither side emits. The `_v1Reserved` slot pushes V2 to discriminant 1.
|
|
102
|
-
*/
|
|
103
101
|
export declare const VersionedHandshakeProposal: import("scale-ts").Codec<{
|
|
104
102
|
tag: "_v1Reserved";
|
|
105
103
|
value: undefined;
|
|
@@ -131,121 +129,21 @@ export declare const VersionedHandshakeProposal: import("scale-ts").Codec<{
|
|
|
131
129
|
}, string][];
|
|
132
130
|
};
|
|
133
131
|
}>;
|
|
134
|
-
/**
|
|
135
|
-
* 32 + 32 + 32 + 65 + 65 = 226 bytes (spec v0.2.2).
|
|
136
|
-
*
|
|
137
|
-
* `ssoEncPubKey` is `papp_encr_pub` per spec § Encrypted response — V2 — the
|
|
138
|
-
* P-256 public key PApp uses for SSO session ECDH. Required to compute
|
|
139
|
-
* `shared_secret_session = ECDH(host_encr_secret, ssoEncPubKey)`. PApp keypair
|
|
140
|
-
* derivation is implementation-defined: Android derives under `//wallet//sso`
|
|
141
|
-
* (`SsoDerivationDomains.SSO_DERIVATION_DOMAIN`); iOS currently reuses
|
|
142
|
-
* `//wallet//chat`. The host treats this field as opaque public material —
|
|
143
|
-
* which keypair PApp picked is invisible here.
|
|
144
|
-
*/
|
|
145
132
|
export declare const HandshakeSuccessV2: import("scale-ts").Codec<{
|
|
146
133
|
identityAccountId: Uint8Array<ArrayBufferLike>;
|
|
147
134
|
rootAccountId: Uint8Array<ArrayBufferLike>;
|
|
148
135
|
identityChatPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
149
136
|
ssoEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
150
137
|
deviceEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
138
|
+
rootEntropySource: Uint8Array<ArrayBufferLike>;
|
|
151
139
|
}>;
|
|
152
|
-
|
|
153
|
-
identityAccountId: Uint8Array;
|
|
154
|
-
/** Nullable for v0.2 peers (Android `feature/location-for-handshake`). */
|
|
155
|
-
rootAccountId: Uint8Array | null;
|
|
156
|
-
identityChatPrivateKey: Uint8Array;
|
|
157
|
-
/**
|
|
158
|
-
* Nullable for v0.2 / v0.2.1 peers (any PApp build that does not yet ship
|
|
159
|
-
* spec v0.2.2). The host's SSO session transport stays inactive while
|
|
160
|
-
* null — sign/vrf/etc operations fail at the boundary until PApp emits
|
|
161
|
-
* the new field.
|
|
162
|
-
*/
|
|
163
|
-
ssoEncPubKey: Uint8Array | null;
|
|
164
|
-
deviceEncPubKey: Uint8Array;
|
|
165
|
-
};
|
|
166
|
-
/** 32 + 32 + 32 + 65 = 161 bytes (spec v0.2.1) */
|
|
167
|
-
export declare const HandshakeSuccessV2_v021: import("scale-ts").Codec<{
|
|
168
|
-
identityAccountId: Uint8Array<ArrayBufferLike>;
|
|
169
|
-
rootAccountId: Uint8Array<ArrayBufferLike>;
|
|
170
|
-
identityChatPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
171
|
-
deviceEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
172
|
-
}>;
|
|
173
|
-
/** 32 + 32 + 65 = 129 bytes (spec v0.2 — Android `feature/location-for-handshake`) */
|
|
174
|
-
export declare const HandshakeSuccessV2Legacy: import("scale-ts").Codec<{
|
|
175
|
-
identityAccountId: Uint8Array<ArrayBufferLike>;
|
|
176
|
-
identityChatPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
177
|
-
deviceEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
178
|
-
}>;
|
|
179
|
-
export type DecodedHandshakeResponseV2 = {
|
|
180
|
-
tag: 'Pending';
|
|
181
|
-
value: {
|
|
182
|
-
tag: 'AllowanceAllocation';
|
|
183
|
-
value: undefined;
|
|
184
|
-
};
|
|
185
|
-
} | {
|
|
186
|
-
tag: 'Success';
|
|
187
|
-
value: HandshakeSuccessV2Value;
|
|
188
|
-
} | {
|
|
189
|
-
tag: 'Failed';
|
|
190
|
-
value: string;
|
|
191
|
-
};
|
|
192
|
-
/**
|
|
193
|
-
* Length-dispatched decoder for the inner `EncryptedHandshakeResponseV2`
|
|
194
|
-
* plaintext. Three Success body shapes are accepted, one per spec rev:
|
|
195
|
-
*
|
|
196
|
-
* 226 bytes (spec v0.2.2) — includes `ssoEncPubKey` (`papp_encr_pub`).
|
|
197
|
-
* 161 bytes (spec v0.2.1) — `ssoEncPubKey` absent; surfaced as `null`.
|
|
198
|
-
* 129 bytes (spec v0.2) — `rootAccountId` AND `ssoEncPubKey` absent.
|
|
199
|
-
*
|
|
200
|
-
* Older peers degrade gracefully: chat continues to work via
|
|
201
|
-
* `identityChatPrivateKey`, but the SSO session transport (which needs
|
|
202
|
-
* `ssoEncPubKey` to derive `shared_secret_session`) stays inactive on
|
|
203
|
-
* the host until the peer upgrades.
|
|
204
|
-
*/
|
|
205
|
-
export declare const decodeEncryptedHandshakeResponseV2: (bytes: Uint8Array) => DecodedHandshakeResponseV2;
|
|
206
|
-
/**
|
|
207
|
-
* Derive the user identity chat P-256 public key from the shared private
|
|
208
|
-
* scalar received in `HandshakeSuccessV2`. Both desktop and mobile must derive
|
|
209
|
-
* identically (uncompressed 65-byte form) so downstream session topics agree.
|
|
210
|
-
*/
|
|
140
|
+
/** Derive the identity chat P-256 public key (uncompressed) from its private scalar. */
|
|
211
141
|
export declare const deriveIdentityChatPublicKey: (privateKey: Uint8Array) => Uint8Array;
|
|
212
|
-
/**
|
|
213
|
-
* Inner Pending sub-statuses. Only `AllowanceAllocation` today.
|
|
214
|
-
*
|
|
215
|
-
* Encoded with its own SCALE discriminant byte — the peer's SCALE library does
|
|
216
|
-
* NOT elide enum-variant indices for sealed interfaces (verified on the wire:
|
|
217
|
-
* `Pending(AllowanceAllocation)` arrives as `0x00 0x00`). Both sides must keep
|
|
218
|
-
* the discriminant when encoding so dispatch agrees.
|
|
219
|
-
*/
|
|
220
142
|
export declare const HandshakeStatusV2: import("scale-ts").Codec<{
|
|
221
143
|
tag: "AllowanceAllocation";
|
|
222
144
|
value: undefined;
|
|
223
145
|
}>;
|
|
224
|
-
|
|
225
|
-
tag: 'Pending';
|
|
226
|
-
value: {
|
|
227
|
-
tag: 'AllowanceAllocation';
|
|
228
|
-
value: undefined;
|
|
229
|
-
};
|
|
230
|
-
} | {
|
|
231
|
-
tag: 'Success';
|
|
232
|
-
value: HandshakeSuccessV2Value;
|
|
233
|
-
} | {
|
|
234
|
-
tag: 'Failed';
|
|
235
|
-
value: string;
|
|
236
|
-
};
|
|
237
|
-
/**
|
|
238
|
-
* Inner handshake response variant. Wire shape (matches peer SCALE encoding):
|
|
239
|
-
*
|
|
240
|
-
* - Pending → 0x00 || HandshakeStatusV2 (today: 0x00 for AllowanceAllocation), 2 bytes total
|
|
241
|
-
* - Success → 0x01 || HandshakeSuccessV2 (161 bytes), 162 bytes total
|
|
242
|
-
* - Failed → 0x02 || SCALE-encoded UTF-8 reason string
|
|
243
|
-
*
|
|
244
|
-
* Earlier builds shipped a custom length-dispatched codec assuming elision —
|
|
245
|
-
* that assumption was wrong and produced false `Failed("")` reads on every
|
|
246
|
-
* `Pending` response. Native scale-ts `Enum` preserves the discriminant and
|
|
247
|
-
* matches the peer's wire format directly.
|
|
248
|
-
*/
|
|
146
|
+
/** Inner handshake response variant. */
|
|
249
147
|
export declare const EncryptedHandshakeResponseV2: import("scale-ts").Codec<{
|
|
250
148
|
tag: "Failed";
|
|
251
149
|
value: string;
|
|
@@ -263,6 +161,7 @@ export declare const EncryptedHandshakeResponseV2: import("scale-ts").Codec<{
|
|
|
263
161
|
identityChatPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
264
162
|
ssoEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
265
163
|
deviceEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
164
|
+
rootEntropySource: Uint8Array<ArrayBufferLike>;
|
|
266
165
|
};
|
|
267
166
|
}>;
|
|
268
167
|
export declare const HandshakeResponseV2: import("scale-ts").Codec<{
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* `Success` carries:
|
|
12
12
|
* - `identityAccountId` — user identity sr25519 accountId (32 bytes).
|
|
13
|
-
*
|
|
13
|
+
* Addressing for chat / username lookup / session
|
|
14
14
|
* topic derivation.
|
|
15
15
|
* - `rootAccountId` — user root sr25519 accountId (32 bytes). Parent
|
|
16
16
|
* for soft-derivation of product accounts; PApp
|
|
@@ -23,18 +23,20 @@
|
|
|
23
23
|
* P-256 uncompressed). Tells the host which key to
|
|
24
24
|
* use when addressing chat envelopes back to the
|
|
25
25
|
* authorising PApp device.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* - `ssoEncPubKey` — `papp_encr_pub` (65 bytes, P-256 uncompressed); see
|
|
27
|
+
* the `HandshakeSuccessV2` codec doc below.
|
|
28
|
+
* - `rootEntropySource` — 32 bytes; `blake2b256_keyed(rootAccountSecret,
|
|
29
|
+
* "product-entropy-derivation")` per RFC-0007 (layer 1).
|
|
30
|
+
* Lets the host derive product entropy deterministically
|
|
31
|
+
* (`host_derive_entropy`) without ever holding the raw
|
|
32
|
+
* root account secret. See the codec doc below.
|
|
31
33
|
*/
|
|
32
34
|
import { p256 } from '@noble/curves/nist.js';
|
|
33
35
|
import { Bytes, Enum, Struct, Tuple, Vector, _void, str } from 'scale-ts';
|
|
34
36
|
const AccountIdCodec = Bytes(32);
|
|
35
37
|
const PublicKeyCodec = Bytes(65);
|
|
36
38
|
const PrivateKeyCodec = Bytes(32);
|
|
37
|
-
|
|
39
|
+
const EntropySourceCodec = Bytes(32);
|
|
38
40
|
export const MetadataKey = Enum({
|
|
39
41
|
Custom: str,
|
|
40
42
|
HostName: _void,
|
|
@@ -52,146 +54,27 @@ export const HandshakeProposalV2 = Struct({
|
|
|
52
54
|
device: Device,
|
|
53
55
|
metadata: Vector(MetadataEntry),
|
|
54
56
|
});
|
|
55
|
-
/**
|
|
56
|
-
* V2 lives at SCALE discriminant 1; index 0 is reserved for legacy V1 which
|
|
57
|
-
* neither side emits. The `_v1Reserved` slot pushes V2 to discriminant 1.
|
|
58
|
-
*/
|
|
59
57
|
export const VersionedHandshakeProposal = Enum({
|
|
60
58
|
_v1Reserved: _void,
|
|
61
59
|
V2: HandshakeProposalV2,
|
|
62
60
|
});
|
|
63
|
-
//
|
|
64
|
-
/**
|
|
65
|
-
* 32 + 32 + 32 + 65 + 65 = 226 bytes (spec v0.2.2).
|
|
66
|
-
*
|
|
67
|
-
* `ssoEncPubKey` is `papp_encr_pub` per spec § Encrypted response — V2 — the
|
|
68
|
-
* P-256 public key PApp uses for SSO session ECDH. Required to compute
|
|
69
|
-
* `shared_secret_session = ECDH(host_encr_secret, ssoEncPubKey)`. PApp keypair
|
|
70
|
-
* derivation is implementation-defined: Android derives under `//wallet//sso`
|
|
71
|
-
* (`SsoDerivationDomains.SSO_DERIVATION_DOMAIN`); iOS currently reuses
|
|
72
|
-
* `//wallet//chat`. The host treats this field as opaque public material —
|
|
73
|
-
* which keypair PApp picked is invisible here.
|
|
74
|
-
*/
|
|
61
|
+
// V2 Response
|
|
75
62
|
export const HandshakeSuccessV2 = Struct({
|
|
76
63
|
identityAccountId: AccountIdCodec,
|
|
77
64
|
rootAccountId: AccountIdCodec,
|
|
78
65
|
identityChatPrivateKey: PrivateKeyCodec,
|
|
66
|
+
/** PApp's P-256 SSO ECDH public key (`papp_encr_pub`). */
|
|
79
67
|
ssoEncPubKey: PublicKeyCodec,
|
|
80
68
|
deviceEncPubKey: PublicKeyCodec,
|
|
69
|
+
/** Layer-1 source for deterministic product entropy derivation (RFC-0007). */
|
|
70
|
+
rootEntropySource: EntropySourceCodec,
|
|
81
71
|
});
|
|
82
|
-
/**
|
|
83
|
-
export const HandshakeSuccessV2_v021 = Struct({
|
|
84
|
-
identityAccountId: AccountIdCodec,
|
|
85
|
-
rootAccountId: AccountIdCodec,
|
|
86
|
-
identityChatPrivateKey: PrivateKeyCodec,
|
|
87
|
-
deviceEncPubKey: PublicKeyCodec,
|
|
88
|
-
});
|
|
89
|
-
/** 32 + 32 + 65 = 129 bytes (spec v0.2 — Android `feature/location-for-handshake`) */
|
|
90
|
-
export const HandshakeSuccessV2Legacy = Struct({
|
|
91
|
-
identityAccountId: AccountIdCodec,
|
|
92
|
-
identityChatPrivateKey: PrivateKeyCodec,
|
|
93
|
-
deviceEncPubKey: PublicKeyCodec,
|
|
94
|
-
});
|
|
95
|
-
/**
|
|
96
|
-
* Length-dispatched decoder for the inner `EncryptedHandshakeResponseV2`
|
|
97
|
-
* plaintext. Three Success body shapes are accepted, one per spec rev:
|
|
98
|
-
*
|
|
99
|
-
* 226 bytes (spec v0.2.2) — includes `ssoEncPubKey` (`papp_encr_pub`).
|
|
100
|
-
* 161 bytes (spec v0.2.1) — `ssoEncPubKey` absent; surfaced as `null`.
|
|
101
|
-
* 129 bytes (spec v0.2) — `rootAccountId` AND `ssoEncPubKey` absent.
|
|
102
|
-
*
|
|
103
|
-
* Older peers degrade gracefully: chat continues to work via
|
|
104
|
-
* `identityChatPrivateKey`, but the SSO session transport (which needs
|
|
105
|
-
* `ssoEncPubKey` to derive `shared_secret_session`) stays inactive on
|
|
106
|
-
* the host until the peer upgrades.
|
|
107
|
-
*/
|
|
108
|
-
export const decodeEncryptedHandshakeResponseV2 = (bytes) => {
|
|
109
|
-
if (bytes.length === 0)
|
|
110
|
-
throw new Error('EncryptedHandshakeResponseV2: empty plaintext');
|
|
111
|
-
const tag = bytes[0];
|
|
112
|
-
// `slice` not `subarray` — scale-ts decoders read from buffer byteOffset 0.
|
|
113
|
-
const body = bytes.slice(1);
|
|
114
|
-
if (tag === 0) {
|
|
115
|
-
if (body.length === 0)
|
|
116
|
-
throw new Error('EncryptedHandshakeResponseV2: Pending body empty');
|
|
117
|
-
return { tag: 'Pending', value: { tag: 'AllowanceAllocation', value: undefined } };
|
|
118
|
-
}
|
|
119
|
-
if (tag === 1) {
|
|
120
|
-
if (body.length === 226) {
|
|
121
|
-
const decoded = HandshakeSuccessV2.dec(body);
|
|
122
|
-
return {
|
|
123
|
-
tag: 'Success',
|
|
124
|
-
value: {
|
|
125
|
-
identityAccountId: decoded.identityAccountId,
|
|
126
|
-
rootAccountId: decoded.rootAccountId,
|
|
127
|
-
identityChatPrivateKey: decoded.identityChatPrivateKey,
|
|
128
|
-
ssoEncPubKey: decoded.ssoEncPubKey,
|
|
129
|
-
deviceEncPubKey: decoded.deviceEncPubKey,
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
if (body.length === 161) {
|
|
134
|
-
const decoded = HandshakeSuccessV2_v021.dec(body);
|
|
135
|
-
return {
|
|
136
|
-
tag: 'Success',
|
|
137
|
-
value: {
|
|
138
|
-
identityAccountId: decoded.identityAccountId,
|
|
139
|
-
rootAccountId: decoded.rootAccountId,
|
|
140
|
-
identityChatPrivateKey: decoded.identityChatPrivateKey,
|
|
141
|
-
ssoEncPubKey: null,
|
|
142
|
-
deviceEncPubKey: decoded.deviceEncPubKey,
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
if (body.length === 129) {
|
|
147
|
-
const decoded = HandshakeSuccessV2Legacy.dec(body);
|
|
148
|
-
return {
|
|
149
|
-
tag: 'Success',
|
|
150
|
-
value: {
|
|
151
|
-
identityAccountId: decoded.identityAccountId,
|
|
152
|
-
rootAccountId: null,
|
|
153
|
-
identityChatPrivateKey: decoded.identityChatPrivateKey,
|
|
154
|
-
ssoEncPubKey: null,
|
|
155
|
-
deviceEncPubKey: decoded.deviceEncPubKey,
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
throw new Error(`EncryptedHandshakeResponseV2: Success body length ${body.length} not in {129, 161, 226}`);
|
|
160
|
-
}
|
|
161
|
-
if (tag === 2) {
|
|
162
|
-
return { tag: 'Failed', value: str.dec(body) };
|
|
163
|
-
}
|
|
164
|
-
throw new Error(`EncryptedHandshakeResponseV2: unknown variant tag ${tag}`);
|
|
165
|
-
};
|
|
166
|
-
/**
|
|
167
|
-
* Derive the user identity chat P-256 public key from the shared private
|
|
168
|
-
* scalar received in `HandshakeSuccessV2`. Both desktop and mobile must derive
|
|
169
|
-
* identically (uncompressed 65-byte form) so downstream session topics agree.
|
|
170
|
-
*/
|
|
72
|
+
/** Derive the identity chat P-256 public key (uncompressed) from its private scalar. */
|
|
171
73
|
export const deriveIdentityChatPublicKey = (privateKey) => p256.getPublicKey(privateKey, false);
|
|
172
|
-
/**
|
|
173
|
-
* Inner Pending sub-statuses. Only `AllowanceAllocation` today.
|
|
174
|
-
*
|
|
175
|
-
* Encoded with its own SCALE discriminant byte — the peer's SCALE library does
|
|
176
|
-
* NOT elide enum-variant indices for sealed interfaces (verified on the wire:
|
|
177
|
-
* `Pending(AllowanceAllocation)` arrives as `0x00 0x00`). Both sides must keep
|
|
178
|
-
* the discriminant when encoding so dispatch agrees.
|
|
179
|
-
*/
|
|
180
74
|
export const HandshakeStatusV2 = Enum({
|
|
181
75
|
AllowanceAllocation: _void,
|
|
182
76
|
});
|
|
183
|
-
/**
|
|
184
|
-
* Inner handshake response variant. Wire shape (matches peer SCALE encoding):
|
|
185
|
-
*
|
|
186
|
-
* - Pending → 0x00 || HandshakeStatusV2 (today: 0x00 for AllowanceAllocation), 2 bytes total
|
|
187
|
-
* - Success → 0x01 || HandshakeSuccessV2 (161 bytes), 162 bytes total
|
|
188
|
-
* - Failed → 0x02 || SCALE-encoded UTF-8 reason string
|
|
189
|
-
*
|
|
190
|
-
* Earlier builds shipped a custom length-dispatched codec assuming elision —
|
|
191
|
-
* that assumption was wrong and produced false `Failed("")` reads on every
|
|
192
|
-
* `Pending` response. Native scale-ts `Enum` preserves the discriminant and
|
|
193
|
-
* matches the peer's wire format directly.
|
|
194
|
-
*/
|
|
77
|
+
/** Inner handshake response variant. */
|
|
195
78
|
export const EncryptedHandshakeResponseV2 = Enum({
|
|
196
79
|
Pending: HandshakeStatusV2,
|
|
197
80
|
Success: HandshakeSuccessV2,
|
|
@@ -55,6 +55,10 @@ export type StartPairingDeps = {
|
|
|
55
55
|
* `initialProcessedDataHex` value is up to date.
|
|
56
56
|
*/
|
|
57
57
|
onStatementProcessed?: (dataHex: string) => void;
|
|
58
|
+
/**
|
|
59
|
+
* Turns on logging for statement store interaction
|
|
60
|
+
*/
|
|
61
|
+
__DEBUG?: boolean;
|
|
58
62
|
};
|
|
59
63
|
export type Pairing = {
|
|
60
64
|
qrPayload: string;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* higher layers drive a UI hook off this and update an onboarding screen.
|
|
24
24
|
*/
|
|
25
25
|
import { BehaviorSubject } from 'rxjs';
|
|
26
|
-
import {
|
|
26
|
+
import { EncryptedHandshakeResponseV2, VersionedHandshakeResponse } from '../scale/handshakeV2.js';
|
|
27
27
|
import { decryptResponseEnvelope } from './envelope.js';
|
|
28
28
|
import { buildPairingDeeplink } from './proposal.js';
|
|
29
29
|
import { advance, fromInnerResponse, isTerminal, submitted } from './state.js';
|
|
@@ -75,14 +75,13 @@ export const startPairingV2 = (deps) => {
|
|
|
75
75
|
pollHandle = null;
|
|
76
76
|
};
|
|
77
77
|
const log = (msg, extra) => {
|
|
78
|
+
if (!deps.__DEBUG)
|
|
79
|
+
return;
|
|
78
80
|
if (extra === undefined)
|
|
79
81
|
console.info(`[sso-v2] ${msg}`);
|
|
80
82
|
else
|
|
81
83
|
console.info(`[sso-v2] ${msg}`, extra);
|
|
82
84
|
};
|
|
83
|
-
log(`subscribing to pairing topic ${toHexFull(topic)}`);
|
|
84
|
-
log(`device statementAccountId ${toHexFull(deps.deviceIdentity.statementAccountPublicKey)}`);
|
|
85
|
-
log(`device encryptionPublicKey ${toHexFull(deps.deviceIdentity.encryptionPublicKey)}`);
|
|
86
85
|
// One-shot probe: query the topic right away so we know whether any answer
|
|
87
86
|
// statement was already posted before our subscription connected.
|
|
88
87
|
void deps.statementStore.queryStatements({ matchAll: [topic] }).match(statements => log(`queryStatements probe: ${statements.length} statement(s)`), err => log('queryStatements probe failed', err));
|
|
@@ -121,7 +120,7 @@ export const startPairingV2 = (deps) => {
|
|
|
121
120
|
const peerStatementAccountId = extractStatementSigner(statement);
|
|
122
121
|
let next;
|
|
123
122
|
try {
|
|
124
|
-
next = fromInnerResponse(
|
|
123
|
+
next = fromInnerResponse(EncryptedHandshakeResponseV2.dec(innerBytes), peerStatementAccountId);
|
|
125
124
|
}
|
|
126
125
|
catch (err) {
|
|
127
126
|
log(`inner decode failed; innerBytes (${innerBytes.length}b) = ${toHexFull(innerBytes)}`, err);
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* The state object is what UIs render and what the chat layer gates on
|
|
15
15
|
* before submitting any V2 statements.
|
|
16
16
|
*/
|
|
17
|
-
import type {
|
|
17
|
+
import type { CodecType } from 'scale-ts';
|
|
18
|
+
import type { EncryptedHandshakeResponseV2, HandshakeSuccessV2 } from '../scale/handshakeV2.js';
|
|
18
19
|
export type HandshakeIdleState = {
|
|
19
20
|
tag: 'Idle';
|
|
20
21
|
};
|
|
@@ -25,45 +26,14 @@ export type HandshakePendingState = {
|
|
|
25
26
|
tag: 'Pending';
|
|
26
27
|
reason: 'AllowanceAllocation';
|
|
27
28
|
};
|
|
28
|
-
export type HandshakeSuccessState = {
|
|
29
|
+
export type HandshakeSuccessState = CodecType<typeof HandshakeSuccessV2> & {
|
|
29
30
|
tag: 'Success';
|
|
30
|
-
/** User identity sr25519 accountId (32 bytes). */
|
|
31
|
-
identityAccountId: Uint8Array;
|
|
32
|
-
/**
|
|
33
|
-
* User root sr25519 accountId (32 bytes) — the parent for soft-derivation
|
|
34
|
-
* of product accounts. Nullable: peers on spec v0.2 (Android
|
|
35
|
-
* `feature/location-for-handshake`) omit this field. Product-account
|
|
36
|
-
* derivation degrades gracefully when absent; chat does not use it.
|
|
37
|
-
*/
|
|
38
|
-
rootAccountId: Uint8Array | null;
|
|
39
|
-
/**
|
|
40
|
-
* User identity chat P-256 private key (32 bytes raw scalar) shared by
|
|
41
|
-
* PApp with this device per the multi-device spec. Sensitive; persist in
|
|
42
|
-
* OS-keychain-backed secure storage and never forward.
|
|
43
|
-
*/
|
|
44
|
-
identityChatPrivateKey: Uint8Array;
|
|
45
31
|
/**
|
|
46
32
|
* Derived locally from `identityChatPrivateKey` via P-256 scalar
|
|
47
33
|
* multiplication (uncompressed 65-byte form). Both sides MUST derive
|
|
48
34
|
* identically; downstream session topics depend on it.
|
|
49
35
|
*/
|
|
50
36
|
identityChatPublicKey: Uint8Array;
|
|
51
|
-
/**
|
|
52
|
-
* Encryption public key of the authorising PApp device (65 bytes,
|
|
53
|
-
* P-256 uncompressed). Used by the host when addressing chat envelopes
|
|
54
|
-
* back to the authorising device.
|
|
55
|
-
*/
|
|
56
|
-
deviceEncPubKey: Uint8Array;
|
|
57
|
-
/**
|
|
58
|
-
* `papp_encr_pub` from the Mobile SSO spec (v0.2.2 — 65 bytes, P-256
|
|
59
|
-
* uncompressed). The host's SSO session transport derives
|
|
60
|
-
* `shared_secret_session = ECDH(host_encr_secret, ssoEncPubKey)` from
|
|
61
|
-
* this. Nullable because v0.2 and v0.2.1 peers don't ship it; while
|
|
62
|
-
* null the host's SSO transport stays inactive (sign/vrf/etc continue
|
|
63
|
-
* to fail at the boundary) and chat keeps working through
|
|
64
|
-
* `identityChatPrivateKey`.
|
|
65
|
-
*/
|
|
66
|
-
ssoEncPubKey: Uint8Array | null;
|
|
67
37
|
/**
|
|
68
38
|
* The pairing-topic statement was signed by PApp's device statement
|
|
69
39
|
* account. `HandshakeSuccessV2` doesn't carry it in the encrypted body, so
|
|
@@ -81,11 +51,11 @@ export type HandshakeState = HandshakeIdleState | HandshakeSubmittedState | Hand
|
|
|
81
51
|
export declare const idle: () => HandshakeIdleState;
|
|
82
52
|
export declare const submitted: () => HandshakeSubmittedState;
|
|
83
53
|
/**
|
|
84
|
-
* Translate
|
|
85
|
-
*
|
|
86
|
-
*
|
|
54
|
+
* Translate a decoded `EncryptedHandshakeResponseV2` into the public state.
|
|
55
|
+
* Pure — no I/O. The caller decrypts the outer envelope and decodes the inner
|
|
56
|
+
* payload via `EncryptedHandshakeResponseV2.dec` first.
|
|
87
57
|
*/
|
|
88
|
-
export declare const fromInnerResponse: (response:
|
|
58
|
+
export declare const fromInnerResponse: (response: CodecType<typeof EncryptedHandshakeResponseV2>, peerStatementAccountId?: Uint8Array | null) => HandshakeState;
|
|
89
59
|
/**
|
|
90
60
|
* Forward-only transition guard: rejects regressions like Success → Pending.
|
|
91
61
|
* Idempotent on same-tag transitions (returns current by reference) so callers
|
|
@@ -18,9 +18,9 @@ import { deriveIdentityChatPublicKey } from '../scale/handshakeV2.js';
|
|
|
18
18
|
export const idle = () => ({ tag: 'Idle' });
|
|
19
19
|
export const submitted = () => ({ tag: 'Submitted' });
|
|
20
20
|
/**
|
|
21
|
-
* Translate
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* Translate a decoded `EncryptedHandshakeResponseV2` into the public state.
|
|
22
|
+
* Pure — no I/O. The caller decrypts the outer envelope and decodes the inner
|
|
23
|
+
* payload via `EncryptedHandshakeResponseV2.dec` first.
|
|
24
24
|
*/
|
|
25
25
|
export const fromInnerResponse = (response, peerStatementAccountId = null) => {
|
|
26
26
|
switch (response.tag) {
|
|
@@ -36,6 +36,7 @@ export const fromInnerResponse = (response, peerStatementAccountId = null) => {
|
|
|
36
36
|
identityChatPublicKey: deriveIdentityChatPublicKey(response.value.identityChatPrivateKey),
|
|
37
37
|
deviceEncPubKey: response.value.deviceEncPubKey,
|
|
38
38
|
ssoEncPubKey: response.value.ssoEncPubKey,
|
|
39
|
+
rootEntropySource: response.value.rootEntropySource,
|
|
39
40
|
peerStatementAccountId,
|
|
40
41
|
};
|
|
41
42
|
case 'Failed':
|
|
@@ -6,7 +6,6 @@ type StoredUserSecrets = CodecType<typeof StoredUserSecretsCodec>;
|
|
|
6
6
|
declare const StoredUserSecretsCodec: import("scale-ts").Codec<{
|
|
7
7
|
ssSecret: SsSecret;
|
|
8
8
|
encrSecret: EncrSecret;
|
|
9
|
-
entropy: Uint8Array<ArrayBufferLike>;
|
|
10
9
|
identityChatPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
11
10
|
}>;
|
|
12
11
|
export type UserSecretRepository = ReturnType<typeof createUserSecretRepository>;
|
|
@@ -8,26 +8,17 @@ import { toError } from '../helpers/utils.js';
|
|
|
8
8
|
const StoredUserSecretsCodec = Struct({
|
|
9
9
|
ssSecret: BrandedBytesCodec(),
|
|
10
10
|
encrSecret: BrandedBytesCodec(),
|
|
11
|
-
entropy: Bytes(),
|
|
12
11
|
// V2 addition: user identity chat private key (P-256 raw scalar, 32 bytes).
|
|
13
12
|
// Sensitive — kept encrypted at rest alongside the per-session ss/encr secrets.
|
|
14
13
|
identityChatPrivateKey: Bytes(32),
|
|
15
14
|
});
|
|
16
15
|
export function createUserSecretRepository(salt, storage) {
|
|
17
|
-
const baseKey = '
|
|
16
|
+
const baseKey = 'UserSecretsV2';
|
|
18
17
|
const encode = fromThrowable(StoredUserSecretsCodec.enc, toError);
|
|
19
18
|
const decode = fromThrowable((value) => {
|
|
20
19
|
if (!value)
|
|
21
20
|
return null;
|
|
22
|
-
|
|
23
|
-
return StoredUserSecretsCodec.dec(value);
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
// 0.7.x V1 blobs are missing `identityChatPrivateKey` and decode short.
|
|
27
|
-
// Treat as absent — a fresh handshake (required after 0.8.0 upgrade)
|
|
28
|
-
// will overwrite.
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
21
|
+
return StoredUserSecretsCodec.dec(value);
|
|
31
22
|
}, toError);
|
|
32
23
|
const encrypt = fromThrowable((value) => {
|
|
33
24
|
const aes = getAes(salt);
|
|
@@ -15,16 +15,18 @@ declare const storedUserSessionCodec: import("scale-ts").Codec<{
|
|
|
15
15
|
pin: string | undefined;
|
|
16
16
|
};
|
|
17
17
|
rootAccountId: AccountId;
|
|
18
|
-
identityAccountId: AccountId
|
|
19
|
-
identityChatPublicKey: Uint8Array<ArrayBufferLike
|
|
20
|
-
ssoEncPubKey: Uint8Array<ArrayBufferLike
|
|
18
|
+
identityAccountId: AccountId;
|
|
19
|
+
identityChatPublicKey: Uint8Array<ArrayBufferLike>;
|
|
20
|
+
ssoEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
21
|
+
rootEntropySource: Uint8Array<ArrayBufferLike>;
|
|
21
22
|
}>;
|
|
22
23
|
type StoredUserSessionV2Extras = {
|
|
23
|
-
identityAccountId
|
|
24
|
-
identityChatPublicKey
|
|
25
|
-
ssoEncPubKey
|
|
24
|
+
identityAccountId: AccountId;
|
|
25
|
+
identityChatPublicKey: Uint8Array;
|
|
26
|
+
ssoEncPubKey: Uint8Array;
|
|
27
|
+
rootEntropySource: Uint8Array;
|
|
26
28
|
};
|
|
27
|
-
export declare function createStoredUserSession(localAccount: LocalSessionAccount, remoteAccount: RemoteSessionAccount, rootAccountId: AccountId, extras
|
|
29
|
+
export declare function createStoredUserSession(localAccount: LocalSessionAccount, remoteAccount: RemoteSessionAccount, rootAccountId: AccountId, extras: StoredUserSessionV2Extras): StoredUserSession;
|
|
28
30
|
export declare const createUserSessionRepository: (storage: StorageAdapter) => {
|
|
29
31
|
add(value: {
|
|
30
32
|
id: string;
|
|
@@ -38,9 +40,10 @@ export declare const createUserSessionRepository: (storage: StorageAdapter) => {
|
|
|
38
40
|
pin: string | undefined;
|
|
39
41
|
};
|
|
40
42
|
rootAccountId: AccountId;
|
|
41
|
-
identityAccountId: AccountId
|
|
42
|
-
identityChatPublicKey: Uint8Array<ArrayBufferLike
|
|
43
|
-
ssoEncPubKey: Uint8Array<ArrayBufferLike
|
|
43
|
+
identityAccountId: AccountId;
|
|
44
|
+
identityChatPublicKey: Uint8Array<ArrayBufferLike>;
|
|
45
|
+
ssoEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
46
|
+
rootEntropySource: Uint8Array<ArrayBufferLike>;
|
|
44
47
|
}): import("neverthrow").ResultAsync<{
|
|
45
48
|
id: string;
|
|
46
49
|
localAccount: {
|
|
@@ -53,9 +56,10 @@ export declare const createUserSessionRepository: (storage: StorageAdapter) => {
|
|
|
53
56
|
pin: string | undefined;
|
|
54
57
|
};
|
|
55
58
|
rootAccountId: AccountId;
|
|
56
|
-
identityAccountId: AccountId
|
|
57
|
-
identityChatPublicKey: Uint8Array<ArrayBufferLike
|
|
58
|
-
ssoEncPubKey: Uint8Array<ArrayBufferLike
|
|
59
|
+
identityAccountId: AccountId;
|
|
60
|
+
identityChatPublicKey: Uint8Array<ArrayBufferLike>;
|
|
61
|
+
ssoEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
62
|
+
rootEntropySource: Uint8Array<ArrayBufferLike>;
|
|
59
63
|
}, Error>;
|
|
60
64
|
filter(fn: (value: {
|
|
61
65
|
id: string;
|
|
@@ -69,9 +73,10 @@ export declare const createUserSessionRepository: (storage: StorageAdapter) => {
|
|
|
69
73
|
pin: string | undefined;
|
|
70
74
|
};
|
|
71
75
|
rootAccountId: AccountId;
|
|
72
|
-
identityAccountId: AccountId
|
|
73
|
-
identityChatPublicKey: Uint8Array<ArrayBufferLike
|
|
74
|
-
ssoEncPubKey: Uint8Array<ArrayBufferLike
|
|
76
|
+
identityAccountId: AccountId;
|
|
77
|
+
identityChatPublicKey: Uint8Array<ArrayBufferLike>;
|
|
78
|
+
ssoEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
79
|
+
rootEntropySource: Uint8Array<ArrayBufferLike>;
|
|
75
80
|
}) => boolean): import("neverthrow").ResultAsync<{
|
|
76
81
|
id: string;
|
|
77
82
|
localAccount: {
|
|
@@ -84,9 +89,10 @@ export declare const createUserSessionRepository: (storage: StorageAdapter) => {
|
|
|
84
89
|
pin: string | undefined;
|
|
85
90
|
};
|
|
86
91
|
rootAccountId: AccountId;
|
|
87
|
-
identityAccountId: AccountId
|
|
88
|
-
identityChatPublicKey: Uint8Array<ArrayBufferLike
|
|
89
|
-
ssoEncPubKey: Uint8Array<ArrayBufferLike
|
|
92
|
+
identityAccountId: AccountId;
|
|
93
|
+
identityChatPublicKey: Uint8Array<ArrayBufferLike>;
|
|
94
|
+
ssoEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
95
|
+
rootEntropySource: Uint8Array<ArrayBufferLike>;
|
|
90
96
|
}[], Error>;
|
|
91
97
|
mutate(fn: (value: {
|
|
92
98
|
id: string;
|
|
@@ -100,9 +106,10 @@ export declare const createUserSessionRepository: (storage: StorageAdapter) => {
|
|
|
100
106
|
pin: string | undefined;
|
|
101
107
|
};
|
|
102
108
|
rootAccountId: AccountId;
|
|
103
|
-
identityAccountId: AccountId
|
|
104
|
-
identityChatPublicKey: Uint8Array<ArrayBufferLike
|
|
105
|
-
ssoEncPubKey: Uint8Array<ArrayBufferLike
|
|
109
|
+
identityAccountId: AccountId;
|
|
110
|
+
identityChatPublicKey: Uint8Array<ArrayBufferLike>;
|
|
111
|
+
ssoEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
112
|
+
rootEntropySource: Uint8Array<ArrayBufferLike>;
|
|
106
113
|
}[]) => {
|
|
107
114
|
id: string;
|
|
108
115
|
localAccount: {
|
|
@@ -115,9 +122,10 @@ export declare const createUserSessionRepository: (storage: StorageAdapter) => {
|
|
|
115
122
|
pin: string | undefined;
|
|
116
123
|
};
|
|
117
124
|
rootAccountId: AccountId;
|
|
118
|
-
identityAccountId: AccountId
|
|
119
|
-
identityChatPublicKey: Uint8Array<ArrayBufferLike
|
|
120
|
-
ssoEncPubKey: Uint8Array<ArrayBufferLike
|
|
125
|
+
identityAccountId: AccountId;
|
|
126
|
+
identityChatPublicKey: Uint8Array<ArrayBufferLike>;
|
|
127
|
+
ssoEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
128
|
+
rootEntropySource: Uint8Array<ArrayBufferLike>;
|
|
121
129
|
}[]): import("neverthrow").ResultAsync<{
|
|
122
130
|
id: string;
|
|
123
131
|
localAccount: {
|
|
@@ -130,9 +138,10 @@ export declare const createUserSessionRepository: (storage: StorageAdapter) => {
|
|
|
130
138
|
pin: string | undefined;
|
|
131
139
|
};
|
|
132
140
|
rootAccountId: AccountId;
|
|
133
|
-
identityAccountId: AccountId
|
|
134
|
-
identityChatPublicKey: Uint8Array<ArrayBufferLike
|
|
135
|
-
ssoEncPubKey: Uint8Array<ArrayBufferLike
|
|
141
|
+
identityAccountId: AccountId;
|
|
142
|
+
identityChatPublicKey: Uint8Array<ArrayBufferLike>;
|
|
143
|
+
ssoEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
144
|
+
rootEntropySource: Uint8Array<ArrayBufferLike>;
|
|
136
145
|
}[], Error>;
|
|
137
146
|
read(): import("neverthrow").ResultAsync<{
|
|
138
147
|
id: string;
|
|
@@ -146,9 +155,10 @@ export declare const createUserSessionRepository: (storage: StorageAdapter) => {
|
|
|
146
155
|
pin: string | undefined;
|
|
147
156
|
};
|
|
148
157
|
rootAccountId: AccountId;
|
|
149
|
-
identityAccountId: AccountId
|
|
150
|
-
identityChatPublicKey: Uint8Array<ArrayBufferLike
|
|
151
|
-
ssoEncPubKey: Uint8Array<ArrayBufferLike
|
|
158
|
+
identityAccountId: AccountId;
|
|
159
|
+
identityChatPublicKey: Uint8Array<ArrayBufferLike>;
|
|
160
|
+
ssoEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
161
|
+
rootEntropySource: Uint8Array<ArrayBufferLike>;
|
|
152
162
|
}[], Error>;
|
|
153
163
|
write(value: {
|
|
154
164
|
id: string;
|
|
@@ -162,9 +172,10 @@ export declare const createUserSessionRepository: (storage: StorageAdapter) => {
|
|
|
162
172
|
pin: string | undefined;
|
|
163
173
|
};
|
|
164
174
|
rootAccountId: AccountId;
|
|
165
|
-
identityAccountId: AccountId
|
|
166
|
-
identityChatPublicKey: Uint8Array<ArrayBufferLike
|
|
167
|
-
ssoEncPubKey: Uint8Array<ArrayBufferLike
|
|
175
|
+
identityAccountId: AccountId;
|
|
176
|
+
identityChatPublicKey: Uint8Array<ArrayBufferLike>;
|
|
177
|
+
ssoEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
178
|
+
rootEntropySource: Uint8Array<ArrayBufferLike>;
|
|
168
179
|
}[]): import("neverthrow").ResultAsync<null, Error> | import("neverthrow").ResultAsync<{
|
|
169
180
|
id: string;
|
|
170
181
|
localAccount: {
|
|
@@ -177,9 +188,10 @@ export declare const createUserSessionRepository: (storage: StorageAdapter) => {
|
|
|
177
188
|
pin: string | undefined;
|
|
178
189
|
};
|
|
179
190
|
rootAccountId: AccountId;
|
|
180
|
-
identityAccountId: AccountId
|
|
181
|
-
identityChatPublicKey: Uint8Array<ArrayBufferLike
|
|
182
|
-
ssoEncPubKey: Uint8Array<ArrayBufferLike
|
|
191
|
+
identityAccountId: AccountId;
|
|
192
|
+
identityChatPublicKey: Uint8Array<ArrayBufferLike>;
|
|
193
|
+
ssoEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
194
|
+
rootEntropySource: Uint8Array<ArrayBufferLike>;
|
|
183
195
|
}[], Error>;
|
|
184
196
|
clear(): import("neverthrow").ResultAsync<void, Error>;
|
|
185
197
|
subscribe(fn: (value: {
|
|
@@ -194,9 +206,10 @@ export declare const createUserSessionRepository: (storage: StorageAdapter) => {
|
|
|
194
206
|
pin: string | undefined;
|
|
195
207
|
};
|
|
196
208
|
rootAccountId: AccountId;
|
|
197
|
-
identityAccountId: AccountId
|
|
198
|
-
identityChatPublicKey: Uint8Array<ArrayBufferLike
|
|
199
|
-
ssoEncPubKey: Uint8Array<ArrayBufferLike
|
|
209
|
+
identityAccountId: AccountId;
|
|
210
|
+
identityChatPublicKey: Uint8Array<ArrayBufferLike>;
|
|
211
|
+
ssoEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
212
|
+
rootEntropySource: Uint8Array<ArrayBufferLike>;
|
|
200
213
|
}[]) => void): VoidFunction;
|
|
201
214
|
};
|
|
202
215
|
export {};
|
|
@@ -2,7 +2,7 @@ import { AccountIdCodec, LocalSessionAccountCodec, RemoteSessionAccountCodec } f
|
|
|
2
2
|
import { fieldListView } from '@novasamatech/storage-adapter';
|
|
3
3
|
import { nanoid } from 'nanoid';
|
|
4
4
|
import { fromHex, toHex } from 'polkadot-api/utils';
|
|
5
|
-
import { Bytes,
|
|
5
|
+
import { Bytes, Struct, Vector, str } from 'scale-ts';
|
|
6
6
|
// V2 fields trail V1 fields so a future schema rev can append further
|
|
7
7
|
// `Option`-wrapped fields without breaking decode of 0.8.0 blobs.
|
|
8
8
|
const storedUserSessionCodec = Struct({
|
|
@@ -10,15 +10,18 @@ const storedUserSessionCodec = Struct({
|
|
|
10
10
|
localAccount: LocalSessionAccountCodec,
|
|
11
11
|
remoteAccount: RemoteSessionAccountCodec,
|
|
12
12
|
rootAccountId: AccountIdCodec,
|
|
13
|
-
identityAccountId:
|
|
14
|
-
identityChatPublicKey:
|
|
15
|
-
// `papp_encr_pub` (
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
ssoEncPubKey:
|
|
13
|
+
identityAccountId: AccountIdCodec,
|
|
14
|
+
identityChatPublicKey: Bytes(65),
|
|
15
|
+
// `papp_encr_pub` (65-byte uncompressed P-256). Persisted so the host can
|
|
16
|
+
// rebuild its SSO session transport (`shared_secret_session =
|
|
17
|
+
// ECDH(host_encr_secret, ssoEncPubKey)`) on a cold start without re-running
|
|
18
|
+
// the handshake.
|
|
19
|
+
ssoEncPubKey: Bytes(65),
|
|
20
|
+
// RFC-0007 layer-1 `rootEntropySource` from the handshake; consumed by the
|
|
21
|
+
// host's `host_derive_entropy` handler via `deriveProductEntropyFromSource`.
|
|
22
|
+
rootEntropySource: Bytes(32),
|
|
20
23
|
});
|
|
21
|
-
export function createStoredUserSession(localAccount, remoteAccount, rootAccountId, extras
|
|
24
|
+
export function createStoredUserSession(localAccount, remoteAccount, rootAccountId, extras) {
|
|
22
25
|
return {
|
|
23
26
|
id: nanoid(12),
|
|
24
27
|
localAccount,
|
|
@@ -27,25 +30,15 @@ export function createStoredUserSession(localAccount, remoteAccount, rootAccount
|
|
|
27
30
|
identityAccountId: extras.identityAccountId,
|
|
28
31
|
identityChatPublicKey: extras.identityChatPublicKey,
|
|
29
32
|
ssoEncPubKey: extras.ssoEncPubKey,
|
|
33
|
+
rootEntropySource: extras.rootEntropySource,
|
|
30
34
|
};
|
|
31
35
|
}
|
|
32
36
|
export const createUserSessionRepository = (storage) => {
|
|
33
37
|
const codec = Vector(storedUserSessionCodec);
|
|
34
38
|
return fieldListView({
|
|
35
39
|
storage,
|
|
36
|
-
key: '
|
|
37
|
-
from: x =>
|
|
38
|
-
try {
|
|
39
|
-
return codec.dec(fromHex(x));
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
// 0.7.x V1 blobs use the prior codec shape and won't decode against
|
|
43
|
-
// V2's extended struct. Treat as empty so the caller (and the
|
|
44
|
-
// fieldListView mutate machinery) start clean; the next write
|
|
45
|
-
// overwrites the bad blob.
|
|
46
|
-
return [];
|
|
47
|
-
}
|
|
48
|
-
},
|
|
40
|
+
key: 'SsoSessionsV2',
|
|
41
|
+
from: x => codec.dec(fromHex(x)),
|
|
49
42
|
to: x => toHex(codec.enc(x)),
|
|
50
43
|
});
|
|
51
44
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novasamatech/host-papp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.6",
|
|
5
5
|
"description": "Polkadot app integration",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"@noble/ciphers": "2.2.0",
|
|
35
35
|
"@noble/curves": "2.2.0",
|
|
36
36
|
"@noble/hashes": "2.2.0",
|
|
37
|
-
"@novasamatech/host-api": "0.8.
|
|
38
|
-
"@novasamatech/scale": "0.8.
|
|
39
|
-
"@novasamatech/statement-store": "0.8.
|
|
40
|
-
"@novasamatech/storage-adapter": "0.8.
|
|
37
|
+
"@novasamatech/host-api": "0.8.6",
|
|
38
|
+
"@novasamatech/scale": "0.8.6",
|
|
39
|
+
"@novasamatech/statement-store": "0.8.6",
|
|
40
|
+
"@novasamatech/storage-adapter": "0.8.6",
|
|
41
41
|
"@polkadot-api/utils": "^0.4.0",
|
|
42
42
|
"@polkadot-labs/hdkd-helpers": "^0.0.30",
|
|
43
43
|
"nanoevents": "9.1.0",
|