@oxy.so/core 7.28.0 → 7.30.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.
Files changed (44) hide show
  1. package/dist/cjs/api/identity.js +40 -1
  2. package/dist/cjs/crypto/index.js +28 -1
  3. package/dist/cjs/crypto/keyManager.js +175 -107
  4. package/dist/cjs/crypto/keychainGroupMigration.js +353 -0
  5. package/dist/cjs/crypto/keychainItems.js +70 -0
  6. package/dist/cjs/crypto/organizationKeys.js +80 -72
  7. package/dist/cjs/crypto/retiredKeys.js +371 -0
  8. package/dist/cjs/crypto/secureStoreOptions.js +2 -0
  9. package/dist/cjs/session/index.js +11 -1
  10. package/dist/cjs/utils/keychainAccessGroups.js +248 -0
  11. package/dist/esm/api/identity.js +40 -1
  12. package/dist/esm/crypto/index.js +6 -0
  13. package/dist/esm/crypto/keyManager.js +132 -64
  14. package/dist/esm/crypto/keychainGroupMigration.js +348 -0
  15. package/dist/esm/crypto/keychainItems.js +61 -0
  16. package/dist/esm/crypto/organizationKeys.js +54 -46
  17. package/dist/esm/crypto/retiredKeys.js +361 -0
  18. package/dist/esm/crypto/secureStoreOptions.js +1 -0
  19. package/dist/esm/session/index.js +3 -0
  20. package/dist/esm/utils/keychainAccessGroups.js +231 -0
  21. package/dist/types/api/identity.d.ts +10 -0
  22. package/dist/types/crypto/identitySigner.d.ts +23 -0
  23. package/dist/types/crypto/index.d.ts +6 -0
  24. package/dist/types/crypto/keyManager.d.ts +32 -2
  25. package/dist/types/crypto/keychainGroupMigration.d.ts +27 -0
  26. package/dist/types/crypto/keychainItems.d.ts +41 -0
  27. package/dist/types/crypto/organizationKeys.d.ts +11 -9
  28. package/dist/types/crypto/retiredKeys.d.ts +116 -0
  29. package/dist/types/crypto/secureStoreOptions.d.ts +15 -0
  30. package/dist/types/session/index.d.ts +2 -0
  31. package/dist/types/utils/keychainAccessGroups.d.ts +104 -0
  32. package/package.json +3 -3
  33. package/src/api/identity.ts +46 -3
  34. package/src/crypto/identitySigner.ts +23 -0
  35. package/src/crypto/index.ts +40 -0
  36. package/src/crypto/keyManager.ts +165 -102
  37. package/src/crypto/keychainGroupMigration.ts +440 -0
  38. package/src/crypto/keychainItems.ts +73 -0
  39. package/src/crypto/organizationKeys.ts +72 -57
  40. package/src/crypto/retiredKeys.ts +453 -0
  41. package/src/crypto/secureStoreOptions.ts +32 -0
  42. package/src/session/index.ts +11 -0
  43. package/src/utils/keychainAccessGroups.ts +308 -0
  44. 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 access group `group.so.oxy.shared`, like the personal
13
- * shared slot. Only Commons writes it.
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 keychain group, an IPC answer or a log.
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 = 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
- /** Same set the server's account ids come from: a legacy 24-hex id or a lowercase uuid. */
46
- const ACCOUNT_ID = /^(?:[0-9a-f]{24}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
47
- const IOS_KEYCHAIN_GROUP = 'group.so.oxy.shared';
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
- async function secureStore() {
119
- return (0, protocol_1.loadSecureStore)();
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(INDEX_STORAGE_KEY, {
123
- keychainService: INDEX_KEYCHAIN_SERVICE,
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(INDEX_STORAGE_KEY, JSON.stringify([...new Set(ids)].sort()), {
143
- keychainService: INDEX_KEYCHAIN_SERVICE,
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
- /** Where this platform keeps the pair: the iOS keychain group, or Commons' own Android slot. */
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: ANDROID_KEYCHAIN_SERVICE,
162
+ keychainService: keychainItems_1.ORGANIZATION_ANDROID_KEYCHAIN_SERVICE,
160
163
  }
161
- : { keychainService: ANDROID_KEYCHAIN_SERVICE };
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 secureStore();
174
+ const store = await pairStore();
172
175
  const options = pairOptions(store, false);
173
- const privateKey = await store.getItemAsync(privateKeyName(accountId), options);
174
- const publicKey = await store.getItemAsync(publicKeyName(accountId), options);
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
- const store = await secureStore();
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(privateKeyName(accountId), options);
192
- const publicKey = await store.getItemAsync(publicKeyName(accountId), options);
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 calls it (the keychain group is shared).
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 secureStore();
229
- await store.setItemAsync(privateKeyName(accountId), privateKey, pairOptions(store, true));
230
- await store.setItemAsync(publicKeyName(accountId), publicKey, pairOptions(store, false));
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 store.setItemAsync(mnemonicName(accountId), normalizePhrase(key.mnemonic), {
237
- keychainService: MNEMONIC_KEYCHAIN_SERVICE,
238
- keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
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 store.deleteItemAsync(mnemonicName(accountId), {
243
- keychainService: MNEMONIC_KEYCHAIN_SERVICE,
248
+ await phrases.deleteItemAsync((0, keychainItems_1.organizationMnemonicName)(accountId), {
249
+ keychainService: keychainItems_1.ORGANIZATION_MNEMONIC_KEYCHAIN_SERVICE,
244
250
  });
245
251
  }
246
- await writeIndex(store, [...(await readIndex(store)), accountId]);
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 keychain group. `null` on Android outside
276
- * Commons. Use it to sign within the app only; never transmit it.
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 secureStore();
297
- return store.getItemAsync(mnemonicName(accountId), {
298
- keychainService: MNEMONIC_KEYCHAIN_SERVICE,
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 secureStore());
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 secureStore();
319
+ const store = await pairStore();
313
320
  const options = pairOptions(store, false);
314
- await store.deleteItemAsync(privateKeyName(accountId), options);
315
- await store.deleteItemAsync(publicKeyName(accountId), options);
316
- await store.deleteItemAsync(mnemonicName(accountId), {
317
- keychainService: MNEMONIC_KEYCHAIN_SERVICE,
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
- await writeIndex(store, (await readIndex(store)).filter((id) => id !== accountId));
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 secureStore();
341
+ const store = await pairStore();
334
342
  let mirrored = 0;
335
- for (const accountId of await readIndex(store)) {
343
+ for (const accountId of await readIndex(await indexStore())) {
336
344
  const options = pairOptions(store, false);
337
- const privateKey = await store.getItemAsync(privateKeyName(accountId), options);
338
- const publicKey = await store.getItemAsync(publicKeyName(accountId), options);
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);