goscript 0.2.1 → 0.2.2

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 (36) hide show
  1. package/compiler/gotest/runner.go +98 -0
  2. package/compiler/gotest/runner_test.go +45 -0
  3. package/compiler/lowering.go +4 -2
  4. package/compiler/protobuf-ts-binding.go +1 -1
  5. package/compiler/protobuf-ts-binding_test.go +109 -0
  6. package/compiler/skeleton_test.go +4 -1
  7. package/dist/gs/builtin/hostio.js +2 -2
  8. package/dist/gs/builtin/hostio.js.map +1 -1
  9. package/dist/gs/crypto/aes/index.d.ts +15 -0
  10. package/dist/gs/crypto/aes/index.js +57 -0
  11. package/dist/gs/crypto/aes/index.js.map +1 -0
  12. package/dist/gs/crypto/cipher/index.d.ts +41 -0
  13. package/dist/gs/crypto/cipher/index.js +255 -0
  14. package/dist/gs/crypto/cipher/index.js.map +1 -0
  15. package/dist/gs/golang.org/x/crypto/chacha20poly1305/index.d.ts +31 -0
  16. package/dist/gs/golang.org/x/crypto/chacha20poly1305/index.js +117 -0
  17. package/dist/gs/golang.org/x/crypto/chacha20poly1305/index.js.map +1 -0
  18. package/dist/gs/io/io.js +18 -2
  19. package/dist/gs/io/io.js.map +1 -1
  20. package/dist/gs/runtime/debug/index.js +2 -1
  21. package/dist/gs/runtime/debug/index.js.map +1 -1
  22. package/go.mod +2 -2
  23. package/go.sum +2 -0
  24. package/gs/builtin/hostio.test.ts +8 -3
  25. package/gs/builtin/hostio.ts +2 -4
  26. package/gs/crypto/aes/index.test.ts +120 -0
  27. package/gs/crypto/aes/index.ts +76 -0
  28. package/gs/crypto/cipher/index.ts +345 -0
  29. package/gs/crypto/cipher/meta.json +6 -0
  30. package/gs/golang.org/x/crypto/chacha20poly1305/index.test.ts +91 -0
  31. package/gs/golang.org/x/crypto/chacha20poly1305/index.ts +245 -0
  32. package/gs/io/io.test.ts +56 -1
  33. package/gs/io/io.ts +19 -2
  34. package/gs/runtime/debug/index.test.ts +32 -4
  35. package/gs/runtime/debug/index.ts +5 -2
  36. package/package.json +10 -3
@@ -344,9 +344,7 @@ function detectHostRuntime(): HostRuntime {
344
344
  )
345
345
  }
346
346
  if (fd === 1 || fd === 2) {
347
- fallbackConsoleWriter(fd === 2 ? 'error' : 'log')(
348
- decoder.decode(buffer),
349
- )
347
+ fallbackConsoleWriter('log')(decoder.decode(buffer))
350
348
  return buffer.length
351
349
  }
352
350
  throw new HostUnsupportedError()
@@ -381,7 +379,7 @@ function detectHostRuntime(): HostRuntime {
381
379
  )
382
380
  return
383
381
  }
384
- fallbackConsoleWriter('error')(data)
382
+ fallbackConsoleWriter('log')(data)
385
383
  }
386
384
 
