@oxy.so/core 4.5.0-oxy-one.1 → 4.5.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/README.md +1 -1
- package/dist/cjs/HttpService.js +63 -19
- package/dist/cjs/api/accounts.js +10 -3
- package/dist/cjs/api/agency.js +3 -1
- package/dist/cjs/api/apps.js +15 -4
- package/dist/cjs/api/assets.js +39 -11
- package/dist/cjs/api/auth.js +28 -8
- package/dist/cjs/api/billing.js +24 -7
- package/dist/cjs/api/civic.js +14 -4
- package/dist/cjs/api/follows.js +9 -2
- package/dist/cjs/api/identity.js +31 -9
- package/dist/cjs/api/nodes.js +3 -1
- package/dist/cjs/api/privacy.js +8 -2
- package/dist/cjs/api/session.js +15 -4
- package/dist/cjs/api/store.js +15 -5
- package/dist/cjs/api/topics.js +10 -3
- package/dist/cjs/api/users.js +16 -4
- package/dist/cjs/boot/sessionColdBoot.js +40 -13
- package/dist/cjs/constants/version.js +6 -6
- package/dist/cjs/crypto/identityMarker.js +3 -1
- package/dist/cjs/crypto/identityProof.js +6 -1
- package/dist/cjs/crypto/keyManager.js +87 -32
- package/dist/cjs/crypto/recoveryPhrase.js +2 -2
- package/dist/cjs/crypto/signatureService.js +3 -4
- package/dist/cjs/i18n/index.js +16 -3
- package/dist/cjs/i18n/locales/ar-SA.js +366 -366
- package/dist/cjs/i18n/locales/ca-ES.js +366 -366
- package/dist/cjs/i18n/locales/de-DE.js +366 -366
- package/dist/cjs/i18n/locales/en-US.js +1994 -1994
- package/dist/cjs/i18n/locales/es-ES.js +1994 -1994
- package/dist/cjs/i18n/locales/fr-FR.js +366 -366
- package/dist/cjs/i18n/locales/it-IT.js +366 -366
- package/dist/cjs/i18n/locales/ja-JP.js +366 -366
- package/dist/cjs/i18n/locales/ko-KR.js +366 -366
- package/dist/cjs/i18n/locales/pt-PT.js +366 -366
- package/dist/cjs/i18n/locales/zh-CN.js +366 -366
- package/dist/cjs/inference/OxyInferenceClient.js +12 -18
- package/dist/cjs/models/interfaces.js +11 -11
- package/dist/cjs/server/OxyServer.js +86 -28
- package/dist/cjs/server/agentAccount.js +6 -2
- package/dist/cjs/server/aliaMachineCredential.js +39 -14
- package/dist/cjs/server/auth.js +5 -4
- package/dist/cjs/server/capabilityTicket.js +5 -5
- package/dist/cjs/server/index.js +2 -1
- package/dist/cjs/server/liveCapabilityTicket.js +4 -1
- package/dist/cjs/server/middleware.js +61 -42
- package/dist/cjs/server/namespaces.js +28 -7
- package/dist/cjs/server/requesterAssertion.js +36 -30
- package/dist/cjs/server/serviceTokenLifetime.js +7 -4
- package/dist/cjs/server/traffic.js +53 -14
- package/dist/cjs/server/trafficNodeHttp.js +33 -8
- package/dist/cjs/server/workloadIdentity.client.js +4 -0
- package/dist/cjs/server/workloadIdentity.js +28 -1
- package/dist/cjs/session/SessionClient.js +54 -17
- package/dist/cjs/session/accountDelegationAccess.js +4 -6
- package/dist/cjs/session/accountDialogController.js +1 -1
- package/dist/cjs/session/authStateStore.js +18 -9
- package/dist/cjs/session/identityPin.js +6 -2
- package/dist/cjs/session/refresh.js +81 -23
- package/dist/cjs/session/sharedDeviceCredential.js +28 -15
- package/dist/cjs/shared/utils/errorUtils.js +1 -1
- package/dist/cjs/shared/utils/networkUtils.js +1 -1
- package/dist/cjs/shared/utils/themeUtils.js +1 -1
- package/dist/cjs/utils/accountCacheSweep.js +3 -1
- package/dist/cjs/utils/accountUtils.js +1 -3
- package/dist/cjs/utils/asyncUtils.js +1 -1
- package/dist/cjs/utils/authHelpers.js +2 -1
- package/dist/cjs/utils/avatarUtils.js +4 -2
- package/dist/cjs/utils/cache.js +1 -1
- package/dist/cjs/utils/cacheKey.js +3 -1
- package/dist/cjs/utils/deviceManager.js +4 -4
- package/dist/cjs/utils/errorUtils.js +6 -4
- package/dist/cjs/utils/languageUtils.js +404 -57
- package/dist/cjs/utils/profileLinks.js +1 -3
- package/dist/cjs/utils/requestUtils.js +7 -1
- package/dist/cjs/utils/userIdentity.js +1 -1
- package/dist/cjs/utils/validationUtils.js +5 -6
- package/dist/esm/HttpService.js +64 -20
- package/dist/esm/api/accounts.js +11 -4
- package/dist/esm/api/agency.js +3 -1
- package/dist/esm/api/apps.js +15 -4
- package/dist/esm/api/assets.js +39 -11
- package/dist/esm/api/auth.js +28 -8
- package/dist/esm/api/billing.js +25 -8
- package/dist/esm/api/civic.js +14 -4
- package/dist/esm/api/follows.js +9 -2
- package/dist/esm/api/identity.js +31 -9
- package/dist/esm/api/nodes.js +3 -1
- package/dist/esm/api/privacy.js +8 -2
- package/dist/esm/api/session.js +15 -4
- package/dist/esm/api/store.js +15 -5
- package/dist/esm/api/topics.js +10 -3
- package/dist/esm/api/users.js +16 -4
- package/dist/esm/boot/sessionColdBoot.js +41 -14
- package/dist/esm/constants/version.js +6 -6
- package/dist/esm/crypto/identityMarker.js +3 -1
- package/dist/esm/crypto/identityProof.js +6 -1
- package/dist/esm/crypto/internal.js +1 -1
- package/dist/esm/crypto/keyManager.js +87 -32
- package/dist/esm/crypto/recoveryPhrase.js +2 -2
- package/dist/esm/crypto/signatureService.js +3 -4
- package/dist/esm/i18n/index.js +16 -3
- package/dist/esm/i18n/locales/ar-SA.js +366 -366
- package/dist/esm/i18n/locales/ca-ES.js +366 -366
- package/dist/esm/i18n/locales/de-DE.js +366 -366
- package/dist/esm/i18n/locales/en-US.js +1994 -1994
- package/dist/esm/i18n/locales/es-ES.js +1994 -1994
- package/dist/esm/i18n/locales/fr-FR.js +366 -366
- package/dist/esm/i18n/locales/it-IT.js +366 -366
- package/dist/esm/i18n/locales/ja-JP.js +366 -366
- package/dist/esm/i18n/locales/ko-KR.js +366 -366
- package/dist/esm/i18n/locales/pt-PT.js +366 -366
- package/dist/esm/i18n/locales/zh-CN.js +366 -366
- package/dist/esm/index.js +1 -1
- package/dist/esm/inference/OxyInferenceClient.js +13 -19
- package/dist/esm/models/interfaces.js +11 -11
- package/dist/esm/server/OxyServer.js +87 -29
- package/dist/esm/server/agentAccount.js +6 -2
- package/dist/esm/server/aliaMachineCredential.js +39 -14
- package/dist/esm/server/auth.js +6 -5
- package/dist/esm/server/capabilityTicket.js +6 -6
- package/dist/esm/server/index.js +5 -5
- package/dist/esm/server/liveCapabilityTicket.js +6 -3
- package/dist/esm/server/middleware.js +62 -43
- package/dist/esm/server/namespaces.js +31 -10
- package/dist/esm/server/requesterAssertion.js +36 -30
- package/dist/esm/server/serviceTokenLifetime.js +7 -4
- package/dist/esm/server/traffic.js +55 -16
- package/dist/esm/server/trafficNodeHttp.js +34 -9
- package/dist/esm/server/userInvalidation.js +1 -1
- package/dist/esm/server/workloadIdentity.client.js +3 -0
- package/dist/esm/server/workloadIdentity.js +27 -1
- package/dist/esm/session/SessionClient.js +54 -17
- package/dist/esm/session/accountDelegationAccess.js +5 -7
- package/dist/esm/session/accountDialogController.js +1 -1
- package/dist/esm/session/authStateStore.js +18 -9
- package/dist/esm/session/identityPin.js +6 -2
- package/dist/esm/session/refresh.js +82 -24
- package/dist/esm/session/sharedDeviceCredential.js +28 -15
- package/dist/esm/shared/utils/errorUtils.js +1 -1
- package/dist/esm/shared/utils/networkUtils.js +1 -1
- package/dist/esm/shared/utils/themeUtils.js +1 -1
- package/dist/esm/utils/accountCacheSweep.js +3 -1
- package/dist/esm/utils/accountUtils.js +1 -3
- package/dist/esm/utils/asyncUtils.js +1 -1
- package/dist/esm/utils/authHelpers.js +2 -1
- package/dist/esm/utils/avatarUtils.js +4 -2
- package/dist/esm/utils/cache.js +1 -1
- package/dist/esm/utils/cacheKey.js +3 -1
- package/dist/esm/utils/deviceManager.js +4 -4
- package/dist/esm/utils/errorUtils.js +6 -4
- package/dist/esm/utils/languageUtils.js +404 -57
- package/dist/esm/utils/profileLinks.js +1 -3
- package/dist/esm/utils/requestUtils.js +7 -1
- package/dist/esm/utils/userIdentity.js +1 -1
- package/dist/esm/utils/validationUtils.js +5 -6
- package/dist/types/HttpService.d.ts +1 -1
- package/dist/types/api/accounts.d.ts +1 -1
- package/dist/types/api/apps.d.ts +4 -4
- package/dist/types/crypto/internal.d.ts +1 -1
- package/dist/types/index.d.ts +4 -4
- package/dist/types/server/index.d.ts +6 -6
- package/dist/types/server/workloadIdentity.client.d.ts +5 -1
- package/dist/types/server/workloadIdentity.d.ts +26 -0
- package/dist/types/session/accountDelegationAccess.d.ts +1 -1
- package/dist/types/session/index.d.ts +1 -1
- package/dist/types/utils/userIdentity.d.ts +1 -1
- package/dist/types/utils/validationUtils.d.ts +4 -5
- package/package.json +2 -3
- package/src/HttpService.ts +183 -53
- package/src/OxyServices.errors.ts +7 -3
- package/src/OxyServices.ts +6 -2
- package/src/api/accounts.ts +76 -26
- package/src/api/agency.ts +24 -9
- package/src/api/appData.ts +12 -2
- package/src/api/apps.ts +81 -31
- package/src/api/assets.ts +113 -43
- package/src/api/auth.ts +145 -77
- package/src/api/billing.ts +112 -65
- package/src/api/civic.ts +61 -19
- package/src/api/contacts.ts +4 -1
- package/src/api/devices.ts +54 -21
- package/src/api/follows.ts +88 -31
- package/src/api/identity.ts +101 -33
- package/src/api/linkedAccounts.ts +26 -8
- package/src/api/nodes.ts +18 -6
- package/src/api/notifications.ts +26 -6
- package/src/api/privacy.ts +42 -8
- package/src/api/reputation.ts +18 -7
- package/src/api/session.ts +23 -5
- package/src/api/store.ts +81 -31
- package/src/api/topics.ts +18 -6
- package/src/api/users.ts +98 -35
- package/src/boot/sessionColdBoot.ts +60 -19
- package/src/civic/payloads.ts +1 -4
- package/src/client/context.ts +6 -1
- package/src/constants/version.ts +7 -6
- package/src/crypto/aead.ts +1 -5
- package/src/crypto/ecdh.ts +1 -4
- package/src/crypto/identityDerivations.ts +5 -1
- package/src/crypto/identityMarker.ts +3 -1
- package/src/crypto/identityProof.ts +6 -1
- package/src/crypto/index.ts +11 -11
- package/src/crypto/internal.ts +6 -1
- package/src/crypto/keyManager.ts +348 -98
- package/src/crypto/recoveryPhrase.ts +14 -9
- package/src/crypto/registrationPow.ts +1 -1
- package/src/crypto/signatureService.ts +14 -6
- package/src/i18n/accountCategoryLabels.ts +1 -4
- package/src/i18n/accountRoleLabels.ts +1 -4
- package/src/i18n/index.ts +25 -7
- package/src/i18n/locales/ar-SA.ts +375 -366
- package/src/i18n/locales/ca-ES.ts +382 -366
- package/src/i18n/locales/de-DE.ts +387 -366
- package/src/i18n/locales/en-US.ts +2034 -1994
- package/src/i18n/locales/es-ES.ts +2043 -1994
- package/src/i18n/locales/fr-FR.ts +387 -366
- package/src/i18n/locales/it-IT.ts +380 -366
- package/src/i18n/locales/ja-JP.ts +380 -366
- package/src/i18n/locales/ko-KR.ts +377 -366
- package/src/i18n/locales/pt-PT.ts +383 -366
- package/src/i18n/locales/zh-CN.ts +373 -366
- package/src/i18n/trustTierLabels.ts +1 -4
- package/src/index.ts +371 -349
- package/src/inference/OxyInferenceClient.ts +784 -780
- package/src/models/interfaces.ts +30 -27
- package/src/server/OxyServer.ts +227 -68
- package/src/server/agentAccount.ts +8 -2
- package/src/server/aliaMachineCredential.ts +57 -16
- package/src/server/auth.ts +9 -5
- package/src/server/capabilityTicket.ts +30 -21
- package/src/server/index.ts +29 -6
- package/src/server/liveCapabilityTicket.ts +31 -9
- package/src/server/middleware.ts +212 -121
- package/src/server/namespaces.ts +247 -94
- package/src/server/rateLimit.ts +2 -4
- package/src/server/requesterAssertion.ts +93 -54
- package/src/server/serviceTokenLifetime.ts +9 -4
- package/src/server/traffic.ts +117 -23
- package/src/server/trafficNodeHttp.ts +61 -14
- package/src/server/userInvalidation.ts +1 -4
- package/src/server/workloadIdentity.client.ts +12 -1
- package/src/server/workloadIdentity.ts +52 -5
- package/src/session/SessionClient.ts +170 -49
- package/src/session/accountDelegationAccess.ts +24 -31
- package/src/session/accountDialogController.ts +59 -14
- package/src/session/accountSwitchTargets.ts +10 -15
- package/src/session/authStateStore.ts +121 -81
- package/src/session/deviceDirectory.ts +1 -3
- package/src/session/identityPin.ts +10 -11
- package/src/session/identitySession.ts +3 -1
- package/src/session/index.ts +87 -82
- package/src/session/refresh.ts +138 -44
- package/src/session/sessionClientHost.ts +1 -3
- package/src/session/sharedDeviceCredential.ts +35 -14
- package/src/session/socketLoader.ts +9 -2
- package/src/shared/utils/errorUtils.ts +5 -2
- package/src/shared/utils/networkUtils.ts +5 -8
- package/src/shared/utils/themeUtils.ts +2 -6
- package/src/types/buffer.d.ts +66 -21
- package/src/types/expo-secure-store.d.ts +5 -1
- package/src/utils/accountCacheSweep.ts +4 -5
- package/src/utils/accountUtils.ts +107 -108
- package/src/utils/apiUtils.ts +3 -3
- package/src/utils/asyncUtils.ts +2 -2
- package/src/utils/authHelpers.ts +10 -5
- package/src/utils/avatarUtils.ts +11 -6
- package/src/utils/cache.ts +9 -10
- package/src/utils/cacheKey.ts +5 -1
- package/src/utils/coldBoot.ts +1 -3
- package/src/utils/commonsDelivery.ts +1 -4
- package/src/utils/deviceManager.ts +15 -14
- package/src/utils/errorUtils.ts +68 -86
- package/src/utils/languageUtils.ts +407 -58
- package/src/utils/oauthPkce.ts +1 -4
- package/src/utils/platform.ts +0 -1
- package/src/utils/profileLinks.ts +2 -5
- package/src/utils/requestUtils.ts +18 -14
- package/src/utils/sessionUtils.ts +34 -33
- package/src/utils/userHandle.ts +3 -1
- package/src/utils/userIdentity.ts +1 -1
- package/src/utils/validationUtils.ts +11 -9
package/src/crypto/keyManager.ts
CHANGED
|
@@ -93,7 +93,7 @@ export class IdentityAlreadyExistsError extends Error {
|
|
|
93
93
|
super(
|
|
94
94
|
'An identity already exists on this device. Refusing to overwrite without explicit consent. ' +
|
|
95
95
|
'If you really want to replace it, ensure the user has saved their recovery phrase, then call ' +
|
|
96
|
-
'the operation with { overwrite: true }.'
|
|
96
|
+
'the operation with { overwrite: true }.',
|
|
97
97
|
);
|
|
98
98
|
this.existingPublicKey = existingPublicKey;
|
|
99
99
|
}
|
|
@@ -107,7 +107,10 @@ export class IdentityAlreadyExistsError extends Error {
|
|
|
107
107
|
*/
|
|
108
108
|
export class IdentityPersistError extends Error {
|
|
109
109
|
override readonly name = 'IdentityPersistError';
|
|
110
|
-
constructor(
|
|
110
|
+
constructor(
|
|
111
|
+
message: string,
|
|
112
|
+
readonly cause?: unknown,
|
|
113
|
+
) {
|
|
111
114
|
super(message);
|
|
112
115
|
}
|
|
113
116
|
}
|
|
@@ -126,7 +129,10 @@ export class IdentityPersistError extends Error {
|
|
|
126
129
|
*/
|
|
127
130
|
export class IdentityUnavailableError extends Error {
|
|
128
131
|
override readonly name = 'IdentityUnavailableError';
|
|
129
|
-
constructor(
|
|
132
|
+
constructor(
|
|
133
|
+
message: string,
|
|
134
|
+
readonly cause?: unknown,
|
|
135
|
+
) {
|
|
130
136
|
super(message);
|
|
131
137
|
}
|
|
132
138
|
}
|
|
@@ -334,7 +340,7 @@ async function initExpoCrypto(): Promise<ExpoCryptoLike> {
|
|
|
334
340
|
*/
|
|
335
341
|
function uint8ArrayToHex(bytes: Uint8Array): string {
|
|
336
342
|
return Array.from(bytes)
|
|
337
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
343
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
338
344
|
.join('');
|
|
339
345
|
}
|
|
340
346
|
|
|
@@ -356,7 +362,7 @@ async function getSecureRandomBytes(length: number): Promise<Uint8Array> {
|
|
|
356
362
|
const Crypto = await initExpoCrypto();
|
|
357
363
|
return Crypto.getRandomBytes(length);
|
|
358
364
|
}
|
|
359
|
-
|
|
365
|
+
|
|
360
366
|
// In Node.js, use Node's crypto module.
|
|
361
367
|
//
|
|
362
368
|
// `loadNodeCrypto` is per-platform: the default variant performs
|
|
@@ -499,7 +505,9 @@ export class KeyManager {
|
|
|
499
505
|
store: Awaited<ReturnType<typeof initSecureStore>>,
|
|
500
506
|
service?: string,
|
|
501
507
|
): OxySecureStoreOptions {
|
|
502
|
-
const opts: OxySecureStoreOptions = {
|
|
508
|
+
const opts: OxySecureStoreOptions = {
|
|
509
|
+
keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
510
|
+
};
|
|
503
511
|
if (service) {
|
|
504
512
|
opts.keychainService = service;
|
|
505
513
|
}
|
|
@@ -566,7 +574,11 @@ export class KeyManager {
|
|
|
566
574
|
} catch (error) {
|
|
567
575
|
// Advisory only — a failed write just re-runs the cheap slot check next launch.
|
|
568
576
|
if (isDev()) {
|
|
569
|
-
logger.debug(
|
|
577
|
+
logger.debug(
|
|
578
|
+
'Failed to set slots-migrated flag (advisory)',
|
|
579
|
+
{ component: 'KeyManager' },
|
|
580
|
+
error,
|
|
581
|
+
);
|
|
570
582
|
}
|
|
571
583
|
}
|
|
572
584
|
}
|
|
@@ -741,7 +753,11 @@ export class KeyManager {
|
|
|
741
753
|
backupPublic = await store.getItemAsync(STORAGE_KEYS.BACKUP_PUBLIC_KEY);
|
|
742
754
|
} catch (error) {
|
|
743
755
|
if (isDev()) {
|
|
744
|
-
logger.debug(
|
|
756
|
+
logger.debug(
|
|
757
|
+
'Legacy backup unreadable during migration (non-fatal)',
|
|
758
|
+
{ component: 'KeyManager' },
|
|
759
|
+
error,
|
|
760
|
+
);
|
|
745
761
|
}
|
|
746
762
|
backupPrivate = null;
|
|
747
763
|
backupPublic = null;
|
|
@@ -773,7 +789,11 @@ export class KeyManager {
|
|
|
773
789
|
KeyManager._slotOpts(V2_BACKUP_KEYCHAIN_SERVICE),
|
|
774
790
|
);
|
|
775
791
|
} catch (error) {
|
|
776
|
-
logger.warn(
|
|
792
|
+
logger.warn(
|
|
793
|
+
'Failed to migrate identity backup slot to v2 (non-fatal)',
|
|
794
|
+
{ component: 'KeyManager' },
|
|
795
|
+
error,
|
|
796
|
+
);
|
|
777
797
|
}
|
|
778
798
|
}
|
|
779
799
|
|
|
@@ -795,8 +815,16 @@ export class KeyManager {
|
|
|
795
815
|
private static async _bestEffortDeleteV2Primary(
|
|
796
816
|
store: Awaited<ReturnType<typeof initSecureStore>>,
|
|
797
817
|
): Promise<void> {
|
|
798
|
-
await KeyManager._bestEffortDelete(
|
|
799
|
-
|
|
818
|
+
await KeyManager._bestEffortDelete(
|
|
819
|
+
store,
|
|
820
|
+
V2_STORAGE_KEYS.PRIVATE_KEY,
|
|
821
|
+
V2_PRIMARY_KEYCHAIN_SERVICE,
|
|
822
|
+
);
|
|
823
|
+
await KeyManager._bestEffortDelete(
|
|
824
|
+
store,
|
|
825
|
+
V2_STORAGE_KEYS.PUBLIC_KEY,
|
|
826
|
+
V2_PRIMARY_KEYCHAIN_SERVICE,
|
|
827
|
+
);
|
|
800
828
|
}
|
|
801
829
|
|
|
802
830
|
private static async _bestEffortDeleteLegacyPrimaryAndBackup(
|
|
@@ -812,9 +840,21 @@ export class KeyManager {
|
|
|
812
840
|
private static async _bestEffortDeleteBackupsAllGenerations(
|
|
813
841
|
store: Awaited<ReturnType<typeof initSecureStore>>,
|
|
814
842
|
): Promise<void> {
|
|
815
|
-
await KeyManager._bestEffortDelete(
|
|
816
|
-
|
|
817
|
-
|
|
843
|
+
await KeyManager._bestEffortDelete(
|
|
844
|
+
store,
|
|
845
|
+
V2_STORAGE_KEYS.BACKUP_PRIVATE_KEY,
|
|
846
|
+
V2_BACKUP_KEYCHAIN_SERVICE,
|
|
847
|
+
);
|
|
848
|
+
await KeyManager._bestEffortDelete(
|
|
849
|
+
store,
|
|
850
|
+
V2_STORAGE_KEYS.BACKUP_PUBLIC_KEY,
|
|
851
|
+
V2_BACKUP_KEYCHAIN_SERVICE,
|
|
852
|
+
);
|
|
853
|
+
await KeyManager._bestEffortDelete(
|
|
854
|
+
store,
|
|
855
|
+
V2_STORAGE_KEYS.BACKUP_TIMESTAMP,
|
|
856
|
+
V2_BACKUP_KEYCHAIN_SERVICE,
|
|
857
|
+
);
|
|
818
858
|
await KeyManager._bestEffortDelete(store, STORAGE_KEYS.BACKUP_PRIVATE_KEY);
|
|
819
859
|
await KeyManager._bestEffortDelete(store, STORAGE_KEYS.BACKUP_PUBLIC_KEY);
|
|
820
860
|
await KeyManager._bestEffortDelete(store, STORAGE_KEYS.BACKUP_TIMESTAMP);
|
|
@@ -837,7 +877,11 @@ export class KeyManager {
|
|
|
837
877
|
}
|
|
838
878
|
KeyManager.invalidateSharedCache();
|
|
839
879
|
} catch (error) {
|
|
840
|
-
logger.warn(
|
|
880
|
+
logger.warn(
|
|
881
|
+
'Failed to clear shared identity slot during force delete',
|
|
882
|
+
{ component: 'KeyManager' },
|
|
883
|
+
error,
|
|
884
|
+
);
|
|
841
885
|
}
|
|
842
886
|
}
|
|
843
887
|
|
|
@@ -850,7 +894,9 @@ export class KeyManager {
|
|
|
850
894
|
);
|
|
851
895
|
}
|
|
852
896
|
if (!(await signer.write(privateKey, publicKey))) {
|
|
853
|
-
throw new IdentityPersistError(
|
|
897
|
+
throw new IdentityPersistError(
|
|
898
|
+
`The identity signer store (${signer.name}) did not confirm the write.`,
|
|
899
|
+
);
|
|
854
900
|
}
|
|
855
901
|
}
|
|
856
902
|
|
|
@@ -863,7 +909,11 @@ export class KeyManager {
|
|
|
863
909
|
try {
|
|
864
910
|
await signer.clear();
|
|
865
911
|
} catch (error) {
|
|
866
|
-
logger.warn(
|
|
912
|
+
logger.warn(
|
|
913
|
+
`Failed to clear the identity signer store (${signer.name})`,
|
|
914
|
+
{ component: 'KeyManager' },
|
|
915
|
+
error,
|
|
916
|
+
);
|
|
867
917
|
} finally {
|
|
868
918
|
KeyManager.invalidateSharedCache();
|
|
869
919
|
}
|
|
@@ -882,10 +932,16 @@ export class KeyManager {
|
|
|
882
932
|
}
|
|
883
933
|
try {
|
|
884
934
|
if (!(await signer.write(privateKey, publicKey))) {
|
|
885
|
-
logger.warn(`The identity signer store (${signer.name}) did not confirm the write`, {
|
|
935
|
+
logger.warn(`The identity signer store (${signer.name}) did not confirm the write`, {
|
|
936
|
+
component: 'KeyManager',
|
|
937
|
+
});
|
|
886
938
|
}
|
|
887
939
|
} catch (error) {
|
|
888
|
-
logger.warn(
|
|
940
|
+
logger.warn(
|
|
941
|
+
`Failed to write the identity signer store (${signer.name})`,
|
|
942
|
+
{ component: 'KeyManager' },
|
|
943
|
+
error,
|
|
944
|
+
);
|
|
889
945
|
} finally {
|
|
890
946
|
KeyManager.invalidateSharedCache();
|
|
891
947
|
}
|
|
@@ -982,7 +1038,7 @@ export class KeyManager {
|
|
|
982
1038
|
await store.setItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, publicKey, publicOpts);
|
|
983
1039
|
} catch (error) {
|
|
984
1040
|
throw new Error(
|
|
985
|
-
`Failed to create shared identity on iOS. Ensure your app has the Keychain Sharing capability enabled with access group "${IOS_KEYCHAIN_GROUP}". Error: ${error}
|
|
1041
|
+
`Failed to create shared identity on iOS. Ensure your app has the Keychain Sharing capability enabled with access group "${IOS_KEYCHAIN_GROUP}". Error: ${error}`,
|
|
986
1042
|
);
|
|
987
1043
|
}
|
|
988
1044
|
} else if (isAndroid()) {
|
|
@@ -1118,11 +1174,15 @@ export class KeyManager {
|
|
|
1118
1174
|
return {
|
|
1119
1175
|
primaryPublicKey,
|
|
1120
1176
|
sharedPublicKey,
|
|
1121
|
-
activePublicKey: isAndroid()
|
|
1177
|
+
activePublicKey: isAndroid()
|
|
1178
|
+
? (primaryPublicKey ?? sharedPublicKey)
|
|
1179
|
+
: (sharedPublicKey ?? primaryPublicKey),
|
|
1122
1180
|
// Only a real disagreement is out of sync. A device with just one slot
|
|
1123
1181
|
// populated has nothing to contradict.
|
|
1124
1182
|
inSync:
|
|
1125
|
-
primaryPublicKey === null ||
|
|
1183
|
+
primaryPublicKey === null ||
|
|
1184
|
+
sharedPublicKey === null ||
|
|
1185
|
+
primaryPublicKey === sharedPublicKey,
|
|
1126
1186
|
};
|
|
1127
1187
|
}
|
|
1128
1188
|
|
|
@@ -1286,7 +1346,11 @@ export class KeyManager {
|
|
|
1286
1346
|
const storage = await KeyManager._advisoryStorage();
|
|
1287
1347
|
await storage?.setItem(ROTATION_PENDING_KEY, '');
|
|
1288
1348
|
} catch (error) {
|
|
1289
|
-
logger.warn(
|
|
1349
|
+
logger.warn(
|
|
1350
|
+
'Failed to clear the pending key rotation marker',
|
|
1351
|
+
{ component: 'KeyManager' },
|
|
1352
|
+
error,
|
|
1353
|
+
);
|
|
1290
1354
|
}
|
|
1291
1355
|
}
|
|
1292
1356
|
|
|
@@ -1320,7 +1384,11 @@ export class KeyManager {
|
|
|
1320
1384
|
) {
|
|
1321
1385
|
return false;
|
|
1322
1386
|
}
|
|
1323
|
-
await KeyManager._persistIdentityAtomic(
|
|
1387
|
+
await KeyManager._persistIdentityAtomic(
|
|
1388
|
+
pair.privateKey,
|
|
1389
|
+
pair.publicKey.toLowerCase(),
|
|
1390
|
+
'import',
|
|
1391
|
+
);
|
|
1324
1392
|
KeyManager.invalidateCache();
|
|
1325
1393
|
await KeyManager.completeKeyRotation();
|
|
1326
1394
|
logger.warn(
|
|
@@ -1413,7 +1481,9 @@ export class KeyManager {
|
|
|
1413
1481
|
priorPrivate = await store.getItemAsync(layout.primaryPrivateKeyName, primaryReadOpts);
|
|
1414
1482
|
priorPublic = await store.getItemAsync(layout.primaryPublicKeyName, primaryReadOpts);
|
|
1415
1483
|
} catch (error) {
|
|
1416
|
-
logger.error('Failed to read existing primary before persist', error, {
|
|
1484
|
+
logger.error('Failed to read existing primary before persist', error, {
|
|
1485
|
+
component: 'KeyManager',
|
|
1486
|
+
});
|
|
1417
1487
|
throw new IdentityPersistError(
|
|
1418
1488
|
'Could not read existing identity before writing a new one; refusing to overwrite blind.',
|
|
1419
1489
|
error,
|
|
@@ -1449,11 +1519,24 @@ export class KeyManager {
|
|
|
1449
1519
|
KeyManager.canonicalPrivateKey(priorPrivate),
|
|
1450
1520
|
backupPrivWriteOpts,
|
|
1451
1521
|
);
|
|
1452
|
-
await store.setItemAsync(
|
|
1453
|
-
|
|
1522
|
+
await store.setItemAsync(
|
|
1523
|
+
layout.backupPublicKeyName,
|
|
1524
|
+
priorPublic.toLowerCase(),
|
|
1525
|
+
backupPubWriteOpts,
|
|
1526
|
+
);
|
|
1527
|
+
await store.setItemAsync(
|
|
1528
|
+
layout.backupTimestampName,
|
|
1529
|
+
Date.now().toString(),
|
|
1530
|
+
backupPubWriteOpts,
|
|
1531
|
+
);
|
|
1454
1532
|
} catch (error) {
|
|
1455
|
-
logger.error('Failed to back up existing identity before overwrite', error, {
|
|
1456
|
-
|
|
1533
|
+
logger.error('Failed to back up existing identity before overwrite', error, {
|
|
1534
|
+
component: 'KeyManager',
|
|
1535
|
+
});
|
|
1536
|
+
throw new IdentityPersistError(
|
|
1537
|
+
'Failed to back up existing identity before overwrite',
|
|
1538
|
+
error,
|
|
1539
|
+
);
|
|
1457
1540
|
}
|
|
1458
1541
|
}
|
|
1459
1542
|
}
|
|
@@ -1464,9 +1547,15 @@ export class KeyManager {
|
|
|
1464
1547
|
// new primary is proven durable.
|
|
1465
1548
|
try {
|
|
1466
1549
|
await store.setItemAsync(layout.primaryPublicKeyName, canonicalPublic, primaryPubWriteOpts);
|
|
1467
|
-
await store.setItemAsync(
|
|
1550
|
+
await store.setItemAsync(
|
|
1551
|
+
layout.primaryPrivateKeyName,
|
|
1552
|
+
canonicalPrivate,
|
|
1553
|
+
primaryPrivWriteOpts,
|
|
1554
|
+
);
|
|
1468
1555
|
} catch (error) {
|
|
1469
|
-
logger.error('Failed to write primary identity to secure store', error, {
|
|
1556
|
+
logger.error('Failed to write primary identity to secure store', error, {
|
|
1557
|
+
component: 'KeyManager',
|
|
1558
|
+
});
|
|
1470
1559
|
await KeyManager._rollbackPrimary(store, layout, priorPrivate, priorPublic);
|
|
1471
1560
|
throw new IdentityPersistError('Failed to write identity to secure store', error);
|
|
1472
1561
|
}
|
|
@@ -1493,9 +1582,13 @@ export class KeyManager {
|
|
|
1493
1582
|
readBackPrivate?.toLowerCase() !== canonicalPrivate ||
|
|
1494
1583
|
readBackPublic?.toLowerCase() !== canonicalPublic
|
|
1495
1584
|
) {
|
|
1496
|
-
logger.error('Identity round-trip mismatch after write', undefined, {
|
|
1585
|
+
logger.error('Identity round-trip mismatch after write', undefined, {
|
|
1586
|
+
component: 'KeyManager',
|
|
1587
|
+
});
|
|
1497
1588
|
await KeyManager._rollbackPrimary(store, layout, priorPrivate, priorPublic);
|
|
1498
|
-
throw new IdentityPersistError(
|
|
1589
|
+
throw new IdentityPersistError(
|
|
1590
|
+
'Identity write was not persisted correctly (round-trip mismatch).',
|
|
1591
|
+
);
|
|
1499
1592
|
}
|
|
1500
1593
|
|
|
1501
1594
|
// Final sanity: derive public from the stored private and confirm the
|
|
@@ -1535,7 +1628,9 @@ export class KeyManager {
|
|
|
1535
1628
|
priorBackupPublic = await store.getItemAsync(layout.backupPublicKeyName, backupReadOpts);
|
|
1536
1629
|
priorBackupTimestamp = await store.getItemAsync(layout.backupTimestampName, backupReadOpts);
|
|
1537
1630
|
} catch (error) {
|
|
1538
|
-
logger.error('Failed to snapshot identity backup before refresh', error, {
|
|
1631
|
+
logger.error('Failed to snapshot identity backup before refresh', error, {
|
|
1632
|
+
component: 'KeyManager',
|
|
1633
|
+
});
|
|
1539
1634
|
await KeyManager._rollbackPrimary(store, layout, priorPrivate, priorPublic);
|
|
1540
1635
|
throw new IdentityPersistError('Failed to snapshot identity backup before refresh', error);
|
|
1541
1636
|
}
|
|
@@ -1543,12 +1638,27 @@ export class KeyManager {
|
|
|
1543
1638
|
try {
|
|
1544
1639
|
await store.setItemAsync(layout.backupPrivateKeyName, canonicalPrivate, backupPrivWriteOpts);
|
|
1545
1640
|
await store.setItemAsync(layout.backupPublicKeyName, canonicalPublic, backupPubWriteOpts);
|
|
1546
|
-
await store.setItemAsync(
|
|
1641
|
+
await store.setItemAsync(
|
|
1642
|
+
layout.backupTimestampName,
|
|
1643
|
+
Date.now().toString(),
|
|
1644
|
+
backupPubWriteOpts,
|
|
1645
|
+
);
|
|
1547
1646
|
} catch (error) {
|
|
1548
|
-
logger.error('Failed to refresh identity backup after primary write', error, {
|
|
1549
|
-
|
|
1647
|
+
logger.error('Failed to refresh identity backup after primary write', error, {
|
|
1648
|
+
component: 'KeyManager',
|
|
1649
|
+
});
|
|
1650
|
+
await KeyManager._rollbackBackup(
|
|
1651
|
+
store,
|
|
1652
|
+
layout,
|
|
1653
|
+
priorBackupPrivate,
|
|
1654
|
+
priorBackupPublic,
|
|
1655
|
+
priorBackupTimestamp,
|
|
1656
|
+
);
|
|
1550
1657
|
await KeyManager._rollbackPrimary(store, layout, priorPrivate, priorPublic);
|
|
1551
|
-
throw new IdentityPersistError(
|
|
1658
|
+
throw new IdentityPersistError(
|
|
1659
|
+
'Failed to refresh identity backup after primary write',
|
|
1660
|
+
error,
|
|
1661
|
+
);
|
|
1552
1662
|
}
|
|
1553
1663
|
|
|
1554
1664
|
// Update cache only after we are certain the identity is durable, then fan
|
|
@@ -1603,7 +1713,11 @@ export class KeyManager {
|
|
|
1603
1713
|
}
|
|
1604
1714
|
const sameIdentity = existing?.publicKey === publicKey;
|
|
1605
1715
|
const keptMnemonic = mnemonic ?? (sameIdentity ? existing?.mnemonic : undefined);
|
|
1606
|
-
if (
|
|
1716
|
+
if (
|
|
1717
|
+
sameIdentity &&
|
|
1718
|
+
existing?.privateKey === privateKey &&
|
|
1719
|
+
existing?.mnemonic === keptMnemonic
|
|
1720
|
+
) {
|
|
1607
1721
|
return true; // Already current; do not churn the store.
|
|
1608
1722
|
}
|
|
1609
1723
|
await store.write(
|
|
@@ -1635,12 +1749,19 @@ export class KeyManager {
|
|
|
1635
1749
|
try {
|
|
1636
1750
|
await store.clear();
|
|
1637
1751
|
} catch (error) {
|
|
1638
|
-
logger.warn(
|
|
1752
|
+
logger.warn(
|
|
1753
|
+
`Failed to clear the identity device backup (${store.name})`,
|
|
1754
|
+
{ component: 'KeyManager' },
|
|
1755
|
+
error,
|
|
1756
|
+
);
|
|
1639
1757
|
}
|
|
1640
1758
|
}
|
|
1641
1759
|
|
|
1642
1760
|
/** True when `phrase` is a valid BIP-39 phrase that derives `publicKey`. @internal */
|
|
1643
|
-
private static async _phraseDerivesPublicKey(
|
|
1761
|
+
private static async _phraseDerivesPublicKey(
|
|
1762
|
+
phrase: string,
|
|
1763
|
+
publicKey: string,
|
|
1764
|
+
): Promise<boolean> {
|
|
1644
1765
|
try {
|
|
1645
1766
|
// Imported lazily: `./recoveryPhrase` imports this module, and the phrase
|
|
1646
1767
|
// derivation is frozen there, so it is reused rather than copied.
|
|
@@ -1653,10 +1774,17 @@ export class KeyManager {
|
|
|
1653
1774
|
}
|
|
1654
1775
|
|
|
1655
1776
|
/** The primary pair when it reads back healthy, else `null`. Never throws. @internal */
|
|
1656
|
-
private static async _readHealthyPrimary(): Promise<{
|
|
1777
|
+
private static async _readHealthyPrimary(): Promise<{
|
|
1778
|
+
privateKey: string;
|
|
1779
|
+
publicKey: string;
|
|
1780
|
+
} | null> {
|
|
1657
1781
|
try {
|
|
1658
1782
|
const direct = await KeyManager._readPrimaryDirect();
|
|
1659
|
-
if (
|
|
1783
|
+
if (
|
|
1784
|
+
KeyManager._isHealthyPair(direct.privateKey, direct.publicKey) &&
|
|
1785
|
+
direct.privateKey &&
|
|
1786
|
+
direct.publicKey
|
|
1787
|
+
) {
|
|
1660
1788
|
return {
|
|
1661
1789
|
privateKey: KeyManager.canonicalPrivateKey(direct.privateKey),
|
|
1662
1790
|
publicKey: direct.publicKey.toLowerCase(),
|
|
@@ -1734,7 +1862,11 @@ export class KeyManager {
|
|
|
1734
1862
|
await writeIdentityMarker({ publicKey, origin });
|
|
1735
1863
|
}
|
|
1736
1864
|
} catch (error) {
|
|
1737
|
-
logger.warn(
|
|
1865
|
+
logger.warn(
|
|
1866
|
+
'Failed to sync identity marker after persist (non-fatal)',
|
|
1867
|
+
{ component: 'KeyManager' },
|
|
1868
|
+
error,
|
|
1869
|
+
);
|
|
1738
1870
|
}
|
|
1739
1871
|
}
|
|
1740
1872
|
|
|
@@ -1756,24 +1888,46 @@ export class KeyManager {
|
|
|
1756
1888
|
const backupReadOpts = KeyManager._slotOpts(layout.backupService);
|
|
1757
1889
|
try {
|
|
1758
1890
|
if (priorBackupPrivate) {
|
|
1759
|
-
await store.setItemAsync(
|
|
1891
|
+
await store.setItemAsync(
|
|
1892
|
+
layout.backupPrivateKeyName,
|
|
1893
|
+
priorBackupPrivate,
|
|
1894
|
+
backupPrivWriteOpts,
|
|
1895
|
+
);
|
|
1760
1896
|
} else {
|
|
1761
|
-
try {
|
|
1897
|
+
try {
|
|
1898
|
+
await store.deleteItemAsync(layout.backupPrivateKeyName, backupReadOpts);
|
|
1899
|
+
} catch {
|
|
1900
|
+
/* best effort */
|
|
1901
|
+
}
|
|
1762
1902
|
}
|
|
1763
1903
|
|
|
1764
1904
|
if (priorBackupPublic) {
|
|
1765
1905
|
await store.setItemAsync(layout.backupPublicKeyName, priorBackupPublic, backupPubWriteOpts);
|
|
1766
1906
|
} else {
|
|
1767
|
-
try {
|
|
1907
|
+
try {
|
|
1908
|
+
await store.deleteItemAsync(layout.backupPublicKeyName, backupReadOpts);
|
|
1909
|
+
} catch {
|
|
1910
|
+
/* best effort */
|
|
1911
|
+
}
|
|
1768
1912
|
}
|
|
1769
1913
|
|
|
1770
1914
|
if (priorBackupTimestamp) {
|
|
1771
|
-
await store.setItemAsync(
|
|
1915
|
+
await store.setItemAsync(
|
|
1916
|
+
layout.backupTimestampName,
|
|
1917
|
+
priorBackupTimestamp,
|
|
1918
|
+
backupPubWriteOpts,
|
|
1919
|
+
);
|
|
1772
1920
|
} else {
|
|
1773
|
-
try {
|
|
1921
|
+
try {
|
|
1922
|
+
await store.deleteItemAsync(layout.backupTimestampName, backupReadOpts);
|
|
1923
|
+
} catch {
|
|
1924
|
+
/* best effort */
|
|
1925
|
+
}
|
|
1774
1926
|
}
|
|
1775
1927
|
} catch (rollbackError) {
|
|
1776
|
-
logger.error('Failed to roll back identity backup after a failed refresh', rollbackError, {
|
|
1928
|
+
logger.error('Failed to roll back identity backup after a failed refresh', rollbackError, {
|
|
1929
|
+
component: 'KeyManager',
|
|
1930
|
+
});
|
|
1777
1931
|
}
|
|
1778
1932
|
}
|
|
1779
1933
|
|
|
@@ -1803,11 +1957,21 @@ export class KeyManager {
|
|
|
1803
1957
|
} else {
|
|
1804
1958
|
// There was no prior identity — leave the device empty rather than
|
|
1805
1959
|
// half-written so hasIdentity() does not lie.
|
|
1806
|
-
try {
|
|
1807
|
-
|
|
1960
|
+
try {
|
|
1961
|
+
await store.deleteItemAsync(layout.primaryPublicKeyName, primaryReadOpts);
|
|
1962
|
+
} catch {
|
|
1963
|
+
/* best effort */
|
|
1964
|
+
}
|
|
1965
|
+
try {
|
|
1966
|
+
await store.deleteItemAsync(layout.primaryPrivateKeyName, primaryReadOpts);
|
|
1967
|
+
} catch {
|
|
1968
|
+
/* best effort */
|
|
1969
|
+
}
|
|
1808
1970
|
}
|
|
1809
1971
|
} catch (rollbackError) {
|
|
1810
|
-
logger.error('Failed to roll back primary identity after a failed write', rollbackError, {
|
|
1972
|
+
logger.error('Failed to roll back primary identity after a failed write', rollbackError, {
|
|
1973
|
+
component: 'KeyManager',
|
|
1974
|
+
});
|
|
1811
1975
|
} finally {
|
|
1812
1976
|
// Whatever happened, the cached verdict is no longer trustworthy.
|
|
1813
1977
|
KeyManager.invalidateCache();
|
|
@@ -1825,7 +1989,9 @@ export class KeyManager {
|
|
|
1825
1989
|
*/
|
|
1826
1990
|
static async createIdentity(options?: { overwrite?: boolean }): Promise<string> {
|
|
1827
1991
|
if (isWebPlatform()) {
|
|
1828
|
-
throw new Error(
|
|
1992
|
+
throw new Error(
|
|
1993
|
+
'Identity creation is only available on native platforms (iOS/Android). Please use the native app to create your identity.',
|
|
1994
|
+
);
|
|
1829
1995
|
}
|
|
1830
1996
|
|
|
1831
1997
|
// CRITICAL SAFEGUARD: never silently overwrite an existing identity.
|
|
@@ -1861,7 +2027,10 @@ export class KeyManager {
|
|
|
1861
2027
|
*
|
|
1862
2028
|
* @internal
|
|
1863
2029
|
*/
|
|
1864
|
-
private static async _readPrimaryDirect(): Promise<{
|
|
2030
|
+
private static async _readPrimaryDirect(): Promise<{
|
|
2031
|
+
privateKey: string | null;
|
|
2032
|
+
publicKey: string | null;
|
|
2033
|
+
}> {
|
|
1865
2034
|
const migration = await KeyManager._ensureIdentitySlotsMigrated();
|
|
1866
2035
|
if (migration.mode === 'deferred') {
|
|
1867
2036
|
throw new IdentityUnavailableError(
|
|
@@ -1877,7 +2046,10 @@ export class KeyManager {
|
|
|
1877
2046
|
const publicKey = await store.getItemAsync(layout.primaryPublicKeyName, readOpts);
|
|
1878
2047
|
return { privateKey, publicKey };
|
|
1879
2048
|
} catch (error) {
|
|
1880
|
-
throw new IdentityUnavailableError(
|
|
2049
|
+
throw new IdentityUnavailableError(
|
|
2050
|
+
'Could not read existing identity; refusing to write blind.',
|
|
2051
|
+
error,
|
|
2052
|
+
);
|
|
1881
2053
|
}
|
|
1882
2054
|
}
|
|
1883
2055
|
|
|
@@ -1894,7 +2066,9 @@ export class KeyManager {
|
|
|
1894
2066
|
options?: { overwrite?: boolean },
|
|
1895
2067
|
): Promise<string> {
|
|
1896
2068
|
if (isWebPlatform()) {
|
|
1897
|
-
throw new Error(
|
|
2069
|
+
throw new Error(
|
|
2070
|
+
'Identity import is only available on native platforms (iOS/Android). Please use the native app to import your identity.',
|
|
2071
|
+
);
|
|
1898
2072
|
}
|
|
1899
2073
|
|
|
1900
2074
|
if (!KeyManager.isValidPrivateKey(privateKey)) {
|
|
@@ -2061,7 +2235,11 @@ export class KeyManager {
|
|
|
2061
2235
|
if (KeyManager.deviceBackupStore) {
|
|
2062
2236
|
const primary = await KeyManager._readHealthyPrimary();
|
|
2063
2237
|
if (primary && (await KeyManager._phraseDerivesPublicKey(mnemonic, primary.publicKey))) {
|
|
2064
|
-
await KeyManager._writeDeviceBackup(
|
|
2238
|
+
await KeyManager._writeDeviceBackup(
|
|
2239
|
+
primary.privateKey,
|
|
2240
|
+
primary.publicKey,
|
|
2241
|
+
mnemonic.trim().toLowerCase(),
|
|
2242
|
+
);
|
|
2065
2243
|
}
|
|
2066
2244
|
}
|
|
2067
2245
|
}
|
|
@@ -2091,7 +2269,10 @@ export class KeyManager {
|
|
|
2091
2269
|
if (isDev()) {
|
|
2092
2270
|
logger.warn('Failed to read recovery mnemonic', { component: 'KeyManager' }, error);
|
|
2093
2271
|
}
|
|
2094
|
-
throw new IdentityUnavailableError(
|
|
2272
|
+
throw new IdentityUnavailableError(
|
|
2273
|
+
'Failed to read recovery mnemonic from secure storage.',
|
|
2274
|
+
error,
|
|
2275
|
+
);
|
|
2095
2276
|
}
|
|
2096
2277
|
}
|
|
2097
2278
|
|
|
@@ -2105,7 +2286,11 @@ export class KeyManager {
|
|
|
2105
2286
|
return; // Identity storage is only available on native platforms
|
|
2106
2287
|
}
|
|
2107
2288
|
const store = await initSecureStore();
|
|
2108
|
-
await KeyManager._bestEffortDelete(
|
|
2289
|
+
await KeyManager._bestEffortDelete(
|
|
2290
|
+
store,
|
|
2291
|
+
RECOVERY_MNEMONIC_STORAGE_KEY,
|
|
2292
|
+
RECOVERY_MNEMONIC_KEYCHAIN_SERVICE,
|
|
2293
|
+
);
|
|
2109
2294
|
}
|
|
2110
2295
|
|
|
2111
2296
|
/**
|
|
@@ -2132,7 +2317,10 @@ export class KeyManager {
|
|
|
2132
2317
|
|
|
2133
2318
|
const migration = await KeyManager._ensureIdentitySlotsMigrated();
|
|
2134
2319
|
if (migration.mode === 'deferred') {
|
|
2135
|
-
throw new IdentityUnavailableError(
|
|
2320
|
+
throw new IdentityUnavailableError(
|
|
2321
|
+
'Identity storage is temporarily unavailable.',
|
|
2322
|
+
migration.cause,
|
|
2323
|
+
);
|
|
2136
2324
|
}
|
|
2137
2325
|
|
|
2138
2326
|
let privateKey: string | null;
|
|
@@ -2140,15 +2328,23 @@ export class KeyManager {
|
|
|
2140
2328
|
try {
|
|
2141
2329
|
const store = await initSecureStore();
|
|
2142
2330
|
[privateKey, publicKey] = await Promise.all([
|
|
2143
|
-
store.getItemAsync(
|
|
2144
|
-
|
|
2331
|
+
store.getItemAsync(
|
|
2332
|
+
migration.layout.primaryPrivateKeyName,
|
|
2333
|
+
KeyManager._slotOpts(migration.layout.primaryService),
|
|
2334
|
+
),
|
|
2335
|
+
store.getItemAsync(
|
|
2336
|
+
migration.layout.primaryPublicKeyName,
|
|
2337
|
+
KeyManager._slotOpts(migration.layout.primaryService),
|
|
2338
|
+
),
|
|
2145
2339
|
]);
|
|
2146
2340
|
} catch (error) {
|
|
2147
2341
|
// Storage threw — could be a transient keychain lock (e.g., background
|
|
2148
2342
|
// fetch before the device is unlocked). Do NOT cache; throw a TYPED error
|
|
2149
2343
|
// so callers distinguish "temporarily unavailable" from "genuinely absent"
|
|
2150
2344
|
// instead of silently treating a locked keystore as a blank device.
|
|
2151
|
-
logger.error('Failed to read identity from secure storage', error, {
|
|
2345
|
+
logger.error('Failed to read identity from secure storage', error, {
|
|
2346
|
+
component: 'KeyManager',
|
|
2347
|
+
});
|
|
2152
2348
|
throw new IdentityUnavailableError('Failed to read identity from secure storage.', error);
|
|
2153
2349
|
}
|
|
2154
2350
|
|
|
@@ -2163,9 +2359,7 @@ export class KeyManager {
|
|
|
2163
2359
|
// sign-in flow when SignatureService.sign() can't find the keypair.
|
|
2164
2360
|
if (KeyManager.isValidPrivateKey(privateKey) && KeyManager.isValidPublicKey(publicKey)) {
|
|
2165
2361
|
try {
|
|
2166
|
-
const derived = deriveSecp256k1PublicKey(
|
|
2167
|
-
KeyManager.canonicalPrivateKey(privateKey),
|
|
2168
|
-
);
|
|
2362
|
+
const derived = deriveSecp256k1PublicKey(KeyManager.canonicalPrivateKey(privateKey));
|
|
2169
2363
|
// Hex equality is case-insensitive; normalize on both sides to
|
|
2170
2364
|
// tolerate legacy uppercase-stored public keys.
|
|
2171
2365
|
hasIdentity = derived.toLowerCase() === publicKey.toLowerCase();
|
|
@@ -2183,10 +2377,9 @@ export class KeyManager {
|
|
|
2183
2377
|
);
|
|
2184
2378
|
}
|
|
2185
2379
|
} else {
|
|
2186
|
-
logger.warn(
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
);
|
|
2380
|
+
logger.warn('KeyManager.hasIdentity: stored key material is malformed', {
|
|
2381
|
+
component: 'KeyManager',
|
|
2382
|
+
});
|
|
2190
2383
|
}
|
|
2191
2384
|
}
|
|
2192
2385
|
|
|
@@ -2300,16 +2493,18 @@ export class KeyManager {
|
|
|
2300
2493
|
* @param userConfirmed - If true, user has explicitly confirmed deletion (default: false)
|
|
2301
2494
|
*/
|
|
2302
2495
|
static async deleteIdentity(
|
|
2303
|
-
skipBackup = false,
|
|
2496
|
+
skipBackup = false,
|
|
2304
2497
|
force = false,
|
|
2305
|
-
userConfirmed = false
|
|
2498
|
+
userConfirmed = false,
|
|
2306
2499
|
): Promise<void> {
|
|
2307
2500
|
if (isWebPlatform()) {
|
|
2308
2501
|
return; // Identity storage is only available on native platforms, nothing to delete
|
|
2309
2502
|
}
|
|
2310
2503
|
// CRITICAL SAFEGUARD: Require explicit user confirmation unless force is true
|
|
2311
2504
|
if (!force && !userConfirmed) {
|
|
2312
|
-
throw new Error(
|
|
2505
|
+
throw new Error(
|
|
2506
|
+
'Identity deletion requires explicit user confirmation. This is a safety measure to prevent accidental data loss.',
|
|
2507
|
+
);
|
|
2313
2508
|
}
|
|
2314
2509
|
|
|
2315
2510
|
if (!force) {
|
|
@@ -2328,11 +2523,17 @@ export class KeyManager {
|
|
|
2328
2523
|
try {
|
|
2329
2524
|
const backupSuccess = await KeyManager.backupIdentity();
|
|
2330
2525
|
if (!backupSuccess && isDev()) {
|
|
2331
|
-
logger.warn('Failed to backup identity before deletion - proceeding anyway', {
|
|
2526
|
+
logger.warn('Failed to backup identity before deletion - proceeding anyway', {
|
|
2527
|
+
component: 'KeyManager',
|
|
2528
|
+
});
|
|
2332
2529
|
}
|
|
2333
2530
|
} catch (backupError) {
|
|
2334
2531
|
if (isDev()) {
|
|
2335
|
-
logger.warn(
|
|
2532
|
+
logger.warn(
|
|
2533
|
+
'Failed to backup identity before deletion',
|
|
2534
|
+
{ component: 'KeyManager' },
|
|
2535
|
+
backupError,
|
|
2536
|
+
);
|
|
2336
2537
|
}
|
|
2337
2538
|
}
|
|
2338
2539
|
}
|
|
@@ -2378,7 +2579,11 @@ export class KeyManager {
|
|
|
2378
2579
|
try {
|
|
2379
2580
|
await clearIdentityMarker();
|
|
2380
2581
|
} catch (error) {
|
|
2381
|
-
logger.warn(
|
|
2582
|
+
logger.warn(
|
|
2583
|
+
'Failed to clear identity marker during delete',
|
|
2584
|
+
{ component: 'KeyManager' },
|
|
2585
|
+
error,
|
|
2586
|
+
);
|
|
2382
2587
|
}
|
|
2383
2588
|
|
|
2384
2589
|
// Invalidate cache LAST — its subscriber fan-out fires only after both the
|
|
@@ -2418,8 +2623,16 @@ export class KeyManager {
|
|
|
2418
2623
|
privateKey,
|
|
2419
2624
|
KeyManager._privateWriteOpts(store, layout.backupService),
|
|
2420
2625
|
);
|
|
2421
|
-
await store.setItemAsync(
|
|
2422
|
-
|
|
2626
|
+
await store.setItemAsync(
|
|
2627
|
+
layout.backupPublicKeyName,
|
|
2628
|
+
publicKey,
|
|
2629
|
+
KeyManager._slotOpts(layout.backupService),
|
|
2630
|
+
);
|
|
2631
|
+
await store.setItemAsync(
|
|
2632
|
+
layout.backupTimestampName,
|
|
2633
|
+
Date.now().toString(),
|
|
2634
|
+
KeyManager._slotOpts(layout.backupService),
|
|
2635
|
+
);
|
|
2423
2636
|
|
|
2424
2637
|
return true;
|
|
2425
2638
|
} catch (error) {
|
|
@@ -2473,7 +2686,9 @@ export class KeyManager {
|
|
|
2473
2686
|
const probeHash = '0'.repeat(64);
|
|
2474
2687
|
const signature = signSecp256k1Digest(canonicalPrivateKey, probeHash);
|
|
2475
2688
|
if (!verifySecp256k1Digest(publicKey, probeHash, signature)) {
|
|
2476
|
-
logger.error('Identity sign/verify probe failed during integrity check', undefined, {
|
|
2689
|
+
logger.error('Identity sign/verify probe failed during integrity check', undefined, {
|
|
2690
|
+
component: 'KeyManager',
|
|
2691
|
+
});
|
|
2477
2692
|
return false;
|
|
2478
2693
|
}
|
|
2479
2694
|
|
|
@@ -2554,15 +2769,23 @@ export class KeyManager {
|
|
|
2554
2769
|
}
|
|
2555
2770
|
|
|
2556
2771
|
// Load + validate the backup.
|
|
2557
|
-
const backupPrivateKey = await store.getItemAsync(
|
|
2772
|
+
const backupPrivateKey = await store.getItemAsync(
|
|
2773
|
+
layout.backupPrivateKeyName,
|
|
2774
|
+
backupReadOpts,
|
|
2775
|
+
);
|
|
2558
2776
|
const backupPublicKey = await store.getItemAsync(layout.backupPublicKeyName, backupReadOpts);
|
|
2559
2777
|
|
|
2560
2778
|
if (!backupPrivateKey || !backupPublicKey) {
|
|
2561
2779
|
return false; // No backup available
|
|
2562
2780
|
}
|
|
2563
2781
|
|
|
2564
|
-
if (
|
|
2565
|
-
|
|
2782
|
+
if (
|
|
2783
|
+
!KeyManager.isValidPrivateKey(backupPrivateKey) ||
|
|
2784
|
+
!KeyManager.isValidPublicKey(backupPublicKey)
|
|
2785
|
+
) {
|
|
2786
|
+
logger.warn('Backup identity is malformed; refusing to restore', {
|
|
2787
|
+
component: 'KeyManager',
|
|
2788
|
+
});
|
|
2566
2789
|
return false;
|
|
2567
2790
|
}
|
|
2568
2791
|
|
|
@@ -2571,7 +2794,9 @@ export class KeyManager {
|
|
|
2571
2794
|
// be considered valid.
|
|
2572
2795
|
const derivedPublicKey = KeyManager.derivePublicKey(backupPrivateKey);
|
|
2573
2796
|
if (derivedPublicKey.toLowerCase() !== backupPublicKey.toLowerCase()) {
|
|
2574
|
-
logger.warn('Backup public key does not match derived; refusing to restore', {
|
|
2797
|
+
logger.warn('Backup public key does not match derived; refusing to restore', {
|
|
2798
|
+
component: 'KeyManager',
|
|
2799
|
+
});
|
|
2575
2800
|
return false;
|
|
2576
2801
|
}
|
|
2577
2802
|
|
|
@@ -2581,10 +2806,7 @@ export class KeyManager {
|
|
|
2581
2806
|
// the user. We check the private key too (not just the public): a
|
|
2582
2807
|
// present private key that derives to a non-backup public means a real,
|
|
2583
2808
|
// different identity is sitting in the primary slot.
|
|
2584
|
-
if (
|
|
2585
|
-
primaryPublic &&
|
|
2586
|
-
primaryPublic.toLowerCase() !== backupPublicKey.toLowerCase()
|
|
2587
|
-
) {
|
|
2809
|
+
if (primaryPublic && primaryPublic.toLowerCase() !== backupPublicKey.toLowerCase()) {
|
|
2588
2810
|
logger.error(
|
|
2589
2811
|
'Primary public key is present, corrupt-or-mismatched, AND differs from the backup. Refusing to restore to avoid switching accounts.',
|
|
2590
2812
|
undefined,
|
|
@@ -2610,7 +2832,9 @@ export class KeyManager {
|
|
|
2610
2832
|
try {
|
|
2611
2833
|
await KeyManager._persistIdentityAtomic(backupPrivateKey, backupPublicKey, 'restore');
|
|
2612
2834
|
} catch (error) {
|
|
2613
|
-
logger.error('Failed to persist identity restored from backup', error, {
|
|
2835
|
+
logger.error('Failed to persist identity restored from backup', error, {
|
|
2836
|
+
component: 'KeyManager',
|
|
2837
|
+
});
|
|
2614
2838
|
return false;
|
|
2615
2839
|
}
|
|
2616
2840
|
|
|
@@ -2685,7 +2909,9 @@ export class KeyManager {
|
|
|
2685
2909
|
const backupCandidate = await KeyManager._readBackupCandidate();
|
|
2686
2910
|
if (backupCandidate) {
|
|
2687
2911
|
if (backupCandidate.publicKey.toLowerCase() === expectedPublic) {
|
|
2688
|
-
if (
|
|
2912
|
+
if (
|
|
2913
|
+
await KeyManager._commitRecovery(backupCandidate.privateKey, backupCandidate.publicKey)
|
|
2914
|
+
) {
|
|
2689
2915
|
return { recovered: true, source: 'backup', publicKey: backupCandidate.publicKey };
|
|
2690
2916
|
}
|
|
2691
2917
|
} else {
|
|
@@ -2697,7 +2923,9 @@ export class KeyManager {
|
|
|
2697
2923
|
const sharedCandidate = await KeyManager._readSharedCandidate();
|
|
2698
2924
|
if (sharedCandidate) {
|
|
2699
2925
|
if (sharedCandidate.publicKey.toLowerCase() === expectedPublic) {
|
|
2700
|
-
if (
|
|
2926
|
+
if (
|
|
2927
|
+
await KeyManager._commitRecovery(sharedCandidate.privateKey, sharedCandidate.publicKey)
|
|
2928
|
+
) {
|
|
2701
2929
|
return { recovered: true, source: 'shared', publicKey: sharedCandidate.publicKey };
|
|
2702
2930
|
}
|
|
2703
2931
|
} else {
|
|
@@ -2739,7 +2967,11 @@ export class KeyManager {
|
|
|
2739
2967
|
publicKey: record.publicKey.toLowerCase(),
|
|
2740
2968
|
};
|
|
2741
2969
|
} catch (error) {
|
|
2742
|
-
logger.warn(
|
|
2970
|
+
logger.warn(
|
|
2971
|
+
`Recovery: failed to read the device backup (${store.name})`,
|
|
2972
|
+
{ component: 'KeyManager' },
|
|
2973
|
+
error,
|
|
2974
|
+
);
|
|
2743
2975
|
return null;
|
|
2744
2976
|
}
|
|
2745
2977
|
}
|
|
@@ -2753,18 +2985,28 @@ export class KeyManager {
|
|
|
2753
2985
|
if (!(await KeyManager._commitRecovery(record.privateKey, record.publicKey))) {
|
|
2754
2986
|
return false;
|
|
2755
2987
|
}
|
|
2756
|
-
if (
|
|
2988
|
+
if (
|
|
2989
|
+
record.mnemonic &&
|
|
2990
|
+
(await KeyManager._phraseDerivesPublicKey(record.mnemonic, record.publicKey))
|
|
2991
|
+
) {
|
|
2757
2992
|
try {
|
|
2758
2993
|
await KeyManager.storeRecoveryMnemonic(record.mnemonic);
|
|
2759
2994
|
} catch (error) {
|
|
2760
|
-
logger.warn(
|
|
2995
|
+
logger.warn(
|
|
2996
|
+
'Recovery: restored the identity but not its phrase slot',
|
|
2997
|
+
{ component: 'KeyManager' },
|
|
2998
|
+
error,
|
|
2999
|
+
);
|
|
2761
3000
|
}
|
|
2762
3001
|
}
|
|
2763
3002
|
return true;
|
|
2764
3003
|
}
|
|
2765
3004
|
|
|
2766
3005
|
/** Read the active-layout backup slot as a healthy candidate, or null. @internal */
|
|
2767
|
-
private static async _readBackupCandidate(): Promise<{
|
|
3006
|
+
private static async _readBackupCandidate(): Promise<{
|
|
3007
|
+
privateKey: string;
|
|
3008
|
+
publicKey: string;
|
|
3009
|
+
} | null> {
|
|
2768
3010
|
try {
|
|
2769
3011
|
const migration = await KeyManager._ensureIdentitySlotsMigrated();
|
|
2770
3012
|
if (migration.mode === 'deferred') {
|
|
@@ -2790,7 +3032,10 @@ export class KeyManager {
|
|
|
2790
3032
|
* keychain group, or on Android Commons' own signer store (read in-process;
|
|
2791
3033
|
* Commons never gets it back over IPC). @internal
|
|
2792
3034
|
*/
|
|
2793
|
-
private static async _readSharedCandidate(): Promise<{
|
|
3035
|
+
private static async _readSharedCandidate(): Promise<{
|
|
3036
|
+
privateKey: string;
|
|
3037
|
+
publicKey: string;
|
|
3038
|
+
} | null> {
|
|
2794
3039
|
try {
|
|
2795
3040
|
let privateKey: string | null = null;
|
|
2796
3041
|
let publicKey: string | null = null;
|
|
@@ -2819,7 +3064,9 @@ export class KeyManager {
|
|
|
2819
3064
|
KeyManager.invalidateCache();
|
|
2820
3065
|
return true;
|
|
2821
3066
|
} catch (error) {
|
|
2822
|
-
logger.error('Recovery: failed to persist recovered identity', error, {
|
|
3067
|
+
logger.error('Recovery: failed to persist recovered identity', error, {
|
|
3068
|
+
component: 'KeyManager',
|
|
3069
|
+
});
|
|
2823
3070
|
return false;
|
|
2824
3071
|
}
|
|
2825
3072
|
}
|
|
@@ -2966,7 +3213,10 @@ export class KeyManager {
|
|
|
2966
3213
|
* @returns the signature, or `null` on web / when no identity key is
|
|
2967
3214
|
* available to this app.
|
|
2968
3215
|
*/
|
|
2969
|
-
static async signSocialReceive(
|
|
3216
|
+
static async signSocialReceive(
|
|
3217
|
+
index: number,
|
|
3218
|
+
digest: string,
|
|
3219
|
+
): Promise<CommonsSocialReceiveSignature | null> {
|
|
2970
3220
|
if (isWebPlatform()) {
|
|
2971
3221
|
return null;
|
|
2972
3222
|
}
|