@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.
- package/dist/cjs/api/identity.js +40 -1
- package/dist/cjs/crypto/index.js +28 -1
- package/dist/cjs/crypto/keyManager.js +175 -107
- package/dist/cjs/crypto/keychainGroupMigration.js +353 -0
- package/dist/cjs/crypto/keychainItems.js +70 -0
- package/dist/cjs/crypto/organizationKeys.js +80 -72
- package/dist/cjs/crypto/retiredKeys.js +371 -0
- package/dist/cjs/crypto/secureStoreOptions.js +2 -0
- package/dist/cjs/session/index.js +11 -1
- package/dist/cjs/utils/keychainAccessGroups.js +248 -0
- package/dist/esm/api/identity.js +40 -1
- package/dist/esm/crypto/index.js +6 -0
- package/dist/esm/crypto/keyManager.js +132 -64
- package/dist/esm/crypto/keychainGroupMigration.js +348 -0
- package/dist/esm/crypto/keychainItems.js +61 -0
- package/dist/esm/crypto/organizationKeys.js +54 -46
- package/dist/esm/crypto/retiredKeys.js +361 -0
- package/dist/esm/crypto/secureStoreOptions.js +1 -0
- package/dist/esm/session/index.js +3 -0
- package/dist/esm/utils/keychainAccessGroups.js +231 -0
- package/dist/types/api/identity.d.ts +10 -0
- package/dist/types/crypto/identitySigner.d.ts +23 -0
- package/dist/types/crypto/index.d.ts +6 -0
- package/dist/types/crypto/keyManager.d.ts +32 -2
- package/dist/types/crypto/keychainGroupMigration.d.ts +27 -0
- package/dist/types/crypto/keychainItems.d.ts +41 -0
- package/dist/types/crypto/organizationKeys.d.ts +11 -9
- package/dist/types/crypto/retiredKeys.d.ts +116 -0
- package/dist/types/crypto/secureStoreOptions.d.ts +15 -0
- package/dist/types/session/index.d.ts +2 -0
- package/dist/types/utils/keychainAccessGroups.d.ts +104 -0
- package/package.json +3 -3
- package/src/api/identity.ts +46 -3
- package/src/crypto/identitySigner.ts +23 -0
- package/src/crypto/index.ts +40 -0
- package/src/crypto/keyManager.ts +165 -102
- package/src/crypto/keychainGroupMigration.ts +440 -0
- package/src/crypto/keychainItems.ts +73 -0
- package/src/crypto/organizationKeys.ts +72 -57
- package/src/crypto/retiredKeys.ts +453 -0
- package/src/crypto/secureStoreOptions.ts +32 -0
- package/src/session/index.ts +11 -0
- package/src/utils/keychainAccessGroups.ts +308 -0
- package/src/types/expo-secure-store.d.ts +0 -21
package/src/crypto/keyManager.ts
CHANGED
|
@@ -32,6 +32,22 @@ 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 { forgetRetiredIdentityKeys, getRetiredPrivateKey, retiredKeyScope } from './retiredKeys';
|
|
36
|
+
import {
|
|
37
|
+
type KeychainItemClass,
|
|
38
|
+
groupedKeychainStore,
|
|
39
|
+
resolveKeychainAccess,
|
|
40
|
+
} from '../utils/keychainAccessGroups';
|
|
41
|
+
import { ensureKeychainGroupsMigrated } from './keychainGroupMigration';
|
|
42
|
+
import {
|
|
43
|
+
RECOVERY_MNEMONIC_KEYCHAIN_SERVICE,
|
|
44
|
+
RECOVERY_MNEMONIC_STORAGE_KEY,
|
|
45
|
+
STORAGE_KEYS,
|
|
46
|
+
V2_BACKUP_KEYCHAIN_SERVICE,
|
|
47
|
+
V2_PRIMARY_KEYCHAIN_SERVICE,
|
|
48
|
+
V2_STORAGE_KEYS,
|
|
49
|
+
} from './keychainItems';
|
|
50
|
+
import type { OxySecureStoreOptions } from './secureStoreOptions';
|
|
35
51
|
import {
|
|
36
52
|
type IdentityMarker,
|
|
37
53
|
clearIdentityMarker,
|
|
@@ -53,37 +69,6 @@ import {
|
|
|
53
69
|
*/
|
|
54
70
|
const DEVICE_BACKUP_TOMBSTONE = JSON.stringify({ deleted: true });
|
|
55
71
|
|
|
56
|
-
/**
|
|
57
|
-
* Options for expo-secure-store calls made by KeyManager.
|
|
58
|
-
*
|
|
59
|
-
* Defined as a standalone interface (not extending expo-secure-store's
|
|
60
|
-
* `SecureStoreOptions`) so that server / Node.js consumers of @oxy.so/core
|
|
61
|
-
* do not transitively pull in expo-modules-core's type declarations under
|
|
62
|
-
* NodeNext module resolution (which would cause NodeJS.Timeout / number
|
|
63
|
-
* pollution across all timer APIs). The fields mirror SecureStoreOptions
|
|
64
|
-
* exactly — any object satisfying this interface is safe to pass to the
|
|
65
|
-
* real expo-secure-store methods (which accept `options?: object`).
|
|
66
|
-
*/
|
|
67
|
-
interface OxySecureStoreOptions {
|
|
68
|
-
/** Keychain service name (Android keystore / iOS keychain tag). */
|
|
69
|
-
keychainService?: string;
|
|
70
|
-
/**
|
|
71
|
-
* Keychain / Keystore item accessibility. Use the constants from the
|
|
72
|
-
* `ExpoSecureStoreLike` handle returned by `initSecureStore()`, e.g.
|
|
73
|
-
* `store.WHEN_UNLOCKED_THIS_DEVICE_ONLY`.
|
|
74
|
-
*/
|
|
75
|
-
keychainAccessible?: number;
|
|
76
|
-
/** Prompt shown to the user for biometric/auth-protected items. */
|
|
77
|
-
authenticationPrompt?: string;
|
|
78
|
-
/** Whether Face ID / Touch ID / biometric auth is required to read the item. */
|
|
79
|
-
requireAuthentication?: boolean;
|
|
80
|
-
/**
|
|
81
|
-
* iOS Keychain access group. Required for sharing identity material across
|
|
82
|
-
* apps in the Oxy ecosystem via Keychain Sharing entitlements.
|
|
83
|
-
*/
|
|
84
|
-
keychainAccessGroup?: string;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
72
|
/**
|
|
88
73
|
* Thrown when an identity-mutating operation (createIdentity / importKeyPair)
|
|
89
74
|
* is invoked while a valid identity already exists on the device.
|
|
@@ -181,17 +166,6 @@ export type IdentityRecoveryResult =
|
|
|
181
166
|
| { recovered: true; source: 'backup' | 'shared' | 'device-backup'; publicKey: string }
|
|
182
167
|
| { recovered: false; reason: 'not-lost' | 'no-sources' | 'mismatch' | 'unavailable' };
|
|
183
168
|
|
|
184
|
-
const STORAGE_KEYS = {
|
|
185
|
-
PRIVATE_KEY: 'oxy_identity_private_key',
|
|
186
|
-
PUBLIC_KEY: 'oxy_identity_public_key',
|
|
187
|
-
BACKUP_PRIVATE_KEY: 'oxy_identity_backup_private_key',
|
|
188
|
-
BACKUP_PUBLIC_KEY: 'oxy_identity_backup_public_key',
|
|
189
|
-
BACKUP_TIMESTAMP: 'oxy_identity_backup_timestamp',
|
|
190
|
-
// The cross-app identity in the iOS keychain access group (iOS only).
|
|
191
|
-
SHARED_PRIVATE_KEY: 'oxy_shared_identity_private_key',
|
|
192
|
-
SHARED_PUBLIC_KEY: 'oxy_shared_identity_public_key',
|
|
193
|
-
} as const;
|
|
194
|
-
|
|
195
169
|
/**
|
|
196
170
|
* v2 identity slot layout — blast-radius isolation.
|
|
197
171
|
*
|
|
@@ -207,16 +181,8 @@ const STORAGE_KEYS = {
|
|
|
207
181
|
* Migration from the legacy layout is lazy + verify-before-delete — see
|
|
208
182
|
* {@link KeyManager._runSlotMigration}.
|
|
209
183
|
*/
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const V2_STORAGE_KEYS = {
|
|
214
|
-
PRIVATE_KEY: 'oxy_identity_private_key_v2',
|
|
215
|
-
PUBLIC_KEY: 'oxy_identity_public_key_v2',
|
|
216
|
-
BACKUP_PRIVATE_KEY: 'oxy_identity_backup_private_key_v2',
|
|
217
|
-
BACKUP_PUBLIC_KEY: 'oxy_identity_backup_public_key_v2',
|
|
218
|
-
BACKUP_TIMESTAMP: 'oxy_identity_backup_timestamp_v2',
|
|
219
|
-
} as const;
|
|
184
|
+
// The names (V2_PRIMARY_KEYCHAIN_SERVICE, V2_BACKUP_KEYCHAIN_SERVICE,
|
|
185
|
+
// V2_STORAGE_KEYS) live in `./keychainItems`, beside every other identity item.
|
|
220
186
|
|
|
221
187
|
/**
|
|
222
188
|
* Dedicated keychain slot for the recovery mnemonic (the 12-word phrase).
|
|
@@ -234,8 +200,7 @@ const V2_STORAGE_KEYS = {
|
|
|
234
200
|
* lives ONLY in this slot: it is never mirrored into the identity marker,
|
|
235
201
|
* {@link KeyManager.getIdentityStatus}, logs, or any exported bundle.
|
|
236
202
|
*/
|
|
237
|
-
|
|
238
|
-
const RECOVERY_MNEMONIC_STORAGE_KEY = 'oxy_identity_mnemonic_v1';
|
|
203
|
+
// RECOVERY_MNEMONIC_KEYCHAIN_SERVICE / RECOVERY_MNEMONIC_STORAGE_KEY: `./keychainItems`.
|
|
239
204
|
|
|
240
205
|
/**
|
|
241
206
|
* Advisory AsyncStorage fast-path flag: set once the v2 slots own the identity.
|
|
@@ -301,13 +266,6 @@ type SlotMigrationResult =
|
|
|
301
266
|
| { mode: 'legacy'; layout: ResolvedSlotLayout }
|
|
302
267
|
| { mode: 'deferred'; cause: unknown };
|
|
303
268
|
|
|
304
|
-
/**
|
|
305
|
-
* iOS Keychain Access Group for sharing identities across Oxy apps
|
|
306
|
-
* All Oxy apps must have this access group enabled in their entitlements
|
|
307
|
-
* Format: [Team ID].so.oxy.shared or group.so.oxy.shared
|
|
308
|
-
*/
|
|
309
|
-
const IOS_KEYCHAIN_GROUP = 'group.so.oxy.shared';
|
|
310
|
-
|
|
311
269
|
/**
|
|
312
270
|
* Initialize React Native specific modules
|
|
313
271
|
*
|
|
@@ -317,7 +275,7 @@ const IOS_KEYCHAIN_GROUP = 'group.so.oxy.shared';
|
|
|
317
275
|
* `expo-secure-store`; off RN it throws (and is never called because every
|
|
318
276
|
* caller is gated by `isWebPlatform()` / native-only paths).
|
|
319
277
|
*/
|
|
320
|
-
async function
|
|
278
|
+
async function loadRawSecureStore(): Promise<ExpoSecureStoreLike> {
|
|
321
279
|
try {
|
|
322
280
|
return await loadSecureStore();
|
|
323
281
|
} catch (error) {
|
|
@@ -328,6 +286,31 @@ async function initSecureStore(): Promise<ExpoSecureStoreLike> {
|
|
|
328
286
|
}
|
|
329
287
|
}
|
|
330
288
|
|
|
289
|
+
/**
|
|
290
|
+
* The secure store routed to the keychain group of `item`
|
|
291
|
+
* (`../utils/keychainAccessGroups`): on iOS reads walk the item's group then
|
|
292
|
+
* the pre-split shared group, writes carry the item's group and THROW in an app
|
|
293
|
+
* that may not hold it, and in Commons every access first awaits the
|
|
294
|
+
* access-group migration. Android and web are unchanged (`ungrouped`).
|
|
295
|
+
*
|
|
296
|
+
* Throws when the store cannot load or the keychain cannot say which groups
|
|
297
|
+
* this app holds; every caller already treats a throw here as "storage
|
|
298
|
+
* unavailable".
|
|
299
|
+
*/
|
|
300
|
+
async function keychainItemStore(item: KeychainItemClass): Promise<ExpoSecureStoreLike> {
|
|
301
|
+
const raw = await loadRawSecureStore();
|
|
302
|
+
const access = await resolveKeychainAccess();
|
|
303
|
+
if (access.mode === 'grouped' && access.role === 'holder') {
|
|
304
|
+
await ensureKeychainGroupsMigrated();
|
|
305
|
+
}
|
|
306
|
+
return groupedKeychainStore(raw, access, item);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** The store for the identity slots (primary, backup, legacy, phrase): Commons only on iOS. */
|
|
310
|
+
function initSecureStore(): Promise<ExpoSecureStoreLike> {
|
|
311
|
+
return keychainItemStore('identity-secret');
|
|
312
|
+
}
|
|
313
|
+
|
|
331
314
|
/**
|
|
332
315
|
* Check if we're on web platform
|
|
333
316
|
* Identity storage is only available on native platforms (iOS/Android)
|
|
@@ -875,13 +858,15 @@ export class KeyManager {
|
|
|
875
858
|
* Android signer store is cleared on every delete, in {@link deleteIdentity}.
|
|
876
859
|
*/
|
|
877
860
|
private static async _clearSharedSlot(
|
|
878
|
-
|
|
861
|
+
_identityStore: Awaited<ReturnType<typeof initSecureStore>>,
|
|
879
862
|
): Promise<void> {
|
|
880
863
|
try {
|
|
881
864
|
if (isIOS()) {
|
|
882
|
-
|
|
883
|
-
await
|
|
884
|
-
|
|
865
|
+
// The shared slot lives in two groups of its own, not the identity slots'.
|
|
866
|
+
await (await keychainItemStore('spending-key')).deleteItemAsync(
|
|
867
|
+
STORAGE_KEYS.SHARED_PRIVATE_KEY,
|
|
868
|
+
);
|
|
869
|
+
await (await keychainItemStore('shared')).deleteItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY);
|
|
885
870
|
}
|
|
886
871
|
KeyManager.invalidateSharedCache();
|
|
887
872
|
} catch (error) {
|
|
@@ -964,6 +949,25 @@ export class KeyManager {
|
|
|
964
949
|
KeyManager.cachedHasSharedIdentity = null;
|
|
965
950
|
}
|
|
966
951
|
|
|
952
|
+
/**
|
|
953
|
+
* iOS: write the personal key's cross-app slot — the private key to the
|
|
954
|
+
* wallet group (Commons and the Peable wallet), the public key to the shared
|
|
955
|
+
* group (every Oxy app). Throws in any app that may not hold the private key.
|
|
956
|
+
* @internal
|
|
957
|
+
*/
|
|
958
|
+
private static async _writeIosSharedSlot(privateKey: string, publicKey: string): Promise<void> {
|
|
959
|
+
const privateStore = await keychainItemStore('spending-key');
|
|
960
|
+
const privateOpts: OxySecureStoreOptions = {
|
|
961
|
+
keychainAccessible: privateStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
962
|
+
};
|
|
963
|
+
await privateStore.setItemAsync(STORAGE_KEYS.SHARED_PRIVATE_KEY, privateKey, privateOpts);
|
|
964
|
+
await (await keychainItemStore('shared')).setItemAsync(
|
|
965
|
+
STORAGE_KEYS.SHARED_PUBLIC_KEY,
|
|
966
|
+
publicKey,
|
|
967
|
+
{},
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
|
|
967
971
|
/**
|
|
968
972
|
* Lowercase and pad to canonical 64-hex-char form.
|
|
969
973
|
*
|
|
@@ -1002,8 +1006,10 @@ export class KeyManager {
|
|
|
1002
1006
|
// ==================== SHARED IDENTITY METHODS ====================
|
|
1003
1007
|
// One Oxy identity across every Oxy app on a device.
|
|
1004
1008
|
//
|
|
1005
|
-
// iOS:
|
|
1006
|
-
//
|
|
1009
|
+
// iOS: keychain access groups (docs/identity/ios-keychain-access-groups.md).
|
|
1010
|
+
// The private key is in `<TEAM>.so.oxy.wallet` (Commons and the Peable
|
|
1011
|
+
// wallet only), the public key in `<TEAM>.group.so.oxy.shared`, which every
|
|
1012
|
+
// Oxy app reads. Every other app signs in through Commons (ADR 0029).
|
|
1007
1013
|
// Android: every app has its own UID, so nothing is shared through storage.
|
|
1008
1014
|
// Commons is the ONLY holder of the private key. It keeps the identity
|
|
1009
1015
|
// signer store (`setIdentitySignerStore`) for its identity host, and every
|
|
@@ -1031,22 +1037,11 @@ export class KeyManager {
|
|
|
1031
1037
|
const { privateKey, publicKey } = await KeyManager.generateKeyPair();
|
|
1032
1038
|
|
|
1033
1039
|
if (isIOS()) {
|
|
1034
|
-
const store = await initSecureStore();
|
|
1035
|
-
// Note: keychainAccessGroup requires Keychain Sharing capability in Xcode
|
|
1036
1040
|
try {
|
|
1037
|
-
|
|
1038
|
-
keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
1039
|
-
keychainAccessGroup: IOS_KEYCHAIN_GROUP, // Enables sharing across apps
|
|
1040
|
-
};
|
|
1041
|
-
await store.setItemAsync(STORAGE_KEYS.SHARED_PRIVATE_KEY, privateKey, privateOpts);
|
|
1042
|
-
|
|
1043
|
-
const publicOpts: OxySecureStoreOptions = {
|
|
1044
|
-
keychainAccessGroup: IOS_KEYCHAIN_GROUP,
|
|
1045
|
-
};
|
|
1046
|
-
await store.setItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, publicKey, publicOpts);
|
|
1041
|
+
await KeyManager._writeIosSharedSlot(privateKey, publicKey);
|
|
1047
1042
|
} catch (error) {
|
|
1048
1043
|
throw new Error(
|
|
1049
|
-
`Failed to create shared identity on iOS.
|
|
1044
|
+
`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}`,
|
|
1050
1045
|
);
|
|
1051
1046
|
}
|
|
1052
1047
|
} else if (isAndroid()) {
|
|
@@ -1093,9 +1088,8 @@ export class KeyManager {
|
|
|
1093
1088
|
let publicKey: string | null = null;
|
|
1094
1089
|
|
|
1095
1090
|
if (isIOS()) {
|
|
1096
|
-
const store = await
|
|
1097
|
-
|
|
1098
|
-
publicKey = await store.getItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, opts);
|
|
1091
|
+
const store = await keychainItemStore('shared');
|
|
1092
|
+
publicKey = await store.getItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, {});
|
|
1099
1093
|
} else if (isAndroid()) {
|
|
1100
1094
|
const signer = KeyManager.identitySignerStore;
|
|
1101
1095
|
if (signer) {
|
|
@@ -1136,9 +1130,10 @@ export class KeyManager {
|
|
|
1136
1130
|
}
|
|
1137
1131
|
|
|
1138
1132
|
try {
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1133
|
+
// The wallet group: Commons and the Peable wallet. Every other app's plan
|
|
1134
|
+
// is empty, so this is `null` there without touching the keychain.
|
|
1135
|
+
const store = await keychainItemStore('spending-key');
|
|
1136
|
+
return await store.getItemAsync(STORAGE_KEYS.SHARED_PRIVATE_KEY, {});
|
|
1142
1137
|
} catch (error) {
|
|
1143
1138
|
if (isDev()) {
|
|
1144
1139
|
logger.warn('Failed to get shared private key', { component: 'KeyManager' }, error);
|
|
@@ -1245,17 +1240,7 @@ export class KeyManager {
|
|
|
1245
1240
|
const publicKey = deriveSecp256k1PublicKey(canonicalPrivate);
|
|
1246
1241
|
|
|
1247
1242
|
if (isIOS()) {
|
|
1248
|
-
|
|
1249
|
-
const privateOpts: OxySecureStoreOptions = {
|
|
1250
|
-
keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
1251
|
-
keychainAccessGroup: IOS_KEYCHAIN_GROUP,
|
|
1252
|
-
};
|
|
1253
|
-
await store.setItemAsync(STORAGE_KEYS.SHARED_PRIVATE_KEY, canonicalPrivate, privateOpts);
|
|
1254
|
-
|
|
1255
|
-
const publicOpts: OxySecureStoreOptions = {
|
|
1256
|
-
keychainAccessGroup: IOS_KEYCHAIN_GROUP,
|
|
1257
|
-
};
|
|
1258
|
-
await store.setItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, publicKey, publicOpts);
|
|
1243
|
+
await KeyManager._writeIosSharedSlot(canonicalPrivate, publicKey);
|
|
1259
1244
|
} else if (isAndroid()) {
|
|
1260
1245
|
await KeyManager._writeIdentitySigner(canonicalPrivate, publicKey);
|
|
1261
1246
|
}
|
|
@@ -2627,6 +2612,16 @@ export class KeyManager {
|
|
|
2627
2612
|
if (force) {
|
|
2628
2613
|
await KeyManager._bestEffortDeleteBackupsAllGenerations(store);
|
|
2629
2614
|
await KeyManager._clearSharedSlot(store);
|
|
2615
|
+
// The person's retired keys go with the identity they belonged to.
|
|
2616
|
+
try {
|
|
2617
|
+
await forgetRetiredIdentityKeys();
|
|
2618
|
+
} catch (error) {
|
|
2619
|
+
logger.warn(
|
|
2620
|
+
'Failed to clear retired keys during delete',
|
|
2621
|
+
{ component: 'KeyManager' },
|
|
2622
|
+
error,
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2630
2625
|
}
|
|
2631
2626
|
|
|
2632
2627
|
// The device backup goes on EVERY delete, forced or not: it is restored
|
|
@@ -3230,7 +3225,8 @@ export class KeyManager {
|
|
|
3230
3225
|
* `HDKey.fromMasterSeed` and never touches the identity key.
|
|
3231
3226
|
*
|
|
3232
3227
|
* Key source (native only):
|
|
3233
|
-
* - iOS: the shared
|
|
3228
|
+
* - iOS: the shared slot's private key in the wallet keychain group (Commons
|
|
3229
|
+
* and the Peable wallet only; `null` in every other app), then this device's
|
|
3234
3230
|
* primary identity.
|
|
3235
3231
|
* - Android: Commons (the one app with a signer store) uses its own primary
|
|
3236
3232
|
* identity; every other app asks Commons over IPC and never reads a local
|
|
@@ -3244,18 +3240,37 @@ export class KeyManager {
|
|
|
3244
3240
|
* keychain group, or Commons' identity host. It never falls back to the
|
|
3245
3241
|
* personal key.
|
|
3246
3242
|
*
|
|
3243
|
+
* With `opts.publicKey` the seed is derived from the key of that account
|
|
3244
|
+
* (personal, or `accountId`'s) whose public key it names: the current key
|
|
3245
|
+
* when it matches, else a RETIRED key Commons kept after a rotation or
|
|
3246
|
+
* replacement (`./retiredKeys`, ADR 0061 D7 as amended). Spending only: a
|
|
3247
|
+
* retired key never signs in. Never falls back to another key.
|
|
3248
|
+
*
|
|
3247
3249
|
* @param info Context/domain-binding label (distinct labels → independent seeds).
|
|
3248
3250
|
* @param opts.accountId An organization's account id, for its key.
|
|
3251
|
+
* @param opts.publicKey Selects one key of that account by its public key.
|
|
3249
3252
|
* @returns 32 bytes of derived keying material, or `null` on web / when no
|
|
3250
3253
|
* identity key is available to this app.
|
|
3251
3254
|
*/
|
|
3252
3255
|
static async deriveScopedSeed(
|
|
3253
3256
|
info: string,
|
|
3254
|
-
opts?: { accountId?: string },
|
|
3257
|
+
opts?: { accountId?: string; publicKey?: string },
|
|
3255
3258
|
): Promise<Uint8Array | null> {
|
|
3256
3259
|
if (isWebPlatform()) {
|
|
3257
3260
|
return null;
|
|
3258
3261
|
}
|
|
3262
|
+
if (opts?.publicKey !== undefined) {
|
|
3263
|
+
if (isAndroid() && !KeyManager.identitySignerStore) {
|
|
3264
|
+
const selected = KeyManager._selectorKey(opts.publicKey);
|
|
3265
|
+
if (!selected) return null;
|
|
3266
|
+
const bridge = await loadCommonsIdentityBridge();
|
|
3267
|
+
const seed =
|
|
3268
|
+
(await bridge?.deriveRetiredScopedSeed?.(info, selected, opts.accountId)) ?? null;
|
|
3269
|
+
return seed ? hexToBytes(seed) : null;
|
|
3270
|
+
}
|
|
3271
|
+
const selected = await KeyManager._selectedPrivateKey(opts.publicKey, opts.accountId);
|
|
3272
|
+
return selected ? deriveScopedSeedFromKey(selected, info) : null;
|
|
3273
|
+
}
|
|
3259
3274
|
if (opts?.accountId !== undefined) {
|
|
3260
3275
|
return KeyManager._deriveOrganizationScopedSeed(info, opts.accountId);
|
|
3261
3276
|
}
|
|
@@ -3307,7 +3322,8 @@ export class KeyManager {
|
|
|
3307
3322
|
* compressed public key for the scriptSig.
|
|
3308
3323
|
*
|
|
3309
3324
|
* Key source: as {@link deriveScopedSeed}, including `opts.accountId` for an
|
|
3310
|
-
* organization's key (ADR 0061 D6).
|
|
3325
|
+
* organization's key (ADR 0061 D6) and `opts.publicKey` for one key of the
|
|
3326
|
+
* account, current or retired. On Android outside Commons the signature
|
|
3311
3327
|
* comes from Commons, which allows it for the wallet app only.
|
|
3312
3328
|
*
|
|
3313
3329
|
* @returns the signature, or `null` on web / when no identity key is
|
|
@@ -3316,11 +3332,24 @@ export class KeyManager {
|
|
|
3316
3332
|
static async signSocialReceive(
|
|
3317
3333
|
index: number,
|
|
3318
3334
|
digest: string,
|
|
3319
|
-
opts?: { accountId?: string },
|
|
3335
|
+
opts?: { accountId?: string; publicKey?: string },
|
|
3320
3336
|
): Promise<CommonsSocialReceiveSignature | null> {
|
|
3321
3337
|
if (isWebPlatform()) {
|
|
3322
3338
|
return null;
|
|
3323
3339
|
}
|
|
3340
|
+
if (opts?.publicKey !== undefined) {
|
|
3341
|
+
if (isAndroid() && !KeyManager.identitySignerStore) {
|
|
3342
|
+
const selected = KeyManager._selectorKey(opts.publicKey);
|
|
3343
|
+
if (!selected) return null;
|
|
3344
|
+
const bridge = await loadCommonsIdentityBridge();
|
|
3345
|
+
return (
|
|
3346
|
+
(await bridge?.signRetiredSocialReceive?.(index, digest, selected, opts.accountId)) ??
|
|
3347
|
+
null
|
|
3348
|
+
);
|
|
3349
|
+
}
|
|
3350
|
+
const selected = await KeyManager._selectedPrivateKey(opts.publicKey, opts.accountId);
|
|
3351
|
+
return selected ? signSocialReceiveDigest(selected, index, digest) : null;
|
|
3352
|
+
}
|
|
3324
3353
|
const accountId = opts?.accountId;
|
|
3325
3354
|
if (accountId !== undefined) {
|
|
3326
3355
|
// An organization key (ADR 0061 D6); never the personal one in its place.
|
|
@@ -3345,6 +3374,40 @@ export class KeyManager {
|
|
|
3345
3374
|
return local ? signSocialReceiveDigest(local, index, digest) : null;
|
|
3346
3375
|
}
|
|
3347
3376
|
|
|
3377
|
+
/**
|
|
3378
|
+
* The private key of the account (personal, or `accountId`'s organization)
|
|
3379
|
+
* whose public key is `publicKey`, from this app's own storage: the current
|
|
3380
|
+
* key when it matches, else the retired ring. iOS and Commons only; `null`
|
|
3381
|
+
* for a key this device does not hold. Never another key in its place. @internal
|
|
3382
|
+
*/
|
|
3383
|
+
/**
|
|
3384
|
+
* The selector in the identity host's encoding (lowercase uncompressed), so a
|
|
3385
|
+
* compressed `#key-1` from a DID or a gateway row selects the same key. @internal
|
|
3386
|
+
*/
|
|
3387
|
+
private static _selectorKey(publicKey: string): string | null {
|
|
3388
|
+
return KeyManager.isValidPublicKey(publicKey) ? KeyManager.canonicalPublicKey(publicKey) : null;
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
private static async _selectedPrivateKey(
|
|
3392
|
+
publicKey: string,
|
|
3393
|
+
accountId?: string,
|
|
3394
|
+
): Promise<string | null> {
|
|
3395
|
+
if (retiredKeyScope({ accountId }) === null || !KeyManager.isValidPublicKey(publicKey)) {
|
|
3396
|
+
return null;
|
|
3397
|
+
}
|
|
3398
|
+
const wanted = KeyManager.canonicalPublicKey(publicKey);
|
|
3399
|
+
const current =
|
|
3400
|
+
accountId !== undefined
|
|
3401
|
+
? await getOrganizationPrivateKey(accountId)
|
|
3402
|
+
: isAndroid()
|
|
3403
|
+
? await KeyManager.getPrivateKey()
|
|
3404
|
+
: ((await KeyManager.getSharedPrivateKey()) ?? (await KeyManager.getPrivateKey()));
|
|
3405
|
+
if (current && KeyManager.canonicalPublicKey(KeyManager.derivePublicKey(current)) === wanted) {
|
|
3406
|
+
return current;
|
|
3407
|
+
}
|
|
3408
|
+
return getRetiredPrivateKey(wanted, accountId !== undefined ? { accountId } : undefined);
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3348
3411
|
/**
|
|
3349
3412
|
* Get a shortened version of the public key for display
|
|
3350
3413
|
* Format: first 8 chars...last 8 chars
|