Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal: change password preprocessing step #2347

Merged
merged 11 commits into from Jul 6, 2023
Expand Up @@ -2,6 +2,11 @@ export enum AsymmetricMessagePayloadType {
ContactShare = 'contact-share',
SharedVaultRootKeyChanged = 'shared-vault-root-key-changed',
SenderKeypairChanged = 'sender-keypair-changed',
SharedVaultInvite = 'shared-vault-invite',
SharedVaultMetadataChanged = 'shared-vault-metadata-changed',

/**
* Shared Vault Invites conform to the asymmetric message protocol, but are sent via the dedicated
* SharedVaultInvite model and not the AsymmetricMessage model on the server side.
*/
SharedVaultInvite = 'shared-vault-invite',
}
@@ -1,3 +1,4 @@
import { AsymmetricMessageServiceInterface } from './../AsymmetricMessage/AsymmetricMessageServiceInterface'
import { SyncOptions } from './../Sync/SyncOptions'
import { ImportDataReturnType } from './../Mutator/ImportDataUseCase'
import { ChallengeServiceInterface } from './../Challenge/ChallengeServiceInterface'
Expand Down Expand Up @@ -102,6 +103,8 @@ export interface ApplicationInterface {
get vaults(): VaultServiceInterface
get challenges(): ChallengeServiceInterface
get alerts(): AlertService
get asymmetric(): AsymmetricMessageServiceInterface

readonly identifier: ApplicationIdentifier
readonly platform: Platform
deviceInterface: DeviceInterface
Expand Down
@@ -1,6 +1,6 @@
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { ContactServiceInterface } from './../Contacts/ContactServiceInterface'
import { AsymmetricMessageServerHash, ClientDisplayableError } from '@standardnotes/responses'
import { AsymmetricMessageServerHash, ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses'
import { SyncEvent, SyncEventReceivedAsymmetricMessagesData } from '../Event/SyncEvent'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
Expand All @@ -27,8 +27,12 @@ import { SendOwnContactChangeMessage } from './UseCase/SendOwnContactChangeMessa
import { GetOutboundAsymmetricMessages } from './UseCase/GetOutboundAsymmetricMessages'
import { GetInboundAsymmetricMessages } from './UseCase/GetInboundAsymmetricMessages'
import { GetVaultUseCase } from '../Vaults/UseCase/GetVault'
import { AsymmetricMessageServiceInterface } from './AsymmetricMessageServiceInterface'

export class AsymmetricMessageService extends AbstractService implements InternalEventHandlerInterface {
export class AsymmetricMessageService
extends AbstractService
implements AsymmetricMessageServiceInterface, InternalEventHandlerInterface
{
private messageServer: AsymmetricMessageServer

constructor(
Expand Down Expand Up @@ -69,7 +73,16 @@ export class AsymmetricMessageService extends AbstractService implements Interna
return usecase.execute()
}

async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise<void> {
public async downloadAndProcessInboundMessages(): Promise<void> {
const messages = await this.getInboundMessages()
if (isClientDisplayableError(messages)) {
return
}

await this.handleRemoteReceivedAsymmetricMessages(messages)
}

private async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise<void> {
if (!data.oldKeyPair || !data.oldSigningKeyPair) {
return
}
Expand Down
@@ -0,0 +1,7 @@
import { AsymmetricMessageServerHash, ClientDisplayableError } from '@standardnotes/responses'

export interface AsymmetricMessageServiceInterface {
getOutboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError>
getInboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError>
downloadAndProcessInboundMessages(): Promise<void>
}
2 changes: 1 addition & 1 deletion packages/services/src/Domain/Contacts/ContactService.ts
Expand Up @@ -237,7 +237,7 @@ export class ContactService
}

findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined {
return this.findTrustedContact(invite.user_uuid)
return this.findTrustedContact(invite.sender_uuid)
}

getCollaborationIDForTrustedContact(contact: TrustedContactInterface): string {
Expand Down
Expand Up @@ -21,7 +21,7 @@ import { PublicKeySet } from '@standardnotes/encryption'

export class SelfContactManager {
public selfContact?: TrustedContactInterface
private shouldReloadSelfContact = true

private isReloadingSelfContact = false
private eventDisposers: (() => void)[] = []

Expand All @@ -32,28 +32,34 @@ export class SelfContactManager {
private session: SessionsClientInterface,
private singletons: SingletonManagerInterface,
) {
this.eventDisposers.push(
items.addObserver(ContentType.TrustedContact, () => {
this.shouldReloadSelfContact = true
}),
)

this.eventDisposers.push(
sync.addEventObserver((event) => {
if (event === SyncEvent.SyncCompletedWithAllItemsUploaded || event === SyncEvent.LocalDataIncrementalLoad) {
void this.reloadSelfContact()
if (event === SyncEvent.LocalDataIncrementalLoad) {
this.loadSelfContactFromDatabase()
}

if (event === SyncEvent.SyncCompletedWithAllItemsUploaded) {
void this.reloadSelfContactAndCreateIfNecessary()
}
}),
)
}

public async handleApplicationStage(stage: ApplicationStage): Promise<void> {
if (stage === ApplicationStage.LoadedDatabase_12) {
this.selfContact = this.singletons.findSingleton<TrustedContactInterface>(
ContentType.UserPrefs,
TrustedContact.singletonPredicate,
)
this.loadSelfContactFromDatabase()
}
}

private loadSelfContactFromDatabase(): void {
if (this.selfContact) {
return
}

this.selfContact = this.singletons.findSingleton<TrustedContactInterface>(
ContentType.TrustedContact,
TrustedContact.singletonPredicate,
)
}

public async updateWithNewPublicKeySet(publicKeySet: PublicKeySet) {
Expand All @@ -74,12 +80,16 @@ export class SelfContactManager {
})
}

private async reloadSelfContact() {
private async reloadSelfContactAndCreateIfNecessary() {
if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) {
return
}

if (!this.shouldReloadSelfContact || this.isReloadingSelfContact) {
if (this.selfContact) {
return
}

if (this.isReloadingSelfContact) {
return
}

Expand All @@ -105,17 +115,13 @@ export class SelfContactManager {
}),
}

try {
this.selfContact = await this.singletons.findOrCreateSingleton<TrustedContactContent, TrustedContact>(
TrustedContact.singletonPredicate,
ContentType.TrustedContact,
FillItemContent<TrustedContactContent>(content),
)
this.selfContact = await this.singletons.findOrCreateSingleton<TrustedContactContent, TrustedContact>(
TrustedContact.singletonPredicate,
ContentType.TrustedContact,
FillItemContent<TrustedContactContent>(content),
)

this.shouldReloadSelfContact = false
} finally {
this.isReloadingSelfContact = false
}
this.isReloadingSelfContact = false
}

deinit() {
Expand Down
Expand Up @@ -283,6 +283,7 @@ export class EncryptionService
const usecase = new CreateNewItemsKeyWithRollbackUseCase(
this.mutator,
this.items,
this.storage,
this.operators,
this.rootKeyManager,
)
Expand Down
@@ -1,8 +1,10 @@
import { StorageServiceInterface } from './../../../Storage/StorageServiceInterface'
import { ItemsKeyMutator, OperatorManager, findDefaultItemsKey } from '@standardnotes/encryption'
import { MutatorClientInterface } from '../../../Mutator/MutatorClientInterface'
import { ItemManagerInterface } from '../../../Item/ItemManagerInterface'
import { RootKeyManager } from '../../RootKey/RootKeyManager'
import { CreateNewDefaultItemsKeyUseCase } from './CreateNewDefaultItemsKey'
import { RemoveItemsLocallyUseCase } from '../../../UseCase/RemoveItemsLocally'

export class CreateNewItemsKeyWithRollbackUseCase {
private createDefaultItemsKeyUseCase = new CreateNewDefaultItemsKeyUseCase(
Expand All @@ -12,9 +14,12 @@ export class CreateNewItemsKeyWithRollbackUseCase {
this.rootKeyManager,
)

private removeItemsLocallyUsecase = new RemoveItemsLocallyUseCase(this.items, this.storage)

constructor(
private mutator: MutatorClientInterface,
private items: ItemManagerInterface,
private storage: StorageServiceInterface,
private operatorManager: OperatorManager,
private rootKeyManager: RootKeyManager,
) {}
Expand All @@ -24,7 +29,7 @@ export class CreateNewItemsKeyWithRollbackUseCase {
const newDefaultItemsKey = await this.createDefaultItemsKeyUseCase.execute()

const rollback = async () => {
await this.mutator.setItemToBeDeleted(newDefaultItemsKey)
await this.removeItemsLocallyUsecase.execute([newDefaultItemsKey])

if (currentDefaultItemsKey) {
await this.mutator.changeItem<ItemsKeyMutator>(currentDefaultItemsKey, (mutator) => {
Expand Down
29 changes: 5 additions & 24 deletions packages/services/src/Domain/SharedVaults/SharedVaultService.ts
Expand Up @@ -111,7 +111,9 @@ export class SharedVaultService
)

this.eventDisposers.push(
items.addObserver<TrustedContactInterface>(ContentType.TrustedContact, ({ changed, inserted, source }) => {
items.addObserver<TrustedContactInterface>(ContentType.TrustedContact, async ({ changed, inserted, source }) => {
await this.reprocessCachedInvitesTrustStatusAfterTrustedContactsChange()

if (source === PayloadEmitSource.LocalChanged && inserted.length > 0) {
void this.handleCreationOfNewTrustedContacts(inserted)
}
Expand Down Expand Up @@ -250,8 +252,6 @@ export class SharedVaultService
}

private async handleTrustedContactsChange(contacts: TrustedContactInterface[]): Promise<void> {
await this.reprocessCachedInvitesTrustStatusAfterTrustedContactsChange()

for (const contact of contacts) {
await this.shareContactWithUserAdministeredSharedVaults(contact)
}
Expand Down Expand Up @@ -328,28 +328,9 @@ export class SharedVaultService
}

private async reprocessCachedInvitesTrustStatusAfterTrustedContactsChange(): Promise<void> {
const cachedInvites = this.getCachedPendingInviteRecords()

for (const record of cachedInvites) {
if (record.trusted) {
continue
}
const cachedInvites = this.getCachedPendingInviteRecords().map((record) => record.invite)

const trustedMessageUseCase = new GetAsymmetricMessageTrustedPayload<AsymmetricMessageSharedVaultInvite>(
this.encryption,
this.contacts,
)

const trustedMessage = trustedMessageUseCase.execute({
message: record.invite,
privateKey: this.encryption.getKeyPair().privateKey,
})

if (trustedMessage) {
record.message = trustedMessage
record.trusted = true
}
}
await this.processInboundInvites(cachedInvites)
}

private async processInboundInvites(invites: SharedVaultInviteServerHash[]): Promise<void> {
Expand Down
Expand Up @@ -3,10 +3,12 @@ import { StorageServiceInterface } from '../../Storage/StorageServiceInterface'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
import { AnyItemInterface, VaultListingInterface } from '@standardnotes/models'
import { Uuids } from '@standardnotes/utils'
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import { RemoveItemsLocallyUseCase } from '../../UseCase/RemoveItemsLocally'

export class DeleteExternalSharedVaultUseCase {
private removeItemsLocallyUsecase = new RemoveItemsLocallyUseCase(this.items, this.storage)

constructor(
private items: ItemManagerInterface,
private mutator: MutatorClientInterface,
Expand All @@ -28,15 +30,13 @@ export class DeleteExternalSharedVaultUseCase {
* The data will be removed locally without syncing the items
*/
private async deleteDataSharedByVaultUsers(vault: VaultListingInterface): Promise<void> {
const vaultItems = this.items
.allTrackedItems()
.filter((item) => item.key_system_identifier === vault.systemIdentifier)
this.items.removeItemsLocally(vaultItems as AnyItemInterface[])
const vaultItems = <AnyItemInterface[]>(
this.items.allTrackedItems().filter((item) => item.key_system_identifier === vault.systemIdentifier)
)

const itemsKeys = this.encryption.keys.getKeySystemItemsKeys(vault.systemIdentifier)
this.items.removeItemsLocally(itemsKeys)

await this.storage.deletePayloadsWithUuids([...Uuids(vaultItems), ...Uuids(itemsKeys)])
await this.removeItemsLocallyUsecase.execute([...vaultItems, ...itemsKeys])
}

private async deleteDataOwnedByThisUser(vault: VaultListingInterface): Promise<void> {
Expand Down
14 changes: 14 additions & 0 deletions packages/services/src/Domain/UseCase/RemoveItemsLocally.ts
@@ -0,0 +1,14 @@
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { AnyItemInterface } from '@standardnotes/models'
import { Uuids } from '@standardnotes/utils'

export class RemoveItemsLocallyUseCase {
constructor(private readonly items: ItemManagerInterface, private readonly storage: StorageServiceInterface) {}

async execute(items: AnyItemInterface[]): Promise<void> {
this.items.removeItemsLocally(items)

await this.storage.deletePayloadsWithUuids(Uuids(items))
}
}
1 change: 0 additions & 1 deletion packages/services/src/Domain/User/UserService.ts
Expand Up @@ -589,7 +589,6 @@ export class UserService

this.lockSyncing()

/** Now, change the credentials on the server. Roll back on failure */
const { response } = await this.sessionManager.changeCredentials({
currentServerPassword: currentRootKey.serverPassword as string,
newRootKey: newRootKey,
Expand Down
1 change: 1 addition & 0 deletions packages/services/src/Domain/index.ts
Expand Up @@ -9,6 +9,7 @@ export * from './Application/DeinitMode'
export * from './Application/DeinitSource'

export * from './AsymmetricMessage/AsymmetricMessageService'
export * from './AsymmetricMessage/AsymmetricMessageServiceInterface'

export * from './Auth/AuthClientInterface'
export * from './Auth/AuthManager'
Expand Down
4 changes: 4 additions & 0 deletions packages/snjs/lib/Application/Application.ts
Expand Up @@ -387,6 +387,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.challengeService
}

public get asymmetric(): ExternalServices.AsymmetricMessageServiceInterface {
return this.asymmetricMessageService
}

get homeServer(): ExternalServices.HomeServerServiceInterface | undefined {
return this.homeServerService
}
Expand Down
5 changes: 5 additions & 0 deletions packages/snjs/lib/Services/KeyRecovery/KeyRecoveryService.ts
Expand Up @@ -9,6 +9,7 @@ import {
PayloadEmitSource,
EncryptedItemInterface,
getIncrementedDirtyIndex,
ContentTypeUsesRootKeyEncryption,
} from '@standardnotes/models'
import { SNSyncService } from '../Sync/SyncService'
import { DiskStorageService } from '../Storage/DiskStorageService'
Expand Down Expand Up @@ -187,6 +188,10 @@ export class SNKeyRecoveryService extends AbstractService<KeyRecoveryEvent, Decr
}

public canAttemptDecryptionOfItem(item: EncryptedItemInterface): ClientDisplayableError | true {
if (ContentTypeUsesRootKeyEncryption(item.content_type)) {
return true
}

const keyId = item.payload.items_key_id

if (!keyId) {
Expand Down