@oxy.so/core 7.27.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.
Files changed (47) hide show
  1. package/dist/cjs/crypto/index.js +19 -1
  2. package/dist/cjs/crypto/keyManager.js +106 -106
  3. package/dist/cjs/crypto/keychainGroupMigration.js +353 -0
  4. package/dist/cjs/crypto/keychainItems.js +70 -0
  5. package/dist/cjs/crypto/organizationKeys.js +80 -72
  6. package/dist/cjs/crypto/secureStoreOptions.js +2 -0
  7. package/dist/cjs/i18n/locales/compact/es-ES.generated.js +1 -1
  8. package/dist/cjs/i18n/locales/en-US.js +14 -0
  9. package/dist/cjs/i18n/locales/es-ES.js +14 -0
  10. package/dist/cjs/session/index.js +11 -1
  11. package/dist/cjs/utils/keychainAccessGroups.js +248 -0
  12. package/dist/esm/crypto/index.js +3 -0
  13. package/dist/esm/crypto/keyManager.js +63 -63
  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/secureStoreOptions.js +1 -0
  18. package/dist/esm/i18n/locales/compact/es-ES.generated.js +1 -1
  19. package/dist/esm/i18n/locales/en-US.js +14 -0
  20. package/dist/esm/i18n/locales/es-ES.js +14 -0
  21. package/dist/esm/session/index.js +3 -0
  22. package/dist/esm/utils/keychainAccessGroups.js +231 -0
  23. package/dist/types/crypto/index.d.ts +4 -0
  24. package/dist/types/crypto/keyManager.d.ts +9 -1
  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/secureStoreOptions.d.ts +15 -0
  29. package/dist/types/i18n/locales/en-US.d.ts +14 -0
  30. package/dist/types/i18n/locales/es-ES.d.ts +14 -0
  31. package/dist/types/models/interfaces.d.ts +6 -1
  32. package/dist/types/session/index.d.ts +2 -0
  33. package/dist/types/utils/keychainAccessGroups.d.ts +104 -0
  34. package/package.json +2 -2
  35. package/src/crypto/index.ts +29 -0
  36. package/src/crypto/keyManager.ts +84 -99
  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/secureStoreOptions.ts +32 -0
  41. package/src/i18n/locales/compact/es-ES.generated.ts +1 -1
  42. package/src/i18n/locales/en-US.ts +14 -0
  43. package/src/i18n/locales/es-ES.ts +14 -0
  44. package/src/models/interfaces.ts +6 -0
  45. package/src/session/index.ts +11 -0
  46. package/src/utils/keychainAccessGroups.ts +308 -0
  47. package/src/types/expo-secure-store.d.ts +0 -21
@@ -12,7 +12,8 @@
12
12
  * common chunk, which is the cost the lazy loading exists to avoid.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.verifySignature = exports.signMessage = exports.loadCommonsIdentityBridge = exports.canonicalize = exports.createContentHasher = exports.solveRegistrationPow = exports.deriveIdentityLinkCode = exports.signIdentityProof = exports.digestIdentityPayload = exports.authChallengeDigest = exports.signSocialReceiveDigest = exports.deriveScopedSeedFromKey = exports.MAX_SOCIAL_RECEIVE_INDEX = exports.SOCIAL_RECEIVE_CHAIN_CODE_KEY = exports.SCOPED_SEED_KDF_SALT = exports.deriveSharedSecret = exports.AEAD_NONCE_LENGTH = exports.AEAD_KEY_LENGTH = exports.decryptAead = exports.encryptAead = exports.hkdfSha256 = exports.BACKUP_KDF_ENCRYPTION_INFO = exports.RecoveryPhraseService = exports.SignatureService = exports.isHealthyOrganizationKeyPair = exports.isOrganizationAccountId = exports.syncOrganizationKeys = exports.deleteOrganizationKey = exports.listOrganizationKeys = exports.getOrganizationRecoveryPhrase = exports.getOrganizationPrivateKey = exports.getOrganizationPublicKey = exports.storeOrganizationKey = exports.organizationKeyFromPhrase = exports.generateOrganizationKey = exports.updateIdentityMarker = exports.readIdentityMarker = exports.IdentityUnavailableError = exports.IdentityPersistError = exports.IdentityAlreadyExistsError = exports.KeyManager = void 0;
