Created
May 2, 2022 08:14
-
-
Save bmatusiak/66c7c72771f18c2010d89a7b0dbce357 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
module.exports = function(pwd, extra) { | |
var forge = require("node-forge"); | |
forge.options.usePureJavaScript = true; | |
var EC = require('elliptic').ec; | |
return new Promise((resolve, reject) => { | |
var ec_p256 = new EC('p256'); | |
if (!pwd) | |
pwd = forge.random.getBytesSync(32); | |
var privateKey_d = forge.md.sha256.create().update("d").update(pwd); //decrypt key | |
var privateKey_s = forge.md.sha256.create().update("s").update(pwd); //sign key | |
if (extra) { | |
if (extra instanceof String) | |
extra = [extra]; | |
for (let i = 0; i < extra.length; i++) { | |
privateKey_s = privateKey_s.update(extra[i]); | |
privateKey_d = privateKey_d.update(extra[i]); | |
} | |
} | |
privateKey_s = privateKey_s.digest().toHex(); | |
privateKey_d = privateKey_d.digest().toHex(); | |
var keyA_d = ec_p256.keyFromPrivate(privateKey_d, "hex"); | |
var validation = keyA_d.validate(); | |
if (validation.reason) | |
return reject(validation.reason); | |
var keyA_s = ec_p256.keyFromPrivate(privateKey_s, "hex"); | |
validation = keyA_s.validate(); | |
if (validation.reason) | |
return reject(validation.reason); | |
resolve({ | |
pub: keyBuffer_to_jwk("ECDSA", Buffer.from(keyA_s.getPublic("hex"), "hex")), | |
priv: arrayBufToBase64UrlEncode(Buffer.from(privateKey_s, "hex")), | |
epub: keyBuffer_to_jwk("ECDH", Buffer.from(keyA_d.getPublic("hex"), "hex")), | |
epriv: arrayBufToBase64UrlEncode(Buffer.from(privateKey_d, "hex")), | |
// secret: arrayBufToBase64UrlEncode(Buffer.from(keyA_d.derive(keyA_s.getPublic()).toString("hex"), "hex")) | |
}); | |
}); | |
function arrayBufToBase64UrlEncode(buf) { | |
var btoa = require("btoa"); | |
var binary = ''; | |
var bytes = new Uint8Array(buf); | |
for (var i = 0; i < bytes.byteLength; i++) { | |
binary += String.fromCharCode(bytes[i]); | |
} | |
return btoa(binary) | |
.replace(/\//g, '_') | |
.replace(/=/g, '') | |
.replace(/\+/g, '-'); | |
} | |
function keyBuffer_to_jwk(type, raw_publicKeyRawBuffer) { | |
var key; | |
switch (type) { | |
case "ECDSA": | |
case "ECDH": | |
if (raw_publicKeyRawBuffer[0] == 4) | |
key = arrayBufToBase64UrlEncode(raw_publicKeyRawBuffer.slice(1, 33)) + '.' + arrayBufToBase64UrlEncode(raw_publicKeyRawBuffer.slice(33, 66)); | |
break; | |
default: | |
key = false; | |
break; | |
} | |
return key; | |
} | |
}; |
Here's a version that works in both node and browser without bundling, and without the elliptic
dependency:
let _p256;
async function loadCurve() {
if (_p256) return _p256;
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
if (isBrowser) {
try {
const mod = await import('@noble/curves/p256');
_p256 = mod.p256;
} catch (_) {
const { p256 } = await import('https://esm.sh/@noble/curves/p256');
_p256 = p256;
}
} else {
const mod = await import('@noble/curves/p256');
_p256 = mod.p256;
}
return _p256;
}
let _subtle;
async function loadCryptoSubtle() {
if (_subtle) return _subtle;
if (typeof window !== 'undefined' && window.crypto?.subtle) {
_subtle = window.crypto.subtle;
} else {
const { webcrypto } = await import('crypto');
if (!webcrypto?.subtle) {
throw new Error('No SubtleCrypto available');
}
_subtle = webcrypto.subtle;
}
return _subtle;
}
const TEXT_ENCODER = new TextEncoder();
function normalizeString(s) { return s.normalize('NFC').trim(); }
function arrayBufToBase64UrlEncode(buf) {
const str = String.fromCharCode(...new Uint8Array(buf));
return btoa(str)
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function keyBufferToJwk(pubBuf) {
if (pubBuf[0] !== 4) throw new Error('Expected uncompressed point');
const x = pubBuf.slice(1, 33), y = pubBuf.slice(33, 65);
return `${arrayBufToBase64UrlEncode(x)}.${arrayBufToBase64UrlEncode(y)}`;
}
async function stretchKey(input, salt, iterations = 300_000) {
const subtle = await loadCryptoSubtle();
const baseKey = await subtle.importKey('raw', input, { name: 'PBKDF2' }, false, ['deriveBits']);
const bits = await subtle.deriveBits(
{ name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },
baseKey,
256
);
return new Uint8Array(bits);
}
/**
* @param {string|Uint8Array} [pwd]
* @param {string|string[]} [extra]
*/
export async function derivePair(pwd, extra) {
let pwdBytes;
if (pwd) {
pwdBytes = typeof pwd === 'string'
? TEXT_ENCODER.encode(normalizeString(pwd))
: pwd;
} else {
// full crypto.getRandomValues
let cryptoGlobal = typeof window !== 'undefined' && window.crypto
? window.crypto
: (await import('crypto')).webcrypto;
const rand = new Uint8Array(32);
cryptoGlobal.getRandomValues(rand);
pwdBytes = rand;
}
const extras = extra
? (Array.isArray(extra) ? extra : [extra]).map(e => normalizeString(e.toString()))
: [];
const extraBuf = TEXT_ENCODER.encode(extras.join('|'));
const combined = new Uint8Array(pwdBytes.length + extraBuf.length);
combined.set(pwdBytes, 0);
combined.set(extraBuf, pwdBytes.length);
if (combined.length < 16) throw new Error('Insufficient input entropy.');
const version = 'v1';
const salts = {
signing: TEXT_ENCODER.encode(`signing-${version}`),
encryption: TEXT_ENCODER.encode(`encryption-${version}`)
};
const [privS, privD] = await Promise.all([
stretchKey(combined, salts.signing),
stretchKey(combined, salts.encryption)
]);
const p256 = await loadCurve();
async function makeKP(priv) {
const pubRaw = p256.getPublicKey(priv, false);
return {
pub : keyBufferToJwk(pubRaw),
priv: arrayBufToBase64UrlEncode(priv)
};
}
const [sigKP, ecdhKP] = await Promise.all([makeKP(privS), makeKP(privD)]);
return {
...sigKP,
...ecdhKP,
epub: ecdhKP.pub,
epriv: ecdhKP.priv
};
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
An updated version using
@noble/curves
for much smaller footprint - ~40Kb against 155 Kb ofelliptic