tests: vault tests 3 (#2373)
This commit is contained in:
@@ -14,9 +14,9 @@ export class GetPayloadAuthenticatedDataDetachedUseCase {
|
|||||||
execute(
|
execute(
|
||||||
encrypted: EncryptedOutputParameters,
|
encrypted: EncryptedOutputParameters,
|
||||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
|
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
|
||||||
const itemKeyComponents = deconstructEncryptedPayloadString(encrypted.enc_item_key)
|
const contentKeyComponents = deconstructEncryptedPayloadString(encrypted.enc_item_key)
|
||||||
|
|
||||||
const authenticatedDataString = itemKeyComponents.authenticatedData
|
const authenticatedDataString = contentKeyComponents.authenticatedData
|
||||||
|
|
||||||
const result = this.parseStringUseCase.execute<
|
const result = this.parseStringUseCase.execute<
|
||||||
RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData
|
RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||||
import { ConflictDelta } from './Conflict'
|
import { ConflictDelta } from './Conflict'
|
||||||
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
|
import { FullyFormedPayloadInterface, isDecryptedPayload, PayloadEmitSource } from '../../Abstract/Payload'
|
||||||
import { DeletedPayloadInterface, isDecryptedPayload, PayloadEmitSource } from '../../Abstract/Payload'
|
|
||||||
import { HistoryMap } from '../History'
|
import { HistoryMap } from '../History'
|
||||||
import { extendSyncDelta, SourcelessSyncDeltaEmit, SyncDeltaEmit } from './Abstract/DeltaEmit'
|
import { extendSyncDelta, SourcelessSyncDeltaEmit, SyncDeltaEmit } from './Abstract/DeltaEmit'
|
||||||
import { DeltaInterface } from './Abstract/DeltaInterface'
|
import { DeltaInterface } from './Abstract/DeltaInterface'
|
||||||
@@ -11,7 +10,7 @@ import { getIncrementedDirtyIndex } from '../DirtyCounter/DirtyCounter'
|
|||||||
export class DeltaFileImport implements DeltaInterface {
|
export class DeltaFileImport implements DeltaInterface {
|
||||||
constructor(
|
constructor(
|
||||||
readonly baseCollection: ImmutablePayloadCollection,
|
readonly baseCollection: ImmutablePayloadCollection,
|
||||||
private readonly applyPayloads: DecryptedPayloadInterface[],
|
private readonly applyPayloads: FullyFormedPayloadInterface[],
|
||||||
protected readonly historyMap: HistoryMap,
|
protected readonly historyMap: HistoryMap,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -31,10 +30,7 @@ export class DeltaFileImport implements DeltaInterface {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolvePayload(
|
private resolvePayload(payload: FullyFormedPayloadInterface, currentResults: SyncDeltaEmit): SourcelessSyncDeltaEmit {
|
||||||
payload: DecryptedPayloadInterface | DeletedPayloadInterface,
|
|
||||||
currentResults: SyncDeltaEmit,
|
|
||||||
): SourcelessSyncDeltaEmit {
|
|
||||||
/**
|
/**
|
||||||
* Check to see if we've already processed a payload for this id.
|
* Check to see if we've already processed a payload for this id.
|
||||||
* If so, that would be the latest value, and not what's in the base collection.
|
* If so, that would be the latest value, and not what's in the base collection.
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ export interface EncryptionProviderInterface {
|
|||||||
setNewRootKeyWrapper(wrappingKey: RootKeyInterface): Promise<void>
|
setNewRootKeyWrapper(wrappingKey: RootKeyInterface): Promise<void>
|
||||||
|
|
||||||
createNewItemsKeyWithRollback(): Promise<() => Promise<void>>
|
createNewItemsKeyWithRollback(): Promise<() => Promise<void>>
|
||||||
reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void>
|
|
||||||
getSureDefaultItemsKey(): ItemsKeyInterface
|
getSureDefaultItemsKey(): ItemsKeyInterface
|
||||||
|
|
||||||
createRandomizedKeySystemRootKey(dto: { systemIdentifier: KeySystemIdentifier }): KeySystemRootKeyInterface
|
createRandomizedKeySystemRootKey(dto: { systemIdentifier: KeySystemIdentifier }): KeySystemRootKeyInterface
|
||||||
|
|||||||
@@ -240,10 +240,6 @@ export class EncryptionService
|
|||||||
return this.itemsEncryption.repersistAllItems()
|
return this.itemsEncryption.repersistAllItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
public async reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void> {
|
|
||||||
await this.rootKeyManager.reencryptApplicableItemsAfterUserRootKeyChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
public async createNewItemsKeyWithRollback(): Promise<() => Promise<void>> {
|
public async createNewItemsKeyWithRollback(): Promise<() => Promise<void>> {
|
||||||
return this._createNewItemsKeyWithRollback.execute()
|
return this._createNewItemsKeyWithRollback.execute()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { MutatorClientInterface } from './../../../Mutator/MutatorClientInterface'
|
||||||
|
import { ItemManagerInterface } from './../../../Item/ItemManagerInterface'
|
||||||
|
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||||
|
import { ContentTypesUsingRootKeyEncryption } from '@standardnotes/models'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the user root key changes, we must re-encrypt all relevant items with this new root key (by simply re-syncing).
|
||||||
|
*/
|
||||||
|
export class ReencryptTypeAItems implements UseCaseInterface<void> {
|
||||||
|
constructor(private items: ItemManagerInterface, private mutator: MutatorClientInterface) {}
|
||||||
|
|
||||||
|
public async execute(): Promise<Result<void>> {
|
||||||
|
const items = this.items.getItems(ContentTypesUsingRootKeyEncryption())
|
||||||
|
if (items.length > 0) {
|
||||||
|
/**
|
||||||
|
* Do not call sync after marking dirty.
|
||||||
|
* Re-encrypting items keys is called by consumers who have specific flows who
|
||||||
|
* will sync on their own timing
|
||||||
|
*/
|
||||||
|
await this.mutator.setItemsDirty(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,9 +76,7 @@ export class KeySystemKeyManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRootKeyFromStorageForVault(
|
getRootKeyFromStorageForVault(keySystemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface | undefined {
|
||||||
keySystemIdentifier: KeySystemIdentifier,
|
|
||||||
): KeySystemRootKeyInterface | undefined {
|
|
||||||
const payload = this.storage.getValue<DecryptedTransferPayload<KeySystemRootKeyContent>>(
|
const payload = this.storage.getValue<DecryptedTransferPayload<KeySystemRootKeyContent>>(
|
||||||
this.storageKeyForRootKey(keySystemIdentifier),
|
this.storageKeyForRootKey(keySystemIdentifier),
|
||||||
)
|
)
|
||||||
@@ -94,6 +92,10 @@ export class KeySystemKeyManager
|
|||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMemCachedRootKey(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface {
|
||||||
|
return this.rootKeyMemoryCache[systemIdentifier]
|
||||||
|
}
|
||||||
|
|
||||||
private storageKeyForRootKey(systemIdentifier: KeySystemIdentifier): string {
|
private storageKeyForRootKey(systemIdentifier: KeySystemIdentifier): string {
|
||||||
return `${RootKeyStorageKeyPrefix}${systemIdentifier}`
|
return `${RootKeyStorageKeyPrefix}${systemIdentifier}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,18 +9,16 @@ import { ProtocolVersion, compareVersions } from '@standardnotes/common'
|
|||||||
import {
|
import {
|
||||||
BackupFile,
|
BackupFile,
|
||||||
BackupFileDecryptedContextualPayload,
|
BackupFileDecryptedContextualPayload,
|
||||||
ComponentContent,
|
|
||||||
CopyPayloadWithContentOverride,
|
|
||||||
CreateDecryptedBackupFileContextPayload,
|
CreateDecryptedBackupFileContextPayload,
|
||||||
CreateEncryptedBackupFileContextPayload,
|
CreateEncryptedBackupFileContextPayload,
|
||||||
DecryptedItemInterface,
|
DecryptedItemInterface,
|
||||||
DecryptedPayloadInterface,
|
|
||||||
isDecryptedPayload,
|
isDecryptedPayload,
|
||||||
|
isEncryptedPayload,
|
||||||
isEncryptedTransferPayload,
|
isEncryptedTransferPayload,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||||
import { Challenge, ChallengePrompt, ChallengeReason, ChallengeValidation } from '../Challenge'
|
import { Challenge, ChallengePrompt, ChallengeReason, ChallengeValidation } from '../Challenge'
|
||||||
import { ContentType } from '@standardnotes/domain-core'
|
import { Result } from '@standardnotes/domain-core'
|
||||||
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
|
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
|
||||||
|
|
||||||
const Strings = {
|
const Strings = {
|
||||||
@@ -57,44 +55,22 @@ export class ImportDataUseCase {
|
|||||||
* .affectedItems: Items that were either created or dirtied by this import
|
* .affectedItems: Items that were either created or dirtied by this import
|
||||||
* .errorCount: The number of items that were not imported due to failure to decrypt.
|
* .errorCount: The number of items that were not imported due to failure to decrypt.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async execute(data: BackupFile, awaitSync = false): Promise<ImportDataReturnType> {
|
async execute(data: BackupFile, awaitSync = false): Promise<ImportDataReturnType> {
|
||||||
if (data.version) {
|
if (data.version) {
|
||||||
/**
|
const result = this.validateVersion(data.version)
|
||||||
* Prior to 003 backup files did not have a version field so we cannot
|
if (result.isFailed()) {
|
||||||
* stop importing if there is no backup file version, only if there is
|
return { error: new ClientDisplayableError(result.getError()) }
|
||||||
* an unsupported version.
|
|
||||||
*/
|
|
||||||
const version = data.version as ProtocolVersion
|
|
||||||
|
|
||||||
const supportedVersions = this.encryption.supportedVersions()
|
|
||||||
if (!supportedVersions.includes(version)) {
|
|
||||||
return { error: new ClientDisplayableError(Strings.UnsupportedBackupFileVersion) }
|
|
||||||
}
|
|
||||||
|
|
||||||
const userVersion = this.encryption.getUserVersion()
|
|
||||||
if (userVersion && compareVersions(version, userVersion) === 1) {
|
|
||||||
/** File was made with a greater version than the user's account */
|
|
||||||
return { error: new ClientDisplayableError(Strings.BackupFileMoreRecentThanAccount) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let password: string | undefined
|
let password: string | undefined
|
||||||
|
|
||||||
if (data.auth_params || data.keyParams) {
|
if (data.auth_params || data.keyParams) {
|
||||||
/** Get import file password. */
|
const passwordResult = await this.getFilePassword()
|
||||||
const challenge = new Challenge(
|
if (passwordResult.isFailed()) {
|
||||||
[new ChallengePrompt(ChallengeValidation.None, Strings.FileAccountPassword, undefined, true)],
|
return { error: new ClientDisplayableError(passwordResult.getError()) }
|
||||||
ChallengeReason.DecryptEncryptedFile,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
const passwordResponse = await this.challengeService.promptForChallengeResponse(challenge)
|
|
||||||
if (passwordResponse == undefined) {
|
|
||||||
/** Challenge was canceled */
|
|
||||||
return { error: new ClientDisplayableError('Import aborted') }
|
|
||||||
}
|
}
|
||||||
this.challengeService.completeChallenge(challenge)
|
password = passwordResult.getValue()
|
||||||
password = passwordResponse?.values[0].value as string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await this.protectionService.authorizeFileImport())) {
|
if (!(await this.protectionService.authorizeFileImport())) {
|
||||||
@@ -110,31 +86,23 @@ export class ImportDataUseCase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const decryptedPayloadsOrError = await this._decryptBackFile.execute(data, password)
|
const decryptedPayloadsOrError = await this._decryptBackFile.execute(data, password)
|
||||||
|
|
||||||
if (decryptedPayloadsOrError instanceof ClientDisplayableError) {
|
if (decryptedPayloadsOrError instanceof ClientDisplayableError) {
|
||||||
return { error: decryptedPayloadsOrError }
|
return { error: decryptedPayloadsOrError }
|
||||||
}
|
}
|
||||||
|
|
||||||
const validPayloads = decryptedPayloadsOrError.filter(isDecryptedPayload).map((payload) => {
|
const decryptedPayloads = decryptedPayloadsOrError.filter(isDecryptedPayload)
|
||||||
/* Don't want to activate any components during import process in
|
const encryptedPayloads = decryptedPayloadsOrError.filter(isEncryptedPayload)
|
||||||
* case of exceptions breaking up the import proccess */
|
const acceptableEncryptedPayloads = encryptedPayloads.filter((payload) => {
|
||||||
if (payload.content_type === ContentType.TYPES.Component && (payload.content as ComponentContent).active) {
|
return payload.key_system_identifier !== undefined
|
||||||
const typedContent = payload as DecryptedPayloadInterface<ComponentContent>
|
|
||||||
return CopyPayloadWithContentOverride(typedContent, {
|
|
||||||
active: false,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
const importablePayloads = [...decryptedPayloads, ...acceptableEncryptedPayloads]
|
||||||
|
|
||||||
const affectedUuids = await this.payloadManager.importPayloads(
|
const affectedUuids = await this.payloadManager.importPayloads(
|
||||||
validPayloads,
|
importablePayloads,
|
||||||
this.historyService.getHistoryMapCopy(),
|
this.historyService.getHistoryMapCopy(),
|
||||||
)
|
)
|
||||||
|
|
||||||
const promise = this.sync.sync()
|
const promise = this.sync.sync()
|
||||||
|
|
||||||
if (awaitSync) {
|
if (awaitSync) {
|
||||||
await promise
|
await promise
|
||||||
}
|
}
|
||||||
@@ -143,7 +111,42 @@ export class ImportDataUseCase {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
affectedItems: affectedItems,
|
affectedItems: affectedItems,
|
||||||
errorCount: decryptedPayloadsOrError.length - validPayloads.length,
|
errorCount: decryptedPayloadsOrError.length - importablePayloads.length,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getFilePassword(): Promise<Result<string>> {
|
||||||
|
const challenge = new Challenge(
|
||||||
|
[new ChallengePrompt(ChallengeValidation.None, Strings.FileAccountPassword, undefined, true)],
|
||||||
|
ChallengeReason.DecryptEncryptedFile,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
const passwordResponse = await this.challengeService.promptForChallengeResponse(challenge)
|
||||||
|
if (passwordResponse == undefined) {
|
||||||
|
/** Challenge was canceled */
|
||||||
|
return Result.fail('Import aborted')
|
||||||
|
}
|
||||||
|
this.challengeService.completeChallenge(challenge)
|
||||||
|
return Result.ok(passwordResponse?.values[0].value as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prior to 003 backup files did not have a version field so we cannot
|
||||||
|
* stop importing if there is no backup file version, only if there is
|
||||||
|
* an unsupported version.
|
||||||
|
*/
|
||||||
|
private validateVersion(version: ProtocolVersion): Result<void> {
|
||||||
|
const supportedVersions = this.encryption.supportedVersions()
|
||||||
|
if (!supportedVersions.includes(version)) {
|
||||||
|
return Result.fail(Strings.UnsupportedBackupFileVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userVersion = this.encryption.getUserVersion()
|
||||||
|
if (userVersion && compareVersions(version, userVersion) === 1) {
|
||||||
|
/** File was made with a greater version than the user's account */
|
||||||
|
return Result.fail(Strings.BackupFileMoreRecentThanAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
EncryptedPayloadInterface,
|
EncryptedPayloadInterface,
|
||||||
FullyFormedPayloadInterface,
|
FullyFormedPayloadInterface,
|
||||||
PayloadEmitSource,
|
PayloadEmitSource,
|
||||||
DecryptedPayloadInterface,
|
|
||||||
HistoryMap,
|
HistoryMap,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
import { IntegrityPayload } from '@standardnotes/responses'
|
import { IntegrityPayload } from '@standardnotes/responses'
|
||||||
@@ -24,7 +23,7 @@ export interface PayloadManagerInterface {
|
|||||||
*/
|
*/
|
||||||
get nonDeletedItems(): FullyFormedPayloadInterface[]
|
get nonDeletedItems(): FullyFormedPayloadInterface[]
|
||||||
|
|
||||||
importPayloads(payloads: DecryptedPayloadInterface[], historyMap: HistoryMap): Promise<string[]>
|
importPayloads(payloads: FullyFormedPayloadInterface[], historyMap: HistoryMap): Promise<string[]>
|
||||||
|
|
||||||
removePayloadLocally(payload: FullyFormedPayloadInterface | FullyFormedPayloadInterface[]): void
|
removePayloadLocally(payload: FullyFormedPayloadInterface | FullyFormedPayloadInterface[]): void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
EncryptionOperatorsInterface,
|
EncryptionOperatorsInterface,
|
||||||
} from '@standardnotes/encryption'
|
} from '@standardnotes/encryption'
|
||||||
import {
|
import {
|
||||||
ContentTypesUsingRootKeyEncryption,
|
|
||||||
DecryptedPayload,
|
DecryptedPayload,
|
||||||
DecryptedTransferPayload,
|
DecryptedTransferPayload,
|
||||||
EncryptedPayload,
|
EncryptedPayload,
|
||||||
@@ -32,12 +31,11 @@ import { StorageValueModes } from '../Storage/StorageTypes'
|
|||||||
import { EncryptTypeAPayload } from '../Encryption/UseCase/TypeA/EncryptPayload'
|
import { EncryptTypeAPayload } from '../Encryption/UseCase/TypeA/EncryptPayload'
|
||||||
import { DecryptTypeAPayload } from '../Encryption/UseCase/TypeA/DecryptPayload'
|
import { DecryptTypeAPayload } from '../Encryption/UseCase/TypeA/DecryptPayload'
|
||||||
import { AbstractService } from '../Service/AbstractService'
|
import { AbstractService } from '../Service/AbstractService'
|
||||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
|
||||||
import { MutatorClientInterface } from '../Mutator/MutatorClientInterface'
|
|
||||||
import { RootKeyManagerEvent } from './RootKeyManagerEvent'
|
import { RootKeyManagerEvent } from './RootKeyManagerEvent'
|
||||||
import { ValidatePasscodeResult } from './ValidatePasscodeResult'
|
import { ValidatePasscodeResult } from './ValidatePasscodeResult'
|
||||||
import { ValidateAccountPasswordResult } from './ValidateAccountPasswordResult'
|
import { ValidateAccountPasswordResult } from './ValidateAccountPasswordResult'
|
||||||
import { KeyMode } from './KeyMode'
|
import { KeyMode } from './KeyMode'
|
||||||
|
import { ReencryptTypeAItems } from '../Encryption/UseCase/TypeA/ReencryptTypeAItems'
|
||||||
|
|
||||||
export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||||
private rootKey?: RootKeyInterface
|
private rootKey?: RootKeyInterface
|
||||||
@@ -47,10 +45,9 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
|||||||
constructor(
|
constructor(
|
||||||
private device: DeviceInterface,
|
private device: DeviceInterface,
|
||||||
private storage: StorageServiceInterface,
|
private storage: StorageServiceInterface,
|
||||||
private items: ItemManagerInterface,
|
|
||||||
private mutator: MutatorClientInterface,
|
|
||||||
private operators: EncryptionOperatorsInterface,
|
private operators: EncryptionOperatorsInterface,
|
||||||
private identifier: ApplicationIdentifier,
|
private identifier: ApplicationIdentifier,
|
||||||
|
private _reencryptTypeAItems: ReencryptTypeAItems,
|
||||||
eventBus: InternalEventBusInterface,
|
eventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(eventBus)
|
super(eventBus)
|
||||||
@@ -58,6 +55,12 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
|||||||
|
|
||||||
override deinit() {
|
override deinit() {
|
||||||
super.deinit()
|
super.deinit()
|
||||||
|
;(this.device as unknown) = undefined
|
||||||
|
;(this.storage as unknown) = undefined
|
||||||
|
;(this.operators as unknown) = undefined
|
||||||
|
;(this.identifier as unknown) = undefined
|
||||||
|
;(this._reencryptTypeAItems as unknown) = undefined
|
||||||
|
|
||||||
this.rootKey = undefined
|
this.rootKey = undefined
|
||||||
this.memoizedRootKeyParams = undefined
|
this.memoizedRootKeyParams = undefined
|
||||||
}
|
}
|
||||||
@@ -307,7 +310,7 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
|||||||
if (this.keyMode === KeyMode.WrapperOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
if (this.keyMode === KeyMode.WrapperOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||||
this.setRootKeyInstance(wrappingKey)
|
this.setRootKeyInstance(wrappingKey)
|
||||||
await this.reencryptApplicableItemsAfterUserRootKeyChange()
|
await this._reencryptTypeAItems.execute()
|
||||||
} else {
|
} else {
|
||||||
await this.wrapAndPersistRootKey(wrappingKey)
|
await this.wrapAndPersistRootKey(wrappingKey)
|
||||||
}
|
}
|
||||||
@@ -473,19 +476,4 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
|||||||
keyParams: keyParams.getPortableValue(),
|
keyParams: keyParams.getPortableValue(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* When the root key changes, we must re-encrypt all relevant items with this new root key (by simply re-syncing).
|
|
||||||
*/
|
|
||||||
public async reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void> {
|
|
||||||
const items = this.items.getItems(ContentTypesUsingRootKeyEncryption())
|
|
||||||
if (items.length > 0) {
|
|
||||||
/**
|
|
||||||
* Do not call sync after marking dirty.
|
|
||||||
* Re-encrypting items keys is called by consumers who have specific flows who
|
|
||||||
* will sync on their own timing
|
|
||||||
*/
|
|
||||||
await this.mutator.setItemsDirty(items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ReencryptTypeAItems } from './../Encryption/UseCase/TypeA/ReencryptTypeAItems'
|
||||||
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
|
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
|
||||||
import { UserApiServiceInterface } from '@standardnotes/api'
|
import { UserApiServiceInterface } from '@standardnotes/api'
|
||||||
import { UserRequestType } from '@standardnotes/common'
|
import { UserRequestType } from '@standardnotes/common'
|
||||||
@@ -25,6 +26,7 @@ describe('UserService', () => {
|
|||||||
let challengeService: ChallengeServiceInterface
|
let challengeService: ChallengeServiceInterface
|
||||||
let protectionService: ProtectionsClientInterface
|
let protectionService: ProtectionsClientInterface
|
||||||
let userApiService: UserApiServiceInterface
|
let userApiService: UserApiServiceInterface
|
||||||
|
let reencryptTypeAItems: ReencryptTypeAItems
|
||||||
let internalEventBus: InternalEventBusInterface
|
let internalEventBus: InternalEventBusInterface
|
||||||
|
|
||||||
const createService = () =>
|
const createService = () =>
|
||||||
@@ -38,6 +40,7 @@ describe('UserService', () => {
|
|||||||
challengeService,
|
challengeService,
|
||||||
protectionService,
|
protectionService,
|
||||||
userApiService,
|
userApiService,
|
||||||
|
reencryptTypeAItems,
|
||||||
internalEventBus,
|
internalEventBus,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { AccountEvent } from './AccountEvent'
|
|||||||
import { SignedInOrRegisteredEventPayload } from './SignedInOrRegisteredEventPayload'
|
import { SignedInOrRegisteredEventPayload } from './SignedInOrRegisteredEventPayload'
|
||||||
import { CredentialsChangeFunctionResponse } from './CredentialsChangeFunctionResponse'
|
import { CredentialsChangeFunctionResponse } from './CredentialsChangeFunctionResponse'
|
||||||
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
|
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
|
||||||
|
import { ReencryptTypeAItems } from '../Encryption/UseCase/TypeA/ReencryptTypeAItems'
|
||||||
|
|
||||||
export class UserService
|
export class UserService
|
||||||
extends AbstractService<AccountEvent, AccountEventData>
|
extends AbstractService<AccountEvent, AccountEventData>
|
||||||
@@ -49,33 +50,48 @@ export class UserService
|
|||||||
private readonly MINIMUM_PASSWORD_LENGTH = 8
|
private readonly MINIMUM_PASSWORD_LENGTH = 8
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private sessionManager: SessionsClientInterface,
|
private sessions: SessionsClientInterface,
|
||||||
private sync: SyncServiceInterface,
|
private sync: SyncServiceInterface,
|
||||||
private storageService: StorageServiceInterface,
|
private storage: StorageServiceInterface,
|
||||||
private itemManager: ItemManagerInterface,
|
private items: ItemManagerInterface,
|
||||||
private encryptionService: EncryptionProviderInterface,
|
private encryption: EncryptionProviderInterface,
|
||||||
private alertService: AlertService,
|
private alerts: AlertService,
|
||||||
private challengeService: ChallengeServiceInterface,
|
private challenges: ChallengeServiceInterface,
|
||||||
private protectionService: ProtectionsClientInterface,
|
private protections: ProtectionsClientInterface,
|
||||||
private userApiService: UserApiServiceInterface,
|
private userApi: UserApiServiceInterface,
|
||||||
|
private _reencryptTypeAItems: ReencryptTypeAItems,
|
||||||
protected override internalEventBus: InternalEventBusInterface,
|
protected override internalEventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(internalEventBus)
|
super(internalEventBus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override deinit(): void {
|
||||||
|
super.deinit()
|
||||||
|
;(this.sessions as unknown) = undefined
|
||||||
|
;(this.sync as unknown) = undefined
|
||||||
|
;(this.storage as unknown) = undefined
|
||||||
|
;(this.items as unknown) = undefined
|
||||||
|
;(this.encryption as unknown) = undefined
|
||||||
|
;(this.alerts as unknown) = undefined
|
||||||
|
;(this.challenges as unknown) = undefined
|
||||||
|
;(this.protections as unknown) = undefined
|
||||||
|
;(this.userApi as unknown) = undefined
|
||||||
|
;(this._reencryptTypeAItems as unknown) = undefined
|
||||||
|
}
|
||||||
|
|
||||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||||
if (event.type === AccountEvent.SignedInOrRegistered) {
|
if (event.type === AccountEvent.SignedInOrRegistered) {
|
||||||
const payload = (event.payload as AccountEventData).payload as SignedInOrRegisteredEventPayload
|
const payload = (event.payload as AccountEventData).payload as SignedInOrRegisteredEventPayload
|
||||||
this.sync.resetSyncState()
|
this.sync.resetSyncState()
|
||||||
|
|
||||||
await this.storageService.setPersistencePolicy(
|
await this.storage.setPersistencePolicy(
|
||||||
payload.ephemeral ? StoragePersistencePolicies.Ephemeral : StoragePersistencePolicies.Default,
|
payload.ephemeral ? StoragePersistencePolicies.Ephemeral : StoragePersistencePolicies.Default,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (payload.mergeLocal) {
|
if (payload.mergeLocal) {
|
||||||
await this.sync.markAllItemsAsNeedingSyncAndPersist()
|
await this.sync.markAllItemsAsNeedingSyncAndPersist()
|
||||||
} else {
|
} else {
|
||||||
void this.itemManager.removeAllItemsFromMemory()
|
void this.items.removeAllItemsFromMemory()
|
||||||
await this.clearDatabase()
|
await this.clearDatabase()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,37 +104,24 @@ export class UserService
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!payload.awaitSync) {
|
if (!payload.awaitSync) {
|
||||||
void this.encryptionService.decryptErroredPayloads()
|
void this.encryption.decryptErroredPayloads()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (payload.awaitSync) {
|
if (payload.awaitSync) {
|
||||||
await syncPromise
|
await syncPromise
|
||||||
|
|
||||||
await this.encryptionService.decryptErroredPayloads()
|
await this.encryption.decryptErroredPayloads()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override deinit(): void {
|
|
||||||
super.deinit()
|
|
||||||
;(this.sessionManager as unknown) = undefined
|
|
||||||
;(this.sync as unknown) = undefined
|
|
||||||
;(this.storageService as unknown) = undefined
|
|
||||||
;(this.itemManager as unknown) = undefined
|
|
||||||
;(this.encryptionService as unknown) = undefined
|
|
||||||
;(this.alertService as unknown) = undefined
|
|
||||||
;(this.challengeService as unknown) = undefined
|
|
||||||
;(this.protectionService as unknown) = undefined
|
|
||||||
;(this.userApiService as unknown) = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserUuid(): string {
|
getUserUuid(): string {
|
||||||
return this.sessionManager.userUuid
|
return this.sessions.userUuid
|
||||||
}
|
}
|
||||||
|
|
||||||
isSignedIn(): boolean {
|
isSignedIn(): boolean {
|
||||||
return this.sessionManager.isSignedIn()
|
return this.sessions.isSignedIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,7 +134,7 @@ export class UserService
|
|||||||
ephemeral = false,
|
ephemeral = false,
|
||||||
mergeLocal = true,
|
mergeLocal = true,
|
||||||
): Promise<UserRegistrationResponseBody> {
|
): Promise<UserRegistrationResponseBody> {
|
||||||
if (this.encryptionService.hasAccount()) {
|
if (this.encryption.hasAccount()) {
|
||||||
throw Error('Tried to register when an account already exists.')
|
throw Error('Tried to register when an account already exists.')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +146,7 @@ export class UserService
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.lockSyncing()
|
this.lockSyncing()
|
||||||
const response = await this.sessionManager.register(email, password, ephemeral)
|
const response = await this.sessions.register(email, password, ephemeral)
|
||||||
|
|
||||||
await this.notifyEventSync(AccountEvent.SignedInOrRegistered, {
|
await this.notifyEventSync(AccountEvent.SignedInOrRegistered, {
|
||||||
payload: {
|
payload: {
|
||||||
@@ -177,7 +180,7 @@ export class UserService
|
|||||||
mergeLocal = true,
|
mergeLocal = true,
|
||||||
awaitSync = false,
|
awaitSync = false,
|
||||||
): Promise<HttpResponse<SignInResponse>> {
|
): Promise<HttpResponse<SignInResponse>> {
|
||||||
if (this.encryptionService.hasAccount()) {
|
if (this.encryption.hasAccount()) {
|
||||||
throw Error('Tried to sign in when an account already exists.')
|
throw Error('Tried to sign in when an account already exists.')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +194,7 @@ export class UserService
|
|||||||
/** Prevent a timed sync from occuring while signing in. */
|
/** Prevent a timed sync from occuring while signing in. */
|
||||||
this.lockSyncing()
|
this.lockSyncing()
|
||||||
|
|
||||||
const { response } = await this.sessionManager.signIn(email, password, strict, ephemeral)
|
const { response } = await this.sessions.signIn(email, password, strict, ephemeral)
|
||||||
|
|
||||||
if (!isErrorResponse(response)) {
|
if (!isErrorResponse(response)) {
|
||||||
const notifyingFunction = awaitSync ? this.notifyEventSync.bind(this) : this.notifyEvent.bind(this)
|
const notifyingFunction = awaitSync ? this.notifyEventSync.bind(this) : this.notifyEvent.bind(this)
|
||||||
@@ -218,7 +221,7 @@ export class UserService
|
|||||||
message?: string
|
message?: string
|
||||||
}> {
|
}> {
|
||||||
if (
|
if (
|
||||||
!(await this.protectionService.authorizeAction(ChallengeReason.DeleteAccount, {
|
!(await this.protections.authorizeAction(ChallengeReason.DeleteAccount, {
|
||||||
fallBackToAccountPassword: true,
|
fallBackToAccountPassword: true,
|
||||||
requireAccountPassword: true,
|
requireAccountPassword: true,
|
||||||
forcePrompt: false,
|
forcePrompt: false,
|
||||||
@@ -230,8 +233,8 @@ export class UserService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const uuid = this.sessionManager.getSureUser().uuid
|
const uuid = this.sessions.getSureUser().uuid
|
||||||
const response = await this.userApiService.deleteAccount(uuid)
|
const response = await this.userApi.deleteAccount(uuid)
|
||||||
if (isErrorResponse(response)) {
|
if (isErrorResponse(response)) {
|
||||||
return {
|
return {
|
||||||
error: true,
|
error: true,
|
||||||
@@ -241,7 +244,7 @@ export class UserService
|
|||||||
|
|
||||||
await this.signOut(true)
|
await this.signOut(true)
|
||||||
|
|
||||||
void this.alertService.alert(InfoStrings.AccountDeleted)
|
void this.alerts.alert(InfoStrings.AccountDeleted)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
error: false,
|
error: false,
|
||||||
@@ -249,9 +252,9 @@ export class UserService
|
|||||||
}
|
}
|
||||||
|
|
||||||
async submitUserRequest(requestType: UserRequestType): Promise<boolean> {
|
async submitUserRequest(requestType: UserRequestType): Promise<boolean> {
|
||||||
const userUuid = this.sessionManager.getSureUser().uuid
|
const userUuid = this.sessions.getSureUser().uuid
|
||||||
try {
|
try {
|
||||||
const result = await this.userApiService.submitUserRequest({
|
const result = await this.userApi.submitUserRequest({
|
||||||
userUuid,
|
userUuid,
|
||||||
requestType,
|
requestType,
|
||||||
})
|
})
|
||||||
@@ -274,11 +277,7 @@ export class UserService
|
|||||||
public async correctiveSignIn(rootKey: SNRootKey): Promise<HttpResponse<SignInResponse>> {
|
public async correctiveSignIn(rootKey: SNRootKey): Promise<HttpResponse<SignInResponse>> {
|
||||||
this.lockSyncing()
|
this.lockSyncing()
|
||||||
|
|
||||||
const response = await this.sessionManager.bypassChecksAndSignInWithRootKey(
|
const response = await this.sessions.bypassChecksAndSignInWithRootKey(rootKey.keyParams.identifier, rootKey, false)
|
||||||
rootKey.keyParams.identifier,
|
|
||||||
rootKey,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isErrorResponse(response)) {
|
if (!isErrorResponse(response)) {
|
||||||
await this.notifyEvent(AccountEvent.SignedInOrRegistered, {
|
await this.notifyEvent(AccountEvent.SignedInOrRegistered, {
|
||||||
@@ -313,16 +312,16 @@ export class UserService
|
|||||||
}): Promise<CredentialsChangeFunctionResponse> {
|
}): Promise<CredentialsChangeFunctionResponse> {
|
||||||
const result = await this.performCredentialsChange(parameters)
|
const result = await this.performCredentialsChange(parameters)
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
void this.alertService.alert(result.error.message)
|
void this.alerts.alert(result.error.message)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
public async signOut(force = false, source = DeinitSource.SignOut): Promise<void> {
|
public async signOut(force = false, source = DeinitSource.SignOut): Promise<void> {
|
||||||
const performSignOut = async () => {
|
const performSignOut = async () => {
|
||||||
await this.sessionManager.signOut()
|
await this.sessions.signOut()
|
||||||
await this.encryptionService.deleteWorkspaceSpecificKeyStateFromDevice()
|
await this.encryption.deleteWorkspaceSpecificKeyStateFromDevice()
|
||||||
await this.storageService.clearAllData()
|
await this.storage.clearAllData()
|
||||||
await this.notifyEvent(AccountEvent.SignedOut, { payload: { source } })
|
await this.notifyEvent(AccountEvent.SignedOut, { payload: { source } })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,10 +331,10 @@ export class UserService
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const dirtyItems = this.itemManager.getDirtyItems()
|
const dirtyItems = this.items.getDirtyItems()
|
||||||
if (dirtyItems.length > 0) {
|
if (dirtyItems.length > 0) {
|
||||||
const singular = dirtyItems.length === 1
|
const singular = dirtyItems.length === 1
|
||||||
const didConfirm = await this.alertService.confirm(
|
const didConfirm = await this.alerts.confirm(
|
||||||
`There ${singular ? 'is' : 'are'} ${dirtyItems.length} ${
|
`There ${singular ? 'is' : 'are'} ${dirtyItems.length} ${
|
||||||
singular ? 'item' : 'items'
|
singular ? 'item' : 'items'
|
||||||
} with unsynced changes. If you sign out, these changes will be lost forever. Are you sure you want to sign out?`,
|
} with unsynced changes. If you sign out, these changes will be lost forever. Are you sure you want to sign out?`,
|
||||||
@@ -353,7 +352,7 @@ export class UserService
|
|||||||
canceled?: true
|
canceled?: true
|
||||||
error?: { message: string }
|
error?: { message: string }
|
||||||
}> {
|
}> {
|
||||||
if (!this.sessionManager.isUserMissingKeyPair()) {
|
if (!this.sessions.isUserMissingKeyPair()) {
|
||||||
throw Error('Cannot update account with first time keypair if user already has a keypair')
|
throw Error('Cannot update account with first time keypair if user already has a keypair')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,8 +366,8 @@ export class UserService
|
|||||||
canceled?: true
|
canceled?: true
|
||||||
error?: { message: string }
|
error?: { message: string }
|
||||||
}> {
|
}> {
|
||||||
const hasPasscode = this.encryptionService.hasPasscode()
|
const hasPasscode = this.encryption.hasPasscode()
|
||||||
const hasAccount = this.encryptionService.hasAccount()
|
const hasAccount = this.encryption.hasAccount()
|
||||||
const prompts = []
|
const prompts = []
|
||||||
if (hasPasscode) {
|
if (hasPasscode) {
|
||||||
prompts.push(
|
prompts.push(
|
||||||
@@ -389,11 +388,11 @@ export class UserService
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
const challenge = new Challenge(prompts, ChallengeReason.ProtocolUpgrade, true)
|
const challenge = new Challenge(prompts, ChallengeReason.ProtocolUpgrade, true)
|
||||||
const response = await this.challengeService.promptForChallengeResponse(challenge)
|
const response = await this.challenges.promptForChallengeResponse(challenge)
|
||||||
if (!response) {
|
if (!response) {
|
||||||
return { canceled: true }
|
return { canceled: true }
|
||||||
}
|
}
|
||||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
const dismissBlockingDialog = await this.alerts.blockingDialog(
|
||||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||||
Messages.UPGRADING_ENCRYPTION,
|
Messages.UPGRADING_ENCRYPTION,
|
||||||
)
|
)
|
||||||
@@ -436,11 +435,11 @@ export class UserService
|
|||||||
if (passcode.length < this.MINIMUM_PASSCODE_LENGTH) {
|
if (passcode.length < this.MINIMUM_PASSCODE_LENGTH) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (!(await this.protectionService.authorizeAddingPasscode())) {
|
if (!(await this.protections.authorizeAddingPasscode())) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
const dismissBlockingDialog = await this.alerts.blockingDialog(
|
||||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||||
Messages.SETTING_PASSCODE,
|
Messages.SETTING_PASSCODE,
|
||||||
)
|
)
|
||||||
@@ -453,11 +452,11 @@ export class UserService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async removePasscode(): Promise<boolean> {
|
public async removePasscode(): Promise<boolean> {
|
||||||
if (!(await this.protectionService.authorizeRemovingPasscode())) {
|
if (!(await this.protections.authorizeRemovingPasscode())) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
const dismissBlockingDialog = await this.alerts.blockingDialog(
|
||||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||||
Messages.REMOVING_PASSCODE,
|
Messages.REMOVING_PASSCODE,
|
||||||
)
|
)
|
||||||
@@ -479,11 +478,11 @@ export class UserService
|
|||||||
if (newPasscode.length < this.MINIMUM_PASSCODE_LENGTH) {
|
if (newPasscode.length < this.MINIMUM_PASSCODE_LENGTH) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (!(await this.protectionService.authorizeChangingPasscode())) {
|
if (!(await this.protections.authorizeChangingPasscode())) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
const dismissBlockingDialog = await this.alerts.blockingDialog(
|
||||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||||
origination === KeyParamsOrigination.ProtocolUpgrade
|
origination === KeyParamsOrigination.ProtocolUpgrade
|
||||||
? Messages.ProtocolUpgradeStrings.UpgradingPasscode
|
? Messages.ProtocolUpgradeStrings.UpgradingPasscode
|
||||||
@@ -499,7 +498,7 @@ export class UserService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async populateSessionFromDemoShareToken(token: Base64String): Promise<void> {
|
public async populateSessionFromDemoShareToken(token: Base64String): Promise<void> {
|
||||||
await this.sessionManager.populateSessionFromDemoShareToken(token)
|
await this.sessions.populateSessionFromDemoShareToken(token)
|
||||||
await this.notifyEvent(AccountEvent.SignedInOrRegistered, {
|
await this.notifyEvent(AccountEvent.SignedInOrRegistered, {
|
||||||
payload: {
|
payload: {
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
@@ -512,14 +511,14 @@ export class UserService
|
|||||||
|
|
||||||
private async setPasscodeWithoutWarning(passcode: string, origination: KeyParamsOrigination) {
|
private async setPasscodeWithoutWarning(passcode: string, origination: KeyParamsOrigination) {
|
||||||
const identifier = UuidGenerator.GenerateUuid()
|
const identifier = UuidGenerator.GenerateUuid()
|
||||||
const key = await this.encryptionService.createRootKey(identifier, passcode, origination)
|
const key = await this.encryption.createRootKey(identifier, passcode, origination)
|
||||||
await this.encryptionService.setNewRootKeyWrapper(key)
|
await this.encryption.setNewRootKeyWrapper(key)
|
||||||
await this.rewriteItemsKeys()
|
await this.rewriteItemsKeys()
|
||||||
await this.sync.sync()
|
await this.sync.sync()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async removePasscodeWithoutWarning() {
|
private async removePasscodeWithoutWarning() {
|
||||||
await this.encryptionService.removePasscode()
|
await this.encryption.removePasscode()
|
||||||
await this.rewriteItemsKeys()
|
await this.rewriteItemsKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,9 +531,9 @@ export class UserService
|
|||||||
* https://github.com/standardnotes/desktop/issues/131
|
* https://github.com/standardnotes/desktop/issues/131
|
||||||
*/
|
*/
|
||||||
private async rewriteItemsKeys(): Promise<void> {
|
private async rewriteItemsKeys(): Promise<void> {
|
||||||
const itemsKeys = this.itemManager.getDisplayableItemsKeys()
|
const itemsKeys = this.items.getDisplayableItemsKeys()
|
||||||
const payloads = itemsKeys.map((key) => key.payloadRepresentation())
|
const payloads = itemsKeys.map((key) => key.payloadRepresentation())
|
||||||
await this.storageService.deletePayloads(payloads)
|
await this.storage.deletePayloads(payloads)
|
||||||
await this.sync.persistPayloads(payloads)
|
await this.sync.persistPayloads(payloads)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,7 +546,7 @@ export class UserService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private clearDatabase(): Promise<void> {
|
private clearDatabase(): Promise<void> {
|
||||||
return this.storageService.clearAllPayloads()
|
return this.storage.clearAllPayloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async performCredentialsChange(parameters: {
|
private async performCredentialsChange(parameters: {
|
||||||
@@ -558,7 +557,7 @@ export class UserService
|
|||||||
newPassword?: string
|
newPassword?: string
|
||||||
passcode?: string
|
passcode?: string
|
||||||
}): Promise<CredentialsChangeFunctionResponse> {
|
}): Promise<CredentialsChangeFunctionResponse> {
|
||||||
const { wrappingKey, canceled } = await this.challengeService.getWrappingKeyIfApplicable(parameters.passcode)
|
const { wrappingKey, canceled } = await this.challenges.getWrappingKeyIfApplicable(parameters.passcode)
|
||||||
|
|
||||||
if (canceled) {
|
if (canceled) {
|
||||||
return { error: Error(Messages.CredentialsChangeStrings.PasscodeRequired) }
|
return { error: Error(Messages.CredentialsChangeStrings.PasscodeRequired) }
|
||||||
@@ -572,14 +571,14 @@ export class UserService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const accountPasswordValidation = await this.encryptionService.validateAccountPassword(parameters.currentPassword)
|
const accountPasswordValidation = await this.encryption.validateAccountPassword(parameters.currentPassword)
|
||||||
if (!accountPasswordValidation.valid) {
|
if (!accountPasswordValidation.valid) {
|
||||||
return {
|
return {
|
||||||
error: Error(Messages.INVALID_PASSWORD),
|
error: Error(Messages.INVALID_PASSWORD),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = this.sessionManager.getUser() as User
|
const user = this.sessions.getUser() as User
|
||||||
const currentEmail = user.email
|
const currentEmail = user.email
|
||||||
const { currentRootKey, newRootKey } = await this.recomputeRootKeysForCredentialChange({
|
const { currentRootKey, newRootKey } = await this.recomputeRootKeysForCredentialChange({
|
||||||
currentPassword: parameters.currentPassword,
|
currentPassword: parameters.currentPassword,
|
||||||
@@ -591,7 +590,7 @@ export class UserService
|
|||||||
|
|
||||||
this.lockSyncing()
|
this.lockSyncing()
|
||||||
|
|
||||||
const { response } = await this.sessionManager.changeCredentials({
|
const { response } = await this.sessions.changeCredentials({
|
||||||
currentServerPassword: currentRootKey.serverPassword as string,
|
currentServerPassword: currentRootKey.serverPassword as string,
|
||||||
newRootKey: newRootKey,
|
newRootKey: newRootKey,
|
||||||
wrappingKey,
|
wrappingKey,
|
||||||
@@ -604,20 +603,20 @@ export class UserService
|
|||||||
return { error: Error(response.data.error?.message) }
|
return { error: Error(response.data.error?.message) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const rollback = await this.encryptionService.createNewItemsKeyWithRollback()
|
const rollback = await this.encryption.createNewItemsKeyWithRollback()
|
||||||
await this.encryptionService.reencryptApplicableItemsAfterUserRootKeyChange()
|
await this._reencryptTypeAItems.execute()
|
||||||
await this.sync.sync({ awaitAll: true })
|
await this.sync.sync({ awaitAll: true })
|
||||||
|
|
||||||
const defaultItemsKey = this.encryptionService.getSureDefaultItemsKey()
|
const defaultItemsKey = this.encryption.getSureDefaultItemsKey()
|
||||||
const itemsKeyWasSynced = !defaultItemsKey.neverSynced
|
const itemsKeyWasSynced = !defaultItemsKey.neverSynced
|
||||||
|
|
||||||
if (!itemsKeyWasSynced) {
|
if (!itemsKeyWasSynced) {
|
||||||
await this.sessionManager.changeCredentials({
|
await this.sessions.changeCredentials({
|
||||||
currentServerPassword: newRootKey.serverPassword as string,
|
currentServerPassword: newRootKey.serverPassword as string,
|
||||||
newRootKey: currentRootKey,
|
newRootKey: currentRootKey,
|
||||||
wrappingKey,
|
wrappingKey,
|
||||||
})
|
})
|
||||||
await this.encryptionService.reencryptApplicableItemsAfterUserRootKeyChange()
|
await this._reencryptTypeAItems.execute()
|
||||||
await rollback()
|
await rollback()
|
||||||
await this.sync.sync({ awaitAll: true })
|
await this.sync.sync({ awaitAll: true })
|
||||||
|
|
||||||
@@ -634,11 +633,11 @@ export class UserService
|
|||||||
newEmail?: string
|
newEmail?: string
|
||||||
newPassword?: string
|
newPassword?: string
|
||||||
}): Promise<{ currentRootKey: SNRootKey; newRootKey: SNRootKey }> {
|
}): Promise<{ currentRootKey: SNRootKey; newRootKey: SNRootKey }> {
|
||||||
const currentRootKey = await this.encryptionService.computeRootKey(
|
const currentRootKey = await this.encryption.computeRootKey(
|
||||||
parameters.currentPassword,
|
parameters.currentPassword,
|
||||||
(await this.encryptionService.getRootKeyParams()) as SNRootKeyParams,
|
this.encryption.getRootKeyParams() as SNRootKeyParams,
|
||||||
)
|
)
|
||||||
const newRootKey = await this.encryptionService.createRootKey(
|
const newRootKey = await this.encryption.createRootKey(
|
||||||
parameters.newEmail ?? parameters.currentEmail,
|
parameters.newEmail ?? parameters.currentEmail,
|
||||||
parameters.newPassword ?? parameters.currentPassword,
|
parameters.newPassword ?? parameters.currentPassword,
|
||||||
parameters.origination,
|
parameters.origination,
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export * from './Encryption/UseCase/TypeA/DecryptPayload'
|
|||||||
export * from './Encryption/UseCase/TypeA/DecryptPayloadWithKeyLookup'
|
export * from './Encryption/UseCase/TypeA/DecryptPayloadWithKeyLookup'
|
||||||
export * from './Encryption/UseCase/TypeA/EncryptPayload'
|
export * from './Encryption/UseCase/TypeA/EncryptPayload'
|
||||||
export * from './Encryption/UseCase/TypeA/EncryptPayloadWithKeyLookup'
|
export * from './Encryption/UseCase/TypeA/EncryptPayloadWithKeyLookup'
|
||||||
|
export * from './Encryption/UseCase/TypeA/ReencryptTypeAItems'
|
||||||
export * from './Event/ApplicationEvent'
|
export * from './Event/ApplicationEvent'
|
||||||
export * from './Event/ApplicationEventCallback'
|
export * from './Event/ApplicationEventCallback'
|
||||||
export * from './Event/ApplicationStageChangedEventPayload'
|
export * from './Event/ApplicationStageChangedEventPayload'
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ import {
|
|||||||
DeleteContact,
|
DeleteContact,
|
||||||
VaultLockService,
|
VaultLockService,
|
||||||
RemoveItemsFromMemory,
|
RemoveItemsFromMemory,
|
||||||
|
ReencryptTypeAItems,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { ItemManager } from '../../Services/Items/ItemManager'
|
import { ItemManager } from '../../Services/Items/ItemManager'
|
||||||
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
|
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
|
||||||
@@ -202,6 +203,10 @@ export class Dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private registerUseCaseMakers() {
|
private registerUseCaseMakers() {
|
||||||
|
this.factory.set(TYPES.ReencryptTypeAItems, () => {
|
||||||
|
return new ReencryptTypeAItems(this.get(TYPES.ItemManager), this.get(TYPES.MutatorService))
|
||||||
|
})
|
||||||
|
|
||||||
this.factory.set(TYPES.ImportDataUseCase, () => {
|
this.factory.set(TYPES.ImportDataUseCase, () => {
|
||||||
return new ImportDataUseCase(
|
return new ImportDataUseCase(
|
||||||
this.get(TYPES.ItemManager),
|
this.get(TYPES.ItemManager),
|
||||||
@@ -616,10 +621,9 @@ export class Dependencies {
|
|||||||
return new RootKeyManager(
|
return new RootKeyManager(
|
||||||
this.get(TYPES.DeviceInterface),
|
this.get(TYPES.DeviceInterface),
|
||||||
this.get(TYPES.DiskStorageService),
|
this.get(TYPES.DiskStorageService),
|
||||||
this.get(TYPES.ItemManager),
|
|
||||||
this.get(TYPES.MutatorService),
|
|
||||||
this.get(TYPES.EncryptionOperators),
|
this.get(TYPES.EncryptionOperators),
|
||||||
this.options.identifier,
|
this.options.identifier,
|
||||||
|
this.get(TYPES.ReencryptTypeAItems),
|
||||||
this.get(TYPES.InternalEventBus),
|
this.get(TYPES.InternalEventBus),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -1086,6 +1090,7 @@ export class Dependencies {
|
|||||||
this.get(TYPES.ChallengeService),
|
this.get(TYPES.ChallengeService),
|
||||||
this.get(TYPES.ProtectionService),
|
this.get(TYPES.ProtectionService),
|
||||||
this.get(TYPES.UserApiService),
|
this.get(TYPES.UserApiService),
|
||||||
|
this.get(TYPES.ReencryptTypeAItems),
|
||||||
this.get(TYPES.InternalEventBus),
|
this.get(TYPES.InternalEventBus),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ export const TYPES = {
|
|||||||
DecryptBackupFile: Symbol.for('DecryptBackupFile'),
|
DecryptBackupFile: Symbol.for('DecryptBackupFile'),
|
||||||
IsVaultOwner: Symbol.for('IsVaultOwner'),
|
IsVaultOwner: Symbol.for('IsVaultOwner'),
|
||||||
RemoveItemsFromMemory: Symbol.for('RemoveItemsFromMemory'),
|
RemoveItemsFromMemory: Symbol.for('RemoveItemsFromMemory'),
|
||||||
|
ReencryptTypeAItems: Symbol.for('ReencryptTypeAItems'),
|
||||||
|
|
||||||
// Mappers
|
// Mappers
|
||||||
SessionStorageMapper: Symbol.for('SessionStorageMapper'),
|
SessionStorageMapper: Symbol.for('SessionStorageMapper'),
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export class Migration2_202_1 extends Migration {
|
|||||||
this.registerStageHandler(ApplicationStage.FullSyncCompleted_13, async () => {
|
this.registerStageHandler(ApplicationStage.FullSyncCompleted_13, async () => {
|
||||||
await this.migrateComponentDataToUserPreferences()
|
await this.migrateComponentDataToUserPreferences()
|
||||||
await this.migrateActiveComponentsToUserPreferences()
|
await this.migrateActiveComponentsToUserPreferences()
|
||||||
await this.deleteComponentsWhichAreNativeFeatures()
|
|
||||||
|
|
||||||
this.markDone()
|
this.markDone()
|
||||||
})
|
})
|
||||||
@@ -70,29 +69,4 @@ export class Migration2_202_1 extends Migration {
|
|||||||
await this.services.preferences.setValueDetached(PrefKey.ActiveThemes, Uuids(activeThemes))
|
await this.services.preferences.setValueDetached(PrefKey.ActiveThemes, Uuids(activeThemes))
|
||||||
await this.services.preferences.setValueDetached(PrefKey.ActiveComponents, Uuids(activeComponents))
|
await this.services.preferences.setValueDetached(PrefKey.ActiveComponents, Uuids(activeComponents))
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteComponentsWhichAreNativeFeatures(): Promise<void> {
|
|
||||||
const componentsToDelete = [
|
|
||||||
...this.services.itemManager.getItems<ComponentInterface>(ContentType.TYPES.Component),
|
|
||||||
...this.services.itemManager.getItems<ComponentInterface>(ContentType.TYPES.Theme),
|
|
||||||
].filter((candidate) => {
|
|
||||||
const nativeFeature = FindNativeFeature(candidate.identifier)
|
|
||||||
if (!nativeFeature) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDeprecatedAndThusShouldNotDeleteComponentSinceUserHasItRetained = nativeFeature.deprecated
|
|
||||||
if (isDeprecatedAndThusShouldNotDeleteComponentSinceUserHasItRetained) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (componentsToDelete.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.services.mutator.setItemsToBeDeleted(componentsToDelete)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,13 +286,11 @@ export class PayloadManager extends AbstractService implements PayloadManagerInt
|
|||||||
/**
|
/**
|
||||||
* Imports an array of payloads from an external source (such as a backup file)
|
* Imports an array of payloads from an external source (such as a backup file)
|
||||||
* and marks the items as dirty.
|
* and marks the items as dirty.
|
||||||
* @returns Resulting items
|
|
||||||
*/
|
*/
|
||||||
public async importPayloads(payloads: DecryptedPayloadInterface[], historyMap: HistoryMap): Promise<string[]> {
|
public async importPayloads(payloads: FullyFormedPayloadInterface[], historyMap: HistoryMap): Promise<string[]> {
|
||||||
const sourcedPayloads = payloads.map((p) => p.copy(undefined, PayloadSource.FileImport))
|
const sourcedPayloads = payloads.map((p) => p.copy(undefined, PayloadSource.FileImport))
|
||||||
|
|
||||||
const delta = new DeltaFileImport(this.getMasterCollection(), sourcedPayloads, historyMap)
|
const delta = new DeltaFileImport(this.getMasterCollection(), sourcedPayloads, historyMap)
|
||||||
|
|
||||||
const emit = delta.result()
|
const emit = delta.result()
|
||||||
|
|
||||||
await this.emitDeltaEmit(emit)
|
await this.emitDeltaEmit(emit)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const VaultTests = {
|
|||||||
'vaults/pkc.test.js',
|
'vaults/pkc.test.js',
|
||||||
'vaults/contacts.test.js',
|
'vaults/contacts.test.js',
|
||||||
'vaults/crypto.test.js',
|
'vaults/crypto.test.js',
|
||||||
|
'vaults/importing.test.js',
|
||||||
'vaults/asymmetric-messages.test.js',
|
'vaults/asymmetric-messages.test.js',
|
||||||
'vaults/keypair-change.test.js',
|
'vaults/keypair-change.test.js',
|
||||||
'vaults/signatures.test.js',
|
'vaults/signatures.test.js',
|
||||||
@@ -16,7 +17,7 @@ export const VaultTests = {
|
|||||||
'vaults/conflicts.test.js',
|
'vaults/conflicts.test.js',
|
||||||
'vaults/deletion.test.js',
|
'vaults/deletion.test.js',
|
||||||
'vaults/permissions.test.js',
|
'vaults/permissions.test.js',
|
||||||
'vaults/key_rotation.test.js',
|
'vaults/key-rotation.test.js',
|
||||||
'vaults/files.test.js',
|
'vaults/files.test.js',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -755,7 +755,7 @@ describe('keys', function () {
|
|||||||
currentServerPassword: currentRootKey.serverPassword,
|
currentServerPassword: currentRootKey.serverPassword,
|
||||||
newRootKey,
|
newRootKey,
|
||||||
})
|
})
|
||||||
await this.application.encryption.reencryptApplicableItemsAfterUserRootKeyChange()
|
await this.application.dependencies.get(TYPES.ReencryptTypeAItems).execute()
|
||||||
/** Note: this may result in a deadlock if features_service syncs and results in an error */
|
/** Note: this may result in a deadlock if features_service syncs and results in an error */
|
||||||
await this.application.sync.sync({ awaitAll: true })
|
await this.application.sync.sync({ awaitAll: true })
|
||||||
|
|
||||||
|
|||||||
@@ -369,6 +369,17 @@ export class AppContext {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spyOnFunctionResult(object, functionName) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
sinon.stub(object, functionName).callsFake(async (params) => {
|
||||||
|
object[functionName].restore()
|
||||||
|
const result = await object[functionName](params)
|
||||||
|
resolve(result)
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
resolveWhenAsymmetricMessageProcessingCompletes() {
|
resolveWhenAsymmetricMessageProcessingCompletes() {
|
||||||
return this.resolveWhenAsyncFunctionCompletes(this.asymmetric, 'handleRemoteReceivedAsymmetricMessages')
|
return this.resolveWhenAsyncFunctionCompletes(this.asymmetric, 'handleRemoteReceivedAsymmetricMessages')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,71 +121,4 @@ describe('migrations', () => {
|
|||||||
|
|
||||||
await Factory.safeDeinit(application)
|
await Factory.safeDeinit(application)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('2.202.1', () => {
|
|
||||||
let application
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
application = await Factory.createAppWithRandNamespace()
|
|
||||||
|
|
||||||
await application.prepareForLaunch({
|
|
||||||
receiveChallenge: () => {},
|
|
||||||
})
|
|
||||||
await application.launch(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await Factory.safeDeinit(application)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('remove components that are available as native features', async function () {
|
|
||||||
const editor = CreateDecryptedItemFromPayload(
|
|
||||||
new DecryptedPayload({
|
|
||||||
uuid: '123',
|
|
||||||
content_type: ContentType.TYPES.Component,
|
|
||||||
content: FillItemContent({
|
|
||||||
package_info: {
|
|
||||||
identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
await application.mutator.insertItem(editor)
|
|
||||||
await application.sync.sync()
|
|
||||||
|
|
||||||
expect(application.items.getItems(ContentType.TYPES.Component).length).to.equal(1)
|
|
||||||
|
|
||||||
/** Run migration */
|
|
||||||
const migration = new Migration2_202_1(application.migrations.services)
|
|
||||||
await migration.handleStage(ApplicationStage.FullSyncCompleted_13)
|
|
||||||
await application.sync.sync()
|
|
||||||
|
|
||||||
expect(application.items.getItems(ContentType.TYPES.Component).length).to.equal(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('do not remove components that are available as native features but deprecated', async function () {
|
|
||||||
const editor = CreateDecryptedItemFromPayload(
|
|
||||||
new DecryptedPayload({
|
|
||||||
uuid: '123',
|
|
||||||
content_type: ContentType.TYPES.Component,
|
|
||||||
content: FillItemContent({
|
|
||||||
package_info: {
|
|
||||||
identifier: NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
await application.mutator.insertItem(editor)
|
|
||||||
await application.sync.sync()
|
|
||||||
|
|
||||||
expect(application.items.getItems(ContentType.TYPES.Component).length).to.equal(1)
|
|
||||||
|
|
||||||
/** Run migration */
|
|
||||||
const migration = new Migration2_202_1(application.migrations.services)
|
|
||||||
await migration.handleStage(ApplicationStage.FullSyncCompleted_13)
|
|
||||||
await application.sync.sync()
|
|
||||||
|
|
||||||
expect(application.items.getItems(ContentType.TYPES.Component).length).to.equal(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -882,8 +882,4 @@ describe('importing', function () {
|
|||||||
expect(application.items.referencesForItem(importedTag).length).to.equal(1)
|
expect(application.items.referencesForItem(importedTag).length).to.equal(1)
|
||||||
expect(application.items.itemsReferencingItem(importedNote).length).to.equal(1)
|
expect(application.items.itemsReferencingItem(importedNote).length).to.equal(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should decrypt backup file which contains a vaulted note without a synced key system root key', async () => {
|
|
||||||
console.error('TODO: Implement this test')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ describe('contacts', function () {
|
|||||||
await deinitContactContext()
|
await deinitContactContext()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should be able to refresh a contact using a collaborationID that includes full chain of previouos public keys', async () => {
|
it('should be able to refresh a contact using a collaborationID that includes full chain of previous public keys', async () => {
|
||||||
console.error('TODO: implement test')
|
console.error('TODO: implement test')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,12 +35,25 @@ describe('shared vault crypto', function () {
|
|||||||
expect(recreatedContext.encryption.getSigningKeyPair()).to.not.be.undefined
|
expect(recreatedContext.encryption.getSigningKeyPair()).to.not.be.undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
it('changing user password should re-encrypt all key system root keys', async () => {
|
it('changing user password should re-encrypt all key system root keys and contacts with new user root key', async () => {
|
||||||
console.error('TODO: implement')
|
await Collaboration.createPrivateVault(context)
|
||||||
})
|
const spy = context.spyOnFunctionResult(context.application.sync, 'payloadsByPreparingForServer')
|
||||||
|
await context.changePassword('new_password')
|
||||||
|
|
||||||
it('changing user password should re-encrypt all trusted contacts', async () => {
|
const payloads = await spy
|
||||||
console.error('TODO: implement')
|
const keyPayloads = payloads.filter(
|
||||||
|
(payload) =>
|
||||||
|
payload.content_type === ContentType.TYPES.KeySystemRootKey ||
|
||||||
|
payload.content_type === ContentType.TYPES.TrustedContact,
|
||||||
|
)
|
||||||
|
expect(keyPayloads.length).to.equal(2)
|
||||||
|
|
||||||
|
for (const payload of payloads) {
|
||||||
|
const keyParams = context.encryption.getEmbeddedPayloadAuthenticatedData(new EncryptedPayload(payload)).kp
|
||||||
|
|
||||||
|
const userKeyParams = context.encryption.getRootKeyParams().content
|
||||||
|
expect(keyParams).to.eql(userKeyParams)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
58
packages/snjs/mocha/vaults/importing.test.js
Normal file
58
packages/snjs/mocha/vaults/importing.test.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import * as Factory from '../lib/factory.js'
|
||||||
|
import * as Collaboration from '../lib/Collaboration.js'
|
||||||
|
|
||||||
|
chai.use(chaiAsPromised)
|
||||||
|
const expect = chai.expect
|
||||||
|
|
||||||
|
describe.skip('vault importing', function () {
|
||||||
|
this.timeout(Factory.TwentySecondTimeout)
|
||||||
|
|
||||||
|
let context
|
||||||
|
|
||||||
|
afterEach(async function () {
|
||||||
|
await context.deinit()
|
||||||
|
localStorage.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
localStorage.clear()
|
||||||
|
|
||||||
|
context = await Factory.createAppContextWithRealCrypto()
|
||||||
|
|
||||||
|
await context.launch()
|
||||||
|
await context.register()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should import vaulted items with synced root key', async () => {
|
||||||
|
console.error('TODO: implement')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should import vaulted items with non-present root key', async () => {
|
||||||
|
const vault = await context.vaults.createUserInputtedPasswordVault({
|
||||||
|
name: 'test vault',
|
||||||
|
userInputtedPassword: 'test password',
|
||||||
|
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
|
||||||
|
})
|
||||||
|
|
||||||
|
const note = await context.createSyncedNote('foo', 'bar')
|
||||||
|
await Collaboration.moveItemToVault(context, vault, note)
|
||||||
|
|
||||||
|
const backupData = await context.application.createEncryptedBackupFileForAutomatedDesktopBackups()
|
||||||
|
|
||||||
|
const otherContext = await Factory.createAppContextWithRealCrypto()
|
||||||
|
await otherContext.launch()
|
||||||
|
|
||||||
|
await otherContext.application.importData(backupData)
|
||||||
|
|
||||||
|
const expectedImportedItems = ['vault-items-key', 'note']
|
||||||
|
const invalidItems = otherContext.items.invalidItems
|
||||||
|
expect(invalidItems.length).to.equal(expectedImportedItems.length)
|
||||||
|
|
||||||
|
const encryptedItem = invalidItems[0]
|
||||||
|
expect(encryptedItem.key_system_identifier).to.equal(vault.systemIdentifier)
|
||||||
|
expect(encryptedItem.errorDecrypting).to.be.true
|
||||||
|
expect(encryptedItem.uuid).to.equal(note.uuid)
|
||||||
|
|
||||||
|
await otherContext.deinit()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -4,7 +4,7 @@ import * as Collaboration from '../lib/Collaboration.js'
|
|||||||
chai.use(chaiAsPromised)
|
chai.use(chaiAsPromised)
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
describe('shared vault key rotation', function () {
|
describe('vault key rotation', function () {
|
||||||
this.timeout(Factory.TwentySecondTimeout)
|
this.timeout(Factory.TwentySecondTimeout)
|
||||||
|
|
||||||
let context
|
let context
|
||||||
@@ -29,17 +29,66 @@ describe('shared vault key rotation', function () {
|
|||||||
|
|
||||||
contactContext.lockSyncing()
|
contactContext.lockSyncing()
|
||||||
|
|
||||||
const spy = sinon.spy(context.keys, 'queueVaultItemsKeysForReencryption')
|
const callSpy = sinon.spy(context.keys, 'queueVaultItemsKeysForReencryption')
|
||||||
|
const syncSpy = context.spyOnFunctionResult(context.application.sync, 'payloadsByPreparingForServer')
|
||||||
|
|
||||||
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
|
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
|
||||||
await context.vaults.rotateVaultRootKey(sharedVault)
|
await context.vaults.rotateVaultRootKey(sharedVault)
|
||||||
await promise
|
await promise
|
||||||
|
await syncSpy
|
||||||
|
|
||||||
expect(spy.callCount).to.equal(1)
|
expect(callSpy.callCount).to.equal(1)
|
||||||
|
|
||||||
|
const payloads = await syncSpy
|
||||||
|
const keyPayloads = payloads.filter((payload) => payload.content_type === ContentType.TYPES.KeySystemItemsKey)
|
||||||
|
expect(keyPayloads.length).to.equal(2)
|
||||||
|
|
||||||
|
const vaultRootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
|
||||||
|
|
||||||
|
for (const payload of keyPayloads) {
|
||||||
|
const keyParams = context.encryption.getEmbeddedPayloadAuthenticatedData(new EncryptedPayload(payload)).kp
|
||||||
|
expect(keyParams).to.eql(vaultRootKey.keyParams)
|
||||||
|
}
|
||||||
|
|
||||||
deinitContactContext()
|
deinitContactContext()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should update value of local storage mode key', async () => {
|
||||||
|
const vault = await context.vaults.createUserInputtedPasswordVault({
|
||||||
|
name: 'test vault',
|
||||||
|
userInputtedPassword: 'test password',
|
||||||
|
storagePreference: KeySystemRootKeyStorageMode.Local,
|
||||||
|
})
|
||||||
|
|
||||||
|
const beforeKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier)
|
||||||
|
|
||||||
|
await context.vaults.rotateVaultRootKey(vault, 'test password')
|
||||||
|
|
||||||
|
const afterKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier)
|
||||||
|
|
||||||
|
expect(afterKey.keyParams.creationTimestamp).to.be.greaterThan(beforeKey.keyParams.creationTimestamp)
|
||||||
|
expect(afterKey.key).to.not.equal(beforeKey.key)
|
||||||
|
expect(afterKey.itemsKey).to.not.equal(beforeKey.itemsKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update value of mem storage mode key', async () => {
|
||||||
|
const vault = await context.vaults.createUserInputtedPasswordVault({
|
||||||
|
name: 'test vault',
|
||||||
|
userInputtedPassword: 'test password',
|
||||||
|
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
|
||||||
|
})
|
||||||
|
|
||||||
|
const beforeKey = context.keys.getMemCachedRootKey(vault.systemIdentifier)
|
||||||
|
|
||||||
|
await context.vaults.rotateVaultRootKey(vault, 'test password')
|
||||||
|
|
||||||
|
const afterKey = context.keys.getMemCachedRootKey(vault.systemIdentifier)
|
||||||
|
|
||||||
|
expect(afterKey.keyParams.creationTimestamp).to.be.greaterThan(beforeKey.keyParams.creationTimestamp)
|
||||||
|
expect(afterKey.key).to.not.equal(beforeKey.key)
|
||||||
|
expect(afterKey.itemsKey).to.not.equal(beforeKey.itemsKey)
|
||||||
|
})
|
||||||
|
|
||||||
it("rotating a vault's key should send an asymmetric message to all members", async () => {
|
it("rotating a vault's key should send an asymmetric message to all members", async () => {
|
||||||
const { sharedVault, contactContext, deinitContactContext } =
|
const { sharedVault, contactContext, deinitContactContext } =
|
||||||
await Collaboration.createSharedVaultWithAcceptedInvite(context)
|
await Collaboration.createSharedVaultWithAcceptedInvite(context)
|
||||||
@@ -104,10 +104,27 @@ describe('shared vaults', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should convert a vault to a shared vault', async () => {
|
it('should convert a vault to a shared vault', async () => {
|
||||||
console.error('TODO')
|
const privateVault = await context.vaults.createRandomizedVault({
|
||||||
})
|
name: 'My Private Vault',
|
||||||
|
})
|
||||||
|
|
||||||
it('should send metadata change message when changing name or description', async () => {
|
const note = await context.createSyncedNote('foo', 'bar')
|
||||||
console.error('TODO')
|
await context.vaults.moveItemToVault(privateVault, note)
|
||||||
|
|
||||||
|
const sharedVault = await context.sharedVaults.convertVaultToSharedVault(privateVault)
|
||||||
|
|
||||||
|
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
|
||||||
|
context,
|
||||||
|
sharedVault,
|
||||||
|
)
|
||||||
|
|
||||||
|
await Collaboration.acceptAllInvites(thirdPartyContext)
|
||||||
|
|
||||||
|
const contextNote = thirdPartyContext.items.findItem(note.uuid)
|
||||||
|
expect(contextNote).to.not.be.undefined
|
||||||
|
expect(contextNote.title).to.equal('foo')
|
||||||
|
expect(contextNote.text).to.equal(note.text)
|
||||||
|
|
||||||
|
await deinitThirdPartyContext()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user