Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 53
Use public key fingerprint as S/MIME Certificate id #3570#3575
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
c9a4e8739688c05d3e08445d38a2fb4ff53757e19acbc6967cd34692a7dc997b190f6c0dc891964cade773233782e2732c09730001013a6e421e4055823f7957869a6b69e65ee3f6105844e01ac9df1d9207cebfFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
Comment on lines
+51
to
+54
Collaborator There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is interesting - could we instead filter when pulling pubkeys from storage? Eg ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't store this info yet, only in ids (fingerprints) -- postfixed with "-X509", and in | ||
| 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<void> => { | ||
| 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<void> => { | ||
| if (!db.objectStoreNames.contains('contacts')) { | ||
| return; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -391,9 +391,6 @@ export class OpenPGPKey { | ||
| } | ||
| public static fingerprintToLongid = (fingerprint: string) => { | ||
tomholub marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -8,19 +8,19 @@ import { Buf } from '../../buf.js'; | ||
| export class SmimeKey { | ||
| public static parse = async (text: string): Promise<Key> => { | ||
| 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<Key> => { | ||
| 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; | ||
| @@ -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,25 +88,38 @@ 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(); | ||
tomholub marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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: 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, | ||
| identities: emails, | ||
| 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, | ||
| issuerAndSerialNumber: forge.asn1.toDer(issuerAndSerialNumberAsn1).getBytes() | ||
| } as Key; | ||
| (key as unknown as { rawArmored: string }).rawArmored = pem; | ||
| return key; | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A small nitpick - maybe this migration
updateX509FingerprintsAndLongidsshould be done aftermoveContactsToEmailsAndPubkeysis already migrated? (switch line order)Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The contacts migration
moveContactsToEmailsAndPubkeyssets correct fingerprints and longids, we only need to fix contacts migrated by the previous version