@oxy.so/core 7.29.0 → 7.31.0
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/cjs/api/identity.js +40 -1
- package/dist/cjs/crypto/index.js +11 -2
- package/dist/cjs/crypto/keyManager.js +69 -1
- package/dist/cjs/crypto/retiredKeys.js +371 -0
- package/dist/cjs/session/authStateStore.js +13 -0
- package/dist/cjs/session/index.js +5 -2
- package/dist/cjs/utils/keychainAccessGroups.js +107 -36
- package/dist/esm/api/identity.js +40 -1
- package/dist/esm/crypto/index.js +3 -0
- package/dist/esm/crypto/keyManager.js +69 -1
- package/dist/esm/crypto/retiredKeys.js +361 -0
- package/dist/esm/session/authStateStore.js +12 -0
- package/dist/esm/session/index.js +1 -1
- package/dist/esm/utils/keychainAccessGroups.js +108 -37
- package/dist/types/api/identity.d.ts +10 -0
- package/dist/types/crypto/identitySigner.d.ts +23 -0
- package/dist/types/crypto/index.d.ts +3 -1
- package/dist/types/crypto/keyManager.d.ts +23 -1
- package/dist/types/crypto/retiredKeys.d.ts +116 -0
- package/dist/types/session/authStateStore.d.ts +9 -0
- package/dist/types/session/index.d.ts +2 -2
- package/dist/types/utils/keychainAccessGroups.d.ts +72 -20
- package/package.json +3 -3
- package/src/api/identity.ts +46 -3
- package/src/crypto/identitySigner.ts +23 -0
- package/src/crypto/index.ts +13 -0
- package/src/crypto/keyManager.ts +81 -3
- package/src/crypto/keychainGroupMigration.ts +6 -1
- package/src/crypto/retiredKeys.ts +453 -0
- package/src/session/authStateStore.ts +14 -0
- package/src/session/index.ts +8 -1
- package/src/utils/keychainAccessGroups.ts +165 -38
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* | `<TEAM>.so.oxy.identity` | Commons only (listed FIRST) | `identity-secret` |
|
|
8
8
|
* | `<TEAM>.so.oxy.wallet` | Commons, the Peable wallet | `spending-key` |
|
|
9
9
|
* | `<TEAM>.group.so.oxy.shared` | every Oxy app | `shared` |
|
|
10
|
+
* | `<TEAM>.<bundle id>` | that app alone | `app` |
|
|
10
11
|
*
|
|
11
12
|
* - `identity-secret`: the personal root (primary and backup slots), the
|
|
12
13
|
* personal and organization recovery phrases, the organization index. Only
|
|
@@ -16,14 +17,21 @@
|
|
|
16
17
|
* (`deriveScopedSeed`, `signSocialReceive`). Commons writes them; the Peable
|
|
17
18
|
* wallet reads them.
|
|
18
19
|
* - `shared`: what every Oxy app may read: the shared public key and the
|
|
19
|
-
* device-session credential.
|
|
20
|
+
* device-session credential (the one cross-app session join point).
|
|
21
|
+
* - `app`: an app's OWN items, readable by no other app: its session
|
|
22
|
+
* credential (`oxy.auth.v1` and the rest of the SDK's session store), its
|
|
23
|
+
* identity pin, its settings, the Peable wallet's imported phrases. The group
|
|
24
|
+
* is the app's application identifier, which Apple makes every app a member
|
|
25
|
+
* of; no app lists it and no other app can name it. Android's counterpart is
|
|
26
|
+
* the app's own storage (ADR 0062 D6).
|
|
20
27
|
*
|
|
21
28
|
* The ROLE of this binary comes from the keychain itself: an item written
|
|
22
29
|
* without a group lands in the app's default group, the first entry of its
|
|
23
30
|
* `keychain-access-groups` entitlement, and `@oxy.so/app-preset` lets only
|
|
24
31
|
* Commons put the identity group first and only the Peable wallet the wallet
|
|
25
32
|
* group. The same probe gives the team prefix, which `kSecAttrAccessGroup`
|
|
26
|
-
* needs and JavaScript cannot otherwise see
|
|
33
|
+
* needs and JavaScript cannot otherwise see; a second probe proves the app's
|
|
34
|
+
* own group (`<prefix><bundle id>`) is writable.
|
|
27
35
|
*
|
|
28
36
|
* Android and web have no access groups: there every helper here answers the
|
|
29
37
|
* single-store behaviour (`ungrouped`), and Android's narrowing is Commons'
|
|
@@ -33,7 +41,12 @@
|
|
|
33
41
|
* This module never imports the identity stack: services' device-session store
|
|
34
42
|
* and Peable's wallet storage use it too.
|
|
35
43
|
*/
|
|
36
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
type KeychainApplicationAccessGroup,
|
|
46
|
+
type KeychainDefaultAccessGroup,
|
|
47
|
+
loadKeychainApplicationAccessGroup,
|
|
48
|
+
loadKeychainDefaultAccessGroup,
|
|
49
|
+
} from '@oxy.so/protocol';
|
|
37
50
|
import { isIOS } from './platform';
|
|
38
51
|
|
|
39
52
|
/** The group names after the team prefix, exactly as the entitlements list them. */
|
|
@@ -45,19 +58,34 @@ export const KEYCHAIN_ACCESS_GROUPS = {
|
|
|
45
58
|
|
|
46
59
|
export type KeychainAccessGroupName = keyof typeof KEYCHAIN_ACCESS_GROUPS;
|
|
47
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Every group an item class can be assigned: the three Oxy groups, or `app`,
|
|
63
|
+
* this app's own group, whose name is the app's and so is not a constant.
|
|
64
|
+
*/
|
|
65
|
+
export type KeychainGroup = KeychainAccessGroupName | 'app';
|
|
66
|
+
|
|
48
67
|
/** What a keychain item IS, which decides its group. See the table above. */
|
|
49
|
-
export type KeychainItemClass = 'identity-secret' | 'spending-key' | 'shared';
|
|
68
|
+
export type KeychainItemClass = 'identity-secret' | 'spending-key' | 'shared' | 'app';
|
|
50
69
|
|
|
51
70
|
/** Which holder this binary is, from its default access group. */
|
|
52
71
|
export type KeychainRole = 'holder' | 'wallet' | 'member';
|
|
53
72
|
|
|
54
73
|
/**
|
|
55
74
|
* - `grouped`: iOS with the probe answered. `prefix` ends with its dot
|
|
56
|
-
* (`ABCDE12345.`); `defaultGroup` is the full default group string
|
|
75
|
+
* (`ABCDE12345.`); `defaultGroup` is the full default group string;
|
|
76
|
+
* `appGroup`, when the native module proved it, this app's own group
|
|
77
|
+
* (`ABCDE12345.so.oxy.mention`). Absent on a native build that predates that
|
|
78
|
+
* probe: see {@link ownKeychainItemPlan} for what such a build does.
|
|
57
79
|
* - `ungrouped`: Android, web, or an iOS native build without the probe.
|
|
58
80
|
*/
|
|
59
81
|
export type KeychainAccess =
|
|
60
|
-
| {
|
|
82
|
+
| {
|
|
83
|
+
mode: 'grouped';
|
|
84
|
+
prefix: string;
|
|
85
|
+
defaultGroup: string;
|
|
86
|
+
role: KeychainRole;
|
|
87
|
+
appGroup?: string;
|
|
88
|
+
}
|
|
61
89
|
| { mode: 'ungrouped' };
|
|
62
90
|
|
|
63
91
|
/** The `expo-secure-store` option this module sets (its real name since 58). */
|
|
@@ -77,7 +105,9 @@ export class KeychainAccessUnavailableError extends Error {
|
|
|
77
105
|
* The single place each item class is assigned a group. A new kind of item
|
|
78
106
|
* (F1's retained keys, for one) picks its class and reuses everything below.
|
|
79
107
|
*/
|
|
80
|
-
export function keychainGroupFor(item: KeychainItemClass): KeychainAccessGroupName
|
|
108
|
+
export function keychainGroupFor(item: Exclude<KeychainItemClass, 'app'>): KeychainAccessGroupName;
|
|
109
|
+
export function keychainGroupFor(item: KeychainItemClass): KeychainGroup;
|
|
110
|
+
export function keychainGroupFor(item: KeychainItemClass): KeychainGroup {
|
|
81
111
|
switch (item) {
|
|
82
112
|
case 'identity-secret':
|
|
83
113
|
return 'identity';
|
|
@@ -85,6 +115,8 @@ export function keychainGroupFor(item: KeychainItemClass): KeychainAccessGroupNa
|
|
|
85
115
|
return 'wallet';
|
|
86
116
|
case 'shared':
|
|
87
117
|
return 'shared';
|
|
118
|
+
case 'app':
|
|
119
|
+
return 'app';
|
|
88
120
|
}
|
|
89
121
|
}
|
|
90
122
|
|
|
@@ -96,12 +128,18 @@ export function mayHoldKeychainItem(role: KeychainRole, item: KeychainItemClass)
|
|
|
96
128
|
case 'wallet':
|
|
97
129
|
return item !== 'identity-secret';
|
|
98
130
|
case 'member':
|
|
99
|
-
return item === 'shared';
|
|
131
|
+
return item === 'shared' || item === 'app';
|
|
100
132
|
}
|
|
101
133
|
}
|
|
102
134
|
|
|
103
|
-
/**
|
|
104
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Parse the default access group the probe reported, and the app's own group
|
|
137
|
+
* when the second probe proved it. Exported for tests.
|
|
138
|
+
*/
|
|
139
|
+
export function keychainAccessFromDefaultGroup(
|
|
140
|
+
defaultGroup: string,
|
|
141
|
+
appGroup?: string,
|
|
142
|
+
): KeychainAccess {
|
|
105
143
|
const dot = defaultGroup.indexOf('.');
|
|
106
144
|
if (dot <= 0 || dot === defaultGroup.length - 1) {
|
|
107
145
|
// Not `<prefix>.<group>`: an unsigned simulator build. Nothing to split.
|
|
@@ -115,7 +153,9 @@ export function keychainAccessFromDefaultGroup(defaultGroup: string): KeychainAc
|
|
|
115
153
|
: group === KEYCHAIN_ACCESS_GROUPS.wallet
|
|
116
154
|
? 'wallet'
|
|
117
155
|
: 'member';
|
|
118
|
-
return
|
|
156
|
+
return appGroup?.startsWith(prefix)
|
|
157
|
+
? { mode: 'grouped', prefix, defaultGroup, role, appGroup }
|
|
158
|
+
: { mode: 'grouped', prefix, defaultGroup, role };
|
|
119
159
|
}
|
|
120
160
|
|
|
121
161
|
let resolvedAccess: KeychainAccess | null = null;
|
|
@@ -144,10 +184,31 @@ export async function resolveKeychainAccess(): Promise<KeychainAccess> {
|
|
|
144
184
|
if (probe.state === 'unavailable') {
|
|
145
185
|
throw new KeychainAccessUnavailableError(probe.cause);
|
|
146
186
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
187
|
+
if (probe.state !== 'resolved') {
|
|
188
|
+
resolvedAccess = { mode: 'ungrouped' };
|
|
189
|
+
return resolvedAccess;
|
|
190
|
+
}
|
|
191
|
+
const access = keychainAccessFromDefaultGroup(probe.accessGroup);
|
|
192
|
+
if (access.mode === 'ungrouped') {
|
|
193
|
+
resolvedAccess = access;
|
|
194
|
+
return resolvedAccess;
|
|
195
|
+
}
|
|
196
|
+
// The app's own group. A thrown probe is NOT degraded to "no app group": this
|
|
197
|
+
// process would then write the app's items to the old place while the next
|
|
198
|
+
// one reads the new place first, and see a stale value.
|
|
199
|
+
let own: KeychainApplicationAccessGroup;
|
|
200
|
+
try {
|
|
201
|
+
own = await loadKeychainApplicationAccessGroup(access.prefix);
|
|
202
|
+
} catch (cause) {
|
|
203
|
+
throw new KeychainAccessUnavailableError(cause);
|
|
204
|
+
}
|
|
205
|
+
if (own.state === 'unavailable') {
|
|
206
|
+
throw new KeychainAccessUnavailableError(own.cause);
|
|
207
|
+
}
|
|
208
|
+
resolvedAccess = keychainAccessFromDefaultGroup(
|
|
209
|
+
probe.accessGroup,
|
|
210
|
+
own.state === 'resolved' ? own.accessGroup : undefined,
|
|
211
|
+
);
|
|
151
212
|
return resolvedAccess;
|
|
152
213
|
}
|
|
153
214
|
|
|
@@ -156,12 +217,26 @@ export function resetKeychainAccessForTests(): void {
|
|
|
156
217
|
resolvedAccess = null;
|
|
157
218
|
}
|
|
158
219
|
|
|
159
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* The full `kSecAttrAccessGroup` value of `group`, or `undefined` for "no
|
|
222
|
+
* group" (the app's default): always when ungrouped, and for `app` on a build
|
|
223
|
+
* that cannot place items there (see {@link ownKeychainItemPlan}).
|
|
224
|
+
*/
|
|
160
225
|
export function keychainAccessGroupValue(
|
|
161
226
|
access: KeychainAccess,
|
|
162
|
-
group:
|
|
227
|
+
group: KeychainGroup,
|
|
163
228
|
): string | undefined {
|
|
164
|
-
|
|
229
|
+
if (access.mode === 'ungrouped') {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
if (group === 'app') {
|
|
233
|
+
// Commons' and the Peable wallet's default groups are theirs (no other app
|
|
234
|
+
// writes its own items there), so a build without the app-group probe
|
|
235
|
+
// keeps its own items there, as ADR 0062 D3 put them. Every other app's
|
|
236
|
+
// default is the shared group: no group, the calls it always made.
|
|
237
|
+
return access.appGroup ?? (access.role === 'member' ? undefined : access.defaultGroup);
|
|
238
|
+
}
|
|
239
|
+
return `${access.prefix}${KEYCHAIN_ACCESS_GROUPS[group]}`;
|
|
165
240
|
}
|
|
166
241
|
|
|
167
242
|
function option(value: string | undefined): KeychainGroupOption {
|
|
@@ -169,10 +244,11 @@ function option(value: string | undefined): KeychainGroupOption {
|
|
|
169
244
|
}
|
|
170
245
|
|
|
171
246
|
/**
|
|
172
|
-
* The option bags a READ of `item` tries, in order: its own group, then
|
|
173
|
-
*
|
|
174
|
-
* drops that fallback). Empty when this
|
|
175
|
-
* answers `null` without touching the
|
|
247
|
+
* The option bags a READ of `item` tries, in order: its own group, then, for
|
|
248
|
+
* an identity secret or a spending key, the shared group, where those lived
|
|
249
|
+
* before the split (release 4 of the doc drops that fallback). Empty when this
|
|
250
|
+
* app may not hold the item: the caller answers `null` without touching the
|
|
251
|
+
* keychain. An app's own items (`app`) take {@link ownKeychainItemPlan}.
|
|
176
252
|
*/
|
|
177
253
|
export function keychainReadPlan(
|
|
178
254
|
access: KeychainAccess,
|
|
@@ -186,7 +262,8 @@ export function keychainReadPlan(
|
|
|
186
262
|
}
|
|
187
263
|
const own = keychainGroupFor(item);
|
|
188
264
|
const plan = [option(keychainAccessGroupValue(access, own))];
|
|
189
|
-
if (own
|
|
265
|
+
if (own === 'identity' || own === 'wallet') {
|
|
266
|
+
// Where the split moved these items from.
|
|
190
267
|
plan.push(option(keychainAccessGroupValue(access, 'shared')));
|
|
191
268
|
}
|
|
192
269
|
return plan;
|
|
@@ -224,29 +301,79 @@ export function keychainWriteOption(
|
|
|
224
301
|
}
|
|
225
302
|
|
|
226
303
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* group the split changed: written to the default group explicitly — where a
|
|
230
|
-
* write without a group goes anyway — and read from it FIRST, then from every
|
|
231
|
-
* group (`{}`), where all of them were written before. Without the explicit
|
|
232
|
-
* order a read could return whichever of two copies the keychain met first.
|
|
233
|
-
* Deletes reach every group, as they always did.
|
|
304
|
+
* Who else writes an own item's NAME, which decides whose an earlier copy of
|
|
305
|
+
* it can be.
|
|
234
306
|
*
|
|
235
|
-
*
|
|
307
|
+
* - `this-app`: no other app uses the name (the Peable wallet's own items). A
|
|
308
|
+
* copy anywhere this app can read is its own.
|
|
309
|
+
* - `every-oxy-app`: every Oxy app writes the same name (the SDK's session
|
|
310
|
+
* store: `oxy.auth.v1`, the warm token, the identity pin). Before the
|
|
311
|
+
* app group, every iOS app but Commons and the Peable wallet wrote it to the
|
|
312
|
+
* ONE copy in the shared group, so a copy there may be another app's
|
|
313
|
+
* session, or Commons' vault session. Only this app's own locations are
|
|
314
|
+
* read, written or deleted; the shared copy is left to the apps still
|
|
315
|
+
* running a build that uses it.
|
|
316
|
+
* - `every-oxy-app-withholding`: as `every-oxy-app`, for a value whose only
|
|
317
|
+
* effect is to WITHHOLD (the explicit sign-out marker). A shared copy still
|
|
318
|
+
* counts — honouring another app's marker can keep this app from signing in
|
|
319
|
+
* automatically, never sign it in — and its delete reaches that copy, as a
|
|
320
|
+
* release always did before.
|
|
236
321
|
*/
|
|
237
|
-
export
|
|
322
|
+
export type OwnKeychainItemNames = 'this-app' | 'every-oxy-app' | 'every-oxy-app-withholding';
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* The plan for an app's OWN items (class `app`, ADR 0062 D6): written to the
|
|
326
|
+
* app's own group (`<prefix><bundle id>`), which no other app can read, and
|
|
327
|
+
* read from it FIRST. Then:
|
|
328
|
+
*
|
|
329
|
+
* - from where this app kept them before: Commons' and the Peable wallet's
|
|
330
|
+
* default group (D3), which no other app writes its own items to;
|
|
331
|
+
* - for `this-app` names, from every group (`{}`), where they were before the
|
|
332
|
+
* split; deletes reach every group;
|
|
333
|
+
* - for `every-oxy-app` names, nowhere else (see {@link OwnKeychainItemNames}),
|
|
334
|
+
* and deletes reach exactly the read locations, earlier ones first, so a
|
|
335
|
+
* failed delete never leaves an older copy as the only one;
|
|
336
|
+
* - for `every-oxy-app-withholding` names, the shared group last on read and
|
|
337
|
+
* first on delete.
|
|
338
|
+
*
|
|
339
|
+
* Nothing is copied or deleted to migrate: the next write lands in the app
|
|
340
|
+
* group and the read order makes it win, so a crash or a failure at any step
|
|
341
|
+
* leaves the value the app had.
|
|
342
|
+
*
|
|
343
|
+
* A native build whose module predates the app-group probe (`appGroup`
|
|
344
|
+
* absent) keeps its items where that build put them: Commons and the wallet in
|
|
345
|
+
* their default group, every other app the calls it always made (the one
|
|
346
|
+
* shared item, until that app's next native build). Android and web: no
|
|
347
|
+
* options.
|
|
348
|
+
*/
|
|
349
|
+
export function ownKeychainItemPlan(
|
|
350
|
+
access: KeychainAccess,
|
|
351
|
+
names: OwnKeychainItemNames = 'this-app',
|
|
352
|
+
): {
|
|
238
353
|
read: KeychainGroupOption[];
|
|
239
354
|
write: KeychainGroupOption;
|
|
240
355
|
delete: KeychainGroupOption[];
|
|
241
356
|
} {
|
|
242
|
-
if (access.mode === 'ungrouped'
|
|
357
|
+
if (access.mode === 'ungrouped') {
|
|
243
358
|
return { read: [{}], write: {}, delete: [{}] };
|
|
244
359
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
write:
|
|
248
|
-
|
|
249
|
-
|
|
360
|
+
const own = keychainAccessGroupValue(access, keychainGroupFor('app'));
|
|
361
|
+
if (own === undefined) {
|
|
362
|
+
return { read: [{}], write: {}, delete: [{}] };
|
|
363
|
+
}
|
|
364
|
+
const earlier =
|
|
365
|
+
access.role !== 'member' && own !== access.defaultGroup ? [option(access.defaultGroup)] : [];
|
|
366
|
+
const mine = [option(own), ...earlier];
|
|
367
|
+
switch (names) {
|
|
368
|
+
case 'this-app':
|
|
369
|
+
return { read: [...mine, {}], write: option(own), delete: [{}] };
|
|
370
|
+
case 'every-oxy-app':
|
|
371
|
+
return { read: mine, write: option(own), delete: [...mine].reverse() };
|
|
372
|
+
case 'every-oxy-app-withholding': {
|
|
373
|
+
const read = [...mine, option(keychainAccessGroupValue(access, 'shared'))];
|
|
374
|
+
return { read, write: option(own), delete: [...read].reverse() };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
250
377
|
}
|
|
251
378
|
|
|
252
379
|
/** The subset of `expo-secure-store` a grouped store wraps. */
|