@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
package/src/crypto/keyManager.ts
CHANGED
|
@@ -32,13 +32,13 @@ import { isDev, logger } from '../logger';
|
|
|
32
32
|
import { deriveScopedSeedFromKey, signSocialReceiveDigest } from './identityDerivations';
|
|
33
33
|
import { type IdentitySignerStore, registerIdentitySignerStore } from './identitySigner';
|
|
34
34
|
import { getOrganizationPrivateKey, isOrganizationAccountId } from './organizationKeys';
|
|
35
|
-
import {
|
|
35
|
+
import { getRetiredPrivateKey, retiredKeyScope, unmirrorPersonalRetiredKeys } from './retiredKeys';
|
|
36
36
|
import {
|
|
37
37
|
type KeychainItemClass,
|
|
38
38
|
groupedKeychainStore,
|
|
39
39
|
resolveKeychainAccess,
|
|
40
40
|
} from '../utils/keychainAccessGroups';
|
|
41
|
-
import {
|
|
41
|
+
import { prepareKeychainAccess } from './keychainGroupMigration';
|
|
42
42
|
import {
|
|
43
43
|
RECOVERY_MNEMONIC_KEYCHAIN_SERVICE,
|
|
44
44
|
RECOVERY_MNEMONIC_STORAGE_KEY,
|
|
@@ -300,9 +300,7 @@ async function loadRawSecureStore(): Promise<ExpoSecureStoreLike> {
|
|
|
300
300
|
async function keychainItemStore(item: KeychainItemClass): Promise<ExpoSecureStoreLike> {
|
|
301
301
|
const raw = await loadRawSecureStore();
|
|
302
302
|
const access = await resolveKeychainAccess();
|
|
303
|
-
|
|
304
|
-
await ensureKeychainGroupsMigrated();
|
|
305
|
-
}
|
|
303
|
+
await prepareKeychainAccess(access);
|
|
306
304
|
return groupedKeychainStore(raw, access, item);
|
|
307
305
|
}
|
|
308
306
|
|
|
@@ -1072,6 +1070,14 @@ export class KeyManager {
|
|
|
1072
1070
|
* `signSocialReceive`) is fresh, so a cached key here would let a caller
|
|
1073
1071
|
* compare a new seed against an old key.
|
|
1074
1072
|
*
|
|
1073
|
+
* iOS, in an app that holds the private key (Commons, the Peable wallet:
|
|
1074
|
+
* keychain roles `holder` and `wallet`): DERIVED from the private key in the
|
|
1075
|
+
* wallet group, never read from the public-key item, which sits in the shared
|
|
1076
|
+
* group every Oxy app can write (R5-1); `null` when that app holds no private
|
|
1077
|
+
* key. Every other iOS app reads the public-key item: it holds nothing it
|
|
1078
|
+
* could derive from, and it never spends. Money code uses
|
|
1079
|
+
* {@link getIdentityPublicKeyForMoney}.
|
|
1080
|
+
*
|
|
1075
1081
|
* @returns Shared public key or null if no shared identity exists
|
|
1076
1082
|
*/
|
|
1077
1083
|
static async getSharedPublicKey(): Promise<string | null> {
|
|
@@ -1079,6 +1085,16 @@ export class KeyManager {
|
|
|
1079
1085
|
return null;
|
|
1080
1086
|
}
|
|
1081
1087
|
|
|
1088
|
+
if (isIOS() && (await KeyManager._holdsSpendingKeys())) {
|
|
1089
|
+
// Not cached: a rotation in Commons changes it under the wallet.
|
|
1090
|
+
try {
|
|
1091
|
+
const privateKey = await KeyManager.getSharedPrivateKey();
|
|
1092
|
+
return privateKey ? KeyManager.derivePublicKey(privateKey) : null;
|
|
1093
|
+
} catch {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1082
1098
|
// Return cached value if available (iOS only; see above).
|
|
1083
1099
|
if (!isAndroid() && KeyManager.cachedSharedPublicKey !== null) {
|
|
1084
1100
|
return KeyManager.cachedSharedPublicKey;
|
|
@@ -1113,6 +1129,70 @@ export class KeyManager {
|
|
|
1113
1129
|
}
|
|
1114
1130
|
}
|
|
1115
1131
|
|
|
1132
|
+
/**
|
|
1133
|
+
* Whether this iOS app holds spending keys (keychain role `holder` or
|
|
1134
|
+
* `wallet`), so a public key it uses for money must be derived from them.
|
|
1135
|
+
* Throws while the keychain cannot say. @internal
|
|
1136
|
+
*/
|
|
1137
|
+
private static async _holdsSpendingKeys(): Promise<boolean> {
|
|
1138
|
+
const access = await resolveKeychainAccess();
|
|
1139
|
+
return access.mode === 'grouped' && access.role !== 'member';
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* THE public key of the identity a money holder (the Peable wallet, Commons)
|
|
1144
|
+
* receives and spends with: the personal key, or with `opts.accountId` that
|
|
1145
|
+
* organization's (ADR 0061 D6). It is the public half of exactly the private
|
|
1146
|
+
* key {@link deriveScopedSeed} and {@link signSocialReceive} use, so an
|
|
1147
|
+
* address shown from it is one this app can spend (R5-1).
|
|
1148
|
+
*
|
|
1149
|
+
* - iOS (Commons, the Peable wallet): DERIVED from the private key in the
|
|
1150
|
+
* restricted keychain groups (personal: the wallet group's slot, then
|
|
1151
|
+
* Commons' primary; organization: its pair). Never the shared group's
|
|
1152
|
+
* public-key item, which every Oxy app can write. `null` when no private
|
|
1153
|
+
* key is held, and in every other iOS app.
|
|
1154
|
+
* - Android: inside Commons derived from its own key; elsewhere Commons'
|
|
1155
|
+
* identity host (`describe`), which answers from Commons' own signer
|
|
1156
|
+
* store over signature-protected IPC.
|
|
1157
|
+
* - Web: `null`.
|
|
1158
|
+
*
|
|
1159
|
+
* Compare the answer with the account's published `#key-1` before showing a
|
|
1160
|
+
* receive address (Peable ADR 0002). Lowercase uncompressed (`04…`).
|
|
1161
|
+
*/
|
|
1162
|
+
static async getIdentityPublicKeyForMoney(opts?: { accountId?: string }): Promise<string | null> {
|
|
1163
|
+
if (isWebPlatform()) {
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
const accountId = opts?.accountId;
|
|
1167
|
+
if (accountId !== undefined) {
|
|
1168
|
+
if (!isOrganizationAccountId(accountId)) return null;
|
|
1169
|
+
if (isAndroid() && !KeyManager.identitySignerStore) {
|
|
1170
|
+
const bridge = await loadCommonsIdentityBridge();
|
|
1171
|
+
const description = (await bridge?.describe(accountId)) ?? null;
|
|
1172
|
+
return description?.accountId === accountId && description.publicKey
|
|
1173
|
+
? KeyManager.canonicalPublicKey(description.publicKey)
|
|
1174
|
+
: null;
|
|
1175
|
+
}
|
|
1176
|
+
const organizationKey = await getOrganizationPrivateKey(accountId);
|
|
1177
|
+
return organizationKey ? KeyManager.derivePublicKey(organizationKey) : null;
|
|
1178
|
+
}
|
|
1179
|
+
if (isAndroid()) {
|
|
1180
|
+
if (KeyManager.identitySignerStore) {
|
|
1181
|
+
const local = await KeyManager.getPrivateKey();
|
|
1182
|
+
return local ? KeyManager.derivePublicKey(local) : null;
|
|
1183
|
+
}
|
|
1184
|
+
const bridge = await loadCommonsIdentityBridge();
|
|
1185
|
+
const publicKey = (await bridge?.describe())?.publicKey ?? null;
|
|
1186
|
+
return publicKey ? KeyManager.canonicalPublicKey(publicKey) : null;
|
|
1187
|
+
}
|
|
1188
|
+
if (!isIOS()) {
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1191
|
+
const privateKey =
|
|
1192
|
+
(await KeyManager.getSharedPrivateKey()) ?? (await KeyManager.getPrivateKey());
|
|
1193
|
+
return privateKey ? KeyManager.derivePublicKey(privateKey) : null;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1116
1196
|
/**
|
|
1117
1197
|
* The shared private key, where this app may hold it: iOS only.
|
|
1118
1198
|
*
|
|
@@ -2612,16 +2692,12 @@ export class KeyManager {
|
|
|
2612
2692
|
if (force) {
|
|
2613
2693
|
await KeyManager._bestEffortDeleteBackupsAllGenerations(store);
|
|
2614
2694
|
await KeyManager._clearSharedSlot(store);
|
|
2615
|
-
// The
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
{ component: 'KeyManager' },
|
|
2622
|
-
error,
|
|
2623
|
-
);
|
|
2624
|
-
}
|
|
2695
|
+
// The retired ring is NOT cleared (R4-4): it is money under the person's
|
|
2696
|
+
// previous keys, and "Start over" or a forced delete must not cost it.
|
|
2697
|
+
// Every personal entry is bound to its owner's account id (R5-2), so the
|
|
2698
|
+
// same person signing in again finds it, and a different person on this
|
|
2699
|
+
// phone never lists, derives from or signs with it. Only deleting the Oxy
|
|
2700
|
+
// ACCOUNT clears it, explicitly (`forgetRetiredIdentityKeys`).
|
|
2625
2701
|
}
|
|
2626
2702
|
|
|
2627
2703
|
// The device backup goes on EVERY delete, forced or not: it is restored
|
|
@@ -2630,8 +2706,19 @@ export class KeyManager {
|
|
|
2630
2706
|
await KeyManager._clearDeviceBackup();
|
|
2631
2707
|
|
|
2632
2708
|
// Same for the Android signer store: Commons' identity host must stop
|
|
2633
|
-
// answering for a deleted identity, forced or not
|
|
2709
|
+
// answering for a deleted identity, forced or not — its personal retired
|
|
2710
|
+
// keys included (R5-2): they stay in Commons' own ring, bound to their
|
|
2711
|
+
// owner, but the host must not hand them to the next person on the phone.
|
|
2634
2712
|
await KeyManager._clearIdentitySigner();
|
|
2713
|
+
try {
|
|
2714
|
+
await unmirrorPersonalRetiredKeys();
|
|
2715
|
+
} catch (error) {
|
|
2716
|
+
logger.warn(
|
|
2717
|
+
'Failed to withdraw the personal retired keys from the identity host',
|
|
2718
|
+
{ component: 'KeyManager' },
|
|
2719
|
+
error,
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2635
2722
|
|
|
2636
2723
|
// Clear the marker AFTER key deletion succeeds — a marker must never outlive
|
|
2637
2724
|
// its identity (a leftover marker would route a truly-absent device to
|
|
@@ -3244,17 +3331,22 @@ export class KeyManager {
|
|
|
3244
3331
|
* (personal, or `accountId`'s) whose public key it names: the current key
|
|
3245
3332
|
* when it matches, else a RETIRED key Commons kept after a rotation or
|
|
3246
3333
|
* replacement (`./retiredKeys`, ADR 0061 D7 as amended). Spending only: a
|
|
3247
|
-
* retired key never signs in. Never falls back to another key.
|
|
3334
|
+
* retired key never signs in. Never falls back to another key. A PERSONAL
|
|
3335
|
+
* retired key is reached only with `opts.ownerAccountId`, the signed-in
|
|
3336
|
+
* person's account id, and only when the ring keeps it for that account
|
|
3337
|
+
* (R5-2); on Android outside Commons the identity host holds only the
|
|
3338
|
+
* personal keys of the person Commons is signed in as.
|
|
3248
3339
|
*
|
|
3249
3340
|
* @param info Context/domain-binding label (distinct labels → independent seeds).
|
|
3250
3341
|
* @param opts.accountId An organization's account id, for its key.
|
|
3251
3342
|
* @param opts.publicKey Selects one key of that account by its public key.
|
|
3343
|
+
* @param opts.ownerAccountId The person's account id, for a personal retired key.
|
|
3252
3344
|
* @returns 32 bytes of derived keying material, or `null` on web / when no
|
|
3253
3345
|
* identity key is available to this app.
|
|
3254
3346
|
*/
|
|
3255
3347
|
static async deriveScopedSeed(
|
|
3256
3348
|
info: string,
|
|
3257
|
-
opts?: { accountId?: string; publicKey?: string },
|
|
3349
|
+
opts?: { accountId?: string; publicKey?: string; ownerAccountId?: string },
|
|
3258
3350
|
): Promise<Uint8Array | null> {
|
|
3259
3351
|
if (isWebPlatform()) {
|
|
3260
3352
|
return null;
|
|
@@ -3268,7 +3360,7 @@ export class KeyManager {
|
|
|
3268
3360
|
(await bridge?.deriveRetiredScopedSeed?.(info, selected, opts.accountId)) ?? null;
|
|
3269
3361
|
return seed ? hexToBytes(seed) : null;
|
|
3270
3362
|
}
|
|
3271
|
-
const selected = await KeyManager._selectedPrivateKey(opts
|
|
3363
|
+
const selected = await KeyManager._selectedPrivateKey({ ...opts, publicKey: opts.publicKey });
|
|
3272
3364
|
return selected ? deriveScopedSeedFromKey(selected, info) : null;
|
|
3273
3365
|
}
|
|
3274
3366
|
if (opts?.accountId !== undefined) {
|
|
@@ -3332,7 +3424,7 @@ export class KeyManager {
|
|
|
3332
3424
|
static async signSocialReceive(
|
|
3333
3425
|
index: number,
|
|
3334
3426
|
digest: string,
|
|
3335
|
-
opts?: { accountId?: string; publicKey?: string },
|
|
3427
|
+
opts?: { accountId?: string; publicKey?: string; ownerAccountId?: string },
|
|
3336
3428
|
): Promise<CommonsSocialReceiveSignature | null> {
|
|
3337
3429
|
if (isWebPlatform()) {
|
|
3338
3430
|
return null;
|
|
@@ -3347,7 +3439,7 @@ export class KeyManager {
|
|
|
3347
3439
|
null
|
|
3348
3440
|
);
|
|
3349
3441
|
}
|
|
3350
|
-
const selected = await KeyManager._selectedPrivateKey(opts
|
|
3442
|
+
const selected = await KeyManager._selectedPrivateKey({ ...opts, publicKey: opts.publicKey });
|
|
3351
3443
|
return selected ? signSocialReceiveDigest(selected, index, digest) : null;
|
|
3352
3444
|
}
|
|
3353
3445
|
const accountId = opts?.accountId;
|
|
@@ -3388,10 +3480,12 @@ export class KeyManager {
|
|
|
3388
3480
|
return KeyManager.isValidPublicKey(publicKey) ? KeyManager.canonicalPublicKey(publicKey) : null;
|
|
3389
3481
|
}
|
|
3390
3482
|
|
|
3391
|
-
private static async _selectedPrivateKey(
|
|
3392
|
-
publicKey: string
|
|
3393
|
-
accountId?: string
|
|
3394
|
-
|
|
3483
|
+
private static async _selectedPrivateKey(opts: {
|
|
3484
|
+
publicKey: string;
|
|
3485
|
+
accountId?: string;
|
|
3486
|
+
ownerAccountId?: string;
|
|
3487
|
+
}): Promise<string | null> {
|
|
3488
|
+
const { publicKey, accountId, ownerAccountId } = opts;
|
|
3395
3489
|
if (retiredKeyScope({ accountId }) === null || !KeyManager.isValidPublicKey(publicKey)) {
|
|
3396
3490
|
return null;
|
|
3397
3491
|
}
|
|
@@ -3405,7 +3499,14 @@ export class KeyManager {
|
|
|
3405
3499
|
if (current && KeyManager.canonicalPublicKey(KeyManager.derivePublicKey(current)) === wanted) {
|
|
3406
3500
|
return current;
|
|
3407
3501
|
}
|
|
3408
|
-
return getRetiredPrivateKey(
|
|
3502
|
+
return getRetiredPrivateKey(
|
|
3503
|
+
wanted,
|
|
3504
|
+
accountId !== undefined
|
|
3505
|
+
? { accountId }
|
|
3506
|
+
: ownerAccountId !== undefined
|
|
3507
|
+
? { ownerAccountId }
|
|
3508
|
+
: undefined,
|
|
3509
|
+
);
|
|
3409
3510
|
}
|
|
3410
3511
|
|
|
3411
3512
|
/**
|