@oxy.so/core 7.32.0 → 7.33.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.
@@ -237,7 +237,7 @@ class IdentityApi {
237
237
  try {
238
238
  if (!oldPrivateKeyForRing)
239
239
  throw new Error('no readable private key');
240
- await retireIdentityKey({ privateKey: oldPrivateKeyForRing, publicKey: oldPublicKey });
240
+ await retireIdentityKey({ privateKey: oldPrivateKeyForRing, publicKey: oldPublicKey }, { ownerAccountId: userId });
241
241
  }
242
242
  catch (retainError) {
243
243
  logger_1.logger.warn('Could not keep the current key as a retired key; not rotating.', { component: 'oxy.identity', method: 'rotateKey' }, retainError);
@@ -670,9 +670,12 @@ class IdentityBackupApi {
670
670
  if ((0, platform_1.isWeb)())
671
671
  return [];
672
672
  const { exportRetiredIdentityKeys } = await Promise.resolve().then(() => __importStar(require('../crypto/index')));
673
+ const userId = this.ctx.oxy.session.userId;
674
+ if (!userId)
675
+ throw new Error('No authenticated user — sign in before backing up.');
673
676
  let held;
674
677
  try {
675
- held = await exportRetiredIdentityKeys();
678
+ held = await exportRetiredIdentityKeys({ ownerAccountId: userId });
676
679
  }
677
680
  catch {
678
681
  return []; // Not Commons: this app holds no ring to carry.
@@ -731,6 +734,11 @@ class IdentityBackupApi {
731
734
  */
732
735
  async keepCarriedRetiredKeys(payload) {
733
736
  const { KeyManager, retireIdentityKey } = await Promise.resolve().then(() => __importStar(require('../crypto/index')));
737
+ // The backup is the signed-in account's (its phrase opened it): its keys
738
+ // are kept for that account only (R5-2).
739
+ const ownerAccountId = this.ctx.oxy.session.userId;
740
+ if (!ownerAccountId)
741
+ throw new Error('No authenticated user — sign in before restoring.');
734
742
  let kept = 0;
735
743
  for (const key of Array.isArray(payload.retiredKeys) ? payload.retiredKeys : []) {
736
744
  try {
@@ -741,7 +749,7 @@ class IdentityBackupApi {
741
749
  if (KeyManager.derivePublicKey(key.privateKey).toLowerCase() !== key.publicKey.toLowerCase()) {
742
750
  continue;
743
751
  }
744
- await retireIdentityKey({ privateKey: key.privateKey, publicKey: key.publicKey });
752
+ await retireIdentityKey({ privateKey: key.privateKey, publicKey: key.publicKey }, { ownerAccountId });
745
753
  kept += 1;
746
754
  }
747
755
  catch (error) {
@@ -12,8 +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.AEAD_NONCE_LENGTH = exports.AEAD_KEY_LENGTH = exports.decryptAead = exports.encryptAead = exports.hkdfSha256 = exports.BACKUP_KDF_ENCRYPTION_INFO = exports.RecoveryPhraseService = exports.SignatureService = exports.exportRetiredIdentityKeys = exports.retiredKeyFromPrivateKey = exports.retiredKeyFromPhrase = exports.syncRetiredKeys = exports.forgetRetiredIdentityKeys = exports.forgetRetiredIdentityKey = exports.listRetiredIdentityKeys = exports.retireIdentityKey = exports.getPreSplitExposure = exports.ensureKeychainGroupsMigrated = exports.groupedKeychainStore = exports.deleteThroughPlan = exports.readThroughPlan = exports.ownKeychainItemPlan = exports.keychainWriteOption = exports.keychainDeletePlan = exports.keychainReadPlan = exports.keychainAccessGroupValue = exports.resolveKeychainAccess = exports.DEV_KEYCHAIN_GROUP_SUFFIX = exports.mayWriteKeychainItem = 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 = 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 = void 0;
15
+ exports.hkdfSha256 = exports.BACKUP_KDF_ENCRYPTION_INFO = exports.RecoveryPhraseService = exports.SignatureService = exports.RetiredKeyRingUnreadableError = exports.unmirrorPersonalRetiredKeys = exports.countQuarantinedRetiredKeys = exports.claimRetiredIdentityKeys = exports.exportRetiredIdentityKeys = exports.retiredKeyFromPrivateKey = exports.retiredKeyFromPhrase = exports.syncRetiredKeys = exports.forgetRetiredIdentityKeys = exports.forgetRetiredIdentityKey = exports.listRetiredIdentityKeys = exports.retireIdentityKey = exports.getPreSplitExposure = exports.ensureKeychainGroupsMigrated = exports.groupedKeychainStore = exports.deleteThroughPlan = exports.readThroughPlan = exports.ownKeychainItemPlan = exports.keychainWriteOption = exports.keychainDeletePlan = exports.keychainReadPlan = exports.keychainAccessGroupValue = exports.resolveKeychainAccess = exports.DEV_KEYCHAIN_GROUP_SUFFIX = exports.mayWriteKeychainItem = 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 = 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 = void 0;
17
17
  // MUST stay the first import: the shim has to exist before any module below
18
18
  // evaluates or draws randomness (see `./polyfill`).
19
19
  require("./polyfill");
@@ -69,6 +69,10 @@ Object.defineProperty(exports, "syncRetiredKeys", { enumerable: true, get: funct
69
69
  Object.defineProperty(exports, "retiredKeyFromPhrase", { enumerable: true, get: function () { return retiredKeys_1.retiredKeyFromPhrase; } });
70
70
  Object.defineProperty(exports, "retiredKeyFromPrivateKey", { enumerable: true, get: function () { return retiredKeys_1.retiredKeyFromPrivateKey; } });
71
71
  Object.defineProperty(exports, "exportRetiredIdentityKeys", { enumerable: true, get: function () { return retiredKeys_1.exportRetiredIdentityKeys; } });
72
+ Object.defineProperty(exports, "claimRetiredIdentityKeys", { enumerable: true, get: function () { return retiredKeys_1.claimRetiredIdentityKeys; } });
73
+ Object.defineProperty(exports, "countQuarantinedRetiredKeys", { enumerable: true, get: function () { return retiredKeys_1.countQuarantinedRetiredKeys; } });
74
+ Object.defineProperty(exports, "unmirrorPersonalRetiredKeys", { enumerable: true, get: function () { return retiredKeys_1.unmirrorPersonalRetiredKeys; } });
75
+ Object.defineProperty(exports, "RetiredKeyRingUnreadableError", { enumerable: true, get: function () { return retiredKeys_1.RetiredKeyRingUnreadableError; } });
72
76
  var signatureService_1 = require("./signatureService");
73
77
  Object.defineProperty(exports, "SignatureService", { enumerable: true, get: function () { return signatureService_1.SignatureService; } });
74
78
  var recoveryPhrase_1 = require("./recoveryPhrase");
@@ -788,12 +788,30 @@ class KeyManager {
788
788
  * `signSocialReceive`) is fresh, so a cached key here would let a caller
789
789
  * compare a new seed against an old key.
790
790
  *
791
+ * iOS, in an app that holds the private key (Commons, the Peable wallet:
792
+ * keychain roles `holder` and `wallet`): DERIVED from the private key in the
793
+ * wallet group, never read from the public-key item, which sits in the shared
794
+ * group every Oxy app can write (R5-1); `null` when that app holds no private
795
+ * key. Every other iOS app reads the public-key item: it holds nothing it
796
+ * could derive from, and it never spends. Money code uses
797
+ * {@link getIdentityPublicKeyForMoney}.
798
+ *
791
799
  * @returns Shared public key or null if no shared identity exists
792
800
  */
793
801
  static async getSharedPublicKey() {
794
802
  if (isWebPlatform()) {
795
803
  return null;
796
804
  }
805
+ if ((0, platform_1.isIOS)() && (await KeyManager._holdsSpendingKeys())) {
806
+ // Not cached: a rotation in Commons changes it under the wallet.
807
+ try {
808
+ const privateKey = await KeyManager.getSharedPrivateKey();
809
+ return privateKey ? KeyManager.derivePublicKey(privateKey) : null;
810
+ }
811
+ catch {
812
+ return null;
813
+ }
814
+ }
797
815
  // Return cached value if available (iOS only; see above).
798
816
  if (!(0, platform_1.isAndroid)() && KeyManager.cachedSharedPublicKey !== null) {
799
817
  return KeyManager.cachedSharedPublicKey;
@@ -826,6 +844,68 @@ class KeyManager {
826
844
  return null;
827
845
  }
828
846
  }
847
+ /**
848
+ * Whether this iOS app holds spending keys (keychain role `holder` or
849
+ * `wallet`), so a public key it uses for money must be derived from them.
850
+ * Throws while the keychain cannot say. @internal
851
+ */
852
+ static async _holdsSpendingKeys() {
853
+ const access = await (0, keychainAccessGroups_1.resolveKeychainAccess)();
854
+ return access.mode === 'grouped' && access.role !== 'member';
855
+ }
856
+ /**
857
+ * THE public key of the identity a money holder (the Peable wallet, Commons)
858
+ * receives and spends with: the personal key, or with `opts.accountId` that
859
+ * organization's (ADR 0061 D6). It is the public half of exactly the private
860
+ * key {@link deriveScopedSeed} and {@link signSocialReceive} use, so an
861
+ * address shown from it is one this app can spend (R5-1).
862
+ *
863
+ * - iOS (Commons, the Peable wallet): DERIVED from the private key in the
864
+ * restricted keychain groups (personal: the wallet group's slot, then
865
+ * Commons' primary; organization: its pair). Never the shared group's
866
+ * public-key item, which every Oxy app can write. `null` when no private
867
+ * key is held, and in every other iOS app.
868
+ * - Android: inside Commons derived from its own key; elsewhere Commons'
869
+ * identity host (`describe`), which answers from Commons' own signer
870
+ * store over signature-protected IPC.
871
+ * - Web: `null`.
872
+ *
873
+ * Compare the answer with the account's published `#key-1` before showing a
874
+ * receive address (Peable ADR 0002). Lowercase uncompressed (`04…`).
875
+ */
876
+ static async getIdentityPublicKeyForMoney(opts) {
877
+ if (isWebPlatform()) {
878
+ return null;
879
+ }
880
+ const accountId = opts?.accountId;
881
+ if (accountId !== undefined) {
882
+ if (!(0, organizationKeys_1.isOrganizationAccountId)(accountId))
883
+ return null;
884
+ if ((0, platform_1.isAndroid)() && !KeyManager.identitySignerStore) {
885
+ const bridge = await (0, protocol_1.loadCommonsIdentityBridge)();
886
+ const description = (await bridge?.describe(accountId)) ?? null;
887
+ return description?.accountId === accountId && description.publicKey
888
+ ? KeyManager.canonicalPublicKey(description.publicKey)
889
+ : null;
890
+ }
891
+ const organizationKey = await (0, organizationKeys_1.getOrganizationPrivateKey)(accountId);
892
+ return organizationKey ? KeyManager.derivePublicKey(organizationKey) : null;
893
+ }
894
+ if ((0, platform_1.isAndroid)()) {
895
+ if (KeyManager.identitySignerStore) {
896
+ const local = await KeyManager.getPrivateKey();
897
+ return local ? KeyManager.derivePublicKey(local) : null;
898
+ }
899
+ const bridge = await (0, protocol_1.loadCommonsIdentityBridge)();
900
+ const publicKey = (await bridge?.describe())?.publicKey ?? null;
901
+ return publicKey ? KeyManager.canonicalPublicKey(publicKey) : null;
902
+ }
903
+ if (!(0, platform_1.isIOS)()) {
904
+ return null;
905
+ }
906
+ const privateKey = (await KeyManager.getSharedPrivateKey()) ?? (await KeyManager.getPrivateKey());
907
+ return privateKey ? KeyManager.derivePublicKey(privateKey) : null;
908
+ }
829
909
  /**
830
910
  * The shared private key, where this app may hold it: iOS only.
831
911
  *
@@ -2089,18 +2169,26 @@ class KeyManager {
2089
2169
  await KeyManager._clearSharedSlot(store);
2090
2170
  // The retired ring is NOT cleared (R4-4): it is money under the person's
2091
2171
  // previous keys, and "Start over" or a forced delete must not cost it.
2092
- // The same person restoring their identity finds it again; a different
2093
- // identity on this phone cannot use it (each entry is checked against
2094
- // that account's key history before use). Only deleting the Oxy ACCOUNT
2095
- // clears it, explicitly (`forgetRetiredIdentityKeys`, Commons' flow).
2172
+ // Every personal entry is bound to its owner's account id (R5-2), so the
2173
+ // same person signing in again finds it, and a different person on this
2174
+ // phone never lists, derives from or signs with it. Only deleting the Oxy
2175
+ // ACCOUNT clears it, explicitly (`forgetRetiredIdentityKeys`).
2096
2176
  }
2097
2177
  // The device backup goes on EVERY delete, forced or not: it is restored
2098
2178
  // silently, so leaving it would bring a deleted identity back on the next
2099
2179
  // launch. Best-effort, like the other recovery sources.
2100
2180
  await KeyManager._clearDeviceBackup();
2101
2181
  // Same for the Android signer store: Commons' identity host must stop
2102
- // answering for a deleted identity, forced or not.
2182
+ // answering for a deleted identity, forced or not — its personal retired
2183
+ // keys included (R5-2): they stay in Commons' own ring, bound to their
2184
+ // owner, but the host must not hand them to the next person on the phone.
2103
2185
  await KeyManager._clearIdentitySigner();
2186
+ try {
2187
+ await (0, retiredKeys_1.unmirrorPersonalRetiredKeys)();
2188
+ }
2189
+ catch (error) {
2190
+ logger_1.logger.warn('Failed to withdraw the personal retired keys from the identity host', { component: 'KeyManager' }, error);
2191
+ }
2104
2192
  // Clear the marker AFTER key deletion succeeds — a marker must never outlive
2105
2193
  // its identity (a leftover marker would route a truly-absent device to
2106
2194
  // `recovery` instead of `welcome`).
@@ -2629,11 +2717,16 @@ class KeyManager {
2629
2717
  * (personal, or `accountId`'s) whose public key it names: the current key
2630
2718
  * when it matches, else a RETIRED key Commons kept after a rotation or
2631
2719
  * replacement (`./retiredKeys`, ADR 0061 D7 as amended). Spending only: a
2632
- * retired key never signs in. Never falls back to another key.
2720
+ * retired key never signs in. Never falls back to another key. A PERSONAL
2721
+ * retired key is reached only with `opts.ownerAccountId`, the signed-in
2722
+ * person's account id, and only when the ring keeps it for that account
2723
+ * (R5-2); on Android outside Commons the identity host holds only the
2724
+ * personal keys of the person Commons is signed in as.
2633
2725
  *
2634
2726
  * @param info Context/domain-binding label (distinct labels → independent seeds).
2635
2727
  * @param opts.accountId An organization's account id, for its key.
2636
2728
  * @param opts.publicKey Selects one key of that account by its public key.
2729
+ * @param opts.ownerAccountId The person's account id, for a personal retired key.
2637
2730
  * @returns 32 bytes of derived keying material, or `null` on web / when no
2638
2731
  * identity key is available to this app.
2639
2732
  */
@@ -2650,7 +2743,7 @@ class KeyManager {
2650
2743
  const seed = (await bridge?.deriveRetiredScopedSeed?.(info, selected, opts.accountId)) ?? null;
2651
2744
  return seed ? hexToBytes(seed) : null;
2652
2745
  }
2653
- const selected = await KeyManager._selectedPrivateKey(opts.publicKey, opts.accountId);
2746
+ const selected = await KeyManager._selectedPrivateKey({ ...opts, publicKey: opts.publicKey });
2654
2747
  return selected ? (0, identityDerivations_1.deriveScopedSeedFromKey)(selected, info) : null;
2655
2748
  }
2656
2749
  if (opts?.accountId !== undefined) {
@@ -2718,7 +2811,7 @@ class KeyManager {
2718
2811
  return ((await bridge?.signRetiredSocialReceive?.(index, digest, selected, opts.accountId)) ??
2719
2812
  null);
2720
2813
  }
2721
- const selected = await KeyManager._selectedPrivateKey(opts.publicKey, opts.accountId);
2814
+ const selected = await KeyManager._selectedPrivateKey({ ...opts, publicKey: opts.publicKey });
2722
2815
  return selected ? (0, identityDerivations_1.signSocialReceiveDigest)(selected, index, digest) : null;
2723
2816
  }
2724
2817
  const accountId = opts?.accountId;
@@ -2757,7 +2850,8 @@ class KeyManager {
2757
2850
  static _selectorKey(publicKey) {
2758
2851
  return KeyManager.isValidPublicKey(publicKey) ? KeyManager.canonicalPublicKey(publicKey) : null;
2759
2852
  }
2760
- static async _selectedPrivateKey(publicKey, accountId) {
2853
+ static async _selectedPrivateKey(opts) {
2854
+ const { publicKey, accountId, ownerAccountId } = opts;
2761
2855
  if ((0, retiredKeys_1.retiredKeyScope)({ accountId }) === null || !KeyManager.isValidPublicKey(publicKey)) {
2762
2856
  return null;
2763
2857
  }
@@ -2770,7 +2864,11 @@ class KeyManager {
2770
2864
  if (current && KeyManager.canonicalPublicKey(KeyManager.derivePublicKey(current)) === wanted) {
2771
2865
  return current;
2772
2866
  }
2773
- return (0, retiredKeys_1.getRetiredPrivateKey)(wanted, accountId !== undefined ? { accountId } : undefined);
2867
+ return (0, retiredKeys_1.getRetiredPrivateKey)(wanted, accountId !== undefined
2868
+ ? { accountId }
2869
+ : ownerAccountId !== undefined
2870
+ ? { ownerAccountId }
2871
+ : undefined);
2774
2872
  }
2775
2873
  /**
2776
2874
  * Get a shortened version of the public key for display