@oxy.so/core 7.27.0 → 7.29.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/crypto/index.js +19 -1
- package/dist/cjs/crypto/keyManager.js +106 -106
- package/dist/cjs/crypto/keychainGroupMigration.js +353 -0
- package/dist/cjs/crypto/keychainItems.js +70 -0
- package/dist/cjs/crypto/organizationKeys.js +80 -72
- package/dist/cjs/crypto/secureStoreOptions.js +2 -0
- package/dist/cjs/i18n/locales/compact/es-ES.generated.js +1 -1
- package/dist/cjs/i18n/locales/en-US.js +14 -0
- package/dist/cjs/i18n/locales/es-ES.js +14 -0
- package/dist/cjs/session/index.js +11 -1
- package/dist/cjs/utils/keychainAccessGroups.js +248 -0
- package/dist/esm/crypto/index.js +3 -0
- package/dist/esm/crypto/keyManager.js +63 -63
- package/dist/esm/crypto/keychainGroupMigration.js +348 -0
- package/dist/esm/crypto/keychainItems.js +61 -0
- package/dist/esm/crypto/organizationKeys.js +54 -46
- package/dist/esm/crypto/secureStoreOptions.js +1 -0
- package/dist/esm/i18n/locales/compact/es-ES.generated.js +1 -1
- package/dist/esm/i18n/locales/en-US.js +14 -0
- package/dist/esm/i18n/locales/es-ES.js +14 -0
- package/dist/esm/session/index.js +3 -0
- package/dist/esm/utils/keychainAccessGroups.js +231 -0
- package/dist/types/crypto/index.d.ts +4 -0
- package/dist/types/crypto/keyManager.d.ts +9 -1
- package/dist/types/crypto/keychainGroupMigration.d.ts +27 -0
- package/dist/types/crypto/keychainItems.d.ts +41 -0
- package/dist/types/crypto/organizationKeys.d.ts +11 -9
- package/dist/types/crypto/secureStoreOptions.d.ts +15 -0
- package/dist/types/i18n/locales/en-US.d.ts +14 -0
- package/dist/types/i18n/locales/es-ES.d.ts +14 -0
- package/dist/types/models/interfaces.d.ts +6 -1
- package/dist/types/session/index.d.ts +2 -0
- package/dist/types/utils/keychainAccessGroups.d.ts +104 -0
- package/package.json +2 -2
- package/src/crypto/index.ts +29 -0
- package/src/crypto/keyManager.ts +84 -99
- package/src/crypto/keychainGroupMigration.ts +440 -0
- package/src/crypto/keychainItems.ts +73 -0
- package/src/crypto/organizationKeys.ts +72 -57
- package/src/crypto/secureStoreOptions.ts +32 -0
- package/src/i18n/locales/compact/es-ES.generated.ts +1 -1
- package/src/i18n/locales/en-US.ts +14 -0
- package/src/i18n/locales/es-ES.ts +14 -0
- package/src/models/interfaces.ts +6 -0
- package/src/session/index.ts +11 -0
- package/src/utils/keychainAccessGroups.ts +308 -0
- package/src/types/expo-secure-store.d.ts +0 -21
|
@@ -666,6 +666,20 @@ const dict = {
|
|
|
666
666
|
month: 'Month',
|
|
667
667
|
year: 'Year',
|
|
668
668
|
},
|
|
669
|
+
pronouns: {
|
|
670
|
+
title: 'Pronouns',
|
|
671
|
+
subtitle: 'How people should refer to you',
|
|
672
|
+
label: 'Pronouns',
|
|
673
|
+
options: {
|
|
674
|
+
unspecified: 'Not set',
|
|
675
|
+
he_him: 'He/him',
|
|
676
|
+
she_her: 'She/her',
|
|
677
|
+
they_them: 'They/them',
|
|
678
|
+
he_they: 'He/they',
|
|
679
|
+
she_they: 'She/they',
|
|
680
|
+
it_its: 'It/its',
|
|
681
|
+
},
|
|
682
|
+
},
|
|
669
683
|
locations: {
|
|
670
684
|
title: 'Locations',
|
|
671
685
|
subtitle: "Places you've been or live",
|
|
@@ -368,6 +368,20 @@ const dict = {
|
|
|
368
368
|
month: 'Mes',
|
|
369
369
|
year: 'Año',
|
|
370
370
|
},
|
|
371
|
+
pronouns: {
|
|
372
|
+
title: 'Pronombres',
|
|
373
|
+
subtitle: 'Cómo deben referirse a ti las personas',
|
|
374
|
+
label: 'Pronombres',
|
|
375
|
+
options: {
|
|
376
|
+
unspecified: 'Sin definir',
|
|
377
|
+
he_him: 'He/him',
|
|
378
|
+
she_her: 'She/her',
|
|
379
|
+
they_them: 'They/them',
|
|
380
|
+
he_they: 'He/they',
|
|
381
|
+
she_they: 'She/they',
|
|
382
|
+
it_its: 'It/its',
|
|
383
|
+
},
|
|
384
|
+
},
|
|
371
385
|
locations: {
|
|
372
386
|
title: 'Ubicaciones',
|
|
373
387
|
subtitle: 'Lugares donde has estado o vives',
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* the API with a token it already has never needs it.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.
|
|
11
|
+
exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = exports.refreshDeviceSecretArm = exports.refreshPersistedSession = exports.establishIdentitySession = exports.resolveIdentityPin = exports.IDENTITY_PIN_STORAGE_KEY = exports.identityPinMatches = exports.createMemoryIdentityPinStore = exports.createNativeIdentityPinStore = exports.createWebIdentityPinStore = exports.resolveKeychainAccess = exports.deleteThroughPlan = exports.readThroughPlan = exports.ownKeychainItemPlan = exports.keychainAccessGroupValue = exports.KeychainAccessUnavailableError = exports.readLocalDeviceCredential = exports.publishProvenDeviceCredential = exports.normalizeSharedDeviceSessionRead = exports.decideSharedDevicePublish = exports.decideSharedDeviceJoin = exports.createSharedMirroringAuthStateStore = exports.AUTH_STATE_STORAGE_KEY = exports.createMemoryAuthStateStore = exports.createNativeAuthStateStore = exports.createWebAuthStateStore = exports.createAccountDialogController = exports.AccountDialogController = exports.resolveAccountDelegationAccess = exports.canSwitchIntoAccount = exports.isSwitchTargetAccount = exports.deviceComparisonCode = exports.showsPrincipalHeaders = exports.buildSwitcherRows = exports.resolveDeviceContext = exports.resolveActiveContext = exports.projectDevicePrincipals = exports.directoryHandle = exports.directoryDisplayName = exports.canActivateContext = exports.accountIdsOf = exports.activeUserOf = exports.activeSessionIdOf = exports.deviceStateToClientSessions = exports.createSessionClient = exports.createSessionClientHost = exports.SessionClient = exports.DeviceManager = void 0;
|
|
12
|
+
exports.runColdBoot = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = void 0;
|
|
12
13
|
// Session code reaches the identity key (identity-bound sessions), so the
|
|
13
14
|
// crypto shim goes first — see `../crypto/polyfill`.
|
|
14
15
|
require("../crypto/polyfill");
|
|
@@ -107,6 +108,15 @@ Object.defineProperty(exports, "decideSharedDevicePublish", { enumerable: true,
|
|
|
107
108
|
Object.defineProperty(exports, "normalizeSharedDeviceSessionRead", { enumerable: true, get: function () { return sharedDeviceCredential_1.normalizeSharedDeviceSessionRead; } });
|
|
108
109
|
Object.defineProperty(exports, "publishProvenDeviceCredential", { enumerable: true, get: function () { return sharedDeviceCredential_1.publishProvenDeviceCredential; } });
|
|
109
110
|
Object.defineProperty(exports, "readLocalDeviceCredential", { enumerable: true, get: function () { return sharedDeviceCredential_1.readLocalDeviceCredential; } });
|
|
111
|
+
// Where the shared device credential (and an app's own items) live on iOS:
|
|
112
|
+
// docs/identity/ios-keychain-access-groups.md, ADR 0062.
|
|
113
|
+
var keychainAccessGroups_1 = require("../utils/keychainAccessGroups");
|
|
114
|
+
Object.defineProperty(exports, "KeychainAccessUnavailableError", { enumerable: true, get: function () { return keychainAccessGroups_1.KeychainAccessUnavailableError; } });
|
|
115
|
+
Object.defineProperty(exports, "keychainAccessGroupValue", { enumerable: true, get: function () { return keychainAccessGroups_1.keychainAccessGroupValue; } });
|
|
116
|
+
Object.defineProperty(exports, "ownKeychainItemPlan", { enumerable: true, get: function () { return keychainAccessGroups_1.ownKeychainItemPlan; } });
|
|
117
|
+
Object.defineProperty(exports, "readThroughPlan", { enumerable: true, get: function () { return keychainAccessGroups_1.readThroughPlan; } });
|
|
118
|
+
Object.defineProperty(exports, "deleteThroughPlan", { enumerable: true, get: function () { return keychainAccessGroups_1.deleteThroughPlan; } });
|
|
119
|
+
Object.defineProperty(exports, "resolveKeychainAccess", { enumerable: true, get: function () { return keychainAccessGroups_1.resolveKeychainAccess; } });
|
|
110
120
|
// Identity-bound sessions (the identity vault). The pin is the durable
|
|
111
121
|
// `{publicKey, accountId}` binding between this device's PRIMARY identity key
|
|
112
122
|
// and the account it authenticates as; it is what keeps such a client from
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeychainAccessUnavailableError = exports.KEYCHAIN_ACCESS_GROUPS = void 0;
|
|
4
|
+
exports.keychainGroupFor = keychainGroupFor;
|
|
5
|
+
exports.mayHoldKeychainItem = mayHoldKeychainItem;
|
|
6
|
+
exports.keychainAccessFromDefaultGroup = keychainAccessFromDefaultGroup;
|
|
7
|
+
exports.resolveKeychainAccess = resolveKeychainAccess;
|
|
8
|
+
exports.resetKeychainAccessForTests = resetKeychainAccessForTests;
|
|
9
|
+
exports.keychainAccessGroupValue = keychainAccessGroupValue;
|
|
10
|
+
exports.keychainReadPlan = keychainReadPlan;
|
|
11
|
+
exports.keychainDeletePlan = keychainDeletePlan;
|
|
12
|
+
exports.keychainWriteOption = keychainWriteOption;
|
|
13
|
+
exports.ownKeychainItemPlan = ownKeychainItemPlan;
|
|
14
|
+
exports.readThroughPlan = readThroughPlan;
|
|
15
|
+
exports.deleteThroughPlan = deleteThroughPlan;
|
|
16
|
+
exports.groupedKeychainStore = groupedKeychainStore;
|
|
17
|
+
/**
|
|
18
|
+
* iOS keychain access groups: which group each Oxy keychain item lives in, and
|
|
19
|
+
* which apps may read it (docs/identity/ios-keychain-access-groups.md, ADR 0062).
|
|
20
|
+
*
|
|
21
|
+
* | Group | Held by | Item class |
|
|
22
|
+
* |-------------------------------|------------------------------|-------------------|
|
|
23
|
+
* | `<TEAM>.so.oxy.identity` | Commons only (listed FIRST) | `identity-secret` |
|
|
24
|
+
* | `<TEAM>.so.oxy.wallet` | Commons, the Peable wallet | `spending-key` |
|
|
25
|
+
* | `<TEAM>.group.so.oxy.shared` | every Oxy app | `shared` |
|
|
26
|
+
*
|
|
27
|
+
* - `identity-secret`: the personal root (primary and backup slots), the
|
|
28
|
+
* personal and organization recovery phrases, the organization index. Only
|
|
29
|
+
* Commons may read or write them.
|
|
30
|
+
* - `spending-key`: private keys an app other than Commons signs or derives
|
|
31
|
+
* with in process — the personal key's shared slot and organization pairs
|
|
32
|
+
* (`deriveScopedSeed`, `signSocialReceive`). Commons writes them; the Peable
|
|
33
|
+
* wallet reads them.
|
|
34
|
+
* - `shared`: what every Oxy app may read: the shared public key and the
|
|
35
|
+
* device-session credential.
|
|
36
|
+
*
|
|
37
|
+
* The ROLE of this binary comes from the keychain itself: an item written
|
|
38
|
+
* without a group lands in the app's default group, the first entry of its
|
|
39
|
+
* `keychain-access-groups` entitlement, and `@oxy.so/app-preset` lets only
|
|
40
|
+
* Commons put the identity group first and only the Peable wallet the wallet
|
|
41
|
+
* group. The same probe gives the team prefix, which `kSecAttrAccessGroup`
|
|
42
|
+
* needs and JavaScript cannot otherwise see.
|
|
43
|
+
*
|
|
44
|
+
* Android and web have no access groups: there every helper here answers the
|
|
45
|
+
* single-store behaviour (`ungrouped`), and Android's narrowing is Commons'
|
|
46
|
+
* identity host. An iOS build whose native module predates the probe is also
|
|
47
|
+
* `ungrouped` — exactly what that build's entitlements (one shared group) do.
|
|
48
|
+
*
|
|
49
|
+
* This module never imports the identity stack: services' device-session store
|
|
50
|
+
* and Peable's wallet storage use it too.
|
|
51
|
+
*/
|
|
52
|
+
const protocol_1 = require("@oxy.so/protocol");
|
|
53
|
+
const platform_1 = require("./platform");
|
|
54
|
+
/** The group names after the team prefix, exactly as the entitlements list them. */
|
|
55
|
+
exports.KEYCHAIN_ACCESS_GROUPS = {
|
|
56
|
+
identity: 'so.oxy.identity',
|
|
57
|
+
wallet: 'so.oxy.wallet',
|
|
58
|
+
shared: 'group.so.oxy.shared',
|
|
59
|
+
};
|
|
60
|
+
/** Thrown while the probe cannot answer (before first unlock). Transient. */
|
|
61
|
+
class KeychainAccessUnavailableError extends Error {
|
|
62
|
+
constructor(cause) {
|
|
63
|
+
super('The keychain cannot say which access groups this app holds right now.');
|
|
64
|
+
this.cause = cause;
|
|
65
|
+
this.name = 'KeychainAccessUnavailableError';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.KeychainAccessUnavailableError = KeychainAccessUnavailableError;
|
|
69
|
+
/**
|
|
70
|
+
* The single place each item class is assigned a group. A new kind of item
|
|
71
|
+
* (F1's retained keys, for one) picks its class and reuses everything below.
|
|
72
|
+
*/
|
|
73
|
+
function keychainGroupFor(item) {
|
|
74
|
+
switch (item) {
|
|
75
|
+
case 'identity-secret':
|
|
76
|
+
return 'identity';
|
|
77
|
+
case 'spending-key':
|
|
78
|
+
return 'wallet';
|
|
79
|
+
case 'shared':
|
|
80
|
+
return 'shared';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Whether `role` holds the group `item` lives in. */
|
|
84
|
+
function mayHoldKeychainItem(role, item) {
|
|
85
|
+
switch (role) {
|
|
86
|
+
case 'holder':
|
|
87
|
+
return true;
|
|
88
|
+
case 'wallet':
|
|
89
|
+
return item !== 'identity-secret';
|
|
90
|
+
case 'member':
|
|
91
|
+
return item === 'shared';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Parse the default access group the probe reported. Exported for tests. */
|
|
95
|
+
function keychainAccessFromDefaultGroup(defaultGroup) {
|
|
96
|
+
const dot = defaultGroup.indexOf('.');
|
|
97
|
+
if (dot <= 0 || dot === defaultGroup.length - 1) {
|
|
98
|
+
// Not `<prefix>.<group>`: an unsigned simulator build. Nothing to split.
|
|
99
|
+
return { mode: 'ungrouped' };
|
|
100
|
+
}
|
|
101
|
+
const prefix = defaultGroup.slice(0, dot + 1);
|
|
102
|
+
const group = defaultGroup.slice(dot + 1);
|
|
103
|
+
const role = group === exports.KEYCHAIN_ACCESS_GROUPS.identity
|
|
104
|
+
? 'holder'
|
|
105
|
+
: group === exports.KEYCHAIN_ACCESS_GROUPS.wallet
|
|
106
|
+
? 'wallet'
|
|
107
|
+
: 'member';
|
|
108
|
+
return { mode: 'grouped', prefix, defaultGroup, role };
|
|
109
|
+
}
|
|
110
|
+
let resolvedAccess = null;
|
|
111
|
+
/**
|
|
112
|
+
* This binary's keychain access, memoized once known. THROWS
|
|
113
|
+
* {@link KeychainAccessUnavailableError} while the probe cannot answer, which
|
|
114
|
+
* every caller already treats as "storage unavailable, retry".
|
|
115
|
+
*/
|
|
116
|
+
async function resolveKeychainAccess() {
|
|
117
|
+
if (resolvedAccess) {
|
|
118
|
+
return resolvedAccess;
|
|
119
|
+
}
|
|
120
|
+
if (!(0, platform_1.isIOS)()) {
|
|
121
|
+
// Not memoized: until `@oxy.so/services` registers the platform, a phone
|
|
122
|
+
// reads as "not iOS", and caching that would keep the whole process on the
|
|
123
|
+
// pre-split behaviour.
|
|
124
|
+
return { mode: 'ungrouped' };
|
|
125
|
+
}
|
|
126
|
+
let probe;
|
|
127
|
+
try {
|
|
128
|
+
probe = await (0, protocol_1.loadKeychainDefaultAccessGroup)();
|
|
129
|
+
}
|
|
130
|
+
catch (cause) {
|
|
131
|
+
throw new KeychainAccessUnavailableError(cause);
|
|
132
|
+
}
|
|
133
|
+
if (probe.state === 'unavailable') {
|
|
134
|
+
throw new KeychainAccessUnavailableError(probe.cause);
|
|
135
|
+
}
|
|
136
|
+
resolvedAccess =
|
|
137
|
+
probe.state === 'resolved'
|
|
138
|
+
? keychainAccessFromDefaultGroup(probe.accessGroup)
|
|
139
|
+
: { mode: 'ungrouped' };
|
|
140
|
+
return resolvedAccess;
|
|
141
|
+
}
|
|
142
|
+
/** Forget the memoized access. Tests only. @internal */
|
|
143
|
+
function resetKeychainAccessForTests() {
|
|
144
|
+
resolvedAccess = null;
|
|
145
|
+
}
|
|
146
|
+
/** The full `kSecAttrAccessGroup` value of `group`, or `undefined` when ungrouped. */
|
|
147
|
+
function keychainAccessGroupValue(access, group) {
|
|
148
|
+
return access.mode === 'grouped' ? `${access.prefix}${exports.KEYCHAIN_ACCESS_GROUPS[group]}` : undefined;
|
|
149
|
+
}
|
|
150
|
+
function option(value) {
|
|
151
|
+
return value === undefined ? {} : { accessGroup: value };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The option bags a READ of `item` tries, in order: its own group, then the
|
|
155
|
+
* shared group, where every item lived before the split (release 4 of the doc
|
|
156
|
+
* drops that fallback). Empty when this app may not hold the item: the caller
|
|
157
|
+
* answers `null` without touching the keychain.
|
|
158
|
+
*/
|
|
159
|
+
function keychainReadPlan(access, item) {
|
|
160
|
+
if (access.mode === 'ungrouped') {
|
|
161
|
+
return [{}];
|
|
162
|
+
}
|
|
163
|
+
if (!mayHoldKeychainItem(access.role, item)) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const own = keychainGroupFor(item);
|
|
167
|
+
const plan = [option(keychainAccessGroupValue(access, own))];
|
|
168
|
+
if (own !== 'shared') {
|
|
169
|
+
plan.push(option(keychainAccessGroupValue(access, 'shared')));
|
|
170
|
+
}
|
|
171
|
+
return plan;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Every place a DELETE of `item` must reach: the read plan, so a delete never
|
|
175
|
+
* leaves a pre-split copy behind in the shared group.
|
|
176
|
+
*/
|
|
177
|
+
function keychainDeletePlan(access, item) {
|
|
178
|
+
return keychainReadPlan(access, item);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The option a WRITE of `item` carries. THROWS when this app may not hold the
|
|
182
|
+
* item: an identity secret written by any app but Commons would land in that
|
|
183
|
+
* app's default group, a second holder nobody knows about.
|
|
184
|
+
*/
|
|
185
|
+
function keychainWriteOption(access, item) {
|
|
186
|
+
if (access.mode === 'ungrouped') {
|
|
187
|
+
return {};
|
|
188
|
+
}
|
|
189
|
+
if (!mayHoldKeychainItem(access.role, item)) {
|
|
190
|
+
throw new Error(`This app (keychain role "${access.role}") may not hold a ${item} keychain item; only Commons writes the identity.`);
|
|
191
|
+
}
|
|
192
|
+
return option(keychainAccessGroupValue(access, keychainGroupFor(item)));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The plan for an app's OWN items (its session credential, its settings, an
|
|
196
|
+
* imported wallet's phrase) in Commons and the Peable wallet, whose default
|
|
197
|
+
* group the split changed: written to the default group explicitly — where a
|
|
198
|
+
* write without a group goes anyway — and read from it FIRST, then from every
|
|
199
|
+
* group (`{}`), where all of them were written before. Without the explicit
|
|
200
|
+
* order a read could return whichever of two copies the keychain met first.
|
|
201
|
+
* Deletes reach every group, as they always did.
|
|
202
|
+
*
|
|
203
|
+
* Every other app (`member`) and Android/web keep the calls they always made.
|
|
204
|
+
*/
|
|
205
|
+
function ownKeychainItemPlan(access) {
|
|
206
|
+
if (access.mode === 'ungrouped' || access.role === 'member') {
|
|
207
|
+
return { read: [{}], write: {}, delete: [{}] };
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
read: [option(access.defaultGroup), {}],
|
|
211
|
+
write: option(access.defaultGroup),
|
|
212
|
+
delete: [{}],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** Read `key` through `plan`: the first non-null answer. A thrown read propagates. */
|
|
216
|
+
async function readThroughPlan(store, key, options, plan) {
|
|
217
|
+
for (const group of plan) {
|
|
218
|
+
const value = await store.getItemAsync(key, { ...options, ...group });
|
|
219
|
+
if (value !== null && value !== undefined) {
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
/** Delete `key` from every location in `plan`. A thrown delete propagates. */
|
|
226
|
+
async function deleteThroughPlan(store, key, options, plan) {
|
|
227
|
+
for (const group of plan) {
|
|
228
|
+
await store.deleteItemAsync(key, { ...options, ...group });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* `store` with every call routed to `item`'s groups for this app: reads walk
|
|
233
|
+
* the read plan, writes carry the write option (throwing for a role that may
|
|
234
|
+
* not hold the item), deletes reach every location. The constants of the
|
|
235
|
+
* wrapped store are kept, so `store.WHEN_UNLOCKED_THIS_DEVICE_ONLY` still reads.
|
|
236
|
+
*/
|
|
237
|
+
function groupedKeychainStore(store, access, item) {
|
|
238
|
+
const plan = keychainReadPlan(access, item);
|
|
239
|
+
const wrapped = {
|
|
240
|
+
getItemAsync: (key, options) => readThroughPlan(store, key, options ?? {}, plan),
|
|
241
|
+
setItemAsync: async (key, value, options) => {
|
|
242
|
+
const group = keychainWriteOption(access, item);
|
|
243
|
+
await store.setItemAsync(key, value, { ...(options ?? {}), ...group });
|
|
244
|
+
},
|
|
245
|
+
deleteItemAsync: (key, options) => deleteThroughPlan(store, key, options ?? {}, plan),
|
|
246
|
+
};
|
|
247
|
+
return Object.assign(Object.create(store), wrapped);
|
|
248
|
+
}
|
package/dist/esm/crypto/index.js
CHANGED
|
@@ -17,6 +17,9 @@ export { KeyManager, IdentityAlreadyExistsError, IdentityPersistError, IdentityU
|
|
|
17
17
|
export { readIdentityMarker, updateIdentityMarker } from './identityMarker.js';
|
|
18
18
|
// Organization keys on the device (docs/adr/0061-organization-identity-key.md D6)
|
|
19
19
|
export { generateOrganizationKey, organizationKeyFromPhrase, storeOrganizationKey, getOrganizationPublicKey, getOrganizationPrivateKey, getOrganizationRecoveryPhrase, listOrganizationKeys, deleteOrganizationKey, syncOrganizationKeys, isOrganizationAccountId, isHealthyOrganizationKeyPair, } from './organizationKeys.js';
|
|
20
|
+
// iOS keychain access groups (docs/identity/ios-keychain-access-groups.md, ADR 0062)
|
|
21
|
+
export { KEYCHAIN_ACCESS_GROUPS, KeychainAccessUnavailableError, keychainGroupFor, mayHoldKeychainItem, resolveKeychainAccess, keychainAccessGroupValue, keychainReadPlan, keychainDeletePlan, keychainWriteOption, ownKeychainItemPlan, readThroughPlan, deleteThroughPlan, groupedKeychainStore, } from '../utils/keychainAccessGroups.js';
|
|
22
|
+
export { ensureKeychainGroupsMigrated } from './keychainGroupMigration.js';
|
|
20
23
|
export { SignatureService } from './signatureService.js';
|
|
21
24
|
export { RecoveryPhraseService, BACKUP_KDF_ENCRYPTION_INFO } from './recoveryPhrase.js';
|
|
22
25
|
// Low-level primitives (encrypted backup, device transfer)
|
|
@@ -11,6 +11,9 @@ import { isDev, logger } from '../logger/index.js';
|
|
|
11
11
|
import { deriveScopedSeedFromKey, signSocialReceiveDigest } from './identityDerivations.js';
|
|
12
12
|
import { registerIdentitySignerStore } from './identitySigner.js';
|
|
13
13
|
import { getOrganizationPrivateKey, isOrganizationAccountId } from './organizationKeys.js';
|
|
14
|
+
import { groupedKeychainStore, resolveKeychainAccess, } from '../utils/keychainAccessGroups.js';
|
|
15
|
+
import { ensureKeychainGroupsMigrated } from './keychainGroupMigration.js';
|
|
16
|
+
import { RECOVERY_MNEMONIC_KEYCHAIN_SERVICE, RECOVERY_MNEMONIC_STORAGE_KEY, STORAGE_KEYS, V2_BACKUP_KEYCHAIN_SERVICE, V2_PRIMARY_KEYCHAIN_SERVICE, V2_STORAGE_KEYS, } from './keychainItems.js';
|
|
14
17
|
import { clearIdentityMarker, readIdentityMarker, updateIdentityMarker, writeIdentityMarker, } from './identityMarker.js';
|
|
15
18
|
import { DEVICE_BACKUP_VERSION, parseDeviceBackup, serializeDeviceBackup, } from './deviceBackup.js';
|
|
16
19
|
/**
|
|
@@ -68,16 +71,6 @@ export class IdentityUnavailableError extends Error {
|
|
|
68
71
|
this.name = 'IdentityUnavailableError';
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
|
-
const STORAGE_KEYS = {
|
|
72
|
-
PRIVATE_KEY: 'oxy_identity_private_key',
|
|
73
|
-
PUBLIC_KEY: 'oxy_identity_public_key',
|
|
74
|
-
BACKUP_PRIVATE_KEY: 'oxy_identity_backup_private_key',
|
|
75
|
-
BACKUP_PUBLIC_KEY: 'oxy_identity_backup_public_key',
|
|
76
|
-
BACKUP_TIMESTAMP: 'oxy_identity_backup_timestamp',
|
|
77
|
-
// The cross-app identity in the iOS keychain access group (iOS only).
|
|
78
|
-
SHARED_PRIVATE_KEY: 'oxy_shared_identity_private_key',
|
|
79
|
-
SHARED_PUBLIC_KEY: 'oxy_shared_identity_public_key',
|
|
80
|
-
};
|
|
81
74
|
/**
|
|
82
75
|
* v2 identity slot layout — blast-radius isolation.
|
|
83
76
|
*
|
|
@@ -93,15 +86,8 @@ const STORAGE_KEYS = {
|
|
|
93
86
|
* Migration from the legacy layout is lazy + verify-before-delete — see
|
|
94
87
|
* {@link KeyManager._runSlotMigration}.
|
|
95
88
|
*/
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const V2_STORAGE_KEYS = {
|
|
99
|
-
PRIVATE_KEY: 'oxy_identity_private_key_v2',
|
|
100
|
-
PUBLIC_KEY: 'oxy_identity_public_key_v2',
|
|
101
|
-
BACKUP_PRIVATE_KEY: 'oxy_identity_backup_private_key_v2',
|
|
102
|
-
BACKUP_PUBLIC_KEY: 'oxy_identity_backup_public_key_v2',
|
|
103
|
-
BACKUP_TIMESTAMP: 'oxy_identity_backup_timestamp_v2',
|
|
104
|
-
};
|
|
89
|
+
// The names (V2_PRIMARY_KEYCHAIN_SERVICE, V2_BACKUP_KEYCHAIN_SERVICE,
|
|
90
|
+
// V2_STORAGE_KEYS) live in `./keychainItems`, beside every other identity item.
|
|
105
91
|
/**
|
|
106
92
|
* Dedicated keychain slot for the recovery mnemonic (the 12-word phrase).
|
|
107
93
|
*
|
|
@@ -118,8 +104,7 @@ const V2_STORAGE_KEYS = {
|
|
|
118
104
|
* lives ONLY in this slot: it is never mirrored into the identity marker,
|
|
119
105
|
* {@link KeyManager.getIdentityStatus}, logs, or any exported bundle.
|
|
120
106
|
*/
|
|
121
|
-
|
|
122
|
-
const RECOVERY_MNEMONIC_STORAGE_KEY = 'oxy_identity_mnemonic_v1';
|
|
107
|
+
// RECOVERY_MNEMONIC_KEYCHAIN_SERVICE / RECOVERY_MNEMONIC_STORAGE_KEY: `./keychainItems`.
|
|
123
108
|
/**
|
|
124
109
|
* Advisory AsyncStorage fast-path flag: set once the v2 slots own the identity.
|
|
125
110
|
* Re-derivable (its loss just re-runs the cheap slot check), so it lives in
|
|
@@ -154,12 +139,6 @@ const LEGACY_SLOT_LAYOUT = {
|
|
|
154
139
|
backupPublicKeyName: STORAGE_KEYS.BACKUP_PUBLIC_KEY,
|
|
155
140
|
backupTimestampName: STORAGE_KEYS.BACKUP_TIMESTAMP,
|
|
156
141
|
};
|
|
157
|
-
/**
|
|
158
|
-
* iOS Keychain Access Group for sharing identities across Oxy apps
|
|
159
|
-
* All Oxy apps must have this access group enabled in their entitlements
|
|
160
|
-
* Format: [Team ID].so.oxy.shared or group.so.oxy.shared
|
|
161
|
-
*/
|
|
162
|
-
const IOS_KEYCHAIN_GROUP = 'group.so.oxy.shared';
|
|
163
142
|
/**
|
|
164
143
|
* Initialize React Native specific modules
|
|
165
144
|
*
|
|
@@ -169,7 +148,7 @@ const IOS_KEYCHAIN_GROUP = 'group.so.oxy.shared';
|
|
|
169
148
|
* `expo-secure-store`; off RN it throws (and is never called because every
|
|
170
149
|
* caller is gated by `isWebPlatform()` / native-only paths).
|
|
171
150
|
*/
|
|
172
|
-
async function
|
|
151
|
+
async function loadRawSecureStore() {
|
|
173
152
|
try {
|
|
174
153
|
return await loadSecureStore();
|
|
175
154
|
}
|
|
@@ -178,6 +157,29 @@ async function initSecureStore() {
|
|
|
178
157
|
throw new Error(`Failed to load expo-secure-store: ${errorMessage}. Make sure expo-secure-store is installed and properly configured.`);
|
|
179
158
|
}
|
|
180
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* The secure store routed to the keychain group of `item`
|
|
162
|
+
* (`../utils/keychainAccessGroups`): on iOS reads walk the item's group then
|
|
163
|
+
* the pre-split shared group, writes carry the item's group and THROW in an app
|
|
164
|
+
* that may not hold it, and in Commons every access first awaits the
|
|
165
|
+
* access-group migration. Android and web are unchanged (`ungrouped`).
|
|
166
|
+
*
|
|
167
|
+
* Throws when the store cannot load or the keychain cannot say which groups
|
|
168
|
+
* this app holds; every caller already treats a throw here as "storage
|
|
169
|
+
* unavailable".
|
|
170
|
+
*/
|
|
171
|
+
async function keychainItemStore(item) {
|
|
172
|
+
const raw = await loadRawSecureStore();
|
|
173
|
+
const access = await resolveKeychainAccess();
|
|
174
|
+
if (access.mode === 'grouped' && access.role === 'holder') {
|
|
175
|
+
await ensureKeychainGroupsMigrated();
|
|
176
|
+
}
|
|
177
|
+
return groupedKeychainStore(raw, access, item);
|
|
178
|
+
}
|
|
179
|
+
/** The store for the identity slots (primary, backup, legacy, phrase): Commons only on iOS. */
|
|
180
|
+
function initSecureStore() {
|
|
181
|
+
return keychainItemStore('identity-secret');
|
|
182
|
+
}
|
|
181
183
|
/**
|
|
182
184
|
* Check if we're on web platform
|
|
183
185
|
* Identity storage is only available on native platforms (iOS/Android)
|
|
@@ -568,12 +570,12 @@ export class KeyManager {
|
|
|
568
570
|
* rung. Best-effort — the shared slot is a redundant convenience copy. The
|
|
569
571
|
* Android signer store is cleared on every delete, in {@link deleteIdentity}.
|
|
570
572
|
*/
|
|
571
|
-
static async _clearSharedSlot(
|
|
573
|
+
static async _clearSharedSlot(_identityStore) {
|
|
572
574
|
try {
|
|
573
575
|
if (isIOS()) {
|
|
574
|
-
|
|
575
|
-
await
|
|
576
|
-
await
|
|
576
|
+
// The shared slot lives in two groups of its own, not the identity slots'.
|
|
577
|
+
await (await keychainItemStore('spending-key')).deleteItemAsync(STORAGE_KEYS.SHARED_PRIVATE_KEY);
|
|
578
|
+
await (await keychainItemStore('shared')).deleteItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY);
|
|
577
579
|
}
|
|
578
580
|
KeyManager.invalidateSharedCache();
|
|
579
581
|
}
|
|
@@ -640,6 +642,20 @@ export class KeyManager {
|
|
|
640
642
|
KeyManager.cachedSharedPublicKey = null;
|
|
641
643
|
KeyManager.cachedHasSharedIdentity = null;
|
|
642
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* iOS: write the personal key's cross-app slot — the private key to the
|
|
647
|
+
* wallet group (Commons and the Peable wallet), the public key to the shared
|
|
648
|
+
* group (every Oxy app). Throws in any app that may not hold the private key.
|
|
649
|
+
* @internal
|
|
650
|
+
*/
|
|
651
|
+
static async _writeIosSharedSlot(privateKey, publicKey) {
|
|
652
|
+
const privateStore = await keychainItemStore('spending-key');
|
|
653
|
+
const privateOpts = {
|
|
654
|
+
keychainAccessible: privateStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
655
|
+
};
|
|
656
|
+
await privateStore.setItemAsync(STORAGE_KEYS.SHARED_PRIVATE_KEY, privateKey, privateOpts);
|
|
657
|
+
await (await keychainItemStore('shared')).setItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, publicKey, {});
|
|
658
|
+
}
|
|
643
659
|
/**
|
|
644
660
|
* Lowercase and pad to canonical 64-hex-char form.
|
|
645
661
|
*
|
|
@@ -675,8 +691,10 @@ export class KeyManager {
|
|
|
675
691
|
// ==================== SHARED IDENTITY METHODS ====================
|
|
676
692
|
// One Oxy identity across every Oxy app on a device.
|
|
677
693
|
//
|
|
678
|
-
// iOS:
|
|
679
|
-
//
|
|
694
|
+
// iOS: keychain access groups (docs/identity/ios-keychain-access-groups.md).
|
|
695
|
+
// The private key is in `<TEAM>.so.oxy.wallet` (Commons and the Peable
|
|
696
|
+
// wallet only), the public key in `<TEAM>.group.so.oxy.shared`, which every
|
|
697
|
+
// Oxy app reads. Every other app signs in through Commons (ADR 0029).
|
|
680
698
|
// Android: every app has its own UID, so nothing is shared through storage.
|
|
681
699
|
// Commons is the ONLY holder of the private key. It keeps the identity
|
|
682
700
|
// signer store (`setIdentitySignerStore`) for its identity host, and every
|
|
@@ -701,21 +719,11 @@ export class KeyManager {
|
|
|
701
719
|
}
|
|
702
720
|
const { privateKey, publicKey } = await KeyManager.generateKeyPair();
|
|
703
721
|
if (isIOS()) {
|
|
704
|
-
const store = await initSecureStore();
|
|
705
|
-
// Note: keychainAccessGroup requires Keychain Sharing capability in Xcode
|
|
706
722
|
try {
|
|
707
|
-
|
|
708
|
-
keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
709
|
-
keychainAccessGroup: IOS_KEYCHAIN_GROUP, // Enables sharing across apps
|
|
710
|
-
};
|
|
711
|
-
await store.setItemAsync(STORAGE_KEYS.SHARED_PRIVATE_KEY, privateKey, privateOpts);
|
|
712
|
-
const publicOpts = {
|
|
713
|
-
keychainAccessGroup: IOS_KEYCHAIN_GROUP,
|
|
714
|
-
};
|
|
715
|
-
await store.setItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, publicKey, publicOpts);
|
|
723
|
+
await KeyManager._writeIosSharedSlot(privateKey, publicKey);
|
|
716
724
|
}
|
|
717
725
|
catch (error) {
|
|
718
|
-
throw new Error(`Failed to create shared identity on iOS.
|
|
726
|
+
throw new Error(`Failed to create shared identity on iOS. Only Commons may write it, with the keychain access groups of docs/identity/ios-keychain-access-groups.md. Error: ${error}`);
|
|
719
727
|
}
|
|
720
728
|
}
|
|
721
729
|
else if (isAndroid()) {
|
|
@@ -755,9 +763,8 @@ export class KeyManager {
|
|
|
755
763
|
try {
|
|
756
764
|
let publicKey = null;
|
|
757
765
|
if (isIOS()) {
|
|
758
|
-
const store = await
|
|
759
|
-
|
|
760
|
-
publicKey = await store.getItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, opts);
|
|
766
|
+
const store = await keychainItemStore('shared');
|
|
767
|
+
publicKey = await store.getItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, {});
|
|
761
768
|
}
|
|
762
769
|
else if (isAndroid()) {
|
|
763
770
|
const signer = KeyManager.identitySignerStore;
|
|
@@ -797,9 +804,10 @@ export class KeyManager {
|
|
|
797
804
|
return null;
|
|
798
805
|
}
|
|
799
806
|
try {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
807
|
+
// The wallet group: Commons and the Peable wallet. Every other app's plan
|
|
808
|
+
// is empty, so this is `null` there without touching the keychain.
|
|
809
|
+
const store = await keychainItemStore('spending-key');
|
|
810
|
+
return await store.getItemAsync(STORAGE_KEYS.SHARED_PRIVATE_KEY, {});
|
|
803
811
|
}
|
|
804
812
|
catch (error) {
|
|
805
813
|
if (isDev()) {
|
|
@@ -893,16 +901,7 @@ export class KeyManager {
|
|
|
893
901
|
const canonicalPrivate = KeyManager.canonicalPrivateKey(privateKey);
|
|
894
902
|
const publicKey = deriveSecp256k1PublicKey(canonicalPrivate);
|
|
895
903
|
if (isIOS()) {
|
|
896
|
-
|
|
897
|
-
const privateOpts = {
|
|
898
|
-
keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
899
|
-
keychainAccessGroup: IOS_KEYCHAIN_GROUP,
|
|
900
|
-
};
|
|
901
|
-
await store.setItemAsync(STORAGE_KEYS.SHARED_PRIVATE_KEY, canonicalPrivate, privateOpts);
|
|
902
|
-
const publicOpts = {
|
|
903
|
-
keychainAccessGroup: IOS_KEYCHAIN_GROUP,
|
|
904
|
-
};
|
|
905
|
-
await store.setItemAsync(STORAGE_KEYS.SHARED_PUBLIC_KEY, publicKey, publicOpts);
|
|
904
|
+
await KeyManager._writeIosSharedSlot(canonicalPrivate, publicKey);
|
|
906
905
|
}
|
|
907
906
|
else if (isAndroid()) {
|
|
908
907
|
await KeyManager._writeIdentitySigner(canonicalPrivate, publicKey);
|
|
@@ -2567,7 +2566,8 @@ export class KeyManager {
|
|
|
2567
2566
|
* `HDKey.fromMasterSeed` and never touches the identity key.
|
|
2568
2567
|
*
|
|
2569
2568
|
* Key source (native only):
|
|
2570
|
-
* - iOS: the shared
|
|
2569
|
+
* - iOS: the shared slot's private key in the wallet keychain group (Commons
|
|
2570
|
+
* and the Peable wallet only; `null` in every other app), then this device's
|
|
2571
2571
|
* primary identity.
|
|
2572
2572
|
* - Android: Commons (the one app with a signer store) uses its own primary
|
|
2573
2573
|
* identity; every other app asks Commons over IPC and never reads a local
|