import { EntryStorage , BrowserStorage } from "../models/storage" ; import { Encryption } from "../models/encryption" ; import * as CryptoJS from "crypto-js" ; import { OTPType , OTPAlgorithm } from "../models/otp" ; import { ActionContext } from "vuex" ; export class Accounts implements Module { async getModule ( ) { const cachedPassphrase = await this . getCachedPassphrase ( ) ; const encryption = new Encryption ( cachedPassphrase ) ; const shouldShowPassphrase = await EntryStorage . hasEncryptionKey ( ) ; const entries = shouldShowPassphrase ? [ ] : await this . getEntries ( ) ; return { state : { entries , encryption , OTPType , OTPAlgorithm , shouldShowPassphrase , sectorStart : false , sectorOffset : 0 , second : 0 , filter : true , siteName : await this . getSiteName ( ) , showSearch : false , exportData : await EntryStorage . getExport ( entries ) , exportEncData : await EntryStorage . getExport ( entries , true ) , key : await BrowserStorage . getKey ( ) , wrongPassword : false , initComplete : false , } , getters : { shouldFilter ( state , getters ) { return ( localStorage . smartFilter !== "false" && getters . matchedEntries . length ) ; } , matchedEntries : ( state ) => { return this . matchedEntries ( state . siteName , state . entries ) ; } , currentlyEncrypted ( state ) { for ( const entry of state . entries ) { if ( entry . secret === null ) { return true ; } } return false ; } , entries ( state ) { const pinnedEntries = state . entries . filter ( ( entry ) => entry . pinned ) ; const unpinnedEntries = state . entries . filter ( ( entry ) => ! entry . pinned ) ; return [ ... pinnedEntries , ... unpinnedEntries ] ; } , } , mutations : { stopFilter ( state ) { state . filter = false ; } , showSearch ( state ) { state . showSearch = true ; } , updateCodes ( state ) { let second = new Date ( ) . getSeconds ( ) ; if ( localStorage . offset ) { second += Number ( localStorage . offset ) + 60 ; } second = second % 60 ; state . second = second ; let currentlyEncrypted = false ; for ( const entry of state . entries ) { if ( entry . secret === null ) { currentlyEncrypted = true ; } } if ( ! state . sectorStart && state . entries . length > 0 && ! currentlyEncrypted ) { state . sectorStart = true ; state . sectorOffset = - second ; } const entries = state . entries as OTPEntryInterface [ ] ; for ( let i = 0 ; i < entries . length ; i ++ ) { if ( entries [ i ] . type !== OTPType . hotp && entries [ i ] . type !== OTPType . hhex ) { entries [ i ] . generate ( ) ; } } } , loadCodes ( state , newCodes ) { state . entries = newCodes ; } , moveCode ( state , opts ) { state . entries . splice ( opts . to , 0 , state . entries . splice ( opts . from , 1 ) [ 0 ] ) ; for ( let i = 0 ; i < state . entries . length ; i ++ ) { if ( state . entries [ i ] . index !== i ) { state . entries [ i ] . index = i ; } } } , pinEntry ( state , entry ) { state . entries [ entry . index ] . pinned = ! entry . pinned ; } , updateExport ( state , exportData ) { state . exportData = exportData ; } , updateEncExport ( state , exportData ) { state . exportEncData = exportData ; } , updateKeyExport ( state , key ) { state . key = key ; } , wrongPassword ( state ) { state . wrongPassword = true ; } , initComplete ( state ) { state . initComplete = true ; } , } , actions : { deleteCode : async ( state , hash ) => { const index = state . state . entries . findIndex ( ( entry ) => entry . hash === hash ) ; if ( index > - 1 ) { state . state . entries . splice ( index , 1 ) ; } state . commit ( "updateExport" , await EntryStorage . getExport ( state . state . entries ) ) ; state . commit ( "updateEncExport" , await EntryStorage . getExport ( state . state . entries , true ) ) ; } , addCode : async ( state , entry ) => { state . state . entries . unshift ( entry ) ; state . commit ( "updateExport" , await EntryStorage . getExport ( state . state . entries ) ) ; state . commit ( "updateEncExport" , await EntryStorage . getExport ( state . state . entries , true ) ) ; } , applyPassphrase : async ( state , password ) => { if ( ! password ) { return ; } state . commit ( "currentView/changeView" , "LoadingPage" , { root : true } ) ; const encKey = await BrowserStorage . getKey ( ) ; if ( ! encKey ) { state . state . encryption . updateEncryptionPassword ( password ) ; await state . dispatch ( "updateEntries" ) ; if ( state . getters . currentlyEncrypted ) { state . commit ( "style/hideInfo" , true , { root : true } ) ; return ; } const wordArray = CryptoJS . lib . WordArray . random ( 120 ) ; const encKey = CryptoJS . AES . encrypt ( wordArray , password ) . toString ( ) ; const encKeyHash = await new Promise ( ( resolve ) => { const iframe = document . getElementById ( "argon-sandbox" ) ; const message = { action : "hash" , value : wordArray . toString ( ) , } ; if ( iframe ) { window . addEventListener ( "message" , ( response ) => { resolve ( response . data . response ) ; } ) ; iframe . contentWindow . postMessage ( message , "*" ) ; } } ) ; if ( ! encKeyHash ) { state . commit ( "style/hideInfo" , true , { root : true } ) ; return ; } const oldKeys = [ ] ; for ( const entry of state . state . entries ) { await entry . changeEncryption ( new Encryption ( wordArray . toString ( ) ) ) ; oldKeys . push ( entry . hash ) ; entry . genUUID ( ) ; } await BrowserStorage . set ( { key : { enc : encKey , hash : encKeyHash } , } ) ; await EntryStorage . set ( state . state . entries ) ; await new Promise ( ( resolve ) => { BrowserStorage . remove ( oldKeys , ( ) => { resolve ( ) ; } ) ; } ) ; state . state . encryption . updateEncryptionPassword ( wordArray . toString ( ) ) ; await state . dispatch ( "updateEntries" ) ; } else { const key = CryptoJS . AES . decrypt ( encKey . enc , password ) . toString ( ) ; const isCorrectPassword = await new Promise ( ( resolve ) => { const iframe = document . getElementById ( "argon-sandbox" ) ; const message = { action : "verify" , value : key , hash : encKey . hash , } ; if ( iframe ) { window . addEventListener ( "message" , ( response ) => { resolve ( response . data . response ) ; } ) ; iframe . contentWindow . postMessage ( message , "*" ) ; } } ) ; if ( ! isCorrectPassword ) { state . commit ( "wrongPassword" ) ; state . commit ( "currentView/changeView" , "EnterPasswordPage" , { root : true , } ) ; return ; } state . state . encryption . updateEncryptionPassword ( key ) ; await state . dispatch ( "updateEntries" ) ; let needUpdateStorage = false ; for ( const entry of state . state . entries ) { if ( ! entry . encSecret ) { await entry . changeEncryption ( state . state . encryption ) ; needUpdateStorage = true ; } } if ( needUpdateStorage ) { await EntryStorage . set ( state . state . entries ) ; await state . dispatch ( "updateEntries" ) ; } if ( ! state . getters . currentlyEncrypted ) { chrome . runtime . sendMessage ( { action : "cachePassphrase" , value : key , } ) ; } } state . commit ( "style/hideInfo" , true , { root : true } ) ; return ; } , changePassphrase : async ( state , password ) => { if ( password ) { const wordArray = CryptoJS . lib . WordArray . random ( 120 ) ; const encKey = CryptoJS . AES . encrypt ( wordArray , password ) . toString ( ) ; const encKeyHash = await new Promise ( ( resolve ) => { const iframe = document . getElementById ( "argon-sandbox" ) ; const message = { action : "hash" , value : wordArray . toString ( ) , } ; if ( iframe ) { window . addEventListener ( "message" , ( response ) => { resolve ( response . data . response ) ; } ) ; iframe . contentWindow . postMessage ( message , "*" ) ; } } ) ; if ( ! encKeyHash ) { return ; } const removeHashes = [ ] ; for ( const entry of state . state . entries ) { await entry . changeEncryption ( new Encryption ( wordArray . toString ( ) ) ) ; if ( /[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/i . test ( entry . hash ) ) { removeHashes . push ( entry . hash ) ; entry . genUUID ( ) ; } } await BrowserStorage . set ( { key : { enc : encKey , hash : encKeyHash } , } ) ; await EntryStorage . set ( state . state . entries ) ; if ( removeHashes . length ) { await new Promise ( ( resolve ) => { BrowserStorage . remove ( removeHashes , ( ) => { resolve ( ) ; } ) ; } ) ; } state . state . encryption . updateEncryptionPassword ( wordArray . toString ( ) ) ; await state . dispatch ( "updateEntries" ) ; if ( navigator . userAgent . indexOf ( "Chrome" ) !== - 1 ) { await BrowserStorage . clearLogs ( ) ; } chrome . runtime . sendMessage ( { action : "cachePassphrase" , value : wordArray . toString ( ) , } ) ; } else { for ( const entry of state . state . entries ) { await entry . changeEncryption ( new Encryption ( "" ) ) ; } await EntryStorage . set ( state . state . entries ) ; state . state . encryption . updateEncryptionPassword ( "" ) ; BrowserStorage . remove ( "key" ) ; await state . dispatch ( "updateEntries" ) ; chrome . runtime . sendMessage ( { action : "lock" , } ) ; } localStorage . removeItem ( "encodedPhrase" ) ; } , updateEntries : async ( state ) => { const entries = await this . getEntries ( ) ; if ( state . state . encryption . getEncryptionStatus ( ) ) { for ( const entry of entries ) { await entry . applyEncryption ( state . state . encryption as Encryption ) ; } } state . commit ( "loadCodes" , entries ) ; state . commit ( "updateCodes" ) ; state . commit ( "updateExport" , await EntryStorage . getExport ( state . state . entries ) ) ; state . commit ( "updateEncExport" , await EntryStorage . getExport ( state . state . entries , true ) ) ; state . commit ( "updateKeyExport" , await BrowserStorage . getKey ( ) ) ; state . commit ( "initComplete" ) ; return ; } , clearFilter : ( state ) => { state . commit ( "stopFilter" ) ; if ( state . state . entries . length >= 10 ) { state . commit ( "showSearch" ) ; } } , migrateStorage : async ( state , newStorageLocation ) => { if ( localStorage . storageLocation === "sync" && newStorageLocation === "local" ) { return new Promise ( ( resolve , reject ) => { chrome . storage . sync . get ( ( syncData ) => { chrome . storage . local . set ( syncData , ( ) => { chrome . storage . local . get ( ( localData ) => { if ( Object . keys ( syncData ) . every ( ( value ) => Object . keys ( localData ) . indexOf ( value ) >= 0 ) ) { localStorage . storageLocation = "local" ; chrome . storage . sync . clear ( ) ; resolve ( "updateSuccess" ) ; return ; } else { reject ( " All data not transferred successfully." ) ; return ; } } ) ; } ) ; } ) ; } ) ; } else if ( localStorage . storageLocation === "local" && newStorageLocation === "sync" ) { return new Promise ( ( resolve , reject ) => { chrome . storage . local . get ( ( localData ) => { chrome . storage . sync . set ( localData , ( ) => { chrome . storage . sync . get ( ( syncData ) => { if ( Object . keys ( localData ) . every ( ( value ) => Object . keys ( syncData ) . indexOf ( value ) >= 0 ) ) { localStorage . storageLocation = "sync" ; chrome . storage . local . clear ( ) ; resolve ( "updateSuccess" ) ; return ; } else { reject ( " All data not transferred successfully." ) ; return ; } } ) ; } ) ; } ) ; } ) ; } } , } , namespaced : true , } ; } private async getSiteName ( ) { return new Promise ( ( resolve ) => { chrome . tabs . query ( { active : true , lastFocusedWindow : true } , ( tabs ) => { const tab = tabs [ 0 ] ; const query = new URLSearchParams ( document . location . search . substring ( 1 ) ) ; let title ; let url ; const titleFromQuery = query . get ( "title" ) ; const urlFromQuery = query . get ( "url" ) ; if ( titleFromQuery && urlFromQuery ) { title = decodeURIComponent ( titleFromQuery ) ; url = decodeURIComponent ( urlFromQuery ) ; } else { if ( ! tab ) { return resolve ( [ null , null ] ) ; } title = tab . title ?. replace ( /[^a-z0-9]/gi , "" ) . toLowerCase ( ) ?? null ; url = tab . url ?? null ; } if ( ! url ) { return resolve ( [ title , null ] ) ; } const urlParser = new URL ( url ) ; const hostname = urlParser . hostname ; let nameFromDomain = "" ; if ( /^\d+\.\d+\.\d+\.\d+$/ . test ( hostname ) ) { nameFromDomain = hostname ; } if ( hostname . indexOf ( "." ) === - 1 ) { nameFromDomain = hostname ; } const hostLevelUnits = hostname . split ( "." ) ; if ( hostLevelUnits . length === 2 ) { nameFromDomain = hostLevelUnits [ 0 ] ; } if ( hostLevelUnits . length > 2 ) { if ( [ "com" , "net" , "org" , "edu" , "gov" , "co" ] . indexOf ( hostLevelUnits [ hostLevelUnits . length - 2 ] ) !== - 1 ) { nameFromDomain = hostLevelUnits [ hostLevelUnits . length - 3 ] ; } else { nameFromDomain = hostLevelUnits [ hostLevelUnits . length - 2 ] ; } } nameFromDomain = nameFromDomain . replace ( /-/g , "" ) . toLowerCase ( ) ; return resolve ( [ title , nameFromDomain , hostname ] ) ; } ) ; } ) ; } private getCachedPassphrase ( ) { return new Promise ( ( resolve ) => { chrome . runtime . sendMessage ( { action : "passphrase" } , ( passphrase ) => { return resolve ( passphrase ) ; } ) ; } ) ; } private async getEntries ( ) { const otpEntries = await EntryStorage . get ( ) ; return otpEntries ; } private matchedEntries ( siteName , entries ) { if ( siteName . length < 2 ) { return false ; } const matched = [ ] ; for ( const entry of entries ) { if ( this . isMatchedEntry ( siteName , entry ) ) { matched . push ( entry . hash ) ; } } return matched ; } private isMatchedEntry ( siteName , entry ) { if ( ! entry . issuer ) { return false ; } const issuerHostMatches = entry . issuer . split ( "::" ) ; const issuer = issuerHostMatches [ 0 ] . replace ( /[^0-9a-z]/gi , "" ) . toLowerCase ( ) ; if ( ! issuer ) { return false ; } const siteTitle = siteName [ 0 ] || "" ; const siteNameFromHost = siteName [ 1 ] || "" ; const siteHost = siteName [ 2 ] || "" ; if ( issuerHostMatches . length > 1 ) { if ( siteHost && siteHost . indexOf ( issuerHostMatches [ 1 ] ) !== - 1 ) { return true ; } } if ( siteTitle && siteTitle . indexOf ( issuer ) !== - 1 ) { return true ; } if ( siteNameFromHost && issuer . indexOf ( siteNameFromHost ) !== - 1 ) { return true ; } return false ; } }