refactor: application dependency management (#2363)

This commit is contained in:
Mo
2023-07-23 15:54:31 -05:00
committed by GitHub
parent e698b1c990
commit a77535456c
299 changed files with 7415 additions and 4890 deletions

View File

@@ -13,4 +13,5 @@ export const SharedVaultInvitesPaths = {
`/v1/shared-vaults/${sharedVaultUuid}/invites/${inviteUuid}`, `/v1/shared-vaults/${sharedVaultUuid}/invites/${inviteUuid}`,
deleteAllSharedVaultInvites: (sharedVaultUuid: string) => `/v1/shared-vaults/${sharedVaultUuid}/invites`, deleteAllSharedVaultInvites: (sharedVaultUuid: string) => `/v1/shared-vaults/${sharedVaultUuid}/invites`,
deleteAllInboundInvites: '/v1/shared-vaults/invites/inbound', deleteAllInboundInvites: '/v1/shared-vaults/invites/inbound',
deleteAllOutboundInvites: '/v1/shared-vaults/invites/outbound',
} }

View File

@@ -72,4 +72,8 @@ export class SharedVaultInvitesServer implements SharedVaultInvitesServerInterfa
deleteAllInboundInvites(): Promise<HttpResponse<{ success: boolean }>> { deleteAllInboundInvites(): Promise<HttpResponse<{ success: boolean }>> {
return this.httpService.delete(SharedVaultInvitesPaths.deleteAllInboundInvites) return this.httpService.delete(SharedVaultInvitesPaths.deleteAllInboundInvites)
} }
deleteAllOutboundInvites(): Promise<HttpResponse<{ success: boolean }>> {
return this.httpService.delete(SharedVaultInvitesPaths.deleteAllOutboundInvites)
}
} }

View File

@@ -32,4 +32,5 @@ export interface SharedVaultInvitesServerInterface {
): Promise<HttpResponse<DeleteAllSharedVaultInvitesResponse>> ): Promise<HttpResponse<DeleteAllSharedVaultInvitesResponse>>
deleteInvite(params: DeleteInviteRequestParams): Promise<HttpResponse<DeleteInviteResponse>> deleteInvite(params: DeleteInviteRequestParams): Promise<HttpResponse<DeleteInviteResponse>>
deleteAllInboundInvites(): Promise<HttpResponse<{ success: boolean }>> deleteAllInboundInvites(): Promise<HttpResponse<{ success: boolean }>>
deleteAllOutboundInvites(): Promise<HttpResponse<{ success: boolean }>>
} }

View File

@@ -13,6 +13,7 @@ import {
KeySystemRootKeyInterface, KeySystemRootKeyInterface,
RootKeyInterface, RootKeyInterface,
KeySystemRootKeyParamsInterface, KeySystemRootKeyParamsInterface,
PortablePublicKeySet,
} from '@standardnotes/models' } from '@standardnotes/models'
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { firstHalfOfString, secondHalfOfString, splitString, UuidGenerator } from '@standardnotes/utils' import { firstHalfOfString, secondHalfOfString, splitString, UuidGenerator } from '@standardnotes/utils'
@@ -28,11 +29,12 @@ import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
import { LegacyAttachedData } from '../../Types/LegacyAttachedData' import { LegacyAttachedData } from '../../Types/LegacyAttachedData'
import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData' import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData'
import { OperatorInterface } from '../OperatorInterface/OperatorInterface' import { OperatorInterface } from '../OperatorInterface/OperatorInterface'
import { PublicKeySet } from '../Types/PublicKeySet'
import { AsymmetricDecryptResult } from '../Types/AsymmetricDecryptResult' import { AsymmetricDecryptResult } from '../Types/AsymmetricDecryptResult'
import { AsymmetricSignatureVerificationDetachedResult } from '../Types/AsymmetricSignatureVerificationDetachedResult' import { AsymmetricSignatureVerificationDetachedResult } from '../Types/AsymmetricSignatureVerificationDetachedResult'
import { AsyncOperatorInterface } from '../OperatorInterface/AsyncOperatorInterface' import { AsyncOperatorInterface } from '../OperatorInterface/AsyncOperatorInterface'
import { ContentType } from '@standardnotes/domain-core' import { ContentType, Result } from '@standardnotes/domain-core'
import { AsymmetricItemAdditionalData } from '../../Types/EncryptionAdditionalData'
const NO_IV = '00000000000000000000000000000000' const NO_IV = '00000000000000000000000000000000'
@@ -272,11 +274,23 @@ export class SNProtocolOperator001 implements OperatorInterface, AsyncOperatorIn
throw new Error('Method not implemented.') throw new Error('Method not implemented.')
} }
asymmetricDecryptOwnMessage(_dto: {
message: string
ownPrivateKey: string
recipientPublicKey: string
}): Result<AsymmetricDecryptResult> {
throw new Error('Method not implemented.')
}
asymmetricSignatureVerifyDetached(_encryptedString: string): AsymmetricSignatureVerificationDetachedResult { asymmetricSignatureVerifyDetached(_encryptedString: string): AsymmetricSignatureVerificationDetachedResult {
throw new Error('Method not implemented.') throw new Error('Method not implemented.')
} }
getSenderPublicKeySetFromAsymmetricallyEncryptedString(_string: string): PublicKeySet { asymmetricStringGetAdditionalData(_dto: { encryptedString: string }): Result<AsymmetricItemAdditionalData> {
throw new Error('Method not implemented.')
}
getSenderPublicKeySetFromAsymmetricallyEncryptedString(_string: string): PortablePublicKeySet {
throw new Error('Method not implemented.') throw new Error('Method not implemented.')
} }
} }

View File

@@ -12,6 +12,7 @@ import {
KeySystemIdentifier, KeySystemIdentifier,
RootKeyInterface, RootKeyInterface,
KeySystemRootKeyParamsInterface, KeySystemRootKeyParamsInterface,
PortablePublicKeySet,
} from '@standardnotes/models' } from '@standardnotes/models'
import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common' import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
import { HexString, PkcKeyPair, PureCryptoInterface, Utf8String } from '@standardnotes/sncrypto-common' import { HexString, PkcKeyPair, PureCryptoInterface, Utf8String } from '@standardnotes/sncrypto-common'
@@ -30,9 +31,9 @@ import { OperatorInterface } from '../OperatorInterface/OperatorInterface'
import { AsymmetricallyEncryptedString } from '../Types/Types' import { AsymmetricallyEncryptedString } from '../Types/Types'
import { AsymmetricItemAdditionalData } from '../../Types/EncryptionAdditionalData' import { AsymmetricItemAdditionalData } from '../../Types/EncryptionAdditionalData'
import { V004AsymmetricStringComponents } from './V004AlgorithmTypes' import { V004AsymmetricStringComponents } from './V004AlgorithmTypes'
import { AsymmetricEncryptUseCase } from './UseCase/Asymmetric/AsymmetricEncrypt' import { AsymmetricEncrypt004 } from './UseCase/Asymmetric/AsymmetricEncrypt'
import { ParseConsistentBase64JsonPayloadUseCase } from './UseCase/Utils/ParseConsistentBase64JsonPayload' import { ParseConsistentBase64JsonPayloadUseCase } from './UseCase/Utils/ParseConsistentBase64JsonPayload'
import { AsymmetricDecryptUseCase } from './UseCase/Asymmetric/AsymmetricDecrypt' import { AsymmetricDecrypt004 } from './UseCase/Asymmetric/AsymmetricDecrypt'
import { GenerateDecryptedParametersUseCase } from './UseCase/Symmetric/GenerateDecryptedParameters' import { GenerateDecryptedParametersUseCase } from './UseCase/Symmetric/GenerateDecryptedParameters'
import { GenerateEncryptedParametersUseCase } from './UseCase/Symmetric/GenerateEncryptedParameters' import { GenerateEncryptedParametersUseCase } from './UseCase/Symmetric/GenerateEncryptedParameters'
import { DeriveRootKeyUseCase } from './UseCase/RootKey/DeriveRootKey' import { DeriveRootKeyUseCase } from './UseCase/RootKey/DeriveRootKey'
@@ -41,14 +42,15 @@ import { CreateRootKeyUseCase } from './UseCase/RootKey/CreateRootKey'
import { UuidGenerator } from '@standardnotes/utils' import { UuidGenerator } from '@standardnotes/utils'
import { CreateKeySystemItemsKeyUseCase } from './UseCase/KeySystem/CreateKeySystemItemsKey' import { CreateKeySystemItemsKeyUseCase } from './UseCase/KeySystem/CreateKeySystemItemsKey'
import { AsymmetricDecryptResult } from '../Types/AsymmetricDecryptResult' import { AsymmetricDecryptResult } from '../Types/AsymmetricDecryptResult'
import { PublicKeySet } from '../Types/PublicKeySet'
import { CreateRandomKeySystemRootKey } from './UseCase/KeySystem/CreateRandomKeySystemRootKey' import { CreateRandomKeySystemRootKey } from './UseCase/KeySystem/CreateRandomKeySystemRootKey'
import { CreateUserInputKeySystemRootKey } from './UseCase/KeySystem/CreateUserInputKeySystemRootKey' import { CreateUserInputKeySystemRootKey } from './UseCase/KeySystem/CreateUserInputKeySystemRootKey'
import { AsymmetricSignatureVerificationDetachedResult } from '../Types/AsymmetricSignatureVerificationDetachedResult' import { AsymmetricSignatureVerificationDetachedResult } from '../Types/AsymmetricSignatureVerificationDetachedResult'
import { AsymmetricSignatureVerificationDetachedUseCase } from './UseCase/Asymmetric/AsymmetricSignatureVerificationDetached' import { AsymmetricSignatureVerificationDetached004 } from './UseCase/Asymmetric/AsymmetricSignatureVerificationDetached'
import { DeriveKeySystemRootKeyUseCase } from './UseCase/KeySystem/DeriveKeySystemRootKey' import { DeriveKeySystemRootKeyUseCase } from './UseCase/KeySystem/DeriveKeySystemRootKey'
import { SyncOperatorInterface } from '../OperatorInterface/SyncOperatorInterface' import { SyncOperatorInterface } from '../OperatorInterface/SyncOperatorInterface'
import { ContentType } from '@standardnotes/domain-core' import { ContentType, Result } from '@standardnotes/domain-core'
import { AsymmetricStringGetAdditionalData004 } from './UseCase/Asymmetric/AsymmetricStringGetAdditionalData'
import { AsymmetricDecryptOwnMessage004 } from './UseCase/Asymmetric/AsymmetricDecryptOwnMessage'
export class SNProtocolOperator004 implements OperatorInterface, SyncOperatorInterface { export class SNProtocolOperator004 implements OperatorInterface, SyncOperatorInterface {
constructor(protected readonly crypto: PureCryptoInterface) {} constructor(protected readonly crypto: PureCryptoInterface) {}
@@ -167,7 +169,7 @@ export class SNProtocolOperator004 implements OperatorInterface, SyncOperatorInt
senderSigningKeyPair: PkcKeyPair senderSigningKeyPair: PkcKeyPair
recipientPublicKey: HexString recipientPublicKey: HexString
}): AsymmetricallyEncryptedString { }): AsymmetricallyEncryptedString {
const usecase = new AsymmetricEncryptUseCase(this.crypto) const usecase = new AsymmetricEncrypt004(this.crypto)
return usecase.execute(dto) return usecase.execute(dto)
} }
@@ -175,18 +177,34 @@ export class SNProtocolOperator004 implements OperatorInterface, SyncOperatorInt
stringToDecrypt: AsymmetricallyEncryptedString stringToDecrypt: AsymmetricallyEncryptedString
recipientSecretKey: HexString recipientSecretKey: HexString
}): AsymmetricDecryptResult | null { }): AsymmetricDecryptResult | null {
const usecase = new AsymmetricDecryptUseCase(this.crypto) const usecase = new AsymmetricDecrypt004(this.crypto)
return usecase.execute(dto)
}
asymmetricDecryptOwnMessage(dto: {
message: AsymmetricallyEncryptedString
ownPrivateKey: HexString
recipientPublicKey: HexString
}): Result<AsymmetricDecryptResult> {
const usecase = new AsymmetricDecryptOwnMessage004(this.crypto)
return usecase.execute(dto) return usecase.execute(dto)
} }
asymmetricSignatureVerifyDetached( asymmetricSignatureVerifyDetached(
encryptedString: AsymmetricallyEncryptedString, encryptedString: AsymmetricallyEncryptedString,
): AsymmetricSignatureVerificationDetachedResult { ): AsymmetricSignatureVerificationDetachedResult {
const usecase = new AsymmetricSignatureVerificationDetachedUseCase(this.crypto) const usecase = new AsymmetricSignatureVerificationDetached004(this.crypto)
return usecase.execute({ encryptedString }) return usecase.execute({ encryptedString })
} }
getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PublicKeySet { asymmetricStringGetAdditionalData(dto: {
encryptedString: AsymmetricallyEncryptedString
}): Result<AsymmetricItemAdditionalData> {
const usecase = new AsymmetricStringGetAdditionalData004(this.crypto)
return usecase.execute(dto)
}
getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PortablePublicKeySet {
const [_, __, ___, additionalDataString] = <V004AsymmetricStringComponents>string.split(':') const [_, __, ___, additionalDataString] = <V004AsymmetricStringComponents>string.split(':')
const parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto) const parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto)
const additionalData = parseBase64Usecase.execute<AsymmetricItemAdditionalData>(additionalDataString) const additionalData = parseBase64Usecase.execute<AsymmetricItemAdditionalData>(additionalDataString)

View File

