@oxy.so/core 7.23.2 → 7.25.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.
@@ -1,6 +1,39 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AccountsApi = exports.AccountInvitationsApi = exports.AccountAgentKeysApi = exports.AccountMembersApi = exports.kindAcceptsAccountCategories = exports.isSelectableAccountCategoryId = exports.isOperatorSwitchTargetKind = exports.isDelegatedActAsEligibleKind = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.MAX_ACCOUNT_CATEGORIES = exports.ACCOUNT_KINDS = exports.ACCOUNT_CATEGORY_IDS = void 0;
36
+ exports.AccountsApi = exports.AccountInvitationsApi = exports.AccountIdentityKeyApi = exports.AccountAgentKeysApi = exports.AccountMembersApi = exports.kindAcceptsAccountCategories = exports.isSelectableAccountCategoryId = exports.isOperatorSwitchTargetKind = exports.isDelegatedActAsEligibleKind = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.MAX_ACCOUNT_CATEGORIES = exports.ACCOUNT_KINDS = exports.ACCOUNT_CATEGORY_IDS = void 0;
4
37
  const client_1 = require("@oxy.so/contracts/client");
5
38
  const userIdentity_1 = require("../utils/userIdentity");
6
39
  const identityCacheSweep_1 = require("../utils/identityCacheSweep");
@@ -88,6 +121,73 @@ class AccountAgentKeysApi {
88
121
  }
89
122
  }
90
123
  exports.AccountAgentKeysApi = AccountAgentKeysApi;
124
+ /**
125
+ * `oxy.accounts.identityKey` — an organization's identity key (ADR 0061): its
126
+ * DID `#key-1`, the key Peable derives its FairCoin receive addresses from.
127
+ * Only the organization's owners and admins may read or change it; the key is
128
+ * created and held in Commons and never signs anybody in.
129
+ *
130
+ * `get` answers `publicKey: null` for an organization that has no key yet (its
131
+ * DID lists no `#key-1`): that is when a client shows its owners "Set up the
132
+ * organization's Peable wallet". A 403 (`ORGANIZATION_KEY_FORBIDDEN`) means the
133
+ * signed-in person is not an owner or admin of a claimed, active organization.
134
+ */
135
+ class AccountIdentityKeyApi {
136
+ constructor(ctx) {
137
+ this.ctx = ctx;
138
+ }
139
+ /** The organization's key (`null` without one) and every key it has had, oldest first. */
140
+ async get(accountId) {
141
+ return (0, client_1.parseContract)(client_1.organizationIdentityKeyStateCheck, await this.ctx.request('GET', `/accounts/${enc(accountId)}/identity-key`, undefined, { cache: false }));
142
+ }
143
+ /** A one-use challenge for one change, with the claims every signer signs. */
144
+ async requestChallenge(accountId, request) {
145
+ return (0, client_1.parseContract)(client_1.organizationKeyChallengeCheck, await this.ctx.request('POST', `/accounts/${enc(accountId)}/identity-key/challenge`, (0, client_1.parseContract)(client_1.organizationKeyChallengeRequestCheck, request), { cache: false }));
146
+ }
147
+ /** Apply a change with its signatures (see {@link link} for the whole ceremony). */
148
+ async apply(accountId, request) {
149
+ return (0, client_1.parseContract)(client_1.organizationIdentityKeyStateCheck, await this.ctx.request('POST', `/accounts/${enc(accountId)}/identity-key`, (0, client_1.parseContract)(client_1.organizationKeyApplyRequestCheck, request), { cache: false }));
150
+ }
151
+ /**
152
+ * Commons' whole ceremony. NATIVE-ONLY: the actor is THIS device's personal
153
+ * root (`KeyManager`), which must be the signed-in person's. `key` is the
154
+ * organization key the device generated (`create`, or `replace` with a new
155
+ * one); `previousKey` is the current organization key when the device holds
156
+ * it, so the replacement is recorded as proven by it.
157
+ */
158
+ async link(accountId, input) {
159
+ const { KeyManager, signIdentityProof } = await Promise.resolve().then(() => __importStar(require('../crypto/index')));
160
+ const [rootPrivateKey, rootPublicKey] = await Promise.all([
161
+ KeyManager.getPrivateKey(),
162
+ KeyManager.getPublicKey(),
163
+ ]);
164
+ if (!rootPrivateKey || !rootPublicKey) {
165
+ throw new Error('No identity on this device to sign with');
166
+ }
167
+ const challenge = await this.requestChallenge(accountId, {
168
+ operation: input.operation,
169
+ publicKey: input.key.publicKey,
170
+ });
171
+ const root = rootPublicKey.trim().toLowerCase();
172
+ if (challenge.actorPublicKey !== root) {
173
+ throw new Error("This device's identity is not the signed-in account's");
174
+ }
175
+ const sign = async (signer, publicKey) => (await signIdentityProof({ privateKey: signer.privateKey, publicKey }, (0, client_1.organizationKeyProofClaims)(challenge, publicKey))).signature;
176
+ const previousPublicKey = challenge.previousPublicKey;
177
+ return this.apply(accountId, {
178
+ operation: input.operation,
179
+ publicKey: challenge.publicKey,
180
+ challenge: challenge.challenge,
181
+ expiresAt: challenge.expiresAt,
182
+ actorSignature: await sign({ privateKey: rootPrivateKey, publicKey: root }, root),
183
+ keySignature: await sign(input.key, challenge.publicKey),
184
+ ...(input.previousKey && previousPublicKey
185
+ ? { previousKeySignature: await sign(input.previousKey, previousPublicKey) }
186
+ : {}),
187
+ });
188
+ }
189
+ }
190
+ exports.AccountIdentityKeyApi = AccountIdentityKeyApi;
91
191
  /** Consent-based invitations; never cached or queued as offline mutations. */
