import { CipherType } from "../enums/cipherType" ; import { FieldType } from "../enums/fieldType" ; import { UriMatchType } from "../enums/uriMatchType" ; import { CipherData } from "../models/data/cipherData" ; import { Attachment } from "../models/domain/attachment" ; import { Card } from "../models/domain/card" ; import { Cipher } from "../models/domain/cipher" ; import Domain from "../models/domain/domainBase" ; import { EncArrayBuffer } from "../models/domain/encArrayBuffer" ; import { EncString } from "../models/domain/encString" ; import { Field } from "../models/domain/field" ; import { Identity } from "../models/domain/identity" ; import { Login } from "../models/domain/login" ; import { LoginUri } from "../models/domain/loginUri" ; import { Password } from "../models/domain/password" ; import { SecureNote } from "../models/domain/secureNote" ; import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey" ; import { AttachmentRequest } from "../models/request/attachmentRequest" ; import { CipherBulkDeleteRequest } from "../models/request/cipherBulkDeleteRequest" ; import { CipherBulkMoveRequest } from "../models/request/cipherBulkMoveRequest" ; import { CipherBulkRestoreRequest } from "../models/request/cipherBulkRestoreRequest" ; import { CipherBulkShareRequest } from "../models/request/cipherBulkShareRequest" ; import { CipherCollectionsRequest } from "../models/request/cipherCollectionsRequest" ; import { CipherCreateRequest } from "../models/request/cipherCreateRequest" ; import { CipherRequest } from "../models/request/cipherRequest" ; import { CipherShareRequest } from "../models/request/cipherShareRequest" ; import { CipherResponse } from "../models/response/cipherResponse" ; import { ErrorResponse } from "../models/response/errorResponse" ; import { AttachmentView } from "../models/view/attachmentView" ; import { CipherView } from "../models/view/cipherView" ; import { FieldView } from "../models/view/fieldView" ; import { PasswordHistoryView } from "../models/view/passwordHistoryView" ; import { View } from "../models/view/view" ; import { SortedCiphersCache } from "../models/domain/sortedCiphersCache" ; import { ApiService } from "../abstractions/api.service" ; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service" ; import { CryptoService } from "../abstractions/crypto.service" ; import { FileUploadService } from "../abstractions/fileUpload.service" ; import { I18nService } from "../abstractions/i18n.service" ; import { SearchService } from "../abstractions/search.service" ; import { SettingsService } from "../abstractions/settings.service" ; import { StateService } from "../abstractions/state.service" ; import { LogService } from "../abstractions/log.service" ; import { sequentialize } from "../misc/sequentialize" ; import { Utils } from "../misc/utils" ; const DomainMatchBlacklist = new Map < string , Set < string > > ( [ [ "google.com" , new Set ( [ "script.google.com" ] ) ] , ] ) ; export class CipherService implements CipherServiceAbstraction { private sortedCiphersCache : SortedCiphersCache = new SortedCiphersCache ( this . sortCiphersByLastUsed ) ; constructor ( private cryptoService , private settingsService , private apiService , private fileUploadService , private i18nService , private searchService , private logService , private stateService ) { } async getDecryptedCipherCache ( ) { const decryptedCiphers = await this . stateService . getDecryptedCiphers ( ) ; return decryptedCiphers ; } async setDecryptedCipherCache ( value ) { await this . stateService . setDecryptedCiphers ( value ) ; if ( this . searchService != null ) { if ( value == null ) { this . searchService ( ) . clearIndex ( ) ; } else { this . searchService ( ) . indexCiphers ( ) ; } } } async clearCache ( userId ? ) { await this . clearDecryptedCiphersState ( userId ) ; } async encrypt ( model , key ? , originalCipher = null ) { if ( model . id != null ) { if ( originalCipher == null ) { originalCipher = await this . get ( model . id ) ; } if ( originalCipher != null ) { const existingCipher = await originalCipher . decrypt ( ) ; model . passwordHistory = existingCipher . passwordHistory || [ ] ; if ( model . type === CipherType . Login && existingCipher . type === CipherType . Login ) { if ( existingCipher . login . password != null && existingCipher . login . password !== "" && existingCipher . login . password !== model . login . password ) { const ph = new PasswordHistoryView ( ) ; ph . password = existingCipher . login . password ; ph . lastUsedDate = model . login . passwordRevisionDate = new Date ( ) ; model . passwordHistory . splice ( 0 , 0 , ph ) ; } else { model . login . passwordRevisionDate = existingCipher . login . passwordRevisionDate ; } } if ( existingCipher . hasFields ) { const existingHiddenFields = existingCipher . fields . filter ( ( f ) => f . type === FieldType . Hidden && f . name != null && f . name !== "" && f . value != null && f . value !== "" ) ; const hiddenFields = model . fields == null ? [ ] : model . fields . filter ( ( f ) => f . type === FieldType . Hidden && f . name != null && f . name !== "" ) ; existingHiddenFields . forEach ( ( ef ) => { const matchedField = hiddenFields . find ( ( f ) => f . name === ef . name ) ; if ( matchedField == null || matchedField . value !== ef . value ) { const ph = new PasswordHistoryView ( ) ; ph . password = ef . name + ": " + ef . value ; ph . lastUsedDate = new Date ( ) ; model . passwordHistory . splice ( 0 , 0 , ph ) ; } } ) ; } } if ( model . passwordHistory != null && model . passwordHistory . length === 0 ) { model . passwordHistory = null ; } else if ( model . passwordHistory != null && model . passwordHistory . length > 5 ) { model . passwordHistory = model . passwordHistory . slice ( 0 , 5 ) ; } } const cipher = new Cipher ( ) ; cipher . id = model . id ; cipher . folderId = model . folderId ; cipher . favorite = model . favorite ; cipher . organizationId = model . organizationId ; cipher . type = model . type ; cipher . collectionIds = model . collectionIds ; cipher . revisionDate = model . revisionDate ; cipher . reprompt = model . reprompt ; if ( key == null && cipher . organizationId != null ) { key = await this . cryptoService . getOrgKey ( cipher . organizationId ) ; if ( key == null ) { throw new Error ( "Cannot encrypt cipher for organization. No key." ) ; } } await Promise . all ( [ this . encryptObjProperty ( model , cipher , { name : null , notes : null , } , key ) , this . encryptCipherData ( cipher , model , key ) , this . encryptFields ( model . fields , key ) . then ( ( fields ) => { cipher . fields = fields ; } ) , this . encryptPasswordHistories ( model . passwordHistory , key ) . then ( ( ph ) => { cipher . passwordHistory = ph ; } ) , this . encryptAttachments ( model . attachments , key ) . then ( ( attachments ) => { cipher . attachments = attachments ; } ) , ] ) ; return cipher ; } async encryptAttachments ( attachmentsModel , key ) { if ( attachmentsModel == null || attachmentsModel . length === 0 ) { return null ; } const promises = [ ] ; const encAttachments = [ ] ; attachmentsModel . forEach ( async ( model ) => { const attachment = new Attachment ( ) ; attachment . id = model . id ; attachment . size = model . size ; attachment . sizeName = model . sizeName ; attachment . url = model . url ; const promise = this . encryptObjProperty ( model , attachment , { fileName : null , } , key ) . then ( async ( ) => { if ( model . key != null ) { attachment . key = await this . cryptoService . encrypt ( model . key . key , key ) ; } encAttachments . push ( attachment ) ; } ) ; promises . push ( promise ) ; } ) ; await Promise . all ( promises ) ; return encAttachments ; } async encryptFields ( fieldsModel , key ) { if ( ! fieldsModel || ! fieldsModel . length ) { return null ; } const self = this ; const encFields = [ ] ; await fieldsModel . reduce ( async ( promise , field ) => { await promise ; const encField = await self . encryptField ( field , key ) ; encFields . push ( encField ) ; } , Promise . resolve ( ) ) ; return encFields ; } async encryptField ( fieldModel , key ) { const field = new Field ( ) ; field . type = fieldModel . type ; field . linkedId = fieldModel . linkedId ; if ( fieldModel . type === FieldType . Boolean && fieldModel . value !== "true" ) { fieldModel . value = "false" ; } await this . encryptObjProperty ( fieldModel , field , { name : null , value : null , } , key ) ; return field ; } async encryptPasswordHistories ( phModels , key ) { if ( ! phModels || ! phModels . length ) { return null ; } const self = this ; const encPhs = [ ] ; await phModels . reduce ( async ( promise , ph ) => { await promise ; const encPh = await self . encryptPasswordHistory ( ph , key ) ; encPhs . push ( encPh ) ; } , Promise . resolve ( ) ) ; return encPhs ; } async encryptPasswordHistory ( phModel , key ) { const ph = new Password ( ) ; ph . lastUsedDate = phModel . lastUsedDate ; await this . encryptObjProperty ( phModel , ph , { password : null , } , key ) ; return ph ; } async get ( id ) { const ciphers = await this . stateService . getEncryptedCiphers ( ) ; if ( ciphers == null || ! ciphers . hasOwnProperty ( id ) ) { return null ; } const localData = await this . stateService . getLocalData ( ) ; return new Cipher ( ciphers [ id ] , false , localData ? localData [ id ] : null ) ; } async getAll ( ) { const localData = await this . stateService . getLocalData ( ) ; const ciphers = await this . stateService . getEncryptedCiphers ( ) ; const response = [ ] ; for ( const id in ciphers ) { if ( ciphers . hasOwnProperty ( id ) ) { response . push ( new Cipher ( ciphers [ id ] , false , localData ? localData [ id ] : null ) ) ; } } return response ; } @ sequentialize ( ( ) => "getAllDecrypted" ) async getAllDecrypted ( ) { const userId = await this . stateService . getUserId ( ) ; if ( ( await this . getDecryptedCipherCache ( ) ) != null ) { if ( this . searchService != null && ( this . searchService ( ) . indexedEntityId ?? userId ) !== userId ) { await this . searchService ( ) . indexCiphers ( userId , await this . getDecryptedCipherCache ( ) ) ; } return await this . getDecryptedCipherCache ( ) ; } const decCiphers = [ ] ; const hasKey = await this . cryptoService . hasKey ( ) ; if ( ! hasKey ) { throw new Error ( "No key." ) ; } const promises = [ ] ; const ciphers = await this . getAll ( ) ; ciphers . forEach ( async ( cipher ) => { promises . push ( cipher . decrypt ( ) . then ( ( c ) => decCiphers . push ( c ) ) ) ; } ) ; await Promise . all ( promises ) ; decCiphers . sort ( this . getLocaleSortingFunction ( ) ) ; await this . stateService . setDecryptedCiphers ( decCiphers ) ; return decCiphers ; } async getAllDecryptedForGrouping ( groupingId , folder = true ) { const ciphers = await this . getAllDecrypted ( ) ; return ciphers . filter ( ( cipher ) => { if ( cipher . isDeleted ) { return false ; } if ( folder && cipher . folderId === groupingId ) { return true ; } else if ( ! folder && cipher . collectionIds != null && cipher . collectionIds . indexOf ( groupingId ) > - 1 ) { return true ; } return false ; } ) ; } async getAllDecryptedForUrl ( url , includeOtherTypes ? , defaultMatch = null ) { if ( url == null && includeOtherTypes == null ) { return Promise . resolve ( [ ] ) ; } const domain = Utils . getDomain ( url ) ; const eqDomainsPromise = domain == null ? Promise . resolve ( [ ] ) : this . settingsService . getEquivalentDomains ( ) . then ( ( eqDomains ) => { let matches = [ ] ; eqDomains . forEach ( ( eqDomain ) => { if ( eqDomain . length && eqDomain . indexOf ( domain ) >= 0 ) { matches = matches . concat ( eqDomain ) ; } } ) ; if ( ! matches . length ) { matches . push ( domain ) ; } return matches ; } ) ; const result = await Promise . all ( [ eqDomainsPromise , this . getAllDecrypted ( ) ] ) ; const matchingDomains = result [ 0 ] ; const ciphers = result [ 1 ] ; if ( defaultMatch == null ) { defaultMatch = await this . stateService . getDefaultUriMatch ( ) ; if ( defaultMatch == null ) { defaultMatch = UriMatchType . Domain ; } } return ciphers . filter ( ( cipher ) => { if ( cipher . deletedDate != null ) { return false ; } if ( includeOtherTypes != null && includeOtherTypes . indexOf ( cipher . type ) > - 1 ) { return true ; } if ( url != null && cipher . type === CipherType . Login && cipher . login . uris != null ) { for ( let i = 0 ; i < cipher . login . uris . length ; i ++ ) { const u = cipher . login . uris [ i ] ; if ( u . uri == null ) { continue ; } const match = u . match == null ? defaultMatch : u . match ; switch ( match ) { case UriMatchType . Domain : if ( domain != null && u . domain != null && matchingDomains . indexOf ( u . domain ) > - 1 ) { if ( DomainMatchBlacklist . has ( u . domain ) ) { const domainUrlHost = Utils . getHost ( url ) ; if ( ! DomainMatchBlacklist . get ( u . domain ) . has ( domainUrlHost ) ) { return true ; } } else { return true ; } } break ; case UriMatchType . Host : const urlHost = Utils . getHost ( url ) ; if ( urlHost != null && urlHost === Utils . getHost ( u . uri ) ) { return true ; } break ; case UriMatchType . Exact : if ( url === u . uri ) { return true ; } break ; case UriMatchType . StartsWith : if ( url . startsWith ( u . uri ) ) { return true ; } break ; case UriMatchType . RegularExpression : try { const regex = new RegExp ( u . uri , "i" ) ; if ( regex . test ( url ) ) { return true ; } } catch ( e ) { this . logService . error ( e ) ; } break ; case UriMatchType . Never : default : break ; } } } return false ; } ) ; } async getAllFromApiForOrganization ( organizationId ) { const ciphers = await this . apiService . getCiphersOrganization ( organizationId ) ; if ( ciphers != null && ciphers . data != null && ciphers . data . length ) { const decCiphers = [ ] ; const promises = [ ] ; ciphers . data . forEach ( ( r ) => { const data = new CipherData ( r ) ; const cipher = new Cipher ( data ) ; promises . push ( cipher . decrypt ( ) . then ( ( c ) => decCiphers . push ( c ) ) ) ; } ) ; await Promise . all ( promises ) ; decCiphers . sort ( this . getLocaleSortingFunction ( ) ) ; return decCiphers ; } else { return [ ] ; } } async getLastUsedForUrl ( url , autofillOnPageLoad = false ) { return this . getCipherForUrl ( url , true , false , autofillOnPageLoad ) ; } async getLastLaunchedForUrl ( url , autofillOnPageLoad = false ) { return this . getCipherForUrl ( url , false , true , autofillOnPageLoad ) ; } async getNextCipherForUrl ( url ) { return this . getCipherForUrl ( url , false , false , false ) ; } updateLastUsedIndexForUrl ( url ) { this . sortedCiphersCache . updateLastUsedIndex ( url ) ; } async updateLastUsedDate ( id ) { let ciphersLocalData = await this . stateService . getLocalData ( ) ; if ( ! ciphersLocalData ) { ciphersLocalData = { } ; } if ( ciphersLocalData [ id ] ) { ciphersLocalData [ id ] . lastUsedDate = new Date ( ) . getTime ( ) ; } else { ciphersLocalData [ id ] = { lastUsedDate : new Date ( ) . getTime ( ) , } ; } await this . stateService . setLocalData ( ciphersLocalData ) ; const decryptedCipherCache = await this . stateService . getDecryptedCiphers ( ) ; if ( ! decryptedCipherCache ) { return ; } for ( let i = 0 ; i < decryptedCipherCache . length ; i ++ ) { const cached = decryptedCipherCache [ i ] ; if ( cached . id === id ) { cached . localData = ciphersLocalData [ id ] ; break ; } } await this . stateService . setDecryptedCiphers ( decryptedCipherCache ) ; } async updateLastLaunchedDate ( id ) { let ciphersLocalData = await this . stateService . getLocalData ( ) ; if ( ! ciphersLocalData ) { ciphersLocalData = { } ; } if ( ciphersLocalData [ id ] ) { ciphersLocalData [ id ] . lastLaunched = new Date ( ) . getTime ( ) ; } else { ciphersLocalData [ id ] = { lastUsedDate : new Date ( ) . getTime ( ) , } ; } await this . stateService . setLocalData ( ciphersLocalData ) ; const decryptedCipherCache = await this . stateService . getDecryptedCiphers ( ) ; if ( ! decryptedCipherCache ) { return ; } for ( let i = 0 ; i < decryptedCipherCache . length ; i ++ ) { const cached = decryptedCipherCache [ i ] ; if ( cached . id === id ) { cached . localData = ciphersLocalData [ id ] ; break ; } } await this . stateService . setDecryptedCiphers ( decryptedCipherCache ) ; } async saveNeverDomain ( domain ) { if ( domain == null ) { return ; } let domains = await this . stateService . getNeverDomains ( ) ; if ( ! domains ) { domains = { } ; } domains [ domain ] = null ; await this . stateService . setNeverDomains ( domains ) ; } async saveWithServer ( cipher ) { let response ; if ( cipher . id == null ) { if ( cipher . collectionIds != null ) { const request = new CipherCreateRequest ( cipher ) ; response = await this . apiService . postCipherCreate ( request ) ; } else { const request = new CipherRequest ( cipher ) ; response = await this . apiService . postCipher ( request ) ; } cipher . id = response . id ; } else { const request = new CipherRequest ( cipher ) ; response = await this . apiService . putCipher ( cipher . id , request ) ; } const data = new CipherData ( response , await this . stateService . getUserId ( ) , cipher . collectionIds ) ; await this . upsert ( data ) ; } async shareWithServer ( cipher , organizationId , collectionIds ) { const attachmentPromises = [ ] ; if ( cipher . attachments != null ) { cipher . attachments . forEach ( ( attachment ) => { if ( attachment . key == null ) { attachmentPromises . push ( this . shareAttachmentWithServer ( attachment , cipher . id , organizationId ) ) ; } } ) ; } await Promise . all ( attachmentPromises ) ; cipher . organizationId = organizationId ; cipher . collectionIds = collectionIds ; const encCipher = await this . encrypt ( cipher ) ; const request = new CipherShareRequest ( encCipher ) ; const response = await this . apiService . putShareCipher ( cipher . id , request ) ; const data = new CipherData ( response , await this . stateService . getUserId ( ) , collectionIds ) ; await this . upsert ( data ) ; } async shareManyWithServer ( ciphers , organizationId , collectionIds ) { const promises = [ ] ; const encCiphers = [ ] ; for ( const cipher of ciphers ) { cipher . organizationId = organizationId ; cipher . collectionIds = collectionIds ; promises . push ( this . encrypt ( cipher ) . then ( ( c ) => { encCiphers . push ( c ) ; } ) ) ; } await Promise . all ( promises ) ; const request = new CipherBulkShareRequest ( encCiphers , collectionIds ) ; await this . apiService . putShareCiphers ( request ) ; const userId = await this . stateService . getUserId ( ) ; await this . upsert ( encCiphers . map ( ( c ) => c . toCipherData ( userId ) ) ) ; } saveAttachmentWithServer ( cipher , unencryptedFile , admin = false ) { return new Promise ( ( resolve , reject ) => { const reader = new FileReader ( ) ; reader . readAsArrayBuffer ( unencryptedFile ) ; reader . onload = async ( evt ) => { try { const cData = await this . saveAttachmentRawWithServer ( cipher , unencryptedFile . name , evt . target . result , admin ) ; resolve ( cData ) ; } catch ( e ) { reject ( e ) ; } } ; reader . onerror = ( _evt ) => { reject ( "Error reading file." ) ; } ; } ) ; } async saveAttachmentRawWithServer ( cipher , filename , data , admin = false ) { const key = await this . cryptoService . getOrgKey ( cipher . organizationId ) ; const encFileName = await this . cryptoService . encrypt ( filename , key ) ; const dataEncKey = await this . cryptoService . makeEncKey ( key ) ; const encData = await this . cryptoService . encryptToBytes ( data , dataEncKey [ 0 ] ) ; const request = { key : dataEncKey [ 1 ] . encryptedString , fileName : encFileName . encryptedString , fileSize : encData . buffer . byteLength , adminRequest : admin , } ; let response ; try { const uploadDataResponse = await this . apiService . postCipherAttachment ( cipher . id , request ) ; response = admin ? uploadDataResponse . cipherMiniResponse : uploadDataResponse . cipherResponse ; await this . fileUploadService . uploadCipherAttachment ( admin , uploadDataResponse , encFileName , encData ) ; } catch ( e ) { if ( ( e instanceof ErrorResponse && ( e as ErrorResponse ) . statusCode === 404 ) || ( e as ErrorResponse ) . statusCode === 405 ) { response = await this . legacyServerAttachmentFileUpload ( admin , cipher . id , encFileName , encData , dataEncKey [ 1 ] ) ; } else if ( e instanceof ErrorResponse ) { throw new Error ( ( e as ErrorResponse ) . getSingleMessage ( ) ) ; } else { throw e ; } } const cData = new CipherData ( response , await this . stateService . getUserId ( ) , cipher . collectionIds ) ; if ( ! admin ) { await this . upsert ( cData ) ; } return new Cipher ( cData ) ; } deprecated async legacyServerAttachmentFileUpload ( admin , cipherId , encFileName , encData , key ) { const fd = new FormData ( ) ; try { const blob = new Blob ( [ encData . buffer ] , { type : "application/octet-stream" } ) ; fd . append ( "key" , key . encryptedString ) ; fd . append ( "data" , blob , encFileName . encryptedString ) ; } catch ( e ) { if ( Utils . isNode && ! Utils . isBrowser ) { fd . append ( "key" , key . encryptedString ) ; fd . append ( "data" , Buffer . from ( encData . buffer ) as any , { filepath : encFileName . encryptedString , contentType : "application/octet-stream" , } as any ) ; } else { throw e ; } } let response ; try { if ( admin ) { response = await this . apiService . postCipherAttachmentAdminLegacy ( cipherId , fd ) ; } else { response = await this . apiService . postCipherAttachmentLegacy ( cipherId , fd ) ; } } catch ( e ) { throw new Error ( ( e as ErrorResponse ) . getSingleMessage ( ) ) ; } return response ; } async saveCollectionsWithServer ( cipher ) { const request = new CipherCollectionsRequest ( cipher . collectionIds ) ; await this . apiService . putCipherCollections ( cipher . id , request ) ; const data = cipher . toCipherData ( await this . stateService . getUserId ( ) ) ; await this . upsert ( data ) ; } async upsert ( cipher ) { let ciphers = await this . stateService . getEncryptedCiphers ( ) ; if ( ciphers == null ) { ciphers = { } ; } if ( cipher instanceof CipherData ) { const c = cipher as CipherData ; ciphers [ c . id ] = c ; } else { ( cipher as CipherData [ ] ) . forEach ( ( c ) => { ciphers [ c . id ] = c ; } ) ; } await this . replace ( ciphers ) ; } async replace ( ciphers ) { await this . clearDecryptedCiphersState ( ) ; await this . stateService . setEncryptedCiphers ( ciphers ) ; } async clear ( userId ? ) { await this . clearEncryptedCiphersState ( userId ) ; await this . clearCache ( userId ) ; } async moveManyWithServer ( ids , folderId ) { await this . apiService . putMoveCiphers ( new CipherBulkMoveRequest ( ids , folderId ) ) ; let ciphers = await this . stateService . getEncryptedCiphers ( ) ; if ( ciphers == null ) { ciphers = { } ; } ids . forEach ( ( id ) => { if ( ciphers . hasOwnProperty ( id ) ) { ciphers [ id ] . folderId = folderId ; } } ) ; await this . clearCache ( ) ; await this . stateService . setEncryptedCiphers ( ciphers ) ; } async delete ( id ) { const ciphers = await this . stateService . getEncryptedCiphers ( ) ; if ( ciphers == null ) { return ; } if ( typeof id === "string" ) { if ( ciphers [ id ] == null ) { return ; } delete ciphers [ id ] ; } else { ( id as string [ ] ) . forEach ( ( i ) => { delete ciphers [ i ] ; } ) ; } await this . clearCache ( ) ; await this . stateService . setEncryptedCiphers ( ciphers ) ; } async deleteWithServer ( id ) { await this . apiService . deleteCipher ( id ) ; await this . delete ( id ) ; } async deleteManyWithServer ( ids ) { await this . apiService . deleteManyCiphers ( new CipherBulkDeleteRequest ( ids ) ) ; await this . delete ( ids ) ; } async deleteAttachment ( id , attachmentId ) { const ciphers = await this . stateService . getEncryptedCiphers ( ) ; if ( ciphers == null || ! ciphers . hasOwnProperty ( id ) || ciphers [ id ] . attachments == null ) { return ; } for ( let i = 0 ; i < ciphers [ id ] . attachments . length ; i ++ ) { if ( ciphers [ id ] . attachments [ i ] . id === attachmentId ) { ciphers [ id ] . attachments . splice ( i , 1 ) ; } } await this . clearCache ( ) ; await this . stateService . setEncryptedCiphers ( ciphers ) ; } async deleteAttachmentWithServer ( id , attachmentId ) { try { await this . apiService . deleteCipherAttachment ( id , attachmentId ) ; } catch ( e ) { return Promise . reject ( ( e as ErrorResponse ) . getSingleMessage ( ) ) ; } await this . deleteAttachment ( id , attachmentId ) ; } sortCiphersByLastUsed ( a , b ) { const aLastUsed = a . localData && a . localData . lastUsedDate ? ( a . localData . lastUsedDate as number ) : null ; const bLastUsed = b . localData && b . localData . lastUsedDate ? ( b . localData . lastUsedDate as number ) : null ; const bothNotNull = aLastUsed != null && bLastUsed != null ; if ( bothNotNull && aLastUsed < bLastUsed ) { return 1 ; } if ( aLastUsed != null && bLastUsed == null ) { return - 1 ; } if ( bothNotNull && aLastUsed > bLastUsed ) { return - 1 ; } if ( bLastUsed != null && aLastUsed == null ) { return 1 ; } return 0 ; } sortCiphersByLastUsedThenName ( a , b ) { const result = this . sortCiphersByLastUsed ( a , b ) ; if ( result !== 0 ) { return result ; } return this . getLocaleSortingFunction ( ) ( a , b ) ; } getLocaleSortingFunction ( ) { return ( a , b ) => { let aName = a . name ; let bName = b . name ; if ( aName == null && bName != null ) { return - 1 ; } if ( aName != null && bName == null ) { return 1 ; } if ( aName == null && bName == null ) { return 0 ; } const result = this . i18nService . collator ? this . i18nService . collator . compare ( aName , bName ) : aName . localeCompare ( bName ) ; if ( result !== 0 || a . type !== CipherType . Login || b . type !== CipherType . Login ) { return result ; } if ( a . login . username != null ) { aName += a . login . username ; } if ( b . login . username != null ) { bName += b . login . username ; } return this . i18nService . collator ? this . i18nService . collator . compare ( aName , bName ) : aName . localeCompare ( bName ) ; } ; } async softDelete ( id ) { const ciphers = await this . stateService . getEncryptedCiphers ( ) ; if ( ciphers == null ) { return ; } const setDeletedDate = ( cipherId ) => { if ( ciphers [ cipherId ] == null ) { return ; } ciphers [ cipherId ] . deletedDate = new Date ( ) . toISOString ( ) ; } ; if ( typeof id === "string" ) { setDeletedDate ( id ) ; } else { ( id as string [ ] ) . forEach ( setDeletedDate ) ; } await this . clearCache ( ) ; await this . stateService . setEncryptedCiphers ( ciphers ) ; } async softDeleteWithServer ( id ) { await this . apiService . putDeleteCipher ( id ) ; await this . softDelete ( id ) ; } async softDeleteManyWithServer ( ids ) { await this . apiService . putDeleteManyCiphers ( new CipherBulkDeleteRequest ( ids ) ) ; await this . softDelete ( ids ) ; } async restore ( cipher ) { const ciphers = await this . stateService . getEncryptedCiphers ( ) ; if ( ciphers == null ) { return ; } const clearDeletedDate = ( c ) => { if ( ciphers [ c . id ] == null ) { return ; } ciphers [ c . id ] . deletedDate = null ; ciphers [ c . id ] . revisionDate = c . revisionDate ; } ; if ( cipher . constructor . name === "Array" ) { ( cipher as { id : string ; revisionDate : string ; } [ ] ) . forEach ( clearDeletedDate ) ; } else { clearDeletedDate ( cipher as { id : string ; revisionDate : string ; } ) ; } await this . clearCache ( ) ; await this . stateService . setEncryptedCiphers ( ciphers ) ; } async restoreWithServer ( id ) { const response = await this . apiService . putRestoreCipher ( id ) ; await this . restore ( { id : id , revisionDate : response . revisionDate } ) ; } async restoreManyWithServer ( ids ) { const response = await this . apiService . putRestoreManyCiphers ( new CipherBulkRestoreRequest ( ids ) ) ; const restores = [ ] ; for ( const cipher of response . data ) { restores . push ( { id : cipher . id , revisionDate : cipher . revisionDate } ) ; } await this . restore ( restores ) ; } private async shareAttachmentWithServer ( attachmentView , cipherId , organizationId ) { const attachmentResponse = await this . apiService . nativeFetch ( new Request ( attachmentView . url , { cache : "no-store" } ) ) ; if ( attachmentResponse . status !== 200 ) { throw Error ( "Failed to download attachment: " + attachmentResponse . status . toString ( ) ) ; } const buf = await attachmentResponse . arrayBuffer ( ) ; const decBuf = await this . cryptoService . decryptFromBytes ( buf , null ) ; const key = await this . cryptoService . getOrgKey ( organizationId ) ; const encFileName = await this . cryptoService . encrypt ( attachmentView . fileName , key ) ; const dataEncKey = await this . cryptoService . makeEncKey ( key ) ; const encData = await this . cryptoService . encryptToBytes ( decBuf , dataEncKey [ 0 ] ) ; const fd = new FormData ( ) ; try { const blob = new Blob ( [ encData . buffer ] , { type : "application/octet-stream" } ) ; fd . append ( "key" , dataEncKey [ 1 ] . encryptedString ) ; fd . append ( "data" , blob , encFileName . encryptedString ) ; } catch ( e ) { if ( Utils . isNode && ! Utils . isBrowser ) { fd . append ( "key" , dataEncKey [ 1 ] . encryptedString ) ; fd . append ( "data" , Buffer . from ( encData . buffer ) as any , { filepath : encFileName . encryptedString , contentType : "application/octet-stream" , } as any ) ; } else { throw e ; } } try { await this . apiService . postShareCipherAttachment ( cipherId , attachmentView . id , fd , organizationId ) ; } catch ( e ) { throw new Error ( ( e as ErrorResponse ) . getSingleMessage ( ) ) ; } } private async encryptObjProperty < V extends View , D extends Domain > ( model , obj , map , key ) { const promises = [ ] ; const self = this ; for ( const prop in map ) { if ( ! map . hasOwnProperty ( prop ) ) { continue ; } ( function ( theProp , theObj ) { const p = Promise . resolve ( ) . then ( ( ) => { const modelProp = ( model as any ) [ map [ theProp ] || theProp ] ; if ( modelProp && modelProp !== "" ) { return self . cryptoService . encrypt ( modelProp , key ) ; } return null ; } ) . then ( ( val ) => { ( theObj as any ) [ theProp ] = val ; } ) ; promises . push ( p ) ; } ) ( prop , obj ) ; } await Promise . all ( promises ) ; } private async encryptCipherData ( cipher , model , key ) { switch ( cipher . type ) { case CipherType . Login : cipher . login = new Login ( ) ; cipher . login . passwordRevisionDate = model . login . passwordRevisionDate ; cipher . login . autofillOnPageLoad = model . login . autofillOnPageLoad ; await this . encryptObjProperty ( model . login , cipher . login , { username : null , password : null , totp : null , } , key ) ; if ( model . login . uris != null ) { cipher . login . uris = [ ] ; for ( let i = 0 ; i < model . login . uris . length ; i ++ ) { const loginUri = new LoginUri ( ) ; loginUri . match = model . login . uris [ i ] . match ; await this . encryptObjProperty ( model . login . uris [ i ] , loginUri , { uri : null , } , key ) ; cipher . login . uris . push ( loginUri ) ; } } return ; case CipherType . SecureNote : cipher . secureNote = new SecureNote ( ) ; cipher . secureNote . type = model . secureNote . type ; return ; case CipherType . Card : cipher . card = new Card ( ) ; await this . encryptObjProperty ( model . card , cipher . card , { cardholderName : null , brand : null , number : null , expMonth : null , expYear : null , code : null , } , key ) ; return ; case CipherType . Identity : cipher . identity = new Identity ( ) ; await this . encryptObjProperty ( model . identity , cipher . identity , { title : null , firstName : null , middleName : null , lastName : null , address1 : null , address2 : null , address3 : null , city : null , state : null , postalCode : null , country : null , company : null , email : null , phone : null , ssn : null , username : null , passportNumber : null , licenseNumber : null , } , key ) ; return ; default : throw new Error ( "Unknown cipher type." ) ; } } private async getCipherForUrl ( url , lastUsed , lastLaunched , autofillOnPageLoad ) { const cacheKey = autofillOnPageLoad ? "autofillOnPageLoad-" + url : url ; if ( ! this . sortedCiphersCache . isCached ( cacheKey ) ) { let ciphers = await this . getAllDecryptedForUrl ( url ) ; if ( ! ciphers ) { return null ; } if ( autofillOnPageLoad ) { const autofillOnPageLoadDefault = await this . stateService . getAutoFillOnPageLoadDefault ( ) ; ciphers = ciphers . filter ( ( cipher ) => cipher . login . autofillOnPageLoad || ( cipher . login . autofillOnPageLoad == null && autofillOnPageLoadDefault !== false ) ) ; if ( ciphers . length === 0 ) { return null ; } } this . sortedCiphersCache . addCiphers ( cacheKey , ciphers ) ; } if ( lastLaunched ) { return this . sortedCiphersCache . getLastLaunched ( cacheKey ) ; } else if ( lastUsed ) { return this . sortedCiphersCache . getLastUsed ( cacheKey ) ; } else { return this . sortedCiphersCache . getNext ( cacheKey ) ; } } private async clearEncryptedCiphersState ( userId ? ) { await this . stateService . setEncryptedCiphers ( null , { userId : userId } ) ; } private async clearDecryptedCiphersState ( userId ? ) { await this . stateService . setDecryptedCiphers ( null , { userId : userId } ) ; this . clearSortedCiphers ( ) ; } private clearSortedCiphers ( ) { this . sortedCiphersCache . clear ( ) ; } }