mirror of
https://github.com/openpgpjs/openpgpjs.git
synced 2025-06-12 09:06:50 +00:00
Merge pull request #1782
This commit is contained in:
commit
79014f00f0
23
README.md
23
README.md
@ -57,19 +57,18 @@ library to convert back and forth between them.
|
|||||||
|
|
||||||
| Curve | Encryption | Signature | NodeCrypto | WebCrypto | Constant-Time |
|
| Curve | Encryption | Signature | NodeCrypto | WebCrypto | Constant-Time |
|
||||||
|:---------------:|:----------:|:---------:|:----------:|:---------:|:-----------------:|
|
|:---------------:|:----------:|:---------:|:----------:|:---------:|:-----------------:|
|
||||||
| curve25519 | ECDH | N/A | No | No | Algorithmically** |
|
| curve25519 | ECDH | N/A | No | Yes* | If native** |
|
||||||
| ed25519 | N/A | EdDSA | No | No | Algorithmically** |
|
| ed25519 | N/A | EdDSA | No | Yes* | If native** |
|
||||||
| nistP256 | ECDH | ECDSA | Yes* | Yes* | If native*** |
|
| nistP256 | ECDH | ECDSA | Yes* | Yes* | If native** |
|
||||||
| nistP384 | ECDH | ECDSA | Yes* | Yes* | If native*** |
|
| nistP384 | ECDH | ECDSA | Yes* | Yes* | If native** |
|
||||||
| nistP521 | ECDH | ECDSA | Yes* | Yes* | If native*** |
|
| nistP521 | ECDH | ECDSA | Yes* | Yes* | If native** |
|
||||||
| brainpoolP256r1 | ECDH | ECDSA | Yes* | No | If native*** |
|
| brainpoolP256r1 | ECDH | ECDSA | Yes* | No | If native** |
|
||||||
| brainpoolP384r1 | ECDH | ECDSA | Yes* | No | If native*** |
|
| brainpoolP384r1 | ECDH | ECDSA | Yes* | No | If native** |
|
||||||
| brainpoolP512r1 | ECDH | ECDSA | Yes* | No | If native*** |
|
| brainpoolP512r1 | ECDH | ECDSA | Yes* | No | If native** |
|
||||||
| secp256k1 | ECDH | ECDSA | Yes* | No | If native*** |
|
| secp256k1 | ECDH | ECDSA | Yes* | No | If native** |
|
||||||
|
|
||||||
\* when available
|
\* when available
|
||||||
\** the curve25519 and ed25519 implementations are algorithmically constant-time, but may not be constant-time after optimizations of the JavaScript compiler
|
\** these curves are only constant-time if the underlying native implementation is available and constant-time
|
||||||
\*** these curves are only constant-time if the underlying native implementation is available and constant-time
|
|
||||||
|
|
||||||
* If the user's browser supports [native WebCrypto](https://caniuse.com/#feat=cryptography) via the `window.crypto.subtle` API, this will be used. Under Node.js the native [crypto module](https://nodejs.org/api/crypto.html#crypto_crypto) is used.
|
* If the user's browser supports [native WebCrypto](https://caniuse.com/#feat=cryptography) via the `window.crypto.subtle` API, this will be used. Under Node.js the native [crypto module](https://nodejs.org/api/crypto.html#crypto_crypto) is used.
|
||||||
|
|
||||||
|
@ -20,16 +20,15 @@
|
|||||||
* @module crypto/public_key/elliptic/ecdh
|
* @module crypto/public_key/elliptic/ecdh
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import nacl from '@openpgp/tweetnacl';
|
|
||||||
import { CurveWithOID, jwkToRawPublic, rawPublicToJWK, privateToJWK, validateStandardParams, checkPublicPointEnconding } from './oid_curves';
|
import { CurveWithOID, jwkToRawPublic, rawPublicToJWK, privateToJWK, validateStandardParams, checkPublicPointEnconding } from './oid_curves';
|
||||||
import * as aesKW from '../../aes_kw';
|
import * as aesKW from '../../aes_kw';
|
||||||
import { getRandomBytes } from '../../random';
|
|
||||||
import hash from '../../hash';
|
import hash from '../../hash';
|
||||||
import enums from '../../../enums';
|
import enums from '../../../enums';
|
||||||
import util from '../../../util';
|
import util from '../../../util';
|
||||||
import { b64ToUint8Array } from '../../../encoding/base64';
|
import { b64ToUint8Array } from '../../../encoding/base64';
|
||||||
import * as pkcs5 from '../../pkcs5';
|
import * as pkcs5 from '../../pkcs5';
|
||||||
import { getCipherParams } from '../../cipher';
|
import { getCipherParams } from '../../cipher';
|
||||||
|
import { generateEphemeralEncryptionMaterial as ecdhXGenerateEphemeralEncryptionMaterial, recomputeSharedSecret as ecdhXRecomputeSharedSecret } from './ecdh_x';
|
||||||
|
|
||||||
const webCrypto = util.getWebCrypto();
|
const webCrypto = util.getWebCrypto();
|
||||||
const nodeCrypto = util.getNodeCrypto();
|
const nodeCrypto = util.getNodeCrypto();
|
||||||
@ -92,10 +91,8 @@ async function kdf(hashAlgo, X, length, param, stripLeading = false, stripTraili
|
|||||||
async function genPublicEphemeralKey(curve, Q) {
|
async function genPublicEphemeralKey(curve, Q) {
|
||||||
switch (curve.type) {
|
switch (curve.type) {
|
||||||
case 'curve25519Legacy': {
|
case 'curve25519Legacy': {
|
||||||
const d = getRandomBytes(32);
|
const { sharedSecret: sharedKey, ephemeralPublicKey } = await ecdhXGenerateEphemeralEncryptionMaterial(enums.publicKey.x25519, Q.subarray(1));
|
||||||
const { secretKey, sharedKey } = await genPrivateEphemeralKey(curve, Q, null, d);
|
const publicKey = util.concatUint8Array([new Uint8Array([curve.wireFormatLeadingByte]), ephemeralPublicKey]);
|
||||||
let { publicKey } = nacl.box.keyPair.fromSecretKey(secretKey);
|
|
||||||
publicKey = util.concatUint8Array([new Uint8Array([curve.wireFormatLeadingByte]), publicKey]);
|
|
||||||
return { publicKey, sharedKey }; // Note: sharedKey is little-endian here, unlike below
|
return { publicKey, sharedKey }; // Note: sharedKey is little-endian here, unlike below
|
||||||
}
|
}
|
||||||
case 'web':
|
case 'web':
|
||||||
@ -159,7 +156,7 @@ async function genPrivateEphemeralKey(curve, V, Q, d) {
|
|||||||
switch (curve.type) {
|
switch (curve.type) {
|
||||||
case 'curve25519Legacy': {
|
case 'curve25519Legacy': {
|
||||||
const secretKey = d.slice().reverse();
|
const secretKey = d.slice().reverse();
|
||||||
const sharedKey = nacl.scalarMult(secretKey, V.subarray(1));
|
const sharedKey = await ecdhXRecomputeSharedSecret(enums.publicKey.x25519, V.subarray(1), Q.subarray(1), secretKey);
|
||||||
return { secretKey, sharedKey }; // Note: sharedKey is little-endian here, unlike below
|
return { secretKey, sharedKey }; // Note: sharedKey is little-endian here, unlike below
|
||||||
}
|
}
|
||||||
case 'web':
|
case 'web':
|
||||||
|
@ -11,6 +11,7 @@ import enums from '../../../enums';
|
|||||||
import util from '../../../util';
|
import util from '../../../util';
|
||||||
import computeHKDF from '../../hkdf';
|
import computeHKDF from '../../hkdf';
|
||||||
import { getCipherParams } from '../../cipher';
|
import { getCipherParams } from '../../cipher';
|
||||||
|
import { b64ToUint8Array, uint8ArrayToB64 } from '../../../encoding/base64';
|
||||||
|
|
||||||
const HKDF_INFO = {
|
const HKDF_INFO = {
|
||||||
x25519: util.encodeUTF8('OpenPGP X25519'),
|
x25519: util.encodeUTF8('OpenPGP X25519'),
|
||||||
@ -24,12 +25,28 @@ const HKDF_INFO = {
|
|||||||
*/
|
*/
|
||||||
export async function generate(algo) {
|
export async function generate(algo) {
|
||||||
switch (algo) {
|
switch (algo) {
|
||||||
case enums.publicKey.x25519: {
|
case enums.publicKey.x25519:
|
||||||
// k stays in little-endian, unlike legacy ECDH over curve25519
|
try {
|
||||||
const k = getRandomBytes(32);
|
const webCrypto = util.getWebCrypto();
|
||||||
const { publicKey: A } = x25519.box.keyPair.fromSecretKey(k);
|
const webCryptoKey = await webCrypto.generateKey('X25519', true, ['deriveKey', 'deriveBits']);
|
||||||
return { A, k };
|
|
||||||
}
|
const privateKey = await webCrypto.exportKey('jwk', webCryptoKey.privateKey);
|
||||||
|
const publicKey = await webCrypto.exportKey('jwk', webCryptoKey.publicKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
A: new Uint8Array(b64ToUint8Array(publicKey.x)),
|
||||||
|
k: b64ToUint8Array(privateKey.d, true)
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name !== 'NotSupportedError') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
// k stays in little-endian, unlike legacy ECDH over curve25519
|
||||||
|
const k = getRandomBytes(32);
|
||||||
|
const { publicKey: A } = x25519.box.keyPair.fromSecretKey(k);
|
||||||
|
return { A, k };
|
||||||
|
}
|
||||||
|
|
||||||
case enums.publicKey.x448: {
|
case enums.publicKey.x448: {
|
||||||
const x448 = await util.getNobleCurve(enums.publicKey.x448);
|
const x448 = await util.getNobleCurve(enums.publicKey.x448);
|
||||||
const k = x448.utils.randomPrivateKey();
|
const k = x448.utils.randomPrivateKey();
|
||||||
@ -87,16 +104,14 @@ export async function validateParams(algo, A, k) {
|
|||||||
* @async
|
* @async
|
||||||
*/
|
*/
|
||||||
export async function encrypt(algo, data, recipientA) {
|
export async function encrypt(algo, data, recipientA) {
|
||||||
|
const { ephemeralPublicKey, sharedSecret } = await generateEphemeralEncryptionMaterial(algo, recipientA);
|
||||||
|
const hkdfInput = util.concatUint8Array([
|
||||||
|
ephemeralPublicKey,
|
||||||
|
recipientA,
|
||||||
|
sharedSecret
|
||||||
|
]);
|
||||||
switch (algo) {
|
switch (algo) {
|
||||||
case enums.publicKey.x25519: {
|
case enums.publicKey.x25519: {
|
||||||
const ephemeralSecretKey = getRandomBytes(32);
|
|
||||||
const sharedSecret = x25519.scalarMult(ephemeralSecretKey, recipientA);
|
|
||||||
const { publicKey: ephemeralPublicKey } = x25519.box.keyPair.fromSecretKey(ephemeralSecretKey);
|
|
||||||
const hkdfInput = util.concatUint8Array([
|
|
||||||
ephemeralPublicKey,
|
|
||||||
recipientA,
|
|
||||||
sharedSecret
|
|
||||||
]);
|
|
||||||
const cipherAlgo = enums.symmetric.aes128;
|
const cipherAlgo = enums.symmetric.aes128;
|
||||||
const { keySize } = getCipherParams(cipherAlgo);
|
const { keySize } = getCipherParams(cipherAlgo);
|
||||||
const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize);
|
const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize);
|
||||||
@ -104,15 +119,6 @@ export async function encrypt(algo, data, recipientA) {
|
|||||||
return { ephemeralPublicKey, wrappedKey };
|
return { ephemeralPublicKey, wrappedKey };
|
||||||
}
|
}
|
||||||
case enums.publicKey.x448: {
|
case enums.publicKey.x448: {
|
||||||
const x448 = await util.getNobleCurve(enums.publicKey.x448);
|
|
||||||
const ephemeralSecretKey = x448.utils.randomPrivateKey();
|
|
||||||
const sharedSecret = x448.getSharedSecret(ephemeralSecretKey, recipientA);
|
|
||||||
const ephemeralPublicKey = x448.getPublicKey(ephemeralSecretKey);
|
|
||||||
const hkdfInput = util.concatUint8Array([
|
|
||||||
ephemeralPublicKey,
|
|
||||||
recipientA,
|
|
||||||
sharedSecret
|
|
||||||
]);
|
|
||||||
const cipherAlgo = enums.symmetric.aes256;
|
const cipherAlgo = enums.symmetric.aes256;
|
||||||
const { keySize } = getCipherParams(enums.symmetric.aes256);
|
const { keySize } = getCipherParams(enums.symmetric.aes256);
|
||||||
const encryptionKey = await computeHKDF(enums.hash.sha512, hkdfInput, new Uint8Array(), HKDF_INFO.x448, keySize);
|
const encryptionKey = await computeHKDF(enums.hash.sha512, hkdfInput, new Uint8Array(), HKDF_INFO.x448, keySize);
|
||||||
@ -137,27 +143,20 @@ export async function encrypt(algo, data, recipientA) {
|
|||||||
* @async
|
* @async
|
||||||
*/
|
*/
|
||||||
export async function decrypt(algo, ephemeralPublicKey, wrappedKey, A, k) {
|
export async function decrypt(algo, ephemeralPublicKey, wrappedKey, A, k) {
|
||||||
|
const sharedSecret = await recomputeSharedSecret(algo, ephemeralPublicKey, A, k);
|
||||||
|
const hkdfInput = util.concatUint8Array([
|
||||||
|
ephemeralPublicKey,
|
||||||
|
A,
|
||||||
|
sharedSecret
|
||||||
|
]);
|
||||||
switch (algo) {
|
switch (algo) {
|
||||||
case enums.publicKey.x25519: {
|
case enums.publicKey.x25519: {
|
||||||
const sharedSecret = x25519.scalarMult(k, ephemeralPublicKey);
|
|
||||||
const hkdfInput = util.concatUint8Array([
|
|
||||||
ephemeralPublicKey,
|
|
||||||
A,
|
|
||||||
sharedSecret
|
|
||||||
]);
|
|
||||||
const cipherAlgo = enums.symmetric.aes128;
|
const cipherAlgo = enums.symmetric.aes128;
|
||||||
const { keySize } = getCipherParams(cipherAlgo);
|
const { keySize } = getCipherParams(cipherAlgo);
|
||||||
const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize);
|
const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize);
|
||||||
return aesKW.unwrap(cipherAlgo, encryptionKey, wrappedKey);
|
return aesKW.unwrap(cipherAlgo, encryptionKey, wrappedKey);
|
||||||
}
|
}
|
||||||
case enums.publicKey.x448: {
|
case enums.publicKey.x448: {
|
||||||
const x448 = await util.getNobleCurve(enums.publicKey.x448);
|
|
||||||
const sharedSecret = x448.getSharedSecret(k, ephemeralPublicKey);
|
|
||||||
const hkdfInput = util.concatUint8Array([
|
|
||||||
ephemeralPublicKey,
|
|
||||||
A,
|
|
||||||
sharedSecret
|
|
||||||
]);
|
|
||||||
const cipherAlgo = enums.symmetric.aes256;
|
const cipherAlgo = enums.symmetric.aes256;
|
||||||
const { keySize } = getCipherParams(enums.symmetric.aes256);
|
const { keySize } = getCipherParams(enums.symmetric.aes256);
|
||||||
const encryptionKey = await computeHKDF(enums.hash.sha512, hkdfInput, new Uint8Array(), HKDF_INFO.x448, keySize);
|
const encryptionKey = await computeHKDF(enums.hash.sha512, hkdfInput, new Uint8Array(), HKDF_INFO.x448, keySize);
|
||||||
@ -180,3 +179,108 @@ export function getPayloadSize(algo) {
|
|||||||
throw new Error('Unsupported ECDH algorithm');
|
throw new Error('Unsupported ECDH algorithm');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate shared secret and ephemeral public key for encryption
|
||||||
|
* @returns {Promise<{ ephemeralPublicKey: Uint8Array, sharedSecret: Uint8Array }>} ephemeral public key (K_A) and shared secret
|
||||||
|
* @async
|
||||||
|
*/
|
||||||
|
export async function generateEphemeralEncryptionMaterial(algo, recipientA) {
|
||||||
|
switch (algo) {
|
||||||
|
case enums.publicKey.x25519:
|
||||||
|
try {
|
||||||
|
const webCrypto = util.getWebCrypto();
|
||||||
|
const jwk = publicKeyToJWK(algo, recipientA);
|
||||||
|
const ephemeralKeyPair = await webCrypto.generateKey('X25519', true, ['deriveKey', 'deriveBits']);
|
||||||
|
const recipientPublicKey = await webCrypto.importKey('jwk', jwk, 'X25519', false, []);
|
||||||
|
const sharedSecretBuffer = await webCrypto.deriveBits(
|
||||||
|
{ name: 'X25519', public: recipientPublicKey },
|
||||||
|
ephemeralKeyPair.privateKey,
|
||||||
|
getPayloadSize(algo) * 8 // in bits
|
||||||
|
);
|
||||||
|
const ephemeralPublicKeyJwt = await webCrypto.exportKey('jwk', ephemeralKeyPair.publicKey);
|
||||||
|
return {
|
||||||
|
sharedSecret: new Uint8Array(sharedSecretBuffer),
|
||||||
|
ephemeralPublicKey: new Uint8Array(b64ToUint8Array(ephemeralPublicKeyJwt.x))
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name !== 'NotSupportedError') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const ephemeralSecretKey = getRandomBytes(getPayloadSize(algo));
|
||||||
|
const sharedSecret = x25519.scalarMult(ephemeralSecretKey, recipientA);
|
||||||
|
const { publicKey: ephemeralPublicKey } = x25519.box.keyPair.fromSecretKey(ephemeralSecretKey);
|
||||||
|
|
||||||
|
return { ephemeralPublicKey, sharedSecret };
|
||||||
|
}
|
||||||
|
case enums.publicKey.x448: {
|
||||||
|
const x448 = await util.getNobleCurve(enums.publicKey.x448);
|
||||||
|
const ephemeralSecretKey = x448.utils.randomPrivateKey();
|
||||||
|
const sharedSecret = x448.getSharedSecret(ephemeralSecretKey, recipientA);
|
||||||
|
const ephemeralPublicKey = x448.getPublicKey(ephemeralSecretKey);
|
||||||
|
return { ephemeralPublicKey, sharedSecret };
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported ECDH algorithm');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function recomputeSharedSecret(algo, ephemeralPublicKey, A, k) {
|
||||||
|
switch (algo) {
|
||||||
|
case enums.publicKey.x25519:
|
||||||
|
try {
|
||||||
|
const webCrypto = util.getWebCrypto();
|
||||||
|
const privateKeyJWK = privateKeyToJWK(algo, A, k);
|
||||||
|
const ephemeralPublicKeyJWK = publicKeyToJWK(algo, ephemeralPublicKey);
|
||||||
|
const privateKey = await webCrypto.importKey('jwk', privateKeyJWK, 'X25519', false, ['deriveKey', 'deriveBits']);
|
||||||
|
const ephemeralPublicKeyReference = await webCrypto.importKey('jwk', ephemeralPublicKeyJWK, 'X25519', false, []);
|
||||||
|
const sharedSecretBuffer = await webCrypto.deriveBits(
|
||||||
|
{ name: 'X25519', public: ephemeralPublicKeyReference },
|
||||||
|
privateKey,
|
||||||
|
getPayloadSize(algo) * 8 // in bits
|
||||||
|
);
|
||||||
|
return new Uint8Array(sharedSecretBuffer);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name !== 'NotSupportedError') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return x25519.scalarMult(k, ephemeralPublicKey);
|
||||||
|
}
|
||||||
|
case enums.publicKey.x448: {
|
||||||
|
const x448 = await util.getNobleCurve(enums.publicKey.x448);
|
||||||
|
const sharedSecret = x448.getSharedSecret(k, ephemeralPublicKey);
|
||||||
|
return sharedSecret;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported ECDH algorithm');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function publicKeyToJWK(algo, publicKey) {
|
||||||
|
switch (algo) {
|
||||||
|
case enums.publicKey.x25519: {
|
||||||
|
const jwk = {
|
||||||
|
kty: 'OKP',
|
||||||
|
crv: 'X25519',
|
||||||
|
x: uint8ArrayToB64(publicKey, true),
|
||||||
|
ext: true
|
||||||
|
};
|
||||||
|
return jwk;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported ECDH algorithm');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function privateKeyToJWK(algo, publicKey, privateKey) {
|
||||||
|
switch (algo) {
|
||||||
|
case enums.publicKey.x25519: {
|
||||||
|
const jwk = publicKeyToJWK(algo, publicKey);
|
||||||
|
jwk.d = uint8ArrayToB64(privateKey, true);
|
||||||
|
return jwk;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported ECDH algorithm');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -25,6 +25,7 @@ import util from '../../../util';
|
|||||||
import enums from '../../../enums';
|
import enums from '../../../enums';
|
||||||
import hash from '../../hash';
|
import hash from '../../hash';
|
||||||
import { getRandomBytes } from '../../random';
|
import { getRandomBytes } from '../../random';
|
||||||
|
import { b64ToUint8Array, uint8ArrayToB64 } from '../../../encoding/base64';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,11 +35,27 @@ import { getRandomBytes } from '../../random';
|
|||||||
*/
|
*/
|
||||||
export async function generate(algo) {
|
export async function generate(algo) {
|
||||||
switch (algo) {
|
switch (algo) {
|
||||||
case enums.publicKey.ed25519: {
|
case enums.publicKey.ed25519:
|
||||||
const seed = getRandomBytes(getPayloadSize(algo));
|
try {
|
||||||
const { publicKey: A } = ed25519.sign.keyPair.fromSeed(seed);
|
const webCrypto = util.getWebCrypto();
|
||||||
return { A, seed };
|
const webCryptoKey = await webCrypto.generateKey('Ed25519', true, ['sign', 'verify']);
|
||||||
}
|
|
||||||
|
const privateKey = await webCrypto.exportKey('jwk', webCryptoKey.privateKey);
|
||||||
|
const publicKey = await webCrypto.exportKey('jwk', webCryptoKey.publicKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
A: new Uint8Array(b64ToUint8Array(publicKey.x)),
|
||||||
|
seed: b64ToUint8Array(privateKey.d, true)
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name !== 'NotSupportedError') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const seed = getRandomBytes(getPayloadSize(algo));
|
||||||
|
const { publicKey: A } = ed25519.sign.keyPair.fromSeed(seed);
|
||||||
|
return { A, seed };
|
||||||
|
}
|
||||||
|
|
||||||
case enums.publicKey.ed448: {
|
case enums.publicKey.ed448: {
|
||||||
const ed448 = await util.getNobleCurve(enums.publicKey.ed448);
|
const ed448 = await util.getNobleCurve(enums.publicKey.ed448);
|
||||||
const seed = ed448.utils.randomPrivateKey();
|
const seed = ed448.utils.randomPrivateKey();
|
||||||
@ -68,11 +85,26 @@ export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashe
|
|||||||
throw new Error('Hash algorithm too weak for EdDSA.');
|
throw new Error('Hash algorithm too weak for EdDSA.');
|
||||||
}
|
}
|
||||||
switch (algo) {
|
switch (algo) {
|
||||||
case enums.publicKey.ed25519: {
|
case enums.publicKey.ed25519:
|
||||||
const secretKey = util.concatUint8Array([privateKey, publicKey]);
|
try {
|
||||||
const signature = ed25519.sign.detached(hashed, secretKey);
|
const webCrypto = util.getWebCrypto();
|
||||||
return { RS: signature };
|
const jwk = privateKeyToJWK(algo, publicKey, privateKey);
|
||||||
}
|
const key = await webCrypto.importKey('jwk', jwk, 'Ed25519', false, ['sign']);
|
||||||
|
|
||||||
|
const signature = new Uint8Array(
|
||||||
|
await webCrypto.sign('Ed25519', key, hashed)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { RS: signature };
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name !== 'NotSupportedError') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const secretKey = util.concatUint8Array([privateKey, publicKey]);
|
||||||
|
const signature = ed25519.sign.detached(hashed, secretKey);
|
||||||
|
return { RS: signature };
|
||||||
|
}
|
||||||
|
|
||||||
case enums.publicKey.ed448: {
|
case enums.publicKey.ed448: {
|
||||||
const ed448 = await util.getNobleCurve(enums.publicKey.ed448);
|
const ed448 = await util.getNobleCurve(enums.publicKey.ed448);
|
||||||
const signature = ed448.sign(hashed, privateKey);
|
const signature = ed448.sign(hashed, privateKey);
|
||||||
@ -101,7 +133,19 @@ export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) {
|
|||||||
}
|
}
|
||||||
switch (algo) {
|
switch (algo) {
|
||||||
case enums.publicKey.ed25519:
|
case enums.publicKey.ed25519:
|
||||||
return ed25519.sign.detached.verify(hashed, RS, publicKey);
|
try {
|
||||||
|
const webCrypto = util.getWebCrypto();
|
||||||
|
const jwk = publicKeyToJWK(algo, publicKey);
|
||||||
|
const key = await webCrypto.importKey('jwk', jwk, 'Ed25519', false, ['verify']);
|
||||||
|
const verified = await webCrypto.verify('Ed25519', key, RS, hashed);
|
||||||
|
return verified;
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name !== 'NotSupportedError') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return ed25519.sign.detached.verify(hashed, RS, publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
case enums.publicKey.ed448: {
|
case enums.publicKey.ed448: {
|
||||||
const ed448 = await util.getNobleCurve(enums.publicKey.ed448);
|
const ed448 = await util.getNobleCurve(enums.publicKey.ed448);
|
||||||
return ed448.verify(RS, hashed, publicKey);
|
return ed448.verify(RS, hashed, publicKey);
|
||||||
@ -125,6 +169,7 @@ export async function validateParams(algo, A, seed) {
|
|||||||
/**
|
/**
|
||||||
* Derive public point A' from private key
|
* Derive public point A' from private key
|
||||||
* and expect A == A'
|
* and expect A == A'
|
||||||
|
* TODO: move to sign-verify using WebCrypto (same as ECDSA) when curve is more widely implemented
|
||||||
*/
|
*/
|
||||||
const { publicKey } = ed25519.sign.keyPair.fromSeed(seed);
|
const { publicKey } = ed25519.sign.keyPair.fromSeed(seed);
|
||||||
return util.equalsUint8Array(A, publicKey);
|
return util.equalsUint8Array(A, publicKey);
|
||||||
@ -164,3 +209,31 @@ export function getPreferredHashAlgo(algo) {
|
|||||||
throw new Error('Unknown EdDSA algo');
|
throw new Error('Unknown EdDSA algo');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const publicKeyToJWK = (algo, publicKey) => {
|
||||||
|
switch (algo) {
|
||||||
|
case enums.publicKey.ed25519: {
|
||||||
|
const jwk = {
|
||||||
|
kty: 'OKP',
|
||||||
|
crv: 'Ed25519',
|
||||||
|
x: uint8ArrayToB64(publicKey, true),
|
||||||
|
ext: true
|
||||||
|
};
|
||||||
|
return jwk;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported EdDSA algorithm');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const privateKeyToJWK = (algo, publicKey, privateKey) => {
|
||||||
|
switch (algo) {
|
||||||
|
case enums.publicKey.ed25519: {
|
||||||
|
const jwk = publicKeyToJWK(algo, publicKey);
|
||||||
|
jwk.d = uint8ArrayToB64(privateKey, true);
|
||||||
|
return jwk;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported EdDSA algorithm');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -26,6 +26,7 @@ import util from '../../../util';
|
|||||||
import enums from '../../../enums';
|
import enums from '../../../enums';
|
||||||
import hash from '../../hash';
|
import hash from '../../hash';
|
||||||
import { CurveWithOID, checkPublicPointEnconding } from './oid_curves';
|
import { CurveWithOID, checkPublicPointEnconding } from './oid_curves';
|
||||||
|
import { sign as eddsaSign, verify as eddsaVerify } from './eddsa';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign a message using the provided legacy EdDSA key
|
* Sign a message using the provided legacy EdDSA key
|
||||||
@ -48,8 +49,7 @@ export async function sign(oid, hashAlgo, message, publicKey, privateKey, hashed
|
|||||||
// see https://tools.ietf.org/id/draft-ietf-openpgp-rfc4880bis-10.html#section-15-7.2
|
// see https://tools.ietf.org/id/draft-ietf-openpgp-rfc4880bis-10.html#section-15-7.2
|
||||||
throw new Error('Hash algorithm too weak for EdDSA.');
|
throw new Error('Hash algorithm too weak for EdDSA.');
|
||||||
}
|
}
|
||||||
const secretKey = util.concatUint8Array([privateKey, publicKey.subarray(1)]);
|
const { RS: signature } = await eddsaSign(enums.publicKey.ed25519, hashAlgo, message, publicKey.subarray(1), privateKey, hashed);
|
||||||
const signature = nacl.sign.detached(hashed, secretKey);
|
|
||||||
// EdDSA signature params are returned in little-endian format
|
// EdDSA signature params are returned in little-endian format
|
||||||
return {
|
return {
|
||||||
r: signature.subarray(0, 32),
|
r: signature.subarray(0, 32),
|
||||||
@ -75,8 +75,8 @@ export async function verify(oid, hashAlgo, { r, s }, m, publicKey, hashed) {
|
|||||||
if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(enums.hash.sha256)) {
|
if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(enums.hash.sha256)) {
|
||||||
throw new Error('Hash algorithm too weak for EdDSA.');
|
throw new Error('Hash algorithm too weak for EdDSA.');
|
||||||
}
|
}
|
||||||
const signature = util.concatUint8Array([r, s]);
|
const RS = util.concatUint8Array([r, s]);
|
||||||
return nacl.sign.detached.verify(hashed, signature, publicKey.subarray(1));
|
return eddsaVerify(enums.publicKey.ed25519, hashAlgo, { RS }, m, publicKey.subarray(1), hashed);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Validate legacy EdDSA parameters
|
* Validate legacy EdDSA parameters
|
||||||
|
@ -20,12 +20,13 @@
|
|||||||
* @module crypto/public_key/elliptic/curve
|
* @module crypto/public_key/elliptic/curve
|
||||||
*/
|
*/
|
||||||
import nacl from '@openpgp/tweetnacl';
|
import nacl from '@openpgp/tweetnacl';
|
||||||
import { getRandomBytes } from '../../random';
|
|
||||||
import enums from '../../../enums';
|
import enums from '../../../enums';
|
||||||
import util from '../../../util';
|
import util from '../../../util';
|
||||||
import { uint8ArrayToB64, b64ToUint8Array } from '../../../encoding/base64';
|
import { uint8ArrayToB64, b64ToUint8Array } from '../../../encoding/base64';
|
||||||
import OID from '../../../type/oid';
|
import OID from '../../../type/oid';
|
||||||
import { UnsupportedError } from '../../../packet/packet';
|
import { UnsupportedError } from '../../../packet/packet';
|
||||||
|
import { generate as eddsaGenerate } from './eddsa';
|
||||||
|
import { generate as ecdhXGenerate } from './ecdh_x';
|
||||||
|
|
||||||
const webCrypto = util.getWebCrypto();
|
const webCrypto = util.getWebCrypto();
|
||||||
const nodeCrypto = util.getNodeCrypto();
|
const nodeCrypto = util.getNodeCrypto();
|
||||||
@ -181,18 +182,14 @@ class CurveWithOID {
|
|||||||
case 'node':
|
case 'node':
|
||||||
return nodeGenKeyPair(this.name);
|
return nodeGenKeyPair(this.name);
|
||||||
case 'curve25519Legacy': {
|
case 'curve25519Legacy': {
|
||||||
const privateKey = getRandomBytes(32);
|
const { k, A } = await ecdhXGenerate(enums.publicKey.x25519);
|
||||||
privateKey[0] = (privateKey[0] & 127) | 64;
|
const privateKey = k.slice().reverse();
|
||||||
privateKey[31] &= 248;
|
const publicKey = util.concatUint8Array([new Uint8Array([this.wireFormatLeadingByte]), A]);
|
||||||
const secretKey = privateKey.slice().reverse();
|
|
||||||
const { publicKey: rawPublicKey } = nacl.box.keyPair.fromSecretKey(secretKey);
|
|
||||||
const publicKey = util.concatUint8Array([new Uint8Array([this.wireFormatLeadingByte]), rawPublicKey]);
|
|
||||||
return { publicKey, privateKey };
|
return { publicKey, privateKey };
|
||||||
}
|
}
|
||||||
case 'ed25519Legacy': {
|
case 'ed25519Legacy': {
|
||||||
const privateKey = getRandomBytes(32);
|
const { seed: privateKey, A } = await eddsaGenerate(enums.publicKey.ed25519);
|
||||||
const keyPair = nacl.sign.keyPair.fromSeed(privateKey);
|
const publicKey = util.concatUint8Array([new Uint8Array([this.wireFormatLeadingByte]), A]);
|
||||||
const publicKey = util.concatUint8Array([new Uint8Array([this.wireFormatLeadingByte]), keyPair.publicKey]);
|
|
||||||
return { publicKey, privateKey };
|
return { publicKey, privateKey };
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -12,7 +12,9 @@ import util from '../../src/util';
|
|||||||
|
|
||||||
import * as input from './testInputs';
|
import * as input from './testInputs';
|
||||||
|
|
||||||
export default () => (openpgp.config.ci ? describe.skip : describe)('X25519 Cryptography (legacy format)', function () {
|
const isSafariOrHeadlessWebKit = () => typeof window !== 'undefined' && window.navigator.userAgent.match(/WebKit/) && !window.navigator.userAgent.match(/Chrome/);
|
||||||
|
|
||||||
|
export default () => describe('X25519 Cryptography (legacy format)', function () {
|
||||||
const data = {
|
const data = {
|
||||||
light: {
|
light: {
|
||||||
id: '1ecdf026c0245830',
|
id: '1ecdf026c0245830',
|
||||||
@ -216,7 +218,9 @@ export default () => (openpgp.config.ci ? describe.skip : describe)('X25519 Cryp
|
|||||||
expect(await result.signatures[0].verified).to.be.true;
|
expect(await result.signatures[0].verified).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Ed25519 Test Vectors from RFC8032', function () {
|
// Safari implements the non-deterministic version of EdDSA (https://cfrg.github.io/draft-irtf-cfrg-det-sigs-with-noise/draft-irtf-cfrg-det-sigs-with-noise.html),
|
||||||
|
// hence these test vectors do not apply.
|
||||||
|
(isSafariOrHeadlessWebKit() ? describe.skip : describe)('Ed25519 Test Vectors from RFC8032', function () {
|
||||||
// https://tools.ietf.org/html/rfc8032#section-7.1
|
// https://tools.ietf.org/html/rfc8032#section-7.1
|
||||||
function testVector(vector) {
|
function testVector(vector) {
|
||||||
const curve = new elliptic.CurveWithOID(openpgp.enums.curve.ed25519Legacy);
|
const curve = new elliptic.CurveWithOID(openpgp.enums.curve.ed25519Legacy);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user