feat(encryption): refactor circular dependencies on services

This commit is contained in:
Karol Sójko
2022-08-05 11:59:02 +02:00
parent 183f68c9c1
commit ffb2193924
40 changed files with 502 additions and 380 deletions

View File

@@ -39,7 +39,6 @@
"@standardnotes/common": "^1.23.1",
"@standardnotes/models": "workspace:*",
"@standardnotes/responses": "workspace:*",
"@standardnotes/services": "workspace:*",
"@standardnotes/sncrypto-common": "workspace:*",
"@standardnotes/utils": "workspace:*",
"reflect-metadata": "^0.1.13"

View File

@@ -1,248 +0,0 @@
import {
AnyKeyParamsContent,
compareVersions,
ContentType,
leftVersionGreaterThanOrEqualToRight,
ProtocolVersion,
} from '@standardnotes/common'
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 { isItemsKey, SNItemsKey } from '../Keys/ItemsKey/ItemsKey'
import { ContentTypeUsesRootKeyEncryption } from '../Keys/RootKey/Functions'
import { CreateAnyKeyParams } from '../Keys/RootKey/KeyParamsFunctions'
import { SNRootKey } from '../Keys/RootKey/RootKey'
import { SNRootKeyParams } from '../Keys/RootKey/RootKeyParams'
import { EncryptionService } from '../Service/Encryption/EncryptionService'
import { BackupFileType } from './BackupFileType'
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

@@ -1,95 +0,0 @@
import {
DecryptedPayloadInterface,
EncryptedPayloadInterface,
isDecryptedPayload,
ItemsKeyContent,
RootKeyInterface,
} from '@standardnotes/models'
import {
ChallengePrompt,
ChallengeReason,
ChallengeServiceInterface,
ChallengeValidation,
} from '@standardnotes/services'
import { EncryptionProvider } from '../../Service/Encryption/EncryptionProvider'
import { SNRootKeyParams } from '../RootKey/RootKeyParams'
import { KeyRecoveryStrings } from './KeyRecoveryStrings'
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'
}

View File

