@oxy.so/core 7.31.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.
- package/dist/cjs/api/identity.js +155 -24
- package/dist/cjs/crypto/identityMarker.js +15 -0
- package/dist/cjs/crypto/index.js +11 -2
- package/dist/cjs/crypto/keyManager.js +111 -16
- package/dist/cjs/crypto/keychainGroupMigration.js +327 -160
- package/dist/cjs/crypto/organizationKeys.js +33 -5
- package/dist/cjs/crypto/retiredKeys.js +315 -64
- package/dist/cjs/utils/keychainAccessGroups.js +111 -25
- package/dist/esm/api/identity.js +155 -24
- package/dist/esm/crypto/identityMarker.js +14 -0
- package/dist/esm/crypto/index.js +3 -3
- package/dist/esm/crypto/keyManager.js +113 -18
- package/dist/esm/crypto/keychainGroupMigration.js +327 -162
- package/dist/esm/crypto/organizationKeys.js +34 -6
- package/dist/esm/crypto/retiredKeys.js +306 -63
- package/dist/esm/utils/keychainAccessGroups.js +107 -24
- package/dist/types/api/identity.d.ts +36 -0
- package/dist/types/crypto/identityMarker.d.ts +8 -0
- package/dist/types/crypto/index.d.ts +3 -3
- package/dist/types/crypto/keyManager.d.ts +45 -1
- package/dist/types/crypto/keychainGroupMigration.d.ts +28 -6
- package/dist/types/crypto/organizationKeys.d.ts +4 -2
- package/dist/types/crypto/retiredKeys.d.ts +108 -17
- package/dist/types/utils/keychainAccessGroups.d.ts +41 -11
- package/package.json +1 -1
- package/src/api/identity.ts +198 -41
- package/src/crypto/identityMarker.ts +15 -0
- package/src/crypto/index.ts +12 -1
- package/src/crypto/keyManager.ts +127 -26
- package/src/crypto/keychainGroupMigration.ts +390 -185
- package/src/crypto/organizationKeys.ts +38 -6
- package/src/crypto/retiredKeys.ts +369 -65
- package/src/utils/keychainAccessGroups.ts +121 -24
|
@@ -11,9 +11,9 @@ import { isDev, logger } from '../logger/index.js';
|
|
|
11
11
|
import { deriveScopedSeedFromKey, signSocialReceiveDigest } from './identityDerivations.js';
|
|
12
12
|
import { registerIdentitySignerStore } from './identitySigner.js';
|
|
13
13
|
import { getOrganizationPrivateKey, isOrganizationAccountId } from './organizationKeys.js';
|
|
14
|
-
import {
|
|
14
|
+
import { getRetiredPrivateKey, retiredKeyScope, unmirrorPersonalRetiredKeys } from './retiredKeys.js';
|
|
15
15
|
import { groupedKeychainStore, resolveKeychainAccess, } from '../utils/keychainAccessGroups.js';
|
|
16
|
-
import {
|
|
16
|
+
import { prepareKeychainAccess } from './keychainGroupMigration.js';
|
|
17
17
|
import { RECOVERY_MNEMONIC_KEYCHAIN_SERVICE, RECOVERY_MNEMONIC_STORAGE_KEY, STORAGE_KEYS, V2_BACKUP_KEYCHAIN_SERVICE, V2_PRIMARY_KEYCHAIN_SERVICE, V2_STORAGE_KEYS, } from './keychainItems.js';
|
|
18
18
|
import { clearIdentityMarker, readIdentityMarker, updateIdentityMarker, writeIdentityMarker, } from './identityMarker.js';
|
|
19
19
|
import { DEVICE_BACKUP_VERSION, parseDeviceBackup, serializeDeviceBackup, } from './deviceBackup.js';
|
|
@@ -172,9 +172,7 @@ async function loadRawSecureStore() {
|
|
|
172
172
|
async function keychainItemStore(item) {
|
|
173
173
|
const raw = await loadRawSecureStore();
|
|
174
174
|
const access = await resolveKeychainAccess();
|
|
175
|
-
|
|
176
|
-
await ensureKeychainGroupsMigrated();
|
|
177
|
-
}
|
|
175
|
+
await prepareKeychainAccess(access);
|
|
178
176
|
return groupedKeychainStore(raw, access, item);
|
|
179
177
|
}
|
|
180
178
|
/** The store for the identity slots (primary, backup, legacy, phrase): Commons only on iOS. */
|
|
@@ -751,12 +749,30 @@ export class KeyManager {
|
|
|
751
749
|
* `signSocialReceive`) is fresh, so a cached key here would let a caller
|
|
752
750
|
* compare a new seed against an old key.
|
|
753
751
|
*
|
|
752
|
+
* iOS, in an app that holds the private key (Commons, the Peable wallet:
|
|
753
|
+
* keychain roles `holder` and `wallet`): DERIVED from the private key in the
|
|
754
|
+
* wallet group, never read from the public-key item, which sits in the shared
|
|
755
|
+
* group every Oxy app can write (R5-1); `null` when that app holds no private
|
|
756
|
+
* key. Every other iOS app reads the public-key item: it holds nothing it
|
|
757
|
+
* could derive from, and it never spends. Money code uses
|
|
758
|
+
* {@link getIdentityPublicKeyForMoney}.
|
|
759
|
+
*
|
|
754
760
|
* @returns Shared public key or null if no shared identity exists
|
|
755
761
|
*/
|
|
756
762
|
static async getSharedPublicKey() {
|
|
757
763
|
if (isWebPlatform()) {
|
|
758
764
|
return null;
|
|
759
765
|
}
|
|
766
|
+
if (isIOS() && (await KeyManager._holdsSpendingKeys())) {
|
|
767
|
+
// Not cached: a rotation in Commons changes it under the wallet.
|
|
768
|
+
try {
|
|
769
|
+
const privateKey = await KeyManager.getSharedPrivateKey();
|
|
770
|
+
return privateKey ? KeyManager.derivePublicKey(privateKey) : null;
|
|
771
|
+
}
|
|
772
|
+
catch {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
760
776
|
// Return cached value if available (iOS only; see above).
|
|
761
777
|
if (!isAndroid() && KeyManager.cachedSharedPublicKey !== null) {
|
|
762
778
|
return KeyManager.cachedSharedPublicKey;
|
|
@@ -789,6 +805,68 @@ export class KeyManager {
|
|
|
789
805
|
return null;
|
|
790
806
|
}
|
|
791
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* Whether this iOS app holds spending keys (keychain role `holder` or
|
|
810
|
+
* `wallet`), so a public key it uses for money must be derived from them.
|
|
811
|
+
* Throws while the keychain cannot say. @internal
|
|
812
|
+
*/
|
|
813
|
+
static async _holdsSpendingKeys() {
|
|
814
|
+
const access = await resolveKeychainAccess();
|
|
815
|
+
return access.mode === 'grouped' && access.role !== 'member';
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* THE public key of the identity a money holder (the Peable wallet, Commons)
|
|
819
|
+
* receives and spends with: the personal key, or with `opts.accountId` that
|
|
820
|
+
* organization's (ADR 0061 D6). It is the public half of exactly the private
|
|
821
|
+
* key {@link deriveScopedSeed} and {@link signSocialReceive} use, so an
|
|
822
|
+
* address shown from it is one this app can spend (R5-1).
|
|
823
|
+
*
|
|
824
|
+
* - iOS (Commons, the Peable wallet): DERIVED from the private key in the
|
|
825
|
+
* restricted keychain groups (personal: the wallet group's slot, then
|
|
826
|
+
* Commons' primary; organization: its pair). Never the shared group's
|
|
827
|
+
* public-key item, which every Oxy app can write. `null` when no private
|
|
828
|
+
* key is held, and in every other iOS app.
|
|
829
|
+
* - Android: inside Commons derived from its own key; elsewhere Commons'
|
|
830
|
+
* identity host (`describe`), which answers from Commons' own signer
|
|
831
|
+
* store over signature-protected IPC.
|
|
832
|
+
* - Web: `null`.
|
|
833
|
+
*
|
|
834
|
+
* Compare the answer with the account's published `#key-1` before showing a
|
|
835
|
+
* receive address (Peable ADR 0002). Lowercase uncompressed (`04…`).
|
|
836
|
+
*/
|
|
837
|
+
static async getIdentityPublicKeyForMoney(opts) {
|
|
838
|
+
if (isWebPlatform()) {
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
const accountId = opts?.accountId;
|
|
842
|
+
if (accountId !== undefined) {
|
|
843
|
+
if (!isOrganizationAccountId(accountId))
|
|
844
|
+
return null;
|
|
845
|
+
if (isAndroid() && !KeyManager.identitySignerStore) {
|
|
846
|
+
const bridge = await loadCommonsIdentityBridge();
|
|
847
|
+
const description = (await bridge?.describe(accountId)) ?? null;
|
|
848
|
+
return description?.accountId === accountId && description.publicKey
|
|
849
|
+
? KeyManager.canonicalPublicKey(description.publicKey)
|
|
850
|
+
: null;
|
|
851
|
+
}
|
|
852
|
+
const organizationKey = await getOrganizationPrivateKey(accountId);
|
|
853
|
+
return organizationKey ? KeyManager.derivePublicKey(organizationKey) : null;
|
|
854
|
+
}
|
|
855
|
+
if (isAndroid()) {
|
|
856
|
+
if (KeyManager.identitySignerStore) {
|
|
857
|
+
const local = await KeyManager.getPrivateKey();
|
|
858
|
+
return local ? KeyManager.derivePublicKey(local) : null;
|
|
859
|
+
}
|
|
860
|
+
const bridge = await loadCommonsIdentityBridge();
|
|
861
|
+
const publicKey = (await bridge?.describe())?.publicKey ?? null;
|
|
862
|
+
return publicKey ? KeyManager.canonicalPublicKey(publicKey) : null;
|
|
863
|
+
}
|
|
864
|
+
if (!isIOS()) {
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
const privateKey = (await KeyManager.getSharedPrivateKey()) ?? (await KeyManager.getPrivateKey());
|
|
868
|
+
return privateKey ? KeyManager.derivePublicKey(privateKey) : null;
|
|
869
|
+
}
|
|
792
870
|
/**
|
|
793
871
|
* The shared private key, where this app may hold it: iOS only.
|
|
794
872
|
*
|
|
@@ -2050,21 +2128,28 @@ export class KeyManager {
|
|
|
2050
2128
|
if (force) {
|
|
2051
2129
|
await KeyManager._bestEffortDeleteBackupsAllGenerations(store);
|
|
2052
2130
|
await KeyManager._clearSharedSlot(store);
|
|
2053
|
-
// The
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
}
|
|
2131
|
+
// The retired ring is NOT cleared (R4-4): it is money under the person's
|
|
2132
|
+
// previous keys, and "Start over" or a forced delete must not cost it.
|
|
2133
|
+
// Every personal entry is bound to its owner's account id (R5-2), so the
|
|
2134
|
+
// same person signing in again finds it, and a different person on this
|
|
2135
|
+
// phone never lists, derives from or signs with it. Only deleting the Oxy
|
|
2136
|
+
// ACCOUNT clears it, explicitly (`forgetRetiredIdentityKeys`).
|
|
2060
2137
|
}
|
|
2061
2138
|
// The device backup goes on EVERY delete, forced or not: it is restored
|
|
2062
2139
|
// silently, so leaving it would bring a deleted identity back on the next
|
|
2063
2140
|
// launch. Best-effort, like the other recovery sources.
|
|
2064
2141
|
await KeyManager._clearDeviceBackup();
|
|
2065
2142
|
// Same for the Android signer store: Commons' identity host must stop
|
|
2066
|
-
// answering for a deleted identity, forced or not
|
|
2143
|
+
// answering for a deleted identity, forced or not — its personal retired
|
|
2144
|
+
// keys included (R5-2): they stay in Commons' own ring, bound to their
|
|
2145
|
+
// owner, but the host must not hand them to the next person on the phone.
|
|
2067
2146
|
await KeyManager._clearIdentitySigner();
|
|
2147
|
+
try {
|
|
2148
|
+
await unmirrorPersonalRetiredKeys();
|
|
2149
|
+
}
|
|
2150
|
+
catch (error) {
|
|
2151
|
+
logger.warn('Failed to withdraw the personal retired keys from the identity host', { component: 'KeyManager' }, error);
|
|
2152
|
+
}
|
|
2068
2153
|
// Clear the marker AFTER key deletion succeeds — a marker must never outlive
|
|
2069
2154
|
// its identity (a leftover marker would route a truly-absent device to
|
|
2070
2155
|
// `recovery` instead of `welcome`).
|
|
@@ -2593,11 +2678,16 @@ export class KeyManager {
|
|
|
2593
2678
|
* (personal, or `accountId`'s) whose public key it names: the current key
|
|
2594
2679
|
* when it matches, else a RETIRED key Commons kept after a rotation or
|
|
2595
2680
|
* replacement (`./retiredKeys`, ADR 0061 D7 as amended). Spending only: a
|
|
2596
|
-
* retired key never signs in. Never falls back to another key.
|
|
2681
|
+
* retired key never signs in. Never falls back to another key. A PERSONAL
|
|
2682
|
+
* retired key is reached only with `opts.ownerAccountId`, the signed-in
|
|
2683
|
+
* person's account id, and only when the ring keeps it for that account
|
|
2684
|
+
* (R5-2); on Android outside Commons the identity host holds only the
|
|
2685
|
+
* personal keys of the person Commons is signed in as.
|
|
2597
2686
|
*
|
|
2598
2687
|
* @param info Context/domain-binding label (distinct labels → independent seeds).
|
|
2599
2688
|
* @param opts.accountId An organization's account id, for its key.
|
|
2600
2689
|
* @param opts.publicKey Selects one key of that account by its public key.
|
|
2690
|
+
* @param opts.ownerAccountId The person's account id, for a personal retired key.
|
|
2601
2691
|
* @returns 32 bytes of derived keying material, or `null` on web / when no
|
|
2602
2692
|
* identity key is available to this app.
|
|
2603
2693
|
*/
|
|
@@ -2614,7 +2704,7 @@ export class KeyManager {
|
|
|
2614
2704
|
const seed = (await bridge?.deriveRetiredScopedSeed?.(info, selected, opts.accountId)) ?? null;
|
|
2615
2705
|
return seed ? hexToBytes(seed) : null;
|
|
2616
2706
|
}
|
|
2617
|
-
const selected = await KeyManager._selectedPrivateKey(opts
|
|
2707
|
+
const selected = await KeyManager._selectedPrivateKey({ ...opts, publicKey: opts.publicKey });
|
|
2618
2708
|
return selected ? deriveScopedSeedFromKey(selected, info) : null;
|
|
2619
2709
|
}
|
|
2620
2710
|
if (opts?.accountId !== undefined) {
|
|
@@ -2682,7 +2772,7 @@ export class KeyManager {
|
|
|
2682
2772
|
return ((await bridge?.signRetiredSocialReceive?.(index, digest, selected, opts.accountId)) ??
|
|
2683
2773
|
null);
|
|
2684
2774
|
}
|
|
2685
|
-
const selected = await KeyManager._selectedPrivateKey(opts
|
|
2775
|
+
const selected = await KeyManager._selectedPrivateKey({ ...opts, publicKey: opts.publicKey });
|
|
2686
2776
|
return selected ? signSocialReceiveDigest(selected, index, digest) : null;
|
|
2687
2777
|
}
|
|
2688
2778
|
const accountId = opts?.accountId;
|
|
@@ -2721,7 +2811,8 @@ export class KeyManager {
|
|
|
2721
2811
|
static _selectorKey(publicKey) {
|
|
2722
2812
|
return KeyManager.isValidPublicKey(publicKey) ? KeyManager.canonicalPublicKey(publicKey) : null;
|
|
2723
2813
|
}
|
|
2724
|
-
static async _selectedPrivateKey(
|
|
2814
|
+
static async _selectedPrivateKey(opts) {
|
|
2815
|
+
const { publicKey, accountId, ownerAccountId } = opts;
|
|
2725
2816
|
if (retiredKeyScope({ accountId }) === null || !KeyManager.isValidPublicKey(publicKey)) {
|
|
2726
2817
|
return null;
|
|
2727
2818
|
}
|
|
@@ -2734,7 +2825,11 @@ export class KeyManager {
|
|
|
2734
2825
|
if (current && KeyManager.canonicalPublicKey(KeyManager.derivePublicKey(current)) === wanted) {
|
|
2735
2826
|
return current;
|
|
2736
2827
|
}
|
|
2737
|
-
return getRetiredPrivateKey(wanted, accountId !== undefined
|
|
2828
|
+
return getRetiredPrivateKey(wanted, accountId !== undefined
|
|
2829
|
+
? { accountId }
|
|
2830
|
+
: ownerAccountId !== undefined
|
|
2831
|
+
? { ownerAccountId }
|
|
2832
|
+
: undefined);
|
|
2738
2833
|
}
|
|
2739
2834
|
/**
|
|
2740
2835
|
* Get a shortened version of the public key for display
|