92
192
  class AccountInvitationsApi {
93
193
  constructor(ctx) {
@@ -123,6 +223,7 @@ class AccountsApi {
123
223
  this.members = new AccountMembersApi(ctx);
124
224
  this.invitations = new AccountInvitationsApi(ctx);
125
225
  this.agentKeys = new AccountAgentKeysApi(ctx);
226
+ this.identityKey = new AccountIdentityKeyApi(ctx);
126
227
  }
127
228
  /**
128
229
  * List the accounts the caller can access: their own personal (root)
@@ -1,2 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerIdentitySignerStore = registerIdentitySignerStore;
4
+ exports.registeredIdentitySignerStore = registeredIdentitySignerStore;
5
+ /**
6
+ * The store Commons registered, readable by modules that `KeyManager` itself
7
+ * imports (`./organizationKeys`), which therefore cannot import `KeyManager`.
8
+ * Written only by `KeyManager.setIdentitySignerStore`. @internal
9
+ */
10
+ let registeredSignerStore = null;
11
+ /** @internal */
12
+ function registerIdentitySignerStore(store) {
13
+ registeredSignerStore = store;
14
+ }
15
+ /** @internal */
16
+ function registeredIdentitySignerStore() {
17
+ return registeredSignerStore;
18
+ }
@@ -12,7 +12,7 @@
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.verifySignature = exports.signMessage = exports.loadCommonsIdentityBridge = exports.canonicalize = exports.createContentHasher = exports.solveRegistrationPow = exports.deriveIdentityLinkCode = exports.signIdentityProof = exports.digestIdentityPayload = exports.authChallengeDigest = exports.signSocialReceiveDigest = exports.deriveSocialReceiveKey = 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.updateIdentityMarker = exports.readIdentityMarker = exports.IdentityUnavailableError = exports.IdentityPersistError = exports.IdentityAlreadyExistsError = exports.KeyManager = void 0;
15
+ exports.verifySignature = exports.signMessage = exports.loadCommonsIdentityBridge = exports.canonicalize = exports.createContentHasher = exports.solveRegistrationPow = exports.deriveIdentityLinkCode = exports.signIdentityProof = exports.digestIdentityPayload = exports.authChallengeDigest = exports.signSocialReceiveDigest = exports.deriveSocialReceiveKey = 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.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
16
  // MUST stay the first import: the shim has to exist before any module below
17
17
  // evaluates or draws randomness (see `./polyfill`).
18
18
  require("./polyfill");
@@ -24,6 +24,19 @@ Object.defineProperty(exports, "IdentityUnavailableError", { enumerable: true, g
24
24
  var identityMarker_1 = require("./identityMarker");
25
25
  Object.defineProperty(exports, "readIdentityMarker", { enumerable: true, get: function () { return identityMarker_1.readIdentityMarker; } });
26
26
  Object.defineProperty(exports, "updateIdentityMarker", { enumerable: true, get: function () { return identityMarker_1.updateIdentityMarker; } });
27
+ // Organization keys on the device (docs/adr/0061-organization-identity-key.md D6)
28
+ var organizationKeys_1 = require("./organizationKeys");
29
+ Object.defineProperty(exports, "generateOrganizationKey", { enumerable: true, get: function () { return organizationKeys_1.generateOrganizationKey; } });
30
+ Object.defineProperty(exports, "organizationKeyFromPhrase", { enumerable: true, get: function () { return organizationKeys_1.organizationKeyFromPhrase; } });
31
+ Object.defineProperty(exports, "storeOrganizationKey", { enumerable: true, get: function () { return organizationKeys_1.storeOrganizationKey; } });
32
+ Object.defineProperty(exports, "getOrganizationPublicKey", { enumerable: true, get: function () { return organizationKeys_1.getOrganizationPublicKey; } });
33
+ Object.defineProperty(exports, "getOrganizationPrivateKey", { enumerable: true, get: function () { return organizationKeys_1.getOrganizationPrivateKey; } });
34
+ Object.defineProperty(exports, "getOrganizationRecoveryPhrase", { enumerable: true, get: function () { return organizationKeys_1.getOrganizationRecoveryPhrase; } });
35
+ Object.defineProperty(exports, "listOrganizationKeys", { enumerable: true, get: function () { return organizationKeys_1.listOrganizationKeys; } });
36
+ Object.defineProperty(exports, "deleteOrganizationKey", { enumerable: true, get: function () { return organizationKeys_1.deleteOrganizationKey; } });
37
+ Object.defineProperty(exports, "syncOrganizationKeys", { enumerable: true, get: function () { return organizationKeys_1.syncOrganizationKeys; } });
38
+ Object.defineProperty(exports, "isOrganizationAccountId", { enumerable: true, get: function () { return organizationKeys_1.isOrganizationAccountId; } });
39
+ Object.defineProperty(exports, "isHealthyOrganizationKeyPair", { enumerable: true, get: function () { return organizationKeys_1.isHealthyOrganizationKeyPair; } });
27
40
  var signatureService_1 = require("./signatureService");
28
41
  Object.defineProperty(exports, "SignatureService", { enumerable: true, get: function () { return signatureService_1.SignatureService; } });
29
42
  var recoveryPhrase_1 = require("./recoveryPhrase");
@@ -45,6 +45,8 @@ const protocol_1 = require("@oxy.so/protocol");
45
45
  const secp256k1_1 = require("@oxy.so/protocol/secp256k1");
46
46
  const logger_1 = require("../logger");
47
47
  const identityDerivations_1 = require("./identityDerivations");
48
+ const identitySigner_1 = require("./identitySigner");
49
+ const organizationKeys_1 = require("./organizationKeys");
48
50
  const identityMarker_1 = require("./identityMarker");
49
51
  const deviceBackup_1 = require("./deviceBackup");
50
52
  /**
@@ -296,6 +298,7 @@ class KeyManager {
296
298
  */
297
299
  static setIdentitySignerStore(store) {
298
300
  KeyManager.identitySignerStore = store;
301
+ (0, identitySigner_1.registerIdentitySignerStore)(store);
299
302
  KeyManager.invalidateSharedCache();
300
303
  }
301
304
  /** Whether this app registered an identity signer store (Commons on Android). */
@@ -2612,14 +2615,23 @@ class KeyManager {
2612
2615
  * Both reproduce from the user's Oxy recovery phrase, so the seed is
2613
2616
  * recoverable, and the IPC answer is byte-identical to the local one.
2614
2617
  *
2618
+ * With `opts.accountId` the seed is derived from that ORGANIZATION's key
2619
+ * instead (ADR 0061 D6), from the same places: Commons' own copy, the iOS
2620
+ * keychain group, or Commons' identity host. It never falls back to the
2621
+ * personal key.
2622
+ *
2615
2623
  * @param info Context/domain-binding label (distinct labels → independent seeds).
2624
+ * @param opts.accountId An organization's account id, for its key.
2616
2625
  * @returns 32 bytes of derived keying material, or `null` on web / when no
2617
2626
  * identity key is available to this app.
2618
2627
  */
2619
- static async deriveScopedSeed(info) {
2628
+ static async deriveScopedSeed(info, opts) {
2620
2629
  if (isWebPlatform()) {
2621
2630
  return null;
2622
2631
  }
2632
+ if (opts?.accountId !== undefined) {
2633
+ return KeyManager._deriveOrganizationScopedSeed(info, opts.accountId);
2634
+ }
2623
2635
  if ((0, platform_1.isAndroid)()) {
2624
2636
  if (KeyManager.identitySignerStore) {
2625
2637
  // Commons: its own key.
@@ -2636,6 +2648,24 @@ class KeyManager {
2636
2648
  }
2637
2649
  return (0, identityDerivations_1.deriveScopedSeedFromKey)(privateKey, info);
2638
2650
  }
2651
+ /**
2652
+ * {@link deriveScopedSeed} over an ORGANIZATION key (ADR 0061 D6): Commons'
2653
+ * own copy inside Commons and on iOS the keychain group, everywhere else on
2654
+ * Android Commons' identity host. Never falls back to the personal key: an
2655
+ * organization this device cannot reach is `null`.
2656
+ */
2657
+ static async _deriveOrganizationScopedSeed(info, accountId) {
2658
+ if (!(0, organizationKeys_1.isOrganizationAccountId)(accountId)) {
2659
+ return null;
2660
+ }
2661
+ if ((0, platform_1.isAndroid)() && !KeyManager.identitySignerStore) {
2662
+ const bridge = await (0, protocol_1.loadCommonsIdentityBridge)();
2663
+ const seed = (await bridge?.deriveScopedSeed(info, accountId)) ?? null;
2664
+ return seed ? hexToBytes(seed) : null;
2665
+ }
2666
+ const privateKey = await (0, organizationKeys_1.getOrganizationPrivateKey)(accountId);
2667
+ return privateKey ? (0, identityDerivations_1.deriveScopedSeedFromKey)(privateKey, info) : null;
2668
+ }
2639
2669
  /**
2640
2670
  * Sign a 32-byte digest (a transaction sighash, lowercase hex) with the
2641
2671
  * identity's social-receive child key `index`: the non-hardened BIP32 child
@@ -2643,16 +2673,30 @@ class KeyManager {
2643
2673
  * signature is RFC 6979, low-S, DER hex; `publicKey` is the child's
2644
2674
  * compressed public key for the scriptSig.
2645
2675
  *
2646
- * Key source: as {@link deriveScopedSeed}. On Android outside Commons the
2647
- * signature comes from Commons, which allows it for the wallet app only.
2676
+ * Key source: as {@link deriveScopedSeed}, including `opts.accountId` for an
2677
+ * organization's key (ADR 0061 D6). On Android outside Commons the signature
2678
+ * comes from Commons, which allows it for the wallet app only.
2648
2679
  *
2649
2680
  * @returns the signature, or `null` on web / when no identity key is
2650
2681
  * available to this app.
2651
2682
  */
2652
- static async signSocialReceive(index, digest) {
2683
+ static async signSocialReceive(index, digest, opts) {
2653
2684
  if (isWebPlatform()) {
2654
2685
  return null;
2655
2686
  }
2687
+ const accountId = opts?.accountId;
2688
+ if (accountId !== undefined) {
2689
+ // An organization key (ADR 0061 D6); never the personal one in its place.
2690
+ if (!(0, organizationKeys_1.isOrganizationAccountId)(accountId)) {
2691
+ return null;
2692
+ }
2693
+ if ((0, platform_1.isAndroid)() && !KeyManager.identitySignerStore) {
2694
+ const bridge = await (0, protocol_1.loadCommonsIdentityBridge)();
2695
+ return (await bridge?.signSocialReceive(index, digest, accountId)) ?? null;
2696
+ }
2697
+ const organizationKey = await (0, organizationKeys_1.getOrganizationPrivateKey)(accountId);
2698
+ return organizationKey ? (0, identityDerivations_1.signSocialReceiveDigest)(organizationKey, index, digest) : null;
2699
+ }
2656
2700
  if ((0, platform_1.isAndroid)() && !KeyManager.identitySignerStore) {
2657
2701
  // Every app but Commons: Commons signs.
2658
2702
  const bridge = await (0, protocol_1.loadCommonsIdentityBridge)();
@@ -0,0 +1,349 @@
1
+ "use strict";
2
+ /**
3
+ * Organization keys on the device (ADR 0061 D6).
4
+ *
5
+ * An organization's identity key is created in Commons by one of its owners or
6
+ * admins, from a fresh 24-word phrase (`seed[0:32]`, the personal-root
7
+ * derivation), and is published as the organization's DID `#key-1`. Commons
8
+ * holds it beside the person's own root, under the organization's account id;
9
+ * other Oxy apps reach it the way they reach the personal identity, by
10
+ * derivation and never by the key:
11
+ *
12
+ * - iOS: the keychain access group `group.so.oxy.shared`, like the personal
13
+ * shared slot. Only Commons writes it.
14
+ * - Android: Commons' own expo-secure-store slot, mirrored into the identity
15
+ * signer store (`./identitySigner`) that Commons' identity host answers
16
+ * `accountId` requests from. Every other app asks Commons over IPC
17
+ * (`loadCommonsIdentityBridge`), and never gets the key.
18
+ *
19
+ * The phrase is kept only in Commons, on the device that created or imported
20
+ * it, so an owner can show it to another owner (ADR 0061 D5); it never reaches
21
+ * the keychain group, an IPC answer or a log.
22
+ *
23
+ * This module imports nothing from `./keyManager` (which imports it): the
24
+ * signer store comes from `./identitySigner`'s registry.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.isOrganizationAccountId = isOrganizationAccountId;
28
+ exports.isHealthyOrganizationKeyPair = isHealthyOrganizationKeyPair;
29
+ exports.generateOrganizationKey = generateOrganizationKey;
30
+ exports.organizationKeyFromPhrase = organizationKeyFromPhrase;
31
+ exports.storeOrganizationKey = storeOrganizationKey;
32
+ exports.getOrganizationPublicKey = getOrganizationPublicKey;
33
+ exports.getOrganizationPrivateKey = getOrganizationPrivateKey;
34
+ exports.getOrganizationRecoveryPhrase = getOrganizationRecoveryPhrase;
35
+ exports.listOrganizationKeys = listOrganizationKeys;
36
+ exports.deleteOrganizationKey = deleteOrganizationKey;
37
+ exports.syncOrganizationKeys = syncOrganizationKeys;
38
+ const bip39_1 = require("@scure/bip39");
39
+ const english_js_1 = require("@scure/bip39/wordlists/english.js");
40
+ const protocol_1 = require("@oxy.so/protocol");
41
+ const secp256k1_1 = require("@oxy.so/protocol/secp256k1");
42
+ const logger_1 = require("../logger");
43
+ const platform_1 = require("../utils/platform");
44
+ const identitySigner_1 = require("./identitySigner");
45
+ /** Same set the server's account ids come from: a legacy 24-hex id or a lowercase uuid. */
46
+ const ACCOUNT_ID = /^(?:[0-9a-f]{24}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
47
+ const IOS_KEYCHAIN_GROUP = 'group.so.oxy.shared';
48
+ /** Android, Commons-local: its own Keystore key, shared with no personal slot. */
49
+ const ANDROID_KEYCHAIN_SERVICE = 'oxy_org_identity';
50
+ const MNEMONIC_KEYCHAIN_SERVICE = 'oxy_org_identity_mnemonic';
51
+ const INDEX_KEYCHAIN_SERVICE = 'oxy_org_identity_index';
52
+ const INDEX_STORAGE_KEY = 'oxy_org_identity_index_v1';
53
+ const ORGANIZATION_PHRASE_WORDS = 24;
54
+ /** Whether `value` is an account id an organization key can be stored under. */
55
+ function isOrganizationAccountId(value) {
56
+ return ACCOUNT_ID.test(value);
57
+ }
58
+ function assertAccountId(accountId) {
59
+ if (!isOrganizationAccountId(accountId)) {
60
+ throw new Error('organization key: accountId must be a 24-hex or uuid account id');
61
+ }
62
+ }
63
+ const privateKeyName = (accountId) => `oxy_org_identity_private_key.${accountId}`;
64
+ const publicKeyName = (accountId) => `oxy_org_identity_public_key.${accountId}`;
65
+ const mnemonicName = (accountId) => `oxy_org_identity_mnemonic.${accountId}`;
66
+ function toHex(bytes) {
67
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
68
+ }
69
+ /** Lowercase uncompressed public key of `privateKey`. */
70
+ function publicKeyOf(privateKey) {
71
+ return (0, secp256k1_1.deriveSecp256k1PublicKey)((0, secp256k1_1.normalizeSecp256k1PrivateKey)(privateKey));
72
+ }
73
+ /** True only when both halves are well-formed AND the public key derives from the private one. */
74
+ function isHealthyOrganizationKeyPair(privateKey, publicKey) {
75
+ if (!privateKey || !publicKey)
76
+ return false;
77
+ if (!(0, secp256k1_1.isValidSecp256k1PrivateKey)(privateKey) || !(0, secp256k1_1.isValidSecp256k1PublicKey)(publicKey)) {
78
+ return false;
79
+ }
80
+ try {
81
+ return publicKeyOf(privateKey) === (0, secp256k1_1.normalizeSecp256k1PublicKey)(publicKey);
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ function normalizePhrase(phrase) {
88
+ return phrase.trim().toLowerCase().split(/\s+/).join(' ');
89
+ }
90
+ async function pairFromPhrase(phrase) {
91
+ const normalized = normalizePhrase(phrase);
92
+ if (normalized.split(' ').length !== ORGANIZATION_PHRASE_WORDS ||
93
+ !(0, bip39_1.validateMnemonic)(normalized, english_js_1.wordlist)) {
94
+ throw new Error('organization key: not a valid 24-word recovery phrase');
95
+ }
96
+ const seed = await (0, bip39_1.mnemonicToSeed)(normalized);
97
+ const privateKey = toHex(seed.subarray(0, 32));
98
+ return { privateKey, publicKey: publicKeyOf(privateKey) };
99
+ }
100
+ /**
101
+ * A fresh organization key: a 24-word phrase and the pair derived from it
102
+ * exactly as a personal root is (`seed[0:32]`). Stores NOTHING: the caller shows
103
+ * the phrase, links the key (`oxy.accounts.identityKey`), and only then calls
104
+ * {@link storeOrganizationKey}.
105
+ */
106
+ async function generateOrganizationKey() {
107
+ const mnemonic = (0, bip39_1.generateMnemonic)(english_js_1.wordlist, 256);
108
+ return { mnemonic, ...(await pairFromPhrase(mnemonic)) };
109
+ }
110
+ /**
111
+ * The pair a 24-word organization phrase derives. Stores nothing; the caller
112
+ * checks the public key against the organization's DID `#key-1` before keeping
113
+ * it (ADR 0061 D5).
114
+ */
115
+ async function organizationKeyFromPhrase(phrase) {
116
+ return pairFromPhrase(phrase);
117
+ }
118
+ async function secureStore() {
119
+ return (0, protocol_1.loadSecureStore)();
120
+ }
121
+ async function readIndex(store) {
122
+ const raw = await store.getItemAsync(INDEX_STORAGE_KEY, {
123
+ keychainService: INDEX_KEYCHAIN_SERVICE,
124
+ });
125
+ if (!raw)
126
+ return [];
127
+ try {
128
+ const parsed = JSON.parse(raw);
129
+ return Array.isArray(parsed)
130
+ ? parsed.filter((id) => typeof id === 'string' && isOrganizationAccountId(id))
131
+ : [];
132
+ }
133
+ catch (error) {
134
+ logger_1.logger.warn('Organization key index unreadable; treating as empty', {
135
+ component: 'OrganizationKeys',
136
+ error: error instanceof Error ? error.name : 'unknown',
137
+ });
138
+ return [];
139
+ }
140
+ }
141
+ async function writeIndex(store, ids) {
142
+ await store.setItemAsync(INDEX_STORAGE_KEY, JSON.stringify([...new Set(ids)].sort()), {
143
+ keychainService: INDEX_KEYCHAIN_SERVICE,
144
+ });
145
+ }
146
+ /** Where this platform keeps the pair: the iOS keychain group, or Commons' own Android slot. */
147
+ function pairOptions(store, forPrivateWrite) {
148
+ if ((0, platform_1.isIOS)()) {
149
+ return forPrivateWrite
150
+ ? {
151
+ keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
152
+ keychainAccessGroup: IOS_KEYCHAIN_GROUP,
153
+ }
154
+ : { keychainAccessGroup: IOS_KEYCHAIN_GROUP };
155
+ }
156
+ return forPrivateWrite
157
+ ? {
158
+ keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
159
+ keychainService: ANDROID_KEYCHAIN_SERVICE,
160
+ }
161
+ : { keychainService: ANDROID_KEYCHAIN_SERVICE };
162
+ }
163
+ /** The pair from this app's own storage (iOS group; Android Commons slot, then signer store). */
164
+ async function readLocalPair(accountId) {
165
+ if ((0, platform_1.isWeb)())
166
+ return null;
167
+ if ((0, platform_1.isAndroid)()) {
168
+ const signer = (0, identitySigner_1.registeredIdentitySignerStore)();
169
+ if (!signer)
170
+ return null; // Only Commons holds organization keys on Android.
171
+ const store = await secureStore();
172
+ const options = pairOptions(store, false);
173
+ const privateKey = await store.getItemAsync(privateKeyName(accountId), options);
174
+ const publicKey = await store.getItemAsync(publicKeyName(accountId), options);
175
+ if (privateKey && publicKey && isHealthyOrganizationKeyPair(privateKey, publicKey)) {
176
+ return {
177
+ privateKey: (0, secp256k1_1.normalizeSecp256k1PrivateKey)(privateKey),
178
+ publicKey: (0, secp256k1_1.normalizeSecp256k1PublicKey)(publicKey),
179
+ };
180
+ }
181
+ // The signer store is a rung: a wiped expo-secure-store slot still has it.
182
+ const mirrored = (await signer.readOrganization?.(accountId)) ?? null;
183
+ return mirrored && isHealthyOrganizationKeyPair(mirrored.privateKey, mirrored.publicKey)
184
+ ? mirrored
185
+ : null;
186
+ }
187
+ if (!(0, platform_1.isIOS)())
188
+ return null;
189
+ const store = await secureStore();
190
+ const options = pairOptions(store, false);
191
+ const privateKey = await store.getItemAsync(privateKeyName(accountId), options);
192
+ const publicKey = await store.getItemAsync(publicKeyName(accountId), options);
193
+ return privateKey && publicKey && isHealthyOrganizationKeyPair(privateKey, publicKey)
194
+ ? {
195
+ privateKey: (0, secp256k1_1.normalizeSecp256k1PrivateKey)(privateKey),
196
+ publicKey: (0, secp256k1_1.normalizeSecp256k1PublicKey)(publicKey),
197
+ }
198
+ : null;
199
+ }
200
+ /**
201
+ * Keep an organization key on this device. Commons only: on Android it needs
202
+ * the identity signer store Commons registers, so the identity host can serve
203
+ * it; on iOS only Commons calls it (the keychain group is shared).
204
+ *
205
+ * Refuses a pair whose public key does not derive from its private key, and a
206
+ * phrase that does not derive the pair. Overwrites whatever this device held for
207
+ * the organization (a replaced key is the caller's decision, ADR 0061 D7).
208
+ */
209
+ async function storeOrganizationKey(accountId, key) {
210
+ assertAccountId(accountId);
211
+ if ((0, platform_1.isWeb)())
212
+ throw new Error('organization key: only a native device holds one');
213
+ if (!isHealthyOrganizationKeyPair(key.privateKey, key.publicKey)) {
214
+ throw new Error('organization key: the public key does not derive from the private key');
215
+ }
216
+ const privateKey = (0, secp256k1_1.normalizeSecp256k1PrivateKey)(key.privateKey);
217
+ const publicKey = (0, secp256k1_1.normalizeSecp256k1PublicKey)(key.publicKey);
218
+ if (key.mnemonic !== undefined) {
219
+ const fromPhrase = await pairFromPhrase(key.mnemonic);
220
+ if (fromPhrase.publicKey !== publicKey) {
221
+ throw new Error('organization key: the phrase derives a different key');
222
+ }
223
+ }
224
+ const signer = (0, platform_1.isAndroid)() ? (0, identitySigner_1.registeredIdentitySignerStore)() : null;
225
+ if ((0, platform_1.isAndroid)() && !signer?.writeOrganization) {
226
+ throw new Error('On Android only Commons holds organization keys; this app has no identity signer store for them.');
227
+ }
228
+ const store = await secureStore();
229
+ await store.setItemAsync(privateKeyName(accountId), privateKey, pairOptions(store, true));
230
+ await store.setItemAsync(publicKeyName(accountId), publicKey, pairOptions(store, false));
231
+ if (signer?.writeOrganization &&
232
+ !(await signer.writeOrganization(accountId, privateKey, publicKey))) {
233
+ throw new Error(`The identity signer store (${signer.name}) did not confirm the organization key.`);
234
+ }
235
+ if (key.mnemonic !== undefined) {
236
+ await store.setItemAsync(mnemonicName(accountId), normalizePhrase(key.mnemonic), {
237
+ keychainService: MNEMONIC_KEYCHAIN_SERVICE,
238
+ keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
239
+ });
240
+ }
241
+ else {
242
+ await store.deleteItemAsync(mnemonicName(accountId), {
243
+ keychainService: MNEMONIC_KEYCHAIN_SERVICE,
244
+ });
245
+ }
246
+ await writeIndex(store, [...(await readIndex(store)), accountId]);
247
+ }
248
+ /**
249
+ * The organization's public key as THIS device can reach it, or `null`.
250
+ * iOS: the keychain group. Android: Commons' own slot inside Commons, and
251
+ * Commons' identity host (`describe` with `accountId`) everywhere else. Compare
252
+ * it with the organization's DID `#key-1` before receiving (Peable ADR 0002).
253
+ */
254
+ async function getOrganizationPublicKey(accountId) {
255
+ if (!isOrganizationAccountId(accountId) || (0, platform_1.isWeb)())
256
+ return null;
257
+ try {
258
+ if ((0, platform_1.isAndroid)() && !(0, identitySigner_1.registeredIdentitySignerStore)()) {
259
+ const bridge = await (0, protocol_1.loadCommonsIdentityBridge)();
260
+ const description = (await bridge?.describe(accountId)) ?? null;
261
+ return description?.accountId === accountId ? description.publicKey : null;
262
+ }
263
+ return (await readLocalPair(accountId))?.publicKey ?? null;
264
+ }
265
+ catch (error) {
266
+ logger_1.logger.warn('Failed to read an organization public key', {
267
+ component: 'OrganizationKeys',
268
+ error: error instanceof Error ? error.name : 'unknown',
269
+ });
270
+ return null;
271
+ }
272
+ }
273
+ /**
274
+ * The organization's private key, where this app may hold it: Commons (both
275
+ * platforms) and, on iOS, the keychain group. `null` on Android outside
276
+ * Commons. Use it to sign within the app only; never transmit it.
277
+ */
278
+ async function getOrganizationPrivateKey(accountId) {
279
+ if (!isOrganizationAccountId(accountId))
280
+ return null;
281
+ try {
282
+ return (await readLocalPair(accountId))?.privateKey ?? null;
283
+ }
284
+ catch (error) {
285
+ logger_1.logger.warn('Failed to read an organization private key', {
286
+ component: 'OrganizationKeys',
287
+ error: error instanceof Error ? error.name : 'unknown',
288
+ });
289
+ return null;
290
+ }
291
+ }
292
+ /** The organization's phrase, if this device created or imported it with one. Commons only. */
293
+ async function getOrganizationRecoveryPhrase(accountId) {
294
+ if (!isOrganizationAccountId(accountId) || (0, platform_1.isWeb)())
295
+ return null;
296
+ const store = await secureStore();
297
+ return store.getItemAsync(mnemonicName(accountId), {
298
+ keychainService: MNEMONIC_KEYCHAIN_SERVICE,
299
+ });
300
+ }
301
+ /** The organizations this device keeps a key for (Commons' own index). */
302
+ async function listOrganizationKeys() {
303
+ if ((0, platform_1.isWeb)())
304
+ return [];
305
+ return readIndex(await secureStore());
306
+ }
307
+ /** Forget an organization key on this device: its pair, its phrase, its signer copy and its index entry. */
308
+ async function deleteOrganizationKey(accountId) {
309
+ assertAccountId(accountId);
310
+ if ((0, platform_1.isWeb)())
311
+ return;
312
+ const store = await secureStore();
313
+ const options = pairOptions(store, false);
314
+ await store.deleteItemAsync(privateKeyName(accountId), options);
315
+ await store.deleteItemAsync(publicKeyName(accountId), options);
316
+ await store.deleteItemAsync(mnemonicName(accountId), {
317
+ keychainService: MNEMONIC_KEYCHAIN_SERVICE,
318
+ });
319
+ const signer = (0, platform_1.isAndroid)() ? (0, identitySigner_1.registeredIdentitySignerStore)() : null;
320
+ await signer?.clearOrganization?.(accountId);
321
+ await writeIndex(store, (await readIndex(store)).filter((id) => id !== accountId));
322
+ }
323
+ /**
324
+ * Commons on Android: copy every held organization key into the identity
325
+ * signer store again, so the identity host answers for it after that file was
326
+ * rebuilt (its self-heal leaves it empty). Call on every boot, beside
327
+ * `KeyManager.syncSharedIdentity`. Resolves how many it mirrored. Elsewhere 0.
328
+ */
329
+ async function syncOrganizationKeys() {
330
+ const signer = (0, platform_1.isAndroid)() ? (0, identitySigner_1.registeredIdentitySignerStore)() : null;
331
+ if (!signer?.writeOrganization || !signer.readOrganization)
332
+ return 0;
333
+ const store = await secureStore();
334
+ let mirrored = 0;
335
+ for (const accountId of await readIndex(store)) {
336
+ const options = pairOptions(store, false);
337
+ const privateKey = await store.getItemAsync(privateKeyName(accountId), options);
338
+ const publicKey = await store.getItemAsync(publicKeyName(accountId), options);
339
+ if (!privateKey || !publicKey || !isHealthyOrganizationKeyPair(privateKey, publicKey))
340
+ continue;
341
+ const current = await signer.readOrganization(accountId);
342
+ if (current?.publicKey.toLowerCase() === publicKey.toLowerCase())
343
+ continue;
344
+ if (await signer.writeOrganization(accountId, (0, secp256k1_1.normalizeSecp256k1PrivateKey)(privateKey), publicKey.toLowerCase())) {
345
+ mirrored += 1;
346
+ }
347
+ }
348
+ return mirrored;
349
+ }