internal: incomplete vault systems behind feature flag (#2340)

This commit is contained in:
Mo
2023-06-30 09:01:56 -05:00
committed by GitHub
parent d16e401bb9
commit b032eb9c9b
638 changed files with 20321 additions and 4813 deletions

View File

@@ -3,16 +3,18 @@ import { FileItem } from '@standardnotes/models'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { ChallengeServiceInterface } from '../Challenge'
import { InternalEventBusInterface } from '..'
import { InternalEventBusInterface, MutatorClientInterface } from '..'
import { AlertService } from '../Alert/AlertService'
import { ApiServiceInterface } from '../Api/ApiServiceInterface'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { FileService } from './FileService'
import { BackupServiceInterface } from '@standardnotes/files'
import { HttpServiceInterface } from '@standardnotes/api'
describe('fileService', () => {
let apiService: ApiServiceInterface
let itemManager: ItemManagerInterface
let mutator: MutatorClientInterface
let syncService: SyncServiceInterface
let alertService: AlertService
let crypto: PureCryptoInterface
@@ -21,26 +23,28 @@ describe('fileService', () => {
let encryptor: EncryptionProviderInterface
let internalEventBus: InternalEventBusInterface
let backupService: BackupServiceInterface
let http: HttpServiceInterface
beforeEach(() => {
apiService = {} as jest.Mocked<ApiServiceInterface>
apiService.addEventObserver = jest.fn()
apiService.createFileValetToken = jest.fn()
apiService.createUserFileValetToken = jest.fn()
apiService.deleteFile = jest.fn().mockReturnValue({})
const numChunks = 1
apiService.downloadFile = jest
.fn()
.mockImplementation(
(
_file: string,
_chunkIndex: number,
_apiToken: string,
_rangeStart: number,
onBytesReceived: (bytes: Uint8Array) => void,
) => {
(params: {
_file: string
_chunkIndex: number
_apiToken: string
_ownershipType: string
_rangeStart: number
onBytesReceived: (bytes: Uint8Array) => void
}) => {
return new Promise<void>((resolve) => {
for (let i = 0; i < numChunks; i++) {
onBytesReceived(Uint8Array.from([0xaa]))
params.onBytesReceived(Uint8Array.from([0xaa]))
}
resolve()
@@ -49,11 +53,13 @@ describe('fileService', () => {
)
itemManager = {} as jest.Mocked<ItemManagerInterface>
itemManager.createItem = jest.fn()
itemManager.createTemplateItem = jest.fn().mockReturnValue({})
itemManager.setItemToBeDeleted = jest.fn()
itemManager.addObserver = jest.fn()
itemManager.changeItem = jest.fn()
mutator = {} as jest.Mocked<MutatorClientInterface>
mutator.createItem = jest.fn()
mutator.setItemToBeDeleted = jest.fn()
mutator.changeItem = jest.fn()
challengor = {} as jest.Mocked<ChallengeServiceInterface>
@@ -75,12 +81,15 @@ describe('fileService', () => {
backupService.readEncryptedFileFromBackup = jest.fn()
backupService.getFileBackupInfo = jest.fn()
http = {} as jest.Mocked<HttpServiceInterface>
fileService = new FileService(
apiService,
itemManager,
mutator,
syncService,
encryptor,
challengor,
http,
alertService,
crypto,
internalEventBus,
@@ -152,7 +161,7 @@ describe('fileService', () => {
} as jest.Mocked<FileItem>
const alertMock = (alertService.confirm = jest.fn().mockReturnValue(true))
const deleteItemMock = (itemManager.setItemToBeDeleted = jest.fn())
const deleteItemMock = (mutator.setItemToBeDeleted = jest.fn())
apiService.deleteFile = jest.fn().mockReturnValue({ data: { error: true } })

View File

@@ -1,4 +1,10 @@
import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses'
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import {
ClientDisplayableError,
ValetTokenOperation,
isClientDisplayableError,
isErrorResponse,
} from '@standardnotes/responses'
import { ContentType } from '@standardnotes/common'
import {
FileItem,
@@ -9,6 +15,8 @@ import {
FileContent,
EncryptedPayload,
isEncryptedPayload,
VaultListingInterface,
SharedVaultListingInterface,
} from '@standardnotes/models'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { spaceSeparatedStrings, UuidGenerator } from '@standardnotes/utils'
@@ -36,29 +44,37 @@ import {
import { AlertService, ButtonType } from '../Alert/AlertService'
import { ChallengeServiceInterface } from '../Challenge'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { AbstractService } from '../Service/AbstractService'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { DecryptItemsKeyWithUserFallback } from '../Encryption/Functions'
import { log, LoggingDomain } from '../Logging'
import {
SharedVaultMoveType,
SharedVaultServer,
SharedVaultServerInterface,
HttpServiceInterface,
} from '@standardnotes/api'
const OneHundredMb = 100 * 1_000_000
export class FileService extends AbstractService implements FilesClientInterface {
private encryptedCache: FileMemoryCache = new FileMemoryCache(OneHundredMb)
private sharedVault: SharedVaultServerInterface
constructor(
private api: FilesApiInterface,
private itemManager: ItemManagerInterface,
private mutator: MutatorClientInterface,
private syncService: SyncServiceInterface,
private encryptor: EncryptionProviderInterface,
private challengor: ChallengeServiceInterface,
http: HttpServiceInterface,
private alertService: AlertService,
private crypto: PureCryptoInterface,
protected override internalEventBus: InternalEventBusInterface,
private backupsService?: BackupServiceInterface,
) {
super(internalEventBus)
this.sharedVault = new SharedVaultServer(http)
}
override deinit(): void {
@@ -67,7 +83,6 @@ export class FileService extends AbstractService implements FilesClientInterface
this.encryptedCache.clear()
;(this.encryptedCache as unknown) = undefined
;(this.api as unknown) = undefined
;(this.itemManager as unknown) = undefined
;(this.encryptor as unknown) = undefined
;(this.syncService as unknown) = undefined
;(this.alertService as unknown) = undefined
@@ -79,14 +94,109 @@ export class FileService extends AbstractService implements FilesClientInterface
return 5_000_000
}
private async createUserValetToken(
remoteIdentifier: string,
operation: ValetTokenOperation,
unencryptedFileSizeForUpload?: number | undefined,
): Promise<string | ClientDisplayableError> {
return this.api.createUserFileValetToken(remoteIdentifier, operation, unencryptedFileSizeForUpload)
}
private async createSharedVaultValetToken(params: {
sharedVaultUuid: string
remoteIdentifier: string
operation: ValetTokenOperation
fileUuidRequiredForExistingFiles?: string
unencryptedFileSizeForUpload?: number | undefined
moveOperationType?: SharedVaultMoveType
sharedVaultToSharedVaultMoveTargetUuid?: string
}): Promise<string | ClientDisplayableError> {
if (params.operation !== 'write' && !params.fileUuidRequiredForExistingFiles) {
throw new Error('File UUID is required for for non-write operations')
}
const valetTokenResponse = await this.sharedVault.createSharedVaultFileValetToken({
sharedVaultUuid: params.sharedVaultUuid,
fileUuid: params.fileUuidRequiredForExistingFiles,
remoteIdentifier: params.remoteIdentifier,
operation: params.operation,
unencryptedFileSize: params.unencryptedFileSizeForUpload,
moveOperationType: params.moveOperationType,
sharedVaultToSharedVaultMoveTargetUuid: params.sharedVaultToSharedVaultMoveTargetUuid,
})
if (isErrorResponse(valetTokenResponse)) {
return new ClientDisplayableError('Could not create valet token')
}
return valetTokenResponse.data.valetToken
}
public async moveFileToSharedVault(
file: FileItem,
sharedVault: SharedVaultListingInterface,
): Promise<void | ClientDisplayableError> {
const valetTokenResult = await this.createSharedVaultValetToken({
sharedVaultUuid: file.shared_vault_uuid ? file.shared_vault_uuid : sharedVault.sharing.sharedVaultUuid,
remoteIdentifier: file.remoteIdentifier,
operation: 'move',
fileUuidRequiredForExistingFiles: file.uuid,
moveOperationType: file.shared_vault_uuid ? 'shared-vault-to-shared-vault' : 'user-to-shared-vault',
sharedVaultToSharedVaultMoveTargetUuid: file.shared_vault_uuid ? sharedVault.sharing.sharedVaultUuid : undefined,
})
if (isClientDisplayableError(valetTokenResult)) {
return valetTokenResult
}
const moveResult = await this.api.moveFile(valetTokenResult)
if (!moveResult) {
return new ClientDisplayableError('Could not move file')
}
}
public async moveFileOutOfSharedVault(file: FileItem): Promise<void | ClientDisplayableError> {
if (!file.shared_vault_uuid) {
return new ClientDisplayableError('File is not in a shared vault')
}
const valetTokenResult = await this.createSharedVaultValetToken({
sharedVaultUuid: file.shared_vault_uuid,
remoteIdentifier: file.remoteIdentifier,
operation: 'move',
fileUuidRequiredForExistingFiles: file.uuid,
moveOperationType: 'shared-vault-to-user',
})
if (isClientDisplayableError(valetTokenResult)) {
return valetTokenResult
}
const moveResult = await this.api.moveFile(valetTokenResult)
if (!moveResult) {
return new ClientDisplayableError('Could not move file')
}
}
public async beginNewFileUpload(
sizeInBytes: number,
vault?: VaultListingInterface,
): Promise<EncryptAndUploadFileOperation | ClientDisplayableError> {
const remoteIdentifier = UuidGenerator.GenerateUuid()
const tokenResult = await this.api.createFileValetToken(remoteIdentifier, 'write', sizeInBytes)
const valetTokenResult =
vault && vault.isSharedVaultListing()
? await this.createSharedVaultValetToken({
sharedVaultUuid: vault.sharing.sharedVaultUuid,
remoteIdentifier,
operation: 'write',
unencryptedFileSizeForUpload: sizeInBytes,
})
: await this.createUserValetToken(remoteIdentifier, 'write', sizeInBytes)
if (tokenResult instanceof ClientDisplayableError) {
return tokenResult
if (valetTokenResult instanceof ClientDisplayableError) {
return valetTokenResult
}
const key = this.crypto.generateRandomKey(FileProtocolV1Constants.KeySize)
@@ -97,9 +207,18 @@ export class FileService extends AbstractService implements FilesClientInterface
decryptedSize: sizeInBytes,
}
const uploadOperation = new EncryptAndUploadFileOperation(fileParams, tokenResult, this.crypto, this.api)
const uploadOperation = new EncryptAndUploadFileOperation(
fileParams,
valetTokenResult,
this.crypto,
this.api,
vault,
)
const uploadSessionStarted = await this.api.startUploadSession(tokenResult)
const uploadSessionStarted = await this.api.startUploadSession(
valetTokenResult,
vault && vault.isSharedVaultListing() ? 'shared-vault' : 'user',
)
if (isErrorResponse(uploadSessionStarted) || !uploadSessionStarted.data.uploadId) {
return new ClientDisplayableError('Could not start upload session')
@@ -127,7 +246,10 @@ export class FileService extends AbstractService implements FilesClientInterface
operation: EncryptAndUploadFileOperation,
fileMetadata: FileMetadata,
): Promise<FileItem | ClientDisplayableError> {
const uploadSessionClosed = await this.api.closeUploadSession(operation.getApiToken())
const uploadSessionClosed = await this.api.closeUploadSession(
operation.getValetToken(),
operation.vault && operation.vault.isSharedVaultListing() ? 'shared-vault' : 'user',
)
if (!uploadSessionClosed) {
return new ClientDisplayableError('Could not close upload session')
@@ -145,10 +267,11 @@ export class FileService extends AbstractService implements FilesClientInterface
remoteIdentifier: result.remoteIdentifier,
}
const file = await this.itemManager.createItem<FileItem>(
const file = await this.mutator.createItem<FileItem>(
ContentType.File,
FillItemContentSpecialized(fileContent),
true,
operation.vault,
)
await this.syncService.sync()
@@ -215,7 +338,20 @@ export class FileService extends AbstractService implements FilesClientInterface
let cacheEntryAggregate = new Uint8Array()
const operation = new DownloadAndDecryptFileOperation(file, this.crypto, this.api)
const tokenResult = file.shared_vault_uuid
? await this.createSharedVaultValetToken({
sharedVaultUuid: file.shared_vault_uuid,
remoteIdentifier: file.remoteIdentifier,
operation: 'read',
fileUuidRequiredForExistingFiles: file.uuid,
})
: await this.createUserValetToken(file.remoteIdentifier, 'read')
if (tokenResult instanceof ClientDisplayableError) {
return tokenResult
}
const operation = new DownloadAndDecryptFileOperation(file, this.crypto, this.api, tokenResult)
const result = await operation.run(async ({ decrypted, encrypted, progress }): Promise<void> => {
if (addToCache) {
@@ -235,13 +371,20 @@ export class FileService extends AbstractService implements FilesClientInterface
public async deleteFile(file: FileItem): Promise<ClientDisplayableError | undefined> {
this.encryptedCache.remove(file.uuid)
const tokenResult = await this.api.createFileValetToken(file.remoteIdentifier, 'delete')
const tokenResult = file.shared_vault_uuid
? await this.createSharedVaultValetToken({
sharedVaultUuid: file.shared_vault_uuid,
remoteIdentifier: file.remoteIdentifier,
operation: 'delete',
fileUuidRequiredForExistingFiles: file.uuid,
})
: await this.createUserValetToken(file.remoteIdentifier, 'delete')
if (tokenResult instanceof ClientDisplayableError) {
return tokenResult
}
const result = await this.api.deleteFile(tokenResult)
const result = await this.api.deleteFile(tokenResult, file.shared_vault_uuid ? 'shared-vault' : 'user')
if (result.data?.error) {
const deleteAnyway = await this.alertService.confirm(
@@ -261,7 +404,7 @@ export class FileService extends AbstractService implements FilesClientInterface
}
}
await this.itemManager.setItemToBeDeleted(file)
await this.mutator.setItemToBeDeleted(file)
await this.syncService.sync()
return undefined