one 2.6.20 → 2.6.22

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.
Files changed (154) hide show
  1. package/android/src/main/java/dev/onejs/onenative/OneNativeCameraManager.kt +24 -0
  2. package/android/src/main/java/dev/onejs/onenative/OneNativeCameraView.kt +197 -2
  3. package/cpp/HybridOneCrypto.cpp +327 -1
  4. package/cpp/HybridOneCrypto.hpp +8 -0
  5. package/dist/cjs/index.cjs +25 -22
  6. package/dist/cjs/index.native.cjs +25 -22
  7. package/dist/cjs/index.native.js +25 -22
  8. package/dist/cjs/index.native.js.map +1 -1
  9. package/dist/cjs/one.cjs +1 -0
  10. package/dist/cjs/one.native.cjs +1 -0
  11. package/dist/cjs/one.native.js +1 -0
  12. package/dist/cjs/one.native.js.map +1 -1
  13. package/dist/cjs/platform/camera/index.cjs +3 -2
  14. package/dist/cjs/platform/camera/index.native.cjs +89 -3
  15. package/dist/cjs/platform/camera/index.native.js +89 -3
  16. package/dist/cjs/platform/camera/index.native.js.map +1 -1
  17. package/dist/cjs/platform/crypto/aes.cjs +259 -0
  18. package/dist/cjs/platform/crypto/aes.native.cjs +343 -0
  19. package/dist/cjs/platform/crypto/aes.native.js +345 -0
  20. package/dist/cjs/platform/crypto/aes.native.js.map +1 -0
  21. package/dist/cjs/platform/crypto/bytes.cjs +132 -0
  22. package/dist/cjs/platform/crypto/bytes.native.cjs +175 -0
  23. package/dist/cjs/platform/crypto/bytes.native.js +177 -0
  24. package/dist/cjs/platform/crypto/bytes.native.js.map +1 -0
  25. package/dist/cjs/platform/crypto/digest-api.cjs +65 -0
  26. package/dist/cjs/platform/crypto/digest-api.native.cjs +69 -0
  27. package/dist/cjs/platform/crypto/digest-api.native.js +71 -0
  28. package/dist/cjs/platform/crypto/digest-api.native.js.map +1 -0
  29. package/dist/cjs/platform/crypto/digest.cjs +26 -15
  30. package/dist/cjs/platform/crypto/digest.native.cjs +26 -15
  31. package/dist/cjs/platform/crypto/digest.native.js +26 -15
  32. package/dist/cjs/platform/crypto/digest.native.js.map +1 -1
  33. package/dist/cjs/platform/crypto/index.cjs +19 -1
  34. package/dist/cjs/platform/crypto/index.native.cjs +39 -1
  35. package/dist/cjs/platform/crypto/index.native.js +39 -1
  36. package/dist/cjs/platform/crypto/index.native.js.map +1 -1
  37. package/dist/cjs/platform/crypto/types.cjs +16 -0
  38. package/dist/cjs/platform/crypto/types.native.cjs +18 -0
  39. package/dist/cjs/platform/crypto/types.native.js +20 -0
  40. package/dist/cjs/platform/crypto/types.native.js.map +1 -0
  41. package/dist/cjs/platform/crypto/web.cjs +73 -0
  42. package/dist/cjs/platform/extras.cjs +4 -0
  43. package/dist/cjs/platform/extras.native.cjs +4 -0
  44. package/dist/cjs/platform/extras.native.js +4 -0
  45. package/dist/cjs/platform/extras.native.js.map +1 -1
  46. package/dist/cjs/platform/specs/OneNativeCameraNativeComponent.cjs +37 -21
  47. package/dist/cjs/platform/specs/OneNativeCameraNativeComponent.native.cjs +37 -21
  48. package/dist/cjs/platform/specs/OneNativeCameraNativeComponent.native.js +37 -21
  49. package/dist/cjs/platform/specs/OneNativeCameraNativeComponent.native.js.map +3 -3
  50. package/dist/cjs/router/router.cjs +4 -1
  51. package/dist/esm/index.js +2 -2
  52. package/dist/esm/index.mjs +2 -2
  53. package/dist/esm/index.native.js +2 -2
  54. package/dist/esm/one.mjs +2 -1
  55. package/dist/esm/one.mjs.map +1 -1
  56. package/dist/esm/one.native.js +2 -1
  57. package/dist/esm/one.native.js.map +1 -1
  58. package/dist/esm/platform/camera/index.mjs +4 -2
  59. package/dist/esm/platform/camera/index.mjs.map +1 -1
  60. package/dist/esm/platform/camera/index.native.js +90 -4
  61. package/dist/esm/platform/camera/index.native.js.map +1 -1
  62. package/dist/esm/platform/crypto/aes.mjs +236 -0
  63. package/dist/esm/platform/crypto/aes.mjs.map +1 -0
  64. package/dist/esm/platform/crypto/aes.native.js +318 -0
  65. package/dist/esm/platform/crypto/aes.native.js.map +1 -0
  66. package/dist/esm/platform/crypto/bytes.mjs +104 -0
  67. package/dist/esm/platform/crypto/bytes.mjs.map +1 -0
  68. package/dist/esm/platform/crypto/bytes.native.js +145 -0
  69. package/dist/esm/platform/crypto/bytes.native.js.map +1 -0
  70. package/dist/esm/platform/crypto/digest-api.mjs +42 -0
  71. package/dist/esm/platform/crypto/digest-api.mjs.map +1 -0
  72. package/dist/esm/platform/crypto/digest-api.native.js +44 -0
  73. package/dist/esm/platform/crypto/digest-api.native.js.map +1 -0
  74. package/dist/esm/platform/crypto/digest.mjs +23 -15
  75. package/dist/esm/platform/crypto/digest.mjs.map +1 -1
  76. package/dist/esm/platform/crypto/digest.native.js +23 -15
  77. package/dist/esm/platform/crypto/digest.native.js.map +1 -1
  78. package/dist/esm/platform/crypto/index.mjs +14 -1
  79. package/dist/esm/platform/crypto/index.mjs.map +1 -1
  80. package/dist/esm/platform/crypto/index.native.js +34 -1
  81. package/dist/esm/platform/crypto/index.native.js.map +1 -1
  82. package/dist/esm/platform/crypto/types.mjs +0 -0
  83. package/dist/esm/platform/crypto/types.native.js +0 -0
  84. package/dist/esm/platform/crypto/web.mjs +50 -0
  85. package/dist/esm/platform/crypto/web.mjs.map +1 -0
  86. package/dist/esm/platform/extras.mjs +2 -1
  87. package/dist/esm/platform/extras.native.js +2 -1
  88. package/dist/esm/platform/specs/OneNativeCameraNativeComponent.mjs +25 -9
  89. package/dist/esm/platform/specs/OneNativeCameraNativeComponent.mjs.map +3 -3
  90. package/dist/esm/platform/specs/OneNativeCameraNativeComponent.native.js +25 -9
  91. package/dist/esm/platform/specs/OneNativeCameraNativeComponent.native.js.map +3 -3
  92. package/dist/esm/router/router.mjs +5 -2
  93. package/dist/esm/router/router.mjs.map +1 -1
  94. package/ios/OneNativeCameraComponentView.mm +28 -0
  95. package/ios/OneNativeCameraView.swift +163 -0
  96. package/nitrogen/generated/shared/c++/HybridOneCryptoSpec.cpp +2 -0
  97. package/nitrogen/generated/shared/c++/HybridOneCryptoSpec.hpp +4 -1
  98. package/package.json +12 -12
  99. package/src/index.ts +4 -0
  100. package/src/one.ts +3 -0
  101. package/src/platform/camera/index.native.tsx +116 -12
  102. package/src/platform/camera/index.tsx +15 -4
  103. package/src/platform/camera/types.ts +22 -0
  104. package/src/platform/crypto/aes.ts +345 -0
  105. package/src/platform/crypto/bytes.ts +120 -0
  106. package/src/platform/crypto/digest-api.ts +50 -0
  107. package/src/platform/crypto/digest.ts +24 -15
  108. package/src/platform/crypto/index.native.ts +68 -0
  109. package/src/platform/crypto/index.ts +31 -0
  110. package/src/platform/crypto/types.ts +64 -0
  111. package/src/platform/crypto/web.ts +69 -0
  112. package/src/platform/extras.ts +15 -0
  113. package/src/platform/index.native.ts +4 -0
  114. package/src/platform/index.ts +4 -0
  115. package/src/platform/specs/OneCrypto.nitro.ts +19 -0
  116. package/src/platform/specs/OneNativeCameraNativeComponent.ts +35 -2
  117. package/src/router/router.ts +7 -4
  118. package/types/index.d.ts +2 -1
  119. package/types/index.d.ts.map +1 -1
  120. package/types/one.d.ts +2 -1
  121. package/types/one.d.ts.map +1 -1
  122. package/types/platform/camera/index.d.ts +3 -3
  123. package/types/platform/camera/index.d.ts.map +1 -1
  124. package/types/platform/camera/index.native.d.ts +3 -3
  125. package/types/platform/camera/index.native.d.ts.map +1 -1
  126. package/types/platform/camera/types.d.ts +18 -0
  127. package/types/platform/camera/types.d.ts.map +1 -1
  128. package/types/platform/crypto/aes.d.ts +211 -0
  129. package/types/platform/crypto/aes.d.ts.map +1 -0
  130. package/types/platform/crypto/bytes.d.ts +8 -0
  131. package/types/platform/crypto/bytes.d.ts.map +1 -0
  132. package/types/platform/crypto/digest-api.d.ts +9 -0
  133. package/types/platform/crypto/digest-api.d.ts.map +1 -0
  134. package/types/platform/crypto/digest.d.ts +5 -0
  135. package/types/platform/crypto/digest.d.ts.map +1 -1
  136. package/types/platform/crypto/index.d.ts +127 -0
  137. package/types/platform/crypto/index.d.ts.map +1 -1
  138. package/types/platform/crypto/index.native.d.ts +127 -0
  139. package/types/platform/crypto/index.native.d.ts.map +1 -1
  140. package/types/platform/crypto/types.d.ts +43 -0
  141. package/types/platform/crypto/types.d.ts.map +1 -0
  142. package/types/platform/crypto/web.d.ts +5 -0
  143. package/types/platform/crypto/web.d.ts.map +1 -0
  144. package/types/platform/extras.d.ts +2 -0
  145. package/types/platform/extras.d.ts.map +1 -1
  146. package/types/platform/index.d.ts +1 -1
  147. package/types/platform/index.d.ts.map +1 -1
  148. package/types/platform/index.native.d.ts +1 -1
  149. package/types/platform/index.native.d.ts.map +1 -1
  150. package/types/platform/specs/OneCrypto.nitro.d.ts +2 -0
  151. package/types/platform/specs/OneCrypto.nitro.d.ts.map +1 -1
  152. package/types/platform/specs/OneNativeCameraNativeComponent.d.ts +18 -2
  153. package/types/platform/specs/OneNativeCameraNativeComponent.d.ts.map +1 -1
  154. package/types/router/router.d.ts.map +1 -1
