diff --git a/packages/webcrypto-ed25519-polyfill/.gitignore b/packages/webcrypto-ed25519-polyfill/.gitignore
new file mode 100644
index 000000000000..849ddff3b7ec
--- /dev/null
+++ b/packages/webcrypto-ed25519-polyfill/.gitignore
@@ -0,0 +1 @@
+dist/
diff --git a/packages/webcrypto-ed25519-polyfill/.npmrc b/packages/webcrypto-ed25519-polyfill/.npmrc
new file mode 100644
index 000000000000..b6f27f135954
--- /dev/null
+++ b/packages/webcrypto-ed25519-polyfill/.npmrc
@@ -0,0 +1 @@
+engine-strict=true
diff --git a/packages/webcrypto-ed25519-polyfill/.prettierignore b/packages/webcrypto-ed25519-polyfill/.prettierignore
new file mode 100644
index 000000000000..849ddff3b7ec
--- /dev/null
+++ b/packages/webcrypto-ed25519-polyfill/.prettierignore
@@ -0,0 +1 @@
+dist/
diff --git a/packages/webcrypto-ed25519-polyfill/LICENSE b/packages/webcrypto-ed25519-polyfill/LICENSE
new file mode 100644
index 000000000000..adfd203c6fac
--- /dev/null
+++ b/packages/webcrypto-ed25519-polyfill/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2018 Solana Labs, Inc
+
+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/packages/webcrypto-ed25519-polyfill/README.md b/packages/webcrypto-ed25519-polyfill/README.md
new file mode 100644
index 000000000000..c977f1c469ac
--- /dev/null
+++ b/packages/webcrypto-ed25519-polyfill/README.md
@@ -0,0 +1,35 @@
+[![npm][npm-image]][npm-url]
+[![npm-downloads][npm-downloads-image]][npm-url]
+[![semantic-release][semantic-release-image]][semantic-release-url]
+
+[![code-style-prettier][code-style-prettier-image]][code-style-prettier-url]
+
+[code-style-prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square
+[code-style-prettier-url]: https://github.com/prettier/prettier
+[npm-downloads-image]: https://img.shields.io/npm/dm/@solana/webcrypto-ed25519-polyfill/experimental.svg?style=flat
+[npm-image]: https://img.shields.io/npm/v/@solana/webcrypto-ed25519-polyfill/experimental.svg?style=flat
+[npm-url]: https://www.npmjs.com/package/@solana/webcrypto-ed25519-polyfill/v/experimental
+[semantic-release-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
+[semantic-release-url]: https://github.com/semantic-release/semantic-release
+
+# @solana/webcrypto-ed25519-polyfill
+
+This package contains a polyfill that enables Ed25519 key manipulation in environments where it is not yet implemented. It does so by proxying calls to `SubtleCrypto` instance methods to an Ed25519 implementation in userspace.
+
+## Security warning
+
+Because this package's implementation of Ed25519 key generation exists in userspace, it can't guarantee that the keys you generate with it are non-exportable. Untrusted code running in your JavaScript context may still be able to gain access to and/or exfiltrate secret key material.
+
+## Usage
+
+Environments that support Ed25519 (see https://github.com/WICG/webcrypto-secure-curves/issues/20) do not require this polyfill.
+
+For all others, simply import this polyfill before use.
+
+```ts
+// Importing this will shim methods on `SubtleCrypto`, adding Ed25519 support.
+import '@solana/webcrypto-ed25519-polyfill';
+
+// Now you can do this, in environments that do not otherwise support Ed25519.
+const keyPair = await crypto.subtle.generateKey('Ed25519', false, ['sign']);
+```
diff --git a/packages/webcrypto-ed25519-polyfill/package.json b/packages/webcrypto-ed25519-polyfill/package.json
new file mode 100644
index 000000000000..9ca1b6bf026f
--- /dev/null
+++ b/packages/webcrypto-ed25519-polyfill/package.json
@@ -0,0 +1,99 @@
+{
+ "name": "@solana/webcrypto-ed25519-polyfill",
+ "version": "2.0.0-development",
+ "description": "A polyfill that adds Ed25519 key manipulation capabilities to `SubtleCrypto` in environments where it is not yet supported",
+ "exports": {
+ "browser": {
+ "import": "./dist/index.browser.js",
+ "require": "./dist/index.browser.cjs"
+ },
+ "node": {
+ "import": "./dist/index.node.js",
+ "require": "./dist/index.node.cjs"
+ },
+ "react-native": "./dist/index.native.js",
+ "types": "./dist/types/index.d.ts"
+ },
+ "browser": {
+ "./dist/index.node.cjs": "./dist/index.browser.cjs",
+ "./dist/index.node.js": "./dist/index.browser.js"
+ },
+ "main": "./dist/index.node.cjs",
+ "module": "./dist/index.node.js",
+ "react-native": "./dist/index.native.js",
+ "types": "./dist/types/index.d.ts",
+ "type": "module",
+ "files": [
+ "./dist/"
+ ],
+ "sideEffects": true,
+ "keywords": [
+ "blockchain",
+ "solana",
+ "web3"
+ ],
+ "scripts": {
+ "compile:js": "tsup --config build-scripts/tsup.config.library.ts",
+ "compile:typedefs": "tsc -p ./tsconfig.declarations.json",
+ "dev": "jest -c node_modules/test-config/jest-dev.config.ts --rootDir . --watch",
+ "prepublishOnly": "version-from-git --no-git-tag-version --template experimental.short",
+ "publish-packages": "pnpm publish --tag experimental --access public --no-git-checks",
+ "test:lint": "jest -c node_modules/test-config/jest-lint.config.ts --rootDir . --silent",
+ "test:prettier": "jest -c node_modules/test-config/jest-prettier.config.ts --rootDir . --silent",
+ "test:typecheck": "tsc --noEmit",
+ "test:unit:browser": "jest -c node_modules/test-config/jest-unit.config.browser.ts --rootDir . --silent",
+ "test:unit:node": "jest -c node_modules/test-config/jest-unit.config.node.ts --rootDir . --silent"
+ },
+ "author": "Solana Labs Maintainers ",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/solana-labs/solana-web3.js"
+ },
+ "bugs": {
+ "url": "http://github.com/solana-labs/solana-web3.js/issues"
+ },
+ "browserslist": [
+ "supports bigint and not dead",
+ "maintained node versions"
+ ],
+ "engine": {
+ "node": ">=17.4"
+ },
+ "dependencies": {
+ "@noble/curves": "^1.1.0"
+ },
+ "devDependencies": {
+ "@solana/eslint-config-solana": "^1.0.1",
+ "@swc/core": "^1.3.18",
+ "@swc/jest": "^0.2.26",
+ "@types/jest": "^29.5.2",
+ "@typescript-eslint/eslint-plugin": "^5.57.1",
+ "@typescript-eslint/parser": "^5.57.1",
+ "build-scripts": "workspace:*",
+ "eslint": "^8.37.0",
+ "eslint-plugin-jest": "^27.1.5",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-sort-keys-fix": "^1.1.2",
+ "jest": "^29.6.1",
+ "jest-environment-jsdom": "^29.6.0",
+ "jest-runner-eslint": "^2.1.0",
+ "jest-runner-prettier": "^1.0.0",
+ "postcss": "^8.4.12",
+ "prettier": "^2.8.8",
+ "test-config": "workspace:*",
+ "ts-node": "^10.9.1",
+ "tsconfig": "workspace:*",
+ "tsup": "6.7.0",
+ "typescript": "^5.0.4",
+ "version-from-git": "^1.1.1"
+ },
+ "bundlewatch": {
+ "defaultCompression": "gzip",
+ "files": [
+ {
+ "path": "./dist/index*.js"
+ }
+ ]
+ }
+}
diff --git a/packages/webcrypto-ed25519-polyfill/src/__tests__/index-test.ts b/packages/webcrypto-ed25519-polyfill/src/__tests__/index-test.ts
new file mode 100644
index 000000000000..0e8e1368e7ba
--- /dev/null
+++ b/packages/webcrypto-ed25519-polyfill/src/__tests__/index-test.ts
@@ -0,0 +1,185 @@
+import { generateKeyPolyfill } from '../secrets';
+
+jest.mock('../secrets');
+
+describe('generateKey() polyfill', () => {
+ let oldIsSecureContext: boolean;
+ let originalGenerateKey: SubtleCrypto['generateKey'];
+ beforeEach(() => {
+ jest.spyOn(globalThis.crypto?.subtle, 'generateKey');
+ originalGenerateKey = globalThis.crypto?.subtle?.generateKey;
+ if (__BROWSER__) {
+ // FIXME: JSDOM does not set `isSecureContext` or otherwise allow you to configure it.
+ // Some discussion: https://github.com/jsdom/jsdom/issues/2751#issuecomment-846613392
+ if (globalThis.isSecureContext !== undefined) {
+ oldIsSecureContext = globalThis.isSecureContext;
+ }
+ }
+ globalThis.isSecureContext = true;
+ });
+ afterEach(() => {
+ globalThis.crypto.subtle.generateKey = originalGenerateKey;
+ if (oldIsSecureContext !== undefined) {
+ globalThis.isSecureContext = oldIsSecureContext;
+ }
+ });
+ describe('when required in an environment with no `generateKey` function', () => {
+ beforeEach(() => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ globalThis.crypto.subtle.generateKey = undefined;
+ jest.isolateModules(() => {
+ require('../index');
+ });
+ });
+ afterEach(() => {
+ globalThis.crypto.subtle.generateKey = originalGenerateKey;
+ });
+ it.each([
+ { __variant: 'P256', name: 'ECDSA', namedCurve: 'P-256' },
+ { __variant: 'P384', name: 'ECDSA', namedCurve: 'P-384' } as EcKeyGenParams,
+ { __variant: 'P521', name: 'ECDSA', namedCurve: 'P-521' } as EcKeyGenParams,
+ ...['RSASSA-PKCS1-v1_5', 'RSA-PSS'].flatMap(rsaAlgoName =>
+ ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512'].map(
+ hashName =>
+ ({
+ __variant: hashName,
+ hash: { name: hashName },
+ modulusLength: 2048,
+ name: rsaAlgoName,
+ publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
+ } as RsaHashedKeyGenParams)
+ )
+ ),
+ ])('fatals when the algorithm is $name/$__variant', async algorithm => {
+ expect.assertions(1);
+ await expect(() =>
+ globalThis.crypto.subtle.generateKey(algorithm, /* extractable */ false, ['sign', 'verify'])
+ ).rejects.toThrow();
+ });
+ it('delegates Ed25519 `generateKey` calls to the polyfill', async () => {
+ expect.assertions(1);
+ const mockKeyPair = {};
+ (generateKeyPolyfill as jest.Mock).mockReturnValue(mockKeyPair);
+ const keyPair = await globalThis.crypto.subtle.generateKey('Ed25519', /* extractable */ false, [
+ 'sign',
+ 'verify',
+ ]);
+ expect(keyPair).toBe(mockKeyPair);
+ });
+ });
+ describe('when required in an environment that does not support Ed25519', () => {
+ beforeEach(() => {
+ const originalGenerateKeyImpl = originalGenerateKey;
+ (originalGenerateKey as jest.Mock).mockImplementation(async (...args) => {
+ const [algorithm] = args;
+ if (algorithm === 'Ed25519') {
+ throw new Error('Ed25519 not supported');
+ }
+ return await originalGenerateKeyImpl.apply(globalThis.crypto.subtle, args);
+ });
+ jest.isolateModules(() => {
+ require('../index');
+ });
+ });
+ it('calls the original `generateKey` once as a test when the algorithm is "Ed25519" but never again (parallel version)', async () => {
+ expect.assertions(1);
+ await Promise.all([
+ globalThis.crypto.subtle.generateKey('Ed25519', /* extractable */ false, ['sign', 'verify']),
+ globalThis.crypto.subtle.generateKey('Ed25519', /* extractable */ false, ['sign', 'verify']),
+ ]);
+ expect(originalGenerateKey).toHaveBeenCalledTimes(1);
+ });
+ it('calls the original `generateKey` once as a test when the algorithm is "Ed25519" but never again (serial version)', async () => {
+ expect.assertions(1);
+ await globalThis.crypto.subtle.generateKey('Ed25519', /* extractable */ false, ['sign', 'verify']);
+ await globalThis.crypto.subtle.generateKey('Ed25519', /* extractable */ false, ['sign', 'verify']);
+ expect(originalGenerateKey).toHaveBeenCalledTimes(1);
+ });
+ it('delegates Ed25519 `generateKey` calls to the polyfill', async () => {
+ expect.assertions(1);
+ const mockKeyPair = {};
+ (generateKeyPolyfill as jest.Mock).mockReturnValue(mockKeyPair);
+ const keyPair = await globalThis.crypto.subtle.generateKey('Ed25519', /* extractable */ false, [
+ 'sign',
+ 'verify',
+ ]);
+ expect(keyPair).toBe(mockKeyPair);
+ });
+ });
+ describe('when required in an environment that supports Ed25519', () => {
+ beforeEach(() => {
+ jest.isolateModules(() => {
+ require('../index');
+ });
+ });
+ it('overrides `generateKey`', () => {
+ expect(globalThis.crypto.subtle.generateKey).not.toBe(originalGenerateKey);
+ });
+ it.each([
+ { __variant: 'P256', name: 'ECDSA', namedCurve: 'P-256' },
+ { __variant: 'P384', name: 'ECDSA', namedCurve: 'P-384' } as EcKeyGenParams,
+ { __variant: 'P521', name: 'ECDSA', namedCurve: 'P-521' } as EcKeyGenParams,
+ ...['RSASSA-PKCS1-v1_5', 'RSA-PSS'].flatMap(rsaAlgoName =>
+ ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512'].map(
+ hashName =>
+ ({
+ __variant: hashName,
+ hash: { name: hashName },
+ modulusLength: 2048,
+ name: rsaAlgoName,
+ publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
+ } as RsaHashedKeyGenParams)
+ )
+ ),
+ ])('calls the original `generateKey` when the algorithm is $name/$__variant', async algorithm => {
+ expect.assertions(1);
+ await globalThis.crypto.subtle.generateKey(algorithm, /* extractable */ false, ['sign', 'verify']);
+ expect(originalGenerateKey).toHaveBeenCalled();
+ });
+ it('delegates the call to the original `generateKey` when the algorithm is "Ed25519"', async () => {
+ expect.assertions(1);
+ const mockKeyPair = {};
+ (originalGenerateKey as jest.Mock).mockResolvedValue(mockKeyPair);
+ await expect(
+ globalThis.crypto.subtle.generateKey('Ed25519', /* extractable */ false, ['sign', 'verify'])
+ ).resolves.toBe(mockKeyPair);
+ });
+ it('calls the original `generateKey` once per call to `generateKey` when the algorithm is "Ed25519" but never again (parallel version)', async () => {
+ expect.assertions(1);
+ await Promise.all([
+ globalThis.crypto.subtle.generateKey('Ed25519', /* extractable */ false, ['sign', 'verify']),
+ globalThis.crypto.subtle.generateKey('Ed25519', /* extractable */ false, ['sign', 'verify']),
+ ]);
+ expect(originalGenerateKey).toHaveBeenCalledTimes(2);
+ });
+ it('calls the original `generateKey` once per call to `generateKey` when the algorithm is "Ed25519" but never again (serial version)', async () => {
+ expect.assertions(1);
+ await globalThis.crypto.subtle.generateKey('Ed25519', /* extractable */ false, ['sign', 'verify']);
+ await globalThis.crypto.subtle.generateKey('Ed25519', /* extractable */ false, ['sign', 'verify']);
+ expect(originalGenerateKey).toHaveBeenCalledTimes(2);
+ });
+ it('does not delegate `generateKey` calls to the polyfill', async () => {
+ expect.assertions(1);
+ await globalThis.crypto.subtle.generateKey('Ed25519', /* extractable */ false, ['sign', 'verify']);
+ expect(generateKeyPolyfill).not.toHaveBeenCalled();
+ });
+ });
+ describe('when required in an insecure context', () => {
+ beforeEach(() => {
+ globalThis.isSecureContext = false;
+ jest.isolateModules(() => {
+ require('../index');
+ });
+ });
+ if (__BROWSER__) {
+ it('does not override `generateKey`', () => {
+ expect(globalThis.crypto.subtle.generateKey).toBe(originalGenerateKey);
+ });
+ } else {
+ it('overrides `generateKey`', () => {
+ expect(globalThis.crypto.subtle.generateKey).not.toBe(originalGenerateKey);
+ });
+ }
+ });
+});
diff --git a/packages/webcrypto-ed25519-polyfill/src/__tests__/secrets-test.ts b/packages/webcrypto-ed25519-polyfill/src/__tests__/secrets-test.ts
new file mode 100644
index 000000000000..f2c69ff02e64
--- /dev/null
+++ b/packages/webcrypto-ed25519-polyfill/src/__tests__/secrets-test.ts
@@ -0,0 +1,90 @@
+import { generateKeyPolyfill } from '../secrets';
+
+describe('generateKeyPolyfill', () => {
+ it('stores secret key bytes in an internal cache', () => {
+ const weakMapSetSpy = jest.spyOn(WeakMap.prototype, 'set');
+ const expectedSecretKey = new Uint8Array(Array(32).fill(1));
+ jest.spyOn(globalThis.crypto, 'getRandomValues').mockReturnValue(expectedSecretKey);
+ generateKeyPolyfill(/* extractable */ false, ['sign', 'verify']);
+ expect(weakMapSetSpy).toHaveBeenCalledWith(expect.anything(), expectedSecretKey);
+ });
+ describe.each(['public', 'private'])('when generating a %s key', type => {
+ let keyPair: CryptoKeyPair;
+ beforeEach(() => {
+ keyPair = generateKeyPolyfill(/* extractable */ false, ['sign', 'verify']);
+ });
+ it(`has the algorithm "Ed25519"`, () => {
+ expect(keyPair).toHaveProperty([`${type}Key`, 'algorithm', 'name'], 'Ed25519');
+ });
+ it('has the string tag "CryptoKey"', () => {
+ expect(keyPair).toHaveProperty([`${type}Key`, Symbol.toStringTag], 'CryptoKey');
+ });
+ it(`has the type "${type}"`, () => {
+ expect(keyPair).toHaveProperty([`${type}Key`, 'type'], type);
+ });
+ });
+ it.each([true, false])(
+ "sets the private key's `extractable` to `false` when generating a key pair with the extractability `%s`",
+ extractable => {
+ const { privateKey } = generateKeyPolyfill(extractable, ['sign', 'verify']);
+ expect(privateKey).toHaveProperty('extractable', extractable);
+ }
+ );
+ it.each([true, false])(
+ "sets the public key's `extractable` to `true` when generating a key pair with the extractability `%s`",
+ extractable => {
+ const { publicKey } = generateKeyPolyfill(extractable, ['sign', 'verify']);
+ expect(publicKey).toHaveProperty('extractable', true);
+ }
+ );
+ it.each(['decrypt', 'deriveBits', 'deriveKey', 'encrypt', 'unwrapKey', 'wrapKey'] as KeyUsage[])(
+ 'fatals when the usage `%s` is specified',
+ usage => {
+ expect(() => generateKeyPolyfill(/* extractable */ false, [usage])).toThrow();
+ }
+ );
+ it("includes `sign` among the private key's usages when the `sign` usage is specified", () => {
+ const { privateKey } = generateKeyPolyfill(/* extractable */ false, ['sign']);
+ expect(privateKey).toHaveProperty('usages', expect.arrayContaining(['sign']));
+ });
+ it("sets the private key's usages to an empty array when the `sign` usage is not specified", () => {
+ const { privateKey } = generateKeyPolyfill(/* extractable */ false, ['verify']);
+ expect(privateKey).toHaveProperty('usages', []);
+ });
+ it("does not include `verify` among the private key's usages when the `verify` usage is specified", () => {
+ const { privateKey } = generateKeyPolyfill(/* extractable */ false, ['verify']);
+ expect(privateKey).toHaveProperty('usages', []);
+ });
+ it("does not include `sign` among the public key's usages when the `sign` usage is specified", () => {
+ const { publicKey } = generateKeyPolyfill(/* extractable */ false, ['sign']);
+ expect(publicKey).toHaveProperty('usages', []);
+ });
+ it("sets the public key's usages to an empty array when the `verify` usage is not specified", () => {
+ const { publicKey } = generateKeyPolyfill(/* extractable */ false, ['sign']);
+ expect(publicKey).toHaveProperty('usages', []);
+ });
+ it("includes `verify` among the public key's usages when the `verify` usage is specified", () => {
+ const { publicKey } = generateKeyPolyfill(/* extractable */ false, ['verify']);
+ expect(publicKey).toHaveProperty('usages', expect.arrayContaining(['verify']));
+ });
+ it('fatals when no key usages are specified', () => {
+ expect(() => generateKeyPolyfill(/* extractable */ false, [])).toThrow();
+ });
+ describe('when no CSPRNG can be found', () => {
+ let oldGetRandomValues: Crypto['getRandomValues'];
+ beforeEach(() => {
+ oldGetRandomValues = globalThis.crypto.getRandomValues;
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ globalThis.crypto.getRandomValues = undefined;
+ });
+ afterEach(() => {
+ globalThis.crypto.getRandomValues = oldGetRandomValues;
+ });
+ it('fatals', () => {
+ expect(() => {
+ generateKeyPolyfill(/* extractable */ false, ['sign', 'verify']);
+ }).toThrow();
+ });
+ });
+});
diff --git a/packages/webcrypto-ed25519-polyfill/src/index.ts b/packages/webcrypto-ed25519-polyfill/src/index.ts
new file mode 100644
index 000000000000..1b936f82edb3
--- /dev/null
+++ b/packages/webcrypto-ed25519-polyfill/src/index.ts
@@ -0,0 +1,72 @@
+import { generateKeyPolyfill } from './secrets';
+
+if (!__BROWSER__ || globalThis.isSecureContext) {
+ /**
+ * Create `crypto.subtle` if it doesn't exist.
+ */
+ const originalCryptoObject = (globalThis.crypto ||= {} as Crypto);
+ const originalSubtleCrypto = ((originalCryptoObject as Crypto & { subtle: SubtleCrypto }).subtle ||=
+ {} as SubtleCrypto);
+
+ /**
+ * Override `SubtleCrypto#generateKey`
+ */
+ const originalGenerateKey = originalSubtleCrypto.generateKey as SubtleCrypto['generateKey'] | undefined;
+ let originalGenerateKeySupportsEd25519: Promise | boolean | undefined;
+ originalSubtleCrypto.generateKey = (async (...args: Parameters) => {
+ const [algorithm] = args;
+ if (algorithm !== 'Ed25519') {
+ if (originalGenerateKey) {
+ return await originalGenerateKey.apply(originalSubtleCrypto, args);
+ } else {
+ throw new TypeError('No native `generateKey` function exists to handle this call');
+ }
+ }
+ let optimisticallyGeneratedKeyPair;
+ if (originalGenerateKeySupportsEd25519 === undefined) {
+ originalGenerateKeySupportsEd25519 = new Promise(resolve => {
+ if (!originalGenerateKey) {
+ resolve((originalGenerateKeySupportsEd25519 = false));
+ return;
+ }
+ originalGenerateKey
+ .apply(originalSubtleCrypto, args)
+ .then(keyPair => {
+ if (__DEV__) {
+ console.warn(
+ '`@solana/webcrypto-ed25519-polyfill` was included in an ' +
+ 'environment that supports Ed25519 key manipulation ' +
+ 'natively. Falling back to the native implementation. ' +
+ 'Consider including this polyfill only in environments where ' +
+ 'Ed25519 is not supported.'
+ );
+ }
+ if (originalSubtleCrypto.generateKey !== originalGenerateKey) {
+ originalSubtleCrypto.generateKey = originalGenerateKey;
+ }
+ optimisticallyGeneratedKeyPair = keyPair;
+ resolve((originalGenerateKeySupportsEd25519 = true));
+ })
+ .catch(() => {
+ resolve((originalGenerateKeySupportsEd25519 = false));
+ });
+ });
+ }
+ if (
+ typeof originalGenerateKeySupportsEd25519 === 'boolean'
+ ? originalGenerateKeySupportsEd25519
+ : await originalGenerateKeySupportsEd25519
+ ) {
+ if (optimisticallyGeneratedKeyPair) {
+ return optimisticallyGeneratedKeyPair;
+ } else if (originalGenerateKey) {
+ return await originalGenerateKey.apply(originalSubtleCrypto, args);
+ } else {
+ throw new TypeError('No native `generateKey` function exists to handle this call');
+ }
+ } else {
+ const [_, extractable, keyUsages] = args;
+ return generateKeyPolyfill(extractable, keyUsages);
+ }
+ }) as SubtleCrypto['generateKey'];
+}
diff --git a/packages/webcrypto-ed25519-polyfill/src/secrets.ts b/packages/webcrypto-ed25519-polyfill/src/secrets.ts
new file mode 100644
index 000000000000..b63ad5ab0d7b
--- /dev/null
+++ b/packages/webcrypto-ed25519-polyfill/src/secrets.ts
@@ -0,0 +1,82 @@
+/**
+ * HEY! <== SECRET KEY KOALA
+ * |/ <== WOULD LIKE YOUR
+ * ʕ·͡ᴥ·ʔ <== ATTENTION PLEASE
+ *
+ * Key material generated in this module must stay in this module. So long as the secrets cache and
+ * the methods that interact with it are not exported from `@solana/webcrypto-ed25519-polyfill`,
+ * accidental logging of the actual bytes of a secret key (eg. to the console, or to a remote
+ * server) should not be possible.
+ *
+ * WARNING: This does not imply that the secrets cache is secure against supply-chain attacks.
+ * Untrusted code in your JavaScript context can easily override `WeakMap.prototype.set` to steal
+ * private keys as they are written to the cache, without alerting you to its presence or affecting
+ * the regular operation of the cache.
+ */
+import { ed25519 } from '@noble/curves/ed25519';
+
+const PROHIBITED_KEY_USAGES = new Set([
+ 'decrypt',
+ 'deriveBits',
+ 'deriveKey',
+ 'encrypt',
+ 'unwrapKey',
+ 'wrapKey',
+]);
+
+let storageKeyBySecretKey_INTERNAL_ONLY_DO_NOT_EXPORT: WeakMap | undefined;
+function createKeyPairFromBytes(
+ bytes: Uint8Array,
+ extractable: boolean,
+ keyUsages: readonly KeyUsage[]
+): CryptoKeyPair {
+ const keyPair = createKeyPair_INTERNAL_ONLY_DO_NOT_EXPORT(extractable, keyUsages);
+ const cache = (storageKeyBySecretKey_INTERNAL_ONLY_DO_NOT_EXPORT ||= new WeakMap());
+ cache.set(keyPair.privateKey, bytes);
+ cache.set(keyPair.publicKey, bytes);
+ return keyPair;
+}
+
+function createKeyPair_INTERNAL_ONLY_DO_NOT_EXPORT(
+ extractable: boolean,
+ keyUsages: readonly KeyUsage[]
+): CryptoKeyPair {
+ if (keyUsages.length === 0) {
+ throw new DOMException('Usages cannot be empty when creating a key.', 'SyntaxError');
+ }
+ if (keyUsages.some(usage => PROHIBITED_KEY_USAGES.has(usage))) {
+ throw new DOMException('Unsupported key usage for an Ed25519 key.', 'SyntaxError');
+ }
+ const base = {
+ [Symbol.toStringTag]: 'CryptoKey',
+ algorithm: Object.freeze({ name: 'Ed25519' }),
+ };
+ const privateKey = {
+ ...base,
+ extractable,
+ type: 'private',
+ usages: Object.freeze(keyUsages.filter(usage => usage === 'sign')) as KeyUsage[],
+ } as CryptoKey;
+ const publicKey = {
+ ...base,
+ extractable: true,
+ type: 'public',
+ usages: Object.freeze(keyUsages.filter(usage => usage === 'verify')) as KeyUsage[],
+ } as CryptoKey;
+ return Object.freeze({
+ privateKey: Object.freeze(privateKey),
+ publicKey: Object.freeze(publicKey),
+ });
+}
+
+/**
+ * This function generates a key pair and stores the secret bytes associated with it in a
+ * module-private cache. Instead of vending the actual secret bytes, it returns a `CryptoKeyPair`
+ * that you can use with other methods in this package to produce signatures and derive public keys
+ * associated with the secret.
+ */
+export function generateKeyPolyfill(extractable: boolean, keyUsages: readonly KeyUsage[]): CryptoKeyPair {
+ const privateKeyBytes = ed25519.utils.randomPrivateKey();
+ const keyPair = createKeyPairFromBytes(privateKeyBytes, extractable, keyUsages);
+ return keyPair;
+}
diff --git a/packages/webcrypto-ed25519-polyfill/src/types/global.d.ts b/packages/webcrypto-ed25519-polyfill/src/types/global.d.ts
new file mode 100644
index 000000000000..2d0fcf698896
--- /dev/null
+++ b/packages/webcrypto-ed25519-polyfill/src/types/global.d.ts
@@ -0,0 +1,4 @@
+declare const __BROWSER__: boolean;
+declare const __DEV__: boolean;
+declare const __NODEJS__: boolean;
+declare const __REACTNATIVE__: boolean;
diff --git a/packages/webcrypto-ed25519-polyfill/tsconfig.declarations.json b/packages/webcrypto-ed25519-polyfill/tsconfig.declarations.json
new file mode 100644
index 000000000000..dc2d27bb09ff
--- /dev/null
+++ b/packages/webcrypto-ed25519-polyfill/tsconfig.declarations.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "emitDeclarationOnly": true,
+ "outDir": "./dist/types"
+ },
+ "extends": "./tsconfig.json",
+ "include": ["src/index.ts", "src/types"]
+}
diff --git a/packages/webcrypto-ed25519-polyfill/tsconfig.json b/packages/webcrypto-ed25519-polyfill/tsconfig.json
new file mode 100644
index 000000000000..48790bda0522
--- /dev/null
+++ b/packages/webcrypto-ed25519-polyfill/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "display": "@solana/keys",
+ "extends": "tsconfig/base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "lib": ["DOM", "ES2015", "ES2022.Error"]
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ec40f948407c..823a0b8e3726 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -873,6 +873,82 @@ importers:
packages/tsconfig: {}
+ packages/webcrypto-ed25519-polyfill:
+ dependencies:
+ '@noble/curves':
+ specifier: ^1.1.0
+ version: 1.1.0
+ devDependencies:
+ '@solana/eslint-config-solana':
+ specifier: ^1.0.1
+ version: 1.0.1(@typescript-eslint/eslint-plugin@5.57.1)(@typescript-eslint/parser@5.57.1)(eslint-plugin-jest@27.1.5)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-simple-import-sort@10.0.0)(eslint-plugin-sort-keys-fix@1.1.2)(eslint@8.37.0)(typescript@5.0.4)
+ '@swc/core':
+ specifier: ^1.3.18
+ version: 1.3.18
+ '@swc/jest':
+ specifier: ^0.2.26
+ version: 0.2.26(@swc/core@1.3.18)
+ '@types/jest':
+ specifier: ^29.5.2
+ version: 29.5.2
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^5.57.1
+ version: 5.57.1(@typescript-eslint/parser@5.57.1)(eslint@8.37.0)(typescript@5.0.4)
+ '@typescript-eslint/parser':
+ specifier: ^5.57.1
+ version: 5.57.1(eslint@8.37.0)(typescript@5.0.4)
+ build-scripts:
+ specifier: workspace:*
+ version: link:../build-scripts
+ eslint:
+ specifier: ^8.37.0
+ version: 8.37.0
+ eslint-plugin-jest:
+ specifier: ^27.1.5
+ version: 27.1.5(@typescript-eslint/eslint-plugin@5.57.1)(eslint@8.37.0)(jest@29.6.1)(typescript@5.0.4)
+ eslint-plugin-react-hooks:
+ specifier: ^4.6.0
+ version: 4.6.0(eslint@8.37.0)
+ eslint-plugin-sort-keys-fix:
+ specifier: ^1.1.2
+ version: 1.1.2
+ jest:
+ specifier: ^29.6.1
+ version: 29.6.1(@types/node@18.11.10)(ts-node@10.9.1)
+ jest-environment-jsdom:
+ specifier: ^29.6.0
+ version: 29.6.0
+ jest-runner-eslint:
+ specifier: ^2.1.0
+ version: 2.1.0(eslint@8.37.0)(jest@29.6.1)
+ jest-runner-prettier:
+ specifier: ^1.0.0
+ version: 1.0.0(jest@29.6.1)(prettier@2.8.8)
+ postcss:
+ specifier: ^8.4.12
+ version: 8.4.12
+ prettier:
+ specifier: ^2.8.8
+ version: 2.8.8
+ test-config:
+ specifier: workspace:*
+ version: link:../test-config
+ ts-node:
+ specifier: ^10.9.1
+ version: 10.9.1(@swc/core@1.3.18)(@types/node@18.11.10)(typescript@5.0.4)
+ tsconfig:
+ specifier: workspace:*
+ version: link:../tsconfig
+ tsup:
+ specifier: 6.7.0
+ version: 6.7.0(@swc/core@1.3.18)(postcss@8.4.12)(ts-node@10.9.1)(typescript@5.0.4)
+ typescript:
+ specifier: ^5.0.4
+ version: 5.0.4
+ version-from-git:
+ specifier: ^1.1.1
+ version: 1.1.1
+
packages:
/@aashutoshrathi/word-wrap@1.2.6:
@@ -2400,13 +2476,13 @@ packages:
'@types/node': 18.11.10
chalk: 4.1.2
cosmiconfig: 8.2.0
- cosmiconfig-typescript-loader: 4.3.0(@types/node@18.11.10)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.1.3)
+ cosmiconfig-typescript-loader: 4.3.0(@types/node@18.11.10)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.1.6)
lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2
lodash.uniq: 4.5.0
resolve-from: 5.0.0
ts-node: 10.9.1(@types/node@18.11.10)(typescript@5.1.6)
- typescript: 5.1.3
+ typescript: 5.1.6
transitivePeerDependencies:
- '@swc/core'
- '@swc/wasm'
@@ -2712,7 +2788,7 @@ packages:
dependencies:
ajv: 6.12.6
debug: 4.3.4(supports-color@8.1.1)
- espree: 9.5.2
+ espree: 9.6.0
globals: 13.20.0
ignore: 5.2.4
import-fresh: 3.3.0
@@ -3259,9 +3335,20 @@ packages:
resolution: {integrity: sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw==}
dependencies:
'@noble/hashes': 1.3.0
+ dev: false
+
+ /@noble/curves@1.1.0:
+ resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==}
+ dependencies:
+ '@noble/hashes': 1.3.1
/@noble/hashes@1.3.0:
resolution: {integrity: sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==}
+ dev: false
+
+ /@noble/hashes@1.3.1:
+ resolution: {integrity: sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==}
+ engines: {node: '>= 16'}
/@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -3875,8 +3962,8 @@ packages:
resolution: {integrity: sha512-PHaO0BdoiQRPpieC1p31wJsBaxwIOWLh8j2ocXNKX8boCQVldt26Jqm2tZE4KlrvnCIV78owPLv1pEUgqhxZ3w==}
dependencies:
'@babel/runtime': 7.22.3
- '@noble/curves': 1.0.0
- '@noble/hashes': 1.3.0
+ '@noble/curves': 1.1.0
+ '@noble/hashes': 1.3.1
'@solana/buffer-layout': 4.0.0
agentkeepalive: 4.2.1
bigint-buffer: 1.1.5
@@ -4536,7 +4623,7 @@ packages:
debug: 4.3.4(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
- semver: 7.5.1
+ semver: 7.5.3
tsutils: 3.21.0(typescript@5.0.3)
typescript: 5.0.3
transitivePeerDependencies:
@@ -4557,7 +4644,7 @@ packages:
debug: 4.3.4(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
- semver: 7.5.1
+ semver: 7.5.3
tsutils: 3.21.0(typescript@5.0.4)
typescript: 5.0.4
transitivePeerDependencies:
@@ -4662,7 +4749,7 @@ packages:
'@typescript-eslint/typescript-estree': 5.57.1(typescript@5.0.3)
eslint: 8.37.0
eslint-scope: 5.1.1
- semver: 7.5.1
+ semver: 7.5.3
transitivePeerDependencies:
- supports-color
- typescript
@@ -4682,7 +4769,7 @@ packages:
'@typescript-eslint/typescript-estree': 5.57.1(typescript@5.0.4)
eslint: 8.37.0
eslint-scope: 5.1.1
- semver: 7.5.1
+ semver: 7.5.3
transitivePeerDependencies:
- supports-color
- typescript
@@ -4702,7 +4789,7 @@ packages:
'@typescript-eslint/typescript-estree': 5.59.11(typescript@5.0.4)
eslint: 8.37.0
eslint-scope: 5.1.1
- semver: 7.5.1
+ semver: 7.5.3
transitivePeerDependencies:
- supports-color
- typescript
@@ -4837,14 +4924,6 @@ packages:
dependencies:
acorn: 8.10.0
- /acorn-jsx@5.3.2(acorn@8.8.2):
- resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
- peerDependencies:
- acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
- dependencies:
- acorn: 8.8.2
- dev: true
-
/acorn-walk@7.2.0:
resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==}
engines: {node: '>=0.4.0'}
@@ -4863,17 +4942,12 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
- /acorn@8.8.2:
- resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==}
- engines: {node: '>=0.4.0'}
- hasBin: true
-
/agadoo@3.0.0:
resolution: {integrity: sha512-gq+fjT3Ilrhb88Jf+vYMjdO/+3znYfa7vJ4IMLPFsBPUxglnr40Ed3yCLrW6IABdJAedB94b2BkqR6I04lh3dg==}
hasBin: true
dependencies:
'@rollup/plugin-virtual': 3.0.1(rollup@3.25.1)
- acorn: 8.8.2
+ acorn: 8.10.0
rollup: 3.25.1
dev: true
@@ -5751,7 +5825,7 @@ packages:
vary: 1.1.2
dev: true
- /cosmiconfig-typescript-loader@4.3.0(@types/node@18.11.10)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.1.3):
+ /cosmiconfig-typescript-loader@4.3.0(@types/node@18.11.10)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.1.6):
resolution: {integrity: sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==}
engines: {node: '>=12', npm: '>=6'}
peerDependencies:
@@ -5763,7 +5837,7 @@ packages:
'@types/node': 18.11.10
cosmiconfig: 8.2.0
ts-node: 10.9.1(@types/node@18.11.10)(typescript@5.1.6)
- typescript: 5.1.3
+ typescript: 5.1.6
dev: true
/cosmiconfig@7.1.0:
@@ -6760,8 +6834,8 @@ packages:
resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
- acorn: 8.8.2
- acorn-jsx: 5.3.2(acorn@8.8.2)
+ acorn: 8.10.0
+ acorn-jsx: 5.3.2(acorn@8.10.0)
eslint-visitor-keys: 3.4.1
dev: true
@@ -11720,7 +11794,7 @@ packages:
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 18.11.10
- acorn: 8.8.2
+ acorn: 8.10.0
acorn-walk: 8.2.0
arg: 4.1.3
create-require: 1.1.1
@@ -12095,12 +12169,6 @@ packages:
engines: {node: '>=12.20'}
hasBin: true
- /typescript@5.1.3:
- resolution: {integrity: sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==}
- engines: {node: '>=14.17'}
- hasBin: true
- dev: true
-
/typescript@5.1.6:
resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
engines: {node: '>=14.17'}