@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/dist/cjs/api/identity.js
CHANGED
|
@@ -56,6 +56,7 @@ exports.signOwnChainRecord = signOwnChainRecord;
|
|
|
56
56
|
* AEAD, signatures) load on first use, never with the client itself.
|
|
57
57
|
*/
|
|
58
58
|
const client_1 = require("@oxy.so/contracts/client");
|
|
59
|
+
const client_2 = require("@oxy.so/contracts/client");
|
|
59
60
|
const platform_1 = require("../utils/platform");
|
|
60
61
|
const logger_1 = require("../logger");
|
|
61
62
|
/** Short-TTL read cache for public identity reads. */
|
|
@@ -196,7 +197,7 @@ class IdentityApi {
|
|
|
196
197
|
if (!userId) {
|
|
197
198
|
throw new Error('No authenticated user — sign in before rotating your key.');
|
|
198
199
|
}
|
|
199
|
-
const { KeyManager, RecoveryPhraseService, SignatureService, signMessage } = await Promise.resolve().then(() => __importStar(require('../crypto/index')));
|
|
200
|
+
const { KeyManager, RecoveryPhraseService, SignatureService, signMessage, retireIdentityKey } = await Promise.resolve().then(() => __importStar(require('../crypto/index')));
|
|
200
201
|
// 1. The NEW identity (in memory only). The UI may pre-derive + pre-show
|
|
201
202
|
// it and pass it back here so the phrase shown === the phrase committed.
|
|
202
203
|
const pending = options.pendingIdentity ?? (await RecoveryPhraseService.derivePendingIdentity());
|
|
@@ -204,6 +205,7 @@ class IdentityApi {
|
|
|
204
205
|
// 2. Resolve the OLD signing capability from the chosen proof mode.
|
|
205
206
|
let oldPublicKey;
|
|
206
207
|
let signWithOldKey;
|
|
208
|
+
let oldPrivateKeyForRing;
|
|
207
209
|
if (options.proof === 'phrase') {
|
|
208
210
|
const phrase = options.phrase?.trim();
|
|
209
211
|
if (!phrase) {
|
|
@@ -212,6 +214,7 @@ class IdentityApi {
|
|
|
212
214
|
const oldPrivateKey = await RecoveryPhraseService.derivePrivateKeyFromPhrase(phrase);
|
|
213
215
|
oldPublicKey = KeyManager.derivePublicKey(oldPrivateKey);
|
|
214
216
|
signWithOldKey = (message) => signMessage(message, oldPrivateKey);
|
|
217
|
+
oldPrivateKeyForRing = oldPrivateKey;
|
|
215
218
|
}
|
|
216
219
|
else {
|
|
217
220
|
const currentPublicKey = await KeyManager.getPublicKey();
|
|
@@ -220,6 +223,25 @@ class IdentityApi {
|
|
|
220
223
|
}
|
|
221
224
|
oldPublicKey = currentPublicKey;
|
|
222
225
|
signWithOldKey = (message) => SignatureService.sign(message);
|
|
226
|
+
oldPrivateKeyForRing = (0, platform_1.isWeb)() ? null : await KeyManager.getPrivateKey();
|
|
227
|
+
}
|
|
228
|
+
// 2b. Keep the key being replaced in the retired ring BEFORE anything
|
|
229
|
+
// changes (oxy ADR 0061 D7 as amended, Peable ADR 0002): coins keep
|
|
230
|
+
// arriving under it after the swap, and the Peable wallet spends them
|
|
231
|
+
// through the `publicKey` selector of `deriveScopedSeed` /
|
|
232
|
+
// `signSocialReceive`. A device that cannot keep it does not rotate:
|
|
233
|
+
// the phrase would become the only way to those coins. Native only —
|
|
234
|
+
// on web no key is stored. Idempotent, so a retried rotation is fine.
|
|
235
|
+
if (!(0, platform_1.isWeb)()) {
|
|
236
|
+
try {
|
|
237
|
+
if (!oldPrivateKeyForRing)
|
|
238
|
+
throw new Error('no readable private key');
|
|
239
|
+
await retireIdentityKey({ privateKey: oldPrivateKeyForRing, publicKey: oldPublicKey });
|
|
240
|
+
}
|
|
241
|
+
catch (retainError) {
|
|
242
|
+
logger_1.logger.warn('Could not keep the current key as a retired key; not rotating.', { component: 'oxy.identity', method: 'rotateKey' }, retainError);
|
|
243
|
+
throw new Error('Your current key could not be kept on this device, so it was not rotated. Nothing changed.');
|
|
244
|
+
}
|
|
223
245
|
}
|
|
224
246
|
// 3. Request a single-use rotate_key challenge (bearer).
|
|
225
247
|
const { challenge } = await this.ctx.request('POST', '/auth/rotate/challenge', undefined, {
|
|
@@ -376,6 +398,23 @@ class IdentityApi {
|
|
|
376
398
|
keys.push(`GET:/u/${encodeURIComponent(userId)}/did.json`);
|
|
377
399
|
this.ctx.http.invalidateCache({ keys, prefixes: ['GET:/users/me'] });
|
|
378
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* The signed-in personal account's current root (`null` without one) and
|
|
403
|
+
* every root a rotation replaced, oldest first, all canonical (uncompressed,
|
|
404
|
+
* lowercase). Commons reads it to accept a previous recovery phrase only when
|
|
405
|
+
* it derives a key that WAS this account's root. A retired root is never any
|
|
406
|
+
* account's again. First-party only; a non-personal account is refused
|
|
407
|
+
* (`IDENTITY_NOT_PERSONAL_ACCOUNT`).
|
|
408
|
+
*/
|
|
409
|
+
async keyHistory() {
|
|
410
|
+
const res = await this.ctx.request('GET', '/identity/key-history', undefined, {
|
|
411
|
+
cache: false,
|
|
412
|
+
});
|
|
413
|
+
const parsed = (0, client_1.checkContract)(client_2.identityKeyHistoryCheck, res);
|
|
414
|
+
if (!parsed)
|
|
415
|
+
throw new Error('identity/key-history returned an unexpected response shape');
|
|
416
|
+
return parsed;
|
|
417
|
+
}
|
|
379
418
|
}
|
|
380
419
|
exports.IdentityApi = IdentityApi;
|
|
381
420
|
/** `oxy.identity.links` — joining Commons' root to an account (ADR 0029 D3). */
|
package/dist/cjs/crypto/index.js
CHANGED
|
@@ -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.
|
|
15
|
+
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.retiredKeyFromPhrase = exports.syncRetiredKeys = exports.forgetRetiredIdentityKeys = exports.forgetRetiredIdentityKey = exports.listRetiredIdentityKeys = exports.retireIdentityKey = 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 = exports.solveRegistrationPow = exports.deriveIdentityLinkCode = exports.signIdentityProof = exports.digestIdentityPayload = exports.authChallengeDigest = exports.signSocialReceiveDigest = 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,32 @@ 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; } });
|
|
58
|
+
// Retired identity keys: kept for spending after a rotation or replacement
|
|
59
|
+
// (docs/adr/0061-organization-identity-key.md D7 as amended 2026-11-06)
|
|
60
|
+
var retiredKeys_1 = require("./retiredKeys");
|
|
61
|
+
Object.defineProperty(exports, "retireIdentityKey", { enumerable: true, get: function () { return retiredKeys_1.retireIdentityKey; } });
|
|
62
|
+
Object.defineProperty(exports, "listRetiredIdentityKeys", { enumerable: true, get: function () { return retiredKeys_1.listRetiredIdentityKeys; } });
|
|
63
|
+
Object.defineProperty(exports, "forgetRetiredIdentityKey", { enumerable: true, get: function () { return retiredKeys_1.forgetRetiredIdentityKey; } });
|
|
64
|
+
Object.defineProperty(exports, "forgetRetiredIdentityKeys", { enumerable: true, get: function () { return retiredKeys_1.forgetRetiredIdentityKeys; } });
|
|
65
|
+
Object.defineProperty(exports, "syncRetiredKeys", { enumerable: true, get: function () { return retiredKeys_1.syncRetiredKeys; } });
|
|
66
|
+
Object.defineProperty(exports, "retiredKeyFromPhrase", { enumerable: true, get: function () { return retiredKeys_1.retiredKeyFromPhrase; } });
|
|
40
67
|
var signatureService_1 = require("./signatureService");
|
|
41
68
|
Object.defineProperty(exports, "SignatureService", { enumerable: true, get: function () { return signatureService_1.SignatureService; } });
|
|
42
69
|
var recoveryPhrase_1 = require("./recoveryPhrase");
|
|
@@ -47,6 +47,10 @@ 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 retiredKeys_1 = require("./retiredKeys");
|
|
51
|
+
const keychainAccessGroups_1 = require("../utils/keychainAccessGroups");
|
|
52
|
+
const keychainGroupMigration_1 = require("./keychainGroupMigration");
|
|
53
|
+
const keychainItems_1 = require("./keychainItems");
|
|
50
54
|
const identityMarker_1 = require("./identityMarker");
|
|
51
55
|
const deviceBackup_1 = require("./deviceBackup");
|
|
52
56
|
/**
|
|
@@ -107,16 +111,6 @@ class IdentityUnavailableError extends Error {
|
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
113
|
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
114
|
/**
|
|
121
115
|
* v2 identity slot layout — blast-radius isolation.
|
|
122
116
|
*
|
|
@@ -132,15 +126,8 @@ const STORAGE_KEYS = {
|
|
|
132
126
|
* Migration from the legacy layout is lazy + verify-before-delete — see
|
|
133
127
|
* {@link KeyManager._runSlotMigration}.
|
|
134
128
|
*/
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
};
|
|
129
|
+
// The names (V2_PRIMARY_KEYCHAIN_SERVICE, V2_BACKUP_KEYCHAIN_SERVICE,
|
|
130
|
+
// V2_STORAGE_KEYS) live in `./keychainItems`, beside every other identity item.
|
|
144
131
|
/**
|
|
145
132
|
* Dedicated keychain slot for the recovery mnemonic (the 12-word phrase).
|
|
146
133
|
*
|
|
@@ -157,8 +144,7 @@ const V2_STORAGE_KEYS = {
|
|
|
157
144
|
* lives ONLY in this slot: it is never mirrored into the identity marker,
|
|
158
145
|
* {@link KeyManager.getIdentityStatus}, logs, or any exported bundle.
|
|
159
146
|
*/
|
|
160
|
-
|
|
161
|
-
const RECOVERY_MNEMONIC_STORAGE_KEY = 'oxy_identity_mnemonic_v1';
|
|
147
|
+
// RECOVERY_MNEMONIC_KEYCHAIN_SERVICE / RECOVERY_MNEMONIC_STORAGE_KEY: `./keychainItems`.
|
|
162
148
|
/**
|
|
163
149
|
* Advisory AsyncStorage fast-path flag: set once the v2 slots own the identity.
|
|
164
150
|
* Re-derivable (its loss just re-runs the cheap slot check), so it lives in
|
|
@@ -176,29 +162,23 @@ const SLOTS_MIGRATED_FLAG_KEY = 'oxy_identity_slots_migrated_v2';
|
|
|
176
162
|
*/
|
|
177
163
|
const ROTATION_PENDING_KEY = 'oxy_identity_rotation_pending_v1';
|
|
178
164
|
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,
|
|
165
|
+
primaryService: keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE,
|
|
166
|
+
primaryPrivateKeyName: keychainItems_1.V2_STORAGE_KEYS.PRIVATE_KEY,
|
|
167
|
+
primaryPublicKeyName: keychainItems_1.V2_STORAGE_KEYS.PUBLIC_KEY,
|
|
168
|
+
backupService: keychainItems_1.V2_BACKUP_KEYCHAIN_SERVICE,
|
|
169
|
+
backupPrivateKeyName: keychainItems_1.V2_STORAGE_KEYS.BACKUP_PRIVATE_KEY,
|
|
170
|
+
backupPublicKeyName: keychainItems_1.V2_STORAGE_KEYS.BACKUP_PUBLIC_KEY,
|
|
171
|
+
backupTimestampName: keychainItems_1.V2_STORAGE_KEYS.BACKUP_TIMESTAMP,
|
|
186
172
|
};
|
|
187
173
|
const LEGACY_SLOT_LAYOUT = {
|
|
188
174
|
primaryService: undefined,
|
|
189
|
-
primaryPrivateKeyName: STORAGE_KEYS.PRIVATE_KEY,
|
|
190
|
-
primaryPublicKeyName: STORAGE_KEYS.PUBLIC_KEY,
|
|
175
|
+
primaryPrivateKeyName: keychainItems_1.STORAGE_KEYS.PRIVATE_KEY,
|
|
176
|
+
primaryPublicKeyName: keychainItems_1.STORAGE_KEYS.PUBLIC_KEY,
|
|
191
177
|
backupService: undefined,
|
|
192
|
-
backupPrivateKeyName: STORAGE_KEYS.BACKUP_PRIVATE_KEY,
|
|
193
|
-
backupPublicKeyName: STORAGE_KEYS.BACKUP_PUBLIC_KEY,
|
|
194
|
-
backupTimestampName: STORAGE_KEYS.BACKUP_TIMESTAMP,
|
|
178
|
+
backupPrivateKeyName: keychainItems_1.STORAGE_KEYS.BACKUP_PRIVATE_KEY,
|
|
179
|
+
backupPublicKeyName: keychainItems_1.STORAGE_KEYS.BACKUP_PUBLIC_KEY,
|
|
180
|
+
backupTimestampName: keychainItems_1.STORAGE_KEYS.BACKUP_TIMESTAMP,
|
|
195
181
|
};
|
|
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
182
|
/**
|
|
203
183
|
* Initialize React Native specific modules
|
|
204
184
|
*
|
|
@@ -208,7 +188,7 @@ const IOS_KEYCHAIN_GROUP = 'group.so.oxy.shared';
|
|
|
208
188
|
* `expo-secure-store`; off RN it throws (and is never called because every
|
|
209
189
|
* caller is gated by `isWebPlatform()` / native-only paths).
|
|
210
190
|
*/
|
|
211
|
-
async function
|
|
191
|
+
async function loadRawSecureStore() {
|
|
212
192
|
try {
|
|
213
193
|
return await (0, protocol_1.loadSecureStore)();
|
|
214
194
|
}
|
|
@@ -217,6 +197,29 @@ async function initSecureStore() {
|
|
|
217
197
|
throw new Error(`Failed to load expo-secure-store: ${errorMessage}. Make sure expo-secure-store is installed and properly configured.`);
|
|
218
198
|
}
|
|
219
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* The secure store routed to the keychain group of `item`
|
|
202
|
+
* (`../utils/keychainAccessGroups`): on iOS reads walk the item's group then
|
|
203
|
+
* the pre-split shared group, writes carry the item's group and THROW in an app
|
|
204
|
+
* that may not hold it, and in Commons every access first awaits the
|
|
205
|
+
* access-group migration. Android and web are unchanged (`ungrouped`).
|
|
206
|
+
*
|
|
207
|
+
* Throws when the store cannot load or the keychain cannot say which groups
|
|
208
|
+
* this app holds; every caller already treats a throw here as "storage
|
|
209
|
+
* unavailable".
|
|
210
|
+
*/
|
|
211
|
+
async function keychainItemStore(item) {
|
|
212
|
+
const raw = await loadRawSecureStore();
|
|
213
|
+
const access = await (0, keychainAccessGroups_1.resolveKeychainAccess)();
|
|
214
|
+
if (access.mode === 'grouped' && access.role === 'holder') {
|
|
215
|
+
await (0, keychainGroupMigration_1.ensureKeychainGroupsMigrated)();
|
|
216
|
+
}
|
|
217
|
+
return (0, keychainAccessGroups_1.groupedKeychainStore)(raw, access, item);
|
|
218
|
+
}
|
|
219
|
+
/** The store for the identity slots (primary, backup, legacy, phrase): Commons only on iOS. */
|
|
220
|
+
function initSecureStore() {
|
|
221
|
+
return keychainItemStore('identity-secret');
|
|
222
|
+
}
|
|
220
223
|
/**
|
|
221
224
|
* Check if we're on web platform
|
|
222
225
|
* Identity storage is only available on native platforms (iOS/Android)
|
|
@@ -466,8 +469,8 @@ class KeyManager {
|
|
|
466
469
|
let v2Private;
|
|
467
470
|
let v2Public;
|
|
468
471
|
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));
|
|
472
|
+
v2Private = await store.getItemAsync(keychainItems_1.V2_STORAGE_KEYS.PRIVATE_KEY, KeyManager._slotOpts(keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE));
|
|
473
|
+
v2Public = await store.getItemAsync(keychainItems_1.V2_STORAGE_KEYS.PUBLIC_KEY, KeyManager._slotOpts(keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE));
|
|
471
474
|
}
|
|
472
475
|
catch (error) {
|
|
473
476
|
return { mode: 'deferred', cause: error };
|
|
@@ -490,8 +493,8 @@ class KeyManager {
|
|
|
490
493
|
let legacyPrivate;
|
|
491
494
|
let legacyPublic;
|
|
492
495
|
try {
|
|
493
|
-
legacyPrivate = await store.getItemAsync(STORAGE_KEYS.PRIVATE_KEY);
|
|
494
|
-
legacyPublic = await store.getItemAsync(STORAGE_KEYS.PUBLIC_KEY);
|
|
496
|
+
legacyPrivate = await store.getItemAsync(keychainItems_1.STORAGE_KEYS.PRIVATE_KEY);
|
|
497
|
+
legacyPublic = await store.getItemAsync(keychainItems_1.STORAGE_KEYS.PUBLIC_KEY);
|
|
495
498
|
}
|
|
496
499
|
catch (error) {
|
|
497
500
|
return { mode: 'deferred', cause: error };
|
|
@@ -505,10 +508,10 @@ class KeyManager {
|
|
|
505
508
|
const canonicalPrivate = KeyManager.canonicalPrivateKey(legacyPrivate);
|
|
506
509
|
const canonicalPublic = legacyPublic.toLowerCase();
|
|
507
510
|
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));
|
|
511
|
+
await store.setItemAsync(keychainItems_1.V2_STORAGE_KEYS.PUBLIC_KEY, canonicalPublic, KeyManager._slotOpts(keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE));
|
|
512
|
+
await store.setItemAsync(keychainItems_1.V2_STORAGE_KEYS.PRIVATE_KEY, canonicalPrivate, KeyManager._privateWriteOpts(store, keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE));
|
|
513
|
+
const readBackPrivate = await store.getItemAsync(keychainItems_1.V2_STORAGE_KEYS.PRIVATE_KEY, KeyManager._slotOpts(keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE));
|
|
514
|
+
const readBackPublic = await store.getItemAsync(keychainItems_1.V2_STORAGE_KEYS.PUBLIC_KEY, KeyManager._slotOpts(keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE));
|
|
512
515
|
const verified = readBackPrivate?.toLowerCase() === canonicalPrivate &&
|
|
513
516
|
readBackPublic?.toLowerCase() === canonicalPublic &&
|
|
514
517
|
KeyManager._isHealthyPair(readBackPrivate, readBackPublic);
|
|
@@ -543,8 +546,8 @@ class KeyManager {
|
|
|
543
546
|
let backupPrivate = null;
|
|
544
547
|
let backupPublic = null;
|
|
545
548
|
try {
|
|
546
|
-
backupPrivate = await store.getItemAsync(STORAGE_KEYS.BACKUP_PRIVATE_KEY);
|
|
547
|
-
backupPublic = await store.getItemAsync(STORAGE_KEYS.BACKUP_PUBLIC_KEY);
|
|
549
|
+
backupPrivate = await store.getItemAsync(keychainItems_1.STORAGE_KEYS.BACKUP_PRIVATE_KEY);
|
|
550
|
+
backupPublic = await store.getItemAsync(keychainItems_1.STORAGE_KEYS.BACKUP_PUBLIC_KEY);
|
|
548
551
|
}
|
|
549
552
|
catch (error) {
|
|
550
553
|
if ((0, logger_1.isDev)()) {
|
|
@@ -563,9 +566,9 @@ class KeyManager {
|
|
|
563
566
|
seedPrivate = primaryPrivate;
|
|
564
567
|
seedPublic = primaryPublic;
|
|
565
568
|
}
|
|
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));
|
|
569
|
+
await store.setItemAsync(keychainItems_1.V2_STORAGE_KEYS.BACKUP_PUBLIC_KEY, seedPublic, KeyManager._slotOpts(keychainItems_1.V2_BACKUP_KEYCHAIN_SERVICE));
|
|
570
|
+
await store.setItemAsync(keychainItems_1.V2_STORAGE_KEYS.BACKUP_PRIVATE_KEY, seedPrivate, KeyManager._privateWriteOpts(store, keychainItems_1.V2_BACKUP_KEYCHAIN_SERVICE));
|
|
571
|
+
await store.setItemAsync(keychainItems_1.V2_STORAGE_KEYS.BACKUP_TIMESTAMP, Date.now().toString(), KeyManager._slotOpts(keychainItems_1.V2_BACKUP_KEYCHAIN_SERVICE));
|
|
569
572
|
}
|
|
570
573
|
catch (error) {
|
|
571
574
|
logger_1.logger.warn('Failed to migrate identity backup slot to v2 (non-fatal)', { component: 'KeyManager' }, error);
|
|
@@ -583,23 +586,23 @@ class KeyManager {
|
|
|
583
586
|
}
|
|
584
587
|
}
|
|
585
588
|
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);
|
|
589
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.V2_STORAGE_KEYS.PRIVATE_KEY, keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE);
|
|
590
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.V2_STORAGE_KEYS.PUBLIC_KEY, keychainItems_1.V2_PRIMARY_KEYCHAIN_SERVICE);
|
|
588
591
|
}
|
|
589
592
|
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);
|
|
593
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.PRIVATE_KEY);
|
|
594
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.PUBLIC_KEY);
|
|
595
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.BACKUP_PRIVATE_KEY);
|
|
596
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.BACKUP_PUBLIC_KEY);
|
|
597
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.BACKUP_TIMESTAMP);
|
|
595
598
|
}
|
|
596
599
|
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);
|
|
600
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.V2_STORAGE_KEYS.BACKUP_PRIVATE_KEY, keychainItems_1.V2_BACKUP_KEYCHAIN_SERVICE);
|
|
601
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.V2_STORAGE_KEYS.BACKUP_PUBLIC_KEY, keychainItems_1.V2_BACKUP_KEYCHAIN_SERVICE);
|
|
602
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.V2_STORAGE_KEYS.BACKUP_TIMESTAMP, keychainItems_1.V2_BACKUP_KEYCHAIN_SERVICE);
|
|
603
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.BACKUP_PRIVATE_KEY);
|
|
604
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.BACKUP_PUBLIC_KEY);
|
|
605
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.BACKUP_TIMESTAMP);
|
|
603
606
|
}
|
|
604
607
|
/**
|
|
605
608
|
* Clear the iOS keychain-group copy of the identity (force-delete only) so a
|
|
@@ -607,12 +610,12 @@ class KeyManager {
|
|
|
607
610
|
* rung. Best-effort — the shared slot is a redundant convenience copy. The
|
|
608
611
|
* Android signer store is cleared on every delete, in {@link deleteIdentity}.
|
|
609
612
|
*/
|
|
610
|
-
static async _clearSharedSlot(
|
|
613
|
+
static async _clearSharedSlot(_identityStore) {
|
|
611
614
|
try {
|
|
612
615
|
if ((0, platform_1.isIOS)()) {
|
|
613
|
-
|
|
614
|
-
await
|
|
615
|
-
await
|
|
616
|
+
// The shared slot lives in two groups of its own, not the identity slots'.
|
|
617
|
+
await (await keychainItemStore('spending-key')).deleteItemAsync(keychainItems_1.STORAGE_KEYS.SHARED_PRIVATE_KEY);
|
|
618
|
+
await (await keychainItemStore('shared')).deleteItemAsync(keychainItems_1.STORAGE_KEYS.SHARED_PUBLIC_KEY);
|
|
616
619
|
}
|
|
617
620
|
KeyManager.invalidateSharedCache();
|
|
618
621
|
}
|
|
@@ -679,6 +682,20 @@ class KeyManager {
|
|
|
679
682
|
KeyManager.cachedSharedPublicKey = null;
|
|
680
683
|
KeyManager.cachedHasSharedIdentity = null;
|
|
681
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* iOS: write the personal key's cross-app slot — the private key to the
|
|
687
|
+
* wallet group (Commons and the Peable wallet), the public key to the shared
|
|
688
|
+
* group (every Oxy app). Throws in any app that may not hold the private key.
|
|
689
|
+
* @internal
|
|
690
|
+
*/
|
|
691
|
+
static async _writeIosSharedSlot(privateKey, publicKey) {
|
|
692
|
+
const privateStore = await keychainItemStore('spending-key');
|
|
693
|
+
const privateOpts = {
|
|
694
|
+
keychainAccessible: privateStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
695
|
+
};
|
|
696
|
+
await privateStore.setItemAsync(keychainItems_1.STORAGE_KEYS.SHARED_PRIVATE_KEY, privateKey, privateOpts);
|
|
697
|
+
await (await keychainItemStore('shared')).setItemAsync(keychainItems_1.STORAGE_KEYS.SHARED_PUBLIC_KEY, publicKey, {});
|
|
698
|
+
}
|
|
682
699
|
/**
|
|
683
700
|
* Lowercase and pad to canonical 64-hex-char form.
|
|
684
701
|
*
|
|
@@ -714,8 +731,10 @@ class KeyManager {
|
|
|
714
731
|
// ==================== SHARED IDENTITY METHODS ====================
|
|
715
732
|
// One Oxy identity across every Oxy app on a device.
|
|
716
733
|
//
|
|
717
|
-
// iOS:
|
|
718
|
-
//
|
|
734
|
+
// iOS: keychain access groups (docs/identity/ios-keychain-access-groups.md).
|
|
735
|
+
// The private key is in `<TEAM>.so.oxy.wallet` (Commons and the Peable
|
|
736
|
+
// wallet only), the public key in `<TEAM>.group.so.oxy.shared`, which every
|
|
737
|
+
// Oxy app reads. Every other app signs in through Commons (ADR 0029).
|
|
719
738
|
// Android: every app has its own UID, so nothing is shared through storage.
|
|
720
739
|
// Commons is the ONLY holder of the private key. It keeps the identity
|
|
721
740
|
// signer store (`setIdentitySignerStore`) for its identity host, and every
|
|
@@ -740,21 +759,11 @@ class KeyManager {
|
|
|
740
759
|
}
|
|
741
760
|
const { privateKey, publicKey } = await KeyManager.generateKeyPair();
|
|
742
761
|
if ((0, platform_1.isIOS)()) {
|
|
743
|
-
const store = await initSecureStore();
|
|
744
|
-
// Note: keychainAccessGroup requires Keychain Sharing capability in Xcode
|
|
745
762
|
try {
|
|
746
|
-
|
|
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);
|
|
763
|
+
await KeyManager._writeIosSharedSlot(privateKey, publicKey);
|
|
755
764
|
}
|
|
756
765
|
catch (error) {
|
|
757
|
-
throw new Error(`Failed to create shared identity on iOS.
|
|
766
|
+
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
767
|
}
|
|
759
768
|
}
|
|
760
769
|
else if ((0, platform_1.isAndroid)()) {
|
|
@@ -794,9 +803,8 @@ class KeyManager {
|
|
|
794
803
|
try {
|
|
795
804
|
let publicKey = null;
|
|
796
805
|
if ((0, platform_1.isIOS)()) {
|
|
797
|
-
const store = await
|
|
798
|
-
|
|
799
|
-
publicKey = await store.getItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, opts);
|
|
806
|
+
const store = await keychainItemStore('shared');
|
|
807
|
+
publicKey = await store.getItemAsync(keychainItems_1.STORAGE_KEYS.SHARED_PUBLIC_KEY, {});
|
|
800
808
|
}
|
|
801
809
|
else if ((0, platform_1.isAndroid)()) {
|
|
802
810
|
const signer = KeyManager.identitySignerStore;
|
|
@@ -836,9 +844,10 @@ class KeyManager {
|
|
|
836
844
|
return null;
|
|
837
845
|
}
|
|
838
846
|
try {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
847
|
+
// The wallet group: Commons and the Peable wallet. Every other app's plan
|
|
848
|
+
// is empty, so this is `null` there without touching the keychain.
|
|
849
|
+
const store = await keychainItemStore('spending-key');
|
|
850
|
+
return await store.getItemAsync(keychainItems_1.STORAGE_KEYS.SHARED_PRIVATE_KEY, {});
|
|
842
851
|
}
|
|
843
852
|
catch (error) {
|
|
844
853
|
if ((0, logger_1.isDev)()) {
|
|
@@ -932,16 +941,7 @@ class KeyManager {
|
|
|
932
941
|
const canonicalPrivate = KeyManager.canonicalPrivateKey(privateKey);
|
|
933
942
|
const publicKey = (0, secp256k1_1.deriveSecp256k1PublicKey)(canonicalPrivate);
|
|
934
943
|
if ((0, platform_1.isIOS)()) {
|
|
935
|
-
|
|
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);
|
|
944
|
+
await KeyManager._writeIosSharedSlot(canonicalPrivate, publicKey);
|
|
945
945
|
}
|
|
946
946
|
else if ((0, platform_1.isAndroid)()) {
|
|
947
947
|
await KeyManager._writeIdentitySigner(canonicalPrivate, publicKey);
|
|
@@ -1778,7 +1778,7 @@ class KeyManager {
|
|
|
1778
1778
|
}
|
|
1779
1779
|
try {
|
|
1780
1780
|
const store = await initSecureStore();
|
|
1781
|
-
await store.setItemAsync(RECOVERY_MNEMONIC_STORAGE_KEY, mnemonic, KeyManager._privateWriteOpts(store, RECOVERY_MNEMONIC_KEYCHAIN_SERVICE));
|
|
1781
|
+
await store.setItemAsync(keychainItems_1.RECOVERY_MNEMONIC_STORAGE_KEY, mnemonic, KeyManager._privateWriteOpts(store, keychainItems_1.RECOVERY_MNEMONIC_KEYCHAIN_SERVICE));
|
|
1782
1782
|
}
|
|
1783
1783
|
catch (error) {
|
|
1784
1784
|
if ((0, logger_1.isDev)()) {
|
|
@@ -1813,7 +1813,7 @@ class KeyManager {
|
|
|
1813
1813
|
}
|
|
1814
1814
|
try {
|
|
1815
1815
|
const store = await initSecureStore();
|
|
1816
|
-
return await store.getItemAsync(RECOVERY_MNEMONIC_STORAGE_KEY, KeyManager._slotOpts(RECOVERY_MNEMONIC_KEYCHAIN_SERVICE));
|
|
1816
|
+
return await store.getItemAsync(keychainItems_1.RECOVERY_MNEMONIC_STORAGE_KEY, KeyManager._slotOpts(keychainItems_1.RECOVERY_MNEMONIC_KEYCHAIN_SERVICE));
|
|
1817
1817
|
}
|
|
1818
1818
|
catch (error) {
|
|
1819
1819
|
if ((0, logger_1.isDev)()) {
|
|
@@ -1859,7 +1859,7 @@ class KeyManager {
|
|
|
1859
1859
|
return; // Identity storage is only available on native platforms
|
|
1860
1860
|
}
|
|
1861
1861
|
const store = await initSecureStore();
|
|
1862
|
-
await KeyManager._bestEffortDelete(store, RECOVERY_MNEMONIC_STORAGE_KEY, RECOVERY_MNEMONIC_KEYCHAIN_SERVICE);
|
|
1862
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.RECOVERY_MNEMONIC_STORAGE_KEY, keychainItems_1.RECOVERY_MNEMONIC_KEYCHAIN_SERVICE);
|
|
1863
1863
|
}
|
|
1864
1864
|
/**
|
|
1865
1865
|
* Check if a complete, parseable identity exists on this device.
|
|
@@ -2078,8 +2078,8 @@ class KeyManager {
|
|
|
2078
2078
|
await store.deleteItemAsync(layout.primaryPublicKeyName, readOpts);
|
|
2079
2079
|
}
|
|
2080
2080
|
await KeyManager._bestEffortDeleteV2Primary(store);
|
|
2081
|
-
await KeyManager._bestEffortDelete(store, STORAGE_KEYS.PRIVATE_KEY);
|
|
2082
|
-
await KeyManager._bestEffortDelete(store, STORAGE_KEYS.PUBLIC_KEY);
|
|
2081
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.PRIVATE_KEY);
|
|
2082
|
+
await KeyManager._bestEffortDelete(store, keychainItems_1.STORAGE_KEYS.PUBLIC_KEY);
|
|
2083
2083
|
// Always drop the stored recovery mnemonic — it is scoped to the identity
|
|
2084
2084
|
// being deleted, so a leftover would let Settings reveal a stale phrase for
|
|
2085
2085
|
// an identity that no longer exists (or a DIFFERENT one after re-onboarding).
|
|
@@ -2089,6 +2089,13 @@ class KeyManager {
|
|
|
2089
2089
|
if (force) {
|
|
2090
2090
|
await KeyManager._bestEffortDeleteBackupsAllGenerations(store);
|
|
2091
2091
|
await KeyManager._clearSharedSlot(store);
|
|
2092
|
+
// The person's retired keys go with the identity they belonged to.
|
|
2093
|
+
try {
|
|
2094
|
+
await (0, retiredKeys_1.forgetRetiredIdentityKeys)();
|
|
2095
|
+
}
|
|
2096
|
+
catch (error) {
|
|
2097
|
+
logger_1.logger.warn('Failed to clear retired keys during delete', { component: 'KeyManager' }, error);
|
|
2098
|
+
}
|
|
2092
2099
|
}
|
|
2093
2100
|
// The device backup goes on EVERY delete, forced or not: it is restored
|
|
2094
2101
|
// silently, so leaving it would bring a deleted identity back on the next
|
|
@@ -2606,7 +2613,8 @@ class KeyManager {
|
|
|
2606
2613
|
* `HDKey.fromMasterSeed` and never touches the identity key.
|
|
2607
2614
|
*
|
|
2608
2615
|
* Key source (native only):
|
|
2609
|
-
* - iOS: the shared
|
|
2616
|
+
* - iOS: the shared slot's private key in the wallet keychain group (Commons
|
|
2617
|
+
* and the Peable wallet only; `null` in every other app), then this device's
|
|
2610
2618
|
* primary identity.
|
|
2611
2619
|
* - Android: Commons (the one app with a signer store) uses its own primary
|
|
2612
2620
|
* identity; every other app asks Commons over IPC and never reads a local
|
|
@@ -2620,8 +2628,15 @@ class KeyManager {
|
|
|
2620
2628
|
* keychain group, or Commons' identity host. It never falls back to the
|
|
2621
2629
|
* personal key.
|
|
2622
2630
|
*
|
|
2631
|
+
* With `opts.publicKey` the seed is derived from the key of that account
|
|
2632
|
+
* (personal, or `accountId`'s) whose public key it names: the current key
|
|
2633
|
+
* when it matches, else a RETIRED key Commons kept after a rotation or
|
|
2634
|
+
* replacement (`./retiredKeys`, ADR 0061 D7 as amended). Spending only: a
|
|
2635
|
+
* retired key never signs in. Never falls back to another key.
|
|
2636
|
+
*
|
|
2623
2637
|
* @param info Context/domain-binding label (distinct labels → independent seeds).
|
|
2624
2638
|
* @param opts.accountId An organization's account id, for its key.
|
|
2639
|
+
* @param opts.publicKey Selects one key of that account by its public key.
|
|
2625
2640
|
* @returns 32 bytes of derived keying material, or `null` on web / when no
|
|
2626
2641
|
* identity key is available to this app.
|
|
2627
2642
|
*/
|
|
@@ -2629,6 +2644,18 @@ class KeyManager {
|
|
|
2629
2644
|
if (isWebPlatform()) {
|
|
2630
2645
|
return null;
|
|
2631
2646
|
}
|
|
2647
|
+
if (opts?.publicKey !== undefined) {
|
|
2648
|
+
if ((0, platform_1.isAndroid)() && !KeyManager.identitySignerStore) {
|
|
2649
|
+
const selected = KeyManager._selectorKey(opts.publicKey);
|
|
2650
|
+
if (!selected)
|
|
2651
|
+
return null;
|
|
2652
|
+
const bridge = await (0, protocol_1.loadCommonsIdentityBridge)();
|
|
2653
|
+
const seed = (await bridge?.deriveRetiredScopedSeed?.(info, selected, opts.accountId)) ?? null;
|
|
2654
|
+
return seed ? hexToBytes(seed) : null;
|
|
2655
|
+
}
|
|
2656
|
+
const selected = await KeyManager._selectedPrivateKey(opts.publicKey, opts.accountId);
|
|
2657
|
+
return selected ? (0, identityDerivations_1.deriveScopedSeedFromKey)(selected, info) : null;
|
|
2658
|
+
}
|
|
2632
2659
|
if (opts?.accountId !== undefined) {
|
|
2633
2660
|
return KeyManager._deriveOrganizationScopedSeed(info, opts.accountId);
|
|
2634
2661
|
}
|
|
@@ -2674,7 +2701,8 @@ class KeyManager {
|
|
|
2674
2701
|
* compressed public key for the scriptSig.
|
|
2675
2702
|
*
|
|
2676
2703
|
* Key source: as {@link deriveScopedSeed}, including `opts.accountId` for an
|
|
2677
|
-
* organization's key (ADR 0061 D6).
|
|
2704
|
+
* organization's key (ADR 0061 D6) and `opts.publicKey` for one key of the
|
|
2705
|
+
* account, current or retired. On Android outside Commons the signature
|
|
2678
2706
|
* comes from Commons, which allows it for the wallet app only.
|
|
2679
2707
|
*
|
|
2680
2708
|
* @returns the signature, or `null` on web / when no identity key is
|
|
@@ -2684,6 +2712,18 @@ class KeyManager {
|
|
|
2684
2712
|
if (isWebPlatform()) {
|
|
2685
2713
|
return null;
|
|
2686
2714
|
}
|
|
2715
|
+
if (opts?.publicKey !== undefined) {
|
|
2716
|
+
if ((0, platform_1.isAndroid)() && !KeyManager.identitySignerStore) {
|
|
2717
|
+
const selected = KeyManager._selectorKey(opts.publicKey);
|
|
2718
|
+
if (!selected)
|
|
2719
|
+
return null;
|
|
2720
|
+
const bridge = await (0, protocol_1.loadCommonsIdentityBridge)();
|
|
2721
|
+
return ((await bridge?.signRetiredSocialReceive?.(index, digest, selected, opts.accountId)) ??
|
|
2722
|
+
null);
|
|
2723
|
+
}
|
|
2724
|
+
const selected = await KeyManager._selectedPrivateKey(opts.publicKey, opts.accountId);
|
|
2725
|
+
return selected ? (0, identityDerivations_1.signSocialReceiveDigest)(selected, index, digest) : null;
|
|
2726
|
+
}
|
|
2687
2727
|
const accountId = opts?.accountId;
|
|
2688
2728
|
if (accountId !== undefined) {
|
|
2689
2729
|
// An organization key (ADR 0061 D6); never the personal one in its place.
|
|
@@ -2707,6 +2747,34 @@ class KeyManager {
|
|
|
2707
2747
|
: ((await KeyManager.getSharedPrivateKey()) ?? (await KeyManager.getPrivateKey()));
|
|
2708
2748
|
return local ? (0, identityDerivations_1.signSocialReceiveDigest)(local, index, digest) : null;
|
|
2709
2749
|
}
|
|
2750
|
+
/**
|
|
2751
|
+
* The private key of the account (personal, or `accountId`'s organization)
|
|
2752
|
+
* whose public key is `publicKey`, from this app's own storage: the current
|
|
2753
|
+
* key when it matches, else the retired ring. iOS and Commons only; `null`
|
|
2754
|
+
* for a key this device does not hold. Never another key in its place. @internal
|
|
2755
|
+
*/
|
|
2756
|
+
/**
|
|
2757
|
+
* The selector in the identity host's encoding (lowercase uncompressed), so a
|
|
2758
|
+
* compressed `#key-1` from a DID or a gateway row selects the same key. @internal
|
|
2759
|
+
*/
|
|
2760
|
+
static _selectorKey(publicKey) {
|
|
2761
|
+
return KeyManager.isValidPublicKey(publicKey) ? KeyManager.canonicalPublicKey(publicKey) : null;
|
|
2762
|
+
}
|
|
2763
|
+
static async _selectedPrivateKey(publicKey, accountId) {
|
|
2764
|
+
if ((0, retiredKeys_1.retiredKeyScope)({ accountId }) === null || !KeyManager.isValidPublicKey(publicKey)) {
|
|
2765
|
+
return null;
|
|
2766
|
+
}
|
|
2767
|
+
const wanted = KeyManager.canonicalPublicKey(publicKey);
|
|
2768
|
+
const current = accountId !== undefined
|
|
2769
|
+
? await (0, organizationKeys_1.getOrganizationPrivateKey)(accountId)
|
|
2770
|
+
: (0, platform_1.isAndroid)()
|
|
2771
|
+
? await KeyManager.getPrivateKey()
|
|
2772
|
+
: ((await KeyManager.getSharedPrivateKey()) ?? (await KeyManager.getPrivateKey()));
|
|
2773
|
+
if (current && KeyManager.canonicalPublicKey(KeyManager.derivePublicKey(current)) === wanted) {
|
|
2774
|
+
return current;
|
|
2775
|
+
}
|
|
2776
|
+
return (0, retiredKeys_1.getRetiredPrivateKey)(wanted, accountId !== undefined ? { accountId } : undefined);
|
|
2777
|
+
}
|
|
2710
2778
|
/**
|
|
2711
2779
|
* Get a shortened version of the public key for display
|
|
2712
2780
|
* Format: first 8 chars...last 8 chars
|