@@ -0,0 +1,345 @@
1
+ import { base64ToBytes, bytesToBase64, bytesToHex, concatBytes, exactBytes, hexToBytes } from './bytes'
2
+ import type {
3
+ AESDecryptOptions,
4
+ AESEncryptOptions,
5
+ AESKeySize,
6
+ AESSealedDataConfig,
7
+ BinaryInput,
8
+ } from './types'
9
+
10
+ export type { AESDecryptOptions, AESEncryptOptions, AESKeySize, AESSealedDataConfig, BinaryInput }
11
+
12
+ // expo-crypto's AES-GCM api over an injected engine: native is the shared
13
+ // C++ hybrid object, web is SubtleCrypto. seal returns ciphertext with the
14
+ // tag appended; open takes that form. everything else (key and sealed-data
15
+ // handling, encodings, validation) is shared here so both entries behave
16
+ // identically and the logic unit-tests in node.
17
+ export interface AesEngine {
18
+ seal(
19
+ key: Uint8Array<ArrayBuffer>,
20
+ iv: Uint8Array<ArrayBuffer>,
21
+ plaintext: Uint8Array<ArrayBuffer>,
22
+ additionalData: Uint8Array<ArrayBuffer> | undefined,
23
+ tagLength: number
24
+ ): Promise<Uint8Array<ArrayBuffer>>
25
+ open(
26
+ key: Uint8Array<ArrayBuffer>,
27
+ iv: Uint8Array<ArrayBuffer>,
28
+ ciphertextWithTag: Uint8Array<ArrayBuffer>,
29
+ additionalData: Uint8Array<ArrayBuffer> | undefined,
30
+ tagLength: number
31
+ ): Promise<Uint8Array<ArrayBuffer>>
32
+ random(bytes: Uint8Array<ArrayBuffer>): void
33
+ }
34
+
35
+ const KEY_SIZES: readonly AESKeySize[] = [128, 192, 256]
36
+ const TAG_LENGTHS: readonly number[] = [16, 15, 14, 13, 12, 8, 4]
37
+ const DEFAULT_IV_LENGTH = 12
38
+ const DEFAULT_TAG_LENGTH = 16
39
+
40
+ export function assertTagLength(tagLength: number, verb: string): void {
41
+ if (!TAG_LENGTHS.includes(tagLength)) {
42
+ throw new Error(`${verb}: tagLength must be one of: ${TAG_LENGTHS.join(', ')}`)
43
+ }
44
+ }
45
+
46
+ export function toBytes(input: BinaryInput, verb: string): Uint8Array<ArrayBuffer> {
47
+ if (typeof input === 'string') return base64ToBytes(input, verb)
48
+ if (input instanceof ArrayBuffer) return new Uint8Array(input.slice(0))
49
+ if (ArrayBuffer.isView(input) && input.buffer instanceof ArrayBuffer) {
50
+ return exactBytes(new Uint8Array(input.buffer, input.byteOffset, input.byteLength))
51
+ }
52
+ throw new TypeError(`${verb}: expected a base64 string, Uint8Array, or ArrayBuffer`)
53
+ }
54
+
55
+ type SealedParts = {
56
+ iv: Uint8Array<ArrayBuffer>
57
+ ciphertext: Uint8Array<ArrayBuffer>
58
+ tag: Uint8Array<ArrayBuffer>
59
+ }
60
+
61
+ const sealedParts = new WeakMap<object, SealedParts>()
62
+ const keyBytes = new WeakMap<object, Uint8Array<ArrayBuffer>>()
63
+
64
+ function encodeParts(bytes: Uint8Array, encoding: 'base64' | 'bytes' | undefined) {
65
+ return encoding === 'base64' ? bytesToBase64(bytes) : exactBytes(bytes)
66
+ }
67
+
68
+ export function createAesApi(engine: AesEngine) {
69
+ class AESEncryptionKey {
70
+ readonly size: AESKeySize
71
+
72
+ // construct through generate or import; direct construction still
73
+ // validates, so a key object can never hold a non-AES length.
74
+ constructor(bytes: Uint8Array<ArrayBuffer>) {
75
+ if (!KEY_SIZES.includes((bytes.length * 8) as AESKeySize)) {
76
+ throw new Error(
77
+ `Crypto.AESEncryptionKey: key must be 128, 192, or 256 bits, got ${bytes.length * 8}`
78
+ )
79
+ }
80
+ this.size = (bytes.length * 8) as AESKeySize
81
+ keyBytes.set(this, exactBytes(bytes))
82
+ }
83
+
84
+ static async generate(size: AESKeySize = 256): Promise<AESEncryptionKey> {
85
+ if (!KEY_SIZES.includes(size)) {
86
+ throw new Error(`Crypto.AESEncryptionKey.generate: size must be one of: ${KEY_SIZES.join(', ')}`)
87
+ }
88
+ const bytes = new Uint8Array(size / 8)
89
+ engine.random(bytes)
90
+ return new AESEncryptionKey(bytes)
91
+ }
92
+
93
+ static async import(bytes: Uint8Array): Promise<AESEncryptionKey>
94
+ static async import(encoded: string, encoding: 'hex' | 'base64'): Promise<AESEncryptionKey>
95
+ static async import(
96
+ input: Uint8Array | string,
97
+ encoding?: 'hex' | 'base64'
98
+ ): Promise<AESEncryptionKey> {
99
+ const verb = 'Crypto.AESEncryptionKey.import'
100
+ let bytes: Uint8Array<ArrayBuffer>
101
+ if (typeof input === 'string') {
102
+ if (encoding === 'hex') bytes = hexToBytes(input, verb)
103
+ else if (encoding === 'base64') bytes = base64ToBytes(input, verb)
104
+ else throw new Error(`${verb}: encoding must be 'hex' or 'base64'`)
105
+ } else if (input instanceof Uint8Array) {
106
+ bytes = exactBytes(input)
107
+ } else {
108
+ throw new TypeError(`${verb}: expected bytes or an encoded string`)
109
+ }
110
+ if (!KEY_SIZES.includes((bytes.length * 8) as AESKeySize)) {
111
+ throw new Error(`${verb}: key must be 128, 192, or 256 bits, got ${bytes.length * 8}`)
112
+ }
113
+ return new AESEncryptionKey(bytes)
114
+ }
115
+
116
+ async bytes(): Promise<Uint8Array<ArrayBuffer>> {
117
+ return exactBytes(keyBytes.get(this)!)
118
+ }
119
+
120
+ async encoded(encoding: 'hex' | 'base64'): Promise<string> {
121
+ const bytes = keyBytes.get(this)!
122
+ if (encoding === 'hex') return bytesToHex(bytes)
123
+ if (encoding === 'base64') return bytesToBase64(bytes)
124
+ throw new Error(`Crypto.AESEncryptionKey.encoded: encoding must be 'hex' or 'base64'`)
125
+ }
126
+ }
127
+
128
+ class AESSealedData {
129
+ // construct through fromCombined or fromParts. direct construction still
130
+ // validates, so sealed data can never carry an empty iv or a tag gcm
131
+ // does not allow.
132
+ constructor(parts: SealedParts) {
133
+ if (parts.iv.length === 0) throw new Error('Crypto.AESSealedData: iv must not be empty')
134
+ assertTagLength(parts.tag.length, 'Crypto.AESSealedData')
135
+ sealedParts.set(this, parts)
136
+ }
137
+
138
+ static fromCombined(combined: BinaryInput, config: AESSealedDataConfig = {}): AESSealedData {
139
+ const verb = 'Crypto.AESSealedData.fromCombined'
140
+ const ivLength = config.ivLength ?? DEFAULT_IV_LENGTH
141
+ const tagLength = config.tagLength ?? DEFAULT_TAG_LENGTH
142
+ assertTagLength(tagLength, verb)
143
+ if (!Number.isInteger(ivLength) || ivLength <= 0) {
144
+ throw new Error(`${verb}: ivLength must be a positive integer`)
145
+ }
146
+ const bytes = toBytes(combined, verb)
147
+ if (bytes.length < ivLength + tagLength) {
148
+ throw new Error(`${verb}: combined data is shorter than its iv and tag`)
149
+ }
150
+ return new AESSealedData({
151
+ iv: exactBytes(bytes.subarray(0, ivLength)),
152
+ ciphertext: exactBytes(bytes.subarray(ivLength, bytes.length - tagLength)),
153
+ tag: exactBytes(bytes.subarray(bytes.length - tagLength)),
154
+ })
155
+ }
156
+
157
+ static fromParts(iv: BinaryInput, ciphertext: BinaryInput, tag: BinaryInput): AESSealedData
158
+ static fromParts(
159
+ iv: BinaryInput,
160
+ ciphertextWithTag: BinaryInput,
161
+ tagLength?: number
162
+ ): AESSealedData
163
+ static fromParts(
164
+ ivInput: BinaryInput,
165
+ ciphertextInput: BinaryInput,
166
+ tagOrLength?: BinaryInput | number
167
+ ): AESSealedData {
168
+ const verb = 'Crypto.AESSealedData.fromParts'
169
+ const iv = toBytes(ivInput, verb)
170
+ const ciphertext = toBytes(ciphertextInput, verb)
171
+ if (tagOrLength === undefined || typeof tagOrLength === 'number') {
172
+ const tagLength = tagOrLength ?? DEFAULT_TAG_LENGTH
173
+ assertTagLength(tagLength, verb)
174
+ if (ciphertext.length < tagLength) {
175
+ throw new Error(`${verb}: ciphertext is shorter than its tag`)
176
+ }
177
+ return new AESSealedData({
178
+ iv,
179
+ ciphertext: exactBytes(ciphertext.subarray(0, ciphertext.length - tagLength)),
180
+ tag: exactBytes(ciphertext.subarray(ciphertext.length - tagLength)),
181
+ })
182
+ }
183
+ const tag = toBytes(tagOrLength, verb)
184
+ assertTagLength(tag.length, verb)
185
+ return new AESSealedData({ iv, ciphertext, tag })
186
+ }
187
+
188
+ // internal: aesEncrypt builds sealed data from engine output without
189
+ // going through the public parsers (which would just re-split it).
190
+ static fromSeal(iv: Uint8Array<ArrayBuffer>, ciphertextWithTag: Uint8Array, tagLength: number) {
191
+ return new AESSealedData({
192
+ iv,
193
+ ciphertext: exactBytes(ciphertextWithTag.subarray(0, ciphertextWithTag.length - tagLength)),
194
+ tag: exactBytes(ciphertextWithTag.subarray(ciphertextWithTag.length - tagLength)),
195
+ })
196
+ }
197
+
198
+ get ivSize(): number {
199
+ return sealedParts.get(this)!.iv.length
200
+ }
201
+
202
+ get tagSize(): number {
203
+ return sealedParts.get(this)!.tag.length
204
+ }
205
+
206
+ get combinedSize(): number {
207
+ const parts = sealedParts.get(this)!
208
+ return parts.iv.length + parts.ciphertext.length + parts.tag.length
209
+ }
210
+
211
+ async iv(encoding: 'base64'): Promise<string>
212
+ async iv(encoding?: 'bytes'): Promise<Uint8Array<ArrayBuffer>>
213
+ async iv(encoding?: 'base64' | 'bytes') {
214
+ return encodeParts(sealedParts.get(this)!.iv, encoding)
215
+ }
216
+
217
+ async tag(encoding: 'base64'): Promise<string>
218
+ async tag(encoding?: 'bytes'): Promise<Uint8Array<ArrayBuffer>>
219
+ async tag(encoding?: 'base64' | 'bytes') {
220
+ return encodeParts(sealedParts.get(this)!.tag, encoding)
221
+ }
222
+
223
+ async ciphertext(options: {
224
+ encoding: 'base64'
225
+ includeTag: boolean
226
+ }): Promise<string>
227
+ async ciphertext(options: {
228
+ encoding: 'bytes'
229
+ includeTag: boolean
230
+ }): Promise<Uint8Array<ArrayBuffer>>
231
+ async ciphertext(options: { encoding: 'base64' | 'bytes'; includeTag: boolean }) {
232
+ const parts = sealedParts.get(this)!
233
+ const bytes = options?.includeTag
234
+ ? concatBytes([parts.ciphertext, parts.tag])
235
+ : parts.ciphertext
236
+ return encodeParts(bytes, options?.encoding)
237
+ }
238
+
239
+ async combined(encoding: 'base64'): Promise<string>
240
+ async combined(encoding?: 'bytes'): Promise<Uint8Array<ArrayBuffer>>
241
+ async combined(encoding?: 'base64' | 'bytes') {
242
+ const parts = sealedParts.get(this)!
243
+ return encodeParts(concatBytes([parts.iv, parts.ciphertext, parts.tag]), encoding)
244
+ }
245
+ }
246
+
247
+ async function keyMaterial(key: AESEncryptionKey, verb: string): Promise<Uint8Array<ArrayBuffer>> {
248
+ const own = keyBytes.get(key)
249
+ if (own) return exactBytes(own)
250
+ // a key minted by the other entry in the same bundle: read it back
251
+ // through its public shape instead of rejecting a foreign instance.
252
+ if (key && typeof key.bytes === 'function') return exactBytes(await key.bytes())
253
+ throw new TypeError(`${verb}: expected an AESEncryptionKey`)
254
+ }
255
+
256
+ async function partsOf(sealed: AESSealedData, verb: string): Promise<SealedParts> {
257
+ const own = sealedParts.get(sealed)
258
+ if (own) {
259
+ return {
260
+ iv: exactBytes(own.iv),
261
+ ciphertext: exactBytes(own.ciphertext),
262
+ tag: exactBytes(own.tag),
263
+ }
264
+ }
265
+ if (sealed && typeof sealed.iv === 'function' && typeof sealed.combined === 'function') {
266
+ const iv = exactBytes(await sealed.iv())
267
+ const tag = exactBytes(await sealed.tag())
268
+ const ciphertext = exactBytes(await sealed.ciphertext({ encoding: 'bytes', includeTag: false }))
269
+ return { iv, ciphertext, tag }
270
+ }
271
+ throw new TypeError(`${verb}: expected an AESSealedData`)
272
+ }
273
+
274
+ async function aesEncrypt(
275
+ plaintext: BinaryInput,
276
+ key: AESEncryptionKey,
277
+ options: AESEncryptOptions = {}
278
+ ): Promise<AESSealedData> {
279
+ const verb = 'Crypto.aesEncrypt'
280
+ const tagLength = options.tagLength ?? DEFAULT_TAG_LENGTH
281
+ assertTagLength(tagLength, verb)
282
+ let iv: Uint8Array<ArrayBuffer>
283
+ const nonce = options.nonce
284
+ if (nonce && 'bytes' in nonce && nonce.bytes !== undefined) {
285
+ iv = toBytes(nonce.bytes, verb)
286
+ if (iv.length === 0) throw new Error(`${verb}: nonce bytes must not be empty`)
287
+ } else {
288
+ const length = nonce && 'length' in nonce && nonce.length !== undefined ? nonce.length : DEFAULT_IV_LENGTH
289
+ if (!Number.isInteger(length) || length <= 0) {
290
+ throw new Error(`${verb}: nonce length must be a positive integer`)
291
+ }
292
+ iv = new Uint8Array(length)
293
+ engine.random(iv)
294
+ }
295
+ const additionalData =
296
+ options.additionalData === undefined ? undefined : toBytes(options.additionalData, verb)
297
+ const plainBytes = toBytes(plaintext, verb)
298
+ const sealed = await engine.seal(
299
+ await keyMaterial(key, verb),
300
+ iv,
301
+ plainBytes,
302
+ additionalData,
303
+ tagLength
304
+ )
305
+ // the apple engine always seals with a 16-byte tag, as expo-crypto does
306
+ // there: gcm ciphertext is plaintext-sized, so the sealed length says
307
+ // which tag the platform actually produced.
308
+ const actualTag = sealed.length - plainBytes.length
309
+ return AESSealedData.fromSeal(iv, sealed, actualTag > 0 ? actualTag : tagLength)
310
+ }
311
+
312
+ async function aesDecrypt(
313
+ sealedData: AESSealedData,
314
+ key: AESEncryptionKey,
315
+ options: AESDecryptOptions & { output: 'base64' }
316
+ ): Promise<string>
317
+ async function aesDecrypt(
318
+ sealedData: AESSealedData,
319
+ key: AESEncryptionKey,
320
+ options?: AESDecryptOptions
321
+ ): Promise<Uint8Array<ArrayBuffer>>
322
+ async function aesDecrypt(
323
+ sealedData: AESSealedData,
324
+ key: AESEncryptionKey,
325
+ options: AESDecryptOptions = {}
326
+ ): Promise<string | Uint8Array<ArrayBuffer>> {
327
+ const verb = 'Crypto.aesDecrypt'
328
+ const parts = await partsOf(sealedData, verb)
329
+ const additionalData =
330
+ options.additionalData === undefined ? undefined : toBytes(options.additionalData, verb)
331
+ const plaintext = await engine.open(
332
+ await keyMaterial(key, verb),
333
+ parts.iv,
334
+ concatBytes([parts.ciphertext, parts.tag]),
335
+ additionalData,
336
+ parts.tag.length
337
+ )
338
+ return options.output === 'base64' ? bytesToBase64(plaintext) : plaintext
339
+ }
340
+
341
+ return { AESEncryptionKey, AESSealedData, aesEncrypt, aesDecrypt }
342
+ }
343
+
344
+ export type AESEncryptionKey = InstanceType<ReturnType<typeof createAesApi>['AESEncryptionKey']>
345
+ export type AESSealedData = InstanceType<ReturnType<typeof createAesApi>['AESSealedData']>
@@ -0,0 +1,120 @@
1
+ // pure byte encodings for the crypto api: hex, base64 and utf-8 without
2
+ // depending on host globals (Hermes has no TextEncoder and atob/btoa are
3
+ // string-in/string-out with latin1 semantics, so binary hosted there would
4
+ // corrupt bytes above 127).
5
+
6
+ const HEX_DIGITS = '0123456789abcdef'
7
+
8
+ export function bytesToHex(bytes: Uint8Array): string {
9
+ let out = ''
10
+ for (const byte of bytes) out += HEX_DIGITS[byte >> 4] + HEX_DIGITS[byte & 15]
11
+ return out
12
+ }
13
+
14
+ export function hexToBytes(hex: string, verb: string): Uint8Array<ArrayBuffer> {
15
+ if (hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex)) {
16
+ throw new Error(`${verb}: expected an even-length hex string`)
17
+ }
18
+ const out = new Uint8Array(hex.length / 2)
19
+ for (let index = 0; index < out.length; index++) {
20
+ out[index] = parseInt(hex.slice(index * 2, index * 2 + 2), 16)
21
+ }
22
+ return out
23
+ }
24
+
25
+ const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
26
+ const BASE64_VALUES = new Int16Array(128).fill(-1)
27
+ for (let index = 0; index < BASE64_ALPHABET.length; index++) {
28
+ BASE64_VALUES[BASE64_ALPHABET.charCodeAt(index)] = index
29
+ }
30
+
31
+ export function bytesToBase64(bytes: Uint8Array): string {
32
+ let out = ''
33
+ for (let index = 0; index < bytes.length; index += 3) {
34
+ const first = bytes[index]
35
+ const second = bytes[index + 1]
36
+ const third = bytes[index + 2]
37
+ out += BASE64_ALPHABET[first >> 2]
38
+ out += BASE64_ALPHABET[((first & 3) << 4) | ((second ?? 0) >> 4)]
39
+ out += second === undefined ? '=' : BASE64_ALPHABET[((second & 15) << 2) | ((third ?? 0) >> 6)]
40
+ out += third === undefined ? '=' : BASE64_ALPHABET[third & 63]
41
+ }
42
+ return out
43
+ }
44
+
45
+ // strict: standard alphabet, length a multiple of four, padding only at the
46
+ // end. anything else throws instead of decoding to a best guess.
47
+ export function base64ToBytes(base64: string, verb: string): Uint8Array<ArrayBuffer> {
48
+ if (base64.length % 4 !== 0 || /[^A-Za-z0-9+/=]/.test(base64)) {
49
+ throw new Error(`${verb}: expected a base64 string`)
50
+ }
51
+ const padding = base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0
52
+ if (base64.slice(0, base64.length - padding).includes('=')) {
53
+ throw new Error(`${verb}: expected a base64 string`)
54
+ }
55
+ const out = new Uint8Array((base64.length / 4) * 3 - padding)
56
+ let at = 0
57
+ for (let index = 0; index < base64.length; index += 4) {
58
+ const values = [0, 1, 2, 3].map((offset) => {
59
+ const code = base64.charCodeAt(index + offset)
60
+ return code === 61 ? 0 : BASE64_VALUES[code]
61
+ })
62
+ if (values.some((value) => value < 0)) throw new Error(`${verb}: expected a base64 string`)
63
+ if (at < out.length) out[at++] = (values[0] << 2) | (values[1] >> 4)
64
+ if (at < out.length) out[at++] = ((values[1] & 15) << 4) | (values[2] >> 2)
65
+ if (at < out.length) out[at++] = ((values[2] & 3) << 6) | values[3]
66
+ }
67
+ return out
68
+ }
69
+
70
+ // utf-8 encoding for digestString. unpaired surrogates encode as the
71
+ // replacement character, matching TextEncoder.
72
+ export function utf8Encode(text: string): Uint8Array<ArrayBuffer> {
73
+ const bytes: number[] = []
74
+ for (let index = 0; index < text.length; index++) {
75
+ let code = text.charCodeAt(index)
76
+ if (code >= 0xd800 && code <= 0xdbff) {
77
+ const next = text.charCodeAt(index + 1)
78
+ if (next >= 0xdc00 && next <= 0xdfff) {
79
+ code = 0x10000 + ((code - 0xd800) << 10) + (next - 0xdc00)
80
+ index++
81
+ } else {
82
+ code = 0xfffd
83
+ }
84
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
85
+ code = 0xfffd
86
+ }
87
+ if (code < 0x80) {
88
+ bytes.push(code)
89
+ } else if (code < 0x800) {
90
+ bytes.push(0xc0 | (code >> 6), 0x80 | (code & 63))
91
+ } else if (code < 0x10000) {
92
+ bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 63), 0x80 | (code & 63))
93
+ } else {
94
+ bytes.push(
95
+ 0xf0 | (code >> 18),
96
+ 0x80 | ((code >> 12) & 63),
97
+ 0x80 | ((code >> 6) & 63),
98
+ 0x80 | (code & 63)
99
+ )
100
+ }
101
+ }
102
+ return new Uint8Array(bytes)
103
+ }
104
+
105
+ // a fresh exact-size copy: what native receives, so a caller mutating its
106
+ // view after the call cannot change bytes in flight.
107
+ export function exactBytes(view: Uint8Array): Uint8Array<ArrayBuffer> {
108
+ return new Uint8Array(view)
109
+ }
110
+
111
+ export function concatBytes(parts: Uint8Array[]): Uint8Array<ArrayBuffer> {
112
+ const length = parts.reduce((sum, part) => sum + part.length, 0)
113
+ const out = new Uint8Array(length)
114
+ let at = 0
115
+ for (const part of parts) {
116
+ out.set(part, at)
117
+ at += part.length
118
+ }
119
+ return out
120
+ }
@@ -0,0 +1,50 @@
1
+ import { bytesToBase64, bytesToHex, utf8Encode } from './bytes'
2
+ import { bufferSourceRange, type DigestSource } from './digest'
3
+ import type { DigestAlgorithm, DigestStringOptions } from './types'
4
+
5
+ // expo-crypto's digest contract over the shared digest source: algorithm
6
+ // names are Expo's strings, unknown ones throw at the js boundary on web and
7
+ // native alike, and digestString hashes the string's utf-8 bytes.
8
+ export const DIGEST_ALGORITHMS: readonly DigestAlgorithm[] = [
9
+ 'MD5',
10
+ 'SHA-1',
11
+ 'SHA-256',
12
+ 'SHA-384',
13
+ 'SHA-512',
14
+ ]
15
+
16
+ export function assertDigestAlgorithm(algorithm: DigestAlgorithm, verb: string): void {
17
+ if (!DIGEST_ALGORITHMS.includes(algorithm)) {
18
+ throw new Error(
19
+ `${verb}: unknown algorithm ${JSON.stringify(algorithm)}. Expected one of: ${DIGEST_ALGORITHMS.join(', ')}`
20
+ )
21
+ }
22
+ }
23
+
24
+ export function createDigestApi(source: DigestSource) {
25
+ async function digest(algorithm: DigestAlgorithm, data: BufferSource): Promise<ArrayBuffer> {
26
+ assertDigestAlgorithm(algorithm, 'Crypto.digest')
27
+ const { buffer, offset, length } = bufferSourceRange(data, 'Crypto.digest')
28
+ return source(algorithm, buffer, offset, length)
29
+ }
30
+
31
+ async function digestString(
32
+ algorithm: DigestAlgorithm,
33
+ data: string,
34
+ options: DigestStringOptions = {}
35
+ ): Promise<string> {
36
+ assertDigestAlgorithm(algorithm, 'Crypto.digestString')
37
+ if (typeof data !== 'string') {
38
+ throw new TypeError('Crypto.digestString: data must be a string')
39
+ }
40
+ const encoding = options?.encoding ?? 'hex'
41
+ if (encoding !== 'hex' && encoding !== 'base64') {
42
+ throw new Error(`Crypto.digestString: encoding must be 'hex' or 'base64'`)
43
+ }
44
+ const bytes = utf8Encode(data)
45
+ const hashed = new Uint8Array(await source(algorithm, bytes.buffer, 0, bytes.length))
46
+ return encoding === 'base64' ? bytesToBase64(hashed) : bytesToHex(hashed)
47
+ }
48
+
49
+ return { digest, digestString }
50
+ }
@@ -21,6 +21,29 @@ function namedError(name: string, message: string): Error {
21
21
  return error
22
22
  }
