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

@@ -0,0 +1,11 @@
import { FileContent } from '@standardnotes/models'
import { FileOwnershipType } from './FileOwnershipType'
export type DownloadFileParams = {
file: { encryptedChunkSizes: FileContent['encryptedChunkSizes'] }
chunkIndex: number
valetToken: string
ownershipType: FileOwnershipType
contentRangeStart: number
onBytesReceived: (bytes: Uint8Array) => Promise<void>
}

View File

@@ -0,0 +1 @@
export type FileOwnershipType = 'user' | 'shared-vault'

View File

@@ -1,28 +1,38 @@
import { StartUploadSessionResponse, HttpResponse, ClientDisplayableError } from '@standardnotes/responses'
import { FileContent } from '@standardnotes/models'
import {
StartUploadSessionResponse,
HttpResponse,
ClientDisplayableError,
ValetTokenOperation,
} from '@standardnotes/responses'
import { DownloadFileParams } from './DownloadFileParams'
import { FileOwnershipType } from './FileOwnershipType'
export interface FilesApiInterface {
startUploadSession(apiToken: string): Promise<HttpResponse<StartUploadSessionResponse>>
uploadFileBytes(apiToken: string, chunkId: number, encryptedBytes: Uint8Array): Promise<boolean>
closeUploadSession(apiToken: string): Promise<boolean>
downloadFile(
file: { encryptedChunkSizes: FileContent['encryptedChunkSizes'] },
chunkIndex: number,
apiToken: string,
contentRangeStart: number,
onBytesReceived: (bytes: Uint8Array) => Promise<void>,
): Promise<ClientDisplayableError | undefined>
deleteFile(apiToken: string): Promise<HttpResponse>
createFileValetToken(
createUserFileValetToken(
remoteIdentifier: string,
operation: 'write' | 'read' | 'delete',
operation: ValetTokenOperation,
unencryptedFileSize?: number,
): Promise<string | ClientDisplayableError>
getFilesDownloadUrl(): string
startUploadSession(
valetToken: string,
ownershipType: FileOwnershipType,
): Promise<HttpResponse<StartUploadSessionResponse>>
uploadFileBytes(
valetToken: string,
ownershipType: FileOwnershipType,
chunkId: number,
encryptedBytes: Uint8Array,
): Promise<boolean>
closeUploadSession(valetToken: string, ownershipType: FileOwnershipType): Promise<boolean>
downloadFile(params: DownloadFileParams): Promise<ClientDisplayableError | undefined>
moveFile(valetToken: string): Promise<boolean>
deleteFile(valetToken: string, ownershipType: FileOwnershipType): Promise<HttpResponse>
getFilesDownloadUrl(ownershipType: FileOwnershipType): string
}

View File

@@ -9,10 +9,12 @@ describe('download and decrypt', () => {
let apiService: FilesApiInterface
let operation: DownloadAndDecryptFileOperation
let file: {
uuid: string
encryptedChunkSizes: FileContent['encryptedChunkSizes']
encryptionHeader: FileContent['encryptionHeader']
remoteIdentifier: FileContent['remoteIdentifier']
key: FileContent['key']
shared_vault_uuid: string | undefined
}
let crypto: PureCryptoInterface
@@ -26,16 +28,16 @@ describe('download and decrypt', () => {
apiService.downloadFile = jest
.fn()
.mockImplementation(
(
_file: string,
_chunkIndex: number,
_apiToken: string,
_rangeStart: number,
onBytesReceived: (bytes: Uint8Array) => void,
) => {
(params: {
_file: string
_chunkIndex: number
_apiToken: string
_rangeStart: number
onBytesReceived: (bytes: Uint8Array) => void
}) => {
const receiveFile = async () => {
for (let i = 0; i < NumChunks; i++) {
onBytesReceived(chunkOfSize(size))
params.onBytesReceived(chunkOfSize(size))
await sleep(100, false)
}
@@ -50,7 +52,7 @@ describe('download and decrypt', () => {
beforeEach(() => {
apiService = {} as jest.Mocked<FilesApiInterface>
apiService.createFileValetToken = jest.fn()
apiService.createUserFileValetToken = jest.fn()
downloadChunksOfSize(5)
crypto = {} as jest.Mocked<PureCryptoInterface>
@@ -62,17 +64,19 @@ describe('download and decrypt', () => {
crypto.xchacha20StreamDecryptorPush = jest.fn().mockReturnValue({ message: new Uint8Array([0xaa]), tag: 0 })
file = {
uuid: '123',
encryptedChunkSizes: [100_000],
remoteIdentifier: '123',
key: 'secret',
encryptionHeader: 'some-header',
shared_vault_uuid: undefined,
}
})
it('run should resolve when operation is complete', async () => {
let receivedBytes = new Uint8Array()
operation = new DownloadAndDecryptFileOperation(file, crypto, apiService)
operation = new DownloadAndDecryptFileOperation(file, crypto, apiService, 'own')
await operation.run(async (result) => {
if (result) {
@@ -87,15 +91,17 @@ describe('download and decrypt', () => {
it('should correctly report progress', async () => {
file = {
uuid: '123',
encryptedChunkSizes: [100_000, 200_000, 200_000],
remoteIdentifier: '123',
key: 'secret',
encryptionHeader: 'some-header',
shared_vault_uuid: undefined,
}
downloadChunksOfSize(100_000)
operation = new DownloadAndDecryptFileOperation(file, crypto, apiService)
operation = new DownloadAndDecryptFileOperation(file, crypto, apiService, 'own')
const progress: FileDownloadProgress = await new Promise((resolve) => {
// eslint-disable-next-line @typescript-eslint/require-await

View File

@@ -21,6 +21,8 @@ export class DownloadAndDecryptFileOperation {
constructor(
private readonly file: {
uuid: string
shared_vault_uuid: string | undefined
encryptedChunkSizes: FileContent['encryptedChunkSizes']
encryptionHeader: FileContent['encryptionHeader']
remoteIdentifier: FileContent['remoteIdentifier']
@@ -28,8 +30,9 @@ export class DownloadAndDecryptFileOperation {
},
private readonly crypto: PureCryptoInterface,
private readonly api: FilesApiInterface,
valetToken: string,
) {
this.downloader = new FileDownloader(this.file, this.api)
this.downloader = new FileDownloader(this.file, this.api, valetToken)
}
private createDecryptor(): FileDecryptor {

View File

@@ -3,7 +3,7 @@ import { FileUploadResult } from '../Types/FileUploadResult'
import { FileUploader } from '../UseCase/FileUploader'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { FileEncryptor } from '../UseCase/FileEncryptor'
import { FileContent } from '@standardnotes/models'
import { FileContent, VaultListingInterface } from '@standardnotes/models'
import { FilesApiInterface } from '../Api/FilesApiInterface'
export class EncryptAndUploadFileOperation {
@@ -22,9 +22,10 @@ export class EncryptAndUploadFileOperation {
key: FileContent['key']
remoteIdentifier: FileContent['remoteIdentifier']
},
private apiToken: string,
private valetToken: string,
private crypto: PureCryptoInterface,
private api: FilesApiInterface,
public readonly vault?: VaultListingInterface,
) {
this.encryptor = new FileEncryptor(file, this.crypto)
this.uploader = new FileUploader(this.api)
@@ -32,8 +33,8 @@ export class EncryptAndUploadFileOperation {
this.encryptionHeader = this.encryptor.initializeHeader()
}
public getApiToken(): string {
return this.apiToken
public getValetToken(): string {
return this.valetToken
}
public getProgress(): FileUploadProgress {
@@ -79,7 +80,12 @@ export class EncryptAndUploadFileOperation {
}
private async uploadBytes(encryptedBytes: Uint8Array, chunkId: number): Promise<boolean> {
const success = await this.uploader.uploadBytes(encryptedBytes, chunkId, this.apiToken)
const success = await this.uploader.uploadBytes(
encryptedBytes,
this.vault && this.vault.sharing ? 'shared-vault' : 'user',
chunkId,
this.valetToken,
)
return success
}

View File

@@ -1,5 +1,5 @@
import { EncryptAndUploadFileOperation } from '../Operations/EncryptAndUpload'
import { FileItem, FileMetadata } from '@standardnotes/models'
import { FileItem, FileMetadata, VaultListingInterface, SharedVaultListingInterface } from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { FileDownloadProgress } from '../Types/FileDownloadProgress'
import { FileSystemApi } from '../Api/FileSystemApi'
@@ -8,15 +8,18 @@ import { FileSystemNoSelection } from '../Api/FileSystemNoSelection'
import { FileBackupMetadataFile } from '../Device/FileBackupMetadataFile'
export interface FilesClientInterface {
beginNewFileUpload(sizeInBytes: number): Promise<EncryptAndUploadFileOperation | ClientDisplayableError>
minimumChunkSize(): number
beginNewFileUpload(
sizeInBytes: number,
vault?: VaultListingInterface,
): Promise<EncryptAndUploadFileOperation | ClientDisplayableError>
pushBytesForUpload(
operation: EncryptAndUploadFileOperation,
bytes: Uint8Array,
chunkId: number,
isFinalChunk: boolean,
): Promise<ClientDisplayableError | undefined>
finishUpload(
operation: EncryptAndUploadFileOperation,
fileMetadata: FileMetadata,
@@ -29,20 +32,21 @@ export interface FilesClientInterface {
deleteFile(file: FileItem): Promise<ClientDisplayableError | undefined>
minimumChunkSize(): number
isFileNameFileBackupRelated(name: string): 'metadata' | 'binary' | false
decryptBackupMetadataFile(metdataFile: FileBackupMetadataFile): Promise<FileItem | undefined>
moveFileToSharedVault(
file: FileItem,
sharedVault: SharedVaultListingInterface,
): Promise<void | ClientDisplayableError>
moveFileOutOfSharedVault(file: FileItem): Promise<void | ClientDisplayableError>
selectFile(fileSystem: FileSystemApi): Promise<FileHandleRead | FileSystemNoSelection>
isFileNameFileBackupRelated(name: string): 'metadata' | 'binary' | false
decryptBackupMetadataFile(metdataFile: FileBackupMetadataFile): Promise<FileItem | undefined>
readBackupFileAndSaveDecrypted(
fileHandle: FileHandleRead,
file: FileItem,
fileSystem: FileSystemApi,
): Promise<'success' | 'aborted' | 'failed'>
readBackupFileBytesDecrypted(
fileHandle: FileHandleRead,
file: FileItem,

View File

@@ -6,6 +6,8 @@ describe('file downloader', () => {
let apiService: FilesApiInterface
let downloader: FileDownloader
let file: {
uuid: string
shared_vault_uuid: string | undefined
encryptedChunkSizes: FileContent['encryptedChunkSizes']
remoteIdentifier: FileContent['remoteIdentifier']
}
@@ -14,20 +16,20 @@ describe('file downloader', () => {
beforeEach(() => {
apiService = {} as jest.Mocked<FilesApiInterface>
apiService.createFileValetToken = jest.fn()
apiService.createUserFileValetToken = jest.fn()
apiService.downloadFile = jest
.fn()
.mockImplementation(
(
_file: string,
_chunkIndex: number,
_apiToken: string,
_rangeStart: number,
onBytesReceived: (bytes: Uint8Array) => void,
) => {
(params: {
_file: string
_chunkIndex: number
_apiToken: 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()
@@ -36,6 +38,8 @@ describe('file downloader', () => {
)
file = {
uuid: '123',
shared_vault_uuid: undefined,
encryptedChunkSizes: [100_000],
remoteIdentifier: '123',
}
@@ -44,7 +48,7 @@ describe('file downloader', () => {
it('should pass back bytes as they are received', async () => {
let receivedBytes = new Uint8Array()
downloader = new FileDownloader(file, apiService)
downloader = new FileDownloader(file, apiService, 'valet-token')
expect(receivedBytes.length).toBe(0)

View File

@@ -21,10 +21,13 @@ export class FileDownloader {
constructor(
private file: {
uuid: string
shared_vault_uuid: string | undefined
encryptedChunkSizes: FileContent['encryptedChunkSizes']
remoteIdentifier: FileContent['remoteIdentifier']
},
private readonly api: FilesApiInterface,
private readonly valetToken: string,
) {}
private getProgress(): FileDownloadProgress {
@@ -40,22 +43,10 @@ export class FileDownloader {
}
public async run(onEncryptedBytes: OnEncryptedBytes): Promise<FileDownloaderResult> {
const tokenResult = await this.getValetToken()
if (tokenResult instanceof ClientDisplayableError) {
return tokenResult
}
return this.performDownload(tokenResult, onEncryptedBytes)
return this.performDownload(onEncryptedBytes)
}
private async getValetToken(): Promise<string | ClientDisplayableError> {
const tokenResult = await this.api.createFileValetToken(this.file.remoteIdentifier, 'read')
return tokenResult
}
private async performDownload(valetToken: string, onEncryptedBytes: OnEncryptedBytes): Promise<FileDownloaderResult> {
private async performDownload(onEncryptedBytes: OnEncryptedBytes): Promise<FileDownloaderResult> {
const chunkIndex = 0
const startRange = 0
@@ -69,7 +60,14 @@ export class FileDownloader {
await onEncryptedBytes(bytes, this.getProgress(), this.abort)
}
const downloadPromise = this.api.downloadFile(this.file, chunkIndex, valetToken, startRange, onRemoteBytesReceived)
const downloadPromise = this.api.downloadFile({
file: this.file,
chunkIndex,
valetToken: this.valetToken,
contentRangeStart: startRange,
onBytesReceived: onRemoteBytesReceived,
ownershipType: this.file.shared_vault_uuid ? 'shared-vault' : 'user',
})
const result = await Promise.race([this.abortDeferred.promise, downloadPromise])

View File

@@ -2,7 +2,7 @@ import { FilesApiInterface } from '../Api/FilesApiInterface'
import { FileUploader } from './FileUploader'
describe('file uploader', () => {
let apiService
let apiService: FilesApiInterface
let uploader: FileUploader
beforeEach(() => {
@@ -14,7 +14,7 @@ describe('file uploader', () => {
it('should return true when a chunk is uploaded', async () => {
const bytes = new Uint8Array()
const success = await uploader.uploadBytes(bytes, 2, 'api-token')
const success = await uploader.uploadBytes(bytes, 'user', 2, 'api-token')
expect(success).toEqual(true)
})

View File

@@ -1,10 +1,16 @@
import { FileOwnershipType } from '../Api/FileOwnershipType'
import { FilesApiInterface } from '../Api/FilesApiInterface'
export class FileUploader {
constructor(private apiService: FilesApiInterface) {}
public async uploadBytes(encryptedBytes: Uint8Array, chunkId: number, apiToken: string): Promise<boolean> {
const result = await this.apiService.uploadFileBytes(apiToken, chunkId, encryptedBytes)
public async uploadBytes(
encryptedBytes: Uint8Array,
ownershipType: FileOwnershipType,
chunkId: number,
apiToken: string,
): Promise<boolean> {
const result = await this.apiService.uploadFileBytes(apiToken, ownershipType, chunkId, encryptedBytes)
return result
}

View File

@@ -5,6 +5,9 @@ export * from './Api/FilesApiInterface'
export * from './Api/FileSystemApi'
export * from './Api/FileSystemNoSelection'
export * from './Api/FileSystemResult'
export * from './Api/DownloadFileParams'
export * from './Api/FileOwnershipType'
export * from './Cache/FileMemoryCache'
export * from './Chunker/ByteChunker'
export * from './Chunker/OnChunkCallback'