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

fix: deriveKey in webkit linux (workaround) #313

Merged
merged 12 commits into from
Apr 4, 2023
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
"test:chrome-webworker": "aegir test -t webworker",
"test:firefox": "aegir test -t browser -- --browser firefox",
"test:firefox-webworker": "aegir test -t webworker -- --browser firefox",
"test:webkit": "bash -c '[ \"${CI}\" == \"true\" ] && playwright install-deps'; aegir test -t browser -- --browser webkit",
"test:node": "aegir test -t node --cov",
"test:electron-main": "aegir test -t electron-main",
"release": "aegir release",
Expand Down
40 changes: 33 additions & 7 deletions src/ciphers/aes-gcm.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ import { fromString } from 'uint8arrays/from-string'
import webcrypto from '../webcrypto.js'
import type { CreateOptions, AESCipher } from './interface.js'

export function isWebkitLinux (): boolean {
return typeof navigator !== 'undefined' && navigator.userAgent.includes('Safari') && navigator.userAgent.includes('Linux') && !navigator.userAgent.includes('Chrome')
}

// WebKit on Linux does not support deriving a key from an empty PBKDF2 key.
// So, as a workaround, we provide the generated key as a constant. We test that
// this generated key is accurate in test/workaround.spec.ts
// Generated via:
// await crypto.subtle.exportKey('jwk',
// await crypto.subtle.deriveKey(
// { name: 'PBKDF2', salt: new Uint8Array(16), iterations: 32767, hash: { name: 'SHA-256' } },
// await crypto.subtle.importKey('raw', new Uint8Array(0), { name: 'PBKDF2' }, false, ['deriveKey']),
// { name: 'AES-GCM', length: 128 }, true, ['encrypt', 'decrypt'])
// )
export const derivedEmptyPasswordKey = { alg: 'A128GCM', ext: true, k: 'scm9jmO_4BJAgdwWGVulLg', key_ops: ['encrypt', 'decrypt'], kty: 'oct' }

// Based off of code from https://github.com/luke-park/SecureCompatibleEncryptionExamples

export function create (opts?: CreateOptions): AESCipher {
Expand All @@ -29,10 +45,15 @@ export function create (opts?: CreateOptions): AESCipher {
password = fromString(password)
}

let cryptoKey: CryptoKey
if (password.length === 0 && isWebkitLinux()) {
cryptoKey = await crypto.subtle.importKey('jwk', derivedEmptyPasswordKey, { name: 'AES-GCM' }, true, ['encrypt'])
} else {
// Derive a key using PBKDF2.
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } }
const rawKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey', 'deriveBits'])
const cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['encrypt'])
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } }
const rawKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey'])
cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['encrypt'])
}

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

// Derive the key using PBKDF2.
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } }
const rawKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey', 'deriveBits'])
const cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['decrypt'])
let cryptoKey: CryptoKey
if (password.length === 0 && isWebkitLinux()) {
cryptoKey = await crypto.subtle.importKey('jwk', derivedEmptyPasswordKey, { name: 'AES-GCM' }, true, ['decrypt'])
} else {
// Derive the key using PBKDF2.
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } }
const rawKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey'])
cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['decrypt'])
}

// Decrypt the string.
const plaintext = await crypto.subtle.decrypt(aesGcm, cryptoKey, ciphertext)
Expand Down
18 changes: 18 additions & 0 deletions test/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,22 @@ describe('libp2p-crypto', function () {
return expect(crypto.keys.generateKeyPairFromSeed('invalid-key-type', seed, 512)).to.eventually.be.rejected.with.property('code', 'ERR_UNSUPPORTED_KEY_DERIVATION_TYPE')
})

// https://github.com/libp2p/js-libp2p-crypto/issues/314
function isSafari (): boolean {
return typeof navigator !== 'undefined' && navigator.userAgent.includes('AppleWebKit') && !navigator.userAgent.includes('Chrome') && navigator.userAgent.includes('Mac')
}

