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.
- package/compiler/gotest/runner.go +98 -0
- package/compiler/gotest/runner_test.go +45 -0
- package/compiler/lowering.go +4 -2
- package/compiler/protobuf-ts-binding.go +1 -1
- package/compiler/protobuf-ts-binding_test.go +109 -0
- package/compiler/skeleton_test.go +4 -1
- package/dist/gs/builtin/hostio.js +2 -2
- package/dist/gs/builtin/hostio.js.map +1 -1
- package/dist/gs/crypto/aes/index.d.ts +15 -0
- package/dist/gs/crypto/aes/index.js +57 -0
- package/dist/gs/crypto/aes/index.js.map +1 -0
- package/dist/gs/crypto/cipher/index.d.ts +41 -0
- package/dist/gs/crypto/cipher/index.js +255 -0
- package/dist/gs/crypto/cipher/index.js.map +1 -0
- package/dist/gs/golang.org/x/crypto/chacha20poly1305/index.d.ts +31 -0
- package/dist/gs/golang.org/x/crypto/chacha20poly1305/index.js +117 -0
- package/dist/gs/golang.org/x/crypto/chacha20poly1305/index.js.map +1 -0
- package/dist/gs/io/io.js +18 -2
- package/dist/gs/io/io.js.map +1 -1
- package/dist/gs/runtime/debug/index.js +2 -1
- package/dist/gs/runtime/debug/index.js.map +1 -1
- package/go.mod +2 -2
- package/go.sum +2 -0
- package/gs/builtin/hostio.test.ts +8 -3
- package/gs/builtin/hostio.ts +2 -4
- package/gs/crypto/aes/index.test.ts +120 -0
- package/gs/crypto/aes/index.ts +76 -0
- package/gs/crypto/cipher/index.ts +345 -0
- package/gs/crypto/cipher/meta.json +6 -0
- package/gs/golang.org/x/crypto/chacha20poly1305/index.test.ts +91 -0
- package/gs/golang.org/x/crypto/chacha20poly1305/index.ts +245 -0
- package/gs/io/io.test.ts +56 -1
- package/gs/io/io.ts +19 -2
- package/gs/runtime/debug/index.test.ts +32 -4
- package/gs/runtime/debug/index.ts +5 -2
- package/package.json +10 -3
package/gs/builtin/hostio.ts
CHANGED
|
@@ -344,9 +344,7 @@ function detectHostRuntime(): HostRuntime {
|
|
|
344
344
|
)
|
|
345
345
|
}
|
|
346
346
|
if (fd === 1 || fd === 2) {
|
|
347
|
-
fallbackConsoleWriter(
|
|
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('
|
|
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,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
|
+
}
|