chore(wasm): dual-build to support non-simd environments

This commit is contained in:
alina sireneva
2025-03-25 23:44:17 +03:00
parent cbf9cb867d
commit b92a778e64
12 changed files with 227 additions and 9 deletions

View File

@@ -16,11 +16,10 @@ import {
// we currently prefer wasm for ctr because bun mostly uses browserify polyfills for node:crypto
// which are slow AND semi-broken
// native node-api addon is broken on macos so we don't support it either
export class BunCryptoProvider extends BaseCryptoProvider implements ICryptoProvider {
async initialize(): Promise<void> {
const wasmFile = require.resolve('@mtcute/wasm/mtcute.wasm')
const wasmFile = require.resolve('@mtcute/wasm/mtcute-simd.wasm')
const wasm = await readFile(wasmFile)
initSync(wasm)
}

View File

@@ -5,7 +5,7 @@ import { createRequire } from 'node:module'
import { deflateSync, gunzipSync } from 'node:zlib'
import { BaseCryptoProvider } from '@mtcute/core/utils.js'
import { ige256Decrypt, ige256Encrypt, initSync } from '@mtcute/wasm'
import { ige256Decrypt, ige256Encrypt, initSync, SIMD_AVAILABLE } from '@mtcute/wasm'
export class NodeCryptoProvider extends BaseCryptoProvider {
createAesCtr(key: Uint8Array, iv: Uint8Array): IAesCtr {
@@ -69,7 +69,8 @@ export class NodeCryptoProvider extends BaseCryptoProvider {
async initialize(): Promise<void> {
const require = createRequire(import.meta.url)
const wasmFile = require.resolve('@mtcute/wasm/mtcute.wasm')
const file = SIMD_AVAILABLE ? 'mtcute-simd.wasm' : 'mtcute.wasm'
const wasmFile = require.resolve(`@mtcute/wasm/${file}`)
const wasm = await readFile(wasmFile)
initSync(wasm)
}

View File

@@ -14,9 +14,11 @@ export default () => {
],
preparePackageJson({ packageJson }) {
packageJson.exports['./mtcute.wasm'] = './mtcute.wasm'
packageJson.exports['./mtcute-simd.wasm'] = './mtcute-simd.wasm'
},
finalize({ packageDir, outDir }) {
fs.cpSync(resolve(packageDir, 'src/mtcute.wasm'), resolve(outDir, 'mtcute.wasm'))
fs.cpSync(resolve(packageDir, 'src/mtcute-simd.wasm'), resolve(outDir, 'mtcute-simd.wasm'))
},
}
}

View File

@@ -13,3 +13,4 @@ RUN make
FROM scratch AS binaries
COPY --from=build /src/mtcute.wasm ../
COPY --from=build /src/mtcute-simd.wasm ../

View File

@@ -20,7 +20,6 @@ CFLAGS_WASM := \
-target wasm32-unknown-unknown \
-nostdlib -ffreestanding -DFREESTANDING \
-mbulk-memory \
-msimd128 \
-Wl,--no-entry,--export-dynamic,--lto-O3
CFLAGS := $(CFLAGS_WASM) \
@@ -44,12 +43,15 @@ ifneq ($(OS),Windows_NT)
endif
endif
OUT := ../mtcute.wasm
OUT := ../src/mtcute.wasm
OUT_SIMD := ../src/mtcute-simd.wasm
$(OUT): $(SOURCES)
$(CC) $(CFLAGS) -I . -I utils -o $@ $^
$(OUT_SIMD): $(SOURCES)
$(CC) $(CFLAGS) -msimd128 -I . -I utils -o $@ $^
clean:
rm -f $(OUT)
rm -f $(OUT) $(OUT_SIMD)
all: $(OUT)
all: $(OUT) $(OUT_SIMD)

View File

@@ -9,7 +9,8 @@
"sideEffects": false,
"exports": {
".": "./src/index.ts",
"./mtcute.wasm": "./src/mtcute.wasm"
"./mtcute.wasm": "./src/mtcute.wasm",
"./mtcute-simd.wasm": "./src/mtcute-simd.wasm"
},
"scripts": {
"build:wasm": "docker build --output=lib --target=binaries lib"

View File

@@ -2,12 +2,19 @@ import type { MtcuteWasmModule, SyncInitInput } from './types.js'
export * from './types.js'
export const SIMD_AVAILABLE: boolean = /* @__PURE__ */ WebAssembly.validate(new Uint8Array(
[0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11],
))
export function getWasmUrl(): URL {
// would be nice if we could just use `new URL('@mtcute/wasm/mtcute.wasm', import.meta.url)`
// wherever this is used, but vite does some funky stuff with transitive dependencies
// making it not work. probably related to https://github.com/vitejs/vite/issues/8427,
// but asking the user to deoptimize the entire @mtcute/web is definitely not a good idea
// so we'll just use this hack for now
if (SIMD_AVAILABLE) {
return new URL(/* @vite-ignore */ './mtcute-simd.wasm', import.meta.url)
}
return new URL(/* @vite-ignore */ './mtcute.wasm', import.meta.url)
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,148 @@
import { hex } from '@fuman/utils'
import { beforeAll, describe, expect, it } from 'vitest'
import { __getWasm, createCtr256, ctr256, freeCtr256 } from '../src/index.js'
import { initWasmSimd } from './init.js'
beforeAll(async () => {
await initWasmSimd()
})
describe('aes-ctr (simd)', () => {
const key = hex.decode('603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4')
const iv = hex.decode('F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF')
describe('NIST', () => {
// https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CTR.pdf
const data = hex.decode(
`6BC1BEE2 2E409F96 E93D7E11 7393172A
AE2D8A57 1E03AC9C 9EB76FAC 45AF8E51
30C81C46 A35CE411 E5FBC119 1A0A52EF
F69F2445 DF4F9B17 AD2B417B E66C3710`.replace(/\s/g, ''),
)
const dataEnc = hex.decode(
`601EC313 775789A5 B7A7F504 BBF3D228
F443E3CA 4D62B59A CA84E990 CACAF5C5
2B0930DA A23DE94C E87017BA 2D84988D
DFC9C58D B67AADA6 13C2DD08 457941A6`.replace(/\s/g, ''),
)
it('should correctly encrypt', () => {
const ctr = createCtr256(key, iv)
const res = ctr256(ctr, data)
freeCtr256(ctr)
expect(hex.encode(res)).toEqual(hex.encode(dataEnc))
})
it('should correctly decrypt', () => {
const ctr = createCtr256(key, iv)
const res = ctr256(ctr, dataEnc)
freeCtr256(ctr)
expect(hex.encode(res)).toEqual(hex.encode(data))
})
})
describe('stream', () => {
const data = hex.decode('6BC1BEE22E409F96E93D7E117393172A')
const dataEnc1 = hex.decode('601ec313775789a5b7a7f504bbf3d228')
const dataEnc2 = hex.decode('31afd77f7d218690bd0ef82dfcf66cbe')
const dataEnc3 = hex.decode('7000927e2f2192cbe4b6a8b2441ddd48')
it('should correctly encrypt', () => {
const ctr = createCtr256(key, iv)
const res1 = ctr256(ctr, data)
const res2 = ctr256(ctr, data)
const res3 = ctr256(ctr, data)
freeCtr256(ctr)
expect(hex.encode(res1)).toEqual(hex.encode(dataEnc1))
expect(hex.encode(res2)).toEqual(hex.encode(dataEnc2))
expect(hex.encode(res3)).toEqual(hex.encode(dataEnc3))
})
it('should correctly decrypt', () => {
const ctr = createCtr256(key, iv)
const res1 = ctr256(ctr, dataEnc1)
const res2 = ctr256(ctr, dataEnc2)
const res3 = ctr256(ctr, dataEnc3)
freeCtr256(ctr)
expect(hex.encode(res1)).toEqual(hex.encode(data))
expect(hex.encode(res2)).toEqual(hex.encode(data))
expect(hex.encode(res3)).toEqual(hex.encode(data))
})
})
describe('stream (unaligned)', () => {
const data = hex.decode('6BC1BEE22E40')
const dataEnc1 = hex.decode('601ec3137757')
const dataEnc2 = hex.decode('7df2e078a555')
const dataEnc3 = hex.decode('a3a17be0742e')
const dataEnc4 = hex.decode('025ced833746')
const dataEnc5 = hex.decode('3ff238dea125')
const dataEnc6 = hex.decode('1055a52302dc')
it('should correctly encrypt', () => {
const ctr = createCtr256(key, iv)
const res1 = ctr256(ctr, data)
const res2 = ctr256(ctr, data)
const res3 = ctr256(ctr, data)
const res4 = ctr256(ctr, data)
const res5 = ctr256(ctr, data)
const res6 = ctr256(ctr, data)
freeCtr256(ctr)
expect(hex.encode(res1)).toEqual(hex.encode(dataEnc1))
expect(hex.encode(res2)).toEqual(hex.encode(dataEnc2))
expect(hex.encode(res3)).toEqual(hex.encode(dataEnc3))
expect(hex.encode(res4)).toEqual(hex.encode(dataEnc4))
expect(hex.encode(res5)).toEqual(hex.encode(dataEnc5))
expect(hex.encode(res6)).toEqual(hex.encode(dataEnc6))
})
it('should correctly decrypt', () => {
const ctr = createCtr256(key, iv)
const res1 = ctr256(ctr, dataEnc1)
const res2 = ctr256(ctr, dataEnc2)
const res3 = ctr256(ctr, dataEnc3)
const res4 = ctr256(ctr, dataEnc4)
const res5 = ctr256(ctr, dataEnc5)
const res6 = ctr256(ctr, dataEnc6)
freeCtr256(ctr)
expect(hex.encode(res1)).toEqual(hex.encode(data))
expect(hex.encode(res2)).toEqual(hex.encode(data))
expect(hex.encode(res3)).toEqual(hex.encode(data))
expect(hex.encode(res4)).toEqual(hex.encode(data))
expect(hex.encode(res5)).toEqual(hex.encode(data))
expect(hex.encode(res6)).toEqual(hex.encode(data))
})
})
it('should not leak memory', () => {
const data = hex.decode('6BC1BEE22E409F96E93D7E117393172A')
const mem = __getWasm().memory.buffer
const memSize = mem.byteLength
for (let i = 0; i < 100; i++) {
const ctrEnc = createCtr256(key, iv)
const ctrDec = createCtr256(key, iv)
for (let i = 0; i < 100; i++) {
ctr256(ctrDec, ctr256(ctrEnc, data))
}
freeCtr256(ctrEnc)
freeCtr256(ctrDec)
}
expect(mem.byteLength).toEqual(memSize)
})
})

View File

@@ -0,0 +1,41 @@
import { hex } from '@fuman/utils'
import { beforeAll, describe, expect, it } from 'vitest'
import { __getWasm, ige256Decrypt, ige256Encrypt } from '../src/index.js'
import { initWasmSimd } from './init.js'
beforeAll(async () => {
await initWasmSimd()
})
describe('aes-ige (simd)', () => {
const key = hex.decode('5468697320697320616E20696D706C655468697320697320616E20696D706C65')
const iv = hex.decode('6D656E746174696F6E206F6620494745206D6F646520666F72204F70656E5353')
const data = hex.decode('99706487a1cde613bc6de0b6f24b1c7aa448c8b9c3403e3467a8cad89340f53b')
const dataEnc = hex.decode('792ea8ae577b1a66cb3bd92679b8030ca54ee631976bd3a04547fdcb4639fa69')
it('should correctly encrypt', () => {
const aes = ige256Encrypt(data, key, iv)
expect(hex.encode(aes)).toEqual(hex.encode(dataEnc))
})
it('should correctly decrypt', () => {
const aes = ige256Decrypt(dataEnc, key, iv)
expect(hex.encode(aes)).toEqual(hex.encode(data))
})
it('should not leak memory', () => {
const mem = __getWasm().memory.buffer
const memSize = mem.byteLength
for (let i = 0; i < 10000; i++) {
ige256Decrypt(ige256Encrypt(data, key, iv), key, iv)
}
expect(mem.byteLength).toEqual(memSize)
})
})

View File

@@ -15,3 +15,19 @@ export async function initWasm(): Promise<void> {
const buffer = await blob.arrayBuffer()
initSync(buffer)
}
export async function initWasmSimd(): Promise<void> {
const url = new URL('../src/mtcute-simd.wasm', import.meta.url)
if (import.meta.env.TEST_ENV === 'node') {
const fs = await import('node:fs/promises')
const blob = await fs.readFile(url)
initSync(blob)
return
}
const blob = await fetch(url)
const buffer = await blob.arrayBuffer()
initSync(buffer)
}