@oxy.so/core 7.30.0 → 7.32.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 +146 -23
- package/dist/cjs/crypto/identityMarker.js +15 -0
- package/dist/cjs/crypto/index.js +7 -2
- package/dist/cjs/crypto/keyManager.js +7 -10
- package/dist/cjs/crypto/keychainGroupMigration.js +327 -160
- package/dist/cjs/crypto/organizationKeys.js +33 -5
- package/dist/cjs/crypto/retiredKeys.js +80 -18
- package/dist/cjs/session/authStateStore.js +13 -0
- package/dist/cjs/session/index.js +5 -2
- package/dist/cjs/utils/keychainAccessGroups.js +208 -51
- package/dist/esm/api/identity.js +146 -23
- package/dist/esm/crypto/identityMarker.js +14 -0
- package/dist/esm/crypto/index.js +3 -3
- package/dist/esm/crypto/keyManager.js +9 -12
- package/dist/esm/crypto/keychainGroupMigration.js +327 -162
- package/dist/esm/crypto/organizationKeys.js +34 -6
- package/dist/esm/crypto/retiredKeys.js +76 -17
- package/dist/esm/session/authStateStore.js +12 -0
- package/dist/esm/session/index.js +1 -1
- package/dist/esm/utils/keychainAccessGroups.js +205 -51
- package/dist/types/api/identity.d.ts +36 -0
- package/dist/types/crypto/identityMarker.d.ts +8 -0
- package/dist/types/crypto/index.d.ts +4 -4
- package/dist/types/crypto/keychainGroupMigration.d.ts +28 -6
- package/dist/types/crypto/organizationKeys.d.ts +4 -2
- package/dist/types/crypto/retiredKeys.d.ts +30 -4
- package/dist/types/session/authStateStore.d.ts +9 -0
- package/dist/types/session/index.d.ts +2 -2
- package/dist/types/utils/keychainAccessGroups.d.ts +108 -26
- package/package.json +2 -2
- package/src/api/identity.ts +185 -40
- package/src/crypto/identityMarker.ts +15 -0
- package/src/crypto/index.ts +10 -1
- package/src/crypto/keyManager.ts +9 -15
- package/src/crypto/keychainGroupMigration.ts +396 -186
- package/src/crypto/organizationKeys.ts +38 -6
- package/src/crypto/retiredKeys.ts +87 -17
- package/src/session/authStateStore.ts +14 -0
- package/src/session/index.ts +8 -1
- package/src/utils/keychainAccessGroups.ts +276 -52
package/src/api/identity.ts
CHANGED
|
@@ -47,6 +47,7 @@ import type { OxyContext } from '../client/context';
|
|
|
47
47
|
import type { PendingIdentityResult } from '../crypto/recoveryPhrase';
|
|
48
48
|
import { isAndroid, isWeb } from '../utils/platform';
|
|
49
49
|
import { logger } from '../logger';
|
|
50
|
+
import { extractErrorStatus } from '../utils/errorUtils';
|
|
50
51
|
|
|
51
52
|
/** Short-TTL read cache for public identity reads. */
|
|
52
53
|
const SHORT_TTL = 60 * 1000;
|
|
@@ -128,6 +129,14 @@ export interface RotateKeyResult {
|
|
|
128
129
|
* should surface a recovery prompt. Omitted on full success.
|
|
129
130
|
*/
|
|
130
131
|
localPersistFailed?: true;
|
|
132
|
+
/**
|
|
133
|
+
* Whether the encrypted identity backup was re-created under the NEW phrase,
|
|
134
|
+
* carrying the retired ring with the key just replaced (R4-4). The server
|
|
135
|
+
* deletes the old backup in the rotation's transaction; `false` (native, the
|
|
136
|
+
* upload failed) means the caller should offer "Create backup" again, or the
|
|
137
|
+
* replaced key lives only on this phone. Absent on web.
|
|
138
|
+
*/
|
|
139
|
+
backupRefreshed?: boolean;
|
|
131
140
|
}
|
|
132
141
|
|
|
133
142
|
/**
|
|
@@ -476,11 +485,36 @@ export class IdentityApi {
|
|
|
476
485
|
}
|
|
477
486
|
}
|
|
478
487
|
|
|
488
|
+
// 8. The rotation deleted the encrypted backup (it was sealed under the
|
|
489
|
+
// replaced phrase). Seal a new one under the new phrase, carrying the
|
|
490
|
+
// retired ring — now with the key just replaced — so the coins under it
|
|
491
|
+
// survive a lost phone as long as the NEW phrase does (R4-4). Always,
|
|
492
|
+
// not only when one existed: after a rotation it is the only copy of the
|
|
493
|
+
// replaced key off this phone. Best-effort: the rotation already succeeded.
|
|
494
|
+
let backupRefreshed: boolean | undefined;
|
|
495
|
+
if (!isWeb() && !localPersistFailed) {
|
|
496
|
+
try {
|
|
497
|
+
await this.backup.create(pending.phrase);
|
|
498
|
+
backupRefreshed = true;
|
|
499
|
+
} catch (backupError) {
|
|
500
|
+
backupRefreshed = false;
|
|
501
|
+
logger.warn(
|
|
502
|
+
'Key rotated, but the encrypted backup could not be re-created under the new phrase.',
|
|
503
|
+
{ component: 'oxy.identity', method: 'rotateKey' },
|
|
504
|
+
backupError,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
479
509
|
this.invalidate(userId);
|
|
480
510
|
|
|
481
|
-
return
|
|
482
|
-
|
|
483
|
-
|
|
511
|
+
return {
|
|
512
|
+
newPublicKey,
|
|
513
|
+
newPhrase: pending.phrase,
|
|
514
|
+
words: pending.words,
|
|
515
|
+
...(localPersistFailed ? { localPersistFailed: true as const } : {}),
|
|
516
|
+
...(backupRefreshed === undefined ? {} : { backupRefreshed }),
|
|
517
|
+
};
|
|
484
518
|
}
|
|
485
519
|
|
|
486
520
|
/**
|
|
@@ -736,11 +770,29 @@ const BACKUP_ALGORITHM = 'xchacha20poly1305' as const;
|
|
|
736
770
|
*/
|
|
737
771
|
const PUBLIC_KEY_HINT_LENGTH = 16;
|
|
738
772
|
|
|
739
|
-
/**
|
|
773
|
+
/** One retired key carried in the backup (R4-4). */
|
|
774
|
+
interface BackupRetiredKey {
|
|
775
|
+
privateKey: string;
|
|
776
|
+
publicKey: string;
|
|
777
|
+
retiredAt: number;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* The decrypted backup payload. `retiredKeys` (since core 2026-11, optional,
|
|
782
|
+
* ignored by older readers) carries the person's retired ring, so the CURRENT
|
|
783
|
+
* phrase restores the keys their earlier coins sit under (ADR 0061 D7 as
|
|
784
|
+
* amended): a lost phone then costs no more than the current phrase does.
|
|
785
|
+
*/
|
|
740
786
|
interface BackupPayload {
|
|
741
787
|
privateKey: string;
|
|
742
788
|
publicKey: string;
|
|
743
789
|
createdAt: string;
|
|
790
|
+
retiredKeys?: BackupRetiredKey[];
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** Whether a request error is the restore lookup's constant-shape 404. */
|
|
794
|
+
function isNotFound(error: unknown): boolean {
|
|
795
|
+
return extractErrorStatus(error) === 404;
|
|
744
796
|
}
|
|
745
797
|
|
|
746
798
|
/** Encode bytes as lowercase hex (cross-platform, no Buffer dependency). */
|
|
@@ -806,7 +858,13 @@ export class IdentityBackupApi {
|
|
|
806
858
|
const createdAt = new Date().toISOString();
|
|
807
859
|
const publicKeyHint = publicKey.slice(0, PUBLIC_KEY_HINT_LENGTH);
|
|
808
860
|
|
|
809
|
-
const
|
|
861
|
+
const retiredKeys = await this.retiredKeysToCarry(publicKey);
|
|
862
|
+
const payload: BackupPayload = {
|
|
863
|
+
privateKey,
|
|
864
|
+
publicKey,
|
|
865
|
+
createdAt,
|
|
866
|
+
...(retiredKeys.length > 0 ? { retiredKeys } : {}),
|
|
867
|
+
};
|
|
810
868
|
const plaintext = new TextEncoder().encode(JSON.stringify(payload));
|
|
811
869
|
const aad = buildBackupAad(BACKUP_ENVELOPE_VERSION, publicKeyHint);
|
|
812
870
|
const { nonce, ciphertext } = encryptAead(backupKey, plaintext, aad);
|
|
@@ -827,6 +885,120 @@ export class IdentityBackupApi {
|
|
|
827
885
|
});
|
|
828
886
|
}
|
|
829
887
|
|
|
888
|
+
/**
|
|
889
|
+
* The retired ring this device holds for the person, limited to keys Oxy's
|
|
890
|
+
* key history names as THIS account's retired keys (the ring's iOS group is
|
|
891
|
+
* writable by the wallet app too, and a previous person's ring can outlive a
|
|
892
|
+
* "Start over"). Empty where no ring is held (web, any app but Commons).
|
|
893
|
+
* Throws when the history cannot be read while a ring exists, so a backup
|
|
894
|
+
* never silently drops keys it should carry.
|
|
895
|
+
*/
|
|
896
|
+
private async retiredKeysToCarry(currentPublicKey: string): Promise<BackupRetiredKey[]> {
|
|
897
|
+
if (isWeb()) return [];
|
|
898
|
+
const { exportRetiredIdentityKeys } = await import('../crypto/index');
|
|
899
|
+
let held: BackupRetiredKey[];
|
|
900
|
+
try {
|
|
901
|
+
held = await exportRetiredIdentityKeys();
|
|
902
|
+
} catch {
|
|
903
|
+
return []; // Not Commons: this app holds no ring to carry.
|
|
904
|
+
}
|
|
905
|
+
if (held.length === 0) return [];
|
|
906
|
+
const history = await this.ctx.request<unknown>('GET', '/identity/key-history', undefined, {
|
|
907
|
+
cache: false,
|
|
908
|
+
});
|
|
909
|
+
const parsed = checkContract(identityKeyHistoryCheck, history);
|
|
910
|
+
if (!parsed) throw new Error('identity/key-history returned an unexpected response shape');
|
|
911
|
+
const retired = new Set(parsed.retired.map((entry) => entry.publicKey.toLowerCase()));
|
|
912
|
+
const current = currentPublicKey.toLowerCase();
|
|
913
|
+
return held.filter(
|
|
914
|
+
(key) => key.publicKey.toLowerCase() !== current && retired.has(key.publicKey.toLowerCase()),
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Fetch and open the backup the phrase locates, checking it is the phrase's
|
|
920
|
+
* own identity. With `missingIsNull`, `null` when there is none (404);
|
|
921
|
+
* otherwise the request's own error propagates.
|
|
922
|
+
*/
|
|
923
|
+
private async open(phrase: string, missingIsNull = false): Promise<BackupPayload | null> {
|
|
924
|
+
const { KeyManager, RecoveryPhraseService, decryptAead } = await import('../crypto/index');
|
|
925
|
+
const { backupKey, lookupId } = await RecoveryPhraseService.deriveBackupMaterial(phrase);
|
|
926
|
+
let envelope: EncryptedBackupEnvelope;
|
|
927
|
+
try {
|
|
928
|
+
envelope = await this.ctx.request<EncryptedBackupEnvelope>(
|
|
929
|
+
'GET',
|
|
930
|
+
`/identity/backup/${encodeURIComponent(lookupId)}`,
|
|
931
|
+
undefined,
|
|
932
|
+
{ cache: false },
|
|
933
|
+
);
|
|
934
|
+
} catch (error) {
|
|
935
|
+
if (missingIsNull && isNotFound(error)) return null;
|
|
936
|
+
throw error;
|
|
937
|
+
}
|
|
938
|
+
if (envelope.algorithm !== BACKUP_ALGORITHM) {
|
|
939
|
+
throw new Error(`Unsupported backup algorithm: ${envelope.algorithm}`);
|
|
940
|
+
}
|
|
941
|
+
const aad = buildBackupAad(envelope.version, envelope.publicKeyHint);
|
|
942
|
+
const plaintext = decryptAead(
|
|
943
|
+
backupKey,
|
|
944
|
+
fromHex(envelope.nonce),
|
|
945
|
+
fromHex(envelope.ciphertext),
|
|
946
|
+
aad,
|
|
947
|
+
);
|
|
948
|
+
const payload = JSON.parse(new TextDecoder().decode(plaintext)) as BackupPayload;
|
|
949
|
+
if (!payload.privateKey || !payload.publicKey) {
|
|
950
|
+
throw new Error('Backup payload is missing key material');
|
|
951
|
+
}
|
|
952
|
+
const phrasePk = (await RecoveryPhraseService.derivePublicKeyFromPhrase(phrase)).toLowerCase();
|
|
953
|
+
const payloadPk = payload.publicKey.toLowerCase();
|
|
954
|
+
const privatePk = KeyManager.derivePublicKey(payload.privateKey).toLowerCase();
|
|
955
|
+
if (phrasePk !== payloadPk || privatePk !== payloadPk) {
|
|
956
|
+
throw new Error('Backup payload does not match the recovery phrase');
|
|
957
|
+
}
|
|
958
|
+
return payload;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Keep every retired key a backup carries in this device's ring. Each pair
|
|
963
|
+
* must be healthy and must not be the current key; the envelope itself is
|
|
964
|
+
* authenticated by the phrase. Commons only (the ring refuses elsewhere).
|
|
965
|
+
* Resolves how many keys are now held; a key that fails is skipped.
|
|
966
|
+
*/
|
|
967
|
+
private async keepCarriedRetiredKeys(payload: BackupPayload): Promise<number> {
|
|
968
|
+
const { KeyManager, retireIdentityKey } = await import('../crypto/index');
|
|
969
|
+
let kept = 0;
|
|
970
|
+
for (const key of Array.isArray(payload.retiredKeys) ? payload.retiredKeys : []) {
|
|
971
|
+
try {
|
|
972
|
+
if (typeof key?.privateKey !== 'string' || typeof key.publicKey !== 'string') continue;
|
|
973
|
+
if (key.publicKey.toLowerCase() === payload.publicKey.toLowerCase()) continue;
|
|
974
|
+
if (
|
|
975
|
+
KeyManager.derivePublicKey(key.privateKey).toLowerCase() !== key.publicKey.toLowerCase()
|
|
976
|
+
) {
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
await retireIdentityKey({ privateKey: key.privateKey, publicKey: key.publicKey });
|
|
980
|
+
kept += 1;
|
|
981
|
+
} catch (error) {
|
|
982
|
+
logger.warn(
|
|
983
|
+
'Could not keep a retired key from the encrypted backup',
|
|
984
|
+
{ component: 'oxy.identity', method: 'backup.restoreRetiredKeys' },
|
|
985
|
+
error,
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return kept;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Restore ONLY the retired keys the phrase's backup carries into this
|
|
994
|
+
* device's ring (Commons, after a phrase import or a silent device-backup
|
|
995
|
+
* restore). Resolves how many were kept; 0 when there is no backup.
|
|
996
|
+
*/
|
|
997
|
+
async restoreRetiredKeys(phrase: string): Promise<number> {
|
|
998
|
+
const payload = await this.open(phrase, true);
|
|
999
|
+
return payload ? this.keepCarriedRetiredKeys(payload) : 0;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
830
1002
|
/**
|
|
831
1003
|
* Whether the signed-in user has a stored backup, plus its non-sensitive hint
|
|
832
1004
|
* and timestamp. Returns no ciphertext and no locator.
|
|
@@ -859,40 +1031,13 @@ export class IdentityBackupApi {
|
|
|
859
1031
|
* fails authentication (tamper), or an existing identity blocks the import.
|
|
860
1032
|
*/
|
|
861
1033
|
async restore(phrase: string, options?: { overwrite?: boolean }): Promise<string> {
|
|
862
|
-
const { KeyManager
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
);
|
|
871
|
-
|
|
872
|
-
if (envelope.algorithm !== BACKUP_ALGORITHM) {
|
|
873
|
-
throw new Error(`Unsupported backup algorithm: ${envelope.algorithm}`);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
const aad = buildBackupAad(envelope.version, envelope.publicKeyHint);
|
|
877
|
-
const plaintext = decryptAead(
|
|
878
|
-
backupKey,
|
|
879
|
-
fromHex(envelope.nonce),
|
|
880
|
-
fromHex(envelope.ciphertext),
|
|
881
|
-
aad,
|
|
882
|
-
);
|
|
883
|
-
const payload = JSON.parse(new TextDecoder().decode(plaintext)) as BackupPayload;
|
|
884
|
-
|
|
885
|
-
if (!payload.privateKey || !payload.publicKey) {
|
|
886
|
-
throw new Error('Backup payload is missing key material');
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
const phrasePk = (await RecoveryPhraseService.derivePublicKeyFromPhrase(phrase)).toLowerCase();
|
|
890
|
-
const payloadPk = payload.publicKey.toLowerCase();
|
|
891
|
-
const privatePk = KeyManager.derivePublicKey(payload.privateKey).toLowerCase();
|
|
892
|
-
if (phrasePk !== payloadPk || privatePk !== payloadPk) {
|
|
893
|
-
throw new Error('Backup payload does not match the recovery phrase');
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
return KeyManager.importKeyPair(payload.privateKey, { overwrite: options?.overwrite === true });
|
|
1034
|
+
const { KeyManager } = await import('../crypto/index');
|
|
1035
|
+
const payload = await this.open(phrase);
|
|
1036
|
+
if (!payload) throw new Error('No encrypted backup was found for this recovery phrase.');
|
|
1037
|
+
const publicKey = await KeyManager.importKeyPair(payload.privateKey, {
|
|
1038
|
+
overwrite: options?.overwrite === true,
|
|
1039
|
+
});
|
|
1040
|
+
await this.keepCarriedRetiredKeys(payload);
|
|
1041
|
+
return publicKey;
|
|
897
1042
|
}
|
|
898
1043
|
}
|
|
@@ -185,6 +185,21 @@ export async function readIdentityMarker(): Promise<IdentityMarker | null> {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Read the marker, THROWING when the storage cannot answer (instead of the
|
|
190
|
+
* fail-open `null` of {@link readIdentityMarker}). For a caller that must not
|
|
191
|
+
* mistake "unreadable" for "never written": the iOS keychain migration, which
|
|
192
|
+
* adopts a pre-split secret only when this marker vouches for it, and records
|
|
193
|
+
* once that there is nothing to adopt.
|
|
194
|
+
*/
|
|
195
|
+
export async function readIdentityMarkerStrict(): Promise<IdentityMarker | null> {
|
|
196
|
+
const storage = await getStorage();
|
|
197
|
+
if (!storage) {
|
|
198
|
+
throw new Error('The identity marker storage is unavailable.');
|
|
199
|
+
}
|
|
200
|
+
return deserialize(await storage.getItem(IDENTITY_MARKER_STORAGE_KEY));
|
|
201
|
+
}
|
|
202
|
+
|
|
188
203
|
/**
|
|
189
204
|
* Write (create/replace) the marker. Returns `true` when it durably landed,
|
|
190
205
|
* `false` when storage was unavailable or the write threw. Callers treat a
|
package/src/crypto/index.ts
CHANGED
|
@@ -51,6 +51,8 @@ export {
|
|
|
51
51
|
KeychainAccessUnavailableError,
|
|
52
52
|
keychainGroupFor,
|
|
53
53
|
mayHoldKeychainItem,
|
|
54
|
+
mayWriteKeychainItem,
|
|
55
|
+
DEV_KEYCHAIN_GROUP_SUFFIX,
|
|
54
56
|
resolveKeychainAccess,
|
|
55
57
|
keychainAccessGroupValue,
|
|
56
58
|
keychainReadPlan,
|
|
@@ -64,12 +66,17 @@ export {
|
|
|
64
66
|
export type {
|
|
65
67
|
KeychainAccess,
|
|
66
68
|
KeychainAccessGroupName,
|
|
69
|
+
KeychainGroup,
|
|
67
70
|
KeychainGroupOption,
|
|
68
71
|
KeychainItemClass,
|
|
69
72
|
KeychainRole,
|
|
70
73
|
KeychainStoreLike,
|
|
74
|
+
OwnKeychainItemNames,
|
|
71
75
|
} from '../utils/keychainAccessGroups';
|
|
72
|
-
export {
|
|
76
|
+
export {
|
|
77
|
+
ensureKeychainGroupsMigrated,
|
|
78
|
+
getPreSplitExposure,
|
|
79
|
+
} from './keychainGroupMigration';
|
|
73
80
|
export type {
|
|
74
81
|
KeychainGroupMigrationReport,
|
|
75
82
|
KeychainItemMigrationOutcome,
|
|
@@ -83,6 +90,8 @@ export {
|
|
|
83
90
|
forgetRetiredIdentityKeys,
|
|
84
91
|
syncRetiredKeys,
|
|
85
92
|
retiredKeyFromPhrase,
|
|
93
|
+
retiredKeyFromPrivateKey,
|
|
94
|
+
exportRetiredIdentityKeys,
|
|
86
95
|
} from './retiredKeys';
|
|
87
96
|
export type { RetiredIdentityKey, RetiredKeyPair, RetiredKeySelector } from './retiredKeys';
|
|
88
97
|
export { SignatureService } from './signatureService';
|
package/src/crypto/keyManager.ts
CHANGED
|
@@ -32,13 +32,13 @@ import { isDev, logger } from '../logger';
|
|
|
32
32
|
import { deriveScopedSeedFromKey, signSocialReceiveDigest } from './identityDerivations';
|
|
33
33
|
import { type IdentitySignerStore, registerIdentitySignerStore } from './identitySigner';
|
|
34
34
|
import { getOrganizationPrivateKey, isOrganizationAccountId } from './organizationKeys';
|
|
35
|
-
import {
|
|
35
|
+
import { getRetiredPrivateKey, retiredKeyScope } from './retiredKeys';
|
|
36
36
|
import {
|
|
37
37
|
type KeychainItemClass,
|
|
38
38
|
groupedKeychainStore,
|
|
39
39
|
resolveKeychainAccess,
|
|
40
40
|
} from '../utils/keychainAccessGroups';
|
|
41
|
-
import {
|
|
41
|
+
import { prepareKeychainAccess } from './keychainGroupMigration';
|
|
42
42
|
import {
|
|
43
43
|
RECOVERY_MNEMONIC_KEYCHAIN_SERVICE,
|
|
44
44
|
RECOVERY_MNEMONIC_STORAGE_KEY,
|
|
@@ -300,9 +300,7 @@ async function loadRawSecureStore(): Promise<ExpoSecureStoreLike> {
|
|
|
300
300
|
async function keychainItemStore(item: KeychainItemClass): Promise<ExpoSecureStoreLike> {
|
|
301
301
|
const raw = await loadRawSecureStore();
|
|
302
302
|
const access = await resolveKeychainAccess();
|
|
303
|
-
|
|
304
|
-
await ensureKeychainGroupsMigrated();
|
|
305
|
-
}
|
|
303
|
+
await prepareKeychainAccess(access);
|
|
306
304
|
return groupedKeychainStore(raw, access, item);
|
|
307
305
|
}
|
|
308
306
|
|
|
@@ -2612,16 +2610,12 @@ export class KeyManager {
|
|
|
2612
2610
|
if (force) {
|
|
2613
2611
|
await KeyManager._bestEffortDeleteBackupsAllGenerations(store);
|
|
2614
2612
|
await KeyManager._clearSharedSlot(store);
|
|
2615
|
-
// The
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
{ component: 'KeyManager' },
|
|
2622
|
-
error,
|
|
2623
|
-
);
|
|
2624
|
-
}
|
|
2613
|
+
// The retired ring is NOT cleared (R4-4): it is money under the person's
|
|
2614
|
+
// previous keys, and "Start over" or a forced delete must not cost it.
|
|
2615
|
+
// The same person restoring their identity finds it again; a different
|
|
2616
|
+
// identity on this phone cannot use it (each entry is checked against
|
|
2617
|
+
// that account's key history before use). Only deleting the Oxy ACCOUNT
|
|
2618
|
+
// clears it, explicitly (`forgetRetiredIdentityKeys`, Commons' flow).
|
|
2625
2619
|
}
|
|
2626
2620
|
|
|
2627
2621
|
// The device backup goes on EVERY delete, forced or not: it is restored
|