tests: vault tests 3 (#2373)
This commit is contained in:
@@ -14,9 +14,9 @@ export class GetPayloadAuthenticatedDataDetachedUseCase {
|
||||
execute(
|
||||
encrypted: EncryptedOutputParameters,
|
||||
): 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<
|
||||
RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { ConflictDelta } from './Conflict'
|
||||
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
|
||||
import { DeletedPayloadInterface, isDecryptedPayload, PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { FullyFormedPayloadInterface, isDecryptedPayload, PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { HistoryMap } from '../History'
|
||||
import { extendSyncDelta, SourcelessSyncDeltaEmit, SyncDeltaEmit } from './Abstract/DeltaEmit'
|
||||
import { DeltaInterface } from './Abstract/DeltaInterface'
|
||||
@@ -11,7 +10,7 @@ import { getIncrementedDirtyIndex } from '../DirtyCounter/DirtyCounter'
|
||||
export class DeltaFileImport implements DeltaInterface {
|
||||
constructor(
|
||||
readonly baseCollection: ImmutablePayloadCollection,
|
||||
private readonly applyPayloads: DecryptedPayloadInterface[],
|
||||
private readonly applyPayloads: FullyFormedPayloadInterface[],
|
||||
protected readonly historyMap: HistoryMap,
|
||||
) {}
|
||||
|
||||
@@ -31,10 +30,7 @@ export class DeltaFileImport implements DeltaInterface {
|
||||
return result
|
||||
}
|
||||
|
||||
private resolvePayload(
|
||||
payload: DecryptedPayloadInterface | DeletedPayloadInterface,
|
||||
currentResults: SyncDeltaEmit,
|
||||
): SourcelessSyncDeltaEmit {
|
||||
private resolvePayload(payload: FullyFormedPayloadInterface, currentResults: SyncDeltaEmit): SourcelessSyncDeltaEmit {
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -89,7 +89,6 @@ export interface EncryptionProviderInterface {
|
||||
setNewRootKeyWrapper(wrappingKey: RootKeyInterface): Promise<void>
|
||||
|
||||
createNewItemsKeyWithRollback(): Promise<() => Promise<void>>
|
||||
reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void>
|
||||
getSureDefaultItemsKey(): ItemsKeyInterface
|
||||
|
||||
createRandomizedKeySystemRootKey(dto: { systemIdentifier: KeySystemIdentifier }): KeySystemRootKeyInterface
|
||||
|
||||
@@ -240,10 +240,6 @@ export class EncryptionService
|
||||
return this.itemsEncryption.repersistAllItems()
|
||||
}
|
||||
|
||||
public async reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void> {
|
||||
await this.rootKeyManager.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
}
|
||||
|
||||
public async createNewItemsKeyWithRollback(): Promise<() => Promise<void>> {
|
||||
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(
|
||||
keySystemIdentifier: KeySystemIdentifier,
|
||||
): KeySystemRootKeyInterface | undefined {
|
||||
getRootKeyFromStorageForVault(keySystemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface | undefined {
|
||||
const payload = this.storage.getValue<DecryptedTransferPayload<KeySystemRootKeyContent>>(
|
||||
this.storageKeyForRootKey(keySystemIdentifier),
|
||||
)
|
||||
@@ -94,6 +92,10 @@ export class KeySystemKeyManager
|
||||
return key
|
||||
}
|
||||
|
||||
getMemCachedRootKey(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface {
|
||||
return this.rootKeyMemoryCache[systemIdentifier]
|
||||
}
|
||||
|
||||
private storageKeyForRootKey(systemIdentifier: KeySystemIdentifier): string {
|
||||
return `${RootKeyStorageKeyPrefix}${systemIdentifier}`
|
||||
}
|
||||
|
||||
@@ -9,18 +9,16 @@ import { ProtocolVersion, compareVersions } from '@standardnotes/common'
|
||||
import {
|
||||
BackupFile,
|
||||
BackupFileDecryptedContextualPayload,
|
||||
ComponentContent,
|
||||
CopyPayloadWithContentOverride,
|
||||
CreateDecryptedBackupFileContextPayload,
|
||||
CreateEncryptedBackupFileContextPayload,
|
||||
DecryptedItemInterface,
|
||||
DecryptedPayloadInterface,
|
||||
isDecryptedPayload,
|
||||
isEncryptedPayload,
|
||||
isEncryptedTransferPayload,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { Challenge, ChallengePrompt, ChallengeReason, ChallengeValidation } from '../Challenge'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
|
||||
|
||||
const Strings = {
|
||||
@@ -57,44 +55,22 @@ export class ImportDataUseCase {
|
||||
* .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.
|
||||
*/
|
||||
|
||||
async execute(data: BackupFile, awaitSync = false): Promise<ImportDataReturnType> {
|
||||
if (data.version) {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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) }
|
||||
const result = this.validateVersion(data.version)
|
||||
if (result.isFailed()) {
|
||||
return { error: new ClientDisplayableError(result.getError()) }
|
||||
}
|
||||
}
|
||||
|
||||
let password: string | undefined
|
||||
|
||||
if (data.auth_params || data.keyParams) {
|
||||
/** Get import file password. */
|
||||
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 { error: new ClientDisplayableError('Import aborted') }
|
||||
const passwordResult = await this.getFilePassword()
|
||||
if (passwordResult.isFailed()) {
|
||||
return { error: new ClientDisplayableError(passwordResult.getError()) }
|
||||
}
|
||||
this.challengeService.completeChallenge(challenge)
|
||||
password = passwordResponse?.values[0].value as string
|
||||
password = passwordResult.getValue()
|
||||
}
|
||||
|
||||
if (!(await this.protectionService.authorizeFileImport())) {
|
||||
@@ -110,31 +86,23 @@ export class ImportDataUseCase {
|
||||
})
|
||||
|
||||
const decryptedPayloadsOrError = await this._decryptBackFile.execute(data, password)
|
||||
|
||||
if (decryptedPayloadsOrError instanceof ClientDisplayableError) {
|
||||
return { error: decryptedPayloadsOrError }
|
||||
}
|
||||
|
||||
const validPayloads = decryptedPayloadsOrError.filter(isDecryptedPayload).map((payload) => {
|
||||
/* Don't want to activate any components during import process in
|
||||
* case of exceptions breaking up the import proccess */
|
||||
if (payload.content_type === ContentType.TYPES.Component && (payload.content as ComponentContent).active) {
|
||||
const typedContent = payload as DecryptedPayloadInterface<ComponentContent>
|
||||
return CopyPayloadWithContentOverride(typedContent, {
|
||||
active: false,
|
||||
})
|
||||
} else {
|
||||
return payload
|
||||
}
|
||||
const decryptedPayloads = decryptedPayloadsOrError.filter(isDecryptedPayload)
|
||||
const encryptedPayloads = decryptedPayloadsOrError.filter(isEncryptedPayload)
|
||||
const acceptableEncryptedPayloads = encryptedPayloads.filter((payload) => {
|
||||
return payload.key_system_identifier !== undefined
|
||||
})
|
||||
const importablePayloads = [...decryptedPayloads, ...acceptableEncryptedPayloads]
|
||||
|
||||
const affectedUuids = await this.payloadManager.importPayloads(
|
||||
validPayloads,
|
||||
importablePayloads,
|
||||
this.historyService.getHistoryMapCopy(),
|
||||
)
|
||||
|
||||
const promise = this.sync.sync()
|
||||
|
||||
if (awaitSync) {
|
||||
await promise
|
||||
}
|
||||
@@ -143,7 +111,42 @@ export class ImportDataUseCase {
|
||||
|
||||
return {
|
||||
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,
|
||||
FullyFormedPayloadInterface,
|
||||
PayloadEmitSource,
|
||||
DecryptedPayloadInterface,
|
||||
HistoryMap,
|
||||
} from '@standardnotes/models'
|
||||
import { IntegrityPayload } from '@standardnotes/responses'
|
||||
@@ -24,7 +23,7 @@ export interface PayloadManagerInterface {
|
||||
*/
|
||||
get nonDeletedItems(): FullyFormedPayloadInterface[]
|
||||
|
||||
importPayloads(payloads: DecryptedPayloadInterface[], historyMap: HistoryMap): Promise<string[]>
|
||||
importPayloads(payloads: FullyFormedPayloadInterface[], historyMap: HistoryMap): Promise<string[]>
|
||||
|
||||
removePayloadLocally(payload: FullyFormedPayloadInterface | FullyFormedPayloadInterface[]): void
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
EncryptionOperatorsInterface,
|
||||
} from '@standardnotes/encryption'
|
||||
import {
|
||||
ContentTypesUsingRootKeyEncryption,
|
||||
DecryptedPayload,
|
||||
DecryptedTransferPayload,
|
||||
EncryptedPayload,
|
||||
@@ -32,12 +31,11 @@ import { StorageValueModes } from '../Storage/StorageTypes'
|
||||
import { EncryptTypeAPayload } from '../Encryption/UseCase/TypeA/EncryptPayload'
|
||||
import { DecryptTypeAPayload } from '../Encryption/UseCase/TypeA/DecryptPayload'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { MutatorClientInterface } from '../Mutator/MutatorClientInterface'
|
||||
import { RootKeyManagerEvent } from './RootKeyManagerEvent'
|
||||
import { ValidatePasscodeResult } from './ValidatePasscodeResult'
|
||||
import { ValidateAccountPasswordResult } from './ValidateAccountPasswordResult'
|
||||
import { KeyMode } from './KeyMode'
|
||||
import { ReencryptTypeAItems } from '../Encryption/UseCase/TypeA/ReencryptTypeAItems'
|
||||
|
||||
export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||
private rootKey?: RootKeyInterface
|
||||
@@ -47,10 +45,9 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||
constructor(
|
||||
private device: DeviceInterface,
|
||||
private storage: StorageServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private operators: EncryptionOperatorsInterface,
|
||||
private identifier: ApplicationIdentifier,
|
||||
private _reencryptTypeAItems: ReencryptTypeAItems,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
@@ -58,6 +55,12 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||
|
||||
override 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.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.setRootKeyInstance(wrappingKey)
|
||||
await this.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
await this._reencryptTypeAItems.execute()
|
||||
} else {
|
||||
await this.wrapAndPersistRootKey(wrappingKey)
|
||||
}
|
||||
@@ -473,19 +476,4 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||
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 { UserApiServiceInterface } from '@standardnotes/api'
|
||||
import { UserRequestType } from '@standardnotes/common'
|
||||
@@ -25,6 +26,7 @@ describe('UserService', () => {
|
||||
let challengeService: ChallengeServiceInterface
|
||||
let protectionService: ProtectionsClientInterface
|
||||
let userApiService: UserApiServiceInterface
|
||||
let reencryptTypeAItems: ReencryptTypeAItems
|
||||
let internalEventBus: InternalEventBusInterface
|
||||
|
||||
const createService = () =>
|
||||
@@ -38,6 +40,7 @@ describe('UserService', () => {
|
||||
challengeService,
|
||||
protectionService,
|
||||
userApiService,
|
||||
reencryptTypeAItems,
|
||||
internalEventBus,
|
||||
)
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import { AccountEvent } from './AccountEvent'
|
||||
import { SignedInOrRegisteredEventPayload } from './SignedInOrRegisteredEventPayload'
|
||||
import { CredentialsChangeFunctionResponse } from './CredentialsChangeFunctionResponse'
|
||||
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
|
||||
import { ReencryptTypeAItems } from '../Encryption/UseCase/TypeA/ReencryptTypeAItems'
|
||||
|
||||
export class UserService
|
||||
extends AbstractService<AccountEvent, AccountEventData>
|
||||
@@ -49,33 +50,48 @@ export class UserService
|
||||
private readonly MINIMUM_PASSWORD_LENGTH = 8
|
||||
|
||||
constructor(
|
||||
private sessionManager: SessionsClientInterface,
|
||||
private sessions: SessionsClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private storageService: StorageServiceInterface,
|
||||
private itemManager: ItemManagerInterface,
|
||||
private encryptionService: EncryptionProviderInterface,
|
||||
private alertService: AlertService,
|
||||
private challengeService: ChallengeServiceInterface,
|
||||
private protectionService: ProtectionsClientInterface,
|
||||
private userApiService: UserApiServiceInterface,
|
||||
private storage: StorageServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private alerts: AlertService,
|
||||
private challenges: ChallengeServiceInterface,
|
||||
private protections: ProtectionsClientInterface,
|
||||
private userApi: UserApiServiceInterface,
|
||||
private _reencryptTypeAItems: ReencryptTypeAItems,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
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> {
|
||||
if (event.type === AccountEvent.SignedInOrRegistered) {
|
||||
const payload = (event.payload as AccountEventData).payload as SignedInOrRegisteredEventPayload
|
||||
this.sync.resetSyncState()
|
||||
|
||||
await this.storageService.setPersistencePolicy(
|
||||
await this.storage.setPersistencePolicy(
|
||||
payload.ephemeral ? StoragePersistencePolicies.Ephemeral : StoragePersistencePolicies.Default,
|
||||
)
|
||||
|
||||
if (payload.mergeLocal) {
|
||||
await this.sync.markAllItemsAsNeedingSyncAndPersist()
|
||||
} else {
|
||||
void this.itemManager.removeAllItemsFromMemory()
|
||||
void this.items.removeAllItemsFromMemory()
|
||||
await this.clearDatabase()
|
||||
}
|
||||
|
||||
@@ -88,37 +104,24 @@ export class UserService
|
||||
})
|
||||
.then(() => {
|
||||
if (!payload.awaitSync) {
|
||||
void this.encryptionService.decryptErroredPayloads()
|
||||
void this.encryption.decryptErroredPayloads()
|
||||
}
|
||||
})
|
||||
|
||||
if (payload.awaitSync) {
|
||||
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 {
|
||||
return this.sessionManager.userUuid
|
||||
return this.sessions.userUuid
|
||||
}
|
||||
|
||||
isSignedIn(): boolean {
|
||||
return this.sessionManager.isSignedIn()
|
||||
return this.sessions.isSignedIn()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,7 +134,7 @@ export class UserService
|
||||
ephemeral = false,
|
||||
mergeLocal = true,
|
||||
): Promise<UserRegistrationResponseBody> {
|
||||
if (this.encryptionService.hasAccount()) {
|
||||
if (this.encryption.hasAccount()) {
|
||||
throw Error('Tried to register when an account already exists.')
|
||||
}
|
||||
|
||||
@@ -143,7 +146,7 @@ export class UserService
|
||||
|
||||
try {
|
||||
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, {
|
||||
payload: {
|
||||
@@ -177,7 +180,7 @@ export class UserService
|
||||
mergeLocal = true,
|
||||
awaitSync = false,
|
||||
): Promise<HttpResponse<SignInResponse>> {
|
||||
if (this.encryptionService.hasAccount()) {
|
||||
if (this.encryption.hasAccount()) {
|
||||
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. */
|
||||
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)) {
|
||||
const notifyingFunction = awaitSync ? this.notifyEventSync.bind(this) : this.notifyEvent.bind(this)
|
||||
@@ -218,7 +221,7 @@ export class UserService
|
||||
message?: string
|
||||
}> {
|
||||
if (
|
||||
!(await this.protectionService.authorizeAction(ChallengeReason.DeleteAccount, {
|
||||
!(await this.protections.authorizeAction(ChallengeReason.DeleteAccount, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: true,
|
||||
forcePrompt: false,
|
||||
@@ -230,8 +233,8 @@ export class UserService
|
||||
}
|
||||
}
|
||||
|
||||
const uuid = this.sessionManager.getSureUser().uuid
|
||||
const response = await this.userApiService.deleteAccount(uuid)
|
||||
const uuid = this.sessions.getSureUser().uuid
|
||||
const response = await this.userApi.deleteAccount(uuid)
|
||||
if (isErrorResponse(response)) {
|
||||
return {
|
||||
error: true,
|
||||
@@ -241,7 +244,7 @@ export class UserService
|
||||
|
||||
await this.signOut(true)
|
||||
|
||||
void this.alertService.alert(InfoStrings.AccountDeleted)
|
||||
void this.alerts.alert(InfoStrings.AccountDeleted)
|
||||
|
||||
return {
|
||||
error: false,
|
||||
@@ -249,9 +252,9 @@ export class UserService
|
||||
}
|
||||
|
||||
async submitUserRequest(requestType: UserRequestType): Promise<boolean> {
|
||||
const userUuid = this.sessionManager.getSureUser().uuid
|
||||
const userUuid = this.sessions.getSureUser().uuid
|
||||
try {
|
||||
const result = await this.userApiService.submitUserRequest({
|
||||
const result = await this.userApi.submitUserRequest({
|
||||
userUuid,
|
||||
requestType,
|
||||
})
|
||||
@@ -274,11 +277,7 @@ export class UserService
|
||||
public async correctiveSignIn(rootKey: SNRootKey): Promise<HttpResponse<SignInResponse>> {
|
||||
this.lockSyncing()
|
||||
|
||||
const response = await this.sessionManager.bypassChecksAndSignInWithRootKey(
|
||||
rootKey.keyParams.identifier,
|
||||
rootKey,
|
||||
false,
|
||||
)
|
||||
const response = await this.sessions.bypassChecksAndSignInWithRootKey(rootKey.keyParams.identifier, rootKey, false)
|
||||
|
||||
if (!isErrorResponse(response)) {
|
||||
await this.notifyEvent(AccountEvent.SignedInOrRegistered, {
|
||||
@@ -313,16 +312,16 @@ export class UserService
|
||||
}): Promise<CredentialsChangeFunctionResponse> {
|
||||
const result = await this.performCredentialsChange(parameters)
|
||||
if (result.error) {
|
||||
void this.alertService.alert(result.error.message)
|
||||
void this.alerts.alert(result.error.message)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public async signOut(force = false, source = DeinitSource.SignOut): Promise<void> {
|
||||
const performSignOut = async () => {
|
||||
await this.sessionManager.signOut()
|
||||
await this.encryptionService.deleteWorkspaceSpecificKeyStateFromDevice()
|
||||
await this.storageService.clearAllData()
|
||||
await this.sessions.signOut()
|
||||
await this.encryption.deleteWorkspaceSpecificKeyStateFromDevice()
|
||||
await this.storage.clearAllData()
|
||||
await this.notifyEvent(AccountEvent.SignedOut, { payload: { source } })
|
||||
}
|
||||
|
||||
@@ -332,10 +331,10 @@ export class UserService
|
||||
return
|
||||
}
|
||||
|
||||
const dirtyItems = this.itemManager.getDirtyItems()
|
||||
const dirtyItems = this.items.getDirtyItems()
|
||||
if (dirtyItems.length > 0) {
|
||||
const singular = dirtyItems.length === 1
|
||||
const didConfirm = await this.alertService.confirm(
|
||||
const didConfirm = await this.alerts.confirm(
|
||||
`There ${singular ? 'is' : 'are'} ${dirtyItems.length} ${
|
||||
singular ? 'item' : 'items'
|
||||
} 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
|
||||
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')
|
||||
}
|
||||
|
||||
@@ -367,8 +366,8 @@ export class UserService
|
||||
canceled?: true
|
||||
error?: { message: string }
|
||||
}> {
|
||||
const hasPasscode = this.encryptionService.hasPasscode()
|
||||
const hasAccount = this.encryptionService.hasAccount()
|
||||
const hasPasscode = this.encryption.hasPasscode()
|
||||
const hasAccount = this.encryption.hasAccount()
|
||||
const prompts = []
|
||||
if (hasPasscode) {
|
||||
prompts.push(
|
||||
@@ -389,11 +388,11 @@ export class UserService
|
||||
)
|
||||
}
|
||||
const challenge = new Challenge(prompts, ChallengeReason.ProtocolUpgrade, true)
|
||||
const response = await this.challengeService.promptForChallengeResponse(challenge)
|
||||
const response = await this.challenges.promptForChallengeResponse(challenge)
|
||||
if (!response) {
|
||||
return { canceled: true }
|
||||
}
|
||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
||||
const dismissBlockingDialog = await this.alerts.blockingDialog(
|
||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||
Messages.UPGRADING_ENCRYPTION,
|
||||
)
|
||||
@@ -436,11 +435,11 @@ export class UserService
|
||||
if (passcode.length < this.MINIMUM_PASSCODE_LENGTH) {
|
||||
return false
|
||||
}
|
||||
if (!(await this.protectionService.authorizeAddingPasscode())) {
|
||||
if (!(await this.protections.authorizeAddingPasscode())) {
|
||||
return false
|
||||
}
|
||||
|
||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
||||
const dismissBlockingDialog = await this.alerts.blockingDialog(
|
||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||
Messages.SETTING_PASSCODE,
|
||||
)
|
||||
@@ -453,11 +452,11 @@ export class UserService
|
||||
}
|
||||
|
||||
public async removePasscode(): Promise<boolean> {
|
||||
if (!(await this.protectionService.authorizeRemovingPasscode())) {
|
||||
if (!(await this.protections.authorizeRemovingPasscode())) {
|
||||
return false
|
||||
}
|
||||
|
||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
||||
const dismissBlockingDialog = await this.alerts.blockingDialog(
|
||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||
Messages.REMOVING_PASSCODE,
|
||||
)
|
||||
@@ -479,11 +478,11 @@ export class UserService
|
||||
if (newPasscode.length < this.MINIMUM_PASSCODE_LENGTH) {
|
||||
return false
|
||||
}
|
||||
if (!(await this.protectionService.authorizeChangingPasscode())) {
|
||||
if (!(await this.protections.authorizeChangingPasscode())) {
|
||||
return false
|
||||
}
|
||||
|
||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
||||
const dismissBlockingDialog = await this.alerts.blockingDialog(
|
||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||
origination === KeyParamsOrigination.ProtocolUpgrade
|
||||
? Messages.ProtocolUpgradeStrings.UpgradingPasscode
|
||||
@@ -499,7 +498,7 @@ export class UserService
|
||||
}
|
||||
|
||||
public async populateSessionFromDemoShareToken(token: Base64String): Promise<void> {
|
||||
await this.sessionManager.populateSessionFromDemoShareToken(token)
|
||||
await this.sessions.populateSessionFromDemoShareToken(token)
|
||||
await this.notifyEvent(AccountEvent.SignedInOrRegistered, {
|
||||
payload: {
|
||||
ephemeral: false,
|
||||
@@ -512,14 +511,14 @@ export class UserService
|
||||
|
||||
private async setPasscodeWithoutWarning(passcode: string, origination: KeyParamsOrigination) {
|
||||
const identifier = UuidGenerator.GenerateUuid()
|
||||
const key = await this.encryptionService.createRootKey(identifier, passcode, origination)
|
||||
await this.encryptionService.setNewRootKeyWrapper(key)
|
||||
const key = await this.encryption.createRootKey(identifier, passcode, origination)
|
||||
await this.encryption.setNewRootKeyWrapper(key)
|
||||
await this.rewriteItemsKeys()
|
||||
await this.sync.sync()
|
||||
}
|
||||
|
||||
private async removePasscodeWithoutWarning() {
|
||||
await this.encryptionService.removePasscode()
|
||||
await this.encryption.removePasscode()
|
||||
await this.rewriteItemsKeys()
|
||||
}
|
||||
|
||||
@@ -532,9 +531,9 @@ export class UserService
|
||||
* https://github.com/standardnotes/desktop/issues/131
|
||||
*/
|
||||
private async rewriteItemsKeys(): Promise<void> {
|
||||
const itemsKeys = this.itemManager.getDisplayableItemsKeys()
|
||||
const itemsKeys = this.items.getDisplayableItemsKeys()
|
||||
const payloads = itemsKeys.map((key) => key.payloadRepresentation())
|
||||
await this.storageService.deletePayloads(payloads)
|
||||
await this.storage.deletePayloads(payloads)
|
||||
await this.sync.persistPayloads(payloads)
|
||||
}
|
||||
|
||||
@@ -547,7 +546,7 @@ export class UserService
|
||||
}
|
||||
|
||||
private clearDatabase(): Promise<void> {
|
||||
return this.storageService.clearAllPayloads()
|
||||
return this.storage.clearAllPayloads()
|
||||
}
|
||||
|
||||
private async performCredentialsChange(parameters: {
|
||||
@@ -558,7 +557,7 @@ export class UserService
|
||||
newPassword?: string
|
||||
passcode?: string
|
||||
}): Promise<CredentialsChangeFunctionResponse> {
|
||||
const { wrappingKey, canceled } = await this.challengeService.getWrappingKeyIfApplicable(parameters.passcode)
|
||||
const { wrappingKey, canceled } = await this.challenges.getWrappingKeyIfApplicable(parameters.passcode)
|
||||
|
||||
if (canceled) {
|
||||
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) {
|
||||
return {
|
||||
error: Error(Messages.INVALID_PASSWORD),
|
||||
}
|
||||
}
|
||||
|
||||
const user = this.sessionManager.getUser() as User
|
||||
const user = this.sessions.getUser() as User
|
||||
const currentEmail = user.email
|
||||
const { currentRootKey, newRootKey } = await this.recomputeRootKeysForCredentialChange({
|
||||
currentPassword: parameters.currentPassword,
|
||||
@@ -591,7 +590,7 @@ export class UserService
|
||||
|
||||
this.lockSyncing()
|
||||
|
||||
const { response } = await this.sessionManager.changeCredentials({
|
||||
const { response } = await this.sessions.changeCredentials({
|
||||
currentServerPassword: currentRootKey.serverPassword as string,
|
||||
newRootKey: newRootKey,
|
||||
wrappingKey,
|
||||
@@ -604,20 +603,20 @@ export class UserService
|
||||
return { error: Error(response.data.error?.message) }
|
||||
}
|
||||
|
||||
const rollback = await this.encryptionService.createNewItemsKeyWithRollback()
|
||||
await this.encryptionService.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
const rollback = await this.encryption.createNewItemsKeyWithRollback()
|
||||
await this._reencryptTypeAItems.execute()
|
||||
await this.sync.sync({ awaitAll: true })
|
||||
|
||||
const defaultItemsKey = this.encryptionService.getSureDefaultItemsKey()
|
||||
const defaultItemsKey = this.encryption.getSureDefaultItemsKey()
|
||||
const itemsKeyWasSynced = !defaultItemsKey.neverSynced
|
||||
|
||||
if (!itemsKeyWasSynced) {
|
||||
await this.sessionManager.changeCredentials({
|
||||
await this.sessions.changeCredentials({
|
||||
currentServerPassword: newRootKey.serverPassword as string,
|
||||
newRootKey: currentRootKey,
|
||||
wrappingKey,
|
||||
})
|
||||
await this.encryptionService.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
await this._reencryptTypeAItems.execute()
|
||||
await rollback()
|
||||
await this.sync.sync({ awaitAll: true })
|
||||
|
||||
@@ -634,11 +633,11 @@ export class UserService
|
||||
newEmail?: string
|
||||
newPassword?: string
|
||||
}): Promise<{ currentRootKey: SNRootKey; newRootKey: SNRootKey }> {
|
||||
const currentRootKey = await this.encryptionService.computeRootKey(
|
||||
const currentRootKey = await this.encryption.computeRootKey(
|
||||
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.newPassword ?? parameters.currentPassword,
|
||||
parameters.origination,
|
||||
|
||||
@@ -75,6 +75,7 @@ export * from './Encryption/UseCase/TypeA/DecryptPayload'
|
||||
export * from './Encryption/UseCase/TypeA/DecryptPayloadWithKeyLookup'
|
||||
export * from './Encryption/UseCase/TypeA/EncryptPayload'
|
||||
export * from './Encryption/UseCase/TypeA/EncryptPayloadWithKeyLookup'
|
||||
export * from './Encryption/UseCase/TypeA/ReencryptTypeAItems'
|
||||
export * from './Event/ApplicationEvent'
|
||||
export * from './Event/ApplicationEventCallback'
|
||||
export * from './Event/ApplicationStageChangedEventPayload'
|
||||
|
||||
@@ -119,6 +119,7 @@ import {
|
||||
DeleteContact,
|
||||
VaultLockService,
|
||||
RemoveItemsFromMemory,
|
||||
ReencryptTypeAItems,
|
||||
} from '@standardnotes/services'
|
||||
import { ItemManager } from '../../Services/Items/ItemManager'
|
||||
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
|
||||
@@ -202,6 +203,10 @@ export class Dependencies {
|
||||
}
|
||||
|
||||
private registerUseCaseMakers() {
|
||||
this.factory.set(TYPES.ReencryptTypeAItems, () => {
|
||||
return new ReencryptTypeAItems(this.get(TYPES.ItemManager), this.get(TYPES.MutatorService))
|
||||
})
|
||||
|
||||
this.factory.set(TYPES.ImportDataUseCase, () => {
|
||||
return new ImportDataUseCase(
|
||||
this.get(TYPES.ItemManager),
|
||||
@@ -616,10 +621,9 @@ export class Dependencies {
|
||||
return new RootKeyManager(
|
||||
this.get(TYPES.DeviceInterface),
|
||||
this.get(TYPES.DiskStorageService),
|
||||
this.get(TYPES.ItemManager),
|
||||
this.get(TYPES.MutatorService),
|
||||
this.get(TYPES.EncryptionOperators),
|
||||
this.options.identifier,
|
||||
this.get(TYPES.ReencryptTypeAItems),
|
||||
this.get(TYPES.InternalEventBus),
|
||||
)
|
||||
})
|
||||
@@ -1086,6 +1090,7 @@ export class Dependencies {
|
||||
this.get(TYPES.ChallengeService),
|
||||
this.get(TYPES.ProtectionService),
|
||||
this.get(TYPES.UserApiService),
|
||||
this.get(TYPES.ReencryptTypeAItems),
|
||||
this.get(TYPES.InternalEventBus),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -151,6 +151,7 @@ export const TYPES = {
|
||||
DecryptBackupFile: Symbol.for('DecryptBackupFile'),
|
||||
IsVaultOwner: Symbol.for('IsVaultOwner'),
|
||||
RemoveItemsFromMemory: Symbol.for('RemoveItemsFromMemory'),
|
||||
ReencryptTypeAItems: Symbol.for('ReencryptTypeAItems'),
|
||||
|
||||
// Mappers
|
||||
SessionStorageMapper: Symbol.for('SessionStorageMapper'),
|
||||
|
||||
@@ -14,7 +14,6 @@ export class Migration2_202_1 extends Migration {
|
||||
this.registerStageHandler(ApplicationStage.FullSyncCompleted_13, async () => {
|
||||
await this.migrateComponentDataToUserPreferences()
|
||||
await this.migrateActiveComponentsToUserPreferences()
|
||||
await this.deleteComponentsWhichAreNativeFeatures()
|
||||
|
||||
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.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)
|
||||
* 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 delta = new DeltaFileImport(this.getMasterCollection(), sourcedPayloads, historyMap)
|
||||
|
||||
const emit = delta.result()
|
||||
|
||||
await this.emitDeltaEmit(emit)
|
||||
|
||||
@@ -6,6 +6,7 @@ export const VaultTests = {
|
||||
'vaults/pkc.test.js',
|
||||
'vaults/contacts.test.js',
|
||||
'vaults/crypto.test.js',
|
||||
'vaults/importing.test.js',
|
||||
'vaults/asymmetric-messages.test.js',
|
||||
'vaults/keypair-change.test.js',
|
||||
'vaults/signatures.test.js',
|
||||
@@ -16,7 +17,7 @@ export const VaultTests = {
|
||||
'vaults/conflicts.test.js',
|
||||
'vaults/deletion.test.js',
|
||||
'vaults/permissions.test.js',
|
||||
'vaults/key_rotation.test.js',
|
||||
'vaults/key-rotation.test.js',
|
||||
'vaults/files.test.js',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -755,7 +755,7 @@ describe('keys', function () {
|
||||
currentServerPassword: currentRootKey.serverPassword,
|
||||
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 */
|
||||
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() {
|
||||
return this.resolveWhenAsyncFunctionCompletes(this.asymmetric, 'handleRemoteReceivedAsymmetricMessages')
|
||||
}
|
||||
|
||||
@@ -121,71 +121,4 @@ describe('migrations', () => {
|
||||
|
||||
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.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()
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -35,12 +35,25 @@ describe('shared vault crypto', function () {
|
||||
expect(recreatedContext.encryption.getSigningKeyPair()).to.not.be.undefined
|
||||
})
|
||||
|
||||
it('changing user password should re-encrypt all key system root keys', async () => {
|
||||
console.error('TODO: implement')
|
||||
})
|
||||
it('changing user password should re-encrypt all key system root keys and contacts with new user root key', async () => {
|
||||
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 () => {
|
||||
console.error('TODO: implement')
|
||||
const payloads = await spy
|
||||
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)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('shared vault key rotation', function () {
|
||||
describe('vault key rotation', function () {
|
||||
this.timeout(Factory.TwentySecondTimeout)
|
||||
|
||||
let context
|
||||
@@ -29,17 +29,66 @@ describe('shared vault key rotation', function () {
|
||||
|
||||
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)
|
||||
await context.vaults.rotateVaultRootKey(sharedVault)
|
||||
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()
|
||||
})
|
||||
|
||||
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 () => {
|
||||
const { sharedVault, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInvite(context)
|
||||
@@ -104,10 +104,27 @@ describe('shared vaults', function () {
|
||||
})
|
||||
|
||||
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 () => {
|
||||
console.error('TODO')
|
||||
const note = await context.createSyncedNote('foo', 'bar')
|
||||
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