internal: incomplete vault systems behind feature flag (#2340)

This commit is contained in:
Mo
2023-06-30 09:01:56 -05:00
committed by GitHub
parent d16e401bb9
commit b032eb9c9b
638 changed files with 20321 additions and 4813 deletions

View File

@@ -1,251 +0,0 @@
import {
AnyKeyParamsContent,
compareVersions,
ContentType,
leftVersionGreaterThanOrEqualToRight,
ProtocolVersion,
} from '@standardnotes/common'
import {
BackupFileType,
ContentTypeUsesRootKeyEncryption,
CreateAnyKeyParams,
isItemsKey,
SNItemsKey,
SNRootKey,
SNRootKeyParams,
} from '@standardnotes/encryption'
import {
BackupFile,
CreateDecryptedItemFromPayload,
CreatePayloadSplit,
DecryptedPayload,
DecryptedPayloadInterface,
EncryptedPayload,
EncryptedPayloadInterface,
isDecryptedPayload,
isDecryptedTransferPayload,
isEncryptedPayload,
isEncryptedTransferPayload,
ItemsKeyContent,
ItemsKeyInterface,
PayloadInterface,
} from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { extendArray } from '@standardnotes/utils'
import { EncryptionService } from './EncryptionService'
export async function DecryptBackupFile(
file: BackupFile,
protocolService: EncryptionService,
password?: string,
): Promise<ClientDisplayableError | (EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
const payloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = file.items.map((item) => {
if (isEncryptedTransferPayload(item)) {
return new EncryptedPayload(item)
} else if (isDecryptedTransferPayload(item)) {
return new DecryptedPayload(item)
} else {
throw Error('Unhandled case in decryptBackupFile')
}
})
const { encrypted, decrypted } = CreatePayloadSplit(payloads)
const type = getBackupFileType(file, payloads)
switch (type) {
case BackupFileType.Corrupt:
return new ClientDisplayableError('Invalid backup file.')
case BackupFileType.Encrypted: {
if (!password) {
throw Error('Attempting to decrypt encrypted file with no password')
}
const keyParamsData = (file.keyParams || file.auth_params) as AnyKeyParamsContent
return [
...decrypted,
...(await decryptEncrypted(password, CreateAnyKeyParams(keyParamsData), encrypted, protocolService)),
]
}
case BackupFileType.EncryptedWithNonEncryptedItemsKey:
return [...decrypted, ...(await decryptEncryptedWithNonEncryptedItemsKey(payloads, protocolService))]
case BackupFileType.FullyDecrypted:
return [...decrypted, ...encrypted]
}
}
function getBackupFileType(file: BackupFile, payloads: PayloadInterface[]): BackupFileType {
if (file.keyParams || file.auth_params) {
return BackupFileType.Encrypted
} else {
const hasEncryptedItem = payloads.find(isEncryptedPayload)
const hasDecryptedItemsKey = payloads.find(
(payload) => payload.content_type === ContentType.ItemsKey && isDecryptedPayload(payload),
)
if (hasEncryptedItem && hasDecryptedItemsKey) {
return BackupFileType.EncryptedWithNonEncryptedItemsKey
} else if (!hasEncryptedItem) {
return BackupFileType.FullyDecrypted
} else {
return BackupFileType.Corrupt
}
}
}
async function decryptEncryptedWithNonEncryptedItemsKey(
allPayloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[],
protocolService: EncryptionService,
): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
const decryptedItemsKeys: DecryptedPayloadInterface<ItemsKeyContent>[] = []
const encryptedPayloads: EncryptedPayloadInterface[] = []
allPayloads.forEach((payload) => {
if (payload.content_type === ContentType.ItemsKey && isDecryptedPayload(payload)) {
decryptedItemsKeys.push(payload as DecryptedPayloadInterface<ItemsKeyContent>)
} else if (isEncryptedPayload(payload)) {
encryptedPayloads.push(payload)
}
})
const itemsKeys = decryptedItemsKeys.map((p) => CreateDecryptedItemFromPayload<ItemsKeyContent, SNItemsKey>(p))
return decryptWithItemsKeys(encryptedPayloads, itemsKeys, protocolService)
}
function findKeyToUseForPayload(
payload: EncryptedPayloadInterface,
availableKeys: ItemsKeyInterface[],
protocolService: EncryptionService,
keyParams?: SNRootKeyParams,
fallbackRootKey?: SNRootKey,
): ItemsKeyInterface | SNRootKey | undefined {
let itemsKey: ItemsKeyInterface | SNRootKey | undefined
if (payload.items_key_id) {
itemsKey = protocolService.itemsKeyForPayload(payload)
if (itemsKey) {
return itemsKey
}
}
itemsKey = availableKeys.find((itemsKeyPayload) => {
return payload.items_key_id === itemsKeyPayload.uuid
})
if (itemsKey) {
return itemsKey
}
if (!keyParams) {
return undefined
}
const payloadVersion = payload.version as ProtocolVersion
/**
* Payloads with versions <= 003 use root key directly for encryption.
* However, if the incoming key params are >= 004, this means we should
* have an items key based off the 003 root key. We can't use the 004
* root key directly because it's missing dataAuthenticationKey.
*/
if (leftVersionGreaterThanOrEqualToRight(keyParams.version, ProtocolVersion.V004)) {
itemsKey = protocolService.defaultItemsKeyForItemVersion(payloadVersion, availableKeys)
} else if (compareVersions(payloadVersion, ProtocolVersion.V003) <= 0) {
itemsKey = fallbackRootKey
}
return itemsKey
}
async function decryptWithItemsKeys(
payloads: EncryptedPayloadInterface[],
itemsKeys: ItemsKeyInterface[],
protocolService: EncryptionService,
keyParams?: SNRootKeyParams,
fallbackRootKey?: SNRootKey,
): Promise<(DecryptedPayloadInterface | EncryptedPayloadInterface)[]> {
const results: (DecryptedPayloadInterface | EncryptedPayloadInterface)[] = []
for (const encryptedPayload of payloads) {
if (ContentTypeUsesRootKeyEncryption(encryptedPayload.content_type)) {
continue
}
try {
const key = findKeyToUseForPayload(encryptedPayload, itemsKeys, protocolService, keyParams, fallbackRootKey)
if (!key) {
results.push(
encryptedPayload.copy({
errorDecrypting: true,
}),
)
continue
}
if (isItemsKey(key)) {
const decryptedPayload = await protocolService.decryptSplitSingle({
usesItemsKey: {
items: [encryptedPayload],
key: key,
},
})
results.push(decryptedPayload)
} else {
const decryptedPayload = await protocolService.decryptSplitSingle({
usesRootKey: {
items: [encryptedPayload],
key: key,
},
})
results.push(decryptedPayload)
}
} catch (e) {
results.push(
encryptedPayload.copy({
errorDecrypting: true,
}),
)
console.error('Error decrypting payload', encryptedPayload, e)
}
}
return results
}
async function decryptEncrypted(
password: string,
keyParams: SNRootKeyParams,
payloads: EncryptedPayloadInterface[],
protocolService: EncryptionService,
): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
const results: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = []
const rootKey = await protocolService.computeRootKey(password, keyParams)
const itemsKeysPayloads = payloads.filter((payload) => {
return payload.content_type === ContentType.ItemsKey
})
const itemsKeysDecryptionResults = await protocolService.decryptSplit({
usesRootKey: {
items: itemsKeysPayloads,
key: rootKey,
},
})
extendArray(results, itemsKeysDecryptionResults)
const decryptedPayloads = await decryptWithItemsKeys(
payloads,
itemsKeysDecryptionResults.filter(isDecryptedPayload).map((p) => CreateDecryptedItemFromPayload(p)),
protocolService,
keyParams,
rootKey,
)
extendArray(results, decryptedPayloads)
return results
}