15
+ exports.solveRegistrationPow = exports.deriveIdentityLinkCode = exports.signIdentityProof = exports.digestIdentityPayload = exports.authChallengeDigest = exports.signSocialReceiveDigest = exports.deriveScopedSeedFromKey = exports.MAX_SOCIAL_RECEIVE_INDEX = exports.SOCIAL_RECEIVE_CHAIN_CODE_KEY = exports.SCOPED_SEED_KDF_SALT = exports.deriveSharedSecret = exports.AEAD_NONCE_LENGTH = exports.AEAD_KEY_LENGTH = exports.decryptAead = exports.encryptAead = exports.hkdfSha256 = exports.BACKUP_KDF_ENCRYPTION_INFO = exports.RecoveryPhraseService = exports.SignatureService = exports.ensureKeychainGroupsMigrated = exports.groupedKeychainStore = exports.deleteThroughPlan = exports.readThroughPlan = exports.ownKeychainItemPlan = exports.keychainWriteOption = exports.keychainDeletePlan = exports.keychainReadPlan = exports.keychainAccessGroupValue = exports.resolveKeychainAccess = exports.mayHoldKeychainItem = exports.keychainGroupFor = exports.KeychainAccessUnavailableError = exports.KEYCHAIN_ACCESS_GROUPS = exports.isHealthyOrganizationKeyPair = exports.isOrganizationAccountId = exports.syncOrganizationKeys = exports.deleteOrganizationKey = exports.listOrganizationKeys = exports.getOrganizationRecoveryPhrase = exports.getOrganizationPrivateKey = exports.getOrganizationPublicKey = exports.storeOrganizationKey = exports.organizationKeyFromPhrase = exports.generateOrganizationKey = exports.updateIdentityMarker = exports.readIdentityMarker = exports.IdentityUnavailableError = exports.IdentityPersistError = exports.IdentityAlreadyExistsError = exports.KeyManager = void 0;
16
+ exports.verifySignature = exports.signMessage = exports.loadCommonsIdentityBridge = exports.canonicalize = exports.createContentHasher = void 0;
16
17
  // MUST stay the first import: the shim has to exist before any module below
17
18
  // evaluates or draws randomness (see `./polyfill`).
18
19
  require("./polyfill");
