From c96c29acd2b46a189227f97da43707b6b0390e23 Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Tue, 15 Oct 2024 11:33:48 -0500 Subject: [PATCH 1/6] Create shared encryption module --- shared/encryption/.eslintrc.cjs | 57 +++++++++ shared/encryption/LICENSE | 21 ++++ shared/encryption/README.md | 3 + shared/encryption/package.json | 102 ++++++++++++++++ shared/encryption/rollup.config.js | 65 +++++++++++ shared/encryption/src/Ciphertext.ts | 43 +++++++ shared/encryption/src/crypto.browser.ts | 5 + shared/encryption/src/crypto.ts | 5 + shared/encryption/src/encryption.ts | 135 ++++++++++++++++++++++ shared/encryption/src/index.ts | 2 + shared/encryption/test/encryption.test.ts | 72 ++++++++++++ shared/encryption/tsconfig.json | 26 +++++ shared/encryption/vitest.config.ts | 17 +++ yarn.lock | 34 ++++++ 14 files changed, 587 insertions(+) create mode 100644 shared/encryption/.eslintrc.cjs create mode 100644 shared/encryption/LICENSE create mode 100644 shared/encryption/README.md create mode 100644 shared/encryption/package.json create mode 100644 shared/encryption/rollup.config.js create mode 100644 shared/encryption/src/Ciphertext.ts create mode 100644 shared/encryption/src/crypto.browser.ts create mode 100644 shared/encryption/src/crypto.ts create mode 100644 shared/encryption/src/encryption.ts create mode 100644 shared/encryption/src/index.ts create mode 100644 shared/encryption/test/encryption.test.ts create mode 100644 shared/encryption/tsconfig.json create mode 100644 shared/encryption/vitest.config.ts diff --git a/shared/encryption/.eslintrc.cjs b/shared/encryption/.eslintrc.cjs new file mode 100644 index 00000000..afc7f8b0 --- /dev/null +++ b/shared/encryption/.eslintrc.cjs @@ -0,0 +1,57 @@ +module.exports = { + parser: "@typescript-eslint/parser", + extends: [ + "eslint:recommended", + "standard", + "prettier", + "plugin:@typescript-eslint/recommended", + "eslint-config-prettier", + "plugin:jsdoc/recommended", + ], + parserOptions: { + sourceType: "module", + warnOnUnsupportedTypeScriptVersion: false, + project: "tsconfig.json", + }, + rules: { + "@typescript-eslint/consistent-type-exports": [ + "error", + { + fixMixedExportsWithInlineTypeSpecifier: false, + }, + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/consistent-type-imports": "error", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + ignoreRestSiblings: true, + varsIgnorePattern: "^_", + }, + ], + "prettier/prettier": "error", + "jsdoc/require-jsdoc": "off", + "jsdoc/require-description": "off", + "jsdoc/require-param": "off", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "off", + // this is necessary to ensure that the crypto library is available + // in node and the browser + "no-restricted-syntax": [ + "error", + { + selector: "ImportDeclaration[source.value=/^(node:)?crypto$/]", + message: + "Do not import directly from `crypto`, use `src/crypto/crypto` instead.", + }, + { + selector: "ImportDeclaration[source.value=/^\\.\\./]", + message: + "Relative parent imports are not allowed, use path aliases instead.", + }, + ], + }, + plugins: ["@typescript-eslint", "prettier", "jsdoc"], +}; diff --git a/shared/encryption/LICENSE b/shared/encryption/LICENSE new file mode 100644 index 00000000..11eedfd1 --- /dev/null +++ b/shared/encryption/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 XMTP (xmtp.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/shared/encryption/README.md b/shared/encryption/README.md new file mode 100644 index 00000000..fba1c0cc --- /dev/null +++ b/shared/encryption/README.md @@ -0,0 +1,3 @@ +# XMTP Encryption + +This package provides encryption and decryption for XMTP. diff --git a/shared/encryption/package.json b/shared/encryption/package.json new file mode 100644 index 00000000..49acdbdc --- /dev/null +++ b/shared/encryption/package.json @@ -0,0 +1,102 @@ +{ + "name": "@xmtp/encryption", + "version": "0.0.0", + "private": true, + "description": "XMTP encryption library", + "keywords": [ + "xmtp", + "messaging", + "web3", + "sdk", + "js", + "javascript", + "node", + "nodejs" + ], + "license": "MIT", + "author": "XMTP Labs ", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "browser": "./dist/browser/index.js", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./node": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./browser": { + "types": "./dist/index.d.ts", + "default": "./dist/browser/index.js" + } + }, + "main": "dist/index.cjs", + "module": "dist/index.js", + "browser": "dist/browser/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "yarn clean:dist && rollup -c", + "clean": "rimraf .turbo && yarn clean:dist && yarn clean:deps", + "clean:deps": "rimraf node_modules", + "clean:dist": "rimraf dist", + "lint": "eslint . --ignore-path ../../.gitignore", + "test": "yarn test:node && yarn test:browser", + "test:browser": "vitest run --environment happy-dom", + "test:cov": "vitest run --coverage", + "test:node": "vitest run", + "typecheck": "tsc" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome versions", + "last 3 firefox versions", + "last 3 safari versions" + ] + }, + "dependencies": { + "@xmtp/proto": "^3.68.0" + }, + "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.0", + "@types/node": "^20.14.10", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "@vitest/coverage-v8": "^2.1.2", + "@xmtp/rollup-plugin-resolve-extensions": "1.0.1", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-config-standard": "^17.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jsdoc": "^48.7.0", + "eslint-plugin-n": "^17.9.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-promise": "^6.4.0", + "happy-dom": "^15.7.4", + "rimraf": "^6.0.1", + "rollup": "^4.24.0", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-filesize": "^10.0.0", + "rollup-plugin-tsconfig-paths": "^1.5.2", + "typescript": "^5.6.3", + "vite": "5.4.8", + "vite-tsconfig-paths": "^5.0.1", + "vitest": "^2.1.2" + }, + "packageManager": "yarn@4.3.1", + "engines": { + "node": ">=20" + } +} diff --git a/shared/encryption/rollup.config.js b/shared/encryption/rollup.config.js new file mode 100644 index 00000000..dc184b8f --- /dev/null +++ b/shared/encryption/rollup.config.js @@ -0,0 +1,65 @@ +import terser from "@rollup/plugin-terser"; +import typescript from "@rollup/plugin-typescript"; +import { resolveExtensions } from "@xmtp/rollup-plugin-resolve-extensions"; +import { defineConfig } from "rollup"; +import { dts } from "rollup-plugin-dts"; +import filesize from "rollup-plugin-filesize"; +import tsConfigPaths from "rollup-plugin-tsconfig-paths"; + +const external = ["@xmtp/proto", "node:crypto"]; + +const plugins = [ + tsConfigPaths(), + typescript({ + declaration: false, + declarationMap: false, + }), + filesize({ + showMinifiedSize: false, + }), +]; + +export default defineConfig([ + { + input: "src/index.ts", + output: { + file: "dist/index.js", + format: "es", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.cjs", + format: "cjs", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.d.ts", + format: "es", + }, + plugins: [tsConfigPaths(), dts()], + }, + { + input: "src/index.ts", + output: { + file: "dist/browser/index.js", + format: "es", + sourcemap: true, + }, + plugins: [ + resolveExtensions({ extensions: [".browser"] }), + terser(), + ...plugins, + ], + external, + }, +]); diff --git a/shared/encryption/src/Ciphertext.ts b/shared/encryption/src/Ciphertext.ts new file mode 100644 index 00000000..293961c8 --- /dev/null +++ b/shared/encryption/src/Ciphertext.ts @@ -0,0 +1,43 @@ +import { ciphertext } from "@xmtp/proto"; + +export const AESKeySize = 32; // bytes +export const KDFSaltSize = 32; // bytes +// AES-GCM defaults from https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams +export const AESGCMNonceSize = 12; // property iv +export const AESGCMTagLength = 16; // property tagLength + +// Ciphertext packages the encrypted ciphertext with the salt and nonce used to produce it. +// salt and nonce are not secret, and should be transmitted/stored along with the encrypted ciphertext. +export default class Ciphertext implements ciphertext.Ciphertext { + aes256GcmHkdfSha256: ciphertext.Ciphertext_Aes256gcmHkdfsha256 | undefined; // eslint-disable-line camelcase + + constructor(obj: ciphertext.Ciphertext) { + if (!obj.aes256GcmHkdfSha256) { + throw new Error("invalid ciphertext"); + } + if (obj.aes256GcmHkdfSha256.payload.length < AESGCMTagLength) { + throw new Error( + `invalid ciphertext ciphertext length: ${obj.aes256GcmHkdfSha256.payload.length}`, + ); + } + if (obj.aes256GcmHkdfSha256.hkdfSalt.length !== KDFSaltSize) { + throw new Error( + `invalid ciphertext salt length: ${obj.aes256GcmHkdfSha256.hkdfSalt.length}`, + ); + } + if (obj.aes256GcmHkdfSha256.gcmNonce.length !== AESGCMNonceSize) { + throw new Error( + `invalid ciphertext nonce length: ${obj.aes256GcmHkdfSha256.gcmNonce.length}`, + ); + } + this.aes256GcmHkdfSha256 = obj.aes256GcmHkdfSha256; + } + + toBytes(): Uint8Array { + return ciphertext.Ciphertext.encode(this).finish(); + } + + static fromBytes(bytes: Uint8Array): Ciphertext { + return new Ciphertext(ciphertext.Ciphertext.decode(bytes)); + } +} diff --git a/shared/encryption/src/crypto.browser.ts b/shared/encryption/src/crypto.browser.ts new file mode 100644 index 00000000..e34bab87 --- /dev/null +++ b/shared/encryption/src/crypto.browser.ts @@ -0,0 +1,5 @@ +/*********************************************************************************************** + * DO NOT IMPORT THIS FILE DIRECTLY + ***********************************************************************************************/ +const crypto = window.crypto; +export default crypto; diff --git a/shared/encryption/src/crypto.ts b/shared/encryption/src/crypto.ts new file mode 100644 index 00000000..4a903cde --- /dev/null +++ b/shared/encryption/src/crypto.ts @@ -0,0 +1,5 @@ +// eslint-disable-next-line no-restricted-syntax +import { webcrypto } from "node:crypto"; + +const crypto = webcrypto; +export default crypto; diff --git a/shared/encryption/src/encryption.ts b/shared/encryption/src/encryption.ts new file mode 100644 index 00000000..ed3124f5 --- /dev/null +++ b/shared/encryption/src/encryption.ts @@ -0,0 +1,135 @@ +import type { ciphertext } from "@xmtp/proto"; +import Ciphertext, { AESGCMNonceSize, KDFSaltSize } from "@/Ciphertext"; +import crypto from "@/crypto"; + +const hkdfNoInfo = new Uint8Array().buffer; +const hkdfNoSalt = new Uint8Array().buffer; + +// This is a variation of https://github.com/paulmillr/noble-secp256k1/blob/main/index.ts#L1378-L1388 +// that uses `digest('SHA-256', bytes)` instead of `digest('SHA-256', bytes.buffer)` +// which seems to produce different results. +export async function sha256(bytes: Uint8Array): Promise { + return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); +} + +// symmetric authenticated encryption of plaintext using the secret; +// additionalData is used to protect un-encrypted parts of the message (header) +// in the authentication scope of the encryption. +export async function encrypt( + plain: Uint8Array, + secret: Uint8Array, + additionalData?: Uint8Array, +): Promise { + const salt = crypto.getRandomValues(new Uint8Array(KDFSaltSize)); + const nonce = crypto.getRandomValues(new Uint8Array(AESGCMNonceSize)); + const key = await hkdf(secret, salt); + const encrypted: ArrayBuffer = await crypto.subtle.encrypt( + aesGcmParams(nonce, additionalData), + key, + plain, + ); + return new Ciphertext({ + aes256GcmHkdfSha256: { + payload: new Uint8Array(encrypted), + hkdfSalt: salt, + gcmNonce: nonce, + }, + }); +} + +// symmetric authenticated decryption of the encrypted ciphertext using the secret and additionalData +export async function decrypt( + encrypted: Ciphertext | ciphertext.Ciphertext, + secret: Uint8Array, + additionalData?: Uint8Array, +): Promise { + if (!encrypted.aes256GcmHkdfSha256) { + throw new Error("invalid payload ciphertext"); + } + const key = await hkdf(secret, encrypted.aes256GcmHkdfSha256.hkdfSalt); + const decrypted: ArrayBuffer = await crypto.subtle.decrypt( + aesGcmParams(encrypted.aes256GcmHkdfSha256.gcmNonce, additionalData), + key, + encrypted.aes256GcmHkdfSha256.payload, + ); + return new Uint8Array(decrypted); +} + +// helper for building Web Crypto API encryption parameter structure +function aesGcmParams( + nonce: Uint8Array, + additionalData?: Uint8Array, +): AesGcmParams { + const spec: AesGcmParams = { + name: "AES-GCM", + iv: nonce, + }; + if (additionalData) { + spec.additionalData = additionalData; + } + return spec; +} + +// Derive AES-256-GCM key from a shared secret and salt. +// Returns crypto.CryptoKey suitable for the encrypt/decrypt API +async function hkdf(secret: Uint8Array, salt: Uint8Array): Promise { + const key = await crypto.subtle.importKey("raw", secret, "HKDF", false, [ + "deriveKey", + ]); + return crypto.subtle.deriveKey( + { name: "HKDF", hash: "SHA-256", salt, info: hkdfNoInfo }, + key, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"], + ); +} + +export async function hkdfHmacKey( + secret: Uint8Array, + info: Uint8Array, +): Promise { + const key = await crypto.subtle.importKey("raw", secret, "HKDF", false, [ + "deriveKey", + ]); + return crypto.subtle.deriveKey( + { name: "HKDF", hash: "SHA-256", salt: hkdfNoSalt, info }, + key, + { name: "HMAC", hash: "SHA-256", length: 256 }, + true, + ["sign", "verify"], + ); +} + +export async function generateHmacSignature( + secret: Uint8Array, + info: Uint8Array, + message: Uint8Array, +): Promise { + const key = await hkdfHmacKey(secret, info); + const signed = await crypto.subtle.sign("HMAC", key, message); + return new Uint8Array(signed); +} + +export async function verifyHmacSignature( + key: CryptoKey, + signature: Uint8Array, + message: Uint8Array, +): Promise { + return await crypto.subtle.verify("HMAC", key, signature, message); +} + +export async function exportHmacKey(key: CryptoKey): Promise { + const exported = await crypto.subtle.exportKey("raw", key); + return new Uint8Array(exported); +} + +export async function importHmacKey(key: Uint8Array): Promise { + return crypto.subtle.importKey( + "raw", + key, + { name: "HMAC", hash: "SHA-256", length: 256 }, + true, + ["sign", "verify"], + ); +} diff --git a/shared/encryption/src/index.ts b/shared/encryption/src/index.ts new file mode 100644 index 00000000..96556fa2 --- /dev/null +++ b/shared/encryption/src/index.ts @@ -0,0 +1,2 @@ +export { default as Ciphertext } from "./Ciphertext"; +export * from "./encryption"; diff --git a/shared/encryption/test/encryption.test.ts b/shared/encryption/test/encryption.test.ts new file mode 100644 index 00000000..8f03bf1a --- /dev/null +++ b/shared/encryption/test/encryption.test.ts @@ -0,0 +1,72 @@ +import crypto from "@/crypto"; +import { + exportHmacKey, + generateHmacSignature, + hkdfHmacKey, + importHmacKey, + verifyHmacSignature, +} from "@/encryption"; + +describe("HMAC encryption", () => { + it("generates and validates HMAC", async () => { + const secret = crypto.getRandomValues(new Uint8Array(32)); + const info = crypto.getRandomValues(new Uint8Array(32)); + const message = crypto.getRandomValues(new Uint8Array(32)); + const hmac = await generateHmacSignature(secret, info, message); + const key = await hkdfHmacKey(secret, info); + const valid = await verifyHmacSignature(key, hmac, message); + expect(valid).toBe(true); + }); + + it("generates and validates HMAC with imported key", async () => { + const secret = crypto.getRandomValues(new Uint8Array(32)); + const info = crypto.getRandomValues(new Uint8Array(32)); + const message = crypto.getRandomValues(new Uint8Array(32)); + const hmac = await generateHmacSignature(secret, info, message); + const key = await hkdfHmacKey(secret, info); + const exportedKey = await exportHmacKey(key); + const importedKey = await importHmacKey(exportedKey); + const valid = await verifyHmacSignature(importedKey, hmac, message); + expect(valid).toBe(true); + }); + + it("generates different HMAC keys with different infos", async () => { + const secret = crypto.getRandomValues(new Uint8Array(32)); + const info1 = crypto.getRandomValues(new Uint8Array(32)); + const info2 = crypto.getRandomValues(new Uint8Array(32)); + const key1 = await hkdfHmacKey(secret, info1); + const key2 = await hkdfHmacKey(secret, info2); + + expect(await exportHmacKey(key1)).not.toEqual(await exportHmacKey(key2)); + }); + + it("fails to validate HMAC with wrong message", async () => { + const secret = crypto.getRandomValues(new Uint8Array(32)); + const info = crypto.getRandomValues(new Uint8Array(32)); + const message = crypto.getRandomValues(new Uint8Array(32)); + const hmac = await generateHmacSignature(secret, info, message); + const key = await hkdfHmacKey(secret, info); + const valid = await verifyHmacSignature( + key, + hmac, + crypto.getRandomValues(new Uint8Array(32)), + ); + expect(valid).toBe(false); + }); + + it("fails to validate HMAC with wrong key", async () => { + const secret = crypto.getRandomValues(new Uint8Array(32)); + const info = crypto.getRandomValues(new Uint8Array(32)); + const message = crypto.getRandomValues(new Uint8Array(32)); + const hmac = await generateHmacSignature(secret, info, message); + const valid = await verifyHmacSignature( + await hkdfHmacKey( + crypto.getRandomValues(new Uint8Array(32)), + crypto.getRandomValues(new Uint8Array(32)), + ), + hmac, + message, + ); + expect(valid).toBe(false); + }); +}); diff --git a/shared/encryption/tsconfig.json b/shared/encryption/tsconfig.json new file mode 100644 index 00000000..f62b35ee --- /dev/null +++ b/shared/encryption/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "downlevelIteration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "lib": ["dom"], + "moduleResolution": "bundler", + "noEmit": true, + "paths": { + "@/*": ["./src/*"] + }, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "es2021", + "types": ["vitest/globals"] + }, + "include": [ + "src/**/*", + "test/**/*", + ".eslintrc.cjs", + "rollup.config.js", + "vitest.config.ts" + ] +} diff --git a/shared/encryption/vitest.config.ts b/shared/encryption/vitest.config.ts new file mode 100644 index 00000000..7f3e1cef --- /dev/null +++ b/shared/encryption/vitest.config.ts @@ -0,0 +1,17 @@ +/// +import { defineConfig, mergeConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig as defineVitestConfig } from "vitest/config"; + +// https://vitejs.dev/config/ +const viteConfig = defineConfig({ + plugins: [tsconfigPaths()], +}); + +const vitestConfig = defineVitestConfig({ + test: { + globals: true, + }, +}); + +export default mergeConfig(viteConfig, vitestConfig); diff --git a/yarn.lock b/yarn.lock index 5e894e11..9672a437 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2697,6 +2697,40 @@ __metadata: languageName: unknown linkType: soft +"@xmtp/encryption@workspace:shared/encryption": + version: 0.0.0-use.local + resolution: "@xmtp/encryption@workspace:shared/encryption" + dependencies: + "@rollup/plugin-terser": "npm:^0.4.4" + "@rollup/plugin-typescript": "npm:^12.1.0" + "@types/node": "npm:^20.14.10" + "@typescript-eslint/eslint-plugin": "npm:^7.18.0" + "@typescript-eslint/parser": "npm:^7.18.0" + "@vitest/coverage-v8": "npm:^2.1.2" + "@xmtp/proto": "npm:^3.68.0" + "@xmtp/rollup-plugin-resolve-extensions": "npm:1.0.1" + eslint: "npm:^8.57.0" + eslint-config-prettier: "npm:^9.1.0" + eslint-config-standard: "npm:^17.1.0" + eslint-plugin-import: "npm:^2.29.1" + eslint-plugin-jsdoc: "npm:^48.7.0" + eslint-plugin-n: "npm:^17.9.0" + eslint-plugin-node: "npm:^11.1.0" + eslint-plugin-prettier: "npm:^5.1.3" + eslint-plugin-promise: "npm:^6.4.0" + happy-dom: "npm:^15.7.4" + rimraf: "npm:^6.0.1" + rollup: "npm:^4.24.0" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-filesize: "npm:^10.0.0" + rollup-plugin-tsconfig-paths: "npm:^1.5.2" + typescript: "npm:^5.6.3" + vite: "npm:5.4.8" + vite-tsconfig-paths: "npm:^5.0.1" + vitest: "npm:^2.1.2" + languageName: unknown + linkType: soft + "@xmtp/node-bindings@npm:^0.0.13": version: 0.0.13 resolution: "@xmtp/node-bindings@npm:0.0.13" From 8c0a44d2270d54fe5700bcf073cadc85be6800e2 Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Tue, 15 Oct 2024 14:23:50 -0500 Subject: [PATCH 2/6] Update imports/exports --- sdks/js-sdk/bench/helpers.ts | 2 +- sdks/js-sdk/package.json | 2 + sdks/js-sdk/src/Invitation.ts | 4 +- sdks/js-sdk/src/Message.ts | 3 +- sdks/js-sdk/src/PreparedMessage.ts | 2 +- sdks/js-sdk/src/conversations/Conversation.ts | 2 +- sdks/js-sdk/src/crypto/Ciphertext.ts | 43 ------ sdks/js-sdk/src/crypto/PrivateKey.ts | 3 +- sdks/js-sdk/src/crypto/PublicKey.ts | 2 +- .../src/crypto/SignedEciesCiphertext.ts | 2 +- sdks/js-sdk/src/crypto/crypto.browser.ts | 5 - sdks/js-sdk/src/crypto/crypto.ts | 5 - sdks/js-sdk/src/crypto/ecies.ts | 2 +- sdks/js-sdk/src/crypto/encryption.ts | 135 ------------------ sdks/js-sdk/src/index.ts | 4 +- sdks/js-sdk/src/keystore/InMemoryKeystore.ts | 12 +- sdks/js-sdk/src/keystore/encryption.ts | 2 +- .../keystore/providers/NetworkKeyManager.ts | 4 +- sdks/js-sdk/test/Invitation.test.ts | 3 +- sdks/js-sdk/test/Message.test.ts | 2 +- .../test/crypto/SignedEciesCiphertext.test.ts | 2 +- sdks/js-sdk/test/crypto/encryption.test.ts | 72 ---------- sdks/js-sdk/test/crypto/index.test.ts | 3 +- .../test/keystore/InMemoryKeystore.test.ts | 12 +- .../test/keystore/conversationStores.test.ts | 2 +- sdks/js-sdk/test/keystore/encryption.test.ts | 2 +- .../persistence/EncryptedPersistence.test.ts | 2 +- .../keystore/privatePreferencesStore.test.ts | 2 +- .../providers/NetworkKeystoreProvider.test.ts | 3 +- sdks/js-sdk/test/utils/topic.test.ts | 2 +- shared/encryption/src/index.ts | 1 + yarn.lock | 45 +++++- 32 files changed, 81 insertions(+), 306 deletions(-) delete mode 100644 sdks/js-sdk/src/crypto/Ciphertext.ts delete mode 100644 sdks/js-sdk/src/crypto/crypto.browser.ts delete mode 100644 sdks/js-sdk/src/crypto/crypto.ts delete mode 100644 sdks/js-sdk/src/crypto/encryption.ts delete mode 100644 sdks/js-sdk/test/crypto/encryption.test.ts diff --git a/sdks/js-sdk/bench/helpers.ts b/sdks/js-sdk/bench/helpers.ts index 1e673da5..4621c773 100644 --- a/sdks/js-sdk/bench/helpers.ts +++ b/sdks/js-sdk/bench/helpers.ts @@ -1,7 +1,7 @@ +import { crypto } from "@xmtp/encryption"; import type Benchmark from "benchmark"; import { cycle, save, suite } from "benny"; import type { Config } from "benny/lib/internal/common-types"; -import crypto from "@/crypto/crypto"; import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; import { newWallet } from "@test/helpers"; diff --git a/sdks/js-sdk/package.json b/sdks/js-sdk/package.json index 594ec2fa..2339988d 100644 --- a/sdks/js-sdk/package.json +++ b/sdks/js-sdk/package.json @@ -98,6 +98,7 @@ "@xmtp/consent-proof-signature": "^0.1.3", "@xmtp/content-type-primitives": "^1.0.1", "@xmtp/content-type-text": "^1.0.0", + "@xmtp/encryption": "workspace:*", "@xmtp/proto": "^3.68.0", "@xmtp/user-preferences-bindings-wasm": "^0.3.6", "async-mutex": "^0.5.0", @@ -108,6 +109,7 @@ "devDependencies": { "@metamask/providers": "^17.1.1", "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.0", "@types/benchmark": "^2.1.5", diff --git a/sdks/js-sdk/src/Invitation.ts b/sdks/js-sdk/src/Invitation.ts index 7fa85442..2bbf6772 100644 --- a/sdks/js-sdk/src/Invitation.ts +++ b/sdks/js-sdk/src/Invitation.ts @@ -1,10 +1,8 @@ +import { Ciphertext, crypto, decrypt, encrypt } from "@xmtp/encryption"; import { invitation, type messageApi } from "@xmtp/proto"; import Long from "long"; import { dateToNs } from "@/utils/date"; import { buildDirectMessageTopicV2 } from "@/utils/topic"; -import Ciphertext from "./crypto/Ciphertext"; -import crypto from "./crypto/crypto"; -import { decrypt, encrypt } from "./crypto/encryption"; import type { PrivateKeyBundleV2 } from "./crypto/PrivateKeyBundle"; import { SignedPublicKeyBundle } from "./crypto/PublicKeyBundle"; diff --git a/sdks/js-sdk/src/Message.ts b/sdks/js-sdk/src/Message.ts index 8fe8d1e6..cd14d58a 100644 --- a/sdks/js-sdk/src/Message.ts +++ b/sdks/js-sdk/src/Message.ts @@ -1,4 +1,5 @@ import type { ContentTypeId } from "@xmtp/content-type-primitives"; +import { Ciphertext, sha256 } from "@xmtp/encryption"; import { message as proto, type conversationReference } from "@xmtp/proto"; import Long from "long"; import { PublicKey } from "@/crypto/PublicKey"; @@ -9,8 +10,6 @@ import { ConversationV2, type Conversation, } from "./conversations/Conversation"; -import Ciphertext from "./crypto/Ciphertext"; -import { sha256 } from "./crypto/encryption"; import { bytesToHex } from "./crypto/utils"; import type { KeystoreInterfaces } from "./keystore/rpcDefinitions"; import { dateToNs, nsToDate } from "./utils/date"; diff --git a/sdks/js-sdk/src/PreparedMessage.ts b/sdks/js-sdk/src/PreparedMessage.ts index fb614e0b..ea020fed 100644 --- a/sdks/js-sdk/src/PreparedMessage.ts +++ b/sdks/js-sdk/src/PreparedMessage.ts @@ -1,5 +1,5 @@ +import { sha256 } from "@xmtp/encryption"; import type { Envelope } from "@xmtp/proto/ts/dist/types/message_api/v1/message_api.pb"; -import { sha256 } from "./crypto/encryption"; import { bytesToHex } from "./crypto/utils"; import type { DecodedMessage } from "./Message"; diff --git a/sdks/js-sdk/src/conversations/Conversation.ts b/sdks/js-sdk/src/conversations/Conversation.ts index b51cbd9f..6d7af2f1 100644 --- a/sdks/js-sdk/src/conversations/Conversation.ts +++ b/sdks/js-sdk/src/conversations/Conversation.ts @@ -1,4 +1,5 @@ import { ContentTypeText } from "@xmtp/content-type-text"; +import { sha256 } from "@xmtp/encryption"; import { message, content as proto, @@ -15,7 +16,6 @@ import type { } from "@/Client"; import type Client from "@/Client"; import type { ConsentState } from "@/Contacts"; -import { sha256 } from "@/crypto/encryption"; import { SignedPublicKey } from "@/crypto/PublicKey"; import { PublicKeyBundle, diff --git a/sdks/js-sdk/src/crypto/Ciphertext.ts b/sdks/js-sdk/src/crypto/Ciphertext.ts deleted file mode 100644 index 293961c8..00000000 --- a/sdks/js-sdk/src/crypto/Ciphertext.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ciphertext } from "@xmtp/proto"; - -export const AESKeySize = 32; // bytes -export const KDFSaltSize = 32; // bytes -// AES-GCM defaults from https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams -export const AESGCMNonceSize = 12; // property iv -export const AESGCMTagLength = 16; // property tagLength - -// Ciphertext packages the encrypted ciphertext with the salt and nonce used to produce it. -// salt and nonce are not secret, and should be transmitted/stored along with the encrypted ciphertext. -export default class Ciphertext implements ciphertext.Ciphertext { - aes256GcmHkdfSha256: ciphertext.Ciphertext_Aes256gcmHkdfsha256 | undefined; // eslint-disable-line camelcase - - constructor(obj: ciphertext.Ciphertext) { - if (!obj.aes256GcmHkdfSha256) { - throw new Error("invalid ciphertext"); - } - if (obj.aes256GcmHkdfSha256.payload.length < AESGCMTagLength) { - throw new Error( - `invalid ciphertext ciphertext length: ${obj.aes256GcmHkdfSha256.payload.length}`, - ); - } - if (obj.aes256GcmHkdfSha256.hkdfSalt.length !== KDFSaltSize) { - throw new Error( - `invalid ciphertext salt length: ${obj.aes256GcmHkdfSha256.hkdfSalt.length}`, - ); - } - if (obj.aes256GcmHkdfSha256.gcmNonce.length !== AESGCMNonceSize) { - throw new Error( - `invalid ciphertext nonce length: ${obj.aes256GcmHkdfSha256.gcmNonce.length}`, - ); - } - this.aes256GcmHkdfSha256 = obj.aes256GcmHkdfSha256; - } - - toBytes(): Uint8Array { - return ciphertext.Ciphertext.encode(this).finish(); - } - - static fromBytes(bytes: Uint8Array): Ciphertext { - return new Ciphertext(ciphertext.Ciphertext.decode(bytes)); - } -} diff --git a/sdks/js-sdk/src/crypto/PrivateKey.ts b/sdks/js-sdk/src/crypto/PrivateKey.ts index 1e9a6f5c..95b6bcb5 100644 --- a/sdks/js-sdk/src/crypto/PrivateKey.ts +++ b/sdks/js-sdk/src/crypto/PrivateKey.ts @@ -1,8 +1,7 @@ import * as secp from "@noble/secp256k1"; +import { decrypt, encrypt, sha256, type Ciphertext } from "@xmtp/encryption"; import { privateKey } from "@xmtp/proto"; import Long from "long"; -import type Ciphertext from "./Ciphertext"; -import { decrypt, encrypt, sha256 } from "./encryption"; import { PublicKey, SignedPublicKey, UnsignedPublicKey } from "./PublicKey"; import Signature, { ecdsaSignerKey, diff --git a/sdks/js-sdk/src/crypto/PublicKey.ts b/sdks/js-sdk/src/crypto/PublicKey.ts index 18fbdc59..78d94bf6 100644 --- a/sdks/js-sdk/src/crypto/PublicKey.ts +++ b/sdks/js-sdk/src/crypto/PublicKey.ts @@ -1,9 +1,9 @@ import * as secp from "@noble/secp256k1"; +import { sha256 } from "@xmtp/encryption"; import { publicKey } from "@xmtp/proto"; import Long from "long"; import { hashMessage, hexToBytes, type Hex } from "viem"; import type { Signer } from "@/types/Signer"; -import { sha256 } from "./encryption"; import Signature, { WalletSigner } from "./Signature"; import { computeAddress, equalBytes, splitSignature } from "./utils"; diff --git a/sdks/js-sdk/src/crypto/SignedEciesCiphertext.ts b/sdks/js-sdk/src/crypto/SignedEciesCiphertext.ts index b4dbd033..c195a87d 100644 --- a/sdks/js-sdk/src/crypto/SignedEciesCiphertext.ts +++ b/sdks/js-sdk/src/crypto/SignedEciesCiphertext.ts @@ -1,5 +1,5 @@ +import { sha256 } from "@xmtp/encryption"; import { ciphertext } from "@xmtp/proto"; -import { sha256 } from "./encryption"; import type { PrivateKey, SignedPrivateKey } from "./PrivateKey"; import type { PublicKey, SignedPublicKey } from "./PublicKey"; import Signature from "./Signature"; diff --git a/sdks/js-sdk/src/crypto/crypto.browser.ts b/sdks/js-sdk/src/crypto/crypto.browser.ts deleted file mode 100644 index e34bab87..00000000 --- a/sdks/js-sdk/src/crypto/crypto.browser.ts +++ /dev/null @@ -1,5 +0,0 @@ -/*********************************************************************************************** - * DO NOT IMPORT THIS FILE DIRECTLY - ***********************************************************************************************/ -const crypto = window.crypto; -export default crypto; diff --git a/sdks/js-sdk/src/crypto/crypto.ts b/sdks/js-sdk/src/crypto/crypto.ts deleted file mode 100644 index 38329179..00000000 --- a/sdks/js-sdk/src/crypto/crypto.ts +++ /dev/null @@ -1,5 +0,0 @@ -// eslint-disable-next-line no-restricted-syntax -import { webcrypto } from "crypto"; - -const crypto = webcrypto; -export default crypto; diff --git a/sdks/js-sdk/src/crypto/ecies.ts b/sdks/js-sdk/src/crypto/ecies.ts index 6e3ce332..0df1ea45 100644 --- a/sdks/js-sdk/src/crypto/ecies.ts +++ b/sdks/js-sdk/src/crypto/ecies.ts @@ -3,8 +3,8 @@ * `elliptic` is a CommonJS module and has issues with named imports * DO NOT CHANGE THIS TO A NAMED IMPORT */ +import { crypto } from "@xmtp/encryption"; import elliptic from "elliptic"; -import crypto from "./crypto"; const EC = elliptic.ec; const ec = new EC("secp256k1"); diff --git a/sdks/js-sdk/src/crypto/encryption.ts b/sdks/js-sdk/src/crypto/encryption.ts deleted file mode 100644 index 3574b4f2..00000000 --- a/sdks/js-sdk/src/crypto/encryption.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { ciphertext } from "@xmtp/proto"; -import Ciphertext, { AESGCMNonceSize, KDFSaltSize } from "./Ciphertext"; -import crypto from "./crypto"; - -const hkdfNoInfo = new Uint8Array().buffer; -const hkdfNoSalt = new Uint8Array().buffer; - -// This is a variation of https://github.com/paulmillr/noble-secp256k1/blob/main/index.ts#L1378-L1388 -// that uses `digest('SHA-256', bytes)` instead of `digest('SHA-256', bytes.buffer)` -// which seems to produce different results. -export async function sha256(bytes: Uint8Array): Promise { - return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); -} - -// symmetric authenticated encryption of plaintext using the secret; -// additionalData is used to protect un-encrypted parts of the message (header) -// in the authentication scope of the encryption. -export async function encrypt( - plain: Uint8Array, - secret: Uint8Array, - additionalData?: Uint8Array, -): Promise { - const salt = crypto.getRandomValues(new Uint8Array(KDFSaltSize)); - const nonce = crypto.getRandomValues(new Uint8Array(AESGCMNonceSize)); - const key = await hkdf(secret, salt); - const encrypted: ArrayBuffer = await crypto.subtle.encrypt( - aesGcmParams(nonce, additionalData), - key, - plain, - ); - return new Ciphertext({ - aes256GcmHkdfSha256: { - payload: new Uint8Array(encrypted), - hkdfSalt: salt, - gcmNonce: nonce, - }, - }); -} - -// symmetric authenticated decryption of the encrypted ciphertext using the secret and additionalData -export async function decrypt( - encrypted: Ciphertext | ciphertext.Ciphertext, - secret: Uint8Array, - additionalData?: Uint8Array, -): Promise { - if (!encrypted.aes256GcmHkdfSha256) { - throw new Error("invalid payload ciphertext"); - } - const key = await hkdf(secret, encrypted.aes256GcmHkdfSha256.hkdfSalt); - const decrypted: ArrayBuffer = await crypto.subtle.decrypt( - aesGcmParams(encrypted.aes256GcmHkdfSha256.gcmNonce, additionalData), - key, - encrypted.aes256GcmHkdfSha256.payload, - ); - return new Uint8Array(decrypted); -} - -// helper for building Web Crypto API encryption parameter structure -function aesGcmParams( - nonce: Uint8Array, - additionalData?: Uint8Array, -): AesGcmParams { - const spec: AesGcmParams = { - name: "AES-GCM", - iv: nonce, - }; - if (additionalData) { - spec.additionalData = additionalData; - } - return spec; -} - -// Derive AES-256-GCM key from a shared secret and salt. -// Returns crypto.CryptoKey suitable for the encrypt/decrypt API -async function hkdf(secret: Uint8Array, salt: Uint8Array): Promise { - const key = await crypto.subtle.importKey("raw", secret, "HKDF", false, [ - "deriveKey", - ]); - return crypto.subtle.deriveKey( - { name: "HKDF", hash: "SHA-256", salt, info: hkdfNoInfo }, - key, - { name: "AES-GCM", length: 256 }, - false, - ["encrypt", "decrypt"], - ); -} - -export async function hkdfHmacKey( - secret: Uint8Array, - info: Uint8Array, -): Promise { - const key = await crypto.subtle.importKey("raw", secret, "HKDF", false, [ - "deriveKey", - ]); - return crypto.subtle.deriveKey( - { name: "HKDF", hash: "SHA-256", salt: hkdfNoSalt, info }, - key, - { name: "HMAC", hash: "SHA-256", length: 256 }, - true, - ["sign", "verify"], - ); -} - -export async function generateHmacSignature( - secret: Uint8Array, - info: Uint8Array, - message: Uint8Array, -): Promise { - const key = await hkdfHmacKey(secret, info); - const signed = await crypto.subtle.sign("HMAC", key, message); - return new Uint8Array(signed); -} - -export async function verifyHmacSignature( - key: CryptoKey, - signature: Uint8Array, - message: Uint8Array, -): Promise { - return await crypto.subtle.verify("HMAC", key, signature, message); -} - -export async function exportHmacKey(key: CryptoKey): Promise { - const exported = await crypto.subtle.exportKey("raw", key); - return new Uint8Array(exported); -} - -export async function importHmacKey(key: Uint8Array): Promise { - return crypto.subtle.importKey( - "raw", - key, - { name: "HMAC", hash: "SHA-256", length: 256 }, - true, - ["sign", "verify"], - ); -} diff --git a/sdks/js-sdk/src/index.ts b/sdks/js-sdk/src/index.ts index 17a763eb..68ad609a 100644 --- a/sdks/js-sdk/src/index.ts +++ b/sdks/js-sdk/src/index.ts @@ -6,7 +6,7 @@ export { PrivateKeyBundleV1, PrivateKeyBundleV2, } from "./crypto/PrivateKeyBundle"; -export { default as Ciphertext } from "./crypto/Ciphertext"; +export { Ciphertext } from "@xmtp/encryption"; export { PublicKey, SignedPublicKey } from "./crypto/PublicKey"; export { PublicKeyBundle, @@ -21,7 +21,7 @@ export { hkdfHmacKey, importHmacKey, verifyHmacSignature, -} from "./crypto/encryption"; +} from "@xmtp/encryption"; export { default as Stream } from "./Stream"; export type { Signer } from "./types/Signer"; export type { diff --git a/sdks/js-sdk/src/keystore/InMemoryKeystore.ts b/sdks/js-sdk/src/keystore/InMemoryKeystore.ts index d56d8fb0..155529d1 100644 --- a/sdks/js-sdk/src/keystore/InMemoryKeystore.ts +++ b/sdks/js-sdk/src/keystore/InMemoryKeystore.ts @@ -1,3 +1,9 @@ +import { + crypto, + exportHmacKey, + generateHmacSignature, + hkdfHmacKey, +} from "@xmtp/encryption"; import { keystore, privatePreferences, @@ -8,13 +14,7 @@ import { import Long from "long"; import type { PublishParams } from "@/ApiClient"; import LocalAuthenticator from "@/authn/LocalAuthenticator"; -import crypto from "@/crypto/crypto"; import { hmacSha256Sign } from "@/crypto/ecies"; -import { - exportHmacKey, - generateHmacSignature, - hkdfHmacKey, -} from "@/crypto/encryption"; import type { PrivateKey } from "@/crypto/PrivateKey"; import { PrivateKeyBundleV2, diff --git a/sdks/js-sdk/src/keystore/encryption.ts b/sdks/js-sdk/src/keystore/encryption.ts index dbcd747d..434fccf6 100644 --- a/sdks/js-sdk/src/keystore/encryption.ts +++ b/sdks/js-sdk/src/keystore/encryption.ts @@ -1,5 +1,5 @@ +import { decrypt, encrypt } from "@xmtp/encryption"; import type { ciphertext } from "@xmtp/proto"; -import { decrypt, encrypt } from "@/crypto/encryption"; import type { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; import type { PublicKeyBundle } from "@/crypto/PublicKeyBundle"; diff --git a/sdks/js-sdk/src/keystore/providers/NetworkKeyManager.ts b/sdks/js-sdk/src/keystore/providers/NetworkKeyManager.ts index c8242000..a84f72db 100644 --- a/sdks/js-sdk/src/keystore/providers/NetworkKeyManager.ts +++ b/sdks/js-sdk/src/keystore/providers/NetworkKeyManager.ts @@ -1,10 +1,8 @@ +import { Ciphertext, crypto, decrypt, encrypt } from "@xmtp/encryption"; import { privateKey as proto } from "@xmtp/proto"; import { getAddress, hexToBytes, verifyMessage, type Hex } from "viem"; import LocalAuthenticator from "@/authn/LocalAuthenticator"; import type { PreEventCallback } from "@/Client"; -import Ciphertext from "@/crypto/Ciphertext"; -import crypto from "@/crypto/crypto"; -import { decrypt, encrypt } from "@/crypto/encryption"; import { decodePrivateKeyBundle, PrivateKeyBundleV1, diff --git a/sdks/js-sdk/test/Invitation.test.ts b/sdks/js-sdk/test/Invitation.test.ts index 03bfb905..d19ee02d 100644 --- a/sdks/js-sdk/test/Invitation.test.ts +++ b/sdks/js-sdk/test/Invitation.test.ts @@ -1,6 +1,5 @@ +import { Ciphertext, crypto } from "@xmtp/encryption"; import Long from "long"; -import Ciphertext from "@/crypto/Ciphertext"; -import crypto from "@/crypto/crypto"; import { NoMatchingPreKeyError } from "@/crypto/errors"; import { PrivateKeyBundleV2 } from "@/crypto/PrivateKeyBundle"; import { diff --git a/sdks/js-sdk/test/Message.test.ts b/sdks/js-sdk/test/Message.test.ts index 79f27fa2..48acf4f5 100644 --- a/sdks/js-sdk/test/Message.test.ts +++ b/sdks/js-sdk/test/Message.test.ts @@ -1,11 +1,11 @@ import { ContentTypeText } from "@xmtp/content-type-text"; +import { sha256 } from "@xmtp/encryption"; import type { Wallet } from "ethers"; import { createWalletClient, http } from "viem"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import { mainnet } from "viem/chains"; import Client from "@/Client"; import { ConversationV1 } from "@/conversations/Conversation"; -import { sha256 } from "@/crypto/encryption"; import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; import { bytesToHex, equalBytes } from "@/crypto/utils"; import { KeystoreError } from "@/keystore/errors"; diff --git a/sdks/js-sdk/test/crypto/SignedEciesCiphertext.test.ts b/sdks/js-sdk/test/crypto/SignedEciesCiphertext.test.ts index 0ddd84be..a9999a33 100644 --- a/sdks/js-sdk/test/crypto/SignedEciesCiphertext.test.ts +++ b/sdks/js-sdk/test/crypto/SignedEciesCiphertext.test.ts @@ -1,4 +1,4 @@ -import crypto from "@/crypto/crypto"; +import { crypto } from "@xmtp/encryption"; import { encrypt, getPublic } from "@/crypto/ecies"; import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; import SignedEciesCiphertext from "@/crypto/SignedEciesCiphertext"; diff --git a/sdks/js-sdk/test/crypto/encryption.test.ts b/sdks/js-sdk/test/crypto/encryption.test.ts deleted file mode 100644 index b282de00..00000000 --- a/sdks/js-sdk/test/crypto/encryption.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import crypto from "@/crypto/crypto"; -import { - exportHmacKey, - generateHmacSignature, - hkdfHmacKey, - importHmacKey, - verifyHmacSignature, -} from "@/crypto/encryption"; - -describe("HMAC encryption", () => { - it("generates and validates HMAC", async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)); - const info = crypto.getRandomValues(new Uint8Array(32)); - const message = crypto.getRandomValues(new Uint8Array(32)); - const hmac = await generateHmacSignature(secret, info, message); - const key = await hkdfHmacKey(secret, info); - const valid = await verifyHmacSignature(key, hmac, message); - expect(valid).toBe(true); - }); - - it("generates and validates HMAC with imported key", async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)); - const info = crypto.getRandomValues(new Uint8Array(32)); - const message = crypto.getRandomValues(new Uint8Array(32)); - const hmac = await generateHmacSignature(secret, info, message); - const key = await hkdfHmacKey(secret, info); - const exportedKey = await exportHmacKey(key); - const importedKey = await importHmacKey(exportedKey); - const valid = await verifyHmacSignature(importedKey, hmac, message); - expect(valid).toBe(true); - }); - - it("generates different HMAC keys with different infos", async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)); - const info1 = crypto.getRandomValues(new Uint8Array(32)); - const info2 = crypto.getRandomValues(new Uint8Array(32)); - const key1 = await hkdfHmacKey(secret, info1); - const key2 = await hkdfHmacKey(secret, info2); - - expect(await exportHmacKey(key1)).not.toEqual(await exportHmacKey(key2)); - }); - - it("fails to validate HMAC with wrong message", async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)); - const info = crypto.getRandomValues(new Uint8Array(32)); - const message = crypto.getRandomValues(new Uint8Array(32)); - const hmac = await generateHmacSignature(secret, info, message); - const key = await hkdfHmacKey(secret, info); - const valid = await verifyHmacSignature( - key, - hmac, - crypto.getRandomValues(new Uint8Array(32)), - ); - expect(valid).toBe(false); - }); - - it("fails to validate HMAC with wrong key", async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)); - const info = crypto.getRandomValues(new Uint8Array(32)); - const message = crypto.getRandomValues(new Uint8Array(32)); - const hmac = await generateHmacSignature(secret, info, message); - const valid = await verifyHmacSignature( - await hkdfHmacKey( - crypto.getRandomValues(new Uint8Array(32)), - crypto.getRandomValues(new Uint8Array(32)), - ), - hmac, - message, - ); - expect(valid).toBe(false); - }); -}); diff --git a/sdks/js-sdk/test/crypto/index.test.ts b/sdks/js-sdk/test/crypto/index.test.ts index abfdce06..08cfaf73 100644 --- a/sdks/js-sdk/test/crypto/index.test.ts +++ b/sdks/js-sdk/test/crypto/index.test.ts @@ -1,6 +1,5 @@ +import { crypto, decrypt, encrypt } from "@xmtp/encryption"; import { assert } from "vitest"; -import crypto from "@/crypto/crypto"; -import { decrypt, encrypt } from "@/crypto/encryption"; import { PrivateKey } from "@/crypto/PrivateKey"; import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; import { PublicKeyBundle } from "@/crypto/PublicKeyBundle"; diff --git a/sdks/js-sdk/test/keystore/InMemoryKeystore.test.ts b/sdks/js-sdk/test/keystore/InMemoryKeystore.test.ts index e1be27ce..c7b422e5 100644 --- a/sdks/js-sdk/test/keystore/InMemoryKeystore.test.ts +++ b/sdks/js-sdk/test/keystore/InMemoryKeystore.test.ts @@ -1,15 +1,15 @@ +import { + generateHmacSignature, + hkdfHmacKey, + importHmacKey, + verifyHmacSignature, +} from "@xmtp/encryption"; import { keystore, privateKey } from "@xmtp/proto"; import type { CreateInviteResponse } from "@xmtp/proto/ts/dist/types/keystore_api/v1/keystore.pb"; import Long from "long"; import { toBytes } from "viem"; import { assert } from "vitest"; import Token from "@/authn/Token"; -import { - generateHmacSignature, - hkdfHmacKey, - importHmacKey, - verifyHmacSignature, -} from "@/crypto/encryption"; import { PrivateKeyBundleV1, PrivateKeyBundleV2, diff --git a/sdks/js-sdk/test/keystore/conversationStores.test.ts b/sdks/js-sdk/test/keystore/conversationStores.test.ts index 1283bf2f..4503fc5f 100644 --- a/sdks/js-sdk/test/keystore/conversationStores.test.ts +++ b/sdks/js-sdk/test/keystore/conversationStores.test.ts @@ -1,4 +1,4 @@ -import crypto from "@/crypto/crypto"; +import { crypto } from "@xmtp/encryption"; import { V1Store, V2Store, diff --git a/sdks/js-sdk/test/keystore/encryption.test.ts b/sdks/js-sdk/test/keystore/encryption.test.ts index a179b8da..07a39445 100644 --- a/sdks/js-sdk/test/keystore/encryption.test.ts +++ b/sdks/js-sdk/test/keystore/encryption.test.ts @@ -1,5 +1,5 @@ +import { Ciphertext } from "@xmtp/encryption"; import { Wallet } from "ethers"; -import Ciphertext from "@/crypto/Ciphertext"; import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; import { equalBytes } from "@/crypto/utils"; import { decryptV1, encryptV1 } from "@/keystore/encryption"; diff --git a/sdks/js-sdk/test/keystore/persistence/EncryptedPersistence.test.ts b/sdks/js-sdk/test/keystore/persistence/EncryptedPersistence.test.ts index a98b2ec7..eece0aa0 100644 --- a/sdks/js-sdk/test/keystore/persistence/EncryptedPersistence.test.ts +++ b/sdks/js-sdk/test/keystore/persistence/EncryptedPersistence.test.ts @@ -1,4 +1,4 @@ -import crypto from "@/crypto/crypto"; +import { crypto } from "@xmtp/encryption"; import type { PrivateKey } from "@/crypto/PrivateKey"; import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; import SignedEciesCiphertext from "@/crypto/SignedEciesCiphertext"; diff --git a/sdks/js-sdk/test/keystore/privatePreferencesStore.test.ts b/sdks/js-sdk/test/keystore/privatePreferencesStore.test.ts index b5c1400f..7b944126 100644 --- a/sdks/js-sdk/test/keystore/privatePreferencesStore.test.ts +++ b/sdks/js-sdk/test/keystore/privatePreferencesStore.test.ts @@ -1,5 +1,5 @@ +import { crypto } from "@xmtp/encryption"; import type { PrivatePreferencesAction } from "@xmtp/proto/ts/dist/types/message_contents/private_preferences.pb"; -import crypto from "@/crypto/crypto"; import InMemoryPersistence from "@/keystore/persistence/InMemoryPersistence"; import { PrivatePreferencesStore } from "@/keystore/privatePreferencesStore"; diff --git a/sdks/js-sdk/test/keystore/providers/NetworkKeystoreProvider.test.ts b/sdks/js-sdk/test/keystore/providers/NetworkKeystoreProvider.test.ts index 0c1b9465..666bb6a1 100644 --- a/sdks/js-sdk/test/keystore/providers/NetworkKeystoreProvider.test.ts +++ b/sdks/js-sdk/test/keystore/providers/NetworkKeystoreProvider.test.ts @@ -1,10 +1,9 @@ +import { crypto, encrypt } from "@xmtp/encryption"; import { privateKey } from "@xmtp/proto"; import { hexToBytes, type Hex } from "viem"; import { vi } from "vitest"; import ApiClient, { ApiUrls } from "@/ApiClient"; import LocalAuthenticator from "@/authn/LocalAuthenticator"; -import crypto from "@/crypto/crypto"; -import { encrypt } from "@/crypto/encryption"; import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; import TopicPersistence from "@/keystore/persistence/TopicPersistence"; import { KeystoreProviderUnavailableError } from "@/keystore/providers/errors"; diff --git a/sdks/js-sdk/test/utils/topic.test.ts b/sdks/js-sdk/test/utils/topic.test.ts index fcecdc7e..425fc386 100644 --- a/sdks/js-sdk/test/utils/topic.test.ts +++ b/sdks/js-sdk/test/utils/topic.test.ts @@ -1,4 +1,4 @@ -import crypto from "@/crypto/crypto"; +import { crypto } from "@xmtp/encryption"; import { buildContentTopic, buildDirectMessageTopicV2, diff --git a/shared/encryption/src/index.ts b/shared/encryption/src/index.ts index 96556fa2..9bc0f254 100644 --- a/shared/encryption/src/index.ts +++ b/shared/encryption/src/index.ts @@ -1,2 +1,3 @@ export { default as Ciphertext } from "./Ciphertext"; +export { default as crypto } from "./crypto"; export * from "./encryption"; diff --git a/yarn.lock b/yarn.lock index 9672a437..7d759321 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1720,6 +1720,24 @@ __metadata: languageName: node linkType: hard +"@rollup/plugin-node-resolve@npm:^15.3.0": + version: 15.3.0 + resolution: "@rollup/plugin-node-resolve@npm:15.3.0" + dependencies: + "@rollup/pluginutils": "npm:^5.0.1" + "@types/resolve": "npm:1.20.2" + deepmerge: "npm:^4.2.2" + is-module: "npm:^1.0.0" + resolve: "npm:^1.22.1" + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 10/214596dd0ecf0822a135e6cb604f6a4469bac4a9d6b43608d277b47c34762e800b79f5f1c18ea0f7317448165ac0cff2439b35446641e093a5bc5c372940c819 + languageName: node + linkType: hard + "@rollup/plugin-terser@npm:^0.4.4": version: 0.4.4 resolution: "@rollup/plugin-terser@npm:0.4.4" @@ -1755,7 +1773,7 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:^5.1.0": +"@rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.1.0": version: 5.1.2 resolution: "@rollup/pluginutils@npm:5.1.2" dependencies: @@ -2213,6 +2231,13 @@ __metadata: languageName: node linkType: hard +"@types/resolve@npm:1.20.2": + version: 1.20.2 + resolution: "@types/resolve@npm:1.20.2" + checksum: 10/1bff0d3875e7e1557b6c030c465beca9bf3b1173ebc6937cac547654b0af3bb3ff0f16470e9c4d7c5dc308ad9ac8627c38dbff24ef698b66673ff5bd4ead7f7e + languageName: node + linkType: hard + "@types/unist@npm:*, @types/unist@npm:^3.0.0": version: 3.0.3 resolution: "@types/unist@npm:3.0.3" @@ -2697,7 +2722,7 @@ __metadata: languageName: unknown linkType: soft -"@xmtp/encryption@workspace:shared/encryption": +"@xmtp/encryption@workspace:*, @xmtp/encryption@workspace:shared/encryption": version: 0.0.0-use.local resolution: "@xmtp/encryption@workspace:shared/encryption" dependencies: @@ -2854,6 +2879,7 @@ __metadata: "@metamask/providers": "npm:^17.1.1" "@noble/secp256k1": "npm:1.7.1" "@rollup/plugin-json": "npm:^6.1.0" + "@rollup/plugin-node-resolve": "npm:^15.3.0" "@rollup/plugin-terser": "npm:^0.4.4" "@rollup/plugin-typescript": "npm:^12.1.0" "@types/benchmark": "npm:^2.1.5" @@ -2867,6 +2893,7 @@ __metadata: "@xmtp/consent-proof-signature": "npm:^0.1.3" "@xmtp/content-type-primitives": "npm:^1.0.1" "@xmtp/content-type-text": "npm:^1.0.0" + "@xmtp/encryption": "workspace:*" "@xmtp/proto": "npm:^3.68.0" "@xmtp/rollup-plugin-resolve-extensions": "npm:1.0.1" "@xmtp/user-preferences-bindings-wasm": "npm:^0.3.6" @@ -3867,6 +3894,13 @@ __metadata: languageName: node linkType: hard +"deepmerge@npm:^4.2.2": + version: 4.3.1 + resolution: "deepmerge@npm:4.3.1" + checksum: 10/058d9e1b0ff1a154468bf3837aea436abcfea1ba1d165ddaaf48ca93765fdd01a30d33c36173da8fbbed951dd0a267602bc782fe288b0fc4b7e1e7091afc4529 + languageName: node + linkType: hard + "define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": version: 1.1.4 resolution: "define-data-property@npm:1.1.4" @@ -5807,6 +5841,13 @@ __metadata: languageName: node linkType: hard +"is-module@npm:^1.0.0": + version: 1.0.0 + resolution: "is-module@npm:1.0.0" + checksum: 10/8cd5390730c7976fb4e8546dd0b38865ee6f7bacfa08dfbb2cc07219606755f0b01709d9361e01f13009bbbd8099fa2927a8ed665118a6105d66e40f1b838c3f + languageName: node + linkType: hard + "is-negative-zero@npm:^2.0.3": version: 2.0.3 resolution: "is-negative-zero@npm:2.0.3" From 4276763de40666e33e014722b45590994c00ac56 Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Tue, 15 Oct 2024 14:24:02 -0500 Subject: [PATCH 3/6] Update rollup config --- sdks/js-sdk/rollup.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdks/js-sdk/rollup.config.js b/sdks/js-sdk/rollup.config.js index 8d19c44b..c5b71421 100644 --- a/sdks/js-sdk/rollup.config.js +++ b/sdks/js-sdk/rollup.config.js @@ -1,4 +1,5 @@ import json from "@rollup/plugin-json"; +import { nodeResolve } from "@rollup/plugin-node-resolve"; import terser from "@rollup/plugin-terser"; import typescript from "@rollup/plugin-typescript"; import { resolveExtensions } from "@xmtp/rollup-plugin-resolve-extensions"; @@ -35,6 +36,9 @@ const plugins = [ json({ preferConst: true, }), + nodeResolve({ + resolveOnly: ["@xmtp/encryption"], + }), ]; export default defineConfig([ From a23b9c202d57fc13bb7bf6f41570bf646273aec5 Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Tue, 15 Oct 2024 14:37:35 -0500 Subject: [PATCH 4/6] Update remote attachment imports --- content-types/content-type-remote-attachment/package.json | 5 +++-- .../content-type-remote-attachment/rollup.config.js | 7 +++++-- .../content-type-remote-attachment/src/RemoteAttachment.ts | 3 +-- .../src/encryption.browser.ts | 2 -- .../content-type-remote-attachment/src/encryption.ts | 3 --- content-types/content-type-remote-attachment/src/utils.ts | 6 ------ yarn.lock | 2 ++ 7 files changed, 11 insertions(+), 17 deletions(-) delete mode 100644 content-types/content-type-remote-attachment/src/encryption.browser.ts delete mode 100644 content-types/content-type-remote-attachment/src/encryption.ts delete mode 100644 content-types/content-type-remote-attachment/src/utils.ts diff --git a/content-types/content-type-remote-attachment/package.json b/content-types/content-type-remote-attachment/package.json index 2dda6959..d65cf4cd 100644 --- a/content-types/content-type-remote-attachment/package.json +++ b/content-types/content-type-remote-attachment/package.json @@ -66,10 +66,11 @@ "dependencies": { "@noble/secp256k1": "^1.7.1", "@xmtp/content-type-primitives": "^1.0.1", - "@xmtp/proto": "^3.61.1", - "@xmtp/xmtp-js": "^11.6.3" + "@xmtp/encryption": "workspace:*", + "@xmtp/proto": "^3.61.1" }, "devDependencies": { + "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.0", "@types/node": "^18.19.22", diff --git a/content-types/content-type-remote-attachment/rollup.config.js b/content-types/content-type-remote-attachment/rollup.config.js index 74e5cdfe..b11bf152 100644 --- a/content-types/content-type-remote-attachment/rollup.config.js +++ b/content-types/content-type-remote-attachment/rollup.config.js @@ -1,3 +1,4 @@ +import { nodeResolve } from "@rollup/plugin-node-resolve"; import terser from "@rollup/plugin-terser"; import typescript from "@rollup/plugin-typescript"; import { resolveExtensions } from "@xmtp/rollup-plugin-resolve-extensions"; @@ -13,13 +14,15 @@ const plugins = [ filesize({ showMinifiedSize: false, }), + nodeResolve({ + resolveOnly: ["@xmtp/encryption"], + }), ]; const external = [ "@noble/secp256k1", - "@xmtp/proto", "@xmtp/content-type-primitives", - "@xmtp/xmtp-js", + "@xmtp/proto", "node:crypto", ]; diff --git a/content-types/content-type-remote-attachment/src/RemoteAttachment.ts b/content-types/content-type-remote-attachment/src/RemoteAttachment.ts index e40616a2..9cae318f 100644 --- a/content-types/content-type-remote-attachment/src/RemoteAttachment.ts +++ b/content-types/content-type-remote-attachment/src/RemoteAttachment.ts @@ -5,9 +5,8 @@ import { type ContentCodec, type EncodedContent, } from "@xmtp/content-type-primitives"; +import { Ciphertext, crypto, decrypt, encrypt } from "@xmtp/encryption"; import { content as proto } from "@xmtp/proto"; -import { Ciphertext, decrypt, encrypt } from "@xmtp/xmtp-js"; -import { crypto } from "./encryption"; export const ContentTypeRemoteAttachment = new ContentTypeId({ authorityId: "xmtp.org", diff --git a/content-types/content-type-remote-attachment/src/encryption.browser.ts b/content-types/content-type-remote-attachment/src/encryption.browser.ts deleted file mode 100644 index edc514af..00000000 --- a/content-types/content-type-remote-attachment/src/encryption.browser.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line prefer-destructuring -export const crypto = window.crypto; diff --git a/content-types/content-type-remote-attachment/src/encryption.ts b/content-types/content-type-remote-attachment/src/encryption.ts deleted file mode 100644 index bd4ac7fe..00000000 --- a/content-types/content-type-remote-attachment/src/encryption.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { webcrypto } from "node:crypto"; - -export const crypto = webcrypto; diff --git a/content-types/content-type-remote-attachment/src/utils.ts b/content-types/content-type-remote-attachment/src/utils.ts deleted file mode 100644 index acb5d3dd..00000000 --- a/content-types/content-type-remote-attachment/src/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This is a variation of https://github.com/paulmillr/noble-secp256k1/blob/main/index.ts#L1378-L1388 -// that uses `digest('SHA-256', bytes)` instead of `digest('SHA-256', bytes.buffer)` -// which seems to produce different results. -export async function sha256(bytes: Uint8Array): Promise { - return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); -} diff --git a/yarn.lock b/yarn.lock index 7d759321..b4ee287b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2626,10 +2626,12 @@ __metadata: resolution: "@xmtp/content-type-remote-attachment@workspace:content-types/content-type-remote-attachment" dependencies: "@noble/secp256k1": "npm:^1.7.1" + "@rollup/plugin-node-resolve": "npm:^15.3.0" "@rollup/plugin-terser": "npm:^0.4.4" "@rollup/plugin-typescript": "npm:^12.1.0" "@types/node": "npm:^18.19.22" "@xmtp/content-type-primitives": "npm:^1.0.1" + "@xmtp/encryption": "workspace:*" "@xmtp/proto": "npm:^3.61.1" "@xmtp/rollup-plugin-resolve-extensions": "npm:^1.0.1" "@xmtp/xmtp-js": "npm:^11.6.3" From 826b120b39bf4b7740c7587c26c1108a0488feaf Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Tue, 15 Oct 2024 15:08:35 -0500 Subject: [PATCH 5/6] Update description --- shared/encryption/README.md | 2 +- shared/encryption/package.json | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/shared/encryption/README.md b/shared/encryption/README.md index fba1c0cc..8c57e74c 100644 --- a/shared/encryption/README.md +++ b/shared/encryption/README.md @@ -1,3 +1,3 @@ # XMTP Encryption -This package provides encryption and decryption for XMTP. +This package provides private,shared encryption functions for use with XMTP. diff --git a/shared/encryption/package.json b/shared/encryption/package.json index 49acdbdc..cf38d01a 100644 --- a/shared/encryption/package.json +++ b/shared/encryption/package.json @@ -2,17 +2,6 @@ "name": "@xmtp/encryption", "version": "0.0.0", "private": true, - "description": "XMTP encryption library", - "keywords": [ - "xmtp", - "messaging", - "web3", - "sdk", - "js", - "javascript", - "node", - "nodejs" - ], "license": "MIT", "author": "XMTP Labs ", "type": "module", From 0c7897f6eed80eb46767f498fa51d518e8aa8552 Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Tue, 15 Oct 2024 15:10:27 -0500 Subject: [PATCH 6/6] Add encryption workflow to CI --- .github/workflows/encryption.yml | 92 ++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 .github/workflows/encryption.yml diff --git a/.github/workflows/encryption.yml b/.github/workflows/encryption.yml new file mode 100644 index 00000000..ef10f35b --- /dev/null +++ b/.github/workflows/encryption.yml @@ -0,0 +1,92 @@ +name: Encryption + +on: + push: + branches: + - main + + pull_request: + paths: + - "shared/encryption/**" + - ".github/workflows/encryption.yml" + - ".node-version" + - ".nvmrc" + - ".yarnrc.yml" + - "turbo.json" + +jobs: + typecheck: + name: Typecheck + runs-on: warp-ubuntu-latest-x64-8x + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "yarn" + env: + SKIP_YARN_COREPACK_CHECK: "1" + - name: Enable corepack + run: corepack enable + - name: Install dependencies + run: yarn + - name: Typecheck + run: yarn turbo run typecheck --filter='./shared/encryption' + + lint: + name: Lint + runs-on: warp-ubuntu-latest-x64-8x + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "yarn" + env: + SKIP_YARN_COREPACK_CHECK: "1" + - name: Enable corepack + run: corepack enable + - name: Install dependencies + run: yarn + - name: Lint + run: yarn turbo run lint --filter='./shared/encryption' + + test: + name: Test + runs-on: warp-ubuntu-latest-x64-8x + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "yarn" + env: + SKIP_YARN_COREPACK_CHECK: "1" + - name: Enable corepack + run: corepack enable + - name: Install dependencies + run: yarn + - name: Start dev environment + run: ./dev/up + - name: Sleep for 5 seconds + run: sleep 5s + - name: Run tests + run: yarn turbo run test --filter='./shared/encryption' + + build: + name: Build + runs-on: warp-ubuntu-latest-x64-8x + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "yarn" + env: + SKIP_YARN_COREPACK_CHECK: "1" + - name: Enable corepack + run: corepack enable + - name: Install dependencies + run: yarn + - name: Build + run: yarn turbo run build --filter='./shared/encryption'