feat: experimental 005 operator (#1753)
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import { RootKeyParamsInterface } from '@standardnotes/models'
|
import { RootKeyParamsInterface } from '@standardnotes/models'
|
||||||
|
|
||||||
import { ErrorMessage } from '../../Error/ErrorMessage'
|
import { ErrorMessage } from '../../Error/ErrorMessage'
|
||||||
import { ApiCallError } from '../../Error/ApiCallError'
|
import { ApiCallError } from '../../Error/ApiCallError'
|
||||||
import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse'
|
import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse'
|
||||||
|
|||||||
@@ -44,3 +44,8 @@ export enum V004Algorithm {
|
|||||||
EncryptionKeyLength = 256,
|
EncryptionKeyLength = 256,
|
||||||
EncryptionNonceLength = 192,
|
EncryptionNonceLength = 192,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum V005Algorithm {
|
||||||
|
AsymmetricEncryptionNonceLength = 192,
|
||||||
|
SymmetricEncryptionNonceLength = 192,
|
||||||
|
}
|
||||||
|
|||||||
@@ -254,21 +254,21 @@ export class SNProtocolOperator004 implements SynchronousOperator {
|
|||||||
encrypted: EncryptedParameters,
|
encrypted: EncryptedParameters,
|
||||||
key: ItemsKeyInterface | SNRootKey,
|
key: ItemsKeyInterface | SNRootKey,
|
||||||
): DecryptedParameters<C> | ErrorDecryptingParameters {
|
): DecryptedParameters<C> | ErrorDecryptingParameters {
|
||||||
const itemKeyComponents = this.deconstructEncryptedPayloadString(encrypted.enc_item_key)
|
const contentKeyComponents = this.deconstructEncryptedPayloadString(encrypted.enc_item_key)
|
||||||
const authenticatedData = this.stringToAuthenticatedData(itemKeyComponents.authenticatedData, {
|
const authenticatedData = this.stringToAuthenticatedData(contentKeyComponents.authenticatedData, {
|
||||||
u: encrypted.uuid,
|
u: encrypted.uuid,
|
||||||
v: encrypted.version,
|
v: encrypted.version,
|
||||||
})
|
})
|
||||||
|
|
||||||
const useAuthenticatedString = this.authenticatedDataToString(authenticatedData)
|
const useAuthenticatedString = this.authenticatedDataToString(authenticatedData)
|
||||||
const itemKey = this.decryptString004(
|
const contentKey = this.decryptString004(
|
||||||
itemKeyComponents.ciphertext,
|
contentKeyComponents.ciphertext,
|
||||||
key.itemsKey,
|
key.itemsKey,
|
||||||
itemKeyComponents.nonce,
|
contentKeyComponents.nonce,
|
||||||
useAuthenticatedString,
|
useAuthenticatedString,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!itemKey) {
|
if (!contentKey) {
|
||||||
console.error('Error decrypting itemKey parameters', encrypted)
|
console.error('Error decrypting itemKey parameters', encrypted)
|
||||||
return {
|
return {
|
||||||
uuid: encrypted.uuid,
|
uuid: encrypted.uuid,
|
||||||
@@ -279,10 +279,11 @@ export class SNProtocolOperator004 implements SynchronousOperator {
|
|||||||
const contentComponents = this.deconstructEncryptedPayloadString(encrypted.content)
|
const contentComponents = this.deconstructEncryptedPayloadString(encrypted.content)
|
||||||
const content = this.decryptString004(
|
const content = this.decryptString004(
|
||||||
contentComponents.ciphertext,
|
contentComponents.ciphertext,
|
||||||
itemKey,
|
contentKey,
|
||||||
contentComponents.nonce,
|
contentComponents.nonce,
|
||||||
useAuthenticatedString,
|
useAuthenticatedString,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return {
|
return {
|
||||||
uuid: encrypted.uuid,
|
uuid: encrypted.uuid,
|
||||||
@@ -305,6 +306,7 @@ export class SNProtocolOperator004 implements SynchronousOperator {
|
|||||||
V004Algorithm.ArgonMemLimit,
|
V004Algorithm.ArgonMemLimit,
|
||||||
V004Algorithm.ArgonOutputKeyBytes,
|
V004Algorithm.ArgonOutputKeyBytes,
|
||||||
)
|
)
|
||||||
|
|
||||||
const partitions = Utils.splitString(derivedKey, 2)
|
const partitions = Utils.splitString(derivedKey, 2)
|
||||||
const masterKey = partitions[0]
|
const masterKey = partitions[0]
|
||||||
const serverPassword = partitions[1]
|
const serverPassword = partitions[1]
|
||||||
|
|||||||
@@ -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
packages/encryption/src/Domain/Operator/005/Operator005.ts
Normal file
80
packages/encryption/src/Domain/Operator/005/Operator005.ts
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
|
|||||||
import { KeyedDecryptionSplit } from '../../Split/KeyedDecryptionSplit'
|
import { KeyedDecryptionSplit } from '../../Split/KeyedDecryptionSplit'
|
||||||
import { KeyedEncryptionSplit } from '../../Split/KeyedEncryptionSplit'
|
import { KeyedEncryptionSplit } from '../../Split/KeyedEncryptionSplit'
|
||||||
|
|
||||||
export interface EncryptionProvider {
|
export interface EncryptionProviderInterface {
|
||||||
encryptSplitSingle(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface>
|
encryptSplitSingle(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface>
|
||||||
|
|
||||||
encryptSplit(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface[]>
|
encryptSplit(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface[]>
|
||||||
@@ -14,11 +14,12 @@ export * from './Operator/001/Operator001'
|
|||||||
export * from './Operator/002/Operator002'
|
export * from './Operator/002/Operator002'
|
||||||
export * from './Operator/003/Operator003'
|
export * from './Operator/003/Operator003'
|
||||||
export * from './Operator/004/Operator004'
|
export * from './Operator/004/Operator004'
|
||||||
|
export * from './Operator/005/Operator005'
|
||||||
export * from './Operator/Functions'
|
export * from './Operator/Functions'
|
||||||
export * from './Operator/Operator'
|
export * from './Operator/Operator'
|
||||||
export * from './Operator/OperatorManager'
|
export * from './Operator/OperatorManager'
|
||||||
export * from './Operator/OperatorWrapper'
|
export * from './Operator/OperatorWrapper'
|
||||||
export * from './Service/Encryption/EncryptionProvider'
|
export * from './Service/Encryption/EncryptionProviderInterface'
|
||||||
export * from './Service/Functions'
|
export * from './Service/Functions'
|
||||||
export * from './Service/RootKey/KeyMode'
|
export * from './Service/RootKey/KeyMode'
|
||||||
export * from './Service/RootKey/RootKeyServiceEvent'
|
export * from './Service/RootKey/RootKeyServiceEvent'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Base64String,
|
Base64String,
|
||||||
HexString,
|
HexString,
|
||||||
|
PkcKeyPair,
|
||||||
PureCryptoInterface,
|
PureCryptoInterface,
|
||||||
SodiumConstant,
|
SodiumConstant,
|
||||||
StreamDecryptorResult,
|
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() {
|
public generateUUID() {
|
||||||
const randomBuf = Sodium.randombytes_buf(16)
|
const randomBuf = Sodium.randombytes_buf(16)
|
||||||
const tempBuf = new Uint8Array(randomBuf.length / 2)
|
const tempBuf = new Uint8Array(randomBuf.length / 2)
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ export class DecryptedItem<C extends ItemContent = ItemContent>
|
|||||||
|
|
||||||
constructor(payload: DecryptedPayloadInterface<C>) {
|
constructor(payload: DecryptedPayloadInterface<C>) {
|
||||||
super(payload)
|
super(payload)
|
||||||
this.conflictOf = payload.content.conflict_of
|
|
||||||
|
|
||||||
const userModVal = this.getAppDomainValueWithDefault(AppDataField.UserModifiedDate, this.serverUpdatedAt || 0)
|
const userModVal = this.getAppDomainValueWithDefault(AppDataField.UserModifiedDate, this.serverUpdatedAt || 0)
|
||||||
|
|
||||||
this.userModifiedDate = new Date(userModVal as number | Date)
|
this.userModifiedDate = new Date(userModVal as number | Date)
|
||||||
|
|
||||||
|
this.conflictOf = payload.content.conflict_of
|
||||||
this.updatedAtString = dateToLocalizedString(this.userModifiedDate)
|
this.updatedAtString = dateToLocalizedString(this.userModifiedDate)
|
||||||
this.protected = useBoolean(this.payload.content.protected, false)
|
this.protected = useBoolean(this.payload.content.protected, false)
|
||||||
this.trashed = useBoolean(this.payload.content.trashed, false)
|
this.trashed = useBoolean(this.payload.content.trashed, false)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ContentType, Uuid } from '@standardnotes/common'
|
import { ContentType, Uuid } from '@standardnotes/common'
|
||||||
import { EncryptionProvider } from '@standardnotes/encryption'
|
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||||
import { PayloadEmitSource, FileItem, CreateEncryptedBackupFileContextPayload } from '@standardnotes/models'
|
import { PayloadEmitSource, FileItem, CreateEncryptedBackupFileContextPayload } from '@standardnotes/models'
|
||||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||||
import { FilesApiInterface, FileBackupMetadataFile, FileBackupsDevice, FileBackupsMapping } from '@standardnotes/files'
|
import { FilesApiInterface, FileBackupMetadataFile, FileBackupsDevice, FileBackupsMapping } from '@standardnotes/files'
|
||||||
@@ -15,7 +15,7 @@ export class FilesBackupService extends AbstractService {
|
|||||||
constructor(
|
constructor(
|
||||||
private items: ItemManagerInterface,
|
private items: ItemManagerInterface,
|
||||||
private api: FilesApiInterface,
|
private api: FilesApiInterface,
|
||||||
private encryptor: EncryptionProvider,
|
private encryptor: EncryptionProviderInterface,
|
||||||
private device: FileBackupsDevice,
|
private device: FileBackupsDevice,
|
||||||
private status: StatusServiceInterface,
|
private status: StatusServiceInterface,
|
||||||
protected override internalEventBus: InternalEventBusInterface,
|
protected override internalEventBus: InternalEventBusInterface,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
DecryptedParameters,
|
DecryptedParameters,
|
||||||
EncryptedParameters,
|
EncryptedParameters,
|
||||||
encryptedParametersFromPayload,
|
encryptedParametersFromPayload,
|
||||||
EncryptionProvider,
|
EncryptionProviderInterface,
|
||||||
ErrorDecryptingParameters,
|
ErrorDecryptingParameters,
|
||||||
findDefaultItemsKey,
|
findDefaultItemsKey,
|
||||||
FindPayloadInDecryptionSplit,
|
FindPayloadInDecryptionSplit,
|
||||||
@@ -100,7 +100,7 @@ import { EncryptionServiceEvent } from './EncryptionServiceEvent'
|
|||||||
* It also exposes public methods that allows consumers to retrieve an items key
|
* It also exposes public methods that allows consumers to retrieve an items key
|
||||||
* for a particular payload, and also retrieve all available items keys.
|
* 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 operatorManager: OperatorManager
|
||||||
private readonly itemsEncryption: ItemsEncryptionService
|
private readonly itemsEncryption: ItemsEncryptionService
|
||||||
private readonly rootKeyEncryption: RootKeyEncryptionService
|
private readonly rootKeyEncryption: RootKeyEncryptionService
|
||||||
@@ -714,7 +714,7 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
|||||||
await this.rootKeyEncryption.createNewDefaultItemsKey()
|
await this.rootKeyEncryption.createNewDefaultItemsKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.syncUnsycnedItemsKeys()
|
this.syncUnsyncedItemsKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleFullSyncCompletion() {
|
private async handleFullSyncCompletion() {
|
||||||
@@ -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.
|
* 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.
|
* 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()) {
|
if (!this.hasAccount()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
ItemsKeyContent,
|
ItemsKeyContent,
|
||||||
RootKeyInterface,
|
RootKeyInterface,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
import { EncryptionProvider, KeyRecoveryStrings, SNRootKeyParams } from '@standardnotes/encryption'
|
import { EncryptionProviderInterface, KeyRecoveryStrings, SNRootKeyParams } from '@standardnotes/encryption'
|
||||||
import { ChallengeServiceInterface } from '../Challenge/ChallengeServiceInterface'
|
import { ChallengeServiceInterface } from '../Challenge/ChallengeServiceInterface'
|
||||||
import { ChallengePrompt } from '../Challenge/Prompt/ChallengePrompt'
|
import { ChallengePrompt } from '../Challenge/Prompt/ChallengePrompt'
|
||||||
import { ChallengeReason } from '../Challenge/Types/ChallengeReason'
|
import { ChallengeReason } from '../Challenge/Types/ChallengeReason'
|
||||||
@@ -13,7 +13,7 @@ import { ChallengeValidation } from '../Challenge/Types/ChallengeValidation'
|
|||||||
|
|
||||||
export async function DecryptItemsKeyWithUserFallback(
|
export async function DecryptItemsKeyWithUserFallback(
|
||||||
itemsKey: EncryptedPayloadInterface,
|
itemsKey: EncryptedPayloadInterface,
|
||||||
encryptor: EncryptionProvider,
|
encryptor: EncryptionProviderInterface,
|
||||||
challengor: ChallengeServiceInterface,
|
challengor: ChallengeServiceInterface,
|
||||||
): Promise<DecryptedPayloadInterface<ItemsKeyContent> | 'failed' | 'aborted'> {
|
): Promise<DecryptedPayloadInterface<ItemsKeyContent> | 'failed' | 'aborted'> {
|
||||||
const decryptionResult = await encryptor.decryptSplitSingle<ItemsKeyContent>({
|
const decryptionResult = await encryptor.decryptSplitSingle<ItemsKeyContent>({
|
||||||
@@ -37,7 +37,7 @@ export async function DecryptItemsKeyWithUserFallback(
|
|||||||
|
|
||||||
export async function DecryptItemsKeyByPromptingUser(
|
export async function DecryptItemsKeyByPromptingUser(
|
||||||
itemsKey: EncryptedPayloadInterface,
|
itemsKey: EncryptedPayloadInterface,
|
||||||
encryptor: EncryptionProvider,
|
encryptor: EncryptionProviderInterface,
|
||||||
challengor: ChallengeServiceInterface,
|
challengor: ChallengeServiceInterface,
|
||||||
keyParams?: SNRootKeyParams,
|
keyParams?: SNRootKeyParams,
|
||||||
): Promise<
|
): Promise<
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common'
|
import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common'
|
||||||
import { FileItem } from '@standardnotes/models'
|
import { FileItem } from '@standardnotes/models'
|
||||||
import { EncryptionProvider } from '@standardnotes/encryption'
|
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||||
|
|
||||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||||
import { ChallengeServiceInterface } from '../Challenge'
|
import { ChallengeServiceInterface } from '../Challenge'
|
||||||
@@ -19,7 +19,7 @@ describe('fileService', () => {
|
|||||||
let crypto: PureCryptoInterface
|
let crypto: PureCryptoInterface
|
||||||
let challengor: ChallengeServiceInterface
|
let challengor: ChallengeServiceInterface
|
||||||
let fileService: FileService
|
let fileService: FileService
|
||||||
let encryptor: EncryptionProvider
|
let encryptor: EncryptionProviderInterface
|
||||||
let internalEventBus: InternalEventBusInterface
|
let internalEventBus: InternalEventBusInterface
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -41,7 +41,7 @@ describe('fileService', () => {
|
|||||||
syncService = {} as jest.Mocked<SyncServiceInterface>
|
syncService = {} as jest.Mocked<SyncServiceInterface>
|
||||||
syncService.sync = jest.fn()
|
syncService.sync = jest.fn()
|
||||||
|
|
||||||
encryptor = {} as jest.Mocked<EncryptionProvider>
|
encryptor = {} as jest.Mocked<EncryptionProviderInterface>
|
||||||
|
|
||||||
alertService = {} as jest.Mocked<AlertService>
|
alertService = {} as jest.Mocked<AlertService>
|
||||||
alertService.confirm = jest.fn().mockReturnValue(true)
|
alertService.confirm = jest.fn().mockReturnValue(true)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||||
import { UuidGenerator } from '@standardnotes/utils'
|
import { UuidGenerator } from '@standardnotes/utils'
|
||||||
import { EncryptionProvider, SNItemsKey } from '@standardnotes/encryption'
|
import { EncryptionProviderInterface, SNItemsKey } from '@standardnotes/encryption'
|
||||||
import {
|
import {
|
||||||
DownloadAndDecryptFileOperation,
|
DownloadAndDecryptFileOperation,
|
||||||
EncryptAndUploadFileOperation,
|
EncryptAndUploadFileOperation,
|
||||||
@@ -49,7 +49,7 @@ export class FileService extends AbstractService implements FilesClientInterface
|
|||||||
private api: FilesApiInterface,
|
private api: FilesApiInterface,
|
||||||
private itemManager: ItemManagerInterface,
|
private itemManager: ItemManagerInterface,
|
||||||
private syncService: SyncServiceInterface,
|
private syncService: SyncServiceInterface,
|
||||||
private encryptor: EncryptionProvider,
|
private encryptor: EncryptionProviderInterface,
|
||||||
private challengor: ChallengeServiceInterface,
|
private challengor: ChallengeServiceInterface,
|
||||||
private alertService: AlertService,
|
private alertService: AlertService,
|
||||||
private crypto: PureCryptoInterface,
|
private crypto: PureCryptoInterface,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PkcKeyPair } from '../Types'
|
||||||
import { Base64String } from '../Types/Base64String'
|
import { Base64String } from '../Types/Base64String'
|
||||||
import { Base64URLSafeString } from '../Types/Base64URLSafeString'
|
import { Base64URLSafeString } from '../Types/Base64URLSafeString'
|
||||||
import { HexString } from '../Types/HexString'
|
import { HexString } from '../Types/HexString'
|
||||||
@@ -27,7 +28,7 @@ export interface PureCryptoInterface {
|
|||||||
* @param bits - Length of key in bits
|
* @param bits - Length of key in bits
|
||||||
* @returns A string key in hex format
|
* @returns A string key in hex format
|
||||||
*/
|
*/
|
||||||
generateRandomKey(bits: number): string
|
generateRandomKey(bits: number): HexString
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @legacy
|
* @legacy
|
||||||
@@ -98,7 +99,7 @@ export interface PureCryptoInterface {
|
|||||||
* @param assocData
|
* @param assocData
|
||||||
* @returns Base64 ciphertext string
|
* @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
|
* Decrypt a message (and associated data) with XChaCha20-Poly1305
|
||||||
@@ -112,7 +113,7 @@ export interface PureCryptoInterface {
|
|||||||
ciphertext: Base64String,
|
ciphertext: Base64String,
|
||||||
nonce: HexString,
|
nonce: HexString,
|
||||||
key: HexString,
|
key: HexString,
|
||||||
assocData: Utf8String | Uint8Array,
|
assocData?: Utf8String | Uint8Array,
|
||||||
): Utf8String | null
|
): Utf8String | null
|
||||||
|
|
||||||
xchacha20StreamInitEncryptor(key: HexString): StreamEncryptor
|
xchacha20StreamInitEncryptor(key: HexString): StreamEncryptor
|
||||||
@@ -132,6 +133,22 @@ export interface PureCryptoInterface {
|
|||||||
assocData: Utf8String,
|
assocData: Utf8String,
|
||||||
): { message: Uint8Array; tag: SodiumConstant } | false
|
): { 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
|
* Converts a plain string into base64
|
||||||
* @param text - A plain string
|
* @param text - A plain string
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export class SNWebCrypto implements PureCryptoInterface {
|
|||||||
return this.webCryptoDeriveBits(key, salt, iterations, length)
|
return this.webCryptoDeriveBits(key, salt, iterations, length)
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateRandomKey(bits: number): string {
|
public generateRandomKey(bits: number): HexString {
|
||||||
const bytes = bits / 8
|
const bytes = bits / 8
|
||||||
const arrayBuffer = Utils.getGlobalScope().crypto.getRandomValues(new Uint8Array(bytes))
|
const arrayBuffer = Utils.getGlobalScope().crypto.getRandomValues(new Uint8Array(bytes))
|
||||||
return Utils.arrayBufferToHexString(arrayBuffer)
|
return Utils.arrayBufferToHexString(arrayBuffer)
|
||||||
@@ -249,14 +249,14 @@ export class SNWebCrypto implements PureCryptoInterface {
|
|||||||
plaintext: Utf8String,
|
plaintext: Utf8String,
|
||||||
nonce: HexString,
|
nonce: HexString,
|
||||||
key: HexString,
|
key: HexString,
|
||||||
assocData: Utf8String,
|
assocData?: Utf8String,
|
||||||
): Base64String {
|
): Base64String {
|
||||||
if (nonce.length !== 48) {
|
if (nonce.length !== 48) {
|
||||||
throw Error('Nonce must be 24 bytes')
|
throw Error('Nonce must be 24 bytes')
|
||||||
}
|
}
|
||||||
const arrayBuffer = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
|
const arrayBuffer = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
|
||||||
plaintext,
|
plaintext,
|
||||||
assocData,
|
assocData || null,
|
||||||
null,
|
null,
|
||||||
Utils.hexStringToArrayBuffer(nonce),
|
Utils.hexStringToArrayBuffer(nonce),
|
||||||
Utils.hexStringToArrayBuffer(key),
|
Utils.hexStringToArrayBuffer(key),
|
||||||
@@ -268,7 +268,7 @@ export class SNWebCrypto implements PureCryptoInterface {
|
|||||||
ciphertext: Base64String,
|
ciphertext: Base64String,
|
||||||
nonce: HexString,
|
nonce: HexString,
|
||||||
key: HexString,
|
key: HexString,
|
||||||
assocData: Utf8String | Uint8Array,
|
assocData?: Utf8String | Uint8Array,
|
||||||
): Utf8String | null {
|
): Utf8String | null {
|
||||||
if (nonce.length !== 48) {
|
if (nonce.length !== 48) {
|
||||||
throw Error('Nonce must be 24 bytes')
|
throw Error('Nonce must be 24 bytes')
|
||||||
@@ -277,7 +277,7 @@ export class SNWebCrypto implements PureCryptoInterface {
|
|||||||
return sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
|
return sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||||
null,
|
null,
|
||||||
Utils.base64ToArrayBuffer(ciphertext),
|
Utils.base64ToArrayBuffer(ciphertext),
|
||||||
assocData,
|
assocData || null,
|
||||||
Utils.hexStringToArrayBuffer(nonce),
|
Utils.hexStringToArrayBuffer(nonce),
|
||||||
Utils.hexStringToArrayBuffer(key),
|
Utils.hexStringToArrayBuffer(key),
|
||||||
'text',
|
'text',
|
||||||
@@ -368,7 +368,7 @@ export class SNWebCrypto implements PureCryptoInterface {
|
|||||||
nonce: HexString,
|
nonce: HexString,
|
||||||
senderPublicKey: HexString,
|
senderPublicKey: HexString,
|
||||||
recipientSecretKey: HexString,
|
recipientSecretKey: HexString,
|
||||||
): Base64String {
|
): Utf8String {
|
||||||
const result = sodium.crypto_box_open_easy(
|
const result = sodium.crypto_box_open_easy(
|
||||||
Utils.base64ToArrayBuffer(ciphertext),
|
Utils.base64ToArrayBuffer(ciphertext),
|
||||||
Utils.hexStringToArrayBuffer(nonce),
|
Utils.hexStringToArrayBuffer(nonce),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ContentType } from '@standardnotes/common'
|
import { ContentType } from '@standardnotes/common'
|
||||||
import { ItemsKeyInterface } from '@standardnotes/models'
|
import { ItemsKeyInterface } from '@standardnotes/models'
|
||||||
import { dateSorted } from '@standardnotes/utils'
|
import { dateSorted } from '@standardnotes/utils'
|
||||||
import { SNRootKeyParams, EncryptionProvider } from '@standardnotes/encryption'
|
import { SNRootKeyParams, EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||||
import { DecryptionQueueItem, KeyRecoveryOperationResult } from './Types'
|
import { DecryptionQueueItem, KeyRecoveryOperationResult } from './Types'
|
||||||
import { serverKeyParamsAreSafe } from './Utils'
|
import { serverKeyParamsAreSafe } from './Utils'
|
||||||
import { ChallengeServiceInterface, DecryptItemsKeyByPromptingUser } from '@standardnotes/services'
|
import { ChallengeServiceInterface, DecryptItemsKeyByPromptingUser } from '@standardnotes/services'
|
||||||
@@ -11,7 +11,7 @@ export class KeyRecoveryOperation {
|
|||||||
constructor(
|
constructor(
|
||||||
private queueItem: DecryptionQueueItem,
|
private queueItem: DecryptionQueueItem,
|
||||||
private itemManager: ItemManager,
|
private itemManager: ItemManager,
|
||||||
private protocolService: EncryptionProvider,
|
private protocolService: EncryptionProviderInterface,
|
||||||
private challengeService: ChallengeServiceInterface,
|
private challengeService: ChallengeServiceInterface,
|
||||||
private clientParams: SNRootKeyParams | undefined,
|
private clientParams: SNRootKeyParams | undefined,
|
||||||
private serverParams: SNRootKeyParams | undefined,
|
private serverParams: SNRootKeyParams | undefined,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
ChallengeReason,
|
ChallengeReason,
|
||||||
MutatorClientInterface,
|
MutatorClientInterface,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { EncryptionProvider } from '@standardnotes/encryption'
|
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||||
import { ContentType, ProtocolVersion, compareVersions } from '@standardnotes/common'
|
import { ContentType, ProtocolVersion, compareVersions } from '@standardnotes/common'
|
||||||
import { ItemManager } from '../Items'
|
import { ItemManager } from '../Items'
|
||||||
@@ -49,7 +49,7 @@ export class MutatorService extends AbstractService implements MutatorClientInte
|
|||||||
private itemManager: ItemManager,
|
private itemManager: ItemManager,
|
||||||
private syncService: SNSyncService,
|
private syncService: SNSyncService,
|
||||||
private protectionService: SNProtectionService,
|
private protectionService: SNProtectionService,
|
||||||
private encryption: EncryptionProvider,
|
private encryption: EncryptionProviderInterface,
|
||||||
private payloadManager: PayloadManager,
|
private payloadManager: PayloadManager,
|
||||||
private challengeService: ChallengeService,
|
private challengeService: ChallengeService,
|
||||||
private componentManager: SNComponentManager,
|
private componentManager: SNComponentManager,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
* key can decrypt wrapped storage.
|
* key can decrypt wrapped storage.
|
||||||
*/
|
*/
|
||||||
export class DiskStorageService extends Services.AbstractService implements Services.StorageServiceInterface {
|
export class DiskStorageService extends Services.AbstractService implements Services.StorageServiceInterface {
|
||||||
private encryptionProvider!: Encryption.EncryptionProvider
|
private encryptionProvider!: Encryption.EncryptionProviderInterface
|
||||||
private storagePersistable = false
|
private storagePersistable = false
|
||||||
private persistencePolicy!: Services.StoragePersistencePolicies
|
private persistencePolicy!: Services.StoragePersistencePolicies
|
||||||
private encryptionPolicy!: Services.StorageEncryptionPolicy
|
private encryptionPolicy!: Services.StorageEncryptionPolicy
|
||||||
@@ -53,7 +53,7 @@ export class DiskStorageService extends Services.AbstractService implements Serv
|
|||||||
void this.setEncryptionPolicy(Services.StorageEncryptionPolicy.Default, false)
|
void this.setEncryptionPolicy(Services.StorageEncryptionPolicy.Default, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
public provideEncryptionProvider(provider: Encryption.EncryptionProvider): void {
|
public provideEncryptionProvider(provider: Encryption.EncryptionProviderInterface): void {
|
||||||
this.encryptionProvider = provider
|
this.encryptionProvider = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,14 @@ describe('basic auth', function () {
|
|||||||
let error = null
|
let error = null
|
||||||
try {
|
try {
|
||||||
await this.application.register(this.email, password)
|
await this.application.register(this.email, password)
|
||||||
} catch(caughtError) {
|
} catch (caughtError) {
|
||||||
error = caughtError
|
error = caughtError
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(error.message).to.equal('Your password must be at least 8 characters in length. '
|
expect(error.message).to.equal(
|
||||||
+ 'For your security, please choose a longer password or, ideally, a passphrase, and try again.')
|
'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
|
expect(await this.application.protocolService.getRootKey()).to.not.be.ok
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ export default class FakeWebCrypto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generateRandomKey(bits) {
|
generateRandomKey(bits) {
|
||||||
const length = bits / 8
|
const bitsPerHexChar = 4
|
||||||
|
const length = bits / bitsPerHexChar
|
||||||
return this.randomString(length)
|
return this.randomString(length)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +108,13 @@ export default class FakeWebCrypto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
argon2(password, salt, iterations, bytes, length) {
|
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) {
|
xchacha20Encrypt(plaintext, nonce, key, assocData) {
|
||||||
@@ -128,6 +135,33 @@ export default class FakeWebCrypto {
|
|||||||
return data.plaintext
|
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() {
|
generateOtpSecret() {
|
||||||
return 'WQVV2GFBRQWU3UQZWQFZC37PSNRXKTA6'
|
return 'WQVV2GFBRQWU3UQZWQFZC37PSNRXKTA6'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user