@@ -1,4 +1,10 @@
import * as Models from '@standardnotes/models'
import {
DecryptedPayloadInterface,
ItemsKeyInterface,
RootKeyInterface,
ItemContent,
EncryptedPayloadInterface,
} from '@standardnotes/models'
import {
DecryptedParameters,
EncryptedParameters,
@@ -9,8 +15,8 @@ import { isAsyncOperator } from './Functions'
import { OperatorManager } from './OperatorManager'
export async function encryptPayload(
payload: Models.DecryptedPayloadInterface,
key: Models.ItemsKeyInterface | Models.RootKeyInterface,
payload: DecryptedPayloadInterface,
key: ItemsKeyInterface | RootKeyInterface,
operatorManager: OperatorManager,
): Promise<EncryptedParameters> {
const operator = operatorManager.operatorForVersion(key.keyVersion)
@@ -29,9 +35,9 @@ export async function encryptPayload(
return encryptionParameters
}
export async function decryptPayload<C extends Models.ItemContent = Models.ItemContent>(
payload: Models.EncryptedPayloadInterface,
key: Models.ItemsKeyInterface | Models.RootKeyInterface,
export async function decryptPayload<C extends ItemContent = ItemContent>(
payload: EncryptedPayloadInterface,
key: ItemsKeyInterface | RootKeyInterface,
operatorManager: OperatorManager,
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
const operator = operatorManager.operatorForVersion(payload.version)

View File

@@ -1,751 +0,0 @@
import * as Common from '@standardnotes/common'
import * as Models from '@standardnotes/models'
import {
BackupFile,
CreateDecryptedBackupFileContextPayload,
CreateEncryptedBackupFileContextPayload,
EncryptedPayload,
isDecryptedPayload,
isEncryptedPayload,
RootKeyInterface,
} from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import * as Services from '@standardnotes/services'
import { DiagnosticInfo } from '@standardnotes/services'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import * as Utils from '@standardnotes/utils'
import { isNotUndefined } from '@standardnotes/utils'
import { V001Algorithm, V002Algorithm } from '../../Algorithm'
import { DecryptBackupFile } from '../../Backups/BackupFileDecryptor'
import { CreateAnyKeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
import { SNRootKey } from '../../Keys/RootKey/RootKey'
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
import { OperatorManager } from '../../Operator/OperatorManager'
import {
CreateEncryptionSplitWithKeyLookup,
FindPayloadInDecryptionSplit,
FindPayloadInEncryptionSplit,
} from '../../Split/EncryptionSplit'
import { SplitPayloadsByEncryptionType } from '../../Split/Functions'
import { KeyedDecryptionSplit } from '../../Split/KeyedDecryptionSplit'
import { KeyedEncryptionSplit } from '../../Split/KeyedEncryptionSplit'
import {
DecryptedParameters,
EncryptedParameters,
encryptedParametersFromPayload,
ErrorDecryptingParameters,
isErrorDecryptingParameters,
} from '../../Types/EncryptedParameters'
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
import { LegacyAttachedData } from '../../Types/LegacyAttachedData'
import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData'
import { findDefaultItemsKey } from '../Functions'
import { ItemsEncryptionService } from '../Items/ItemsEncryption'
import { KeyMode } from '../RootKey/KeyMode'
import * as RootKeyEncryption from '../RootKey/RootKeyEncryption'
import { EncryptionProvider } from './EncryptionProvider'
export enum EncryptionServiceEvent {
RootKeyStatusChanged = 'RootKeyStatusChanged',
}
/**
* 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 Services.AbstractService<EncryptionServiceEvent> implements EncryptionProvider {
private operatorManager: OperatorManager
private readonly itemsEncryption: ItemsEncryptionService
private readonly rootKeyEncryption: RootKeyEncryption.RootKeyEncryptionService
private rootKeyObserverDisposer: () => void
constructor(
private itemManager: Services.ItemManagerInterface,
private payloadManager: Services.PayloadManagerInterface,
public deviceInterface: Services.DeviceInterface,
private storageService: Services.StorageServiceInterface,
private identifier: Common.ApplicationIdentifier,
public crypto: PureCryptoInterface,
protected override internalEventBus: Services.InternalEventBusInterface,
) {
super(internalEventBus)
this.crypto = crypto
this.operatorManager = new OperatorManager(crypto)
this.itemsEncryption = new ItemsEncryptionService(
itemManager,
payloadManager,
storageService,
this.operatorManager,
internalEventBus,
)
this.rootKeyEncryption = new RootKeyEncryption.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 === RootKeyEncryption.RootKeyServiceEvent.RootKeyStatusChanged) {
void this.notifyEvent(EncryptionServiceEvent.RootKeyStatusChanged)
}
})
Utils.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 Common.ProtocolVersionLatest
}
public hasAccount() {
return this.rootKeyEncryption.hasAccount()
}
public hasRootKeyEncryptionSource(): boolean {
return this.rootKeyEncryption.hasRootKeyEncryptionSource()
}
public getUserVersion(): Common.ProtocolVersion | undefined {
return this.rootKeyEncryption.getUserVersion()
}
public async upgradeAvailable() {
const accountUpgradeAvailable = this.accountUpgradeAvailable()
const passcodeUpgradeAvailable = await this.passcodeUpgradeAvailable()
return accountUpgradeAvailable || passcodeUpgradeAvailable
}
public getSureDefaultItemsKey(): Models.ItemsKeyInterface {
return this.itemsEncryption.getDefaultItemsKey() as Models.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: Models.EncryptedPayloadInterface): Models.ItemsKeyInterface | undefined {
return this.itemsEncryption.itemsKeyForPayload(payload)
}
public defaultItemsKeyForItemVersion(
version: Common.ProtocolVersion,
fromKeys?: Models.ItemsKeyInterface[],
): Models.ItemsKeyInterface | undefined {
return this.itemsEncryption.defaultItemsKeyForItemVersion(version, fromKeys)
}
public async encryptSplitSingle(split: KeyedEncryptionSplit): Promise<Models.EncryptedPayloadInterface> {
return (await this.encryptSplit(split))[0]
}
public async encryptSplit(split: KeyedEncryptionSplit): Promise<Models.EncryptedPayloadInterface[]> {
const allEncryptedParams: EncryptedParameters[] = []
if (split.usesRootKey) {
const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloads(
split.usesRootKey.items,
split.usesRootKey.key,
)
Utils.extendArray(allEncryptedParams, rootKeyEncrypted)
}
if (split.usesItemsKey) {
const itemsKeyEncrypted = await this.itemsEncryption.encryptPayloads(
split.usesItemsKey.items,
split.usesItemsKey.key,
)
Utils.extendArray(allEncryptedParams, itemsKeyEncrypted)
}
if (split.usesRootKeyWithKeyLookup) {
const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloadsWithKeyLookup(
split.usesRootKeyWithKeyLookup.items,
)
Utils.extendArray(allEncryptedParams, rootKeyEncrypted)
}
if (split.usesItemsKeyWithKeyLookup) {
const itemsKeyEncrypted = await this.itemsEncryption.encryptPayloadsWithKeyLookup(
split.usesItemsKeyWithKeyLookup.items,
)
Utils.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 Models.ItemContent = Models.ItemContent,
P extends Models.DecryptedPayloadInterface<C> = Models.DecryptedPayloadInterface<C>,
>(split: KeyedDecryptionSplit): Promise<P | Models.EncryptedPayloadInterface> {
const results = await this.decryptSplit<C, P>(split)
return results[0]
}
public async decryptSplit<
C extends Models.ItemContent = Models.ItemContent,
P extends Models.DecryptedPayloadInterface<C> = Models.DecryptedPayloadInterface<C>,
>(split: KeyedDecryptionSplit): Promise<(P | Models.EncryptedPayloadInterface)[]> {
const resultParams: (DecryptedParameters<C> | ErrorDecryptingParameters)[] = []
if (split.usesRootKey) {
const rootKeyDecrypted = await this.rootKeyEncryption.decryptPayloads<C>(
split.usesRootKey.items,
split.usesRootKey.key,
)
Utils.extendArray(resultParams, rootKeyDecrypted)
}
if (split.usesRootKeyWithKeyLookup) {
const rootKeyDecrypted = await this.rootKeyEncryption.decryptPayloadsWithKeyLookup<C>(
split.usesRootKeyWithKeyLookup.items,
)
Utils.extendArray(resultParams, rootKeyDecrypted)
}
if (split.usesItemsKey) {
const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloads<C>(
split.usesItemsKey.items,
split.usesItemsKey.key,
)
Utils.extendArray(resultParams, itemsKeyDecrypted)
}
if (split.usesItemsKeyWithKeyLookup) {
const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloadsWithKeyLookup<C>(
split.usesItemsKeyWithKeyLookup.items,
)
Utils.extendArray(resultParams, itemsKeyDecrypted)
}
const packagedResults = resultParams.map((params) => {
const original = FindPayloadInDecryptionSplit(params.uuid, split)
if (isErrorDecryptingParameters(params)) {
return new Models.EncryptedPayload({
...original.ejected(),
...params,
})
} else {
return new Models.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 !== Common.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 (Common.compareVersions(keyParams.version, Common.ProtocolVersion.V004) >= 0) {
/* keyParams.version >= 004 */
return true
} else {
return !!Utils.isWebCryptoAvailable() || Utils.isReactNativeEnvironment()
}
}
public supportedVersions(): Common.ProtocolVersion[] {
return [
Common.ProtocolVersion.V001,
Common.ProtocolVersion.V002,
Common.ProtocolVersion.V003,
Common.ProtocolVersion.V004,
]
}
/**
* Determines whether the input version is greater than the latest supported library version.
*/
public isVersionNewerThanLibraryVersion(version: Common.ProtocolVersion) {
const libraryVersion = Common.ProtocolVersionLatest
return Common.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: Common.ProtocolVersion) {
if (Common.compareVersions(version, Common.ProtocolVersion.V003) >= 0) {
throw 'Cost minimums only apply to versions <= 002'
}
if (version === Common.ProtocolVersion.V001) {
return V001Algorithm.PbkdfMinCost
} else if (version === Common.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: Common.KeyParamsOrigination,
version?: Common.ProtocolVersion,
) {
return this.rootKeyEncryption.createRootKey(identifier, password, origination, version)
}
public async decryptBackupFile(
file: BackupFile,
password?: string,
): Promise<
ClientDisplayableError | (Models.EncryptedPayloadInterface | Models.DecryptedPayloadInterface<Models.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: Common.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: Common.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 !== Common.ContentType.ItemsKey,
)
const data: BackupFile = {
version: Common.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: Models.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: Models.EncryptedPayloadInterface): SNRootKeyParams | undefined {
const authenticatedData = this.getEmbeddedPayloadAuthenticatedData(key)
if (!authenticatedData) {
return undefined
}
if (Common.isVersionLessThanOrEqualTo(key.version, Common.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 (Common.compareVersions(rootKey.keyVersion, Common.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<Models.ItemsKeyInterface> {
return this.rootKeyEncryption.createNewDefaultItemsKey()
}
public getPasswordCreatedDate(): Date | undefined {
const rootKey = this.getRootKey()
return rootKey ? rootKey.keyParams.createdDate : undefined
}
public async onSyncEvent(eventName: Services.SyncEvent) {
if (eventName === Services.SyncEvent.SyncCompletedWithAllItemsUploaded) {
await this.handleFullSyncCompletion()
}
if (eventName === Services.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 = !Utils.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 (Utils.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()),
},
}
}
}

View File

@@ -1,250 +0,0 @@
import { ContentType, ProtocolVersion } from '@standardnotes/common'
import * as Models from '@standardnotes/models'
import { isEncryptedPayload } from '@standardnotes/models'
import * as Services from '@standardnotes/services'
import { DiagnosticInfo } from '@standardnotes/services'
import { Uuids } from '@standardnotes/utils'
import { OperatorManager } from '../../Operator/OperatorManager'
import * as OperatorWrapper from '../../Operator/OperatorWrapper'
import { StandardException } from '../../StandardException'
import {
DecryptedParameters,
EncryptedParameters,
ErrorDecryptingParameters,
isErrorDecryptingParameters,
} from '../../Types/EncryptedParameters'
import { findDefaultItemsKey } from '../Functions'
export class ItemsEncryptionService extends Services.AbstractService {
private removeItemsObserver!: () => void
public userVersion?: ProtocolVersion
constructor(
private itemManager: Services.ItemManagerInterface,
private payloadManager: Services.PayloadManagerInterface,
private storageService: Services.StorageServiceInterface,
private operatorManager: OperatorManager,
protected override internalEventBus: Services.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: Models.EncryptedPayloadInterface): Models.ItemsKeyInterface | undefined {
return this.getItemsKeys().find(
(key) => key.uuid === payload.items_key_id || key.duplicateOf === payload.items_key_id,
)
}
public getDefaultItemsKey(): Models.ItemsKeyInterface | undefined {
return findDefaultItemsKey(this.getItemsKeys())
}
private keyToUseForItemEncryption(): Models.ItemsKeyInterface | StandardException {
const defaultKey = this.getDefaultItemsKey()
let result: Models.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: Models.EncryptedPayloadInterface,
): Models.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: Models.DecryptedPayloadInterface): Promise<EncryptedParameters> {
const key = this.keyToUseForItemEncryption()
if (key instanceof StandardException) {
throw Error(key.message)
}
return this.encryptPayload(payload, key)
}
public async encryptPayload(
payload: Models.DecryptedPayloadInterface,
key: Models.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 OperatorWrapper.encryptPayload(payload, key, this.operatorManager)
}
public async encryptPayloads(
payloads: Models.DecryptedPayloadInterface[],
key: Models.ItemsKeyInterface,
): Promise<EncryptedParameters[]> {
return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key)))
}
public async encryptPayloadsWithKeyLookup(
payloads: Models.DecryptedPayloadInterface[],
): Promise<EncryptedParameters[]> {
return Promise.all(payloads.map((payload) => this.encryptPayloadWithKeyLookup(payload)))
}
public async decryptPayloadWithKeyLookup<C extends Models.ItemContent = Models.ItemContent>(
payload: Models.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 Models.ItemContent = Models.ItemContent>(
payload: Models.EncryptedPayloadInterface,
key: Models.ItemsKeyInterface,
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
if (!payload.content) {
return {
uuid: payload.uuid,
errorDecrypting: true,
}
}
return OperatorWrapper.decryptPayload(payload, key, this.operatorManager)
}
public async decryptPayloadsWithKeyLookup<C extends Models.ItemContent = Models.ItemContent>(
payloads: Models.EncryptedPayloadInterface[],
): Promise<(DecryptedParameters<C> | ErrorDecryptingParameters)[]> {
return Promise.all(payloads.map((payload) => this.decryptPayloadWithKeyLookup<C>(payload)))
}
public async decryptPayloads<C extends Models.ItemContent = Models.ItemContent>(
payloads: Models.EncryptedPayloadInterface[],
key: Models.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 = Models.SureFindPayload(payloads, params.uuid)
if (isErrorDecryptingParameters(params)) {
return new Models.EncryptedPayload({
...original.ejected(),
...params,
})
} else {
return new Models.DecryptedPayload({
...original.ejected(),
...params,
})
}
})
await this.payloadManager.emitPayloads(decryptedPayloads, Models.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?: Models.ItemsKeyInterface[],
): Models.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,
},
}
}
}

