diff --git a/packages/common/src/services/auth/hedgehogLib/authWorker.ts b/packages/common/src/services/auth/hedgehogLib/authWorker.ts deleted file mode 100644 index a15248b85eb..00000000000 --- a/packages/common/src/services/auth/hedgehogLib/authWorker.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-disable */ -// Runs createKey (scrypt) inside a WebWorker. The function is stringified and -// loaded from a Blob, so it must not reference anything outside its own body. -// Copied from @audius/hedgehog 3.0.0-alpha.1 authWorker.js. Changing the scrypt -// implementation or how the inputs are encoded changes every user's key. -export const authWorker = function () { - const s = self as any - - // Package and deps for https://github.com/ricmoo/scrypt-js - s.importScripts('https://cdn.jsdelivr.net/npm/scrypt-js@2.0.4/scrypt.min.js') - s.importScripts( - 'https://cdn.jsdelivr.net/npm/scrypt-js@2.0.4/thirdparty/buffer.min.js' - ) - - const createKey = (encryptStr: string, ivHex: string) => { - const encryptStrBuffer = s.buffer.SlowBuffer(encryptStr) - const ivBuffer = s.buffer.SlowBuffer(ivHex) - - const N = 32768 - const r = 8 - const p = 1 - const dkLen = 32 - - s.scrypt( - encryptStrBuffer, - ivBuffer, - N, - r, - p, - dkLen, - function (error: unknown, _progress: unknown, key: unknown) { - if (error) { - throw error - } else if (key) { - key = new s.buffer.SlowBuffer(key) - const keyHex = (key as any).toString('hex') - s.postMessage({ keyHex, keyBuffer: key }) - } - // no else clause here, the progress triggers the else clause - // but there's no key yet at that point - } - ) - } - - s.onmessage = (e: MessageEvent) => { - if (!e) return - const d = JSON.parse(e.data) - createKey(d.encryptStr, d.ivHex) - } -} diff --git a/packages/common/src/services/auth/hedgehogLib/hedgehogLib.test.ts b/packages/common/src/services/auth/hedgehogLib/hedgehogLib.test.ts index 1efc40b4d05..6c45375c5b0 100644 --- a/packages/common/src/services/auth/hedgehogLib/hedgehogLib.test.ts +++ b/packages/common/src/services/auth/hedgehogLib/hedgehogLib.test.ts @@ -14,6 +14,7 @@ import { recoverMessageAddress } from 'viem' import { signMessage } from 'viem/accounts' import { describe, expect, it } from 'vitest' +import { createKeyAsync, createKeySync } from './scrypt' import { hexToBytesLikeBuffer, waitUntil } from './utils' import { @@ -388,6 +389,42 @@ describe('createKey (scrypt)', () => { }, 60_000) }) +describe('createKey (scrypt) vs the old web worker', () => { + // Outputs of the scrypt-js@2.0.4 + buffer scripts the old web worker loaded + // from jsdelivr, run with the same inputs and JSON round-trip. + const vectors: Array<[string, string, string]> = [ + [ + 'hunter2', + '0123456789abcdef0123456789abcdef', + 'e9f55191be7eeaab074726a9860cb95e57eeac613e5b25415655903fc70a25c4' + ], + [ + 'dylan@audius.example:::correct horse battery staple', + 'a1b2c3d4e5f60718293a4b5c6d7e8f90', + 'aac7f65da104b69fa2e8c95ec4f6a30f43ce4377f55576611f28928a86d93053' + ], + [ + 'pässwörd ñ 日本語 🎵🎧', + 'ffeeddccbbaa99887766554433221100', + 'd4acf4d5eb6a4828cab6f9e02db11fef876fbfd243e968308fbbe983ab4eec79' + ], + [ + '', + 'deadbeefdeadbeefdeadbeefdeadbeef', + 'ad5bfdb843b3788277d9d287ebf5b45c127577c12e585579a0946ad55f1109db' + ] + ] + + it('createKeySync and createKeyAsync match', async () => { + for (const [str, iv, keyHex] of vectors) { + const sync = createKeySync(str, iv) + expect(sync.keyHex).toBe(keyHex) + expect(Buffer.from(sync.keyBuffer).toString('hex')).toBe(keyHex) + expect((await createKeyAsync(str, iv)).keyHex).toBe(keyHex) + } + }, 60_000) +}) + describe('identity auth signature (personal_sign)', () => { it('matches eth-sig-util personalSign byte for byte', async () => { for (let i = 0; i < 100; i++) { diff --git a/packages/common/src/services/auth/hedgehogLib/index.ts b/packages/common/src/services/auth/hedgehogLib/index.ts index f999db65e43..fc811aff3f3 100644 --- a/packages/common/src/services/auth/hedgehogLib/index.ts +++ b/packages/common/src/services/auth/hedgehogLib/index.ts @@ -6,8 +6,7 @@ export { bufferFromHexString, getPlatformCreateKey, getPlatformLocalStorage, - waitUntil, - WebWorker + waitUntil } from './utils' export type { ChangeCredentialsArgs, diff --git a/packages/common/src/services/auth/hedgehogLib/scrypt.ts b/packages/common/src/services/auth/hedgehogLib/scrypt.ts new file mode 100644 index 00000000000..9a28134c821 --- /dev/null +++ b/packages/common/src/services/auth/hedgehogLib/scrypt.ts @@ -0,0 +1,30 @@ +import { scrypt, scryptAsync } from '@noble/hashes/scrypt' +import { bytesToHex } from '@noble/hashes/utils' + +import type { PrivateKey } from './types' + +// Changing these params or how the inputs are encoded changes every user's key. +// The ivHex string itself (its utf8 bytes, not the decoded hex) is the salt. +const SCRYPT_PARAMS = { N: 32768, r: 8, p: 1, dkLen: 32 } + +const encoder = new TextEncoder() + +const toPrivateKey = (keyBuffer: Uint8Array): PrivateKey => ({ + keyHex: bytesToHex(keyBuffer), + keyBuffer +}) + +/** Blocks for about a second. Only call this off the main thread. */ +export const createKeySync = (encryptStr: string, ivHex: string) => + toPrivateKey( + scrypt(encoder.encode(encryptStr), encoder.encode(ivHex), SCRYPT_PARAMS) + ) + +export const createKeyAsync = async (encryptStr: string, ivHex: string) => + toPrivateKey( + await scryptAsync( + encoder.encode(encryptStr), + encoder.encode(ivHex), + SCRYPT_PARAMS + ) + ) diff --git a/packages/common/src/services/auth/hedgehogLib/utils.ts b/packages/common/src/services/auth/hedgehogLib/utils.ts index 5ce46d16a3b..445aef03494 100644 --- a/packages/common/src/services/auth/hedgehogLib/utils.ts +++ b/packages/common/src/services/auth/hedgehogLib/utils.ts @@ -1,10 +1,4 @@ -import { authWorker } from './authWorker' -import type { CreateKey, LocalStorage, PrivateKey } from './types' - -const SCRYPT_N = 32768 -const SCRYPT_R = 8 -const SCRYPT_P = 1 -const SCRYPT_DK_LEN = 32 +import type { CreateKey, LocalStorage } from './types' // https://stackoverflow.com/questions/38987784/how-to-convert-a-hexadecimal-string-to-uint8array-and-back-in-javascript export function bufferFromHexString(hexString: string) { @@ -53,14 +47,6 @@ export function randomBytes(length: number) { return bytes } -export function WebWorker(worker: () => void) { - if (typeof window !== 'undefined' && window.Worker) { - const code = worker.toString() - const blob = new Blob(['(' + code + ')()']) - return new window.Worker(URL.createObjectURL(blob)) - } else throw new Error('Cannot call web worker on the server') -} - function isReactNativeEnv() { return typeof navigator !== 'undefined' && navigator.product === 'ReactNative' } @@ -74,10 +60,6 @@ function isWebEnv() { ) } -function isNodeEnv() { - return typeof window === 'undefined' || window === null -} - export function getPlatformLocalStorage(): LocalStorage { if (isWebEnv()) { return window.localStorage @@ -87,41 +69,15 @@ export function getPlatformLocalStorage(): LocalStorage { ) } +/** + * Default scrypt createKey. Runs on the calling thread, so web passes its own + * worker-based createKey and mobile passes a native one. + */ export function getPlatformCreateKey(): CreateKey { - /** - * Given a user encryptStr and initialization vector, generate a private key - * @param encryptStr String to encrypt (can be user password or some kind of lookup key) - * @param ivHex hex string iv value. Its utf8 bytes (not the decoded hex) are the salt. - */ - const createKey = async (encryptStr: string, ivHex: string) => { - if (isWebEnv()) { - return new Promise((resolve) => { - const worker = WebWorker(authWorker) - worker.postMessage(JSON.stringify({ encryptStr, ivHex })) - - worker.onmessage = (event) => { - resolve(event.data) - } - }) - } - - if (isNodeEnv()) { - const { scryptAsync } = await import('@noble/hashes/scrypt') - const encoder = new TextEncoder() - const keyBuffer = await scryptAsync( - encoder.encode(encryptStr), - encoder.encode(ivHex), - { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P, dkLen: SCRYPT_DK_LEN } - ) - return { keyHex: bytesToHexString(keyBuffer), keyBuffer } - } - - throw new Error( - 'Please pass in valid createKey function into the Hedgehog constructor' - ) + return async (encryptStr, ivHex) => { + const { createKeyAsync } = await import('./scrypt') + return await createKeyAsync(encryptStr, ivHex) } - - return createKey } /** diff --git a/packages/web/src/services/audius-sdk/auth.ts b/packages/web/src/services/audius-sdk/auth.ts index 82826cb2de6..f62eed9d7e5 100644 --- a/packages/web/src/services/audius-sdk/auth.ts +++ b/packages/web/src/services/audius-sdk/auth.ts @@ -14,6 +14,8 @@ import { audiusChain } from 'app/audiusChain' import { env } from '../env' import { localStorage } from '../local-storage' +import { createKey } from './createKey' + export const getAudiusWalletClient = async (): Promise => { // Check if the user has already connected Hedgehog first... await authService.hedgehogInstance.waitUntilReady() @@ -102,7 +104,8 @@ export const getAudiusWalletClient = async (): Promise => { export const authService = createAuthService({ localStorage, - identityServiceEndpoint: env.IDENTITY_SERVICE + identityServiceEndpoint: env.IDENTITY_SERVICE, + createKey }) export const solanaWalletService = createHedgehogSolanaWalletService( diff --git a/packages/web/src/services/audius-sdk/createKey.ts b/packages/web/src/services/audius-sdk/createKey.ts new file mode 100644 index 00000000000..f5d78ec7f93 --- /dev/null +++ b/packages/web/src/services/audius-sdk/createKey.ts @@ -0,0 +1,24 @@ +import type { PrivateKey } from '@audius/common/src/services/auth/hedgehogLib/types' + +/** + * Runs scrypt in a worker so login doesn't freeze the page. The worker is + * bundled inline and started from a blob URL, which also works under the + * desktop app's custom scheme. + */ +export const createKey = async (encryptStr: string, ivHex: string) => { + const { default: CreateKeyWorker } = await import( + './createKey.worker?worker&inline' + ) + const worker = new CreateKeyWorker() + try { + return await new Promise((resolve, reject) => { + worker.onmessage = (event: MessageEvent) => + resolve(event.data) + worker.onerror = (event) => + reject(event.error ?? new Error(event.message)) + worker.postMessage({ encryptStr, ivHex }) + }) + } finally { + worker.terminate() + } +} diff --git a/packages/web/src/services/audius-sdk/createKey.worker.ts b/packages/web/src/services/audius-sdk/createKey.worker.ts new file mode 100644 index 00000000000..9009aabecc0 --- /dev/null +++ b/packages/web/src/services/audius-sdk/createKey.worker.ts @@ -0,0 +1,8 @@ +import { createKeySync } from '@audius/common/src/services/auth/hedgehogLib/scrypt' + +self.onmessage = ( + event: MessageEvent<{ encryptStr: string; ivHex: string }> +) => { + const { encryptStr, ivHex } = event.data + self.postMessage(createKeySync(encryptStr, ivHex)) +}