@symbiote-native/crypto 3.0.1 → 3.0.3

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/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # @symbiote-native/crypto
2
2
 
3
3
  A wrapper package for [SymbioteNative](../../README.md) that makes
4
- [`expo-crypto`](https://github.com/expo/expo/tree/main/packages/expo-crypto) — cryptographically
5
- secure random bytes, `randomUUID`, and string/buffer digest hashing — usable from **every**
4
+ [`expo-crypto`](https://github.com/expo/expo/tree/main/packages/expo-crypto) - cryptographically
5
+ secure random bytes, `randomUUID`, and string/buffer digest hashing - usable from **every**
6
6
  adapter, React, Vue, Svelte, Solid, and Angular. Like `@symbiote-native/local-auth`, every
7
7
  function here is a plain sync/async call with no per-instance state or event stream, so there is
8
- no hook/composable/service to wrap — every adapter's entry point is a plain re-export of the same
9
- `core`. AES encryption (`expo-crypto`'s `aes/` subfolder) is out of scope for this pass.
8
+ no hook/composable/service to wrap - every adapter's entry point is a plain re-export of the same
9
+ `core`. AES-GCM encryption (`expo-crypto`'s `aes/` subfolder) is ported too.
10
10
 
11
11
  ## Install
12
12
 
@@ -22,39 +22,39 @@ npx @symbiote-native/cli new my-app --crypto
22
22
  npx @symbiote-native/cli add --crypto
23
23
  ```
24
24
 
25
- Either way: installs `@symbiote-native/crypto` and wires the native autolinking automatically — see
25
+ Either way: installs `@symbiote-native/crypto` and wires the native autolinking automatically - see
26
26
  [`@symbiote-native/cli`](../cli).
27
27
 
28
28
  <details>
29
- <summary>Manual install (no CLI — installing and wiring native autolinking by hand)</summary>
29
+ <summary>Manual install (no CLI - installing and wiring native autolinking by hand)</summary>
30
30
 
31
31
  ```bash
32
32
  npm install @symbiote-native/crypto
33
33
  ```
34
34
 
35
35
  `expo-crypto` and `expo-modules-core` come along as regular dependencies, pinned to exact
36
- versions — never install either yourself, and never add the `expo` meta-package to your project
36
+ versions - never install either yourself, and never add the `expo` meta-package to your project
37
37
  (it bundles its own Metro/Babel pipeline, which conflicts with this project's own).
38
38
 
39
39
  ## Required one-time step: native autolinking wiring
40
40
 
41
41
  Unlike a plain RN native module, `expo-crypto`'s native code is discovered by
42
- `expo-modules-autolinking`, not RN's own `react-native.config.cjs` mechanism — this needs wiring
42
+ `expo-modules-autolinking`, not RN's own `react-native.config.cjs` mechanism - this needs wiring
43
43
  into the native host app **once**, covering this package and every other `expo-modules-core`
44
44
  package with zero further changes:
45
45
 
46
46
  | Platform | Touches |
47
47
  | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
48
- | iOS | `ios/Podfile` — add `use_expo_modules!` |
49
- | iOS | `AppDelegate.swift` — Expo's runtime-bootstrap hook |
50
- | Android | `settings.gradle` / `app/build.gradle` — resolve and include the Expo Gradle projects |
51
- | Android | `MainApplication.kt` — Expo's bootstrap hook, plus a hand-written native-module name map (there's no `expo` meta-package here to auto-generate one) |
48
+ | iOS | `ios/Podfile` - add `use_expo_modules!` |
49
+ | iOS | `AppDelegate.swift` - Expo's runtime-bootstrap hook |
50
+ | Android | `settings.gradle` / `app/build.gradle` - resolve and include the Expo Gradle projects |
51
+ | Android | `MainApplication.kt` - Expo's bootstrap hook, plus a hand-written native-module name map (there's no `expo` meta-package here to auto-generate one) |
52
52
 
53
53
  Full mechanics live in the `symbiote-expo-native-module` project skill. Reference
54
54
  implementation: `examples/expo-react/ios/Podfile` and
55
55
  `examples/expo-react/android/app/src/main/java/com/canaryexpo/MainApplication.kt`.
56
56
 
57
- No platform permission strings are needed for this package — random-byte generation and
57
+ No platform permission strings are needed for this package - random-byte generation and
58
58
  digest hashing touch no protected device capability.
59
59
 
60
60
  </details>
@@ -67,11 +67,11 @@ src/core/ getRandomBytes / getRandomBytesAsync / getRandomValues / randomUUI
67
67
  option/result types, and the CryptoError validation-error class.
68
68
  native-module.ts resolves the native module via expo-modules-core's
69
69
  requireNativeModule.
70
- src/angular/ @symbiote-native/crypto/angular — export * from '../core'
70
+ src/angular/ @symbiote-native/crypto/angular - export * from '../core'
71
71
  ```
72
72
 
73
- `./react`, `./vue`, `./svelte`, and `./solid` are `exports`-map aliases straight onto `src/core/`
74
- — no physical per-framework file, since there's nothing to subscribe to or clean up. `./angular`
73
+ `./react`, `./vue`, `./svelte`, and `./solid` are `exports`-map aliases straight onto `src/core/`:
74
+ no physical per-framework file, since there's nothing to subscribe to or clean up. `./angular`
75
75
  stays a physical file/subpath since Angular ships through a separate `ngc`/AOT build (`build-ngc/`).
76
76
 
77
77
  ## Use it
@@ -245,9 +245,9 @@ export class CryptoScreen {
245
245
  }
246
246
  ```
247
247
 
248
- There's no per-instance service to `inject()` in the Angular case — every function is a plain
248
+ There's no per-instance service to `inject()` in the Angular case - every function is a plain
249
249
  free function off the core package, called straight from the component body (or the constructor,
250
- on Angular). All six examples mirror the real canary demo screens —
250
+ on Angular). All six examples mirror the real canary demo screens:
251
251
  `examples/expo-react/screens/CryptoScreen.tsx`, `examples/expo-vue-sfc/screens/CryptoScreen.vue`,
252
252
  `examples/expo-vue-tsx/screens/CryptoScreen.tsx`, `examples/expo-svelte/screens/CryptoScreen.svelte`,
253
253
  `examples/expo-solid/screens/CryptoScreen.tsx`, `examples/expo-angular/src/screens/CryptoScreen.ts`.
@@ -271,13 +271,39 @@ digest(algorithm: CryptoDigestAlgorithm, data: BufferSource): Promise<ArrayBuffe
271
271
 
272
272
  Plus `CryptoDigestAlgorithm` (`SHA1`/`SHA256`/`SHA384`/`SHA512`/`MD2`\*/`MD4`\*/`MD5`, \*iOS
273
273
  only), `CryptoEncoding` (`HEX`/`BASE64`), `ICryptoDigestOptions`, `IDigest`, `ITypedArray` (and its
274
- `IUintBasedTypedArray`/`IIntBasedTypedArray` halves), and the `CryptoError` validation-error class
275
- — ported from upstream's `Crypto.types.ts`/`Crypto.ts`, renamed with this repo's `I`-prefix
274
+ `IUintBasedTypedArray`/`IIntBasedTypedArray` halves), and the `CryptoError` validation-error class,
275
+ ported from upstream's `Crypto.types.ts`/`Crypto.ts`, renamed with this repo's `I`-prefix
276
276
  convention for exported types (`ts-js-best-practices`).
277
277
 
278
+ ### AES-GCM
279
+
280
+ Ported from upstream's `aes/` subfolder onto a second native module, `ExpoCryptoAES`:
281
+
282
+ ```ts
283
+ import {
284
+ AESEncryptionKey,
285
+ AESKeySize,
286
+ AESSealedData,
287
+ aesDecryptAsync,
288
+ aesEncryptAsync,
289
+ } from '@symbiote-native/crypto';
290
+
291
+ const key = await AESEncryptionKey.generate(AESKeySize.AES256);
292
+ const sealed = await aesEncryptAsync(plaintextBase64, key);
293
+ const plain = await aesDecryptAsync(sealed, key, { output: 'base64' });
294
+ ```
295
+
296
+ `AESEncryptionKey` and `AESSealedData` extend the native classes: `generate`/`import`/`bytes`/
297
+ `encoded` on the key, `fromParts`/`fromCombined` plus `iv`/`tag`/`ciphertext`/`combined` on the
298
+ sealed data. A string input is base64. `ArrayBuffer` inputs become a `Uint8Array`, and binary
299
+ `nonce.bytes`/`additionalData` go to native as base64, as upstream does. `tagLength` is ignored on
300
+ Apple (always 16), and `AES192` is unsupported on web. Option and input types carry the
301
+ `I`-prefix: `IAesEncryptOptions`, `IAesDecryptOptions`, `IAesSealedDataConfig`, `IBinaryInput`,
302
+ `IGcmNonceParam`, `IGcmTagByteLength`.
303
+
278
304
  ```ts
279
305
  import { digestStringAsync, randomUUID } from '@symbiote-native/crypto';
280
- // or the framework-scoped entry points — identical surface, re-exported verbatim:
306
+ // or the framework-scoped entry points - identical surface, re-exported verbatim:
281
307
  import { randomUUID } from '@symbiote-native/crypto/react';
282
308
  import { randomUUID } from '@symbiote-native/crypto/vue';
283
309
  import { randomUUID } from '@symbiote-native/crypto/svelte';
@@ -288,21 +314,32 @@ import { randomUUID } from '@symbiote-native/crypto/angular';
288
314
  ## Notes
289
315
 
290
316
  - `getRandomBytes`/`getRandomBytesAsync` validate `byteCount` is a number in `0`-`1024`
291
- (inclusive), throwing a plain `TypeError` otherwise, and floor a fractional count — matching
317
+ (inclusive), throwing a plain `TypeError` otherwise, and floor a fractional count - matching
292
318
  upstream exactly. This port skips upstream's `__DEV__`/remote-debugger `Math.random` fallback:
293
319
  that's a React Native debugging-tool concern, not applicable to this package's native-call
294
320
  path.
295
321
  - `digestStringAsync` validates `algorithm`/`data`/`options.encoding` and throws `CryptoError` (a
296
- `TypeError` subclass, `code: 'ERR_CRYPTO'`) on an invalid value — mirroring upstream's own
322
+ `TypeError` subclass, `code: 'ERR_CRYPTO'`) on an invalid value - mirroring upstream's own
297
323
  `CryptoError`.
298
324
  - `digest` prefers a native `digestAsync` when present; otherwise it allocates a fixed-size
299
325
  output buffer (sized via the `digestLengths` lookup table) and calls the sync native `digest`.
300
326
 
327
+ ## Common questions
328
+
329
+ - **`digestStringAsync` is `undefined`.** The native module did not load: rebuild after install.
330
+ - **`randomUUID` returns a promise.** Reported after a native binary update; restart and rebuild.
331
+ - **Which algorithms?** Prefer SHA-256 / SHA-512; SHA-1 is not recommended for security use.
332
+ - **`undefined` under Jest.** The native module is absent in tests; inject a fake.
333
+
334
+ Sources: [expo/expo#18215](https://github.com/expo/expo/issues/18215),
335
+ [expo/expo#24021](https://github.com/expo/expo/issues/24021),
336
+ [expo/expo#6512](https://github.com/expo/expo/issues/6512).
337
+
301
338
  ## Test it
302
339
 
303
- No Fabric/Descriptor angle at all — every function here is a pure sync/async-function surface,
340
+ No Fabric/Descriptor angle at all - every function here is a pure sync/async-function surface,
304
341
  never a view or per-instance state. Tests inject a fake native-module object in place of the
305
- real `requireNativeModule` resolution (`src/core/crypto.test.ts`, `vitest`) — no
342
+ real `requireNativeModule` resolution (`src/core/crypto.test.ts`, `src/core/aes/aes.test.ts`, `vitest`) - no
306
343
  `installFabric()`, no ViewConfig. Native rendering itself is verified on-device (see the parent
307
344
  [README](../../README.md) for the project's testing model).
308
345
 
@@ -0,0 +1,55 @@
1
+ import { NativeModule } from 'expo-modules-core';
2
+ import type { IAesDecryptOptions, IAesEncryptOptions, IAesSealedDataConfig, IBinaryInput, IGcmTagByteLength, AESKeySize } from './aes-types';
3
+ type IEncoding = 'hex' | 'base64';
4
+ declare class EncryptionKey {
5
+ static generate(size?: AESKeySize): Promise<EncryptionKey>;
6
+ static import(bytes: Uint8Array): Promise<EncryptionKey>;
7
+ static import(hexString: string, encoding: IEncoding): Promise<EncryptionKey>;
8
+ /** Key size in bits */
9
+ size: AESKeySize;
10
+ bytes(): Promise<Uint8Array>;
11
+ encoded(encoding: IEncoding): Promise<string>;
12
+ }
13
+ declare class SealedData {
14
+ static fromCombined(combined: IBinaryInput, config?: IAesSealedDataConfig): SealedData;
15
+ static fromParts(iv: IBinaryInput, ciphertext: IBinaryInput, tag: IBinaryInput): SealedData;
16
+ static fromParts(iv: IBinaryInput, ciphertextWithTag: IBinaryInput, tagLength?: number): SealedData;
17
+ ciphertext(options: {
18
+ includeTag?: boolean;
19
+ encoding: 'base64';
20
+ }): Promise<string>;
21
+ ciphertext(options?: {
22
+ includeTag?: boolean;
23
+ encoding?: 'bytes';
24
+ }): Promise<Uint8Array>;
25
+ ciphertext(options: {
26
+ includeTag?: boolean;
27
+ encoding?: 'base64' | 'bytes';
28
+ }): Promise<string | Uint8Array>;
29
+ iv(encoding?: 'bytes'): Promise<Uint8Array>;
30
+ iv(encoding: 'base64'): Promise<string>;
31
+ iv(encoding?: 'bytes' | 'base64'): Promise<string | Uint8Array>;
32
+ tag(encoding?: 'bytes'): Promise<Uint8Array>;
33
+ tag(encoding: 'base64'): Promise<string>;
34
+ tag(encoding?: 'bytes' | 'base64'): Promise<string | Uint8Array>;
35
+ combined(encoding?: 'bytes'): Promise<Uint8Array>;
36
+ combined(encoding: 'base64'): Promise<string>;
37
+ combined(encoding?: 'bytes' | 'base64'): Promise<string | Uint8Array>;
38
+ /** IV + ciphertext + tag, in bytes */
39
+ readonly combinedSize: number;
40
+ readonly ivSize: number;
41
+ readonly tagSize: IGcmTagByteLength;
42
+ }
43
+ export type INativeAesEncryptOptions = Omit<IAesEncryptOptions, 'nonce'> & {
44
+ nonce?: number | IBinaryInput | undefined;
45
+ };
46
+ declare class NativeAesCryptoModule extends NativeModule {
47
+ EncryptionKey: typeof EncryptionKey;
48
+ SealedData: typeof SealedData;
49
+ generateKey(size?: AESKeySize): Promise<EncryptionKey>;
50
+ importKey(keyInput: string | Uint8Array, encoding?: IEncoding): Promise<EncryptionKey>;
51
+ encryptAsync(plaintext: IBinaryInput, key: EncryptionKey, options?: INativeAesEncryptOptions): Promise<SealedData>;
52
+ decryptAsync(sealedData: SealedData, key: EncryptionKey, options?: IAesDecryptOptions): Promise<string | Uint8Array>;
53
+ }
54
+ export declare const expoCryptoAes: NativeAesCryptoModule;
55
+ export {};
@@ -0,0 +1,2 @@
1
+ import { NativeModule, requireNativeModule } from 'expo-modules-core';
2
+ export const expoCryptoAes = requireNativeModule('ExpoCryptoAES');
@@ -0,0 +1,36 @@
1
+ export type IBinaryInput = string | Uint8Array | ArrayBuffer;
2
+ export declare enum AESKeySize {
3
+ AES128 = 128,
4
+ AES192 = 192,
5
+ AES256 = 256
6
+ }
7
+ export type IGcmTagByteLength = 16 | 15 | 14 | 13 | 12 | 8 | 4;
8
+ export type IAesSealedDataConfig = {
9
+ /** @default 12 */
10
+ ivLength: number;
11
+ /** @default 16 */
12
+ tagLength: IGcmTagByteLength;
13
+ };
14
+ export type IAesDecryptOptions = {
15
+ /** @default 'bytes' */
16
+ output?: 'bytes' | 'base64';
17
+ additionalData?: IBinaryInput;
18
+ };
19
+ export type IBase64DecryptOptions = IAesDecryptOptions & {
20
+ output: 'base64';
21
+ };
22
+ export type IBytesDecryptOptions = IAesDecryptOptions & {
23
+ output?: 'bytes';
24
+ };
25
+ export type IGcmNonceParam = {
26
+ /** Byte length of the nonce to generate. @default 12 */
27
+ length: number;
28
+ } | {
29
+ bytes: IBinaryInput;
30
+ };
31
+ export type IAesEncryptOptions = {
32
+ /** @default { length: 12 } */
33
+ nonce?: IGcmNonceParam;
34
+ tagLength?: IGcmTagByteLength;
35
+ additionalData?: IBinaryInput;
36
+ };
@@ -0,0 +1,7 @@
1
+ export var AESKeySize;
2
+ (function (AESKeySize) {
3
+ AESKeySize[AESKeySize["AES128"] = 128] = "AES128";
4
+ // Unsupported on web
5
+ AESKeySize[AESKeySize["AES192"] = 192] = "AES192";
6
+ AESKeySize[AESKeySize["AES256"] = 256] = "AES256";
7
+ })(AESKeySize || (AESKeySize = {}));
@@ -0,0 +1,16 @@
1
+ import { expoCryptoAes } from './aes-native-module';
2
+ import type { IAesDecryptOptions, IAesEncryptOptions, IAesSealedDataConfig, IBase64DecryptOptions, IBinaryInput, IBytesDecryptOptions } from './aes-types';
3
+ export declare class AESEncryptionKey extends expoCryptoAes.EncryptionKey {
4
+ }
5
+ export declare class AESSealedData extends expoCryptoAes.SealedData {
6
+ /** `tag` is a separate tag, or the tag length when the ciphertext already carries it */
7
+ static fromParts(iv: IBinaryInput, ciphertext: IBinaryInput, tag: IBinaryInput): AESSealedData;
8
+ static fromParts(iv: IBinaryInput, ciphertextWithTag: IBinaryInput, tagLength?: number): AESSealedData;
9
+ /** `combined` holds IV, ciphertext and tag back to back */
10
+ static fromCombined(combined: IBinaryInput, config?: IAesSealedDataConfig): AESSealedData;
11
+ }
12
+ /** AES-GCM encryption. A string plaintext must be base64-encoded */
13
+ export declare function aesEncryptAsync(plaintext: IBinaryInput, key: AESEncryptionKey, options?: IAesEncryptOptions): Promise<AESSealedData>;
14
+ export declare function aesDecryptAsync(sealedData: AESSealedData, key: AESEncryptionKey, options: IBase64DecryptOptions): Promise<string>;
15
+ export declare function aesDecryptAsync(sealedData: AESSealedData, key: AESEncryptionKey, options?: IBytesDecryptOptions): Promise<Uint8Array>;
16
+ export declare function aesDecryptAsync(sealedData: AESSealedData, key: AESEncryptionKey, options?: IAesDecryptOptions): Promise<string | Uint8Array>;
@@ -0,0 +1,59 @@
1
+ import { expoCryptoAes, } from './aes-native-module.js';
2
+ // Native has no ArrayBuffer support, and encodes binary options as base64 strings
3
+ function convertBinaryInput(input, useBase64 = false) {
4
+ const bytes = input instanceof ArrayBuffer ? new Uint8Array(input) : input;
5
+ if (typeof bytes !== 'string' && useBase64) {
6
+ return uint8ArrayToBase64(bytes);
7
+ }
8
+ return bytes;
9
+ }
10
+ function uint8ArrayToBase64(bytes) {
11
+ let binaryString = '';
12
+ for (const byte of bytes) {
13
+ binaryString += String.fromCharCode(byte);
14
+ }
15
+ return btoa(binaryString);
16
+ }
17
+ export class AESEncryptionKey extends expoCryptoAes.EncryptionKey {
18
+ }
19
+ export class AESSealedData extends expoCryptoAes.SealedData {
20
+ static fromParts(iv, ciphertext, tag) {
21
+ const processedIv = convertBinaryInput(iv);
22
+ const processedCiphertext = convertBinaryInput(ciphertext);
23
+ if (tag === undefined || typeof tag === 'number') {
24
+ return expoCryptoAes.SealedData.fromParts(processedIv, processedCiphertext, tag);
25
+ }
26
+ return expoCryptoAes.SealedData.fromParts(processedIv, processedCiphertext, convertBinaryInput(tag));
27
+ }
28
+ /** `combined` holds IV, ciphertext and tag back to back */
29
+ static fromCombined(combined, config) {
30
+ return expoCryptoAes.SealedData.fromCombined(convertBinaryInput(combined), config);
31
+ }
32
+ }
33
+ /** AES-GCM encryption. A string plaintext must be base64-encoded */
34
+ export function aesEncryptAsync(plaintext, key, options = {}) {
35
+ const { nonce, additionalData, ...rest } = options;
36
+ let nativeOptions = { ...rest };
37
+ if (nonce) {
38
+ nativeOptions = {
39
+ ...nativeOptions,
40
+ nonce: 'bytes' in nonce ? convertBinaryInput(nonce.bytes, true) : nonce.length,
41
+ };
42
+ }
43
+ if (additionalData) {
44
+ nativeOptions = {
45
+ ...nativeOptions,
46
+ additionalData: convertBinaryInput(additionalData, true),
47
+ };
48
+ }
49
+ return expoCryptoAes.encryptAsync(convertBinaryInput(plaintext), key, nativeOptions);
50
+ }
51
+ export function aesDecryptAsync(sealedData, key, options = {}) {
52
+ const { additionalData, ...rest } = options;
53
+ return expoCryptoAes.decryptAsync(sealedData, key, {
54
+ ...rest,
55
+ additionalData: additionalData
56
+ ? convertBinaryInput(additionalData, true)
57
+ : undefined,
58
+ });
59
+ }
@@ -0,0 +1,2 @@
1
+ export { AESEncryptionKey, AESSealedData, aesEncryptAsync, aesDecryptAsync, } from './aes';
2
+ export { AESKeySize, type IAesDecryptOptions, type IAesEncryptOptions, type IAesSealedDataConfig, type IBase64DecryptOptions, type IBinaryInput, type IBytesDecryptOptions, type IGcmNonceParam, type IGcmTagByteLength, } from './aes-types';
@@ -0,0 +1,2 @@
1
+ export { AESEncryptionKey, AESSealedData, aesEncryptAsync, aesDecryptAsync, } from './aes.js';
2
+ export { AESKeySize, } from './aes-types.js';
@@ -1,2 +1,3 @@
1
1
  export { CryptoError, getRandomBytes, getRandomBytesAsync, getRandomValues, randomUUID, digestStringAsync, digest, } from './crypto';
2
+ export * from './aes';
2
3
  export { CryptoDigestAlgorithm, CryptoEncoding, type ICryptoDigestOptions, type IDigest, type IUintBasedTypedArray, type IIntBasedTypedArray, type ITypedArray, } from './types';
@@ -1,2 +1,3 @@
1
1
  export { CryptoError, getRandomBytes, getRandomBytesAsync, getRandomValues, randomUUID, digestStringAsync, digest, } from './crypto.js';
2
+ export * from './aes/index.js';
2
3
  export { CryptoDigestAlgorithm, CryptoEncoding, } from './types.js';
@@ -0,0 +1,55 @@
1
+ import { NativeModule } from 'expo-modules-core';
2
+ import type { IAesDecryptOptions, IAesEncryptOptions, IAesSealedDataConfig, IBinaryInput, IGcmTagByteLength, AESKeySize } from './aes-types';
3
+ type IEncoding = 'hex' | 'base64';
4
+ declare class EncryptionKey {
5
+ static generate(size?: AESKeySize): Promise<EncryptionKey>;
6
+ static import(bytes: Uint8Array): Promise<EncryptionKey>;
7
+ static import(hexString: string, encoding: IEncoding): Promise<EncryptionKey>;
8
+ /** Key size in bits */
9
+ size: AESKeySize;
10
+ bytes(): Promise<Uint8Array>;
11
+ encoded(encoding: IEncoding): Promise<string>;
12
+ }
13
+ declare class SealedData {
14
+ static fromCombined(combined: IBinaryInput, config?: IAesSealedDataConfig): SealedData;
15
+ static fromParts(iv: IBinaryInput, ciphertext: IBinaryInput, tag: IBinaryInput): SealedData;
16
+ static fromParts(iv: IBinaryInput, ciphertextWithTag: IBinaryInput, tagLength?: number): SealedData;
17
+ ciphertext(options: {
18
+ includeTag?: boolean;
19
+ encoding: 'base64';
20
+ }): Promise<string>;
21
+ ciphertext(options?: {
22
+ includeTag?: boolean;
23
+ encoding?: 'bytes';
24
+ }): Promise<Uint8Array>;
25
+ ciphertext(options: {
26
+ includeTag?: boolean;
27
+ encoding?: 'base64' | 'bytes';
28
+ }): Promise<string | Uint8Array>;
29
+ iv(encoding?: 'bytes'): Promise<Uint8Array>;
30
+ iv(encoding: 'base64'): Promise<string>;
31
+ iv(encoding?: 'bytes' | 'base64'): Promise<string | Uint8Array>;
32
+ tag(encoding?: 'bytes'): Promise<Uint8Array>;
33
+ tag(encoding: 'base64'): Promise<string>;
34
+ tag(encoding?: 'bytes' | 'base64'): Promise<string | Uint8Array>;
35
+ combined(encoding?: 'bytes'): Promise<Uint8Array>;
36
+ combined(encoding: 'base64'): Promise<string>;
37
+ combined(encoding?: 'bytes' | 'base64'): Promise<string | Uint8Array>;
38
+ /** IV + ciphertext + tag, in bytes */
39
+ readonly combinedSize: number;
40
+ readonly ivSize: number;
41
+ readonly tagSize: IGcmTagByteLength;
42
+ }
43
+ export type INativeAesEncryptOptions = Omit<IAesEncryptOptions, 'nonce'> & {
44
+ nonce?: number | IBinaryInput | undefined;
45
+ };
46
+ declare class NativeAesCryptoModule extends NativeModule {
47
+ EncryptionKey: typeof EncryptionKey;
48
+ SealedData: typeof SealedData;
49
+ generateKey(size?: AESKeySize): Promise<EncryptionKey>;
50
+ importKey(keyInput: string | Uint8Array, encoding?: IEncoding): Promise<EncryptionKey>;
51
+ encryptAsync(plaintext: IBinaryInput, key: EncryptionKey, options?: INativeAesEncryptOptions): Promise<SealedData>;
52
+ decryptAsync(sealedData: SealedData, key: EncryptionKey, options?: IAesDecryptOptions): Promise<string | Uint8Array>;
53
+ }
54
+ export declare const expoCryptoAes: NativeAesCryptoModule;
55
+ export {};
@@ -0,0 +1,3 @@
1
+ import { NativeModule, requireNativeModule } from 'expo-modules-core';
2
+ export const expoCryptoAes = requireNativeModule('ExpoCryptoAES');
3
+ //# sourceMappingURL=aes-native-module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aes-native-module.js","sourceRoot":"","sources":["../../../src/core/aes/aes-native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAmGtE,MAAM,CAAC,MAAM,aAAa,GACxB,mBAAmB,CAAwB,eAAe,CAAC,CAAC"}
@@ -0,0 +1,36 @@
1
+ export type IBinaryInput = string | Uint8Array | ArrayBuffer;
2
+ export declare enum AESKeySize {
3
+ AES128 = 128,
4
+ AES192 = 192,
5
+ AES256 = 256
6
+ }
7
+ export type IGcmTagByteLength = 16 | 15 | 14 | 13 | 12 | 8 | 4;
8
+ export type IAesSealedDataConfig = {
9
+ /** @default 12 */
10
+ ivLength: number;
11
+ /** @default 16 */
12
+ tagLength: IGcmTagByteLength;
13
+ };
14
+ export type IAesDecryptOptions = {
15
+ /** @default 'bytes' */
16
+ output?: 'bytes' | 'base64';
17
+ additionalData?: IBinaryInput;
18
+ };
19
+ export type IBase64DecryptOptions = IAesDecryptOptions & {
20
+ output: 'base64';
21
+ };
22
+ export type IBytesDecryptOptions = IAesDecryptOptions & {
23
+ output?: 'bytes';
24
+ };
25
+ export type IGcmNonceParam = {
26
+ /** Byte length of the nonce to generate. @default 12 */
27
+ length: number;
28
+ } | {
29
+ bytes: IBinaryInput;
30
+ };
31
+ export type IAesEncryptOptions = {
32
+ /** @default { length: 12 } */
33
+ nonce?: IGcmNonceParam;
34
+ tagLength?: IGcmTagByteLength;
35
+ additionalData?: IBinaryInput;
36
+ };
@@ -0,0 +1,8 @@
1
+ export var AESKeySize;
2
+ (function (AESKeySize) {
3
+ AESKeySize[AESKeySize["AES128"] = 128] = "AES128";
4
+ // Unsupported on web
5
+ AESKeySize[AESKeySize["AES192"] = 192] = "AES192";
6
+ AESKeySize[AESKeySize["AES256"] = 256] = "AES256";
7
+ })(AESKeySize || (AESKeySize = {}));
8
+ //# sourceMappingURL=aes-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aes-types.js","sourceRoot":"","sources":["../../../src/core/aes/aes-types.ts"],"names":[],"mappings":"AAGA,MAAM,CAAN,IAAY,UAKX;AALD,WAAY,UAAU;IACpB,iDAAY,CAAA;IACZ,qBAAqB;IACrB,iDAAY,CAAA;IACZ,iDAAY,CAAA;AACd,CAAC,EALW,UAAU,KAAV,UAAU,QAKrB"}
@@ -0,0 +1,16 @@
1
+ import { expoCryptoAes } from './aes-native-module';
2
+ import type { IAesDecryptOptions, IAesEncryptOptions, IAesSealedDataConfig, IBase64DecryptOptions, IBinaryInput, IBytesDecryptOptions } from './aes-types';
3
+ export declare class AESEncryptionKey extends expoCryptoAes.EncryptionKey {
4
+ }
5
+ export declare class AESSealedData extends expoCryptoAes.SealedData {
6
+ /** `tag` is a separate tag, or the tag length when the ciphertext already carries it */
7
+ static fromParts(iv: IBinaryInput, ciphertext: IBinaryInput, tag: IBinaryInput): AESSealedData;
8
+ static fromParts(iv: IBinaryInput, ciphertextWithTag: IBinaryInput, tagLength?: number): AESSealedData;
9
+ /** `combined` holds IV, ciphertext and tag back to back */
10
+ static fromCombined(combined: IBinaryInput, config?: IAesSealedDataConfig): AESSealedData;
11
+ }
12
+ /** AES-GCM encryption. A string plaintext must be base64-encoded */
13
+ export declare function aesEncryptAsync(plaintext: IBinaryInput, key: AESEncryptionKey, options?: IAesEncryptOptions): Promise<AESSealedData>;
14
+ export declare function aesDecryptAsync(sealedData: AESSealedData, key: AESEncryptionKey, options: IBase64DecryptOptions): Promise<string>;
15
+ export declare function aesDecryptAsync(sealedData: AESSealedData, key: AESEncryptionKey, options?: IBytesDecryptOptions): Promise<Uint8Array>;
16
+ export declare function aesDecryptAsync(sealedData: AESSealedData, key: AESEncryptionKey, options?: IAesDecryptOptions): Promise<string | Uint8Array>;
@@ -0,0 +1,60 @@
1
+ import { expoCryptoAes, } from './aes-native-module';
2
+ // Native has no ArrayBuffer support, and encodes binary options as base64 strings
3
+ function convertBinaryInput(input, useBase64 = false) {
4
+ const bytes = input instanceof ArrayBuffer ? new Uint8Array(input) : input;
5
+ if (typeof bytes !== 'string' && useBase64) {
6
+ return uint8ArrayToBase64(bytes);
7
+ }
8
+ return bytes;
9
+ }
10
+ function uint8ArrayToBase64(bytes) {
11
+ let binaryString = '';
12
+ for (const byte of bytes) {
13
+ binaryString += String.fromCharCode(byte);
14
+ }
15
+ return btoa(binaryString);
16
+ }
17
+ export class AESEncryptionKey extends expoCryptoAes.EncryptionKey {
18
+ }
19
+ export class AESSealedData extends expoCryptoAes.SealedData {
20
+ static fromParts(iv, ciphertext, tag) {
21
+ const processedIv = convertBinaryInput(iv);
22
+ const processedCiphertext = convertBinaryInput(ciphertext);
23
+ if (tag === undefined || typeof tag === 'number') {
24
+ return expoCryptoAes.SealedData.fromParts(processedIv, processedCiphertext, tag);
25
+ }
26
+ return expoCryptoAes.SealedData.fromParts(processedIv, processedCiphertext, convertBinaryInput(tag));
27
+ }
28
+ /** `combined` holds IV, ciphertext and tag back to back */
29
+ static fromCombined(combined, config) {
30
+ return expoCryptoAes.SealedData.fromCombined(convertBinaryInput(combined), config);
31
+ }
32
+ }
33
+ /** AES-GCM encryption. A string plaintext must be base64-encoded */
34
+ export function aesEncryptAsync(plaintext, key, options = {}) {
35
+ const { nonce, additionalData, ...rest } = options;
36
+ let nativeOptions = { ...rest };
37
+ if (nonce) {
38
+ nativeOptions = {
39
+ ...nativeOptions,
40
+ nonce: 'bytes' in nonce ? convertBinaryInput(nonce.bytes, true) : nonce.length,
41
+ };
42
+ }
43
+ if (additionalData) {
44
+ nativeOptions = {
45
+ ...nativeOptions,
46
+ additionalData: convertBinaryInput(additionalData, true),
47
+ };
48
+ }
49
+ return expoCryptoAes.encryptAsync(convertBinaryInput(plaintext), key, nativeOptions);
50
+ }
51
+ export function aesDecryptAsync(sealedData, key, options = {}) {
52
+ const { additionalData, ...rest } = options;
53
+ return expoCryptoAes.decryptAsync(sealedData, key, {
54
+ ...rest,
55
+ additionalData: additionalData
56
+ ? convertBinaryInput(additionalData, true)
57
+ : undefined,
58
+ });
59
+ }
60
+ //# sourceMappingURL=aes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aes.js","sourceRoot":"","sources":["../../../src/core/aes/aes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { AESEncryptionKey, AESSealedData, aesEncryptAsync, aesDecryptAsync, } from './aes';
2
+ export { AESKeySize, type IAesDecryptOptions, type IAesEncryptOptions, type IAesSealedDataConfig, type IBase64DecryptOptions, type IBinaryInput, type IBytesDecryptOptions, type IGcmNonceParam, type IGcmTagByteLength, } from './aes-types';
@@ -0,0 +1,3 @@
1
+ export { AESEncryptionKey, AESSealedData, aesEncryptAsync, aesDecryptAsync, } from './aes';
2
+ export { AESKeySize, } from './aes-types';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/aes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,eAAe,GAChB,MAAM,OAAO,CAAC;AACf,OAAO,EACL,UAAU,GASX,MAAM,aAAa,CAAC"}
@@ -1,2 +1,3 @@
1
1
  export { CryptoError, getRandomBytes, getRandomBytesAsync, getRandomValues, randomUUID, digestStringAsync, digest, } from './crypto';
2
+ export * from './aes';
2
3
  export { CryptoDigestAlgorithm, CryptoEncoding, type ICryptoDigestOptions, type IDigest, type IUintBasedTypedArray, type IIntBasedTypedArray, type ITypedArray, } from './types';
@@ -1,3 +1,4 @@
1
1
  export { CryptoError, getRandomBytes, getRandomBytesAsync, getRandomValues, randomUUID, digestStringAsync, digest, } from './crypto';
2
+ export * from './aes';
2
3
  export { CryptoDigestAlgorithm, CryptoEncoding, } from './types';
3
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,UAAU,EACV,iBAAiB,EACjB,MAAM,GACP,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,qBAAqB,EACrB,cAAc,GAMf,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,UAAU,EACV,iBAAiB,EACjB,MAAM,GACP,MAAM,UAAU,CAAC;AAClB,cAAc,OAAO,CAAC;AACtB,OAAO,EACL,qBAAqB,EACrB,cAAc,GAMf,MAAM,SAAS,CAAC"}
package/native-link.json CHANGED
@@ -6,6 +6,11 @@
6
6
  "importPath": "expo.modules.crypto.CryptoModule",
7
7
  "className": "CryptoModule",
8
8
  "nativeName": "ExpoCrypto"
9
+ },
10
+ {
11
+ "importPath": "expo.modules.crypto.aes.AesCryptoModule",
12
+ "className": "AesCryptoModule",
13
+ "nativeName": "ExpoCryptoAES"
9
14
  }
10
15
  ]
11
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/crypto",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "expo-crypto wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. Cryptographically secure random bytes, randomUUID, and string/buffer digest hashing (SHA-1/256/384/512, MD2/MD4/MD5).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "expo-crypto": "57.0.1",
73
- "expo-modules-core": "57.0.5"
73
+ "expo-modules-core": "57.0.20"
74
74
  },
75
75
  "peerDependencies": {
76
76
  "@angular/core": ">=20",
@@ -80,12 +80,12 @@
80
80
  "solid-js": ">=1.9.0",
81
81
  "svelte": ">=5.56.0",
82
82
  "vue": ">=3.5.0",
83
- "@symbiote-native/angular": "^3.1.1",
84
- "@symbiote-native/engine": "^1.3.0",
85
- "@symbiote-native/react": "^3.0.3",
86
- "@symbiote-native/solid": "^3.0.3",
87
- "@symbiote-native/svelte": "^3.0.3",
88
- "@symbiote-native/vue": "^3.0.3"
83
+ "@symbiote-native/angular": "^3.2.0",
84
+ "@symbiote-native/engine": "^1.5.0",
85
+ "@symbiote-native/react": "^3.2.0",
86
+ "@symbiote-native/solid": "^3.1.0",
87
+ "@symbiote-native/svelte": "^3.1.0",
88
+ "@symbiote-native/vue": "^3.2.0"
89
89
  },
90
90
  "peerDependenciesMeta": {
91
91
  "@symbiote-native/angular": {
@@ -133,13 +133,13 @@
133
133
  "solid-js": "^1.9.14",
134
134
  "svelte": "^5.56.0",
135
135
  "typescript": "~6.0.0",
136
- "@symbiote-native/angular": "3.1.1",
137
- "@symbiote-native/engine": "1.3.0",
138
- "@symbiote-native/react": "3.0.3",
139
- "@symbiote-native/solid": "3.0.3",
140
- "@symbiote-native/svelte": "3.0.3",
141
- "@symbiote-native/test-utils": "0.4.3",
142
- "@symbiote-native/vue": "3.0.3"
136
+ "@symbiote-native/angular": "3.2.0",
137
+ "@symbiote-native/engine": "1.5.0",
138
+ "@symbiote-native/react": "3.2.0",
139
+ "@symbiote-native/solid": "3.1.0",
140
+ "@symbiote-native/svelte": "3.1.0",
141
+ "@symbiote-native/test-utils": "0.4.6",
142
+ "@symbiote-native/vue": "3.2.0"
143
143
  },
144
144
  "scripts": {
145
145
  "typecheck": "tsc --build",
@@ -0,0 +1,101 @@
1
+ import { NativeModule, requireNativeModule } from 'expo-modules-core';
2
+
3
+ import type {
4
+ IAesDecryptOptions,
5
+ IAesEncryptOptions,
6
+ IAesSealedDataConfig,
7
+ IBinaryInput,
8
+ IGcmTagByteLength,
9
+ AESKeySize,
10
+ } from './aes-types';
11
+
12
+ type IEncoding = 'hex' | 'base64';
13
+
14
+ declare class EncryptionKey {
15
+ static generate(size?: AESKeySize): Promise<EncryptionKey>;
16
+ static import(bytes: Uint8Array): Promise<EncryptionKey>;
17
+ static import(hexString: string, encoding: IEncoding): Promise<EncryptionKey>;
18
+
19
+ /** Key size in bits */
20
+ size: AESKeySize;
21
+
22
+ bytes(): Promise<Uint8Array>;
23
+ encoded(encoding: IEncoding): Promise<string>;
24
+ }
25
+
26
+ declare class SealedData {
27
+ static fromCombined(
28
+ combined: IBinaryInput,
29
+ config?: IAesSealedDataConfig,
30
+ ): SealedData;
31
+ static fromParts(
32
+ iv: IBinaryInput,
33
+ ciphertext: IBinaryInput,
34
+ tag: IBinaryInput,
35
+ ): SealedData;
36
+ static fromParts(
37
+ iv: IBinaryInput,
38
+ ciphertextWithTag: IBinaryInput,
39
+ tagLength?: number,
40
+ ): SealedData;
41
+
42
+ ciphertext(options: {
43
+ includeTag?: boolean;
44
+ encoding: 'base64';
45
+ }): Promise<string>;
46
+ ciphertext(options?: {
47
+ includeTag?: boolean;
48
+ encoding?: 'bytes';
49
+ }): Promise<Uint8Array>;
50
+ ciphertext(options: {
51
+ includeTag?: boolean;
52
+ encoding?: 'base64' | 'bytes';
53
+ }): Promise<string | Uint8Array>;
54
+
55
+ iv(encoding?: 'bytes'): Promise<Uint8Array>;
56
+ iv(encoding: 'base64'): Promise<string>;
57
+ iv(encoding?: 'bytes' | 'base64'): Promise<string | Uint8Array>;
58
+
59
+ tag(encoding?: 'bytes'): Promise<Uint8Array>;
60
+ tag(encoding: 'base64'): Promise<string>;
61
+ tag(encoding?: 'bytes' | 'base64'): Promise<string | Uint8Array>;
62
+
63
+ combined(encoding?: 'bytes'): Promise<Uint8Array>;
64
+ combined(encoding: 'base64'): Promise<string>;
65
+ combined(encoding?: 'bytes' | 'base64'): Promise<string | Uint8Array>;
66
+
67
+ /** IV + ciphertext + tag, in bytes */
68
+ readonly combinedSize: number;
69
+ readonly ivSize: number;
70
+ readonly tagSize: IGcmTagByteLength;
71
+ }
72
+
73
+ // Native takes the nonce as a bare length (to generate) or as bytes
74
+ export type INativeAesEncryptOptions = Omit<IAesEncryptOptions, 'nonce'> & {
75
+ nonce?: number | IBinaryInput | undefined;
76
+ };
77
+
78
+ declare class NativeAesCryptoModule extends NativeModule {
79
+ EncryptionKey: typeof EncryptionKey;
80
+ SealedData: typeof SealedData;
81
+
82
+ generateKey(size?: AESKeySize): Promise<EncryptionKey>;
83
+ importKey(
84
+ keyInput: string | Uint8Array,
85
+ encoding?: IEncoding,
86
+ ): Promise<EncryptionKey>;
87
+
88
+ encryptAsync(
89
+ plaintext: IBinaryInput,
90
+ key: EncryptionKey,
91
+ options?: INativeAesEncryptOptions,
92
+ ): Promise<SealedData>;
93
+ decryptAsync(
94
+ sealedData: SealedData,
95
+ key: EncryptionKey,
96
+ options?: IAesDecryptOptions,
97
+ ): Promise<string | Uint8Array>;
98
+ }
99
+
100
+ export const expoCryptoAes =
101
+ requireNativeModule<NativeAesCryptoModule>('ExpoCryptoAES');
@@ -0,0 +1,48 @@
1
+ // A string input must be base64-encoded
2
+ export type IBinaryInput = string | Uint8Array | ArrayBuffer;
3
+
4
+ export enum AESKeySize {
5
+ AES128 = 128,
6
+ // Unsupported on web
7
+ AES192 = 192,
8
+ AES256 = 256,
9
+ }
10
+
11
+ // GCM tag length in bytes: 16 is the default and the only value Apple supports for encryption
12
+ export type IGcmTagByteLength = 16 | 15 | 14 | 13 | 12 | 8 | 4;
13
+
14
+ export type IAesSealedDataConfig = {
15
+ /** @default 12 */
16
+ ivLength: number;
17
+ /** @default 16 */
18
+ tagLength: IGcmTagByteLength;
19
+ };
20
+
21
+ export type IAesDecryptOptions = {
22
+ /** @default 'bytes' */
23
+ output?: 'bytes' | 'base64';
24
+ // GCM additional authenticated data, base64 when a string
25
+ additionalData?: IBinaryInput;
26
+ };
27
+
28
+ export type IBase64DecryptOptions = IAesDecryptOptions & { output: 'base64' };
29
+
30
+ export type IBytesDecryptOptions = IAesDecryptOptions & { output?: 'bytes' };
31
+
32
+ export type IGcmNonceParam =
33
+ | {
34
+ /** Byte length of the nonce to generate. @default 12 */
35
+ length: number;
36
+ }
37
+ | {
38
+ bytes: IBinaryInput;
39
+ };
40
+
41
+ export type IAesEncryptOptions = {
42
+ /** @default { length: 12 } */
43
+ nonce?: IGcmNonceParam;
44
+ // Ignored on Apple, where the tag is always 16 bytes
45
+ tagLength?: IGcmTagByteLength;
46
+ // GCM additional authenticated data, base64 when a string
47
+ additionalData?: IBinaryInput;
48
+ };
@@ -0,0 +1,142 @@
1
+ import {
2
+ expoCryptoAes,
3
+ type INativeAesEncryptOptions,
4
+ } from './aes-native-module';
5
+ import type {
6
+ IAesDecryptOptions,
7
+ IAesEncryptOptions,
8
+ IAesSealedDataConfig,
9
+ IBase64DecryptOptions,
10
+ IBinaryInput,
11
+ IBytesDecryptOptions,
12
+ } from './aes-types';
13
+
14
+ // Hermes global, absent from the ES2022 lib
15
+ declare function btoa(data: string): string;
16
+
17
+ // Native has no ArrayBuffer support, and encodes binary options as base64 strings
18
+ function convertBinaryInput(
19
+ input: IBinaryInput,
20
+ useBase64 = false,
21
+ ): IBinaryInput {
22
+ const bytes = input instanceof ArrayBuffer ? new Uint8Array(input) : input;
23
+ if (typeof bytes !== 'string' && useBase64) {
24
+ return uint8ArrayToBase64(bytes);
25
+ }
26
+ return bytes;
27
+ }
28
+
29
+ function uint8ArrayToBase64(bytes: Uint8Array): string {
30
+ let binaryString = '';
31
+ for (const byte of bytes) {
32
+ binaryString += String.fromCharCode(byte);
33
+ }
34
+ return btoa(binaryString);
35
+ }
36
+
37
+ export class AESEncryptionKey extends expoCryptoAes.EncryptionKey {}
38
+
39
+ export class AESSealedData extends expoCryptoAes.SealedData {
40
+ /** `tag` is a separate tag, or the tag length when the ciphertext already carries it */
41
+ static override fromParts(
42
+ iv: IBinaryInput,
43
+ ciphertext: IBinaryInput,
44
+ tag: IBinaryInput,
45
+ ): AESSealedData;
46
+ static override fromParts(
47
+ iv: IBinaryInput,
48
+ ciphertextWithTag: IBinaryInput,
49
+ tagLength?: number,
50
+ ): AESSealedData;
51
+
52
+ static override fromParts(
53
+ iv: IBinaryInput,
54
+ ciphertext: IBinaryInput,
55
+ tag?: IBinaryInput | number,
56
+ ): AESSealedData {
57
+ const processedIv = convertBinaryInput(iv);
58
+ const processedCiphertext = convertBinaryInput(ciphertext);
59
+
60
+ if (tag === undefined || typeof tag === 'number') {
61
+ return expoCryptoAes.SealedData.fromParts(
62
+ processedIv,
63
+ processedCiphertext,
64
+ tag,
65
+ );
66
+ }
67
+ return expoCryptoAes.SealedData.fromParts(
68
+ processedIv,
69
+ processedCiphertext,
70
+ convertBinaryInput(tag),
71
+ );
72
+ }
73
+
74
+ /** `combined` holds IV, ciphertext and tag back to back */
75
+ static override fromCombined(
76
+ combined: IBinaryInput,
77
+ config?: IAesSealedDataConfig,
78
+ ): AESSealedData {
79
+ return expoCryptoAes.SealedData.fromCombined(
80
+ convertBinaryInput(combined),
81
+ config,
82
+ );
83
+ }
84
+ }
85
+
86
+ /** AES-GCM encryption. A string plaintext must be base64-encoded */
87
+ export function aesEncryptAsync(
88
+ plaintext: IBinaryInput,
89
+ key: AESEncryptionKey,
90
+ options: IAesEncryptOptions = {},
91
+ ): Promise<AESSealedData> {
92
+ const { nonce, additionalData, ...rest } = options;
93
+ let nativeOptions: INativeAesEncryptOptions = { ...rest };
94
+ if (nonce) {
95
+ nativeOptions = {
96
+ ...nativeOptions,
97
+ nonce:
98
+ 'bytes' in nonce ? convertBinaryInput(nonce.bytes, true) : nonce.length,
99
+ };
100
+ }
101
+ if (additionalData) {
102
+ nativeOptions = {
103
+ ...nativeOptions,
104
+ additionalData: convertBinaryInput(additionalData, true),
105
+ };
106
+ }
107
+ return expoCryptoAes.encryptAsync(
108
+ convertBinaryInput(plaintext),
109
+ key,
110
+ nativeOptions,
111
+ );
112
+ }
113
+
114
+ export function aesDecryptAsync(
115
+ sealedData: AESSealedData,
116
+ key: AESEncryptionKey,
117
+ options: IBase64DecryptOptions,
118
+ ): Promise<string>;
119
+ export function aesDecryptAsync(
120
+ sealedData: AESSealedData,
121
+ key: AESEncryptionKey,
122
+ options?: IBytesDecryptOptions,
123
+ ): Promise<Uint8Array>;
124
+ export function aesDecryptAsync(
125
+ sealedData: AESSealedData,
126
+ key: AESEncryptionKey,
127
+ options?: IAesDecryptOptions,
128
+ ): Promise<string | Uint8Array>;
129
+
130
+ export function aesDecryptAsync(
131
+ sealedData: AESSealedData,
132
+ key: AESEncryptionKey,
133
+ options: IAesDecryptOptions = {},
134
+ ): Promise<string | Uint8Array> {
135
+ const { additionalData, ...rest } = options;
136
+ return expoCryptoAes.decryptAsync(sealedData, key, {
137
+ ...rest,
138
+ additionalData: additionalData
139
+ ? convertBinaryInput(additionalData, true)
140
+ : undefined,
141
+ });
142
+ }
@@ -0,0 +1,17 @@
1
+ export {
2
+ AESEncryptionKey,
3
+ AESSealedData,
4
+ aesEncryptAsync,
5
+ aesDecryptAsync,
6
+ } from './aes';
7
+ export {
8
+ AESKeySize,
9
+ type IAesDecryptOptions,
10
+ type IAesEncryptOptions,
11
+ type IAesSealedDataConfig,
12
+ type IBase64DecryptOptions,
13
+ type IBinaryInput,
14
+ type IBytesDecryptOptions,
15
+ type IGcmNonceParam,
16
+ type IGcmTagByteLength,
17
+ } from './aes-types';
package/src/core/index.ts CHANGED
@@ -7,6 +7,7 @@ export {
7
7
  digestStringAsync,
8
8
  digest,
9
9
  } from './crypto';
10
+ export * from './aes';
10
11
  export {
11
12
  CryptoDigestAlgorithm,
12
13
  CryptoEncoding,