diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index 6c124e5f..87562060 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -50,15 +50,24 @@ function buildEcdhParam(public_algo, oid, cipher_algo, hash_algo, fingerprint) { } // Key Derivation Function (RFC 6637) -async function kdf(hash_algo, S, length, param, curve, compat) { - const len = compat ? - S.byteLength() : - curve.curve.curve.p.byteLength(); +async function kdf(hash_algo, S, length, param, curve, stripLeading=false, stripTrailing=false) { + const len = curve.curve.curve.p.byteLength(); // Note: this is not ideal, but the RFC's are unclear // https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-02#appendix-B - const X = curve.curve.curve.type === 'mont' ? + let X = curve.curve.curve.type === 'mont' ? S.toArrayLike(Uint8Array, 'le', len) : S.toArrayLike(Uint8Array, 'be', len); + let i; + if (stripLeading) { + // Work around old go crypto bug + for (i = 0; i < X.length && X[i] === 0; i++); + X = X.subarray(i); + } + if (stripTrailing) { + // Work around old OpenPGP.js bug + for (i = X.length - 1; i >= 0 && X[i] === 0; i--); + X = X.subarray(0, i + 1); + } const digest = await hash.digest(hash_algo, util.concatUint8Array([ new Uint8Array([0, 0, 0, 1]), X, @@ -100,7 +109,7 @@ async function encrypt(oid, cipher_algo, hash_algo, m, Q, fingerprint) { const { V, S } = await genPublicEphemeralKey(curve, Q); const param = buildEcdhParam(enums.publicKey.ecdh, oid, cipher_algo, hash_algo, fingerprint); cipher_algo = enums.read(enums.symmetric, cipher_algo); - const Z = await kdf(hash_algo, S, cipher[cipher_algo].keySize, param, curve, false); + const Z = await kdf(hash_algo, S, cipher[cipher_algo].keySize, param, curve); const C = aes_kw.wrap(Z, m.toString()); return { V, C }; } @@ -138,13 +147,17 @@ async function decrypt(oid, cipher_algo, hash_algo, V, C, d, fingerprint) { const S = await genPrivateEphemeralKey(curve, V, d); const param = buildEcdhParam(enums.publicKey.ecdh, oid, cipher_algo, hash_algo, fingerprint); cipher_algo = enums.read(enums.symmetric, cipher_algo); - try { - const Z = await kdf(hash_algo, S, cipher[cipher_algo].keySize, param, curve, false); - return new BN(aes_kw.unwrap(Z, C)); - } catch(e) {} - // Work around old OpenPGP.js bug. - const Z = await kdf(hash_algo, S, cipher[cipher_algo].keySize, param, curve, true); - return new BN(aes_kw.unwrap(Z, C)); + let err; + for (let i = 0; i < 3; i++) { + try { + // Work around old go crypto bug and old OpenPGP.js bug, respectively. + const Z = await kdf(hash_algo, S, cipher[cipher_algo].keySize, param, curve, i === 1, i === 2); + return new BN(aes_kw.unwrap(Z, C)); + } catch (e) { + err = e; + } + } + throw err; } export default { encrypt, decrypt, genPublicEphemeralKey, genPrivateEphemeralKey, buildEcdhParam, kdf }; diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 68eed30f..fa2f04f5 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -335,6 +335,45 @@ OBqYz6mzZAWQZqsjbg4= =zrks -----END PGP PRIVATE KEY BLOCK-----`; +const ecdh_msg_bad_2 = `-----BEGIN PGP MESSAGE----- +Version: ProtonMail +Comment: https://protonmail.com + +wV4DtM+8giJEGNISAQhA2rYu8+B41rJi6Gsr4TVeKyDtI0KjhhlLZs891rCG +6X4wxNkxCuTJZax7gQZbDKh2kETK/RH75s9g7H/WV9kZ192NTGmMFiKiautH +c5BGRGxM0sDfAQZb3ZsAUORHKPP7FczMv5aMU2Ko7O2FHc06bMdnZ/ag7GMF +Bdl4EizttNTQ5sNCAdIXUoA8BJLHPgPiglnfTqqx3ynkBNMzfH46oKf08oJ+ +6CAQhJdif67/iDX8BRtaKDICBpv3b5anJht7irOBqf9XX13SGkmqKYF3T8eB +W7ZV5EdCTC9KU+1BBPfPEi93F4OHsG/Jo80e5MDN24/wNxC67h7kUQiy3H4s +al+5mSAKcIfZJA4NfPJg9zSoHgfRNGI8Q7ao+c8CLPiefGcMsakNsWUdRyBT +SSLH3z/7AH4GxBvhDEEG3cZwmXzZAJMZmzTa+SrsxZzRpGB/aawyRntOWm8w +6Lq9ntq4S8suj/YK62dJpJxFl8xs+COngpMDvCexX9lYlh/r/y4JRQl06oUK +wv7trvi89TkK3821qHxr7XwI1Ncr2qDJVNlN4W+b6WFyLXnXaJAUMyZ/6inm +RR8BoR2KkEAku3Ne/G5QI51ktNJ7cCodeVOkZj8+iip1/AGyjxZCybq/N8rc +bpOWdMhJ6Hy+JzGNY1qNXcHJPw== +=99Fs +-----END PGP MESSAGE-----`; + +const ecdh_dec_key_2 = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: OpenPGP.js v4.4.9 +Comment: https://openpgpjs.org + +xYYEXEg93hYJKwYBBAHaRw8BAQdAeoA+T4vr3P0hFFsbzJpgy7/ZnKCrlehr +Myk5QAsBYgf+CQMIQ76YL5sEx+Zgr7DLZ5fhQn1U9+8aLIQaIbaT51nEjEMD +7h6mrJmp7oIr4PyijsIU+0LasXh/qlNeVQVWSygDq9L4nXDEGQhlMq3oH1FN +NM07InBha292c2thdGVzdEBwcm90b25tYWlsLmNvbSIgPHBha292c2thdGVz +dEBwcm90b25tYWlsLmNvbT7CdwQQFgoAHwUCXEg93gYLCQcIAwIEFQgKAgMW +AgECGQECGwMCHgEACgkQp7+eOYEhwd6x5AD9E0LA62odFFDH76wjEYrPCvOH +cYM56/5ZqZoGPPmbE98BAKCz/SQ90tiCMmlLEDXGX+a1bi6ttozqrnSQigic +DI4Ix4sEXEg93hIKKwYBBAGXVQEFAQEHQPDXy2mDfbMKOpCBZB2Ic5bfoWGV +iXvCFMnTLRWfGHUkAwEIB/4JAwhxMnjHjyALomBWSsoYxxB6rj6JKnWeikyj +yjXZdZqdK5F+0rk4M0l7lF0wt5PhT2uMCLB7aH/mSFN1cz7sBeJl3w2soJsT +ve/fP/8NfzP0wmEEGBYIAAkFAlxIPd4CGwwACgkQp7+eOYEhwd5MWQEAp0E4 +QTnEnG8lYXhOqnOw676oV2kEU6tcTj3DdM+cW/sA/jH3FQQjPf+mA/7xqKIv +EQr2Mx42THr260IFYp5E/rIA +=oA0b +-----END PGP PRIVATE KEY BLOCK-----`; + function withCompression(tests) { const compressionTypes = Object.keys(openpgp.enums.compression).map(k => openpgp.enums.compression[k]); @@ -2301,6 +2340,14 @@ describe('[Sauce Labs Group 2] OpenPGP.js public api tests', function() { expect(decrypted.data).to.equal('\n'); }); + it('should decrypt broken ECC message from old go crypto', async function() { + const { keys: [key] } = await openpgp.key.readArmored(ecdh_dec_key_2); + const message = await openpgp.message.readArmored(ecdh_msg_bad_2); + await key.decrypt('12345'); + const decrypted = await openpgp.decrypt({ message, privateKeys: [key] }); + expect(decrypted.data).to.equal('Tesssst


Sent from ProtonMail mobile


'); + }); + }); describe('Errors', function() {