View File

@@ -0,0 +1,282 @@
import {
AnyKeyParamsContent,
compareVersions,
ContentType,
leftVersionGreaterThanOrEqualToRight,
ProtocolVersion,
} from '@standardnotes/common'
import {
BackupFileType,
CreateAnyKeyParams,
isItemsKey,
isKeySystemItemsKey,
SNItemsKey,
SplitPayloadsByEncryptionType,
} from '@standardnotes/encryption'
import {
ContentTypeUsesKeySystemRootKeyEncryption,
ContentTypeUsesRootKeyEncryption,
BackupFile,
CreateDecryptedItemFromPayload,
CreatePayloadSplit,
DecryptedPayload,
DecryptedPayloadInterface,
EncryptedPayload,
EncryptedPayloadInterface,
isDecryptedPayload,
isDecryptedTransferPayload,
isEncryptedPayload,
isEncryptedTransferPayload,
ItemsKeyContent,
ItemsKeyInterface,
PayloadInterface,
KeySystemItemsKeyInterface,
RootKeyInterface,
KeySystemRootKeyInterface,
isKeySystemRootKey,
RootKeyParamsInterface,
} from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { extendArray } from '@standardnotes/utils'
import { EncryptionService } from './EncryptionService'
export class DecryptBackupFileUseCase {
constructor(private encryption: EncryptionService) {}
async execute(
file: BackupFile,
password?: string,
): Promise<ClientDisplayableError | (EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
const payloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = file.items.map((item) => {
if (isEncryptedTransferPayload(item)) {
return new EncryptedPayload(item)
} else if (isDecryptedTransferPayload(item)) {
return new DecryptedPayload(item)
} else {
throw Error('Unhandled case in decryptBackupFile')
}
})
const { encrypted, decrypted } = CreatePayloadSplit(payloads)
const type = this.getBackupFileType(file, payloads)
switch (type) {
case BackupFileType.Corrupt:
return new ClientDisplayableError('Invalid backup file.')
case BackupFileType.Encrypted: {
if (!password) {
throw Error('Attempting to decrypt encrypted file with no password')
}
const keyParamsData = (file.keyParams || file.auth_params) as AnyKeyParamsContent
const rootKey = await this.encryption.computeRootKey(password, CreateAnyKeyParams(keyParamsData))
const results = await this.decryptEncrypted({
password,
payloads: encrypted,
rootKey,
keyParams: CreateAnyKeyParams(keyParamsData),
})
return [...decrypted, ...results]
}
case BackupFileType.EncryptedWithNonEncryptedItemsKey:
return [...decrypted, ...(await this.decryptEncryptedWithNonEncryptedItemsKey(payloads))]
case BackupFileType.FullyDecrypted:
return [...decrypted, ...encrypted]
}
}
private getBackupFileType(file: BackupFile, payloads: PayloadInterface[]): BackupFileType {
if (file.keyParams || file.auth_params) {
return BackupFileType.Encrypted
} else {
const hasEncryptedItem = payloads.find(isEncryptedPayload)
const hasDecryptedItemsKey = payloads.find(
(payload) => payload.content_type === ContentType.ItemsKey && isDecryptedPayload(payload),
)
if (hasEncryptedItem && hasDecryptedItemsKey) {
return BackupFileType.EncryptedWithNonEncryptedItemsKey
} else if (!hasEncryptedItem) {
return BackupFileType.FullyDecrypted
} else {
return BackupFileType.Corrupt
}
}
}
private async decryptEncrypted(dto: {
password: string
keyParams: RootKeyParamsInterface
payloads: EncryptedPayloadInterface[]
rootKey: RootKeyInterface
}): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
const results: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = []
const { rootKeyEncryption, itemsKeyEncryption } = SplitPayloadsByEncryptionType(dto.payloads)
const rootKeyBasedDecryptionResults = await this.encryption.decryptSplit({
usesRootKey: {
items: rootKeyEncryption || [],
key: dto.rootKey,
},
})
extendArray(results, rootKeyBasedDecryptionResults)
const decryptedPayloads = await this.decrypt({
payloads: itemsKeyEncryption || [],
availableItemsKeys: rootKeyBasedDecryptionResults
.filter(isItemsKey)
.filter(isDecryptedPayload)
.map((p) => CreateDecryptedItemFromPayload(p)),
keyParams: dto.keyParams,
rootKey: dto.rootKey,
})
extendArray(results, decryptedPayloads)
return results
}
private async decryptEncryptedWithNonEncryptedItemsKey(
payloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[],
): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
const decryptedItemsKeys: DecryptedPayloadInterface<ItemsKeyContent>[] = []
const encryptedPayloads: EncryptedPayloadInterface[] = []
payloads.forEach((payload) => {
if (payload.content_type === ContentType.ItemsKey && isDecryptedPayload(payload)) {
decryptedItemsKeys.push(payload as DecryptedPayloadInterface<ItemsKeyContent>)
} else if (isEncryptedPayload(payload)) {
encryptedPayloads.push(payload)
}
})
const itemsKeys = decryptedItemsKeys.map((p) => CreateDecryptedItemFromPayload<ItemsKeyContent, SNItemsKey>(p))
return this.decrypt({ payloads: encryptedPayloads, availableItemsKeys: itemsKeys, rootKey: undefined })
}
private findKeyToUseForPayload(dto: {
payload: EncryptedPayloadInterface
availableKeys: ItemsKeyInterface[]
keyParams?: RootKeyParamsInterface
rootKey?: RootKeyInterface
}): ItemsKeyInterface | RootKeyInterface | KeySystemRootKeyInterface | KeySystemItemsKeyInterface | undefined {
if (ContentTypeUsesRootKeyEncryption(dto.payload.content_type)) {
if (!dto.rootKey) {
throw new Error('Attempting to decrypt root key encrypted payload with no root key')
}
return dto.rootKey
}
if (ContentTypeUsesKeySystemRootKeyEncryption(dto.payload.content_type)) {
throw new Error('Backup file key system root key encryption is not supported')
}
let itemsKey: ItemsKeyInterface | RootKeyInterface | KeySystemItemsKeyInterface | undefined
if (dto.payload.items_key_id) {
itemsKey = this.encryption.itemsKeyForEncryptedPayload(dto.payload)
if (itemsKey) {
return itemsKey
}
}
itemsKey = dto.availableKeys.find((itemsKeyPayload) => {
return dto.payload.items_key_id === itemsKeyPayload.uuid
})
if (itemsKey) {
return itemsKey
}
if (!dto.keyParams) {
return undefined
}
const payloadVersion = dto.payload.version as ProtocolVersion
/**
* Payloads with versions <= 003 use root key directly for encryption.
* However, if the incoming key params are >= 004, this means we should
* have an items key based off the 003 root key. We can't use the 004
* root key directly because it's missing dataAuthenticationKey.
*/
if (leftVersionGreaterThanOrEqualToRight(dto.keyParams.version, ProtocolVersion.V004)) {
itemsKey = this.encryption.defaultItemsKeyForItemVersion(payloadVersion, dto.availableKeys)
} else if (compareVersions(payloadVersion, ProtocolVersion.V003) <= 0) {
itemsKey = dto.rootKey
}
return itemsKey
}
private async decrypt(dto: {
payloads: EncryptedPayloadInterface[]
availableItemsKeys: ItemsKeyInterface[]
rootKey: RootKeyInterface | undefined
keyParams?: RootKeyParamsInterface
}): Promise<(DecryptedPayloadInterface | EncryptedPayloadInterface)[]> {
const results: (DecryptedPayloadInterface | EncryptedPayloadInterface)[] = []
for (const encryptedPayload of dto.payloads) {
try {
const key = this.findKeyToUseForPayload({
payload: encryptedPayload,
availableKeys: dto.availableItemsKeys,
keyParams: dto.keyParams,
rootKey: dto.rootKey,
})
if (!key) {
results.push(
encryptedPayload.copy({
errorDecrypting: true,
}),
)
continue
}
if (isItemsKey(key) || isKeySystemItemsKey(key)) {
const decryptedPayload = await this.encryption.decryptSplitSingle({
usesItemsKey: {
items: [encryptedPayload],
key: key,
},
})
results.push(decryptedPayload)
} else if (isKeySystemRootKey(key)) {
const decryptedPayload = await this.encryption.decryptSplitSingle({
usesKeySystemRootKey: {
items: [encryptedPayload],
key: key,
},
})
results.push(decryptedPayload)
} else {
const decryptedPayload = await this.encryption.decryptSplitSingle({
usesRootKey: {
items: [encryptedPayload],
key: key,
},
})
results.push(decryptedPayload)
}
} catch (e) {
results.push(
encryptedPayload.copy({
errorDecrypting: true,
}),
)
console.error('Error decrypting payload', encryptedPayload, e)
}
}
return results
}
}

