@oxy.so/core 7.32.0 → 7.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/api/identity.js +11 -3
- package/dist/cjs/crypto/index.js +6 -2
- package/dist/cjs/crypto/keyManager.js +108 -10
- package/dist/cjs/crypto/retiredKeys.js +246 -57
- package/dist/esm/api/identity.js +11 -3
- package/dist/esm/crypto/index.js +1 -1
- package/dist/esm/crypto/keyManager.js +109 -11
- package/dist/esm/crypto/retiredKeys.js +241 -57
- package/dist/types/crypto/index.d.ts +1 -1
- package/dist/types/crypto/keyManager.d.ts +45 -1
- package/dist/types/crypto/retiredKeys.d.ts +86 -21
- package/package.json +1 -1
- package/src/api/identity.ts +15 -3
- package/src/crypto/index.ts +4 -0
- package/src/crypto/keyManager.ts +123 -16
- package/src/crypto/retiredKeys.ts +293 -59
package/dist/cjs/api/identity.js
CHANGED
|
@@ -237,7 +237,7 @@ class IdentityApi {
|
|
|
237
237
|
try {
|
|
238
238
|
if (!oldPrivateKeyForRing)
|
|
239
239
|
throw new Error('no readable private key');
|
|
240
|
-
await retireIdentityKey({ privateKey: oldPrivateKeyForRing, publicKey: oldPublicKey });
|
|
240
|
+
await retireIdentityKey({ privateKey: oldPrivateKeyForRing, publicKey: oldPublicKey }, { ownerAccountId: userId });
|
|
241
241
|
}
|
|
242
242
|
catch (retainError) {
|
|
243
243
|
logger_1.logger.warn('Could not keep the current key as a retired key; not rotating.', { component: 'oxy.identity', method: 'rotateKey' }, retainError);
|
|
@@ -670,9 +670,12 @@ class IdentityBackupApi {
|
|
|
670
670
|
if ((0, platform_1.isWeb)())
|
|
671
671
|
return [];
|
|
672
672
|
const { exportRetiredIdentityKeys } = await Promise.resolve().then(() => __importStar(require('../crypto/index')));
|
|
673
|
+
const userId = this.ctx.oxy.session.userId;
|
|
674
|
+
if (!userId)
|
|
675
|
+
throw new Error('No authenticated user — sign in before backing up.');
|
|
673
676
|
let held;
|
|
674
677
|
try {
|
|
675
|
-
held = await exportRetiredIdentityKeys();
|
|
678
|
+
held = await exportRetiredIdentityKeys({ ownerAccountId: userId });
|
|
676
679
|
}
|
|
677
680
|
catch {
|
|
678
681
|
return []; // Not Commons: this app holds no ring to carry.
|
|
@@ -731,6 +734,11 @@ class IdentityBackupApi {
|
|
|
731
734
|
*/
|
|
732
735
|
async keepCarriedRetiredKeys(payload) {
|
|
733
736
|
const { KeyManager, retireIdentityKey } = await Promise.resolve().then(() => __importStar(require('../crypto/index')));
|
|
737
|
+
// The backup is the signed-in account's (its phrase opened it): its keys
|
|
738
|
+
// are kept for that account only (R5-2).
|
|
739
|
+
const ownerAccountId = this.ctx.oxy.session.userId;
|
|
740
|
+
if (!ownerAccountId)
|
|
741
|
+
throw new Error('No authenticated user — sign in before restoring.');
|
|
734
742
|
let kept = 0;
|
|
735
743
|
for (const key of Array.isArray(payload.retiredKeys) ? payload.retiredKeys : []) {
|
|
736
744
|
try {
|
|
@@ -741,7 +749,7 @@ class IdentityBackupApi {
|
|
|
741
749
|
if (KeyManager.derivePublicKey(key.privateKey).toLowerCase() !== key.publicKey.toLowerCase()) {
|
|
742
750
|
continue;
|
|
743
751
|
}
|
|
744
|
-
await retireIdentityKey({ privateKey: key.privateKey, publicKey: key.publicKey });
|
|
752
|
+
await retireIdentityKey({ privateKey: key.privateKey, publicKey: key.publicKey }, { ownerAccountId });
|
|
745
753
|
kept += 1;
|
|
746
754
|
}
|
|
747
755
|
catch (error) {
|
package/dist/cjs/crypto/index.js
CHANGED
|
@@ -12,8 +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.
|
|
16
|
-
exports.verifySignature = exports.signMessage = exports.loadCommonsIdentityBridge = exports.canonicalize = exports.createContentHasher = exports.solveRegistrationPow = exports.deriveIdentityLinkCode = exports.signIdentityProof = exports.digestIdentityPayload = exports.authChallengeDigest = exports.signSocialReceiveDigest = exports.deriveScopedSeedFromKey = exports.MAX_SOCIAL_RECEIVE_INDEX = exports.SOCIAL_RECEIVE_CHAIN_CODE_KEY = exports.SCOPED_SEED_KDF_SALT = exports.deriveSharedSecret = void 0;
|
|
15
|
+
exports.hkdfSha256 = exports.BACKUP_KDF_ENCRYPTION_INFO = exports.RecoveryPhraseService = exports.SignatureService = exports.RetiredKeyRingUnreadableError = exports.unmirrorPersonalRetiredKeys = exports.countQuarantinedRetiredKeys = exports.claimRetiredIdentityKeys = exports.exportRetiredIdentityKeys = exports.retiredKeyFromPrivateKey = exports.retiredKeyFromPhrase = exports.syncRetiredKeys = exports.forgetRetiredIdentityKeys = exports.forgetRetiredIdentityKey = exports.listRetiredIdentityKeys = exports.retireIdentityKey = exports.getPreSplitExposure = exports.ensureKeychainGroupsMigrated = exports.groupedKeychainStore = exports.deleteThroughPlan = exports.readThroughPlan = exports.ownKeychainItemPlan = exports.keychainWriteOption = exports.keychainDeletePlan = exports.keychainReadPlan = exports.keychainAccessGroupValue = exports.resolveKeychainAccess = exports.DEV_KEYCHAIN_GROUP_SUFFIX = exports.mayWriteKeychainItem = 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 = 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 = void 0;
|
|
17
17
|
// MUST stay the first import: the shim has to exist before any module below
|
|
18
18
|
// evaluates or draws randomness (see `./polyfill`).
|
|
19
19
|
require("./polyfill");
|
|
@@ -69,6 +69,10 @@ Object.defineProperty(exports, "syncRetiredKeys", { enumerable: true, get: funct
|
|
|
69
69
|
Object.defineProperty(exports, "retiredKeyFromPhrase", { enumerable: true, get: function () { return retiredKeys_1.retiredKeyFromPhrase; } });
|
|
70
70
|
Object.defineProperty(exports, "retiredKeyFromPrivateKey", { enumerable: true, get: function () { return retiredKeys_1.retiredKeyFromPrivateKey; } });
|
|
71
71
|
Object.defineProperty(exports, "exportRetiredIdentityKeys", { enumerable: true, get: function () { return retiredKeys_1.exportRetiredIdentityKeys; } });
|
|
72
|
+
Object.defineProperty(exports, "claimRetiredIdentityKeys", { enumerable: true, get: function () { return retiredKeys_1.claimRetiredIdentityKeys; } });
|
|
73
|
+
Object.defineProperty(exports, "countQuarantinedRetiredKeys", { enumerable: true, get: function () { return retiredKeys_1.countQuarantinedRetiredKeys; } });
|
|
74
|
+
Object.defineProperty(exports, "unmirrorPersonalRetiredKeys", { enumerable: true, get: function () { return retiredKeys_1.unmirrorPersonalRetiredKeys; } });
|
|
75
|
+
Object.defineProperty(exports, "RetiredKeyRingUnreadableError", { enumerable: true, get: function () { return retiredKeys_1.RetiredKeyRingUnreadableError; } });
|
|
72
76
|
var signatureService_1 = require("./signatureService");
|
|
73
77
|
Object.defineProperty(exports, "SignatureService", { enumerable: true, get: function () { return signatureService_1.SignatureService; } });
|
|
74
78
|
var recoveryPhrase_1 = require("./recoveryPhrase");
|
|
@@ -788,12 +788,30 @@ class KeyManager {
|
|
|
788
788
|
* `signSocialReceive`) is fresh, so a cached key here would let a caller
|
|
789
789
|
* compare a new seed against an old key.
|
|
790
790
|
*
|
|
791
|
+
* iOS, in an app that holds the private key (Commons, the Peable wallet:
|
|
792
|
+
* keychain roles `holder` and `wallet`): DERIVED from the private key in the
|
|
793
|
+
* wallet group, never read from the public-key item, which sits in the shared
|
|
794
|
+
* group every Oxy app can write (R5-1); `null` when that app holds no private
|
|
795
|
+
* key. Every other iOS app reads the public-key item: it holds nothing it
|
|
796
|
+
* could derive from, and it never spends. Money code uses
|
|
797
|
+
* {@link getIdentityPublicKeyForMoney}.
|
|
798
|
+
*
|
|
791
799
|
* @returns Shared public key or null if no shared identity exists
|
|
792
800
|
*/
|
|
793
801
|
static async getSharedPublicKey() {
|
|
794
802
|
if (isWebPlatform()) {
|
|
795
803
|
return null;
|
|
796
804
|
}
|
|
805
|
+
if ((0, platform_1.isIOS)() && (await KeyManager._holdsSpendingKeys())) {
|
|
806
|
+
// Not cached: a rotation in Commons changes it under the wallet.
|
|
807
|
+
try {
|
|
808
|
+
const privateKey = await KeyManager.getSharedPrivateKey();
|
|
809
|
+
return privateKey ? KeyManager.derivePublicKey(privateKey) : null;
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
797
815
|
// Return cached value if available (iOS only; see above).
|
|
798
816
|
if (!(0, platform_1.isAndroid)() && KeyManager.cachedSharedPublicKey !== null) {
|
|
799
817
|
return KeyManager.cachedSharedPublicKey;
|
|
@@ -826,6 +844,68 @@ class KeyManager {
|
|
|
826
844
|
return null;
|
|
827
845
|
}
|
|
828
846
|
}
|
|
847
|
+
/**
|
|
848
|
+
* Whether this iOS app holds spending keys (keychain role `holder` or
|
|
849
|
+
* `wallet`), so a public key it uses for money must be derived from them.
|
|
850
|
+
* Throws while the keychain cannot say. @internal
|
|
851
|
+
*/
|
|
852
|
+
static async _holdsSpendingKeys() {
|
|
853
|
+
const access = await (0, keychainAccessGroups_1.resolveKeychainAccess)();
|
|
854
|
+
return access.mode === 'grouped' && access.role !== 'member';
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* THE public key of the identity a money holder (the Peable wallet, Commons)
|
|
858
|
+
* receives and spends with: the personal key, or with `opts.accountId` that
|
|
859
|
+
* organization's (ADR 0061 D6). It is the public half of exactly the private
|
|
860
|
+
* key {@link deriveScopedSeed} and {@link signSocialReceive} use, so an
|
|
861
|
+
* address shown from it is one this app can spend (R5-1).
|
|
862
|
+
*
|
|
863
|
+
* - iOS (Commons, the Peable wallet): DERIVED from the private key in the
|
|
864
|
+
* restricted keychain groups (personal: the wallet group's slot, then
|
|
865
|
+
* Commons' primary; organization: its pair). Never the shared group's
|
|
866
|
+
* public-key item, which every Oxy app can write. `null` when no private
|
|
867
|
+
* key is held, and in every other iOS app.
|
|
868
|
+
* - Android: inside Commons derived from its own key; elsewhere Commons'
|
|
869
|
+
* identity host (`describe`), which answers from Commons' own signer
|
|
870
|
+
* store over signature-protected IPC.
|
|
871
|
+
* - Web: `null`.
|
|
872
|
+
*
|
|
873
|
+
* Compare the answer with the account's published `#key-1` before showing a
|
|
874
|
+
* receive address (Peable ADR 0002). Lowercase uncompressed (`04…`).
|
|
875
|
+
*/
|
|
876
|
+
static async getIdentityPublicKeyForMoney(opts) {
|
|
877
|
+
if (isWebPlatform()) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
const accountId = opts?.accountId;
|
|
881
|
+
if (accountId !== undefined) {
|
|
882
|
+
if (!(0, organizationKeys_1.isOrganizationAccountId)(accountId))
|
|
883
|
+
return null;
|
|
884
|
+
if ((0, platform_1.isAndroid)() && !KeyManager.identitySignerStore) {
|
|
885
|
+
const bridge = await (0, protocol_1.loadCommonsIdentityBridge)();
|
|
886
|
+
const description = (await bridge?.describe(accountId)) ?? null;
|
|
887
|
+
return description?.accountId === accountId && description.publicKey
|
|
888
|
+
? KeyManager.canonicalPublicKey(description.publicKey)
|
|
889
|
+
: null;
|
|
890
|
+
}
|
|
891
|
+
const organizationKey = await (0, organizationKeys_1.getOrganizationPrivateKey)(accountId);
|
|
892
|
+
return organizationKey ? KeyManager.derivePublicKey(organizationKey) : null;
|
|
893
|
+
}
|
|
894
|
+
if ((0, platform_1.isAndroid)()) {
|
|
895
|
+
if (KeyManager.identitySignerStore) {
|
|
896
|
+
const local = await KeyManager.getPrivateKey();
|
|
897
|
+
return local ? KeyManager.derivePublicKey(local) : null;
|
|
898
|
+
}
|
|
899
|
+
const bridge = await (0, protocol_1.loadCommonsIdentityBridge)();
|
|
900
|
+
const publicKey = (await bridge?.describe())?.publicKey ?? null;
|
|
901
|
+
return publicKey ? KeyManager.canonicalPublicKey(publicKey) : null;
|
|
902
|
+
}
|
|
903
|
+
if (!(0, platform_1.isIOS)()) {
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
const privateKey = (await KeyManager.getSharedPrivateKey()) ?? (await KeyManager.getPrivateKey());
|
|
907
|
+
return privateKey ? KeyManager.derivePublicKey(privateKey) : null;
|
|
908
|
+
}
|
|
829
909
|
/**
|
|
830
910
|
* The shared private key, where this app may hold it: iOS only.
|
|
831
911
|
*
|
|
@@ -2089,18 +2169,26 @@ class KeyManager {
|
|
|
2089
2169
|
await KeyManager._clearSharedSlot(store);
|
|
2090
2170
|
// The retired ring is NOT cleared (R4-4): it is money under the person's
|
|
2091
2171
|
// previous keys, and "Start over" or a forced delete must not cost it.
|
|
2092
|
-
//
|
|
2093
|
-
//
|
|
2094
|
-
//
|
|
2095
|
-
// clears it, explicitly (`forgetRetiredIdentityKeys
|
|
2172
|
+
// Every personal entry is bound to its owner's account id (R5-2), so the
|
|
2173
|
+
// same person signing in again finds it, and a different person on this
|
|
2174
|
+
// phone never lists, derives from or signs with it. Only deleting the Oxy
|
|
2175
|
+
// ACCOUNT clears it, explicitly (`forgetRetiredIdentityKeys`).
|
|
2096
2176
|
}
|
|
2097
2177
|
// The device backup goes on EVERY delete, forced or not: it is restored
|
|
2098
2178
|
// silently, so leaving it would bring a deleted identity back on the next
|
|
2099
2179
|
// launch. Best-effort, like the other recovery sources.
|
|
2100
2180
|
await KeyManager._clearDeviceBackup();
|
|
2101
2181
|
// Same for the Android signer store: Commons' identity host must stop
|
|
2102
|
-
// answering for a deleted identity, forced or not
|
|
2182
|
+
// answering for a deleted identity, forced or not — its personal retired
|
|
2183
|
+
// keys included (R5-2): they stay in Commons' own ring, bound to their
|
|
2184
|
+
// owner, but the host must not hand them to the next person on the phone.
|
|
2103
2185
|
await KeyManager._clearIdentitySigner();
|
|
2186
|
+
try {
|
|
2187
|
+
await (0, retiredKeys_1.unmirrorPersonalRetiredKeys)();
|
|
2188
|
+
}
|
|
2189
|
+
catch (error) {
|
|
2190
|
+
logger_1.logger.warn('Failed to withdraw the personal retired keys from the identity host', { component: 'KeyManager' }, error);
|
|
2191
|
+
}
|
|
2104
2192
|
// Clear the marker AFTER key deletion succeeds — a marker must never outlive
|
|
2105
2193
|
// its identity (a leftover marker would route a truly-absent device to
|
|
2106
2194
|
// `recovery` instead of `welcome`).
|
|
@@ -2629,11 +2717,16 @@ class KeyManager {
|
|
|
2629
2717
|
* (personal, or `accountId`'s) whose public key it names: the current key
|
|
2630
2718
|
* when it matches, else a RETIRED key Commons kept after a rotation or
|
|
2631
2719
|
* replacement (`./retiredKeys`, ADR 0061 D7 as amended). Spending only: a
|
|
2632
|
-
* retired key never signs in. Never falls back to another key.
|
|
2720
|
+
* retired key never signs in. Never falls back to another key. A PERSONAL
|
|
2721
|
+
* retired key is reached only with `opts.ownerAccountId`, the signed-in
|
|
2722
|
+
* person's account id, and only when the ring keeps it for that account
|
|
2723
|
+
* (R5-2); on Android outside Commons the identity host holds only the
|
|
2724
|
+
* personal keys of the person Commons is signed in as.
|
|
2633
2725
|
*
|
|
2634
2726
|
* @param info Context/domain-binding label (distinct labels → independent seeds).
|
|
2635
2727
|
* @param opts.accountId An organization's account id, for its key.
|
|
2636
2728
|
* @param opts.publicKey Selects one key of that account by its public key.
|
|
2729
|
+
* @param opts.ownerAccountId The person's account id, for a personal retired key.
|
|
2637
2730
|
* @returns 32 bytes of derived keying material, or `null` on web / when no
|
|
2638
2731
|
* identity key is available to this app.
|
|
2639
2732
|
*/
|
|
@@ -2650,7 +2743,7 @@ class KeyManager {
|
|
|
2650
2743
|
const seed = (await bridge?.deriveRetiredScopedSeed?.(info, selected, opts.accountId)) ?? null;
|
|
2651
2744
|
return seed ? hexToBytes(seed) : null;
|
|
2652
2745
|
}
|
|
2653
|
-
const selected = await KeyManager._selectedPrivateKey(opts
|
|
2746
|
+
const selected = await KeyManager._selectedPrivateKey({ ...opts, publicKey: opts.publicKey });
|
|
2654
2747
|
return selected ? (0, identityDerivations_1.deriveScopedSeedFromKey)(selected, info) : null;
|
|
2655
2748
|
}
|
|
2656
2749
|
if (opts?.accountId !== undefined) {
|
|
@@ -2718,7 +2811,7 @@ class KeyManager {
|
|
|
2718
2811
|
return ((await bridge?.signRetiredSocialReceive?.(index, digest, selected, opts.accountId)) ??
|
|
2719
2812
|
null);
|
|
2720
2813
|
}
|
|
2721
|
-
const selected = await KeyManager._selectedPrivateKey(opts
|
|
2814
|
+
const selected = await KeyManager._selectedPrivateKey({ ...opts, publicKey: opts.publicKey });
|
|
2722
2815
|
return selected ? (0, identityDerivations_1.signSocialReceiveDigest)(selected, index, digest) : null;
|
|
2723
2816
|
}
|
|
2724
2817
|
const accountId = opts?.accountId;
|
|
@@ -2757,7 +2850,8 @@ class KeyManager {
|
|
|
2757
2850
|
static _selectorKey(publicKey) {
|
|
2758
2851
|
return KeyManager.isValidPublicKey(publicKey) ? KeyManager.canonicalPublicKey(publicKey) : null;
|
|
2759
2852
|
}
|
|
2760
|
-
static async _selectedPrivateKey(
|
|
2853
|
+
static async _selectedPrivateKey(opts) {
|
|
2854
|
+
const { publicKey, accountId, ownerAccountId } = opts;
|
|
2761
2855
|
if ((0, retiredKeys_1.retiredKeyScope)({ accountId }) === null || !KeyManager.isValidPublicKey(publicKey)) {
|
|
2762
2856
|
return null;
|
|
2763
2857
|
}
|
|
@@ -2770,7 +2864,11 @@ class KeyManager {
|
|
|
2770
2864
|
if (current && KeyManager.canonicalPublicKey(KeyManager.derivePublicKey(current)) === wanted) {
|
|
2771
2865
|
return current;
|
|
2772
2866
|
}
|
|
2773
|
-
return (0, retiredKeys_1.getRetiredPrivateKey)(wanted, accountId !== undefined
|
|
2867
|
+
return (0, retiredKeys_1.getRetiredPrivateKey)(wanted, accountId !== undefined
|
|
2868
|
+
? { accountId }
|
|
2869
|
+
: ownerAccountId !== undefined
|
|
2870
|
+
? { ownerAccountId }
|
|
2871
|
+
: undefined);
|
|
2774
2872
|
}
|
|
2775
2873
|
/**
|
|
2776
2874
|
* Get a shortened version of the public key for display
|