@@ -1,27 +1,27 @@
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { getMockedCrypto } from '../../MockedCrypto' import { getMockedCrypto } from '../../MockedCrypto'
import { AsymmetricDecryptUseCase } from './AsymmetricDecrypt' import { AsymmetricDecrypt004 } from './AsymmetricDecrypt'
import { AsymmetricEncryptUseCase } from './AsymmetricEncrypt' import { AsymmetricEncrypt004 } from './AsymmetricEncrypt'
import { V004AsymmetricStringComponents } from '../../V004AlgorithmTypes' import { V004AsymmetricStringComponents } from '../../V004AlgorithmTypes'
import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData' import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData'
describe('asymmetric decrypt use case', () => { describe('asymmetric decrypt use case', () => {
let crypto: PureCryptoInterface let crypto: PureCryptoInterface
let usecase: AsymmetricDecryptUseCase let usecase: AsymmetricDecrypt004
let recipientKeyPair: PkcKeyPair let recipientKeyPair: PkcKeyPair
let senderKeyPair: PkcKeyPair let senderKeyPair: PkcKeyPair
let senderSigningKeyPair: PkcKeyPair let senderSigningKeyPair: PkcKeyPair
beforeEach(() => { beforeEach(() => {
crypto = getMockedCrypto() crypto = getMockedCrypto()
usecase = new AsymmetricDecryptUseCase(crypto) usecase = new AsymmetricDecrypt004(crypto)
recipientKeyPair = crypto.sodiumCryptoBoxSeedKeypair('recipient-seedling') recipientKeyPair = crypto.sodiumCryptoBoxSeedKeypair('recipient-seedling')
senderKeyPair = crypto.sodiumCryptoBoxSeedKeypair('sender-seedling') senderKeyPair = crypto.sodiumCryptoBoxSeedKeypair('sender-seedling')
senderSigningKeyPair = crypto.sodiumCryptoSignSeedKeypair('sender-signing-seedling') senderSigningKeyPair = crypto.sodiumCryptoSignSeedKeypair('sender-signing-seedling')
}) })
const getEncryptedString = () => { const getEncryptedString = () => {
const encryptUsecase = new AsymmetricEncryptUseCase(crypto) const encryptUsecase = new AsymmetricEncrypt004(crypto)
const result = encryptUsecase.execute({ const result = encryptUsecase.execute({
stringToEncrypt: 'foobar', stringToEncrypt: 'foobar',

View File

@@ -5,7 +5,7 @@ import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsisten
import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData' import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData'
import { AsymmetricDecryptResult } from '../../../Types/AsymmetricDecryptResult' import { AsymmetricDecryptResult } from '../../../Types/AsymmetricDecryptResult'
export class AsymmetricDecryptUseCase { export class AsymmetricDecrypt004 {
private parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto) private parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto)
constructor(private readonly crypto: PureCryptoInterface) {} constructor(private readonly crypto: PureCryptoInterface) {}

View File

@@ -0,0 +1,51 @@
import { HexString, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { AsymmetricallyEncryptedString } from '../../../Types/Types'
import { V004AsymmetricStringComponents } from '../../V004AlgorithmTypes'
import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload'
import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData'
import { AsymmetricDecryptResult } from '../../../Types/AsymmetricDecryptResult'
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
export class AsymmetricDecryptOwnMessage004 implements SyncUseCaseInterface<AsymmetricDecryptResult> {
private parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto)
constructor(private readonly crypto: PureCryptoInterface) {}
execute(dto: {
message: AsymmetricallyEncryptedString
ownPrivateKey: HexString
recipientPublicKey: HexString
}): Result<AsymmetricDecryptResult> {
const [_, nonce, ciphertext, additionalDataString] = <V004AsymmetricStringComponents>dto.message.split(':')
const additionalData = this.parseBase64Usecase.execute<AsymmetricItemAdditionalData>(additionalDataString)
try {
const plaintext = this.crypto.sodiumCryptoBoxEasyDecrypt(
ciphertext,
nonce,
dto.recipientPublicKey,
dto.ownPrivateKey,
)
if (!plaintext) {
return Result.fail('Could not decrypt message')
}
const signatureVerified = this.crypto.sodiumCryptoSignVerify(
ciphertext,
additionalData.signingData.signature,
additionalData.signingData.publicKey,
)
return Result.ok({
plaintext,
signatureVerified,
signaturePublicKey: additionalData.signingData.publicKey,
senderPublicKey: additionalData.senderPublicKey,
})
} catch (error) {
return Result.fail('Could not decrypt message')
}
}
}

View File

@@ -1,20 +1,20 @@
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { getMockedCrypto } from '../../MockedCrypto' import { getMockedCrypto } from '../../MockedCrypto'
import { AsymmetricEncryptUseCase } from './AsymmetricEncrypt' import { AsymmetricEncrypt004 } from './AsymmetricEncrypt'
import { V004AsymmetricStringComponents } from '../../V004AlgorithmTypes' import { V004AsymmetricStringComponents } from '../../V004AlgorithmTypes'
import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload' import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload'
import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData' import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData'
describe('asymmetric encrypt use case', () => { describe('asymmetric encrypt use case', () => {
let crypto: PureCryptoInterface let crypto: PureCryptoInterface
let usecase: AsymmetricEncryptUseCase let usecase: AsymmetricEncrypt004
let encryptionKeyPair: PkcKeyPair let encryptionKeyPair: PkcKeyPair
let signingKeyPair: PkcKeyPair let signingKeyPair: PkcKeyPair
let parseBase64Usecase: ParseConsistentBase64JsonPayloadUseCase let parseBase64Usecase: ParseConsistentBase64JsonPayloadUseCase
beforeEach(() => { beforeEach(() => {
crypto = getMockedCrypto() crypto = getMockedCrypto()
usecase = new AsymmetricEncryptUseCase(crypto) usecase = new AsymmetricEncrypt004(crypto)
encryptionKeyPair = crypto.sodiumCryptoBoxSeedKeypair('seedling') encryptionKeyPair = crypto.sodiumCryptoBoxSeedKeypair('seedling')
signingKeyPair = crypto.sodiumCryptoSignSeedKeypair('seedling') signingKeyPair = crypto.sodiumCryptoSignSeedKeypair('seedling')
parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(crypto) parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(crypto)

View File

@@ -5,7 +5,7 @@ import { V004AsymmetricCiphertextPrefix, V004AsymmetricStringComponents } from '
import { CreateConsistentBase64JsonPayloadUseCase } from '../Utils/CreateConsistentBase64JsonPayload' import { CreateConsistentBase64JsonPayloadUseCase } from '../Utils/CreateConsistentBase64JsonPayload'
import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData' import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData'
export class AsymmetricEncryptUseCase { export class AsymmetricEncrypt004 {
private base64DataUsecase = new CreateConsistentBase64JsonPayloadUseCase(this.crypto) private base64DataUsecase = new CreateConsistentBase64JsonPayloadUseCase(this.crypto)
constructor(private readonly crypto: PureCryptoInterface) {} constructor(private readonly crypto: PureCryptoInterface) {}
@@ -21,8 +21,8 @@ export class AsymmetricEncryptUseCase {
const ciphertext = this.crypto.sodiumCryptoBoxEasyEncrypt( const ciphertext = this.crypto.sodiumCryptoBoxEasyEncrypt(
dto.stringToEncrypt, dto.stringToEncrypt,
nonce, nonce,
dto.senderKeyPair.privateKey,
dto.recipientPublicKey, dto.recipientPublicKey,
dto.senderKeyPair.privateKey,
) )
const additionalData: AsymmetricItemAdditionalData = { const additionalData: AsymmetricItemAdditionalData = {

View File

@@ -5,7 +5,7 @@ import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsisten
import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData' import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData'
import { AsymmetricSignatureVerificationDetachedResult } from '../../../Types/AsymmetricSignatureVerificationDetachedResult' import { AsymmetricSignatureVerificationDetachedResult } from '../../../Types/AsymmetricSignatureVerificationDetachedResult'
export class AsymmetricSignatureVerificationDetachedUseCase { export class AsymmetricSignatureVerificationDetached004 {
private parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto) private parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto)
constructor(private readonly crypto: PureCryptoInterface) {} constructor(private readonly crypto: PureCryptoInterface) {}

View File

@@ -0,0 +1,20 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { AsymmetricallyEncryptedString } from '../../../Types/Types'
import { V004AsymmetricStringComponents } from '../../V004AlgorithmTypes'
import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload'
import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData'
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
export class AsymmetricStringGetAdditionalData004 implements SyncUseCaseInterface<AsymmetricItemAdditionalData> {
private parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto)
constructor(private readonly crypto: PureCryptoInterface) {}
execute(dto: { encryptedString: AsymmetricallyEncryptedString }): Result<AsymmetricItemAdditionalData> {
const [_, __, ___, additionalDataString] = <V004AsymmetricStringComponents>dto.encryptedString.split(':')
const additionalData = this.parseBase64Usecase.execute<AsymmetricItemAdditionalData>(additionalDataString)
return Result.ok(additionalData)
}
}

View File

@@ -2,8 +2,9 @@ import { ProtocolVersion, ProtocolVersionLatest } from '@standardnotes/common'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { createOperatorForVersion } from './Functions' import { createOperatorForVersion } from './Functions'
import { AnyOperatorInterface } from './OperatorInterface/TypeCheck' import { AnyOperatorInterface } from './OperatorInterface/TypeCheck'
import { EncryptionOperatorsInterface } from './EncryptionOperatorsInterface'
export class OperatorManager { export class EncryptionOperators implements EncryptionOperatorsInterface {
private operators: Record<string, AnyOperatorInterface> = {} private operators: Record<string, AnyOperatorInterface> = {}
constructor(private crypto: PureCryptoInterface) { constructor(private crypto: PureCryptoInterface) {

View File

@@ -0,0 +1,8 @@
import { ProtocolVersion } from '@standardnotes/common'
import { AnyOperatorInterface } from './OperatorInterface/TypeCheck'
export interface EncryptionOperatorsInterface {
operatorForVersion(version: ProtocolVersion): AnyOperatorInterface
defaultOperator(): AnyOperatorInterface
deinit(): void
}

View File

@@ -6,6 +6,7 @@ import {
KeySystemRootKeyInterface, KeySystemRootKeyInterface,
KeySystemIdentifier, KeySystemIdentifier,
KeySystemRootKeyParamsInterface, KeySystemRootKeyParamsInterface,
PortablePublicKeySet,
} from '@standardnotes/models' } from '@standardnotes/models'
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams' import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
import { EncryptedOutputParameters } from '../../Types/EncryptedParameters' import { EncryptedOutputParameters } from '../../Types/EncryptedParameters'
@@ -15,8 +16,9 @@ import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedA
import { HexString, PkcKeyPair } from '@standardnotes/sncrypto-common' import { HexString, PkcKeyPair } from '@standardnotes/sncrypto-common'
import { AsymmetricallyEncryptedString } from '../Types/Types' import { AsymmetricallyEncryptedString } from '../Types/Types'
import { AsymmetricDecryptResult } from '../Types/AsymmetricDecryptResult' import { AsymmetricDecryptResult } from '../Types/AsymmetricDecryptResult'
import { PublicKeySet } from '../Types/PublicKeySet'
import { AsymmetricSignatureVerificationDetachedResult } from '../Types/AsymmetricSignatureVerificationDetachedResult' import { AsymmetricSignatureVerificationDetachedResult } from '../Types/AsymmetricSignatureVerificationDetachedResult'
import { AsymmetricItemAdditionalData } from '../../Types/EncryptionAdditionalData'
import { Result } from '@standardnotes/domain-core'
/**w /**w
* An operator is responsible for performing crypto operations, such as generating keys * An operator is responsible for performing crypto operations, such as generating keys
@@ -92,11 +94,21 @@ export interface OperatorInterface {
recipientSecretKey: HexString recipientSecretKey: HexString
}): AsymmetricDecryptResult | null }): AsymmetricDecryptResult | null
asymmetricDecryptOwnMessage(dto: {
message: AsymmetricallyEncryptedString
ownPrivateKey: HexString
recipientPublicKey: HexString
}): Result<AsymmetricDecryptResult>
asymmetricSignatureVerifyDetached( asymmetricSignatureVerifyDetached(
encryptedString: AsymmetricallyEncryptedString, encryptedString: AsymmetricallyEncryptedString,
): AsymmetricSignatureVerificationDetachedResult ): AsymmetricSignatureVerificationDetachedResult
getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PublicKeySet asymmetricStringGetAdditionalData(dto: {
encryptedString: AsymmetricallyEncryptedString
}): Result<AsymmetricItemAdditionalData>
getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PortablePublicKeySet
versionForAsymmetricallyEncryptedString(encryptedString: string): ProtocolVersion versionForAsymmetricallyEncryptedString(encryptedString: string): ProtocolVersion
} }

View File

@@ -13,14 +13,14 @@ import {
ErrorDecryptingParameters, ErrorDecryptingParameters,
} from '../Types/EncryptedParameters' } from '../Types/EncryptedParameters'
import { DecryptedParameters } from '../Types/DecryptedParameters' import { DecryptedParameters } from '../Types/DecryptedParameters'
import { OperatorManager } from './OperatorManager'
import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { isAsyncOperator } from './OperatorInterface/TypeCheck' import { isAsyncOperator } from './OperatorInterface/TypeCheck'
import { EncryptionOperatorsInterface } from './EncryptionOperatorsInterface'
export async function encryptPayload( export async function encryptPayload(
payload: DecryptedPayloadInterface, payload: DecryptedPayloadInterface,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
operatorManager: OperatorManager, operatorManager: EncryptionOperatorsInterface,
signingKeyPair: PkcKeyPair | undefined, signingKeyPair: PkcKeyPair | undefined,
): Promise<EncryptedOutputParameters> { ): Promise<EncryptedOutputParameters> {
const operator = operatorManager.operatorForVersion(key.keyVersion) const operator = operatorManager.operatorForVersion(key.keyVersion)
@@ -42,7 +42,7 @@ export async function encryptPayload(
export async function decryptPayload<C extends ItemContent = ItemContent>( export async function decryptPayload<C extends ItemContent = ItemContent>(
payload: EncryptedPayloadInterface, payload: EncryptedPayloadInterface,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
operatorManager: OperatorManager, operatorManager: EncryptionOperatorsInterface,
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> { ): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
const operator = operatorManager.operatorForVersion(payload.version) const operator = operatorManager.operatorForVersion(payload.version)

View File

@@ -1,30 +0,0 @@
import { ItemsKeyInterface } from '@standardnotes/models'
export function findDefaultItemsKey(itemsKeys: ItemsKeyInterface[]): ItemsKeyInterface | undefined {
if (itemsKeys.length === 1) {
return itemsKeys[0]
}
const defaultKeys = itemsKeys.filter((key) => {
return key.isDefault
})
if (defaultKeys.length === 0) {
return undefined
}
if (defaultKeys.length === 1) {
return defaultKeys[0]
}
/**
* Prioritize one that is synced, as neverSynced keys will likely be deleted after
* DownloadFirst sync.
*/
const syncedKeys = defaultKeys.filter((key) => !key.neverSynced)
if (syncedKeys.length > 0) {
return syncedKeys[0]
}
return undefined
}

View File

@@ -1,42 +1,30 @@
export * from './Algorithm' export * from './Algorithm'
export * from './Backups/BackupFileType' export * from './Backups/BackupFileType'
export * from './Keys/ItemsKey/ItemsKey' export * from './Keys/ItemsKey/ItemsKey'
export * from './Keys/ItemsKey/ItemsKeyMutator' export * from './Keys/ItemsKey/ItemsKeyMutator'
export * from './Keys/ItemsKey/Registration' export * from './Keys/ItemsKey/Registration'
export * from './Keys/KeySystemItemsKey/KeySystemItemsKey' export * from './Keys/KeySystemItemsKey/KeySystemItemsKey'
export * from './Keys/KeySystemItemsKey/KeySystemItemsKeyMutator' export * from './Keys/KeySystemItemsKey/KeySystemItemsKeyMutator'
export * from './Keys/KeySystemItemsKey/Registration' export * from './Keys/KeySystemItemsKey/Registration'
export * from './Keys/RootKey/Functions' export * from './Keys/RootKey/Functions'
export * from './Keys/RootKey/KeyParamsFunctions' export * from './Keys/RootKey/KeyParamsFunctions'
export * from './Keys/RootKey/ProtocolVersionForKeyParams' export * from './Keys/RootKey/ProtocolVersionForKeyParams'
export * from './Keys/RootKey/RootKey' export * from './Keys/RootKey/RootKey'
export * from './Keys/RootKey/RootKeyParams' export * from './Keys/RootKey/RootKeyParams'
export * from './Keys/RootKey/ValidKeyParamsKeys' export * from './Keys/RootKey/ValidKeyParamsKeys'
export * from './Keys/Utils/KeyRecoveryStrings' export * from './Keys/Utils/KeyRecoveryStrings'
export * from './Operator/001/Operator001' export * from './Operator/001/Operator001'
export * from './Operator/002/Operator002' export * from './Operator/002/Operator002'
export * from './Operator/003/Operator003' export * from './Operator/003/Operator003'
export * from './Operator/004/Operator004' export * from './Operator/004/Operator004'
export * from './Operator/004/V004AlgorithmHelpers' export * from './Operator/004/V004AlgorithmHelpers'
export * from './Operator/EncryptionOperators'
export * from './Operator/EncryptionOperatorsInterface'
export * from './Operator/Functions' export * from './Operator/Functions'
export * from './Operator/OperatorInterface/OperatorInterface' export * from './Operator/OperatorInterface/OperatorInterface'
export * from './Operator/OperatorManager'
export * from './Operator/OperatorWrapper' export * from './Operator/OperatorWrapper'
export * from './Operator/Types/PublicKeySet'
export * from './Operator/Types/AsymmetricSignatureVerificationDetachedResult' export * from './Operator/Types/AsymmetricSignatureVerificationDetachedResult'
export * from './Operator/Types/Types' export * from './Operator/Types/Types'
export * from './Service/Encryption/EncryptionProviderInterface'
export * from './Service/KeySystemKeyManagerInterface'
export * from './Service/Functions'
export * from './Service/RootKey/KeyMode'
export * from './Split/AbstractKeySplit' export * from './Split/AbstractKeySplit'
export * from './Split/EncryptionSplit' export * from './Split/EncryptionSplit'
export * from './Split/EncryptionTypeSplit' export * from './Split/EncryptionTypeSplit'
@@ -44,11 +32,9 @@ export * from './Split/Functions'
export * from './Split/KeyedDecryptionSplit' export * from './Split/KeyedDecryptionSplit'
export * from './Split/KeyedEncryptionSplit' export * from './Split/KeyedEncryptionSplit'
export * from './StandardException' export * from './StandardException'
export * from './Types/EncryptedParameters'
export * from './Types/DecryptedParameters' export * from './Types/DecryptedParameters'
export * from './Types/EncryptedParameters'
export * from './Types/ItemAuthenticatedData' export * from './Types/ItemAuthenticatedData'
export * from './Types/LegacyAttachedData' export * from './Types/LegacyAttachedData'
export * from './Types/RootKeyEncryptedAuthenticatedData' export * from './Types/RootKeyEncryptedAuthenticatedData'
export * from './Username/PrivateUsername' export * from './Username/PrivateUsername'

View File

@@ -23,7 +23,7 @@ export class DecryptedItemMutator<
this.mutableContent = mutableCopy this.mutableContent = mutableCopy
} }
public override getResult() { public override getResult(): DecryptedPayloadInterface<C> {
if (this.type === MutationType.NonDirtying) { if (this.type === MutationType.NonDirtying) {
return this.immutablePayload.copy({ return this.immutablePayload.copy({
content: this.mutableContent, content: this.mutableContent,

View File

@@ -3,7 +3,6 @@ export enum AsymmetricMessagePayloadType {
SharedVaultRootKeyChanged = 'shared-vault-root-key-changed', SharedVaultRootKeyChanged = 'shared-vault-root-key-changed',
SenderKeypairChanged = 'sender-keypair-changed', SenderKeypairChanged = 'sender-keypair-changed',
SharedVaultMetadataChanged = 'shared-vault-metadata-changed', SharedVaultMetadataChanged = 'shared-vault-metadata-changed',
/** /**
* Shared Vault Invites conform to the asymmetric message protocol, but are sent via the dedicated * Shared Vault Invites conform to the asymmetric message protocol, but are sent via the dedicated
* SharedVaultInvite model and not the AsymmetricMessage model on the server side. * SharedVaultInvite model and not the AsymmetricMessage model on the server side.

View File

@@ -1,13 +1,19 @@
import { KeySystemRootKeyContentSpecialized } from '../../../Syncable/KeySystemRootKey/KeySystemRootKeyContent' import { KeySystemRootKeyContentSpecialized } from '../../../Syncable/KeySystemRootKey/KeySystemRootKeyContent'
import { TrustedContactContentSpecialized } from '../../../Syncable/TrustedContact/TrustedContactContent' import { ContactPublicKeySetJsonInterface } from '../../../Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetJsonInterface'
import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon' import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon'
import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType' import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType'
export type VaultInviteDelegatedContact = {
name?: string
contactUuid: string
publicKeySet: ContactPublicKeySetJsonInterface
}
export type AsymmetricMessageSharedVaultInvite = { export type AsymmetricMessageSharedVaultInvite = {
type: AsymmetricMessagePayloadType.SharedVaultInvite type: AsymmetricMessagePayloadType.SharedVaultInvite
data: AsymmetricMessageDataCommon & { data: AsymmetricMessageDataCommon & {
rootKey: KeySystemRootKeyContentSpecialized rootKey: KeySystemRootKeyContentSpecialized
trustedContacts: TrustedContactContentSpecialized[] trustedContacts: VaultInviteDelegatedContact[]
metadata: { metadata: {
name: string name: string
description?: string description?: string

View File

@@ -1,4 +1,4 @@
import { TrustedContactContentSpecialized } from '../../../Syncable/TrustedContact/TrustedContactContent' import { TrustedContactContentSpecialized } from '../../../Syncable/TrustedContact/Content/TrustedContactContent'
import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon' import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon'
import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType' import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType'

View File

@@ -0,0 +1,11 @@
import { ItemContent } from '../../../Abstract/Content/ItemContent'
import { ContactPublicKeySetJsonInterface } from '../PublicKeySet/ContactPublicKeySetJsonInterface'
export type TrustedContactContentSpecialized = {
name: string
contactUuid: string
publicKeySet: ContactPublicKeySetJsonInterface
isMe: boolean
}
export type TrustedContactContent = TrustedContactContentSpecialized & ItemContent

View File

@@ -0,0 +1,93 @@
import { ContentType } from '@standardnotes/domain-core'
import { DecryptedPayload, PayloadTimestampDefaults } from '../../../Abstract/Payload'
import { TrustedContact } from '../TrustedContact'
import { TrustedContactInterface } from '../TrustedContactInterface'
import { TrustedContactMutator } from './TrustedContactMutator'
import { ContactPublicKeySet } from '../PublicKeySet/ContactPublicKeySet'
import { FillItemContentSpecialized } from '../../../Abstract/Content/ItemContent'
import { TrustedContactContentSpecialized } from '../Content/TrustedContactContent'
import { MutationType } from '../../../Abstract/Item'
import { PortablePublicKeySet } from '../Types/PortablePublicKeySet'
import { ContactPublicKeySetInterface } from '../PublicKeySet/ContactPublicKeySetInterface'
function createMockPublicKeySetChain(): ContactPublicKeySetInterface {
const nMinusOne = new ContactPublicKeySet({
encryption: 'encryption-public-key-n-1',
signing: 'signing-public-key-n-1',
timestamp: new Date(-1),
previousKeySet: undefined,
})
const root = new ContactPublicKeySet({
encryption: 'encryption-public-key',
signing: 'signing-public-key',
timestamp: new Date(),
previousKeySet: nMinusOne,
})
return root
}
describe('TrustedContactMutator', () => {
let contact: TrustedContactInterface
let mutator: TrustedContactMutator
beforeEach(() => {
contact = new TrustedContact(
new DecryptedPayload({
uuid: 'item-uuid',
content_type: ContentType.TYPES.TrustedContact,
...PayloadTimestampDefaults(),
content: FillItemContentSpecialized<TrustedContactContentSpecialized, TrustedContactInterface>({
name: 'test',
contactUuid: 'contact-uuid',
isMe: true,
publicKeySet: createMockPublicKeySetChain(),
}),
}),
)
mutator = new TrustedContactMutator(contact, MutationType.UpdateUserTimestamps)
})
it('should set name', () => {
mutator.name = 'new name'
const result = mutator.getResult()
expect(result.content.name).toEqual('new name')
})
it('should add public key', () => {
const currentKeySet = contact.publicKeySet
const newKeySet: PortablePublicKeySet = {
encryption: 'new-encryption-public-key',
signing: 'new-signing-public-key',
}
mutator.addPublicKey(newKeySet)
const result = new TrustedContact(mutator.getResult())
expect(result.publicKeySet.encryption).toEqual(newKeySet.encryption)
expect(result.publicKeySet.signing).toEqual(newKeySet.signing)
expect(result.publicKeySet.previousKeySet).toEqual(currentKeySet)
})
it('should replace public key set', () => {
const replacement = new ContactPublicKeySet({
encryption: 'encryption-public-key-replacement',
signing: 'signing-public-key-replacement',
timestamp: new Date(),
previousKeySet: undefined,
})
mutator.replacePublicKeySet(replacement.asJson())
const result = new TrustedContact(mutator.getResult())
expect(result.publicKeySet.encryption).toEqual(replacement.encryption)
expect(result.publicKeySet.signing).toEqual(replacement.signing)
})
})

View File

@@ -0,0 +1,27 @@
import { DecryptedItemMutator } from '../../../Abstract/Item'
import { TrustedContactContent } from '../Content/TrustedContactContent'
import { TrustedContactInterface } from '../TrustedContactInterface'
import { ContactPublicKeySet } from '../PublicKeySet/ContactPublicKeySet'
import { PortablePublicKeySet } from '../Types/PortablePublicKeySet'
import { ContactPublicKeySetJsonInterface } from '../PublicKeySet/ContactPublicKeySetJsonInterface'
export class TrustedContactMutator extends DecryptedItemMutator<TrustedContactContent, TrustedContactInterface> {
set name(newName: string) {
this.mutableContent.name = newName
}
addPublicKey(keySet: PortablePublicKeySet): void {
const newKey = new ContactPublicKeySet({
encryption: keySet.encryption,
signing: keySet.signing,
timestamp: new Date(),
previousKeySet: this.immutableItem.publicKeySet,
})
this.mutableContent.publicKeySet = newKey
}
replacePublicKeySet(publicKeySet: ContactPublicKeySetJsonInterface): void {
this.mutableContent.publicKeySet = publicKeySet
}
}

View File

@@ -5,36 +5,18 @@ export class ContactPublicKeySet implements ContactPublicKeySetInterface {
encryption: string encryption: string
signing: string signing: string
timestamp: Date timestamp: Date
isRevoked: boolean previousKeySet?: ContactPublicKeySetInterface
previousKeySet?: ContactPublicKeySet
constructor( constructor(dto: {
encryption: string, encryption: string
signing: string, signing: string
timestamp: Date, timestamp: Date
isRevoked: boolean, previousKeySet: ContactPublicKeySetInterface | undefined
previousKeySet: ContactPublicKeySet | undefined, }) {
) { this.encryption = dto.encryption
this.encryption = encryption this.signing = dto.signing
this.signing = signing this.timestamp = dto.timestamp
this.timestamp = timestamp this.previousKeySet = dto.previousKeySet
this.isRevoked = isRevoked
this.previousKeySet = previousKeySet
}
public findKeySet(params: {
targetEncryptionPublicKey: string
targetSigningPublicKey: string
}): ContactPublicKeySetInterface | undefined {
if (this.encryption === params.targetEncryptionPublicKey && this.signing === params.targetSigningPublicKey) {
return this
}
if (this.previousKeySet) {
return this.previousKeySet.findKeySet(params)
}
return undefined
} }
public findKeySetWithSigningKey(signingKey: string): ContactPublicKeySetInterface | undefined { public findKeySetWithSigningKey(signingKey: string): ContactPublicKeySetInterface | undefined {
@@ -61,13 +43,30 @@ export class ContactPublicKeySet implements ContactPublicKeySetInterface {
return undefined return undefined
} }
asJson(): ContactPublicKeySetJsonInterface {
return {
encryption: this.encryption,
signing: this.signing,
timestamp: this.timestamp,
previousKeySet: this.previousKeySet ? this.previousKeySet.asJson() : undefined,
}
}
mutableCopy(): ContactPublicKeySetInterface {
return new ContactPublicKeySet({
encryption: this.encryption,
signing: this.signing,
timestamp: this.timestamp,
previousKeySet: this.previousKeySet ? ContactPublicKeySet.FromJson(this.previousKeySet.asJson()) : undefined,
})
}
static FromJson(json: ContactPublicKeySetJsonInterface): ContactPublicKeySetInterface { static FromJson(json: ContactPublicKeySetJsonInterface): ContactPublicKeySetInterface {
return new ContactPublicKeySet( return new ContactPublicKeySet({
json.encryption, encryption: json.encryption,
json.signing, signing: json.signing,
new Date(json.timestamp), timestamp: new Date(json.timestamp),
json.isRevoked, previousKeySet: json.previousKeySet ? ContactPublicKeySet.FromJson(json.previousKeySet) : undefined,
json.previousKeySet ? ContactPublicKeySet.FromJson(json.previousKeySet) : undefined, })
)
} }
} }

View File

@@ -1,15 +1,14 @@
import { ContactPublicKeySetJsonInterface } from './ContactPublicKeySetJsonInterface'
export interface ContactPublicKeySetInterface { export interface ContactPublicKeySetInterface {
encryption: string encryption: string
signing: string signing: string
timestamp: Date timestamp: Date
isRevoked: boolean
previousKeySet?: ContactPublicKeySetInterface previousKeySet?: ContactPublicKeySetInterface
findKeySet(params: {
targetEncryptionPublicKey: string
targetSigningPublicKey: string
}): ContactPublicKeySetInterface | undefined
findKeySetWithPublicKey(publicKey: string): ContactPublicKeySetInterface | undefined findKeySetWithPublicKey(publicKey: string): ContactPublicKeySetInterface | undefined
findKeySetWithSigningKey(signingKey: string): ContactPublicKeySetInterface | undefined findKeySetWithSigningKey(signingKey: string): ContactPublicKeySetInterface | undefined
asJson(): ContactPublicKeySetJsonInterface
mutableCopy(): ContactPublicKeySetInterface
} }

View File

@@ -2,6 +2,5 @@ export interface ContactPublicKeySetJsonInterface {
encryption: string encryption: string
signing: string signing: string
timestamp: Date timestamp: Date
isRevoked: boolean
previousKeySet?: ContactPublicKeySetJsonInterface previousKeySet?: ContactPublicKeySetJsonInterface
} }

View File

@@ -1,8 +0,0 @@
import { ContactPublicKeySetInterface } from './ContactPublicKeySetInterface'
export type FindPublicKeySetResult =
| {
publicKeySet: ContactPublicKeySetInterface
current: boolean
}
| undefined

View File

@@ -0,0 +1,109 @@
import { ContentType } from '@standardnotes/domain-core'
import { DecryptedPayload, PayloadTimestampDefaults } from '../../Abstract/Payload'
import { ContactPublicKeySet } from './PublicKeySet/ContactPublicKeySet'
import { ContactPublicKeySetInterface } from './PublicKeySet/ContactPublicKeySetInterface'
import { TrustedContact } from './TrustedContact'
import { TrustedContactInterface } from './TrustedContactInterface'
import { FillItemContentSpecialized } from '../../Abstract/Content/ItemContent'
import { ConflictStrategy } from '../../Abstract/Item'
import { TrustedContactContentSpecialized } from './Content/TrustedContactContent'
import { PublicKeyTrustStatus } from './Types/PublicKeyTrustStatus'
function createMockPublicKeySetChain(): ContactPublicKeySetInterface {
const nMinusOne = new ContactPublicKeySet({
encryption: 'encryption-public-key-n-1',
signing: 'signing-public-key-n-1',
timestamp: new Date(-1),
previousKeySet: undefined,
})
const root = new ContactPublicKeySet({
encryption: 'encryption-public-key',
signing: 'signing-public-key',
timestamp: new Date(),
previousKeySet: nMinusOne,
})
return root
}
function CreateContact(params: {
name?: string
contactUuid?: string
isMe?: boolean
publicKeySet?: ContactPublicKeySetInterface
}): TrustedContactInterface {
return new TrustedContact(
new DecryptedPayload({
uuid: 'item-uuid',
content_type: ContentType.TYPES.TrustedContact,
...PayloadTimestampDefaults(),
content: FillItemContentSpecialized<TrustedContactContentSpecialized, TrustedContactInterface>({
name: params.name ?? 'test',
contactUuid: params.contactUuid ?? 'contact-uuid',
isMe: params.isMe ?? false,
publicKeySet: params.publicKeySet ?? createMockPublicKeySetChain(),
}),
}),
)
}
describe('isSingleton', () => {
it('should be true', () => {
const contact = CreateContact({})
expect(contact.isSingleton).toEqual(true)
})
})
describe('strategyWhenConflictingWithItem', () => {
it('should be KeepBase', () => {
const contact = CreateContact({})
expect(contact.strategyWhenConflictingWithItem({} as unknown as TrustedContactInterface)).toEqual(
ConflictStrategy.KeepBase,
)
})
})
describe('TrustedContact', () => {
describe('getTrustStatusForPublicKey', () => {
it('should be trusted if key set is root', () => {
const contact = CreateContact({ publicKeySet: createMockPublicKeySetChain() })
expect(contact.getTrustStatusForPublicKey('encryption-public-key')).toEqual(PublicKeyTrustStatus.Trusted)
})
it('should be semi-trusted if key set is previous', () => {
const contact = CreateContact({ publicKeySet: createMockPublicKeySetChain() })
expect(contact.getTrustStatusForPublicKey('encryption-public-key-n-1')).toEqual(PublicKeyTrustStatus.Previous)
})
it('should return not trusted if public key is not found', () => {
const contact = CreateContact({ publicKeySet: createMockPublicKeySetChain() })
expect(contact.getTrustStatusForPublicKey('not-found-public-key')).toEqual(PublicKeyTrustStatus.NotTrusted)
})
})
describe('getTrustStatusForSigningPublicKey', () => {
it('should be trusted if key set is root', () => {
const contact = CreateContact({ publicKeySet: createMockPublicKeySetChain() })
expect(contact.getTrustStatusForSigningPublicKey('signing-public-key')).toEqual(PublicKeyTrustStatus.Trusted)
})
it('should be semi-trusted if key set is previous', () => {
const contact = CreateContact({ publicKeySet: createMockPublicKeySetChain() })
expect(contact.getTrustStatusForSigningPublicKey('signing-public-key-n-1')).toEqual(PublicKeyTrustStatus.Previous)
})
it('should return not trusted if public key is not found', () => {
const contact = CreateContact({ publicKeySet: createMockPublicKeySetChain() })
expect(contact.getTrustStatusForSigningPublicKey('not-found-public-key')).toEqual(PublicKeyTrustStatus.NotTrusted)
})
})
})

View File

@@ -1,12 +1,12 @@
import { ConflictStrategy, DecryptedItem, DecryptedItemInterface } from '../../Abstract/Item' import { ConflictStrategy, DecryptedItem, DecryptedItemInterface } from '../../Abstract/Item'
import { DecryptedPayloadInterface } from '../../Abstract/Payload' import { DecryptedPayloadInterface } from '../../Abstract/Payload'
import { HistoryEntryInterface } from '../../Runtime/History' import { HistoryEntryInterface } from '../../Runtime/History'
import { TrustedContactContent } from './TrustedContactContent' import { TrustedContactContent } from './Content/TrustedContactContent'
import { TrustedContactInterface } from './TrustedContactInterface' import { TrustedContactInterface } from './TrustedContactInterface'
import { FindPublicKeySetResult } from './PublicKeySet/FindPublicKeySetResult'
import { ContactPublicKeySet } from './PublicKeySet/ContactPublicKeySet' import { ContactPublicKeySet } from './PublicKeySet/ContactPublicKeySet'
import { ContactPublicKeySetInterface } from './PublicKeySet/ContactPublicKeySetInterface' import { ContactPublicKeySetInterface } from './PublicKeySet/ContactPublicKeySetInterface'
import { Predicate } from '../../Runtime/Predicate/Predicate' import { Predicate } from '../../Runtime/Predicate/Predicate'
import { PublicKeyTrustStatus } from './Types/PublicKeyTrustStatus'
export class TrustedContact extends DecryptedItem<TrustedContactContent> implements TrustedContactInterface { export class TrustedContact extends DecryptedItem<TrustedContactContent> implements TrustedContactInterface {
static singletonPredicate = new Predicate<TrustedContact>('isMe', '=', true) static singletonPredicate = new Predicate<TrustedContact>('isMe', '=', true)
@@ -33,39 +33,36 @@ export class TrustedContact extends DecryptedItem<TrustedContactContent> impleme
return TrustedContact.singletonPredicate return TrustedContact.singletonPredicate
} }
public findKeySet(params: { hasCurrentOrPreviousSigningPublicKey(signingPublicKey: string): boolean {
targetEncryptionPublicKey: string return this.publicKeySet.findKeySetWithSigningKey(signingPublicKey) !== undefined
targetSigningPublicKey: string
}): FindPublicKeySetResult {
const set = this.publicKeySet.findKeySet(params)
if (!set) {
return undefined
}
return {
publicKeySet: set,
current: set === this.publicKeySet,
}
} }
isPublicKeyTrusted(encryptionPublicKey: string): boolean { getTrustStatusForPublicKey(encryptionPublicKey: string): PublicKeyTrustStatus {
const keySet = this.publicKeySet.findKeySetWithPublicKey(encryptionPublicKey) if (this.publicKeySet.encryption === encryptionPublicKey) {
return PublicKeyTrustStatus.Trusted
if (keySet && !keySet.isRevoked) {
return true
} }
return false const previous = this.publicKeySet.findKeySetWithPublicKey(encryptionPublicKey)
if (previous) {
return PublicKeyTrustStatus.Previous
}
return PublicKeyTrustStatus.NotTrusted
} }
isSigningKeyTrusted(signingKey: string): boolean { getTrustStatusForSigningPublicKey(signingPublicKey: string): PublicKeyTrustStatus {
const keySet = this.publicKeySet.findKeySetWithSigningKey(signingKey) if (this.publicKeySet.signing === signingPublicKey) {
return PublicKeyTrustStatus.Trusted
if (keySet && !keySet.isRevoked) {
return true
} }
return false const previous = this.publicKeySet.findKeySetWithSigningKey(signingPublicKey)
if (previous) {
return PublicKeyTrustStatus.Previous
}
return PublicKeyTrustStatus.NotTrusted
} }
override strategyWhenConflictingWithItem( override strategyWhenConflictingWithItem(

View File

@@ -1,11 +0,0 @@
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { ContactPublicKeySetInterface } from './PublicKeySet/ContactPublicKeySetInterface'
export type TrustedContactContentSpecialized = {
name: string
contactUuid: string
publicKeySet: ContactPublicKeySetInterface
isMe: boolean
}
export type TrustedContactContent = TrustedContactContentSpecialized & ItemContent

View File

@@ -1,7 +1,7 @@
import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem' import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem'
import { FindPublicKeySetResult } from './PublicKeySet/FindPublicKeySetResult' import { TrustedContactContent } from './Content/TrustedContactContent'
import { TrustedContactContent } from './TrustedContactContent'
import { ContactPublicKeySetInterface } from './PublicKeySet/ContactPublicKeySetInterface' import { ContactPublicKeySetInterface } from './PublicKeySet/ContactPublicKeySetInterface'
import { PublicKeyTrustStatus } from './Types/PublicKeyTrustStatus'
export interface TrustedContactInterface extends DecryptedItemInterface<TrustedContactContent> { export interface TrustedContactInterface extends DecryptedItemInterface<TrustedContactContent> {
name: string name: string
@@ -9,8 +9,7 @@ export interface TrustedContactInterface extends DecryptedItemInterface<TrustedC
publicKeySet: ContactPublicKeySetInterface publicKeySet: ContactPublicKeySetInterface
isMe: boolean isMe: boolean
findKeySet(params: { targetEncryptionPublicKey: string; targetSigningPublicKey: string }): FindPublicKeySetResult getTrustStatusForPublicKey(encryptionPublicKey: string): PublicKeyTrustStatus
getTrustStatusForSigningPublicKey(signingPublicKey: string): PublicKeyTrustStatus
isPublicKeyTrusted(encryptionPublicKey: string): boolean hasCurrentOrPreviousSigningPublicKey(signingPublicKey: string): boolean
isSigningKeyTrusted(signingKey: string): boolean
} }

View File

@@ -1,26 +0,0 @@
import { DecryptedItemMutator } from '../../Abstract/Item'
import { TrustedContactContent } from './TrustedContactContent'
import { TrustedContactInterface } from './TrustedContactInterface'
import { ContactPublicKeySet } from './PublicKeySet/ContactPublicKeySet'
export class TrustedContactMutator extends DecryptedItemMutator<TrustedContactContent, TrustedContactInterface> {
set name(newName: string) {
this.mutableContent.name = newName
}
addPublicKey(params: { encryption: string; signing: string }): void {
const newKey = new ContactPublicKeySet(
params.encryption,
params.signing,
new Date(),
false,
this.immutableItem.publicKeySet,
)
this.mutableContent.publicKeySet = newKey
}
replacePublicKeySet(publicKeySet: ContactPublicKeySet): void {
this.mutableContent.publicKeySet = publicKeySet
}
}

View File

@@ -1,4 +1,4 @@
export type PublicKeySet = { export type PortablePublicKeySet = {
encryption: string encryption: string
signing: string signing: string
} }

View File

@@ -0,0 +1,5 @@
export enum PublicKeyTrustStatus {
Trusted = 'Trusted',
Previous = 'Previous',
NotTrusted = 'NotTrusted',
}

View File

@@ -27,7 +27,7 @@ import { EncryptedItemInterface } from '../../Abstract/Item/Interfaces/Encrypted
import { DeletedItemInterface } from '../../Abstract/Item/Interfaces/DeletedItem' import { DeletedItemInterface } from '../../Abstract/Item/Interfaces/DeletedItem'
import { SmartViewMutator } from '../../Syncable/SmartView' import { SmartViewMutator } from '../../Syncable/SmartView'
import { TrustedContact } from '../../Syncable/TrustedContact/TrustedContact' import { TrustedContact } from '../../Syncable/TrustedContact/TrustedContact'
import { TrustedContactMutator } from '../../Syncable/TrustedContact/TrustedContactMutator' import { TrustedContactMutator } from '../../Syncable/TrustedContact/Mutator/TrustedContactMutator'
import { KeySystemRootKey } from '../../Syncable/KeySystemRootKey/KeySystemRootKey' import { KeySystemRootKey } from '../../Syncable/KeySystemRootKey/KeySystemRootKey'
import { KeySystemRootKeyMutator } from '../../Syncable/KeySystemRootKey/KeySystemRootKeyMutator' import { KeySystemRootKeyMutator } from '../../Syncable/KeySystemRootKey/KeySystemRootKeyMutator'
import { VaultListing } from '../../Syncable/VaultListing/VaultListing' import { VaultListing } from '../../Syncable/VaultListing/VaultListing'

View File

@@ -97,11 +97,13 @@ export * from './Syncable/Theme'
export * from './Syncable/UserPrefs' export * from './Syncable/UserPrefs'
export * from './Syncable/TrustedContact/TrustedContact' export * from './Syncable/TrustedContact/TrustedContact'
export * from './Syncable/TrustedContact/TrustedContactMutator' export * from './Syncable/TrustedContact/Mutator/TrustedContactMutator'
export * from './Syncable/TrustedContact/TrustedContactContent' export * from './Syncable/TrustedContact/Content/TrustedContactContent'
export * from './Syncable/TrustedContact/TrustedContactInterface' export * from './Syncable/TrustedContact/TrustedContactInterface'
export * from './Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetInterface' export * from './Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetInterface'
export * from './Syncable/TrustedContact/PublicKeySet/ContactPublicKeySet' export * from './Syncable/TrustedContact/PublicKeySet/ContactPublicKeySet'
export * from './Syncable/TrustedContact/Types/PortablePublicKeySet'
export * from './Syncable/TrustedContact/Types/PublicKeyTrustStatus'
export * from './Syncable/KeySystemRootKey/KeySystemRootKey' export * from './Syncable/KeySystemRootKey/KeySystemRootKey'
export * from './Syncable/KeySystemRootKey/KeySystemRootKeyMutator' export * from './Syncable/KeySystemRootKey/KeySystemRootKeyMutator'

View File

@@ -2,6 +2,7 @@ export interface AsymmetricMessageServerHash {
uuid: string uuid: string
user_uuid: string user_uuid: string
sender_uuid: string sender_uuid: string
replaceabilityIdentifier?: string
encrypted_message: string encrypted_message: string
created_at_timestamp: number created_at_timestamp: number
updated_at_timestamp: number updated_at_timestamp: number

View File

@@ -6,7 +6,9 @@ import { SNFeatureRepo } from '@standardnotes/models'
import { ClientDisplayableError, HttpResponse } from '@standardnotes/responses' import { ClientDisplayableError, HttpResponse } from '@standardnotes/responses'
import { AnyFeatureDescription } from '@standardnotes/features' import { AnyFeatureDescription } from '@standardnotes/features'
export interface ApiServiceInterface extends AbstractService<ApiServiceEvent, ApiServiceEventData>, FilesApiInterface { export interface LegacyApiServiceInterface
extends AbstractService<ApiServiceEvent, ApiServiceEventData>,
FilesApiInterface {
isThirdPartyHostUsed(): boolean isThirdPartyHostUsed(): boolean
downloadOfflineFeaturesFromRepo( downloadOfflineFeaturesFromRepo(

View File

@@ -1,3 +1,5 @@
import { HistoryServiceInterface } from './../History/HistoryServiceInterface'
import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface'
import { PreferenceServiceInterface } from './../Preferences/PreferenceServiceInterface' import { PreferenceServiceInterface } from './../Preferences/PreferenceServiceInterface'
import { AsymmetricMessageServiceInterface } from './../AsymmetricMessage/AsymmetricMessageServiceInterface' import { AsymmetricMessageServiceInterface } from './../AsymmetricMessage/AsymmetricMessageServiceInterface'
import { SyncOptions } from './../Sync/SyncOptions' import { SyncOptions } from './../Sync/SyncOptions'
@@ -34,6 +36,7 @@ import { UserClientInterface } from '../User/UserClientInterface'
import { SessionsClientInterface } from '../Session/SessionsClientInterface' import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { HomeServerServiceInterface } from '../HomeServer/HomeServerServiceInterface' import { HomeServerServiceInterface } from '../HomeServer/HomeServerServiceInterface'
import { User } from '@standardnotes/responses' import { User } from '@standardnotes/responses'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
export interface ApplicationInterface { export interface ApplicationInterface {
deinit(mode: DeinitMode, source: DeinitSource): void deinit(mode: DeinitMode, source: DeinitSource): void
@@ -107,9 +110,11 @@ export interface ApplicationInterface {
get alerts(): AlertService get alerts(): AlertService
get asymmetric(): AsymmetricMessageServiceInterface get asymmetric(): AsymmetricMessageServiceInterface
get preferences(): PreferenceServiceInterface get preferences(): PreferenceServiceInterface
get events(): InternalEventBusInterface
get history(): HistoryServiceInterface
get encryption(): EncryptionProviderInterface
readonly identifier: ApplicationIdentifier readonly identifier: ApplicationIdentifier
readonly platform: Platform readonly platform: Platform
deviceInterface: DeviceInterface device: DeviceInterface
alertService: AlertService
} }

View File

@@ -1,31 +1,121 @@
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
import { GetUntrustedPayload } from './UseCase/GetUntrustedPayload'
import { GetInboundMessages } from './UseCase/GetInboundMessages'
import { GetOutboundMessages } from './UseCase/GetOutboundMessages'
import { SendOwnContactChangeMessage } from './UseCase/SendOwnContactChangeMessage'
import { HandleRootKeyChangedMessage } from './UseCase/HandleRootKeyChangedMessage'
import { GetVault } from './../Vaults/UseCase/GetVault'
import { GetTrustedPayload } from './UseCase/GetTrustedPayload'
import { ReplaceContactData } from './../Contacts/UseCase/ReplaceContactData'
import { GetAllContacts } from './../Contacts/UseCase/GetAllContacts'
import { FindContact } from './../Contacts/UseCase/FindContact'
import { CreateOrEditContact } from './../Contacts/UseCase/CreateOrEditContact'
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { HttpServiceInterface } from '@standardnotes/api' import { AsymmetricMessageServer } from '@standardnotes/api'
import { AsymmetricMessageService } from './AsymmetricMessageService' import { AsymmetricMessageService } from './AsymmetricMessageService'
import { ContactServiceInterface } from './../Contacts/ContactServiceInterface'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface' import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { AsymmetricMessageServerHash } from '@standardnotes/responses' import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessagePayloadType } from '@standardnotes/models' import {
AsymmetricMessagePayloadType,
AsymmetricMessageSenderKeypairChanged,
AsymmetricMessageSharedVaultInvite,
AsymmetricMessageSharedVaultMetadataChanged,
AsymmetricMessageSharedVaultRootKeyChanged,
AsymmetricMessageTrustedContactShare,
KeySystemRootKeyContentSpecialized,
TrustedContactInterface,
} from '@standardnotes/models'
describe('AsymmetricMessageService', () => { describe('AsymmetricMessageService', () => {
let sync: jest.Mocked<SyncServiceInterface>
let mutator: jest.Mocked<MutatorClientInterface>
let encryption: jest.Mocked<EncryptionProviderInterface>
let service: AsymmetricMessageService let service: AsymmetricMessageService
beforeEach(() => { beforeEach(() => {
const http = {} as jest.Mocked<HttpServiceInterface> const messageServer = {} as jest.Mocked<AsymmetricMessageServer>
http.delete = jest.fn() messageServer.deleteMessage = jest.fn()
const encryption = {} as jest.Mocked<EncryptionProviderInterface> encryption = {} as jest.Mocked<EncryptionProviderInterface>
const contacts = {} as jest.Mocked<ContactServiceInterface> const createOrEditContact = {} as jest.Mocked<CreateOrEditContact>
const items = {} as jest.Mocked<ItemManagerInterface> const findContact = {} as jest.Mocked<FindContact>
const sync = {} as jest.Mocked<SyncServiceInterface> const getAllContacts = {} as jest.Mocked<GetAllContacts>
const mutator = {} as jest.Mocked<MutatorClientInterface> const replaceContactData = {} as jest.Mocked<ReplaceContactData>
const getTrustedPayload = {} as jest.Mocked<GetTrustedPayload>
const getVault = {} as jest.Mocked<GetVault>
const handleRootKeyChangedMessage = {} as jest.Mocked<HandleRootKeyChangedMessage>
const sendOwnContactChangedMessage = {} as jest.Mocked<SendOwnContactChangeMessage>
const getOutboundMessagesUseCase = {} as jest.Mocked<GetOutboundMessages>
const getInboundMessagesUseCase = {} as jest.Mocked<GetInboundMessages>
const getUntrustedPayload = {} as jest.Mocked<GetUntrustedPayload>
sync = {} as jest.Mocked<SyncServiceInterface>
sync.sync = jest.fn()
mutator = {} as jest.Mocked<MutatorClientInterface>
mutator.changeItem = jest.fn()
const eventBus = {} as jest.Mocked<InternalEventBusInterface> const eventBus = {} as jest.Mocked<InternalEventBusInterface>
eventBus.addEventHandler = jest.fn() eventBus.addEventHandler = jest.fn()
service = new AsymmetricMessageService(http, encryption, contacts, items, mutator, sync, eventBus) service = new AsymmetricMessageService(
messageServer,
encryption,
mutator,
createOrEditContact,
findContact,
getAllContacts,
replaceContactData,
getTrustedPayload,
getVault,
handleRootKeyChangedMessage,
sendOwnContactChangedMessage,
getOutboundMessagesUseCase,
getInboundMessagesUseCase,
getUntrustedPayload,
eventBus,
)
})
describe('sortServerMessages', () => {
it('should prioritize keypair changed messages over other messages', () => {
const messages: AsymmetricMessageServerHash[] = [
{
uuid: 'keypair-changed-message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
},
{
uuid: 'misc-message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 1,
updated_at_timestamp: 1,
},
]
service.getUntrustedMessagePayload = jest.fn()
service.getServerMessageType = jest.fn().mockImplementation((message) => {
if (message.uuid === 'keypair-changed-message') {
return AsymmetricMessagePayloadType.SenderKeypairChanged
} else {
return AsymmetricMessagePayloadType.ContactShare
}
})
const sorted = service.sortServerMessages(messages)
expect(sorted[0].uuid).toEqual('keypair-changed-message')
expect(sorted[1].uuid).toEqual('misc-message')
const reverseSorted = service.sortServerMessages(messages.reverse())
expect(reverseSorted[0].uuid).toEqual('keypair-changed-message')
expect(reverseSorted[1].uuid).toEqual('misc-message')
})
}) })
it('should process incoming messages oldest first', async () => { it('should process incoming messages oldest first', async () => {
@@ -50,7 +140,9 @@ describe('AsymmetricMessageService', () => {
const trustedPayloadMock = { type: AsymmetricMessagePayloadType.ContactShare, data: { recipientUuid: '1' } } const trustedPayloadMock = { type: AsymmetricMessagePayloadType.ContactShare, data: { recipientUuid: '1' } }
service.getTrustedMessagePayload = jest.fn().mockReturnValue(trustedPayloadMock) service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(trustedPayloadMock)
const handleTrustedContactShareMessageMock = jest.fn() const handleTrustedContactShareMessageMock = jest.fn()
service.handleTrustedContactShareMessage = handleTrustedContactShareMessageMock service.handleTrustedContactShareMessage = handleTrustedContactShareMessageMock
@@ -60,4 +152,174 @@ describe('AsymmetricMessageService', () => {
expect(handleTrustedContactShareMessageMock.mock.calls[0][0]).toEqual(messages[1]) expect(handleTrustedContactShareMessageMock.mock.calls[0][0]).toEqual(messages[1])
expect(handleTrustedContactShareMessageMock.mock.calls[1][0]).toEqual(messages[0]) expect(handleTrustedContactShareMessageMock.mock.calls[1][0]).toEqual(messages[0])
}) })
it('should handle ContactShare message', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
}
const decryptedMessagePayload: AsymmetricMessageTrustedContactShare = {
type: AsymmetricMessagePayloadType.ContactShare,
data: {
recipientUuid: '1',
trustedContact: {} as TrustedContactInterface,
},
}
service.handleTrustedContactShareMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(decryptedMessagePayload)
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.handleTrustedContactShareMessage).toHaveBeenCalledWith(message, decryptedMessagePayload)
})
it('should handle SenderKeypairChanged message', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
}
const decryptedMessagePayload: AsymmetricMessageSenderKeypairChanged = {
type: AsymmetricMessagePayloadType.SenderKeypairChanged,
data: {
recipientUuid: '1',
newEncryptionPublicKey: 'new-encryption-public-key',
newSigningPublicKey: 'new-signing-public-key',
},
}
service.handleTrustedSenderKeypairChangedMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(decryptedMessagePayload)
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.handleTrustedSenderKeypairChangedMessage).toHaveBeenCalledWith(message, decryptedMessagePayload)
})
it('should handle SharedVaultRootKeyChanged message', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
}
const decryptedMessagePayload: AsymmetricMessageSharedVaultRootKeyChanged = {
type: AsymmetricMessagePayloadType.SharedVaultRootKeyChanged,
data: {
recipientUuid: '1',
rootKey: {} as KeySystemRootKeyContentSpecialized,
},
}
service.handleTrustedSharedVaultRootKeyChangedMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(decryptedMessagePayload)
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.handleTrustedSharedVaultRootKeyChangedMessage).toHaveBeenCalledWith(message, decryptedMessagePayload)
})
it('should handle SharedVaultMetadataChanged message', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
}
const decryptedMessagePayload: AsymmetricMessageSharedVaultMetadataChanged = {
type: AsymmetricMessagePayloadType.SharedVaultMetadataChanged,
data: {
recipientUuid: '1',
sharedVaultUuid: 'shared-vault-uuid',
name: 'Vault name',
description: 'Vault description',
},
}
service.handleTrustedVaultMetadataChangedMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(decryptedMessagePayload)
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.handleTrustedVaultMetadataChangedMessage).toHaveBeenCalledWith(message, decryptedMessagePayload)
})
it('should throw if message type is SharedVaultInvite', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
}
const decryptedMessagePayload: AsymmetricMessageSharedVaultInvite = {
type: AsymmetricMessagePayloadType.SharedVaultInvite,
data: {
recipientUuid: '1',
},
} as AsymmetricMessageSharedVaultInvite
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(decryptedMessagePayload)
await expect(service.handleRemoteReceivedAsymmetricMessages([message])).rejects.toThrow(
'Shared vault invites payloads are not handled as part of asymmetric messages',
)
})
it('should delete message from server after processing', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
}
const decryptedMessagePayload: AsymmetricMessageTrustedContactShare = {
type: AsymmetricMessagePayloadType.ContactShare,
data: {
recipientUuid: '1',
trustedContact: {} as TrustedContactInterface,
},
}
service.deleteMessageAfterProcessing = jest.fn()
service.handleTrustedContactShareMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(decryptedMessagePayload)
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.deleteMessageAfterProcessing).toHaveBeenCalled()
})
}) })

View File

@@ -1,13 +1,11 @@
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { ContactServiceInterface } from './../Contacts/ContactServiceInterface'
import { AsymmetricMessageServerHash, ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses' import { AsymmetricMessageServerHash, ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses'
import { SyncEvent, SyncEventReceivedAsymmetricMessagesData } from '../Event/SyncEvent' import { SyncEvent, SyncEventReceivedAsymmetricMessagesData } from '../Event/SyncEvent'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface' import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
import { InternalEventInterface } from '../Internal/InternalEventInterface' import { InternalEventInterface } from '../Internal/InternalEventInterface'
import { AbstractService } from '../Service/AbstractService' import { AbstractService } from '../Service/AbstractService'
import { GetAsymmetricMessageTrustedPayload } from './UseCase/GetAsymmetricMessageTrustedPayload' import { GetTrustedPayload } from './UseCase/GetTrustedPayload'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { import {
AsymmetricMessageSharedVaultRootKeyChanged, AsymmetricMessageSharedVaultRootKeyChanged,
AsymmetricMessagePayloadType, AsymmetricMessagePayloadType,
@@ -16,61 +14,68 @@ import {
AsymmetricMessagePayload, AsymmetricMessagePayload,
AsymmetricMessageSharedVaultMetadataChanged, AsymmetricMessageSharedVaultMetadataChanged,
VaultListingMutator, VaultListingMutator,
MutationType,
PayloadEmitSource,
VaultListingInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { HandleTrustedSharedVaultRootKeyChangedMessage } from './UseCase/HandleTrustedSharedVaultRootKeyChangedMessage' import { HandleRootKeyChangedMessage } from './UseCase/HandleRootKeyChangedMessage'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { SessionEvent } from '../Session/SessionEvent' import { SessionEvent } from '../Session/SessionEvent'
import { AsymmetricMessageServer, HttpServiceInterface } from '@standardnotes/api' import { AsymmetricMessageServer } from '@standardnotes/api'
import { UserKeyPairChangedEventData } from '../Session/UserKeyPairChangedEventData' import { UserKeyPairChangedEventData } from '../Session/UserKeyPairChangedEventData'
import { SendOwnContactChangeMessage } from './UseCase/SendOwnContactChangeMessage' import { SendOwnContactChangeMessage } from './UseCase/SendOwnContactChangeMessage'
import { GetOutboundAsymmetricMessages } from './UseCase/GetOutboundAsymmetricMessages' import { GetOutboundMessages } from './UseCase/GetOutboundMessages'
import { GetInboundAsymmetricMessages } from './UseCase/GetInboundAsymmetricMessages' import { GetInboundMessages } from './UseCase/GetInboundMessages'
import { GetVaultUseCase } from '../Vaults/UseCase/GetVault' import { GetVault } from '../Vaults/UseCase/GetVault'
import { AsymmetricMessageServiceInterface } from './AsymmetricMessageServiceInterface' import { AsymmetricMessageServiceInterface } from './AsymmetricMessageServiceInterface'
import { GetUntrustedPayload } from './UseCase/GetUntrustedPayload'
import { FindContact } from '../Contacts/UseCase/FindContact'
import { CreateOrEditContact } from '../Contacts/UseCase/CreateOrEditContact'
import { ReplaceContactData } from '../Contacts/UseCase/ReplaceContactData'
import { GetAllContacts } from '../Contacts/UseCase/GetAllContacts'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
export class AsymmetricMessageService export class AsymmetricMessageService
extends AbstractService extends AbstractService
implements AsymmetricMessageServiceInterface, InternalEventHandlerInterface implements AsymmetricMessageServiceInterface, InternalEventHandlerInterface
{ {
private messageServer: AsymmetricMessageServer
constructor( constructor(
http: HttpServiceInterface, private messageServer: AsymmetricMessageServer,
private encryption: EncryptionProviderInterface, private encryption: EncryptionProviderInterface,
private contacts: ContactServiceInterface,
private items: ItemManagerInterface,
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private sync: SyncServiceInterface, private _createOrEditContact: CreateOrEditContact,
private _findContact: FindContact,
private _getAllContacts: GetAllContacts,
private _replaceContactData: ReplaceContactData,
private _getTrustedPayload: GetTrustedPayload,
private _getVault: GetVault,
private _handleRootKeyChangedMessage: HandleRootKeyChangedMessage,
private _sendOwnContactChangedMessage: SendOwnContactChangeMessage,
private _getOutboundMessagesUseCase: GetOutboundMessages,
private _getInboundMessagesUseCase: GetInboundMessages,
private _getUntrustedPayload: GetUntrustedPayload,
eventBus: InternalEventBusInterface, eventBus: InternalEventBusInterface,
) { ) {
super(eventBus) super(eventBus)
this.messageServer = new AsymmetricMessageServer(http)
eventBus.addEventHandler(this, SyncEvent.ReceivedAsymmetricMessages)
eventBus.addEventHandler(this, SessionEvent.UserKeyPairChanged)
} }
async handleEvent(event: InternalEventInterface): Promise<void> { async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === SessionEvent.UserKeyPairChanged) { switch (event.type) {
void this.messageServer.deleteAllInboundMessages() case SessionEvent.UserKeyPairChanged:
void this.sendOwnContactChangeEventToAllContacts(event.payload as UserKeyPairChangedEventData) void this.messageServer.deleteAllInboundMessages()
} void this.sendOwnContactChangeEventToAllContacts(event.payload as UserKeyPairChangedEventData)
break
if (event.type === SyncEvent.ReceivedAsymmetricMessages) { case SyncEvent.ReceivedAsymmetricMessages:
void this.handleRemoteReceivedAsymmetricMessages(event.payload as SyncEventReceivedAsymmetricMessagesData) void this.handleRemoteReceivedAsymmetricMessages(event.payload as SyncEventReceivedAsymmetricMessagesData)
break
} }
} }
public async getOutboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> { public async getOutboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {
const usecase = new GetOutboundAsymmetricMessages(this.messageServer) return this._getOutboundMessagesUseCase.execute()
return usecase.execute()
} }
public async getInboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> { public async getInboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {
const usecase = new GetInboundAsymmetricMessages(this.messageServer) return this._getInboundMessagesUseCase.execute()
return usecase.execute()
} }
public async downloadAndProcessInboundMessages(): Promise<void> { public async downloadAndProcessInboundMessages(): Promise<void> {
@@ -83,118 +88,223 @@ export class AsymmetricMessageService
} }
private async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise<void> { private async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise<void> {
if (!data.oldKeyPair || !data.oldSigningKeyPair) { if (!data.previous) {
return return
} }
const useCase = new SendOwnContactChangeMessage(this.encryption, this.messageServer) const contacts = this._getAllContacts.execute()
if (contacts.isFailed()) {
return
}
const contacts = this.contacts.getAllContacts() for (const contact of contacts.getValue()) {
for (const contact of contacts) {
if (contact.isMe) { if (contact.isMe) {
continue continue
} }
await useCase.execute({ await this._sendOwnContactChangedMessage.execute({
senderOldKeyPair: data.oldKeyPair, senderOldKeyPair: data.previous.encryption,
senderOldSigningKeyPair: data.oldSigningKeyPair, senderOldSigningKeyPair: data.previous.signing,
senderNewKeyPair: data.newKeyPair, senderNewKeyPair: data.current.encryption,
senderNewSigningKeyPair: data.newSigningKeyPair, senderNewSigningKeyPair: data.current.signing,
contact, contact,
}) })
} }
} }
sortServerMessages(messages: AsymmetricMessageServerHash[]): AsymmetricMessageServerHash[] {
const SortedPriorityTypes = [AsymmetricMessagePayloadType.SenderKeypairChanged]
const priority: AsymmetricMessageServerHash[] = []
const regular: AsymmetricMessageServerHash[] = []
const allMessagesOldestFirst = messages.slice().sort((a, b) => a.created_at_timestamp - b.created_at_timestamp)
const messageTypeMap: Record<string, AsymmetricMessagePayloadType> = {}
for (const message of allMessagesOldestFirst) {
const messageType = this.getServerMessageType(message)
if (!messageType) {
continue
}
messageTypeMap[message.uuid] = messageType
if (SortedPriorityTypes.includes(messageType)) {
priority.push(message)
} else {
regular.push(message)
}
}
const sortedPriority = priority.sort((a, b) => {
const typeA = messageTypeMap[a.uuid]
const typeB = messageTypeMap[b.uuid]
if (typeA !== typeB) {
return SortedPriorityTypes.indexOf(typeA) - SortedPriorityTypes.indexOf(typeB)
}
return a.created_at_timestamp - b.created_at_timestamp
})
const regularMessagesOldestFirst = regular.sort((a, b) => a.created_at_timestamp - b.created_at_timestamp)
return [...sortedPriority, ...regularMessagesOldestFirst]
}
getServerMessageType(message: AsymmetricMessageServerHash): AsymmetricMessagePayloadType | undefined {
const result = this.getUntrustedMessagePayload(message)
if (!result) {
return undefined
}
return result.type
}
async handleRemoteReceivedAsymmetricMessages(messages: AsymmetricMessageServerHash[]): Promise<void> { async handleRemoteReceivedAsymmetricMessages(messages: AsymmetricMessageServerHash[]): Promise<void> {
if (messages.length === 0) { if (messages.length === 0) {
return return
} }
const sortedMessages = messages.slice().sort((a, b) => a.created_at_timestamp - b.created_at_timestamp) const sortedMessages = this.sortServerMessages(messages)
for (const message of sortedMessages) { for (const message of sortedMessages) {
const trustedMessagePayload = this.getTrustedMessagePayload(message) const trustedPayload = this.getTrustedMessagePayload(message)
if (!trustedMessagePayload) { if (!trustedPayload) {
continue continue
} }
if (trustedMessagePayload.data.recipientUuid !== message.user_uuid) { await this.handleTrustedMessageResult(message, trustedPayload)
continue
}
if (trustedMessagePayload.type === AsymmetricMessagePayloadType.ContactShare) {
await this.handleTrustedContactShareMessage(message, trustedMessagePayload)
} else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SenderKeypairChanged) {
await this.handleTrustedSenderKeypairChangedMessage(message, trustedMessagePayload)
} else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SharedVaultRootKeyChanged) {
await this.handleTrustedSharedVaultRootKeyChangedMessage(message, trustedMessagePayload)
} else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SharedVaultMetadataChanged) {
await this.handleVaultMetadataChangedMessage(message, trustedMessagePayload)
} else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SharedVaultInvite) {
throw new Error('Shared vault invites payloads are not handled as part of asymmetric messages')
}
await this.deleteMessageAfterProcessing(message)
} }
} }
getTrustedMessagePayload(message: AsymmetricMessageServerHash): AsymmetricMessagePayload | undefined { private async handleTrustedMessageResult(
const useCase = new GetAsymmetricMessageTrustedPayload(this.encryption, this.contacts) message: AsymmetricMessageServerHash,
payload: AsymmetricMessagePayload,
return useCase.execute({
privateKey: this.encryption.getKeyPair().privateKey,
message,
})
}
private async deleteMessageAfterProcessing(message: AsymmetricMessageServerHash): Promise<void> {
await this.messageServer.deleteMessage({ messageUuid: message.uuid })
}
async handleVaultMetadataChangedMessage(
_message: AsymmetricMessageServerHash,
trustedPayload: AsymmetricMessageSharedVaultMetadataChanged,
): Promise<void> { ): Promise<void> {
const vault = new GetVaultUseCase(this.items).execute({ sharedVaultUuid: trustedPayload.data.sharedVaultUuid }) if (payload.data.recipientUuid !== message.user_uuid) {
if (!vault) {
return return
} }
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => { if (payload.type === AsymmetricMessagePayloadType.ContactShare) {
mutator.name = trustedPayload.data.name await this.handleTrustedContactShareMessage(message, payload)
mutator.description = trustedPayload.data.description } else if (payload.type === AsymmetricMessagePayloadType.SenderKeypairChanged) {
await this.handleTrustedSenderKeypairChangedMessage(message, payload)
} else if (payload.type === AsymmetricMessagePayloadType.SharedVaultRootKeyChanged) {
await this.handleTrustedSharedVaultRootKeyChangedMessage(message, payload)
} else if (payload.type === AsymmetricMessagePayloadType.SharedVaultMetadataChanged) {
await this.handleTrustedVaultMetadataChangedMessage(message, payload)
} else if (payload.type === AsymmetricMessagePayloadType.SharedVaultInvite) {
throw new Error('Shared vault invites payloads are not handled as part of asymmetric messages')
}
await this.deleteMessageAfterProcessing(message)
}
getUntrustedMessagePayload(message: AsymmetricMessageServerHash): AsymmetricMessagePayload | undefined {
const result = this._getUntrustedPayload.execute({
privateKey: this.encryption.getKeyPair().privateKey,
message,
}) })
if (result.isFailed()) {
return undefined
}
return result.getValue()
}
getTrustedMessagePayload(message: AsymmetricMessageServerHash): AsymmetricMessagePayload | undefined {
const contact = this._findContact.execute({ userUuid: message.sender_uuid })
if (contact.isFailed()) {
return undefined
}
const result = this._getTrustedPayload.execute({
privateKey: this.encryption.getKeyPair().privateKey,
sender: contact.getValue(),
message,
})
if (result.isFailed()) {
return undefined
}
return result.getValue()
}
async deleteMessageAfterProcessing(message: AsymmetricMessageServerHash): Promise<void> {
await this.messageServer.deleteMessage({ messageUuid: message.uuid })
}
async handleTrustedVaultMetadataChangedMessage(
_message: AsymmetricMessageServerHash,
trustedPayload: AsymmetricMessageSharedVaultMetadataChanged,
): Promise<void> {
const vault = this._getVault.execute<VaultListingInterface>({
sharedVaultUuid: trustedPayload.data.sharedVaultUuid,
})
if (vault.isFailed()) {
return
}
await this.mutator.changeItem<VaultListingMutator>(
vault.getValue(),
(mutator) => {
mutator.name = trustedPayload.data.name
mutator.description = trustedPayload.data.description
},
MutationType.UpdateUserTimestamps,
PayloadEmitSource.RemoteRetrieved,
)
} }
async handleTrustedContactShareMessage( async handleTrustedContactShareMessage(
_message: AsymmetricMessageServerHash, _message: AsymmetricMessageServerHash,
trustedPayload: AsymmetricMessageTrustedContactShare, trustedPayload: AsymmetricMessageTrustedContactShare,
): Promise<void> { ): Promise<void> {
await this.contacts.createOrUpdateTrustedContactFromContactShare(trustedPayload.data.trustedContact) if (trustedPayload.data.trustedContact.isMe) {
return
}
await this._replaceContactData.execute(trustedPayload.data.trustedContact)
} }
private async handleTrustedSenderKeypairChangedMessage( async handleTrustedSenderKeypairChangedMessage(
message: AsymmetricMessageServerHash, message: AsymmetricMessageServerHash,
trustedPayload: AsymmetricMessageSenderKeypairChanged, trustedPayload: AsymmetricMessageSenderKeypairChanged,
): Promise<void> { ): Promise<void> {
await this.contacts.createOrEditTrustedContact({ await this._createOrEditContact.execute({
contactUuid: message.sender_uuid, contactUuid: message.sender_uuid,
publicKey: trustedPayload.data.newEncryptionPublicKey, publicKey: trustedPayload.data.newEncryptionPublicKey,
signingPublicKey: trustedPayload.data.newSigningPublicKey, signingPublicKey: trustedPayload.data.newSigningPublicKey,
}) })
} }
private async handleTrustedSharedVaultRootKeyChangedMessage( async handleTrustedSharedVaultRootKeyChangedMessage(
_message: AsymmetricMessageServerHash, _message: AsymmetricMessageServerHash,
trustedPayload: AsymmetricMessageSharedVaultRootKeyChanged, trustedPayload: AsymmetricMessageSharedVaultRootKeyChanged,
): Promise<void> { ): Promise<void> {
const useCase = new HandleTrustedSharedVaultRootKeyChangedMessage( await this._handleRootKeyChangedMessage.execute(trustedPayload)
this.mutator, }
this.items,
this.sync, public override deinit(): void {
this.encryption, super.deinit()
) ;(this.messageServer as unknown) = undefined
await useCase.execute(trustedPayload) ;(this.encryption as unknown) = undefined
;(this.mutator as unknown) = undefined
;(this._createOrEditContact as unknown) = undefined
;(this._findContact as unknown) = undefined
;(this._getAllContacts as unknown) = undefined
;(this._replaceContactData as unknown) = undefined
;(this._getTrustedPayload as unknown) = undefined
;(this._getVault as unknown) = undefined
;(this._handleRootKeyChangedMessage as unknown) = undefined
;(this._sendOwnContactChangedMessage as unknown) = undefined
;(this._getOutboundMessagesUseCase as unknown) = undefined
;(this._getInboundMessagesUseCase as unknown) = undefined
;(this._getUntrustedPayload as unknown) = undefined
} }
} }

View File

@@ -1,23 +0,0 @@
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface'
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessagePayload } from '@standardnotes/models'
export class GetAsymmetricMessageTrustedPayload<M extends AsymmetricMessagePayload> {
constructor(private encryption: EncryptionProviderInterface, private contacts: ContactServiceInterface) {}
execute(dto: { privateKey: string; message: AsymmetricMessageServerHash }): M | undefined {
const trustedContact = this.contacts.findTrustedContact(dto.message.sender_uuid)
if (!trustedContact) {
return undefined
}
const decryptionResult = this.encryption.asymmetricallyDecryptMessage<M>({
encryptedString: dto.message.encrypted_message,
trustedSender: trustedContact,
privateKey: dto.privateKey,
})
return decryptionResult
}
}

View File

@@ -1,17 +0,0 @@
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessagePayload } from '@standardnotes/models'
export class GetAsymmetricMessageUntrustedPayload<M extends AsymmetricMessagePayload> {
constructor(private encryption: EncryptionProviderInterface) {}
execute(dto: { privateKey: string; message: AsymmetricMessageServerHash }): M | undefined {
const decryptionResult = this.encryption.asymmetricallyDecryptMessage<M>({
encryptedString: dto.message.encrypted_message,
trustedSender: undefined,
privateKey: dto.privateKey,
})
return decryptionResult
}
}

View File

@@ -1,7 +1,7 @@
import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses' import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessageServerInterface } from '@standardnotes/api' import { AsymmetricMessageServerInterface } from '@standardnotes/api'
export class GetInboundAsymmetricMessages { export class GetInboundMessages {
constructor(private messageServer: AsymmetricMessageServerInterface) {} constructor(private messageServer: AsymmetricMessageServerInterface) {}
async execute(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> { async execute(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {

View File

@@ -1,7 +1,7 @@
import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses' import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessageServerInterface } from '@standardnotes/api' import { AsymmetricMessageServerInterface } from '@standardnotes/api'
export class GetOutboundAsymmetricMessages { export class GetOutboundMessages {
constructor(private messageServer: AsymmetricMessageServerInterface) {} constructor(private messageServer: AsymmetricMessageServerInterface) {}
async execute(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> { async execute(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {

View File

@@ -0,0 +1,17 @@
import { AsymmetricMessagePayloadType } from '@standardnotes/models'
const TypesUsingReplaceableIdentifiers = [
AsymmetricMessagePayloadType.SharedVaultRootKeyChanged,
AsymmetricMessagePayloadType.SharedVaultMetadataChanged,
]
export function GetReplaceabilityIdentifier(
type: AsymmetricMessagePayloadType,
sharedVaultUuid: string,
keySystemIdentifier: string,
): string | undefined {
if (!TypesUsingReplaceableIdentifiers.includes(type)) {
return undefined
}
return [type, sharedVaultUuid, keySystemIdentifier].join(':')
}

View File

@@ -0,0 +1,22 @@
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessagePayload, TrustedContactInterface } from '@standardnotes/models'
import { DecryptMessage } from '../../Encryption/UseCase/Asymmetric/DecryptMessage'
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
export class GetTrustedPayload implements SyncUseCaseInterface<AsymmetricMessagePayload> {
constructor(private decryptMessage: DecryptMessage) {}
execute<M extends AsymmetricMessagePayload>(dto: {
privateKey: string
message: AsymmetricMessageServerHash
sender: TrustedContactInterface
}): Result<M> {
const result = this.decryptMessage.execute<M>({
message: dto.message.encrypted_message,
sender: dto.sender,
privateKey: dto.privateKey,
})
return result
}
}

View File

@@ -0,0 +1,21 @@
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessagePayload } from '@standardnotes/models'
import { DecryptMessage } from '../../Encryption/UseCase/Asymmetric/DecryptMessage'
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
export class GetUntrustedPayload implements SyncUseCaseInterface<AsymmetricMessagePayload> {
constructor(private decryptMessage: DecryptMessage) {}
execute<M extends AsymmetricMessagePayload>(dto: {
privateKey: string
message: AsymmetricMessageServerHash
}): Result<M> {
const result = this.decryptMessage.execute<M>({
message: dto.message.encrypted_message,
sender: undefined,
privateKey: dto.privateKey,
})
return result
}
}

View File

@@ -1,5 +1,4 @@
import { ItemManagerInterface } from './../../Item/ItemManagerInterface' import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface' import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
import { import {
KeySystemRootKeyInterface, KeySystemRootKeyInterface,
@@ -7,18 +6,19 @@ import {
FillItemContent, FillItemContent,
KeySystemRootKeyContent, KeySystemRootKeyContent,
VaultListingMutator, VaultListingMutator,
VaultListingInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { GetVaultUseCase } from '../../Vaults/UseCase/GetVault' import { GetVault } from '../../Vaults/UseCase/GetVault'
import { EncryptionProviderInterface } from '@standardnotes/encryption' import { EncryptionProviderInterface } from '../../Encryption/EncryptionProviderInterface'
export class HandleTrustedSharedVaultRootKeyChangedMessage { export class HandleRootKeyChangedMessage {
constructor( constructor(
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private items: ItemManagerInterface,
private sync: SyncServiceInterface, private sync: SyncServiceInterface,
private encryption: EncryptionProviderInterface, private encryption: EncryptionProviderInterface,
private getVault: GetVault,
) {} ) {}
async execute(message: AsymmetricMessageSharedVaultRootKeyChanged): Promise<void> { async execute(message: AsymmetricMessageSharedVaultRootKeyChanged): Promise<void> {
@@ -30,9 +30,9 @@ export class HandleTrustedSharedVaultRootKeyChangedMessage {
true, true,
) )
const vault = new GetVaultUseCase(this.items).execute({ keySystemIdentifier: rootKeyContent.systemIdentifier }) const vault = this.getVault.execute<VaultListingInterface>({ keySystemIdentifier: rootKeyContent.systemIdentifier })
if (vault) { if (!vault.isFailed()) {
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => { await this.mutator.changeItem<VaultListingMutator>(vault.getValue(), (mutator) => {
mutator.rootKeyParams = rootKeyContent.keyParams mutator.rootKeyParams = rootKeyContent.keyParams
}) })
} }

View File

@@ -1,7 +1,7 @@
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface' import { CreateOrEditContact } from './../../Contacts/UseCase/CreateOrEditContact'
import { HandleTrustedSharedVaultInviteMessage } from './HandleTrustedSharedVaultInviteMessage' import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import { ProcessAcceptedVaultInvite } from './ProcessAcceptedVaultInvite'
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface' import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { import {
AsymmetricMessagePayloadType, AsymmetricMessagePayloadType,
@@ -9,32 +9,25 @@ import {
KeySystemRootKeyContent, KeySystemRootKeyContent,
} from '@standardnotes/models' } from '@standardnotes/models'
describe('HandleTrustedSharedVaultInviteMessage', () => { describe('ProcessAcceptedVaultInvite', () => {
let mutatorMock: jest.Mocked<MutatorClientInterface> let mutator: jest.Mocked<MutatorClientInterface>
let syncServiceMock: jest.Mocked<SyncServiceInterface> let sync: jest.Mocked<SyncServiceInterface>
let contactServiceMock: jest.Mocked<ContactServiceInterface> let createOrEditContact: jest.Mocked<CreateOrEditContact>
beforeEach(() => { beforeEach(() => {
mutatorMock = { mutator = {} as jest.Mocked<MutatorClientInterface>
createItem: jest.fn(), mutator.createItem = jest.fn()
} as any
syncServiceMock = { sync = {} as jest.Mocked<SyncServiceInterface>
sync: jest.fn(), sync.sync = jest.fn()
} as any
contactServiceMock = { createOrEditContact = {} as jest.Mocked<CreateOrEditContact>
createOrEditTrustedContact: jest.fn(), createOrEditContact.execute = jest.fn()
} as any
}) })
it('should create root key before creating vault listing so that propagated vault listings do not appear as locked', async () => { it('should create root key before creating vault listing so that propagated vault listings do not appear as locked', async () => {
const handleTrustedSharedVaultInviteMessage = new HandleTrustedSharedVaultInviteMessage( const handleTrustedSharedVaultInviteMessage = new ProcessAcceptedVaultInvite(mutator, sync, createOrEditContact)
mutatorMock, createOrEditContact
syncServiceMock,
contactServiceMock,
)
const testMessage = { const testMessage = {
type: AsymmetricMessagePayloadType.SharedVaultInvite, type: AsymmetricMessagePayloadType.SharedVaultInvite,
data: { data: {
@@ -54,11 +47,11 @@ describe('HandleTrustedSharedVaultInviteMessage', () => {
await handleTrustedSharedVaultInviteMessage.execute(testMessage, sharedVaultUuid, senderUuid) await handleTrustedSharedVaultInviteMessage.execute(testMessage, sharedVaultUuid, senderUuid)
const keySystemRootKeyCallIndex = mutatorMock.createItem.mock.calls.findIndex( const keySystemRootKeyCallIndex = mutator.createItem.mock.calls.findIndex(
([contentType]) => contentType === ContentType.TYPES.KeySystemRootKey, ([contentType]) => contentType === ContentType.TYPES.KeySystemRootKey,
) )
const vaultListingCallIndex = mutatorMock.createItem.mock.calls.findIndex( const vaultListingCallIndex = mutator.createItem.mock.calls.findIndex(
([contentType]) => contentType === ContentType.TYPES.VaultListing, ([contentType]) => contentType === ContentType.TYPES.VaultListing,
) )

View File

@@ -1,4 +1,3 @@
import { ContactServiceInterface } from './../../Contacts/ContactServiceInterface'
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface' import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
import { import {
KeySystemRootKeyInterface, KeySystemRootKeyInterface,
@@ -11,12 +10,13 @@ import {
} from '@standardnotes/models' } from '@standardnotes/models'
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { CreateOrEditContact } from '../../Contacts/UseCase/CreateOrEditContact'
export class HandleTrustedSharedVaultInviteMessage { export class ProcessAcceptedVaultInvite {
constructor( constructor(
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private sync: SyncServiceInterface, private sync: SyncServiceInterface,
private contacts: ContactServiceInterface, private createOrEditContact: CreateOrEditContact,
) {} ) {}
async execute( async execute(
@@ -47,11 +47,7 @@ export class HandleTrustedSharedVaultInviteMessage {
await this.mutator.createItem(ContentType.TYPES.VaultListing, FillItemContentSpecialized(content), true) await this.mutator.createItem(ContentType.TYPES.VaultListing, FillItemContentSpecialized(content), true)
for (const contact of trustedContacts) { for (const contact of trustedContacts) {
if (contact.isMe) { await this.createOrEditContact.execute({
throw new Error('Should not receive isMe contact from invite')
}
await this.contacts.createOrEditTrustedContact({
name: contact.name, name: contact.name,
contactUuid: contact.contactUuid, contactUuid: contact.contactUuid,
publicKey: contact.publicKeySet.encryption, publicKey: contact.publicKeySet.encryption,

View File

@@ -0,0 +1,58 @@
import { AsymmetricMessageServerHash, isErrorResponse } from '@standardnotes/responses'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { AsymmetricMessageServerInterface } from '@standardnotes/api'
import { ResendMessage } from './ResendMessage'
import { FindContact } from '../../Contacts/UseCase/FindContact'
export class ResendAllMessages implements UseCaseInterface<void> {
constructor(
private resendMessage: ResendMessage,
private messageServer: AsymmetricMessageServerInterface,
private findContact: FindContact,
) {}
async execute(params: {
keys: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
previousKeys?: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
}): Promise<Result<AsymmetricMessageServerHash[]>> {
const messages = await this.messageServer.getOutboundUserMessages()
if (isErrorResponse(messages)) {
return Result.fail('Failed to get outbound user messages')
}
const errors: string[] = []
for (const message of messages.data.messages) {
const recipient = this.findContact.execute({ userUuid: message.user_uuid })
if (recipient) {
errors.push(`Contact not found for invite ${message.user_uuid}`)
continue
}
await this.resendMessage.execute({
keys: params.keys,
previousKeys: params.previousKeys,
message: message,
recipient,
})
await this.messageServer.deleteMessage({
messageUuid: message.uuid,
})
}
if (errors.length > 0) {
return Result.fail(errors.join(', '))
}
return Result.ok()
}
}

View File

@@ -0,0 +1,58 @@
import { DecryptOwnMessage } from '../../Encryption/UseCase/Asymmetric/DecryptOwnMessage'
import { AsymmetricMessagePayload, TrustedContactInterface } from '@standardnotes/models'
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage'
import { SendMessage } from './SendMessage'
export class ResendMessage implements UseCaseInterface<void> {
constructor(
private decryptOwnMessage: DecryptOwnMessage<AsymmetricMessagePayload>,
private sendMessage: SendMessage,
private encryptMessage: EncryptMessage,
) {}
async execute(params: {
keys: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
previousKeys?: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
recipient: TrustedContactInterface
message: AsymmetricMessageServerHash
}): Promise<Result<AsymmetricMessageServerHash>> {
const decryptionResult = this.decryptOwnMessage.execute({
message: params.message.encrypted_message,
privateKey: params.previousKeys?.encryption.privateKey ?? params.keys.encryption.privateKey,
recipientPublicKey: params.recipient.publicKeySet.encryption,
})
if (decryptionResult.isFailed()) {
return Result.fail(decryptionResult.getError())
}
const decryptedMessage = decryptionResult.getValue()
const encryptedMessage = this.encryptMessage.execute({
message: decryptedMessage,
keys: params.keys,
recipientPublicKey: params.recipient.publicKeySet.encryption,
})
if (encryptedMessage.isFailed()) {
return Result.fail(encryptedMessage.getError())
}
const sendMessageResult = await this.sendMessage.execute({
recipientUuid: params.recipient.contactUuid,
encryptedMessage: encryptedMessage.getValue(),
replaceabilityIdentifier: params.message.replaceabilityIdentifier,
})
return sendMessageResult
}
}

View File

@@ -1,14 +1,15 @@
import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses' import { isErrorResponse, AsymmetricMessageServerHash, getErrorFromErrorResponse } from '@standardnotes/responses'
import { AsymmetricMessageServerInterface } from '@standardnotes/api' import { AsymmetricMessageServerInterface } from '@standardnotes/api'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class SendAsymmetricMessageUseCase { export class SendMessage implements UseCaseInterface<AsymmetricMessageServerHash> {
constructor(private messageServer: AsymmetricMessageServerInterface) {} constructor(private messageServer: AsymmetricMessageServerInterface) {}
async execute(params: { async execute(params: {
recipientUuid: string recipientUuid: string
encryptedMessage: string encryptedMessage: string
replaceabilityIdentifier: string | undefined replaceabilityIdentifier: string | undefined
}): Promise<AsymmetricMessageServerHash | ClientDisplayableError> { }): Promise<Result<AsymmetricMessageServerHash>> {
const response = await this.messageServer.createMessage({ const response = await this.messageServer.createMessage({
recipientUuid: params.recipientUuid, recipientUuid: params.recipientUuid,
encryptedMessage: params.encryptedMessage, encryptedMessage: params.encryptedMessage,
@@ -16,9 +17,9 @@ export class SendAsymmetricMessageUseCase {
}) })
if (isErrorResponse(response)) { if (isErrorResponse(response)) {
return ClientDisplayableError.FromNetworkError(response) return Result.fail(getErrorFromErrorResponse(response).message)
} }
return response.data.message return Result.ok(response.data.message)
} }
} }

View File

@@ -1,16 +1,16 @@
import { EncryptionProviderInterface } from '@standardnotes/encryption' import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessageServerHash, ClientDisplayableError } from '@standardnotes/responses'
import { import {
TrustedContactInterface, TrustedContactInterface,
AsymmetricMessagePayloadType, AsymmetricMessagePayloadType,
AsymmetricMessageSenderKeypairChanged, AsymmetricMessageSenderKeypairChanged,
} from '@standardnotes/models' } from '@standardnotes/models'
import { AsymmetricMessageServer } from '@standardnotes/api'
import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { SendAsymmetricMessageUseCase } from './SendAsymmetricMessageUseCase' import { SendMessage } from './SendMessage'
import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class SendOwnContactChangeMessage { export class SendOwnContactChangeMessage implements UseCaseInterface<AsymmetricMessageServerHash> {
constructor(private encryption: EncryptionProviderInterface, private messageServer: AsymmetricMessageServer) {} constructor(private encryptMessage: EncryptMessage, private sendMessage: SendMessage) {}
async execute(params: { async execute(params: {
senderOldKeyPair: PkcKeyPair senderOldKeyPair: PkcKeyPair
@@ -18,7 +18,7 @@ export class SendOwnContactChangeMessage {
senderNewKeyPair: PkcKeyPair senderNewKeyPair: PkcKeyPair
senderNewSigningKeyPair: PkcKeyPair senderNewSigningKeyPair: PkcKeyPair
contact: TrustedContactInterface contact: TrustedContactInterface
}): Promise<AsymmetricMessageServerHash | ClientDisplayableError> { }): Promise<Result<AsymmetricMessageServerHash>> {
const message: AsymmetricMessageSenderKeypairChanged = { const message: AsymmetricMessageSenderKeypairChanged = {
type: AsymmetricMessagePayloadType.SenderKeypairChanged, type: AsymmetricMessagePayloadType.SenderKeypairChanged,
data: { data: {
@@ -28,17 +28,22 @@ export class SendOwnContactChangeMessage {
}, },
} }
const encryptedMessage = this.encryption.asymmetricallyEncryptMessage({ const encryptedMessage = this.encryptMessage.execute({
message: message, message: message,
senderKeyPair: params.senderOldKeyPair, keys: {
senderSigningKeyPair: params.senderOldSigningKeyPair, encryption: params.senderOldKeyPair,
signing: params.senderOldSigningKeyPair,
},
recipientPublicKey: params.contact.publicKeySet.encryption, recipientPublicKey: params.contact.publicKeySet.encryption,
}) })
const sendMessageUseCase = new SendAsymmetricMessageUseCase(this.messageServer) if (encryptedMessage.isFailed()) {
const sendMessageResult = await sendMessageUseCase.execute({ return Result.fail(encryptedMessage.getError())
}
const sendMessageResult = await this.sendMessage.execute({
recipientUuid: params.contact.contactUuid, recipientUuid: params.contact.contactUuid,
encryptedMessage, encryptedMessage: encryptedMessage.getValue(),
replaceabilityIdentifier: undefined, replaceabilityIdentifier: undefined,
}) })

View File

@@ -1,20 +1,20 @@
import { HistoryServiceInterface } from './../History/HistoryServiceInterface' import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
import { PayloadManagerInterface } from './../Payloads/PayloadManagerInterface' import { LegacyApiServiceInterface } from '../Api/LegacyApiServiceInterface'
import { StorageServiceInterface } from './../Storage/StorageServiceInterface' import { HistoryServiceInterface } from '../History/HistoryServiceInterface'
import { SessionsClientInterface } from './../Session/SessionsClientInterface' import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
import { StatusServiceInterface } from './../Status/StatusServiceInterface' import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
import { FilesBackupService } from './BackupService' import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { StatusServiceInterface } from '../Status/StatusServiceInterface'
import { FilesBackupService } from './FilesBackupService'
import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common' import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { ItemManagerInterface } from '../Item/ItemManagerInterface' import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { InternalEventBusInterface } from '..' import { InternalEventBusInterface } from '..'
import { AlertService } from '../Alert/AlertService' import { AlertService } from '../Alert/AlertService'
import { ApiServiceInterface } from '../Api/ApiServiceInterface'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface' import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { DirectoryManagerInterface, FileBackupsDevice } from '@standardnotes/files' import { DirectoryManagerInterface, FileBackupsDevice } from '@standardnotes/files'
describe('backup service', () => { describe('backup service', () => {
let apiService: ApiServiceInterface let apiService: LegacyApiServiceInterface
let itemManager: ItemManagerInterface let itemManager: ItemManagerInterface
let syncService: SyncServiceInterface let syncService: SyncServiceInterface
let alertService: AlertService let alertService: AlertService
@@ -30,7 +30,7 @@ describe('backup service', () => {
let history: HistoryServiceInterface let history: HistoryServiceInterface
beforeEach(() => { beforeEach(() => {
apiService = {} as jest.Mocked<ApiServiceInterface> apiService = {} as jest.Mocked<LegacyApiServiceInterface>
apiService.addEventObserver = jest.fn() apiService.addEventObserver = jest.fn()
apiService.createUserFileValetToken = jest.fn() apiService.createUserFileValetToken = jest.fn()
apiService.downloadFile = jest.fn() apiService.downloadFile = jest.fn()

View File

@@ -1,6 +1,9 @@
import { InternalEventInterface } from './../Internal/InternalEventInterface'
import { ApplicationStageChangedEventPayload } from '../Event/ApplicationStageChangedEventPayload'
import { ApplicationEvent } from '../Event/ApplicationEvent'
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
import { NoteType } from '@standardnotes/features' import { NoteType } from '@standardnotes/features'
import { ApplicationStage } from './../Application/ApplicationStage' import { ApplicationStage } from '../Application/ApplicationStage'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { import {
PayloadEmitSource, PayloadEmitSource,
FileItem, FileItem,
@@ -34,12 +37,16 @@ import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface' import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
import { HistoryServiceInterface } from '../History/HistoryServiceInterface' import { HistoryServiceInterface } from '../History/HistoryServiceInterface'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
const PlaintextBackupsDirectoryName = 'Plaintext Backups' const PlaintextBackupsDirectoryName = 'Plaintext Backups'
export const TextBackupsDirectoryName = 'Text Backups' export const TextBackupsDirectoryName = 'Text Backups'
export const FileBackupsDirectoryName = 'File Backups' export const FileBackupsDirectoryName = 'File Backups'
export class FilesBackupService extends AbstractService implements BackupServiceInterface { export class FilesBackupService
extends AbstractService
implements BackupServiceInterface, InternalEventHandlerInterface
{
private filesObserverDisposer: () => void private filesObserverDisposer: () => void
private notesObserverDisposer: () => void private notesObserverDisposer: () => void
private tagsObserverDisposer: () => void private tagsObserverDisposer: () => void
@@ -98,6 +105,15 @@ export class FilesBackupService extends AbstractService implements BackupService
}) })
} }
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === ApplicationEvent.ApplicationStageChanged) {
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
if (stage === ApplicationStage.Launched_10) {
void this.automaticallyEnableTextBackupsIfPreferenceNotSet()
}
}
}
setSuperConverter(converter: SuperConverterServiceInterface): void { setSuperConverter(converter: SuperConverterServiceInterface): void {
this.markdownConverter = converter this.markdownConverter = converter
} }
@@ -143,12 +159,6 @@ export class FilesBackupService extends AbstractService implements BackupService
;(this.session as unknown) = undefined ;(this.session as unknown) = undefined
} }
override async handleApplicationStage(stage: ApplicationStage): Promise<void> {
if (stage === ApplicationStage.Launched_10) {
void this.automaticallyEnableTextBackupsIfPreferenceNotSet()
}
}
private async automaticallyEnableTextBackupsIfPreferenceNotSet(): Promise<void> { private async automaticallyEnableTextBackupsIfPreferenceNotSet(): Promise<void> {
if (this.storage.getValue(StorageKey.TextBackupsEnabled) == undefined) { if (this.storage.getValue(StorageKey.TextBackupsEnabled) == undefined) {
this.storage.setValue(StorageKey.TextBackupsEnabled, true) this.storage.setValue(StorageKey.TextBackupsEnabled, true)

View File

@@ -10,6 +10,12 @@ import { ChallengeReason } from './Types/ChallengeReason'
import { ChallengeObserver } from './Types/ChallengeObserver' import { ChallengeObserver } from './Types/ChallengeObserver'
export interface ChallengeServiceInterface extends AbstractService { export interface ChallengeServiceInterface extends AbstractService {
sendChallenge: (challenge: ChallengeInterface) => void
submitValuesForChallenge(challenge: ChallengeInterface, values: ChallengeValue[]): Promise<void>
cancelChallenge(challenge: ChallengeInterface): void
isPasscodeLocked(): Promise<boolean>
/** /**
* Resolves when the challenge has been completed. * Resolves when the challenge has been completed.
* For non-validated challenges, will resolve when the first value is submitted. * For non-validated challenges, will resolve when the first value is submitted.

View File

@@ -1,81 +1,62 @@
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { ApplicationStage } from './../Application/ApplicationStage'
import { SingletonManagerInterface } from './../Singleton/SingletonManagerInterface'
import { UserKeyPairChangedEventData } from './../Session/UserKeyPairChangedEventData' import { UserKeyPairChangedEventData } from './../Session/UserKeyPairChangedEventData'
import { SessionEvent } from './../Session/SessionEvent' import { SessionEvent } from './../Session/SessionEvent'
import { InternalEventInterface } from './../Internal/InternalEventInterface' import { InternalEventInterface } from './../Internal/InternalEventInterface'
import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface' import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { SharedVaultInviteServerHash, SharedVaultUserServerHash } from '@standardnotes/responses' import { SharedVaultInviteServerHash, SharedVaultUserServerHash } from '@standardnotes/responses'
import { import { TrustedContactInterface, TrustedContactMutator, DecryptedItemInterface } from '@standardnotes/models'
TrustedContactContent,
TrustedContactContentSpecialized,
TrustedContactInterface,
FillItemContent,
TrustedContactMutator,
DecryptedItemInterface,
} from '@standardnotes/models'
import { AbstractService } from '../Service/AbstractService' import { AbstractService } from '../Service/AbstractService'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface' import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { SessionsClientInterface } from '../Session/SessionsClientInterface' import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { ContactServiceEvent, ContactServiceInterface } from '../Contacts/ContactServiceInterface' import { ContactServiceEvent, ContactServiceInterface } from '../Contacts/ContactServiceInterface'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { UserClientInterface } from '../User/UserClientInterface' import { UserClientInterface } from '../User/UserClientInterface'
import { CollaborationIDData, Version1CollaborationId } from './CollaborationID' import { CollaborationIDData, Version1CollaborationId } from './CollaborationID'
import { EncryptionProviderInterface } from '@standardnotes/encryption' import { ValidateItemSigner } from './UseCase/ValidateItemSigner'
import { ValidateItemSignerUseCase } from './UseCase/ValidateItemSigner' import { ItemSignatureValidationResult } from './UseCase/Types/ItemSignatureValidationResult'
import { ValidateItemSignerResult } from './UseCase/ValidateItemSignerResult' import { FindContact } from './UseCase/FindContact'
import { FindTrustedContactUseCase } from './UseCase/FindTrustedContact' import { SelfContactManager } from './SelfContactManager'
import { SelfContactManager } from './Managers/SelfContactManager' import { CreateOrEditContact } from './UseCase/CreateOrEditContact'
import { CreateOrEditTrustedContactUseCase } from './UseCase/CreateOrEditTrustedContact' import { EditContact } from './UseCase/EditContact'
import { UpdateTrustedContactUseCase } from './UseCase/UpdateTrustedContact' import { GetAllContacts } from './UseCase/GetAllContacts'
import { ContentType } from '@standardnotes/domain-core' import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
export class ContactService export class ContactService
extends AbstractService<ContactServiceEvent> extends AbstractService<ContactServiceEvent>
implements ContactServiceInterface, InternalEventHandlerInterface implements ContactServiceInterface, InternalEventHandlerInterface
{ {
private selfContactManager: SelfContactManager
constructor( constructor(
private sync: SyncServiceInterface, private sync: SyncServiceInterface,
private items: ItemManagerInterface,
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private session: SessionsClientInterface, private session: SessionsClientInterface,
private crypto: PureCryptoInterface, private crypto: PureCryptoInterface,
private user: UserClientInterface, private user: UserClientInterface,
private selfContactManager: SelfContactManager,
private encryption: EncryptionProviderInterface, private encryption: EncryptionProviderInterface,
singletons: SingletonManagerInterface, private _findContact: FindContact,
private _getAllContacts: GetAllContacts,
private _createOrEditContact: CreateOrEditContact,
private _editContact: EditContact,
private _validateItemSigner: ValidateItemSigner,
eventBus: InternalEventBusInterface, eventBus: InternalEventBusInterface,
) { ) {
super(eventBus) super(eventBus)
this.selfContactManager = new SelfContactManager(sync, items, mutator, session, singletons)
eventBus.addEventHandler(this, SessionEvent.UserKeyPairChanged) eventBus.addEventHandler(this, SessionEvent.UserKeyPairChanged)
} }
public override async handleApplicationStage(stage: ApplicationStage): Promise<void> {
await super.handleApplicationStage(stage)
await this.selfContactManager.handleApplicationStage(stage)
}
async handleEvent(event: InternalEventInterface): Promise<void> { async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === SessionEvent.UserKeyPairChanged) { if (event.type === SessionEvent.UserKeyPairChanged) {
const data = event.payload as UserKeyPairChangedEventData const data = event.payload as UserKeyPairChangedEventData
await this.selfContactManager.updateWithNewPublicKeySet({ await this.selfContactManager.updateWithNewPublicKeySet({
encryption: data.newKeyPair.publicKey, encryption: data.current.encryption.publicKey,
signing: data.newSigningKeyPair.publicKey, signing: data.current.signing.publicKey,
}) })
} }
} }
private get userUuid(): string {
return this.session.getSureUser().uuid
}
getSelfContact(): TrustedContactInterface | undefined { getSelfContact(): TrustedContactInterface | undefined {
return this.selfContactManager.selfContact return this.selfContactManager.selfContact
} }
@@ -170,38 +151,11 @@ export class ContactService
contact: TrustedContactInterface, contact: TrustedContactInterface,
params: { name: string; publicKey: string; signingPublicKey: string }, params: { name: string; publicKey: string; signingPublicKey: string },
): Promise<TrustedContactInterface> { ): Promise<TrustedContactInterface> {
const usecase = new UpdateTrustedContactUseCase(this.mutator, this.sync) const updatedContact = await this._editContact.execute(contact, params)
const updatedContact = await usecase.execute(contact, params)
return updatedContact return updatedContact
} }
async createOrUpdateTrustedContactFromContactShare(
data: TrustedContactContentSpecialized,
): Promise<TrustedContactInterface> {
if (data.contactUuid === this.userUuid) {
throw new Error('Cannot receive self from contact share')
}
let contact = this.findTrustedContact(data.contactUuid)
if (contact) {
contact = await this.mutator.changeItem<TrustedContactMutator, TrustedContactInterface>(contact, (mutator) => {
mutator.name = data.name
mutator.replacePublicKeySet(data.publicKeySet)
})
} else {
contact = await this.mutator.createItem<TrustedContactInterface>(
ContentType.TYPES.TrustedContact,
FillItemContent<TrustedContactContent>(data),
true,
)
}
await this.sync.sync()
return contact
}
async createOrEditTrustedContact(params: { async createOrEditTrustedContact(params: {
name?: string name?: string
contactUuid: string contactUuid: string
@@ -209,8 +163,7 @@ export class ContactService
signingPublicKey: string signingPublicKey: string
isMe?: boolean isMe?: boolean
}): Promise<TrustedContactInterface | undefined> { }): Promise<TrustedContactInterface | undefined> {
const usecase = new CreateOrEditTrustedContactUseCase(this.items, this.mutator, this.sync) const contact = await this._createOrEditContact.execute(params)
const contact = await usecase.execute(params)
return contact return contact
} }
@@ -224,12 +177,15 @@ export class ContactService
} }
getAllContacts(): TrustedContactInterface[] { getAllContacts(): TrustedContactInterface[] {
return this.items.getItems(ContentType.TYPES.TrustedContact) return this._getAllContacts.execute().getValue()
} }
findTrustedContact(userUuid: string): TrustedContactInterface | undefined { findTrustedContact(userUuid: string): TrustedContactInterface | undefined {
const usecase = new FindTrustedContactUseCase(this.items) const result = this._findContact.execute({ userUuid })
return usecase.execute({ userUuid }) if (result.isFailed()) {
return undefined
}
return result.getValue()
} }
findTrustedContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined { findTrustedContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined {
@@ -249,16 +205,23 @@ export class ContactService
}) })
} }
isItemAuthenticallySigned(item: DecryptedItemInterface): ValidateItemSignerResult { isItemAuthenticallySigned(item: DecryptedItemInterface): ItemSignatureValidationResult {
const usecase = new ValidateItemSignerUseCase(this.items) return this._validateItemSigner.execute(item)
return usecase.execute(item)
} }
override deinit(): void { override deinit(): void {
super.deinit() super.deinit()
this.selfContactManager.deinit()
;(this.sync as unknown) = undefined ;(this.sync as unknown) = undefined
;(this.items as unknown) = undefined ;(this.mutator as unknown) = undefined
;(this.session as unknown) = undefined
;(this.crypto as unknown) = undefined
;(this.user as unknown) = undefined
;(this.selfContactManager as unknown) = undefined ;(this.selfContactManager as unknown) = undefined
;(this.encryption as unknown) = undefined
;(this._findContact as unknown) = undefined
;(this._getAllContacts as unknown) = undefined
;(this._createOrEditContact as unknown) = undefined
;(this._editContact as unknown) = undefined
;(this._validateItemSigner as unknown) = undefined
} }
} }

View File

@@ -1,11 +1,7 @@
import { import { DecryptedItemInterface, TrustedContactInterface } from '@standardnotes/models'
DecryptedItemInterface,
TrustedContactContentSpecialized,
TrustedContactInterface,
} from '@standardnotes/models'
import { AbstractService } from '../Service/AbstractService' import { AbstractService } from '../Service/AbstractService'
import { SharedVaultInviteServerHash, SharedVaultUserServerHash } from '@standardnotes/responses' import { SharedVaultInviteServerHash, SharedVaultUserServerHash } from '@standardnotes/responses'
import { ValidateItemSignerResult } from './UseCase/ValidateItemSignerResult' import { ItemSignatureValidationResult } from './UseCase/Types/ItemSignatureValidationResult'
export enum ContactServiceEvent {} export enum ContactServiceEvent {}
@@ -26,7 +22,6 @@ export interface ContactServiceInterface extends AbstractService<ContactServiceE
publicKey: string publicKey: string
signingPublicKey: string signingPublicKey: string
}): Promise<TrustedContactInterface | undefined> }): Promise<TrustedContactInterface | undefined>
createOrUpdateTrustedContactFromContactShare(data: TrustedContactContentSpecialized): Promise<TrustedContactInterface>
editTrustedContactFromCollaborationID( editTrustedContactFromCollaborationID(
contact: TrustedContactInterface, contact: TrustedContactInterface,
params: { name: string; collaborationID: string }, params: { name: string; collaborationID: string },
@@ -39,5 +34,5 @@ export interface ContactServiceInterface extends AbstractService<ContactServiceE
findTrustedContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined findTrustedContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined
findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined
isItemAuthenticallySigned(item: DecryptedItemInterface): ValidateItemSignerResult isItemAuthenticallySigned(item: DecryptedItemInterface): ItemSignatureValidationResult
} }

View File

@@ -1,12 +1,15 @@
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface' import { ApplicationStageChangedEventPayload } from './../Event/ApplicationStageChangedEventPayload'
import { InternalFeature } from './../../InternalFeatures/InternalFeature' import { ApplicationEvent } from './../Event/ApplicationEvent'
import { InternalFeatureService } from '../../InternalFeatures/InternalFeatureService' import { InternalEventInterface } from './../Internal/InternalEventInterface'
import { ApplicationStage } from './../../Application/ApplicationStage' import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
import { SingletonManagerInterface } from './../../Singleton/SingletonManagerInterface' import { InternalFeature } from '../InternalFeatures/InternalFeature'
import { SyncEvent } from './../../Event/SyncEvent' import { InternalFeatureService } from '../InternalFeatures/InternalFeatureService'
import { SessionsClientInterface } from '../../Session/SessionsClientInterface' import { ApplicationStage } from '../Application/ApplicationStage'
import { ItemManagerInterface } from '../../Item/ItemManagerInterface' import { SingletonManagerInterface } from '../Singleton/SingletonManagerInterface'
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface' import { SyncEvent } from '../Event/SyncEvent'
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { import {
ContactPublicKeySet, ContactPublicKeySet,
FillItemContent, FillItemContent,
@@ -14,23 +17,25 @@ import {
TrustedContactContent, TrustedContactContent,
TrustedContactContentSpecialized, TrustedContactContentSpecialized,
TrustedContactInterface, TrustedContactInterface,
PortablePublicKeySet,
} from '@standardnotes/models' } from '@standardnotes/models'
import { CreateOrEditTrustedContactUseCase } from '../UseCase/CreateOrEditTrustedContact' import { CreateOrEditContact } from './UseCase/CreateOrEditContact'
import { PublicKeySet } from '@standardnotes/encryption'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
export class SelfContactManager { const SelfContactName = 'Me'
export class SelfContactManager implements InternalEventHandlerInterface {
public selfContact?: TrustedContactInterface public selfContact?: TrustedContactInterface
private isReloadingSelfContact = false private isReloadingSelfContact = false
private eventDisposers: (() => void)[] = [] private eventDisposers: (() => void)[] = []
constructor( constructor(
private sync: SyncServiceInterface, sync: SyncServiceInterface,
private items: ItemManagerInterface, items: ItemManagerInterface,
private mutator: MutatorClientInterface,
private session: SessionsClientInterface, private session: SessionsClientInterface,
private singletons: SingletonManagerInterface, private singletons: SingletonManagerInterface,
private createOrEditContact: CreateOrEditContact,
) { ) {
this.eventDisposers.push( this.eventDisposers.push(
sync.addEventObserver((event) => { sync.addEventObserver((event) => {
@@ -43,11 +48,26 @@ export class SelfContactManager {
} }
}), }),
) )
this.eventDisposers.push(
items.addObserver(ContentType.TYPES.TrustedContact, () => {
const updatedReference = this.singletons.findSingleton<TrustedContact>(
ContentType.TYPES.TrustedContact,
TrustedContact.singletonPredicate,
)
if (updatedReference) {
this.selfContact = updatedReference
}
}),
)
} }
public async handleApplicationStage(stage: ApplicationStage): Promise<void> { async handleEvent(event: InternalEventInterface): Promise<void> {
if (stage === ApplicationStage.LoadedDatabase_12) { if (event.type === ApplicationEvent.ApplicationStageChanged) {
this.loadSelfContactFromDatabase() const stage = (event.payload as ApplicationStageChangedEventPayload).stage
if (stage === ApplicationStage.LoadedDatabase_12) {
this.loadSelfContactFromDatabase()
}
} }
} }
@@ -62,7 +82,7 @@ export class SelfContactManager {
) )
} }
public async updateWithNewPublicKeySet(publicKeySet: PublicKeySet) { public async updateWithNewPublicKeySet(publicKeySet: PortablePublicKeySet) {
if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) { if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) {
return return
} }
@@ -71,9 +91,8 @@ export class SelfContactManager {
return return
} }
const usecase = new CreateOrEditTrustedContactUseCase(this.items, this.mutator, this.sync) await this.createOrEditContact.execute({
await usecase.execute({ name: SelfContactName,
name: 'Me',
contactUuid: this.selfContact.contactUuid, contactUuid: this.selfContact.contactUuid,
publicKey: publicKeySet.encryption, publicKey: publicKeySet.encryption,
signingPublicKey: publicKeySet.signing, signingPublicKey: publicKeySet.signing,
@@ -104,13 +123,12 @@ export class SelfContactManager {
this.isReloadingSelfContact = true this.isReloadingSelfContact = true
const content: TrustedContactContentSpecialized = { const content: TrustedContactContentSpecialized = {
name: 'Me', name: SelfContactName,
isMe: true, isMe: true,
contactUuid: this.session.getSureUser().uuid, contactUuid: this.session.getSureUser().uuid,
publicKeySet: ContactPublicKeySet.FromJson({ publicKeySet: ContactPublicKeySet.FromJson({
encryption: this.session.getPublicKey(), encryption: this.session.getPublicKey(),
signing: this.session.getSigningPublicKey(), signing: this.session.getSigningPublicKey(),
isRevoked: false,
timestamp: new Date(), timestamp: new Date(),
}), }),
} }
@@ -126,10 +144,8 @@ export class SelfContactManager {
deinit() { deinit() {
this.eventDisposers.forEach((disposer) => disposer()) this.eventDisposers.forEach((disposer) => disposer())
;(this.sync as unknown) = undefined
;(this.items as unknown) = undefined
;(this.mutator as unknown) = undefined
;(this.session as unknown) = undefined ;(this.session as unknown) = undefined
;(this.singletons as unknown) = undefined ;(this.singletons as unknown) = undefined
;(this.createOrEditContact as unknown) = undefined
} }
} }

View File

@@ -1,6 +1,5 @@
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface' import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface' import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import { ItemManagerInterface } from './../../Item/ItemManagerInterface'
import { import {
ContactPublicKeySet, ContactPublicKeySet,
FillItemContent, FillItemContent,
@@ -8,16 +7,17 @@ import {
TrustedContactContentSpecialized, TrustedContactContentSpecialized,
TrustedContactInterface, TrustedContactInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { FindTrustedContactUseCase } from './FindTrustedContact' import { FindContact } from './FindContact'
import { UnknownContactName } from '../UnknownContactName' import { UnknownContactName } from '../UnknownContactName'
import { UpdateTrustedContactUseCase } from './UpdateTrustedContact' import { EditContact } from './EditContact'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
export class CreateOrEditTrustedContactUseCase { export class CreateOrEditContact {
constructor( constructor(
private items: ItemManagerInterface,
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private sync: SyncServiceInterface, private sync: SyncServiceInterface,
private findContact: FindContact,
private editContact: EditContact,
) {} ) {}
async execute(params: { async execute(params: {
@@ -27,13 +27,14 @@ export class CreateOrEditTrustedContactUseCase {
signingPublicKey: string signingPublicKey: string
isMe?: boolean isMe?: boolean
}): Promise<TrustedContactInterface | undefined> { }): Promise<TrustedContactInterface | undefined> {
const findUsecase = new FindTrustedContactUseCase(this.items) const existingContact = this.findContact.execute({ userUuid: params.contactUuid })
const existingContact = findUsecase.execute({ userUuid: params.contactUuid })
if (existingContact) { if (!existingContact.isFailed()) {
const updateUsecase = new UpdateTrustedContactUseCase(this.mutator, this.sync) await this.editContact.execute(existingContact.getValue(), {
await updateUsecase.execute(existingContact, { ...params, name: params.name ?? existingContact.name }) ...params,
return existingContact name: params.name ?? existingContact.getValue().name,
})
return existingContact.getValue()
} }
const content: TrustedContactContentSpecialized = { const content: TrustedContactContentSpecialized = {
@@ -41,7 +42,6 @@ export class CreateOrEditTrustedContactUseCase {
publicKeySet: ContactPublicKeySet.FromJson({ publicKeySet: ContactPublicKeySet.FromJson({
encryption: params.publicKey, encryption: params.publicKey,
signing: params.signingPublicKey, signing: params.signingPublicKey,
isRevoked: false,
timestamp: new Date(), timestamp: new Date(),
}), }),
contactUuid: params.contactUuid, contactUuid: params.contactUuid,

View File

@@ -1,8 +1,8 @@
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface' import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface' import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import { TrustedContactInterface, TrustedContactMutator } from '@standardnotes/models' import { TrustedContactInterface, TrustedContactMutator } from '@standardnotes/models'
export class UpdateTrustedContactUseCase { export class EditContact {
constructor(private mutator: MutatorClientInterface, private sync: SyncServiceInterface) {} constructor(private mutator: MutatorClientInterface, private sync: SyncServiceInterface) {}
async execute( async execute(

View File

@@ -0,0 +1,38 @@
import { Predicate, TrustedContactInterface } from '@standardnotes/models'
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
import { FindContactQuery } from './Types/FindContactQuery'
import { ContentType, Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
export class FindContact implements SyncUseCaseInterface<TrustedContactInterface> {
constructor(private items: ItemManagerInterface) {}
execute(query: FindContactQuery): Result<TrustedContactInterface> {
if ('userUuid' in query && query.userUuid) {
const contact = this.items.itemsMatchingPredicate<TrustedContactInterface>(
ContentType.TYPES.TrustedContact,
new Predicate<TrustedContactInterface>('contactUuid', '=', query.userUuid),
)[0]
if (contact) {
return Result.ok(contact)
} else {
return Result.fail('Contact not found')
}
}
if ('signingPublicKey' in query && query.signingPublicKey) {
const allContacts = this.items.getItems<TrustedContactInterface>(ContentType.TYPES.TrustedContact)
const contact = allContacts.find((contact) =>
contact.hasCurrentOrPreviousSigningPublicKey(query.signingPublicKey),
)
if (contact) {
return Result.ok(contact)
} else {
return Result.fail('Contact not found')
}
}
throw new Error('Invalid query')
}
}

View File

@@ -1,29 +0,0 @@
import { Predicate, TrustedContactInterface } from '@standardnotes/models'
import { ItemManagerInterface } from './../../Item/ItemManagerInterface'
import { FindContactQuery } from './FindContactQuery'
import { ContentType } from '@standardnotes/domain-core'
export class FindTrustedContactUseCase {
constructor(private items: ItemManagerInterface) {}
execute(query: FindContactQuery): TrustedContactInterface | undefined {
if ('userUuid' in query && query.userUuid) {
return this.items.itemsMatchingPredicate<TrustedContactInterface>(
ContentType.TYPES.TrustedContact,
new Predicate<TrustedContactInterface>('contactUuid', '=', query.userUuid),
)[0]
}
if ('signingPublicKey' in query && query.signingPublicKey) {
const allContacts = this.items.getItems<TrustedContactInterface>(ContentType.TYPES.TrustedContact)
return allContacts.find((contact) => contact.isSigningKeyTrusted(query.signingPublicKey))
}
if ('publicKey' in query && query.publicKey) {
const allContacts = this.items.getItems<TrustedContactInterface>(ContentType.TYPES.TrustedContact)
return allContacts.find((contact) => contact.isPublicKeyTrusted(query.publicKey))
}
throw new Error('Invalid query')
}
}

View File

@@ -0,0 +1,11 @@
import { TrustedContactInterface } from '@standardnotes/models'
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
import { ContentType, Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
export class GetAllContacts implements SyncUseCaseInterface<TrustedContactInterface[]> {
constructor(private items: ItemManagerInterface) {}
execute(): Result<TrustedContactInterface[]> {
return Result.ok(this.items.getItems(ContentType.TYPES.TrustedContact))
}
}

View File

@@ -0,0 +1,31 @@
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { ReuploadAllInvites } from '../../SharedVaults/UseCase/ReuploadAllInvites'
import { ResendAllMessages } from '../../AsymmetricMessage/UseCase/ResendAllMessages'
export class HandleKeyPairChange implements UseCaseInterface<void> {
constructor(private reuploadAllInvites: ReuploadAllInvites, private resendAllMessages: ResendAllMessages) {}
async execute(dto: {
newKeys: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
previousKeys?: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
}): Promise<Result<void>> {
await this.reuploadAllInvites.execute({
keys: dto.newKeys,
previousKeys: dto.previousKeys,
})
await this.resendAllMessages.execute({
keys: dto.newKeys,
previousKeys: dto.previousKeys,
})
return Result.ok()
}
}

View File

@@ -0,0 +1,55 @@
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import {
FillItemContent,
MutationType,
PayloadEmitSource,
TrustedContactContent,
TrustedContactContentSpecialized,
TrustedContactInterface,
TrustedContactMutator,
} from '@standardnotes/models'
import { FindContact } from './FindContact'
import { ContentType, Result, UseCaseInterface } from '@standardnotes/domain-core'
export class ReplaceContactData implements UseCaseInterface<TrustedContactInterface> {
constructor(
private mutator: MutatorClientInterface,
private sync: SyncServiceInterface,
private findContact: FindContact,
) {}
async execute(data: TrustedContactContentSpecialized): Promise<Result<TrustedContactInterface>> {
const contactResult = this.findContact.execute({ userUuid: data.contactUuid })
if (contactResult.isFailed()) {
const newContact = await this.mutator.createItem<TrustedContactInterface>(
ContentType.TYPES.TrustedContact,
FillItemContent<TrustedContactContent>(data),
true,
)
await this.sync.sync()
return Result.ok(newContact)
}
const existingContact = contactResult.getValue()
if (existingContact.isMe) {
return Result.fail('Cannot replace data for me contact')
}
const updatedContact = await this.mutator.changeItem<TrustedContactMutator, TrustedContactInterface>(
existingContact,
(mutator) => {
mutator.name = data.name
mutator.replacePublicKeySet(data.publicKeySet)
},
MutationType.UpdateUserTimestamps,
PayloadEmitSource.RemoteRetrieved,
)
await this.sync.sync()
return Result.ok(updatedContact)
}
}

View File

@@ -1 +1 @@
export type FindContactQuery = { userUuid: string } | { signingPublicKey: string } | { publicKey: string } export type FindContactQuery = { userUuid: string } | { signingPublicKey: string }

View File

@@ -0,0 +1,6 @@
export enum ItemSignatureValidationResult {
NotApplicable = 'NotApplicable',
Trusted = 'Trusted',
SignedWithNonCurrentKey = 'SignedWithNonCurrentKey',
NotTrusted = 'NotTrusted',
}

View File

@@ -2,21 +2,26 @@ import {
DecryptedItemInterface, DecryptedItemInterface,
PayloadSource, PayloadSource,
PersistentSignatureData, PersistentSignatureData,
PublicKeyTrustStatus,
TrustedContactInterface, TrustedContactInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { ItemManagerInterface } from './../../Item/ItemManagerInterface' import { ValidateItemSigner } from './ValidateItemSigner'
import { ValidateItemSignerUseCase } from './ValidateItemSigner' import { FindContact } from './FindContact'
import { ItemSignatureValidationResult } from './Types/ItemSignatureValidationResult'
import { Result } from '@standardnotes/domain-core'
describe('validate item signer use case', () => { describe('validate item signer use case', () => {
let usecase: ValidateItemSignerUseCase let usecase: ValidateItemSigner
let items: ItemManagerInterface let findContact: FindContact
const trustedContact = {} as jest.Mocked<TrustedContactInterface> const trustedContact = {} as jest.Mocked<TrustedContactInterface>
trustedContact.isSigningKeyTrusted = jest.fn().mockReturnValue(true) trustedContact.getTrustStatusForSigningPublicKey = jest.fn().mockReturnValue(PublicKeyTrustStatus.Trusted)
beforeEach(() => { beforeEach(() => {
items = {} as jest.Mocked<ItemManagerInterface> findContact = {} as jest.Mocked<FindContact>
usecase = new ValidateItemSignerUseCase(items) findContact.execute = jest.fn().mockReturnValue(Result.ok(trustedContact))
usecase = new ValidateItemSigner(findContact)
}) })
const createItem = (params: { const createItem = (params: {
@@ -42,7 +47,7 @@ describe('validate item signer use case', () => {
describe('has last edited by uuid', () => { describe('has last edited by uuid', () => {
describe('trusted contact not found', () => { describe('trusted contact not found', () => {
beforeEach(() => { beforeEach(() => {
items.itemsMatchingPredicate = jest.fn().mockReturnValue([]) findContact.execute = jest.fn().mockReturnValue(Result.fail('Not found'))
}) })
it('should return invalid signing is required', () => { it('should return invalid signing is required', () => {
@@ -53,7 +58,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('no') expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
}) })
it('should return not applicable signing is not required', () => { it('should return not applicable signing is not required', () => {
@@ -64,15 +69,11 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('not-applicable') expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
}) })
}) })
describe('trusted contact found for last editor', () => { describe('trusted contact found for last editor', () => {
beforeEach(() => {
items.itemsMatchingPredicate = jest.fn().mockReturnValue([trustedContact])
})
describe('does not have signature data', () => { describe('does not have signature data', () => {
it('should return not applicable if the item was just recently created', () => { it('should return not applicable if the item was just recently created', () => {
const item = createItem({ const item = createItem({
@@ -83,7 +84,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('not-applicable') expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
}) })
it('should return not applicable if the item was just recently saved', () => { it('should return not applicable if the item was just recently saved', () => {
@@ -95,7 +96,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('not-applicable') expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
}) })
it('should return invalid if signing is required', () => { it('should return invalid if signing is required', () => {
@@ -106,7 +107,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('no') expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
}) })
it('should return not applicable if signing is not required', () => { it('should return not applicable if signing is not required', () => {
@@ -117,7 +118,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('not-applicable') expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
}) })
}) })
@@ -133,7 +134,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('no') expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
}) })
it('should return not applicable if signing is not required', () => { it('should return not applicable if signing is not required', () => {
@@ -146,7 +147,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('not-applicable') expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
}) })
}) })
@@ -164,7 +165,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('no') expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
}) })
it('should return invalid if signature result passes and a trusted contact is NOT found for signature public key', () => { it('should return invalid if signature result passes and a trusted contact is NOT found for signature public key', () => {
@@ -180,10 +181,10 @@ describe('validate item signer use case', () => {
} as jest.Mocked<PersistentSignatureData>, } as jest.Mocked<PersistentSignatureData>,
}) })
items.itemsMatchingPredicate = jest.fn().mockReturnValue([]) findContact.execute = jest.fn().mockReturnValue(Result.fail('Not found'))
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('no') expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
}) })
it('should return valid if signature result passes and a trusted contact is found for signature public key', () => { it('should return valid if signature result passes and a trusted contact is found for signature public key', () => {
@@ -199,10 +200,10 @@ describe('validate item signer use case', () => {
} as jest.Mocked<PersistentSignatureData>, } as jest.Mocked<PersistentSignatureData>,
}) })
items.itemsMatchingPredicate = jest.fn().mockReturnValue([trustedContact]) findContact.execute = jest.fn().mockReturnValue(Result.ok(trustedContact))
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('yes') expect(result).toEqual(ItemSignatureValidationResult.Trusted)
}) })
}) })
}) })
@@ -220,7 +221,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('not-applicable') expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
}) })
it('should return not applicable if the item was just recently saved', () => { it('should return not applicable if the item was just recently saved', () => {
@@ -232,7 +233,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('not-applicable') expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
}) })
it('should return invalid if signing is required', () => { it('should return invalid if signing is required', () => {
@@ -243,7 +244,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('no') expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
}) })
it('should return not applicable if signing is not required', () => { it('should return not applicable if signing is not required', () => {
@@ -254,7 +255,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('not-applicable') expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
}) })
}) })
@@ -270,7 +271,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('no') expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
}) })
it('should return not applicable if signing is not required', () => { it('should return not applicable if signing is not required', () => {
@@ -283,7 +284,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('not-applicable') expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
}) })
}) })
@@ -301,7 +302,7 @@ describe('validate item signer use case', () => {
}) })
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('no') expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
}) })
it('should return invalid if signature result passes and a trusted contact is NOT found for signature public key', () => { it('should return invalid if signature result passes and a trusted contact is NOT found for signature public key', () => {
@@ -317,10 +318,10 @@ describe('validate item signer use case', () => {
} as jest.Mocked<PersistentSignatureData>, } as jest.Mocked<PersistentSignatureData>,
}) })
items.getItems = jest.fn().mockReturnValue([]) findContact.execute = jest.fn().mockReturnValue(Result.fail('Not found'))
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('no') expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
}) })
it('should return valid if signature result passes and a trusted contact is found for signature public key', () => { it('should return valid if signature result passes and a trusted contact is found for signature public key', () => {
@@ -336,10 +337,8 @@ describe('validate item signer use case', () => {
} as jest.Mocked<PersistentSignatureData>, } as jest.Mocked<PersistentSignatureData>,
}) })
items.getItems = jest.fn().mockReturnValue([trustedContact])
const result = usecase.execute(item) const result = usecase.execute(item)
expect(result).toEqual('yes') expect(result).toEqual(ItemSignatureValidationResult.Trusted)
}) })
}) })
}) })

View File

@@ -1,15 +1,12 @@
import { ItemManagerInterface } from './../../Item/ItemManagerInterface'
import { doesPayloadRequireSigning } from '@standardnotes/encryption/src/Domain/Operator/004/V004AlgorithmHelpers' import { doesPayloadRequireSigning } from '@standardnotes/encryption/src/Domain/Operator/004/V004AlgorithmHelpers'
import { DecryptedItemInterface, PayloadSource } from '@standardnotes/models' import { DecryptedItemInterface, PayloadSource, PublicKeyTrustStatus } from '@standardnotes/models'
import { ValidateItemSignerResult } from './ValidateItemSignerResult' import { ItemSignatureValidationResult } from './Types/ItemSignatureValidationResult'
import { FindTrustedContactUseCase } from './FindTrustedContact' import { FindContact } from './FindContact'
export class ValidateItemSignerUseCase { export class ValidateItemSigner {
private findContactUseCase = new FindTrustedContactUseCase(this.items) constructor(private findContact: FindContact) {}
constructor(private items: ItemManagerInterface) {} execute(item: DecryptedItemInterface): ItemSignatureValidationResult {
execute(item: DecryptedItemInterface): ValidateItemSignerResult {
const uuidOfLastEditor = item.last_edited_by_uuid const uuidOfLastEditor = item.last_edited_by_uuid
if (uuidOfLastEditor) { if (uuidOfLastEditor) {
return this.validateSignatureWithLastEditedByUuid(item, uuidOfLastEditor) return this.validateSignatureWithLastEditedByUuid(item, uuidOfLastEditor)
@@ -29,15 +26,15 @@ export class ValidateItemSignerUseCase {
private validateSignatureWithLastEditedByUuid( private validateSignatureWithLastEditedByUuid(
item: DecryptedItemInterface, item: DecryptedItemInterface,
uuidOfLastEditor: string, uuidOfLastEditor: string,
): ValidateItemSignerResult { ): ItemSignatureValidationResult {
const requiresSignature = doesPayloadRequireSigning(item) const requiresSignature = doesPayloadRequireSigning(item)
const trustedContact = this.findContactUseCase.execute({ userUuid: uuidOfLastEditor }) const trustedContact = this.findContact.execute({ userUuid: uuidOfLastEditor })
if (!trustedContact) { if (trustedContact.isFailed()) {
if (requiresSignature) { if (requiresSignature) {
return 'no' return ItemSignatureValidationResult.NotTrusted
} else { } else {
return 'not-applicable' return ItemSignatureValidationResult.NotApplicable
} }
} }
@@ -46,38 +43,41 @@ export class ValidateItemSignerUseCase {
this.isItemLocallyCreatedAndDoesNotRequireSignature(item) || this.isItemLocallyCreatedAndDoesNotRequireSignature(item) ||
this.isItemResutOfRemoteSaveAndDoesNotRequireSignature(item) this.isItemResutOfRemoteSaveAndDoesNotRequireSignature(item)
) { ) {
return 'not-applicable' return ItemSignatureValidationResult.NotApplicable
} }
if (requiresSignature) { if (requiresSignature) {
return 'no' return ItemSignatureValidationResult.NotTrusted
} }
return 'not-applicable' return ItemSignatureValidationResult.NotApplicable
} }
const signatureData = item.signatureData const signatureData = item.signatureData
if (!signatureData.result) { if (!signatureData.result) {
if (signatureData.required) { if (signatureData.required) {
return 'no' return ItemSignatureValidationResult.NotTrusted
} }
return 'not-applicable' return ItemSignatureValidationResult.NotApplicable
} }
const signatureResult = signatureData.result const signatureResult = signatureData.result
if (!signatureResult.passes) { if (!signatureResult.passes) {
return 'no' return ItemSignatureValidationResult.NotTrusted
} }
const signerPublicKey = signatureResult.publicKey const signerPublicKey = signatureResult.publicKey
if (trustedContact.isSigningKeyTrusted(signerPublicKey)) { const trustStatus = trustedContact.getValue().getTrustStatusForSigningPublicKey(signerPublicKey)
return 'yes' if (trustStatus === PublicKeyTrustStatus.Trusted) {
return ItemSignatureValidationResult.Trusted
} else if (trustStatus === PublicKeyTrustStatus.Previous) {
return ItemSignatureValidationResult.SignedWithNonCurrentKey
} }
return 'no' return ItemSignatureValidationResult.NotTrusted
} }
private validateSignatureWithNoLastEditedByUuid(item: DecryptedItemInterface): ValidateItemSignerResult { private validateSignatureWithNoLastEditedByUuid(item: DecryptedItemInterface): ItemSignatureValidationResult {
const requiresSignature = doesPayloadRequireSigning(item) const requiresSignature = doesPayloadRequireSigning(item)
if (!item.signatureData) { if (!item.signatureData) {
@@ -85,38 +85,45 @@ export class ValidateItemSignerUseCase {
this.isItemLocallyCreatedAndDoesNotRequireSignature(item) || this.isItemLocallyCreatedAndDoesNotRequireSignature(item) ||
this.isItemResutOfRemoteSaveAndDoesNotRequireSignature(item) this.isItemResutOfRemoteSaveAndDoesNotRequireSignature(item)
) { ) {
return 'not-applicable' return ItemSignatureValidationResult.NotApplicable
} }
if (requiresSignature) { if (requiresSignature) {
return 'no' return ItemSignatureValidationResult.NotTrusted
} }
return 'not-applicable' return ItemSignatureValidationResult.NotApplicable
} }
const signatureData = item.signatureData const signatureData = item.signatureData
if (!signatureData.result) { if (!signatureData.result) {
if (signatureData.required) { if (signatureData.required) {
return 'no' return ItemSignatureValidationResult.NotTrusted
} }
return 'not-applicable' return ItemSignatureValidationResult.NotApplicable
} }
const signatureResult = signatureData.result const signatureResult = signatureData.result
if (!signatureResult.passes) { if (!signatureResult.passes) {
return 'no' return ItemSignatureValidationResult.NotTrusted
} }
const signerPublicKey = signatureResult.publicKey const signerPublicKey = signatureResult.publicKey
const trustedContact = this.findContactUseCase.execute({ signingPublicKey: signerPublicKey }) const trustedContact = this.findContact.execute({ signingPublicKey: signerPublicKey })
if (trustedContact) { if (trustedContact.isFailed()) {
return 'yes' return ItemSignatureValidationResult.NotTrusted
} }
return 'no' const trustStatus = trustedContact.getValue().getTrustStatusForSigningPublicKey(signerPublicKey)
if (trustStatus === PublicKeyTrustStatus.Trusted) {
return ItemSignatureValidationResult.Trusted
} else if (trustStatus === PublicKeyTrustStatus.Previous) {
return ItemSignatureValidationResult.SignedWithNonCurrentKey
}
return ItemSignatureValidationResult.NotTrusted
} }
} }

View File

@@ -1 +0,0 @@
export type ValidateItemSignerResult = 'not-applicable' | 'yes' | 'no'

View File

@@ -1,4 +1,11 @@
import { AsymmetricSignatureVerificationDetachedResult } from '../../Operator/Types/AsymmetricSignatureVerificationDetachedResult' import {
AsymmetricSignatureVerificationDetachedResult,
SNRootKeyParams,
KeyedDecryptionSplit,
KeyedEncryptionSplit,
ItemAuthenticatedData,
AsymmetricallyEncryptedString,
} from '@standardnotes/encryption'
import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common' import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
import { import {
BackupFile, BackupFile,
@@ -9,23 +16,14 @@ import {
RootKeyInterface, RootKeyInterface,
KeySystemIdentifier, KeySystemIdentifier,
KeySystemItemsKeyInterface, KeySystemItemsKeyInterface,
AsymmetricMessagePayload,
KeySystemRootKeyInterface, KeySystemRootKeyInterface,
KeySystemRootKeyParamsInterface, KeySystemRootKeyParamsInterface,
TrustedContactInterface, PortablePublicKeySet,
} from '@standardnotes/models' } from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
import { KeyedDecryptionSplit } from '../../Split/KeyedDecryptionSplit'
import { KeyedEncryptionSplit } from '../../Split/KeyedEncryptionSplit'
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { PublicKeySet } from '../../Operator/Types/PublicKeySet'
import { KeySystemKeyManagerInterface } from '../KeySystemKeyManagerInterface'
import { AsymmetricallyEncryptedString } from '../../Operator/Types/Types'
export interface EncryptionProviderInterface { export interface EncryptionProviderInterface {
keys: KeySystemKeyManagerInterface initialize(): Promise<void>
encryptSplitSingle(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface> encryptSplitSingle(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface>
encryptSplit(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface[]> encryptSplit(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface[]>
@@ -51,10 +49,12 @@ export interface EncryptionProviderInterface {
isVersionNewerThanLibraryVersion(version: ProtocolVersion): boolean isVersionNewerThanLibraryVersion(version: ProtocolVersion): boolean
platformSupportsKeyDerivation(keyParams: SNRootKeyParams): boolean platformSupportsKeyDerivation(keyParams: SNRootKeyParams): boolean
decryptBackupFile( getPasswordCreatedDate(): Date | undefined
file: BackupFile, getEncryptionDisplayName(): Promise<string>
password?: string, upgradeAvailable(): Promise<boolean>
): Promise<ClientDisplayableError | (EncryptedPayloadInterface | DecryptedPayloadInterface)[]>
createEncryptedBackupFile(): Promise<BackupFile>
createDecryptedBackupFile(): BackupFile
getUserVersion(): ProtocolVersion | undefined getUserVersion(): ProtocolVersion | undefined
hasAccount(): boolean hasAccount(): boolean
@@ -75,6 +75,7 @@ export interface EncryptionProviderInterface {
decryptErroredPayloads(): Promise<void> decryptErroredPayloads(): Promise<void>
deleteWorkspaceSpecificKeyStateFromDevice(): Promise<void> deleteWorkspaceSpecificKeyStateFromDevice(): Promise<void>
unwrapRootKey(wrappingKey: RootKeyInterface): Promise<void>
computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface> computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface>
computeWrappingKey(passcode: string): Promise<RootKeyInterface> computeWrappingKey(passcode: string): Promise<RootKeyInterface>
hasRootKeyEncryptionSource(): boolean hasRootKeyEncryptionSource(): boolean
@@ -110,24 +111,11 @@ export interface EncryptionProviderInterface {
rootKeyToken: string, rootKeyToken: string,
): KeySystemItemsKeyInterface ): KeySystemItemsKeyInterface
reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise<void>
getKeyPair(): PkcKeyPair getKeyPair(): PkcKeyPair
getSigningKeyPair(): PkcKeyPair getSigningKeyPair(): PkcKeyPair
asymmetricallyEncryptMessage(dto: {
message: AsymmetricMessagePayload
senderKeyPair: PkcKeyPair
senderSigningKeyPair: PkcKeyPair
recipientPublicKey: string
}): string
asymmetricallyDecryptMessage<M extends AsymmetricMessagePayload>(dto: {
encryptedString: AsymmetricallyEncryptedString
trustedSender: TrustedContactInterface | undefined
privateKey: string
}): M | undefined
asymmetricSignatureVerifyDetached( asymmetricSignatureVerifyDetached(
encryptedString: AsymmetricallyEncryptedString, encryptedString: AsymmetricallyEncryptedString,
): AsymmetricSignatureVerificationDetachedResult ): AsymmetricSignatureVerificationDetachedResult
getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: string): PublicKeySet getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: string): PortablePublicKeySet
} }

View File

@@ -1,3 +1,4 @@
import { FindDefaultItemsKey } from './UseCase/ItemsKey/FindDefaultItemsKey'
import { InternalEventInterface } from './../Internal/InternalEventInterface' import { InternalEventInterface } from './../Internal/InternalEventInterface'
import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface' import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
@@ -5,27 +6,22 @@ import {
CreateAnyKeyParams, CreateAnyKeyParams,
CreateEncryptionSplitWithKeyLookup, CreateEncryptionSplitWithKeyLookup,
encryptedInputParametersFromPayload, encryptedInputParametersFromPayload,
EncryptionProviderInterface,
ErrorDecryptingParameters, ErrorDecryptingParameters,
findDefaultItemsKey,
FindPayloadInDecryptionSplit, FindPayloadInDecryptionSplit,
FindPayloadInEncryptionSplit, FindPayloadInEncryptionSplit,
isErrorDecryptingParameters, isErrorDecryptingParameters,
ItemAuthenticatedData, ItemAuthenticatedData,
KeyedDecryptionSplit, KeyedDecryptionSplit,
KeyedEncryptionSplit, KeyedEncryptionSplit,
KeyMode,
LegacyAttachedData, LegacyAttachedData,
OperatorManager,
RootKeyEncryptedAuthenticatedData, RootKeyEncryptedAuthenticatedData,
SplitPayloadsByEncryptionType, SplitPayloadsByEncryptionType,
V001Algorithm, V001Algorithm,
V002Algorithm, V002Algorithm,
PublicKeySet,
EncryptedOutputParameters, EncryptedOutputParameters,
KeySystemKeyManagerInterface,
AsymmetricSignatureVerificationDetachedResult, AsymmetricSignatureVerificationDetachedResult,
AsymmetricallyEncryptedString, AsymmetricallyEncryptedString,
EncryptionOperatorsInterface,
} from '@standardnotes/encryption' } from '@standardnotes/encryption'
import { import {
BackupFile, BackupFile,
@@ -42,13 +38,11 @@ import {
RootKeyInterface, RootKeyInterface,
KeySystemItemsKeyInterface, KeySystemItemsKeyInterface,
KeySystemIdentifier, KeySystemIdentifier,
AsymmetricMessagePayload,
KeySystemRootKeyInterface, KeySystemRootKeyInterface,
KeySystemRootKeyParamsInterface, KeySystemRootKeyParamsInterface,
TrustedContactInterface, PortablePublicKeySet,
RootKeyParamsInterface, RootKeyParamsInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { import {
extendArray, extendArray,
@@ -60,7 +54,6 @@ import {
} from '@standardnotes/utils' } from '@standardnotes/utils'
import { import {
AnyKeyParamsContent, AnyKeyParamsContent,
ApplicationIdentifier,
compareVersions, compareVersions,
isVersionLessThanOrEqualTo, isVersionLessThanOrEqualTo,
KeyParamsOrigination, KeyParamsOrigination,
@@ -70,28 +63,27 @@ import {
} from '@standardnotes/common' } from '@standardnotes/common'
import { AbstractService } from '../Service/AbstractService' import { AbstractService } from '../Service/AbstractService'
import { ItemsEncryptionService } from './ItemsEncryption' import { ItemsEncryptionService } from '../ItemsEncryption/ItemsEncryption'
import { ItemManagerInterface } from '../Item/ItemManagerInterface' import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface' import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
import { DeviceInterface } from '../Device/DeviceInterface'
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { SyncEvent } from '../Event/SyncEvent' import { SyncEvent } from '../Event/SyncEvent'
import { DecryptBackupFileUseCase } from './DecryptBackupFileUseCase'
import { EncryptionServiceEvent } from './EncryptionServiceEvent' import { EncryptionServiceEvent } from './EncryptionServiceEvent'
import { DecryptedParameters } from '@standardnotes/encryption/src/Domain/Types/DecryptedParameters' import { DecryptedParameters } from '@standardnotes/encryption/src/Domain/Types/DecryptedParameters'
import { RootKeyManager } from './RootKey/RootKeyManager' import { RootKeyManager } from '../RootKeyManager/RootKeyManager'
import { RootKeyManagerEvent } from './RootKey/RootKeyManagerEvent' import { RootKeyManagerEvent } from '../RootKeyManager/RootKeyManagerEvent'
import { CreateNewItemsKeyWithRollbackUseCase } from './UseCase/ItemsKey/CreateNewItemsKeyWithRollback' import { CreateNewItemsKeyWithRollback } from './UseCase/ItemsKey/CreateNewItemsKeyWithRollback'
import { DecryptErroredRootPayloadsUseCase } from './UseCase/RootEncryption/DecryptErroredPayloads' import { DecryptErroredTypeAPayloads } from './UseCase/TypeA/DecryptErroredPayloads'
import { CreateNewDefaultItemsKeyUseCase } from './UseCase/ItemsKey/CreateNewDefaultItemsKey' import { CreateNewDefaultItemsKey } from './UseCase/ItemsKey/CreateNewDefaultItemsKey'
import { RootKeyDecryptPayloadUseCase } from './UseCase/RootEncryption/DecryptPayload' import { DecryptTypeAPayload } from './UseCase/TypeA/DecryptPayload'
import { RootKeyDecryptPayloadWithKeyLookupUseCase } from './UseCase/RootEncryption/DecryptPayloadWithKeyLookup' import { DecryptTypeAPayloadWithKeyLookup } from './UseCase/TypeA/DecryptPayloadWithKeyLookup'
import { RootKeyEncryptPayloadWithKeyLookupUseCase } from './UseCase/RootEncryption/EncryptPayloadWithKeyLookup' import { EncryptTypeAPayloadWithKeyLookup } from './UseCase/TypeA/EncryptPayloadWithKeyLookup'
import { RootKeyEncryptPayloadUseCase } from './UseCase/RootEncryption/EncryptPayload' import { EncryptTypeAPayload } from './UseCase/TypeA/EncryptPayload'
import { ValidateAccountPasswordResult } from './RootKey/ValidateAccountPasswordResult' import { ValidateAccountPasswordResult } from '../RootKeyManager/ValidateAccountPasswordResult'
import { ValidatePasscodeResult } from './RootKey/ValidatePasscodeResult' import { ValidatePasscodeResult } from '../RootKeyManager/ValidatePasscodeResult'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { EncryptionProviderInterface } from './EncryptionProviderInterface'
import { KeyMode } from '../RootKeyManager/KeyMode'
/** /**
* The encryption service is responsible for the encryption and decryption of payloads, and * The encryption service is responsible for the encryption and decryption of payloads, and
@@ -124,40 +116,28 @@ export class EncryptionService
extends AbstractService<EncryptionServiceEvent> extends AbstractService<EncryptionServiceEvent>
implements EncryptionProviderInterface, InternalEventHandlerInterface implements EncryptionProviderInterface, InternalEventHandlerInterface
{ {
private operators: OperatorManager
private readonly itemsEncryption: ItemsEncryptionService
private readonly rootKeyManager: RootKeyManager
constructor( constructor(
private items: ItemManagerInterface, private items: ItemManagerInterface,
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private payloads: PayloadManagerInterface, private payloads: PayloadManagerInterface,
public device: DeviceInterface, private operators: EncryptionOperatorsInterface,
private storage: StorageServiceInterface, private itemsEncryption: ItemsEncryptionService,
public readonly keys: KeySystemKeyManagerInterface, private rootKeyManager: RootKeyManager,
identifier: ApplicationIdentifier, private crypto: PureCryptoInterface,
public crypto: PureCryptoInterface, private _createNewItemsKeyWithRollback: CreateNewItemsKeyWithRollback,
private _findDefaultItemsKey: FindDefaultItemsKey,
private _decryptErroredRootPayloads: DecryptErroredTypeAPayloads,
private _rootKeyEncryptPayloadWithKeyLookup: EncryptTypeAPayloadWithKeyLookup,
private _rootKeyEncryptPayload: EncryptTypeAPayload,
private _rootKeyDecryptPayload: DecryptTypeAPayload,
private _rootKeyDecryptPayloadWithKeyLookup: DecryptTypeAPayloadWithKeyLookup,
private _createDefaultItemsKey: CreateNewDefaultItemsKey,
protected override internalEventBus: InternalEventBusInterface, protected override internalEventBus: InternalEventBusInterface,
) { ) {
super(internalEventBus) super(internalEventBus)
this.crypto = crypto
this.operators = new OperatorManager(crypto)
this.rootKeyManager = new RootKeyManager(
device,
storage,
items,
mutator,
this.operators,
identifier,
internalEventBus,
)
internalEventBus.addEventHandler(this, RootKeyManagerEvent.RootKeyManagerKeyStatusChanged) internalEventBus.addEventHandler(this, RootKeyManagerEvent.RootKeyManagerKeyStatusChanged)
this.itemsEncryption = new ItemsEncryptionService(items, payloads, storage, this.operators, keys, internalEventBus)
UuidGenerator.SetGenerator(this.crypto.generateUUID) UuidGenerator.SetGenerator(this.crypto.generateUUID)
} }
@@ -168,25 +148,21 @@ export class EncryptionService
} }
} }
public override async blockDeinit(): Promise<void> {
await Promise.all([this.rootKeyManager.blockDeinit(), this.itemsEncryption.blockDeinit()])
return super.blockDeinit()
}
public override deinit(): void { public override deinit(): void {
;(this.items as unknown) = undefined ;(this.items as unknown) = undefined
;(this.payloads as unknown) = undefined ;(this.payloads as unknown) = undefined
;(this.device as unknown) = undefined
;(this.storage as unknown) = undefined
;(this.crypto as unknown) = undefined
;(this.operators as unknown) = undefined ;(this.operators as unknown) = undefined
this.itemsEncryption.deinit()
;(this.itemsEncryption as unknown) = undefined ;(this.itemsEncryption as unknown) = undefined
this.rootKeyManager.deinit()
;(this.rootKeyManager as unknown) = undefined ;(this.rootKeyManager as unknown) = undefined
;(this.crypto as unknown) = undefined
;(this._createNewItemsKeyWithRollback as unknown) = undefined
;(this._findDefaultItemsKey as unknown) = undefined
;(this._decryptErroredRootPayloads as unknown) = undefined
;(this._rootKeyEncryptPayloadWithKeyLookup as unknown) = undefined
;(this._rootKeyEncryptPayload as unknown) = undefined
;(this._rootKeyDecryptPayload as unknown) = undefined
;(this._rootKeyDecryptPayloadWithKeyLookup as unknown) = undefined
;(this._createDefaultItemsKey as unknown) = undefined
super.deinit() super.deinit()
} }
@@ -217,7 +193,7 @@ export class EncryptionService
return !!this.getRootKey()?.signingKeyPair return !!this.getRootKey()?.signingKeyPair
} }
public async initialize() { public async initialize(): Promise<void> {
await this.rootKeyManager.initialize() await this.rootKeyManager.initialize()
} }
@@ -250,7 +226,7 @@ export class EncryptionService
return this.rootKeyManager.getUserVersion() return this.rootKeyManager.getUserVersion()
} }
public async upgradeAvailable() { public async upgradeAvailable(): Promise<boolean> {
const accountUpgradeAvailable = this.accountUpgradeAvailable() const accountUpgradeAvailable = this.accountUpgradeAvailable()
const passcodeUpgradeAvailable = await this.passcodeUpgradeAvailable() const passcodeUpgradeAvailable = await this.passcodeUpgradeAvailable()
return accountUpgradeAvailable || passcodeUpgradeAvailable return accountUpgradeAvailable || passcodeUpgradeAvailable
@@ -268,31 +244,12 @@ export class EncryptionService
await this.rootKeyManager.reencryptApplicableItemsAfterUserRootKeyChange() await this.rootKeyManager.reencryptApplicableItemsAfterUserRootKeyChange()
} }
/**
* When the key system root key changes, we must re-encrypt all vault items keys
* with this new key system root key (by simply re-syncing).
*/
public async reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise<void> {
const keySystemItemsKeys = this.keys.getKeySystemItemsKeys(keySystemIdentifier)
if (keySystemItemsKeys.length > 0) {
await this.mutator.setItemsDirty(keySystemItemsKeys)
}
}
public async createNewItemsKeyWithRollback(): Promise<() => Promise<void>> { public async createNewItemsKeyWithRollback(): Promise<() => Promise<void>> {
const usecase = new CreateNewItemsKeyWithRollbackUseCase( return this._createNewItemsKeyWithRollback.execute()
this.mutator,
this.items,
this.storage,
this.operators,
this.rootKeyManager,
)
return usecase.execute()
} }
public async decryptErroredPayloads(): Promise<void> { public async decryptErroredPayloads(): Promise<void> {
const usecase = new DecryptErroredRootPayloadsUseCase(this.payloads, this.operators, this.keys, this.rootKeyManager) await this._decryptErroredRootPayloads.execute()
await usecase.execute()
await this.itemsEncryption.decryptErroredItemPayloads() await this.itemsEncryption.decryptErroredItemPayloads()
} }
@@ -326,18 +283,10 @@ export class EncryptionService
usesKeySystemRootKeyWithKeyLookup, usesKeySystemRootKeyWithKeyLookup,
} = split } = split
const rootKeyEncryptWithKeyLookupUsecase = new RootKeyEncryptPayloadWithKeyLookupUseCase(
this.operators,
this.keys,
this.rootKeyManager,
)
const rootKeyEncryptUsecase = new RootKeyEncryptPayloadUseCase(this.operators)
const signingKeyPair = this.hasSigningKeyPair() ? this.getSigningKeyPair() : undefined const signingKeyPair = this.hasSigningKeyPair() ? this.getSigningKeyPair() : undefined
if (usesRootKey) { if (usesRootKey) {
const rootKeyEncrypted = await rootKeyEncryptUsecase.executeMany( const rootKeyEncrypted = await this._rootKeyEncryptPayload.executeMany(
usesRootKey.items, usesRootKey.items,
usesRootKey.key, usesRootKey.key,
signingKeyPair, signingKeyPair,
@@ -346,7 +295,7 @@ export class EncryptionService
} }
if (usesRootKeyWithKeyLookup) { if (usesRootKeyWithKeyLookup) {
const rootKeyEncrypted = await rootKeyEncryptWithKeyLookupUsecase.executeMany( const rootKeyEncrypted = await this._rootKeyEncryptPayloadWithKeyLookup.executeMany(
usesRootKeyWithKeyLookup.items, usesRootKeyWithKeyLookup.items,
signingKeyPair, signingKeyPair,
) )
@@ -354,7 +303,7 @@ export class EncryptionService
} }
if (usesKeySystemRootKey) { if (usesKeySystemRootKey) {
const keySystemRootKeyEncrypted = await rootKeyEncryptUsecase.executeMany( const keySystemRootKeyEncrypted = await this._rootKeyEncryptPayload.executeMany(
usesKeySystemRootKey.items, usesKeySystemRootKey.items,
usesKeySystemRootKey.key, usesKeySystemRootKey.key,
signingKeyPair, signingKeyPair,
@@ -363,7 +312,7 @@ export class EncryptionService
} }
if (usesKeySystemRootKeyWithKeyLookup) { if (usesKeySystemRootKeyWithKeyLookup) {
const keySystemRootKeyEncrypted = await rootKeyEncryptWithKeyLookupUsecase.executeMany( const keySystemRootKeyEncrypted = await this._rootKeyEncryptPayloadWithKeyLookup.executeMany(
usesKeySystemRootKeyWithKeyLookup.items, usesKeySystemRootKeyWithKeyLookup.items,
signingKeyPair, signingKeyPair,
) )
@@ -423,32 +372,26 @@ export class EncryptionService
usesKeySystemRootKeyWithKeyLookup, usesKeySystemRootKeyWithKeyLookup,
} = split } = split
const rootKeyDecryptUseCase = new RootKeyDecryptPayloadUseCase(this.operators)
const rootKeyDecryptWithKeyLookupUsecase = new RootKeyDecryptPayloadWithKeyLookupUseCase(
this.operators,
this.keys,
this.rootKeyManager,
)
if (usesRootKey) { if (usesRootKey) {
const rootKeyDecrypted = await rootKeyDecryptUseCase.executeMany<C>(usesRootKey.items, usesRootKey.key) const rootKeyDecrypted = await this._rootKeyDecryptPayload.executeMany<C>(usesRootKey.items, usesRootKey.key)
extendArray(resultParams, rootKeyDecrypted) extendArray(resultParams, rootKeyDecrypted)
} }
if (usesRootKeyWithKeyLookup) { if (usesRootKeyWithKeyLookup) {
const rootKeyDecrypted = await rootKeyDecryptWithKeyLookupUsecase.executeMany<C>(usesRootKeyWithKeyLookup.items) const rootKeyDecrypted = await this._rootKeyDecryptPayloadWithKeyLookup.executeMany<C>(
usesRootKeyWithKeyLookup.items,
)
extendArray(resultParams, rootKeyDecrypted) extendArray(resultParams, rootKeyDecrypted)
} }
if (usesKeySystemRootKey) { if (usesKeySystemRootKey) {
const keySystemRootKeyDecrypted = await rootKeyDecryptUseCase.executeMany<C>( const keySystemRootKeyDecrypted = await this._rootKeyDecryptPayload.executeMany<C>(
usesKeySystemRootKey.items, usesKeySystemRootKey.items,
usesKeySystemRootKey.key, usesKeySystemRootKey.key,
) )
extendArray(resultParams, keySystemRootKeyDecrypted) extendArray(resultParams, keySystemRootKeyDecrypted)
} }
if (usesKeySystemRootKeyWithKeyLookup) { if (usesKeySystemRootKeyWithKeyLookup) {
const keySystemRootKeyDecrypted = await rootKeyDecryptWithKeyLookupUsecase.executeMany<C>( const keySystemRootKeyDecrypted = await this._rootKeyDecryptPayloadWithKeyLookup.executeMany<C>(
usesKeySystemRootKeyWithKeyLookup.items, usesKeySystemRootKeyWithKeyLookup.items,
) )
extendArray(resultParams, keySystemRootKeyDecrypted) extendArray(resultParams, keySystemRootKeyDecrypted)
@@ -640,56 +583,6 @@ export class EncryptionService
.createKeySystemItemsKey(uuid, keySystemIdentifier, sharedVaultUuid, rootKeyToken) .createKeySystemItemsKey(uuid, keySystemIdentifier, sharedVaultUuid, rootKeyToken)
} }
asymmetricallyEncryptMessage(dto: {
message: AsymmetricMessagePayload
senderKeyPair: PkcKeyPair
senderSigningKeyPair: PkcKeyPair
recipientPublicKey: string
}): AsymmetricallyEncryptedString {
const operator = this.operators.defaultOperator()
const encrypted = operator.asymmetricEncrypt({
stringToEncrypt: JSON.stringify(dto.message),
senderKeyPair: dto.senderKeyPair,
senderSigningKeyPair: dto.senderSigningKeyPair,
recipientPublicKey: dto.recipientPublicKey,
})
return encrypted
}
asymmetricallyDecryptMessage<M extends AsymmetricMessagePayload>(dto: {
encryptedString: AsymmetricallyEncryptedString
trustedSender: TrustedContactInterface | undefined
privateKey: string
}): M | undefined {
const defaultOperator = this.operators.defaultOperator()
const version = defaultOperator.versionForAsymmetricallyEncryptedString(dto.encryptedString)
const keyOperator = this.operators.operatorForVersion(version)
const decryptedResult = keyOperator.asymmetricDecrypt({
stringToDecrypt: dto.encryptedString,
recipientSecretKey: dto.privateKey,
})
if (!decryptedResult) {
return undefined
}
if (!decryptedResult.signatureVerified) {
return undefined
}
if (dto.trustedSender) {
if (!dto.trustedSender.isPublicKeyTrusted(decryptedResult.senderPublicKey)) {
return undefined
}
if (!dto.trustedSender.isSigningKeyTrusted(decryptedResult.signaturePublicKey)) {
return undefined
}
}
return JSON.parse(decryptedResult.plaintext)
}
asymmetricSignatureVerifyDetached( asymmetricSignatureVerifyDetached(
encryptedString: AsymmetricallyEncryptedString, encryptedString: AsymmetricallyEncryptedString,
): AsymmetricSignatureVerificationDetachedResult { ): AsymmetricSignatureVerificationDetachedResult {
@@ -699,7 +592,7 @@ export class EncryptionService
return keyOperator.asymmetricSignatureVerifyDetached(encryptedString) return keyOperator.asymmetricSignatureVerifyDetached(encryptedString)
} }
getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PublicKeySet { getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PortablePublicKeySet {
const defaultOperator = this.operators.defaultOperator() const defaultOperator = this.operators.defaultOperator()
const version = defaultOperator.versionForAsymmetricallyEncryptedString(string) const version = defaultOperator.versionForAsymmetricallyEncryptedString(string)
@@ -707,15 +600,6 @@ export class EncryptionService
return keyOperator.getSenderPublicKeySetFromAsymmetricallyEncryptedString(string) return keyOperator.getSenderPublicKeySetFromAsymmetricallyEncryptedString(string)
} }
public async decryptBackupFile(
file: BackupFile,
password?: string,
): Promise<ClientDisplayableError | (EncryptedPayloadInterface | DecryptedPayloadInterface<ItemContent>)[]> {
const usecase = new DecryptBackupFileUseCase(this)
const result = await usecase.execute(file, password)
return result
}
/** /**
* Creates a key params object from a raw object * Creates a key params object from a raw object
* @param keyParams - The raw key params object to create a KeyParams object from * @param keyParams - The raw key params object to create a KeyParams object from
@@ -800,7 +684,7 @@ export class EncryptionService
* If so, they must generate the unwrapping key by getting our saved wrapping key keyParams. * If so, they must generate the unwrapping key by getting our saved wrapping key keyParams.
* After unwrapping, the root key is automatically loaded. * After unwrapping, the root key is automatically loaded.
*/ */
public async unwrapRootKey(wrappingKey: RootKeyInterface) { public async unwrapRootKey(wrappingKey: RootKeyInterface): Promise<void> {
return this.rootKeyManager.unwrapRootKey(wrappingKey) return this.rootKeyManager.unwrapRootKey(wrappingKey)
} }
/** /**
@@ -902,7 +786,7 @@ export class EncryptionService
* A new root key based items key is needed if our default items key content * A new root key based items key is needed if our default items key content
* isnt equal to our current root key * isnt equal to our current root key
*/ */
const defaultItemsKey = findDefaultItemsKey(this.itemsEncryption.getItemsKeys()) const defaultItemsKey = this._findDefaultItemsKey.execute(this.itemsEncryption.getItemsKeys()).getValue()
/** Shouldn't be undefined, but if it is, we'll take the corrective action */ /** Shouldn't be undefined, but if it is, we'll take the corrective action */
if (!defaultItemsKey) { if (!defaultItemsKey) {
@@ -913,8 +797,7 @@ export class EncryptionService
} }
public async createNewDefaultItemsKey(): Promise<ItemsKeyInterface> { public async createNewDefaultItemsKey(): Promise<ItemsKeyInterface> {
const usecase = new CreateNewDefaultItemsKeyUseCase(this.mutator, this.items, this.operators, this.rootKeyManager) return this._createDefaultItemsKey.execute()
return usecase.execute()
} }
public getPasswordCreatedDate(): Date | undefined { public getPasswordCreatedDate(): Date | undefined {
@@ -1001,7 +884,7 @@ export class EncryptionService
private async handleFullSyncCompletion() { private async handleFullSyncCompletion() {
/** Always create a new items key after full sync, if no items key is found */ /** Always create a new items key after full sync, if no items key is found */
const currentItemsKey = findDefaultItemsKey(this.itemsEncryption.getItemsKeys()) const currentItemsKey = this._findDefaultItemsKey.execute(this.itemsEncryption.getItemsKeys()).getValue()
if (!currentItemsKey) { if (!currentItemsKey) {
await this.createNewDefaultItemsKey() await this.createNewDefaultItemsKey()
if (this.rootKeyManager.getKeyMode() === KeyMode.WrapperOnly) { if (this.rootKeyManager.getKeyMode() === KeyMode.WrapperOnly) {

View File

@@ -5,11 +5,12 @@ import {
ItemsKeyContent, ItemsKeyContent,
RootKeyInterface, RootKeyInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { EncryptionProviderInterface, KeyRecoveryStrings, SNRootKeyParams } from '@standardnotes/encryption' import { KeyRecoveryStrings, SNRootKeyParams } from '@standardnotes/encryption'
import { ChallengeServiceInterface } from '../Challenge/ChallengeServiceInterface' import { ChallengeServiceInterface } from '../Challenge/ChallengeServiceInterface'
import { ChallengePrompt } from '../Challenge/Prompt/ChallengePrompt' import { ChallengePrompt } from '../Challenge/Prompt/ChallengePrompt'
import { ChallengeReason } from '../Challenge/Types/ChallengeReason' import { ChallengeReason } from '../Challenge/Types/ChallengeReason'
import { ChallengeValidation } from '../Challenge/Types/ChallengeValidation' import { ChallengeValidation } from '../Challenge/Types/ChallengeValidation'
import { EncryptionProviderInterface } from './EncryptionProviderInterface'
export async function DecryptItemsKeyWithUserFallback( export async function DecryptItemsKeyWithUserFallback(
itemsKey: EncryptedPayloadInterface, itemsKey: EncryptedPayloadInterface,

View File

@@ -0,0 +1,178 @@
import {
ContactPublicKeySet,
ContactPublicKeySetInterface,
PublicKeyTrustStatus,
TrustedContactInterface,
} from '@standardnotes/models'
import { DecryptMessage } from './DecryptMessage'
import { OperatorInterface, EncryptionOperatorsInterface } from '@standardnotes/encryption'
import { ProtocolVersion } from '@standardnotes/common'
function createMockPublicKeySetChain(): ContactPublicKeySetInterface {
const nMinusOne = new ContactPublicKeySet({
encryption: 'encryption-public-key-n-1',
signing: 'signing-public-key-n-1',
timestamp: new Date(-1),
previousKeySet: undefined,
})
const root = new ContactPublicKeySet({
encryption: 'encryption-public-key',
signing: 'signing-public-key',
timestamp: new Date(),
previousKeySet: nMinusOne,
})
return root
}
describe('DecryptMessage', () => {
let usecase: DecryptMessage
let operator: jest.Mocked<OperatorInterface>
beforeEach(() => {
operator = {} as jest.Mocked<OperatorInterface>
operator.versionForAsymmetricallyEncryptedString = jest.fn().mockReturnValue(ProtocolVersion.V004)
const operators = {} as jest.Mocked<EncryptionOperatorsInterface>
operators.defaultOperator = jest.fn().mockReturnValue(operator)
operators.operatorForVersion = jest.fn().mockReturnValue(operator)
usecase = new DecryptMessage(operators)
})
it('should fail if fails to decrypt', () => {
operator.asymmetricDecrypt = jest.fn().mockReturnValue(null)
const result = usecase.execute({
message: 'encrypted',
sender: undefined,
privateKey: 'private-key',
})
expect(result.isFailed()).toEqual(true)
expect(result.getError()).toEqual('Failed to decrypt message')
})
it('should fail if signature is invalid', () => {
operator.asymmetricDecrypt = jest.fn().mockReturnValue({
plaintext: 'decrypted',
signatureVerified: false,
signaturePublicKey: 'signing-public-key',
senderPublicKey: 'encryption-public-key',
})
const result = usecase.execute({
message: 'encrypted',
sender: undefined,
privateKey: 'private-key',
})
expect(result.isFailed()).toEqual(true)
expect(result.getError()).toEqual('Failed to verify signature')
})
describe('with trusted sender', () => {
it('should fail if encryption public key is not trusted', () => {
operator.asymmetricDecrypt = jest.fn().mockReturnValue({
plaintext: 'decrypted',
signatureVerified: true,
signaturePublicKey: 'signing-public-key',
senderPublicKey: 'encryption-public-key',
})
const senderContact = {
name: 'Other',
contactUuid: '456',
publicKeySet: createMockPublicKeySetChain(),
isMe: false,
} as jest.Mocked<TrustedContactInterface>
senderContact.getTrustStatusForPublicKey = jest.fn().mockReturnValue(PublicKeyTrustStatus.NotTrusted)
const result = usecase.execute({
message: 'encrypted',
sender: senderContact,
privateKey: 'private-key',
})
expect(result.isFailed()).toEqual(true)
expect(result.getError()).toEqual('Sender public key is not trusted')
})
it('should fail if signing public key is not trusted', () => {
operator.asymmetricDecrypt = jest.fn().mockReturnValue({
plaintext: 'decrypted',
signatureVerified: true,
signaturePublicKey: 'signing-public-key',
senderPublicKey: 'encryption-public-key',
})
const senderContact = {
name: 'Other',
contactUuid: '456',
publicKeySet: createMockPublicKeySetChain(),
isMe: false,
} as jest.Mocked<TrustedContactInterface>
senderContact.getTrustStatusForPublicKey = jest.fn().mockReturnValue(PublicKeyTrustStatus.Trusted)
senderContact.getTrustStatusForSigningPublicKey = jest.fn().mockReturnValue(PublicKeyTrustStatus.NotTrusted)
const result = usecase.execute({
message: 'encrypted',
sender: senderContact,
privateKey: 'private-key',
})
expect(result.isFailed()).toEqual(true)
expect(result.getError()).toEqual('Signature public key is not trusted')
})
it('should succeed with valid signature and encryption key', () => {
operator.asymmetricDecrypt = jest.fn().mockReturnValue({
plaintext: '{"foo": "bar"}',
signatureVerified: true,
signaturePublicKey: 'signing-public-key',
senderPublicKey: 'encryption-public-key',
})
const senderContact = {
name: 'Other',
contactUuid: '456',
publicKeySet: createMockPublicKeySetChain(),
isMe: false,
} as jest.Mocked<TrustedContactInterface>
senderContact.getTrustStatusForPublicKey = jest.fn().mockReturnValue(PublicKeyTrustStatus.Trusted)
senderContact.getTrustStatusForSigningPublicKey = jest.fn().mockReturnValue(PublicKeyTrustStatus.Trusted)
const result = usecase.execute({
message: 'encrypted',
sender: senderContact,
privateKey: 'private-key',
})
expect(result.isFailed()).toEqual(false)
expect(result.getValue()).toEqual({ foo: 'bar' })
})
})
describe('without trusted sender', () => {
it('should succeed with valid signature and encryption key', () => {
operator.asymmetricDecrypt = jest.fn().mockReturnValue({
plaintext: '{"foo": "bar"}',
signatureVerified: true,
signaturePublicKey: 'signing-public-key',
senderPublicKey: 'encryption-public-key',
})
const result = usecase.execute({
message: 'encrypted',
sender: undefined,
privateKey: 'private-key',
})
expect(result.isFailed()).toEqual(false)
expect(result.getValue()).toEqual({ foo: 'bar' })
})
})
})

View File

@@ -0,0 +1,44 @@
import { SyncUseCaseInterface, Result } from '@standardnotes/domain-core'
import { EncryptionOperatorsInterface } from '@standardnotes/encryption'
import { AsymmetricMessagePayload, PublicKeyTrustStatus, TrustedContactInterface } from '@standardnotes/models'
export class DecryptMessage implements SyncUseCaseInterface<AsymmetricMessagePayload> {
constructor(private operators: EncryptionOperatorsInterface) {}
execute<M extends AsymmetricMessagePayload>(dto: {
message: string
sender: TrustedContactInterface | undefined
privateKey: string
}): Result<M> {
const defaultOperator = this.operators.defaultOperator()
const version = defaultOperator.versionForAsymmetricallyEncryptedString(dto.message)
const keyOperator = this.operators.operatorForVersion(version)
const decryptedResult = keyOperator.asymmetricDecrypt({
stringToDecrypt: dto.message,
recipientSecretKey: dto.privateKey,
})
if (!decryptedResult) {
return Result.fail('Failed to decrypt message')
}
if (!decryptedResult.signatureVerified) {
return Result.fail('Failed to verify signature')
}
if (dto.sender) {
const publicKeyTrustStatus = dto.sender.getTrustStatusForPublicKey(decryptedResult.senderPublicKey)
if (publicKeyTrustStatus !== PublicKeyTrustStatus.Trusted) {
return Result.fail('Sender public key is not trusted')
}
const signingKeyTrustStatus = dto.sender.getTrustStatusForSigningPublicKey(decryptedResult.signaturePublicKey)
if (signingKeyTrustStatus !== PublicKeyTrustStatus.Trusted) {
return Result.fail('Signature public key is not trusted')
}
}
return Result.ok(JSON.parse(decryptedResult.plaintext))
}
}

View File

@@ -0,0 +1,31 @@
import { SyncUseCaseInterface, Result } from '@standardnotes/domain-core'
import { EncryptionOperatorsInterface } from '@standardnotes/encryption'
import { AsymmetricMessagePayload } from '@standardnotes/models'
export class DecryptOwnMessage<M extends AsymmetricMessagePayload> implements SyncUseCaseInterface<M> {
constructor(private operators: EncryptionOperatorsInterface) {}
execute(dto: { message: string; privateKey: string; recipientPublicKey: string }): Result<M> {
const defaultOperator = this.operators.defaultOperator()
const version = defaultOperator.versionForAsymmetricallyEncryptedString(dto.message)
const keyOperator = this.operators.operatorForVersion(version)
const result = keyOperator.asymmetricDecryptOwnMessage({
message: dto.message,
ownPrivateKey: dto.privateKey,
recipientPublicKey: dto.recipientPublicKey,
})
if (result.isFailed()) {
return Result.fail(result.getError())
}
const decryptedObject = result.getValue()
if (!decryptedObject.signatureVerified) {
return Result.fail('Failed to verify signature')
}
return Result.ok(JSON.parse(decryptedObject.plaintext))
}
}

View File

@@ -0,0 +1,28 @@
import { SyncUseCaseInterface, Result } from '@standardnotes/domain-core'
import { EncryptionOperatorsInterface } from '@standardnotes/encryption'
import { AsymmetricMessagePayload } from '@standardnotes/models'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
export class EncryptMessage implements SyncUseCaseInterface<string> {
constructor(private operators: EncryptionOperatorsInterface) {}
execute(dto: {
message: AsymmetricMessagePayload
keys: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
recipientPublicKey: string
}): Result<string> {
const operator = this.operators.defaultOperator()
const encrypted = operator.asymmetricEncrypt({
stringToEncrypt: JSON.stringify(dto.message),
senderKeyPair: dto.keys.encryption,
senderSigningKeyPair: dto.keys.signing,
recipientPublicKey: dto.recipientPublicKey,
})
return Result.ok(encrypted)
}
}

View File

@@ -0,0 +1,13 @@
import { SyncUseCaseInterface, Result } from '@standardnotes/domain-core'
import { AsymmetricallyEncryptedString, EncryptionOperatorsInterface } from '@standardnotes/encryption'
import { AsymmetricItemAdditionalData } from '@standardnotes/encryption/src/Domain/Types/EncryptionAdditionalData'
export class GetMessageAdditionalData implements SyncUseCaseInterface<AsymmetricItemAdditionalData> {
constructor(private operators: EncryptionOperatorsInterface) {}
execute(dto: { message: AsymmetricallyEncryptedString }): Result<AsymmetricItemAdditionalData> {
const operator = this.operators.defaultOperator()
return operator.asymmetricStringGetAdditionalData({ encryptedString: dto.message })
}
}

View File

@@ -37,10 +37,10 @@ import {
} from '@standardnotes/models' } from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses' import { ClientDisplayableError } from '@standardnotes/responses'
import { extendArray } from '@standardnotes/utils' import { extendArray } from '@standardnotes/utils'
import { EncryptionService } from './EncryptionService' import { EncryptionService } from '../EncryptionService'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
export class DecryptBackupFileUseCase { export class DecryptBackupFile {
constructor(private encryption: EncryptionService) {} constructor(private encryption: EncryptionService) {}
async execute( async execute(
@@ -53,7 +53,7 @@ export class DecryptBackupFileUseCase {
} else if (isDecryptedTransferPayload(item)) { } else if (isDecryptedTransferPayload(item)) {
return new DecryptedPayload(item) return new DecryptedPayload(item)
} else { } else {
throw Error('Unhandled case in decryptBackupFile') throw Error('Unhandled case in DecryptBackupFile')
} }
}) })

View File

@@ -1,4 +1,4 @@
import { OperatorManager } from '@standardnotes/encryption' import { EncryptionOperatorsInterface } from '@standardnotes/encryption'
import { ProtocolVersionLastNonrootItemsKey, ProtocolVersionLatest, compareVersions } from '@standardnotes/common' import { ProtocolVersionLastNonrootItemsKey, ProtocolVersionLatest, compareVersions } from '@standardnotes/common'
import { import {
CreateDecryptedItemFromPayload, CreateDecryptedItemFromPayload,
@@ -12,7 +12,7 @@ import {
import { UuidGenerator } from '@standardnotes/utils' import { UuidGenerator } from '@standardnotes/utils'
import { MutatorClientInterface } from '../../../Mutator/MutatorClientInterface' import { MutatorClientInterface } from '../../../Mutator/MutatorClientInterface'
import { ItemManagerInterface } from '../../../Item/ItemManagerInterface' import { ItemManagerInterface } from '../../../Item/ItemManagerInterface'
import { RootKeyManager } from '../../RootKey/RootKeyManager' import { RootKeyManager } from '../../../RootKeyManager/RootKeyManager'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
/** /**
@@ -20,11 +20,11 @@ import { ContentType } from '@standardnotes/domain-core'
* Consumer must call sync. If the protocol version <= 003, only one items key should be created, * Consumer must call sync. If the protocol version <= 003, only one items key should be created,
* and its .itemsKey value should be equal to the root key masterKey value. * and its .itemsKey value should be equal to the root key masterKey value.
*/ */
export class CreateNewDefaultItemsKeyUseCase { export class CreateNewDefaultItemsKey {
constructor( constructor(
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private items: ItemManagerInterface, private items: ItemManagerInterface,
private operatorManager: OperatorManager, private operators: EncryptionOperatorsInterface,
private rootKeyManager: RootKeyManager, private rootKeyManager: RootKeyManager,
) {} ) {}
@@ -48,7 +48,7 @@ export class CreateNewDefaultItemsKeyUseCase {
itemTemplate = CreateDecryptedItemFromPayload(payload) itemTemplate = CreateDecryptedItemFromPayload(payload)
} else { } else {
/** Create independent items key */ /** Create independent items key */
itemTemplate = this.operatorManager.operatorForVersion(operatorVersion).createItemsKey() itemTemplate = this.operators.operatorForVersion(operatorVersion).createItemsKey()
} }
const itemsKeys = this.items.getDisplayableItemsKeys() const itemsKeys = this.items.getDisplayableItemsKeys()

View File

@@ -1,35 +1,25 @@
import { StorageServiceInterface } from './../../../Storage/StorageServiceInterface' import { ItemsKeyMutator } from '@standardnotes/encryption'
import { ItemsKeyMutator, OperatorManager, findDefaultItemsKey } from '@standardnotes/encryption'
import { MutatorClientInterface } from '../../../Mutator/MutatorClientInterface' import { MutatorClientInterface } from '../../../Mutator/MutatorClientInterface'
import { ItemManagerInterface } from '../../../Item/ItemManagerInterface' import { ItemManagerInterface } from '../../../Item/ItemManagerInterface'
import { RootKeyManager } from '../../RootKey/RootKeyManager' import { CreateNewDefaultItemsKey } from './CreateNewDefaultItemsKey'
import { CreateNewDefaultItemsKeyUseCase } from './CreateNewDefaultItemsKey' import { RemoveItemsLocally } from '../../../UseCase/RemoveItemsLocally'
import { RemoveItemsLocallyUseCase } from '../../../UseCase/RemoveItemsLocally' import { FindDefaultItemsKey } from './FindDefaultItemsKey'
export class CreateNewItemsKeyWithRollbackUseCase {
private createDefaultItemsKeyUseCase = new CreateNewDefaultItemsKeyUseCase(
this.mutator,
this.items,
this.operatorManager,
this.rootKeyManager,
)
private removeItemsLocallyUsecase = new RemoveItemsLocallyUseCase(this.items, this.storage)
export class CreateNewItemsKeyWithRollback {
constructor( constructor(
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private items: ItemManagerInterface, private items: ItemManagerInterface,
private storage: StorageServiceInterface, private createDefaultItemsKey: CreateNewDefaultItemsKey,
private operatorManager: OperatorManager, private removeItemsLocally: RemoveItemsLocally,
private rootKeyManager: RootKeyManager, private findDefaultItemsKey: FindDefaultItemsKey,
) {} ) {}
async execute(): Promise<() => Promise<void>> { async execute(): Promise<() => Promise<void>> {
const currentDefaultItemsKey = findDefaultItemsKey(this.items.getDisplayableItemsKeys()) const currentDefaultItemsKey = this.findDefaultItemsKey.execute(this.items.getDisplayableItemsKeys()).getValue()
const newDefaultItemsKey = await this.createDefaultItemsKeyUseCase.execute() const newDefaultItemsKey = await this.createDefaultItemsKey.execute()
const rollback = async () => { const rollback = async () => {
await this.removeItemsLocallyUsecase.execute([newDefaultItemsKey]) await this.removeItemsLocally.execute([newDefaultItemsKey])
if (currentDefaultItemsKey) { if (currentDefaultItemsKey) {
await this.mutator.changeItem<ItemsKeyMutator>(currentDefaultItemsKey, (mutator) => { await this.mutator.changeItem<ItemsKeyMutator>(currentDefaultItemsKey, (mutator) => {

View File

@@ -0,0 +1,33 @@
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { ItemsKeyInterface } from '@standardnotes/models'
export class FindDefaultItemsKey implements SyncUseCaseInterface<ItemsKeyInterface | undefined> {
execute(itemsKeys: ItemsKeyInterface[]): Result<ItemsKeyInterface | undefined> {
if (itemsKeys.length === 1) {
return Result.ok(itemsKeys[0])
}
const defaultKeys = itemsKeys.filter((key) => {
return key.isDefault
})
if (defaultKeys.length === 0) {
return Result.ok(undefined)
}
if (defaultKeys.length === 1) {
return Result.ok(defaultKeys[0])
}
/**
* Prioritize one that is synced, as neverSynced keys will likely be deleted after
* DownloadFirst sync.
*/
const syncedKeys = defaultKeys.filter((key) => !key.neverSynced)
if (syncedKeys.length > 0) {
return Result.ok(syncedKeys[0])
}
return Result.ok(undefined)
}
}

View File

@@ -6,15 +6,16 @@ import {
PayloadEmitSource, PayloadEmitSource,
SureFindPayload, SureFindPayload,
} from '@standardnotes/models' } from '@standardnotes/models'
import { PayloadManagerInterface } from './../../../Payloads/PayloadManagerInterface' import { PayloadManagerInterface } from '../../../Payloads/PayloadManagerInterface'
import { KeySystemKeyManagerInterface, OperatorManager, isErrorDecryptingParameters } from '@standardnotes/encryption' import { isErrorDecryptingParameters, EncryptionOperatorsInterface } from '@standardnotes/encryption'
import { RootKeyDecryptPayloadWithKeyLookupUseCase } from './DecryptPayloadWithKeyLookup' import { DecryptTypeAPayloadWithKeyLookup } from './DecryptPayloadWithKeyLookup'
import { RootKeyManager } from '../../RootKey/RootKeyManager' import { RootKeyManager } from '../../../RootKeyManager/RootKeyManager'
import { KeySystemKeyManagerInterface } from '../../../KeySystem/KeySystemKeyManagerInterface'
export class DecryptErroredRootPayloadsUseCase { export class DecryptErroredTypeAPayloads {
constructor( constructor(
private payloads: PayloadManagerInterface, private payloads: PayloadManagerInterface,
private operatorManager: OperatorManager, private operatorManager: EncryptionOperatorsInterface,
private keySystemKeyManager: KeySystemKeyManagerInterface, private keySystemKeyManager: KeySystemKeyManagerInterface,
private rootKeyManager: RootKeyManager, private rootKeyManager: RootKeyManager,
) {} ) {}
@@ -28,7 +29,7 @@ export class DecryptErroredRootPayloadsUseCase {
return return
} }
const usecase = new RootKeyDecryptPayloadWithKeyLookupUseCase( const usecase = new DecryptTypeAPayloadWithKeyLookup(
this.operatorManager, this.operatorManager,
this.keySystemKeyManager, this.keySystemKeyManager,
this.rootKeyManager, this.rootKeyManager,

View File

@@ -1,8 +1,8 @@
import { import {
DecryptedParameters, DecryptedParameters,
ErrorDecryptingParameters, ErrorDecryptingParameters,
OperatorManager,
decryptPayload, decryptPayload,
EncryptionOperatorsInterface,
} from '@standardnotes/encryption' } from '@standardnotes/encryption'
import { import {
EncryptedPayloadInterface, EncryptedPayloadInterface,
@@ -11,8 +11,8 @@ import {
RootKeyInterface, RootKeyInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
export class RootKeyDecryptPayloadUseCase { export class DecryptTypeAPayload {
constructor(private operatorManager: OperatorManager) {} constructor(private operatorManager: EncryptionOperatorsInterface) {}
async executeOne<C extends ItemContent = ItemContent>( async executeOne<C extends ItemContent = ItemContent>(
payload: EncryptedPayloadInterface, payload: EncryptedPayloadInterface,

View File

@@ -1,9 +1,4 @@
import { import { DecryptedParameters, ErrorDecryptingParameters, EncryptionOperatorsInterface } from '@standardnotes/encryption'
DecryptedParameters,
ErrorDecryptingParameters,
KeySystemKeyManagerInterface,
OperatorManager,
} from '@standardnotes/encryption'
import { import {
ContentTypeUsesKeySystemRootKeyEncryption, ContentTypeUsesKeySystemRootKeyEncryption,
EncryptedPayloadInterface, EncryptedPayloadInterface,
@@ -12,12 +7,13 @@ import {
RootKeyInterface, RootKeyInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { RootKeyDecryptPayloadUseCase } from './DecryptPayload' import { DecryptTypeAPayload } from './DecryptPayload'
import { RootKeyManager } from '../../RootKey/RootKeyManager' import { RootKeyManager } from '../../../RootKeyManager/RootKeyManager'
import { KeySystemKeyManagerInterface } from '../../../KeySystem/KeySystemKeyManagerInterface'
export class RootKeyDecryptPayloadWithKeyLookupUseCase { export class DecryptTypeAPayloadWithKeyLookup {
constructor( constructor(
private operatorManager: OperatorManager, private operators: EncryptionOperatorsInterface,
private keySystemKeyManager: KeySystemKeyManagerInterface, private keySystemKeyManager: KeySystemKeyManagerInterface,
private rootKeyManager: RootKeyManager, private rootKeyManager: RootKeyManager,
) {} ) {}
@@ -43,7 +39,7 @@ export class RootKeyDecryptPayloadWithKeyLookupUseCase {
} }
} }
const usecase = new RootKeyDecryptPayloadUseCase(this.operatorManager) const usecase = new DecryptTypeAPayload(this.operators)
return usecase.executeOne(payload, key) return usecase.executeOne(payload, key)
} }

View File

@@ -1,16 +1,16 @@
import { EncryptedOutputParameters, OperatorManager, encryptPayload } from '@standardnotes/encryption' import { EncryptedOutputParameters, EncryptionOperatorsInterface, encryptPayload } from '@standardnotes/encryption'
import { DecryptedPayloadInterface, KeySystemRootKeyInterface, RootKeyInterface } from '@standardnotes/models' import { DecryptedPayloadInterface, KeySystemRootKeyInterface, RootKeyInterface } from '@standardnotes/models'
import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { PkcKeyPair } from '@standardnotes/sncrypto-common'
export class RootKeyEncryptPayloadUseCase { export class EncryptTypeAPayload {
constructor(private operatorManager: OperatorManager) {} constructor(private operators: EncryptionOperatorsInterface) {}
async executeOne( async executeOne(
payload: DecryptedPayloadInterface, payload: DecryptedPayloadInterface,
key: RootKeyInterface | KeySystemRootKeyInterface, key: RootKeyInterface | KeySystemRootKeyInterface,
signingKeyPair?: PkcKeyPair, signingKeyPair?: PkcKeyPair,
): Promise<EncryptedOutputParameters> { ): Promise<EncryptedOutputParameters> {
return encryptPayload(payload, key, this.operatorManager, signingKeyPair) return encryptPayload(payload, key, this.operators, signingKeyPair)
} }
async executeMany( async executeMany(

View File

@@ -1,4 +1,4 @@
import { EncryptedOutputParameters, KeySystemKeyManagerInterface, OperatorManager } from '@standardnotes/encryption' import { EncryptedOutputParameters, EncryptionOperatorsInterface } from '@standardnotes/encryption'
import { import {
ContentTypeUsesKeySystemRootKeyEncryption, ContentTypeUsesKeySystemRootKeyEncryption,
DecryptedPayloadInterface, DecryptedPayloadInterface,
@@ -7,12 +7,13 @@ import {
} from '@standardnotes/models' } from '@standardnotes/models'
import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { RootKeyEncryptPayloadUseCase } from './EncryptPayload' import { EncryptTypeAPayload } from './EncryptPayload'
import { RootKeyManager } from '../../RootKey/RootKeyManager' import { RootKeyManager } from '../../../RootKeyManager/RootKeyManager'
import { KeySystemKeyManagerInterface } from '../../../KeySystem/KeySystemKeyManagerInterface'
export class RootKeyEncryptPayloadWithKeyLookupUseCase { export class EncryptTypeAPayloadWithKeyLookup {
constructor( constructor(
private operatorManager: OperatorManager, private operators: EncryptionOperatorsInterface,
private keySystemKeyManager: KeySystemKeyManagerInterface, private keySystemKeyManager: KeySystemKeyManagerInterface,
private rootKeyManager: RootKeyManager, private rootKeyManager: RootKeyManager,
) {} ) {}
@@ -35,7 +36,7 @@ export class RootKeyEncryptPayloadWithKeyLookupUseCase {
throw Error('Attempting root key encryption with no root key') throw Error('Attempting root key encryption with no root key')
} }
const usecase = new RootKeyEncryptPayloadUseCase(this.operatorManager) const usecase = new EncryptTypeAPayload(this.operators)
return usecase.executeOne(payload, key, signingKeyPair) return usecase.executeOne(payload, key, signingKeyPair)
} }

View File

@@ -5,6 +5,7 @@ import { FeatureStatus } from './FeatureStatus'
import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunctionResponse' import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunctionResponse'
export interface FeaturesClientInterface { export interface FeaturesClientInterface {
initializeFromDisk(): void
getFeatureStatus(featureId: FeatureIdentifier, options?: { inContextOfItem?: DecryptedItemInterface }): FeatureStatus getFeatureStatus(featureId: FeatureIdentifier, options?: { inContextOfItem?: DecryptedItemInterface }): FeatureStatus
hasMinimumRole(role: string): boolean hasMinimumRole(role: string): boolean

View File

@@ -1,18 +1,19 @@
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
import { LegacyApiServiceInterface } from './../Api/LegacyApiServiceInterface'
import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common' import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common'
import { FileItem } from '@standardnotes/models' import { FileItem } from '@standardnotes/models'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { ItemManagerInterface } from '../Item/ItemManagerInterface' import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { ChallengeServiceInterface } from '../Challenge' import { ChallengeServiceInterface } from '../Challenge'
import { InternalEventBusInterface, MutatorClientInterface } from '..' import { InternalEventBusInterface, MutatorClientInterface } from '..'
import { AlertService } from '../Alert/AlertService' import { AlertService } from '../Alert/AlertService'
import { ApiServiceInterface } from '../Api/ApiServiceInterface'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface' import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { FileService } from './FileService' import { FileService } from './FileService'
import { BackupServiceInterface } from '@standardnotes/files' import { BackupServiceInterface } from '@standardnotes/files'
import { HttpServiceInterface } from '@standardnotes/api' import { HttpServiceInterface } from '@standardnotes/api'
describe('fileService', () => { describe('fileService', () => {
let apiService: ApiServiceInterface let apiService: LegacyApiServiceInterface
let itemManager: ItemManagerInterface let itemManager: ItemManagerInterface
let mutator: MutatorClientInterface let mutator: MutatorClientInterface
let syncService: SyncServiceInterface let syncService: SyncServiceInterface
@@ -26,7 +27,7 @@ describe('fileService', () => {
let http: HttpServiceInterface let http: HttpServiceInterface
beforeEach(() => { beforeEach(() => {
apiService = {} as jest.Mocked<ApiServiceInterface> apiService = {} as jest.Mocked<LegacyApiServiceInterface>
apiService.addEventObserver = jest.fn() apiService.addEventObserver = jest.fn()
apiService.createUserFileValetToken = jest.fn() apiService.createUserFileValetToken = jest.fn()
apiService.deleteFile = jest.fn().mockReturnValue({}) apiService.deleteFile = jest.fn().mockReturnValue({})

View File

@@ -20,7 +20,7 @@ import {
} from '@standardnotes/models' } from '@standardnotes/models'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { spaceSeparatedStrings, UuidGenerator } from '@standardnotes/utils' import { spaceSeparatedStrings, UuidGenerator } from '@standardnotes/utils'
import { EncryptionProviderInterface, SNItemsKey } from '@standardnotes/encryption' import { SNItemsKey } from '@standardnotes/encryption'
import { import {
DownloadAndDecryptFileOperation, DownloadAndDecryptFileOperation,
EncryptAndUploadFileOperation, EncryptAndUploadFileOperation,
@@ -50,6 +50,7 @@ import { DecryptItemsKeyWithUserFallback } from '../Encryption/Functions'
import { log, LoggingDomain } from '../Logging' import { log, LoggingDomain } from '../Logging'
import { SharedVaultServer, SharedVaultServerInterface, HttpServiceInterface } from '@standardnotes/api' import { SharedVaultServer, SharedVaultServerInterface, HttpServiceInterface } from '@standardnotes/api'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
const OneHundredMb = 100 * 1_000_000 const OneHundredMb = 100 * 1_000_000
@@ -60,7 +61,7 @@ export class FileService extends AbstractService implements FilesClientInterface
constructor( constructor(
private api: FilesApiInterface, private api: FilesApiInterface,
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private syncService: SyncServiceInterface, private sync: SyncServiceInterface,
private encryptor: EncryptionProviderInterface, private encryptor: EncryptionProviderInterface,
private challengor: ChallengeServiceInterface, private challengor: ChallengeServiceInterface,
http: HttpServiceInterface, http: HttpServiceInterface,
@@ -80,7 +81,7 @@ export class FileService extends AbstractService implements FilesClientInterface
;(this.encryptedCache as unknown) = undefined ;(this.encryptedCache as unknown) = undefined
;(this.api as unknown) = undefined ;(this.api as unknown) = undefined
;(this.encryptor as unknown) = undefined ;(this.encryptor as unknown) = undefined
;(this.syncService as unknown) = undefined ;(this.sync as unknown) = undefined
;(this.alertService as unknown) = undefined ;(this.alertService as unknown) = undefined
;(this.challengor as unknown) = undefined ;(this.challengor as unknown) = undefined
;(this.crypto as unknown) = undefined ;(this.crypto as unknown) = undefined
@@ -270,7 +271,7 @@ export class FileService extends AbstractService implements FilesClientInterface
operation.vault, operation.vault,
) )
await this.syncService.sync() await this.sync.sync()
return file return file
} }
@@ -401,7 +402,7 @@ export class FileService extends AbstractService implements FilesClientInterface
} }
await this.mutator.setItemToBeDeleted(file) await this.mutator.setItemToBeDeleted(file)
await this.syncService.sync() await this.sync.sync()
return undefined return undefined
} }

View File

@@ -1,5 +1,6 @@
import { HistoryMap } from '@standardnotes/models' import { HistoryEntry, HistoryMap, SNNote } from '@standardnotes/models'
export interface HistoryServiceInterface { export interface HistoryServiceInterface {
getHistoryMapCopy(): HistoryMap getHistoryMapCopy(): HistoryMap
sessionHistoryForItem(item: SNNote): HistoryEntry[]
} }

View File

@@ -1,3 +1,7 @@
import { InternalEventInterface } from './../Internal/InternalEventInterface'
import { ApplicationStageChangedEventPayload } from './../Event/ApplicationStageChangedEventPayload'
import { ApplicationEvent } from './../Event/ApplicationEvent'
import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
import { Result } from '@standardnotes/domain-core' import { Result } from '@standardnotes/domain-core'
import { ApplicationStage } from '../Application/ApplicationStage' import { ApplicationStage } from '../Application/ApplicationStage'
@@ -10,7 +14,10 @@ import { HomeServerServiceInterface } from './HomeServerServiceInterface'
import { HomeServerEnvironmentConfiguration } from './HomeServerEnvironmentConfiguration' import { HomeServerEnvironmentConfiguration } from './HomeServerEnvironmentConfiguration'
import { HomeServerStatus } from './HomeServerStatus' import { HomeServerStatus } from './HomeServerStatus'
export class HomeServerService extends AbstractService implements HomeServerServiceInterface { export class HomeServerService
extends AbstractService
implements HomeServerServiceInterface, InternalEventHandlerInterface
{
private readonly HOME_SERVER_DATA_DIRECTORY_NAME = '.homeserver' private readonly HOME_SERVER_DATA_DIRECTORY_NAME = '.homeserver'
constructor( constructor(
@@ -20,26 +27,28 @@ export class HomeServerService extends AbstractService implements HomeServerServ
super(internalEventBus) super(internalEventBus)
} }
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === ApplicationEvent.ApplicationStageChanged) {
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
switch (stage) {
case ApplicationStage.StorageDecrypted_09: {
await this.setHomeServerDataLocationOnDevice()
break
}
case ApplicationStage.Launched_10: {
await this.startHomeServerIfItIsEnabled()
break
}
}
}
}
override deinit() { override deinit() {
;(this.desktopDevice as unknown) = undefined ;(this.desktopDevice as unknown) = undefined
super.deinit() super.deinit()
} }
override async handleApplicationStage(stage: ApplicationStage) {
await super.handleApplicationStage(stage)
switch (stage) {
case ApplicationStage.StorageDecrypted_09: {
await this.setHomeServerDataLocationOnDevice()
break
}
case ApplicationStage.Launched_10: {
await this.startHomeServerIfItIsEnabled()
break
}
}
}
async getHomeServerStatus(): Promise<HomeServerStatus> { async getHomeServerStatus(): Promise<HomeServerStatus> {
const isHomeServerRunning = await this.desktopDevice.isHomeServerRunning() const isHomeServerRunning = await this.desktopDevice.isHomeServerRunning()

View File

@@ -1,15 +1,14 @@
import { FindDefaultItemsKey } from './../Encryption/UseCase/ItemsKey/FindDefaultItemsKey'
import { ProtocolVersion } from '@standardnotes/common' import { ProtocolVersion } from '@standardnotes/common'
import { import {
DecryptedParameters, DecryptedParameters,
ErrorDecryptingParameters, ErrorDecryptingParameters,
findDefaultItemsKey,
isErrorDecryptingParameters, isErrorDecryptingParameters,
OperatorManager,
StandardException, StandardException,
encryptPayload, encryptPayload,
decryptPayload, decryptPayload,
EncryptedOutputParameters, EncryptedOutputParameters,
KeySystemKeyManagerInterface, EncryptionOperatorsInterface,
} from '@standardnotes/encryption' } from '@standardnotes/encryption'
import { import {
ContentTypeUsesKeySystemRootKeyEncryption, ContentTypeUsesKeySystemRootKeyEncryption,
@@ -33,22 +32,24 @@ import { AbstractService } from '../Service/AbstractService'
import { StorageServiceInterface } from '../Storage/StorageServiceInterface' import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { KeySystemKeyManagerInterface } from '../KeySystem/KeySystemKeyManagerInterface'
export class ItemsEncryptionService extends AbstractService { export class ItemsEncryptionService extends AbstractService {
private removeItemsObserver!: () => void private removeItemsObserver!: () => void
public userVersion?: ProtocolVersion public userVersion?: ProtocolVersion
constructor( constructor(
private itemManager: ItemManagerInterface, private items: ItemManagerInterface,
private payloadManager: PayloadManagerInterface, private payloads: PayloadManagerInterface,
private storageService: StorageServiceInterface, private storage: StorageServiceInterface,
private operatorManager: OperatorManager, private operators: EncryptionOperatorsInterface,
private keys: KeySystemKeyManagerInterface, private keys: KeySystemKeyManagerInterface,
private _findDefaultItemsKey: FindDefaultItemsKey,
protected override internalEventBus: InternalEventBusInterface, protected override internalEventBus: InternalEventBusInterface,
) { ) {
super(internalEventBus) super(internalEventBus)
this.removeItemsObserver = this.itemManager.addObserver([ContentType.TYPES.ItemsKey], ({ changed, inserted }) => { this.removeItemsObserver = this.items.addObserver([ContentType.TYPES.ItemsKey], ({ changed, inserted }) => {
if (changed.concat(inserted).length > 0) { if (changed.concat(inserted).length > 0) {
void this.decryptErroredItemPayloads() void this.decryptErroredItemPayloads()
} }
@@ -56,10 +57,10 @@ export class ItemsEncryptionService extends AbstractService {
} }
public override deinit(): void { public override deinit(): void {
;(this.itemManager as unknown) = undefined ;(this.items as unknown) = undefined
;(this.payloadManager as unknown) = undefined ;(this.payloads as unknown) = undefined
;(this.storageService as unknown) = undefined ;(this.storage as unknown) = undefined
;(this.operatorManager as unknown) = undefined ;(this.operators as unknown) = undefined
;(this.keys as unknown) = undefined ;(this.keys as unknown) = undefined
this.removeItemsObserver() this.removeItemsObserver()
;(this.removeItemsObserver as unknown) = undefined ;(this.removeItemsObserver as unknown) = undefined
@@ -72,22 +73,20 @@ export class ItemsEncryptionService extends AbstractService {
* disk using latest encryption status. * disk using latest encryption status.
*/ */
async repersistAllItems(): Promise<void> { async repersistAllItems(): Promise<void> {
const items = this.itemManager.items const items = this.items.items
const payloads = items.map((item) => item.payload) const payloads = items.map((item) => item.payload)
return this.storageService.savePayloads(payloads) return this.storage.savePayloads(payloads)
} }
public getItemsKeys(): ItemsKeyInterface[] { public getItemsKeys(): ItemsKeyInterface[] {
return this.itemManager.getDisplayableItemsKeys() return this.items.getDisplayableItemsKeys()
} }
public itemsKeyForEncryptedPayload( public itemsKeyForEncryptedPayload(
payload: EncryptedPayloadInterface, payload: EncryptedPayloadInterface,
): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined { ): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined {
const itemsKeys = this.getItemsKeys() const itemsKeys = this.getItemsKeys()
const keySystemItemsKeys = this.itemManager.getItems<KeySystemItemsKeyInterface>( const keySystemItemsKeys = this.items.getItems<KeySystemItemsKeyInterface>(ContentType.TYPES.KeySystemItemsKey)
ContentType.TYPES.KeySystemItemsKey,
)
return [...itemsKeys, ...keySystemItemsKeys].find( return [...itemsKeys, ...keySystemItemsKeys].find(
(key) => key.uuid === payload.items_key_id || key.duplicateOf === payload.items_key_id, (key) => key.uuid === payload.items_key_id || key.duplicateOf === payload.items_key_id,
@@ -95,7 +94,7 @@ export class ItemsEncryptionService extends AbstractService {
} }
public getDefaultItemsKey(): ItemsKeyInterface | undefined { public getDefaultItemsKey(): ItemsKeyInterface | undefined {
return findDefaultItemsKey(this.getItemsKeys()) return this._findDefaultItemsKey.execute(this.getItemsKeys()).getValue()
} }
private keyToUseForItemEncryption( private keyToUseForItemEncryption(
@@ -173,7 +172,7 @@ export class ItemsEncryptionService extends AbstractService {
throw Error('Attempting to encrypt payload with no UuidGenerator.') throw Error('Attempting to encrypt payload with no UuidGenerator.')
} }
return encryptPayload(payload, key, this.operatorManager, signingKeyPair) return encryptPayload(payload, key, this.operators, signingKeyPair)
} }
public async encryptPayloads( public async encryptPayloads(
@@ -218,7 +217,7 @@ export class ItemsEncryptionService extends AbstractService {
} }
} }
return decryptPayload(payload, key, this.operatorManager) return decryptPayload(payload, key, this.operators)
} }
public async decryptPayloadsWithKeyLookup<C extends ItemContent = ItemContent>( public async decryptPayloadsWithKeyLookup<C extends ItemContent = ItemContent>(
@@ -235,7 +234,7 @@ export class ItemsEncryptionService extends AbstractService {
} }
public async decryptErroredItemPayloads(): Promise<void> { public async decryptErroredItemPayloads(): Promise<void> {
const erroredItemPayloads = this.payloadManager.invalidPayloads.filter( const erroredItemPayloads = this.payloads.invalidPayloads.filter(
(i) => (i) =>
!ContentTypeUsesRootKeyEncryption(i.content_type) && !ContentTypeUsesKeySystemRootKeyEncryption(i.content_type), !ContentTypeUsesRootKeyEncryption(i.content_type) && !ContentTypeUsesKeySystemRootKeyEncryption(i.content_type),
) )
@@ -260,7 +259,7 @@ export class ItemsEncryptionService extends AbstractService {
} }
}) })
await this.payloadManager.emitPayloads(decryptedPayloads, PayloadEmitSource.LocalChanged) await this.payloads.emitPayloads(decryptedPayloads, PayloadEmitSource.LocalChanged)
} }
/** /**

View File

@@ -1,3 +1,4 @@
import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { ApplicationStage } from './../Application/ApplicationStage' import { ApplicationStage } from './../Application/ApplicationStage'
import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface' import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface'
@@ -16,13 +17,19 @@ import {
VaultListingInterface, VaultListingInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { ItemManagerInterface } from './../Item/ItemManagerInterface' import { ItemManagerInterface } from './../Item/ItemManagerInterface'
import { KeySystemKeyManagerInterface } from '@standardnotes/encryption'
import { AbstractService } from '../Service/AbstractService' import { AbstractService } from '../Service/AbstractService'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { InternalEventInterface } from '../Internal/InternalEventInterface'
import { ApplicationEvent } from '../Event/ApplicationEvent'
import { ApplicationStageChangedEventPayload } from '../Event/ApplicationStageChangedEventPayload'
import { KeySystemKeyManagerInterface } from './KeySystemKeyManagerInterface'
const RootKeyStorageKeyPrefix = 'key-system-root-key-' const RootKeyStorageKeyPrefix = 'key-system-root-key-'
export class KeySystemKeyManager extends AbstractService implements KeySystemKeyManagerInterface { export class KeySystemKeyManager
extends AbstractService
implements KeySystemKeyManagerInterface, InternalEventHandlerInterface
{
private rootKeyMemoryCache: Record<KeySystemIdentifier, KeySystemRootKeyInterface> = {} private rootKeyMemoryCache: Record<KeySystemIdentifier, KeySystemRootKeyInterface> = {}
constructor( constructor(
@@ -34,9 +41,12 @@ export class KeySystemKeyManager extends AbstractService implements KeySystemKey
super(eventBus) super(eventBus)
} }
public override async handleApplicationStage(stage: ApplicationStage): Promise<void> { async handleEvent(event: InternalEventInterface): Promise<void> {
if (stage === ApplicationStage.StorageDecrypted_09) { if (event.type === ApplicationEvent.ApplicationStageChanged) {
this.loadRootKeysFromStorage() const stage = (event.payload as ApplicationStageChangedEventPayload).stage
if (stage === ApplicationStage.StorageDecrypted_09) {
this.loadRootKeysFromStorage()
}
} }
} }
@@ -59,6 +69,17 @@ export class KeySystemKeyManager extends AbstractService implements KeySystemKey
return `${RootKeyStorageKeyPrefix}${systemIdentifier}` return `${RootKeyStorageKeyPrefix}${systemIdentifier}`
} }
/**
* When the key system root key changes, we must re-encrypt all vault items keys
* with this new key system root key (by simply re-syncing).
*/
public async reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise<void> {
const keySystemItemsKeys = this.getKeySystemItemsKeys(keySystemIdentifier)
if (keySystemItemsKeys.length > 0) {
await this.mutator.setItemsDirty(keySystemItemsKeys)
}
}
public intakeNonPersistentKeySystemRootKey( public intakeNonPersistentKeySystemRootKey(
key: KeySystemRootKeyInterface, key: KeySystemRootKeyInterface,
storage: KeySystemRootKeyStorageMode, storage: KeySystemRootKeyStorageMode,

View File

@@ -21,6 +21,7 @@ export interface KeySystemKeyManagerInterface {
keyIdentifier: string, keyIdentifier: string,
): KeySystemRootKeyInterface | undefined ): KeySystemRootKeyInterface | undefined
getPrimaryKeySystemRootKey(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface | undefined getPrimaryKeySystemRootKey(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface | undefined
reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise<void>
intakeNonPersistentKeySystemRootKey(key: KeySystemRootKeyInterface, storage: KeySystemRootKeyStorageMode): void intakeNonPersistentKeySystemRootKey(key: KeySystemRootKeyInterface, storage: KeySystemRootKeyStorageMode): void
undoIntakeNonPersistentKeySystemRootKey(systemIdentifier: KeySystemIdentifier): void undoIntakeNonPersistentKeySystemRootKey(systemIdentifier: KeySystemIdentifier): void

Some files were not shown because too many files have changed in this diff Show More