View File

@@ -1,9 +1,8 @@
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import {
CreateAnyKeyParams,
CreateEncryptionSplitWithKeyLookup,
DecryptedParameters,
EncryptedParameters,
encryptedParametersFromPayload,
encryptedInputParametersFromPayload,
EncryptionProviderInterface,
ErrorDecryptingParameters,
findDefaultItemsKey,
@@ -23,6 +22,11 @@ import {
SplitPayloadsByEncryptionType,
V001Algorithm,
V002Algorithm,
PublicKeySet,
EncryptedOutputParameters,
KeySystemKeyManagerInterface,
AsymmetricSignatureVerificationDetachedResult,
AsymmetricallyEncryptedString,
} from '@standardnotes/encryption'
import {
BackupFile,
@@ -37,9 +41,15 @@ import {
ItemContent,
ItemsKeyInterface,
RootKeyInterface,
KeySystemItemsKeyInterface,
KeySystemIdentifier,
AsymmetricMessagePayload,
KeySystemRootKeyInterface,
KeySystemRootKeyParamsInterface,
TrustedContactInterface,
} from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import {
extendArray,
isNotUndefined,
@@ -68,10 +78,10 @@ import { DeviceInterface } from '../Device/DeviceInterface'
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { SyncEvent } from '../Event/SyncEvent'
import { DiagnosticInfo } from '../Diagnostics/ServiceDiagnostics'
import { RootKeyEncryptionService } from './RootKeyEncryption'
import { DecryptBackupFile } from './BackupFileDecryptor'
import { DecryptBackupFileUseCase } from './DecryptBackupFileUseCase'
import { EncryptionServiceEvent } from './EncryptionServiceEvent'
import { DecryptedParameters } from '@standardnotes/encryption/src/Domain/Types/DecryptedParameters'
/**
* The encryption service is responsible for the encryption and decryption of payloads, and
@@ -108,9 +118,11 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
constructor(
private itemManager: ItemManagerInterface,
private mutator: MutatorClientInterface,
private payloadManager: PayloadManagerInterface,
public deviceInterface: DeviceInterface,
private storageService: StorageServiceInterface,
public readonly keys: KeySystemKeyManagerInterface,
private identifier: ApplicationIdentifier,
public crypto: PureCryptoInterface,
protected override internalEventBus: InternalEventBusInterface,
@@ -125,17 +137,22 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
payloadManager,
storageService,
this.operatorManager,
keys,
internalEventBus,
)
this.rootKeyEncryption = new RootKeyEncryptionService(
this.itemManager,
this.mutator,
this.operatorManager,
this.deviceInterface,
this.storageService,
this.payloadManager,
keys,
this.identifier,
this.internalEventBus,
)
this.rootKeyObserverDisposer = this.rootKeyEncryption.addEventObserver((event) => {
this.itemsEncryption.userVersion = this.getUserVersion()
if (event === RootKeyServiceEvent.RootKeyStatusChanged) {
@@ -166,6 +183,32 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
super.deinit()
}
/** @throws */
getKeyPair(): PkcKeyPair {
const rootKey = this.getRootKey()
if (!rootKey?.encryptionKeyPair) {
throw new Error('Account keypair not found')
}
return rootKey.encryptionKeyPair
}
/** @throws */
getSigningKeyPair(): PkcKeyPair {
const rootKey = this.getRootKey()
if (!rootKey?.signingKeyPair) {
throw new Error('Account keypair not found')
}
return rootKey.signingKeyPair
}
hasSigningKeyPair(): boolean {
return !!this.getRootKey()?.signingKeyPair
}
public async initialize() {
await this.rootKeyEncryption.initialize()
}
@@ -213,8 +256,12 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
return this.itemsEncryption.repersistAllItems()
}
public async reencryptItemsKeys(): Promise<void> {
await this.rootKeyEncryption.reencryptItemsKeys()
public async reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void> {
await this.rootKeyEncryption.reencryptApplicableItemsAfterUserRootKeyChange()
}
public reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise<void> {
return this.rootKeyEncryption.reencryptKeySystemItemsKeysForVault(keySystemIdentifier)
}
public async createNewItemsKeyWithRollback(): Promise<() => Promise<void>> {
@@ -222,11 +269,14 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
}
public async decryptErroredPayloads(): Promise<void> {
await this.itemsEncryption.decryptErroredPayloads()
await this.rootKeyEncryption.decryptErroredRootPayloads()
await this.itemsEncryption.decryptErroredItemPayloads()
}
public itemsKeyForPayload(payload: EncryptedPayloadInterface): ItemsKeyInterface | undefined {
return this.itemsEncryption.itemsKeyForPayload(payload)
public itemsKeyForEncryptedPayload(
payload: EncryptedPayloadInterface,
): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined {
return this.itemsEncryption.itemsKeyForEncryptedPayload(payload)
}
public defaultItemsKeyForItemVersion(
@@ -241,34 +291,66 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
}
public async encryptSplit(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface[]> {
const allEncryptedParams: EncryptedParameters[] = []
const allEncryptedParams: EncryptedOutputParameters[] = []
if (split.usesRootKey) {
const {
usesRootKey,
usesItemsKey,
usesKeySystemRootKey,
usesRootKeyWithKeyLookup,
usesItemsKeyWithKeyLookup,
usesKeySystemRootKeyWithKeyLookup,
} = split
const signingKeyPair = this.hasSigningKeyPair() ? this.getSigningKeyPair() : undefined
if (usesRootKey) {
const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloads(
split.usesRootKey.items,
split.usesRootKey.key,
usesRootKey.items,
usesRootKey.key,
signingKeyPair,
)
extendArray(allEncryptedParams, rootKeyEncrypted)
}
if (split.usesItemsKey) {
if (usesRootKeyWithKeyLookup) {
const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloadsWithKeyLookup(
usesRootKeyWithKeyLookup.items,
signingKeyPair,
)
extendArray(allEncryptedParams, rootKeyEncrypted)
}
if (usesKeySystemRootKey) {
const keySystemRootKeyEncrypted = await this.rootKeyEncryption.encryptPayloads(
usesKeySystemRootKey.items,
usesKeySystemRootKey.key,
signingKeyPair,
)
extendArray(allEncryptedParams, keySystemRootKeyEncrypted)
}
if (usesKeySystemRootKeyWithKeyLookup) {
const keySystemRootKeyEncrypted = await this.rootKeyEncryption.encryptPayloadsWithKeyLookup(
usesKeySystemRootKeyWithKeyLookup.items,
signingKeyPair,
)
extendArray(allEncryptedParams, keySystemRootKeyEncrypted)
}
if (usesItemsKey) {
const itemsKeyEncrypted = await this.itemsEncryption.encryptPayloads(
split.usesItemsKey.items,
split.usesItemsKey.key,
usesItemsKey.items,
usesItemsKey.key,
signingKeyPair,
)
extendArray(allEncryptedParams, itemsKeyEncrypted)
}
if (split.usesRootKeyWithKeyLookup) {
const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloadsWithKeyLookup(
split.usesRootKeyWithKeyLookup.items,
)
extendArray(allEncryptedParams, rootKeyEncrypted)
}
if (split.usesItemsKeyWithKeyLookup) {
if (usesItemsKeyWithKeyLookup) {
const itemsKeyEncrypted = await this.itemsEncryption.encryptPayloadsWithKeyLookup(
split.usesItemsKeyWithKeyLookup.items,
usesItemsKeyWithKeyLookup.items,
signingKeyPair,
)
extendArray(allEncryptedParams, itemsKeyEncrypted)
}
@@ -300,32 +382,48 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
>(split: KeyedDecryptionSplit): Promise<(P | EncryptedPayloadInterface)[]> {
const resultParams: (DecryptedParameters<C> | ErrorDecryptingParameters)[] = []
if (split.usesRootKey) {
const rootKeyDecrypted = await this.rootKeyEncryption.decryptPayloads<C>(
split.usesRootKey.items,
split.usesRootKey.key,
)
const {
usesRootKey,
usesItemsKey,
usesKeySystemRootKey,
usesRootKeyWithKeyLookup,
usesItemsKeyWithKeyLookup,
usesKeySystemRootKeyWithKeyLookup,
} = split
if (usesRootKey) {
const rootKeyDecrypted = await this.rootKeyEncryption.decryptPayloads<C>(usesRootKey.items, usesRootKey.key)
extendArray(resultParams, rootKeyDecrypted)
}
if (split.usesRootKeyWithKeyLookup) {
if (usesRootKeyWithKeyLookup) {
const rootKeyDecrypted = await this.rootKeyEncryption.decryptPayloadsWithKeyLookup<C>(
split.usesRootKeyWithKeyLookup.items,
usesRootKeyWithKeyLookup.items,
)
extendArray(resultParams, rootKeyDecrypted)
}
if (split.usesItemsKey) {
const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloads<C>(
split.usesItemsKey.items,
split.usesItemsKey.key,
if (usesKeySystemRootKey) {
const keySystemRootKeyDecrypted = await this.rootKeyEncryption.decryptPayloads<C>(
usesKeySystemRootKey.items,
usesKeySystemRootKey.key,
)
extendArray(resultParams, keySystemRootKeyDecrypted)
}
if (usesKeySystemRootKeyWithKeyLookup) {
const keySystemRootKeyDecrypted = await this.rootKeyEncryption.decryptPayloadsWithKeyLookup<C>(
usesKeySystemRootKeyWithKeyLookup.items,
)
extendArray(resultParams, keySystemRootKeyDecrypted)
}
if (usesItemsKey) {
const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloads<C>(usesItemsKey.items, usesItemsKey.key)
extendArray(resultParams, itemsKeyDecrypted)
}
if (split.usesItemsKeyWithKeyLookup) {
if (usesItemsKeyWithKeyLookup) {
const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloadsWithKeyLookup<C>(
split.usesItemsKeyWithKeyLookup.items,
usesItemsKeyWithKeyLookup.items,
)
extendArray(resultParams, itemsKeyDecrypted)
}
@@ -349,6 +447,36 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
return packagedResults
}
async decryptPayloadWithKeyLookup<
C extends ItemContent = ItemContent,
P extends DecryptedPayloadInterface<C> = DecryptedPayloadInterface<C>,
>(
payload: EncryptedPayloadInterface,
): Promise<{
parameters: DecryptedParameters<C> | ErrorDecryptingParameters
payload: P | EncryptedPayloadInterface
}> {
const decryptedParameters = await this.itemsEncryption.decryptPayloadWithKeyLookup<C>(payload)
if (isErrorDecryptingParameters(decryptedParameters)) {
return {
parameters: decryptedParameters,
payload: new EncryptedPayload({
...payload.ejected(),
...decryptedParameters,
}),
}
} else {
return {
parameters: decryptedParameters,
payload: new DecryptedPayload<C>({
...payload.ejected(),
...decryptedParameters,
}) as P,
}
}
}
/**
* Returns true if the user's account protocol version is not equal to the latest version.
*/
@@ -420,27 +548,130 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
* Computes a root key given a password and key params.
* Delegates computation to respective protocol operator.
*/
public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface> {
public async computeRootKey<K extends RootKeyInterface>(password: string, keyParams: SNRootKeyParams): Promise<K> {
return this.rootKeyEncryption.computeRootKey(password, keyParams)
}
/**
* Creates a root key using the latest protocol version
*/
public async createRootKey(
public async createRootKey<K extends RootKeyInterface>(
identifier: string,
password: string,
origination: KeyParamsOrigination,
version?: ProtocolVersion,
) {
): Promise<K> {
return this.rootKeyEncryption.createRootKey(identifier, password, origination, version)
}
createRandomizedKeySystemRootKey(dto: {
systemIdentifier: KeySystemIdentifier
systemName: string
systemDescription?: string
}): KeySystemRootKeyInterface {
return this.operatorManager.defaultOperator().createRandomizedKeySystemRootKey(dto)
}
createUserInputtedKeySystemRootKey(dto: {
systemIdentifier: KeySystemIdentifier
systemName: string
systemDescription?: string
userInputtedPassword: string
}): KeySystemRootKeyInterface {
return this.operatorManager.defaultOperator().createUserInputtedKeySystemRootKey(dto)
}
deriveUserInputtedKeySystemRootKey(dto: {
keyParams: KeySystemRootKeyParamsInterface
userInputtedPassword: string
}): KeySystemRootKeyInterface {
return this.operatorManager.defaultOperator().deriveUserInputtedKeySystemRootKey(dto)
}
createKeySystemItemsKey(
uuid: string,
keySystemIdentifier: KeySystemIdentifier,
sharedVaultUuid: string | undefined,
rootKeyToken: string,
): KeySystemItemsKeyInterface {
return this.operatorManager
.defaultOperator()
.createKeySystemItemsKey(uuid, keySystemIdentifier, sharedVaultUuid, rootKeyToken)
}
asymmetricallyEncryptMessage(dto: {
message: AsymmetricMessagePayload
senderKeyPair: PkcKeyPair
senderSigningKeyPair: PkcKeyPair
recipientPublicKey: string
}): AsymmetricallyEncryptedString {
const operator = this.operatorManager.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.operatorManager.defaultOperator()
const version = defaultOperator.versionForAsymmetricallyEncryptedString(dto.encryptedString)
const keyOperator = this.operatorManager.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(
encryptedString: AsymmetricallyEncryptedString,
): AsymmetricSignatureVerificationDetachedResult {
const defaultOperator = this.operatorManager.defaultOperator()
const version = defaultOperator.versionForAsymmetricallyEncryptedString(encryptedString)
const keyOperator = this.operatorManager.operatorForVersion(version)
return keyOperator.asymmetricSignatureVerifyDetached(encryptedString)
}
getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PublicKeySet {
const defaultOperator = this.operatorManager.defaultOperator()
const version = defaultOperator.versionForAsymmetricallyEncryptedString(string)
const keyOperator = this.operatorManager.operatorForVersion(version)
return keyOperator.getSenderPublicKeySetFromAsymmetricallyEncryptedString(string)
}
public async decryptBackupFile(
file: BackupFile,
password?: string,
): Promise<ClientDisplayableError | (EncryptedPayloadInterface | DecryptedPayloadInterface<ItemContent>)[]> {
const result = await DecryptBackupFile(file, this, password)
const usecase = new DecryptBackupFileUseCase(this)
const result = await usecase.execute(file, password)
return result
}
@@ -468,7 +699,7 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
items: ejected,
}
const keyParams = await this.getRootKeyParams()
const keyParams = this.getRootKeyParams()
data.keyParams = keyParams?.getPortableValue()
return data
}
@@ -504,7 +735,7 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
return (await this.rootKeyEncryption.hasRootKeyWrapper()) && this.rootKeyEncryption.getRootKey() == undefined
}
public async getRootKeyParams() {
public getRootKeyParams() {
return this.rootKeyEncryption.getRootKeyParams()
}
@@ -517,7 +748,7 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
* Wrapping key params are read from disk.
*/
public async computeWrappingKey(passcode: string) {
const keyParams = await this.rootKeyEncryption.getSureRootKeyWrapperKeyParams()
const keyParams = this.rootKeyEncryption.getSureRootKeyWrapperKeyParams()
const key = await this.computeRootKey(passcode, keyParams)
return key
}
@@ -545,17 +776,21 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
await this.rootKeyEncryption.removeRootKeyWrapper()
}
public async setRootKey(key: SNRootKey, wrappingKey?: SNRootKey) {
public async setRootKey(key: RootKeyInterface, wrappingKey?: SNRootKey) {
await this.rootKeyEncryption.setRootKey(key, wrappingKey)
}
/**
* Returns the in-memory root key value.
*/
public getRootKey() {
public getRootKey(): RootKeyInterface | undefined {
return this.rootKeyEncryption.getRootKey()
}
public getSureRootKey(): RootKeyInterface {
return this.rootKeyEncryption.getRootKey() as RootKeyInterface
}
/**
* Deletes root key and wrapper from keychain. Used when signing out of application.
*/
@@ -571,26 +806,31 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
return this.rootKeyEncryption.validatePasscode(passcode)
}
public getEmbeddedPayloadAuthenticatedData(
public getEmbeddedPayloadAuthenticatedData<D extends ItemAuthenticatedData>(
payload: EncryptedPayloadInterface,
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
): D | undefined {
const version = payload.version
if (!version) {
return undefined
}
const operator = this.operatorManager.operatorForVersion(version)
const authenticatedData = operator.getPayloadAuthenticatedData(encryptedParametersFromPayload(payload))
return authenticatedData
const authenticatedData = operator.getPayloadAuthenticatedDataForExternalUse(
encryptedInputParametersFromPayload(payload),
)
return authenticatedData as D
}
/** Returns the key params attached to this key's encrypted payload */
public getKeyEmbeddedKeyParams(key: EncryptedPayloadInterface): SNRootKeyParams | undefined {
public getKeyEmbeddedKeyParamsFromItemsKey(key: EncryptedPayloadInterface): SNRootKeyParams | undefined {
const authenticatedData = this.getEmbeddedPayloadAuthenticatedData(key)
if (!authenticatedData) {
return undefined
}
if (isVersionLessThanOrEqualTo(key.version, ProtocolVersion.V003)) {
const rawKeyParams = authenticatedData as LegacyAttachedData
const rawKeyParams = authenticatedData as unknown as LegacyAttachedData
return this.createKeyParams(rawKeyParams)
} else {
const rawKeyParams = (authenticatedData as RootKeyEncryptedAuthenticatedData).kp
@@ -683,7 +923,7 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
const hasSyncedItemsKey = !isNullOrUndefined(defaultSyncedKey)
if (hasSyncedItemsKey) {
/** Delete all never synced keys */
await this.itemManager.setItemsToBeDeleted(neverSyncedKeys)
await this.mutator.setItemsToBeDeleted(neverSyncedKeys)
} else {
/**
* No previous synced items key.
@@ -692,14 +932,14 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
* we end up with 0 items keys, create a new one. This covers the case when you open
* the app offline and it creates an 004 key, and then you sign into an 003 account.
*/
const rootKeyParams = await this.getRootKeyParams()
const rootKeyParams = this.getRootKeyParams()
if (rootKeyParams) {
/** If neverSynced.version != rootKey.version, delete. */
const toDelete = neverSyncedKeys.filter((itemsKey) => {
return itemsKey.keyVersion !== rootKeyParams.version
})
if (toDelete.length > 0) {
await this.itemManager.setItemsToBeDeleted(toDelete)
await this.mutator.setItemsToBeDeleted(toDelete)
}
if (this.itemsEncryption.getItemsKeys().length === 0) {
@@ -741,26 +981,7 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
const unsyncedKeys = this.itemsEncryption.getItemsKeys().filter((key) => key.neverSynced && !key.dirty)
if (unsyncedKeys.length > 0) {
void this.itemManager.setItemsDirty(unsyncedKeys)
}
}
override async getDiagnostics(): Promise<DiagnosticInfo | undefined> {
return {
encryption: {
getLatestVersion: this.getLatestVersion(),
hasAccount: this.hasAccount(),
hasRootKeyEncryptionSource: this.hasRootKeyEncryptionSource(),
getUserVersion: this.getUserVersion(),
upgradeAvailable: await this.upgradeAvailable(),
accountUpgradeAvailable: this.accountUpgradeAvailable(),
passcodeUpgradeAvailable: await this.passcodeUpgradeAvailable(),
hasPasscode: this.hasPasscode(),
isPasscodeLocked: await this.isPasscodeLocked(),
needsNewRootKeyBasedItemsKey: this.needsNewRootKeyBasedItemsKey(),
...(await this.itemsEncryption.getDiagnostics()),
...(await this.rootKeyEncryption.getDiagnostics()),
},
void this.mutator.setItemsDirty(unsyncedKeys)
}
}
}

View File

@@ -49,7 +49,7 @@ export async function DecryptItemsKeyByPromptingUser(
| 'aborted'
> {
if (!keyParams) {
keyParams = encryptor.getKeyEmbeddedKeyParams(itemsKey)
keyParams = encryptor.getKeyEmbeddedKeyParamsFromItemsKey(itemsKey)
}
if (!keyParams) {

View File

@@ -1,7 +1,6 @@
import { ContentType, ProtocolVersion } from '@standardnotes/common'
import {
DecryptedParameters,
EncryptedParameters,
ErrorDecryptingParameters,
findDefaultItemsKey,
isErrorDecryptingParameters,
@@ -9,26 +8,30 @@ import {
StandardException,
encryptPayload,
decryptPayload,
EncryptedOutputParameters,
KeySystemKeyManagerInterface,
} from '@standardnotes/encryption'
import {
ContentTypeUsesKeySystemRootKeyEncryption,
DecryptedPayload,
DecryptedPayloadInterface,
EncryptedPayload,
EncryptedPayloadInterface,
KeySystemRootKeyInterface,
isEncryptedPayload,
ItemContent,
ItemsKeyInterface,
PayloadEmitSource,
KeySystemItemsKeyInterface,
SureFindPayload,
ContentTypeUsesRootKeyEncryption,
} from '@standardnotes/models'
import { Uuids } from '@standardnotes/utils'
import { DiagnosticInfo } from '../Diagnostics/ServiceDiagnostics'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
import { AbstractService } from '../Service/AbstractService'
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
export class ItemsEncryptionService extends AbstractService {
private removeItemsObserver!: () => void
@@ -39,13 +42,14 @@ export class ItemsEncryptionService extends AbstractService {
private payloadManager: PayloadManagerInterface,
private storageService: StorageServiceInterface,
private operatorManager: OperatorManager,
private keys: KeySystemKeyManagerInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
this.removeItemsObserver = this.itemManager.addObserver([ContentType.ItemsKey], ({ changed, inserted }) => {
if (changed.concat(inserted).length > 0) {
void this.decryptErroredPayloads()
void this.decryptErroredItemPayloads()
}
})
}
@@ -54,6 +58,8 @@ export class ItemsEncryptionService extends AbstractService {
;(this.itemManager as unknown) = undefined
;(this.payloadManager as unknown) = undefined
;(this.storageService as unknown) = undefined
;(this.operatorManager as unknown) = undefined
;(this.keys as unknown) = undefined
this.removeItemsObserver()
;(this.removeItemsObserver as unknown) = undefined
super.deinit()
@@ -70,12 +76,17 @@ export class ItemsEncryptionService extends AbstractService {
return this.storageService.savePayloads(payloads)
}
public getItemsKeys() {
public getItemsKeys(): ItemsKeyInterface[] {
return this.itemManager.getDisplayableItemsKeys()
}
public itemsKeyForPayload(payload: EncryptedPayloadInterface): ItemsKeyInterface | undefined {
return this.getItemsKeys().find(
public itemsKeyForEncryptedPayload(
payload: EncryptedPayloadInterface,
): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined {
const itemsKeys = this.getItemsKeys()
const keySystemItemsKeys = this.itemManager.getItems<KeySystemItemsKeyInterface>(ContentType.KeySystemItemsKey)
return [...itemsKeys, ...keySystemItemsKeys].find(
(key) => key.uuid === payload.items_key_id || key.duplicateOf === payload.items_key_id,
)
}
@@ -84,8 +95,20 @@ export class ItemsEncryptionService extends AbstractService {
return findDefaultItemsKey(this.getItemsKeys())
}
private keyToUseForItemEncryption(): ItemsKeyInterface | StandardException {
private keyToUseForItemEncryption(
payload: DecryptedPayloadInterface,
): ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | StandardException {
if (payload.key_system_identifier) {
const keySystemItemsKey = this.keys.getPrimaryKeySystemItemsKey(payload.key_system_identifier)
if (!keySystemItemsKey) {
return new StandardException('Cannot find key system items key to use for encryption')
}
return keySystemItemsKey
}
const defaultKey = this.getDefaultItemsKey()
let result: ItemsKeyInterface | undefined = undefined
if (this.userVersion && this.userVersion !== defaultKey?.keyVersion) {
@@ -107,9 +130,11 @@ export class ItemsEncryptionService extends AbstractService {
return result
}
private keyToUseForDecryptionOfPayload(payload: EncryptedPayloadInterface): ItemsKeyInterface | undefined {
private keyToUseForDecryptionOfPayload(
payload: EncryptedPayloadInterface,
): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined {
if (payload.items_key_id) {
const itemsKey = this.itemsKeyForPayload(payload)
const itemsKey = this.itemsKeyForEncryptedPayload(payload)
return itemsKey
}
@@ -117,20 +142,24 @@ export class ItemsEncryptionService extends AbstractService {
return defaultKey
}
public async encryptPayloadWithKeyLookup(payload: DecryptedPayloadInterface): Promise<EncryptedParameters> {
const key = this.keyToUseForItemEncryption()
public async encryptPayloadWithKeyLookup(
payload: DecryptedPayloadInterface,
signingKeyPair?: PkcKeyPair,
): Promise<EncryptedOutputParameters> {
const key = this.keyToUseForItemEncryption(payload)
if (key instanceof StandardException) {
throw Error(key.message)
}
return this.encryptPayload(payload, key)
return this.encryptPayload(payload, key, signingKeyPair)
}
public async encryptPayload(
payload: DecryptedPayloadInterface,
key: ItemsKeyInterface,
): Promise<EncryptedParameters> {
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface,
signingKeyPair?: PkcKeyPair,
): Promise<EncryptedOutputParameters> {
if (isEncryptedPayload(payload)) {
throw Error('Attempting to encrypt already encrypted payload.')
}
@@ -141,18 +170,22 @@ export class ItemsEncryptionService extends AbstractService {
throw Error('Attempting to encrypt payload with no UuidGenerator.')
}
return encryptPayload(payload, key, this.operatorManager)
return encryptPayload(payload, key, this.operatorManager, signingKeyPair)
}
public async encryptPayloads(
payloads: DecryptedPayloadInterface[],
key: ItemsKeyInterface,
): Promise<EncryptedParameters[]> {
return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key)))
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface,
signingKeyPair?: PkcKeyPair,
): Promise<EncryptedOutputParameters[]> {
return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key, signingKeyPair)))
}
public async encryptPayloadsWithKeyLookup(payloads: DecryptedPayloadInterface[]): Promise<EncryptedParameters[]> {
return Promise.all(payloads.map((payload) => this.encryptPayloadWithKeyLookup(payload)))
public async encryptPayloadsWithKeyLookup(
payloads: DecryptedPayloadInterface[],
signingKeyPair?: PkcKeyPair,
): Promise<EncryptedOutputParameters[]> {
return Promise.all(payloads.map((payload) => this.encryptPayloadWithKeyLookup(payload, signingKeyPair)))
}
public async decryptPayloadWithKeyLookup<C extends ItemContent = ItemContent>(
@@ -173,7 +206,7 @@ export class ItemsEncryptionService extends AbstractService {
public async decryptPayload<C extends ItemContent = ItemContent>(
payload: EncryptedPayloadInterface,
key: ItemsKeyInterface,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface,
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
if (!payload.content) {
return {
@@ -193,21 +226,24 @@ export class ItemsEncryptionService extends AbstractService {
public async decryptPayloads<C extends ItemContent = ItemContent>(
payloads: EncryptedPayloadInterface[],
key: ItemsKeyInterface,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface,
): Promise<(DecryptedParameters<C> | ErrorDecryptingParameters)[]> {
return Promise.all(payloads.map((payload) => this.decryptPayload<C>(payload, key)))
}
public async decryptErroredPayloads(): Promise<void> {
const payloads = this.payloadManager.invalidPayloads.filter((i) => i.content_type !== ContentType.ItemsKey)
if (payloads.length === 0) {
public async decryptErroredItemPayloads(): Promise<void> {
const erroredItemPayloads = this.payloadManager.invalidPayloads.filter(
(i) =>
!ContentTypeUsesRootKeyEncryption(i.content_type) && !ContentTypeUsesKeySystemRootKeyEncryption(i.content_type),
)
if (erroredItemPayloads.length === 0) {
return
}
const resultParams = await this.decryptPayloadsWithKeyLookup(payloads)
const resultParams = await this.decryptPayloadsWithKeyLookup(erroredItemPayloads)
const decryptedPayloads = resultParams.map((params) => {
const original = SureFindPayload(payloads, params.uuid)
const original = SureFindPayload(erroredItemPayloads, params.uuid)
if (isErrorDecryptingParameters(params)) {
return new EncryptedPayload({
...original.ejected(),
@@ -247,15 +283,4 @@ export class ItemsEncryptionService extends AbstractService {
return key.keyVersion === version
})
}
override async getDiagnostics(): Promise<DiagnosticInfo | undefined> {
const keyForItems = this.keyToUseForItemEncryption()
return {
itemsEncryption: {
itemsKeysIds: Uuids(this.getItemsKeys()),
defaultItemsKeyId: this.getDefaultItemsKey()?.uuid,
keyToUseForItemEncryptionId: keyForItems instanceof StandardException ? undefined : keyForItems.uuid,
},
}
}
}

View File

@@ -1,3 +1,4 @@
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import {
ApplicationIdentifier,
ProtocolVersionLatest,
@@ -17,15 +18,19 @@ import {
CreateAnyKeyParams,
SNRootKey,
isErrorDecryptingParameters,
EncryptedParameters,
DecryptedParameters,
ErrorDecryptingParameters,
findDefaultItemsKey,
ItemsKeyMutator,
encryptPayload,
decryptPayload,
EncryptedOutputParameters,
DecryptedParameters,
KeySystemKeyManagerInterface,
} from '@standardnotes/encryption'
import {
ContentTypeUsesKeySystemRootKeyEncryption,
ContentTypesUsingRootKeyEncryption,
ContentTypeUsesRootKeyEncryption,
CreateDecryptedItemFromPayload,
DecryptedPayload,
DecryptedPayloadInterface,
@@ -34,25 +39,29 @@ import {
EncryptedPayloadInterface,
EncryptedTransferPayload,
FillItemContentSpecialized,
KeySystemRootKeyInterface,
ItemContent,
ItemsKeyContent,
ItemsKeyContentSpecialized,
ItemsKeyInterface,
NamespacedRootKeyInKeychain,
PayloadEmitSource,
PayloadTimestampDefaults,
RootKeyContent,
RootKeyInterface,
SureFindPayload,
KeySystemIdentifier,
} from '@standardnotes/models'
import { UuidGenerator } from '@standardnotes/utils'
import { DeviceInterface } from '../Device/DeviceInterface'
import { DiagnosticInfo } from '../Diagnostics/ServiceDiagnostics'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { AbstractService } from '../Service/AbstractService'
import { StorageKey } from '../Storage/StorageKeys'
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
import { StorageValueModes } from '../Storage/StorageTypes'
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEvent> {
private rootKey?: RootKeyInterface
@@ -60,10 +69,13 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
public memoizedRootKeyParams?: SNRootKeyParams
constructor(
private itemManager: ItemManagerInterface,
private items: ItemManagerInterface,
private mutator: MutatorClientInterface,
private operatorManager: OperatorManager,
public deviceInterface: DeviceInterface,
private storageService: StorageServiceInterface,
private payloadManager: PayloadManagerInterface,
private keys: KeySystemKeyManagerInterface,
private identifier: ApplicationIdentifier,
protected override internalEventBus: InternalEventBusInterface,
) {
@@ -71,7 +83,13 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
}
public override deinit(): void {
;(this.itemManager as unknown) = undefined
;(this.items as unknown) = undefined
;(this.operatorManager as unknown) = undefined
;(this.deviceInterface as unknown) = undefined
;(this.storageService as unknown) = undefined
;(this.payloadManager as unknown) = undefined
;(this.keys as unknown) = undefined
this.rootKey = undefined
this.memoizedRootKeyParams = undefined
super.deinit()
@@ -144,7 +162,7 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
if (this.hasAccount()) {
return this.getSureUserVersion()
} else if (this.hasPasscode()) {
const passcodeParams = await this.getSureRootKeyWrapperKeyParams()
const passcodeParams = this.getSureRootKeyWrapperKeyParams()
return passcodeParams.version
}
@@ -170,7 +188,7 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
return undefined
}
const keyParams = await this.getSureRootKeyParams()
const keyParams = this.getSureRootKeyParams()
return CreateNewRootKey({
...rawKey,
@@ -193,11 +211,8 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
})
}
public async getRootKeyWrapperKeyParams(): Promise<SNRootKeyParams | undefined> {
const rawKeyParams = await this.storageService.getValue(
StorageKey.RootKeyWrapperKeyParams,
StorageValueModes.Nonwrapped,
)
public getRootKeyWrapperKeyParams(): SNRootKeyParams | undefined {
const rawKeyParams = this.storageService.getValue(StorageKey.RootKeyWrapperKeyParams, StorageValueModes.Nonwrapped)
if (!rawKeyParams) {
return undefined
@@ -206,11 +221,11 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
return CreateAnyKeyParams(rawKeyParams as AnyKeyParamsContent)
}
public async getSureRootKeyWrapperKeyParams() {
return this.getRootKeyWrapperKeyParams() as Promise<SNRootKeyParams>
public getSureRootKeyWrapperKeyParams() {
return this.getRootKeyWrapperKeyParams() as SNRootKeyParams
}
public async getRootKeyParams(): Promise<SNRootKeyParams | undefined> {
public getRootKeyParams(): SNRootKeyParams | undefined {
if (this.keyMode === KeyMode.WrapperOnly) {
return this.getRootKeyWrapperKeyParams()
} else if (this.keyMode === KeyMode.RootKeyOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
@@ -222,22 +237,22 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
}
}
public async getSureRootKeyParams(): Promise<SNRootKeyParams> {
return this.getRootKeyParams() as Promise<SNRootKeyParams>
public getSureRootKeyParams(): SNRootKeyParams {
return this.getRootKeyParams() as SNRootKeyParams
}
public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface> {
public async computeRootKey<K extends RootKeyInterface>(password: string, keyParams: SNRootKeyParams): Promise<K> {
const version = keyParams.version
const operator = this.operatorManager.operatorForVersion(version)
return operator.computeRootKey(password, keyParams)
}
public async createRootKey(
public async createRootKey<K extends RootKeyInterface>(
identifier: string,
password: string,
origination: KeyParamsOrigination,
version?: ProtocolVersion,
) {
): Promise<K> {
const operator = version ? this.operatorManager.operatorForVersion(version) : this.operatorManager.defaultOperator()
return operator.createRootKey(identifier, password, origination)
}
@@ -291,8 +306,8 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
}
}
private async recomputeAccountKeyParams(): Promise<SNRootKeyParams | undefined> {
const rawKeyParams = await this.storageService.getValue(StorageKey.RootKeyParams, StorageValueModes.Nonwrapped)
private recomputeAccountKeyParams(): SNRootKeyParams | undefined {
const rawKeyParams = this.storageService.getValue(StorageKey.RootKeyParams, StorageValueModes.Nonwrapped)
if (!rawKeyParams) {
return
@@ -308,10 +323,12 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
*/
private async wrapAndPersistRootKey(wrappingKey: SNRootKey) {
const rootKey = this.getSureRootKey()
const value: DecryptedTransferPayload = {
...rootKey.payload.ejected(),
content: FillItemContentSpecialized(rootKey.persistableValueWhenWrapping()),
}
const payload = new DecryptedPayload(value)
const wrappedKey = await this.encryptPayload(payload, wrappingKey)
@@ -371,7 +388,7 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
if (this.keyMode === KeyMode.WrapperOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
if (this.keyMode === KeyMode.WrapperOnly) {
this.setRootKeyInstance(wrappingKey)
await this.reencryptItemsKeys()
await this.reencryptApplicableItemsAfterUserRootKeyChange()
} else {
await this.wrapAndPersistRootKey(wrappingKey)
}
@@ -487,35 +504,65 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
}
private getItemsKeys() {
return this.itemManager.getDisplayableItemsKeys()
return this.items.getDisplayableItemsKeys()
}
public async encrypPayloadWithKeyLookup(payload: DecryptedPayloadInterface): Promise<EncryptedParameters> {
const key = this.getRootKey()
private async encryptPayloadWithKeyLookup(
payload: DecryptedPayloadInterface,
signingKeyPair?: PkcKeyPair,
): Promise<EncryptedOutputParameters> {
let key: RootKeyInterface | KeySystemRootKeyInterface | undefined
if (ContentTypeUsesKeySystemRootKeyEncryption(payload.content_type)) {
if (!payload.key_system_identifier) {
throw Error(`Key system-encrypted payload ${payload.content_type}is missing a key_system_identifier`)
}
key = this.keys.getPrimaryKeySystemRootKey(payload.key_system_identifier)
} else {
key = this.getRootKey()
}
if (key == undefined) {
throw Error('Attempting root key encryption with no root key')
}
return this.encryptPayload(payload, key)
return this.encryptPayload(payload, key, signingKeyPair)
}
public async encryptPayloadsWithKeyLookup(payloads: DecryptedPayloadInterface[]): Promise<EncryptedParameters[]> {
return Promise.all(payloads.map((payload) => this.encrypPayloadWithKeyLookup(payload)))
public async encryptPayloadsWithKeyLookup(
payloads: DecryptedPayloadInterface[],
signingKeyPair?: PkcKeyPair,
): Promise<EncryptedOutputParameters[]> {
return Promise.all(payloads.map((payload) => this.encryptPayloadWithKeyLookup(payload, signingKeyPair)))
}
public async encryptPayload(payload: DecryptedPayloadInterface, key: RootKeyInterface): Promise<EncryptedParameters> {
return encryptPayload(payload, key, this.operatorManager)
public async encryptPayload(
payload: DecryptedPayloadInterface,
key: RootKeyInterface | KeySystemRootKeyInterface,
signingKeyPair?: PkcKeyPair,
): Promise<EncryptedOutputParameters> {
return encryptPayload(payload, key, this.operatorManager, signingKeyPair)
}
public async encryptPayloads(payloads: DecryptedPayloadInterface[], key: RootKeyInterface) {
return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key)))
public async encryptPayloads(
payloads: DecryptedPayloadInterface[],
key: RootKeyInterface | KeySystemRootKeyInterface,
signingKeyPair?: PkcKeyPair,
) {
return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key, signingKeyPair)))
}
public async decryptPayloadWithKeyLookup<C extends ItemContent = ItemContent>(
payload: EncryptedPayloadInterface,
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
const key = this.getRootKey()
let key: RootKeyInterface | KeySystemRootKeyInterface | undefined
if (ContentTypeUsesKeySystemRootKeyEncryption(payload.content_type)) {
if (!payload.key_system_identifier) {
throw Error('Key system root key encrypted payload is missing key_system_identifier')
}
key = this.keys.getPrimaryKeySystemRootKey(payload.key_system_identifier)
} else {
key = this.getRootKey()
}
if (key == undefined) {
return {
@@ -530,7 +577,7 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
public async decryptPayload<C extends ItemContent = ItemContent>(
payload: EncryptedPayloadInterface,
key: RootKeyInterface,
key: RootKeyInterface | KeySystemRootKeyInterface,
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
return decryptPayload(payload, key, this.operatorManager)
}
@@ -543,25 +590,63 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
public async decryptPayloads<C extends ItemContent = ItemContent>(
payloads: EncryptedPayloadInterface[],
key: RootKeyInterface,
key: RootKeyInterface | KeySystemRootKeyInterface,
): Promise<(DecryptedParameters<C> | ErrorDecryptingParameters)[]> {
return Promise.all(payloads.map((payload) => this.decryptPayload<C>(payload, key)))
}
/**
* When the root key changes (non-null only), we must re-encrypt all items
* keys with this new root key (by simply re-syncing).
*/
public async reencryptItemsKeys(): Promise<void> {
const itemsKeys = this.getItemsKeys()
public async decryptErroredRootPayloads(): Promise<void> {
const erroredRootPayloads = this.payloadManager.invalidPayloads.filter(
(i) =>
ContentTypeUsesRootKeyEncryption(i.content_type) || ContentTypeUsesKeySystemRootKeyEncryption(i.content_type),
)
if (erroredRootPayloads.length === 0) {
return
}
if (itemsKeys.length > 0) {
const resultParams = await this.decryptPayloadsWithKeyLookup(erroredRootPayloads)
const decryptedPayloads = resultParams.map((params) => {
const original = SureFindPayload(erroredRootPayloads, params.uuid)
if (isErrorDecryptingParameters(params)) {
return new EncryptedPayload({
...original.ejected(),
...params,
})
} else {
return new DecryptedPayload({
...original.ejected(),
...params,
})
}
})
await this.payloadManager.emitPayloads(decryptedPayloads, PayloadEmitSource.LocalChanged)
}
/**
* When the root key changes, we must re-encrypt all relevant items with this new root key (by simply re-syncing).
*/
public async reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void> {
const items = this.items.getItems(ContentTypesUsingRootKeyEncryption())
if (items.length > 0) {
/**
* Do not call sync after marking dirty.
* Re-encrypting items keys is called by consumers who have specific flows who
* will sync on their own timing
*/
await this.itemManager.setItemsDirty(itemsKeys)
await this.mutator.setItemsDirty(items)
}
}
/**
* 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)
}
}
@@ -599,14 +684,13 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
})
for (const key of defaultKeys) {
await this.itemManager.changeItemsKey(key, (mutator) => {
await this.mutator.changeItemsKey(key, (mutator) => {
mutator.isDefault = false
})
}
const itemsKey = (await this.itemManager.insertItem(itemTemplate)) as ItemsKeyInterface
await this.itemManager.changeItemsKey(itemsKey, (mutator) => {
const itemsKey = await this.mutator.insertItem<ItemsKeyInterface>(itemTemplate)
await this.mutator.changeItemsKey(itemsKey, (mutator) => {
mutator.isDefault = true
})
@@ -618,10 +702,10 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
const newDefaultItemsKey = await this.createNewDefaultItemsKey()
const rollback = async () => {
await this.itemManager.setItemToBeDeleted(newDefaultItemsKey)
await this.mutator.setItemToBeDeleted(newDefaultItemsKey)
if (currentDefaultItemsKey) {
await this.itemManager.changeItem<ItemsKeyMutator>(currentDefaultItemsKey, (mutator) => {
await this.mutator.changeItem<ItemsKeyMutator>(currentDefaultItemsKey, (mutator) => {
mutator.isDefault = true
})
}
@@ -629,19 +713,4 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
return rollback
}
override async getDiagnostics(): Promise<DiagnosticInfo | undefined> {
return {
rootKeyEncryption: {
hasRootKey: this.rootKey != undefined,
keyMode: KeyMode[this.keyMode],
hasRootKeyWrapper: await this.hasRootKeyWrapper(),
hasAccount: this.hasAccount(),
hasRootKeyEncryptionSource: this.hasRootKeyEncryptionSource(),
hasPasscode: this.hasPasscode(),
getEncryptionSourceVersion: this.hasRootKeyEncryptionSource() && (await this.getEncryptionSourceVersion()),
getUserVersion: this.getUserVersion(),
},
}
}
}