@@ -37,6 +38,23 @@ Object.defineProperty(exports, "deleteOrganizationKey", { enumerable: true, get:
37
38
  Object.defineProperty(exports, "syncOrganizationKeys", { enumerable: true, get: function () { return organizationKeys_1.syncOrganizationKeys; } });
38
39
  Object.defineProperty(exports, "isOrganizationAccountId", { enumerable: true, get: function () { return organizationKeys_1.isOrganizationAccountId; } });
39
40
  Object.defineProperty(exports, "isHealthyOrganizationKeyPair", { enumerable: true, get: function () { return organizationKeys_1.isHealthyOrganizationKeyPair; } });
41
+ // iOS keychain access groups (docs/identity/ios-keychain-access-groups.md, ADR 0062)
42
+ var keychainAccessGroups_1 = require("../utils/keychainAccessGroups");
43
+ Object.defineProperty(exports, "KEYCHAIN_ACCESS_GROUPS", { enumerable: true, get: function () { return keychainAccessGroups_1.KEYCHAIN_ACCESS_GROUPS; } });
44
+ Object.defineProperty(exports, "KeychainAccessUnavailableError", { enumerable: true, get: function () { return keychainAccessGroups_1.KeychainAccessUnavailableError; } });
45
+ Object.defineProperty(exports, "keychainGroupFor", { enumerable: true, get: function () { return keychainAccessGroups_1.keychainGroupFor; } });
46
+ Object.defineProperty(exports, "mayHoldKeychainItem", { enumerable: true, get: function () { return keychainAccessGroups_1.mayHoldKeychainItem; } });
47
+ Object.defineProperty(exports, "resolveKeychainAccess", { enumerable: true, get: function () { return keychainAccessGroups_1.resolveKeychainAccess; } });
48
+ Object.defineProperty(exports, "keychainAccessGroupValue", { enumerable: true, get: function () { return keychainAccessGroups_1.keychainAccessGroupValue; } });
49
+ Object.defineProperty(exports, "keychainReadPlan", { enumerable: true, get: function () { return keychainAccessGroups_1.keychainReadPlan; } });
50
+ Object.defineProperty(exports, "keychainDeletePlan", { enumerable: true, get: function () { return keychainAccessGroups_1.keychainDeletePlan; } });
51
+ Object.defineProperty(exports, "keychainWriteOption", { enumerable: true, get: function () { return keychainAccessGroups_1.keychainWriteOption; } });
52
+ Object.defineProperty(exports, "ownKeychainItemPlan", { enumerable: true, get: function () { return keychainAccessGroups_1.ownKeychainItemPlan; } });
53
+ Object.defineProperty(exports, "readThroughPlan", { enumerable: true, get: function () { return keychainAccessGroups_1.readThroughPlan; } });
54
+ Object.defineProperty(exports, "deleteThroughPlan", { enumerable: true, get: function () { return keychainAccessGroups_1.deleteThroughPlan; } });
55
+ Object.defineProperty(exports, "groupedKeychainStore", { enumerable: true, get: function () { return keychainAccessGroups_1.groupedKeychainStore; } });
56
+ var keychainGroupMigration_1 = require("./keychainGroupMigration");
57
+ Object.defineProperty(exports, "ensureKeychainGroupsMigrated", { enumerable: true, get: function () { return keychainGroupMigration_1.ensureKeychainGroupsMigrated; } });
40
58
  var signatureService_1 = require("./signatureService");
41
59
  Object.defineProperty(exports, "SignatureService", { enumerable: true, get: function () { return signatureService_1.SignatureService; } });
42
60
  var recoveryPhrase_1 = require("./recoveryPhrase");
@@ -47,6 +47,9 @@ const logger_1 = require("../logger");
47
47
  const identityDerivations_1 = require("./identityDerivations");
48
48
  const identitySigner_1 = require("./identitySigner");
49
49
  const organizationKeys_1 = require("./organizationKeys");
50
+ const keychainAccessGroups_1 = require("../utils/keychainAccessGroups");
51
+ const keychainGroupMigration_1 = require("./keychainGroupMigration");
52
+ const keychainItems_1 = require("./keychainItems");
50
53
  const identityMarker_1 = require("./identityMarker");
51
54
  const deviceBackup_1 = require("./deviceBackup");
52
55
  /**
@@ -107,16 +110,6 @@ class IdentityUnavailableError extends Error {
107
110
  }
108
111
  }
109
112
  exports.IdentityUnavailableError = IdentityUnavailableError;
110
- const STORAGE_KEYS = {
111
- PRIVATE_KEY: 'oxy_identity_private_key',
112
- PUBLIC_KEY: 'oxy_identity_public_key',
113
- BACKUP_PRIVATE_KEY: 'oxy_identity_backup_private_key',
114
- BACKUP_PUBLIC_KEY: 'oxy_identity_backup_public_key',
115
- BACKUP_TIMESTAMP: 'oxy_identity_backup_timestamp',
116
- // The cross-app identity in the iOS keychain access group (iOS only).
117
- SHARED_PRIVATE_KEY: 'oxy_shared_identity_private_key',
118
- SHARED_PUBLIC_KEY: 'oxy_shared_identity_public_key',
119
- };
120
113
  /**
121
114
  * v2 identity slot layout — blast-radius isolation.
122
115
  *
@@ -132,15 +125,8 @@ const STORAGE_KEYS = {
132
125
  * Migration from the legacy layout is lazy + verify-before-delete — see
133
126
  * {@link KeyManager._runSlotMigration}.
134
127
  */
135
- const V2_PRIMARY_KEYCHAIN_SERVICE = 'oxy_identity';
136
- const V2_BACKUP_KEYCHAIN_SERVICE = 'oxy_identity_backup';
137
- const V2_STORAGE_KEYS = {
138
- PRIVATE_KEY: 'oxy_identity_private_key_v2',
139
- PUBLIC_KEY: 'oxy_identity_public_key_v2',
140
- BACKUP_PRIVATE_KEY: 'oxy_identity_backup_private_key_v2',
141
- BACKUP_PUBLIC_KEY: 'oxy_identity_backup_public_key_v2',
142
- BACKUP_TIMESTAMP: 'oxy_identity_backup_timestamp_v2',
143
- };
128
+ // The names (V2_PRIMARY_KEYCHAIN_SERVICE, V2_BACKUP_KEYCHAIN_SERVICE,
129
+ // V2_STORAGE_KEYS) live in `./keychainItems`, beside every other identity item.
144
130
  /**
145
131
  * Dedicated keychain slot for the recovery mnemonic (the 12-word phrase).
146
132
  *
@@ -157,8 +143,7 @@ const V2_STORAGE_KEYS = {
157
143
  * lives ONLY in this slot: it is never mirrored into the identity marker,
158
144
  * {@link KeyManager.getIdentityStatus}, logs, or any exported bundle.
159
145
  */
160
- const RECOVERY_MNEMONIC_KEYCHAIN_SERVICE = 'oxy_identity_mnemonic';
161
- const RECOVERY_MNEMONIC_STORAGE_KEY = 'oxy_identity_mnemonic_v1';
146
+ // RECOVERY_MNEMONIC_KEYCHAIN_SERVICE / RECOVERY_MNEMONIC_STORAGE_KEY: `./keychainItems`.
162
147
  /**
163
148
  * Advisory AsyncStorage fast-path flag: set once the v2 slots own the identity.
164
149
  * Re-derivable (its loss just re-runs the cheap slot check), so it lives in
@@ -176,29 +161,23 @@ const SLOTS_MIGRATED_FLAG_KEY = 'oxy_identity_slots_migrated_v2';
176
161
  */
177
162
  const ROTATION_PENDING_KEY = 'oxy_identity_rotation_pending_v1';
178
163
  const V2_SLOT_LAYOUT = {
179
- primaryService: V2_PRIMARY_KEYCHAIN_SERVICE,
180
- primaryPrivateKeyName: V2_STORAGE_KEYS.PRIVATE_KEY,
181
- primaryPublicKeyName: V2_STORAGE_KEYS.PUBLIC_KEY,
182
- backupService: V2_BACKUP_KEYCHAIN_SERVICE,
183
- backupPrivateKeyName: V2_STORAGE_KEYS.BACKUP_PRIVATE_KEY,
184
- backupPublicKeyName: V2_STORAGE_KEYS.BACKUP_PUBLIC_KEY,
185
- backupTimestampName: V2_STORAGE_KEYS.BACKUP_TIMESTAMP,
164
+ primaryService: keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE,
165
+ primaryPrivateKeyName: keychainItems_1.V2_STORAGE_KEYS.PRIVATE_KEY,
166
+ primaryPublicKeyName: keychainItems_1.V2_STORAGE_KEYS.PUBLIC_KEY,
167
+ backupService: keychainItems_1.V2_BACKUP_KEYCHAIN_SERVICE,
168
+ backupPrivateKeyName: keychainItems_1.V2_STORAGE_KEYS.BACKUP_PRIVATE_KEY,
169
+ backupPublicKeyName: keychainItems_1.V2_STORAGE_KEYS.BACKUP_PUBLIC_KEY,
170
+ backupTimestampName: keychainItems_1.V2_STORAGE_KEYS.BACKUP_TIMESTAMP,
186
171
  };
187
172
  const LEGACY_SLOT_LAYOUT = {
188
173
  primaryService: undefined,
189
- primaryPrivateKeyName: STORAGE_KEYS.PRIVATE_KEY,
190
- primaryPublicKeyName: STORAGE_KEYS.PUBLIC_KEY,
174
+ primaryPrivateKeyName: keychainItems_1.STORAGE_KEYS.PRIVATE_KEY,
175
+ primaryPublicKeyName: keychainItems_1.STORAGE_KEYS.PUBLIC_KEY,
191
176
  backupService: undefined,
192
- backupPrivateKeyName: STORAGE_KEYS.BACKUP_PRIVATE_KEY,
193
- backupPublicKeyName: STORAGE_KEYS.BACKUP_PUBLIC_KEY,
194
- backupTimestampName: STORAGE_KEYS.BACKUP_TIMESTAMP,
177
+ backupPrivateKeyName: keychainItems_1.STORAGE_KEYS.BACKUP_PRIVATE_KEY,
178
+ backupPublicKeyName: keychainItems_1.STORAGE_KEYS.BACKUP_PUBLIC_KEY,
179
+ backupTimestampName: keychainItems_1.STORAGE_KEYS.BACKUP_TIMESTAMP,
195
180
  };
196
- /**
197
- * iOS Keychain Access Group for sharing identities across Oxy apps
198
- * All Oxy apps must have this access group enabled in their entitlements
199
- * Format: [Team ID].so.oxy.shared or group.so.oxy.shared
200
- */
201
- const IOS_KEYCHAIN_GROUP = 'group.so.oxy.shared';
202
181
  /**
203
182
  * Initialize React Native specific modules
204
183
  *
@@ -208,7 +187,7 @@ const IOS_KEYCHAIN_GROUP = 'group.so.oxy.shared';
208
187
  * `expo-secure-store`; off RN it throws (and is never called because every
209
188
  * caller is gated by `isWebPlatform()` / native-only paths).
210
189
  */
211
- async function initSecureStore() {
190
+ async function loadRawSecureStore() {
212
191
  try {
213
192
  return await (0, protocol_1.loadSecureStore)();
214
193
  }
@@ -217,6 +196,29 @@ async function initSecureStore() {
217
196
  throw new Error(`Failed to load expo-secure-store: ${errorMessage}. Make sure expo-secure-store is installed and properly configured.`);
218
197
  }
219
198
  }
199
+ /**
200
+ * The secure store routed to the keychain group of `item`
201
+ * (`../utils/keychainAccessGroups`): on iOS reads walk the item's group then
202
+ * the pre-split shared group, writes carry the item's group and THROW in an app
203
+ * that may not hold it, and in Commons every access first awaits the
204
+ * access-group migration. Android and web are unchanged (`ungrouped`).
205
+ *
206
+ * Throws when the store cannot load or the keychain cannot say which groups
207
+ * this app holds; every caller already treats a throw here as "storage
208
+ * unavailable".
209
+ */
210
+ async function keychainItemStore(item) {
211
+ const raw = await loadRawSecureStore();
212
+ const access = await (0, keychainAccessGroups_1.resolveKeychainAccess)();
213
+ if (access.mode === 'grouped' && access.role === 'holder') {
214
+ await (0, keychainGroupMigration_1.ensureKeychainGroupsMigrated)();
215
+ }
216
+ return (0, keychainAccessGroups_1.groupedKeychainStore)(raw, access, item);
217
+ }
218
+ /** The store for the identity slots (primary, backup, legacy, phrase): Commons only on iOS. */
219
+ function initSecureStore() {
220
+ return keychainItemStore('identity-secret');
221
+ }
220
222
  /**
221
223
  * Check if we're on web platform
222
224
  * Identity storage is only available on native platforms (iOS/Android)
@@ -466,8 +468,8 @@ class KeyManager {
466
468
  let v2Private;
467
469
  let v2Public;
468
470
  try {
469
- v2Private = await store.getItemAsync(V2_STORAGE_KEYS.PRIVATE_KEY, KeyManager._slotOpts(V2_PRIMARY_KEYCHAIN_SERVICE));
470
- v2Public = await store.getItemAsync(V2_STORAGE_KEYS.PUBLIC_KEY, KeyManager._slotOpts(V2_PRIMARY_KEYCHAIN_SERVICE));
471
+ v2Private = await store.getItemAsync(keychainItems_1.V2_STORAGE_KEYS.PRIVATE_KEY, KeyManager._slotOpts(keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE));
472
+ v2Public = await store.getItemAsync(keychainItems_1.V2_STORAGE_KEYS.PUBLIC_KEY, KeyManager._slotOpts(keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE));
471
473
  }
472
474
  catch (error) {
473
475
  return { mode: 'deferred', cause: error };
@@ -490,8 +492,8 @@ class KeyManager {
490
492
  let legacyPrivate;
491
493
  let legacyPublic;
492
494
  try {
493
- legacyPrivate = await store.getItemAsync(STORAGE_KEYS.PRIVATE_KEY);
494
- legacyPublic = await store.getItemAsync(STORAGE_KEYS.PUBLIC_KEY);
495
+ legacyPrivate = await store.getItemAsync(keychainItems_1.STORAGE_KEYS.PRIVATE_KEY);
496
+ legacyPublic = await store.getItemAsync(keychainItems_1.STORAGE_KEYS.PUBLIC_KEY);
495
497
  }
496
498
  catch (error) {
497
499
  return { mode: 'deferred', cause: error };
@@ -505,10 +507,10 @@ class KeyManager {
505
507
  const canonicalPrivate = KeyManager.canonicalPrivateKey(legacyPrivate);
506
508
  const canonicalPublic = legacyPublic.toLowerCase();
507
509
  try {
508
- await store.setItemAsync(V2_STORAGE_KEYS.PUBLIC_KEY, canonicalPublic, KeyManager._slotOpts(V2_PRIMARY_KEYCHAIN_SERVICE));
509
- await store.setItemAsync(V2_STORAGE_KEYS.PRIVATE_KEY, canonicalPrivate, KeyManager._privateWriteOpts(store, V2_PRIMARY_KEYCHAIN_SERVICE));
510
- const readBackPrivate = await store.getItemAsync(V2_STORAGE_KEYS.PRIVATE_KEY, KeyManager._slotOpts(V2_PRIMARY_KEYCHAIN_SERVICE));
511
- const readBackPublic = await store.getItemAsync(V2_STORAGE_KEYS.PUBLIC_KEY, KeyManager._slotOpts(V2_PRIMARY_KEYCHAIN_SERVICE));
510
+ await store.setItemAsync(keychainItems_1.V2_STORAGE_KEYS.PUBLIC_KEY, canonicalPublic, KeyManager._slotOpts(keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE));
511
+ await store.setItemAsync(keychainItems_1.V2_STORAGE_KEYS.PRIVATE_KEY, canonicalPrivate, KeyManager._privateWriteOpts(store, keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE));
512
+ const readBackPrivate = await store.getItemAsync(keychainItems_1.V2_STORAGE_KEYS.PRIVATE_KEY, KeyManager._slotOpts(keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE));
513
+ const readBackPublic = await store.getItemAsync(keychainItems_1.V2_STORAGE_KEYS.PUBLIC_KEY, KeyManager._slotOpts(keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE));
512
514
  const verified = readBackPrivate?.toLowerCase() === canonicalPrivate &&
513
515
  readBackPublic?.toLowerCase() === canonicalPublic &&
514
516
  KeyManager._isHealthyPair(readBackPrivate, readBackPublic);
@@ -543,8 +545,8 @@ class KeyManager {
543
545
  let backupPrivate = null;
544
546
  let backupPublic = null;
545
547
  try {
546
- backupPrivate = await store.getItemAsync(STORAGE_KEYS.BACKUP_PRIVATE_KEY);
547
- backupPublic = await store.getItemAsync(STORAGE_KEYS.BACKUP_PUBLIC_KEY);
548
+ backupPrivate = await store.getItemAsync(keychainItems_1.STORAGE_KEYS.BACKUP_PRIVATE_KEY);
549
+ backupPublic = await store.getItemAsync(keychainItems_1.STORAGE_KEYS.BACKUP_PUBLIC_KEY);
548
550
  }
549
551
  catch (error) {
550
552
  if ((0, logger_1.isDev)()) {
@@ -563,9 +565,9 @@ class KeyManager {
563
565
  seedPrivate = primaryPrivate;
564
566
  seedPublic = primaryPublic;
565
567
  }
566
- await store.setItemAsync(V2_STORAGE_KEYS.BACKUP_PUBLIC_KEY, seedPublic, KeyManager._slotOpts(V2_BACKUP_KEYCHAIN_SERVICE));
567
- await store.setItemAsync(V2_STORAGE_KEYS.BACKUP_PRIVATE_KEY, seedPrivate, KeyManager._privateWriteOpts(store, V2_BACKUP_KEYCHAIN_SERVICE));
568
- await store.setItemAsync(V2_STORAGE_KEYS.BACKUP_TIMESTAMP, Date.now().toString(), KeyManager._slotOpts(V2_BACKUP_KEYCHAIN_SERVICE));
568
+ await store.setItemAsync(keychainItems_1.V2_STORAGE_KEYS.BACKUP_PUBLIC_KEY, seedPublic, KeyManager._slotOpts(keychainItems_1.V2_BACKUP_KEYCHAIN_SERVICE));
569
+ await store.setItemAsync(keychainItems_1.V2_STORAGE_KEYS.BACKUP_PRIVATE_KEY, seedPrivate, KeyManager._privateWriteOpts(store, keychainItems_1.V2_BACKUP_KEYCHAIN_SERVICE));
570
+ await store.setItemAsync(keychainItems_1.V2_STORAGE_KEYS.BACKUP_TIMESTAMP, Date.now().toString(), KeyManager._slotOpts(keychainItems_1.V2_BACKUP_KEYCHAIN_SERVICE));
569
571
  }
570
572
  catch (error) {
571
573
  logger_1.logger.warn('Failed to migrate identity backup slot to v2 (non-fatal)', { component: 'KeyManager' }, error);
@@ -583,23 +585,23 @@ class KeyManager {
583
585
  }
584
586
  }
585
587
  static async _bestEffortDeleteV2Primary(store) {
586
- await KeyManager._bestEffortDelete(store, V2_STORAGE_KEYS.PRIVATE_KEY, V2_PRIMARY_KEYCHAIN_SERVICE);
587
- await KeyManager._bestEffortDelete(store, V2_STORAGE_KEYS.PUBLIC_KEY, V2_PRIMARY_KEYCHAIN_SERVICE);
588
+ await KeyManager._bestEffortDelete(store, keychainItems_1.V2_STORAGE_KEYS.PRIVATE_KEY, keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE);
589
+ await KeyManager._bestEffortDelete(store, keychainItems_1.V2_STORAGE_KEYS.PUBLIC_KEY, keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE);
588
590
  }
589
591
  static async _bestEffortDeleteLegacyPrimaryAndBackup(store) {
590
- await KeyManager._bestEffortDelete(store, STORAGE_KEYS.PRIVATE_KEY);
591
- await KeyManager._bestEffortDelete(store, STORAGE_KEYS.PUBLIC_KEY);
592
- await KeyManager._bestEffortDelete(store, STORAGE_KEYS.BACKUP_PRIVATE_KEY);
593
- await KeyManager._bestEffortDelete(store, STORAGE_KEYS.BACKUP_PUBLIC_KEY);
594
- await KeyManager._bestEffortDelete(store, STORAGE_KEYS.BACKUP_TIMESTAMP);
592
+ await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.PRIVATE_KEY);
593
+ await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.PUBLIC_KEY);
594
+ await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.BACKUP_PRIVATE_KEY);
595
+ await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.BACKUP_PUBLIC_KEY);
596
+ await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.BACKUP_TIMESTAMP);
595
597
  }
596
598
  static async _bestEffortDeleteBackupsAllGenerations(store) {
597
- await KeyManager._bestEffortDelete(store, V2_STORAGE_KEYS.BACKUP_PRIVATE_KEY, V2_BACKUP_KEYCHAIN_SERVICE);
598
- await KeyManager._bestEffortDelete(store, V2_STORAGE_KEYS.BACKUP_PUBLIC_KEY, V2_BACKUP_KEYCHAIN_SERVICE);
599
- await KeyManager._bestEffortDelete(store, V2_STORAGE_KEYS.BACKUP_TIMESTAMP, V2_BACKUP_KEYCHAIN_SERVICE);
600
- await KeyManager._bestEffortDelete(store, STORAGE_KEYS.BACKUP_PRIVATE_KEY);
601
- await KeyManager._bestEffortDelete(store, STORAGE_KEYS.BACKUP_PUBLIC_KEY);
602
- await KeyManager._bestEffortDelete(store, STORAGE_KEYS.BACKUP_TIMESTAMP);
599
+ await KeyManager._bestEffortDelete(store, keychainItems_1.V2_STORAGE_KEYS.BACKUP_PRIVATE_KEY, keychainItems_1.V2_BACKUP_KEYCHAIN_SERVICE);
600
+ await KeyManager._bestEffortDelete(store, keychainItems_1.V2_STORAGE_KEYS.BACKUP_PUBLIC_KEY, keychainItems_1.V2_BACKUP_KEYCHAIN_SERVICE);
601
+ await KeyManager._bestEffortDelete(store, keychainItems_1.V2_STORAGE_KEYS.BACKUP_TIMESTAMP, keychainItems_1.V2_BACKUP_KEYCHAIN_SERVICE);
602
+ await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.BACKUP_PRIVATE_KEY);
603
+ await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.BACKUP_PUBLIC_KEY);
604
+ await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.BACKUP_TIMESTAMP);
603
605
  }
604
606
  /**
605
607
  * Clear the iOS keychain-group copy of the identity (force-delete only) so a
@@ -607,12 +609,12 @@ class KeyManager {
607
609
  * rung. Best-effort — the shared slot is a redundant convenience copy. The
608
610
  * Android signer store is cleared on every delete, in {@link deleteIdentity}.
609
611
  */
610
- static async _clearSharedSlot(store) {
612
+ static async _clearSharedSlot(_identityStore) {
611
613
  try {
612
614
  if ((0, platform_1.isIOS)()) {
613
- const opts = { keychainAccessGroup: IOS_KEYCHAIN_GROUP };
614
- await store.deleteItemAsync(STORAGE_KEYS.SHARED_PRIVATE_KEY, opts);
615
- await store.deleteItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, opts);
615
+ // The shared slot lives in two groups of its own, not the identity slots'.
616
+ await (await keychainItemStore('spending-key')).deleteItemAsync(keychainItems_1.STORAGE_KEYS.SHARED_PRIVATE_KEY);
617
+ await (await keychainItemStore('shared')).deleteItemAsync(keychainItems_1.STORAGE_KEYS.SHARED_PUBLIC_KEY);
616
618
  }
617
619
  KeyManager.invalidateSharedCache();
618
620
  }
@@ -679,6 +681,20 @@ class KeyManager {
679
681
  KeyManager.cachedSharedPublicKey = null;
680
682
  KeyManager.cachedHasSharedIdentity = null;
681
683
  }
684
+ /**
685
+ * iOS: write the personal key's cross-app slot — the private key to the
686
+ * wallet group (Commons and the Peable wallet), the public key to the shared
687
+ * group (every Oxy app). Throws in any app that may not hold the private key.
688
+ * @internal
689
+ */
690
+ static async _writeIosSharedSlot(privateKey, publicKey) {
691
+ const privateStore = await keychainItemStore('spending-key');
692
+ const privateOpts = {
693
+ keychainAccessible: privateStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
694
+ };
695
+ await privateStore.setItemAsync(keychainItems_1.STORAGE_KEYS.SHARED_PRIVATE_KEY, privateKey, privateOpts);
696
+ await (await keychainItemStore('shared')).setItemAsync(keychainItems_1.STORAGE_KEYS.SHARED_PUBLIC_KEY, publicKey, {});
697
+ }
682
698
  /**
683
699
  * Lowercase and pad to canonical 64-hex-char form.
684
700
  *
@@ -714,8 +730,10 @@ class KeyManager {
714
730
  // ==================== SHARED IDENTITY METHODS ====================
715
731
  // One Oxy identity across every Oxy app on a device.
716
732
  //
717
- // iOS: the keychain access group `group.so.oxy.shared`. Every Oxy app reads
718
- // the same keychain item, and no app can wipe another's keychain.
733
+ // iOS: keychain access groups (docs/identity/ios-keychain-access-groups.md).
734
+ // The private key is in `<TEAM>.so.oxy.wallet` (Commons and the Peable
735
+ // wallet only), the public key in `<TEAM>.group.so.oxy.shared`, which every
736
+ // Oxy app reads. Every other app signs in through Commons (ADR 0029).
719
737
  // Android: every app has its own UID, so nothing is shared through storage.
720
738
  // Commons is the ONLY holder of the private key. It keeps the identity
721
739
  // signer store (`setIdentitySignerStore`) for its identity host, and every
@@ -740,21 +758,11 @@ class KeyManager {
740
758
  }
741
759
  const { privateKey, publicKey } = await KeyManager.generateKeyPair();
742
760
  if ((0, platform_1.isIOS)()) {
743
- const store = await initSecureStore();
744
- // Note: keychainAccessGroup requires Keychain Sharing capability in Xcode
745
761
  try {
746
- const privateOpts = {
747
- keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
748
- keychainAccessGroup: IOS_KEYCHAIN_GROUP, // Enables sharing across apps
749
- };
750
- await store.setItemAsync(STORAGE_KEYS.SHARED_PRIVATE_KEY, privateKey, privateOpts);
751
- const publicOpts = {
752
- keychainAccessGroup: IOS_KEYCHAIN_GROUP,
753
- };
754
- await store.setItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, publicKey, publicOpts);
762
+ await KeyManager._writeIosSharedSlot(privateKey, publicKey);
755
763
  }
756
764
  catch (error) {
757
- throw new Error(`Failed to create shared identity on iOS. Ensure your app has the Keychain Sharing capability enabled with access group "${IOS_KEYCHAIN_GROUP}". Error: ${error}`);
765
+ throw new Error(`Failed to create shared identity on iOS. Only Commons may write it, with the keychain access groups of docs/identity/ios-keychain-access-groups.md. Error: ${error}`);
758
766
  }
759
767
  }
760
768
  else if ((0, platform_1.isAndroid)()) {
@@ -794,9 +802,8 @@ class KeyManager {
794
802
  try {
795
803
  let publicKey = null;
796
804
  if ((0, platform_1.isIOS)()) {
797
- const store = await initSecureStore();
798
- const opts = { keychainAccessGroup: IOS_KEYCHAIN_GROUP };
799
- publicKey = await store.getItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, opts);
805
+ const store = await keychainItemStore('shared');
806
+ publicKey = await store.getItemAsync(keychainItems_1.STORAGE_KEYS.SHARED_PUBLIC_KEY, {});
800
807
  }
801
808
  else if ((0, platform_1.isAndroid)()) {
802
809
  const signer = KeyManager.identitySignerStore;
@@ -836,9 +843,10 @@ class KeyManager {
836
843
  return null;
837
844
  }
838
845
  try {
839
- const store = await initSecureStore();
840
- const opts = { keychainAccessGroup: IOS_KEYCHAIN_GROUP };
841
- return await store.getItemAsync(STORAGE_KEYS.SHARED_PRIVATE_KEY, opts);
846
+ // The wallet group: Commons and the Peable wallet. Every other app's plan
847
+ // is empty, so this is `null` there without touching the keychain.
848
+ const store = await keychainItemStore('spending-key');
849
+ return await store.getItemAsync(keychainItems_1.STORAGE_KEYS.SHARED_PRIVATE_KEY, {});
842
850
  }
843
851
  catch (error) {
844
852
  if ((0, logger_1.isDev)()) {
@@ -932,16 +940,7 @@ class KeyManager {
932
940
  const canonicalPrivate = KeyManager.canonicalPrivateKey(privateKey);
933
941
  const publicKey = (0, secp256k1_1.deriveSecp256k1PublicKey)(canonicalPrivate);
934
942
  if ((0, platform_1.isIOS)()) {
935
- const store = await initSecureStore();
936
- const privateOpts = {
937
- keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
938
- keychainAccessGroup: IOS_KEYCHAIN_GROUP,
939
- };
940
- await store.setItemAsync(STORAGE_KEYS.SHARED_PRIVATE_KEY, canonicalPrivate, privateOpts);
941
- const publicOpts = {
942
- keychainAccessGroup: IOS_KEYCHAIN_GROUP,
943
- };
944
- await store.setItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, publicKey, publicOpts);
943
+ await KeyManager._writeIosSharedSlot(canonicalPrivate, publicKey);
945
944
  }
946
945
  else if ((0, platform_1.isAndroid)()) {
947
946
  await KeyManager._writeIdentitySigner(canonicalPrivate, publicKey);
@@ -1778,7 +1777,7 @@ class KeyManager {
1778
1777
  }
1779
1778
  try {
1780
1779
  const store = await initSecureStore();
1781
- await store.setItemAsync(RECOVERY_MNEMONIC_STORAGE_KEY, mnemonic, KeyManager._privateWriteOpts(store, RECOVERY_MNEMONIC_KEYCHAIN_SERVICE));
1780
+ await store.setItemAsync(keychainItems_1.RECOVERY_MNEMONIC_STORAGE_KEY, mnemonic, KeyManager._privateWriteOpts(store, keychainItems_1.RECOVERY_MNEMONIC_KEYCHAIN_SERVICE));
1782
1781
  }
1783
1782
  catch (error) {
1784
1783
  if ((0, logger_1.isDev)()) {
@@ -1813,7 +1812,7 @@ class KeyManager {
1813
1812
  }
1814
1813
  try {
1815
1814
  const store = await initSecureStore();
1816
- return await store.getItemAsync(RECOVERY_MNEMONIC_STORAGE_KEY, KeyManager._slotOpts(RECOVERY_MNEMONIC_KEYCHAIN_SERVICE));
1815
+ return await store.getItemAsync(keychainItems_1.RECOVERY_MNEMONIC_STORAGE_KEY, KeyManager._slotOpts(keychainItems_1.RECOVERY_MNEMONIC_KEYCHAIN_SERVICE));
1817
1816
  }
1818
1817
  catch (error) {
1819
1818
  if ((0, logger_1.isDev)()) {
@@ -1859,7 +1858,7 @@ class KeyManager {
1859
1858
  return; // Identity storage is only available on native platforms
1860
1859
  }
1861
1860
  const store = await initSecureStore();
1862
- await KeyManager._bestEffortDelete(store, RECOVERY_MNEMONIC_STORAGE_KEY, RECOVERY_MNEMONIC_KEYCHAIN_SERVICE);
1861
+ await KeyManager._bestEffortDelete(store, keychainItems_1.RECOVERY_MNEMONIC_STORAGE_KEY, keychainItems_1.RECOVERY_MNEMONIC_KEYCHAIN_SERVICE);
1863
1862
  }
1864
1863
  /**
1865
1864
  * Check if a complete, parseable identity exists on this device.
@@ -2078,8 +2077,8 @@ class KeyManager {
2078
2077
  await store.deleteItemAsync(layout.primaryPublicKeyName, readOpts);
2079
2078
  }
2080
2079
  await KeyManager._bestEffortDeleteV2Primary(store);
2081
- await KeyManager._bestEffortDelete(store, STORAGE_KEYS.PRIVATE_KEY);
2082
- await KeyManager._bestEffortDelete(store, STORAGE_KEYS.PUBLIC_KEY);
2080
+ await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.PRIVATE_KEY);
2081
+ await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.PUBLIC_KEY);
2083
2082
  // Always drop the stored recovery mnemonic — it is scoped to the identity
2084
2083
  // being deleted, so a leftover would let Settings reveal a stale phrase for
2085
2084
  // an identity that no longer exists (or a DIFFERENT one after re-onboarding).
@@ -2606,7 +2605,8 @@ class KeyManager {
2606
2605
  * `HDKey.fromMasterSeed` and never touches the identity key.
2607
2606
  *
2608
2607
  * Key source (native only):
2609
- * - iOS: the shared identity in `group.so.oxy.shared`, then this device's
2608
+ * - iOS: the shared slot's private key in the wallet keychain group (Commons
2609
+ * and the Peable wallet only; `null` in every other app), then this device's
2610
2610
  * primary identity.
2611
2611
  * - Android: Commons (the one app with a signer store) uses its own primary
2612
2612
  * identity; every other app asks Commons over IPC and never reads a local