@oxy.so/protocol 1.9.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 +3 -1
- package/dist/cjs/platform/crypto.js +8 -0
- package/dist/cjs/platform/crypto.native.js +70 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/platform/crypto.js +6 -0
- package/dist/esm/platform/crypto.native.js +68 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/platform/crypto.d.ts +4 -2
- package/dist/types/platform/crypto.native.d.ts +4 -2
- package/dist/types/platform/expoTypes.d.ts +42 -0
- package/package.json +2 -2
- package/src/__tests__/keychainDefaultAccessGroup.test.ts +101 -0
- package/src/index.ts +4 -0
- package/src/platform/crypto.native.ts +94 -0
- package/src/platform/crypto.ts +14 -0
- package/src/platform/expoTypes.ts +43 -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.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
|
// ---------------------------------------------------------------------------
|
|
@@ -78,3 +78,5 @@ Object.defineProperty(exports, "loadAsyncStorage", { enumerable: true, get: func
|
|
|
78
78
|
Object.defineProperty(exports, "getRandomBytesRN", { enumerable: true, get: function () { return crypto_1.getRandomBytesRN; } });
|
|
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
|
+
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; } });
|
|
@@ -113,6 +113,8 @@ exports.loadSecureStore = loadSecureStore;
|
|
|
113
113
|
exports.loadAsyncStorage = loadAsyncStorage;
|
|
114
114
|
exports.loadCommonsIdentityBridge = loadCommonsIdentityBridge;
|
|
115
115
|
exports.loadKeychainDefaultAccessGroup = loadKeychainDefaultAccessGroup;
|
|
116
|
+
exports.loadKeychainApplicationAccessGroup = loadKeychainApplicationAccessGroup;
|
|
117
|
+
exports.loadKeychainReadWithoutInteraction = loadKeychainReadWithoutInteraction;
|
|
116
118
|
const platform_1 = require("./platform");
|
|
117
119
|
// ---------------------------------------------------------------------------
|
|
118
120
|
// Node `crypto` — Node built-in
|
|
@@ -184,3 +186,9 @@ function loadCommonsIdentityBridge() {
|
|
|
184
186
|
function loadKeychainDefaultAccessGroup() {
|
|
185
187
|
return Promise.resolve({ state: 'unsupported' });
|
|
186
188
|
}
|
|
189
|
+
function loadKeychainApplicationAccessGroup(_prefix) {
|
|
190
|
+
return Promise.resolve({ state: 'unsupported' });
|
|
191
|
+
}
|
|
192
|
+
function loadKeychainReadWithoutInteraction() {
|
|
193
|
+
return Promise.resolve(null);
|
|
194
|
+
}
|
|
@@ -60,6 +60,8 @@ exports.loadSecureStore = loadSecureStore;
|
|
|
60
60
|
exports.loadAsyncStorage = loadAsyncStorage;
|
|
61
61
|
exports.loadCommonsIdentityBridge = loadCommonsIdentityBridge;
|
|
62
62
|
exports.loadKeychainDefaultAccessGroup = loadKeychainDefaultAccessGroup;
|
|
63
|
+
exports.loadKeychainApplicationAccessGroup = loadKeychainApplicationAccessGroup;
|
|
64
|
+
exports.loadKeychainReadWithoutInteraction = loadKeychainReadWithoutInteraction;
|
|
63
65
|
const expo_modules_core_1 = require("expo-modules-core");
|
|
64
66
|
const optionalPeer_1 = require("./optionalPeer");
|
|
65
67
|
const random_native_1 = require("./random.native");
|
|
@@ -335,3 +337,71 @@ async function loadKeychainDefaultAccessGroup() {
|
|
|
335
337
|
keychainDefaultAccessGroup = { state: 'resolved', accessGroup: answer };
|
|
336
338
|
return keychainDefaultAccessGroup;
|
|
337
339
|
}
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// iOS keychain application access group (ADR 0062 D6): `<prefix><bundle id>`,
|
|
342
|
+
// the group every app is a member of and no other app can name — where an app
|
|
343
|
+
// keeps the items that are its alone. Same caching as the default group: a
|
|
344
|
+
// resolved or unsupported answer is constant for the binary; a thrown probe is
|
|
345
|
+
// asked again.
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
/** A team prefix as the default group carries it: no dots, then one dot. */
|
|
348
|
+
const KEYCHAIN_PREFIX = /^[A-Za-z0-9]+\.$/;
|
|
349
|
+
let keychainApplicationAccessGroup = null;
|
|
350
|
+
async function loadKeychainApplicationAccessGroup(prefix) {
|
|
351
|
+
if (keychainApplicationAccessGroup) {
|
|
352
|
+
return keychainApplicationAccessGroup;
|
|
353
|
+
}
|
|
354
|
+
if (!KEYCHAIN_PREFIX.test(prefix)) {
|
|
355
|
+
return {
|
|
356
|
+
state: 'unavailable',
|
|
357
|
+
cause: new Error('keychain application group probe needs a team prefix'),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const native = (0, expo_modules_core_1.requireOptionalNativeModule)('OxyIdentity');
|
|
361
|
+
const probe = native?.keychainApplicationAccessGroup;
|
|
362
|
+
if (!native || typeof probe !== 'function') {
|
|
363
|
+
keychainApplicationAccessGroup = { state: 'unsupported' };
|
|
364
|
+
return keychainApplicationAccessGroup;
|
|
365
|
+
}
|
|
366
|
+
let answer;
|
|
367
|
+
try {
|
|
368
|
+
answer = await probe.call(native, prefix);
|
|
369
|
+
}
|
|
370
|
+
catch (cause) {
|
|
371
|
+
return { state: 'unavailable', cause };
|
|
372
|
+
}
|
|
373
|
+
if (answer === null || answer === undefined) {
|
|
374
|
+
// Not entitled (or not iOS): this binary keeps the placement it was built for.
|
|
375
|
+
keychainApplicationAccessGroup = { state: 'unsupported' };
|
|
376
|
+
return keychainApplicationAccessGroup;
|
|
377
|
+
}
|
|
378
|
+
if (typeof answer !== 'string' || !answer.startsWith(prefix) || !ACCESS_GROUP.test(answer)) {
|
|
379
|
+
// A native bug, not a locked keychain: asking again would answer the same,
|
|
380
|
+
// and `unavailable` would leave the app without storage. Keep the
|
|
381
|
+
// placement this build had before the probe existed.
|
|
382
|
+
return { state: 'unsupported' };
|
|
383
|
+
}
|
|
384
|
+
keychainApplicationAccessGroup = { state: 'resolved', accessGroup: answer };
|
|
385
|
+
return keychainApplicationAccessGroup;
|
|
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, } from './platform/crypto.js';
|
|
40
|
+
export { loadNodeCrypto, loadExpoCrypto, loadSecureStore, loadAsyncStorage, getRandomBytesRN, loadCommonsIdentityBridge, loadKeychainDefaultAccessGroup, loadKeychainApplicationAccessGroup, loadKeychainReadWithoutInteraction, } from './platform/crypto.js';
|
|
@@ -141,3 +141,9 @@ export function loadCommonsIdentityBridge() {
|
|
|
141
141
|
export function loadKeychainDefaultAccessGroup() {
|
|
142
142
|
return Promise.resolve({ state: 'unsupported' });
|
|
143
143
|
}
|
|
144
|
+
export function loadKeychainApplicationAccessGroup(_prefix) {
|
|
145
|
+
return Promise.resolve({ state: 'unsupported' });
|
|
146
|
+
}
|
|
147
|
+
export function loadKeychainReadWithoutInteraction() {
|
|
148
|
+
return Promise.resolve(null);
|
|
149
|
+
}
|
|
@@ -325,3 +325,71 @@ export async function loadKeychainDefaultAccessGroup() {
|
|
|
325
325
|
keychainDefaultAccessGroup = { state: 'resolved', accessGroup: answer };
|
|
326
326
|
return keychainDefaultAccessGroup;
|
|
327
327
|
}
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// iOS keychain application access group (ADR 0062 D6): `<prefix><bundle id>`,
|
|
330
|
+
// the group every app is a member of and no other app can name — where an app
|
|
331
|
+
// keeps the items that are its alone. Same caching as the default group: a
|
|
332
|
+
// resolved or unsupported answer is constant for the binary; a thrown probe is
|
|
333
|
+
// asked again.
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
/** A team prefix as the default group carries it: no dots, then one dot. */
|
|
336
|
+
const KEYCHAIN_PREFIX = /^[A-Za-z0-9]+\.$/;
|
|
337
|
+
let keychainApplicationAccessGroup = null;
|
|
338
|
+
export async function loadKeychainApplicationAccessGroup(prefix) {
|
|
339
|
+
if (keychainApplicationAccessGroup) {
|
|
340
|
+
return keychainApplicationAccessGroup;
|
|
341
|
+
}
|
|
342
|
+
if (!KEYCHAIN_PREFIX.test(prefix)) {
|
|
343
|
+
return {
|
|
344
|
+
state: 'unavailable',
|
|
345
|
+
cause: new Error('keychain application group probe needs a team prefix'),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const native = requireOptionalNativeModule('OxyIdentity');
|
|
349
|
+
const probe = native?.keychainApplicationAccessGroup;
|
|
350
|
+
if (!native || typeof probe !== 'function') {
|
|
351
|
+
keychainApplicationAccessGroup = { state: 'unsupported' };
|
|
352
|
+
return keychainApplicationAccessGroup;
|
|
353
|
+
}
|
|
354
|
+
let answer;
|
|
355
|
+
try {
|
|
356
|
+
answer = await probe.call(native, prefix);
|
|
357
|
+
}
|
|
358
|
+
catch (cause) {
|
|
359
|
+
return { state: 'unavailable', cause };
|
|
360
|
+
}
|
|
361
|
+
if (answer === null || answer === undefined) {
|
|
362
|
+
// Not entitled (or not iOS): this binary keeps the placement it was built for.
|
|
363
|
+
keychainApplicationAccessGroup = { state: 'unsupported' };
|
|
364
|
+
return keychainApplicationAccessGroup;
|
|
365
|
+
}
|
|
366
|
+
if (typeof answer !== 'string' || !answer.startsWith(prefix) || !ACCESS_GROUP.test(answer)) {
|
|
367
|
+
// A native bug, not a locked keychain: asking again would answer the same,
|
|
368
|
+
// and `unavailable` would leave the app without storage. Keep the
|
|
369
|
+
// placement this build had before the probe existed.
|
|
370
|
+
return { state: 'unsupported' };
|
|
371
|
+
}
|
|
372
|
+
keychainApplicationAccessGroup = { state: 'resolved', accessGroup: answer };
|
|
373
|
+
return keychainApplicationAccessGroup;
|
|
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, } from './platform/crypto';
|
|
33
|
-
export type { CommonsIdentityBridge, CommonsIdentityDescription, CommonsIdentityProof, CommonsSocialReceiveSignature, ExpoCryptoLike, ExpoSecureStoreLike, 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, KeychainDefaultAccessGroup } from './expoTypes';
|
|
75
|
-
export type { CommonsIdentityBridge, CommonsIdentityDescription, CommonsIdentityProof, CommonsSocialReceiveSignature, ExpoCryptoLike, ExpoSecureStoreLike, 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>;
|
|
@@ -86,3 +86,5 @@ export declare function loadAsyncStorage(): Promise<{
|
|
|
86
86
|
export { getRandomBytesRN } from './random';
|
|
87
87
|
export declare function loadCommonsIdentityBridge(): Promise<CommonsIdentityBridge | null>;
|
|
88
88
|
export declare function loadKeychainDefaultAccessGroup(): Promise<KeychainDefaultAccessGroup>;
|
|
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, KeychainDefaultAccessGroup } from './expoTypes';
|
|
55
|
-
export type { CommonsIdentityBridge, CommonsIdentityDescription, CommonsIdentityProof, CommonsSocialReceiveSignature, ExpoCryptoLike, ExpoSecureStoreLike, 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>;
|
|
@@ -68,3 +68,5 @@ export declare function loadAsyncStorage(): Promise<{
|
|
|
68
68
|
export { getRandomBytesRN } from './random.native';
|
|
69
69
|
export declare function loadCommonsIdentityBridge(): Promise<CommonsIdentityBridge | null>;
|
|
70
70
|
export declare function loadKeychainDefaultAccessGroup(): Promise<KeychainDefaultAccessGroup>;
|
|
71
|
+
export declare function loadKeychainApplicationAccessGroup(prefix: string): Promise<KeychainApplicationAccessGroup>;
|
|
72
|
+
export declare function loadKeychainReadWithoutInteraction(): Promise<KeychainReadWithoutInteraction | null>;
|
|
@@ -192,3 +192,45 @@ export type KeychainDefaultAccessGroup = {
|
|
|
192
192
|
state: 'unavailable';
|
|
193
193
|
cause: unknown;
|
|
194
194
|
};
|
|
195
|
+
/**
|
|
196
|
+
* What the iOS keychain says about this app's OWN access group: its
|
|
197
|
+
* application identifier, `<prefix><bundle id>` (`ABCDE12345.so.oxy.mention`).
|
|
198
|
+
* Apple makes every app a member of that group whatever its
|
|
199
|
+
* `keychain-access-groups` lists, and no other app can name it, so it is where
|
|
200
|
+
* an app keeps the items that are its alone (its session credential): the
|
|
201
|
+
* keychain's counterpart of Android's per-app storage (ADR 0062 D6).
|
|
202
|
+
*
|
|
203
|
+
* The native module writes a probe item into that group EXPLICITLY and answers
|
|
204
|
+
* the group only when the keychain took it.
|
|
205
|
+
*
|
|
206
|
+
* - `resolved`: the group string.
|
|
207
|
+
* - `unsupported`: not iOS, a native build whose `OxyIdentity` module predates
|
|
208
|
+
* this probe, a keychain that refused the group as not entitled, or an answer
|
|
209
|
+
* that is not this team's group. Constant for the binary: callers keep the
|
|
210
|
+
* placement that build was made for.
|
|
211
|
+
* - `unavailable`: the probe threw (before first unlock). Transient; never
|
|
212
|
+
* cached.
|
|
213
|
+
*/
|
|
214
|
+
export type KeychainApplicationAccessGroup = {
|
|
215
|
+
state: 'resolved';
|
|
216
|
+
accessGroup: string;
|
|
217
|
+
} | {
|
|
218
|
+
state: 'unsupported';
|
|
219
|
+
} | {
|
|
220
|
+
state: 'unavailable';
|
|
221
|
+
cause: unknown;
|
|
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",
|
|
@@ -63,3 +63,104 @@ describe('loadKeychainDefaultAccessGroup (native)', () => {
|
|
|
63
63
|
await expect(mod.loadKeychainDefaultAccessGroup()).resolves.toEqual({ state: 'unsupported' });
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
describe('loadKeychainApplicationAccessGroup (native, ADR 0062 D6)', () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
jest.resetModules();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const load = async (prefix = 'ABCDE12345.') =>
|
|
73
|
+
(await import('../platform/crypto.native')).loadKeychainApplicationAccessGroup(prefix);
|
|
74
|
+
|
|
75
|
+
test('a native module without the probe, or a refused group, is unsupported', async () => {
|
|
76
|
+
mockNative({ keychainDefaultAccessGroup: async () => 'ABCDE12345.group.so.oxy.shared' });
|
|
77
|
+
await expect(load()).resolves.toEqual({ state: 'unsupported' });
|
|
78
|
+
jest.resetModules();
|
|
79
|
+
mockNative({ keychainApplicationAccessGroup: async () => null });
|
|
80
|
+
await expect(load()).resolves.toEqual({ state: 'unsupported' });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('passes this team’s group through with the prefix, and remembers it', async () => {
|
|
84
|
+
const keychainApplicationAccessGroup = jest.fn(async () => 'ABCDE12345.so.oxy.mention');
|
|
85
|
+
mockNative({ keychainApplicationAccessGroup });
|
|
86
|
+
const mod = await import('../platform/crypto.native');
|
|
87
|
+
await expect(mod.loadKeychainApplicationAccessGroup('ABCDE12345.')).resolves.toEqual({
|
|
88
|
+
state: 'resolved',
|
|
89
|
+
accessGroup: 'ABCDE12345.so.oxy.mention',
|
|
90
|
+
});
|
|
91
|
+
await mod.loadKeychainApplicationAccessGroup('ABCDE12345.');
|
|
92
|
+
expect(keychainApplicationAccessGroup).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(keychainApplicationAccessGroup).toHaveBeenCalledWith('ABCDE12345.');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('a throw is unavailable and asked again; a bad prefix is never sent', async () => {
|
|
97
|
+
let locked = true;
|
|
98
|
+
const keychainApplicationAccessGroup = jest.fn(async () => {
|
|
99
|
+
if (locked) throw new Error('errSecInteractionNotAllowed');
|
|
100
|
+
return 'ABCDE12345.so.oxy.mention';
|
|
101
|
+
});
|
|
102
|
+
mockNative({ keychainApplicationAccessGroup });
|
|
103
|
+
const mod = await import('../platform/crypto.native');
|
|
104
|
+
expect((await mod.loadKeychainApplicationAccessGroup('ABCDE12345.')).state).toBe('unavailable');
|
|
105
|
+
locked = false;
|
|
106
|
+
expect((await mod.loadKeychainApplicationAccessGroup('ABCDE12345.')).state).toBe('resolved');
|
|
107
|
+
jest.resetModules();
|
|
108
|
+
mockNative({ keychainApplicationAccessGroup });
|
|
109
|
+
keychainApplicationAccessGroup.mockClear();
|
|
110
|
+
for (const prefix of ['', 'ABCDE12345', 'A.B.']) {
|
|
111
|
+
expect((await load(prefix)).state).toBe('unavailable');
|
|
112
|
+
}
|
|
113
|
+
expect(keychainApplicationAccessGroup).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('another team’s group or junk is unsupported: the earlier placement, never no storage', async () => {
|
|
117
|
+
for (const junk of [42, 'XYZ.so.oxy.mention', 'ABCDE12345.', { group: 'x' }]) {
|
|
118
|
+
jest.resetModules();
|
|
119
|
+
mockNative({ keychainApplicationAccessGroup: async () => junk });
|
|
120
|
+
expect((await load()).state).toBe('unsupported');
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('the web / Node variant is unsupported', async () => {
|
|
125
|
+
const mod = await import('../platform/crypto');
|
|
126
|
+
await expect(mod.loadKeychainApplicationAccessGroup('ABCDE12345.')).resolves.toEqual({
|
|
127
|
+
state: 'unsupported',
|
|
128
|
+
});
|
|
129
|
+
});
|
|
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
|
@@ -100,6 +100,8 @@ export {
|
|
|
100
100
|
getRandomBytesRN,
|
|
101
101
|
loadCommonsIdentityBridge,
|
|
102
102
|
loadKeychainDefaultAccessGroup,
|
|
103
|
+
loadKeychainApplicationAccessGroup,
|
|
104
|
+
loadKeychainReadWithoutInteraction,
|
|
103
105
|
} from './platform/crypto';
|
|
104
106
|
|
|
105
107
|
// ---------------------------------------------------------------------------
|
|
@@ -116,5 +118,7 @@ export type {
|
|
|
116
118
|
CommonsSocialReceiveSignature,
|
|
117
119
|
ExpoCryptoLike,
|
|
118
120
|
ExpoSecureStoreLike,
|
|
121
|
+
KeychainApplicationAccessGroup,
|
|
119
122
|
KeychainDefaultAccessGroup,
|
|
123
|
+
KeychainReadWithoutInteraction,
|
|
120
124
|
} from './platform/expoTypes';
|
|
@@ -60,7 +60,9 @@ import type {
|
|
|
60
60
|
CommonsSocialReceiveSignature,
|
|
61
61
|
ExpoCryptoLike,
|
|
62
62
|
ExpoSecureStoreLike,
|
|
63
|
+
KeychainApplicationAccessGroup,
|
|
63
64
|
KeychainDefaultAccessGroup,
|
|
65
|
+
KeychainReadWithoutInteraction,
|
|
64
66
|
} from './expoTypes';
|
|
65
67
|
import { missingOptionalPeerError } from './optionalPeer';
|
|
66
68
|
import { requireExpoCrypto } from './random.native';
|
|
@@ -74,7 +76,9 @@ export type {
|
|
|
74
76
|
CommonsSocialReceiveSignature,
|
|
75
77
|
ExpoCryptoLike,
|
|
76
78
|
ExpoSecureStoreLike,
|
|
79
|
+
KeychainApplicationAccessGroup,
|
|
77
80
|
KeychainDefaultAccessGroup,
|
|
81
|
+
KeychainReadWithoutInteraction,
|
|
78
82
|
};
|
|
79
83
|
|
|
80
84
|
// ---------------------------------------------------------------------------
|
|
@@ -225,6 +229,23 @@ interface OxyIdentityNativeModule {
|
|
|
225
229
|
* Older native modules and Android lack it.
|
|
226
230
|
*/
|
|
227
231
|
keychainDefaultAccessGroup?(): Promise<unknown>;
|
|
232
|
+
/**
|
|
233
|
+
* iOS only, added after the default-group probe (ADR 0062 D6): writes a
|
|
234
|
+
* probe item into `<prefix><bundle id>` explicitly and answers that group,
|
|
235
|
+
* or `null` when the keychain refused it as not entitled. Older native
|
|
236
|
+
* modules and Android lack it.
|
|
237
|
+
*/
|
|
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>;
|
|
228
249
|
/**
|
|
229
250
|
* Retired keys (Commons protocol 4), added after the organization methods and
|
|
230
251
|
* just as absent on an older native module. `accountId` is `null` for the
|
|
@@ -484,3 +505,76 @@ export async function loadKeychainDefaultAccessGroup(): Promise<KeychainDefaultA
|
|
|
484
505
|
keychainDefaultAccessGroup = { state: 'resolved', accessGroup: answer };
|
|
485
506
|
return keychainDefaultAccessGroup;
|
|
486
507
|
}
|
|
508
|
+
|
|
509
|
+
// ---------------------------------------------------------------------------
|
|
510
|
+
// iOS keychain application access group (ADR 0062 D6): `<prefix><bundle id>`,
|
|
511
|
+
// the group every app is a member of and no other app can name — where an app
|
|
512
|
+
// keeps the items that are its alone. Same caching as the default group: a
|
|
513
|
+
// resolved or unsupported answer is constant for the binary; a thrown probe is
|
|
514
|
+
// asked again.
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
|
|
517
|
+
/** A team prefix as the default group carries it: no dots, then one dot. */
|
|
518
|
+
const KEYCHAIN_PREFIX = /^[A-Za-z0-9]+\.$/;
|
|
519
|
+
|
|
520
|
+
let keychainApplicationAccessGroup: KeychainApplicationAccessGroup | null = null;
|
|
521
|
+
|
|
522
|
+
export async function loadKeychainApplicationAccessGroup(
|
|
523
|
+
prefix: string,
|
|
524
|
+
): Promise<KeychainApplicationAccessGroup> {
|
|
525
|
+
if (keychainApplicationAccessGroup) {
|
|
526
|
+
return keychainApplicationAccessGroup;
|
|
527
|
+
}
|
|
528
|
+
if (!KEYCHAIN_PREFIX.test(prefix)) {
|
|
529
|
+
return {
|
|
530
|
+
state: 'unavailable',
|
|
531
|
+
cause: new Error('keychain application group probe needs a team prefix'),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const native = requireOptionalNativeModule<Partial<OxyIdentityNativeModule>>('OxyIdentity');
|
|
535
|
+
const probe = native?.keychainApplicationAccessGroup;
|
|
536
|
+
if (!native || typeof probe !== 'function') {
|
|
537
|
+
keychainApplicationAccessGroup = { state: 'unsupported' };
|
|
538
|
+
return keychainApplicationAccessGroup;
|
|
539
|
+
}
|
|
540
|
+
let answer: unknown;
|
|
541
|
+
try {
|
|
542
|
+
answer = await probe.call(native, prefix);
|
|
543
|
+
} catch (cause) {
|
|
544
|
+
return { state: 'unavailable', cause };
|
|
545
|
+
}
|
|
546
|
+
if (answer === null || answer === undefined) {
|
|
547
|
+
// Not entitled (or not iOS): this binary keeps the placement it was built for.
|
|
548
|
+
keychainApplicationAccessGroup = { state: 'unsupported' };
|
|
549
|
+
return keychainApplicationAccessGroup;
|
|
550
|
+
}
|
|
551
|
+
if (typeof answer !== 'string' || !answer.startsWith(prefix) || !ACCESS_GROUP.test(answer)) {
|
|
552
|
+
// A native bug, not a locked keychain: asking again would answer the same,
|
|
553
|
+
// and `unavailable` would leave the app without storage. Keep the
|
|
554
|
+
// placement this build had before the probe existed.
|
|
555
|
+
return { state: 'unsupported' };
|
|
556
|
+
}
|
|
557
|
+
keychainApplicationAccessGroup = { state: 'resolved', accessGroup: answer };
|
|
558
|
+
return keychainApplicationAccessGroup;
|
|
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
|
@@ -80,7 +80,9 @@ import type {
|
|
|
80
80
|
CommonsSocialReceiveSignature,
|
|
81
81
|
ExpoCryptoLike,
|
|
82
82
|
ExpoSecureStoreLike,
|
|
83
|
+
KeychainApplicationAccessGroup,
|
|
83
84
|
KeychainDefaultAccessGroup,
|
|
85
|
+
KeychainReadWithoutInteraction,
|
|
84
86
|
} from './expoTypes';
|
|
85
87
|
|
|
86
88
|
// Re-export the interfaces so consumers can import them from the same
|
|
@@ -92,7 +94,9 @@ export type {
|
|
|
92
94
|
CommonsSocialReceiveSignature,
|
|
93
95
|
ExpoCryptoLike,
|
|
94
96
|
ExpoSecureStoreLike,
|
|
97
|
+
KeychainApplicationAccessGroup,
|
|
95
98
|
KeychainDefaultAccessGroup,
|
|
99
|
+
KeychainReadWithoutInteraction,
|
|
96
100
|
};
|
|
97
101
|
|
|
98
102
|
// ---------------------------------------------------------------------------
|
|
@@ -186,3 +190,13 @@ export function loadCommonsIdentityBridge(): Promise<CommonsIdentityBridge | nul
|
|
|
186
190
|
export function loadKeychainDefaultAccessGroup(): Promise<KeychainDefaultAccessGroup> {
|
|
187
191
|
return Promise.resolve({ state: 'unsupported' });
|
|
188
192
|
}
|
|
193
|
+
|
|
194
|
+
export function loadKeychainApplicationAccessGroup(
|
|
195
|
+
_prefix: string,
|
|
196
|
+
): Promise<KeychainApplicationAccessGroup> {
|
|
197
|
+
return Promise.resolve({ state: 'unsupported' });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function loadKeychainReadWithoutInteraction(): Promise<KeychainReadWithoutInteraction | null> {
|
|
201
|
+
return Promise.resolve(null);
|
|
202
|
+
}
|
|
@@ -207,3 +207,46 @@ export type KeychainDefaultAccessGroup =
|
|
|
207
207
|
| { state: 'resolved'; accessGroup: string }
|
|
208
208
|
| { state: 'unsupported' }
|
|
209
209
|
| { state: 'unavailable'; cause: unknown };
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* What the iOS keychain says about this app's OWN access group: its
|
|
213
|
+
* application identifier, `<prefix><bundle id>` (`ABCDE12345.so.oxy.mention`).
|
|
214
|
+
* Apple makes every app a member of that group whatever its
|
|
215
|
+
* `keychain-access-groups` lists, and no other app can name it, so it is where
|
|
216
|
+
* an app keeps the items that are its alone (its session credential): the
|
|
217
|
+
* keychain's counterpart of Android's per-app storage (ADR 0062 D6).
|
|
218
|
+
*
|
|
219
|
+
* The native module writes a probe item into that group EXPLICITLY and answers
|
|
220
|
+
* the group only when the keychain took it.
|
|
221
|
+
*
|
|
222
|
+
* - `resolved`: the group string.
|
|
223
|
+
* - `unsupported`: not iOS, a native build whose `OxyIdentity` module predates
|
|
224
|
+
* this probe, a keychain that refused the group as not entitled, or an answer
|
|
225
|
+
* that is not this team's group. Constant for the binary: callers keep the
|
|
226
|
+
* placement that build was made for.
|
|
227
|
+
* - `unavailable`: the probe threw (before first unlock). Transient; never
|
|
228
|
+
* cached.
|
|
229
|
+
*/
|
|
230
|
+
export type KeychainApplicationAccessGroup =
|
|
231
|
+
| { state: 'resolved'; accessGroup: string }
|
|
232
|
+
| { state: 'unsupported' }
|
|
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>;
|