feat(encryption): refactor circular dependencies on services
This commit is contained in:
251
packages/services/src/Domain/Encryption/BackupFileDecryptor.ts
Normal file
251
packages/services/src/Domain/Encryption/BackupFileDecryptor.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
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
|
||||
}
|
||||
766
packages/services/src/Domain/Encryption/EncryptionService.ts
Normal file
766
packages/services/src/Domain/Encryption/EncryptionService.ts
Normal file
@@ -0,0 +1,766 @@
|
||||
import {
|
||||
CreateAnyKeyParams,
|
||||
CreateEncryptionSplitWithKeyLookup,
|
||||
DecryptedParameters,
|
||||
EncryptedParameters,
|
||||
encryptedParametersFromPayload,
|
||||
EncryptionProvider,
|
||||
ErrorDecryptingParameters,
|
||||
findDefaultItemsKey,
|
||||
FindPayloadInDecryptionSplit,
|
||||
FindPayloadInEncryptionSplit,
|
||||
isErrorDecryptingParameters,
|
||||
ItemAuthenticatedData,
|
||||
KeyedDecryptionSplit,
|
||||
KeyedEncryptionSplit,
|
||||
KeyMode,
|
||||
LegacyAttachedData,
|
||||
OperatorManager,
|
||||
RootKeyEncryptedAuthenticatedData,
|
||||
RootKeyServiceEvent,
|
||||
SNRootKey,
|
||||
SNRootKeyParams,
|
||||
SplitPayloadsByEncryptionType,
|
||||
V001Algorithm,
|
||||
V002Algorithm,
|
||||
} from '@standardnotes/encryption'
|
||||
import {
|
||||
BackupFile,
|
||||
CreateDecryptedBackupFileContextPayload,
|
||||
CreateEncryptedBackupFileContextPayload,
|
||||
DecryptedPayload,
|
||||
DecryptedPayloadInterface,
|
||||
EncryptedPayload,
|
||||
EncryptedPayloadInterface,
|
||||
isDecryptedPayload,
|
||||
isEncryptedPayload,
|
||||
ItemContent,
|
||||
ItemsKeyInterface,
|
||||
RootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import {
|
||||
extendArray,
|
||||
isNotUndefined,
|
||||
isNullOrUndefined,
|
||||
isReactNativeEnvironment,
|
||||
isWebCryptoAvailable,
|
||||
UuidGenerator,
|
||||
} from '@standardnotes/utils'
|
||||
import {
|
||||
AnyKeyParamsContent,
|
||||
ApplicationIdentifier,
|
||||
compareVersions,
|
||||
ContentType,
|
||||
isVersionLessThanOrEqualTo,
|
||||
KeyParamsOrigination,
|
||||
ProtocolVersion,
|
||||
ProtocolVersionLastNonrootItemsKey,
|
||||
ProtocolVersionLatest,
|
||||
} from '@standardnotes/common'
|
||||
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { ItemsEncryptionService } from './ItemsEncryption'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
|
||||
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 { EncryptionServiceEvent } from './EncryptionServiceEvent'
|
||||
|
||||
/**
|
||||
* The encryption service is responsible for the encryption and decryption of payloads, and
|
||||
* handles delegation of a task to the respective protocol operator. Each version of the protocol
|
||||
* (001, 002, 003, 004, etc) uses a respective operator version to perform encryption operations.
|
||||
* Operators are located in /protocol/operator.
|
||||
* The protocol service depends on the keyManager for determining which key to use for the
|
||||
* encryption and decryption of a particular payload.
|
||||
* The protocol service is also responsible for dictating which protocol versions are valid,
|
||||
* and which are no longer valid or not supported.
|
||||
|
||||
* The key manager is responsible for managing root key and root key wrapper states.
|
||||
* When the key manager is initialized, it initiates itself with a keyMode, which
|
||||
* dictates the entire flow of key management. The key manager's responsibilities include:
|
||||
* - interacting with the device keychain to save or clear the root key
|
||||
* - interacting with storage to save root key params or wrapper params, or the wrapped root key.
|
||||
* - exposing methods that allow the application to unwrap the root key (unlock the application)
|
||||
*
|
||||
* It also exposes two primary methods for determining what key should be used to encrypt
|
||||
* or decrypt a particular payload. Some payloads are encrypted directly with the rootKey
|
||||
* (such as itemsKeys and encryptedStorage). Others are encrypted with itemsKeys (notes, tags, etc).
|
||||
|
||||
* The items key manager manages the lifecycle of items keys.
|
||||
* It is responsible for creating the default items key when conditions call for it
|
||||
* (such as after the first sync completes and no key exists).
|
||||
* It also exposes public methods that allows consumers to retrieve an items key
|
||||
* for a particular payload, and also retrieve all available items keys.
|
||||
*/
|
||||
export class EncryptionService extends AbstractService<EncryptionServiceEvent> implements EncryptionProvider {
|
||||
private operatorManager: OperatorManager
|
||||
private readonly itemsEncryption: ItemsEncryptionService
|
||||
private readonly rootKeyEncryption: RootKeyEncryptionService
|
||||
private rootKeyObserverDisposer: () => void
|
||||
|
||||
constructor(
|
||||
private itemManager: ItemManagerInterface,
|
||||
private payloadManager: PayloadManagerInterface,
|
||||
public deviceInterface: DeviceInterface,
|
||||
private storageService: StorageServiceInterface,
|
||||
private identifier: ApplicationIdentifier,
|
||||
public crypto: PureCryptoInterface,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
this.crypto = crypto
|
||||
|
||||
this.operatorManager = new OperatorManager(crypto)
|
||||
|
||||
this.itemsEncryption = new ItemsEncryptionService(
|
||||
itemManager,
|
||||
payloadManager,
|
||||
storageService,
|
||||
this.operatorManager,
|
||||
internalEventBus,
|
||||
)
|
||||
|
||||
this.rootKeyEncryption = new RootKeyEncryptionService(
|
||||
this.itemManager,
|
||||
this.operatorManager,
|
||||
this.deviceInterface,
|
||||
this.storageService,
|
||||
this.identifier,
|
||||
this.internalEventBus,
|
||||
)
|
||||
this.rootKeyObserverDisposer = this.rootKeyEncryption.addEventObserver((event) => {
|
||||
this.itemsEncryption.userVersion = this.getUserVersion()
|
||||
if (event === RootKeyServiceEvent.RootKeyStatusChanged) {
|
||||
void this.notifyEvent(EncryptionServiceEvent.RootKeyStatusChanged)
|
||||
}
|
||||
})
|
||||
|
||||
UuidGenerator.SetGenerator(this.crypto.generateUUID)
|
||||
}
|
||||
|
||||
public override deinit(): void {
|
||||
;(this.itemManager as unknown) = undefined
|
||||
;(this.payloadManager as unknown) = undefined
|
||||
;(this.deviceInterface as unknown) = undefined
|
||||
;(this.storageService as unknown) = undefined
|
||||
;(this.crypto as unknown) = undefined
|
||||
;(this.operatorManager as unknown) = undefined
|
||||
|
||||
this.rootKeyObserverDisposer()
|
||||
;(this.rootKeyObserverDisposer as unknown) = undefined
|
||||
|
||||
this.itemsEncryption.deinit()
|
||||
;(this.itemsEncryption as unknown) = undefined
|
||||
|
||||
this.rootKeyEncryption.deinit()
|
||||
;(this.rootKeyEncryption as unknown) = undefined
|
||||
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
await this.rootKeyEncryption.initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns encryption protocol display name for active account/wrapper
|
||||
*/
|
||||
public async getEncryptionDisplayName(): Promise<string> {
|
||||
const version = await this.rootKeyEncryption.getEncryptionSourceVersion()
|
||||
|
||||
if (version) {
|
||||
return this.operatorManager.operatorForVersion(version).getEncryptionDisplayName()
|
||||
}
|
||||
|
||||
throw Error('Attempting to access encryption display name wtihout source')
|
||||
}
|
||||
|
||||
public getLatestVersion() {
|
||||
return ProtocolVersionLatest
|
||||
}
|
||||
|
||||
public hasAccount() {
|
||||
return this.rootKeyEncryption.hasAccount()
|
||||
}
|
||||
|
||||
public hasRootKeyEncryptionSource(): boolean {
|
||||
return this.rootKeyEncryption.hasRootKeyEncryptionSource()
|
||||
}
|
||||
|
||||
public getUserVersion(): ProtocolVersion | undefined {
|
||||
return this.rootKeyEncryption.getUserVersion()
|
||||
}
|
||||
|
||||
public async upgradeAvailable() {
|
||||
const accountUpgradeAvailable = this.accountUpgradeAvailable()
|
||||
const passcodeUpgradeAvailable = await this.passcodeUpgradeAvailable()
|
||||
return accountUpgradeAvailable || passcodeUpgradeAvailable
|
||||
}
|
||||
|
||||
public getSureDefaultItemsKey(): ItemsKeyInterface {
|
||||
return this.itemsEncryption.getDefaultItemsKey() as ItemsKeyInterface
|
||||
}
|
||||
|
||||
async repersistAllItems(): Promise<void> {
|
||||
return this.itemsEncryption.repersistAllItems()
|
||||
}
|
||||
|
||||
public async reencryptItemsKeys(): Promise<void> {
|
||||
await this.rootKeyEncryption.reencryptItemsKeys()
|
||||
}
|
||||
|
||||
public async createNewItemsKeyWithRollback(): Promise<() => Promise<void>> {
|
||||
return this.rootKeyEncryption.createNewItemsKeyWithRollback()
|
||||
}
|
||||
|
||||
public async decryptErroredPayloads(): Promise<void> {
|
||||
await this.itemsEncryption.decryptErroredPayloads()
|
||||
}
|
||||
|
||||
public itemsKeyForPayload(payload: EncryptedPayloadInterface): ItemsKeyInterface | undefined {
|
||||
return this.itemsEncryption.itemsKeyForPayload(payload)
|
||||
}
|
||||
|
||||
public defaultItemsKeyForItemVersion(
|
||||
version: ProtocolVersion,
|
||||
fromKeys?: ItemsKeyInterface[],
|
||||
): ItemsKeyInterface | undefined {
|
||||
return this.itemsEncryption.defaultItemsKeyForItemVersion(version, fromKeys)
|
||||
}
|
||||
|
||||
public async encryptSplitSingle(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface> {
|
||||
return (await this.encryptSplit(split))[0]
|
||||
}
|
||||
|
||||
public async encryptSplit(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface[]> {
|
||||
const allEncryptedParams: EncryptedParameters[] = []
|
||||
|
||||
if (split.usesRootKey) {
|
||||
const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloads(
|
||||
split.usesRootKey.items,
|
||||
split.usesRootKey.key,
|
||||
)
|
||||
extendArray(allEncryptedParams, rootKeyEncrypted)
|
||||
}
|
||||
|
||||
if (split.usesItemsKey) {
|
||||
const itemsKeyEncrypted = await this.itemsEncryption.encryptPayloads(
|
||||
split.usesItemsKey.items,
|
||||
split.usesItemsKey.key,
|
||||
)
|
||||
extendArray(allEncryptedParams, itemsKeyEncrypted)
|
||||
}
|
||||
|
||||
if (split.usesRootKeyWithKeyLookup) {
|
||||
const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloadsWithKeyLookup(
|
||||
split.usesRootKeyWithKeyLookup.items,
|
||||
)
|
||||
extendArray(allEncryptedParams, rootKeyEncrypted)
|
||||
}
|
||||
|
||||
if (split.usesItemsKeyWithKeyLookup) {
|
||||
const itemsKeyEncrypted = await this.itemsEncryption.encryptPayloadsWithKeyLookup(
|
||||
split.usesItemsKeyWithKeyLookup.items,
|
||||
)
|
||||
extendArray(allEncryptedParams, itemsKeyEncrypted)
|
||||
}
|
||||
|
||||
const packagedEncrypted = allEncryptedParams.map((encryptedParams) => {
|
||||
const original = FindPayloadInEncryptionSplit(encryptedParams.uuid, split)
|
||||
return new EncryptedPayload({
|
||||
...original,
|
||||
...encryptedParams,
|
||||
waitingForKey: false,
|
||||
errorDecrypting: false,
|
||||
})
|
||||
})
|
||||
|
||||
return packagedEncrypted
|
||||
}
|
||||
|
||||
public async decryptSplitSingle<
|
||||
C extends ItemContent = ItemContent,
|
||||
P extends DecryptedPayloadInterface<C> = DecryptedPayloadInterface<C>,
|
||||
>(split: KeyedDecryptionSplit): Promise<P | EncryptedPayloadInterface> {
|
||||
const results = await this.decryptSplit<C, P>(split)
|
||||
return results[0]
|
||||
}
|
||||
|
||||
public async decryptSplit<
|
||||
C extends ItemContent = ItemContent,
|
||||
P extends DecryptedPayloadInterface<C> = DecryptedPayloadInterface<C>,
|
||||
>(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,
|
||||
)
|
||||
extendArray(resultParams, rootKeyDecrypted)
|
||||
}
|
||||
|
||||
if (split.usesRootKeyWithKeyLookup) {
|
||||
const rootKeyDecrypted = await this.rootKeyEncryption.decryptPayloadsWithKeyLookup<C>(
|
||||
split.usesRootKeyWithKeyLookup.items,
|
||||
)
|
||||
extendArray(resultParams, rootKeyDecrypted)
|
||||
}
|
||||
|
||||
if (split.usesItemsKey) {
|
||||
const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloads<C>(
|
||||
split.usesItemsKey.items,
|
||||
split.usesItemsKey.key,
|
||||
)
|
||||
extendArray(resultParams, itemsKeyDecrypted)
|
||||
}
|
||||
|
||||
if (split.usesItemsKeyWithKeyLookup) {
|
||||
const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloadsWithKeyLookup<C>(
|
||||
split.usesItemsKeyWithKeyLookup.items,
|
||||
)
|
||||
extendArray(resultParams, itemsKeyDecrypted)
|
||||
}
|
||||
|
||||
const packagedResults = resultParams.map((params) => {
|
||||
const original = FindPayloadInDecryptionSplit(params.uuid, split)
|
||||
|
||||
if (isErrorDecryptingParameters(params)) {
|
||||
return new EncryptedPayload({
|
||||
...original.ejected(),
|
||||
...params,
|
||||
})
|
||||
} else {
|
||||
return new DecryptedPayload<C>({
|
||||
...original.ejected(),
|
||||
...params,
|
||||
}) as P
|
||||
}
|
||||
})
|
||||
|
||||
return packagedResults
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user's account protocol version is not equal to the latest version.
|
||||
*/
|
||||
public accountUpgradeAvailable(): boolean {
|
||||
const userVersion = this.getUserVersion()
|
||||
if (!userVersion) {
|
||||
return false
|
||||
}
|
||||
return userVersion !== ProtocolVersionLatest
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user's account protocol version is not equal to the latest version.
|
||||
*/
|
||||
public async passcodeUpgradeAvailable(): Promise<boolean> {
|
||||
return this.rootKeyEncryption.passcodeUpgradeAvailable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current environment is capable of supporting
|
||||
* key derivation.
|
||||
*/
|
||||
public platformSupportsKeyDerivation(keyParams: SNRootKeyParams) {
|
||||
/**
|
||||
* If the version is 003 or lower, key derivation is supported unless the browser is
|
||||
* IE or Edge (or generally, where WebCrypto is not available) or React Native environment is detected.
|
||||
*
|
||||
* Versions 004 and above are always supported.
|
||||
*/
|
||||
if (compareVersions(keyParams.version, ProtocolVersion.V004) >= 0) {
|
||||
/* keyParams.version >= 004 */
|
||||
return true
|
||||
} else {
|
||||
return !!isWebCryptoAvailable() || isReactNativeEnvironment()
|
||||
}
|
||||
}
|
||||
|
||||
public supportedVersions(): ProtocolVersion[] {
|
||||
return [ProtocolVersion.V001, ProtocolVersion.V002, ProtocolVersion.V003, ProtocolVersion.V004]
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the input version is greater than the latest supported library version.
|
||||
*/
|
||||
public isVersionNewerThanLibraryVersion(version: ProtocolVersion) {
|
||||
const libraryVersion = ProtocolVersionLatest
|
||||
return compareVersions(version, libraryVersion) === 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Versions 001 and 002 of the protocol supported dynamic costs, as reported by the server.
|
||||
* This function returns the client-enforced minimum cost, to prevent the server from
|
||||
* overwhelmingly under-reporting the cost.
|
||||
*/
|
||||
public costMinimumForVersion(version: ProtocolVersion) {
|
||||
if (compareVersions(version, ProtocolVersion.V003) >= 0) {
|
||||
throw 'Cost minimums only apply to versions <= 002'
|
||||
}
|
||||
if (version === ProtocolVersion.V001) {
|
||||
return V001Algorithm.PbkdfMinCost
|
||||
} else if (version === ProtocolVersion.V002) {
|
||||
return V002Algorithm.PbkdfMinCost
|
||||
} else {
|
||||
throw `Invalid version for cost minimum: ${version}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
return this.rootKeyEncryption.computeRootKey(password, keyParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a root key using the latest protocol version
|
||||
*/
|
||||
public async createRootKey(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: KeyParamsOrigination,
|
||||
version?: ProtocolVersion,
|
||||
) {
|
||||
return this.rootKeyEncryption.createRootKey(identifier, password, origination, version)
|
||||
}
|
||||
|
||||
public async decryptBackupFile(
|
||||
file: BackupFile,
|
||||
password?: string,
|
||||
): Promise<ClientDisplayableError | (EncryptedPayloadInterface | DecryptedPayloadInterface<ItemContent>)[]> {
|
||||
const result = await DecryptBackupFile(file, this, password)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a key params object from a raw object
|
||||
* @param keyParams - The raw key params object to create a KeyParams object from
|
||||
*/
|
||||
public createKeyParams(keyParams: AnyKeyParamsContent) {
|
||||
return CreateAnyKeyParams(keyParams)
|
||||
}
|
||||
|
||||
public async createEncryptedBackupFile(): Promise<BackupFile> {
|
||||
const payloads = this.itemManager.items.map((item) => item.payload)
|
||||
|
||||
const split = SplitPayloadsByEncryptionType(payloads)
|
||||
|
||||
const keyLookupSplit = CreateEncryptionSplitWithKeyLookup(split)
|
||||
|
||||
const result = await this.encryptSplit(keyLookupSplit)
|
||||
|
||||
const ejected = result.map((payload) => CreateEncryptedBackupFileContextPayload(payload))
|
||||
|
||||
const data: BackupFile = {
|
||||
version: ProtocolVersionLatest,
|
||||
items: ejected,
|
||||
}
|
||||
|
||||
const keyParams = await this.getRootKeyParams()
|
||||
data.keyParams = keyParams?.getPortableValue()
|
||||
return data
|
||||
}
|
||||
|
||||
public createDecryptedBackupFile(): BackupFile {
|
||||
const payloads = this.payloadManager.nonDeletedItems.filter((item) => item.content_type !== ContentType.ItemsKey)
|
||||
|
||||
const data: BackupFile = {
|
||||
version: ProtocolVersionLatest,
|
||||
items: payloads
|
||||
.map((payload) => {
|
||||
if (isDecryptedPayload(payload)) {
|
||||
return CreateDecryptedBackupFileContextPayload(payload)
|
||||
} else if (isEncryptedPayload(payload)) {
|
||||
return CreateEncryptedBackupFileContextPayload(payload)
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
.filter(isNotUndefined),
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
public hasPasscode(): boolean {
|
||||
return this.rootKeyEncryption.hasPasscode()
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns True if the root key has not yet been unwrapped (passcode locked).
|
||||
*/
|
||||
public async isPasscodeLocked() {
|
||||
return (await this.rootKeyEncryption.hasRootKeyWrapper()) && this.rootKeyEncryption.getRootKey() == undefined
|
||||
}
|
||||
|
||||
public async getRootKeyParams() {
|
||||
return this.rootKeyEncryption.getRootKeyParams()
|
||||
}
|
||||
|
||||
public getAccountKeyParams() {
|
||||
return this.rootKeyEncryption.memoizedRootKeyParams
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the root key wrapping key given a passcode.
|
||||
* Wrapping key params are read from disk.
|
||||
*/
|
||||
public async computeWrappingKey(passcode: string) {
|
||||
const keyParams = await this.rootKeyEncryption.getSureRootKeyWrapperKeyParams()
|
||||
const key = await this.computeRootKey(passcode, keyParams)
|
||||
return key
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwraps the persisted root key value using the supplied wrappingKey.
|
||||
* Application interfaces must check to see if the root key requires unwrapping on load.
|
||||
* If so, they must generate the unwrapping key by getting our saved wrapping key keyParams.
|
||||
* After unwrapping, the root key is automatically loaded.
|
||||
*/
|
||||
public async unwrapRootKey(wrappingKey: RootKeyInterface) {
|
||||
return this.rootKeyEncryption.unwrapRootKey(wrappingKey)
|
||||
}
|
||||
/**
|
||||
* Encrypts rootKey and saves it in storage instead of keychain, and then
|
||||
* clears keychain. This is because we don't want to store large encrypted
|
||||
* payloads in the keychain. If the root key is not wrapped, it is stored
|
||||
* in plain form in the user's secure keychain.
|
||||
*/
|
||||
public async setNewRootKeyWrapper(wrappingKey: SNRootKey) {
|
||||
return this.rootKeyEncryption.setNewRootKeyWrapper(wrappingKey)
|
||||
}
|
||||
|
||||
public async removePasscode(): Promise<void> {
|
||||
await this.rootKeyEncryption.removeRootKeyWrapper()
|
||||
}
|
||||
|
||||
public async setRootKey(key: SNRootKey, wrappingKey?: SNRootKey) {
|
||||
await this.rootKeyEncryption.setRootKey(key, wrappingKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the in-memory root key value.
|
||||
*/
|
||||
public getRootKey() {
|
||||
return this.rootKeyEncryption.getRootKey()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes root key and wrapper from keychain. Used when signing out of application.
|
||||
*/
|
||||
public async deleteWorkspaceSpecificKeyStateFromDevice() {
|
||||
await this.rootKeyEncryption.deleteWorkspaceSpecificKeyStateFromDevice()
|
||||
}
|
||||
|
||||
public async validateAccountPassword(password: string) {
|
||||
return this.rootKeyEncryption.validateAccountPassword(password)
|
||||
}
|
||||
|
||||
public async validatePasscode(passcode: string) {
|
||||
return this.rootKeyEncryption.validatePasscode(passcode)
|
||||
}
|
||||
|
||||
public getEmbeddedPayloadAuthenticatedData(
|
||||
payload: EncryptedPayloadInterface,
|
||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
|
||||
const version = payload.version
|
||||
if (!version) {
|
||||
return undefined
|
||||
}
|
||||
const operator = this.operatorManager.operatorForVersion(version)
|
||||
const authenticatedData = operator.getPayloadAuthenticatedData(encryptedParametersFromPayload(payload))
|
||||
return authenticatedData
|
||||
}
|
||||
|
||||
/** Returns the key params attached to this key's encrypted payload */
|
||||
public getKeyEmbeddedKeyParams(key: EncryptedPayloadInterface): SNRootKeyParams | undefined {
|
||||
const authenticatedData = this.getEmbeddedPayloadAuthenticatedData(key)
|
||||
if (!authenticatedData) {
|
||||
return undefined
|
||||
}
|
||||
if (isVersionLessThanOrEqualTo(key.version, ProtocolVersion.V003)) {
|
||||
const rawKeyParams = authenticatedData as LegacyAttachedData
|
||||
return this.createKeyParams(rawKeyParams)
|
||||
} else {
|
||||
const rawKeyParams = (authenticatedData as RootKeyEncryptedAuthenticatedData).kp
|
||||
return this.createKeyParams(rawKeyParams)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A new rootkey-based items key is needed if a user changes their account password
|
||||
* on an 003 client and syncs on a signed in 004 client.
|
||||
*/
|
||||
public needsNewRootKeyBasedItemsKey(): boolean {
|
||||
if (!this.hasAccount()) {
|
||||
return false
|
||||
}
|
||||
|
||||
const rootKey = this.rootKeyEncryption.getRootKey()
|
||||
if (!rootKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (compareVersions(rootKey.keyVersion, ProtocolVersionLastNonrootItemsKey) > 0) {
|
||||
/** Is >= 004, not needed */
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* A new root key based items key is needed if our default items key content
|
||||
* isnt equal to our current root key
|
||||
*/
|
||||
const defaultItemsKey = findDefaultItemsKey(this.itemsEncryption.getItemsKeys())
|
||||
|
||||
/** Shouldn't be undefined, but if it is, we'll take the corrective action */
|
||||
if (!defaultItemsKey) {
|
||||
return true
|
||||
}
|
||||
|
||||
return defaultItemsKey.itemsKey !== rootKey.itemsKey
|
||||
}
|
||||
|
||||
public async createNewDefaultItemsKey(): Promise<ItemsKeyInterface> {
|
||||
return this.rootKeyEncryption.createNewDefaultItemsKey()
|
||||
}
|
||||
|
||||
public getPasswordCreatedDate(): Date | undefined {
|
||||
const rootKey = this.getRootKey()
|
||||
return rootKey ? rootKey.keyParams.createdDate : undefined
|
||||
}
|
||||
|
||||
public async onSyncEvent(eventName: SyncEvent) {
|
||||
if (eventName === SyncEvent.SyncCompletedWithAllItemsUploaded) {
|
||||
await this.handleFullSyncCompletion()
|
||||
}
|
||||
if (eventName === SyncEvent.DownloadFirstSyncCompleted) {
|
||||
await this.handleDownloadFirstSyncCompletion()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a download-first sync completes, it means we've completed a (potentially multipage)
|
||||
* sync where we only downloaded what the server had before uploading anything. We will be
|
||||
* allowed to make local accomadations here before the server begins with the upload
|
||||
* part of the sync (automatically runs after download-first sync completes).
|
||||
* We use this to see if the server has any default itemsKeys, and if so, allows us to
|
||||
* delete any never-synced items keys we have here locally.
|
||||
*/
|
||||
private async handleDownloadFirstSyncCompletion() {
|
||||
if (!this.hasAccount()) {
|
||||
return
|
||||
}
|
||||
|
||||
const itemsKeys = this.itemsEncryption.getItemsKeys()
|
||||
|
||||
const neverSyncedKeys = itemsKeys.filter((key) => {
|
||||
return key.neverSynced
|
||||
})
|
||||
|
||||
const syncedKeys = itemsKeys.filter((key) => {
|
||||
return !key.neverSynced
|
||||
})
|
||||
|
||||
/**
|
||||
* Find isDefault items key that have been previously synced.
|
||||
* If we find one, this means we can delete any non-synced keys.
|
||||
*/
|
||||
const defaultSyncedKey = syncedKeys.find((key) => {
|
||||
return key.isDefault
|
||||
})
|
||||
|
||||
const hasSyncedItemsKey = !isNullOrUndefined(defaultSyncedKey)
|
||||
if (hasSyncedItemsKey) {
|
||||
/** Delete all never synced keys */
|
||||
await this.itemManager.setItemsToBeDeleted(neverSyncedKeys)
|
||||
} else {
|
||||
/**
|
||||
* No previous synced items key.
|
||||
* We can keep the one(s) we have, only if their version is equal to our root key
|
||||
* version. If their version is not equal to our root key version, delete them. If
|
||||
* 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()
|
||||
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)
|
||||
}
|
||||
|
||||
if (this.itemsEncryption.getItemsKeys().length === 0) {
|
||||
await this.rootKeyEncryption.createNewDefaultItemsKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
/** If we do not have an items key for our current account version, create one */
|
||||
const userVersion = this.getUserVersion()
|
||||
const accountVersionedKey = this.itemsEncryption.getItemsKeys().find((key) => key.keyVersion === userVersion)
|
||||
if (isNullOrUndefined(accountVersionedKey)) {
|
||||
await this.rootKeyEncryption.createNewDefaultItemsKey()
|
||||
}
|
||||
|
||||
this.syncUnsycnedItemsKeys()
|
||||
}
|
||||
|
||||
private async handleFullSyncCompletion() {
|
||||
/** Always create a new items key after full sync, if no items key is found */
|
||||
const currentItemsKey = findDefaultItemsKey(this.itemsEncryption.getItemsKeys())
|
||||
if (!currentItemsKey) {
|
||||
await this.rootKeyEncryption.createNewDefaultItemsKey()
|
||||
if (this.rootKeyEncryption.keyMode === KeyMode.WrapperOnly) {
|
||||
return this.itemsEncryption.repersistAllItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* There is presently an issue where an items key created while signed out of account (
|
||||
* or possibly signed in but with invalid session), then signing into account, results in that
|
||||
* items key never syncing to the account even though it is being used to encrypt synced items.
|
||||
* Until we can determine its cause, this corrective function will find any such keys and sync them.
|
||||
*/
|
||||
private syncUnsycnedItemsKeys(): void {
|
||||
if (!this.hasAccount()) {
|
||||
return
|
||||
}
|
||||
|
||||
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()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum EncryptionServiceEvent {
|
||||
RootKeyStatusChanged = 'RootKeyStatusChanged',
|
||||
}
|
||||
91
packages/services/src/Domain/Encryption/Functions.ts
Normal file
91
packages/services/src/Domain/Encryption/Functions.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
DecryptedPayloadInterface,
|
||||
EncryptedPayloadInterface,
|
||||
isDecryptedPayload,
|
||||
ItemsKeyContent,
|
||||
RootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { EncryptionProvider, KeyRecoveryStrings, SNRootKeyParams } from '@standardnotes/encryption'
|
||||
import { ChallengeServiceInterface } from '../Challenge/ChallengeServiceInterface'
|
||||
import { ChallengePrompt } from '../Challenge/Prompt/ChallengePrompt'
|
||||
import { ChallengeReason } from '../Challenge/Types/ChallengeReason'
|
||||
import { ChallengeValidation } from '../Challenge/Types/ChallengeValidation'
|
||||
|
||||
export async function DecryptItemsKeyWithUserFallback(
|
||||
itemsKey: EncryptedPayloadInterface,
|
||||
encryptor: EncryptionProvider,
|
||||
challengor: ChallengeServiceInterface,
|
||||
): Promise<DecryptedPayloadInterface<ItemsKeyContent> | 'failed' | 'aborted'> {
|
||||
const decryptionResult = await encryptor.decryptSplitSingle<ItemsKeyContent>({
|
||||
usesRootKeyWithKeyLookup: {
|
||||
items: [itemsKey],
|
||||
},
|
||||
})
|
||||
|
||||
if (isDecryptedPayload(decryptionResult)) {
|
||||
return decryptionResult
|
||||
}
|
||||
|
||||
const secondDecryptionResult = await DecryptItemsKeyByPromptingUser(itemsKey, encryptor, challengor)
|
||||
|
||||
if (secondDecryptionResult === 'aborted' || secondDecryptionResult === 'failed') {
|
||||
return secondDecryptionResult
|
||||
}
|
||||
|
||||
return secondDecryptionResult.decryptedKey
|
||||
}
|
||||
|
||||
export async function DecryptItemsKeyByPromptingUser(
|
||||
itemsKey: EncryptedPayloadInterface,
|
||||
encryptor: EncryptionProvider,
|
||||
challengor: ChallengeServiceInterface,
|
||||
keyParams?: SNRootKeyParams,
|
||||
): Promise<
|
||||
| {
|
||||
decryptedKey: DecryptedPayloadInterface<ItemsKeyContent>
|
||||
rootKey: RootKeyInterface
|
||||
}
|
||||
| 'failed'
|
||||
| 'aborted'
|
||||
> {
|
||||
if (!keyParams) {
|
||||
keyParams = encryptor.getKeyEmbeddedKeyParams(itemsKey)
|
||||
}
|
||||
|
||||
if (!keyParams) {
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
const challenge = challengor.createChallenge(
|
||||
[new ChallengePrompt(ChallengeValidation.None, undefined, undefined, true)],
|
||||
ChallengeReason.Custom,
|
||||
true,
|
||||
KeyRecoveryStrings.KeyRecoveryLoginFlowPrompt(keyParams),
|
||||
KeyRecoveryStrings.KeyRecoveryPasswordRequired,
|
||||
)
|
||||
|
||||
const response = await challengor.promptForChallengeResponse(challenge)
|
||||
|
||||
if (!response) {
|
||||
return 'aborted'
|
||||
}
|
||||
|
||||
const password = response.values[0].value as string
|
||||
|
||||
const rootKey = await encryptor.computeRootKey(password, keyParams)
|
||||
|
||||
const secondDecryptionResult = await encryptor.decryptSplitSingle<ItemsKeyContent>({
|
||||
usesRootKey: {
|
||||
items: [itemsKey],
|
||||
key: rootKey,
|
||||
},
|
||||
})
|
||||
|
||||
challengor.completeChallenge(challenge)
|
||||
|
||||
if (isDecryptedPayload(secondDecryptionResult)) {
|
||||
return { decryptedKey: secondDecryptionResult, rootKey }
|
||||
}
|
||||
|
||||
return 'failed'
|
||||
}
|
||||
261
packages/services/src/Domain/Encryption/ItemsEncryption.ts
Normal file
261
packages/services/src/Domain/Encryption/ItemsEncryption.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
import {
|
||||
DecryptedParameters,
|
||||
EncryptedParameters,
|
||||
ErrorDecryptingParameters,
|
||||
findDefaultItemsKey,
|
||||
isErrorDecryptingParameters,
|
||||
OperatorManager,
|
||||
StandardException,
|
||||
encryptPayload,
|
||||
decryptPayload,
|
||||
} from '@standardnotes/encryption'
|
||||
import {
|
||||
DecryptedPayload,
|
||||
DecryptedPayloadInterface,
|
||||
EncryptedPayload,
|
||||
EncryptedPayloadInterface,
|
||||
isEncryptedPayload,
|
||||
ItemContent,
|
||||
ItemsKeyInterface,
|
||||
PayloadEmitSource,
|
||||
SureFindPayload,
|
||||
} 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'
|
||||
|
||||
export class ItemsEncryptionService extends AbstractService {
|
||||
private removeItemsObserver!: () => void
|
||||
public userVersion?: ProtocolVersion
|
||||
|
||||
constructor(
|
||||
private itemManager: ItemManagerInterface,
|
||||
private payloadManager: PayloadManagerInterface,
|
||||
private storageService: StorageServiceInterface,
|
||||
private operatorManager: OperatorManager,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
|
||||
this.removeItemsObserver = this.itemManager.addObserver([ContentType.ItemsKey], ({ changed, inserted }) => {
|
||||
if (changed.concat(inserted).length > 0) {
|
||||
void this.decryptErroredPayloads()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public override deinit(): void {
|
||||
;(this.itemManager as unknown) = undefined
|
||||
;(this.payloadManager as unknown) = undefined
|
||||
;(this.storageService as unknown) = undefined
|
||||
this.removeItemsObserver()
|
||||
;(this.removeItemsObserver as unknown) = undefined
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
/**
|
||||
* If encryption status changes (esp. on mobile, where local storage encryption
|
||||
* can be disabled), consumers may call this function to repersist all items to
|
||||
* disk using latest encryption status.
|
||||
*/
|
||||
async repersistAllItems(): Promise<void> {
|
||||
const items = this.itemManager.items
|
||||
const payloads = items.map((item) => item.payload)
|
||||
return this.storageService.savePayloads(payloads)
|
||||
}
|
||||
|
||||
public getItemsKeys() {
|
||||
return this.itemManager.getDisplayableItemsKeys()
|
||||
}
|
||||
|
||||
public itemsKeyForPayload(payload: EncryptedPayloadInterface): ItemsKeyInterface | undefined {
|
||||
return this.getItemsKeys().find(
|
||||
(key) => key.uuid === payload.items_key_id || key.duplicateOf === payload.items_key_id,
|
||||
)
|
||||
}
|
||||
|
||||
public getDefaultItemsKey(): ItemsKeyInterface | undefined {
|
||||
return findDefaultItemsKey(this.getItemsKeys())
|
||||
}
|
||||
|
||||
private keyToUseForItemEncryption(): ItemsKeyInterface | StandardException {
|
||||
const defaultKey = this.getDefaultItemsKey()
|
||||
let result: ItemsKeyInterface | undefined = undefined
|
||||
|
||||
if (this.userVersion && this.userVersion !== defaultKey?.keyVersion) {
|
||||
/**
|
||||
* The default key appears to be either newer or older than the user's account version
|
||||
* We could throw an exception here, but will instead fall back to a corrective action:
|
||||
* return any items key that corresponds to the user's version
|
||||
*/
|
||||
const itemsKeys = this.getItemsKeys()
|
||||
result = itemsKeys.find((key) => key.keyVersion === this.userVersion)
|
||||
} else {
|
||||
result = defaultKey
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return new StandardException('Cannot find items key to use for encryption')
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private keyToUseForDecryptionOfPayload(payload: EncryptedPayloadInterface): ItemsKeyInterface | undefined {
|
||||
if (payload.items_key_id) {
|
||||
const itemsKey = this.itemsKeyForPayload(payload)
|
||||
return itemsKey
|
||||
}
|
||||
|
||||
const defaultKey = this.defaultItemsKeyForItemVersion(payload.version)
|
||||
return defaultKey
|
||||
}
|
||||
|
||||
public async encryptPayloadWithKeyLookup(payload: DecryptedPayloadInterface): Promise<EncryptedParameters> {
|
||||
const key = this.keyToUseForItemEncryption()
|
||||
|
||||
if (key instanceof StandardException) {
|
||||
throw Error(key.message)
|
||||
}
|
||||
|
||||
return this.encryptPayload(payload, key)
|
||||
}
|
||||
|
||||
public async encryptPayload(
|
||||
payload: DecryptedPayloadInterface,
|
||||
key: ItemsKeyInterface,
|
||||
): Promise<EncryptedParameters> {
|
||||
if (isEncryptedPayload(payload)) {
|
||||
throw Error('Attempting to encrypt already encrypted payload.')
|
||||
}
|
||||
if (!payload.content) {
|
||||
throw Error('Attempting to encrypt payload with no content.')
|
||||
}
|
||||
if (!payload.uuid) {
|
||||
throw Error('Attempting to encrypt payload with no UuidGenerator.')
|
||||
}
|
||||
|
||||
return encryptPayload(payload, key, this.operatorManager)
|
||||
}
|
||||
|
||||
public async encryptPayloads(
|
||||
payloads: DecryptedPayloadInterface[],
|
||||
key: ItemsKeyInterface,
|
||||
): Promise<EncryptedParameters[]> {
|
||||
return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key)))
|
||||
}
|
||||
|
||||
public async encryptPayloadsWithKeyLookup(payloads: DecryptedPayloadInterface[]): Promise<EncryptedParameters[]> {
|
||||
return Promise.all(payloads.map((payload) => this.encryptPayloadWithKeyLookup(payload)))
|
||||
}
|
||||
|
||||
public async decryptPayloadWithKeyLookup<C extends ItemContent = ItemContent>(
|
||||
payload: EncryptedPayloadInterface,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
const key = this.keyToUseForDecryptionOfPayload(payload)
|
||||
|
||||
if (key == undefined) {
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
errorDecrypting: true,
|
||||
waitingForKey: true,
|
||||
}
|
||||
}
|
||||
|
||||
return this.decryptPayload(payload, key)
|
||||
}
|
||||
|
||||
public async decryptPayload<C extends ItemContent = ItemContent>(
|
||||
payload: EncryptedPayloadInterface,
|
||||
key: ItemsKeyInterface,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
if (!payload.content) {
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
}
|
||||
|
||||
return decryptPayload(payload, key, this.operatorManager)
|
||||
}
|
||||
|
||||
public async decryptPayloadsWithKeyLookup<C extends ItemContent = ItemContent>(
|
||||
payloads: EncryptedPayloadInterface[],
|
||||
): Promise<(DecryptedParameters<C> | ErrorDecryptingParameters)[]> {
|
||||
return Promise.all(payloads.map((payload) => this.decryptPayloadWithKeyLookup<C>(payload)))
|
||||
}
|
||||
|
||||
public async decryptPayloads<C extends ItemContent = ItemContent>(
|
||||
payloads: EncryptedPayloadInterface[],
|
||||
key: ItemsKeyInterface,
|
||||
): 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) {
|
||||
return
|
||||
}
|
||||
|
||||
const resultParams = await this.decryptPayloadsWithKeyLookup(payloads)
|
||||
|
||||
const decryptedPayloads = resultParams.map((params) => {
|
||||
const original = SureFindPayload(payloads, 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 migrating from non-items key architecture, many items will not have a
|
||||
* relationship with any key object. For those items, we can be sure that only 1 key
|
||||
* object will correspond to that protocol version.
|
||||
* @returns The items key object to decrypt items encrypted
|
||||
* with previous protocol version.
|
||||
*/
|
||||
public defaultItemsKeyForItemVersion(
|
||||
version: ProtocolVersion,
|
||||
fromKeys?: ItemsKeyInterface[],
|
||||
): ItemsKeyInterface | undefined {
|
||||
/** Try to find one marked default first */
|
||||
const searchKeys = fromKeys || this.getItemsKeys()
|
||||
const priorityKey = searchKeys.find((key) => {
|
||||
return key.isDefault && key.keyVersion === version
|
||||
})
|
||||
if (priorityKey) {
|
||||
return priorityKey
|
||||
}
|
||||
return searchKeys.find((key) => {
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
647
packages/services/src/Domain/Encryption/RootKeyEncryption.ts
Normal file
647
packages/services/src/Domain/Encryption/RootKeyEncryption.ts
Normal file
@@ -0,0 +1,647 @@
|
||||
import {
|
||||
ApplicationIdentifier,
|
||||
ProtocolVersionLatest,
|
||||
ProtocolVersion,
|
||||
AnyKeyParamsContent,
|
||||
KeyParamsOrigination,
|
||||
compareVersions,
|
||||
ProtocolVersionLastNonrootItemsKey,
|
||||
ContentType,
|
||||
} from '@standardnotes/common'
|
||||
import {
|
||||
RootKeyServiceEvent,
|
||||
KeyMode,
|
||||
SNRootKeyParams,
|
||||
OperatorManager,
|
||||
CreateNewRootKey,
|
||||
CreateAnyKeyParams,
|
||||
SNRootKey,
|
||||
isErrorDecryptingParameters,
|
||||
EncryptedParameters,
|
||||
DecryptedParameters,
|
||||
ErrorDecryptingParameters,
|
||||
findDefaultItemsKey,
|
||||
ItemsKeyMutator,
|
||||
encryptPayload,
|
||||
decryptPayload,
|
||||
} from '@standardnotes/encryption'
|
||||
import {
|
||||
CreateDecryptedItemFromPayload,
|
||||
DecryptedPayload,
|
||||
DecryptedPayloadInterface,
|
||||
DecryptedTransferPayload,
|
||||
EncryptedPayload,
|
||||
EncryptedPayloadInterface,
|
||||
EncryptedTransferPayload,
|
||||
FillItemContentSpecialized,
|
||||
ItemContent,
|
||||
ItemsKeyContent,
|
||||
ItemsKeyContentSpecialized,
|
||||
ItemsKeyInterface,
|
||||
NamespacedRootKeyInKeychain,
|
||||
PayloadTimestampDefaults,
|
||||
RootKeyContent,
|
||||
RootKeyInterface,
|
||||
} 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'
|
||||
|
||||
export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEvent> {
|
||||
private rootKey?: RootKeyInterface
|
||||
public keyMode = KeyMode.RootKeyNone
|
||||
public memoizedRootKeyParams?: SNRootKeyParams
|
||||
|
||||
constructor(
|
||||
private itemManager: ItemManagerInterface,
|
||||
private operatorManager: OperatorManager,
|
||||
public deviceInterface: DeviceInterface,
|
||||
private storageService: StorageServiceInterface,
|
||||
private identifier: ApplicationIdentifier,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
}
|
||||
|
||||
public override deinit(): void {
|
||||
;(this.itemManager as unknown) = undefined
|
||||
this.rootKey = undefined
|
||||
this.memoizedRootKeyParams = undefined
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
const wrappedRootKey = this.getWrappedRootKey()
|
||||
const accountKeyParams = await this.recomputeAccountKeyParams()
|
||||
const hasWrapper = await this.hasRootKeyWrapper()
|
||||
const hasRootKey = wrappedRootKey != undefined || accountKeyParams != undefined
|
||||
|
||||
if (hasWrapper && hasRootKey) {
|
||||
this.keyMode = KeyMode.RootKeyPlusWrapper
|
||||
} else if (hasWrapper && !hasRootKey) {
|
||||
this.keyMode = KeyMode.WrapperOnly
|
||||
} else if (!hasWrapper && hasRootKey) {
|
||||
this.keyMode = KeyMode.RootKeyOnly
|
||||
} else if (!hasWrapper && !hasRootKey) {
|
||||
this.keyMode = KeyMode.RootKeyNone
|
||||
} else {
|
||||
throw 'Invalid key mode condition'
|
||||
}
|
||||
|
||||
if (this.keyMode === KeyMode.RootKeyOnly) {
|
||||
this.setRootKeyInstance(await this.getRootKeyFromKeychain())
|
||||
await this.handleKeyStatusChange()
|
||||
}
|
||||
}
|
||||
|
||||
private async handleKeyStatusChange() {
|
||||
await this.recomputeAccountKeyParams()
|
||||
void this.notifyEvent(RootKeyServiceEvent.RootKeyStatusChanged)
|
||||
}
|
||||
|
||||
public async passcodeUpgradeAvailable() {
|
||||
const passcodeParams = await this.getRootKeyWrapperKeyParams()
|
||||
if (!passcodeParams) {
|
||||
return false
|
||||
}
|
||||
return passcodeParams.version !== ProtocolVersionLatest
|
||||
}
|
||||
|
||||
public async hasRootKeyWrapper() {
|
||||
const wrapper = await this.getRootKeyWrapperKeyParams()
|
||||
return wrapper != undefined
|
||||
}
|
||||
|
||||
public hasAccount() {
|
||||
switch (this.keyMode) {
|
||||
case KeyMode.RootKeyNone:
|
||||
case KeyMode.WrapperOnly:
|
||||
return false
|
||||
case KeyMode.RootKeyOnly:
|
||||
case KeyMode.RootKeyPlusWrapper:
|
||||
return true
|
||||
default:
|
||||
throw Error(`Unhandled keyMode value '${this.keyMode}'.`)
|
||||
}
|
||||
}
|
||||
|
||||
public hasRootKeyEncryptionSource(): boolean {
|
||||
return this.hasAccount() || this.hasPasscode()
|
||||
}
|
||||
|
||||
public hasPasscode() {
|
||||
return this.keyMode === KeyMode.WrapperOnly || this.keyMode === KeyMode.RootKeyPlusWrapper
|
||||
}
|
||||
|
||||
public async getEncryptionSourceVersion(): Promise<ProtocolVersion> {
|
||||
if (this.hasAccount()) {
|
||||
return this.getSureUserVersion()
|
||||
} else if (this.hasPasscode()) {
|
||||
const passcodeParams = await this.getSureRootKeyWrapperKeyParams()
|
||||
return passcodeParams.version
|
||||
}
|
||||
|
||||
throw Error('Attempting to access encryption source version without source')
|
||||
}
|
||||
|
||||
public getUserVersion(): ProtocolVersion | undefined {
|
||||
const keyParams = this.memoizedRootKeyParams
|
||||
return keyParams?.version
|
||||
}
|
||||
|
||||
private getSureUserVersion(): ProtocolVersion {
|
||||
const keyParams = this.memoizedRootKeyParams as SNRootKeyParams
|
||||
return keyParams.version
|
||||
}
|
||||
|
||||
private async getRootKeyFromKeychain() {
|
||||
const rawKey = (await this.deviceInterface.getNamespacedKeychainValue(this.identifier)) as
|
||||
| NamespacedRootKeyInKeychain
|
||||
| undefined
|
||||
|
||||
if (rawKey == undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const keyParams = await this.getSureRootKeyParams()
|
||||
|
||||
return CreateNewRootKey({
|
||||
...rawKey,
|
||||
keyParams: keyParams.getPortableValue(),
|
||||
})
|
||||
}
|
||||
|
||||
private async saveRootKeyToKeychain() {
|
||||
if (this.getRootKey() == undefined) {
|
||||
throw 'Attempting to non-existent root key to the keychain.'
|
||||
}
|
||||
if (this.keyMode !== KeyMode.RootKeyOnly) {
|
||||
throw 'Should not be persisting wrapped key to keychain.'
|
||||
}
|
||||
|
||||
const rawKey = this.getSureRootKey().getKeychainValue()
|
||||
|
||||
return this.executeCriticalFunction(() => {
|
||||
return this.deviceInterface.setNamespacedKeychainValue(rawKey, this.identifier)
|
||||
})
|
||||
}
|
||||
|
||||
public async getRootKeyWrapperKeyParams(): Promise<SNRootKeyParams | undefined> {
|
||||
const rawKeyParams = await this.storageService.getValue(
|
||||
StorageKey.RootKeyWrapperKeyParams,
|
||||
StorageValueModes.Nonwrapped,
|
||||
)
|
||||
|
||||
if (!rawKeyParams) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return CreateAnyKeyParams(rawKeyParams as AnyKeyParamsContent)
|
||||
}
|
||||
|
||||
public async getSureRootKeyWrapperKeyParams() {
|
||||
return this.getRootKeyWrapperKeyParams() as Promise<SNRootKeyParams>
|
||||
}
|
||||
|
||||
public async getRootKeyParams(): Promise<SNRootKeyParams | undefined> {
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
return this.getRootKeyWrapperKeyParams()
|
||||
} else if (this.keyMode === KeyMode.RootKeyOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
return this.recomputeAccountKeyParams()
|
||||
} else if (this.keyMode === KeyMode.RootKeyNone) {
|
||||
return undefined
|
||||
} else {
|
||||
throw `Unhandled key mode for getRootKeyParams ${this.keyMode}`
|
||||
}
|
||||
}
|
||||
|
||||
public async getSureRootKeyParams(): Promise<SNRootKeyParams> {
|
||||
return this.getRootKeyParams() as Promise<SNRootKeyParams>
|
||||
}
|
||||
|
||||
public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface> {
|
||||
const version = keyParams.version
|
||||
const operator = this.operatorManager.operatorForVersion(version)
|
||||
return operator.computeRootKey(password, keyParams)
|
||||
}
|
||||
|
||||
public async createRootKey(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: KeyParamsOrigination,
|
||||
version?: ProtocolVersion,
|
||||
) {
|
||||
const operator = version ? this.operatorManager.operatorForVersion(version) : this.operatorManager.defaultOperator()
|
||||
return operator.createRootKey(identifier, password, origination)
|
||||
}
|
||||
|
||||
private getSureMemoizedRootKeyParams(): SNRootKeyParams {
|
||||
return this.memoizedRootKeyParams as SNRootKeyParams
|
||||
}
|
||||
|
||||
public async validateAccountPassword(password: string) {
|
||||
const key = await this.computeRootKey(password, this.getSureMemoizedRootKeyParams())
|
||||
const valid = this.getSureRootKey().compare(key)
|
||||
if (valid) {
|
||||
return { valid, artifacts: { rootKey: key } }
|
||||
} else {
|
||||
return { valid: false }
|
||||
}
|
||||
}
|
||||
|
||||
public async validatePasscode(passcode: string) {
|
||||
const keyParams = await this.getSureRootKeyWrapperKeyParams()
|
||||
const key = await this.computeRootKey(passcode, keyParams)
|
||||
const valid = await this.validateWrappingKey(key)
|
||||
if (valid) {
|
||||
return { valid, artifacts: { wrappingKey: key } }
|
||||
} else {
|
||||
return { valid: false }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We know a wrappingKey is correct if it correctly decrypts
|
||||
* wrapped root key.
|
||||
*/
|
||||
public async validateWrappingKey(wrappingKey: SNRootKey) {
|
||||
const wrappedRootKey = this.getWrappedRootKey()
|
||||
|
||||
/** If wrapper only, storage is encrypted directly with wrappingKey */
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
return this.storageService.canDecryptWithKey(wrappingKey)
|
||||
} else if (this.keyMode === KeyMode.RootKeyOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
/**
|
||||
* In these modes, storage is encrypted with account keys, and
|
||||
* account keys are encrypted with wrappingKey. Here we validate
|
||||
* by attempting to decrypt account keys.
|
||||
*/
|
||||
const wrappedKeyPayload = new EncryptedPayload(wrappedRootKey)
|
||||
const decrypted = await this.decryptPayload(wrappedKeyPayload, wrappingKey)
|
||||
return !isErrorDecryptingParameters(decrypted)
|
||||
} else {
|
||||
throw 'Unhandled case in validateWrappingKey'
|
||||
}
|
||||
}
|
||||
|
||||
private async recomputeAccountKeyParams(): Promise<SNRootKeyParams | undefined> {
|
||||
const rawKeyParams = await this.storageService.getValue(StorageKey.RootKeyParams, StorageValueModes.Nonwrapped)
|
||||
|
||||
if (!rawKeyParams) {
|
||||
return
|
||||
}
|
||||
|
||||
this.memoizedRootKeyParams = CreateAnyKeyParams(rawKeyParams as AnyKeyParamsContent)
|
||||
return this.memoizedRootKeyParams
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the current in-memory root key value using the wrappingKey,
|
||||
* then persists the wrapped value to disk.
|
||||
*/
|
||||
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)
|
||||
const wrappedKeyPayload = new EncryptedPayload({
|
||||
...payload.ejected(),
|
||||
...wrappedKey,
|
||||
errorDecrypting: false,
|
||||
waitingForKey: false,
|
||||
})
|
||||
|
||||
this.storageService.setValue(StorageKey.WrappedRootKey, wrappedKeyPayload.ejected(), StorageValueModes.Nonwrapped)
|
||||
}
|
||||
|
||||
public async unwrapRootKey(wrappingKey: RootKeyInterface) {
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
this.setRootKeyInstance(wrappingKey)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.keyMode !== KeyMode.RootKeyPlusWrapper) {
|
||||
throw 'Invalid key mode condition for unwrapping.'
|
||||
}
|
||||
|
||||
const wrappedKey = this.getWrappedRootKey()
|
||||
const payload = new EncryptedPayload(wrappedKey)
|
||||
const decrypted = await this.decryptPayload<RootKeyContent>(payload, wrappingKey)
|
||||
|
||||
if (isErrorDecryptingParameters(decrypted)) {
|
||||
throw Error('Unable to decrypt root key with provided wrapping key.')
|
||||
} else {
|
||||
const decryptedPayload = new DecryptedPayload<RootKeyContent>({
|
||||
...payload.ejected(),
|
||||
...decrypted,
|
||||
})
|
||||
this.setRootKeyInstance(new SNRootKey(decryptedPayload))
|
||||
await this.handleKeyStatusChange()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts rootKey and saves it in storage instead of keychain, and then
|
||||
* clears keychain. This is because we don't want to store large encrypted
|
||||
* payloads in the keychain. If the root key is not wrapped, it is stored
|
||||
* in plain form in the user's secure keychain.
|
||||
*/
|
||||
public async setNewRootKeyWrapper(wrappingKey: SNRootKey) {
|
||||
if (this.keyMode === KeyMode.RootKeyNone) {
|
||||
this.keyMode = KeyMode.WrapperOnly
|
||||
} else if (this.keyMode === KeyMode.RootKeyOnly) {
|
||||
this.keyMode = KeyMode.RootKeyPlusWrapper
|
||||
} else {
|
||||
throw Error('Attempting to set wrapper on already wrapped key.')
|
||||
}
|
||||
|
||||
await this.deviceInterface.clearNamespacedKeychainValue(this.identifier)
|
||||
|
||||
if (this.keyMode === KeyMode.WrapperOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
this.setRootKeyInstance(wrappingKey)
|
||||
await this.reencryptItemsKeys()
|
||||
} else {
|
||||
await this.wrapAndPersistRootKey(wrappingKey)
|
||||
}
|
||||
|
||||
this.storageService.setValue(
|
||||
StorageKey.RootKeyWrapperKeyParams,
|
||||
wrappingKey.keyParams.getPortableValue(),
|
||||
StorageValueModes.Nonwrapped,
|
||||
)
|
||||
|
||||
await this.handleKeyStatusChange()
|
||||
} else {
|
||||
throw Error('Invalid keyMode on setNewRootKeyWrapper')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes root key wrapper from local storage and stores root key bare in secure keychain.
|
||||
*/
|
||||
public async removeRootKeyWrapper(): Promise<void> {
|
||||
if (this.keyMode !== KeyMode.WrapperOnly && this.keyMode !== KeyMode.RootKeyPlusWrapper) {
|
||||
throw Error('Attempting to remove root key wrapper on unwrapped key.')
|
||||
}
|
||||
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
this.keyMode = KeyMode.RootKeyNone
|
||||
this.setRootKeyInstance(undefined)
|
||||
} else if (this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
this.keyMode = KeyMode.RootKeyOnly
|
||||
}
|
||||
|
||||
await this.storageService.removeValue(StorageKey.WrappedRootKey, StorageValueModes.Nonwrapped)
|
||||
await this.storageService.removeValue(StorageKey.RootKeyWrapperKeyParams, StorageValueModes.Nonwrapped)
|
||||
|
||||
if (this.keyMode === KeyMode.RootKeyOnly) {
|
||||
await this.saveRootKeyToKeychain()
|
||||
}
|
||||
|
||||
await this.handleKeyStatusChange()
|
||||
}
|
||||
|
||||
public async setRootKey(key: SNRootKey, wrappingKey?: SNRootKey) {
|
||||
if (!key.keyParams) {
|
||||
throw Error('keyParams must be supplied if setting root key.')
|
||||
}
|
||||
|
||||
if (this.getRootKey() === key) {
|
||||
throw Error('Attempting to set root key as same current value.')
|
||||
}
|
||||
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
this.keyMode = KeyMode.RootKeyPlusWrapper
|
||||
} else if (this.keyMode === KeyMode.RootKeyNone) {
|
||||
this.keyMode = KeyMode.RootKeyOnly
|
||||
} else if (this.keyMode === KeyMode.RootKeyOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
/** Root key is simply changing, mode stays the same */
|
||||
/** this.keyMode = this.keyMode; */
|
||||
} else {
|
||||
throw Error(`Unhandled key mode for setNewRootKey ${this.keyMode}`)
|
||||
}
|
||||
|
||||
this.setRootKeyInstance(key)
|
||||
|
||||
this.storageService.setValue(
|
||||
StorageKey.RootKeyParams,
|
||||
key.keyParams.getPortableValue(),
|
||||
StorageValueModes.Nonwrapped,
|
||||
)
|
||||
|
||||
if (this.keyMode === KeyMode.RootKeyOnly) {
|
||||
await this.saveRootKeyToKeychain()
|
||||
} else if (this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
if (!wrappingKey) {
|
||||
throw Error('wrappingKey must be supplied')
|
||||
}
|
||||
await this.wrapAndPersistRootKey(wrappingKey)
|
||||
}
|
||||
|
||||
await this.handleKeyStatusChange()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes root key and wrapper from keychain. Used when signing out of application.
|
||||
*/
|
||||
public async deleteWorkspaceSpecificKeyStateFromDevice() {
|
||||
await this.deviceInterface.clearNamespacedKeychainValue(this.identifier)
|
||||
await this.storageService.removeValue(StorageKey.WrappedRootKey, StorageValueModes.Nonwrapped)
|
||||
await this.storageService.removeValue(StorageKey.RootKeyWrapperKeyParams, StorageValueModes.Nonwrapped)
|
||||
await this.storageService.removeValue(StorageKey.RootKeyParams, StorageValueModes.Nonwrapped)
|
||||
this.keyMode = KeyMode.RootKeyNone
|
||||
this.setRootKeyInstance(undefined)
|
||||
|
||||
await this.handleKeyStatusChange()
|
||||
}
|
||||
|
||||
private getWrappedRootKey() {
|
||||
return this.storageService.getValue<EncryptedTransferPayload>(
|
||||
StorageKey.WrappedRootKey,
|
||||
StorageValueModes.Nonwrapped,
|
||||
)
|
||||
}
|
||||
|
||||
public setRootKeyInstance(rootKey: RootKeyInterface | undefined): void {
|
||||
this.rootKey = rootKey
|
||||
}
|
||||
|
||||
public getRootKey(): RootKeyInterface | undefined {
|
||||
return this.rootKey
|
||||
}
|
||||
|
||||
private getSureRootKey(): RootKeyInterface {
|
||||
return this.rootKey as RootKeyInterface
|
||||
}
|
||||
|
||||
private getItemsKeys() {
|
||||
return this.itemManager.getDisplayableItemsKeys()
|
||||
}
|
||||
|
||||
public async encrypPayloadWithKeyLookup(payload: DecryptedPayloadInterface): Promise<EncryptedParameters> {
|
||||
const key = this.getRootKey()
|
||||
|
||||
if (key == undefined) {
|
||||
throw Error('Attempting root key encryption with no root key')
|
||||
}
|
||||
|
||||
return this.encryptPayload(payload, key)
|
||||
}
|
||||
|
||||
public async encryptPayloadsWithKeyLookup(payloads: DecryptedPayloadInterface[]): Promise<EncryptedParameters[]> {
|
||||
return Promise.all(payloads.map((payload) => this.encrypPayloadWithKeyLookup(payload)))
|
||||
}
|
||||
|
||||
public async encryptPayload(payload: DecryptedPayloadInterface, key: RootKeyInterface): Promise<EncryptedParameters> {
|
||||
return encryptPayload(payload, key, this.operatorManager)
|
||||
}
|
||||
|
||||
public async encryptPayloads(payloads: DecryptedPayloadInterface[], key: RootKeyInterface) {
|
||||
return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key)))
|
||||
}
|
||||
|
||||
public async decryptPayloadWithKeyLookup<C extends ItemContent = ItemContent>(
|
||||
payload: EncryptedPayloadInterface,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
const key = this.getRootKey()
|
||||
|
||||
if (key == undefined) {
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
errorDecrypting: true,
|
||||
waitingForKey: true,
|
||||
}
|
||||
}
|
||||
|
||||
return this.decryptPayload(payload, key)
|
||||
}
|
||||
|
||||
public async decryptPayload<C extends ItemContent = ItemContent>(
|
||||
payload: EncryptedPayloadInterface,
|
||||
key: RootKeyInterface,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
return decryptPayload(payload, key, this.operatorManager)
|
||||
}
|
||||
|
||||
public async decryptPayloadsWithKeyLookup<C extends ItemContent = ItemContent>(
|
||||
payloads: EncryptedPayloadInterface[],
|
||||
): Promise<(DecryptedParameters<C> | ErrorDecryptingParameters)[]> {
|
||||
return Promise.all(payloads.map((payload) => this.decryptPayloadWithKeyLookup<C>(payload)))
|
||||
}
|
||||
|
||||
public async decryptPayloads<C extends ItemContent = ItemContent>(
|
||||
payloads: EncryptedPayloadInterface[],
|
||||
key: RootKeyInterface,
|
||||
): 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()
|
||||
|
||||
if (itemsKeys.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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new random items key to use for item encryption, and adds it to model management.
|
||||
* 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.
|
||||
*/
|
||||
public async createNewDefaultItemsKey(): Promise<ItemsKeyInterface> {
|
||||
const rootKey = this.getSureRootKey()
|
||||
const operatorVersion = rootKey ? rootKey.keyVersion : ProtocolVersionLatest
|
||||
let itemTemplate: ItemsKeyInterface
|
||||
|
||||
if (compareVersions(operatorVersion, ProtocolVersionLastNonrootItemsKey) <= 0) {
|
||||
/** Create root key based items key */
|
||||
const payload = new DecryptedPayload<ItemsKeyContent>({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: FillItemContentSpecialized<ItemsKeyContentSpecialized, ItemsKeyContent>({
|
||||
itemsKey: rootKey.masterKey,
|
||||
dataAuthenticationKey: rootKey.dataAuthenticationKey,
|
||||
version: operatorVersion,
|
||||
}),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
itemTemplate = CreateDecryptedItemFromPayload(payload)
|
||||
} else {
|
||||
/** Create independent items key */
|
||||
itemTemplate = this.operatorManager.operatorForVersion(operatorVersion).createItemsKey()
|
||||
}
|
||||
|
||||
const itemsKeys = this.getItemsKeys()
|
||||
const defaultKeys = itemsKeys.filter((key) => {
|
||||
return key.isDefault
|
||||
})
|
||||
|
||||
for (const key of defaultKeys) {
|
||||
await this.itemManager.changeItemsKey(key, (mutator) => {
|
||||
mutator.isDefault = false
|
||||
})
|
||||
}
|
||||
|
||||
const itemsKey = (await this.itemManager.insertItem(itemTemplate)) as ItemsKeyInterface
|
||||
|
||||
await this.itemManager.changeItemsKey(itemsKey, (mutator) => {
|
||||
mutator.isDefault = true
|
||||
})
|
||||
|
||||
return itemsKey
|
||||
}
|
||||
|
||||
public async createNewItemsKeyWithRollback(): Promise<() => Promise<void>> {
|
||||
const currentDefaultItemsKey = findDefaultItemsKey(this.getItemsKeys())
|
||||
const newDefaultItemsKey = await this.createNewDefaultItemsKey()
|
||||
|
||||
const rollback = async () => {
|
||||
await this.itemManager.setItemToBeDeleted(newDefaultItemsKey)
|
||||
|
||||
if (currentDefaultItemsKey) {
|
||||
await this.itemManager.changeItem<ItemsKeyMutator>(currentDefaultItemsKey, (mutator) => {
|
||||
mutator.isDefault = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user