'use strict' ; import { Buf } from '../buf.js' ; import { Catch } from '../../platform/catch.js' ; import { MsgBlockParser } from '../msg-block-parser.js' ; import { PgpArmor } from './pgp/pgp-armor.js' ; import { opgp } from './pgp/openpgpjs-custom.js' ; import { OpenPGPKey } from './pgp/openpgp-key.js' ; import { SmimeKey } from './smime/smime-key.js' ; import { MsgBlock } from '../msg-block.js' ; /**
 * This is a common Key interface for both OpenPGP and X.509 keys.
 *
 * Since Key objects are frequently JSON serialized (e.g. via message passing)
 * all dates are expressed as number of milliseconds since Unix Epoch.
 * This is what `Date.now()` returns and `new Date(x)` accepts.
 */ export interface Key extends KeyIdentity { allIds : string [ ] ; created : number ; revoked : boolean ; lastModified : number | undefined ; expiration : number | undefined ; usableForEncryption : boolean ; usableForSigning : boolean ; usableForEncryptionButExpired : boolean ; usableForSigningButExpired : boolean ; missingPrivateKeyForSigning : boolean ; missingPrivateKeyForDecryption : boolean ; emails : string [ ] ; identities : string [ ] ; fullyDecrypted : boolean ; fullyEncrypted : boolean ; isPublic : boolean ; isPrivate : boolean ; algo : { algorithm : string ; curve ? : string ; bits ? : number ; algorithmId : number ; } ; issuerAndSerialNumber ? : string | undefined ; } export type PubkeyResult = { pubkey : Key ; email : string ; isMine : boolean ; } ; export type Contact = { email : string ; name : string | null ; pubkey : Key | undefined ; hasPgp : 0 | 1 ; fingerprint : string | null ; lastUse : number | null ; pubkeyLastCheck : number | null ; expiresOn : number | null ; revoked : boolean ; } ; export interface KeyInfo { private : string ; public : string ; longid : string ; fingerprints : string [ ] ; emails ? : string [ ] ; } export interface PubkeyInfo { pubkey : Key ; revoked : boolean ; } export interface PubkeyInfoWithLastCheck extends PubkeyInfo { lastCheck ? : number | undefined ; } export interface KeyIdentity { id : string ; type : 'openpgp' | 'x509' ; } export interface TypedKeyInfo extends KeyInfo , KeyIdentity { } export interface ExtendedKeyInfo extends TypedKeyInfo { passphrase ? : string ; } export type KeyAlgo = 'curve25519' | 'rsa2048' | 'rsa3072' | 'rsa4096' ; export type PrvPacket = ( OpenPGP . packet . SecretKey | OpenPGP . packet . SecretSubkey ) ; export class UnexpectedKeyTypeError extends Error { } export class KeyUtil { public static identityEquals = ( keyIdentity1 , keyIdentity2 ) => { return keyIdentity1 . id === keyIdentity2 . id && keyIdentity1 . type === keyIdentity2 . type ; } ; public static filterKeys < T extends KeyIdentity > ( kis , ids ) { return kis . filter ( ki => ids . some ( i => KeyUtil . identityEquals ( i , ki ) ) ) ; } public static filterKeysByTypeAndSenderEmail = ( keys , email , type ) : TypedKeyInfo [ ] => { let foundKeys = [ ] ; if ( type ) { foundKeys = keys . filter ( key => key . emails ?. includes ( email . toLowerCase ( ) ) && key . type === type ) ; if ( ! foundKeys . length ) { foundKeys = keys . filter ( key => key . type === type ) ; } } else { foundKeys = keys . filter ( key => key . emails ?. includes ( email . toLowerCase ( ) ) ) ; if ( ! foundKeys . length ) { foundKeys = [ ... keys ] ; } } return foundKeys ; } ; public static groupByType < T extends { type : string ; } > ( items ) { return items . reduce ( ( rv , x ) => { ( rv [ x . type ] = rv [ x . type ] || [ ] ) . push ( x ) ; return rv ; } , { } ) ; } public static isWithoutSelfCertifications = async ( key ) => { if ( key . type !== 'openpgp' ) { return false ; } return await OpenPGPKey . isWithoutSelfCertifications ( key ) ; } ; /**
     * Read many keys, could be armored or binary, in single armor or separately, useful for importing keychains of various formats
     */ public static readMany = async ( fileData ) : Promise < { keys : Key [ ] ; errs : Error [ ] ; } > => { const allKeys = [ ] ; const allErrs = [ ] ; const { blocks } = MsgBlockParser . detectBlocks ( fileData . toUtfStr ( 'ignore' ) ) ; const isImportable = ( block ) => block . type === 'publicKey' || block . type === 'privateKey' || block . type === 'certificate' ; const armoredPublicKeyBlocks = blocks . filter ( isImportable ) ; const pushKeysAndErrs = async ( content , isArmored ) => { try { if ( isArmored ) { allKeys . push ( ... await KeyUtil . parseMany ( content . toString ( ) ) ) ; } else { const buf = typeof content === 'string' ? Buf . fromUtfStr ( content ) : content ; const { keys , err } = await KeyUtil . readBinary ( buf ) ; allKeys . push ( ... keys ) ; allErrs . push ( ... err ) ; } } catch ( e ) { allErrs . push ( e instanceof Error ? e : new Error ( String ( e ) ) ) ; } } ; if ( armoredPublicKeyBlocks . length ) { for ( const block of blocks ) { await pushKeysAndErrs ( block . content , true ) ; } } else { await pushKeysAndErrs ( fileData , false ) ; } return { keys : allKeys , errs : allErrs } ; } ; public static parse = async ( text ) : Promise < Key > => { const keys = await KeyUtil . parseMany ( text ) ; const keysLength = keys . length ; if ( keysLength > 1 ) { throw new Error ( `Found ${ keysLength } keys, expected one` ) ; } return keys [ 0 ] ; } ; public static parseMany = async ( text ) : Promise < Key [ ] > => { const keyType = KeyUtil . getKeyType ( text ) ; if ( keyType === 'openpgp' ) { return await OpenPGPKey . parseMany ( text ) ; } else if ( keyType === 'x509' ) { return [ SmimeKey . parse ( text ) ] ; } throw new UnexpectedKeyTypeError ( `Key type is ${ keyType }, expecting OpenPGP or x509 S/MIME` ) ; } ; public static readBinary = async ( key , passPhrase ? ) : Promise < { keys : Key [ ] ; err : Error [ ] ; } > => { const allKeys = [ ] , allErr = [ ] ; try { const { keys , err } = await opgp . key . read ( key ) ; if ( keys . length > 0 ) { for ( const key of keys ) { const parsed = await OpenPGPKey . convertExternalLibraryObjToKey ( key ) ; allKeys . push ( parsed ) ; } } if ( err ) { allErr . push ( ... err ) ; } } catch ( e ) { allErr . push ( e as Error ) ; } if ( ! allKeys . length ) { try { allKeys . push ( SmimeKey . parseDecryptBinary ( key , passPhrase ?? '' ) ) ; return { keys : allKeys , err : [ ] } ; } catch ( e ) { allErr . push ( e as Error ) ; } } return { keys : allKeys , err : allErr } ; } ; public static parseBinary = async ( key , passPhrase ? ) : Promise < Key [ ] > => { const { keys , err } = await KeyUtil . readBinary ( key , passPhrase ) ; if ( keys . length > 0 ) { return keys ; } throw new Error ( err . length ? err . map ( ( e , i ) => ( i + 1 ) + '. ' + e . message ) . join ( '\n' ) : 'Should not happen: no keys and no errors.' ) ; } ; public static armor = ( pubkey ) : string => { const armored = ( pubkey as unknown as { rawArmored : string ; } ) . rawArmored ; if ( ! armored ) { throw new Error ( 'The Key object has no rawArmored field.' ) ; } return armored ; } ; public static diagnose = async ( key , passphrase ) : Promise < Map < string , string > > => { let result = new Map < string , string > ( ) ; result . set ( `Key type` , key . type ) ; if ( key . type === 'openpgp' ) { const opgpresult = await OpenPGPKey . diagnose ( key , passphrase ) ; result = new Map < string , string > ( [ ... result , ... opgpresult ] ) ; } result . set ( `expiration` , KeyUtil . formatResult ( key . expiration ) ) ; result . set ( `internal dateBeforeExpiration` , await KeyUtil . formatResultAsync ( async ( ) => KeyUtil . dateBeforeExpirationIfAlreadyExpired ( key ) ) ) ; result . set ( `internal usableForEncryptionButExpired` , KeyUtil . formatResult ( key . usableForEncryptionButExpired ) ) ; result . set ( `internal usableForSigningButExpired` , KeyUtil . formatResult ( key . usableForSigningButExpired ) ) ; return result ; } ; public static formatResultAsync = async ( f ) : Promise < string > => { try { return KeyUtil . formatResult ( await f ( ) ) ; } catch ( e ) { return `[${ String ( e ) }]` ; } } ; public static formatResult = ( value ) : string => { return `[-] ${ String ( value ) }` ; } ; public static asPublicKey = async ( key ) : Promise < Key > => { if ( key . type === 'openpgp' ) { return await OpenPGPKey . asPublicKey ( key ) ; } else if ( key . type === 'x509' ) { return SmimeKey . asPublicKey ( key ) ; } throw new UnexpectedKeyTypeError ( `Key type is ${ key . type }, expecting OpenPGP or x509 S/MIME` ) ; } ; public static expired = ( key ) : boolean => { const exp = key . expiration ; if ( ! exp ) { return false ; } return Date . now ( ) > exp ; } ; public static dateBeforeExpirationIfAlreadyExpired = ( key ) : Date | undefined => { const expiration = key . expiration ; return expiration && KeyUtil . expired ( key ) ? new Date ( expiration - 1000 ) : undefined ; } ; public static normalize = async ( armored ) : Promise < { normalized : string ; keys : OpenPGP . key . Key [ ] ; } > => { try { let keys = [ ] ; armored = PgpArmor . normalize ( armored , 'key' ) ; if ( RegExp ( PgpArmor . headers ( 'publicKey' , 're' ) . begin ) . test ( armored ) ) { keys = ( await opgp . key . readArmored ( armored ) ) . keys ; } else if ( RegExp ( PgpArmor . headers ( 'privateKey' , 're' ) . begin ) . test ( armored ) ) { keys = ( await opgp . key . readArmored ( armored ) ) . keys ; } else if ( RegExp ( PgpArmor . headers ( 'encryptedMsg' , 're' ) . begin ) . test ( armored ) ) { keys = [ new opgp . key . Key ( ( await opgp . message . readArmored ( armored ) ) . packets ) ] ; } for ( const k of keys ) { for ( const u of k . users ) { u . otherCertifications = [ ] ; } } return { normalized : keys . map ( k => k . armor ( ) ) . join ( '\n' ) , keys } ; } catch ( error ) { Catch . reportErr ( error ) ; return { normalized : '' , keys : [ ] } ; } } ; public static checkPassPhrase = async ( pkey , passphrase ) : Promise < boolean > => { const key = await KeyUtil . parse ( pkey ) ; return await KeyUtil . decrypt ( key , passphrase ) ; } ; public static getKeyType = ( pubkey ) : 'openpgp' | 'x509' | 'unknown' => { if ( pubkey . includes ( PgpArmor . headers ( 'certificate' ) . begin ) ) { return 'x509' ; } else if ( pubkey . startsWith ( PgpArmor . headers ( 'pkcs12' ) . begin ) ) { return 'x509' ; } else if ( pubkey . startsWith ( PgpArmor . headers ( 'publicKey' ) . begin ) ) { return 'openpgp' ; } else if ( pubkey . startsWith ( PgpArmor . headers ( 'privateKey' ) . begin ) ) { return 'openpgp' ; } else { return 'unknown' ; } } ; public static decrypt = async ( key , passphrase , optionalKeyid ? , optionalBehaviorFlag ? ) : Promise < boolean > => { if ( key . type === 'openpgp' ) { return await OpenPGPKey . decryptKey ( key , passphrase , optionalKeyid , optionalBehaviorFlag ) ; } else if ( key . type === 'x509' ) { return await SmimeKey . decryptKey ( key , passphrase , optionalBehaviorFlag ) ; } else { throw new Error ( `KeyUtil.decrypt does not support key type ${ key . type }` ) ; } } ; public static encrypt = async ( key , passphrase ) => { if ( key . type === 'openpgp' ) { return await OpenPGPKey . encryptKey ( key , passphrase ) ; } else if ( key . type === 'x509' ) { return await SmimeKey . encryptKey ( key , passphrase ) ; } else { throw new Error ( `KeyUtil.encrypt does not support key type ${ key . type }` ) ; } } ; public static reformatKey = async ( privateKey , passphrase , userIds , expireSeconds ) => { if ( privateKey . type === 'openpgp' ) { return await OpenPGPKey . reformatKey ( privateKey , passphrase , userIds , expireSeconds ) ; } else { throw new Error ( `KeyUtil.reformatKey does not support key type ${ privateKey . type }` ) ; } } ; public static revoke = async ( key ) : Promise < string | undefined > => { if ( key . type === 'openpgp' ) { return await OpenPGPKey . revoke ( key ) ; } else { throw new Error ( `KeyUtil.revoke does not support key type ${ key . type }` ) ; } } ; public static keyInfoObj = async ( prv ) : Promise < KeyInfo > => { 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 ( pubkey ) , longid : KeyUtil . getPrimaryLongid ( pubkey ) , emails : prv . emails , fingerprints : prv . allIds , } ; } ; public static typedKeyInfoObj = async ( prv ) : Promise < TypedKeyInfo > => { return { ... await KeyUtil . keyInfoObj ( prv ) , id : prv . id , type : prv . type } ; } ; public static getPubkeyLongids = ( pubkey ) : string [ ] => { if ( pubkey . type !== 'x509' ) { return pubkey . allIds . map ( id => OpenPGPKey . fingerprintToLongid ( id ) ) ; } return [ KeyUtil . getPrimaryLongid ( pubkey ) ] ; } ; public static getPrimaryLongid = ( pubkey ) : string => { if ( pubkey . type !== 'x509' ) { return OpenPGPKey . fingerprintToLongid ( pubkey . id ) ; } return SmimeKey . getKeyLongid ( pubkey ) ; } ; public static getKeyInfoLongids = ( ki ) : string [ ] => { if ( ki . type !== 'x509' ) { return ki . fingerprints . map ( fp => OpenPGPKey . fingerprintToLongid ( fp ) ) ; } return [ ki . longid ] ; } ; /**
     * Used for comparing public keys that were fetched vs the stored ones
     * Soon also for private keys: https://github.com/FlowCrypt/flowcrypt-browser/issues/2602
     */ public static isFetchedNewer = ( { stored , fetched } ) => { if ( ! stored . lastModified ) { return ! ! fetched . lastModified ; } if ( ! fetched . lastModified ) { return false ; } return fetched . lastModified > stored . lastModified ; } ; public static sortPubkeyInfos = ( pubkeyInfos ) : PubkeyInfo [ ] => { return pubkeyInfos . sort ( ( a , b ) => KeyUtil . getSortValue ( b ) - KeyUtil . getSortValue ( a ) ) ; } ; private static getSortValue = ( pubinfo ) : number => { const expirationSortValue = ( typeof pubinfo . pubkey . expiration === 'undefined' ) ? Infinity : pubinfo . pubkey . expiration ! ; return ( pubinfo . revoked || pubinfo . pubkey . revoked ) ? - Infinity : expirationSortValue ; } ; }