@oxy.so/core 7.28.0 → 7.29.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/crypto/index.js +19 -1
- package/dist/cjs/crypto/keyManager.js +106 -106
- package/dist/cjs/crypto/keychainGroupMigration.js +353 -0
- package/dist/cjs/crypto/keychainItems.js +70 -0
- package/dist/cjs/crypto/organizationKeys.js +80 -72
- package/dist/cjs/crypto/secureStoreOptions.js +2 -0
- package/dist/cjs/session/index.js +11 -1
- package/dist/cjs/utils/keychainAccessGroups.js +248 -0
- package/dist/esm/crypto/index.js +3 -0
- package/dist/esm/crypto/keyManager.js +63 -63
- package/dist/esm/crypto/keychainGroupMigration.js +348 -0
- package/dist/esm/crypto/keychainItems.js +61 -0
- package/dist/esm/crypto/organizationKeys.js +54 -46
- package/dist/esm/crypto/secureStoreOptions.js +1 -0
- package/dist/esm/session/index.js +3 -0
- package/dist/esm/utils/keychainAccessGroups.js +231 -0
- package/dist/types/crypto/index.d.ts +4 -0
- package/dist/types/crypto/keyManager.d.ts +9 -1
- package/dist/types/crypto/keychainGroupMigration.d.ts +27 -0
- package/dist/types/crypto/keychainItems.d.ts +41 -0
- package/dist/types/crypto/organizationKeys.d.ts +11 -9
- package/dist/types/crypto/secureStoreOptions.d.ts +15 -0
- package/dist/types/session/index.d.ts +2 -0
- package/dist/types/utils/keychainAccessGroups.d.ts +104 -0
- package/package.json +2 -2
- package/src/crypto/index.ts +29 -0
- package/src/crypto/keyManager.ts +84 -99
- package/src/crypto/keychainGroupMigration.ts +440 -0
- package/src/crypto/keychainItems.ts +73 -0
- package/src/crypto/organizationKeys.ts +72 -57
- package/src/crypto/secureStoreOptions.ts +32 -0
- package/src/session/index.ts +11 -0
- package/src/utils/keychainAccessGroups.ts +308 -0
- package/src/types/expo-secure-store.d.ts +0 -21
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* other Oxy apps reach it the way they reach the personal identity, by
|
|
10
10
|
* derivation and never by the key:
|
|
11
11
|
*
|
|
12
|
-
* - iOS: the keychain
|
|
13
|
-
*
|
|
12
|
+
* - iOS: the pair in the wallet keychain group (`<TEAM>.so.oxy.wallet`), which
|
|
13
|
+
* only Commons and the Peable wallet hold; the phrase and the index in the
|
|
14
|
+
* identity group, Commons only (docs/identity/ios-keychain-access-groups.md).
|
|
15
|
+
* Only Commons writes them; every other app reads nothing.
|
|
14
16
|
* - Android: Commons' own expo-secure-store slot, mirrored into the identity
|
|
15
17
|
* signer store (`./identitySigner`) that Commons' identity host answers
|
|
16
18
|
* `accountId` requests from. Every other app asks Commons over IPC
|
|
@@ -18,13 +20,13 @@
|
|
|
18
20
|
*
|
|
19
21
|
* The phrase is kept only in Commons, on the device that created or imported
|
|
20
22
|
* it, so an owner can show it to another owner (ADR 0061 D5); it never reaches
|
|
21
|
-
* the
|
|
23
|
+
* the wallet or shared group, an IPC answer or a log.
|
|
22
24
|
*
|
|
23
25
|
* This module imports nothing from `./keyManager` (which imports it): the
|
|
24
26
|
* signer store comes from `./identitySigner`'s registry.
|
|
25
27
|
*/
|
|
26
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
-
exports.isOrganizationAccountId =
|
|
29
|
+
exports.isOrganizationAccountId = void 0;
|
|
28
30
|
exports.isHealthyOrganizationKeyPair = isHealthyOrganizationKeyPair;
|
|
29
31
|
exports.generateOrganizationKey = generateOrganizationKey;
|
|
30
32
|
exports.organizationKeyFromPhrase = organizationKeyFromPhrase;
|
|
@@ -40,29 +42,18 @@ const english_js_1 = require("@scure/bip39/wordlists/english.js");
|
|
|
40
42
|
const protocol_1 = require("@oxy.so/protocol");
|
|
41
43
|
const secp256k1_1 = require("@oxy.so/protocol/secp256k1");
|
|
42
44
|
const logger_1 = require("../logger");
|
|
45
|
+
const keychainAccessGroups_1 = require("../utils/keychainAccessGroups");
|
|
43
46
|
const platform_1 = require("../utils/platform");
|
|
44
47
|
const identitySigner_1 = require("./identitySigner");
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
/** Android, Commons-local: its own Keystore key, shared with no personal slot. */
|
|
49
|
-
const ANDROID_KEYCHAIN_SERVICE = 'oxy_org_identity';
|
|
50
|
-
const MNEMONIC_KEYCHAIN_SERVICE = 'oxy_org_identity_mnemonic';
|
|
51
|
-
const INDEX_KEYCHAIN_SERVICE = 'oxy_org_identity_index';
|
|
52
|
-
const INDEX_STORAGE_KEY = 'oxy_org_identity_index_v1';
|
|
48
|
+
const keychainGroupMigration_1 = require("./keychainGroupMigration");
|
|
49
|
+
const keychainItems_1 = require("./keychainItems");
|
|
50
|
+
Object.defineProperty(exports, "isOrganizationAccountId", { enumerable: true, get: function () { return keychainItems_1.isOrganizationAccountId; } });
|
|
53
51
|
const ORGANIZATION_PHRASE_WORDS = 24;
|
|
54
|
-
/** Whether `value` is an account id an organization key can be stored under. */
|
|
55
|
-
function isOrganizationAccountId(value) {
|
|
56
|
-
return ACCOUNT_ID.test(value);
|
|
57
|
-
}
|
|
58
52
|
function assertAccountId(accountId) {
|
|
59
|
-
if (!isOrganizationAccountId(accountId)) {
|
|
53
|
+
if (!(0, keychainItems_1.isOrganizationAccountId)(accountId)) {
|
|
60
54
|
throw new Error('organization key: accountId must be a 24-hex or uuid account id');
|
|
61
55
|
}
|
|
62
56
|
}
|
|
63
|
-
const privateKeyName = (accountId) => `oxy_org_identity_private_key.${accountId}`;
|
|
64
|
-
const publicKeyName = (accountId) => `oxy_org_identity_public_key.${accountId}`;
|
|
65
|
-
const mnemonicName = (accountId) => `oxy_org_identity_mnemonic.${accountId}`;
|
|
66
57
|
function toHex(bytes) {
|
|
67
58
|
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
68
59
|
}
|
|
@@ -115,19 +106,33 @@ async function generateOrganizationKey() {
|
|
|
115
106
|
async function organizationKeyFromPhrase(phrase) {
|
|
116
107
|
return pairFromPhrase(phrase);
|
|
117
108
|
}
|
|
118
|
-
|
|
119
|
-
|
|
109
|
+
/**
|
|
110
|
+
* The secure store for one class of organization item: on iOS the pair is a
|
|
111
|
+
* `spending-key` (wallet group), the phrase and the index `identity-secret`
|
|
112
|
+
* (identity group); see `../utils/keychainAccessGroups`. In Commons every
|
|
113
|
+
* access first awaits the access-group migration. Android is unchanged.
|
|
114
|
+
*/
|
|
115
|
+
async function secureStore(item) {
|
|
116
|
+
const raw = await (0, protocol_1.loadSecureStore)();
|
|
117
|
+
const access = await (0, keychainAccessGroups_1.resolveKeychainAccess)();
|
|
118
|
+
if (access.mode === 'grouped' && access.role === 'holder') {
|
|
119
|
+
await (0, keychainGroupMigration_1.ensureKeychainGroupsMigrated)();
|
|
120
|
+
}
|
|
121
|
+
return (0, keychainAccessGroups_1.groupedKeychainStore)(raw, access, item);
|
|
120
122
|
}
|
|
123
|
+
const pairStore = () => secureStore('spending-key');
|
|
124
|
+
const phraseStore = () => secureStore('identity-secret');
|
|
125
|
+
const indexStore = () => secureStore('identity-secret');
|
|
121
126
|
async function readIndex(store) {
|
|
122
|
-
const raw = await store.getItemAsync(
|
|
123
|
-
keychainService:
|
|
127
|
+
const raw = await store.getItemAsync(keychainItems_1.ORGANIZATION_INDEX_STORAGE_KEY, {
|
|
128
|
+
keychainService: keychainItems_1.ORGANIZATION_INDEX_KEYCHAIN_SERVICE,
|
|
124
129
|
});
|
|
125
130
|
if (!raw)
|
|
126
131
|
return [];
|
|
127
132
|
try {
|
|
128
133
|
const parsed = JSON.parse(raw);
|
|
129
134
|
return Array.isArray(parsed)
|
|
130
|
-
? parsed.filter((id) => typeof id === 'string' && isOrganizationAccountId(id))
|
|
135
|
+
? parsed.filter((id) => typeof id === 'string' && (0, keychainItems_1.isOrganizationAccountId)(id))
|
|
131
136
|
: [];
|
|
132
137
|
}
|
|
133
138
|
catch (error) {
|
|
@@ -139,26 +144,24 @@ async function readIndex(store) {
|
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
146
|
async function writeIndex(store, ids) {
|
|
142
|
-
await store.setItemAsync(
|
|
143
|
-
keychainService:
|
|
147
|
+
await store.setItemAsync(keychainItems_1.ORGANIZATION_INDEX_STORAGE_KEY, (0, keychainItems_1.serializeOrganizationIndex)(ids), {
|
|
148
|
+
keychainService: keychainItems_1.ORGANIZATION_INDEX_KEYCHAIN_SERVICE,
|
|
144
149
|
});
|
|
145
150
|
}
|
|
146
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* The options of the pair on this platform: on iOS no keychain service (the
|
|
153
|
+
* wallet group comes from {@link pairStore}), on Android Commons' own slot.
|
|
154
|
+
*/
|
|
147
155
|
function pairOptions(store, forPrivateWrite) {
|
|
148
156
|
if ((0, platform_1.isIOS)()) {
|
|
149
|
-
return forPrivateWrite
|
|
150
|
-
? {
|
|
151
|
-
keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
152
|
-
keychainAccessGroup: IOS_KEYCHAIN_GROUP,
|
|
153
|
-
}
|
|
154
|
-
: { keychainAccessGroup: IOS_KEYCHAIN_GROUP };
|
|
157
|
+
return forPrivateWrite ? { keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY } : {};
|
|
155
158
|
}
|
|
156
159
|
return forPrivateWrite
|
|
157
160
|
? {
|
|
158
161
|
keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
159
|
-
keychainService:
|
|
162
|
+
keychainService: keychainItems_1.ORGANIZATION_ANDROID_KEYCHAIN_SERVICE,
|
|
160
163
|
}
|
|
161
|
-
: { keychainService:
|
|
164
|
+
: { keychainService: keychainItems_1.ORGANIZATION_ANDROID_KEYCHAIN_SERVICE };
|
|
162
165
|
}
|
|
163
166
|
/** The pair from this app's own storage (iOS group; Android Commons slot, then signer store). */
|
|
164
167
|
async function readLocalPair(accountId) {
|
|
@@ -168,10 +171,10 @@ async function readLocalPair(accountId) {
|
|
|
168
171
|
const signer = (0, identitySigner_1.registeredIdentitySignerStore)();
|
|
169
172
|
if (!signer)
|
|
170
173
|
return null; // Only Commons holds organization keys on Android.
|
|
171
|
-
const store = await
|
|
174
|
+
const store = await pairStore();
|
|
172
175
|
const options = pairOptions(store, false);
|
|
173
|
-
const privateKey = await store.getItemAsync(
|
|
174
|
-
const publicKey = await store.getItemAsync(
|
|
176
|
+
const privateKey = await store.getItemAsync((0, keychainItems_1.organizationPrivateKeyName)(accountId), options);
|
|
177
|
+
const publicKey = await store.getItemAsync((0, keychainItems_1.organizationPublicKeyName)(accountId), options);
|
|
175
178
|
if (privateKey && publicKey && isHealthyOrganizationKeyPair(privateKey, publicKey)) {
|
|
176
179
|
return {
|
|
177
180
|
privateKey: (0, secp256k1_1.normalizeSecp256k1PrivateKey)(privateKey),
|
|
@@ -186,10 +189,12 @@ async function readLocalPair(accountId) {
|
|
|
186
189
|
}
|
|
187
190
|
if (!(0, platform_1.isIOS)())
|
|
188
191
|
return null;
|
|
189
|
-
|
|
192
|
+
// The wallet group: `null` without a keychain read in every app but Commons
|
|
193
|
+
// and the Peable wallet.
|
|
194
|
+
const store = await pairStore();
|
|
190
195
|
const options = pairOptions(store, false);
|
|
191
|
-
const privateKey = await store.getItemAsync(
|
|
192
|
-
const publicKey = await store.getItemAsync(
|
|
196
|
+
const privateKey = await store.getItemAsync((0, keychainItems_1.organizationPrivateKeyName)(accountId), options);
|
|
197
|
+
const publicKey = await store.getItemAsync((0, keychainItems_1.organizationPublicKeyName)(accountId), options);
|
|
193
198
|
return privateKey && publicKey && isHealthyOrganizationKeyPair(privateKey, publicKey)
|
|
194
199
|
? {
|
|
195
200
|
privateKey: (0, secp256k1_1.normalizeSecp256k1PrivateKey)(privateKey),
|
|
@@ -200,7 +205,7 @@ async function readLocalPair(accountId) {
|
|
|
200
205
|
/**
|
|
201
206
|
* Keep an organization key on this device. Commons only: on Android it needs
|
|
202
207
|
* the identity signer store Commons registers, so the identity host can serve
|
|
203
|
-
* it; on iOS only Commons
|
|
208
|
+
* it; on iOS only Commons holds the groups it writes (any other app throws).
|
|
204
209
|
*
|
|
205
210
|
* Refuses a pair whose public key does not derive from its private key, and a
|
|
206
211
|
* phrase that does not derive the pair. Overwrites whatever this device held for
|
|
@@ -225,34 +230,36 @@ async function storeOrganizationKey(accountId, key) {
|
|
|
225
230
|
if ((0, platform_1.isAndroid)() && !signer?.writeOrganization) {
|
|
226
231
|
throw new Error('On Android only Commons holds organization keys; this app has no identity signer store for them.');
|
|
227
232
|
}
|
|
228
|
-
const store = await
|
|
229
|
-
await store.setItemAsync(
|
|
230
|
-
await store.setItemAsync(
|
|
233
|
+
const store = await pairStore();
|
|
234
|
+
await store.setItemAsync((0, keychainItems_1.organizationPrivateKeyName)(accountId), privateKey, pairOptions(store, true));
|
|
235
|
+
await store.setItemAsync((0, keychainItems_1.organizationPublicKeyName)(accountId), publicKey, pairOptions(store, false));
|
|
231
236
|
if (signer?.writeOrganization &&
|
|
232
237
|
!(await signer.writeOrganization(accountId, privateKey, publicKey))) {
|
|
233
238
|
throw new Error(`The identity signer store (${signer.name}) did not confirm the organization key.`);
|
|
234
239
|
}
|
|
240
|
+
const phrases = await phraseStore();
|
|
235
241
|
if (key.mnemonic !== undefined) {
|
|
236
|
-
await
|
|
237
|
-
keychainService:
|
|
238
|
-
keychainAccessible:
|
|
242
|
+
await phrases.setItemAsync((0, keychainItems_1.organizationMnemonicName)(accountId), normalizePhrase(key.mnemonic), {
|
|
243
|
+
keychainService: keychainItems_1.ORGANIZATION_MNEMONIC_KEYCHAIN_SERVICE,
|
|
244
|
+
keychainAccessible: phrases.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
239
245
|
});
|
|
240
246
|
}
|
|
241
247
|
else {
|
|
242
|
-
await
|
|
243
|
-
keychainService:
|
|
248
|
+
await phrases.deleteItemAsync((0, keychainItems_1.organizationMnemonicName)(accountId), {
|
|
249
|
+
keychainService: keychainItems_1.ORGANIZATION_MNEMONIC_KEYCHAIN_SERVICE,
|
|
244
250
|
});
|
|
245
251
|
}
|
|
246
|
-
|
|
252
|
+
const index = await indexStore();
|
|
253
|
+
await writeIndex(index, [...(await readIndex(index)), accountId]);
|
|
247
254
|
}
|
|
248
255
|
/**
|
|
249
256
|
* The organization's public key as THIS device can reach it, or `null`.
|
|
250
|
-
* iOS: the keychain group. Android: Commons' own slot inside Commons, and
|
|
257
|
+
* iOS: the wallet keychain group (Commons, the Peable wallet). Android: Commons' own slot inside Commons, and
|
|
251
258
|
* Commons' identity host (`describe` with `accountId`) everywhere else. Compare
|
|
252
259
|
* it with the organization's DID `#key-1` before receiving (Peable ADR 0002).
|
|
253
260
|
*/
|
|
254
261
|
async function getOrganizationPublicKey(accountId) {
|
|
255
|
-
if (!isOrganizationAccountId(accountId) || (0, platform_1.isWeb)())
|
|
262
|
+
if (!(0, keychainItems_1.isOrganizationAccountId)(accountId) || (0, platform_1.isWeb)())
|
|
256
263
|
return null;
|
|
257
264
|
try {
|
|
258
265
|
if ((0, platform_1.isAndroid)() && !(0, identitySigner_1.registeredIdentitySignerStore)()) {
|
|
@@ -272,11 +279,11 @@ async function getOrganizationPublicKey(accountId) {
|
|
|
272
279
|
}
|
|
273
280
|
/**
|
|
274
281
|
* The organization's private key, where this app may hold it: Commons (both
|
|
275
|
-
* platforms) and, on iOS, the
|
|
276
|
-
*
|
|
282
|
+
* platforms) and, on iOS, the Peable wallet through the wallet keychain group.
|
|
283
|
+
* `null` everywhere else. Use it to sign within the app only; never transmit it.
|
|
277
284
|
*/
|
|
278
285
|
async function getOrganizationPrivateKey(accountId) {
|
|
279
|
-
if (!isOrganizationAccountId(accountId))
|
|
286
|
+
if (!(0, keychainItems_1.isOrganizationAccountId)(accountId))
|
|
280
287
|
return null;
|
|
281
288
|
try {
|
|
282
289
|
return (await readLocalPair(accountId))?.privateKey ?? null;
|
|
@@ -291,34 +298,35 @@ async function getOrganizationPrivateKey(accountId) {
|
|
|
291
298
|
}
|
|
292
299
|
/** The organization's phrase, if this device created or imported it with one. Commons only. */
|
|
293
300
|
async function getOrganizationRecoveryPhrase(accountId) {
|
|
294
|
-
if (!isOrganizationAccountId(accountId) || (0, platform_1.isWeb)())
|
|
301
|
+
if (!(0, keychainItems_1.isOrganizationAccountId)(accountId) || (0, platform_1.isWeb)())
|
|
295
302
|
return null;
|
|
296
|
-
const store = await
|
|
297
|
-
return store.getItemAsync(
|
|
298
|
-
keychainService:
|
|
303
|
+
const store = await phraseStore();
|
|
304
|
+
return store.getItemAsync((0, keychainItems_1.organizationMnemonicName)(accountId), {
|
|
305
|
+
keychainService: keychainItems_1.ORGANIZATION_MNEMONIC_KEYCHAIN_SERVICE,
|
|
299
306
|
});
|
|
300
307
|
}
|
|
301
308
|
/** The organizations this device keeps a key for (Commons' own index). */
|
|
302
309
|
async function listOrganizationKeys() {
|
|
303
310
|
if ((0, platform_1.isWeb)())
|
|
304
311
|
return [];
|
|
305
|
-
return readIndex(await
|
|
312
|
+
return readIndex(await indexStore());
|
|
306
313
|
}
|
|
307
314
|
/** Forget an organization key on this device: its pair, its phrase, its signer copy and its index entry. */
|
|
308
315
|
async function deleteOrganizationKey(accountId) {
|
|
309
316
|
assertAccountId(accountId);
|
|
310
317
|
if ((0, platform_1.isWeb)())
|
|
311
318
|
return;
|
|
312
|
-
const store = await
|
|
319
|
+
const store = await pairStore();
|
|
313
320
|
const options = pairOptions(store, false);
|
|
314
|
-
await store.deleteItemAsync(
|
|
315
|
-
await store.deleteItemAsync(
|
|
316
|
-
await
|
|
317
|
-
keychainService:
|
|
321
|
+
await store.deleteItemAsync((0, keychainItems_1.organizationPrivateKeyName)(accountId), options);
|
|
322
|
+
await store.deleteItemAsync((0, keychainItems_1.organizationPublicKeyName)(accountId), options);
|
|
323
|
+
await (await phraseStore()).deleteItemAsync((0, keychainItems_1.organizationMnemonicName)(accountId), {
|
|
324
|
+
keychainService: keychainItems_1.ORGANIZATION_MNEMONIC_KEYCHAIN_SERVICE,
|
|
318
325
|
});
|
|
319
326
|
const signer = (0, platform_1.isAndroid)() ? (0, identitySigner_1.registeredIdentitySignerStore)() : null;
|
|
320
327
|
await signer?.clearOrganization?.(accountId);
|
|
321
|
-
|
|
328
|
+
const index = await indexStore();
|
|
329
|
+
await writeIndex(index, (await readIndex(index)).filter((id) => id !== accountId));
|
|
322
330
|
}
|
|
323
331
|
/**
|
|
324
332
|
* Commons on Android: copy every held organization key into the identity
|
|
@@ -330,12 +338,12 @@ async function syncOrganizationKeys() {
|
|
|
330
338
|
const signer = (0, platform_1.isAndroid)() ? (0, identitySigner_1.registeredIdentitySignerStore)() : null;
|
|
331
339
|
if (!signer?.writeOrganization || !signer.readOrganization)
|
|
332
340
|
return 0;
|
|
333
|
-
const store = await
|
|
341
|
+
const store = await pairStore();
|
|
334
342
|
let mirrored = 0;
|
|
335
|
-
for (const accountId of await readIndex(
|
|
343
|
+
for (const accountId of await readIndex(await indexStore())) {
|
|
336
344
|
const options = pairOptions(store, false);
|
|
337
|
-
const privateKey = await store.getItemAsync(
|
|
338
|
-
const publicKey = await store.getItemAsync(
|
|
345
|
+
const privateKey = await store.getItemAsync((0, keychainItems_1.organizationPrivateKeyName)(accountId), options);
|
|
346
|
+
const publicKey = await store.getItemAsync((0, keychainItems_1.organizationPublicKeyName)(accountId), options);
|
|
339
347
|
if (!privateKey || !publicKey || !isHealthyOrganizationKeyPair(privateKey, publicKey))
|
|
340
348
|
continue;
|
|
341
349
|
const current = await signer.readOrganization(accountId);
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* the API with a token it already has never needs it.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.
|
|
11
|
+
exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = exports.refreshDeviceSecretArm = exports.refreshPersistedSession = exports.establishIdentitySession = exports.resolveIdentityPin = exports.IDENTITY_PIN_STORAGE_KEY = exports.identityPinMatches = exports.createMemoryIdentityPinStore = exports.createNativeIdentityPinStore = exports.createWebIdentityPinStore = exports.resolveKeychainAccess = exports.deleteThroughPlan = exports.readThroughPlan = exports.ownKeychainItemPlan = exports.keychainAccessGroupValue = exports.KeychainAccessUnavailableError = exports.readLocalDeviceCredential = exports.publishProvenDeviceCredential = exports.normalizeSharedDeviceSessionRead = exports.decideSharedDevicePublish = exports.decideSharedDeviceJoin = exports.createSharedMirroringAuthStateStore = exports.AUTH_STATE_STORAGE_KEY = exports.createMemoryAuthStateStore = exports.createNativeAuthStateStore = exports.createWebAuthStateStore = exports.createAccountDialogController = exports.AccountDialogController = exports.resolveAccountDelegationAccess = exports.canSwitchIntoAccount = exports.isSwitchTargetAccount = exports.deviceComparisonCode = exports.showsPrincipalHeaders = exports.buildSwitcherRows = exports.resolveDeviceContext = exports.resolveActiveContext = exports.projectDevicePrincipals = exports.directoryHandle = exports.directoryDisplayName = exports.canActivateContext = exports.accountIdsOf = exports.activeUserOf = exports.activeSessionIdOf = exports.deviceStateToClientSessions = exports.createSessionClient = exports.createSessionClientHost = exports.SessionClient = exports.DeviceManager = void 0;
|
|
12
|
+
exports.runColdBoot = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = void 0;
|
|
12
13
|
// Session code reaches the identity key (identity-bound sessions), so the
|
|
13
14
|
// crypto shim goes first — see `../crypto/polyfill`.
|
|
14
15
|
require("../crypto/polyfill");
|
|
@@ -107,6 +108,15 @@ Object.defineProperty(exports, "decideSharedDevicePublish", { enumerable: true,
|
|
|
107
108
|
Object.defineProperty(exports, "normalizeSharedDeviceSessionRead", { enumerable: true, get: function () { return sharedDeviceCredential_1.normalizeSharedDeviceSessionRead; } });
|
|
108
109
|
Object.defineProperty(exports, "publishProvenDeviceCredential", { enumerable: true, get: function () { return sharedDeviceCredential_1.publishProvenDeviceCredential; } });
|
|
109
110
|
Object.defineProperty(exports, "readLocalDeviceCredential", { enumerable: true, get: function () { return sharedDeviceCredential_1.readLocalDeviceCredential; } });
|
|
111
|
+
// Where the shared device credential (and an app's own items) live on iOS:
|
|
112
|
+
// docs/identity/ios-keychain-access-groups.md, ADR 0062.
|
|
113
|
+
var keychainAccessGroups_1 = require("../utils/keychainAccessGroups");
|
|
114
|
+
Object.defineProperty(exports, "KeychainAccessUnavailableError", { enumerable: true, get: function () { return keychainAccessGroups_1.KeychainAccessUnavailableError; } });
|
|
115
|
+
Object.defineProperty(exports, "keychainAccessGroupValue", { enumerable: true, get: function () { return keychainAccessGroups_1.keychainAccessGroupValue; } });
|
|
116
|
+
Object.defineProperty(exports, "ownKeychainItemPlan", { enumerable: true, get: function () { return keychainAccessGroups_1.ownKeychainItemPlan; } });
|
|
117
|
+
Object.defineProperty(exports, "readThroughPlan", { enumerable: true, get: function () { return keychainAccessGroups_1.readThroughPlan; } });
|
|
118
|
+
Object.defineProperty(exports, "deleteThroughPlan", { enumerable: true, get: function () { return keychainAccessGroups_1.deleteThroughPlan; } });
|
|
119
|
+
Object.defineProperty(exports, "resolveKeychainAccess", { enumerable: true, get: function () { return keychainAccessGroups_1.resolveKeychainAccess; } });
|
|
110
120
|
// Identity-bound sessions (the identity vault). The pin is the durable
|
|
111
121
|
// `{publicKey, accountId}` binding between this device's PRIMARY identity key
|
|
112
122
|
// and the account it authenticates as; it is what keeps such a client from
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeychainAccessUnavailableError = exports.KEYCHAIN_ACCESS_GROUPS = void 0;
|
|
4
|
+
exports.keychainGroupFor = keychainGroupFor;
|
|
5
|
+
exports.mayHoldKeychainItem = mayHoldKeychainItem;
|
|
6
|
+
exports.keychainAccessFromDefaultGroup = keychainAccessFromDefaultGroup;
|
|
7
|
+
exports.resolveKeychainAccess = resolveKeychainAccess;
|
|
8
|
+
exports.resetKeychainAccessForTests = resetKeychainAccessForTests;
|
|
9
|
+
exports.keychainAccessGroupValue = keychainAccessGroupValue;
|
|
10
|
+
exports.keychainReadPlan = keychainReadPlan;
|
|
11
|
+
exports.keychainDeletePlan = keychainDeletePlan;
|
|
12
|
+
exports.keychainWriteOption = keychainWriteOption;
|
|
13
|
+
exports.ownKeychainItemPlan = ownKeychainItemPlan;
|
|
14
|
+
exports.readThroughPlan = readThroughPlan;
|
|
15
|
+
exports.deleteThroughPlan = deleteThroughPlan;
|
|
16
|
+
exports.groupedKeychainStore = groupedKeychainStore;
|
|
17
|
+
/**
|
|
18
|
+
* iOS keychain access groups: which group each Oxy keychain item lives in, and
|
|
19
|
+
* which apps may read it (docs/identity/ios-keychain-access-groups.md, ADR 0062).
|
|
20
|
+
*
|
|
21
|
+
* | Group | Held by | Item class |
|
|
22
|
+
* |-------------------------------|------------------------------|-------------------|
|
|
23
|
+
* | `<TEAM>.so.oxy.identity` | Commons only (listed FIRST) | `identity-secret` |
|
|
24
|
+
* | `<TEAM>.so.oxy.wallet` | Commons, the Peable wallet | `spending-key` |
|
|
25
|
+
* | `<TEAM>.group.so.oxy.shared` | every Oxy app | `shared` |
|
|
26
|
+
*
|
|
27
|
+
* - `identity-secret`: the personal root (primary and backup slots), the
|
|
28
|
+
* personal and organization recovery phrases, the organization index. Only
|
|
29
|
+
* Commons may read or write them.
|
|
30
|
+
* - `spending-key`: private keys an app other than Commons signs or derives
|
|
31
|
+
* with in process — the personal key's shared slot and organization pairs
|
|
32
|
+
* (`deriveScopedSeed`, `signSocialReceive`). Commons writes them; the Peable
|
|
33
|
+
* wallet reads them.
|
|
34
|
+
* - `shared`: what every Oxy app may read: the shared public key and the
|
|
35
|
+
* device-session credential.
|
|
36
|
+
*
|
|
37
|
+
* The ROLE of this binary comes from the keychain itself: an item written
|
|
38
|
+
* without a group lands in the app's default group, the first entry of its
|
|
39
|
+
* `keychain-access-groups` entitlement, and `@oxy.so/app-preset` lets only
|
|
40
|
+
* Commons put the identity group first and only the Peable wallet the wallet
|
|
41
|
+
* group. The same probe gives the team prefix, which `kSecAttrAccessGroup`
|
|
42
|
+
* needs and JavaScript cannot otherwise see.
|
|
43
|
+
*
|
|
44
|
+
* Android and web have no access groups: there every helper here answers the
|
|
45
|
+
* single-store behaviour (`ungrouped`), and Android's narrowing is Commons'
|
|
46
|
+
* identity host. An iOS build whose native module predates the probe is also
|
|
47
|
+
* `ungrouped` — exactly what that build's entitlements (one shared group) do.
|
|
48
|
+
*
|
|
49
|
+
* This module never imports the identity stack: services' device-session store
|
|
50
|
+
* and Peable's wallet storage use it too.
|
|
51
|
+
*/
|
|
52
|
+
const protocol_1 = require("@oxy.so/protocol");
|
|
53
|
+
const platform_1 = require("./platform");
|
|
54
|
+
/** The group names after the team prefix, exactly as the entitlements list them. */
|
|
55
|
+
exports.KEYCHAIN_ACCESS_GROUPS = {
|
|
56
|
+
identity: 'so.oxy.identity',
|
|
57
|
+
wallet: 'so.oxy.wallet',
|
|
58
|
+
shared: 'group.so.oxy.shared',
|
|
59
|
+
};
|
|
60
|
+
/** Thrown while the probe cannot answer (before first unlock). Transient. */
|
|
61
|
+
class KeychainAccessUnavailableError extends Error {
|
|
62
|
+
constructor(cause) {
|
|
63
|
+
super('The keychain cannot say which access groups this app holds right now.');
|
|
64
|
+
this.cause = cause;
|
|
65
|
+
this.name = 'KeychainAccessUnavailableError';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.KeychainAccessUnavailableError = KeychainAccessUnavailableError;
|
|
69
|
+
/**
|
|
70
|
+
* The single place each item class is assigned a group. A new kind of item
|
|
71
|
+
* (F1's retained keys, for one) picks its class and reuses everything below.
|
|
72
|
+
*/
|
|
73
|
+
function keychainGroupFor(item) {
|
|
74
|
+
switch (item) {
|
|
75
|
+
case 'identity-secret':
|
|
76
|
+
return 'identity';
|
|
77
|
+
case 'spending-key':
|
|
78
|
+
return 'wallet';
|
|
79
|
+
case 'shared':
|
|
80
|
+
return 'shared';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Whether `role` holds the group `item` lives in. */
|
|
84
|
+
function mayHoldKeychainItem(role, item) {
|
|
85
|
+
switch (role) {
|
|
86
|
+
case 'holder':
|
|
87
|
+
return true;
|
|
88
|
+
case 'wallet':
|
|
89
|
+
return item !== 'identity-secret';
|
|
90
|
+
case 'member':
|
|
91
|
+
return item === 'shared';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Parse the default access group the probe reported. Exported for tests. */
|
|
95
|
+
function keychainAccessFromDefaultGroup(defaultGroup) {
|
|
96
|
+
const dot = defaultGroup.indexOf('.');
|
|
97
|
+
if (dot <= 0 || dot === defaultGroup.length - 1) {
|
|
98
|
+
// Not `<prefix>.<group>`: an unsigned simulator build. Nothing to split.
|
|
99
|
+
return { mode: 'ungrouped' };
|
|
100
|
+
}
|
|
101
|
+
const prefix = defaultGroup.slice(0, dot + 1);
|
|
102
|
+
const group = defaultGroup.slice(dot + 1);
|
|
103
|
+
const role = group === exports.KEYCHAIN_ACCESS_GROUPS.identity
|
|
104
|
+
? 'holder'
|
|
105
|
+
: group === exports.KEYCHAIN_ACCESS_GROUPS.wallet
|
|
106
|
+
? 'wallet'
|
|
107
|
+
: 'member';
|
|
108
|
+
return { mode: 'grouped', prefix, defaultGroup, role };
|
|
109
|
+
}
|
|
110
|
+
let resolvedAccess = null;
|
|
111
|
+
/**
|
|
112
|
+
* This binary's keychain access, memoized once known. THROWS
|
|
113
|
+
* {@link KeychainAccessUnavailableError} while the probe cannot answer, which
|
|
114
|
+
* every caller already treats as "storage unavailable, retry".
|
|
115
|
+
*/
|
|
116
|
+
async function resolveKeychainAccess() {
|
|
117
|
+
if (resolvedAccess) {
|
|
118
|
+
return resolvedAccess;
|
|
119
|
+
}
|
|
120
|
+
if (!(0, platform_1.isIOS)()) {
|
|
121
|
+
// Not memoized: until `@oxy.so/services` registers the platform, a phone
|
|
122
|
+
// reads as "not iOS", and caching that would keep the whole process on the
|
|
123
|
+
// pre-split behaviour.
|
|
124
|
+
return { mode: 'ungrouped' };
|
|
125
|
+
}
|
|
126
|
+
let probe;
|
|
127
|
+
try {
|
|
128
|
+
probe = await (0, protocol_1.loadKeychainDefaultAccessGroup)();
|
|
129
|
+
}
|
|
130
|
+
catch (cause) {
|
|
131
|
+
throw new KeychainAccessUnavailableError(cause);
|
|
132
|
+
}
|
|
133
|
+
if (probe.state === 'unavailable') {
|
|
134
|
+
throw new KeychainAccessUnavailableError(probe.cause);
|
|
135
|
+
}
|
|
136
|
+
resolvedAccess =
|
|
137
|
+
probe.state === 'resolved'
|
|
138
|
+
? keychainAccessFromDefaultGroup(probe.accessGroup)
|
|
139
|
+
: { mode: 'ungrouped' };
|
|
140
|
+
return resolvedAccess;
|
|
141
|
+
}
|
|
142
|
+
/** Forget the memoized access. Tests only. @internal */
|
|
143
|
+
function resetKeychainAccessForTests() {
|
|
144
|
+
resolvedAccess = null;
|
|
145
|
+
}
|
|
146
|
+
/** The full `kSecAttrAccessGroup` value of `group`, or `undefined` when ungrouped. */
|
|
147
|
+
function keychainAccessGroupValue(access, group) {
|
|
148
|
+
return access.mode === 'grouped' ? `${access.prefix}${exports.KEYCHAIN_ACCESS_GROUPS[group]}` : undefined;
|
|
149
|
+
}
|
|
150
|
+
function option(value) {
|
|
151
|
+
return value === undefined ? {} : { accessGroup: value };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The option bags a READ of `item` tries, in order: its own group, then the
|
|
155
|
+
* shared group, where every item lived before the split (release 4 of the doc
|
|
156
|
+
* drops that fallback). Empty when this app may not hold the item: the caller
|
|
157
|
+
* answers `null` without touching the keychain.
|
|
158
|
+
*/
|
|
159
|
+
function keychainReadPlan(access, item) {
|
|
160
|
+
if (access.mode === 'ungrouped') {
|
|
161
|
+
return [{}];
|
|
162
|
+
}
|
|
163
|
+
if (!mayHoldKeychainItem(access.role, item)) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const own = keychainGroupFor(item);
|
|
167
|
+
const plan = [option(keychainAccessGroupValue(access, own))];
|
|
168
|
+
if (own !== 'shared') {
|
|
169
|
+
plan.push(option(keychainAccessGroupValue(access, 'shared')));
|
|
170
|
+
}
|
|
171
|
+
return plan;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Every place a DELETE of `item` must reach: the read plan, so a delete never
|
|
175
|
+
* leaves a pre-split copy behind in the shared group.
|
|
176
|
+
*/
|
|
177
|
+
function keychainDeletePlan(access, item) {
|
|
178
|
+
return keychainReadPlan(access, item);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The option a WRITE of `item` carries. THROWS when this app may not hold the
|
|
182
|
+
* item: an identity secret written by any app but Commons would land in that
|
|
183
|
+
* app's default group, a second holder nobody knows about.
|
|
184
|
+
*/
|
|
185
|
+
function keychainWriteOption(access, item) {
|
|
186
|
+
if (access.mode === 'ungrouped') {
|
|
187
|
+
return {};
|
|
188
|
+
}
|
|
189
|
+
if (!mayHoldKeychainItem(access.role, item)) {
|
|
190
|
+
throw new Error(`This app (keychain role "${access.role}") may not hold a ${item} keychain item; only Commons writes the identity.`);
|
|
191
|
+
}
|
|
192
|
+
return option(keychainAccessGroupValue(access, keychainGroupFor(item)));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The plan for an app's OWN items (its session credential, its settings, an
|
|
196
|
+
* imported wallet's phrase) in Commons and the Peable wallet, whose default
|
|
197
|
+
* group the split changed: written to the default group explicitly — where a
|
|
198
|
+
* write without a group goes anyway — and read from it FIRST, then from every
|
|
199
|
+
* group (`{}`), where all of them were written before. Without the explicit
|
|
200
|
+
* order a read could return whichever of two copies the keychain met first.
|
|
201
|
+
* Deletes reach every group, as they always did.
|
|
202
|
+
*
|
|
203
|
+
* Every other app (`member`) and Android/web keep the calls they always made.
|
|
204
|
+
*/
|
|
205
|
+
function ownKeychainItemPlan(access) {
|
|
206
|
+
if (access.mode === 'ungrouped' || access.role === 'member') {
|
|
207
|
+
return { read: [{}], write: {}, delete: [{}] };
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
read: [option(access.defaultGroup), {}],
|
|
211
|
+
write: option(access.defaultGroup),
|
|
212
|
+
delete: [{}],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** Read `key` through `plan`: the first non-null answer. A thrown read propagates. */
|
|
216
|
+
async function readThroughPlan(store, key, options, plan) {
|
|
217
|
+
for (const group of plan) {
|
|
218
|
+
const value = await store.getItemAsync(key, { ...options, ...group });
|
|
219
|
+
if (value !== null && value !== undefined) {
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
/** Delete `key` from every location in `plan`. A thrown delete propagates. */
|
|
226
|
+
async function deleteThroughPlan(store, key, options, plan) {
|
|
227
|
+
for (const group of plan) {
|
|
228
|
+
await store.deleteItemAsync(key, { ...options, ...group });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* `store` with every call routed to `item`'s groups for this app: reads walk
|
|
233
|
+
* the read plan, writes carry the write option (throwing for a role that may
|
|
234
|
+
* not hold the item), deletes reach every location. The constants of the
|
|
235
|
+
* wrapped store are kept, so `store.WHEN_UNLOCKED_THIS_DEVICE_ONLY` still reads.
|
|
236
|
+
*/
|
|
237
|
+
function groupedKeychainStore(store, access, item) {
|
|
238
|
+
const plan = keychainReadPlan(access, item);
|
|
239
|
+
const wrapped = {
|
|
240
|
+
getItemAsync: (key, options) => readThroughPlan(store, key, options ?? {}, plan),
|
|
241
|
+
setItemAsync: async (key, value, options) => {
|
|
242
|
+
const group = keychainWriteOption(access, item);
|
|
243
|
+
await store.setItemAsync(key, value, { ...(options ?? {}), ...group });
|
|
244
|
+
},
|
|
245
|
+
deleteItemAsync: (key, options) => deleteThroughPlan(store, key, options ?? {}, plan),
|
|
246
|
+
};
|
|
247
|
+
return Object.assign(Object.create(store), wrapped);
|
|
248
|
+
}
|
package/dist/esm/crypto/index.js
CHANGED
|
@@ -17,6 +17,9 @@ export { KeyManager, IdentityAlreadyExistsError, IdentityPersistError, IdentityU
|
|
|
17
17
|
export { readIdentityMarker, updateIdentityMarker } from './identityMarker.js';
|
|
18
18
|
// Organization keys on the device (docs/adr/0061-organization-identity-key.md D6)
|
|
19
19
|
export { generateOrganizationKey, organizationKeyFromPhrase, storeOrganizationKey, getOrganizationPublicKey, getOrganizationPrivateKey, getOrganizationRecoveryPhrase, listOrganizationKeys, deleteOrganizationKey, syncOrganizationKeys, isOrganizationAccountId, isHealthyOrganizationKeyPair, } from './organizationKeys.js';
|
|
20
|
+
// iOS keychain access groups (docs/identity/ios-keychain-access-groups.md, ADR 0062)
|
|
21
|
+
export { KEYCHAIN_ACCESS_GROUPS, KeychainAccessUnavailableError, keychainGroupFor, mayHoldKeychainItem, resolveKeychainAccess, keychainAccessGroupValue, keychainReadPlan, keychainDeletePlan, keychainWriteOption, ownKeychainItemPlan, readThroughPlan, deleteThroughPlan, groupedKeychainStore, } from '../utils/keychainAccessGroups.js';
|
|
22
|
+
export { ensureKeychainGroupsMigrated } from './keychainGroupMigration.js';
|
|
20
23
|
export { SignatureService } from './signatureService.js';
|
|
21
24
|
export { RecoveryPhraseService, BACKUP_KDF_ENCRYPTION_INFO } from './recoveryPhrase.js';
|
|
22
25
|
// Low-level primitives (encrypted backup, device transfer)
|