@oxy.so/protocol 1.10.0 → 1.11.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/index.js +2 -1
- package/dist/cjs/platform/crypto.js +4 -0
- package/dist/cjs/platform/crypto.native.js +22 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/platform/crypto.js +3 -0
- package/dist/esm/platform/crypto.native.js +21 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/platform/crypto.d.ts +3 -2
- package/dist/types/platform/crypto.native.d.ts +3 -2
- package/dist/types/platform/expoTypes.d.ts +14 -0
- package/package.json +2 -2
- package/src/__tests__/keychainDefaultAccessGroup.test.ts +36 -0
- package/src/index.ts +2 -0
- package/src/platform/crypto.native.ts +33 -0
- package/src/platform/crypto.ts +6 -0
- package/src/platform/expoTypes.ts +19 -0
package/dist/cjs/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* subpath so they never enter React Native / web bundles.
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.loadKeychainApplicationAccessGroup = exports.loadKeychainDefaultAccessGroup = exports.loadCommonsIdentityBridge = exports.getRandomBytesRN = exports.loadAsyncStorage = exports.loadSecureStore = exports.loadExpoCrypto = exports.loadNodeCrypto = exports.isNodeJS = exports.isReactNative = exports.isAuthorizedKey = exports.REGISTRATION_POW_DIFFICULTY_BITS = exports.meetsRegistrationPowDifficulty = exports.countLeadingZeroBits = exports.registrationPowMessage = exports.verifyCheckpointSignature = exports.signCheckpoint = exports.checkpointHash = exports.checkpointSigningInput = exports.verifyInclusionProof = exports.inclusionProof = exports.buildTransparencyTreeFromHeads = exports.buildTransparencyTree = exports.transparencyLeafHash = exports.EMPTY_TRANSPARENCY_ROOT = exports.verifyAndAppend = exports.DEFAULT_CLOCK_SKEW_MS = exports.verifyEnvelope = exports.checkContinuity = exports.UNCHAINED_SEQ = exports.verifyEnvelopeSignature = exports.signEnvelope = exports.verifySignature = exports.signMessage = exports.computeRecordId = exports.sha256 = exports.signedRecordSigningInput = exports.canonicalize = void 0;
|
|
14
|
+
exports.loadKeychainReadWithoutInteraction = exports.loadKeychainApplicationAccessGroup = exports.loadKeychainDefaultAccessGroup = exports.loadCommonsIdentityBridge = exports.getRandomBytesRN = exports.loadAsyncStorage = exports.loadSecureStore = exports.loadExpoCrypto = exports.loadNodeCrypto = exports.isNodeJS = exports.isReactNative = exports.isAuthorizedKey = exports.REGISTRATION_POW_DIFFICULTY_BITS = exports.meetsRegistrationPowDifficulty = exports.countLeadingZeroBits = exports.registrationPowMessage = exports.verifyCheckpointSignature = exports.signCheckpoint = exports.checkpointHash = exports.checkpointSigningInput = exports.verifyInclusionProof = exports.inclusionProof = exports.buildTransparencyTreeFromHeads = exports.buildTransparencyTree = exports.transparencyLeafHash = exports.EMPTY_TRANSPARENCY_ROOT = exports.verifyAndAppend = exports.DEFAULT_CLOCK_SKEW_MS = exports.verifyEnvelope = exports.checkContinuity = exports.UNCHAINED_SEQ = exports.verifyEnvelopeSignature = exports.signEnvelope = exports.verifySignature = exports.signMessage = exports.computeRecordId = exports.sha256 = exports.signedRecordSigningInput = exports.canonicalize = void 0;
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Envelope — canonical JSON, signing input, content address, signing/verify
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
@@ -79,3 +79,4 @@ Object.defineProperty(exports, "getRandomBytesRN", { enumerable: true, get: func
|
|
|
79
79
|
Object.defineProperty(exports, "loadCommonsIdentityBridge", { enumerable: true, get: function () { return crypto_1.loadCommonsIdentityBridge; } });
|
|
80
80
|
Object.defineProperty(exports, "loadKeychainDefaultAccessGroup", { enumerable: true, get: function () { return crypto_1.loadKeychainDefaultAccessGroup; } });
|
|
81
81
|
Object.defineProperty(exports, "loadKeychainApplicationAccessGroup", { enumerable: true, get: function () { return crypto_1.loadKeychainApplicationAccessGroup; } });
|
|
82
|
+
Object.defineProperty(exports, "loadKeychainReadWithoutInteraction", { enumerable: true, get: function () { return crypto_1.loadKeychainReadWithoutInteraction; } });
|
|
@@ -114,6 +114,7 @@ exports.loadAsyncStorage = loadAsyncStorage;
|
|
|
114
114
|
exports.loadCommonsIdentityBridge = loadCommonsIdentityBridge;
|
|
115
115
|
exports.loadKeychainDefaultAccessGroup = loadKeychainDefaultAccessGroup;
|
|
116
116
|
exports.loadKeychainApplicationAccessGroup = loadKeychainApplicationAccessGroup;
|
|
117
|
+
exports.loadKeychainReadWithoutInteraction = loadKeychainReadWithoutInteraction;
|
|
117
118
|
const platform_1 = require("./platform");
|
|
118
119
|
// ---------------------------------------------------------------------------
|
|
119
120
|
// Node `crypto` — Node built-in
|
|
@@ -188,3 +189,6 @@ function loadKeychainDefaultAccessGroup() {
|
|
|
188
189
|
function loadKeychainApplicationAccessGroup(_prefix) {
|
|
189
190
|
return Promise.resolve({ state: 'unsupported' });
|
|
190
191
|
}
|
|
192
|
+
function loadKeychainReadWithoutInteraction() {
|
|
193
|
+
return Promise.resolve(null);
|
|
194
|
+
}
|
|
@@ -61,6 +61,7 @@ exports.loadAsyncStorage = loadAsyncStorage;
|
|
|
61
61
|
exports.loadCommonsIdentityBridge = loadCommonsIdentityBridge;
|
|
62
62
|
exports.loadKeychainDefaultAccessGroup = loadKeychainDefaultAccessGroup;
|
|
63
63
|
exports.loadKeychainApplicationAccessGroup = loadKeychainApplicationAccessGroup;
|
|
64
|
+
exports.loadKeychainReadWithoutInteraction = loadKeychainReadWithoutInteraction;
|
|
64
65
|
const expo_modules_core_1 = require("expo-modules-core");
|
|
65
66
|
const optionalPeer_1 = require("./optionalPeer");
|
|
66
67
|
const random_native_1 = require("./random.native");
|
|
@@ -383,3 +384,24 @@ async function loadKeychainApplicationAccessGroup(prefix) {
|
|
|
383
384
|
keychainApplicationAccessGroup = { state: 'resolved', accessGroup: answer };
|
|
384
385
|
return keychainApplicationAccessGroup;
|
|
385
386
|
}
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// iOS keychain read without user interaction (R5-9). `null` when the native
|
|
389
|
+
// module predates it (or off iOS): the caller then reads through
|
|
390
|
+
// expo-secure-store, as before. The answer crossed the native boundary, so only
|
|
391
|
+
// a string or `null` is accepted.
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
async function loadKeychainReadWithoutInteraction() {
|
|
394
|
+
const native = (0, expo_modules_core_1.requireOptionalNativeModule)('OxyIdentity');
|
|
395
|
+
const read = native?.readKeychainItemWithoutInteraction;
|
|
396
|
+
if (!native || typeof read !== 'function')
|
|
397
|
+
return null;
|
|
398
|
+
return async (key, service, accessGroup) => {
|
|
399
|
+
const answer = await read.call(native, key, service ?? null, accessGroup);
|
|
400
|
+
if (answer === null || answer === undefined)
|
|
401
|
+
return null;
|
|
402
|
+
if (typeof answer !== 'string') {
|
|
403
|
+
throw new Error('keychain read answered something that is not a string');
|
|
404
|
+
}
|
|
405
|
+
return answer;
|
|
406
|
+
};
|
|
407
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -37,4 +37,4 @@ export { isAuthorizedKey } from './identity/resolver.js';
|
|
|
37
37
|
// Platform — runtime predicates + lazy crypto/storage loaders
|
|
38
38
|
// ---------------------------------------------------------------------------
|
|
39
39
|
export { isReactNative, isNodeJS } from './platform/platform.js';
|
|
40
|
-
export { loadNodeCrypto, loadExpoCrypto, loadSecureStore, loadAsyncStorage, getRandomBytesRN, loadCommonsIdentityBridge, loadKeychainDefaultAccessGroup, loadKeychainApplicationAccessGroup, } from './platform/crypto.js';
|
|
40
|
+
export { loadNodeCrypto, loadExpoCrypto, loadSecureStore, loadAsyncStorage, getRandomBytesRN, loadCommonsIdentityBridge, loadKeychainDefaultAccessGroup, loadKeychainApplicationAccessGroup, loadKeychainReadWithoutInteraction, } from './platform/crypto.js';
|
|
@@ -144,3 +144,6 @@ export function loadKeychainDefaultAccessGroup() {
|
|
|
144
144
|
export function loadKeychainApplicationAccessGroup(_prefix) {
|
|
145
145
|
return Promise.resolve({ state: 'unsupported' });
|
|
146
146
|
}
|
|
147
|
+
export function loadKeychainReadWithoutInteraction() {
|
|
148
|
+
return Promise.resolve(null);
|
|
149
|
+
}
|
|
@@ -372,3 +372,24 @@ export async function loadKeychainApplicationAccessGroup(prefix) {
|
|
|
372
372
|
keychainApplicationAccessGroup = { state: 'resolved', accessGroup: answer };
|
|
373
373
|
return keychainApplicationAccessGroup;
|
|
374
374
|
}
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// iOS keychain read without user interaction (R5-9). `null` when the native
|
|
377
|
+
// module predates it (or off iOS): the caller then reads through
|
|
378
|
+
// expo-secure-store, as before. The answer crossed the native boundary, so only
|
|
379
|
+
// a string or `null` is accepted.
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
export async function loadKeychainReadWithoutInteraction() {
|
|
382
|
+
const native = requireOptionalNativeModule('OxyIdentity');
|
|
383
|
+
const read = native?.readKeychainItemWithoutInteraction;
|
|
384
|
+
if (!native || typeof read !== 'function')
|
|
385
|
+
return null;
|
|
386
|
+
return async (key, service, accessGroup) => {
|
|
387
|
+
const answer = await read.call(native, key, service ?? null, accessGroup);
|
|
388
|
+
if (answer === null || answer === undefined)
|
|
389
|
+
return null;
|
|
390
|
+
if (typeof answer !== 'string') {
|
|
391
|
+
throw new Error('keychain read answered something that is not a string');
|
|
392
|
+
}
|
|
393
|
+
return answer;
|
|
394
|
+
};
|
|
395
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -29,5 +29,5 @@ export { registrationPowMessage, countLeadingZeroBits, meetsRegistrationPowDiffi
|
|
|
29
29
|
export { isAuthorizedKey } from './identity/resolver';
|
|
30
30
|
export type { VerificationMethodResolver, ResolvedVerificationMethods, KeyAuthorization, } from './identity/resolver';
|
|
31
31
|
export { isReactNative, isNodeJS } from './platform/platform';
|
|
32
|
-
export { loadNodeCrypto, loadExpoCrypto, loadSecureStore, loadAsyncStorage, getRandomBytesRN, loadCommonsIdentityBridge, loadKeychainDefaultAccessGroup, loadKeychainApplicationAccessGroup, } from './platform/crypto';
|
|
33
|
-
export type { CommonsIdentityBridge, CommonsIdentityDescription, CommonsIdentityProof, CommonsSocialReceiveSignature, ExpoCryptoLike, ExpoSecureStoreLike, KeychainApplicationAccessGroup, KeychainDefaultAccessGroup, } from './platform/expoTypes';
|
|
32
|
+
export { loadNodeCrypto, loadExpoCrypto, loadSecureStore, loadAsyncStorage, getRandomBytesRN, loadCommonsIdentityBridge, loadKeychainDefaultAccessGroup, loadKeychainApplicationAccessGroup, loadKeychainReadWithoutInteraction, } from './platform/crypto';
|
|
33
|
+
export type { CommonsIdentityBridge, CommonsIdentityDescription, CommonsIdentityProof, CommonsSocialReceiveSignature, ExpoCryptoLike, ExpoSecureStoreLike, KeychainApplicationAccessGroup, KeychainDefaultAccessGroup, KeychainReadWithoutInteraction, } from './platform/expoTypes';
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
* see a file containing only specifiers it can understand — no tricks, no
|
|
72
72
|
* runtime parsing risks.
|
|
73
73
|
*/
|
|
74
|
-
import type { CommonsIdentityBridge, CommonsIdentityDescription, CommonsIdentityProof, CommonsSocialReceiveSignature, ExpoCryptoLike, ExpoSecureStoreLike, KeychainApplicationAccessGroup, KeychainDefaultAccessGroup } from './expoTypes';
|
|
75
|
-
export type { CommonsIdentityBridge, CommonsIdentityDescription, CommonsIdentityProof, CommonsSocialReceiveSignature, ExpoCryptoLike, ExpoSecureStoreLike, KeychainApplicationAccessGroup, KeychainDefaultAccessGroup, };
|
|
74
|
+
import type { CommonsIdentityBridge, CommonsIdentityDescription, CommonsIdentityProof, CommonsSocialReceiveSignature, ExpoCryptoLike, ExpoSecureStoreLike, KeychainApplicationAccessGroup, KeychainDefaultAccessGroup, KeychainReadWithoutInteraction } from './expoTypes';
|
|
75
|
+
export type { CommonsIdentityBridge, CommonsIdentityDescription, CommonsIdentityProof, CommonsSocialReceiveSignature, ExpoCryptoLike, ExpoSecureStoreLike, KeychainApplicationAccessGroup, KeychainDefaultAccessGroup, KeychainReadWithoutInteraction, };
|
|
76
76
|
export declare function loadNodeCrypto(): Promise<typeof import('crypto')>;
|
|
77
77
|
export declare function loadExpoCrypto(): Promise<ExpoCryptoLike>;
|
|
78
78
|
export declare function loadSecureStore(): Promise<ExpoSecureStoreLike>;
|
|
@@ -87,3 +87,4 @@ export { getRandomBytesRN } from './random';
|
|
|
87
87
|
export declare function loadCommonsIdentityBridge(): Promise<CommonsIdentityBridge | null>;
|
|
88
88
|
export declare function loadKeychainDefaultAccessGroup(): Promise<KeychainDefaultAccessGroup>;
|
|
89
89
|
export declare function loadKeychainApplicationAccessGroup(_prefix: string): Promise<KeychainApplicationAccessGroup>;
|
|
90
|
+
export declare function loadKeychainReadWithoutInteraction(): Promise<KeychainReadWithoutInteraction | null>;
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
* `expo-modules-core` is a NON-optional peer (every RN app has it via `expo`),
|
|
52
52
|
* so it stays a plain static import.
|
|
53
53
|
*/
|
|
54
|
-
import type { CommonsIdentityBridge, CommonsIdentityDescription, CommonsIdentityProof, CommonsSocialReceiveSignature, ExpoCryptoLike, ExpoSecureStoreLike, KeychainApplicationAccessGroup, KeychainDefaultAccessGroup } from './expoTypes';
|
|
55
|
-
export type { CommonsIdentityBridge, CommonsIdentityDescription, CommonsIdentityProof, CommonsSocialReceiveSignature, ExpoCryptoLike, ExpoSecureStoreLike, KeychainApplicationAccessGroup, KeychainDefaultAccessGroup, };
|
|
54
|
+
import type { CommonsIdentityBridge, CommonsIdentityDescription, CommonsIdentityProof, CommonsSocialReceiveSignature, ExpoCryptoLike, ExpoSecureStoreLike, KeychainApplicationAccessGroup, KeychainDefaultAccessGroup, KeychainReadWithoutInteraction } from './expoTypes';
|
|
55
|
+
export type { CommonsIdentityBridge, CommonsIdentityDescription, CommonsIdentityProof, CommonsSocialReceiveSignature, ExpoCryptoLike, ExpoSecureStoreLike, KeychainApplicationAccessGroup, KeychainDefaultAccessGroup, KeychainReadWithoutInteraction, };
|
|
56
56
|
/** Persistent KV storage surface used from `@react-native-async-storage/async-storage`. */
|
|
57
57
|
type AsyncStorageLike = {
|
|
58
58
|
getItem: (key: string) => Promise<string | null>;
|
|
@@ -69,3 +69,4 @@ export { getRandomBytesRN } from './random.native';
|
|
|
69
69
|
export declare function loadCommonsIdentityBridge(): Promise<CommonsIdentityBridge | null>;
|
|
70
70
|
export declare function loadKeychainDefaultAccessGroup(): Promise<KeychainDefaultAccessGroup>;
|
|
71
71
|
export declare function loadKeychainApplicationAccessGroup(prefix: string): Promise<KeychainApplicationAccessGroup>;
|
|
72
|
+
export declare function loadKeychainReadWithoutInteraction(): Promise<KeychainReadWithoutInteraction | null>;
|
|
@@ -220,3 +220,17 @@ export type KeychainApplicationAccessGroup = {
|
|
|
220
220
|
state: 'unavailable';
|
|
221
221
|
cause: unknown;
|
|
222
222
|
};
|
|
223
|
+
/**
|
|
224
|
+
* iOS: read one `expo-secure-store` item WITHOUT ever asking the user
|
|
225
|
+
* (round-5 review R5-9). `expo-secure-store`'s own read tries the item's
|
|
226
|
+
* `<service>:no-auth` copy, then its `<service>:auth` copy (an item stored
|
|
227
|
+
* with `requireAuthentication`, which shows a Face ID / passcode prompt),
|
|
228
|
+
* then the legacy `<service>` copy. Any app in a shared access group can plant
|
|
229
|
+
* an `:auth` copy, so a read of that group could stall Commons behind prompts.
|
|
230
|
+
* This reader tries `<service>:no-auth` then `<service>`, both with an
|
|
231
|
+
* `LAContext` whose `interactionNotAllowed` is set, and never the `:auth`
|
|
232
|
+
* copy: an item that needs the user answers `null`. `service` defaults to
|
|
233
|
+
* expo-secure-store's `app`. A keychain error other than "not found" or
|
|
234
|
+
* "interaction not allowed" throws.
|
|
235
|
+
*/
|
|
236
|
+
export type KeychainReadWithoutInteraction = (key: string, service: string | undefined, accessGroup: string) => Promise<string | null>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/protocol",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Oxy Protocol — the app-agnostic base substrate: signed-record envelope, canonical JSON, signature/verification, and platform crypto. Reused by any Oxy app to decentralize its own content.",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
},
|
|
134
134
|
"dependencies": {
|
|
135
135
|
"@noble/curves": "^2.4.0",
|
|
136
|
-
"@oxy.so/contracts": "^6.
|
|
136
|
+
"@oxy.so/contracts": "^6.13.0"
|
|
137
137
|
},
|
|
138
138
|
"peerDependencies": {
|
|
139
139
|
"@react-native-async-storage/async-storage": "<4.0.0",
|
|
@@ -128,3 +128,39 @@ describe('loadKeychainApplicationAccessGroup (native, ADR 0062 D6)', () => {
|
|
|
128
128
|
});
|
|
129
129
|
});
|
|
130
130
|
});
|
|
131
|
+
|
|
132
|
+
describe('loadKeychainReadWithoutInteraction (native, R5-9)', () => {
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
jest.resetModules();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('a native module without it answers null: the caller reads as before', async () => {
|
|
138
|
+
mockNative({ describe: async () => null });
|
|
139
|
+
const mod = await import('../platform/crypto.native');
|
|
140
|
+
await expect(mod.loadKeychainReadWithoutInteraction()).resolves.toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('passes the key, service and group through; only a string or null crosses back', async () => {
|
|
144
|
+
const readKeychainItemWithoutInteraction = jest.fn(async (key: string) =>
|
|
145
|
+
key === 'present' ? 'value' : key === 'junk' ? 42 : null,
|
|
146
|
+
);
|
|
147
|
+
mockNative({ readKeychainItemWithoutInteraction });
|
|
148
|
+
const mod = await import('../platform/crypto.native');
|
|
149
|
+
const read = await mod.loadKeychainReadWithoutInteraction();
|
|
150
|
+
expect(read).not.toBeNull();
|
|
151
|
+
await expect(read?.('present', 'svc', 'ABCDE12345.group.so.oxy.shared')).resolves.toBe('value');
|
|
152
|
+
expect(readKeychainItemWithoutInteraction).toHaveBeenCalledWith(
|
|
153
|
+
'present',
|
|
154
|
+
'svc',
|
|
155
|
+
'ABCDE12345.group.so.oxy.shared',
|
|
156
|
+
);
|
|
157
|
+
await expect(read?.('absent', undefined, 'g')).resolves.toBeNull();
|
|
158
|
+
expect(readKeychainItemWithoutInteraction).toHaveBeenLastCalledWith('absent', null, 'g');
|
|
159
|
+
await expect(read?.('junk', undefined, 'g')).rejects.toThrow('not a string');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('the web / Node variant has none', async () => {
|
|
163
|
+
const mod = await import('../platform/crypto');
|
|
164
|
+
await expect(mod.loadKeychainReadWithoutInteraction()).resolves.toBeNull();
|
|
165
|
+
});
|
|
166
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -101,6 +101,7 @@ export {
|
|
|
101
101
|
loadCommonsIdentityBridge,
|
|
102
102
|
loadKeychainDefaultAccessGroup,
|
|
103
103
|
loadKeychainApplicationAccessGroup,
|
|
104
|
+
loadKeychainReadWithoutInteraction,
|
|
104
105
|
} from './platform/crypto';
|
|
105
106
|
|
|
106
107
|
// ---------------------------------------------------------------------------
|
|
@@ -119,4 +120,5 @@ export type {
|
|
|
119
120
|
ExpoSecureStoreLike,
|
|
120
121
|
KeychainApplicationAccessGroup,
|
|
121
122
|
KeychainDefaultAccessGroup,
|
|
123
|
+
KeychainReadWithoutInteraction,
|
|
122
124
|
} from './platform/expoTypes';
|
|
@@ -62,6 +62,7 @@ import type {
|
|
|
62
62
|
ExpoSecureStoreLike,
|
|
63
63
|
KeychainApplicationAccessGroup,
|
|
64
64
|
KeychainDefaultAccessGroup,
|
|
65
|
+
KeychainReadWithoutInteraction,
|
|
65
66
|
} from './expoTypes';
|
|
66
67
|
import { missingOptionalPeerError } from './optionalPeer';
|
|
67
68
|
import { requireExpoCrypto } from './random.native';
|
|
@@ -77,6 +78,7 @@ export type {
|
|
|
77
78
|
ExpoSecureStoreLike,
|
|
78
79
|
KeychainApplicationAccessGroup,
|
|
79
80
|
KeychainDefaultAccessGroup,
|
|
81
|
+
KeychainReadWithoutInteraction,
|
|
80
82
|
};
|
|
81
83
|
|
|
82
84
|
// ---------------------------------------------------------------------------
|
|
@@ -234,6 +236,16 @@ interface OxyIdentityNativeModule {
|
|
|
234
236
|
* modules and Android lack it.
|
|
235
237
|
*/
|
|
236
238
|
keychainApplicationAccessGroup?(prefix: string): Promise<unknown>;
|
|
239
|
+
/**
|
|
240
|
+
* iOS only, added in round 5 (R5-9): read one expo-secure-store item without
|
|
241
|
+
* user interaction, never its `:auth` copy. Older native modules and Android
|
|
242
|
+
* lack it.
|
|
243
|
+
*/
|
|
244
|
+
readKeychainItemWithoutInteraction?(
|
|
245
|
+
key: string,
|
|
246
|
+
service: string | null,
|
|
247
|
+
accessGroup: string,
|
|
248
|
+
): Promise<unknown>;
|
|
237
249
|
/**
|
|
238
250
|
* Retired keys (Commons protocol 4), added after the organization methods and
|
|
239
251
|
* just as absent on an older native module. `accountId` is `null` for the
|
|
@@ -545,3 +557,24 @@ export async function loadKeychainApplicationAccessGroup(
|
|
|
545
557
|
keychainApplicationAccessGroup = { state: 'resolved', accessGroup: answer };
|
|
546
558
|
return keychainApplicationAccessGroup;
|
|
547
559
|
}
|
|
560
|
+
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
// iOS keychain read without user interaction (R5-9). `null` when the native
|
|
563
|
+
// module predates it (or off iOS): the caller then reads through
|
|
564
|
+
// expo-secure-store, as before. The answer crossed the native boundary, so only
|
|
565
|
+
// a string or `null` is accepted.
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
|
|
568
|
+
export async function loadKeychainReadWithoutInteraction(): Promise<KeychainReadWithoutInteraction | null> {
|
|
569
|
+
const native = requireOptionalNativeModule<Partial<OxyIdentityNativeModule>>('OxyIdentity');
|
|
570
|
+
const read = native?.readKeychainItemWithoutInteraction;
|
|
571
|
+
if (!native || typeof read !== 'function') return null;
|
|
572
|
+
return async (key, service, accessGroup) => {
|
|
573
|
+
const answer: unknown = await read.call(native, key, service ?? null, accessGroup);
|
|
574
|
+
if (answer === null || answer === undefined) return null;
|
|
575
|
+
if (typeof answer !== 'string') {
|
|
576
|
+
throw new Error('keychain read answered something that is not a string');
|
|
577
|
+
}
|
|
578
|
+
return answer;
|
|
579
|
+
};
|
|
580
|
+
}
|
package/src/platform/crypto.ts
CHANGED
|
@@ -82,6 +82,7 @@ import type {
|
|
|
82
82
|
ExpoSecureStoreLike,
|
|
83
83
|
KeychainApplicationAccessGroup,
|
|
84
84
|
KeychainDefaultAccessGroup,
|
|
85
|
+
KeychainReadWithoutInteraction,
|
|
85
86
|
} from './expoTypes';
|
|
86
87
|
|
|
87
88
|
// Re-export the interfaces so consumers can import them from the same
|
|
@@ -95,6 +96,7 @@ export type {
|
|
|
95
96
|
ExpoSecureStoreLike,
|
|
96
97
|
KeychainApplicationAccessGroup,
|
|
97
98
|
KeychainDefaultAccessGroup,
|
|
99
|
+
KeychainReadWithoutInteraction,
|
|
98
100
|
};
|
|
99
101
|
|
|
100
102
|
// ---------------------------------------------------------------------------
|
|
@@ -194,3 +196,7 @@ export function loadKeychainApplicationAccessGroup(
|
|
|
194
196
|
): Promise<KeychainApplicationAccessGroup> {
|
|
195
197
|
return Promise.resolve({ state: 'unsupported' });
|
|
196
198
|
}
|
|
199
|
+
|
|
200
|
+
export function loadKeychainReadWithoutInteraction(): Promise<KeychainReadWithoutInteraction | null> {
|
|
201
|
+
return Promise.resolve(null);
|
|
202
|
+
}
|
|
@@ -231,3 +231,22 @@ export type KeychainApplicationAccessGroup =
|
|
|
231
231
|
| { state: 'resolved'; accessGroup: string }
|
|
232
232
|
| { state: 'unsupported' }
|
|
233
233
|
| { state: 'unavailable'; cause: unknown };
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* iOS: read one `expo-secure-store` item WITHOUT ever asking the user
|
|
237
|
+
* (round-5 review R5-9). `expo-secure-store`'s own read tries the item's
|
|
238
|
+
* `<service>:no-auth` copy, then its `<service>:auth` copy (an item stored
|
|
239
|
+
* with `requireAuthentication`, which shows a Face ID / passcode prompt),
|
|
240
|
+
* then the legacy `<service>` copy. Any app in a shared access group can plant
|
|
241
|
+
* an `:auth` copy, so a read of that group could stall Commons behind prompts.
|
|
242
|
+
* This reader tries `<service>:no-auth` then `<service>`, both with an
|
|
243
|
+
* `LAContext` whose `interactionNotAllowed` is set, and never the `:auth`
|
|
244
|
+
* copy: an item that needs the user answers `null`. `service` defaults to
|
|
245
|
+
* expo-secure-store's `app`. A keychain error other than "not found" or
|
|
246
|
+
* "interaction not allowed" throws.
|
|
247
|
+
*/
|
|
248
|
+
export type KeychainReadWithoutInteraction = (
|
|
249
|
+
key: string,
|
|
250
|
+
service: string | undefined,
|
|
251
|
+
accessGroup: string,
|
|
252
|
+
) => Promise<string | null>;
|