import { equalCT , Serializable } from "./encoding" ; import { API , RequestEmailVerificationParams , CompleteEmailVerificationParams , RequestMFACodeParams , RetrieveMFATokenParams , RetrieveMFATokenResponse , InitAuthParams , InitAuthResponse , CreateAccountParams , RecoverAccountParams , CreateSessionParams , GetInviteParams , GetAttachmentParams , DeleteAttachmentParams , GetLegacyDataParams } from "./api" ; import { Storage , VoidStorage } from "./storage" ; import { Attachment , AttachmentStorage } from "./attachment" ; import { Session , SessionID } from "./session" ; import { Account , AccountID } from "./account" ; import { Auth } from "./auth" ; import { MFARequest , MFAPurpose } from "./mfa" ; import { Request , Response } from "./transport" ; import { Err , ErrorCode } from "./error" ; import { Vault , VaultID } from "./vault" ; import { Org , OrgID , OrgRole } from "./org" ; import { Invite } from "./invite" ; import { Messenger } from "./messenger" ; import { Server as SRPServer } from "./srp" ; import { DeviceInfo } from "./platform" ; import { uuid } from "./util" ; import { MFAMessage , InviteCreatedMessage , InviteAcceptedMessage , MemberAddedMessage } from "./messages" ; import { BillingProvider , UpdateBillingParams , BillingAddress } from "./billing" ; import { AccountQuota , OrgQuota } from "./quota" ; import { loadLanguage } from "@padloc/locale/src/translate" ; import { Logger } from "./log" ; import { PBES2Container } from "./container" ; const pendingAuths = new Map < string , SRPServer > ( ) ; /** Server configuration */ export class ServerConfig { /** URL where the client interface is hosted. Used for creating links into the application */ clientUrl = "" ; /** Email address to report critical errors to */ reportErrors = "" ; /** Multi-factor authentication mode used for login */ mfa : "email" | "none" = "email" ; /** Maximum accepted request age */ maxRequestAge = 60 * 60 * 1000 ; /** Default quota applied to new accounts */ accountQuota ? : Partial < AccountQuota > ; /** Default quota applied to new Orgs */ orgQuota ? : Partial < OrgQuota > ; /** Whether or not to require email verification before createing an account */ verifyEmailOnSignup = true ; constructor ( vals ? ) { if ( vals ) { Object . assign ( this , vals ) ; } } } /**
 * Request context
 */ export interface Context { /** Current [[Session]] */ session ? : Session ; /** [[Account]] associated with current session */ account ? : Account ; /** Information about the device the request is coming from */ device ? : DeviceInfo ; } export interface LegacyServer { getStore ( email ) : Promise < PBES2Container | null > ; deleteAccount ( email ) : Promise < void > ; } /**
 * Controller class for processing api requests
 */ export class Controller extends API { constructor ( public server , public context ) { super ( ) ; } get config ( ) { return this . server . config ; } get storage ( ) { return this . server . storage ; } get messenger ( ) { return this . server . messenger ; } get logger ( ) { return this . server . logger ; } get attachmentStorage ( ) { return this . server . attachmentStorage ; } get legacyServer ( ) { return this . server . legacyServer ; } get billingProvider ( ) { return this . server . billingProvider ; } async process ( req ) { const def = this . handlerDefinitions . find ( def => def . method === req . method ) ; if ( ! def ) { throw new Err ( ErrorCode . INVALID_REQUEST ) ; } const clientVersion = ( req . device && req . device . appVersion ) || undefined ; const param = req . params && req . params [ 0 ] ; const input = def . input && param ? new def . input ( ) . fromRaw ( param ) : param ; const result = await this [ def . method ] ( input ) ; const toRaw = ( obj ) => ( obj instanceof Serializable ? obj . toRaw ( clientVersion ) : obj ) ; return Array . isArray ( result ) ? result . map ( toRaw ) : toRaw ( result ) ; } async log ( type , data = { } ) { const acc = this . context . account ; this . logger . log ( type , { account : acc && { email : acc . email , id : acc . id , name : acc . name } , device : this . context . device && this . context . device . toRaw ( ) , ... data } ) ; } async requestEmailVerification ( { email , purpose } ) { const v = new MFARequest ( email , MFAPurpose . Login ) ; await v . init ( ) ; await this . storage . save ( v ) ; this . messenger . send ( email , new MFAMessage ( v ) ) ; this . log ( "verifyemail.request" , { email , purpose } ) ; } async completeEmailVerification ( { email , code } ) { try { const { token } = await this . _checkMFACode ( email , code , MFAPurpose . Login ) ; this . log ( "verifyemail.complete" , { email , success : true } ) ; return token ; } catch ( e ) { this . log ( "verifyemail.complete" , { email , success : false } ) ; throw e ; } } async requestMFACode ( { email , purpose , type } ) { const v = new MFARequest ( email , purpose , type ) ; await v . init ( ) ; await this . storage . save ( v ) ; this . messenger . send ( email , new MFAMessage ( v ) ) ; this . log ( "mfa.requestCode" , { email , purpose , type } ) ; } async retrieveMFAToken ( { email , code , purpose } ) { try { const mfa = await this . _checkMFACode ( email , code , purpose ) ; let hasAccount = false ; try { await this . storage . get ( Auth , email ) ; hasAccount = true ; } catch ( e ) { } const hasLegacyAccount = ! ! this . legacyServer && ! ! ( await this . legacyServer . getStore ( email ) ) ; if ( ! hasAccount && hasLegacyAccount ) { await this . storage . delete ( mfa ) ; mfa . purpose = MFAPurpose . Signup ; await this . storage . save ( mfa ) ; } const response = new RetrieveMFATokenResponse ( { token : mfa . token , hasAccount , hasLegacyAccount } ) ; if ( hasLegacyAccount ) { const v = new MFARequest ( email , MFAPurpose . GetLegacyData ) ; await v . init ( ) ; await this . storage . save ( v ) ; response . legacyToken = v . token ; } this . log ( "mfa.retrieveToken" , { email , success : true , hasAccount , hasLegacyAccount } ) ; return response ; } catch ( e ) { this . log ( "mfa.retrieveToken" , { email , success : false } ) ; throw e ; } } async initAuth ( { email , verify } ) { let auth = null ; try { auth = await this . storage . get ( Auth , email ) ; } catch ( e ) { if ( e . code !== ErrorCode . NOT_FOUND ) { throw e ; } } this . logger . log ( "auth.init" , { email , account : auth && { email , id : auth . account } } ) ; const deviceTrusted = auth && this . context . device && auth . trustedDevices . some ( ( { id } ) => id === this . context . device ! . id ) ; if ( this . config . mfa !== "none" && ! deviceTrusted ) { if ( ! verify ) { throw new Err ( ErrorCode . MFA_REQUIRED ) ; } else { await this . _checkMFAToken ( email , verify , MFAPurpose . Login ) ; } } if ( ! auth ) { throw new Err ( ErrorCode . NOT_FOUND , "An account with this email does not exist!" ) ; } const srp = new SRPServer ( ) ; await srp . initialize ( auth . verifier ! ) ; pendingAuths . set ( auth . account , srp ) ; return new InitAuthResponse ( { account : auth . account , keyParams : auth . keyParams , B : srp . B ! } ) ; } async updateAuth ( auth ) { const { account } = this . _requireAuth ( ) ; if ( account . email !== auth . email ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS ) ; } await this . storage . save ( auth ) ; this . log ( "auth.update" ) ; } async createSession ( { account , A , M } ) { const srp = pendingAuths . get ( account ) ; if ( ! srp ) { this . log ( "login" , { account : { id : account } , success : false } ) ; throw new Err ( ErrorCode . INVALID_CREDENTIALS ) ; } await srp . setA ( A ) ; if ( ! equalCT ( M , srp . M1 ! ) ) { this . log ( "login" , { account : { id : account } , success : false } ) ; throw new Err ( ErrorCode . INVALID_CREDENTIALS ) ; } const acc = await this . storage . get ( Account , account ) ; const session = new Session ( ) ; session . id = await uuid ( ) ; session . created = new Date ( ) ; session . account = account ; session . device = this . context . device ; session . key = srp . K ! ; acc . sessions . push ( session . info ) ; await Promise . all ( [ this . storage . save ( session ) , this . storage . save ( acc ) ] ) ; pendingAuths . delete ( account ) ; const auth = await this . storage . get ( Auth , acc . email ) ; if ( this . context . device && ! auth . trustedDevices . some ( ( { id } ) => equalCT ( id , this . context . device ! . id ) ) ) { auth . trustedDevices . push ( this . context . device ) ; } await this . storage . save ( auth ) ; delete session . key ; this . log ( "login" , { account : { email : acc . email , id : acc . id } , success : true } ) ; return session ; } async revokeSession ( id ) { const { account } = this . _requireAuth ( ) ; const session = await this . storage . get ( Session , id ) ; if ( session . account !== account . id ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS ) ; } const i = account . sessions . findIndex ( s => s . id === id ) ; account . sessions . splice ( i , 1 ) ; await Promise . all ( [ this . storage . delete ( session ) , this . storage . save ( account ) ] ) ; this . log ( "logout" ) ; } async createAccount ( { account , auth , verify } ) { if ( this . config . verifyEmailOnSignup ) { await this . _checkMFAToken ( account . email , verify , MFAPurpose . Signup ) ; } try { await this . storage . get ( Auth , auth . id ) ; throw new Err ( ErrorCode . ACCOUNT_EXISTS , "This account already exists!" ) ; } catch ( e ) { if ( e . code !== ErrorCode . NOT_FOUND ) { throw e ; } } account . id = await uuid ( ) ; account . revision = await uuid ( ) ; auth . account = account . id ; if ( this . context . device && ! auth . trustedDevices . some ( ( { id } ) => id === this . context . device ! . id ) ) { auth . trustedDevices . push ( this . context . device ) ; } const vault = new Vault ( ) ; vault . id = await uuid ( ) ; vault . name = "My Vault" ; vault . owner = account . id ; vault . created = new Date ( ) ; vault . updated = new Date ( ) ; account . mainVault = { id : vault . id } ; if ( this . config . accountQuota ) { Object . assign ( account . quota , this . config . accountQuota ) ; } await Promise . all ( [ this . storage . save ( account ) , this . storage . save ( vault ) , this . storage . save ( auth ) ] ) ; if ( this . billingProvider ) { await this . billingProvider . update ( new UpdateBillingParams ( { email : account . email , account : account . id , address : new BillingAddress ( { name : account . name } ) } ) ) ; } account = await this . storage . get ( Account , account . id ) ; this . log ( "account.create" , { account : { email : account . email , id : account . id , name : account . name } } ) ; return account ; } async getAccount ( ) { const { account } = this . _requireAuth ( ) ; this . log ( "account.get" ) ; return account ; } async updateAccount ( { name , email , publicKey , keyParams , encryptionParams , encryptedData , revision } ) { const { account } = this . _requireAuth ( ) ; if ( revision !== account . revision ) { throw new Err ( ErrorCode . OUTDATED_REVISION ) ; } account . revision = await uuid ( ) ; const nameChanged = account . name !== name ; Object . assign ( account , { name , email , publicKey , keyParams , encryptionParams , encryptedData } ) ; account . updated = new Date ( ) ; await this . storage . save ( account ) ; if ( nameChanged ) { for ( const { id } of account . orgs ) { const org = await this . storage . get ( Org , id ) ; org . getMember ( account ) ! . name = name ; await this . updateMetaData ( org ) ; await this . storage . save ( org ) ; } } this . log ( "account.update" ) ; return account ; } async recoverAccount ( { account : { email , publicKey , keyParams , encryptionParams , encryptedData } , auth , verify } ) { await this . _checkMFAToken ( auth . email , verify , MFAPurpose . Recover ) ; const existingAuth = await this . storage . get ( Auth , auth . email ) ; const account = await this . storage . get ( Account , existingAuth . account ) ; Object . assign ( account , { email , publicKey , keyParams , encryptionParams , encryptedData } ) ; const mainVault = new Vault ( ) ; mainVault . id = account . mainVault . id ; mainVault . name = "My Vault" ; mainVault . owner = account . id ; mainVault . created = new Date ( ) ; mainVault . updated = new Date ( ) ; auth . account = account . id ; this . context . device && auth . trustedDevices . push ( this . context . device ) ; await account . sessions . map ( s => this . storage . delete ( Object . assign ( new Session ( ) , s ) ) ) ; for ( const { id } of account . orgs ) { const org = await this . storage . get ( Org , id ) ; if ( ! org . isOwner ( account ) ) { const member = org . getMember ( account ) ! ; member . role = OrgRole . Suspended ; await this . storage . save ( org ) ; } } await Promise . all ( [ this . storage . save ( account ) , this . storage . save ( auth ) , this . storage . save ( mainVault ) ] ) ; this . log ( "account.recover" , { account : { email : account . email , id : account . id , name : account . name } } ) ; return account ; } async deleteAccount ( ) { const { account } = this . _requireAuth ( ) ; const orgs = await Promise . all ( account . orgs . map ( ( { id } ) => this . storage . get ( Org , id ) ) ) ; if ( orgs . some ( org => org . isOwner ( account ) ) ) { throw new Err ( ErrorCode . BAD_REQUEST , "This account is the owner of one or more organizations and cannot " + "be deleted. Please delete all your owned organizations first!" ) ; } for ( const org of orgs ) { org . removeMember ( account ) ; await this . storage . save ( org ) ; } if ( account . billing && this . billingProvider ) { await this . billingProvider . delete ( account . billing ) ; } await this . storage . delete ( Object . assign ( new Vault ( ) , { id : account . mainVault } ) ) ; await account . sessions . map ( s => this . storage . delete ( Object . assign ( new Session ( ) , s ) ) ) ; await this . storage . delete ( new Auth ( account . email ) ) ; await this . storage . delete ( account ) ; this . log ( "account.delete" ) ; } async createOrg ( org ) { const { account } = this . _requireAuth ( ) ; if ( ! org . name ) { throw new Err ( ErrorCode . BAD_REQUEST , "Please provide an organization name!" ) ; } const existingOrgs = await Promise . all ( account . orgs . map ( ( { id } ) => this . storage . get ( Org , id ) ) ) ; const ownedOrgs = existingOrgs . filter ( o => o . owner === account . id ) ; if ( account . quota . orgs !== - 1 && ownedOrgs . length >= account . quota . orgs ) { throw new Err ( ErrorCode . ORG_QUOTA_EXCEEDED , "You have reached the maximum number of organizations for this account!" ) ; } org . id = await uuid ( ) ; org . revision = await uuid ( ) ; org . owner = account . id ; org . created = new Date ( ) ; org . updated = new Date ( ) ; if ( this . config . orgQuota ) { Object . assign ( org . quota , this . config . orgQuota ) ; } await this . storage . save ( org ) ; this . log ( "org.create" , { org : { name : org . name , id : org . id , type : org . type } } ) ; return org ; } async getOrg ( id ) { const { account } = this . _requireAuth ( ) ; const org = await this . storage . get ( Org , id ) ; if ( org . owner !== account . id && ! org . isMember ( account ) ) { throw new Err ( ErrorCode . NOT_FOUND ) ; } this . log ( "org.get" , { org : { name : org . name , id : org . id , type : org . type } } ) ; return org ; } async updateOrg ( { id , name , publicKey , keyParams , encryptionParams , encryptedData , signingParams , accessors , members , groups , vaults , invites , revision , minMemberUpdated } ) { const { account } = this . _requireAuth ( ) ; const org = await this . storage . get ( Org , id ) ; if ( org . frozen ) { throw new Err ( ErrorCode . ORG_FROZEN , 'You can not make any updates to an organization while it is in "frozen" state!' ) ; } if ( revision !== org . revision ) { throw new Err ( ErrorCode . OUTDATED_REVISION ) ; } const isOwner = org . owner === account . id || org . isOwner ( account ) ; const isAdmin = isOwner || org . isAdmin ( account ) ; if ( ! isAdmin ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS , "Only admins can make changes to organizations!" ) ; } if ( minMemberUpdated < org . minMemberUpdated ) { throw new Err ( ErrorCode . BAD_REQUEST , "`minMemberUpdated` property needs to be equal to or larger than the previous one!" ) ; } const addedMembers = members . filter ( m => ! org . isMember ( m ) ) ; const removedMembers = org . members . filter ( ( { id } ) => ! members . some ( m => id === m . id ) ) ; const addedInvites = invites . filter ( ( { id } ) => ! org . getInvite ( id ) ) ; if ( ! isOwner && ( addedMembers . length || removedMembers . length || addedInvites . length || members . some ( ( { id , role } ) => { const member = org . getMember ( { id } ) ; return ! member || member . role !== role ; } ) ) ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS , "Only organization owners can add or remove members, change roles or create invites!" ) ; } if ( org . quota . members !== - 1 && members . length > org . quota . members ) { throw new Err ( ErrorCode . MEMBER_QUOTA_EXCEEDED , "You have reached the maximum number of members for this organization!" ) ; } if ( org . quota . groups !== - 1 && groups . length > org . quota . groups ) { throw new Err ( ErrorCode . GROUP_QUOTA_EXCEEDED , "You have reached the maximum number of groups for this organization!" ) ; } Object . assign ( org , { members , groups , vaults } ) ; if ( isOwner ) { Object . assign ( org , { name , publicKey , keyParams , encryptionParams , encryptedData , signingParams , accessors , invites , minMemberUpdated } ) ; } const promises = [ ] ; for ( const invite of addedInvites ) { promises . push ( ( async ( ) => { let link = `${ this . config . clientUrl }/invite/${ org . id }/${ invite . id }?email=${ invite . email }` ; try { await this . storage . get ( Auth , invite . email ) ; } catch ( e ) { if ( e . code !== ErrorCode . NOT_FOUND ) { throw e ; } const v = new MFARequest ( invite . email , MFAPurpose . Signup ) ; await v . init ( ) ; await this . storage . save ( v ) ; link += `&verify=${ v . token }` ; } this . messenger . send ( invite . email , new InviteCreatedMessage ( invite , link ) ) ; } ) ( ) ) ; } for ( const { id } of removedMembers ) { promises . push ( ( async ( ) => { const acc = await this . storage . get ( Account , id ) ; acc . orgs = acc . orgs . filter ( o => o . id !== org . id ) ; await this . storage . save ( acc ) ; } ) ( ) ) ; } await this . updateMetaData ( org ) ; for ( const member of addedMembers ) { if ( member . id !== account . id ) { this . messenger . send ( member . email , new MemberAddedMessage ( org , `${ this . config . clientUrl }/org/${ org . id }` ) ) ; } } await Promise . all ( promises ) ; await this . storage . save ( org ) ; this . log ( "org.update" , { org : { name : org . name , id : org . id , type : org . type } } ) ; return org ; } async deleteOrg ( id ) { const { account } = this . _requireAuth ( ) ; const org = await this . storage . get ( Org , id ) ; if ( ! org . isOwner ( account ) ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS ) ; } await Promise . all ( org . vaults . map ( v => this . storage . delete ( Object . assign ( new Vault ( ) , v ) ) ) ) ; await Promise . all ( org . members . map ( async ( member ) => { const acc = await this . storage . get ( Account , member . id ) ; acc . orgs = acc . orgs . filter ( ( { id } ) => id !== org . id ) ; await this . storage . save ( acc ) ; } ) ) ; if ( this . billingProvider && org . billing ) { await this . billingProvider . delete ( org . billing ) ; } await this . storage . delete ( org ) ; this . log ( "org.delete" , { org : { name : org . name , id : org . id , type : org . type } } ) ; } async getVault ( id ) { const { account } = this . _requireAuth ( ) ; const vault = await this . storage . get ( Vault , id ) ; const org = vault . org && ( await this . storage . get ( Org , vault . org . id ) ) ; if ( org && org . isSuspended ( account ) ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS , "This vault cannot be synchronized because you're suspended from it's organization." ) ; } if ( ( org && ! org . canRead ( vault , account ) ) || ( ! org && vault . owner !== account . id ) ) { throw new Err ( ErrorCode . NOT_FOUND ) ; } this . log ( "vault.get" , { vault : { id : vault . id , name : vault . name } , org : org && { id : org . id , name : org . name , type : org . type } } ) ; return vault ; } async updateVault ( { id , keyParams , encryptionParams , accessors , encryptedData , revision } ) { const { account } = this . _requireAuth ( ) ; const vault = await this . storage . get ( Vault , id ) ; const org = vault . org && ( await this . storage . get ( Org , vault . org . id ) ) ; if ( org && org . isSuspended ( account ) ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS , "This vault cannot be synchronized because you're suspended from it's organization." ) ; } if ( ( org && ! org . canRead ( vault , account ) ) || ( ! org && vault . owner !== account . id ) ) { throw new Err ( ErrorCode . NOT_FOUND ) ; } if ( org && ! org . canWrite ( vault , account ) ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS ) ; } if ( org && org . frozen ) { throw new Err ( ErrorCode . ORG_FROZEN , 'You can not make any updates to a vault while it\'s organization is in "frozen" state!' ) ; } if ( revision !== vault . revision ) { throw new Err ( ErrorCode . OUTDATED_REVISION ) ; } Object . assign ( vault , { keyParams , encryptionParams , accessors , encryptedData } ) ; vault . revision = await uuid ( ) ; vault . updated = new Date ( ) ; await this . storage . save ( vault ) ; if ( org ) { await this . updateMetaData ( org ) ; await this . storage . save ( org ) ; } else { account . mainVault . revision = vault . revision ; await this . storage . save ( account ) ; } this . log ( "vault.update" , { vault : { id : vault . id , name : vault . name } , org : org && { id : org . id , name : org . name , type : org . type } } ) ; return this . storage . get ( Vault , vault . id ) ; } async createVault ( vault ) { const { account } = this . _requireAuth ( ) ; if ( ! vault . org ) { throw new Err ( ErrorCode . BAD_REQUEST , "Shared vaults have to be attached to an organization." ) ; } const org = await this . storage . get ( Org , vault . org . id ) ; if ( ! org . isAdmin ( account ) ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS ) ; } vault . id = await uuid ( ) ; vault . owner = account . id ; vault . created = vault . updated = new Date ( ) ; vault . revision = await uuid ( ) ; org . vaults . push ( { id : vault . id , name : vault . name } ) ; org . revision = await uuid ( ) ; if ( org . quota . vaults !== - 1 && org . vaults . length > org . quota . vaults ) { throw new Err ( ErrorCode . VAULT_QUOTA_EXCEEDED , "You have reached the maximum number of vaults for this organization!" ) ; } await Promise . all ( [ this . storage . save ( vault ) , this . storage . save ( org ) ] ) ; this . log ( "vault.create" , { vault : { id : vault . id , name : vault . name } , org : org && { id : org . id , name : org . name , type : org . type } } ) ; return vault ; } async deleteVault ( id ) { const { account } = this . _requireAuth ( ) ; const vault = await this . storage . get ( Vault , id ) ; if ( ! vault . org ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS ) ; } const org = await this . storage . get ( Org , vault . org . id ) ; if ( ! org . isAdmin ( account ) ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS ) ; } await this . attachmentStorage . deleteAll ( vault . id ) ; org . vaults = org . vaults . filter ( v => v . id !== vault . id ) ; for ( const each of [ ... org . getGroupsForVault ( vault ) , ... org . getMembersForVault ( vault ) ] ) { each . vaults = each . vaults . filter ( v => v . id !== vault . id ) ; } await this . updateMetaData ( org ) ; await this . storage . save ( org ) ; await this . storage . delete ( vault ) ; this . log ( "vault.delete" , { vault : { id : vault . id , name : vault . name } , org : org && { id : org . id , name : org . name , type : org . type } } ) ; } async getInvite ( { org : orgId , id } ) { const { account } = this . _requireAuth ( ) ; const org = await this . storage . get ( Org , orgId ) ; const invite = org . getInvite ( id ) ; if ( ! invite || ( ! org . isOwner ( account ) && invite . email !== account . email ) ) { throw new Err ( ErrorCode . NOT_FOUND ) ; } this . log ( "invite.get" , { invite : { id : invite . id , email : invite . email } , org : { id : org . id , name : org . name , type : org . type } } ) ; return invite ; } async acceptInvite ( invite ) { if ( ! invite . accepted ) { throw new Err ( ErrorCode . BAD_REQUEST ) ; } const { account } = this . _requireAuth ( ) ; const org = await this . storage . get ( Org , invite . org . id ) ; const existing = org . getInvite ( invite . id ) ; if ( ! existing ) { throw new Err ( ErrorCode . NOT_FOUND ) ; } if ( existing . email !== account . email ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS , "Only the invite recipient can accept the invite." ) ; } if ( ! existing . accepted && invite . invitedBy ) { this . messenger . send ( invite . invitedBy . email , new InviteAcceptedMessage ( invite , `${ this . config . clientUrl }/invite/${ org . id }/${ invite . id }` ) ) ; } org . invites [ org . invites . indexOf ( existing ) ] = invite ; await this . updateMetaData ( org ) ; await this . storage . save ( org ) ; this . log ( "invite.accept" , { invite : { id : invite . id , email : invite . email } , org : { id : org . id , name : org . name , type : org . type } } ) ; } async createAttachment ( att ) { const { account } = this . _requireAuth ( ) ; const vault = await this . storage . get ( Vault , att . vault ) ; const org = vault . org && ( await this . storage . get ( Org , vault . org . id ) ) ; const allowed = org ? org . canWrite ( vault , account ) : vault . owner === account . id ; if ( ! allowed ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS ) ; } att . id = await uuid ( ) ; const currentUsage = org ? org . usedStorage : account . usedStorage ; const quota = org ? org . quota : account . quota ; if ( quota . storage !== - 1 && currentUsage + att . size > quota . storage * 1e9 ) { throw new Err ( ErrorCode . STORAGE_QUOTA_EXCEEDED , org ? "You have reached the storage limit for this organization!" : "You have reached the storage limit for this account!" ) ; } await this . attachmentStorage . put ( att ) ; await this . _updateUsedStorage ( org || account ) ; this . log ( "attachment.create" , { attachment : { type : att . type , size : att . size , id : att . id } , vault : { id : vault . id , name : vault . name } , org : org && { id : org ! . id , name : org ! . name , type : org ! . type } } ) ; return att . id ; } async getAttachment ( { id , vault : vaultId } ) { const { account } = this . _requireAuth ( ) ; const vault = await this . storage . get ( Vault , vaultId ) ; const org = vault . org && ( await this . storage . get ( Org , vault . org . id ) ) ; const allowed = org ? org . canRead ( vault , account ) : vault . owner === account . id ; if ( ! allowed ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS ) ; } const att = await this . attachmentStorage . get ( vaultId , id ) ; this . log ( "attachment.get" , { attachment : { type : att . type , size : att . size , id : att . id } , vault : { id : vault . id , name : vault . name } , org : org && { id : org ! . id , name : org ! . name , type : org ! . type } } ) ; return att ; } async deleteAttachment ( { vault : vaultId , id } ) { const { account } = this . _requireAuth ( ) ; const vault = await this . storage . get ( Vault , vaultId ) ; const org = vault . org && ( await this . storage . get ( Org , vault . org . id ) ) ; const allowed = org ? org . canWrite ( vault , account ) : vault . owner === account . id ; if ( ! allowed ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS ) ; } await this . attachmentStorage . delete ( vaultId , id ) ; await this . _updateUsedStorage ( org || account ) ; this . log ( "attachment.delete" , { attachment : { id } , vault : { id : vault . id , name : vault . name } , org : org && { id : org ! . id , name : org ! . name , type : org ! . type } } ) ; } async updateBilling ( params ) { if ( ! this . billingProvider ) { throw new Err ( ErrorCode . NOT_SUPPORTED ) ; } const { account } = this . _requireAuth ( ) ; params . account = params . account || account . id ; const { account : accId , org : orgId } = params ; if ( orgId ) { const org = await this . storage . get ( Org , orgId ) ; if ( ! org . isOwner ( account ) ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS ) ; } } else if ( accId && accId !== account . id ) { throw new Err ( ErrorCode . INSUFFICIENT_PERMISSIONS ) ; } await this . billingProvider . update ( params ) ; this . log ( "billing.update" , { params : params . toRaw ( ) } ) ; } async getPlans ( ) { this . log ( "billing.getPlans" ) ; return this . billingProvider ? this . billingProvider . getInfo ( ) . plans : [ ] ; } async getBillingProviders ( ) { this . log ( "billing.getProviders" ) ; return this . billingProvider ? [ this . billingProvider . getInfo ( ) ] : [ ] ; } async getLegacyData ( { email , verify } ) { if ( verify ) { await this . _checkMFAToken ( email , verify , MFAPurpose . GetLegacyData ) ; } else { const { account } = this . _requireAuth ( ) ; if ( account . email !== email ) { throw new Err ( ErrorCode . BAD_REQUEST ) ; } } if ( ! this . legacyServer ) { throw new Err ( ErrorCode . NOT_SUPPORTED , "This Padloc instance does not support this feature!" ) ; } const data = await this . legacyServer . getStore ( email ) ; if ( ! data ) { throw new Err ( ErrorCode . NOT_FOUND , "No legacy account found." ) ; } return data ; } async deleteLegacyAccount ( ) { if ( ! this . legacyServer ) { throw new Err ( ErrorCode . NOT_SUPPORTED , "This Padloc instance does not support this feature!" ) ; } const { account } = this . _requireAuth ( ) ; await this . legacyServer . deleteAccount ( account . email ) ; } updateMetaData ( org ) { return this . server . updateMetaData ( org ) ; } private async _updateUsedStorage ( acc ) { const vaults = acc instanceof Org ? acc . vaults : [ acc . mainVault ] ; const usedStorage = ( await Promise . all ( vaults . map ( ( { id } ) => this . attachmentStorage . getUsage ( id ) ) ) ) . reduce ( ( sum , each ) => sum + each , 0 ) ; acc . usedStorage = usedStorage ; await this . storage . save ( acc ) ; } private _requireAuth ( ) { const { account , session } = this . context ; if ( ! session || ! account ) { throw new Err ( ErrorCode . INVALID_SESSION ) ; } return { account , session } ; } private async _checkMFACode ( email , code , purpose ) { let ev ; try { ev = await this . storage . get ( MFARequest , `${ email }_${ purpose }` ) ; } catch ( e ) { if ( e . code === ErrorCode . NOT_FOUND ) { throw new Err ( ErrorCode . MFA_REQUIRED , "Email verification required." ) ; } else { throw e ; } } if ( ! equalCT ( ev . code , code . toLowerCase ( ) ) ) { ev . tries ++ ; if ( ev . tries > 5 ) { await this . storage . delete ( ev ) ; throw new Err ( ErrorCode . MFA_TRIES_EXCEEDED , "Maximum number of tries exceeded!" ) ; } else { await this . storage . save ( ev ) ; throw new Err ( ErrorCode . MFA_FAILED , "Invalid verification code. Please try again!" ) ; } } return ev ; } private async _checkMFAToken ( email , token , purpose ) { let ev ; try { ev = await this . storage . get ( MFARequest , `${ email }_${ purpose }` ) ; } catch ( e ) { if ( e . code === ErrorCode . NOT_FOUND ) { throw new Err ( ErrorCode . MFA_FAILED , "Email verification required." ) ; } else { throw e ; } } if ( ! equalCT ( ev . token , token ) ) { throw new Err ( ErrorCode . MFA_FAILED , "Invalid verification token. Please try again!" ) ; } await this . storage . delete ( ev ) ; } } /**
 * The Padloc server acts as a central repository for [[Account]]s, [[Org]]s
 * and [[Vault]]s. [[Server]] handles authentication, enforces user privileges
 * and acts as a mediator for key exchange between clients.
 *
 * The server component acts on a strict zero-trust, zero-knowledge principle
 * when it comes to sensitive data, meaning no sensitive data is ever exposed
 * to the server at any point, nor should the server (or the person controlling
 * it) ever be able to temper with critical data or trick users into granting
 * them access to encrypted information.
 */ export class Server { constructor ( public config , public storage , public messenger , /** Logger to use */ public logger = new Logger ( new VoidStorage ( ) ) , /** Attachment storage */ public attachmentStorage , public legacyServer ? ) { } public billingProvider ? : BillingProvider ; private _requestQueue = new Map < AccountID | OrgID , Promise < void > > ( ) ; makeController ( ctx ) { return new Controller ( this , ctx ) ; } /** Handles an incoming [[Request]], processing it and constructing a [[Reponse]] */ async handle ( req ) { const res = new Response ( ) ; const context = { } ; try { context . device = req . device ; try { await loadLanguage ( ( context . device && context . device . locale ) || "en" ) ; } catch ( e ) { } await this . _authenticate ( req , context ) ; const done = await this . _addToQueue ( context ) ; const controller = this . makeController ( context ) ; try { res . result = ( await controller . process ( req ) ) || null ; } finally { done ( ) ; } if ( context . session ) { await context . session . authenticate ( res ) ; } } catch ( e ) { this . _handleError ( e , req , res , context ) ; } return res ; } async updateMetaData ( org ) { org . revision = await uuid ( ) ; org . updated = new Date ( ) ; const promises = [ ] ; const deletedVaults = new Set < VaultID > ( ) ; const deletedMembers = new Set < AccountID > ( ) ; for ( const vaultInfo of org . vaults ) { promises . push ( ( async ( ) => { try { const vault = await this . storage . get ( Vault , vaultInfo . id ) ; vault . name = vaultInfo . name ; vault . org = { id : org . id , name : org . name , revision : org . revision } ; await this . storage . save ( vault ) ; vaultInfo . revision = vault . revision ; } catch ( e ) { if ( e . code !== ErrorCode . NOT_FOUND ) { throw e ; } deletedVaults . add ( vaultInfo . id ) ; } } ) ( ) ) ; } for ( const member of org . members ) { promises . push ( ( async ( ) => { try { const acc = await this . storage . get ( Account , member . id ) ; acc . orgs = [ ... acc . orgs . filter ( o => o . id !== org . id ) , { id : org . id , name : org . name , revision : org . revision } ] ; await this . storage . save ( acc ) ; member . name = acc . name ; } catch ( e ) { if ( e . code !== ErrorCode . NOT_FOUND ) { throw e ; } deletedMembers . add ( member . id ) ; } } ) ( ) ) ; } await Promise . all ( promises ) ; org . vaults = org . vaults . filter ( v => ! deletedVaults . has ( v . id ) ) ; org . members = org . members . filter ( m => ! deletedMembers . has ( m . id ) ) ; } private async _addToQueue ( context ) { if ( ! context . account ) { return ( ) => { } ; } const account = context . account ; const resolveFuncs = [ ] ; const promises = [ ] ; for ( const { id } of [ account , ... account . orgs ] ) { const promise = this . _requestQueue . get ( id ) ; if ( promise ) { promises . push ( promise ) ; } this . _requestQueue . set ( id , new Promise ( resolve => resolveFuncs . push ( resolve ) ) ) ; } await Promise . all ( promises ) ; return ( ) => resolveFuncs . forEach ( resolve => resolve ( ) ) ; } private async _authenticate ( req , ctx ) { if ( ! req . auth ) { return ; } let session ; try { session = await this . storage . get ( Session , req . auth . session ) ; } catch ( e ) { if ( e . code === ErrorCode . NOT_FOUND ) { throw new Err ( ErrorCode . INVALID_SESSION ) ; } else { throw e ; } } if ( session . expires && session . expires < new Date ( ) ) { throw new Err ( ErrorCode . SESSION_EXPIRED ) ; } if ( ! ( await session . verify ( req ) ) ) { throw new Err ( ErrorCode . INVALID_REQUEST , "Failed to verify request signature!" ) ; } const age = Date . now ( ) - new Date ( req . auth . time ) . getTime ( ) ; if ( age > this . config . maxRequestAge ) { throw new Err ( ErrorCode . MAX_REQUEST_AGE_EXCEEDED , "The request was rejected because it's timestamp is too far in the past. " + "Please make sure your local clock is set to the correct time and try again!" ) ; } const account = await this . storage . get ( Account , session . account ) ; ctx . session = session ; ctx . account = account ; session . lastUsed = new Date ( ) ; session . device = ctx . device ; session . updated = new Date ( ) ; const i = account . sessions . findIndex ( ( { id } ) => id === session . id ) ; if ( i !== - 1 ) { account . sessions [ i ] = session . info ; } else { account . sessions . push ( session . info ) ; } await Promise . all ( [ this . storage . save ( session ) , this . storage . save ( account ) ] ) ; } private _handleError ( error , req , res , context ) { const e = error instanceof Err ? error : new Err ( ErrorCode . SERVER_ERROR , "Something went wrong while we were processing your request. " + "Our team has been notified and will resolve the problem as soon as possible!" , { report : true , error } ) ; res . error = { code : e . code , message : e . message } ; const evt = this . logger . log ( "error" , { account : context . account && { id : context . account . id , email : context . account . email , name : context . account . name } , device : context . device && context . device . toRaw ( ) , error : e . toRaw ( ) , method : req . method , request : e . report ? req : undefined } ) ; if ( e . report && this . config . reportErrors ) { this . messenger . send ( this . config . reportErrors , { title : "Padloc Error Notification" , text : `The following error occurred at ${ e . time }:\n\n` + `Code: ${ e . code }\n` + `Message: ${ e . message }\n` + `Event ID: ${ evt . id }` , html : "" } ) ; } } }