feat: add @standardnotes/encryption package (#1199)
* feat: add @standardnotes/encryption package * fix: mobile dependency on encryption package * fix: order of build & lint in pr workflows * fix: web dependency on encryption package * fix: remove encryption package composite configuration * fix: import order
This commit is contained in:
215
packages/encryption/src/Domain/Operator/001/Operator001.ts
Normal file
215
packages/encryption/src/Domain/Operator/001/Operator001.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { ContentType, KeyParamsOrigination, ProtocolVersion, ProtocolVersionLength } from '@standardnotes/common'
|
||||
import { Create001KeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
|
||||
import { firstHalfOfString, secondHalfOfString, splitString, UuidGenerator } from '@standardnotes/utils'
|
||||
import { AsynchronousOperator } from '../Operator'
|
||||
import {
|
||||
CreateDecryptedItemFromPayload,
|
||||
ItemsKeyContent,
|
||||
ItemsKeyInterface,
|
||||
FillItemContent,
|
||||
ItemContent,
|
||||
DecryptedPayload,
|
||||
DecryptedPayloadInterface,
|
||||
PayloadTimestampDefaults,
|
||||
} from '@standardnotes/models'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { SNRootKey } from '../../Keys/RootKey/RootKey'
|
||||
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
|
||||
import { V001Algorithm } from '../../Algorithm'
|
||||
import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters'
|
||||
import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData'
|
||||
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
|
||||
import { LegacyAttachedData } from '../../Types/LegacyAttachedData'
|
||||
import { isItemsKey } from '../../Keys/ItemsKey'
|
||||
import { CreateNewRootKey } from '../../Keys/RootKey/Functions'
|
||||
|
||||
const NO_IV = '00000000000000000000000000000000'
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* A legacy operator no longer used to generate new accounts
|
||||
*/
|
||||
export class SNProtocolOperator001 implements AsynchronousOperator {
|
||||
protected readonly crypto: PureCryptoInterface
|
||||
|
||||
constructor(crypto: PureCryptoInterface) {
|
||||
this.crypto = crypto
|
||||
}
|
||||
|
||||
public getEncryptionDisplayName(): string {
|
||||
return 'AES-256'
|
||||
}
|
||||
|
||||
get version(): ProtocolVersion {
|
||||
return ProtocolVersion.V001
|
||||
}
|
||||
|
||||
protected generateNewItemsKeyContent(): ItemsKeyContent {
|
||||
const keyLength = V001Algorithm.EncryptionKeyLength
|
||||
const itemsKey = this.crypto.generateRandomKey(keyLength)
|
||||
const response = FillItemContent<ItemsKeyContent>({
|
||||
itemsKey: itemsKey,
|
||||
version: ProtocolVersion.V001,
|
||||
})
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new random items key to use for item encryption.
|
||||
* The consumer must save/sync this item.
|
||||
*/
|
||||
public createItemsKey(): ItemsKeyInterface {
|
||||
const payload = new DecryptedPayload({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: this.generateNewItemsKeyContent(),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
return CreateDecryptedItemFromPayload(payload)
|
||||
}
|
||||
|
||||
public async createRootKey(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: KeyParamsOrigination,
|
||||
): Promise<SNRootKey> {
|
||||
const pwCost = V001Algorithm.PbkdfMinCost as number
|
||||
const pwNonce = this.crypto.generateRandomKey(V001Algorithm.SaltSeedLength)
|
||||
const pwSalt = await this.crypto.unsafeSha1(identifier + 'SN' + pwNonce)
|
||||
|
||||
const keyParams = Create001KeyParams({
|
||||
email: identifier,
|
||||
pw_cost: pwCost,
|
||||
pw_nonce: pwNonce,
|
||||
pw_salt: pwSalt,
|
||||
version: ProtocolVersion.V001,
|
||||
origination,
|
||||
created: `${Date.now()}`,
|
||||
})
|
||||
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
public getPayloadAuthenticatedData(
|
||||
_encrypted: EncryptedParameters,
|
||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
private async decryptString(ciphertext: string, key: string) {
|
||||
return this.crypto.aes256CbcDecrypt(ciphertext, NO_IV, key)
|
||||
}
|
||||
|
||||
private async encryptString(text: string, key: string) {
|
||||
return this.crypto.aes256CbcEncrypt(text, NO_IV, key)
|
||||
}
|
||||
|
||||
public async generateEncryptedParametersAsync(
|
||||
payload: DecryptedPayloadInterface,
|
||||
key: ItemsKeyInterface | SNRootKey,
|
||||
): Promise<EncryptedParameters> {
|
||||
/**
|
||||
* Generate new item key that is double the key size.
|
||||
* Will be split to create encryption key and authentication key.
|
||||
*/
|
||||
const itemKey = this.crypto.generateRandomKey(V001Algorithm.EncryptionKeyLength * 2)
|
||||
const encItemKey = await this.encryptString(itemKey, key.itemsKey)
|
||||
|
||||
/** Encrypt content */
|
||||
const ek = firstHalfOfString(itemKey)
|
||||
const ak = secondHalfOfString(itemKey)
|
||||
const contentCiphertext = await this.encryptString(JSON.stringify(payload.content), ek)
|
||||
const ciphertext = key.keyVersion + contentCiphertext
|
||||
const authHash = await this.crypto.hmac256(ciphertext, ak)
|
||||
|
||||
if (!authHash) {
|
||||
throw Error('Error generating hmac256 authHash')
|
||||
}
|
||||
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
items_key_id: isItemsKey(key) ? key.uuid : undefined,
|
||||
content: ciphertext,
|
||||
enc_item_key: encItemKey,
|
||||
auth_hash: authHash,
|
||||
version: this.version,
|
||||
}
|
||||
}
|
||||
|
||||
public async generateDecryptedParametersAsync<C extends ItemContent = ItemContent>(
|
||||
encrypted: EncryptedParameters,
|
||||
key: ItemsKeyInterface | SNRootKey,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
if (!encrypted.enc_item_key) {
|
||||
console.error(Error('Missing item encryption key, skipping decryption.'))
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
}
|
||||
|
||||
let encryptedItemKey = encrypted.enc_item_key
|
||||
encryptedItemKey = this.version + encryptedItemKey
|
||||
const itemKeyComponents = this.encryptionComponentsFromString(encryptedItemKey, key.itemsKey)
|
||||
|
||||
const itemKey = await this.decryptString(itemKeyComponents.ciphertext, itemKeyComponents.key)
|
||||
if (!itemKey) {
|
||||
console.error('Error decrypting parameters', encrypted)
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
}
|
||||
|
||||
const ek = firstHalfOfString(itemKey)
|
||||
const itemParams = this.encryptionComponentsFromString(encrypted.content, ek)
|
||||
const content = await this.decryptString(itemParams.ciphertext, itemParams.key)
|
||||
|
||||
if (!content) {
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
content: JSON.parse(content),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private encryptionComponentsFromString(string: string, encryptionKey: string) {
|
||||
const encryptionVersion = string.substring(0, ProtocolVersionLength)
|
||||
return {
|
||||
ciphertext: string.substring(ProtocolVersionLength, string.length),
|
||||
version: encryptionVersion,
|
||||
key: encryptionKey,
|
||||
}
|
||||
}
|
||||
|
||||
protected async deriveKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
const derivedKey = await this.crypto.pbkdf2(
|
||||
password,
|
||||
keyParams.content001.pw_salt,
|
||||
keyParams.content001.pw_cost,
|
||||
V001Algorithm.PbkdfOutputLength,
|
||||
)
|
||||
|
||||
if (!derivedKey) {
|
||||
throw Error('Error deriving PBKDF2 key')
|
||||
}
|
||||
|
||||
const partitions = splitString(derivedKey, 2)
|
||||
|
||||
return CreateNewRootKey({
|
||||
serverPassword: partitions[0],
|
||||
masterKey: partitions[1],
|
||||
version: ProtocolVersion.V001,
|
||||
keyParams: keyParams.getPortableValue(),
|
||||
})
|
||||
}
|
||||
}
|
||||
296
packages/encryption/src/Domain/Operator/002/Operator002.ts
Normal file
296
packages/encryption/src/Domain/Operator/002/Operator002.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
import { Create002KeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
|
||||
import { SNProtocolOperator001 } from '../001/Operator001'
|
||||
import { SNRootKey } from '../../Keys/RootKey/RootKey'
|
||||
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
import { V002Algorithm } from '../../Algorithm'
|
||||
import * as Common from '@standardnotes/common'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import * as Utils from '@standardnotes/utils'
|
||||
import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters'
|
||||
import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData'
|
||||
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
|
||||
import { LegacyAttachedData } from '../../Types/LegacyAttachedData'
|
||||
import { isItemsKey } from '../../Keys/ItemsKey'
|
||||
import { CreateNewRootKey } from '../../Keys/RootKey/Functions'
|
||||
import { ItemContent, PayloadTimestampDefaults } from '@standardnotes/models'
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* A legacy operator no longer used to generate new accounts.
|
||||
*/
|
||||
export class SNProtocolOperator002 extends SNProtocolOperator001 {
|
||||
override get version(): Common.ProtocolVersion {
|
||||
return Common.ProtocolVersion.V002
|
||||
}
|
||||
|
||||
protected override generateNewItemsKeyContent(): Models.ItemsKeyContent {
|
||||
const keyLength = V002Algorithm.EncryptionKeyLength
|
||||
const itemsKey = this.crypto.generateRandomKey(keyLength)
|
||||
const authKey = this.crypto.generateRandomKey(keyLength)
|
||||
const response = Models.FillItemContent<Models.ItemsKeyContent>({
|
||||
itemsKey: itemsKey,
|
||||
dataAuthenticationKey: authKey,
|
||||
version: Common.ProtocolVersion.V002,
|
||||
})
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new random items key to use for item encryption.
|
||||
* The consumer must save/sync this item.
|
||||
*/
|
||||
public override createItemsKey(): Models.ItemsKeyInterface {
|
||||
const payload = new Models.DecryptedPayload({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: Common.ContentType.ItemsKey,
|
||||
content: this.generateNewItemsKeyContent(),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
return Models.CreateDecryptedItemFromPayload(payload)
|
||||
}
|
||||
|
||||
public override async createRootKey(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: Common.KeyParamsOrigination,
|
||||
): Promise<SNRootKey> {
|
||||
const pwCost = Utils.lastElement(V002Algorithm.PbkdfCostsUsed) as number
|
||||
const pwNonce = this.crypto.generateRandomKey(V002Algorithm.SaltSeedLength)
|
||||
const pwSalt = await this.crypto.unsafeSha1(identifier + ':' + pwNonce)
|
||||
|
||||
const keyParams = Create002KeyParams({
|
||||
email: identifier,
|
||||
pw_nonce: pwNonce,
|
||||
pw_cost: pwCost,
|
||||
pw_salt: pwSalt,
|
||||
version: Common.ProtocolVersion.V002,
|
||||
origination,
|
||||
created: `${Date.now()}`,
|
||||
})
|
||||
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that version 002 supported "dynamic" iteration counts. Some accounts
|
||||
* may have had costs of 5000, and others of 101000. Therefore, when computing
|
||||
* the root key, we must use the value returned by the server.
|
||||
*/
|
||||
public override async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
private async decryptString002(text: string, key: string, iv: string) {
|
||||
return this.crypto.aes256CbcDecrypt(text, iv, key)
|
||||
}
|
||||
|
||||
private async encryptString002(text: string, key: string, iv: string) {
|
||||
return this.crypto.aes256CbcEncrypt(text, iv, key)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param keyParams Supplied only when encrypting an items key
|
||||
*/
|
||||
private async encryptTextParams(
|
||||
string: string,
|
||||
encryptionKey: string,
|
||||
authKey: string,
|
||||
uuid: string,
|
||||
version: Common.ProtocolVersion,
|
||||
keyParams?: SNRootKeyParams,
|
||||
) {
|
||||
const iv = this.crypto.generateRandomKey(V002Algorithm.EncryptionIvLength)
|
||||
const contentCiphertext = await this.encryptString002(string, encryptionKey, iv)
|
||||
const ciphertextToAuth = [version, uuid, iv, contentCiphertext].join(':')
|
||||
const authHash = await this.crypto.hmac256(ciphertextToAuth, authKey)
|
||||
|
||||
if (!authHash) {
|
||||
throw Error('Error generating hmac256 authHash')
|
||||
}
|
||||
|
||||
const components: string[] = [version as string, authHash, uuid, iv, contentCiphertext]
|
||||
if (keyParams) {
|
||||
const keyParamsString = this.crypto.base64Encode(JSON.stringify(keyParams.content))
|
||||
components.push(keyParamsString)
|
||||
}
|
||||
const fullCiphertext = components.join(':')
|
||||
return fullCiphertext
|
||||
}
|
||||
|
||||
private async decryptTextParams(
|
||||
ciphertextToAuth: string,
|
||||
contentCiphertext: string,
|
||||
encryptionKey: string,
|
||||
iv: string,
|
||||
authHash: string,
|
||||
authKey: string,
|
||||
) {
|
||||
if (!encryptionKey) {
|
||||
throw 'Attempting to decryptTextParams with null encryptionKey'
|
||||
}
|
||||
const localAuthHash = await this.crypto.hmac256(ciphertextToAuth, authKey)
|
||||
if (!localAuthHash) {
|
||||
throw Error('Error generating hmac256 localAuthHash')
|
||||
}
|
||||
|
||||
if (this.crypto.timingSafeEqual(authHash, localAuthHash) === false) {
|
||||
console.error(Error('Auth hash does not match.'))
|
||||
return null
|
||||
}
|
||||
return this.decryptString002(contentCiphertext, encryptionKey, iv)
|
||||
}
|
||||
|
||||
public override getPayloadAuthenticatedData(
|
||||
encrypted: EncryptedParameters,
|
||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
|
||||
const itemKeyComponents = this.encryptionComponentsFromString002(encrypted.enc_item_key)
|
||||
const authenticatedData = itemKeyComponents.keyParams
|
||||
|
||||
if (!authenticatedData) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const decoded = JSON.parse(this.crypto.base64Decode(authenticatedData))
|
||||
const data: LegacyAttachedData = {
|
||||
...(decoded as Common.AnyKeyParamsContent),
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
public override async generateEncryptedParametersAsync(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
key: Models.ItemsKeyInterface | SNRootKey,
|
||||
): Promise<EncryptedParameters> {
|
||||
/**
|
||||
* Generate new item key that is double the key size.
|
||||
* Will be split to create encryption key and authentication key.
|
||||
*/
|
||||
const itemKey = this.crypto.generateRandomKey(V002Algorithm.EncryptionKeyLength * 2)
|
||||
const encItemKey = await this.encryptTextParams(
|
||||
itemKey,
|
||||
key.itemsKey,
|
||||
key.dataAuthenticationKey as string,
|
||||
payload.uuid,
|
||||
key.keyVersion,
|
||||
key instanceof SNRootKey ? (key as SNRootKey).keyParams : undefined,
|
||||
)
|
||||
|
||||
const ek = Utils.firstHalfOfString(itemKey)
|
||||
const ak = Utils.secondHalfOfString(itemKey)
|
||||
const ciphertext = await this.encryptTextParams(
|
||||
JSON.stringify(payload.content),
|
||||
ek,
|
||||
ak,
|
||||
payload.uuid,
|
||||
key.keyVersion,
|
||||
key instanceof SNRootKey ? (key as SNRootKey).keyParams : undefined,
|
||||
)
|
||||
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
items_key_id: isItemsKey(key) ? key.uuid : undefined,
|
||||
content: ciphertext,
|
||||
enc_item_key: encItemKey,
|
||||
version: this.version,
|
||||
}
|
||||
}
|
||||
|
||||
public override async generateDecryptedParametersAsync<C extends ItemContent = ItemContent>(
|
||||
encrypted: EncryptedParameters,
|
||||
key: Models.ItemsKeyInterface | SNRootKey,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
if (!encrypted.enc_item_key) {
|
||||
console.error(Error('Missing item encryption key, skipping decryption.'))
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
}
|
||||
|
||||
const encryptedItemKey = encrypted.enc_item_key
|
||||
const itemKeyComponents = this.encryptionComponentsFromString002(
|
||||
encryptedItemKey,
|
||||
key.itemsKey,
|
||||
key.dataAuthenticationKey,
|
||||
)
|
||||
|
||||
const itemKey = await this.decryptTextParams(
|
||||
itemKeyComponents.ciphertextToAuth,
|
||||
itemKeyComponents.contentCiphertext,
|
||||
itemKeyComponents.encryptionKey as string,
|
||||
itemKeyComponents.iv,
|
||||
itemKeyComponents.authHash,
|
||||
itemKeyComponents.authKey as string,
|
||||
)
|
||||
if (!itemKey) {
|
||||
console.error('Error decrypting item_key parameters', encrypted)
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
}
|
||||
|
||||
const ek = Utils.firstHalfOfString(itemKey)
|
||||
const ak = Utils.secondHalfOfString(itemKey)
|
||||
const itemParams = this.encryptionComponentsFromString002(encrypted.content, ek, ak)
|
||||
const content = await this.decryptTextParams(
|
||||
itemParams.ciphertextToAuth,
|
||||
itemParams.contentCiphertext,
|
||||
itemParams.encryptionKey as string,
|
||||
itemParams.iv,
|
||||
itemParams.authHash,
|
||||
itemParams.authKey as string,
|
||||
)
|
||||
|
||||
if (!content) {
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
content: JSON.parse(content),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async deriveKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
const derivedKey = await this.crypto.pbkdf2(
|
||||
password,
|
||||
keyParams.content002.pw_salt,
|
||||
keyParams.content002.pw_cost,
|
||||
V002Algorithm.PbkdfOutputLength,
|
||||
)
|
||||
|
||||
if (!derivedKey) {
|
||||
throw Error('Error deriving PBKDF2 key')
|
||||
}
|
||||
|
||||
const partitions = Utils.splitString(derivedKey, 3)
|
||||
|
||||
return CreateNewRootKey({
|
||||
serverPassword: partitions[0],
|
||||
masterKey: partitions[1],
|
||||
dataAuthenticationKey: partitions[2],
|
||||
version: Common.ProtocolVersion.V002,
|
||||
keyParams: keyParams.getPortableValue(),
|
||||
})
|
||||
}
|
||||
|
||||
private encryptionComponentsFromString002(string: string, encryptionKey?: string, authKey?: string) {
|
||||
const components = string.split(':')
|
||||
return {
|
||||
encryptionVersion: components[0],
|
||||
authHash: components[1],
|
||||
uuid: components[2],
|
||||
iv: components[3],
|
||||
contentCiphertext: components[4],
|
||||
keyParams: components[5],
|
||||
ciphertextToAuth: [components[0], components[2], components[3], components[4]].join(':'),
|
||||
encryptionKey: encryptionKey,
|
||||
authKey: authKey,
|
||||
}
|
||||
}
|
||||
}
|
||||
111
packages/encryption/src/Domain/Operator/003/Operator003.ts
Normal file
111
packages/encryption/src/Domain/Operator/003/Operator003.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { splitString, UuidGenerator } from '@standardnotes/utils'
|
||||
import {
|
||||
CreateDecryptedItemFromPayload,
|
||||
DecryptedPayload,
|
||||
ItemsKeyContent,
|
||||
ItemsKeyInterface,
|
||||
FillItemContent,
|
||||
PayloadTimestampDefaults,
|
||||
} from '@standardnotes/models'
|
||||
import { SNRootKey } from '../../Keys/RootKey/RootKey'
|
||||
import { V003Algorithm } from '../../Algorithm'
|
||||
import { Create003KeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
|
||||
import { SNProtocolOperator002 } from '../002/Operator002'
|
||||
import { ContentType, KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
|
||||
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
|
||||
import { CreateNewRootKey } from '../../Keys/RootKey/Functions'
|
||||
|
||||
/**
|
||||
* @legacy
|
||||
* Non-expired operator but no longer used for generating new accounts.
|
||||
* This operator subclasses the 002 operator to share functionality that has not
|
||||
* changed, and overrides functions where behavior may differ.
|
||||
*/
|
||||
export class SNProtocolOperator003 extends SNProtocolOperator002 {
|
||||
override get version(): ProtocolVersion {
|
||||
return ProtocolVersion.V003
|
||||
}
|
||||
|
||||
protected override generateNewItemsKeyContent(): ItemsKeyContent {
|
||||
const keyLength = V003Algorithm.EncryptionKeyLength
|
||||
const itemsKey = this.crypto.generateRandomKey(keyLength)
|
||||
const authKey = this.crypto.generateRandomKey(keyLength)
|
||||
const response = FillItemContent<ItemsKeyContent>({
|
||||
itemsKey: itemsKey,
|
||||
dataAuthenticationKey: authKey,
|
||||
version: ProtocolVersion.V003,
|
||||
})
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new random items key to use for item encryption.
|
||||
* The consumer must save/sync this item.
|
||||
*/
|
||||
public override createItemsKey(): ItemsKeyInterface {
|
||||
const content = this.generateNewItemsKeyContent()
|
||||
const payload = new DecryptedPayload({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: FillItemContent(content),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
return CreateDecryptedItemFromPayload(payload)
|
||||
}
|
||||
|
||||
public override async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
protected override async deriveKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
const salt = await this.generateSalt(
|
||||
keyParams.content003.identifier,
|
||||
ProtocolVersion.V003,
|
||||
V003Algorithm.PbkdfCost,
|
||||
keyParams.content003.pw_nonce,
|
||||
)
|
||||
|
||||
const derivedKey = await this.crypto.pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
V003Algorithm.PbkdfCost,
|
||||
V003Algorithm.PbkdfOutputLength,
|
||||
)
|
||||
|
||||
if (!derivedKey) {
|
||||
throw Error('Error deriving PBKDF2 key')
|
||||
}
|
||||
|
||||
const partitions = splitString(derivedKey, 3)
|
||||
|
||||
return CreateNewRootKey({
|
||||
serverPassword: partitions[0],
|
||||
masterKey: partitions[1],
|
||||
dataAuthenticationKey: partitions[2],
|
||||
version: ProtocolVersion.V003,
|
||||
keyParams: keyParams.getPortableValue(),
|
||||
})
|
||||
}
|
||||
|
||||
public override async createRootKey(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: KeyParamsOrigination,
|
||||
): Promise<SNRootKey> {
|
||||
const version = ProtocolVersion.V003
|
||||
const pwNonce = this.crypto.generateRandomKey(V003Algorithm.SaltSeedLength)
|
||||
const keyParams = Create003KeyParams({
|
||||
identifier: identifier,
|
||||
pw_nonce: pwNonce,
|
||||
version: version,
|
||||
origination: origination,
|
||||
created: `${Date.now()}`,
|
||||
})
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
private async generateSalt(identifier: string, version: ProtocolVersion, cost: number, nonce: string) {
|
||||
const result = await this.crypto.sha256([identifier, 'SF', version, cost, nonce].join(':'))
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
import { DecryptedPayload, ItemContent, ItemsKeyContent, PayloadTimestampDefaults } from '@standardnotes/models'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { SNItemsKey } from '../../Keys/ItemsKey'
|
||||
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
|
||||
import { SNProtocolOperator004 } from './Operator004'
|
||||
|
||||
const b64 = (text: string): string => {
|
||||
return Buffer.from(text).toString('base64')
|
||||
}
|
||||
|
||||
describe('operator 004', () => {
|
||||
let crypto: PureCryptoInterface
|
||||
let operator: SNProtocolOperator004
|
||||
|
||||
beforeEach(() => {
|
||||
crypto = {} as jest.Mocked<PureCryptoInterface>
|
||||
crypto.base64Encode = jest.fn().mockImplementation((text: string) => {
|
||||
return b64(text)
|
||||
})
|
||||
crypto.base64Decode = jest.fn().mockImplementation((text: string) => {
|
||||
return Buffer.from(text, 'base64').toString('ascii')
|
||||
})
|
||||
crypto.xchacha20Encrypt = jest.fn().mockImplementation((text: string) => {
|
||||
return `<e>${text}<e>`
|
||||
})
|
||||
crypto.xchacha20Decrypt = jest.fn().mockImplementation((text: string) => {
|
||||
return text.split('<e>')[1]
|
||||
})
|
||||
crypto.generateRandomKey = jest.fn().mockImplementation(() => {
|
||||
return 'random-string'
|
||||
})
|
||||
|
||||
operator = new SNProtocolOperator004(crypto)
|
||||
})
|
||||
|
||||
it('should generateEncryptedProtocolString', () => {
|
||||
const aad: ItemAuthenticatedData = {
|
||||
u: '123',
|
||||
v: ProtocolVersion.V004,
|
||||
}
|
||||
|
||||
const nonce = 'noncy'
|
||||
const plaintext = 'foo'
|
||||
|
||||
operator.generateEncryptionNonce = jest.fn().mockReturnValue(nonce)
|
||||
|
||||
const result = operator.generateEncryptedProtocolString(plaintext, 'secret', aad)
|
||||
|
||||
expect(result).toEqual(`004:${nonce}:<e>${plaintext}<e>:${b64(JSON.stringify(aad))}`)
|
||||
})
|
||||
|
||||
it('should deconstructEncryptedPayloadString', () => {
|
||||
const string = '004:noncy:<e>foo<e>:eyJ1IjoiMTIzIiwidiI6IjAwNCJ9'
|
||||
|
||||
const result = operator.deconstructEncryptedPayloadString(string)
|
||||
|
||||
expect(result).toEqual({
|
||||
version: '004',
|
||||
nonce: 'noncy',
|
||||
ciphertext: '<e>foo<e>',
|
||||
authenticatedData: 'eyJ1IjoiMTIzIiwidiI6IjAwNCJ9',
|
||||
})
|
||||
})
|
||||
|
||||
it('should generateEncryptedParametersSync', () => {
|
||||
const payload = {
|
||||
uuid: '123',
|
||||
content_type: ContentType.Note,
|
||||
content: { foo: 'bar' } as unknown as jest.Mocked<ItemContent>,
|
||||
...PayloadTimestampDefaults(),
|
||||
} as jest.Mocked<DecryptedPayload>
|
||||
|
||||
const key = new SNItemsKey(
|
||||
new DecryptedPayload<ItemsKeyContent>({
|
||||
uuid: 'key-456',
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: {
|
||||
itemsKey: 'secret',
|
||||
version: ProtocolVersion.V004,
|
||||
} as jest.Mocked<ItemsKeyContent>,
|
||||
...PayloadTimestampDefaults(),
|
||||
}),
|
||||
)
|
||||
|
||||
const result = operator.generateEncryptedParametersSync(payload, key)
|
||||
|
||||
expect(result).toEqual({
|
||||
uuid: '123',
|
||||
items_key_id: 'key-456',
|
||||
content: '004:random-string:<e>{"foo":"bar"}<e>:eyJ1IjoiMTIzIiwidiI6IjAwNCJ9',
|
||||
enc_item_key: '004:random-string:<e>random-string<e>:eyJ1IjoiMTIzIiwidiI6IjAwNCJ9',
|
||||
version: '004',
|
||||
})
|
||||
})
|
||||
})
|
||||
319
packages/encryption/src/Domain/Operator/004/Operator004.ts
Normal file
319
packages/encryption/src/Domain/Operator/004/Operator004.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { ContentType, KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
|
||||
import { Create004KeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
|
||||
import { SynchronousOperator } from '../Operator'
|
||||
import {
|
||||
CreateDecryptedItemFromPayload,
|
||||
FillItemContent,
|
||||
ItemContent,
|
||||
ItemsKeyContent,
|
||||
ItemsKeyInterface,
|
||||
PayloadTimestampDefaults,
|
||||
} from '@standardnotes/models'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { SNRootKey } from '../../Keys/RootKey/RootKey'
|
||||
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
|
||||
import { V004Algorithm } from '../../Algorithm'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import * as Utils from '@standardnotes/utils'
|
||||
import { ContentTypeUsesRootKeyEncryption, CreateNewRootKey } from '../../Keys/RootKey/Functions'
|
||||
import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters'
|
||||
import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData'
|
||||
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
|
||||
import { LegacyAttachedData } from '../../Types/LegacyAttachedData'
|
||||
import { isItemsKey } from '../../Keys/ItemsKey'
|
||||
|
||||
type V004StringComponents = [version: string, nonce: string, ciphertext: string, authenticatedData: string]
|
||||
|
||||
type V004Components = {
|
||||
version: V004StringComponents[0]
|
||||
nonce: V004StringComponents[1]
|
||||
ciphertext: V004StringComponents[2]
|
||||
authenticatedData: V004StringComponents[3]
|
||||
}
|
||||
|
||||
const PARTITION_CHARACTER = ':'
|
||||
|
||||
export class SNProtocolOperator004 implements SynchronousOperator {
|
||||
protected readonly crypto: PureCryptoInterface
|
||||
|
||||
constructor(crypto: PureCryptoInterface) {
|
||||
this.crypto = crypto
|
||||
}
|
||||
|
||||
public getEncryptionDisplayName(): string {
|
||||
return 'XChaCha20-Poly1305'
|
||||
}
|
||||
|
||||
get version(): ProtocolVersion {
|
||||
return ProtocolVersion.V004
|
||||
}
|
||||
|
||||
private generateNewItemsKeyContent() {
|
||||
const itemsKey = this.crypto.generateRandomKey(V004Algorithm.EncryptionKeyLength)
|
||||
const response = FillItemContent<ItemsKeyContent>({
|
||||
itemsKey: itemsKey,
|
||||
version: ProtocolVersion.V004,
|
||||
})
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new random items key to use for item encryption.
|
||||
* The consumer must save/sync this item.
|
||||
*/
|
||||
public createItemsKey(): ItemsKeyInterface {
|
||||
const payload = new Models.DecryptedPayload({
|
||||
uuid: Utils.UuidGenerator.GenerateUuid(),
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: this.generateNewItemsKeyContent(),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
return CreateDecryptedItemFromPayload(payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* We require both a client-side component and a server-side component in generating a
|
||||
* salt. This way, a comprimised server cannot benefit from sending the same seed value
|
||||
* for every user. We mix a client-controlled value that is globally unique
|
||||
* (their identifier), with a server controlled value to produce a salt for our KDF.
|
||||
* @param identifier
|
||||
* @param seed
|
||||
*/
|
||||
private async generateSalt004(identifier: string, seed: string) {
|
||||
const hash = await this.crypto.sha256([identifier, seed].join(PARTITION_CHARACTER))
|
||||
return Utils.truncateHexString(hash, V004Algorithm.ArgonSaltLength)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a root key given a passworf
|
||||
* qwd and previous keyParams
|
||||
* @param password - Plain string representing raw user password
|
||||
* @param keyParams - KeyParams object
|
||||
*/
|
||||
public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new root key given an identifier and a user password
|
||||
* @param identifier - Plain string representing a unique identifier
|
||||
* @param password - Plain string representing raw user password
|
||||
*/
|
||||
public async createRootKey(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: KeyParamsOrigination,
|
||||
): Promise<SNRootKey> {
|
||||
const version = ProtocolVersion.V004
|
||||
const seed = this.crypto.generateRandomKey(V004Algorithm.ArgonSaltSeedLength)
|
||||
const keyParams = Create004KeyParams({
|
||||
identifier: identifier,
|
||||
pw_nonce: seed,
|
||||
version: version,
|
||||
origination: origination,
|
||||
created: `${Date.now()}`,
|
||||
})
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param plaintext - The plaintext to encrypt.
|
||||
* @param rawKey - The key to use to encrypt the plaintext.
|
||||
* @param nonce - The nonce for encryption.
|
||||
* @param authenticatedData - JavaScript object (will be stringified) representing
|
||||
'Additional authenticated data': data you want to be included in authentication.
|
||||
*/
|
||||
encryptString004(plaintext: string, rawKey: string, nonce: string, authenticatedData: ItemAuthenticatedData) {
|
||||
if (!nonce) {
|
||||
throw 'encryptString null nonce'
|
||||
}
|
||||
if (!rawKey) {
|
||||
throw 'encryptString null rawKey'
|
||||
}
|
||||
return this.crypto.xchacha20Encrypt(plaintext, nonce, rawKey, this.authenticatedDataToString(authenticatedData))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ciphertext The encrypted text to decrypt.
|
||||
* @param rawKey The key to use to decrypt the ciphertext.
|
||||
* @param nonce The nonce for decryption.
|
||||
* @param rawAuthenticatedData String representing
|
||||
'Additional authenticated data' - data you want to be included in authentication.
|
||||
*/
|
||||
private decryptString004(ciphertext: string, rawKey: string, nonce: string, rawAuthenticatedData: string) {
|
||||
return this.crypto.xchacha20Decrypt(ciphertext, nonce, rawKey, rawAuthenticatedData)
|
||||
}
|
||||
|
||||
generateEncryptionNonce(): string {
|
||||
return this.crypto.generateRandomKey(V004Algorithm.EncryptionNonceLength)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param plaintext The plaintext text to decrypt.
|
||||
* @param rawKey The key to use to encrypt the plaintext.
|
||||
*/
|
||||
generateEncryptedProtocolString(plaintext: string, rawKey: string, authenticatedData: ItemAuthenticatedData) {
|
||||
const nonce = this.generateEncryptionNonce()
|
||||
|
||||
const ciphertext = this.encryptString004(plaintext, rawKey, nonce, authenticatedData)
|
||||
|
||||
const components: V004StringComponents = [
|
||||
ProtocolVersion.V004 as string,
|
||||
nonce,
|
||||
ciphertext,
|
||||
this.authenticatedDataToString(authenticatedData),
|
||||
]
|
||||
|
||||
return components.join(PARTITION_CHARACTER)
|
||||
}
|
||||
|
||||
deconstructEncryptedPayloadString(payloadString: string): V004Components {
|
||||
const components = payloadString.split(PARTITION_CHARACTER) as V004StringComponents
|
||||
|
||||
return {
|
||||
version: components[0],
|
||||
nonce: components[1],
|
||||
ciphertext: components[2],
|
||||
authenticatedData: components[3],
|
||||
}
|
||||
}
|
||||
|
||||
public getPayloadAuthenticatedData(
|
||||
encrypted: EncryptedParameters,
|
||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
|
||||
const itemKeyComponents = this.deconstructEncryptedPayloadString(encrypted.enc_item_key)
|
||||
const authenticatedDataString = itemKeyComponents.authenticatedData
|
||||
const result = this.stringToAuthenticatedData(authenticatedDataString)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* For items that are encrypted with a root key, we append the root key's key params, so
|
||||
* that in the event the client/user loses a reference to their root key, they may still
|
||||
* decrypt data by regenerating the key based on the attached key params.
|
||||
*/
|
||||
private generateAuthenticatedDataForPayload(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
key: ItemsKeyInterface | SNRootKey,
|
||||
): ItemAuthenticatedData | RootKeyEncryptedAuthenticatedData {
|
||||
const baseData: ItemAuthenticatedData = {
|
||||
u: payload.uuid,
|
||||
v: ProtocolVersion.V004,
|
||||
}
|
||||
if (ContentTypeUsesRootKeyEncryption(payload.content_type)) {
|
||||
return {
|
||||
...baseData,
|
||||
kp: (key as SNRootKey).keyParams.content,
|
||||
}
|
||||
} else {
|
||||
if (!isItemsKey(key)) {
|
||||
throw Error('Attempting to use non-items key for regular item.')
|
||||
}
|
||||
return baseData
|
||||
}
|
||||
}
|
||||
|
||||
private authenticatedDataToString(attachedData: ItemAuthenticatedData) {
|
||||
return this.crypto.base64Encode(JSON.stringify(Utils.sortedCopy(Utils.omitUndefinedCopy(attachedData))))
|
||||
}
|
||||
|
||||
private stringToAuthenticatedData(
|
||||
rawAuthenticatedData: string,
|
||||
override?: Partial<ItemAuthenticatedData>,
|
||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData {
|
||||
const base = JSON.parse(this.crypto.base64Decode(rawAuthenticatedData))
|
||||
return Utils.sortedCopy({
|
||||
...base,
|
||||
...override,
|
||||
})
|
||||
}
|
||||
|
||||
public generateEncryptedParametersSync(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
key: ItemsKeyInterface | SNRootKey,
|
||||
): EncryptedParameters {
|
||||
const itemKey = this.crypto.generateRandomKey(V004Algorithm.EncryptionKeyLength)
|
||||
|
||||
const contentPlaintext = JSON.stringify(payload.content)
|
||||
const authenticatedData = this.generateAuthenticatedDataForPayload(payload, key)
|
||||
const encryptedContentString = this.generateEncryptedProtocolString(contentPlaintext, itemKey, authenticatedData)
|
||||
|
||||
const encryptedItemKey = this.generateEncryptedProtocolString(itemKey, key.itemsKey, authenticatedData)
|
||||
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
items_key_id: isItemsKey(key) ? key.uuid : undefined,
|
||||
content: encryptedContentString,
|
||||
enc_item_key: encryptedItemKey,
|
||||
version: this.version,
|
||||
}
|
||||
}
|
||||
|
||||
public generateDecryptedParametersSync<C extends ItemContent = ItemContent>(
|
||||
encrypted: EncryptedParameters,
|
||||
key: ItemsKeyInterface | SNRootKey,
|
||||
): DecryptedParameters<C> | ErrorDecryptingParameters {
|
||||
const itemKeyComponents = this.deconstructEncryptedPayloadString(encrypted.enc_item_key)
|
||||
const authenticatedData = this.stringToAuthenticatedData(itemKeyComponents.authenticatedData, {
|
||||
u: encrypted.uuid,
|
||||
v: encrypted.version,
|
||||
})
|
||||
|
||||
const useAuthenticatedString = this.authenticatedDataToString(authenticatedData)
|
||||
const itemKey = this.decryptString004(
|
||||
itemKeyComponents.ciphertext,
|
||||
key.itemsKey,
|
||||
itemKeyComponents.nonce,
|
||||
useAuthenticatedString,
|
||||
)
|
||||
|
||||
if (!itemKey) {
|
||||
console.error('Error decrypting itemKey parameters', encrypted)
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
}
|
||||
|
||||
const contentComponents = this.deconstructEncryptedPayloadString(encrypted.content)
|
||||
const content = this.decryptString004(
|
||||
contentComponents.ciphertext,
|
||||
itemKey,
|
||||
contentComponents.nonce,
|
||||
useAuthenticatedString,
|
||||
)
|
||||
if (!content) {
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
content: JSON.parse(content),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async deriveKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
const salt = await this.generateSalt004(keyParams.content004.identifier, keyParams.content004.pw_nonce)
|
||||
const derivedKey = this.crypto.argon2(
|
||||
password,
|
||||
salt,
|
||||
V004Algorithm.ArgonIterations,
|
||||
V004Algorithm.ArgonMemLimit,
|
||||
V004Algorithm.ArgonOutputKeyBytes,
|
||||
)
|
||||
const partitions = Utils.splitString(derivedKey, 2)
|
||||
const masterKey = partitions[0]
|
||||
const serverPassword = partitions[1]
|
||||
|
||||
return CreateNewRootKey({
|
||||
masterKey,
|
||||
serverPassword,
|
||||
version: ProtocolVersion.V004,
|
||||
keyParams: keyParams.getPortableValue(),
|
||||
})
|
||||
}
|
||||
}
|
||||
30
packages/encryption/src/Domain/Operator/Functions.ts
Normal file
30
packages/encryption/src/Domain/Operator/Functions.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { SNProtocolOperator001 } from '../Operator/001/Operator001'
|
||||
import { SNProtocolOperator002 } from '../Operator/002/Operator002'
|
||||
import { SNProtocolOperator003 } from '../Operator/003/Operator003'
|
||||
import { SNProtocolOperator004 } from '../Operator/004/Operator004'
|
||||
import { AsynchronousOperator, SynchronousOperator } from '../Operator/Operator'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
export function createOperatorForVersion(
|
||||
version: ProtocolVersion,
|
||||
crypto: PureCryptoInterface,
|
||||
): AsynchronousOperator | SynchronousOperator {
|
||||
if (version === ProtocolVersion.V001) {
|
||||
return new SNProtocolOperator001(crypto)
|
||||
} else if (version === ProtocolVersion.V002) {
|
||||
return new SNProtocolOperator002(crypto)
|
||||
} else if (version === ProtocolVersion.V003) {
|
||||
return new SNProtocolOperator003(crypto)
|
||||
} else if (version === ProtocolVersion.V004) {
|
||||
return new SNProtocolOperator004(crypto)
|
||||
} else {
|
||||
throw Error(`Unable to find operator for version ${version}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function isAsyncOperator(
|
||||
operator: AsynchronousOperator | SynchronousOperator,
|
||||
): operator is AsynchronousOperator {
|
||||
return (operator as AsynchronousOperator).generateDecryptedParametersAsync !== undefined
|
||||
}
|
||||
86
packages/encryption/src/Domain/Operator/Operator.ts
Normal file
86
packages/encryption/src/Domain/Operator/Operator.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { ItemsKeyInterface, RootKeyInterface } from '@standardnotes/models'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import { SNRootKey } from '../Keys/RootKey/RootKey'
|
||||
import { SNRootKeyParams } from '../Keys/RootKey/RootKeyParams'
|
||||
import { KeyParamsOrigination } from '@standardnotes/common'
|
||||
import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../Types/EncryptedParameters'
|
||||
import { RootKeyEncryptedAuthenticatedData } from '../Types/RootKeyEncryptedAuthenticatedData'
|
||||
import { ItemAuthenticatedData } from '../Types/ItemAuthenticatedData'
|
||||
import { LegacyAttachedData } from '../Types/LegacyAttachedData'
|
||||
|
||||
/**w
|
||||
* An operator is responsible for performing crypto operations, such as generating keys
|
||||
* and encrypting/decrypting payloads. Operators interact directly with
|
||||
* platform dependent SNPureCrypto implementation to directly access cryptographic primitives.
|
||||
* Each operator is versioned according to the protocol version. Functions that are common
|
||||
* across all versions appear in this generic parent class.
|
||||
*/
|
||||
export interface OperatorCommon {
|
||||
createItemsKey(): ItemsKeyInterface
|
||||
/**
|
||||
* Returns encryption protocol display name
|
||||
*/
|
||||
getEncryptionDisplayName(): string
|
||||
|
||||
readonly version: string
|
||||
|
||||
/**
|
||||
* Returns the payload's authenticated data. The passed payload must be in a
|
||||
* non-decrypted, ciphertext state.
|
||||
*/
|
||||
getPayloadAuthenticatedData(
|
||||
encrypted: EncryptedParameters,
|
||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined
|
||||
|
||||
/**
|
||||
* Computes a root key given a password and previous keyParams
|
||||
* @param password - Plain string representing raw user password
|
||||
*/
|
||||
computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey>
|
||||
|
||||
/**
|
||||
* Creates a new root key given an identifier and a user password
|
||||
* @param identifier - Plain string representing a unique identifier
|
||||
* for the user
|
||||
* @param password - Plain string representing raw user password
|
||||
*/
|
||||
createRootKey(identifier: string, password: string, origination: KeyParamsOrigination): Promise<SNRootKey>
|
||||
}
|
||||
|
||||
export interface SynchronousOperator extends OperatorCommon {
|
||||
/**
|
||||
* Converts a bare payload into an encrypted one in the desired format.
|
||||
* @param payload - The non-encrypted payload object to encrypt
|
||||
* @param key - The key to use to encrypt the payload. Can be either
|
||||
* a RootKey (when encrypting payloads that require root key encryption, such as encrypting
|
||||
* items keys), or an ItemsKey (if encrypted regular items)
|
||||
*/
|
||||
generateEncryptedParametersSync(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
key: ItemsKeyInterface | RootKeyInterface,
|
||||
): EncryptedParameters
|
||||
|
||||
generateDecryptedParametersSync<C extends Models.ItemContent = Models.ItemContent>(
|
||||
encrypted: EncryptedParameters,
|
||||
key: ItemsKeyInterface | RootKeyInterface,
|
||||
): DecryptedParameters<C> | ErrorDecryptingParameters
|
||||
}
|
||||
|
||||
export interface AsynchronousOperator extends OperatorCommon {
|
||||
/**
|
||||
* Converts a bare payload into an encrypted one in the desired format.
|
||||
* @param payload - The non-encrypted payload object to encrypt
|
||||
* @param key - The key to use to encrypt the payload. Can be either
|
||||
* a RootKey (when encrypting payloads that require root key encryption, such as encrypting
|
||||
* items keys), or an ItemsKey (if encrypted regular items)
|
||||
*/
|
||||
generateEncryptedParametersAsync(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
key: ItemsKeyInterface | RootKeyInterface,
|
||||
): Promise<EncryptedParameters>
|
||||
|
||||
generateDecryptedParametersAsync<C extends Models.ItemContent = Models.ItemContent>(
|
||||
encrypted: EncryptedParameters,
|
||||
key: ItemsKeyInterface | RootKeyInterface,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters>
|
||||
}
|
||||
34
packages/encryption/src/Domain/Operator/OperatorManager.ts
Normal file
34
packages/encryption/src/Domain/Operator/OperatorManager.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { ProtocolVersion, ProtocolVersionLatest } from '@standardnotes/common'
|
||||
import { createOperatorForVersion } from './Functions'
|
||||
import { AsynchronousOperator, SynchronousOperator } from './Operator'
|
||||
|
||||
export class OperatorManager {
|
||||
private operators: Record<string, AsynchronousOperator | SynchronousOperator> = {}
|
||||
|
||||
constructor(private crypto: PureCryptoInterface) {
|
||||
this.crypto = crypto
|
||||
}
|
||||
|
||||
public deinit(): void {
|
||||
;(this.crypto as unknown) = undefined
|
||||
this.operators = {}
|
||||
}
|
||||
|
||||
public operatorForVersion(version: ProtocolVersion): SynchronousOperator | AsynchronousOperator {
|
||||
const operatorKey = version
|
||||
let operator = this.operators[operatorKey]
|
||||
if (!operator) {
|
||||
operator = createOperatorForVersion(version, this.crypto)
|
||||
this.operators[operatorKey] = operator
|
||||
}
|
||||
return operator
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operator corresponding to the latest protocol version
|
||||
*/
|
||||
public defaultOperator(): SynchronousOperator | AsynchronousOperator {
|
||||
return this.operatorForVersion(ProtocolVersionLatest)
|
||||
}
|
||||
}
|
||||
52
packages/encryption/src/Domain/Operator/OperatorWrapper.ts
Normal file
52
packages/encryption/src/Domain/Operator/OperatorWrapper.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { isAsyncOperator } from './Functions'
|
||||
import { OperatorManager } from './OperatorManager'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import {
|
||||
DecryptedParameters,
|
||||
EncryptedParameters,
|
||||
encryptedParametersFromPayload,
|
||||
ErrorDecryptingParameters,
|
||||
} from '../Types/EncryptedParameters'
|
||||
|
||||
export async function encryptPayload(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
key: Models.ItemsKeyInterface | Models.RootKeyInterface,
|
||||
operatorManager: OperatorManager,
|
||||
): Promise<EncryptedParameters> {
|
||||
const operator = operatorManager.operatorForVersion(key.keyVersion)
|
||||
let encryptionParameters
|
||||
|
||||
if (isAsyncOperator(operator)) {
|
||||
encryptionParameters = await operator.generateEncryptedParametersAsync(payload, key)
|
||||
} else {
|
||||
encryptionParameters = operator.generateEncryptedParametersSync(payload, key)
|
||||
}
|
||||
|
||||
if (!encryptionParameters) {
|
||||
throw 'Unable to generate encryption parameters'
|
||||
}
|
||||
|
||||
return encryptionParameters
|
||||
}
|
||||
|
||||
export async function decryptPayload<C extends Models.ItemContent = Models.ItemContent>(
|
||||
payload: Models.EncryptedPayloadInterface,
|
||||
key: Models.ItemsKeyInterface | Models.RootKeyInterface,
|
||||
operatorManager: OperatorManager,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
const operator = operatorManager.operatorForVersion(payload.version)
|
||||
|
||||
try {
|
||||
if (isAsyncOperator(operator)) {
|
||||
return await operator.generateDecryptedParametersAsync(encryptedParametersFromPayload(payload), key)
|
||||
} else {
|
||||
return operator.generateDecryptedParametersSync(encryptedParametersFromPayload(payload), key)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error decrypting payload', payload, e)
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/encryption/src/Domain/Operator/index.ts
Normal file
9
packages/encryption/src/Domain/Operator/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './001/Operator001'
|
||||
export * from './002/Operator002'
|
||||
export * from './003/Operator003'
|
||||
export * from './004/Operator004'
|
||||
export * from './Operator'
|
||||
export * from '../Types/EncryptedParameters'
|
||||
export * from '../Types/ItemAuthenticatedData'
|
||||
export * from '../Types/LegacyAttachedData'
|
||||
export * from '../Types/RootKeyEncryptedAuthenticatedData'
|
||||
Reference in New Issue
Block a user