View File

@@ -1,643 +0,0 @@
import * as Common from '@standardnotes/common'
import * as Models from '@standardnotes/models'
import {
DecryptedPayload,
FillItemContentSpecialized,
ItemsKeyContent,
ItemsKeyContentSpecialized,
NamespacedRootKeyInKeychain,
PayloadTimestampDefaults,
RootKeyContent,
RootKeyInterface,
} from '@standardnotes/models'
import * as Services from '@standardnotes/services'
import { UuidGenerator } from '@standardnotes/utils'
import { ItemsKeyMutator } from '../../Keys/ItemsKey/ItemsKeyMutator'
import { CreateNewRootKey } from '../../Keys/RootKey/Functions'
import { CreateAnyKeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
import { SNRootKey } from '../../Keys/RootKey/RootKey'
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
import { OperatorManager } from '../../Operator/OperatorManager'
import * as OperatorWrapper from '../../Operator/OperatorWrapper'
import {
DecryptedParameters,
EncryptedParameters,
ErrorDecryptingParameters,
isErrorDecryptingParameters,
} from '../../Types/EncryptedParameters'
import { findDefaultItemsKey } from '../Functions'
import { KeyMode } from './KeyMode'
export enum RootKeyServiceEvent {
RootKeyStatusChanged = 'RootKeyStatusChanged',
}
export class RootKeyEncryptionService extends Services.AbstractService<RootKeyServiceEvent> {
private rootKey?: RootKeyInterface
public keyMode = KeyMode.RootKeyNone
public memoizedRootKeyParams?: SNRootKeyParams
constructor(
private itemManager: Services.ItemManagerInterface,
private operatorManager: OperatorManager,
public deviceInterface: Services.DeviceInterface,
private storageService: Services.StorageServiceInterface,
private identifier: Common.ApplicationIdentifier,
protected override internalEventBus: Services.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 !== Common.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<Common.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(): Common.ProtocolVersion | undefined {
const keyParams = this.memoizedRootKeyParams
return keyParams?.version
}
private getSureUserVersion(): Common.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(
Services.StorageKey.RootKeyWrapperKeyParams,
Services.StorageValueModes.Nonwrapped,
)
if (!rawKeyParams) {
return undefined
}
return CreateAnyKeyParams(rawKeyParams as Common.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: Common.KeyParamsOrigination,
version?: Common.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 Models.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(
Services.StorageKey.RootKeyParams,
Services.StorageValueModes.Nonwrapped,
)
if (!rawKeyParams) {
return
}
this.memoizedRootKeyParams = CreateAnyKeyParams(rawKeyParams as Common.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: Models.DecryptedTransferPayload = {
...rootKey.payload.ejected(),
content: FillItemContentSpecialized(rootKey.persistableValueWhenWrapping()),
}
const payload = new Models.DecryptedPayload(value)
const wrappedKey = await this.encryptPayload(payload, wrappingKey)
const wrappedKeyPayload = new Models.EncryptedPayload({
...payload.ejected(),
...wrappedKey,
errorDecrypting: false,
waitingForKey: false,
})
this.storageService.setValue(
Services.StorageKey.WrappedRootKey,
wrappedKeyPayload.ejected(),
Services.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 Models.EncryptedPayload(wrappedKey)
const decrypted = await this.decryptPayload<Models.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(
Services.StorageKey.RootKeyWrapperKeyParams,
wrappingKey.keyParams.getPortableValue(),
Services.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(Services.StorageKey.WrappedRootKey, Services.StorageValueModes.Nonwrapped)
await this.storageService.removeValue(
Services.StorageKey.RootKeyWrapperKeyParams,
Services.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(
Services.StorageKey.RootKeyParams,
key.keyParams.getPortableValue(),
Services.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(Services.StorageKey.WrappedRootKey, Services.StorageValueModes.Nonwrapped)
await this.storageService.removeValue(
Services.StorageKey.RootKeyWrapperKeyParams,
Services.StorageValueModes.Nonwrapped,
)
await this.storageService.removeValue(Services.StorageKey.RootKeyParams, Services.StorageValueModes.Nonwrapped)
this.keyMode = KeyMode.RootKeyNone
this.setRootKeyInstance(undefined)
await this.handleKeyStatusChange()
}
private getWrappedRootKey() {
return this.storageService.getValue<Models.EncryptedTransferPayload>(
Services.StorageKey.WrappedRootKey,
Services.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: Models.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: Models.DecryptedPayloadInterface[],
): Promise<EncryptedParameters[]> {
return Promise.all(payloads.map((payload) => this.encrypPayloadWithKeyLookup(payload)))
}
public async encryptPayload(
payload: Models.DecryptedPayloadInterface,
key: RootKeyInterface,
): Promise<EncryptedParameters> {
return OperatorWrapper.encryptPayload(payload, key, this.operatorManager)
}
public async encryptPayloads(payloads: Models.DecryptedPayloadInterface[], key: RootKeyInterface) {
return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key)))
}
public async decryptPayloadWithKeyLookup<C extends Models.ItemContent = Models.ItemContent>(
payload: Models.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 Models.ItemContent = Models.ItemContent>(
payload: Models.EncryptedPayloadInterface,
key: RootKeyInterface,
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
return OperatorWrapper.decryptPayload(payload, key, this.operatorManager)
}
public async decryptPayloadsWithKeyLookup<C extends Models.ItemContent = Models.ItemContent>(
payloads: Models.EncryptedPayloadInterface[],
): Promise<(DecryptedParameters<C> | ErrorDecryptingParameters)[]> {
return Promise.all(payloads.map((payload) => this.decryptPayloadWithKeyLookup<C>(payload)))
}
public async decryptPayloads<C extends Models.ItemContent = Models.ItemContent>(
payloads: Models.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<Models.ItemsKeyInterface> {
const rootKey = this.getSureRootKey()
const operatorVersion = rootKey ? rootKey.keyVersion : Common.ProtocolVersionLatest
let itemTemplate: Models.ItemsKeyInterface
if (Common.compareVersions(operatorVersion, Common.ProtocolVersionLastNonrootItemsKey) <= 0) {
/** Create root key based items key */
const payload = new DecryptedPayload<ItemsKeyContent>({
uuid: UuidGenerator.GenerateUuid(),
content_type: Common.ContentType.ItemsKey,
content: Models.FillItemContentSpecialized<ItemsKeyContentSpecialized, ItemsKeyContent>({
itemsKey: rootKey.masterKey,
dataAuthenticationKey: rootKey.dataAuthenticationKey,
version: operatorVersion,
}),
...PayloadTimestampDefaults(),
})
itemTemplate = Models.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 Models.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<Services.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(),
},
}
}
}

View File

@@ -0,0 +1,3 @@
export enum RootKeyServiceEvent {
RootKeyStatusChanged = 'RootKeyStatusChanged',
}

View File

@@ -0,0 +1,22 @@
import { ProtocolVersion } from '@standardnotes/common';
import { EncryptedPayloadInterface, ItemContent } from '@standardnotes/models';
export declare type EncryptedParameters = {
uuid: string;
content: string;
items_key_id: string | undefined;
enc_item_key: string;
version: ProtocolVersion;
/** @deprecated */
auth_hash?: string;
};
export declare type DecryptedParameters<C extends ItemContent = ItemContent> = {
uuid: string;
content: C;
};
export declare type ErrorDecryptingParameters = {
uuid: string;
errorDecrypting: true;
waitingForKey?: boolean;
};
export declare function isErrorDecryptingParameters(x: EncryptedParameters | DecryptedParameters | ErrorDecryptingParameters): x is ErrorDecryptingParameters;
export declare function encryptedParametersFromPayload(payload: EncryptedPayloadInterface): EncryptedParameters;

View File

@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.encryptedParametersFromPayload = exports.isErrorDecryptingParameters = void 0;
function isErrorDecryptingParameters(x) {
return x.errorDecrypting;
}
exports.isErrorDecryptingParameters = isErrorDecryptingParameters;
function encryptedParametersFromPayload(payload) {
return {
uuid: payload.uuid,
content: payload.content,
items_key_id: payload.items_key_id,
enc_item_key: payload.enc_item_key,
version: payload.version,
auth_hash: payload.auth_hash,
};
}
exports.encryptedParametersFromPayload = encryptedParametersFromPayload;

View File

@@ -1,5 +1,4 @@
export * from './Algorithm'
export * from './Backups/BackupFileDecryptor'
export * from './Backups/BackupFileType'
export * from './Keys/ItemsKey/ItemsKey'
export * from './Keys/ItemsKey/ItemsKeyMutator'
@@ -10,7 +9,6 @@ export * from './Keys/RootKey/ProtocolVersionForKeyParams'
export * from './Keys/RootKey/RootKey'
export * from './Keys/RootKey/RootKeyParams'
export * from './Keys/RootKey/ValidKeyParamsKeys'
export * from './Keys/Utils/DecryptItemsKey'
export * from './Keys/Utils/KeyRecoveryStrings'
export * from './Operator/001/Operator001'
export * from './Operator/002/Operator002'
@@ -21,11 +19,9 @@ export * from './Operator/Operator'
export * from './Operator/OperatorManager'
export * from './Operator/OperatorWrapper'
export * from './Service/Encryption/EncryptionProvider'
export * from './Service/Encryption/EncryptionService'
export * from './Service/Functions'
export * from './Service/Items/ItemsEncryption'
export * from './Service/RootKey/KeyMode'
export * from './Service/RootKey/RootKeyEncryption'
export * from './Service/RootKey/RootKeyServiceEvent'
export * from './Split/AbstractKeySplit'
export * from './Split/EncryptionSplit'
export * from './Split/EncryptionTypeSplit'