internal: incomplete vault systems behind feature flag (#2340)
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { DecryptedItemMutator, KeySystemItemsKeyContent } from '@standardnotes/models'
|
||||
|
||||
export class KeySystemItemsKeyMutator extends DecryptedItemMutator<KeySystemItemsKeyContent> {}
|
||||
@@ -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>,
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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({
|
||||
|
||||
87
packages/encryption/src/Domain/Operator/004/MockedCrypto.ts
Normal file
87
packages/encryption/src/Domain/Operator/004/MockedCrypto.ts
Normal 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
|
||||
}
|
||||
@@ -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',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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(':')
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface HashingKey {
|
||||
key: string
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { HexString } from '@standardnotes/sncrypto-common'
|
||||
|
||||
export type AsymmetricDecryptResult = {
|
||||
plaintext: HexString
|
||||
signatureVerified: boolean
|
||||
signaturePublicKey: string
|
||||
senderPublicKey: string
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export type AsymmetricSignatureVerificationDetachedResult =
|
||||
| {
|
||||
signatureVerified: true
|
||||
signaturePublicKey: string
|
||||
senderPublicKey: string
|
||||
}
|
||||
| {
|
||||
signatureVerified: false
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type PublicKeySet = {
|
||||
encryption: string
|
||||
signing: string
|
||||
}
|
||||
4
packages/encryption/src/Domain/Operator/Types/Types.ts
Normal file
4
packages/encryption/src/Domain/Operator/Types/Types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Base64String } from '@standardnotes/sncrypto-common'
|
||||
|
||||
export type AsymmetricallyEncryptedString = Base64String
|
||||
export type SymmetricallyEncryptedString = Base64String
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -2,5 +2,6 @@ import { DecryptedPayloadInterface, EncryptedPayloadInterface } from '@standardn
|
||||
|
||||
export interface EncryptionTypeSplit<T = EncryptedPayloadInterface | DecryptedPayloadInterface> {
|
||||
rootKeyEncryption?: T[]
|
||||
keySystemRootKeyEncryption?: T[]
|
||||
itemsKeyEncryption?: T[]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ItemContent, PersistentSignatureData } from '@standardnotes/models'
|
||||
|
||||
export type DecryptedParameters<C extends ItemContent = ItemContent> = {
|
||||
uuid: string
|
||||
content: C
|
||||
signatureData: PersistentSignatureData
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { ItemAuthenticatedData } from './ItemAuthenticatedData'
|
||||
|
||||
/** Authenticated data for payloads encrypted with a key system root key */
|
||||
export type KeySystemRootKeyEncryptedAuthenticatedData = ItemAuthenticatedData
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user