diff --git a/packages/api/src/Domain/Client/User/UserApiService.ts b/packages/api/src/Domain/Client/User/UserApiService.ts index f5e076325..b116f169c 100644 --- a/packages/api/src/Domain/Client/User/UserApiService.ts +++ b/packages/api/src/Domain/Client/User/UserApiService.ts @@ -1,5 +1,4 @@ import { RootKeyParamsInterface } from '@standardnotes/models' - import { ErrorMessage } from '../../Error/ErrorMessage' import { ApiCallError } from '../../Error/ApiCallError' import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse' diff --git a/packages/encryption/src/Domain/Algorithm.ts b/packages/encryption/src/Domain/Algorithm.ts index e9875bb04..9605320a9 100644 --- a/packages/encryption/src/Domain/Algorithm.ts +++ b/packages/encryption/src/Domain/Algorithm.ts @@ -44,3 +44,8 @@ export enum V004Algorithm { EncryptionKeyLength = 256, EncryptionNonceLength = 192, } + +export enum V005Algorithm { + AsymmetricEncryptionNonceLength = 192, + SymmetricEncryptionNonceLength = 192, +} diff --git a/packages/encryption/src/Domain/Operator/004/Operator004.ts b/packages/encryption/src/Domain/Operator/004/Operator004.ts index ce226a087..aa2a0975d 100644 --- a/packages/encryption/src/Domain/Operator/004/Operator004.ts +++ b/packages/encryption/src/Domain/Operator/004/Operator004.ts @@ -254,21 +254,21 @@ export class SNProtocolOperator004 implements SynchronousOperator { encrypted: EncryptedParameters, key: ItemsKeyInterface | SNRootKey, ): DecryptedParameters | ErrorDecryptingParameters { - const itemKeyComponents = this.deconstructEncryptedPayloadString(encrypted.enc_item_key) - const authenticatedData = this.stringToAuthenticatedData(itemKeyComponents.authenticatedData, { + const contentKeyComponents = this.deconstructEncryptedPayloadString(encrypted.enc_item_key) + const authenticatedData = this.stringToAuthenticatedData(contentKeyComponents.authenticatedData, { u: encrypted.uuid, v: encrypted.version, }) const useAuthenticatedString = this.authenticatedDataToString(authenticatedData) - const itemKey = this.decryptString004( - itemKeyComponents.ciphertext, + const contentKey = this.decryptString004( + contentKeyComponents.ciphertext, key.itemsKey, - itemKeyComponents.nonce, + contentKeyComponents.nonce, useAuthenticatedString, ) - if (!itemKey) { + if (!contentKey) { console.error('Error decrypting itemKey parameters', encrypted) return { uuid: encrypted.uuid, @@ -279,10 +279,11 @@ export class SNProtocolOperator004 implements SynchronousOperator { const contentComponents = this.deconstructEncryptedPayloadString(encrypted.content) const content = this.decryptString004( contentComponents.ciphertext, - itemKey, + contentKey, contentComponents.nonce, useAuthenticatedString, ) + if (!content) { return { uuid: encrypted.uuid, @@ -305,6 +306,7 @@ export class SNProtocolOperator004 implements SynchronousOperator { V004Algorithm.ArgonMemLimit, V004Algorithm.ArgonOutputKeyBytes, ) + const partitions = Utils.splitString(derivedKey, 2) const masterKey = partitions[0] const serverPassword = partitions[1] diff --git a/packages/encryption/src/Domain/Operator/005/Operator005.spec.ts b/packages/encryption/src/Domain/Operator/005/Operator005.spec.ts new file mode 100644 index 000000000..50ce6cb2b --- /dev/null +++ b/packages/encryption/src/Domain/Operator/005/Operator005.spec.ts @@ -0,0 +1,75 @@ +import { ProtocolOperator005 } from './Operator005' +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' + +describe('operator 005', () => { + let crypto: PureCryptoInterface + let operator: ProtocolOperator005 + + beforeEach(() => { + crypto = {} as jest.Mocked + crypto.generateRandomKey = jest.fn().mockImplementation(() => { + return 'random-string' + }) + crypto.xchacha20Encrypt = jest.fn().mockImplementation((text: string) => { + return `${text}` + }) + crypto.xchacha20Decrypt = jest.fn().mockImplementation((text: string) => { + return text.split('')[1] + }) + crypto.sodiumCryptoBoxGenerateKeypair = jest.fn().mockImplementation(() => { + return { privateKey: 'private-key', publicKey: 'public-key', keyType: 'x25519' } + }) + crypto.sodiumCryptoBoxEasyEncrypt = jest.fn().mockImplementation((text: string) => { + return `${text}` + }) + crypto.sodiumCryptoBoxEasyDecrypt = jest.fn().mockImplementation((text: string) => { + return text.split('')[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:foo`) + }) + + 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:${keypair.privateKey}`) + }) + + 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) + }) +}) diff --git a/packages/encryption/src/Domain/Operator/005/Operator005.ts b/packages/encryption/src/Domain/Operator/005/Operator005.ts new file mode 100644 index 000000000..d2d743a21 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/005/Operator005.ts @@ -0,0 +1,80 @@ +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) + } +} diff --git a/packages/encryption/src/Domain/Service/Encryption/EncryptionProvider.ts b/packages/encryption/src/Domain/Service/Encryption/EncryptionProviderInterface.ts similarity index 97% rename from packages/encryption/src/Domain/Service/Encryption/EncryptionProvider.ts rename to packages/encryption/src/Domain/Service/Encryption/EncryptionProviderInterface.ts index 9cca1a5d5..89516d228 100644 --- a/packages/encryption/src/Domain/Service/Encryption/EncryptionProvider.ts +++ b/packages/encryption/src/Domain/Service/Encryption/EncryptionProviderInterface.ts @@ -11,7 +11,7 @@ import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams' import { KeyedDecryptionSplit } from '../../Split/KeyedDecryptionSplit' import { KeyedEncryptionSplit } from '../../Split/KeyedEncryptionSplit' -export interface EncryptionProvider { +export interface EncryptionProviderInterface { encryptSplitSingle(split: KeyedEncryptionSplit): Promise encryptSplit(split: KeyedEncryptionSplit): Promise diff --git a/packages/encryption/src/Domain/index.ts b/packages/encryption/src/Domain/index.ts index 272a7e3fe..a6e96cbd8 100644 --- a/packages/encryption/src/Domain/index.ts +++ b/packages/encryption/src/Domain/index.ts @@ -14,11 +14,12 @@ 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/Functions' export * from './Operator/Operator' export * from './Operator/OperatorManager' export * from './Operator/OperatorWrapper' -export * from './Service/Encryption/EncryptionProvider' +export * from './Service/Encryption/EncryptionProviderInterface' export * from './Service/Functions' export * from './Service/RootKey/KeyMode' export * from './Service/RootKey/RootKeyServiceEvent' diff --git a/packages/mobile/src/Lib/ReactNativeCrypto.ts b/packages/mobile/src/Lib/ReactNativeCrypto.ts index 0fb1e3674..8cae981a4 100644 --- a/packages/mobile/src/Lib/ReactNativeCrypto.ts +++ b/packages/mobile/src/Lib/ReactNativeCrypto.ts @@ -1,6 +1,7 @@ import { Base64String, HexString, + PkcKeyPair, PureCryptoInterface, SodiumConstant, StreamDecryptorResult, @@ -129,6 +130,28 @@ export class SNReactNativeCrypto implements PureCryptoInterface { } } + public sodiumCryptoBoxEasyEncrypt( + _message: Utf8String, + _nonce: HexString, + _senderSecretKey: HexString, + _recipientPublicKey: HexString, + ): Base64String { + throw new Error('Not implemented') + } + + public sodiumCryptoBoxEasyDecrypt( + _ciphertext: Base64String, + _nonce: HexString, + _senderPublicKey: HexString, + _recipientSecretKey: HexString, + ): Utf8String { + throw new Error('Not implemented') + } + + public sodiumCryptoBoxGenerateKeypair(): PkcKeyPair { + throw new Error('Not implemented') + } + public generateUUID() { const randomBuf = Sodium.randombytes_buf(16) const tempBuf = new Uint8Array(randomBuf.length / 2) diff --git a/packages/models/src/Domain/Abstract/Item/Implementations/DecryptedItem.ts b/packages/models/src/Domain/Abstract/Item/Implementations/DecryptedItem.ts index 21740cd11..d45cab07d 100644 --- a/packages/models/src/Domain/Abstract/Item/Implementations/DecryptedItem.ts +++ b/packages/models/src/Domain/Abstract/Item/Implementations/DecryptedItem.ts @@ -24,11 +24,11 @@ export class DecryptedItem constructor(payload: DecryptedPayloadInterface) { super(payload) - this.conflictOf = payload.content.conflict_of const userModVal = this.getAppDomainValueWithDefault(AppDataField.UserModifiedDate, this.serverUpdatedAt || 0) - this.userModifiedDate = new Date(userModVal as number | Date) + + this.conflictOf = payload.content.conflict_of this.updatedAtString = dateToLocalizedString(this.userModifiedDate) this.protected = useBoolean(this.payload.content.protected, false) this.trashed = useBoolean(this.payload.content.trashed, false) diff --git a/packages/services/src/Domain/Backups/BackupService.ts b/packages/services/src/Domain/Backups/BackupService.ts index 9bbb6ca96..91c1a3495 100644 --- a/packages/services/src/Domain/Backups/BackupService.ts +++ b/packages/services/src/Domain/Backups/BackupService.ts @@ -1,5 +1,5 @@ import { ContentType, Uuid } from '@standardnotes/common' -import { EncryptionProvider } from '@standardnotes/encryption' +import { EncryptionProviderInterface } from '@standardnotes/encryption' import { PayloadEmitSource, FileItem, CreateEncryptedBackupFileContextPayload } from '@standardnotes/models' import { ClientDisplayableError } from '@standardnotes/responses' import { FilesApiInterface, FileBackupMetadataFile, FileBackupsDevice, FileBackupsMapping } from '@standardnotes/files' @@ -15,7 +15,7 @@ export class FilesBackupService extends AbstractService { constructor( private items: ItemManagerInterface, private api: FilesApiInterface, - private encryptor: EncryptionProvider, + private encryptor: EncryptionProviderInterface, private device: FileBackupsDevice, private status: StatusServiceInterface, protected override internalEventBus: InternalEventBusInterface, diff --git a/packages/services/src/Domain/Encryption/EncryptionService.ts b/packages/services/src/Domain/Encryption/EncryptionService.ts index e994198d3..bb1da6216 100644 --- a/packages/services/src/Domain/Encryption/EncryptionService.ts +++ b/packages/services/src/Domain/Encryption/EncryptionService.ts @@ -4,7 +4,7 @@ import { DecryptedParameters, EncryptedParameters, encryptedParametersFromPayload, - EncryptionProvider, + EncryptionProviderInterface, ErrorDecryptingParameters, findDefaultItemsKey, FindPayloadInDecryptionSplit, @@ -100,7 +100,7 @@ import { EncryptionServiceEvent } from './EncryptionServiceEvent' * It also exposes public methods that allows consumers to retrieve an items key * for a particular payload, and also retrieve all available items keys. */ -export class EncryptionService extends AbstractService implements EncryptionProvider { +export class EncryptionService extends AbstractService implements EncryptionProviderInterface { private operatorManager: OperatorManager private readonly itemsEncryption: ItemsEncryptionService private readonly rootKeyEncryption: RootKeyEncryptionService @@ -714,7 +714,7 @@ export class EncryptionService extends AbstractService i await this.rootKeyEncryption.createNewDefaultItemsKey() } - this.syncUnsycnedItemsKeys() + this.syncUnsyncedItemsKeys() } private async handleFullSyncCompletion() { @@ -734,7 +734,7 @@ export class EncryptionService extends AbstractService i * items key never syncing to the account even though it is being used to encrypt synced items. * Until we can determine its cause, this corrective function will find any such keys and sync them. */ - private syncUnsycnedItemsKeys(): void { + private syncUnsyncedItemsKeys(): void { if (!this.hasAccount()) { return } diff --git a/packages/services/src/Domain/Encryption/Functions.ts b/packages/services/src/Domain/Encryption/Functions.ts index c3c5105f7..41e464e91 100644 --- a/packages/services/src/Domain/Encryption/Functions.ts +++ b/packages/services/src/Domain/Encryption/Functions.ts @@ -5,7 +5,7 @@ import { ItemsKeyContent, RootKeyInterface, } from '@standardnotes/models' -import { EncryptionProvider, KeyRecoveryStrings, SNRootKeyParams } from '@standardnotes/encryption' +import { EncryptionProviderInterface, KeyRecoveryStrings, SNRootKeyParams } from '@standardnotes/encryption' import { ChallengeServiceInterface } from '../Challenge/ChallengeServiceInterface' import { ChallengePrompt } from '../Challenge/Prompt/ChallengePrompt' import { ChallengeReason } from '../Challenge/Types/ChallengeReason' @@ -13,7 +13,7 @@ import { ChallengeValidation } from '../Challenge/Types/ChallengeValidation' export async function DecryptItemsKeyWithUserFallback( itemsKey: EncryptedPayloadInterface, - encryptor: EncryptionProvider, + encryptor: EncryptionProviderInterface, challengor: ChallengeServiceInterface, ): Promise | 'failed' | 'aborted'> { const decryptionResult = await encryptor.decryptSplitSingle({ @@ -37,7 +37,7 @@ export async function DecryptItemsKeyWithUserFallback( export async function DecryptItemsKeyByPromptingUser( itemsKey: EncryptedPayloadInterface, - encryptor: EncryptionProvider, + encryptor: EncryptionProviderInterface, challengor: ChallengeServiceInterface, keyParams?: SNRootKeyParams, ): Promise< diff --git a/packages/services/src/Domain/Files/FileService.spec.ts b/packages/services/src/Domain/Files/FileService.spec.ts index c967e0209..87f9cdedb 100644 --- a/packages/services/src/Domain/Files/FileService.spec.ts +++ b/packages/services/src/Domain/Files/FileService.spec.ts @@ -1,6 +1,6 @@ import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common' import { FileItem } from '@standardnotes/models' -import { EncryptionProvider } from '@standardnotes/encryption' +import { EncryptionProviderInterface } from '@standardnotes/encryption' import { ItemManagerInterface } from '../Item/ItemManagerInterface' import { ChallengeServiceInterface } from '../Challenge' @@ -19,7 +19,7 @@ describe('fileService', () => { let crypto: PureCryptoInterface let challengor: ChallengeServiceInterface let fileService: FileService - let encryptor: EncryptionProvider + let encryptor: EncryptionProviderInterface let internalEventBus: InternalEventBusInterface beforeEach(() => { @@ -41,7 +41,7 @@ describe('fileService', () => { syncService = {} as jest.Mocked syncService.sync = jest.fn() - encryptor = {} as jest.Mocked + encryptor = {} as jest.Mocked alertService = {} as jest.Mocked alertService.confirm = jest.fn().mockReturnValue(true) diff --git a/packages/services/src/Domain/Files/FileService.ts b/packages/services/src/Domain/Files/FileService.ts index e7320ecf7..c5bf1046c 100644 --- a/packages/services/src/Domain/Files/FileService.ts +++ b/packages/services/src/Domain/Files/FileService.ts @@ -12,7 +12,7 @@ import { } from '@standardnotes/models' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { UuidGenerator } from '@standardnotes/utils' -import { EncryptionProvider, SNItemsKey } from '@standardnotes/encryption' +import { EncryptionProviderInterface, SNItemsKey } from '@standardnotes/encryption' import { DownloadAndDecryptFileOperation, EncryptAndUploadFileOperation, @@ -49,7 +49,7 @@ export class FileService extends AbstractService implements FilesClientInterface private api: FilesApiInterface, private itemManager: ItemManagerInterface, private syncService: SyncServiceInterface, - private encryptor: EncryptionProvider, + private encryptor: EncryptionProviderInterface, private challengor: ChallengeServiceInterface, private alertService: AlertService, private crypto: PureCryptoInterface, diff --git a/packages/sncrypto-common/src/Common/PureCryptoInterface.ts b/packages/sncrypto-common/src/Common/PureCryptoInterface.ts index f6221a58e..595b1656e 100644 --- a/packages/sncrypto-common/src/Common/PureCryptoInterface.ts +++ b/packages/sncrypto-common/src/Common/PureCryptoInterface.ts @@ -1,3 +1,4 @@ +import { PkcKeyPair } from '../Types' import { Base64String } from '../Types/Base64String' import { Base64URLSafeString } from '../Types/Base64URLSafeString' import { HexString } from '../Types/HexString' @@ -27,7 +28,7 @@ export interface PureCryptoInterface { * @param bits - Length of key in bits * @returns A string key in hex format */ - generateRandomKey(bits: number): string + generateRandomKey(bits: number): HexString /** * @legacy @@ -98,7 +99,7 @@ export interface PureCryptoInterface { * @param assocData * @returns Base64 ciphertext string */ - xchacha20Encrypt(plaintext: Utf8String, nonce: HexString, key: HexString, assocData: Utf8String): Base64String + xchacha20Encrypt(plaintext: Utf8String, nonce: HexString, key: HexString, assocData?: Utf8String): Base64String /** * Decrypt a message (and associated data) with XChaCha20-Poly1305 @@ -112,7 +113,7 @@ export interface PureCryptoInterface { ciphertext: Base64String, nonce: HexString, key: HexString, - assocData: Utf8String | Uint8Array, + assocData?: Utf8String | Uint8Array, ): Utf8String | null xchacha20StreamInitEncryptor(key: HexString): StreamEncryptor @@ -132,6 +133,22 @@ export interface PureCryptoInterface { assocData: Utf8String, ): { message: Uint8Array; tag: SodiumConstant } | false + sodiumCryptoBoxEasyEncrypt( + message: Utf8String, + nonce: HexString, + senderSecretKey: HexString, + recipientPublicKey: HexString, + ): Base64String + + sodiumCryptoBoxEasyDecrypt( + ciphertext: Base64String, + nonce: HexString, + senderPublicKey: HexString, + recipientSecretKey: HexString, + ): Utf8String + + sodiumCryptoBoxGenerateKeypair(): PkcKeyPair + /** * Converts a plain string into base64 * @param text - A plain string diff --git a/packages/sncrypto-web/src/crypto.ts b/packages/sncrypto-web/src/crypto.ts index 2114409c2..e58783863 100644 --- a/packages/sncrypto-web/src/crypto.ts +++ b/packages/sncrypto-web/src/crypto.ts @@ -93,7 +93,7 @@ export class SNWebCrypto implements PureCryptoInterface { return this.webCryptoDeriveBits(key, salt, iterations, length) } - public generateRandomKey(bits: number): string { + public generateRandomKey(bits: number): HexString { const bytes = bits / 8 const arrayBuffer = Utils.getGlobalScope().crypto.getRandomValues(new Uint8Array(bytes)) return Utils.arrayBufferToHexString(arrayBuffer) @@ -249,14 +249,14 @@ export class SNWebCrypto implements PureCryptoInterface { plaintext: Utf8String, nonce: HexString, key: HexString, - assocData: Utf8String, + assocData?: Utf8String, ): Base64String { if (nonce.length !== 48) { throw Error('Nonce must be 24 bytes') } const arrayBuffer = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt( plaintext, - assocData, + assocData || null, null, Utils.hexStringToArrayBuffer(nonce), Utils.hexStringToArrayBuffer(key), @@ -268,7 +268,7 @@ export class SNWebCrypto implements PureCryptoInterface { ciphertext: Base64String, nonce: HexString, key: HexString, - assocData: Utf8String | Uint8Array, + assocData?: Utf8String | Uint8Array, ): Utf8String | null { if (nonce.length !== 48) { throw Error('Nonce must be 24 bytes') @@ -277,7 +277,7 @@ export class SNWebCrypto implements PureCryptoInterface { return sodium.crypto_aead_xchacha20poly1305_ietf_decrypt( null, Utils.base64ToArrayBuffer(ciphertext), - assocData, + assocData || null, Utils.hexStringToArrayBuffer(nonce), Utils.hexStringToArrayBuffer(key), 'text', @@ -368,7 +368,7 @@ export class SNWebCrypto implements PureCryptoInterface { nonce: HexString, senderPublicKey: HexString, recipientSecretKey: HexString, - ): Base64String { + ): Utf8String { const result = sodium.crypto_box_open_easy( Utils.base64ToArrayBuffer(ciphertext), Utils.hexStringToArrayBuffer(nonce), diff --git a/packages/snjs/lib/Services/KeyRecovery/KeyRecoveryOperation.ts b/packages/snjs/lib/Services/KeyRecovery/KeyRecoveryOperation.ts index ec733c4b7..39c8af90a 100644 --- a/packages/snjs/lib/Services/KeyRecovery/KeyRecoveryOperation.ts +++ b/packages/snjs/lib/Services/KeyRecovery/KeyRecoveryOperation.ts @@ -1,7 +1,7 @@ import { ContentType } from '@standardnotes/common' import { ItemsKeyInterface } from '@standardnotes/models' import { dateSorted } from '@standardnotes/utils' -import { SNRootKeyParams, EncryptionProvider } from '@standardnotes/encryption' +import { SNRootKeyParams, EncryptionProviderInterface } from '@standardnotes/encryption' import { DecryptionQueueItem, KeyRecoveryOperationResult } from './Types' import { serverKeyParamsAreSafe } from './Utils' import { ChallengeServiceInterface, DecryptItemsKeyByPromptingUser } from '@standardnotes/services' @@ -11,7 +11,7 @@ export class KeyRecoveryOperation { constructor( private queueItem: DecryptionQueueItem, private itemManager: ItemManager, - private protocolService: EncryptionProvider, + private protocolService: EncryptionProviderInterface, private challengeService: ChallengeServiceInterface, private clientParams: SNRootKeyParams | undefined, private serverParams: SNRootKeyParams | undefined, diff --git a/packages/snjs/lib/Services/Mutator/MutatorService.ts b/packages/snjs/lib/Services/Mutator/MutatorService.ts index 2153df6bb..9d6878226 100644 --- a/packages/snjs/lib/Services/Mutator/MutatorService.ts +++ b/packages/snjs/lib/Services/Mutator/MutatorService.ts @@ -8,7 +8,7 @@ import { ChallengeReason, MutatorClientInterface, } from '@standardnotes/services' -import { EncryptionProvider } from '@standardnotes/encryption' +import { EncryptionProviderInterface } from '@standardnotes/encryption' import { ClientDisplayableError } from '@standardnotes/responses' import { ContentType, ProtocolVersion, compareVersions } from '@standardnotes/common' import { ItemManager } from '../Items' @@ -49,7 +49,7 @@ export class MutatorService extends AbstractService implements MutatorClientInte private itemManager: ItemManager, private syncService: SNSyncService, private protectionService: SNProtectionService, - private encryption: EncryptionProvider, + private encryption: EncryptionProviderInterface, private payloadManager: PayloadManager, private challengeService: ChallengeService, private componentManager: SNComponentManager, diff --git a/packages/snjs/lib/Services/Storage/DiskStorageService.ts b/packages/snjs/lib/Services/Storage/DiskStorageService.ts index b7f6f6bd1..9620abc57 100644 --- a/packages/snjs/lib/Services/Storage/DiskStorageService.ts +++ b/packages/snjs/lib/Services/Storage/DiskStorageService.ts @@ -33,7 +33,7 @@ import { * key can decrypt wrapped storage. */ export class DiskStorageService extends Services.AbstractService implements Services.StorageServiceInterface { - private encryptionProvider!: Encryption.EncryptionProvider + private encryptionProvider!: Encryption.EncryptionProviderInterface private storagePersistable = false private persistencePolicy!: Services.StoragePersistencePolicies private encryptionPolicy!: Services.StorageEncryptionPolicy @@ -53,7 +53,7 @@ export class DiskStorageService extends Services.AbstractService implements Serv void this.setEncryptionPolicy(Services.StorageEncryptionPolicy.Default, false) } - public provideEncryptionProvider(provider: Encryption.EncryptionProvider): void { + public provideEncryptionProvider(provider: Encryption.EncryptionProviderInterface): void { this.encryptionProvider = provider } diff --git a/packages/snjs/mocha/auth.test.js b/packages/snjs/mocha/auth.test.js index 3b93aa0f2..00019476d 100644 --- a/packages/snjs/mocha/auth.test.js +++ b/packages/snjs/mocha/auth.test.js @@ -39,12 +39,14 @@ describe('basic auth', function () { let error = null try { await this.application.register(this.email, password) - } catch(caughtError) { + } catch (caughtError) { error = caughtError } - expect(error.message).to.equal('Your password must be at least 8 characters in length. ' - + 'For your security, please choose a longer password or, ideally, a passphrase, and try again.') + expect(error.message).to.equal( + 'Your password must be at least 8 characters in length. ' + + 'For your security, please choose a longer password or, ideally, a passphrase, and try again.', + ) expect(await this.application.protocolService.getRootKey()).to.not.be.ok }) diff --git a/packages/snjs/mocha/lib/fake_web_crypto.js b/packages/snjs/mocha/lib/fake_web_crypto.js index 046b19b7e..f290a9e30 100644 --- a/packages/snjs/mocha/lib/fake_web_crypto.js +++ b/packages/snjs/mocha/lib/fake_web_crypto.js @@ -69,7 +69,8 @@ export default class FakeWebCrypto { } generateRandomKey(bits) { - const length = bits / 8 + const bitsPerHexChar = 4 + const length = bits / bitsPerHexChar return this.randomString(length) } @@ -107,7 +108,13 @@ export default class FakeWebCrypto { } argon2(password, salt, iterations, bytes, length) { - return btoa(password) + const bitsPerHexChar = 4 + const bitsInByte = 8 + const encoded = btoa(password) + const desiredLength = length * (bitsInByte / bitsPerHexChar) + const missingLength = desiredLength - encoded.length + const result = `${encoded}${encoded.repeat(Math.ceil(missingLength / encoded.length))}`.slice(0, desiredLength) + return result } xchacha20Encrypt(plaintext, nonce, key, assocData) { @@ -128,6 +135,33 @@ export default class FakeWebCrypto { return data.plaintext } + sodiumCryptoBoxEasyEncrypt(message, nonce, senderSecretKey, recipientPublicKey) { + const data = { + message, + nonce, + senderSecretKey, + recipientPublicKey, + } + return btoa(JSON.stringify(data)) + } + + sodiumCryptoBoxEasyDecrypt(ciphertext, nonce, senderPublicKey, recipientSecretKey) { + const data = JSON.parse(atob(ciphertext)) + if ( + data.senderPublicKey !== senderPublicKey || + data.recipientSecretKey !== recipientSecretKey || + data.nonce !== nonce || + data.assocData !== assocData + ) { + return undefined + } + return data.message + } + + sodiumCryptoBoxGenerateKeypair() { + return { publicKey: this.randomString(64), privateKey: this.randomString(64), keyType: 'x25519' } + } + generateOtpSecret() { return 'WQVV2GFBRQWU3UQZWQFZC37PSNRXKTA6' }