// marshalled keys seem to be slightly different
// unsure as to if this is just a difference in encoding
// or a bug
describe('go interop', () => {
it('unmarshals private key', async () => {
if (isSafari()) {
// eslint-disable-next-line no-console
console.warn('Skipping test in Safari. Known bug: https://github.com/libp2p/js-libp2p-crypto/issues/314')
return
}

const key = await crypto.keys.unmarshalPrivateKey(fixtures.private.key)
const hash = fixtures.private.hash
expect(fixtures.private.key).to.eql(key.bytes)
Expand All @@ -83,6 +94,13 @@ describe('libp2p-crypto', function () {
it('unmarshal -> marshal, private key', async () => {
const key = await crypto.keys.unmarshalPrivateKey(fixtures.private.key)
const marshalled = crypto.keys.marshalPrivateKey(key)
if (isSafari()) {
// eslint-disable-next-line no-console
console.warn('Running differnt test in Safari. Known bug: https://github.com/libp2p/js-libp2p-crypto/issues/314')
const key2 = await crypto.keys.unmarshalPrivateKey(marshalled)
expect(key2.bytes).to.eql(key.bytes)
return
}
expect(marshalled).to.eql(fixtures.private.key)
})

Expand Down
18 changes: 18 additions & 0 deletions test/keys/ed25519.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,24 @@ describe('ed25519', function () {
expect(key.equals(importedKey)).to.equal(true)
})

it('should export a libp2p-key with no password to encrypt', async () => {
const key = await crypto.keys.generateKeyPair('Ed25519')

if (!(key instanceof Ed25519PrivateKey)) {
throw new Error('Key was incorrect type')
}

const encryptedKey = await key.export('')
// Import the key
const importedKey = await crypto.keys.importKey(encryptedKey, '')

if (!(importedKey instanceof Ed25519PrivateKey)) {
throw new Error('Key was incorrect type')
}

expect(key.equals(importedKey)).to.equal(true)
})

it('should fail to import libp2p-key with wrong password', async () => {
const key = await crypto.keys.generateKeyPair('Ed25519')
const encryptedKey = await key.export('my secret', 'libp2p-key')
Expand Down
21 changes: 21 additions & 0 deletions test/keys/importer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* eslint max-nested-callbacks: ["error", 8] */
/* eslint-env mocha */
import { expect } from 'aegir/chai'

import { importer } from '../../src/keys/importer.js'
import { exporter } from '../../src/keys/exporter.js'

describe('libp2p-crypto importer/exporter', function () {
it('roundtrips', async () => {
for (const password of ['', 'password']) {
const secret = new Uint8Array(32)
for (let i = 0; i < secret.length; i++) {
secret[i] = i
}

const exported = await exporter(secret, password)
const imported = await importer(exported, password)
expect(imported).to.deep.equal(secret)
}
})
})
26 changes: 26 additions & 0 deletions test/workaround.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

/* eslint-env mocha */
import { isWebkitLinux, derivedEmptyPasswordKey } from '../src/ciphers/aes-gcm.browser.js'
import { expect } from 'aegir/chai'

describe('Constant derived key is generated correctly', () => {
it('Generates correctly', async () => {
if (isWebkitLinux() || typeof crypto === 'undefined') {
// WebKit Linux can't generate this. Hence the workaround.
return
}

const generatedKey = await crypto.subtle.exportKey('jwk',
await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: new Uint8Array(16), iterations: 32767, hash: { name: 'SHA-256' } },
await crypto.subtle.importKey('raw', new Uint8Array(0), { name: 'PBKDF2' }, false, ['deriveKey']),
{ name: 'AES-GCM', length: 128 }, true, ['encrypt', 'decrypt'])
)

// Webkit macos flips these. Sort them so they match.
derivedEmptyPasswordKey.key_ops.sort()
generatedKey?.key_ops?.sort()

expect(generatedKey).to.eql(derivedEmptyPasswordKey)
})
})