From c9a4e874427cef74f4128cbb0b714894bea46366 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Tue, 20 Apr 2021 13:12:16 -0400 Subject: [PATCH 01/17] Use public key fingerprint as S/MIME Certificate id #3570 --- extension/js/common/core/crypto/key.ts | 4 +- .../js/common/core/crypto/smime/smime-key.ts | 39 +++++---------- .../js/common/platform/store/contact-store.ts | 8 +++- .../browser-unit-tests/unit-ContactStore.js | 32 +++++++++++++ test/source/tests/tooling/consts.ts | 31 +++++++++++- test/source/tests/unit-node.ts | 48 ++++--------------- 6 files changed, 90 insertions(+), 72 deletions(-) diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index 318a6a26a37..3b176eb7018 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -20,8 +20,8 @@ import { MsgBlock } from '../msg-block.js'; */ export interface Key { type: 'openpgp' | 'x509'; - id: string; // This is a fingerprint for OpenPGP keys and Serial Number for X.509 keys. - allIds: string[]; // a list of fingerprints for OpenPGP key or a Serial Number for X.509 keys. + id: string; // a fingerprint + allIds: string[]; // a list of fingerprints, including those for subkeys created: number; lastModified: number | undefined; // date of last signature, or undefined if never had valid signature expiration: number | undefined; // number of millis of expiration or undefined if never expires diff --git a/extension/js/common/core/crypto/smime/smime-key.ts b/extension/js/common/core/crypto/smime/smime-key.ts index b4557050f65..4a686d4e60b 100644 --- a/extension/js/common/core/crypto/smime/smime-key.ts +++ b/extension/js/common/core/crypto/smime/smime-key.ts @@ -46,29 +46,8 @@ export class SmimeKey { if (!certificate) { throw new Error('No user certificate found.'); } - SmimeKey.removeWeakKeys(certificate); - const emails = SmimeKey.getNormalizedEmailsFromCertificate(certificate); - const key = { - type: 'x509', - id: certificate.serialNumber.toUpperCase(), - allIds: [certificate.serialNumber.toUpperCase()], - usableForEncryption: SmimeKey.isEmailCertificate(certificate), - usableForSigning: SmimeKey.isEmailCertificate(certificate), - usableForEncryptionButExpired: false, - usableForSigningButExpired: false, - emails, - identities: emails, - created: SmimeKey.dateToNumber(certificate.validity.notBefore), - lastModified: SmimeKey.dateToNumber(certificate.validity.notBefore), - expiration: SmimeKey.dateToNumber(certificate.validity.notAfter), - fullyDecrypted: true, - fullyEncrypted: false, - isPublic: certificate.publicKey && !certificate.privateKey, - isPrivate: !!certificate.privateKey, - } as Key; const headers = PgpArmor.headers('pkcs12'); - (key as unknown as { raw: string }).raw = `${headers.begin}\n${forge.util.encode64(bytes)}\n${headers.end}`; - return key; + return SmimeKey.getKeyFromCertificate(certificate, `${headers.begin}\n${forge.util.encode64(bytes)}\n${headers.end}`); } /** @@ -109,14 +88,18 @@ export class SmimeKey { } private static getKeyFromCertificate = (certificate: forge.pki.Certificate, pem: string): Key => { + if (!certificate.publicKey) { + throw new UnreportableError(`This S/MIME x.509 certificate doesn't have a public key`); + } + const fingerprint = forge.pki.getPublicKeyFingerprint(certificate.publicKey, { encoding: 'hex' }).toUpperCase(); SmimeKey.removeWeakKeys(certificate); const emails = SmimeKey.getNormalizedEmailsFromCertificate(certificate); const key = { type: 'x509', - id: certificate.serialNumber.toUpperCase(), - allIds: [certificate.serialNumber.toUpperCase()], - usableForEncryption: certificate.publicKey && SmimeKey.isEmailCertificate(certificate), - usableForSigning: certificate.publicKey && SmimeKey.isEmailCertificate(certificate), + id: fingerprint, + allIds: [fingerprint], + usableForEncryption: SmimeKey.isEmailCertificate(certificate), + usableForSigning: SmimeKey.isEmailCertificate(certificate), usableForEncryptionButExpired: false, usableForSigningButExpired: false, emails, @@ -124,9 +107,9 @@ export class SmimeKey { created: SmimeKey.dateToNumber(certificate.validity.notBefore), lastModified: SmimeKey.dateToNumber(certificate.validity.notBefore), expiration: SmimeKey.dateToNumber(certificate.validity.notAfter), - fullyDecrypted: false, + fullyDecrypted: !!certificate.privateKey, fullyEncrypted: false, - isPublic: certificate.publicKey && !certificate.privateKey, + isPublic: !certificate.privateKey, isPrivate: !!certificate.privateKey, } as Key; (key as unknown as { rawArmored: string }).rawArmored = pem; diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index 6d64a6e013b..bc6c2b602ac 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -271,7 +271,7 @@ export class ContactStore extends AbstractStore { public static updateTx = (tx: IDBTransaction, email: string, update: ContactUpdate) => { if (update.pubkey && !update.pubkeyLastCheck) { - const req = tx.objectStore('pubkeys').get(update.pubkey.id); + const req = tx.objectStore('pubkeys').get(ContactStore.getPubkeyId(update.pubkey)); ContactStore.setReqPipe(req, (pubkey: Pubkey) => ContactStore.updateTxPhase2(tx, email, update, pubkey)); } else { ContactStore.updateTxPhase2(tx, email, update, undefined); @@ -295,13 +295,17 @@ export class ContactStore extends AbstractStore { } } + private static getPubkeyId = (pubkey: Key): string => { + return (pubkey.type === 'x509') ? (pubkey.id + '-X509') : pubkey.id; + } + private static updateTxPhase2 = (tx: IDBTransaction, email: string, update: ContactUpdate, existingPubkey: Pubkey | undefined) => { let pubkeyEntity: Pubkey | undefined; if (update.pubkey) { const keyAttrs = ContactStore.getKeyAttributes(update.pubkey); // todo: will we benefit anything when not saving pubkey if it isn't modified? pubkeyEntity = { - fingerprint: update.pubkey.id, + fingerprint: ContactStore.getPubkeyId(update.pubkey), lastCheck: DateUtility.asNumber(update.pubkeyLastCheck ?? existingPubkey?.lastCheck), expiresOn: keyAttrs.expiresOn, longids: update.pubkey.allIds.map(id => OpenPGPKey.fingerprintToLongid(id)), diff --git a/test/source/tests/browser-unit-tests/unit-ContactStore.js b/test/source/tests/browser-unit-tests/unit-ContactStore.js index 26dad99863b..b9fcbf248eb 100644 --- a/test/source/tests/browser-unit-tests/unit-ContactStore.js +++ b/test/source/tests/browser-unit-tests/unit-ContactStore.js @@ -357,3 +357,35 @@ BROWSER_UNIT_TEST_NAME(`ContactStore gets a valid pubkey by e-mail, or exact pub } return 'pass'; })(); + +BROWSER_UNIT_TEST_NAME(`ContactStore stores postfixed fingerprint internally for X.509 certificate`); +(async () => { + const db = await ContactStore.dbOpen(); + const email = 'actalis@meta.33mail.com'; + const contacts = [ + await ContactStore.obj({ + email, + pubkey: testConstants.smimeCert + })]; + await ContactStore.save(db, contacts); + // extract the entity directly from the database + const entityFp = '16BB407403A3ADC55E1E0E4AF93EEC8FB187C923-X509'; + const fingerprint = '16BB407403A3ADC55E1E0E4AF93EEC8FB187C923'; + const longid = 'F93EEC8FB187C923'; + const entity = await new Promise((resolve, reject) => { + const req = db.transaction(['pubkeys'], 'readonly').objectStore('pubkeys').get(entityFp); + ContactStore.setReqPipe(req, resolve, reject); + }); + if (entity.fingerprint !== entityFp) { + throw Error(`Failed to extract pubkey ${fingerprint}`); + } + const [contactByLongid] = await ContactStore.get(db, [longid]); + if (contactByLongid.pubkey.id !== fingerprint) { + throw Error(`Failed to extract pubkey ${fingerprint}`); + } + const [contactByEmail] = await ContactStore.get(db, [email]); + if (contactByEmail.pubkey.id !== fingerprint) { + throw Error(`Failed to extract pubkey ${fingerprint}`); + } + return 'pass'; +})(); diff --git a/test/source/tests/tooling/consts.ts b/test/source/tests/tooling/consts.ts index 112b2f74060..41df16699b2 100644 --- a/test/source/tests/tooling/consts.ts +++ b/test/source/tests/tooling/consts.ts @@ -762,5 +762,34 @@ UAYXUheuOOBJrHSWc9+sfhJVxG9+JY2xSPoHWfa0q6QZoKQSPXK/Z63cNDK6vDnLbv2VvTFkBlPH wHlKRDVNpiNVKyevQ8PI9iQGMEkKOv23GdDuC0F0rCCnqp6gEVvezjMMk5bMCEiY4sn2+wwVDpC3 5adfA360UnwZSMt5qrk= =EDBf ------END PGP PUBLIC KEY BLOCK-----` +-----END PGP PUBLIC KEY BLOCK-----`, + smimeCert: `-----BEGIN CERTIFICATE----- +MIIE9DCCA9ygAwIBAgIQY/cCXnAPOUUwH7L7pWdPhDANBgkqhkiG9w0BAQsFADCB +jTELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB0JlcmdhbW8xGTAXBgNVBAcMEFBvbnRl +IFNhbiBQaWV0cm8xIzAhBgNVBAoMGkFjdGFsaXMgUy5wLkEuLzAzMzU4NTIwOTY3 +MSwwKgYDVQQDDCNBY3RhbGlzIENsaWVudCBBdXRoZW50aWNhdGlvbiBDQSBHMjAe +Fw0yMDAzMjMxMzU2NDZaFw0yMTAzMjMxMzU2NDZaMCIxIDAeBgNVBAMMF2FjdGFs +aXNAbWV0YS4zM21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEArVVpXBkzGvcqib8rDwqHCaKm2EiPslQ8I0G1ZDxrs6Ke2QXNm3yGVwOzkVvK +eEnuzE5M4BBeh+GwcfvoyS/xI6m44WWnqj65cJoSLA1ypE4D4urv/pzG783y2Vdy +Q96izBdFyevsil89Z2AxZxrFh1RC2XvgXad4yyD4yvVpHskfPexnhLliHl7cpXjw +5D2n1hBGR8CSDbQAgO58PB7Y2ldrTi+rWBu2Akuk/YyWOOiGA8pdfLBIkOFJTeQc +m7+vWP2JTN6Xp+JkGvXQBRaqwyGVg8fSc4e7uGCXZaH5/Na2FXY2OL+tYDDb27zS +3cBrzEbGVjA6raYxcrFWV4PkdwIDAQABo4IBuDCCAbQwDAYDVR0TAQH/BAIwADAf +BgNVHSMEGDAWgBRr8o2eaMElBB9RNFf2FlyU6k1pGjB+BggrBgEFBQcBAQRyMHAw +OwYIKwYBBQUHMAKGL2h0dHA6Ly9jYWNlcnQuYWN0YWxpcy5pdC9jZXJ0cy9hY3Rh +bGlzLWF1dGNsaWcyMDEGCCsGAQUFBzABhiVodHRwOi8vb2NzcDA5LmFjdGFsaXMu +aXQvVkEvQVVUSENMLUcyMCIGA1UdEQQbMBmBF2FjdGFsaXNAbWV0YS4zM21haWwu +Y29tMEcGA1UdIARAMD4wPAYGK4EfARgBMDIwMAYIKwYBBQUHAgEWJGh0dHBzOi8v +d3d3LmFjdGFsaXMuaXQvYXJlYS1kb3dubG9hZDAdBgNVHSUEFjAUBggrBgEFBQcD +AgYIKwYBBQUHAwQwSAYDVR0fBEEwPzA9oDugOYY3aHR0cDovL2NybDA5LmFjdGFs +aXMuaXQvUmVwb3NpdG9yeS9BVVRIQ0wtRzIvZ2V0TGFzdENSTDAdBgNVHQ4EFgQU +FrtAdAOjrcVeHg5K+T7sj7GHySMwDgYDVR0PAQH/BAQDAgWgMA0GCSqGSIb3DQEB +CwUAA4IBAQAa9lXKDmV9874ojmIZEBL1S8mKaSNBWP+n0vp5FO0Yh5oL9lspYTPs +8s6alWUSpVHV8if4uZ2EfcNpNkm9dAajj2n/F/Jyfkp8URu4uvBfm1QColl/zM/D +x4B7FaD2dw0jTF/k5ulDmzUOc4k+j3LtZNbDOZMF/2g05hSKde/he1njlY3oKa9g +VW8ftc2NwiSMthxyEIM+ALbNQVML2oN50gArBn5GeI22/aIBZxjtbEdmSTZIf82H +sOwAnhJ+pD5iIPaF2oa0yN3PvI6IGxLpEv16tQO1N6e5bdP6ZDwqTQJyK+oNTNda +yPLCqVTFJQWaCR5ZTekRQPTDZkjxjxbs +-----END CERTIFICATE-----` }; \ No newline at end of file diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index d3f2e867d16..f5770a8473b 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -105,39 +105,9 @@ export let defineUnitNodeTests = (testVariant: TestVariant) => { t.pass(); }); - const smimeCert = `-----BEGIN CERTIFICATE----- -MIIE9DCCA9ygAwIBAgIQY/cCXnAPOUUwH7L7pWdPhDANBgkqhkiG9w0BAQsFADCB -jTELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB0JlcmdhbW8xGTAXBgNVBAcMEFBvbnRl -IFNhbiBQaWV0cm8xIzAhBgNVBAoMGkFjdGFsaXMgUy5wLkEuLzAzMzU4NTIwOTY3 -MSwwKgYDVQQDDCNBY3RhbGlzIENsaWVudCBBdXRoZW50aWNhdGlvbiBDQSBHMjAe -Fw0yMDAzMjMxMzU2NDZaFw0yMTAzMjMxMzU2NDZaMCIxIDAeBgNVBAMMF2FjdGFs -aXNAbWV0YS4zM21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEArVVpXBkzGvcqib8rDwqHCaKm2EiPslQ8I0G1ZDxrs6Ke2QXNm3yGVwOzkVvK -eEnuzE5M4BBeh+GwcfvoyS/xI6m44WWnqj65cJoSLA1ypE4D4urv/pzG783y2Vdy -Q96izBdFyevsil89Z2AxZxrFh1RC2XvgXad4yyD4yvVpHskfPexnhLliHl7cpXjw -5D2n1hBGR8CSDbQAgO58PB7Y2ldrTi+rWBu2Akuk/YyWOOiGA8pdfLBIkOFJTeQc -m7+vWP2JTN6Xp+JkGvXQBRaqwyGVg8fSc4e7uGCXZaH5/Na2FXY2OL+tYDDb27zS -3cBrzEbGVjA6raYxcrFWV4PkdwIDAQABo4IBuDCCAbQwDAYDVR0TAQH/BAIwADAf -BgNVHSMEGDAWgBRr8o2eaMElBB9RNFf2FlyU6k1pGjB+BggrBgEFBQcBAQRyMHAw -OwYIKwYBBQUHMAKGL2h0dHA6Ly9jYWNlcnQuYWN0YWxpcy5pdC9jZXJ0cy9hY3Rh -bGlzLWF1dGNsaWcyMDEGCCsGAQUFBzABhiVodHRwOi8vb2NzcDA5LmFjdGFsaXMu -aXQvVkEvQVVUSENMLUcyMCIGA1UdEQQbMBmBF2FjdGFsaXNAbWV0YS4zM21haWwu -Y29tMEcGA1UdIARAMD4wPAYGK4EfARgBMDIwMAYIKwYBBQUHAgEWJGh0dHBzOi8v -d3d3LmFjdGFsaXMuaXQvYXJlYS1kb3dubG9hZDAdBgNVHSUEFjAUBggrBgEFBQcD -AgYIKwYBBQUHAwQwSAYDVR0fBEEwPzA9oDugOYY3aHR0cDovL2NybDA5LmFjdGFs -aXMuaXQvUmVwb3NpdG9yeS9BVVRIQ0wtRzIvZ2V0TGFzdENSTDAdBgNVHQ4EFgQU -FrtAdAOjrcVeHg5K+T7sj7GHySMwDgYDVR0PAQH/BAQDAgWgMA0GCSqGSIb3DQEB -CwUAA4IBAQAa9lXKDmV9874ojmIZEBL1S8mKaSNBWP+n0vp5FO0Yh5oL9lspYTPs -8s6alWUSpVHV8if4uZ2EfcNpNkm9dAajj2n/F/Jyfkp8URu4uvBfm1QColl/zM/D -x4B7FaD2dw0jTF/k5ulDmzUOc4k+j3LtZNbDOZMF/2g05hSKde/he1njlY3oKa9g -VW8ftc2NwiSMthxyEIM+ALbNQVML2oN50gArBn5GeI22/aIBZxjtbEdmSTZIf82H -sOwAnhJ+pD5iIPaF2oa0yN3PvI6IGxLpEv16tQO1N6e5bdP6ZDwqTQJyK+oNTNda -yPLCqVTFJQWaCR5ZTekRQPTDZkjxjxbs ------END CERTIFICATE-----`; - ava.default('[unit][KeyUtil.parse] S/MIME key parsing works', async t => { - const key = await KeyUtil.parse(smimeCert); - expect(key.id).to.equal('63F7025E700F3945301FB2FBA5674F84'); + const key = await KeyUtil.parse(testConstants.smimeCert); + expect(key.id).to.equal('16BB407403A3ADC55E1E0E4AF93EEC8FB187C923'); expect(key.type).to.equal('x509'); expect(key.usableForEncryption).to.equal(true); expect(key.usableForSigning).to.equal(true); @@ -523,20 +493,20 @@ vpQiyk4ceuTNkUZ/qmgiMpQLxXZnDDo= }); ava.default('[unit][KeyUtil.readMany] Parsing one S/MIME key', async t => { - const { keys, errs } = await KeyUtil.readMany(Buf.fromUtfStr(smimeCert)); + const { keys, errs } = await KeyUtil.readMany(Buf.fromUtfStr(testConstants.smimeCert)); expect(keys.length).to.equal(1); expect(errs.length).to.equal(0); - expect(keys[0].id).to.equal('63F7025E700F3945301FB2FBA5674F84'); + expect(keys[0].id).to.equal('16BB407403A3ADC55E1E0E4AF93EEC8FB187C923'); expect(keys[0].type).to.equal('x509'); t.pass(); }); ava.default('[unit][KeyUtil.readMany] Parsing unarmored S/MIME certificate', async t => { - const pem = forge.pem.decode(smimeCert)[0]; + const pem = forge.pem.decode(testConstants.smimeCert)[0]; const { keys, errs } = await KeyUtil.readMany(Buf.fromRawBytesStr(pem.body)); expect(keys.length).to.equal(1); expect(errs.length).to.equal(0); - expect(keys[0].id).to.equal('63F7025E700F3945301FB2FBA5674F84'); + expect(keys[0].id).to.equal('16BB407403A3ADC55E1E0E4AF93EEC8FB187C923'); expect(keys[0].type).to.equal('x509'); t.pass(); }); @@ -627,13 +597,13 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY t.pass(); }); - const smimeAndPgp = smimeCert + '\r\n' + expiredPgp; + const smimeAndPgp = testConstants.smimeCert + '\r\n' + expiredPgp; ava.default('[unit][KeyUtil.readMany] Parsing one S/MIME and one OpenPGP armored keys', async t => { const { keys, errs } = await KeyUtil.readMany(Buf.fromUtfStr(smimeAndPgp)); expect(keys.length).to.equal(2); expect(errs.length).to.equal(0); - expect(keys.some(key => key.id === '63F7025E700F3945301FB2FBA5674F84')).to.equal(true); + expect(keys.some(key => key.id === '16BB407403A3ADC55E1E0E4AF93EEC8FB187C923')).to.equal(true); expect(keys.some(key => key.id === '3449178FCAAF758E24CB68BE62CB4E6F9ECA6FA1')).to.equal(true); expect(keys.some(key => key.type === 'openpgp')).to.equal(true); expect(keys.some(key => key.type === 'x509')).to.equal(true); @@ -1539,7 +1509,7 @@ jA== const key = Buffer.from(`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`, 'base64'); const parsed = await KeyUtil.parseBinary(key, 'test'); expect(parsed.length).to.be.equal(1); - expect(parsed[0].id).to.be.equal('25639FA393D577A074F9E4146F74195213042417'); + expect(parsed[0].id).to.be.equal('60EFFE4DF7B2114A77021459C273F0AA864AFF7F'); expect(parsed[0].type).to.be.equal('x509'); expect(parsed[0].emails.length).to.be.equal(1); expect(parsed[0].emails[0]).to.be.equal('test@example.com'); From 39688c0c40d1aa35583f1bb79b5ac2cc06de06ee Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Tue, 20 Apr 2021 14:54:11 -0400 Subject: [PATCH 02/17] test fix --- test/source/tests/compose.ts | 53 ++++++----------------------- test/source/tests/tooling/consts.ts | 1 + 2 files changed, 12 insertions(+), 42 deletions(-) diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index dbd6b625d8b..07b4edd314e 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -21,14 +21,12 @@ import { TestWithBrowser } from './../test'; import { expect } from "chai"; import { BrowserRecipe } from './tooling/browser-recipe'; import { SetupPageRecipe } from './page-recipe/setup-page-recipe'; +import { testConstants } from './tooling/consts'; // tslint:disable:no-blank-lines-func // tslint:disable:no-unused-expression /* eslint-disable max-len */ -// get s/mime cert for testing: https://extrassl.actalis.it/portal/uapub/freemail?lang=en -const smimeCert = "-----BEGIN CERTIFICATE-----\nMIIE9DCCA9ygAwIBAgIQY/cCXnAPOUUwH7L7pWdPhDANBgkqhkiG9w0BAQsFADCB\njTELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB0JlcmdhbW8xGTAXBgNVBAcMEFBvbnRl\nIFNhbiBQaWV0cm8xIzAhBgNVBAoMGkFjdGFsaXMgUy5wLkEuLzAzMzU4NTIwOTY3\nMSwwKgYDVQQDDCNBY3RhbGlzIENsaWVudCBBdXRoZW50aWNhdGlvbiBDQSBHMjAe\nFw0yMDAzMjMxMzU2NDZaFw0yMTAzMjMxMzU2NDZaMCIxIDAeBgNVBAMMF2FjdGFs\naXNAbWV0YS4zM21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEArVVpXBkzGvcqib8rDwqHCaKm2EiPslQ8I0G1ZDxrs6Ke2QXNm3yGVwOzkVvK\neEnuzE5M4BBeh+GwcfvoyS/xI6m44WWnqj65cJoSLA1ypE4D4urv/pzG783y2Vdy\nQ96izBdFyevsil89Z2AxZxrFh1RC2XvgXad4yyD4yvVpHskfPexnhLliHl7cpXjw\n5D2n1hBGR8CSDbQAgO58PB7Y2ldrTi+rWBu2Akuk/YyWOOiGA8pdfLBIkOFJTeQc\nm7+vWP2JTN6Xp+JkGvXQBRaqwyGVg8fSc4e7uGCXZaH5/Na2FXY2OL+tYDDb27zS\n3cBrzEbGVjA6raYxcrFWV4PkdwIDAQABo4IBuDCCAbQwDAYDVR0TAQH/BAIwADAf\nBgNVHSMEGDAWgBRr8o2eaMElBB9RNFf2FlyU6k1pGjB+BggrBgEFBQcBAQRyMHAw\nOwYIKwYBBQUHMAKGL2h0dHA6Ly9jYWNlcnQuYWN0YWxpcy5pdC9jZXJ0cy9hY3Rh\nbGlzLWF1dGNsaWcyMDEGCCsGAQUFBzABhiVodHRwOi8vb2NzcDA5LmFjdGFsaXMu\naXQvVkEvQVVUSENMLUcyMCIGA1UdEQQbMBmBF2FjdGFsaXNAbWV0YS4zM21haWwu\nY29tMEcGA1UdIARAMD4wPAYGK4EfARgBMDIwMAYIKwYBBQUHAgEWJGh0dHBzOi8v\nd3d3LmFjdGFsaXMuaXQvYXJlYS1kb3dubG9hZDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwQwSAYDVR0fBEEwPzA9oDugOYY3aHR0cDovL2NybDA5LmFjdGFs\naXMuaXQvUmVwb3NpdG9yeS9BVVRIQ0wtRzIvZ2V0TGFzdENSTDAdBgNVHQ4EFgQU\nFrtAdAOjrcVeHg5K+T7sj7GHySMwDgYDVR0PAQH/BAQDAgWgMA0GCSqGSIb3DQEB\nCwUAA4IBAQAa9lXKDmV9874ojmIZEBL1S8mKaSNBWP+n0vp5FO0Yh5oL9lspYTPs\n8s6alWUSpVHV8if4uZ2EfcNpNkm9dAajj2n/F/Jyfkp8URu4uvBfm1QColl/zM/D\nx4B7FaD2dw0jTF/k5ulDmzUOc4k+j3LtZNbDOZMF/2g05hSKde/he1njlY3oKa9g\nVW8ftc2NwiSMthxyEIM+ALbNQVML2oN50gArBn5GeI22/aIBZxjtbEdmSTZIf82H\nsOwAnhJ+pD5iIPaF2oa0yN3PvI6IGxLpEv16tQO1N6e5bdP6ZDwqTQJyK+oNTNda\nyPLCqVTFJQWaCR5ZTekRQPTDZkjxjxbs\n-----END CERTIFICATE-----"; - export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: TestWithBrowser) => { if (testVariant !== 'CONSUMER-LIVE-GMAIL') { @@ -563,7 +561,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te // add key + send composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); await ComposePageRecipe.fillMsg(composeFrame, { to: 'testsearchorder3@flowcrypt.com' }, t.title); - await pastePublicKeyManually(composeFrame, inboxPage, 'testsearchorder3@flowcrypt.com', smimeCert); + await pastePublicKeyManually(composeFrame, inboxPage, 'testsearchorder3@flowcrypt.com', testConstants.smimeCert); await composeFrame.waitAndClick('@action-send', { delay: 1 }); await composeFrame.waitAndClick('.swal2-cancel'); await composeFrame.waitAndClick('@action-close-new-message'); @@ -571,7 +569,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te // add key composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); await ComposePageRecipe.fillMsg(composeFrame, { to: 'testsearchorder9@flowcrypt.com' }, t.title); - await pastePublicKeyManually(composeFrame, inboxPage, 'testsearchorder9@flowcrypt.com', smimeCert); + await pastePublicKeyManually(composeFrame, inboxPage, 'testsearchorder9@flowcrypt.com', testConstants.smimeCert); await composeFrame.waitAndClick('@action-close-new-message'); await inboxPage.waitTillGone('@container-new-message'); // send @@ -840,7 +838,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('ci.tests.gmail@flowcrypt.dev')); const composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime@recipient.com' }, t.title); - await pastePublicKeyManually(composeFrame, inboxPage, 'smime@recipient.com', smimeCert); + await pastePublicKeyManually(composeFrame, inboxPage, 'smime@recipient.com', testConstants.smimeCert); await composeFrame.waitAndClick('@action-send', { delay: 2 }); await inboxPage.waitTillGone('@container-new-message'); })); @@ -849,8 +847,8 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('ci.tests.gmail@flowcrypt.dev')); const composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime1@recipient.com', cc: 'smime2@recipient.com' }, t.title); - await pastePublicKeyManually(composeFrame, inboxPage, 'smime1@recipient.com', smimeCert); - await pastePublicKeyManually(composeFrame, inboxPage, 'smime2@recipient.com', smimeCert); + await pastePublicKeyManually(composeFrame, inboxPage, 'smime1@recipient.com', testConstants.smimeCert); + await pastePublicKeyManually(composeFrame, inboxPage, 'smime2@recipient.com', testConstants.smimeCert); await composeFrame.waitAndClick('@action-send', { delay: 2 }); await inboxPage.waitTillGone('@container-new-message'); })); @@ -860,7 +858,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('ci.tests.gmail@flowcrypt.dev')); const composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime.attachment@recipient.com' }, t.title); - await pastePublicKeyManually(composeFrame, inboxPage, 'smime.attachment@recipient.com', smimeCert); + await pastePublicKeyManually(composeFrame, inboxPage, 'smime.attachment@recipient.com', testConstants.smimeCert); const fileInput = await composeFrame.target.$('input[type=file]'); await fileInput!.uploadFile('test/samples/small.txt', 'test/samples/small.png', 'test/samples/small.pdf'); await composeFrame.waitAndClick('@action-send', { delay: 2 }); @@ -871,7 +869,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('ci.tests.gmail@flowcrypt.dev')); const composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime@recipient.com', cc: 'human@flowcrypt.com' }, t.title); - await pastePublicKeyManually(composeFrame, inboxPage, 'smime@recipient.com', smimeCert); + await pastePublicKeyManually(composeFrame, inboxPage, 'smime@recipient.com', testConstants.smimeCert); await composeFrame.waitAndClick('@action-send', { delay: 2 }); await PageRecipe.waitForModalAndRespond(composeFrame, 'error', { contentToCheck: 'Failed to send message due to: Error: Cannot use mixed OpenPGP (human@flowcrypt.com) and S/MIME (smime@recipient.com) public keys yet.If you need to email S/MIME recipient, do not add any OpenPGP recipient at the same time.', @@ -883,7 +881,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const inboxPage = await browser.newPage(t, TestUrls.extensionInbox('ci.tests.gmail@flowcrypt.dev')); const composeFrame = await InboxPageRecipe.openAndGetComposeFrame(inboxPage); await ComposePageRecipe.fillMsg(composeFrame, { to: 'smime@recipient.com' }, t.title); - const brokenCert = smimeCert.split('\n'); + const brokenCert = testConstants.smimeCert.split('\n'); brokenCert.splice(5, 5); // remove 5th to 10th line from cert - make it useless const addPubkeyDialog = await pastePublicKeyManuallyNoClose(composeFrame, inboxPage, 'smime@recipient.com', brokenCert.join('\n')); await addPubkeyDialog.waitAndRespondToModal('error', 'confirm', 'Too few bytes to read ASN.1 value.'); @@ -1011,35 +1009,6 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te ava.default('import S/MIME cert', testWithBrowser('ci.tests.gmail', async (t, browser) => { // the cert since expired, therefore test was updated to reflect that - const smimeCert = `-----BEGIN CERTIFICATE----- -MIIE9DCCA9ygAwIBAgIQY/cCXnAPOUUwH7L7pWdPhDANBgkqhkiG9w0BAQsFADCB -jTELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB0JlcmdhbW8xGTAXBgNVBAcMEFBvbnRl -IFNhbiBQaWV0cm8xIzAhBgNVBAoMGkFjdGFsaXMgUy5wLkEuLzAzMzU4NTIwOTY3 -MSwwKgYDVQQDDCNBY3RhbGlzIENsaWVudCBBdXRoZW50aWNhdGlvbiBDQSBHMjAe -Fw0yMDAzMjMxMzU2NDZaFw0yMTAzMjMxMzU2NDZaMCIxIDAeBgNVBAMMF2FjdGFs -aXNAbWV0YS4zM21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEArVVpXBkzGvcqib8rDwqHCaKm2EiPslQ8I0G1ZDxrs6Ke2QXNm3yGVwOzkVvK -eEnuzE5M4BBeh+GwcfvoyS/xI6m44WWnqj65cJoSLA1ypE4D4urv/pzG783y2Vdy -Q96izBdFyevsil89Z2AxZxrFh1RC2XvgXad4yyD4yvVpHskfPexnhLliHl7cpXjw -5D2n1hBGR8CSDbQAgO58PB7Y2ldrTi+rWBu2Akuk/YyWOOiGA8pdfLBIkOFJTeQc -m7+vWP2JTN6Xp+JkGvXQBRaqwyGVg8fSc4e7uGCXZaH5/Na2FXY2OL+tYDDb27zS -3cBrzEbGVjA6raYxcrFWV4PkdwIDAQABo4IBuDCCAbQwDAYDVR0TAQH/BAIwADAf -BgNVHSMEGDAWgBRr8o2eaMElBB9RNFf2FlyU6k1pGjB+BggrBgEFBQcBAQRyMHAw -OwYIKwYBBQUHMAKGL2h0dHA6Ly9jYWNlcnQuYWN0YWxpcy5pdC9jZXJ0cy9hY3Rh -bGlzLWF1dGNsaWcyMDEGCCsGAQUFBzABhiVodHRwOi8vb2NzcDA5LmFjdGFsaXMu -aXQvVkEvQVVUSENMLUcyMCIGA1UdEQQbMBmBF2FjdGFsaXNAbWV0YS4zM21haWwu -Y29tMEcGA1UdIARAMD4wPAYGK4EfARgBMDIwMAYIKwYBBQUHAgEWJGh0dHBzOi8v -d3d3LmFjdGFsaXMuaXQvYXJlYS1kb3dubG9hZDAdBgNVHSUEFjAUBggrBgEFBQcD -AgYIKwYBBQUHAwQwSAYDVR0fBEEwPzA9oDugOYY3aHR0cDovL2NybDA5LmFjdGFs -aXMuaXQvUmVwb3NpdG9yeS9BVVRIQ0wtRzIvZ2V0TGFzdENSTDAdBgNVHQ4EFgQU -FrtAdAOjrcVeHg5K+T7sj7GHySMwDgYDVR0PAQH/BAQDAgWgMA0GCSqGSIb3DQEB -CwUAA4IBAQAa9lXKDmV9874ojmIZEBL1S8mKaSNBWP+n0vp5FO0Yh5oL9lspYTPs -8s6alWUSpVHV8if4uZ2EfcNpNkm9dAajj2n/F/Jyfkp8URu4uvBfm1QColl/zM/D -x4B7FaD2dw0jTF/k5ulDmzUOc4k+j3LtZNbDOZMF/2g05hSKde/he1njlY3oKa9g -VW8ftc2NwiSMthxyEIM+ALbNQVML2oN50gArBn5GeI22/aIBZxjtbEdmSTZIf82H -sOwAnhJ+pD5iIPaF2oa0yN3PvI6IGxLpEv16tQO1N6e5bdP6ZDwqTQJyK+oNTNda -yPLCqVTFJQWaCR5ZTekRQPTDZkjxjxbs ------END CERTIFICATE-----`; const recipientEmail = 'actalis@meta.33mail.com'; // add S/MIME key manually const settingsPage = await browser.newPage(t, TestUrls.extensionSettings('ci.tests.gmail@flowcrypt.dev')); @@ -1047,7 +1016,7 @@ yPLCqVTFJQWaCR5ZTekRQPTDZkjxjxbs const contactsFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, '@action-open-contacts-page', ['contacts.htm', 'placement=settings']); await contactsFrame.waitAll('@page-contacts'); await contactsFrame.waitAndClick('@action-show-import-public-keys-form', { confirmGone: true }); - await contactsFrame.waitAndType('@input-bulk-public-keys', smimeCert); + await contactsFrame.waitAndType('@input-bulk-public-keys', testConstants.smimeCert); await contactsFrame.waitAndClick('@action-show-parsed-public-keys', { confirmGone: true }); await contactsFrame.waitAll('iframe'); const pubkeyFrame = await contactsFrame.getFrame(['pgp_pubkey.htm']); @@ -1057,7 +1026,7 @@ yPLCqVTFJQWaCR5ZTekRQPTDZkjxjxbs await contactsFrame.waitAndClick('@action-back-to-contact-list', { confirmGone: true }); await contactsFrame.waitAndClick(`@action-show-pubkey-${recipientEmail.replace(/[^a-z0-9]+/g, '')}`, { confirmGone: true }); await contactsFrame.waitForContent('@container-pubkey-details', 'Type: x509'); - await contactsFrame.waitForContent('@container-pubkey-details', 'Fingerprint: 63F7 025E 700F 3945 301F B2FB A567 4F84'); + await contactsFrame.waitForContent('@container-pubkey-details', 'Fingerprint: 16BB 4074 03A3 ADC5 5E1E 0E4A F93E EC8F B187 C923'); await contactsFrame.waitForContent('@container-pubkey-details', `Users: ${recipientEmail}`); await contactsFrame.waitForContent('@container-pubkey-details', 'Created on: Mon Mar 23 2020'); await contactsFrame.waitForContent('@container-pubkey-details', 'Expiration: Tue Mar 23 2021'); diff --git a/test/source/tests/tooling/consts.ts b/test/source/tests/tooling/consts.ts index 41df16699b2..e7d3fb14cfc 100644 --- a/test/source/tests/tooling/consts.ts +++ b/test/source/tests/tooling/consts.ts @@ -763,6 +763,7 @@ wHlKRDVNpiNVKyevQ8PI9iQGMEkKOv23GdDuC0F0rCCnqp6gEVvezjMMk5bMCEiY4sn2+wwVDpC3 5adfA360UnwZSMt5qrk= =EDBf -----END PGP PUBLIC KEY BLOCK-----`, + // get s/mime cert for testing: https://extrassl.actalis.it/portal/uapub/freemail?lang=en smimeCert: `-----BEGIN CERTIFICATE----- MIIE9DCCA9ygAwIBAgIQY/cCXnAPOUUwH7L7pWdPhDANBgkqhkiG9w0BAQsFADCB jTELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB0JlcmdhbW8xGTAXBgNVBAcMEFBvbnRl From 5d3e084110fb1eeebca7bd2b82e4389aabad695b Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 21 Apr 2021 03:12:58 -0400 Subject: [PATCH 03/17] updated comment --- extension/js/common/core/crypto/key.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index 3b176eb7018..e8bd5f535e8 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -20,7 +20,7 @@ import { MsgBlock } from '../msg-block.js'; */ export interface Key { type: 'openpgp' | 'x509'; - id: string; // a fingerprint + id: string; // a fingerprint of the primary key in OpenPGP, and similarly a fingerprint of the actual cryptographic key (eg RSA fingerprint) in S/MIME allIds: string[]; // a list of fingerprints, including those for subkeys created: number; lastModified: number | undefined; // date of last signature, or undefined if never had valid signature From fb4ff534627aee8d9b1dc1b2552cc1a7d4069a18 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sat, 24 Apr 2021 05:14:38 -0400 Subject: [PATCH 04/17] construct IssuerAndSerialNumber as per PKCS7 --- extension/js/common/core/crypto/key.ts | 1 + .../js/common/core/crypto/smime/smime-key.ts | 9 ++++++ extension/types/node-forge.d.ts | 32 ++++++++++++------- test/source/tests/unit-node.ts | 9 ++++++ 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index e8bd5f535e8..a2b86a81afd 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -43,6 +43,7 @@ export interface Key { bits?: number, algorithmId: number }; + issuerAndSerialNumber?: string | undefined; // DER-encoded IssuerAndSerialNumber of X.509 Certificate as raw string } export type PubkeyResult = { pubkey: Key, email: string, isMine: boolean }; diff --git a/extension/js/common/core/crypto/smime/smime-key.ts b/extension/js/common/core/crypto/smime/smime-key.ts index 4a686d4e60b..2cd88d7f246 100644 --- a/extension/js/common/core/crypto/smime/smime-key.ts +++ b/extension/js/common/core/crypto/smime/smime-key.ts @@ -94,6 +94,14 @@ export class SmimeKey { const fingerprint = forge.pki.getPublicKeyFingerprint(certificate.publicKey, { encoding: 'hex' }).toUpperCase(); SmimeKey.removeWeakKeys(certificate); const emails = SmimeKey.getNormalizedEmailsFromCertificate(certificate); + const issuerAndSerialNumberAsn1 = + forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [ + // Name + forge.pki.distinguishedNameToAsn1(certificate.issuer), + // Serial + forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.INTEGER, false, + forge.util.hexToBytes(certificate.serialNumber)) + ]); const key = { type: 'x509', id: fingerprint, @@ -111,6 +119,7 @@ export class SmimeKey { fullyEncrypted: false, isPublic: !certificate.privateKey, isPrivate: !!certificate.privateKey, + issuerAndSerialNumber: forge.asn1.toDer(issuerAndSerialNumberAsn1).getBytes() } as Key; (key as unknown as { rawArmored: string }).rawArmored = pem; return key; diff --git a/extension/types/node-forge.d.ts b/extension/types/node-forge.d.ts index db6e6f02fa4..1c946be1c96 100644 --- a/extension/types/node-forge.d.ts +++ b/extension/types/node-forge.d.ts @@ -205,6 +205,12 @@ declare module "node-forge" { extensions?: any[]; } + interface DistinguishedName { + getField(sn: string | CertificateFieldOptions): any; + addField(attr: CertificateField): void; + attributes: any[]; + hash: string; + } interface Certificate { version: number; @@ -215,18 +221,8 @@ declare module "node-forge" { notBefore: Date; notAfter: Date; }; - issuer: { - getField(sn: string | CertificateFieldOptions): any; - addField(attr: CertificateField): void; - attributes: any[]; - hash: any; - }; - subject: { - getField(sn: string | CertificateFieldOptions): any; - addField(attr: CertificateField): void; - attributes: any[]; - hash: any; - }; + issuer: DistinguishedName; + subject: DistinguishedName; extensions: any[]; privateKey: PrivateKey | undefined; publicKey: PublicKey | undefined; @@ -350,6 +346,8 @@ declare module "node-forge" { function certificateToAsn1(cert: Certificate): asn1.Asn1; + function distinguishedNameToAsn1(dn: DistinguishedName): asn1.Asn1; + function decryptRsaPrivateKey(pem: PEM, passphrase?: string): PrivateKey; function createCertificate(): Certificate; @@ -662,6 +660,7 @@ declare module "node-forge" { namespace pkcs7 { interface PkcsSignedData { + type: '1.2.840.113549.1.7.2'; content?: string | util.ByteBuffer; contentInfo?: { value: any[] }; @@ -681,13 +680,22 @@ declare module "node-forge" { function createSignedData(): PkcsSignedData; interface PkcsEnvelopedData { + type: '1.2.840.113549.1.7.3'; content?: string | util.ByteBuffer; addRecipient(certificate: pki.Certificate): void; encrypt(): void; toAsn1(): asn1.Asn1; } + interface PkcsEncryptedData { + type: '1.2.840.113549.1.7.6'; + // todo: encryptedContent; + // todo: decrypt(key); + // todo: fromAsn1(obj); + } + function createEnvelopedData(): PkcsEnvelopedData; + function messageFromPem(pem: pki.PEM): PkcsEnvelopedData | PkcsSignedData | PkcsEncryptedData; } namespace pkcs5 { diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index f5770a8473b..39ff0248a9c 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -511,6 +511,15 @@ vpQiyk4ceuTNkUZ/qmgiMpQLxXZnDDo= t.pass(); }); + ava.default('[unit][KeyUtil.parse] issuerAndSerialNumber of S/MIME certificate is constructed according to PKCS#7', async t => { + const key = await KeyUtil.parse(testConstants.smimeCert); + const buf = Buf.with((await MsgUtil.encryptMessage( + { pubkeys: [key], data: Buf.fromUtfStr('anything'), armor: true }) as PgpMsgMethod.EncryptX509Result).data); + const raw = buf.toRawBytesStr(); + expect(raw).to.include(key.issuerAndSerialNumber); + t.pass(); + }); + ava.default('[unit][KeyUtil.parse] Correctly extracting email from SubjectAltName of S/MIME certificate', async t => { /* // generate a key pair From cbc6967196f122c323b2c04d07fcfb21e4ea475b Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sun, 25 Apr 2021 06:53:33 -0400 Subject: [PATCH 05/17] enable to search by IssuerAndSerialNumber in ContactStore --- extension/chrome/dev/ci_unit_test.ts | 5 ++- .../js/common/core/crypto/pgp/openpgp-key.ts | 3 -- .../js/common/platform/store/contact-store.ts | 16 ++++++++-- .../browser-unit-tests/unit-ContactStore.js | 31 +++++++++++++++++++ 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/extension/chrome/dev/ci_unit_test.ts b/extension/chrome/dev/ci_unit_test.ts index c25432f11ba..93dbc723d11 100644 --- a/extension/chrome/dev/ci_unit_test.ts +++ b/extension/chrome/dev/ci_unit_test.ts @@ -15,6 +15,8 @@ import { Sks } from '../../js/common/api/key-server/sks.js'; import { Ui } from '../../js/common/browser/ui.js'; import { ContactStore } from '../../js/common/platform/store/contact-store.js'; import { Debug } from '../../js/common/platform/debug.js'; +import * as forge from 'node-forge'; + /** * importing all libs that are tested in ci tests @@ -33,7 +35,8 @@ const libs: any[] = [ MsgUtil, Ui, ContactStore, - Debug + Debug, + forge ]; // add them to global scope so ci can use them diff --git a/extension/js/common/core/crypto/pgp/openpgp-key.ts b/extension/js/common/core/crypto/pgp/openpgp-key.ts index feaeb16d96e..c365d53d6b2 100644 --- a/extension/js/common/core/crypto/pgp/openpgp-key.ts +++ b/extension/js/common/core/crypto/pgp/openpgp-key.ts @@ -391,9 +391,6 @@ export class OpenPGPKey { } public static fingerprintToLongid = (fingerprint: string) => { - if (fingerprint.length === 32) { // s/mime keys - return fingerprint; // leave as is - s/mime has no concept of longids - } if (fingerprint.length === 40) { // pgp keys return fingerprint.substr(-16).toUpperCase(); } diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index bc6c2b602ac..cc0ff089d47 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -7,6 +7,7 @@ import { BrowserMsg } from '../../browser/browser-msg.js'; import { DateUtility, Str } from '../../core/common.js'; import { Key, Contact, KeyUtil } from '../../core/crypto/key.js'; import { OpenPGPKey } from '../../core/crypto/pgp/openpgp-key.js'; +import { Buf } from '../../core/buf.js'; // tslint:disable:no-null-keyword @@ -295,6 +296,17 @@ export class ContactStore extends AbstractStore { } } + public static getPubkeyLongids = (pubkey: Key): string[] => { + if (pubkey.type !== 'x509') { + return pubkey.allIds.map(id => OpenPGPKey.fingerprintToLongid(id)); + } + const encodedIssuerAndSerialNumber = 'X509-' + Buf.fromRawBytesStr(pubkey.issuerAndSerialNumber!).toBase64Str(); + if (!encodedIssuerAndSerialNumber) { + throw new Error(`Cannot extract IssuerAndSerialNumber from the certificate for: ${pubkey.id}`); + } + return [encodedIssuerAndSerialNumber]; + } + private static getPubkeyId = (pubkey: Key): string => { return (pubkey.type === 'x509') ? (pubkey.id + '-X509') : pubkey.id; } @@ -308,7 +320,7 @@ export class ContactStore extends AbstractStore { fingerprint: ContactStore.getPubkeyId(update.pubkey), lastCheck: DateUtility.asNumber(update.pubkeyLastCheck ?? existingPubkey?.lastCheck), expiresOn: keyAttrs.expiresOn, - longids: update.pubkey.allIds.map(id => OpenPGPKey.fingerprintToLongid(id)), + longids: ContactStore.getPubkeyLongids(update.pubkey), armoredKey: KeyUtil.armor(update.pubkey) } as Pubkey; } else if (update.pubkeyLastCheck) { @@ -459,7 +471,7 @@ export class ContactStore extends AbstractStore { } private static dbContactInternalGetOne = async (db: IDBDatabase, emailOrLongid: string): Promise => { - if (!/^[A-F0-9]{16}$/.test(emailOrLongid)) { // email + if (emailOrLongid.includes('@')) { // email const contactWithAllPubkeys = await ContactStore.getOneWithAllPubkeys(db, emailOrLongid); if (!contactWithAllPubkeys) { return contactWithAllPubkeys; diff --git a/test/source/tests/browser-unit-tests/unit-ContactStore.js b/test/source/tests/browser-unit-tests/unit-ContactStore.js index b9fcbf248eb..f5932c99501 100644 --- a/test/source/tests/browser-unit-tests/unit-ContactStore.js +++ b/test/source/tests/browser-unit-tests/unit-ContactStore.js @@ -389,3 +389,34 @@ BROWSER_UNIT_TEST_NAME(`ContactStore stores postfixed fingerprint internally for } return 'pass'; })(); + +BROWSER_UNIT_TEST_NAME(`ContactStore searches S/MIME Certificate by PKCS#7 message recipient`); +(async () => { + const db = await ContactStore.dbOpen(); + const email = 'actalis@meta.33mail.com'; + const pubkey = testConstants.smimeCert; + const contacts = [await ContactStore.obj({ email, pubkey })]; + await ContactStore.save(db, contacts); + const p7 = forge.pkcs7.createEnvelopedData(); + const certificate = forge.pki.certificateFromPem(pubkey); + p7.addRecipient(certificate); + const recipient = p7.recipients[0]; + const issuerAndSerialNumberAsn1 = + forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [ + // Name + forge.pki.distinguishedNameToAsn1({ attributes: recipient.issuer }), + // Serial + forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.INTEGER, false, + forge.util.hexToBytes(recipient.serialNumber)) + ]); + const der = forge.asn1.toDer(issuerAndSerialNumberAsn1).getBytes(); + const buf = Buf.fromRawBytesStr(der); + const [contact] = await ContactStore.get(db, ['X509-' + buf.toBase64Str()]); + const foundCert = KeyUtil.armor(contact.pubkey); + console.log('foundCert'); + console.log(foundCert); + if (foundCert !== pubkey) { + throw new Error(`The certificate wasn't found by S/MIME IssuerAndSerialNumber`); + } + return 'pass'; +})(); From cd346923db843247c61b65584290990da5256ef1 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 28 Apr 2021 12:49:25 -0400 Subject: [PATCH 06/17] careful longid calculation for S/MIME --- extension/js/common/core/crypto/key.ts | 23 +++++++++++++++++-- .../js/common/core/crypto/pgp/msg-util.ts | 2 +- .../js/common/platform/store/contact-store.ts | 15 +----------- .../browser-unit-tests/unit-ContactStore.js | 4 +++- test/source/tests/unit-node.ts | 7 +++--- 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index a2b86a81afd..486fdddfa8c 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -340,13 +340,32 @@ export class KeyUtil { if (!prv.isPrivate) { throw new Error('Key passed into KeyUtil.keyInfoObj must be a Private Key'); } + const pubkey = await KeyUtil.asPublicKey(prv); return { private: KeyUtil.armor(prv), - public: KeyUtil.armor(await KeyUtil.asPublicKey(prv)), - longid: OpenPGPKey.fingerprintToLongid(prv.id), + public: KeyUtil.armor(pubkey), + longid: KeyUtil.getPrimaryLongid(pubkey), emails: prv.emails, fingerprints: prv.allIds, }; } + public static getPubkeyLongids = (pubkey: Key): string[] => { + if (pubkey.type !== 'x509') { + return pubkey.allIds.map(id => OpenPGPKey.fingerprintToLongid(id)); + } + return [KeyUtil.getPrimaryLongid(pubkey)]; + } + + public static getPrimaryLongid = (pubkey: Key): string => { + if (pubkey.type !== 'x509') { + return OpenPGPKey.fingerprintToLongid(pubkey.id); + } + const encodedIssuerAndSerialNumber = 'X509-' + Buf.fromRawBytesStr(pubkey.issuerAndSerialNumber!).toBase64Str(); + if (!encodedIssuerAndSerialNumber) { + throw new Error(`Cannot extract IssuerAndSerialNumber from the certificate for: ${pubkey.id}`); + } + return encodedIssuerAndSerialNumber; + } + } diff --git a/extension/js/common/core/crypto/pgp/msg-util.ts b/extension/js/common/core/crypto/pgp/msg-util.ts index 8bd6d56b71b..a598b6a87c0 100644 --- a/extension/js/common/core/crypto/pgp/msg-util.ts +++ b/extension/js/common/core/crypto/pgp/msg-util.ts @@ -269,7 +269,7 @@ export class MsgUtil { const msgKeyIds = m.getEncryptionKeyIds ? m.getEncryptionKeyIds() : []; const localKeyIds: string[] = []; for (const k of await Promise.all(armoredPubs.map(pub => KeyUtil.parse(pub)))) { - localKeyIds.push(...k.allIds.map(id => OpenPGPKey.fingerprintToLongid(id))); + localKeyIds.push(...KeyUtil.getPubkeyLongids(k)); } const diagnosis = { found_match: false, receivers: msgKeyIds.length }; for (const msgKeyId of msgKeyIds) { diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index cc0ff089d47..84227f2c1d7 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -6,8 +6,6 @@ import { opgp } from '../../core/crypto/pgp/openpgpjs-custom.js'; import { BrowserMsg } from '../../browser/browser-msg.js'; import { DateUtility, Str } from '../../core/common.js'; import { Key, Contact, KeyUtil } from '../../core/crypto/key.js'; -import { OpenPGPKey } from '../../core/crypto/pgp/openpgp-key.js'; -import { Buf } from '../../core/buf.js'; // tslint:disable:no-null-keyword @@ -296,17 +294,6 @@ export class ContactStore extends AbstractStore { } } - public static getPubkeyLongids = (pubkey: Key): string[] => { - if (pubkey.type !== 'x509') { - return pubkey.allIds.map(id => OpenPGPKey.fingerprintToLongid(id)); - } - const encodedIssuerAndSerialNumber = 'X509-' + Buf.fromRawBytesStr(pubkey.issuerAndSerialNumber!).toBase64Str(); - if (!encodedIssuerAndSerialNumber) { - throw new Error(`Cannot extract IssuerAndSerialNumber from the certificate for: ${pubkey.id}`); - } - return [encodedIssuerAndSerialNumber]; - } - private static getPubkeyId = (pubkey: Key): string => { return (pubkey.type === 'x509') ? (pubkey.id + '-X509') : pubkey.id; } @@ -320,7 +307,7 @@ export class ContactStore extends AbstractStore { fingerprint: ContactStore.getPubkeyId(update.pubkey), lastCheck: DateUtility.asNumber(update.pubkeyLastCheck ?? existingPubkey?.lastCheck), expiresOn: keyAttrs.expiresOn, - longids: ContactStore.getPubkeyLongids(update.pubkey), + longids: KeyUtil.getPubkeyLongids(update.pubkey), armoredKey: KeyUtil.armor(update.pubkey) } as Pubkey; } else if (update.pubkeyLastCheck) { diff --git a/test/source/tests/browser-unit-tests/unit-ContactStore.js b/test/source/tests/browser-unit-tests/unit-ContactStore.js index f5932c99501..1708f166643 100644 --- a/test/source/tests/browser-unit-tests/unit-ContactStore.js +++ b/test/source/tests/browser-unit-tests/unit-ContactStore.js @@ -371,7 +371,9 @@ BROWSER_UNIT_TEST_NAME(`ContactStore stores postfixed fingerprint internally for // extract the entity directly from the database const entityFp = '16BB407403A3ADC55E1E0E4AF93EEC8FB187C923-X509'; const fingerprint = '16BB407403A3ADC55E1E0E4AF93EEC8FB187C923'; - const longid = 'F93EEC8FB187C923'; + const longid = 'X509-MIGiMIGNMQswCQYDVQQGEwJJVDEQMA4GA1UECAwHQmVyZ2FtbzEZMBcGA1UEBwwQUG9udGUgU2Fu' + + 'IFBpZXRybzEjMCEGA1UECgwaQWN0YWxpcyBTLnAuQS4vMDMzNTg1MjA5NjcxLDAqBgNVBAMMI0FjdGFsaXMgQ2xpZW50IE' + + 'F1dGhlbnRpY2F0aW9uIENBIEcyAhBj9wJecA85RTAfsvulZ0+E'; const entity = await new Promise((resolve, reject) => { const req = db.transaction(['pubkeys'], 'readonly').objectStore('pubkeys').get(entityFp); ContactStore.setReqPipe(req, resolve, reject); diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index 39ff0248a9c..2ba67478e92 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -743,11 +743,12 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY t.pass(); }); - ava.default('[OpenPGPKey.fingerprintToLongid] for both pgp and s/mime', async t => { + ava.default('[OpenPGPKey.fingerprintToLongid] only works for pgp', async t => { // shorten pgp fingerprint to become longid expect(OpenPGPKey.fingerprintToLongid('3449178FCAAF758E24CB68BE62CB4E6F9ECA6FA1')).to.equal('62CB4E6F9ECA6FA1'); - // leave s/mime id as is - expect(OpenPGPKey.fingerprintToLongid('63F7025E700F3945301FB2FBA5674F84')).to.equal('63F7025E700F3945301FB2FBA5674F84'); + // throw on s/mime id + expect(() => OpenPGPKey.fingerprintToLongid('63F7025E700F3945301FB2FBA5674F84')). + to.throw('Unexpected fingerprint format (len: 32): "63F7025E700F3945301FB2FBA5674F84"'); // throw on broken format expect(() => OpenPGPKey.fingerprintToLongid('aaxx')).to.throw('Unexpected fingerprint format (len: 4): "aaxx"'); t.pass(); From b190f6c18c5a7fbfa54e29dcd385cb0f292c80f4 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 28 Apr 2021 13:17:56 -0400 Subject: [PATCH 07/17] fix --- extension/chrome/dev/ci_unit_test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/chrome/dev/ci_unit_test.ts b/extension/chrome/dev/ci_unit_test.ts index 3781f0b641e..12c3a6e3ee3 100644 --- a/extension/chrome/dev/ci_unit_test.ts +++ b/extension/chrome/dev/ci_unit_test.ts @@ -33,11 +33,11 @@ const libs: any[] = [ MsgUtil, Ui, ContactStore, - Debug, - forge + Debug ]; // add them to global scope so ci can use them for (const lib of libs) { (window as any)[(lib as any).name] = lib; } +(window as any)['forge'] = forge; From 0dc891902613dd511e55998793ffec99ab1233e4 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 28 Apr 2021 13:30:27 -0400 Subject: [PATCH 08/17] fix --- extension/chrome/dev/ci_unit_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/chrome/dev/ci_unit_test.ts b/extension/chrome/dev/ci_unit_test.ts index 12c3a6e3ee3..62c84fd426c 100644 --- a/extension/chrome/dev/ci_unit_test.ts +++ b/extension/chrome/dev/ci_unit_test.ts @@ -40,4 +40,4 @@ const libs: any[] = [ for (const lib of libs) { (window as any)[(lib as any).name] = lib; } -(window as any)['forge'] = forge; +(window as any).forge = forge; From 64cade72a6dd011321e99bc498e7969718cbd5a8 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Thu, 29 Apr 2021 06:56:43 -0400 Subject: [PATCH 09/17] nicer longid handling in mock ContactStore --- test/source/platform/store/contact-store.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/source/platform/store/contact-store.ts b/test/source/platform/store/contact-store.ts index 0a52e8db689..f97d3f9ed7f 100644 --- a/test/source/platform/store/contact-store.ts +++ b/test/source/platform/store/contact-store.ts @@ -18,7 +18,8 @@ export class ContactStore { public static get = async (db: void, emailOrLongid: string[]): Promise<(Contact | undefined)[]> => { const result = DATA.filter(x => emailOrLongid.includes(x.email) || - (x.pubkey && emailOrLongid.includes(OpenPGPKey.fingerprintToLongid(x.pubkey.id)))); + // is there any intersection + (x.pubkey && KeyUtil.getPubkeyLongids(x.pubkey).filter(y => emailOrLongid.includes(y)))); return result; } From 2e2732cb1abbaccd42d63e4133c1421a9c0208fb Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Thu, 29 Apr 2021 07:22:29 -0400 Subject: [PATCH 10/17] tslint fix --- test/source/platform/store/contact-store.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/source/platform/store/contact-store.ts b/test/source/platform/store/contact-store.ts index f97d3f9ed7f..7977dd5b585 100644 --- a/test/source/platform/store/contact-store.ts +++ b/test/source/platform/store/contact-store.ts @@ -3,7 +3,6 @@ // tslint:disable:no-null-keyword import { Contact, Key, KeyUtil } from '../../core/crypto/key'; -import { OpenPGPKey } from '../../core/crypto/pgp/openpgp-key.js'; const DATA: Contact[] = []; From 097300007c39e4e617befe9ade0400b73ad21d0e Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Thu, 29 Apr 2021 07:35:24 -0400 Subject: [PATCH 11/17] X.509 longid support for KeyInfo --- extension/js/common/core/crypto/key.ts | 7 +++++++ extension/js/common/core/crypto/pgp/msg-util.ts | 4 ++-- extension/js/common/platform/store/key-store.ts | 3 ++- test/source/tests/unit-node.ts | 9 ++++++--- test/source/util/index.ts | 3 ++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index 486fdddfa8c..6c42692c857 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -69,6 +69,7 @@ export interface KeyInfo { export interface KeyInfoWithOptionalPp extends KeyInfo { passphrase?: string; + type: 'openpgp' | 'x509' } export type KeyAlgo = 'curve25519' | 'rsa2048' | 'rsa4096'; @@ -368,4 +369,10 @@ export class KeyUtil { return encodedIssuerAndSerialNumber; } + public static getKeyInfoLongids = (ki: KeyInfoWithOptionalPp): string[] => { + if (ki.type !== 'x509') { + return ki.fingerprints.map(fp => OpenPGPKey.fingerprintToLongid(fp)); + } + return [ki.longid]; + } } diff --git a/extension/js/common/core/crypto/pgp/msg-util.ts b/extension/js/common/core/crypto/pgp/msg-util.ts index a598b6a87c0..d3a59ba2d34 100644 --- a/extension/js/common/core/crypto/pgp/msg-util.ts +++ b/extension/js/common/core/crypto/pgp/msg-util.ts @@ -311,8 +311,8 @@ export class MsgUtil { keys.encryptedFor = encryptionKeyids.map(kid => OpenPGPKey.bytesToLongid(kid.bytes)); await MsgUtil.cryptoMsgGetSignedBy(msg, keys); if (keys.encryptedFor.length) { - keys.prvMatching = kiWithPp.filter(ki => ki.fingerprints.some( - fp => keys.encryptedFor.includes(OpenPGPKey.fingerprintToLongid(fp)))); + keys.prvMatching = kiWithPp.filter(ki => KeyUtil.getKeyInfoLongids(ki).some( + longid => keys.encryptedFor.includes(longid))); keys.prvForDecrypt = keys.prvMatching.length ? keys.prvMatching : kiWithPp; } else { // prvs not needed for signed msgs keys.prvForDecrypt = []; diff --git a/extension/js/common/platform/store/key-store.ts b/extension/js/common/platform/store/key-store.ts index a7153c1eb4e..9d3cd540577 100644 --- a/extension/js/common/platform/store/key-store.ts +++ b/extension/js/common/platform/store/key-store.ts @@ -38,7 +38,8 @@ export class KeyStore extends AbstractStore { const keys = await KeyStore.get(acctEmail); const withPp: KeyInfoWithOptionalPp[] = []; for (const ki of keys) { - withPp.push({ ...ki, passphrase: await PassphraseStore.get(acctEmail, ki.fingerprints[0]) }); + const parsed = await KeyUtil.parse(ki.private); + withPp.push({ ...ki, type: parsed.type, passphrase: await PassphraseStore.get(acctEmail, ki.fingerprints[0]) }); } return withPp; } diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index 6e71e68b605..7a4319b8336 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -717,9 +717,11 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY // only encrypt with pub1 const { data } = await MsgUtil.encryptMessage({ pubkeys: [pub1], data: Buf.fromUtfStr('anything'), armor: true }) as PgpMsgMethod.EncryptPgpArmorResult; const m = await opgp.message.readArmored(Buf.fromUint8(data).toUtfStr()); + const parsed1 = await KeyUtil.parse(key1.private); + const parsed2 = await KeyUtil.parse(key2.private); const kisWithPp: KeyInfoWithOptionalPp[] = [ // supply both key1 and key2 for decrypt - { ... await KeyUtil.keyInfoObj(await KeyUtil.parse(key1.private)), passphrase }, - { ... await KeyUtil.keyInfoObj(await KeyUtil.parse(key2.private)), passphrase }, + { ... await KeyUtil.keyInfoObj(parsed1), type: parsed1.type, passphrase }, + { ... await KeyUtil.keyInfoObj(parsed2), type: parsed2.type, passphrase }, ]; // we are testing a private method here because the outcome of this method is not directly testable from the // public method that uses it. It only makes the public method faster, which is hard to test. @@ -804,7 +806,8 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY const justPrimaryPub = tmpPub.armor(); const pubkeys = [await KeyUtil.parse(justPrimaryPub)]; const encrypted = await MsgUtil.encryptMessage({ pubkeys, data, armor: true }) as PgpMsgMethod.EncryptPgpArmorResult; - const kisWithPp: KeyInfoWithOptionalPp[] = [{ ... await KeyUtil.keyInfoObj(await KeyUtil.parse(prvEncryptForSubkeyOnlyProtected)), passphrase }]; + const parsed = await KeyUtil.parse(prvEncryptForSubkeyOnlyProtected); + const kisWithPp: KeyInfoWithOptionalPp[] = [{ ... await KeyUtil.keyInfoObj(parsed), type: parsed.type, passphrase }]; const decrypted = await MsgUtil.decryptMessage({ kisWithPp, encryptedData: encrypted.data }); // todo - later we'll have an org rule for ignoring this, and then it will be expected to pass as follows: // expect(decrypted.success).to.equal(true); diff --git a/test/source/util/index.ts b/test/source/util/index.ts index a19d8a75922..e2acd61795d 100644 --- a/test/source/util/index.ts +++ b/test/source/util/index.ts @@ -73,7 +73,8 @@ export class Config { public static getKeyInfo = async (titles: string[]): Promise => { return await Promise.all(Config._secrets.keys .filter(key => key.armored && titles.includes(key.title)).map(async key => { - return { ...await KeyUtil.keyInfoObj(await KeyUtil.parse(key.armored!)), passphrase: key.passphrase }; + const parsed = await KeyUtil.parse(key.armored!); + return { ...await KeyUtil.keyInfoObj(parsed), type: parsed.type, passphrase: key.passphrase }; })); } From 1013a6e15632211d9f1267ec3b3f2cbd558a30cd Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Thu, 29 Apr 2021 11:12:41 -0400 Subject: [PATCH 12/17] fix --- extension/js/common/platform/store/key-store.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/js/common/platform/store/key-store.ts b/extension/js/common/platform/store/key-store.ts index 9d3cd540577..0c38cc86959 100644 --- a/extension/js/common/platform/store/key-store.ts +++ b/extension/js/common/platform/store/key-store.ts @@ -38,8 +38,8 @@ export class KeyStore extends AbstractStore { const keys = await KeyStore.get(acctEmail); const withPp: KeyInfoWithOptionalPp[] = []; for (const ki of keys) { - const parsed = await KeyUtil.parse(ki.private); - withPp.push({ ...ki, type: parsed.type, passphrase: await PassphraseStore.get(acctEmail, ki.fingerprints[0]) }); + const type = KeyUtil.getKeyType(ki.private); + withPp.push({ ...ki, type, passphrase: await PassphraseStore.get(acctEmail, ki.fingerprints[0]) }); } return withPp; } From 421e40593e89c985d364be6a31f11cec467bb655 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Thu, 29 Apr 2021 11:19:11 -0400 Subject: [PATCH 13/17] fix --- extension/js/common/core/crypto/pgp/msg-util.ts | 7 +++---- extension/js/common/platform/store/key-store.ts | 4 +++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/extension/js/common/core/crypto/pgp/msg-util.ts b/extension/js/common/core/crypto/pgp/msg-util.ts index d3a59ba2d34..0063b083460 100644 --- a/extension/js/common/core/crypto/pgp/msg-util.ts +++ b/extension/js/common/core/crypto/pgp/msg-util.ts @@ -318,7 +318,7 @@ export class MsgUtil { keys.prvForDecrypt = []; } for (const ki of keys.prvForDecrypt) { - const matchingKeyids = MsgUtil.matchingKeyids(ki.fingerprints, encryptionKeyids); + const matchingKeyids = MsgUtil.matchingKeyids(KeyUtil.getKeyInfoLongids(ki), encryptionKeyids); const cachedKey = KeyCache.getDecrypted(ki.longid); if (cachedKey && MsgUtil.isKeyDecryptedFor(cachedKey, matchingKeyids)) { keys.prvForDecryptDecrypted.push({ ki, decrypted: cachedKey }); @@ -338,9 +338,8 @@ export class MsgUtil { return keys; } - private static matchingKeyids = (fingerprints: string[], encryptedForKeyids: OpenPGP.Keyid[]): OpenPGP.Keyid[] => { - const allKeyLongids = fingerprints.map(fp => OpenPGPKey.fingerprintToLongid(fp)); - return encryptedForKeyids.filter(kid => allKeyLongids.includes(OpenPGPKey.bytesToLongid(kid.bytes))); + private static matchingKeyids = (longids: string[], encryptedForKeyids: OpenPGP.Keyid[]): OpenPGP.Keyid[] => { + return encryptedForKeyids.filter(kid => longids.includes(OpenPGPKey.bytesToLongid(kid.bytes))); } private static decryptKeyFor = async (prv: Key, passphrase: string, matchingKeyIds: OpenPGP.Keyid[]): Promise => { diff --git a/extension/js/common/platform/store/key-store.ts b/extension/js/common/platform/store/key-store.ts index 0c38cc86959..5f0ee6a0470 100644 --- a/extension/js/common/platform/store/key-store.ts +++ b/extension/js/common/platform/store/key-store.ts @@ -39,7 +39,9 @@ export class KeyStore extends AbstractStore { const withPp: KeyInfoWithOptionalPp[] = []; for (const ki of keys) { const type = KeyUtil.getKeyType(ki.private); - withPp.push({ ...ki, type, passphrase: await PassphraseStore.get(acctEmail, ki.fingerprints[0]) }); + if (type === 'openpgp' || type === 'x509') { + withPp.push({ ...ki, type, passphrase: await PassphraseStore.get(acctEmail, ki.fingerprints[0]) }); + } } return withPp; } From 5823f79e1fc79fbee771b4245fc6f4683bdec053 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Fri, 30 Apr 2021 10:23:26 -0400 Subject: [PATCH 14/17] Added migration to fix fingerprints and longids of X.509 keys --- .../js/background_page/background_page.ts | 3 +- extension/js/background_page/migrations.ts | 72 ++++++++++++++++++- extension/js/common/core/crypto/key.ts | 4 +- .../js/common/core/crypto/smime/smime-key.ts | 6 +- .../js/common/platform/store/contact-store.ts | 24 ++++--- .../js/common/platform/store/global-store.ts | 4 +- 6 files changed, 95 insertions(+), 18 deletions(-) diff --git a/extension/js/background_page/background_page.ts b/extension/js/background_page/background_page.ts index c86446526d7..4aeac512da8 100644 --- a/extension/js/background_page/background_page.ts +++ b/extension/js/background_page/background_page.ts @@ -9,7 +9,7 @@ import { Catch } from '../common/platform/catch.js'; import { GoogleAuth } from '../common/api/email-provider/gmail/google-auth.js'; import { VERSION } from '../common/core/const.js'; import { injectFcIntoWebmail } from './inject.js'; -import { migrateGlobal, moveContactsToEmailsAndPubkeys } from './migrations.js'; +import { migrateGlobal, moveContactsToEmailsAndPubkeys, updateX509FingerprintsAndLongids } from './migrations.js'; import { opgp } from '../common/core/crypto/pgp/openpgpjs-custom.js'; import { GlobalStoreDict, GlobalStore } from '../common/platform/store/global-store.js'; import { ContactStore } from '../common/platform/store/contact-store.js'; @@ -41,6 +41,7 @@ opgp.initWorker({ path: '/lib/openpgp.worker.js' }); try { db = await ContactStore.dbOpen(); // takes 4-10 ms first time + await updateX509FingerprintsAndLongids(db); await moveContactsToEmailsAndPubkeys(db); } catch (e) { await BgUtils.handleStoreErr(e); diff --git a/extension/js/background_page/migrations.ts b/extension/js/background_page/migrations.ts index 06f0e011fc3..a8b3a6536bc 100644 --- a/extension/js/background_page/migrations.ts +++ b/extension/js/background_page/migrations.ts @@ -3,7 +3,8 @@ 'use strict'; import { Key, KeyInfo, KeyUtil } from '../common/core/crypto/key.js'; -import { ContactStore, ContactUpdate } from '../common/platform/store/contact-store.js'; +import { SmimeKey } from '../common/core/crypto/smime/smime-key.js'; +import { ContactStore, ContactUpdate, Email, Pubkey } from '../common/platform/store/contact-store.js'; import { GlobalStore } from '../common/platform/store/global-store.js'; import { KeyStore } from '../common/platform/store/key-store.js'; @@ -19,6 +20,12 @@ type ContactV3 = { expiresOn: number | null; }; +type PubkeyMigrationData = { + emailsToUpdate: { [email: string]: Email }; + pubkeysToDelete: string[]; + pubkeysToSave: Pubkey[]; +}; + const addKeyInfoFingerprints = async () => { for (const acctEmail of await GlobalStore.acctEmailsGet()) { const originalKis = await KeyStore.get(acctEmail); @@ -40,6 +47,69 @@ export const migrateGlobal = async () => { } }; +const processSmimeKey = (pubkey: Pubkey, tx: IDBTransaction, data: PubkeyMigrationData, next: () => void) => { + if (KeyUtil.getKeyType(pubkey.armoredKey) !== 'x509') { + next(); + return; + } + const key = SmimeKey.parse(pubkey.armoredKey); + const newPubkeyEntity = ContactStore.pubkeyObj(key, pubkey.lastCheck); + data.pubkeysToDelete.push(pubkey.fingerprint); + const req = tx.objectStore('emails').index('index_fingerprints').getAll(pubkey.fingerprint!); + ContactStore.setReqPipe(req, + (emailEntities: Email[]) => { + if (emailEntities.length) { + data.pubkeysToSave.push(newPubkeyEntity); + } + for (const emailEntity of emailEntities) { + const cachedEmail = data.emailsToUpdate[emailEntity.email]; + if (!cachedEmail) { + data.emailsToUpdate[emailEntity.email] = emailEntity; + } + const entityToUpdate = cachedEmail ?? emailEntity; + entityToUpdate.fingerprints = entityToUpdate.fingerprints.filter(fp => fp !== pubkey.fingerprint && fp !== newPubkeyEntity.fingerprint); + entityToUpdate.fingerprints.push(newPubkeyEntity.fingerprint); + } + next(); + }); +}; + +export const updateX509FingerprintsAndLongids = async (db: IDBDatabase): Promise => { + const globalStore = await GlobalStore.get(['contact_store_x509_fingerprints_and_longids_updated']); + if (globalStore.contact_store_x509_fingerprints_and_longids_updated) { + return; + } + console.info('updating ContactStorage to correct longids and fingerprints of X.509 certificates...'); + const tx = db.transaction(['emails', 'pubkeys'], 'readwrite'); + await new Promise((resolve, reject) => { + ContactStore.setTxHandlers(tx, resolve, reject); + const data: PubkeyMigrationData = { emailsToUpdate: {}, pubkeysToDelete: [], pubkeysToSave: [] }; + const search = tx.objectStore('pubkeys').openCursor(); + ContactStore.setReqPipe(search, + (cursor: IDBCursorWithValue) => { + if (!cursor) { + // do updates + for (const fp of data.pubkeysToDelete.filter(fp => !data.pubkeysToSave.some(x => x.fingerprint === fp))) { + // console.log(`Deleting pubkey ${fp}`); + tx.objectStore('pubkeys').delete(fp); + } + for (const pubkey of data.pubkeysToSave) { + // console.log(`Updating pubkey ${pubkey.fingerprint}`); + tx.objectStore('pubkeys').put(pubkey); + } + for (const email of Object.values(data.emailsToUpdate)) { + // console.log(`Updating email ${email.email}`); + tx.objectStore('emails').put(email); + } + } else { + processSmimeKey(cursor.value as Pubkey, tx, data, () => cursor.continue()); + } + }); + }); + await GlobalStore.set({ contact_store_x509_fingerprints_and_longids_updated: true }); + console.info('done updating'); +}; + export const moveContactsToEmailsAndPubkeys = async (db: IDBDatabase): Promise => { if (!db.objectStoreNames.contains('contacts')) { return; diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index 6c42692c857..3fcdf8f975c 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -132,7 +132,7 @@ export class KeyUtil { return await OpenPGPKey.parseMany(text); } else if (keyType === 'x509') { // TODO: No support for parsing multiple S/MIME keys for now - return [await SmimeKey.parse(text)]; + return [SmimeKey.parse(text)]; } throw new UnexpectedKeyTypeError(`Key type is ${keyType}, expecting OpenPGP or x509 S/MIME`); } @@ -162,7 +162,7 @@ export class KeyUtil { } if (!allKeys.length) { try { - allKeys.push(await SmimeKey.parseDecryptBinary(key, passPhrase ?? '')); + allKeys.push(SmimeKey.parseDecryptBinary(key, passPhrase ?? '')); return { keys: allKeys, err: [] }; } catch (e) { allErr.push(e as Error); diff --git a/extension/js/common/core/crypto/smime/smime-key.ts b/extension/js/common/core/crypto/smime/smime-key.ts index 2cd88d7f246..bcbbaf735dc 100644 --- a/extension/js/common/core/crypto/smime/smime-key.ts +++ b/extension/js/common/core/crypto/smime/smime-key.ts @@ -8,19 +8,19 @@ import { Buf } from '../../buf.js'; export class SmimeKey { - public static parse = async (text: string): Promise => { + public static parse = (text: string): Key => { if (text.includes(PgpArmor.headers('certificate').begin)) { return SmimeKey.parsePemCertificate(text); } else if (text.includes(PgpArmor.headers('pkcs12').begin)) { const armoredBytes = text.replace(PgpArmor.headers('pkcs12').begin, '').replace(PgpArmor.headers('pkcs12').end, '').trim(); const emptyPassPhrase = ''; - return await SmimeKey.parseDecryptBinary(Buf.fromBase64Str(armoredBytes), emptyPassPhrase); + return SmimeKey.parseDecryptBinary(Buf.fromBase64Str(armoredBytes), emptyPassPhrase); } else { throw new Error('Could not parse S/MIME key without known headers'); } } - public static parseDecryptBinary = async (buffer: Uint8Array, password: string): Promise => { + public static parseDecryptBinary = (buffer: Uint8Array, password: string): Key => { const bytes = String.fromCharCode.apply(undefined, new Uint8Array(buffer) as unknown as number[]) as string; const asn1 = forge.asn1.fromDer(bytes); let certificate: forge.pki.Certificate | undefined; diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index 84227f2c1d7..0146a673008 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -17,7 +17,7 @@ type DbContactObjArg = { lastCheck?: number | null; // when was the local copy of the pubkey last updated (or checked against Attester) }; -type Email = { +export type Email = { email: string; name: string | null; searchable: string[]; @@ -25,7 +25,7 @@ type Email = { lastUse: number | null; }; -type Pubkey = { +export type Pubkey = { fingerprint: string; armoredKey: string; longids: string[]; @@ -294,6 +294,17 @@ export class ContactStore extends AbstractStore { } } + public static pubkeyObj = (pubkey: Key, lastCheck: number | null | undefined): Pubkey => { + const keyAttrs = ContactStore.getKeyAttributes(pubkey); + return { + fingerprint: ContactStore.getPubkeyId(pubkey), + lastCheck: DateUtility.asNumber(lastCheck), + expiresOn: keyAttrs.expiresOn, + longids: KeyUtil.getPubkeyLongids(pubkey), + armoredKey: KeyUtil.armor(pubkey) + }; + } + private static getPubkeyId = (pubkey: Key): string => { return (pubkey.type === 'x509') ? (pubkey.id + '-X509') : pubkey.id; } @@ -301,15 +312,8 @@ export class ContactStore extends AbstractStore { private static updateTxPhase2 = (tx: IDBTransaction, email: string, update: ContactUpdate, existingPubkey: Pubkey | undefined) => { let pubkeyEntity: Pubkey | undefined; if (update.pubkey) { - const keyAttrs = ContactStore.getKeyAttributes(update.pubkey); + pubkeyEntity = ContactStore.pubkeyObj(update.pubkey, update.pubkeyLastCheck ?? existingPubkey?.lastCheck); // todo: will we benefit anything when not saving pubkey if it isn't modified? - pubkeyEntity = { - fingerprint: ContactStore.getPubkeyId(update.pubkey), - lastCheck: DateUtility.asNumber(update.pubkeyLastCheck ?? existingPubkey?.lastCheck), - expiresOn: keyAttrs.expiresOn, - longids: KeyUtil.getPubkeyLongids(update.pubkey), - armoredKey: KeyUtil.armor(update.pubkey) - } as Pubkey; } else if (update.pubkeyLastCheck) { Catch.report(`Wrongly updating pubkeyLastCheck without specifying pubkey for ${email} - ignoring`); } diff --git a/extension/js/common/platform/store/global-store.ts b/extension/js/common/platform/store/global-store.ts index f197b22f426..8a3e447b342 100644 --- a/extension/js/common/platform/store/global-store.ts +++ b/extension/js/common/platform/store/global-store.ts @@ -18,10 +18,12 @@ export type GlobalStoreDict = { admin_codes?: Dict; install_mobile_app_notification_dismissed?: boolean; key_info_store_fingerprints_added?: boolean; + contact_store_x509_fingerprints_and_longids_updated?: boolean; }; export type GlobalIndex = 'version' | 'account_emails' | 'settings_seen' | 'hide_pass_phrases' | - 'dev_outlook_allow' | 'admin_codes' | 'install_mobile_app_notification_dismissed' | 'key_info_store_fingerprints_added'; + 'dev_outlook_allow' | 'admin_codes' | 'install_mobile_app_notification_dismissed' | 'key_info_store_fingerprints_added' | + 'contact_store_x509_fingerprints_and_longids_updated'; /** * Locally stored data that is not associated with any email account From 57869a6ff39ba273f382521b82f0dca1a967234d Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Fri, 30 Apr 2021 10:44:37 -0400 Subject: [PATCH 15/17] fixed matchingKeyids test --- test/source/tests/unit-node.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index 7a4319b8336..a8e9bfc6b8b 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -737,10 +737,10 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY expect(sortedKeys.prvForDecryptDecrypted[0].ki.longid).to.equal(OpenPGPKey.fingerprintToLongid(pub1.id)); // also test MsgUtil.matchingKeyids // @ts-ignore - const matching1 = await MsgUtil.matchingKeyids(pub1.allIds, m.getEncryptionKeyIds()); + const matching1 = MsgUtil.matchingKeyids(KeyUtil.getPubkeyLongids(pub1), m.getEncryptionKeyIds()); expect(matching1.length).to.equal(1); // @ts-ignore - const matching2 = await MsgUtil.matchingKeyids(pub2.allIds, m.getEncryptionKeyIds()); + const matching2 = MsgUtil.matchingKeyids(KeyUtil.getPubkeyLongids(pub2), m.getEncryptionKeyIds()); expect(matching2.length).to.equal(0); t.pass(); }); From 844e01a8fcc99e26e7ad294d194b16fcd3a3b5df Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sat, 8 May 2021 04:10:17 -0400 Subject: [PATCH 16/17] renamed KeyInfoWithOptionalPp to ExtendedKeyInfo --- extension/js/common/core/crypto/key.ts | 4 ++-- extension/js/common/core/crypto/pgp/msg-util.ts | 12 ++++++------ extension/js/common/platform/store/key-store.ts | 6 +++--- test/source/tests/unit-node.ts | 6 +++--- test/source/util/index.ts | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index 3fcdf8f975c..e25eaabc62b 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -67,7 +67,7 @@ export interface KeyInfo { emails?: string[]; // todo - used to be missing - but migration was supposed to add it? setting back to optional for now } -export interface KeyInfoWithOptionalPp extends KeyInfo { +export interface ExtendedKeyInfo extends KeyInfo { passphrase?: string; type: 'openpgp' | 'x509' } @@ -369,7 +369,7 @@ export class KeyUtil { return encodedIssuerAndSerialNumber; } - public static getKeyInfoLongids = (ki: KeyInfoWithOptionalPp): string[] => { + public static getKeyInfoLongids = (ki: ExtendedKeyInfo): string[] => { if (ki.type !== 'x509') { return ki.fingerprints.map(fp => OpenPGPKey.fingerprintToLongid(fp)); } diff --git a/extension/js/common/core/crypto/pgp/msg-util.ts b/extension/js/common/core/crypto/pgp/msg-util.ts index 0063b083460..a66da63d8e9 100644 --- a/extension/js/common/core/crypto/pgp/msg-util.ts +++ b/extension/js/common/core/crypto/pgp/msg-util.ts @@ -1,7 +1,7 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ 'use strict'; -import { Contact, Key, KeyInfo, KeyInfoWithOptionalPp, KeyUtil } from '../key.js'; +import { Contact, Key, KeyInfo, ExtendedKeyInfo, KeyUtil } from '../key.js'; import { MsgBlockType, ReplaceableMsgBlockType } from '../../msg-block.js'; import { Value } from '../../common.js'; import { Buf } from '../../buf.js'; @@ -26,7 +26,7 @@ export namespace PgpMsgMethod { export namespace Arg { export type Encrypt = { pubkeys: Key[], signingPrv?: Key, pwd?: string, data: Uint8Array, filename?: string, armor: boolean, date?: Date }; export type Type = { data: Uint8Array | string }; - export type Decrypt = { kisWithPp: KeyInfoWithOptionalPp[], encryptedData: Uint8Array, msgPwd?: string }; + export type Decrypt = { kisWithPp: ExtendedKeyInfo[], encryptedData: Uint8Array, msgPwd?: string }; export type DiagnosePubkeys = { armoredPubs: string[], message: Uint8Array }; export type VerifyDetached = { plaintext: Uint8Array, sigText: Uint8Array }; } @@ -53,9 +53,9 @@ type SortedKeysForDecrypt = { forVerification: OpenPGP.key.Key[]; encryptedFor: string[]; signedBy: string[]; - prvMatching: KeyInfoWithOptionalPp[]; - prvForDecrypt: KeyInfoWithOptionalPp[]; - prvForDecryptDecrypted: { ki: KeyInfoWithOptionalPp, decrypted: Key }[]; + prvMatching: ExtendedKeyInfo[]; + prvForDecrypt: ExtendedKeyInfo[]; + prvForDecryptDecrypted: { ki: ExtendedKeyInfo, decrypted: Key }[]; prvForDecryptWithoutPassphrases: KeyInfo[]; }; @@ -296,7 +296,7 @@ export class MsgUtil { } } - private static getSortedKeys = async (kiWithPp: KeyInfoWithOptionalPp[], msg: OpenpgpMsgOrCleartext): Promise => { + private static getSortedKeys = async (kiWithPp: ExtendedKeyInfo[], msg: OpenpgpMsgOrCleartext): Promise => { const keys: SortedKeysForDecrypt = { verificationContacts: [], forVerification: [], diff --git a/extension/js/common/platform/store/key-store.ts b/extension/js/common/platform/store/key-store.ts index 5f0ee6a0470..d190067b3e6 100644 --- a/extension/js/common/platform/store/key-store.ts +++ b/extension/js/common/platform/store/key-store.ts @@ -1,6 +1,6 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ -import { KeyInfo, KeyInfoWithOptionalPp, KeyUtil, Key } from '../../core/crypto/key.js'; +import { KeyInfo, ExtendedKeyInfo, KeyUtil, Key } from '../../core/crypto/key.js'; import { AcctStore } from './acct-store.js'; import { PassphraseStore } from './passphrase-store.js'; import { AbstractStore } from './abstract-store.js'; @@ -34,9 +34,9 @@ export class KeyStore extends AbstractStore { return key as KeyInfo; } - public static getAllWithOptionalPassPhrase = async (acctEmail: string): Promise => { + public static getAllWithOptionalPassPhrase = async (acctEmail: string): Promise => { const keys = await KeyStore.get(acctEmail); - const withPp: KeyInfoWithOptionalPp[] = []; + const withPp: ExtendedKeyInfo[] = []; for (const ki of keys) { const type = KeyUtil.getKeyType(ki.private); if (type === 'openpgp' || type === 'x509') { diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index f31c6b12c5b..097688f2660 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -8,7 +8,7 @@ import { PgpHash } from '../core/crypto/pgp/pgp-hash'; import { TestVariant } from '../util'; import chai = require('chai'); import chaiAsPromised = require('chai-as-promised'); -import { KeyUtil, KeyInfoWithOptionalPp } from '../core/crypto/key'; +import { KeyUtil, ExtendedKeyInfo } from '../core/crypto/key'; import { UnreportableError } from '../platform/catch.js'; import { Buf } from '../core/buf'; import { OpenPGPKey } from '../core/crypto/pgp/openpgp-key'; @@ -724,7 +724,7 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY const m = await opgp.message.readArmored(Buf.fromUint8(data).toUtfStr()); const parsed1 = await KeyUtil.parse(key1.private); const parsed2 = await KeyUtil.parse(key2.private); - const kisWithPp: KeyInfoWithOptionalPp[] = [ // supply both key1 and key2 for decrypt + const kisWithPp: ExtendedKeyInfo[] = [ // supply both key1 and key2 for decrypt { ... await KeyUtil.keyInfoObj(parsed1), type: parsed1.type, passphrase }, { ... await KeyUtil.keyInfoObj(parsed2), type: parsed2.type, passphrase }, ]; @@ -812,7 +812,7 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY const pubkeys = [await KeyUtil.parse(justPrimaryPub)]; const encrypted = await MsgUtil.encryptMessage({ pubkeys, data, armor: true }) as PgpMsgMethod.EncryptPgpArmorResult; const parsed = await KeyUtil.parse(prvEncryptForSubkeyOnlyProtected); - const kisWithPp: KeyInfoWithOptionalPp[] = [{ ... await KeyUtil.keyInfoObj(parsed), type: parsed.type, passphrase }]; + const kisWithPp: ExtendedKeyInfo[] = [{ ... await KeyUtil.keyInfoObj(parsed), type: parsed.type, passphrase }]; const decrypted = await MsgUtil.decryptMessage({ kisWithPp, encryptedData: encrypted.data }); // todo - later we'll have an org rule for ignoring this, and then it will be expected to pass as follows: // expect(decrypted.success).to.equal(true); diff --git a/test/source/util/index.ts b/test/source/util/index.ts index e2acd61795d..ab42aaf8986 100644 --- a/test/source/util/index.ts +++ b/test/source/util/index.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; -import { KeyInfoWithOptionalPp, KeyUtil } from '../core/crypto/key.js'; +import { ExtendedKeyInfo, KeyUtil } from '../core/crypto/key.js'; export type TestVariant = 'CONSUMER-MOCK' | 'ENTERPRISE-MOCK' | 'CONSUMER-LIVE-GMAIL' | 'UNIT-TESTS'; @@ -70,7 +70,7 @@ export class Config { return Config.secrets().keys.filter(k => k.title === title)[0]; } - public static getKeyInfo = async (titles: string[]): Promise => { + public static getKeyInfo = async (titles: string[]): Promise => { return await Promise.all(Config._secrets.keys .filter(key => key.armored && titles.includes(key.title)).map(async key => { const parsed = await KeyUtil.parse(key.armored!); From c9df1d95b7907b70b97b986ed45515a3380e1488 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Sat, 8 May 2021 06:33:15 -0400 Subject: [PATCH 17/17] fixed test ContactStore --- test/source/platform/store/contact-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/source/platform/store/contact-store.ts b/test/source/platform/store/contact-store.ts index 7977dd5b585..ab23bbd4033 100644 --- a/test/source/platform/store/contact-store.ts +++ b/test/source/platform/store/contact-store.ts @@ -18,7 +18,7 @@ export class ContactStore { public static get = async (db: void, emailOrLongid: string[]): Promise<(Contact | undefined)[]> => { const result = DATA.filter(x => emailOrLongid.includes(x.email) || // is there any intersection - (x.pubkey && KeyUtil.getPubkeyLongids(x.pubkey).filter(y => emailOrLongid.includes(y)))); + (x.pubkey && KeyUtil.getPubkeyLongids(x.pubkey).some(y => emailOrLongid.includes(y)))); return result; }