-
Notifications
You must be signed in to change notification settings - Fork 916
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(experimental): a polyfill for
generateKey()
that implement…
…s Ed25519 key generation in userspace ## Summary For environments where Ed25519 key generation is not supported, this polyfill injects a suitable implementation that uses `@noble/curves/ed25519` behind the scenes. Here's the list of environments that do support it: WICG/webcrypto-secure-curves#20 ## Test Plan ``` cd packages/webcrypto-ed25519-polyfill pnpm test:unit:browser pnpm test:unit:node ```
- Loading branch information
1 parent
9134ea7
commit 40b6641
Showing
15 changed files
with
720 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
dist/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
engine-strict=true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
dist/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
[![npm][npm-image]][npm-url] | ||
[![npm-downloads][npm-downloads-image]][npm-url] | ||
[![semantic-release][semantic-release-image]][semantic-release-url] | ||
<br /> | ||
[![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']); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
{ | ||
"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": false, | ||
"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:treeshakability:browser": "agadoo dist/index.browser.js", | ||
"test:treeshakability:native": "agadoo dist/index.node.js", | ||
"test:treeshakability:node": "agadoo dist/index.native.js", | ||
"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 <[email protected]>", | ||
"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", | ||
"agadoo": "^3.0.0", | ||
"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" | ||
} | ||
] | ||
} | ||
} |
185 changes: 185 additions & 0 deletions
185
packages/webcrypto-ed25519-polyfill/src/__tests__/index-test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
} | ||
}); | ||
}); |
Oops, something went wrong.