23
23
 
24
+ // the exact byte range a BufferSource names, or a TypeError. detached
25
+ // buffers throw from the view construction, before any native call.
26
+ export function bufferSourceRange(
27
+ data: BufferSource,
28
+ verb: string
29
+ ): { buffer: ArrayBuffer; offset: number; length: number } {
30
+ let buffer: ArrayBuffer
31
+ let offset = 0
32
+ let length: number
33
+ if (data instanceof ArrayBuffer) {
34
+ buffer = data
35
+ length = data.byteLength
36
+ } else if (ArrayBuffer.isView(data) && data.buffer instanceof ArrayBuffer) {
37
+ buffer = data.buffer
38
+ offset = data.byteOffset
39
+ length = data.byteLength
40
+ } else {
41
+ throw new TypeError(`${verb}: expected an ArrayBuffer or view.`)
42
+ }
43
+ new Uint8Array(buffer, offset, length)
44
+ return { buffer, offset, length }
45
+ }
46
+
24
47
  export function installDigestPolyfill(
25
48
  source: DigestSource,
26
49
  target: DigestTarget = globalThis
@@ -31,21 +54,7 @@ export function installDigestPolyfill(
31
54
  algorithm: AlgorithmIdentifier,
32
55
  data: BufferSource
33
56
  ): Promise<ArrayBuffer> => {
34
- let buffer: ArrayBuffer
35
- let offset = 0
36
- let length: number
37
- if (data instanceof ArrayBuffer) {
38
- buffer = data
39
- length = data.byteLength
40
- } else if (ArrayBuffer.isView(data) && data.buffer instanceof ArrayBuffer) {
41
- buffer = data.buffer
42
- offset = data.byteOffset
43
- length = data.byteLength
44
- } else {
45
- throw new TypeError('crypto.subtle.digest: expected an ArrayBuffer or view.')
46
- }
47
- // reject detached buffers before entering native code, including empty views.
48
- new Uint8Array(buffer, offset, length)
57
+ const { buffer, offset, length } = bufferSourceRange(data, 'crypto.subtle.digest')
49
58
  let name: string
50
59
  if (
51
60
  algorithm !== null &&
@@ -1,7 +1,24 @@
1
+ import { createAesApi, type AesEngine } from './aes'
1
2
  import { installDigestPolyfill } from './digest'
3
+ import { createDigestApi } from './digest-api'
2
4
  import { NitroModules } from 'react-native-nitro-modules'
3
5
  import type { OneCrypto } from '../specs/OneCrypto.nitro'
4
6
  import { installCryptoPolyfill, type RandomSource } from './random'
7
+ import type { Crypto as CryptoApi } from './types'
8
+
9
+ export type {
10
+ AESDecryptOptions,
11
+ AESEncryptOptions,
12
+ AESKeySize,
13
+ AESSealedDataConfig,
14
+ BinaryInput,
15
+ CryptoApi,
16
+ DigestAlgorithm,
17
+ DigestEncoding,
18
+ DigestStringOptions,
19
+ GCMNonceParam,
20
+ } from './types'
21
+ export type { AESEncryptionKey as AESEncryptionKeyType, AESSealedData as AESSealedDataType } from './aes'
5
22
 
6
23
  // secure random from the platform csprng (arc4random_buf on both) through the
7
24
  // OneCrypto c++ hybrid object, created on first use and cached. bytes land in
@@ -25,6 +42,57 @@ const nativeSource: RandomSource = {
25
42
  // lacks them. no-ops when the binary has no OneCrypto: installing a throwing
26
43
  // source would only replace a clear missing-crypto error with a confusing
27
44
  // one at the call site.
45
+ // the public One.Crypto api over the same hybrid object: throwing module
46
+ // access is right here, unlike installCrypto below. a caller asking for a
47
+ // digest or a seal must learn the binary predates it, not get a fake.
48
+ function required(): OneCrypto {
49
+ if (!NitroModules.hasHybridObject('OneCrypto')) {
50
+ throw new Error('Crypto needs a native build that includes one')
51
+ }
52
+ return native()
53
+ }
54
+
55
+ const aesEngine: AesEngine = {
56
+ async seal(key, iv, plaintext, additionalData, tagLength) {
57
+ const sealed = await required().aesEncrypt(
58
+ key.buffer,
59
+ iv.buffer,
60
+ plaintext.buffer,
61
+ additionalData?.buffer,
62
+ tagLength
63
+ )
64
+ return new Uint8Array(sealed)
65
+ },
66
+ async open(key, iv, ciphertextWithTag, additionalData, tagLength) {
67
+ const opened = await required().aesDecrypt(
68
+ key.buffer,
69
+ iv.buffer,
70
+ ciphertextWithTag.buffer,
71
+ additionalData?.buffer,
72
+ tagLength
73
+ )
74
+ return new Uint8Array(opened)
75
+ },
76
+ random(bytes) {
77
+ required().fillRandomBytes(bytes.buffer, bytes.byteOffset, bytes.byteLength)
78
+ },
79
+ }
80
+
81
+ const aes = createAesApi(aesEngine)
82
+ const digestApi = createDigestApi((algorithm, buffer, offset, length) =>
83
+ required().digest(algorithm, buffer, offset, length)
84
+ )
85
+
86
+ export const AESEncryptionKey = aes.AESEncryptionKey
87
+ export const AESSealedData = aes.AESSealedData
88
+
89
+ export const Crypto: CryptoApi = Object.freeze({
90
+ digest: digestApi.digest,
91
+ digestString: digestApi.digestString,
92
+ aesEncrypt: aes.aesEncrypt,
93
+ aesDecrypt: aes.aesDecrypt,
94
+ })
95
+
28
96
  export function installCrypto(): void {
29
97
  if (!NitroModules.hasHybridObject('OneCrypto')) {
30
98
  return
@@ -1,4 +1,22 @@
1
+ import { createAesApi } from './aes'
2
+ import { createDigestApi } from './digest-api'
1
3
  import { formatUuidV4, installCryptoPolyfill, type RandomSource } from './random'
4
+ import type { Crypto as CryptoApi } from './types'
5
+ import { webAesEngine, webDigestSource } from './web'
6
+
7
+ export type {
8
+ AESDecryptOptions,
9
+ AESEncryptOptions,
10
+ AESKeySize,
11
+ AESSealedDataConfig,
12
+ BinaryInput,
13
+ CryptoApi,
14
+ DigestAlgorithm,
15
+ DigestEncoding,
16
+ DigestStringOptions,
17
+ GCMNonceParam,
18
+ } from './types'
19
+ export type { AESEncryptionKey as AESEncryptionKeyType, AESSealedData as AESSealedDataType } from './aes'
2
20
 
3
21
  // web entry. same shape as the native entry, but the random source is the
4
22
  // platform crypto itself: installCrypto only fills genuinely missing
@@ -23,3 +41,16 @@ const source: RandomSource = {
23
41
  export function installCrypto(): void {
24
42
  installCryptoPolyfill(source)
25
43
  }
44
+
45
+ const aes = createAesApi(webAesEngine)
46
+ const digestApi = createDigestApi(webDigestSource)
47
+
48
+ export const AESEncryptionKey = aes.AESEncryptionKey
49
+ export const AESSealedData = aes.AESSealedData
50
+
51
+ export const Crypto: CryptoApi = Object.freeze({
52
+ digest: digestApi.digest,
53
+ digestString: digestApi.digestString,
54
+ aesEncrypt: aes.aesEncrypt,
55
+ aesDecrypt: aes.aesDecrypt,
56
+ })