387
385
  return {
@@ -0,0 +1,120 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import * as $ from '@goscript/builtin/index.js'
4
+ import * as cipher from '@goscript/crypto/cipher/index.js'
5
+
6
+ import { BlockSize, KeySizeError_Error, NewCipher } from './index.js'
7
+
8
+ describe('crypto/aes WebCrypto override', () => {
9
+ test.each([
10
+ [
11
+ 'AES-128',
12
+ '000102030405060708090a0b0c0d0e0f',
13
+ '00112233445566778899aabbccddeeff',
14
+ '69c4e0d86a7b0430d8cdb78070b4c55a',
15
+ ],
16
+ [
17
+ 'AES-192',
18
+ '000102030405060708090a0b0c0d0e0f1011121314151617',
19
+ '00112233445566778899aabbccddeeff',
20
+ 'dda97ca4864cdfe06eaf70a0ec0d7191',
21
+ ],
22
+ [
23
+ 'AES-256',
24
+ '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
25
+ '00112233445566778899aabbccddeeff',
26
+ '8ea2b7ca516745bfeafc49904b496089',
27
+ ],
28
+ ])(
29
+ 'encrypts and decrypts a raw %s block',
30
+ (_name, key, plaintext, ciphertext) => {
31
+ const [block, err] = NewCipher(hex(key))
32
+ expect(err).toBeNull()
33
+
34
+ const encrypted = new Uint8Array(BlockSize)
35
+ block!.Encrypt(encrypted, hex(plaintext))
36
+ expect(toHex(encrypted)).toBe(ciphertext)
37
+
38
+ const decrypted = new Uint8Array(BlockSize)
39
+ block!.Decrypt(decrypted, encrypted)
40
+ expect(toHex(decrypted)).toBe(plaintext)
41
+ },
42
+ )
43
+
44
+ test('seals and opens the NIST AES-256-GCM vector', async () => {
45
+ const [block, blockErr] = NewCipher(
46
+ hex('0000000000000000000000000000000000000000000000000000000000000000'),
47
+ )
48
+ expect(blockErr).toBeNull()
49
+ expect(block?.BlockSize()).toBe(BlockSize)
50
+
51
+ const [aead, aeadErr] = cipher.NewGCM(block)
52
+ expect(aeadErr).toBeNull()
53
+ expect(aead?.NonceSize()).toBe(12)
54
+ expect(aead?.Overhead()).toBe(16)
55
+
56
+ const sealed = await aead!.Seal(
57
+ null,
58
+ hex('000000000000000000000000'),
59
+ hex('00000000000000000000000000000000'),
60
+ null,
61
+ )
62
+ expect(toHex(sealed)).toBe(
63
+ 'cea7403d4d606b6e074ec5d3baf39d18d0d1c8a799996bf0265b98b5d48ab919',
64
+ )
65
+
66
+ const [opened, openErr] = await aead!.Open(
67
+ $.stringToBytes('prefix:'),
68
+ hex('000000000000000000000000'),
69
+ sealed,
70
+ null,
71
+ )
72
+ expect(openErr).toBeNull()
73
+ expect(toHex(opened)).toBe(
74
+ toHex($.stringToBytes('prefix:')) + '00000000000000000000000000000000',
75
+ )
76
+ })
77
+
78
+ test('rejects tampered ciphertext', async () => {
79
+ const [block] = NewCipher(
80
+ hex('0000000000000000000000000000000000000000000000000000000000000000'),
81
+ )
82
+ const [aead] = cipher.NewGCM(block)
83
+ const sealed = await aead!.Seal(
84
+ null,
85
+ hex('000000000000000000000000'),
86
+ $.stringToBytes('hello'),
87
+ $.stringToBytes('aad'),
88
+ )
89
+ sealed![0] ^= 1
90
+
91
+ const [opened, openErr] = await aead!.Open(
92
+ null,
93
+ hex('000000000000000000000000'),
94
+ sealed,
95
+ $.stringToBytes('aad'),
96
+ )
97
+ expect(opened).toBeNull()
98
+ expect(openErr?.Error()).toBe('cipher: message authentication failed')
99
+ })
100
+
101
+ test('rejects invalid key lengths', () => {
102
+ const [block, err] = NewCipher(new Uint8Array(31))
103
+ expect(block).toBeNull()
104
+ expect(err?.Error()).toBe(KeySizeError_Error(31))
105
+ })
106
+ })
107
+
108
+ function hex(input: string): Uint8Array {
109
+ const out = new Uint8Array(input.length / 2)
110
+ for (let idx = 0; idx < out.length; idx++) {
111
+ out[idx] = Number.parseInt(input.slice(idx * 2, idx * 2 + 2), 16)
112
+ }
113
+ return out
114
+ }
115
+
116
+ function toHex(input: Uint8Array | number[] | null): string {
117
+ return Array.from(input ?? [])
118
+ .map((value) => value.toString(16).padStart(2, '0'))
119
+ .join('')
120
+ }
@@ -0,0 +1,76 @@
1
+ import * as $ from '@goscript/builtin/index.js'
2
+ import type * as cipher from '@goscript/crypto/cipher/index.js'
3
+ import { ecb } from '@noble/ciphers/aes.js'
4
+
5
+ export const BlockSize = 16
6
+
7
+ export type KeySizeError = number
8
+
9
+ export class AESBlock implements cipher.Block {
10
+ private keyPromise: Promise<CryptoKey> | null = null
11
+
12
+ constructor(private readonly key: Uint8Array) {}
13
+
14
+ BlockSize(): number {
15
+ return BlockSize
16
+ }
17
+
18
+ Encrypt(dst: $.Bytes, src: $.Bytes): void {
19
+ const srcBytes = $.bytesToUint8Array(src)
20
+ if (srcBytes.length < BlockSize) {
21
+ $.panic('crypto/aes: input not full block')
22
+ }
23
+ if ($.len(dst) < BlockSize) {
24
+ $.panic('crypto/aes: output not full block')
25
+ }
26
+ const out = ecb(this.key, { disablePadding: true }).encrypt(
27
+ srcBytes.subarray(0, BlockSize),
28
+ )
29
+ $.copy(dst as Uint8Array, out)
30
+ }
31
+
32
+ Decrypt(dst: $.Bytes, src: $.Bytes): void {
33
+ const srcBytes = $.bytesToUint8Array(src)
34
+ if (srcBytes.length < BlockSize) {
35
+ $.panic('crypto/aes: input not full block')
36
+ }
37
+ if ($.len(dst) < BlockSize) {
38
+ $.panic('crypto/aes: output not full block')
39
+ }
40
+ const out = ecb(this.key, { disablePadding: true }).decrypt(
41
+ srcBytes.subarray(0, BlockSize),
42
+ )
43
+ $.copy(dst as Uint8Array, out)
44
+ }
45
+
46
+ async webCryptoKey(): Promise<CryptoKey> {
47
+ this.keyPromise ??= subtleCrypto().importKey(
48
+ 'raw',
49
+ this.key as unknown as BufferSource,
50
+ 'AES-GCM',
51
+ false,
52
+ ['encrypt', 'decrypt'],
53
+ )
54
+ return this.keyPromise
55
+ }
56
+ }
57
+
58
+ export function KeySizeError_Error(k: KeySizeError): string {
59
+ return `crypto/aes: invalid key size ${k}`
60
+ }
61
+
62
+ export function NewCipher(key: $.Bytes): [cipher.Block | null, $.GoError] {
63
+ const k = $.len(key)
64
+ if (k !== 16 && k !== 24 && k !== 32) {
65
+ return [null, $.newError(KeySizeError_Error(k))]
66
+ }
67
+ return [new AESBlock($.bytesToUint8Array(key).slice()), null]
68
+ }
69
+
70
+ function subtleCrypto(): SubtleCrypto {
71
+ const subtle = globalThis.crypto?.subtle
72
+ if (subtle == null) {
73
+ throw new Error('crypto/aes: WebCrypto AES-GCM is unavailable')
74
+ }
75
+ return subtle
76
+ }
@@ -0,0 +1,345 @@
1
+ import * as $ from '@goscript/builtin/index.js'
2
+
3
+ export type Block = {
4
+ BlockSize(): number
5
+ Decrypt(dst: $.Bytes, src: $.Bytes): void
6
+ Encrypt(dst: $.Bytes, src: $.Bytes): void
7
+ }
8
+
9
+ $.registerInterfaceType('cipher.Block', null, [
10
+ {
11
+ name: 'BlockSize',
12
+ args: [],
13
+ returns: [{ name: '_r0', type: { kind: $.TypeKind.Basic, name: 'int' } }],
14
+ },
15
+ {
16
+ name: 'Decrypt',
17
+ args: [
18
+ {
19
+ name: 'dst',
20
+ type: {
21
+ kind: $.TypeKind.Slice,
22
+ elemType: { kind: $.TypeKind.Basic, name: 'uint8' },
23
+ },
24
+ },
25
+ {
26
+ name: 'src',
27
+ type: {
28
+ kind: $.TypeKind.Slice,
29
+ elemType: { kind: $.TypeKind.Basic, name: 'uint8' },
30
+ },
31
+ },
32
+ ],
33
+ returns: [],
34
+ },
35
+ {
36
+ name: 'Encrypt',
37
+ args: [
38
+ {
39
+ name: 'dst',
40
+ type: {
41
+ kind: $.TypeKind.Slice,
42
+ elemType: { kind: $.TypeKind.Basic, name: 'uint8' },
43
+ },
44
+ },
45
+ {
46
+ name: 'src',
47
+ type: {
48
+ kind: $.TypeKind.Slice,
49
+ elemType: { kind: $.TypeKind.Basic, name: 'uint8' },
50
+ },
51
+ },
52
+ ],
53
+ returns: [],
54
+ },
55
+ ])
56
+
57
+ export type AEAD = {
58
+ NonceSize(): number
59
+ Open(
60
+ dst: $.Bytes,
61
+ nonce: $.Bytes,
62
+ ciphertext: $.Bytes,
63
+ additionalData: $.Bytes,
64
+ ): [$.Bytes, $.GoError] | Promise<[$.Bytes, $.GoError]>
65
+ Overhead(): number
66
+ Seal(
67
+ dst: $.Bytes,
68
+ nonce: $.Bytes,
69
+ plaintext: $.Bytes,
70
+ additionalData: $.Bytes,
71
+ ): $.Bytes | Promise<$.Bytes>
72
+ }
73
+
74
+ $.registerInterfaceType('cipher.AEAD', null, [
75
+ {
76
+ name: 'NonceSize',
77
+ args: [],
78
+ returns: [{ name: '_r0', type: { kind: $.TypeKind.Basic, name: 'int' } }],
79
+ },
80
+ {
81
+ name: 'Open',
82
+ args: [
83
+ {
84
+ name: 'dst',
85
+ type: {
86
+ kind: $.TypeKind.Slice,
87
+ elemType: { kind: $.TypeKind.Basic, name: 'uint8' },
88
+ },
89
+ },
90
+ {
91
+ name: 'nonce',
92
+ type: {
93
+ kind: $.TypeKind.Slice,
94
+ elemType: { kind: $.TypeKind.Basic, name: 'uint8' },
95
+ },
96
+ },
97
+ {
98
+ name: 'ciphertext',
99
+ type: {
100
+ kind: $.TypeKind.Slice,
101
+ elemType: { kind: $.TypeKind.Basic, name: 'uint8' },
102
+ },
103
+ },
104
+ {
105
+ name: 'additionalData',
106
+ type: {
107
+ kind: $.TypeKind.Slice,
108
+ elemType: { kind: $.TypeKind.Basic, name: 'uint8' },
109
+ },
110
+ },
111
+ ],
112
+ returns: [
113
+ {
114
+ name: '_r0',
115
+ type: {
116
+ kind: $.TypeKind.Slice,
117
+ elemType: { kind: $.TypeKind.Basic, name: 'uint8' },
118
+ },
119
+ },
120
+ { name: '_r1', type: 'error' },
121
+ ],
122
+ },
123
+ {
124
+ name: 'Overhead',
125
+ args: [],
126
+ returns: [{ name: '_r0', type: { kind: $.TypeKind.Basic, name: 'int' } }],
127
+ },
128
+ {
129
+ name: 'Seal',
130
+ args: [
131
+ {
132
+ name: 'dst',
133
+ type: {
134
+ kind: $.TypeKind.Slice,
135
+ elemType: { kind: $.TypeKind.Basic, name: 'uint8' },
136
+ },
137
+ },
138
+ {
139
+ name: 'nonce',
140
+ type: {
141
+ kind: $.TypeKind.Slice,
142
+ elemType: { kind: $.TypeKind.Basic, name: 'uint8' },
143
+ },
144
+ },
145
+ {
146
+ name: 'plaintext',
147
+ type: {
148
+ kind: $.TypeKind.Slice,
149
+ elemType: { kind: $.TypeKind.Basic, name: 'uint8' },
150
+ },
151
+ },
152
+ {
153
+ name: 'additionalData',
154
+ type: {
155
+ kind: $.TypeKind.Slice,
156
+ elemType: { kind: $.TypeKind.Basic, name: 'uint8' },
157
+ },
158
+ },
159
+ ],
160
+ returns: [
161
+ {
162
+ name: '_r0',
163
+ type: {
164
+ kind: $.TypeKind.Slice,
165
+ elemType: { kind: $.TypeKind.Basic, name: 'uint8' },
166
+ },
167
+ },
168
+ ],
169
+ },
170
+ ])
171
+
172
+ export type Stream = {
173
+ XORKeyStream(dst: $.Bytes, src: $.Bytes): void
174
+ }
175
+
176
+ export type BlockMode = {
177
+ BlockSize(): number
178
+ CryptBlocks(dst: $.Bytes, src: $.Bytes): void
179
+ }
180
+
181
+ type WebCryptoBlock = Block & {
182
+ webCryptoKey(): Promise<CryptoKey>
183
+ }
184
+
185
+ class webCryptoGCM implements AEAD {
186
+ constructor(
187
+ private readonly block: WebCryptoBlock,
188
+ private readonly nonceSize: number,
189
+ private readonly tagSize: number,
190
+ ) {}
191
+
192
+ NonceSize(): number {
193
+ return this.nonceSize
194
+ }
195
+
196
+ Overhead(): number {
197
+ return this.tagSize
198
+ }
199
+
200
+ async Seal(
201
+ dst: $.Bytes,
202
+ nonce: $.Bytes,
203
+ plaintext: $.Bytes,
204
+ additionalData: $.Bytes,
205
+ ): Promise<$.Bytes> {
206
+ if ($.len(nonce) !== this.nonceSize) {
207
+ throw new Error('crypto/cipher: incorrect nonce length given to GCM')
208
+ }
209
+ const encrypted = await globalThis.crypto.subtle.encrypt(
210
+ this.algorithm(nonce, additionalData),
211
+ await this.block.webCryptoKey(),
212
+ $.bytesToUint8Array(plaintext) as unknown as BufferSource,
213
+ )
214
+ return appendBytes(dst, new Uint8Array(encrypted))
215
+ }
216
+
217
+ async Open(
218
+ dst: $.Bytes,
219
+ nonce: $.Bytes,
220
+ ciphertext: $.Bytes,
221
+ additionalData: $.Bytes,
222
+ ): Promise<[$.Bytes, $.GoError]> {
223
+ if ($.len(nonce) !== this.nonceSize) {
224
+ throw new Error('crypto/cipher: incorrect nonce length given to GCM')
225
+ }
226
+ try {
227
+ const decrypted = await globalThis.crypto.subtle.decrypt(
228
+ this.algorithm(nonce, additionalData),
229
+ await this.block.webCryptoKey(),
230
+ $.bytesToUint8Array(ciphertext) as unknown as BufferSource,
231
+ )
232
+ return [appendBytes(dst, new Uint8Array(decrypted)), null]
233
+ } catch {
234
+ return [null, $.newError('cipher: message authentication failed')]
235
+ }
236
+ }
237
+
238
+ private algorithm(nonce: $.Bytes, additionalData: $.Bytes): AesGcmParams {
239
+ const params: AesGcmParams = {
240
+ name: 'AES-GCM',
241
+ iv: $.bytesToUint8Array(nonce) as unknown as BufferSource,
242
+ tagLength: this.tagSize * 8,
243
+ }
244
+ if ($.len(additionalData) !== 0) {
245
+ params.additionalData = $.bytesToUint8Array(
246
+ additionalData,
247
+ ) as unknown as BufferSource
248
+ }
249
+ return params
250
+ }
251
+ }
252
+
253
+ export function NewGCM(block: Block | null): [AEAD | null, $.GoError] {
254
+ return NewGCMWithNonceSize(block, 12)
255
+ }
256
+
257
+ export function NewGCMWithNonceSize(
258
+ block: Block | null,
259
+ size: number,
260
+ ): [AEAD | null, $.GoError] {
261
+ return newGCM(block, size, 16)
262
+ }
263
+
264
+ export function NewGCMWithTagSize(
265
+ block: Block | null,
266
+ tagSize: number,
267
+ ): [AEAD | null, $.GoError] {
268
+ if (tagSize < 12 || tagSize > 16) {
269
+ return [null, $.newError('crypto/cipher: incorrect GCM tag size')]
270
+ }
271
+ return newGCM(block, 12, tagSize)
272
+ }
273
+
274
+ export function NewGCMWithRandomNonce(
275
+ _block: Block | null,
276
+ ): [AEAD | null, $.GoError] {
277
+ return [
278
+ null,
279
+ $.newError(
280
+ 'crypto/cipher: NewGCMWithRandomNonce is not implemented in GoScript',
281
+ ),
282
+ ]
283
+ }
284
+
285
+ export function NewCBCDecrypter(_b: Block | null, _iv: $.Bytes): BlockMode {
286
+ throw new Error('crypto/cipher: CBC is not implemented in GoScript')
287
+ }
288
+
289
+ export function NewCBCEncrypter(_b: Block | null, _iv: $.Bytes): BlockMode {
290
+ throw new Error('crypto/cipher: CBC is not implemented in GoScript')
291
+ }
292
+
293
+ export function NewCFBDecrypter(_b: Block | null, _iv: $.Bytes): Stream {
294
+ throw new Error('crypto/cipher: CFB is not implemented in GoScript')
295
+ }
296
+
297
+ export function NewCFBEncrypter(_b: Block | null, _iv: $.Bytes): Stream {
298
+ throw new Error('crypto/cipher: CFB is not implemented in GoScript')
299
+ }
300
+
301
+ export function NewCTR(_b: Block | null, _iv: $.Bytes): Stream {
302
+ throw new Error('crypto/cipher: CTR is not implemented in GoScript')
303
+ }
304
+
305
+ export function NewOFB(_b: Block | null, _iv: $.Bytes): Stream {
306
+ throw new Error('crypto/cipher: OFB is not implemented in GoScript')
307
+ }
308
+
309
+ export class StreamReader {
310
+ constructor(_init?: Partial<{ S: Stream; R: unknown }>) {}
311
+ }
312
+
313
+ export class StreamWriter {
314
+ constructor(_init?: Partial<{ S: Stream; W: unknown }>) {}
315
+ }
316
+
317
+ function newGCM(
318
+ block: Block | null,
319
+ nonceSize: number,
320
+ tagSize: number,
321
+ ): [AEAD | null, $.GoError] {
322
+ if (block == null || block.BlockSize() !== 16) {
323
+ return [null, $.newError('cipher: NewGCM requires 128-bit block cipher')]
324
+ }
325
+ if (nonceSize <= 0) {
326
+ return [null, $.newError('crypto/cipher: incorrect GCM nonce size')]
327
+ }
328
+ if (!isWebCryptoBlock(block)) {
329
+ return [
330
+ null,
331
+ $.newError(
332
+ 'crypto/cipher: AES-GCM requires a WebCrypto AES block in GoScript',
333
+ ),
334
+ ]
335
+ }
336
+ return [new webCryptoGCM(block, nonceSize, tagSize), null]
337
+ }
338
+
339
+ function isWebCryptoBlock(block: Block): block is WebCryptoBlock {
340
+ return typeof (block as Partial<WebCryptoBlock>).webCryptoKey === 'function'
341
+ }
342
+
343
+ function appendBytes(dst: $.Bytes, bytes: Uint8Array): $.Bytes {
344
+ return $.append(dst as any, ...bytes) as $.Bytes
345
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "asyncMethods": {
3
+ "AEAD.Open": true,
4
+ "AEAD.Seal": true
5
+ }
6
+ }
@@ -0,0 +1,91 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import { New, NewX, NonceSize, NonceSizeX, Overhead } from './index.js'
4
+
5
+ describe('chacha20poly1305 override', () => {
6
+ test('matches the RFC 8439 ChaCha20-Poly1305 AEAD vector', () => {
7
+ const [aead, err] = New(
8
+ hex('808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f'),
9
+ )
10
+ expect(err).toBeNull()
11
+ expect(aead?.NonceSize()).toBe(NonceSize)
12
+ expect(aead?.Overhead()).toBe(Overhead)
13
+
14
+ const nonce = hex('070000004041424344454647')
15
+ const aad = hex('50515253c0c1c2c3c4c5c6c7')
16
+ const plaintext = hex(
17
+ '4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e',
18
+ )
19
+ const sealed = aead!.Seal(null, nonce, plaintext, aad)
20
+ expect(toHex(sealed)).toBe(
21
+ 'd31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd0600691',
22
+ )
23
+
24
+ const [opened, openErr] = aead!.Open(null, nonce, sealed, aad)
25
+ expect(openErr).toBeNull()
26
+ expect(toHex(opened)).toBe(toHex(plaintext))
27
+ })
28
+
29
+ test('rejects tampered ciphertexts', () => {
30
+ const [aead, err] = New(
31
+ hex('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'),
32
+ )
33
+ expect(err).toBeNull()
34
+ const sealed = aead!.Seal(
35
+ null,
36
+ hex('000000000000000000000002'),
37
+ hex('68656c6c6f'),
38
+ null,
39
+ )
40
+ sealed![0] ^= 1
41
+
42
+ const [opened, openErr] = aead!.Open(
43
+ null,
44
+ hex('000000000000000000000002'),
45
+ sealed,
46
+ null,
47
+ )
48
+ expect(opened).toBeNull()
49
+ expect(openErr?.Error()).toBe(
50
+ 'chacha20poly1305: message authentication failed',
51
+ )
52
+ })
53
+
54
+ test('round trips XChaCha20-Poly1305', () => {
55
+ const [aead, err] = NewX(
56
+ hex('1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0'),
57
+ )
58
+ expect(err).toBeNull()
59
+ expect(aead?.NonceSize()).toBe(NonceSizeX)
60
+ expect(aead?.Overhead()).toBe(Overhead)
61
+
62
+ const nonce = hex('000000000102030405060708090a0b0c0d0e0f1011121314')
63
+ const aad = hex('f33388860000000000004e91')
64
+ const plaintext = hex('496e7465726f70657261626c6520584368614368613230')
65
+ const sealed = aead!.Seal(null, nonce, plaintext, aad)
66
+
67
+ const [opened, openErr] = aead!.Open(null, nonce, sealed, aad)
68
+ expect(openErr).toBeNull()
69
+ expect(toHex(opened)).toBe(toHex(plaintext))
70
+ })
71
+
72
+ test('rejects invalid key length', () => {
73
+ const [aead, err] = New(new Uint8Array(31))
74
+ expect(aead).toBeNull()
75
+ expect(err?.Error()).toBe('chacha20poly1305: bad key length')
76
+ })
77
+ })
78
+
79
+ function hex(input: string): Uint8Array {
80
+ const out = new Uint8Array(input.length / 2)
81
+ for (let idx = 0; idx < out.length; idx++) {
82
+ out[idx] = Number.parseInt(input.slice(idx * 2, idx * 2 + 2), 16)
83
+ }
84
+ return out
85
+ }
86
+
87
+ function toHex(input: Uint8Array | number[] | null): string {
88
+ return Array.from(input ?? [])
89
+ .map((value) => value.toString(16).padStart(2, '0'))
90
+ .join('')
91
+ }