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:
Karol Sójko
2022-07-05 10:06:03 +02:00
committed by GitHub
parent 60273785c2
commit e5771fcbde
70 changed files with 4682 additions and 27 deletions

View 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,
}
}
}