Skip to content

Commit

Permalink
feat: experimental 005 operator (#1753)
Browse files Browse the repository at this point in the history
  • Loading branch information
moughxyz committed Oct 6, 2022
1 parent c13dd88 commit cbbe913
Show file tree
Hide file tree
Showing 21 changed files with 284 additions and 46 deletions.
1 change: 0 additions & 1 deletion packages/api/src/Domain/Client/User/UserApiService.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
5 changes: 5 additions & 0 deletions packages/encryption/src/Domain/Algorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ export enum V004Algorithm {
EncryptionKeyLength = 256,
EncryptionNonceLength = 192,
}

export enum V005Algorithm {
AsymmetricEncryptionNonceLength = 192,
SymmetricEncryptionNonceLength = 192,
}
16 changes: 9 additions & 7 deletions packages/encryption/src/Domain/Operator/004/Operator004.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,21 +254,21 @@ export class SNProtocolOperator004 implements SynchronousOperator {
encrypted: EncryptedParameters,
key: ItemsKeyInterface | SNRootKey,
): DecryptedParameters<C> | 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,
Expand All @@ -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,
Expand All @@ -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]
Expand Down
75 changes: 75 additions & 0 deletions packages/encryption/src/Domain/Operator/005/Operator005.spec.ts
Original file line number Diff line number Diff line change
@@ -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<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)
})
})
80 changes: 80 additions & 0 deletions packages/encryption/src/Domain/Operator/005/Operator005.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<EncryptedPayloadInterface>

encryptSplit(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface[]>
Expand Down
3 changes: 2 additions & 1 deletion packages/encryption/src/Domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
23 changes: 23 additions & 0 deletions packages/mobile/src/Lib/ReactNativeCrypto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Base64String,
HexString,
PkcKeyPair,
PureCryptoInterface,
SodiumConstant,
StreamDecryptorResult,
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export class DecryptedItem<C extends ItemContent = ItemContent>

constructor(payload: DecryptedPayloadInterface<C>) {
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)
Expand Down
4 changes: 2 additions & 2 deletions packages/services/src/Domain/Backups/BackupService.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions packages/services/src/Domain/Encryption/EncryptionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
DecryptedParameters,
EncryptedParameters,
encryptedParametersFromPayload,
EncryptionProvider,
EncryptionProviderInterface,
ErrorDecryptingParameters,
findDefaultItemsKey,
FindPayloadInDecryptionSplit,
Expand Down Expand Up @@ -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<EncryptionServiceEvent> implements EncryptionProvider {
export class EncryptionService extends AbstractService<EncryptionServiceEvent> implements EncryptionProviderInterface {
private operatorManager: OperatorManager
private readonly itemsEncryption: ItemsEncryptionService
private readonly rootKeyEncryption: RootKeyEncryptionService
Expand Down Expand Up @@ -714,7 +714,7 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
await this.rootKeyEncryption.createNewDefaultItemsKey()
}

this.syncUnsycnedItemsKeys()
this.syncUnsyncedItemsKeys()
}

private async handleFullSyncCompletion() {
Expand All @@ -734,7 +734,7 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> 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
}
Expand Down
6 changes: 3 additions & 3 deletions packages/services/src/Domain/Encryption/Functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ 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'
import { ChallengeValidation } from '../Challenge/Types/ChallengeValidation'

export async function DecryptItemsKeyWithUserFallback(
itemsKey: EncryptedPayloadInterface,
encryptor: EncryptionProvider,
encryptor: EncryptionProviderInterface,
challengor: ChallengeServiceInterface,
): Promise<DecryptedPayloadInterface<ItemsKeyContent> | 'failed' | 'aborted'> {
const decryptionResult = await encryptor.decryptSplitSingle<ItemsKeyContent>({
Expand All @@ -37,7 +37,7 @@ export async function DecryptItemsKeyWithUserFallback(

export async function DecryptItemsKeyByPromptingUser(
itemsKey: EncryptedPayloadInterface,
encryptor: EncryptionProvider,
encryptor: EncryptionProviderInterface,
challengor: ChallengeServiceInterface,
keyParams?: SNRootKeyParams,
): Promise<
Expand Down
6 changes: 3 additions & 3 deletions packages/services/src/Domain/Files/FileService.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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(() => {
Expand All @@ -41,7 +41,7 @@ describe('fileService', () => {
syncService = {} as jest.Mocked<SyncServiceInterface>
syncService.sync = jest.fn()

encryptor = {} as jest.Mocked<EncryptionProvider>
encryptor = {} as jest.Mocked<EncryptionProviderInterface>

alertService = {} as jest.Mocked<AlertService>
alertService.confirm = jest.fn().mockReturnValue(true)
Expand Down

0 comments on commit cbbe913

Please sign in to comment.