import { Layerr } from "layerr" ; import VaultFormat from "./VaultFormat" ; import Vault from "../core/Vault" ; import Credentials from "../credentials/Credentials" ; import { executeArchiveID , executeComment , executeCreateEntry , executeCreateGroup , executeDeleteArchiveAttribute , executeDeleteEntry , executeDeleteEntryAttribute , executeDeleteEntryProperty , executeDeleteGroup , executeDeleteGroupAttribute , executeFormat , executeMoveEntry , executeMoveGroup , executePad , executeSetArchiveAttribute , executeSetEntryAttribute , executeSetEntryProperty , executeSetGroupAttribute , executeTitleGroup } from "./formatA/commands" ; import { COMMAND_MANIFEST , InigoCommand as Inigo , extractCommandComponents , getAllEntries , getAllGroups , stripDestructiveCommands } from "./formatA/tools" ; import Flattener from "./formatA/Flattener" ; import { getFormat , hasValidSignature , sign , stripSignature , vaultContentsEncrypted } from "./formatA/signing" ; import { describeVaultDataset } from "./formatA/describe" ; import VaultComparator from "./formatA/VaultComparator" ; import { getSharedAppEnv } from "../env/appEnv" ; import { decodeStringValue , isEncoded } from "../tools/encoding" ; import { generateUUID } from "../tools/uuid" ; import { getCredentials } from "../credentials/channel" ; import { historyArrayToString , historyStringToArray } from "./common" ; import { EntryChange , EntryChangeType , EntryID , FormatAEntry , FormatAGroup , FormatAVault , GroupID , History , PropertyKeyValueObject , VaultFormatID , VaultID , EntryPropertyType } from "../types" ; const COMMANDS = { aid : executeArchiveID , cen : executeCreateEntry , cgr : executeCreateGroup , cmm : executeComment , daa : executeDeleteArchiveAttribute , dea : executeDeleteEntryAttribute , dem : executeDeleteEntryProperty , den : executeDeleteEntry , dep : executeDeleteEntryProperty , dga : executeDeleteGroupAttribute , dgr : executeDeleteGroup , fmt : executeFormat , men : executeMoveEntry , mgr : executeMoveGroup , pad : executePad , saa : executeSetArchiveAttribute , sea : executeSetEntryAttribute , sem : executeSetEntryProperty , sep : executeSetEntryProperty , sga : executeSetGroupAttribute , tgr : executeTitleGroup } ; const SHARE_COMMAND_EXP = /^\$[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\s/ ; const UUID_LEN = 36 ; const VALID_COMMAND_EXP = /^[a-z]{3}\s.+$/ ; function emptyVault ( ) { return { id : null , groups : [ ] } ; } export default class VaultFormatA extends VaultFormat { static encodeRaw ( rawContent , credentials ) { const compress = getSharedAppEnv ( ) . getProperty ( "compression/v1/compressText" ) ; const encrypt = getSharedAppEnv ( ) . getProperty ( "crypto/v1/encryptText" ) ; const { masterPassword } = getCredentials ( credentials . id ) ; return Promise . resolve ( ) . then ( ( ) => historyArrayToString ( rawContent ) ) . then ( history => compress ( history ) ) . then ( compressed => encrypt ( compressed , masterPassword ) ) . then ( sign ) ; } param { String [ ] } history returns { Object } static extractSharesFromHistory ( history ) { return history . reduce ( ( output , line ) => { if ( SHARE_COMMAND_EXP . test ( line ) ) { const shareID = line . substring ( 1 , 1 + UUID_LEN ) ; const command = line . replace ( SHARE_COMMAND_EXP , "" ) ; output [ shareID ] = output [ shareID ] || [ ] ; output [ shareID ] . push ( command ) ; } else { output . base . push ( line ) ; } return output ; } , { base : [ ] } ) ; } static getFormatID ( ) { return VaultFormatID . A ; } static historiesDiffer ( historyA , historyB ) { const differences = VaultComparator . calculateHistoryDifferences ( historyA , historyB ) ; return differences === null || differences . original . length > 0 || differences . secondary . length > 0 ; } static isEncrypted ( contents ) { return vaultContentsEncrypted ( contents ) ; } static parseEncrypted ( encryptedContent , credentials ) { const decompress = getSharedAppEnv ( ) . getProperty ( "compression/v1/decompressText" ) ; const decrypt = getSharedAppEnv ( ) . getProperty ( "crypto/v1/decryptText" ) ; const { masterPassword } = getCredentials ( credentials . id ) ; return Promise . resolve ( ) . then ( ( ) => { if ( ! hasValidSignature ( encryptedContent ) ) { throw new Error ( "No valid signature in vault" ) ; } return stripSignature ( encryptedContent ) ; } ) . then ( encryptedData => decrypt ( encryptedData , masterPassword ) ) . then ( decrypted => { if ( decrypted && decrypted . length > 0 ) { const decompressed = decompress ( decrypted ) ; if ( decompressed ) { return historyStringToArray ( decompressed , VaultFormatID . A ) ; } } throw new Error ( "Failed reconstructing history: Decryption failed" ) ; } ) ; } static prepareHistoryForMerge ( history ) { return stripDestructiveCommands ( history ) ; } static vaultFromMergedHistories ( local , incoming ) { const differences = VaultComparator . calculateHistoryDifferences ( local , incoming ) ; if ( differences === null ) { throw new Error ( "Failed merging vault histories: No common base" ) ; } if ( differences . secondary . length === 0 ) { return Vault . createFromHistory ( local , VaultFormatA ) ; } const newHistoryMain = VaultFormatA . prepareHistoryForMerge ( differences . original ) ; const newHistoryStaged = VaultFormatA . prepareHistoryForMerge ( differences . secondary ) ; const base = differences . common ; const newVault = new Vault ( VaultFormatA ) ; newVault . format . erase ( ) ; newVault . format . execute ( base . concat ( newHistoryStaged ) . concat ( newHistoryMain ) ) ; newVault . format . dirty = false ; return newVault ; } _history : History ; source : FormatAVault ; constructor ( source = emptyVault ( ) ) { super ( source ) ; this . _history = [ ] ; this . _history . format = VaultFormatID . A ; } cloneEntry ( entry , targetGroupID ) { } cloneGroup ( group , targetGroupID ) { const groupDesc = describeVaultDataset ( group , targetGroupID ) ; this . execute ( groupDesc ) ; } createEntry ( groupID , entryID ) { this . execute ( Inigo . create ( Inigo . Command . CreateEntry ) . addArgument ( groupID ) . addArgument ( entryID ) . generateCommand ( ) ) ; } createGroup ( parentID , groupID ) { this . execute ( Inigo . create ( Inigo . Command . CreateGroup ) . addArgument ( parentID ) . addArgument ( groupID ) . generateCommand ( ) ) ; } deleteEntry ( entryID ) { this . execute ( Inigo . create ( Inigo . Command . DeleteEntry ) . addArgument ( entryID ) . generateCommand ( ) ) ; } deleteEntryAttribute ( entryID , attribute ) { this . execute ( Inigo . create ( Inigo . Command . DeleteEntryAttribute ) . addArgument ( entryID ) . addArgument ( attribute ) . generateCommand ( ) ) ; } deleteEntryProperty ( entryID , property ) { this . execute ( Inigo . create ( Inigo . Command . DeleteEntryProperty ) . addArgument ( entryID ) . addArgument ( property ) . generateCommand ( ) ) ; } deleteGroup ( groupID ) { this . execute ( Inigo . create ( Inigo . Command . DeleteGroup ) . addArgument ( groupID ) . generateCommand ( ) ) ; } deleteGroupAttribute ( groupID , attribute ) { this . execute ( Inigo . create ( Inigo . Command . DeleteGroupAttribute ) . addArgument ( groupID ) . addArgument ( attribute ) . generateCommand ( ) ) ; } deleteVaultAttribute ( attribute ) { this . execute ( Inigo . create ( Inigo . Command . DeleteArchiveAttribute ) . addArgument ( attribute ) . generateCommand ( ) ) ; } erase ( ) { super . erase ( ) ; Object . assign ( this . source , emptyVault ( ) ) ; this . emit ( "erased" ) ; } execute ( commandOrCommands ) { if ( this . readOnly ) { throw new Error ( "Format is in read-only mode" ) ; } const commands = Array . isArray ( commandOrCommands ) ? commandOrCommands : [ commandOrCommands ] ; commands . forEach ( command => this . _executeCommand ( command ) ) ; const lastCommand = commands [ commands . length - 1 ] ; if ( /^pad\s/i . test ( lastCommand ) === false ) { this . _pad ( ) ; } this . dirty = true ; this . emit ( "commandsExecuted" ) ; } findEntryByID ( id ) { return this . getAllEntries ( ) . find ( entry => entry . id === id ) || null ; } findGroupByID ( id ) { return this . getAllGroups ( ) . find ( group => group . id === id ) || null ; } findGroupContainingEntryID ( id ) { const matchingEntry = this . getAllEntries ( ) . find ( entry => entry . id === id ) ; if ( matchingEntry ) { return this . getAllGroups ( ) . find ( group => group . id === matchingEntry . parentID ) || null ; } return null ; } findGroupContainingGroupID ( id ) { const searchGroups = ( groups ) : FormatAGroup => { for ( const group of groups ) { if ( group . id === id ) return null ; const children = group . groups || [ ] ; const childMatch = ! ! children . find ( child => child . id === id ) ; if ( childMatch ) return group ; const deepGroup = searchGroups ( children ) ; if ( deepGroup ) return deepGroup ; } return null ; } ; return searchGroups ( this . getAllGroups ( ) ) ; } generateID ( ) { this . execute ( Inigo . create ( Inigo . Command . ArchiveID ) . addArgument ( generateUUID ( ) ) . generateCommand ( ) ) ; } getAllEntries ( parentID = null ) { return getAllEntries ( this . source , parentID ) ; } getAllGroups ( parentID = null ) { return getAllGroups ( this . source , parentID ) ; } getEntryAttributes ( entrySource ) { return entrySource . attributes ; } getEntryChanges ( entrySource ) { return ( entrySource . history || [ ] ) . filter ( item => item . propertyType === EntryPropertyType . Property ) . map ( item => { const type = ! item . originalValue ? EntryChangeType . Created : typeof item . newValue === "string" ? EntryChangeType . Modified : EntryChangeType . Deleted ; const change = { property : item . property , type , ts : null } ; if ( type === EntryChangeType . Created || EntryChangeType . Modified ) { change . value = item . newValue ; } return change ; } ) ; } getEntryProperties ( entrySource ) { return entrySource . properties ; } getFormat ( ) { return VaultFormatA ; } getGroupAttributes ( groupSource ) { return groupSource . attributes ; } getGroupTitle ( groupSource ) { return groupSource . title ; } getHistory ( ) { if ( ! this . _history . format ) { this . _history . format = VaultFormatID . A ; } return this . _history ; } getItemID ( itemSource ) { return itemSource . id ; } getItemParentID ( itemSource ) { return itemSource . parentID ; } getVaultAttributes ( ) { return ( < FormatAVault > this . source ) . attributes || { } ; } getVaultID ( ) { return this . source . id ; } initialise ( ) { this . execute ( Inigo . create ( Inigo . Command . Format ) . addArgument ( getFormat ( ) ) . generateCommand ( ) ) ; this . generateID ( ) ; } moveEntry ( entryID , groupID ) { this . execute ( Inigo . create ( Inigo . Command . MoveEntry ) . addArgument ( entryID ) . addArgument ( groupID ) . generateCommand ( ) ) ; } moveGroup ( groupID , newParentID ) { this . execute ( Inigo . create ( Inigo . Command . MoveGroup ) . addArgument ( groupID ) . addArgument ( newParentID ) . generateCommand ( ) ) ; } optimise ( ) { const flattener = new Flattener ( this ) ; if ( flattener . canBeFlattened ( ) ) { flattener . flatten ( ) ; } } setEntryAttribute ( entryID , attribute , value ) { this . execute ( Inigo . create ( Inigo . Command . SetEntryAttribute ) . addArgument ( entryID ) . addArgument ( attribute ) . addArgument ( value ) . generateCommand ( ) ) ; } setEntryProperty ( entryID , property , value ) { this . execute ( Inigo . create ( Inigo . Command . SetEntryProperty ) . addArgument ( entryID ) . addArgument ( property ) . addArgument ( value ) . generateCommand ( ) ) ; } setGroupAttribute ( groupID , attribute , value ) { this . execute ( Inigo . create ( Inigo . Command . SetGroupAttribute ) . addArgument ( groupID ) . addArgument ( attribute ) . addArgument ( value ) . generateCommand ( ) ) ; } setGroupTitle ( groupID , title ) { this . execute ( Inigo . create ( Inigo . Command . SetGroupTitle ) . addArgument ( groupID ) . addArgument ( title ) . generateCommand ( ) ) ; } setVaultAttribute ( key , value ) { this . execute ( Inigo . create ( Inigo . Command . SetArchiveAttribute ) . addArgument ( key ) . addArgument ( value ) . generateCommand ( ) ) ; } _executeCommand ( command ) { let currentCommand = command , shareID = null ; if ( SHARE_COMMAND_EXP . test ( currentCommand ) ) { const shareMatch = /^\$([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/ . exec ( currentCommand ) ; shareID = shareMatch [ 1 ] ; currentCommand = currentCommand . replace ( SHARE_COMMAND_EXP , "" ) ; } if ( ! VALID_COMMAND_EXP . test ( currentCommand ) ) { throw new Error ( `Invalid command: ${ command }` ) ; } if ( /^pad\s/i . test ( command ) && /^pad\s/i . test ( this . history [ this . history . length - 1 ] ) ) { return ; } const commandComponents = extractCommandComponents ( currentCommand ) ; const commandKey = commandComponents . shift ( ) . toLowerCase ( ) ; const executeCommand = COMMANDS [ commandKey ] ; try { executeCommand . apply ( null , [ this . source , { shareID } , ... this . _processCommandParameters ( commandKey , commandComponents ) ] ) ; this . history . push ( command ) ; } catch ( err ) { throw new Layerr ( err , `Failed executing vault command: ${ commandKey }` ) ; } } _pad ( ) { this . _executeCommand ( Inigo . generatePaddingCommand ( ) ) ; } _processCommandParameters ( commandKey , parameters ) { const friendlyCommand = Object . keys ( COMMAND_MANIFEST ) . find ( manifestKey => { return COMMAND_MANIFEST [ manifestKey ] . s === commandKey ; } ) ; const commandDescriptor = COMMAND_MANIFEST [ friendlyCommand ] ; if ( ! commandDescriptor ) { throw new Error ( `Cannot process command parameters: no command found for key: ${ commandKey }` ) ; } return parameters . map ( ( parameter , i ) => { if ( commandDescriptor . args [ i ] . encode === true ) { if ( isEncoded ( parameter ) ) { return decodeStringValue ( parameter ) ; } } return parameter ; } ) ; } }