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

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

View File

@@ -1,3 +1,5 @@
import { SodiumConstant } from '@standardnotes/sncrypto-common'
export const V001Algorithm = Object.freeze({
SaltSeedLength: 128,
/**
@@ -41,11 +43,21 @@ export enum V004Algorithm {
ArgonIterations = 5,
ArgonMemLimit = 67108864,
ArgonOutputKeyBytes = 64,
EncryptionKeyLength = 256,
EncryptionNonceLength = 192,
}
export enum V005Algorithm {
AsymmetricEncryptionNonceLength = 192,
SymmetricEncryptionNonceLength = 192,
MasterKeyEncryptionKeyPairSubKeyNumber = 1,
MasterKeyEncryptionKeyPairSubKeyContext = 'sn-pkc-e',
MasterKeyEncryptionKeyPairSubKeyBytes = SodiumConstant.crypto_box_SEEDBYTES,
MasterKeySigningKeyPairSubKeyNumber = 2,
MasterKeySigningKeyPairSubKeyContext = 'sn-pkc-s',
MasterKeySigningKeyPairSubKeyBytes = SodiumConstant.crypto_sign_SEEDBYTES,
PayloadKeyHashingKeySubKeyNumber = 1,
PayloadKeyHashingKeySubKeyContext = 'sn-sym-h',
PayloadKeyHashingKeySubKeyBytes = SodiumConstant.crypto_generichash_KEYBYTES,
}

View File

@@ -7,11 +7,10 @@ import {
HistoryEntryInterface,
ItemsKeyContent,
ItemsKeyInterface,
RootKeyInterface,
} from '@standardnotes/models'
export function isItemsKey(x: ItemsKeyInterface | RootKeyInterface): x is ItemsKeyInterface {
return x.content_type === ContentType.ItemsKey
export function isItemsKey(x: unknown): x is ItemsKeyInterface {
return (x as ItemsKeyInterface).content_type === ContentType.ItemsKey
}
/**

View File

@@ -0,0 +1,41 @@
import { ContentType, ProtocolVersion } from '@standardnotes/common'
import {
ConflictStrategy,
DecryptedItem,
DecryptedItemInterface,
DecryptedPayloadInterface,
HistoryEntryInterface,
KeySystemItemsKeyContent,
KeySystemItemsKeyInterface,
} from '@standardnotes/models'
export function isKeySystemItemsKey(x: unknown): x is KeySystemItemsKeyInterface {
return (x as KeySystemItemsKeyInterface).content_type === ContentType.KeySystemItemsKey
}
/**
* A key used to encrypt other items. Items keys are synced and persisted.
*/
export class KeySystemItemsKey extends DecryptedItem<KeySystemItemsKeyContent> implements KeySystemItemsKeyInterface {
creationTimestamp: number
keyVersion: ProtocolVersion
itemsKey: string
rootKeyToken: string
constructor(payload: DecryptedPayloadInterface<KeySystemItemsKeyContent>) {
super(payload)
this.creationTimestamp = payload.content.creationTimestamp
this.keyVersion = payload.content.version
this.itemsKey = this.payload.content.itemsKey
this.rootKeyToken = this.payload.content.rootKeyToken
}
/** Do not duplicate vault items keys. Always keep original */
override strategyWhenConflictingWithItem(
_item: DecryptedItemInterface,
_previousRevision?: HistoryEntryInterface,
): ConflictStrategy {
return ConflictStrategy.KeepBase
}
}

View File

@@ -0,0 +1,3 @@
import { DecryptedItemMutator, KeySystemItemsKeyContent } from '@standardnotes/models'
export class KeySystemItemsKeyMutator extends DecryptedItemMutator<KeySystemItemsKeyContent> {}

View File

@@ -0,0 +1,10 @@
import { ContentType } from '@standardnotes/common'
import { DecryptedItemMutator, KeySystemItemsKeyContent, RegisterItemClass } from '@standardnotes/models'
import { KeySystemItemsKey } from './KeySystemItemsKey'
import { KeySystemItemsKeyMutator } from './KeySystemItemsKeyMutator'
RegisterItemClass(
ContentType.KeySystemItemsKey,
KeySystemItemsKey,
KeySystemItemsKeyMutator as unknown as DecryptedItemMutator<KeySystemItemsKeyContent>,
)

View File

@@ -5,11 +5,12 @@ import {
PayloadTimestampDefaults,
RootKeyContent,
RootKeyContentSpecialized,
RootKeyInterface,
} from '@standardnotes/models'
import { UuidGenerator } from '@standardnotes/utils'
import { SNRootKey } from './RootKey'
export function CreateNewRootKey(content: RootKeyContentSpecialized): SNRootKey {
export function CreateNewRootKey<K extends RootKeyInterface>(content: RootKeyContentSpecialized): K {
const uuid = UuidGenerator.GenerateUuid()
const payload = new DecryptedPayload<RootKeyContent>({
@@ -19,7 +20,7 @@ export function CreateNewRootKey(content: RootKeyContentSpecialized): SNRootKey
...PayloadTimestampDefaults(),
})
return new SNRootKey(payload)
return new SNRootKey(payload) as K
}
export function FillRootKeyContent(content: Partial<RootKeyContentSpecialized>): RootKeyContent {
@@ -37,15 +38,3 @@ export function FillRootKeyContent(content: Partial<RootKeyContentSpecialized>):
return FillItemContentSpecialized(content)
}
export function ContentTypeUsesRootKeyEncryption(contentType: ContentType): boolean {
return (
contentType === ContentType.RootKey ||
contentType === ContentType.ItemsKey ||
contentType === ContentType.EncryptedStorage
)
}
export function ItemContentTypeUsesRootKeyEncryption(contentType: ContentType): boolean {
return contentType === ContentType.ItemsKey
}

View File

@@ -7,7 +7,7 @@ import {
RootKeyContentInStorage,
RootKeyInterface,
} from '@standardnotes/models'
import { timingSafeEqual } from '@standardnotes/sncrypto-common'
import { PkcKeyPair, timingSafeEqual } from '@standardnotes/sncrypto-common'
import { SNRootKeyParams } from './RootKeyParams'
/**
@@ -47,6 +47,14 @@ export class SNRootKey extends DecryptedItem<RootKeyContent> implements RootKeyI
return this.content.serverPassword
}
get encryptionKeyPair(): PkcKeyPair | undefined {
return this.content.encryptionKeyPair
}
get signingKeyPair(): PkcKeyPair | undefined {
return this.content.signingKeyPair
}
/** 003 and below only. */
public get dataAuthenticationKey(): string | undefined {
return this.content.dataAuthenticationKey
@@ -84,6 +92,8 @@ export class SNRootKey extends DecryptedItem<RootKeyContent> implements RootKeyI
const values: NamespacedRootKeyInKeychain = {
version: this.keyVersion,
masterKey: this.masterKey,
encryptionKeyPair: this.encryptionKeyPair,
signingKeyPair: this.signingKeyPair,
}
if (this.dataAuthenticationKey) {

View File

@@ -8,8 +8,13 @@ import {
ItemsKeyContent,
ItemsKeyInterface,
PayloadTimestampDefaults,
KeySystemItemsKeyInterface,
KeySystemIdentifier,
KeySystemRootKeyInterface,
RootKeyInterface,
KeySystemRootKeyParamsInterface,
} from '@standardnotes/models'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { firstHalfOfString, secondHalfOfString, splitString, UuidGenerator } from '@standardnotes/utils'
import { V001Algorithm } from '../../Algorithm'
import { isItemsKey } from '../../Keys/ItemsKey/ItemsKey'
@@ -17,11 +22,16 @@ import { CreateNewRootKey } from '../../Keys/RootKey/Functions'
import { Create001KeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
import { SNRootKey } from '../../Keys/RootKey/RootKey'
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters'
import { EncryptedOutputParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters'
import { DecryptedParameters } from '../../Types/DecryptedParameters'
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
import { LegacyAttachedData } from '../../Types/LegacyAttachedData'
import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData'
import { AsynchronousOperator } from '../Operator'
import { OperatorInterface } from '../OperatorInterface/OperatorInterface'
import { PublicKeySet } from '../Types/PublicKeySet'
import { AsymmetricDecryptResult } from '../Types/AsymmetricDecryptResult'
import { AsymmetricSignatureVerificationDetachedResult } from '../Types/AsymmetricSignatureVerificationDetachedResult'
import { AsyncOperatorInterface } from '../OperatorInterface/AsyncOperatorInterface'
const NO_IV = '00000000000000000000000000000000'
@@ -29,7 +39,7 @@ const NO_IV = '00000000000000000000000000000000'
* @deprecated
* A legacy operator no longer used to generate new accounts
*/
export class SNProtocolOperator001 implements AsynchronousOperator {
export class SNProtocolOperator001 implements OperatorInterface, AsyncOperatorInterface {
protected readonly crypto: PureCryptoInterface
constructor(crypto: PureCryptoInterface) {
@@ -68,11 +78,11 @@ export class SNProtocolOperator001 implements AsynchronousOperator {
return CreateDecryptedItemFromPayload(payload)
}
public async createRootKey(
public async createRootKey<K extends RootKeyInterface>(
identifier: string,
password: string,
origination: KeyParamsOrigination,
): Promise<SNRootKey> {
): Promise<K> {
const pwCost = V001Algorithm.PbkdfMinCost as number
const pwNonce = this.crypto.generateRandomKey(V001Algorithm.SaltSeedLength)
const pwSalt = await this.crypto.unsafeSha1(identifier + 'SN' + pwNonce)
@@ -90,13 +100,13 @@ export class SNProtocolOperator001 implements AsynchronousOperator {
return this.deriveKey(password, keyParams)
}
public getPayloadAuthenticatedData(
_encrypted: EncryptedParameters,
public getPayloadAuthenticatedDataForExternalUse(
_encrypted: EncryptedOutputParameters,
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
return undefined
}
public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
public async computeRootKey<K extends RootKeyInterface>(password: string, keyParams: SNRootKeyParams): Promise<K> {
return this.deriveKey(password, keyParams)
}
@@ -111,7 +121,7 @@ export class SNProtocolOperator001 implements AsynchronousOperator {
public async generateEncryptedParametersAsync(
payload: DecryptedPayloadInterface,
key: ItemsKeyInterface | SNRootKey,
): Promise<EncryptedParameters> {
): Promise<EncryptedOutputParameters> {
/**
* Generate new item key that is double the key size.
* Will be split to create encryption key and authentication key.
@@ -132,16 +142,19 @@ export class SNProtocolOperator001 implements AsynchronousOperator {
return {
uuid: payload.uuid,
content_type: payload.content_type,
items_key_id: isItemsKey(key) ? key.uuid : undefined,
content: ciphertext,
enc_item_key: encItemKey,
auth_hash: authHash,
version: this.version,
key_system_identifier: payload.key_system_identifier,
shared_vault_uuid: payload.shared_vault_uuid,
}
}
public async generateDecryptedParametersAsync<C extends ItemContent = ItemContent>(
encrypted: EncryptedParameters,
encrypted: EncryptedOutputParameters,
key: ItemsKeyInterface | SNRootKey,
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
if (!encrypted.enc_item_key) {
@@ -178,6 +191,7 @@ export class SNProtocolOperator001 implements AsynchronousOperator {
return {
uuid: encrypted.uuid,
content: JSON.parse(content),
signatureData: { required: false, contentHash: '' },
}
}
}
@@ -191,7 +205,7 @@ export class SNProtocolOperator001 implements AsynchronousOperator {
}
}
protected async deriveKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
protected async deriveKey<K extends RootKeyInterface>(password: string, keyParams: SNRootKeyParams): Promise<K> {
const derivedKey = await this.crypto.pbkdf2(
password,
keyParams.content001.pw_salt,
@@ -205,11 +219,63 @@ export class SNProtocolOperator001 implements AsynchronousOperator {
const partitions = splitString(derivedKey, 2)
return CreateNewRootKey({
return CreateNewRootKey<K>({
serverPassword: partitions[0],
masterKey: partitions[1],
version: ProtocolVersion.V001,
keyParams: keyParams.getPortableValue(),
})
}
createRandomizedKeySystemRootKey(_dto: { systemIdentifier: string }): KeySystemRootKeyInterface {
throw new Error('Method not implemented.')
}
createUserInputtedKeySystemRootKey(_dto: {
systemIdentifier: string
systemName: string
userInputtedPassword: string
}): KeySystemRootKeyInterface {
throw new Error('Method not implemented.')
}
deriveUserInputtedKeySystemRootKey(_dto: {
keyParams: KeySystemRootKeyParamsInterface
userInputtedPassword: string
}): KeySystemRootKeyInterface {
throw new Error('Method not implemented.')
}
createKeySystemItemsKey(
_uuid: string,
_keySystemIdentifier: KeySystemIdentifier,
_sharedVaultUuid: string | undefined,
): KeySystemItemsKeyInterface {
throw new Error('Method not implemented.')
}
versionForAsymmetricallyEncryptedString(_encryptedString: string): ProtocolVersion {
throw new Error('Method not implemented.')
}
asymmetricEncrypt(_dto: {
stringToEncrypt: string
senderKeyPair: PkcKeyPair
senderSigningKeyPair: PkcKeyPair
recipientPublicKey: string
}): string {
throw new Error('Method not implemented.')
}
asymmetricDecrypt(_dto: { stringToDecrypt: string; recipientSecretKey: string }): AsymmetricDecryptResult | null {
throw new Error('Method not implemented.')
}
asymmetricSignatureVerifyDetached(_encryptedString: string): AsymmetricSignatureVerificationDetachedResult {
throw new Error('Method not implemented.')
}
getSenderPublicKeySetFromAsymmetricallyEncryptedString(_string: string): PublicKeySet {
throw new Error('Method not implemented.')
}
}

View File

@@ -9,7 +9,8 @@ import { CreateNewRootKey } from '../../Keys/RootKey/Functions'
import { Create002KeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
import { SNRootKey } from '../../Keys/RootKey/RootKey'
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters'
import { EncryptedOutputParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters'
import { DecryptedParameters } from '../../Types/DecryptedParameters'
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
import { LegacyAttachedData } from '../../Types/LegacyAttachedData'
import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData'
@@ -50,11 +51,11 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 {
return Models.CreateDecryptedItemFromPayload(payload)
}
public override async createRootKey(
public override async createRootKey<K extends Models.RootKeyInterface>(
identifier: string,
password: string,
origination: Common.KeyParamsOrigination,
): Promise<SNRootKey> {
): Promise<K> {
const pwCost = Utils.lastElement(V002Algorithm.PbkdfCostsUsed) as number
const pwNonce = this.crypto.generateRandomKey(V002Algorithm.SaltSeedLength)
const pwSalt = await this.crypto.unsafeSha1(identifier + ':' + pwNonce)
@@ -77,7 +78,10 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 {
* 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> {
public override async computeRootKey<K extends Models.RootKeyInterface>(
password: string,
keyParams: SNRootKeyParams,
): Promise<K> {
return this.deriveKey(password, keyParams)
}
@@ -141,8 +145,8 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 {
return this.decryptString002(contentCiphertext, encryptionKey, iv)
}
public override getPayloadAuthenticatedData(
encrypted: EncryptedParameters,
public override getPayloadAuthenticatedDataForExternalUse(
encrypted: EncryptedOutputParameters,
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
const itemKeyComponents = this.encryptionComponentsFromString002(encrypted.enc_item_key)
const authenticatedData = itemKeyComponents.keyParams
@@ -161,7 +165,7 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 {
public override async generateEncryptedParametersAsync(
payload: Models.DecryptedPayloadInterface,
key: Models.ItemsKeyInterface | SNRootKey,
): Promise<EncryptedParameters> {
): Promise<EncryptedOutputParameters> {
/**
* Generate new item key that is double the key size.
* Will be split to create encryption key and authentication key.
@@ -189,15 +193,18 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 {
return {
uuid: payload.uuid,
content_type: payload.content_type,
items_key_id: isItemsKey(key) ? key.uuid : undefined,
content: ciphertext,
enc_item_key: encItemKey,
version: this.version,
key_system_identifier: payload.key_system_identifier,
shared_vault_uuid: payload.shared_vault_uuid,
}
}
public override async generateDecryptedParametersAsync<C extends ItemContent = ItemContent>(
encrypted: EncryptedParameters,
encrypted: EncryptedOutputParameters,
key: Models.ItemsKeyInterface | SNRootKey,
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
if (!encrypted.enc_item_key) {
@@ -252,11 +259,15 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 {
return {
uuid: encrypted.uuid,
content: JSON.parse(content),
signatureData: { required: false, contentHash: '' },
}
}
}
protected override async deriveKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
protected override async deriveKey<K extends Models.RootKeyInterface>(
password: string,
keyParams: SNRootKeyParams,
): Promise<K> {
const derivedKey = await this.crypto.pbkdf2(
password,
keyParams.content002.pw_salt,
@@ -270,7 +281,7 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 {
const partitions = Utils.splitString(derivedKey, 3)
return CreateNewRootKey({
return CreateNewRootKey<K>({
serverPassword: partitions[0],
masterKey: partitions[1],
dataAuthenticationKey: partitions[2],

View File

@@ -6,12 +6,12 @@ import {
ItemsKeyContent,
ItemsKeyInterface,
PayloadTimestampDefaults,
RootKeyInterface,
} from '@standardnotes/models'
import { splitString, UuidGenerator } from '@standardnotes/utils'
import { V003Algorithm } from '../../Algorithm'
import { CreateNewRootKey } from '../../Keys/RootKey/Functions'
import { Create003KeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
import { SNRootKey } from '../../Keys/RootKey/RootKey'
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
import { SNProtocolOperator002 } from '../002/Operator002'
@@ -53,11 +53,17 @@ export class SNProtocolOperator003 extends SNProtocolOperator002 {
return CreateDecryptedItemFromPayload(payload)
}
public override async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
public override async computeRootKey<K extends RootKeyInterface>(
password: string,
keyParams: SNRootKeyParams,
): Promise<K> {
return this.deriveKey(password, keyParams)
}
protected override async deriveKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
protected override async deriveKey<K extends RootKeyInterface>(
password: string,
keyParams: SNRootKeyParams,
): Promise<K> {
const salt = await this.generateSalt(
keyParams.content003.identifier,
ProtocolVersion.V003,
@@ -78,7 +84,7 @@ export class SNProtocolOperator003 extends SNProtocolOperator002 {
const partitions = splitString(derivedKey, 3)
return CreateNewRootKey({
return CreateNewRootKey<K>({
serverPassword: partitions[0],
masterKey: partitions[1],
dataAuthenticationKey: partitions[2],
@@ -87,11 +93,11 @@ export class SNProtocolOperator003 extends SNProtocolOperator002 {
})
}
public override async createRootKey(
public override async createRootKey<K extends RootKeyInterface>(
identifier: string,
password: string,
origination: KeyParamsOrigination,
): Promise<SNRootKey> {
): Promise<K> {
const version = ProtocolVersion.V003
const pwNonce = this.crypto.generateRandomKey(V003Algorithm.SaltSeedLength)
const keyParams = Create003KeyParams({

View File

@@ -0,0 +1,87 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
export function getMockedCrypto(): PureCryptoInterface {
const crypto = {} as jest.Mocked<PureCryptoInterface>
const mockGenerateKeyPair = (seed: string) => {
const publicKey = `public-key-${seed}`
const privateKey = `private-key-${seed}`
return {
publicKey: `${publicKey}:${privateKey}`,
privateKey: `${privateKey}:${publicKey}`,
}
}
const replaceColonsToAvoidJSONConflicts = (text: string) => {
return text.replace(/:/g, '|')
}
const undoReplaceColonsToAvoidJSONConflicts = (text: string) => {
return text.replace(/\|/g, ':')
}
crypto.base64Encode = jest.fn().mockImplementation((text: string) => {
return `base64-${replaceColonsToAvoidJSONConflicts(text)}`
})
crypto.base64Decode = jest.fn().mockImplementation((text: string) => {
const decodedText = text.split('base64-')[1]
return undoReplaceColonsToAvoidJSONConflicts(decodedText)
})
crypto.xchacha20Encrypt = jest.fn().mockImplementation((text: string) => {
return `<e>${replaceColonsToAvoidJSONConflicts(text)}<e>`
})
crypto.xchacha20Decrypt = jest.fn().mockImplementation((text: string) => {
return undoReplaceColonsToAvoidJSONConflicts(text.split('<e>')[1])
})
crypto.generateRandomKey = jest.fn().mockImplementation(() => {
return 'random-string'
})
crypto.sodiumCryptoBoxEasyEncrypt = jest.fn().mockImplementation((text: string) => {
return `<e>${replaceColonsToAvoidJSONConflicts(text)}<e>`
})
crypto.sodiumCryptoBoxEasyDecrypt = jest.fn().mockImplementation((text: string) => {
return undoReplaceColonsToAvoidJSONConflicts(text.split('<e>')[1])
})
crypto.sodiumCryptoBoxSeedKeypair = jest.fn().mockImplementation((seed: string) => {
return mockGenerateKeyPair(seed)
})
crypto.sodiumCryptoKdfDeriveFromKey = jest
.fn()
.mockImplementation((key: string, subkeyNumber: number, subkeyLength: number, context: string) => {
return `subkey-${key}-${subkeyNumber}-${subkeyLength}-${context}`
})
crypto.sodiumCryptoSign = jest.fn().mockImplementation((message: string, privateKey: string) => {
const signature = `signature:m=${message}:pk=${privateKey}`
return signature
})
crypto.sodiumCryptoSignSeedKeypair = jest.fn().mockImplementation((seed: string) => {
return mockGenerateKeyPair(seed)
})
crypto.sodiumCryptoSignVerify = jest
.fn()
.mockImplementation((message: string, signature: string, publicKey: string) => {
const keyComponents = publicKey.split(':')
const privateKeyComponent = keyComponents[1]
const privateKey = `${privateKeyComponent}:${keyComponents[0]}`
const computedSignature = crypto.sodiumCryptoSign(message, privateKey)
return computedSignature === signature
})
crypto.sodiumCryptoGenericHash = jest.fn().mockImplementation((message: string, key: string) => {
return `hash-${message}-${key}`
})
return crypto
}

View File

@@ -1,69 +1,34 @@
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/ItemsKey'
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
import { SNProtocolOperator004 } from './Operator004'
const b64 = (text: string): string => {
return Buffer.from(text).toString('base64')
}
import { getMockedCrypto } from './MockedCrypto'
import { deconstructEncryptedPayloadString } from './V004AlgorithmHelpers'
describe('operator 004', () => {
let crypto: PureCryptoInterface
const crypto = getMockedCrypto()
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)
const result = deconstructEncryptedPayloadString(string)
expect(result).toEqual({
version: '004',
nonce: 'noncy',
ciphertext: '<e>foo<e>',
authenticatedData: 'eyJ1IjoiMTIzIiwidiI6IjAwNCJ9',
additionalData: 'e30=',
})
})
it('should generateEncryptedParametersSync', () => {
it('should generateEncryptedParameters', () => {
const payload = {
uuid: '123',
content_type: ContentType.Note,
@@ -83,13 +48,16 @@ describe('operator 004', () => {
}),
)
const result = operator.generateEncryptedParametersSync(payload, key)
const result = operator.generateEncryptedParameters(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',
key_system_identifier: undefined,
shared_vault_uuid: undefined,
content: '004:random-string:<e>{"foo"|"bar"}<e>:base64-{"u"|"123","v"|"004"}:base64-{}',
content_type: ContentType.Note,
enc_item_key: '004:random-string:<e>random-string<e>:base64-{"u"|"123","v"|"004"}:base64-{}',
version: '004',
})
})

View File

@@ -1,44 +1,56 @@
import { ContentType, KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
import * as Models from '@standardnotes/models'
import {
CreateDecryptedItemFromPayload,
FillItemContent,
ItemContent,
ItemsKeyContent,
ItemsKeyInterface,
PayloadTimestampDefaults,
DecryptedPayload,
DecryptedPayloadInterface,
KeySystemItemsKeyInterface,
KeySystemRootKeyInterface,
FillItemContentSpecialized,
ItemsKeyContentSpecialized,
KeySystemIdentifier,
RootKeyInterface,
KeySystemRootKeyParamsInterface,
} from '@standardnotes/models'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import * as Utils from '@standardnotes/utils'
import { ContentType, KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
import { HexString, PkcKeyPair, PureCryptoInterface, Utf8String } from '@standardnotes/sncrypto-common'
import { V004Algorithm } from '../../Algorithm'
import { isItemsKey } from '../../Keys/ItemsKey/ItemsKey'
import { ContentTypeUsesRootKeyEncryption, CreateNewRootKey } from '../../Keys/RootKey/Functions'
import { Create004KeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
import { SNRootKey } from '../../Keys/RootKey/RootKey'
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters'
import {
EncryptedInputParameters,
EncryptedOutputParameters,
ErrorDecryptingParameters,
} from '../../Types/EncryptedParameters'
import { DecryptedParameters } from '../../Types/DecryptedParameters'
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
import { LegacyAttachedData } from '../../Types/LegacyAttachedData'
import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData'
import { SynchronousOperator } from '../Operator'
import { OperatorInterface } from '../OperatorInterface/OperatorInterface'
import { AsymmetricallyEncryptedString } from '../Types/Types'
import { AsymmetricItemAdditionalData } from '../../Types/EncryptionAdditionalData'
import { V004AsymmetricStringComponents } from './V004AlgorithmTypes'
import { AsymmetricEncryptUseCase } from './UseCase/Asymmetric/AsymmetricEncrypt'
import { ParseConsistentBase64JsonPayloadUseCase } from './UseCase/Utils/ParseConsistentBase64JsonPayload'
import { AsymmetricDecryptUseCase } from './UseCase/Asymmetric/AsymmetricDecrypt'
import { GenerateDecryptedParametersUseCase } from './UseCase/Symmetric/GenerateDecryptedParameters'
import { GenerateEncryptedParametersUseCase } from './UseCase/Symmetric/GenerateEncryptedParameters'
import { DeriveRootKeyUseCase } from './UseCase/RootKey/DeriveRootKey'
import { GetPayloadAuthenticatedDataDetachedUseCase } from './UseCase/Symmetric/GetPayloadAuthenticatedDataDetached'
import { CreateRootKeyUseCase } from './UseCase/RootKey/CreateRootKey'
import { UuidGenerator } from '@standardnotes/utils'
import { CreateKeySystemItemsKeyUseCase } from './UseCase/KeySystem/CreateKeySystemItemsKey'
import { AsymmetricDecryptResult } from '../Types/AsymmetricDecryptResult'
import { PublicKeySet } from '../Types/PublicKeySet'
import { CreateRandomKeySystemRootKey } from './UseCase/KeySystem/CreateRandomKeySystemRootKey'
import { CreateUserInputKeySystemRootKey } from './UseCase/KeySystem/CreateUserInputKeySystemRootKey'
import { AsymmetricSignatureVerificationDetachedResult } from '../Types/AsymmetricSignatureVerificationDetachedResult'
import { AsymmetricSignatureVerificationDetachedUseCase } from './UseCase/Asymmetric/AsymmetricSignatureVerificationDetached'
import { DeriveKeySystemRootKeyUseCase } from './UseCase/KeySystem/DeriveKeySystemRootKey'
import { SyncOperatorInterface } from '../OperatorInterface/SyncOperatorInterface'
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
}
export class SNProtocolOperator004 implements OperatorInterface, SyncOperatorInterface {
constructor(protected readonly crypto: PureCryptoInterface) {}
public getEncryptionDisplayName(): string {
return 'XChaCha20-Poly1305'
@@ -50,7 +62,7 @@ export class SNProtocolOperator004 implements SynchronousOperator {
private generateNewItemsKeyContent() {
const itemsKey = this.crypto.generateRandomKey(V004Algorithm.EncryptionKeyLength)
const response = FillItemContent<ItemsKeyContent>({
const response = FillItemContentSpecialized<ItemsKeyContentSpecialized>({
itemsKey: itemsKey,
version: ProtocolVersion.V004,
})
@@ -62,260 +74,130 @@ export class SNProtocolOperator004 implements SynchronousOperator {
* The consumer must save/sync this item.
*/
public createItemsKey(): ItemsKeyInterface {
const payload = new Models.DecryptedPayload({
uuid: Utils.UuidGenerator.GenerateUuid(),
const payload = new DecryptedPayload({
uuid: UuidGenerator.GenerateUuid(),
content_type: ContentType.ItemsKey,
content: this.generateNewItemsKeyContent(),
key_system_identifier: undefined,
shared_vault_uuid: undefined,
...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)
createRandomizedKeySystemRootKey(dto: { systemIdentifier: KeySystemIdentifier }): KeySystemRootKeyInterface {
const usecase = new CreateRandomKeySystemRootKey(this.crypto)
return usecase.execute(dto)
}
/**
* 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)
createUserInputtedKeySystemRootKey(dto: {
systemIdentifier: KeySystemIdentifier
userInputtedPassword: string
}): KeySystemRootKeyInterface {
const usecase = new CreateUserInputKeySystemRootKey(this.crypto)
return usecase.execute(dto)
}
/**
* 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(
deriveUserInputtedKeySystemRootKey(dto: {
keyParams: KeySystemRootKeyParamsInterface
userInputtedPassword: string
}): KeySystemRootKeyInterface {
const usecase = new DeriveKeySystemRootKeyUseCase(this.crypto)
return usecase.execute({
keyParams: dto.keyParams,
password: dto.userInputtedPassword,
})
}
public createKeySystemItemsKey(
uuid: string,
keySystemIdentifier: KeySystemIdentifier,
sharedVaultUuid: string | undefined,
rootKeyToken: string,
): KeySystemItemsKeyInterface {
const usecase = new CreateKeySystemItemsKeyUseCase(this.crypto)
return usecase.execute({ uuid, keySystemIdentifier, sharedVaultUuid, rootKeyToken })
}
public async computeRootKey<K extends RootKeyInterface>(
password: Utf8String,
keyParams: SNRootKeyParams,
): Promise<K> {
const usecase = new DeriveRootKeyUseCase(this.crypto)
return usecase.execute(password, keyParams)
}
public async createRootKey<K extends RootKeyInterface>(
identifier: string,
password: string,
password: Utf8String,
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)
): Promise<K> {
const usecase = new CreateRootKeyUseCase(this.crypto)
return usecase.execute(identifier, password, origination)
}
/**
* @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,
public getPayloadAuthenticatedDataForExternalUse(
encrypted: EncryptedOutputParameters,
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
const itemKeyComponents = this.deconstructEncryptedPayloadString(encrypted.enc_item_key)
const authenticatedDataString = itemKeyComponents.authenticatedData
const result = this.stringToAuthenticatedData(authenticatedDataString)
return result
const usecase = new GetPayloadAuthenticatedDataDetachedUseCase(this.crypto)
return usecase.execute(encrypted)
}
/**
* 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
}
public generateEncryptedParameters(
payload: DecryptedPayloadInterface,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
signingKeyPair?: PkcKeyPair,
): EncryptedOutputParameters {
const usecase = new GenerateEncryptedParametersUseCase(this.crypto)
return usecase.execute(payload, key, signingKeyPair)
}
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,
public generateDecryptedParameters<C extends ItemContent = ItemContent>(
encrypted: EncryptedInputParameters,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
): DecryptedParameters<C> | ErrorDecryptingParameters {
const contentKeyComponents = this.deconstructEncryptedPayloadString(encrypted.enc_item_key)
const authenticatedData = this.stringToAuthenticatedData(contentKeyComponents.authenticatedData, {
u: encrypted.uuid,
v: encrypted.version,
})
const usecase = new GenerateDecryptedParametersUseCase(this.crypto)
return usecase.execute(encrypted, key)
}
const useAuthenticatedString = this.authenticatedDataToString(authenticatedData)
const contentKey = this.decryptString004(
contentKeyComponents.ciphertext,
key.itemsKey,
contentKeyComponents.nonce,
useAuthenticatedString,
)
public asymmetricEncrypt(dto: {
stringToEncrypt: Utf8String
senderKeyPair: PkcKeyPair
senderSigningKeyPair: PkcKeyPair
recipientPublicKey: HexString
}): AsymmetricallyEncryptedString {
const usecase = new AsymmetricEncryptUseCase(this.crypto)
return usecase.execute(dto)
}
if (!contentKey) {
console.error('Error decrypting itemKey parameters', encrypted)
return {
uuid: encrypted.uuid,
errorDecrypting: true,
}
}
asymmetricDecrypt(dto: {
stringToDecrypt: AsymmetricallyEncryptedString
recipientSecretKey: HexString
}): AsymmetricDecryptResult | null {
const usecase = new AsymmetricDecryptUseCase(this.crypto)
return usecase.execute(dto)
}
const contentComponents = this.deconstructEncryptedPayloadString(encrypted.content)
const content = this.decryptString004(
contentComponents.ciphertext,
contentKey,
contentComponents.nonce,
useAuthenticatedString,
)
asymmetricSignatureVerifyDetached(
encryptedString: AsymmetricallyEncryptedString,
): AsymmetricSignatureVerificationDetachedResult {
const usecase = new AsymmetricSignatureVerificationDetachedUseCase(this.crypto)
return usecase.execute({ encryptedString })
}
if (!content) {
return {
uuid: encrypted.uuid,
errorDecrypting: true,
}
} else {
return {
uuid: encrypted.uuid,
content: JSON.parse(content),
}
getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PublicKeySet {
const [_, __, ___, additionalDataString] = <V004AsymmetricStringComponents>string.split(':')
const parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto)
const additionalData = parseBase64Usecase.execute<AsymmetricItemAdditionalData>(additionalDataString)
return {
encryption: additionalData.senderPublicKey,
signing: additionalData.signingData.publicKey,
}
}
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(),
})
versionForAsymmetricallyEncryptedString(string: string): ProtocolVersion {
const [versionPrefix] = <V004AsymmetricStringComponents>string.split(':')
const version = versionPrefix.split('_')[0]
return version as ProtocolVersion
}
}

View File

@@ -0,0 +1,81 @@
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { getMockedCrypto } from '../../MockedCrypto'
import { AsymmetricDecryptUseCase } from './AsymmetricDecrypt'
import { AsymmetricEncryptUseCase } from './AsymmetricEncrypt'
import { V004AsymmetricStringComponents } from '../../V004AlgorithmTypes'
import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData'
describe('asymmetric decrypt use case', () => {
let crypto: PureCryptoInterface
let usecase: AsymmetricDecryptUseCase
let recipientKeyPair: PkcKeyPair
let senderKeyPair: PkcKeyPair
let senderSigningKeyPair: PkcKeyPair
beforeEach(() => {
crypto = getMockedCrypto()
usecase = new AsymmetricDecryptUseCase(crypto)
recipientKeyPair = crypto.sodiumCryptoBoxSeedKeypair('recipient-seedling')
senderKeyPair = crypto.sodiumCryptoBoxSeedKeypair('sender-seedling')
senderSigningKeyPair = crypto.sodiumCryptoSignSeedKeypair('sender-signing-seedling')
})
const getEncryptedString = () => {
const encryptUsecase = new AsymmetricEncryptUseCase(crypto)
const result = encryptUsecase.execute({
stringToEncrypt: 'foobar',
senderKeyPair: senderKeyPair,
senderSigningKeyPair: senderSigningKeyPair,
recipientPublicKey: recipientKeyPair.publicKey,
})
return result
}
it('should generate decrypted string', () => {
const encryptedString = getEncryptedString()
const decrypted = usecase.execute({
stringToDecrypt: encryptedString,
recipientSecretKey: recipientKeyPair.privateKey,
})
expect(decrypted).toEqual({
plaintext: 'foobar',
signatureVerified: true,
signaturePublicKey: senderSigningKeyPair.publicKey,
senderPublicKey: senderKeyPair.publicKey,
})
})
it('should fail signature verification if signature is changed', () => {
const encryptedString = getEncryptedString()
const [version, nonce, ciphertext] = <V004AsymmetricStringComponents>encryptedString.split(':')
const corruptAdditionalData: AsymmetricItemAdditionalData = {
signingData: {
publicKey: senderSigningKeyPair.publicKey,
signature: 'corrupt',
},
senderPublicKey: senderKeyPair.publicKey,
}
const corruptedAdditionalDataString = crypto.base64Encode(JSON.stringify(corruptAdditionalData))
const corruptEncryptedString = [version, nonce, ciphertext, corruptedAdditionalDataString].join(':')
const decrypted = usecase.execute({
stringToDecrypt: corruptEncryptedString,
recipientSecretKey: recipientKeyPair.privateKey,
})
expect(decrypted).toEqual({
plaintext: 'foobar',
signatureVerified: false,
signaturePublicKey: senderSigningKeyPair.publicKey,
senderPublicKey: senderKeyPair.publicKey,
})
})
})

View File

@@ -0,0 +1,48 @@
import { HexString, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { AsymmetricallyEncryptedString } from '../../../Types/Types'
import { V004AsymmetricStringComponents } from '../../V004AlgorithmTypes'
import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload'
import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData'
import { AsymmetricDecryptResult } from '../../../Types/AsymmetricDecryptResult'
export class AsymmetricDecryptUseCase {
private parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto)
constructor(private readonly crypto: PureCryptoInterface) {}
execute(dto: {
stringToDecrypt: AsymmetricallyEncryptedString
recipientSecretKey: HexString
}): AsymmetricDecryptResult | null {
const [_, nonce, ciphertext, additionalDataString] = <V004AsymmetricStringComponents>dto.stringToDecrypt.split(':')
const additionalData = this.parseBase64Usecase.execute<AsymmetricItemAdditionalData>(additionalDataString)
try {
const plaintext = this.crypto.sodiumCryptoBoxEasyDecrypt(
ciphertext,
nonce,
additionalData.senderPublicKey,
dto.recipientSecretKey,
)
if (!plaintext) {
return null
}
const signatureVerified = this.crypto.sodiumCryptoSignVerify(
ciphertext,
additionalData.signingData.signature,
additionalData.signingData.publicKey,
)
return {
plaintext,
signatureVerified,
signaturePublicKey: additionalData.signingData.publicKey,
senderPublicKey: additionalData.senderPublicKey,
}
} catch (error) {
return null
}
}
}

View File

@@ -0,0 +1,45 @@
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { getMockedCrypto } from '../../MockedCrypto'
import { AsymmetricEncryptUseCase } from './AsymmetricEncrypt'
import { V004AsymmetricStringComponents } from '../../V004AlgorithmTypes'
import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload'
import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData'
describe('asymmetric encrypt use case', () => {
let crypto: PureCryptoInterface
let usecase: AsymmetricEncryptUseCase
let encryptionKeyPair: PkcKeyPair
let signingKeyPair: PkcKeyPair
let parseBase64Usecase: ParseConsistentBase64JsonPayloadUseCase
beforeEach(() => {
crypto = getMockedCrypto()
usecase = new AsymmetricEncryptUseCase(crypto)
encryptionKeyPair = crypto.sodiumCryptoBoxSeedKeypair('seedling')
signingKeyPair = crypto.sodiumCryptoSignSeedKeypair('seedling')
parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(crypto)
})
it('should generate encrypted string', () => {
const recipientKeyPair = crypto.sodiumCryptoBoxSeedKeypair('recipient-seedling')
const result = usecase.execute({
stringToEncrypt: 'foobar',
senderKeyPair: encryptionKeyPair,
senderSigningKeyPair: signingKeyPair,
recipientPublicKey: recipientKeyPair.publicKey,
})
const [version, nonce, ciphertext, additionalDataString] = <V004AsymmetricStringComponents>result.split(':')
expect(version).toEqual('004_Asym')
expect(nonce).toEqual(expect.any(String))
expect(ciphertext).toEqual(expect.any(String))
expect(additionalDataString).toEqual(expect.any(String))
const additionalData = parseBase64Usecase.execute<AsymmetricItemAdditionalData>(additionalDataString)
expect(additionalData.signingData.publicKey).toEqual(signingKeyPair.publicKey)
expect(additionalData.signingData.signature).toEqual(expect.any(String))
expect(additionalData.senderPublicKey).toEqual(encryptionKeyPair.publicKey)
})
})

View File

@@ -0,0 +1,45 @@
import { HexString, PkcKeyPair, PureCryptoInterface, Utf8String } from '@standardnotes/sncrypto-common'
import { AsymmetricallyEncryptedString } from '../../../Types/Types'
import { V004Algorithm } from '../../../../Algorithm'
import { V004AsymmetricCiphertextPrefix, V004AsymmetricStringComponents } from '../../V004AlgorithmTypes'
import { CreateConsistentBase64JsonPayloadUseCase } from '../Utils/CreateConsistentBase64JsonPayload'
import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData'
export class AsymmetricEncryptUseCase {
private base64DataUsecase = new CreateConsistentBase64JsonPayloadUseCase(this.crypto)
constructor(private readonly crypto: PureCryptoInterface) {}
execute(dto: {
stringToEncrypt: Utf8String
senderKeyPair: PkcKeyPair
senderSigningKeyPair: PkcKeyPair
recipientPublicKey: HexString
}): AsymmetricallyEncryptedString {
const nonce = this.crypto.generateRandomKey(V004Algorithm.AsymmetricEncryptionNonceLength)
const ciphertext = this.crypto.sodiumCryptoBoxEasyEncrypt(
dto.stringToEncrypt,
nonce,
dto.senderKeyPair.privateKey,
dto.recipientPublicKey,
)
const additionalData: AsymmetricItemAdditionalData = {
signingData: {
publicKey: dto.senderSigningKeyPair.publicKey,
signature: this.crypto.sodiumCryptoSign(ciphertext, dto.senderSigningKeyPair.privateKey),
},
senderPublicKey: dto.senderKeyPair.publicKey,
}
const components: V004AsymmetricStringComponents = [
V004AsymmetricCiphertextPrefix,
nonce,
ciphertext,
this.base64DataUsecase.execute(additionalData),
]
return components.join(':')
}
}

View File

@@ -0,0 +1,36 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { AsymmetricallyEncryptedString } from '../../../Types/Types'
import { V004AsymmetricStringComponents } from '../../V004AlgorithmTypes'
import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload'
import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData'
import { AsymmetricSignatureVerificationDetachedResult } from '../../../Types/AsymmetricSignatureVerificationDetachedResult'
export class AsymmetricSignatureVerificationDetachedUseCase {
private parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto)
constructor(private readonly crypto: PureCryptoInterface) {}
execute(dto: { encryptedString: AsymmetricallyEncryptedString }): AsymmetricSignatureVerificationDetachedResult {
const [_, __, ciphertext, additionalDataString] = <V004AsymmetricStringComponents>dto.encryptedString.split(':')
const additionalData = this.parseBase64Usecase.execute<AsymmetricItemAdditionalData>(additionalDataString)
try {
const signatureVerified = this.crypto.sodiumCryptoSignVerify(
ciphertext,
additionalData.signingData.signature,
additionalData.signingData.publicKey,
)
return {
signatureVerified,
signaturePublicKey: additionalData.signingData.publicKey,
senderPublicKey: additionalData.senderPublicKey,
}
} catch (error) {
return {
signatureVerified: false,
}
}
}
}

View File

@@ -0,0 +1,28 @@
import {
ItemsKeyInterface,
KeySystemItemsKeyInterface,
KeySystemRootKeyInterface,
RootKeyInterface,
} from '@standardnotes/models'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { V004Algorithm } from '../../../../Algorithm'
import { HashingKey } from './HashingKey'
export class DeriveHashingKeyUseCase {
constructor(private readonly crypto: PureCryptoInterface) {}
execute(
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
): HashingKey {
const hashingKey = this.crypto.sodiumCryptoKdfDeriveFromKey(
key.itemsKey,
V004Algorithm.PayloadKeyHashingKeySubKeyNumber,
V004Algorithm.PayloadKeyHashingKeySubKeyBytes,
V004Algorithm.PayloadKeyHashingKeySubKeyContext,
)
return {
key: hashingKey,
}
}
}

View File

@@ -0,0 +1,10 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { HashingKey } from './HashingKey'
export class HashStringUseCase {
constructor(private readonly crypto: PureCryptoInterface) {}
execute(string: string, hashingKey: HashingKey): string {
return this.crypto.sodiumCryptoGenericHash(string, hashingKey.key)
}
}

View File

@@ -0,0 +1,3 @@
export interface HashingKey {
key: string
}

View File

@@ -0,0 +1,45 @@
import {
CreateDecryptedItemFromPayload,
DecryptedPayload,
DecryptedTransferPayload,
FillItemContentSpecialized,
KeySystemIdentifier,
KeySystemItemsKeyContentSpecialized,
KeySystemItemsKeyInterface,
PayloadTimestampDefaults,
} from '@standardnotes/models'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { V004Algorithm } from '../../../../Algorithm'
import { ContentType, ProtocolVersion } from '@standardnotes/common'
export class CreateKeySystemItemsKeyUseCase {
constructor(private readonly crypto: PureCryptoInterface) {}
execute(dto: {
uuid: string
keySystemIdentifier: KeySystemIdentifier
sharedVaultUuid: string | undefined
rootKeyToken: string
}): KeySystemItemsKeyInterface {
const key = this.crypto.generateRandomKey(V004Algorithm.EncryptionKeyLength)
const content = FillItemContentSpecialized<KeySystemItemsKeyContentSpecialized>({
itemsKey: key,
creationTimestamp: new Date().getTime(),
version: ProtocolVersion.V004,
rootKeyToken: dto.rootKeyToken,
})
const transferPayload: DecryptedTransferPayload = {
uuid: dto.uuid,
content_type: ContentType.KeySystemItemsKey,
key_system_identifier: dto.keySystemIdentifier,
shared_vault_uuid: dto.sharedVaultUuid,
content: content,
dirty: true,
...PayloadTimestampDefaults(),
}
const payload = new DecryptedPayload(transferPayload)
return CreateDecryptedItemFromPayload(payload)
}
}

View File

@@ -0,0 +1,35 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { V004Algorithm } from '../../../../Algorithm'
import {
KeySystemRootKeyInterface,
KeySystemRootKeyParamsInterface,
KeySystemRootKeyPasswordType,
} from '@standardnotes/models'
import { ProtocolVersion } from '@standardnotes/common'
import { DeriveKeySystemRootKeyUseCase } from './DeriveKeySystemRootKey'
export class CreateRandomKeySystemRootKey {
constructor(private readonly crypto: PureCryptoInterface) {}
execute(dto: { systemIdentifier: string }): KeySystemRootKeyInterface {
const version = ProtocolVersion.V004
const seed = this.crypto.generateRandomKey(V004Algorithm.ArgonSaltSeedLength)
const randomPassword = this.crypto.generateRandomKey(32)
const keyParams: KeySystemRootKeyParamsInterface = {
systemIdentifier: dto.systemIdentifier,
passwordType: KeySystemRootKeyPasswordType.Randomized,
creationTimestamp: new Date().getTime(),
seed,
version,
}
const usecase = new DeriveKeySystemRootKeyUseCase(this.crypto)
return usecase.execute({
password: randomPassword,
keyParams,
})
}
}

View File

@@ -0,0 +1,34 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { V004Algorithm } from '../../../../Algorithm'
import {
KeySystemIdentifier,
KeySystemRootKeyInterface,
KeySystemRootKeyParamsInterface,
KeySystemRootKeyPasswordType,
} from '@standardnotes/models'
import { ProtocolVersion } from '@standardnotes/common'
import { DeriveKeySystemRootKeyUseCase } from './DeriveKeySystemRootKey'
export class CreateUserInputKeySystemRootKey {
constructor(private readonly crypto: PureCryptoInterface) {}
execute(dto: { systemIdentifier: KeySystemIdentifier; userInputtedPassword: string }): KeySystemRootKeyInterface {
const version = ProtocolVersion.V004
const seed = this.crypto.generateRandomKey(V004Algorithm.ArgonSaltSeedLength)
const keyParams: KeySystemRootKeyParamsInterface = {
systemIdentifier: dto.systemIdentifier,
passwordType: KeySystemRootKeyPasswordType.UserInputted,
creationTimestamp: new Date().getTime(),
seed,
version,
}
const usecase = new DeriveKeySystemRootKeyUseCase(this.crypto)
return usecase.execute({
password: dto.userInputtedPassword,
keyParams,
})
}
}

View File

@@ -0,0 +1,60 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { UuidGenerator, splitString, truncateHexString } from '@standardnotes/utils'
import { V004PartitionCharacter } from '../../V004AlgorithmTypes'
import { V004Algorithm } from '../../../../Algorithm'
import {
DecryptedPayload,
FillItemContentSpecialized,
KeySystemRootKey,
KeySystemRootKeyContent,
KeySystemRootKeyContentSpecialized,
KeySystemRootKeyInterface,
PayloadTimestampDefaults,
KeySystemRootKeyParamsInterface,
} from '@standardnotes/models'
import { ContentType, ProtocolVersion } from '@standardnotes/common'
export class DeriveKeySystemRootKeyUseCase {
constructor(private readonly crypto: PureCryptoInterface) {}
execute(dto: { password: string; keyParams: KeySystemRootKeyParamsInterface }): KeySystemRootKeyInterface {
const seed = dto.keyParams.seed
const salt = this.generateSalt(dto.keyParams.systemIdentifier, seed)
const derivedKey = this.crypto.argon2(
dto.password,
salt,
V004Algorithm.ArgonIterations,
V004Algorithm.ArgonMemLimit,
V004Algorithm.ArgonOutputKeyBytes,
)
const partitions = splitString(derivedKey, 2)
const masterKey = partitions[0]
const token = partitions[1]
const uuid = UuidGenerator.GenerateUuid()
const content: KeySystemRootKeyContentSpecialized = {
systemIdentifier: dto.keyParams.systemIdentifier,
key: masterKey,
keyVersion: ProtocolVersion.V004,
keyParams: dto.keyParams,
token,
}
const payload = new DecryptedPayload<KeySystemRootKeyContent>({
uuid: uuid,
content_type: ContentType.KeySystemRootKey,
content: FillItemContentSpecialized(content),
...PayloadTimestampDefaults(),
})
return new KeySystemRootKey(payload)
}
private generateSalt(identifier: string, seed: string) {
const hash = this.crypto.sodiumCryptoGenericHash([identifier, seed].join(V004PartitionCharacter))
return truncateHexString(hash, V004Algorithm.ArgonSaltLength)
}
}

View File

@@ -0,0 +1,29 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { V004Algorithm } from '../../../../Algorithm'
import { RootKeyInterface } from '@standardnotes/models'
import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
import { DeriveRootKeyUseCase } from './DeriveRootKey'
import { Create004KeyParams } from '../../../../Keys/RootKey/KeyParamsFunctions'
export class CreateRootKeyUseCase {
constructor(private readonly crypto: PureCryptoInterface) {}
async execute<K extends RootKeyInterface>(
identifier: string,
password: string,
origination: KeyParamsOrigination,
): Promise<K> {
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()}`,
})
const usecase = new DeriveRootKeyUseCase(this.crypto)
return usecase.execute(password, keyParams)
}
}

View File

@@ -0,0 +1,66 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { splitString, truncateHexString } from '@standardnotes/utils'
import { V004PartitionCharacter } from '../../V004AlgorithmTypes'
import { V004Algorithm } from '../../../../Algorithm'
import { RootKeyInterface } from '@standardnotes/models'
import { SNRootKeyParams } from '../../../../Keys/RootKey/RootKeyParams'
import { CreateNewRootKey } from '../../../../Keys/RootKey/Functions'
import { ProtocolVersion } from '@standardnotes/common'
export class DeriveRootKeyUseCase {
constructor(private readonly crypto: PureCryptoInterface) {}
async execute<K extends RootKeyInterface>(password: string, keyParams: SNRootKeyParams): Promise<K> {
const seed = keyParams.content004.pw_nonce
const salt = await this.generateSalt(keyParams.content004.identifier, seed)
const derivedKey = this.crypto.argon2(
password,
salt,
V004Algorithm.ArgonIterations,
V004Algorithm.ArgonMemLimit,
V004Algorithm.ArgonOutputKeyBytes,
)
const partitions = splitString(derivedKey, 2)
const masterKey = partitions[0]
const serverPassword = partitions[1]
const encryptionKeyPairSeed = this.crypto.sodiumCryptoKdfDeriveFromKey(
masterKey,
V004Algorithm.MasterKeyEncryptionKeyPairSubKeyNumber,
V004Algorithm.MasterKeyEncryptionKeyPairSubKeyBytes,
V004Algorithm.MasterKeyEncryptionKeyPairSubKeyContext,
)
const encryptionKeyPair = this.crypto.sodiumCryptoBoxSeedKeypair(encryptionKeyPairSeed)
const signingKeyPairSeed = this.crypto.sodiumCryptoKdfDeriveFromKey(
masterKey,
V004Algorithm.MasterKeySigningKeyPairSubKeyNumber,
V004Algorithm.MasterKeySigningKeyPairSubKeyBytes,
V004Algorithm.MasterKeySigningKeyPairSubKeyContext,
)
const signingKeyPair = this.crypto.sodiumCryptoSignSeedKeypair(signingKeyPairSeed)
return CreateNewRootKey<K>({
masterKey,
serverPassword,
version: ProtocolVersion.V004,
keyParams: keyParams.getPortableValue(),
encryptionKeyPair,
signingKeyPair,
})
}
/**
* 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 generateSalt(identifier: string, seed: string) {
const hash = await this.crypto.sha256([identifier, seed].join(V004PartitionCharacter))
return truncateHexString(hash, V004Algorithm.ArgonSaltLength)
}
}

View File

@@ -0,0 +1,111 @@
import { CreateAnyKeyParams } from '../../../../Keys/RootKey/KeyParamsFunctions'
import { AnyKeyParamsContent, ContentType, ProtocolVersion } from '@standardnotes/common'
import { GenerateAuthenticatedDataUseCase } from './GenerateAuthenticatedData'
import {
DecryptedPayloadInterface,
ItemsKeyInterface,
KeySystemRootKeyInterface,
RootKeyInterface,
} from '@standardnotes/models'
import { KeySystemItemsKey } from '../../../../Keys/KeySystemItemsKey/KeySystemItemsKey'
describe('generate authenticated data use case', () => {
let usecase: GenerateAuthenticatedDataUseCase
beforeEach(() => {
usecase = new GenerateAuthenticatedDataUseCase()
})
it('should include key params if payload being encrypted is an items key', () => {
const payload = {
uuid: '123',
content_type: ContentType.ItemsKey,
} as jest.Mocked<DecryptedPayloadInterface>
const keyParams = CreateAnyKeyParams({
identifier: 'key-params-123',
} as jest.Mocked<AnyKeyParamsContent>)
const rootKey = {
keyParams,
} as jest.Mocked<RootKeyInterface>
const authenticatedData = usecase.execute(payload, rootKey)
expect(authenticatedData).toEqual({
u: payload.uuid,
v: ProtocolVersion.V004,
kp: keyParams.content,
})
})
it('should include root key params if payload is a key system items key', () => {
const payload = {
uuid: '123',
content_type: ContentType.KeySystemItemsKey,
shared_vault_uuid: 'shared-vault-uuid-123',
key_system_identifier: 'key-system-identifier-123',
} as jest.Mocked<DecryptedPayloadInterface>
const keySystemRootKey = {
keyVersion: ProtocolVersion.V004,
keyParams: {
seed: 'seed-123',
},
content_type: ContentType.KeySystemRootKey,
token: '123',
} as jest.Mocked<KeySystemRootKeyInterface>
const authenticatedData = usecase.execute(payload, keySystemRootKey)
expect(authenticatedData).toEqual({
u: payload.uuid,
v: ProtocolVersion.V004,
kp: keySystemRootKey.keyParams,
ksi: payload.key_system_identifier,
svu: payload.shared_vault_uuid,
})
})
it('should include key system identifier and shared vault uuid', () => {
const payload = {
uuid: '123',
content_type: ContentType.Note,
shared_vault_uuid: 'shared-vault-uuid-123',
key_system_identifier: 'key-system-identifier-123',
} as jest.Mocked<DecryptedPayloadInterface>
const itemsKey = {
creationTimestamp: 123,
keyVersion: ProtocolVersion.V004,
content_type: ContentType.KeySystemItemsKey,
} as jest.Mocked<KeySystemItemsKey>
const authenticatedData = usecase.execute(payload, itemsKey)
expect(authenticatedData).toEqual({
u: payload.uuid,
v: ProtocolVersion.V004,
ksi: payload.key_system_identifier,
svu: payload.shared_vault_uuid,
})
})
it('should include only uuid and version if non-keysystem item with items key', () => {
const payload = {
uuid: '123',
content_type: ContentType.Note,
} as jest.Mocked<DecryptedPayloadInterface>
const itemsKey = {
content_type: ContentType.ItemsKey,
} as jest.Mocked<ItemsKeyInterface>
const authenticatedData = usecase.execute(payload, itemsKey)
expect(authenticatedData).toEqual({
u: payload.uuid,
v: ProtocolVersion.V004,
})
})
})

View File

@@ -0,0 +1,58 @@
import {
DecryptedPayloadInterface,
ItemsKeyInterface,
KeySystemItemsKeyInterface,
KeySystemRootKeyInterface,
RootKeyInterface,
isKeySystemRootKey,
ContentTypeUsesRootKeyEncryption,
ContentTypeUsesKeySystemRootKeyEncryption,
} from '@standardnotes/models'
import { ItemAuthenticatedData } from '../../../../Types/ItemAuthenticatedData'
import { RootKeyEncryptedAuthenticatedData } from '../../../../Types/RootKeyEncryptedAuthenticatedData'
import { KeySystemItemsKeyAuthenticatedData } from '../../../../Types/KeySystemItemsKeyAuthenticatedData'
import { ProtocolVersion } from '@standardnotes/common'
import { isItemsKey } from '../../../../Keys/ItemsKey/ItemsKey'
import { isKeySystemItemsKey } from '../../../../Keys/KeySystemItemsKey/KeySystemItemsKey'
export class GenerateAuthenticatedDataUseCase {
execute(
payload: DecryptedPayloadInterface,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
): ItemAuthenticatedData | RootKeyEncryptedAuthenticatedData | KeySystemItemsKeyAuthenticatedData {
const baseData: ItemAuthenticatedData = {
u: payload.uuid,
v: ProtocolVersion.V004,
}
if (payload.key_system_identifier) {
baseData.ksi = payload.key_system_identifier
}
if (payload.shared_vault_uuid) {
baseData.svu = payload.shared_vault_uuid
}
if (ContentTypeUsesRootKeyEncryption(payload.content_type)) {
return {
...baseData,
kp: (key as RootKeyInterface).keyParams.content,
}
} else if (ContentTypeUsesKeySystemRootKeyEncryption(payload.content_type)) {
if (!isKeySystemRootKey(key)) {
throw Error(
`Attempting to use non-key system root key ${key.content_type} for item content type ${payload.content_type}`,
)
}
return {
...baseData,
kp: key.keyParams,
}
} else {
if (!isItemsKey(key) && !isKeySystemItemsKey(key)) {
throw Error('Attempting to use non-items key for regular item.')
}
return baseData
}
}
}

View File

@@ -0,0 +1,80 @@
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { getMockedCrypto } from '../../MockedCrypto'
import { GenerateDecryptedParametersUseCase } from './GenerateDecryptedParameters'
import { ContentType } from '@standardnotes/common'
import { DecryptedPayloadInterface, ItemsKeyInterface } from '@standardnotes/models'
import { GenerateEncryptedParametersUseCase } from './GenerateEncryptedParameters'
import { EncryptedInputParameters, EncryptedOutputParameters } from '../../../../Types/EncryptedParameters'
describe('generate decrypted parameters usecase', () => {
let crypto: PureCryptoInterface
let usecase: GenerateDecryptedParametersUseCase
let signingKeyPair: PkcKeyPair
let itemsKey: ItemsKeyInterface
beforeEach(() => {
crypto = getMockedCrypto()
usecase = new GenerateDecryptedParametersUseCase(crypto)
itemsKey = {
uuid: 'items-key-id',
itemsKey: 'items-key',
content_type: ContentType.ItemsKey,
} as jest.Mocked<ItemsKeyInterface>
})
const generateEncryptedParameters = <T extends EncryptedOutputParameters>(plaintext: string) => {
const decrypted = {
uuid: '123',
content: {
text: plaintext,
},
content_type: ContentType.Note,
} as unknown as jest.Mocked<DecryptedPayloadInterface>
const encryptedParametersUsecase = new GenerateEncryptedParametersUseCase(crypto)
return encryptedParametersUsecase.execute(decrypted, itemsKey, signingKeyPair) as T
}
describe('without signatures', () => {
it('should generate decrypted parameters', () => {
const encrypted = generateEncryptedParameters<EncryptedInputParameters>('foo')
const result = usecase.execute(encrypted, itemsKey)
expect(result).toEqual({
uuid: expect.any(String),
content: expect.any(Object),
signatureData: {
required: false,
contentHash: expect.any(String),
},
})
})
})
describe('with signatures', () => {
beforeEach(() => {
signingKeyPair = crypto.sodiumCryptoSignSeedKeypair('seedling')
})
it('should generate decrypted parameters', () => {
const encrypted = generateEncryptedParameters<EncryptedInputParameters>('foo')
const result = usecase.execute(encrypted, itemsKey)
expect(result).toEqual({
uuid: expect.any(String),
content: expect.any(Object),
signatureData: {
required: false,
contentHash: expect.any(String),
result: {
passes: true,
publicKey: signingKeyPair.publicKey,
signature: expect.any(String),
},
},
})
})
})
})

View File

@@ -0,0 +1,140 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { deconstructEncryptedPayloadString } from '../../V004AlgorithmHelpers'
import {
ItemContent,
ItemsKeyInterface,
KeySystemItemsKeyInterface,
KeySystemRootKeyInterface,
RootKeyInterface,
} from '@standardnotes/models'
import { StringToAuthenticatedDataUseCase } from '../Utils/StringToAuthenticatedData'
import { CreateConsistentBase64JsonPayloadUseCase } from '../Utils/CreateConsistentBase64JsonPayload'
import { GenerateSymmetricPayloadSignatureResultUseCase } from './GenerateSymmetricPayloadSignatureResult'
import {
EncryptedInputParameters,
EncryptedOutputParameters,
ErrorDecryptingParameters,
} from './../../../../Types/EncryptedParameters'
import { DecryptedParameters } from '../../../../Types/DecryptedParameters'
import { DeriveHashingKeyUseCase } from '../Hash/DeriveHashingKey'
export class GenerateDecryptedParametersUseCase {
private base64DataUsecase = new CreateConsistentBase64JsonPayloadUseCase(this.crypto)
private stringToAuthenticatedDataUseCase = new StringToAuthenticatedDataUseCase(this.crypto)
private signingVerificationUseCase = new GenerateSymmetricPayloadSignatureResultUseCase(this.crypto)
private deriveHashingKeyUseCase = new DeriveHashingKeyUseCase(this.crypto)
constructor(private readonly crypto: PureCryptoInterface) {}
execute<C extends ItemContent = ItemContent>(
encrypted: EncryptedInputParameters,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
): DecryptedParameters<C> | ErrorDecryptingParameters {
const contentKeyResult = this.decryptContentKey(encrypted, key)
if (!contentKeyResult) {
console.error('Error decrypting contentKey from parameters', encrypted)
return {
uuid: encrypted.uuid,
errorDecrypting: true,
}
}
const contentResult = this.decryptContent(encrypted, contentKeyResult.contentKey)
if (!contentResult) {
return {
uuid: encrypted.uuid,
errorDecrypting: true,
}
}
const hashingKey = this.deriveHashingKeyUseCase.execute(key)
const signatureVerificationResult = this.signingVerificationUseCase.execute(
encrypted,
hashingKey,
{
additionalData: contentKeyResult.components.additionalData,
plaintext: contentKeyResult.contentKey,
},
{
additionalData: contentResult.components.additionalData,
plaintext: contentResult.content,
},
)
return {
uuid: encrypted.uuid,
content: JSON.parse(contentResult.content),
signatureData: signatureVerificationResult,
}
}
private decryptContent(encrypted: EncryptedOutputParameters, contentKey: string) {
const contentComponents = deconstructEncryptedPayloadString(encrypted.content)
const contentAuthenticatedData = this.stringToAuthenticatedDataUseCase.execute(
contentComponents.authenticatedData,
{
u: encrypted.uuid,
v: encrypted.version,
ksi: encrypted.key_system_identifier,
svu: encrypted.shared_vault_uuid,
},
)
const authenticatedDataString = this.base64DataUsecase.execute(contentAuthenticatedData)
const content = this.crypto.xchacha20Decrypt(
contentComponents.ciphertext,
contentComponents.nonce,
contentKey,
authenticatedDataString,
)
if (!content) {
return null
}
return {
content,
components: contentComponents,
authenticatedDataString,
}
}
private decryptContentKey(
encrypted: EncryptedOutputParameters,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
) {
const contentKeyComponents = deconstructEncryptedPayloadString(encrypted.enc_item_key)
const contentKeyAuthenticatedData = this.stringToAuthenticatedDataUseCase.execute(
contentKeyComponents.authenticatedData,
{
u: encrypted.uuid,
v: encrypted.version,
ksi: encrypted.key_system_identifier,
svu: encrypted.shared_vault_uuid,
},
)
const authenticatedDataString = this.base64DataUsecase.execute(contentKeyAuthenticatedData)
const contentKey = this.crypto.xchacha20Decrypt(
contentKeyComponents.ciphertext,
contentKeyComponents.nonce,
key.itemsKey,
authenticatedDataString,
)
if (!contentKey) {
return null
}
return {
contentKey,
components: contentKeyComponents,
authenticatedDataString,
}
}
}

View File

@@ -0,0 +1,137 @@
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { getMockedCrypto } from '../../MockedCrypto'
import { AnyKeyParamsContent, ContentType, ProtocolVersion } from '@standardnotes/common'
import { GenerateEncryptedParametersUseCase } from './GenerateEncryptedParameters'
import {
DecryptedPayloadInterface,
ItemsKeyInterface,
KeySystemRootKeyInterface,
RootKeyInterface,
} from '@standardnotes/models'
import { deconstructEncryptedPayloadString } from '../../V004AlgorithmHelpers'
import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload'
import { SymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData'
describe('generate encrypted parameters usecase', () => {
let crypto: PureCryptoInterface
let usecase: GenerateEncryptedParametersUseCase
beforeEach(() => {
crypto = getMockedCrypto()
usecase = new GenerateEncryptedParametersUseCase(crypto)
})
describe('without signing keypair', () => {
it('should generate encrypted parameters', () => {
const decrypted = {
uuid: '123',
content: {
title: 'title',
text: 'text',
},
content_type: ContentType.Note,
} as unknown as jest.Mocked<DecryptedPayloadInterface>
const itemsKey = {
uuid: 'items-key-id',
itemsKey: 'items-key',
content_type: ContentType.ItemsKey,
} as jest.Mocked<ItemsKeyInterface>
const result = usecase.execute(decrypted, itemsKey)
expect(result).toEqual({
uuid: '123',
content_type: ContentType.Note,
items_key_id: 'items-key-id',
content: expect.any(String),
enc_item_key: expect.any(String),
version: ProtocolVersion.V004,
rawSigningDataClientOnly: undefined,
})
})
it('should not include items_key_id if item to encrypt is items key payload', () => {
const decrypted = {
uuid: '123',
content: {
foo: 'bar',
},
content_type: ContentType.ItemsKey,
} as unknown as jest.Mocked<DecryptedPayloadInterface>
const rootKey = {
uuid: 'items-key-id',
itemsKey: 'items-key',
keyParams: {
content: {} as jest.Mocked<AnyKeyParamsContent>,
},
content_type: ContentType.RootKey,
} as jest.Mocked<RootKeyInterface>
const result = usecase.execute(decrypted, rootKey)
expect(result.items_key_id).toBeUndefined()
})
it('should not include items_key_id if item to encrypt is key system items key payload', () => {
const decrypted = {
uuid: '123',
content: {
foo: 'bar',
},
content_type: ContentType.KeySystemItemsKey,
} as unknown as jest.Mocked<DecryptedPayloadInterface>
const rootKey = {
uuid: 'items-key-id',
itemsKey: 'items-key',
content_type: ContentType.KeySystemRootKey,
} as jest.Mocked<KeySystemRootKeyInterface>
const result = usecase.execute(decrypted, rootKey)
expect(result.items_key_id).toBeUndefined()
})
})
describe('with signing keypair', () => {
let signingKeyPair: PkcKeyPair
let parseBase64Usecase: ParseConsistentBase64JsonPayloadUseCase
beforeEach(() => {
signingKeyPair = crypto.sodiumCryptoSignSeedKeypair('seedling')
parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(crypto)
})
it('encrypted string should include additional data', () => {
const decrypted = {
uuid: '123',
content: {
title: 'title',
text: 'text',
},
content_type: ContentType.Note,
} as unknown as jest.Mocked<DecryptedPayloadInterface>
const itemsKey = {
uuid: 'items-key-id',
itemsKey: 'items-key',
content_type: ContentType.ItemsKey,
} as jest.Mocked<ItemsKeyInterface>
const result = usecase.execute(decrypted, itemsKey, signingKeyPair)
const contentComponents = deconstructEncryptedPayloadString(result.content)
const additionalData = parseBase64Usecase.execute<SymmetricItemAdditionalData>(contentComponents.additionalData)
expect(additionalData).toEqual({
signingData: {
signature: expect.any(String),
publicKey: signingKeyPair.publicKey,
},
})
})
})
})

View File

@@ -0,0 +1,120 @@
import { ProtocolVersion } from '@standardnotes/common'
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import {
DecryptedPayloadInterface,
ItemsKeyInterface,
KeySystemItemsKeyInterface,
KeySystemRootKeyInterface,
RootKeyInterface,
} from '@standardnotes/models'
import { CreateConsistentBase64JsonPayloadUseCase } from '../Utils/CreateConsistentBase64JsonPayload'
import { doesPayloadRequireSigning } from '../../V004AlgorithmHelpers'
import { EncryptedOutputParameters } from '../../../../Types/EncryptedParameters'
import { GenerateAuthenticatedDataUseCase } from './GenerateAuthenticatedData'
import { GenerateEncryptedProtocolStringUseCase } from './GenerateEncryptedProtocolString'
import { GenerateSymmetricAdditionalDataUseCase } from './GenerateSymmetricAdditionalData'
import { isItemsKey } from '../../../../Keys/ItemsKey/ItemsKey'
import { isKeySystemItemsKey } from '../../../../Keys/KeySystemItemsKey/KeySystemItemsKey'
import { ItemAuthenticatedData } from '../../../../Types/ItemAuthenticatedData'
import { V004Algorithm } from '../../../../Algorithm'
import { AdditionalData } from '../../../../Types/EncryptionAdditionalData'
import { HashingKey } from '../Hash/HashingKey'
import { DeriveHashingKeyUseCase } from '../Hash/DeriveHashingKey'
export class GenerateEncryptedParametersUseCase {
private generateProtocolStringUseCase = new GenerateEncryptedProtocolStringUseCase(this.crypto)
private generateAuthenticatedDataUseCase = new GenerateAuthenticatedDataUseCase()
private generateAdditionalDataUseCase = new GenerateSymmetricAdditionalDataUseCase(this.crypto)
private encodeBase64DataUsecase = new CreateConsistentBase64JsonPayloadUseCase(this.crypto)
private deriveHashingKeyUseCase = new DeriveHashingKeyUseCase(this.crypto)
constructor(private readonly crypto: PureCryptoInterface) {}
execute(
payload: DecryptedPayloadInterface,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
signingKeyPair?: PkcKeyPair,
): EncryptedOutputParameters {
if (doesPayloadRequireSigning(payload) && !signingKeyPair) {
throw Error('Payload requires signing but no signing key pair was provided.')
}
const commonAuthenticatedData = this.generateAuthenticatedDataUseCase.execute(payload, key)
const hashingKey = this.deriveHashingKeyUseCase.execute(key)
const { contentKey, encryptedContentKey } = this.generateEncryptedContentKey(
key,
hashingKey,
commonAuthenticatedData,
signingKeyPair,
)
const { encryptedContent } = this.generateEncryptedContent(
payload,
hashingKey,
contentKey,
commonAuthenticatedData,
signingKeyPair,
)
return {
uuid: payload.uuid,
content_type: payload.content_type,
items_key_id: isItemsKey(key) || isKeySystemItemsKey(key) ? key.uuid : undefined,
content: encryptedContent,
enc_item_key: encryptedContentKey,
version: ProtocolVersion.V004,
key_system_identifier: payload.key_system_identifier,
shared_vault_uuid: payload.shared_vault_uuid,
}
}
private generateEncryptedContent(
payload: DecryptedPayloadInterface,
hashingKey: HashingKey,
contentKey: string,
commonAuthenticatedData: ItemAuthenticatedData,
signingKeyPair?: PkcKeyPair,
): {
encryptedContent: string
} {
const content = JSON.stringify(payload.content)
const { additionalData } = this.generateAdditionalDataUseCase.execute(content, hashingKey, signingKeyPair)
const encryptedContent = this.generateProtocolStringUseCase.execute(
content,
contentKey,
this.encodeBase64DataUsecase.execute<ItemAuthenticatedData>(commonAuthenticatedData),
this.encodeBase64DataUsecase.execute<AdditionalData>(additionalData),
)
return {
encryptedContent,
}
}
private generateEncryptedContentKey(
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
hashingKey: HashingKey,
commonAuthenticatedData: ItemAuthenticatedData,
signingKeyPair?: PkcKeyPair,
) {
const contentKey = this.crypto.generateRandomKey(V004Algorithm.EncryptionKeyLength)
const { additionalData } = this.generateAdditionalDataUseCase.execute(contentKey, hashingKey, signingKeyPair)
const encryptedContentKey = this.generateProtocolStringUseCase.execute(
contentKey,
key.itemsKey,
this.encodeBase64DataUsecase.execute<ItemAuthenticatedData>(commonAuthenticatedData),
this.encodeBase64DataUsecase.execute<AdditionalData>(additionalData),
)
return {
contentKey,
encryptedContentKey,
}
}
}

View File

@@ -0,0 +1,44 @@
import { ProtocolVersion } from '@standardnotes/common'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { ItemAuthenticatedData } from './../../../../Types/ItemAuthenticatedData'
import { GenerateEncryptedProtocolStringUseCase } from './GenerateEncryptedProtocolString'
import { AdditionalData } from '../../../../Types/EncryptionAdditionalData'
import { getMockedCrypto } from '../../MockedCrypto'
describe('generate encrypted protocol string', () => {
let crypto: PureCryptoInterface
let usecase: GenerateEncryptedProtocolStringUseCase
beforeEach(() => {
crypto = getMockedCrypto()
usecase = new GenerateEncryptedProtocolStringUseCase(crypto)
})
it('should generate encrypted protocol string', () => {
const aad: ItemAuthenticatedData = {
u: '123',
v: ProtocolVersion.V004,
}
const signingData: AdditionalData = {}
const nonce = 'noncy'
crypto.generateRandomKey = jest.fn().mockReturnValue(nonce)
const plaintext = 'foo'
const result = usecase.execute(
plaintext,
'secret',
crypto.base64Encode(JSON.stringify(aad)),
crypto.base64Encode(JSON.stringify(signingData)),
)
expect(result).toEqual(
`004:${nonce}:<e>${plaintext}<e>:${crypto.base64Encode(JSON.stringify(aad))}:${crypto.base64Encode(
JSON.stringify(signingData),
)}`,
)
})
})

View File

@@ -0,0 +1,41 @@
import { Base64String, HexString, PureCryptoInterface, Utf8String } from '@standardnotes/sncrypto-common'
import { V004PartitionCharacter, V004StringComponents } from '../../V004AlgorithmTypes'
import { ProtocolVersion } from '@standardnotes/common'
import { V004Algorithm } from '../../../../Algorithm'
export class GenerateEncryptedProtocolStringUseCase {
constructor(private readonly crypto: PureCryptoInterface) {}
execute(plaintext: string, rawKey: string, authenticatedData: string, additionalData: string): string {
const nonce = this.crypto.generateRandomKey(V004Algorithm.EncryptionNonceLength)
const ciphertext = this.encryptString(plaintext, rawKey, nonce, authenticatedData)
const components: V004StringComponents = [
ProtocolVersion.V004 as string,
nonce,
ciphertext,
authenticatedData,
additionalData,
]
return components.join(V004PartitionCharacter)
}
encryptString(
plaintext: Utf8String,
rawKey: HexString,
nonce: HexString,
authenticatedData: Utf8String,
): Base64String {
if (!nonce) {
throw 'encryptString null nonce'
}
if (!rawKey) {
throw 'encryptString null rawKey'
}
return this.crypto.xchacha20Encrypt(plaintext, nonce, rawKey, authenticatedData)
}
}

View File

@@ -0,0 +1,43 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { getMockedCrypto } from '../../MockedCrypto'
import { GenerateSymmetricAdditionalDataUseCase } from './GenerateSymmetricAdditionalData'
import { HashingKey } from '../Hash/HashingKey'
describe('generate symmetric additional data usecase', () => {
let crypto: PureCryptoInterface
let usecase: GenerateSymmetricAdditionalDataUseCase
beforeEach(() => {
crypto = getMockedCrypto()
usecase = new GenerateSymmetricAdditionalDataUseCase(crypto)
})
it('should generate signing data with signing keypair', () => {
const payloadPlaintext = 'foo'
const hashingKey: HashingKey = { key: 'secret-123' }
const signingKeyPair = crypto.sodiumCryptoSignSeedKeypair('seedling')
const { additionalData, plaintextHash } = usecase.execute(payloadPlaintext, hashingKey, signingKeyPair)
expect(additionalData).toEqual({
signingData: {
publicKey: signingKeyPair.publicKey,
signature: crypto.sodiumCryptoSign(plaintextHash, signingKeyPair.privateKey),
},
})
expect(plaintextHash).toEqual(crypto.sodiumCryptoGenericHash(payloadPlaintext, hashingKey.key))
})
it('should generate empty signing data without signing keypair', () => {
const payloadPlaintext = 'foo'
const hashingKey: HashingKey = { key: 'secret-123' }
const { additionalData, plaintextHash } = usecase.execute(payloadPlaintext, hashingKey, undefined)
expect(additionalData).toEqual({})
expect(plaintextHash).toEqual(crypto.sodiumCryptoGenericHash(payloadPlaintext, hashingKey.key))
})
})

View File

@@ -0,0 +1,37 @@
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { AdditionalData } from '../../../../Types/EncryptionAdditionalData'
import { HashStringUseCase } from '../Hash/HashString'
import { HashingKey } from '../Hash/HashingKey'
export class GenerateSymmetricAdditionalDataUseCase {
private hashUseCase = new HashStringUseCase(this.crypto)
constructor(private readonly crypto: PureCryptoInterface) {}
execute(
payloadPlaintext: string,
hashingKey: HashingKey,
signingKeyPair?: PkcKeyPair,
): { additionalData: AdditionalData; plaintextHash: string } {
const plaintextHash = this.hashUseCase.execute(payloadPlaintext, hashingKey)
if (!signingKeyPair) {
return {
additionalData: {},
plaintextHash,
}
}
const signature = this.crypto.sodiumCryptoSign(plaintextHash, signingKeyPair.privateKey)
return {
additionalData: {
signingData: {
publicKey: signingKeyPair.publicKey,
signature,
},
},
plaintextHash,
}
}
}

View File

@@ -0,0 +1,303 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { getMockedCrypto } from '../../MockedCrypto'
import { EncryptedInputParameters, EncryptedOutputParameters } from '../../../../Types/EncryptedParameters'
import { GenerateSymmetricPayloadSignatureResultUseCase } from './GenerateSymmetricPayloadSignatureResult'
import { GenerateSymmetricAdditionalDataUseCase } from './GenerateSymmetricAdditionalData'
import { CreateConsistentBase64JsonPayloadUseCase } from '../Utils/CreateConsistentBase64JsonPayload'
import { doesPayloadRequireSigning } from '../../V004AlgorithmHelpers'
import { PersistentSignatureData } from '@standardnotes/models'
import { HashStringUseCase } from '../Hash/HashString'
import { HashingKey } from '../Hash/HashingKey'
describe('generate symmetric signing data usecase', () => {
let crypto: PureCryptoInterface
let usecase: GenerateSymmetricPayloadSignatureResultUseCase
let hashUsecase: HashStringUseCase
let additionalDataUseCase: GenerateSymmetricAdditionalDataUseCase
let encodeUseCase: CreateConsistentBase64JsonPayloadUseCase
beforeEach(() => {
crypto = getMockedCrypto()
usecase = new GenerateSymmetricPayloadSignatureResultUseCase(crypto)
hashUsecase = new HashStringUseCase(crypto)
additionalDataUseCase = new GenerateSymmetricAdditionalDataUseCase(crypto)
encodeUseCase = new CreateConsistentBase64JsonPayloadUseCase(crypto)
})
it('payload with shared vault uuid should require signature', () => {
const payload: Partial<EncryptedOutputParameters> = {
shared_vault_uuid: '456',
}
expect(doesPayloadRequireSigning(payload)).toBe(true)
})
it('payload with key system identifier only should not require signature', () => {
const payload: Partial<EncryptedOutputParameters> = {
key_system_identifier: '123',
}
expect(doesPayloadRequireSigning(payload)).toBe(false)
})
it('payload without key system identifier or shared vault uuid should not require signature', () => {
const payload: Partial<EncryptedOutputParameters> = {
key_system_identifier: undefined,
shared_vault_uuid: undefined,
}
expect(doesPayloadRequireSigning(payload)).toBe(false)
})
it('signature should be verified with correct parameters', () => {
const payload = {
key_system_identifier: '123',
shared_vault_uuid: '456',
} as jest.Mocked<EncryptedInputParameters>
const hashingKey: HashingKey = { key: 'secret-123' }
const content = 'contentplaintext'
const contentKey = 'contentkeysecret'
const keypair = crypto.sodiumCryptoSignSeedKeypair('seedling')
const contentAdditionalDataResultResult = additionalDataUseCase.execute(content, hashingKey, keypair)
const contentKeyAdditionalDataResultResult = additionalDataUseCase.execute(contentKey, hashingKey, keypair)
const result = usecase.execute(
payload,
hashingKey,
{
additionalData: encodeUseCase.execute(contentKeyAdditionalDataResultResult.additionalData),
plaintext: contentKey,
},
{
additionalData: encodeUseCase.execute(contentAdditionalDataResultResult.additionalData),
plaintext: content,
},
)
expect(result).toEqual({
required: true,
contentHash: expect.any(String),
result: {
passes: true,
publicKey: keypair.publicKey,
signature: expect.any(String),
},
})
})
it('should return required false with no result if no signing data is provided and signing is not required', () => {
const payloadWithOptionalSigning = {
key_system_identifier: undefined,
shared_vault_uuid: undefined,
} as jest.Mocked<EncryptedInputParameters>
const hashingKey: HashingKey = { key: 'secret-123' }
const content = 'contentplaintext'
const contentKey = 'contentkeysecret'
const contentAdditionalDataResult = additionalDataUseCase.execute(content, hashingKey, undefined)
const contentKeyAdditionalDataResult = additionalDataUseCase.execute(contentKey, hashingKey, undefined)
const result = usecase.execute(
payloadWithOptionalSigning,
hashingKey,
{
additionalData: encodeUseCase.execute(contentKeyAdditionalDataResult.additionalData),
plaintext: contentKey,
},
{
additionalData: encodeUseCase.execute(contentAdditionalDataResult.additionalData),
plaintext: content,
},
)
expect(result).toEqual({
required: false,
contentHash: expect.any(String),
})
})
it('should return required true with fail result if no signing data is provided and signing is required', () => {
const payloadWithRequiredSigning = {
key_system_identifier: '123',
shared_vault_uuid: '456',
} as jest.Mocked<EncryptedInputParameters>
const hashingKey: HashingKey = { key: 'secret-123' }
const content = 'contentplaintext'
const contentKey = 'contentkeysecret'
const contentAdditionalDataResult = additionalDataUseCase.execute(content, hashingKey, undefined)
const contentKeyAdditionalDataResult = additionalDataUseCase.execute(contentKey, hashingKey, undefined)
const result = usecase.execute(
payloadWithRequiredSigning,
hashingKey,
{
additionalData: encodeUseCase.execute(contentKeyAdditionalDataResult.additionalData),
plaintext: contentKey,
},
{
additionalData: encodeUseCase.execute(contentAdditionalDataResult.additionalData),
plaintext: content,
},
)
expect(result).toEqual({
required: true,
contentHash: expect.any(String),
result: {
passes: false,
publicKey: '',
signature: '',
},
})
})
it('should fail if content public key differs from contentKey public key', () => {
const payload = {
key_system_identifier: '123',
shared_vault_uuid: '456',
} as jest.Mocked<EncryptedInputParameters>
const hashingKey: HashingKey = { key: 'secret-123' }
const content = 'contentplaintext'
const contentKey = 'contentkeysecret'
const contentKeyPair = crypto.sodiumCryptoSignSeedKeypair('contentseed')
const contentKeyKeyPair = crypto.sodiumCryptoSignSeedKeypair('contentkeyseed')
const contentAdditionalDataResult = additionalDataUseCase.execute(content, hashingKey, contentKeyPair)
const contentKeyAdditionalDataResult = additionalDataUseCase.execute(contentKey, hashingKey, contentKeyKeyPair)
const result = usecase.execute(
payload,
hashingKey,
{
additionalData: encodeUseCase.execute(contentKeyAdditionalDataResult.additionalData),
plaintext: contentKey,
},
{
additionalData: encodeUseCase.execute(contentAdditionalDataResult.additionalData),
plaintext: content,
},
)
expect(result).toEqual({
required: true,
contentHash: expect.any(String),
result: {
passes: false,
publicKey: '',
signature: '',
},
})
})
it('if content hash has not changed and previous failing signature is supplied, new result should also be failing', () => {
const hashingKey: HashingKey = { key: 'secret-123' }
const content = 'contentplaintext'
const contentKey = 'contentkeysecret'
const contentHash = hashUsecase.execute(content, hashingKey)
const previousResult: PersistentSignatureData = {
required: true,
contentHash: contentHash,
result: {
passes: false,
publicKey: '',
signature: '',
},
}
const payload = {
key_system_identifier: '123',
shared_vault_uuid: '456',
signatureData: previousResult,
} as jest.Mocked<EncryptedInputParameters>
const keypair = crypto.sodiumCryptoSignSeedKeypair('seedling')
const contentAdditionalDataResultResult = additionalDataUseCase.execute(content, hashingKey, keypair)
const contentKeyAdditionalDataResultResult = additionalDataUseCase.execute(contentKey, hashingKey, keypair)
const result = usecase.execute(
payload,
hashingKey,
{
additionalData: encodeUseCase.execute(contentKeyAdditionalDataResultResult.additionalData),
plaintext: contentKey,
},
{
additionalData: encodeUseCase.execute(contentAdditionalDataResultResult.additionalData),
plaintext: content,
},
)
expect(result).toEqual({
required: true,
contentHash: contentHash,
result: {
passes: false,
publicKey: keypair.publicKey,
signature: expect.any(String),
},
})
})
it('previous failing signature should be ignored if content hash has changed', () => {
const hashingKey: HashingKey = { key: 'secret-123' }
const content = 'contentplaintext'
const contentKey = 'contentkeysecret'
const previousResult: PersistentSignatureData = {
required: true,
contentHash: 'different hash',
result: {
passes: false,
publicKey: '',
signature: '',
},
}
const payload = {
key_system_identifier: '123',
shared_vault_uuid: '456',
signatureData: previousResult,
} as jest.Mocked<EncryptedInputParameters>
const keypair = crypto.sodiumCryptoSignSeedKeypair('seedling')
const contentAdditionalDataResultResult = additionalDataUseCase.execute(content, hashingKey, keypair)
const contentKeyAdditionalDataResultResult = additionalDataUseCase.execute(contentKey, hashingKey, keypair)
const result = usecase.execute(
payload,
hashingKey,
{
additionalData: encodeUseCase.execute(contentKeyAdditionalDataResultResult.additionalData),
plaintext: contentKey,
},
{
additionalData: encodeUseCase.execute(contentAdditionalDataResultResult.additionalData),
plaintext: content,
},
)
expect(result).toEqual({
required: true,
contentHash: expect.any(String),
result: {
passes: true,
publicKey: keypair.publicKey,
signature: expect.any(String),
},
})
})
})

View File

@@ -0,0 +1,127 @@
import { EncryptedInputParameters } from '../../../../Types/EncryptedParameters'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { doesPayloadRequireSigning } from '../../V004AlgorithmHelpers'
import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload'
import { SymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData'
import { HashStringUseCase } from '../Hash/HashString'
import { PersistentSignatureData } from '@standardnotes/models'
import { HashingKey } from '../Hash/HashingKey'
/**
* Embedded signatures check the signature on the symmetric string, but this string can change every time we encrypt
* the payload, even though its content hasn't changed. This would mean that if we received a signed payload from User B,
* then saved this payload into local storage by encrypting it, we would lose the signature of the content it came with, and
* it would instead be overwritten by our local user signature, which would always pass.
*
* In addition to embedded signature verification, we'll also hang on to a sticky signature of the content, which
* remains the same until the hash changes. We do not perform any static verification on this data; instead, clients
* can compute authenticity of the content on demand.
*/
export class GenerateSymmetricPayloadSignatureResultUseCase {
private parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto)
private hashUseCase = new HashStringUseCase(this.crypto)
constructor(private readonly crypto: PureCryptoInterface) {}
execute(
payload: EncryptedInputParameters,
hashingKey: HashingKey,
contentKeyParameters: {
additionalData: string
plaintext: string
},
contentParameters: {
additionalData: string
plaintext: string
},
): PersistentSignatureData {
const contentKeyHash = this.hashUseCase.execute(contentKeyParameters.plaintext, hashingKey)
const contentHash = this.hashUseCase.execute(contentParameters.plaintext, hashingKey)
const contentKeyAdditionalData = this.parseBase64Usecase.execute<SymmetricItemAdditionalData>(
contentKeyParameters.additionalData,
)
const contentAdditionalData = this.parseBase64Usecase.execute<SymmetricItemAdditionalData>(
contentParameters.additionalData,
)
const verificationRequired = doesPayloadRequireSigning(payload)
if (!contentKeyAdditionalData.signingData || !contentAdditionalData.signingData) {
if (verificationRequired) {
return {
required: true,
contentHash: contentHash,
result: {
passes: false,
publicKey: '',
signature: '',
},
}
}
return {
required: false,
contentHash: contentHash,
}
}
if (contentKeyAdditionalData.signingData.publicKey !== contentAdditionalData.signingData.publicKey) {
return {
required: verificationRequired,
contentHash: contentHash,
result: {
passes: false,
publicKey: '',
signature: '',
},
}
}
const commonPublicKey = contentKeyAdditionalData.signingData.publicKey
const contentKeySignatureVerified = this.verifySignature(
contentKeyHash,
contentKeyAdditionalData.signingData.signature,
commonPublicKey,
)
const contentSignatureVerified = this.verifySignature(
contentHash,
contentAdditionalData.signingData.signature,
commonPublicKey,
)
let passesStickyContentVerification = true
const previousSignatureResult = payload.signatureData
if (previousSignatureResult) {
const previousSignatureStillApplicable = previousSignatureResult.contentHash === contentHash
if (previousSignatureStillApplicable) {
if (previousSignatureResult.required) {
passesStickyContentVerification = previousSignatureResult.result.passes
} else if (previousSignatureResult.result) {
passesStickyContentVerification = previousSignatureResult.result.passes
}
}
}
const passesAllVerification =
contentKeySignatureVerified && contentSignatureVerified && passesStickyContentVerification
return {
required: verificationRequired,
contentHash: contentHash,
result: {
passes: passesAllVerification,
publicKey: commonPublicKey,
signature: contentAdditionalData.signingData.signature,
},
}
}
private verifySignature(contentHash: string, signature: string, publicKey: string) {
return this.crypto.sodiumCryptoSignVerify(contentHash, signature, publicKey)
}
}

View File

@@ -0,0 +1,27 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { EncryptedOutputParameters } from '../../../../Types/EncryptedParameters'
import { RootKeyEncryptedAuthenticatedData } from '../../../../Types/RootKeyEncryptedAuthenticatedData'
import { ItemAuthenticatedData } from '../../../../Types/ItemAuthenticatedData'
import { LegacyAttachedData } from '../../../../Types/LegacyAttachedData'
import { deconstructEncryptedPayloadString } from '../../V004AlgorithmHelpers'
import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload'
export class GetPayloadAuthenticatedDataDetachedUseCase {
private parseStringUseCase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto)
constructor(private readonly crypto: PureCryptoInterface) {}
execute(
encrypted: EncryptedOutputParameters,
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
const itemKeyComponents = deconstructEncryptedPayloadString(encrypted.enc_item_key)
const authenticatedDataString = itemKeyComponents.authenticatedData
const result = this.parseStringUseCase.execute<
RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData
>(authenticatedDataString)
return result
}
}

View File

@@ -0,0 +1,10 @@
import { Base64String, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import * as Utils from '@standardnotes/utils'
export class CreateConsistentBase64JsonPayloadUseCase {
constructor(private readonly crypto: PureCryptoInterface) {}
execute<T>(jsonObject: T): Base64String {
return this.crypto.base64Encode(JSON.stringify(Utils.sortedCopy(Utils.omitUndefinedCopy(jsonObject))))
}
}

View File

@@ -0,0 +1,9 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
export class ParseConsistentBase64JsonPayloadUseCase {
constructor(private readonly crypto: PureCryptoInterface) {}
execute<P>(stringifiedData: string): P {
return JSON.parse(this.crypto.base64Decode(stringifiedData))
}
}

View File

@@ -0,0 +1,19 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { sortedCopy } from '@standardnotes/utils'
import { RootKeyEncryptedAuthenticatedData } from './../../../../Types/RootKeyEncryptedAuthenticatedData'
import { ItemAuthenticatedData } from './../../../../Types/ItemAuthenticatedData'
export class StringToAuthenticatedDataUseCase {
constructor(private readonly crypto: PureCryptoInterface) {}
execute(
rawAuthenticatedData: string,
override: ItemAuthenticatedData,
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData {
const base = JSON.parse(this.crypto.base64Decode(rawAuthenticatedData))
return sortedCopy({
...base,
...override,
})
}
}

View File

@@ -0,0 +1,20 @@
import { V004Components, V004PartitionCharacter, V004StringComponents } from './V004AlgorithmTypes'
export function doesPayloadRequireSigning(payload: { shared_vault_uuid?: string }) {
return payload.shared_vault_uuid != undefined
}
export function deconstructEncryptedPayloadString(payloadString: string): V004Components {
/** Base64 encoding of JSON.stringify({}) */
const EmptyAdditionalDataString = 'e30='
const components = payloadString.split(V004PartitionCharacter) as V004StringComponents
return {
version: components[0],
nonce: components[1],
ciphertext: components[2],
authenticatedData: components[3],
additionalData: components[4] ?? EmptyAdditionalDataString,
}
}

View File

@@ -0,0 +1,32 @@
export const V004AsymmetricCiphertextPrefix = '004_Asym'
export const V004PartitionCharacter = ':'
export type V004StringComponents = [
version: string,
nonce: string,
ciphertext: string,
authenticatedData: string,
additionalData: string,
]
export type V004Components = {
version: V004StringComponents[0]
nonce: V004StringComponents[1]
ciphertext: V004StringComponents[2]
authenticatedData: V004StringComponents[3]
additionalData: V004StringComponents[4]
}
export type V004AsymmetricStringComponents = [
version: typeof V004AsymmetricCiphertextPrefix,
nonce: string,
ciphertext: string,
additionalData: string,
]
export type V004AsymmetricComponents = {
version: V004AsymmetricStringComponents[0]
nonce: V004AsymmetricStringComponents[1]
ciphertext: V004AsymmetricStringComponents[2]
additionalData: V004AsymmetricStringComponents[3]
}

View File

@@ -1,75 +0,0 @@
import { ProtocolOperator005 } from './Operator005'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
describe('operator 005', () => {
let crypto: PureCryptoInterface
let operator: ProtocolOperator005
beforeEach(() => {
crypto = {} as jest.Mocked<PureCryptoInterface>
crypto.generateRandomKey = jest.fn().mockImplementation(() => {
return 'random-string'
})
crypto.xchacha20Encrypt = jest.fn().mockImplementation((text: string) => {
return `<e>${text}<e>`
})
crypto.xchacha20Decrypt = jest.fn().mockImplementation((text: string) => {
return text.split('<e>')[1]
})
crypto.sodiumCryptoBoxGenerateKeypair = jest.fn().mockImplementation(() => {
return { privateKey: 'private-key', publicKey: 'public-key', keyType: 'x25519' }
})
crypto.sodiumCryptoBoxEasyEncrypt = jest.fn().mockImplementation((text: string) => {
return `<e>${text}<e>`
})
crypto.sodiumCryptoBoxEasyDecrypt = jest.fn().mockImplementation((text: string) => {
return text.split('<e>')[1]
})
operator = new ProtocolOperator005(crypto)
})
it('should generateKeyPair', () => {
const result = operator.generateKeyPair()
expect(result).toEqual({ privateKey: 'private-key', publicKey: 'public-key', keyType: 'x25519' })
})
it('should asymmetricEncryptKey', () => {
const senderKeypair = operator.generateKeyPair()
const recipientKeypair = operator.generateKeyPair()
const plaintext = 'foo'
const result = operator.asymmetricEncryptKey(plaintext, senderKeypair.privateKey, recipientKeypair.publicKey)
expect(result).toEqual(`${'005_KeyAsym'}:random-string:<e>foo<e>`)
})
it('should asymmetricDecryptKey', () => {
const senderKeypair = operator.generateKeyPair()
const recipientKeypair = operator.generateKeyPair()
const plaintext = 'foo'
const ciphertext = operator.asymmetricEncryptKey(plaintext, senderKeypair.privateKey, recipientKeypair.publicKey)
const decrypted = operator.asymmetricDecryptKey(ciphertext, senderKeypair.publicKey, recipientKeypair.privateKey)
expect(decrypted).toEqual('foo')
})
it('should symmetricEncryptPrivateKey', () => {
const keypair = operator.generateKeyPair()
const symmetricKey = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
const encryptedKey = operator.symmetricEncryptPrivateKey(keypair.privateKey, symmetricKey)
expect(encryptedKey).toEqual(`${'005_KeySym'}:random-string:<e>${keypair.privateKey}<e>`)
})
it('should symmetricDecryptPrivateKey', () => {
const keypair = operator.generateKeyPair()
const symmetricKey = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
const encryptedKey = operator.symmetricEncryptPrivateKey(keypair.privateKey, symmetricKey)
const decryptedKey = operator.symmetricDecryptPrivateKey(encryptedKey, symmetricKey)
expect(decryptedKey).toEqual(keypair.privateKey)
})
})

View File

@@ -1,80 +0,0 @@
import { ProtocolVersion } from '@standardnotes/common'
import { Base64String, HexString, PkcKeyPair, Utf8String } from '@standardnotes/sncrypto-common'
import { V005Algorithm } from '../../Algorithm'
import { SNProtocolOperator004 } from '../004/Operator004'
const VersionString = '005'
const SymmetricCiphertextPrefix = `${VersionString}_KeySym`
const AsymmetricCiphertextPrefix = `${VersionString}_KeyAsym`
export type AsymmetricallyEncryptedKey = Base64String
export type SymmetricallyEncryptedPrivateKey = Base64String
/**
* @experimental
* @unreleased
*/
export class ProtocolOperator005 extends SNProtocolOperator004 {
public override getEncryptionDisplayName(): string {
return 'XChaCha20-Poly1305'
}
override get version(): ProtocolVersion {
return VersionString as ProtocolVersion
}
generateKeyPair(): PkcKeyPair {
return this.crypto.sodiumCryptoBoxGenerateKeypair()
}
asymmetricEncryptKey(
keyToEncrypt: HexString,
senderSecretKey: HexString,
recipientPublicKey: HexString,
): AsymmetricallyEncryptedKey {
const nonce = this.crypto.generateRandomKey(V005Algorithm.AsymmetricEncryptionNonceLength)
const ciphertext = this.crypto.sodiumCryptoBoxEasyEncrypt(keyToEncrypt, nonce, senderSecretKey, recipientPublicKey)
return [AsymmetricCiphertextPrefix, nonce, ciphertext].join(':')
}
asymmetricDecryptKey(
keyToDecrypt: AsymmetricallyEncryptedKey,
senderPublicKey: HexString,
recipientSecretKey: HexString,
): Utf8String {
const components = keyToDecrypt.split(':')
const nonce = components[1]
return this.crypto.sodiumCryptoBoxEasyDecrypt(keyToDecrypt, nonce, senderPublicKey, recipientSecretKey)
}
symmetricEncryptPrivateKey(privateKey: HexString, symmetricKey: HexString): SymmetricallyEncryptedPrivateKey {
if (symmetricKey.length !== 64) {
throw new Error('Symmetric key length must be 256 bits')
}
const nonce = this.crypto.generateRandomKey(V005Algorithm.SymmetricEncryptionNonceLength)
const encryptedKey = this.crypto.xchacha20Encrypt(privateKey, nonce, symmetricKey)
return [SymmetricCiphertextPrefix, nonce, encryptedKey].join(':')
}
symmetricDecryptPrivateKey(
encryptedPrivateKey: SymmetricallyEncryptedPrivateKey,
symmetricKey: HexString,
): HexString | null {
if (symmetricKey.length !== 64) {
throw new Error('Symmetric key length must be 256 bits')
}
const components = encryptedPrivateKey.split(':')
const nonce = components[1]
return this.crypto.xchacha20Decrypt(encryptedPrivateKey, nonce, symmetricKey)
}
}

View File

@@ -4,12 +4,9 @@ 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 { AnyOperatorInterface } from './OperatorInterface/TypeCheck'
export function createOperatorForVersion(
version: ProtocolVersion,
crypto: PureCryptoInterface,
): AsynchronousOperator | SynchronousOperator {
export function createOperatorForVersion(version: ProtocolVersion, crypto: PureCryptoInterface): AnyOperatorInterface {
if (version === ProtocolVersion.V001) {
return new SNProtocolOperator001(crypto)
} else if (version === ProtocolVersion.V002) {
@@ -22,9 +19,3 @@ export function createOperatorForVersion(
throw Error(`Unable to find operator for version ${version}`)
}
}
export function isAsyncOperator(
operator: AsynchronousOperator | SynchronousOperator,
): operator is AsynchronousOperator {
return (operator as AsynchronousOperator).generateDecryptedParametersAsync !== undefined
}

View File

@@ -1,86 +0,0 @@
import { KeyParamsOrigination } from '@standardnotes/common'
import * as Models from '@standardnotes/models'
import { ItemsKeyInterface, RootKeyInterface } from '@standardnotes/models'
import { SNRootKey } from '../Keys/RootKey/RootKey'
import { SNRootKeyParams } from '../Keys/RootKey/RootKeyParams'
import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../Types/EncryptedParameters'
import { ItemAuthenticatedData } from '../Types/ItemAuthenticatedData'
import { LegacyAttachedData } from '../Types/LegacyAttachedData'
import { RootKeyEncryptedAuthenticatedData } from '../Types/RootKeyEncryptedAuthenticatedData'
/**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>
}

View File

@@ -0,0 +1,22 @@
import {
DecryptedPayloadInterface,
ItemContent,
ItemsKeyInterface,
KeySystemItemsKeyInterface,
KeySystemRootKeyInterface,
RootKeyInterface,
} from '@standardnotes/models'
import { EncryptedOutputParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters'
import { DecryptedParameters } from '../../Types/DecryptedParameters'
export interface AsyncOperatorInterface {
generateEncryptedParametersAsync(
payload: DecryptedPayloadInterface,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
): Promise<EncryptedOutputParameters>
generateDecryptedParametersAsync<C extends ItemContent = ItemContent>(
encrypted: EncryptedOutputParameters,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters>
}

View File

@@ -0,0 +1,102 @@
import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
import {
ItemsKeyInterface,
RootKeyInterface,
KeySystemItemsKeyInterface,
KeySystemRootKeyInterface,
KeySystemIdentifier,
KeySystemRootKeyParamsInterface,
} from '@standardnotes/models'
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
import { EncryptedOutputParameters } from '../../Types/EncryptedParameters'
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
import { LegacyAttachedData } from '../../Types/LegacyAttachedData'
import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData'
import { HexString, PkcKeyPair } from '@standardnotes/sncrypto-common'
import { AsymmetricallyEncryptedString } from '../Types/Types'
import { AsymmetricDecryptResult } from '../Types/AsymmetricDecryptResult'
import { PublicKeySet } from '../Types/PublicKeySet'
import { AsymmetricSignatureVerificationDetachedResult } from '../Types/AsymmetricSignatureVerificationDetachedResult'
/**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 OperatorInterface {
/**
* Returns encryption protocol display name
*/
getEncryptionDisplayName(): string
readonly version: string
createItemsKey(): ItemsKeyInterface
/**
* Returns the payload's authenticated data. The passed payload must be in a
* non-decrypted, ciphertext state.
*/
getPayloadAuthenticatedDataForExternalUse(
encrypted: EncryptedOutputParameters,
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined
/**
* Computes a root key given a password and previous keyParams
* @param password - Plain string representing raw user password
*/
computeRootKey<K extends RootKeyInterface>(password: string, keyParams: SNRootKeyParams): Promise<K>
/**
* 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<K extends RootKeyInterface>(
identifier: string,
password: string,
origination: KeyParamsOrigination,
): Promise<K>
createRandomizedKeySystemRootKey(dto: { systemIdentifier: KeySystemIdentifier }): KeySystemRootKeyInterface
createUserInputtedKeySystemRootKey(dto: {
systemIdentifier: KeySystemIdentifier
userInputtedPassword: string
}): KeySystemRootKeyInterface
deriveUserInputtedKeySystemRootKey(dto: {
keyParams: KeySystemRootKeyParamsInterface
userInputtedPassword: string
}): KeySystemRootKeyInterface
createKeySystemItemsKey(
uuid: string,
keySystemIdentifier: KeySystemIdentifier,
sharedVaultUuid: string | undefined,
rootKeyToken: string,
): KeySystemItemsKeyInterface
asymmetricEncrypt(dto: {
stringToEncrypt: HexString
senderKeyPair: PkcKeyPair
senderSigningKeyPair: PkcKeyPair
recipientPublicKey: HexString
}): AsymmetricallyEncryptedString
asymmetricDecrypt(dto: {
stringToDecrypt: AsymmetricallyEncryptedString
recipientSecretKey: HexString
}): AsymmetricDecryptResult | null
asymmetricSignatureVerifyDetached(
encryptedString: AsymmetricallyEncryptedString,
): AsymmetricSignatureVerificationDetachedResult
getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PublicKeySet
versionForAsymmetricallyEncryptedString(encryptedString: string): ProtocolVersion
}

View File

@@ -0,0 +1,31 @@
import {
DecryptedPayloadInterface,
ItemContent,
ItemsKeyInterface,
KeySystemItemsKeyInterface,
KeySystemRootKeyInterface,
RootKeyInterface,
} from '@standardnotes/models'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { EncryptedOutputParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters'
import { DecryptedParameters } from '../../Types/DecryptedParameters'
export interface SyncOperatorInterface {
/**
* 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)
*/
generateEncryptedParameters(
payload: DecryptedPayloadInterface,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
signingKeyPair?: PkcKeyPair,
): EncryptedOutputParameters
generateDecryptedParameters<C extends ItemContent = ItemContent>(
encrypted: EncryptedOutputParameters,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
): DecryptedParameters<C> | ErrorDecryptingParameters
}

View File

@@ -0,0 +1,13 @@
import { AsyncOperatorInterface } from './AsyncOperatorInterface'
import { OperatorInterface } from './OperatorInterface'
import { SyncOperatorInterface } from './SyncOperatorInterface'
export type AnyOperatorInterface = OperatorInterface & (AsyncOperatorInterface | SyncOperatorInterface)
export function isAsyncOperator(operator: unknown): operator is AsyncOperatorInterface {
return 'generateEncryptedParametersAsync' in (operator as AsyncOperatorInterface)
}
export function isSyncOperator(operator: unknown): operator is SyncOperatorInterface {
return !isAsyncOperator(operator)
}

View File

@@ -1,10 +1,10 @@
import { ProtocolVersion, ProtocolVersionLatest } from '@standardnotes/common'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { createOperatorForVersion } from './Functions'
import { AsynchronousOperator, SynchronousOperator } from './Operator'
import { AnyOperatorInterface } from './OperatorInterface/TypeCheck'
export class OperatorManager {
private operators: Record<string, AsynchronousOperator | SynchronousOperator> = {}
private operators: Record<string, AnyOperatorInterface> = {}
constructor(private crypto: PureCryptoInterface) {
this.crypto = crypto
@@ -15,7 +15,7 @@ export class OperatorManager {
this.operators = {}
}
public operatorForVersion(version: ProtocolVersion): SynchronousOperator | AsynchronousOperator {
public operatorForVersion(version: ProtocolVersion): AnyOperatorInterface {
const operatorKey = version
let operator = this.operators[operatorKey]
if (!operator) {
@@ -28,7 +28,7 @@ export class OperatorManager {
/**
* Returns the operator corresponding to the latest protocol version
*/
public defaultOperator(): SynchronousOperator | AsynchronousOperator {
public defaultOperator(): AnyOperatorInterface {
return this.operatorForVersion(ProtocolVersionLatest)
}
}

View File

@@ -4,49 +4,53 @@ import {
RootKeyInterface,
ItemContent,
EncryptedPayloadInterface,
KeySystemItemsKeyInterface,
KeySystemRootKeyInterface,
} from '@standardnotes/models'
import {
DecryptedParameters,
EncryptedParameters,
encryptedParametersFromPayload,
EncryptedOutputParameters,
encryptedInputParametersFromPayload,
ErrorDecryptingParameters,
} from '../Types/EncryptedParameters'
import { isAsyncOperator } from './Functions'
import { DecryptedParameters } from '../Types/DecryptedParameters'
import { OperatorManager } from './OperatorManager'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { isAsyncOperator } from './OperatorInterface/TypeCheck'
export async function encryptPayload(
payload: DecryptedPayloadInterface,
key: ItemsKeyInterface | RootKeyInterface,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
operatorManager: OperatorManager,
): Promise<EncryptedParameters> {
signingKeyPair: PkcKeyPair | undefined,
): Promise<EncryptedOutputParameters> {
const operator = operatorManager.operatorForVersion(key.keyVersion)
let encryptionParameters
let result: EncryptedOutputParameters | undefined = undefined
if (isAsyncOperator(operator)) {
encryptionParameters = await operator.generateEncryptedParametersAsync(payload, key)
result = await operator.generateEncryptedParametersAsync(payload, key)
} else {
encryptionParameters = operator.generateEncryptedParametersSync(payload, key)
result = operator.generateEncryptedParameters(payload, key, signingKeyPair)
}
if (!encryptionParameters) {
if (!result) {
throw 'Unable to generate encryption parameters'
}
return encryptionParameters
return result
}
export async function decryptPayload<C extends ItemContent = ItemContent>(
payload: EncryptedPayloadInterface,
key: ItemsKeyInterface | RootKeyInterface,
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
operatorManager: OperatorManager,
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
const operator = operatorManager.operatorForVersion(payload.version)
try {
if (isAsyncOperator(operator)) {
return await operator.generateDecryptedParametersAsync(encryptedParametersFromPayload(payload), key)
return await operator.generateDecryptedParametersAsync(encryptedInputParametersFromPayload(payload), key)
} else {
return operator.generateDecryptedParametersSync(encryptedParametersFromPayload(payload), key)
return operator.generateDecryptedParameters(encryptedInputParametersFromPayload(payload), key)
}
} catch (e) {
console.error('Error decrypting payload', payload, e)

View File

@@ -0,0 +1,8 @@
import { HexString } from '@standardnotes/sncrypto-common'
export type AsymmetricDecryptResult = {
plaintext: HexString
signatureVerified: boolean
signaturePublicKey: string
senderPublicKey: string
}

View File

@@ -0,0 +1,9 @@
export type AsymmetricSignatureVerificationDetachedResult =
| {
signatureVerified: true
signaturePublicKey: string
senderPublicKey: string
}
| {
signatureVerified: false
}

View File

@@ -0,0 +1,4 @@
export type PublicKeySet = {
encryption: string
signing: string
}

View File

@@ -0,0 +1,4 @@
import { Base64String } from '@standardnotes/sncrypto-common'
export type AsymmetricallyEncryptedString = Base64String
export type SymmetricallyEncryptedString = Base64String

View File

@@ -1,3 +1,4 @@
import { AsymmetricSignatureVerificationDetachedResult } from '../../Operator/Types/AsymmetricSignatureVerificationDetachedResult'
import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
import {
BackupFile,
@@ -6,17 +7,26 @@ import {
ItemContent,
ItemsKeyInterface,
RootKeyInterface,
KeySystemIdentifier,
KeySystemItemsKeyInterface,
AsymmetricMessagePayload,
KeySystemRootKeyInterface,
KeySystemRootKeyParamsInterface,
TrustedContactInterface,
} from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
import { KeyedDecryptionSplit } from '../../Split/KeyedDecryptionSplit'
import { KeyedEncryptionSplit } from '../../Split/KeyedEncryptionSplit'
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
import { LegacyAttachedData } from '../../Types/LegacyAttachedData'
import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { PublicKeySet } from '../../Operator/Types/PublicKeySet'
import { KeySystemKeyManagerInterface } from '../KeySystemKeyManagerInterface'
import { AsymmetricallyEncryptedString } from '../../Operator/Types/Types'
export interface EncryptionProviderInterface {
keys: KeySystemKeyManagerInterface
encryptSplitSingle(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface>
encryptSplit(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface[]>
decryptSplitSingle<
@@ -31,29 +41,24 @@ export interface EncryptionProviderInterface {
>(
split: KeyedDecryptionSplit,
): Promise<(P | EncryptedPayloadInterface)[]>
hasRootKeyEncryptionSource(): boolean
getKeyEmbeddedKeyParams(key: EncryptedPayloadInterface): SNRootKeyParams | undefined
computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface>
getEmbeddedPayloadAuthenticatedData<D extends ItemAuthenticatedData>(
payload: EncryptedPayloadInterface,
): D | undefined
getKeyEmbeddedKeyParamsFromItemsKey(key: EncryptedPayloadInterface): SNRootKeyParams | undefined
supportedVersions(): ProtocolVersion[]
isVersionNewerThanLibraryVersion(version: ProtocolVersion): boolean
platformSupportsKeyDerivation(keyParams: SNRootKeyParams): boolean
computeWrappingKey(passcode: string): Promise<RootKeyInterface>
getUserVersion(): ProtocolVersion | undefined
decryptBackupFile(
file: BackupFile,
password?: string,
): Promise<ClientDisplayableError | (EncryptedPayloadInterface | DecryptedPayloadInterface)[]>
getUserVersion(): ProtocolVersion | undefined
hasAccount(): boolean
decryptErroredPayloads(): Promise<void>
deleteWorkspaceSpecificKeyStateFromDevice(): Promise<void>
hasPasscode(): boolean
createRootKey(
identifier: string,
password: string,
origination: KeyParamsOrigination,
version?: ProtocolVersion,
): Promise<RootKeyInterface>
setNewRootKeyWrapper(wrappingKey: RootKeyInterface): Promise<void>
removePasscode(): Promise<void>
validateAccountPassword(password: string): Promise<
| {
@@ -66,11 +71,63 @@ export interface EncryptionProviderInterface {
valid: boolean
}
>
decryptErroredPayloads(): Promise<void>
deleteWorkspaceSpecificKeyStateFromDevice(): Promise<void>
computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface>
computeWrappingKey(passcode: string): Promise<RootKeyInterface>
hasRootKeyEncryptionSource(): boolean
createRootKey<K extends RootKeyInterface>(
identifier: string,
password: string,
origination: KeyParamsOrigination,
version?: ProtocolVersion,
): Promise<K>
getRootKeyParams(): SNRootKeyParams | undefined
setNewRootKeyWrapper(wrappingKey: RootKeyInterface): Promise<void>
createNewItemsKeyWithRollback(): Promise<() => Promise<void>>
reencryptItemsKeys(): Promise<void>
reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void>
getSureDefaultItemsKey(): ItemsKeyInterface
getRootKeyParams(): Promise<SNRootKeyParams | undefined>
getEmbeddedPayloadAuthenticatedData(
payload: EncryptedPayloadInterface,
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined
createRandomizedKeySystemRootKey(dto: { systemIdentifier: KeySystemIdentifier }): KeySystemRootKeyInterface
createUserInputtedKeySystemRootKey(dto: {
systemIdentifier: KeySystemIdentifier
userInputtedPassword: string
}): KeySystemRootKeyInterface
deriveUserInputtedKeySystemRootKey(dto: {
keyParams: KeySystemRootKeyParamsInterface
userInputtedPassword: string
}): KeySystemRootKeyInterface
createKeySystemItemsKey(
uuid: string,
keySystemIdentifier: KeySystemIdentifier,
sharedVaultUuid: string | undefined,
rootKeyToken: string,
): KeySystemItemsKeyInterface
reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise<void>
getKeyPair(): PkcKeyPair
getSigningKeyPair(): PkcKeyPair
asymmetricallyEncryptMessage(dto: {
message: AsymmetricMessagePayload
senderKeyPair: PkcKeyPair
senderSigningKeyPair: PkcKeyPair
recipientPublicKey: string
}): string
asymmetricallyDecryptMessage<M extends AsymmetricMessagePayload>(dto: {
encryptedString: AsymmetricallyEncryptedString
trustedSender: TrustedContactInterface | undefined
privateKey: string
}): M | undefined
asymmetricSignatureVerifyDetached(
encryptedString: AsymmetricallyEncryptedString,
): AsymmetricSignatureVerificationDetachedResult
getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: string): PublicKeySet
}

View File

@@ -0,0 +1,31 @@
import {
EncryptedItemInterface,
KeySystemIdentifier,
KeySystemItemsKeyInterface,
KeySystemRootKeyInterface,
KeySystemRootKeyStorageMode,
VaultListingInterface,
} from '@standardnotes/models'
export interface KeySystemKeyManagerInterface {
getAllKeySystemItemsKeys(): (KeySystemItemsKeyInterface | EncryptedItemInterface)[]
getKeySystemItemsKeys(systemIdentifier: KeySystemIdentifier): KeySystemItemsKeyInterface[]
getPrimaryKeySystemItemsKey(systemIdentifier: KeySystemIdentifier): KeySystemItemsKeyInterface
/** Returns synced root keys, in addition to any local or ephemeral keys */
getAllKeySystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface[]
getSyncedKeySystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface[]
getAllSyncedKeySystemRootKeys(): KeySystemRootKeyInterface[]
getKeySystemRootKeyWithToken(
systemIdentifier: KeySystemIdentifier,
keyIdentifier: string,
): KeySystemRootKeyInterface | undefined
getPrimaryKeySystemRootKey(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface | undefined
intakeNonPersistentKeySystemRootKey(key: KeySystemRootKeyInterface, storage: KeySystemRootKeyStorageMode): void
undoIntakeNonPersistentKeySystemRootKey(systemIdentifier: KeySystemIdentifier): void
clearMemoryOfKeysRelatedToVault(vault: VaultListingInterface): void
deleteNonPersistentSystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): Promise<void>
deleteAllSyncedKeySystemRootKeys(systemIdentifier: KeySystemIdentifier): Promise<void>
}

View File

@@ -1,8 +1,10 @@
import {
DecryptedPayloadInterface,
EncryptedPayloadInterface,
KeySystemRootKeyInterface,
ItemsKeyInterface,
RootKeyInterface,
KeySystemItemsKeyInterface,
} from '@standardnotes/models'
export interface AbstractKeySplit<T = EncryptedPayloadInterface | DecryptedPayloadInterface> {
@@ -10,13 +12,20 @@ export interface AbstractKeySplit<T = EncryptedPayloadInterface | DecryptedPaylo
items: T[]
key: RootKeyInterface
}
usesKeySystemRootKey?: {
items: T[]
key: KeySystemRootKeyInterface
}
usesItemsKey?: {
items: T[]
key: ItemsKeyInterface
key: ItemsKeyInterface | KeySystemItemsKeyInterface
}
usesRootKeyWithKeyLookup?: {
items: T[]
}
usesKeySystemRootKeyWithKeyLookup?: {
items: T[]
}
usesItemsKeyWithKeyLookup?: {
items: T[]
}

View File

@@ -12,6 +12,10 @@ export function CreateEncryptionSplitWithKeyLookup(
result.usesRootKeyWithKeyLookup = { items: payloadSplit.rootKeyEncryption }
}
if (payloadSplit.keySystemRootKeyEncryption) {
result.usesKeySystemRootKeyWithKeyLookup = { items: payloadSplit.keySystemRootKeyEncryption }
}
if (payloadSplit.itemsKeyEncryption) {
result.usesItemsKeyWithKeyLookup = { items: payloadSplit.itemsKeyEncryption }
}
@@ -28,6 +32,10 @@ export function CreateDecryptionSplitWithKeyLookup(
result.usesRootKeyWithKeyLookup = { items: payloadSplit.rootKeyEncryption }
}
if (payloadSplit.keySystemRootKeyEncryption) {
result.usesKeySystemRootKeyWithKeyLookup = { items: payloadSplit.keySystemRootKeyEncryption }
}
if (payloadSplit.itemsKeyEncryption) {
result.usesItemsKeyWithKeyLookup = { items: payloadSplit.itemsKeyEncryption }
}
@@ -46,6 +54,11 @@ export function FindPayloadInEncryptionSplit(uuid: string, split: KeyedEncryptio
return inUsesRootKey
}
const inUsesKeySystemRootKey = split.usesKeySystemRootKey?.items.find((item) => item.uuid === uuid)
if (inUsesKeySystemRootKey) {
return inUsesKeySystemRootKey
}
const inUsesItemsKeyWithKeyLookup = split.usesItemsKeyWithKeyLookup?.items.find((item) => item.uuid === uuid)
if (inUsesItemsKeyWithKeyLookup) {
return inUsesItemsKeyWithKeyLookup
@@ -56,6 +69,13 @@ export function FindPayloadInEncryptionSplit(uuid: string, split: KeyedEncryptio
return inUsesRootKeyWithKeyLookup
}
const inUsesKeySystemRootKeyWithKeyLookup = split.usesKeySystemRootKeyWithKeyLookup?.items.find(
(item) => item.uuid === uuid,
)
if (inUsesKeySystemRootKeyWithKeyLookup) {
return inUsesKeySystemRootKeyWithKeyLookup
}
throw Error('Cannot find payload in encryption split')
}
@@ -70,6 +90,11 @@ export function FindPayloadInDecryptionSplit(uuid: string, split: KeyedDecryptio
return inUsesRootKey
}
const inUsesKeySystemRootKey = split.usesKeySystemRootKey?.items.find((item) => item.uuid === uuid)
if (inUsesKeySystemRootKey) {
return inUsesKeySystemRootKey
}
const inUsesItemsKeyWithKeyLookup = split.usesItemsKeyWithKeyLookup?.items.find((item) => item.uuid === uuid)
if (inUsesItemsKeyWithKeyLookup) {
return inUsesItemsKeyWithKeyLookup
@@ -80,5 +105,12 @@ export function FindPayloadInDecryptionSplit(uuid: string, split: KeyedDecryptio
return inUsesRootKeyWithKeyLookup
}
const inUsesKeySystemRootKeyWithKeyLookup = split.usesKeySystemRootKeyWithKeyLookup?.items.find(
(item) => item.uuid === uuid,
)
if (inUsesKeySystemRootKeyWithKeyLookup) {
return inUsesKeySystemRootKeyWithKeyLookup
}
throw Error('Cannot find payload in encryption split')
}

View File

@@ -2,5 +2,6 @@ import { DecryptedPayloadInterface, EncryptedPayloadInterface } from '@standardn
export interface EncryptionTypeSplit<T = EncryptedPayloadInterface | DecryptedPayloadInterface> {
rootKeyEncryption?: T[]
keySystemRootKeyEncryption?: T[]
itemsKeyEncryption?: T[]
}

View File

@@ -1,5 +1,9 @@
import { DecryptedPayloadInterface, EncryptedPayloadInterface } from '@standardnotes/models'
import { ItemContentTypeUsesRootKeyEncryption } from '../Keys/RootKey/Functions'
import {
DecryptedPayloadInterface,
EncryptedPayloadInterface,
ContentTypeUsesKeySystemRootKeyEncryption,
ContentTypeUsesRootKeyEncryption,
} from '@standardnotes/models'
import { EncryptionTypeSplit } from './EncryptionTypeSplit'
export function SplitPayloadsByEncryptionType<T extends EncryptedPayloadInterface | DecryptedPayloadInterface>(
@@ -7,10 +11,13 @@ export function SplitPayloadsByEncryptionType<T extends EncryptedPayloadInterfac
): EncryptionTypeSplit<T> {
const usesRootKey: T[] = []
const usesItemsKey: T[] = []
const usesKeySystemRootKey: T[] = []
for (const item of payloads) {
if (ItemContentTypeUsesRootKeyEncryption(item.content_type)) {
if (ContentTypeUsesRootKeyEncryption(item.content_type)) {
usesRootKey.push(item)
} else if (ContentTypeUsesKeySystemRootKeyEncryption(item.content_type)) {
usesKeySystemRootKey.push(item)
} else {
usesItemsKey.push(item)
}
@@ -19,5 +26,6 @@ export function SplitPayloadsByEncryptionType<T extends EncryptedPayloadInterfac
return {
rootKeyEncryption: usesRootKey.length > 0 ? usesRootKey : undefined,
itemsKeyEncryption: usesItemsKey.length > 0 ? usesItemsKey : undefined,
keySystemRootKeyEncryption: usesKeySystemRootKey.length > 0 ? usesKeySystemRootKey : undefined,
}
}

View File

@@ -0,0 +1,7 @@
import { ItemContent, PersistentSignatureData } from '@standardnotes/models'
export type DecryptedParameters<C extends ItemContent = ItemContent> = {
uuid: string
content: C
signatureData: PersistentSignatureData
}

View File

@@ -1,20 +1,23 @@
import { ProtocolVersion } from '@standardnotes/common'
import { EncryptedPayloadInterface, ItemContent } from '@standardnotes/models'
import { ContentType, ProtocolVersion } from '@standardnotes/common'
import { EncryptedPayloadInterface, DecryptedPayloadInterface, PersistentSignatureData } from '@standardnotes/models'
import { DecryptedParameters } from './DecryptedParameters'
export type EncryptedParameters = {
export type EncryptedOutputParameters = {
uuid: string
content: string
content_type: ContentType
items_key_id: string | undefined
enc_item_key: string
version: ProtocolVersion
key_system_identifier: string | undefined
shared_vault_uuid: string | undefined
/** @deprecated */
auth_hash?: string
}
export type DecryptedParameters<C extends ItemContent = ItemContent> = {
uuid: string
content: C
export type EncryptedInputParameters = EncryptedOutputParameters & {
signatureData: PersistentSignatureData | undefined
}
export type ErrorDecryptingParameters = {
@@ -24,18 +27,27 @@ export type ErrorDecryptingParameters = {
}
export function isErrorDecryptingParameters(
x: EncryptedParameters | DecryptedParameters | ErrorDecryptingParameters,
x:
| EncryptedOutputParameters
| DecryptedParameters
| ErrorDecryptingParameters
| DecryptedPayloadInterface
| EncryptedPayloadInterface,
): x is ErrorDecryptingParameters {
return (x as ErrorDecryptingParameters).errorDecrypting
}
export function encryptedParametersFromPayload(payload: EncryptedPayloadInterface): EncryptedParameters {
export function encryptedInputParametersFromPayload(payload: EncryptedPayloadInterface): EncryptedInputParameters {
return {
uuid: payload.uuid,
content: payload.content,
content_type: payload.content_type,
items_key_id: payload.items_key_id,
enc_item_key: payload.enc_item_key as string,
version: payload.version,
auth_hash: payload.auth_hash,
key_system_identifier: payload.key_system_identifier,
shared_vault_uuid: payload.shared_vault_uuid,
signatureData: payload.signatureData,
}
}

View File

@@ -0,0 +1,15 @@
export type SigningData = {
signature: string
publicKey: string
}
export type SymmetricItemAdditionalData = {
signingData?: SigningData | undefined
}
export type AsymmetricItemAdditionalData = {
signingData: SigningData
senderPublicKey: string
}
export type AdditionalData = SymmetricItemAdditionalData | AsymmetricItemAdditionalData

View File

@@ -1,6 +1,12 @@
import { ProtocolVersion } from '@standardnotes/common'
type UserUuid = string
type KeySystemIdentifier = string
type SharedVaultUuid = string
export type ItemAuthenticatedData = {
u: string
u: UserUuid
v: ProtocolVersion
ksi?: KeySystemIdentifier
svu?: SharedVaultUuid
}

View File

@@ -0,0 +1,7 @@
import { ItemAuthenticatedData } from './ItemAuthenticatedData'
import { KeySystemRootKeyParamsInterface } from '@standardnotes/models'
/** Authenticated data for key system items key payloads */
export type KeySystemItemsKeyAuthenticatedData = ItemAuthenticatedData & {
kp: KeySystemRootKeyParamsInterface
}

View File

@@ -0,0 +1,4 @@
import { ItemAuthenticatedData } from './ItemAuthenticatedData'
/** Authenticated data for payloads encrypted with a key system root key */
export type KeySystemRootKeyEncryptedAuthenticatedData = ItemAuthenticatedData

View File

@@ -1,28 +1,43 @@
export * from './Algorithm'
export * from './Backups/BackupFileType'
export * from './Keys/ItemsKey/ItemsKey'
export * from './Keys/ItemsKey/ItemsKeyMutator'
export * from './Keys/ItemsKey/Registration'
export * from './Keys/KeySystemItemsKey/KeySystemItemsKey'
export * from './Keys/KeySystemItemsKey/KeySystemItemsKeyMutator'
export * from './Keys/KeySystemItemsKey/Registration'
export * from './Keys/RootKey/Functions'
export * from './Keys/RootKey/KeyParamsFunctions'
export * from './Keys/RootKey/ProtocolVersionForKeyParams'
export * from './Keys/RootKey/RootKey'
export * from './Keys/RootKey/RootKeyParams'
export * from './Keys/RootKey/ValidKeyParamsKeys'
export * from './Keys/Utils/KeyRecoveryStrings'
export * from './Operator/001/Operator001'
export * from './Operator/002/Operator002'
export * from './Operator/003/Operator003'
export * from './Operator/004/Operator004'
export * from './Operator/005/Operator005'
export * from './Operator/004/V004AlgorithmHelpers'
export * from './Operator/Functions'
export * from './Operator/Operator'
export * from './Operator/OperatorInterface/OperatorInterface'
export * from './Operator/OperatorManager'
export * from './Operator/OperatorWrapper'
export * from './Operator/Types/PublicKeySet'
export * from './Operator/Types/AsymmetricSignatureVerificationDetachedResult'
export * from './Operator/Types/Types'
export * from './Service/Encryption/EncryptionProviderInterface'
export * from './Service/KeySystemKeyManagerInterface'
export * from './Service/Functions'
export * from './Service/RootKey/KeyMode'
export * from './Service/RootKey/RootKeyServiceEvent'
export * from './Split/AbstractKeySplit'
export * from './Split/EncryptionSplit'
export * from './Split/EncryptionTypeSplit'
@@ -30,8 +45,11 @@ export * from './Split/Functions'
export * from './Split/KeyedDecryptionSplit'
export * from './Split/KeyedEncryptionSplit'
export * from './StandardException'
export * from './Types/EncryptedParameters'
export * from './Types/DecryptedParameters'
export * from './Types/ItemAuthenticatedData'
export * from './Types/LegacyAttachedData'
export * from './Types/RootKeyEncryptedAuthenticatedData'
export * from './Username/PrivateUsername'