mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-03 16:41:27 +01:00
vault: files sealed under a password that is never stored
Argon2id turns the vault password into a key that seals a random master key; only the sealed copy is kept. Each file gets its own key, sealed under the master key in its header with its media id, and its bytes are AES-256-GCM STREAM records, so a swapped, cut or altered file refuses to open. Uploads with ?encrypt=true are sealed as they stream in. :vault, :unlock, :lock, :passwd, :encrypt, :decrypt and :upload! drive it. The vault locks after WORKSPACE_VAULT_IDLE_MIN idle minutes, on logout and on restart; while locked, sealed files answer 423, share links never serve them, and :export! refuses rather than leave them out. Sealing an existing file rewrites it under a new id and retires the old one: the mirror deletes the plaintext from Proton Drive outright, trash included, whatever WORKSPACE_BACKUP_PRUNE says, and drops it from the readable files/ view. The mirror now compares on-disk sizes, so sealed files are not re-hashed every run.
This commit is contained in:
1 parent
364ba61963
commit
a641d9b6ba
33 files changed
+1573
-54
No files matched your search
+32
-2
@@ -10,6 +10,8 @@ export interface FileInfo {
|
||||
mime: string
|
||||
size: number
|
||||
url: string
|
||||
/** Sealed in the vault: it opens only while the vault is unlocked. */
|
||||
encrypted: boolean
|
||||
}
|
||||
|
||||
export interface NodeSummary {
|
||||
@@ -125,10 +127,19 @@ export interface MediaFile {
|
||||
size: number
|
||||
created_at: number
|
||||
url: string
|
||||
encrypted: boolean
|
||||
/** Every document that embeds this file — a file may be used by several. */
|
||||
node_ids: number[]
|
||||
}
|
||||
|
||||
/** The vault, as `/api/vault` reports it. */
|
||||
export interface VaultStatus {
|
||||
configured: boolean
|
||||
unlocked: boolean
|
||||
/** Seconds until it locks itself if nothing uses it. */
|
||||
locks_in: number | null
|
||||
}
|
||||
|
||||
/** The Proton Drive mirror, as `/api/backup` reports it. */
|
||||
export interface BackupStatus {
|
||||
configured: boolean
|
||||
@@ -285,12 +296,12 @@ export const api = {
|
||||
* Upload files into a space or folder as file nodes. XHR rather than fetch,
|
||||
* because a gigabyte going up needs to say how far along it is.
|
||||
*/
|
||||
putFiles: (parentId: number, files: File[], progress?: (fraction: number) => void) =>
|
||||
putFiles: (parentId: number, files: File[], encrypt: boolean, progress?: (fraction: number) => void) =>
|
||||
new Promise<WorkspaceNode[]>((resolve, reject) => {
|
||||
const form = new FormData()
|
||||
for (const file of files) form.append('file', file)
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', `/api/nodes/${parentId}/files`)
|
||||
xhr.open('POST', `/api/nodes/${parentId}/files${encrypt ? '?encrypt=true' : ''}`)
|
||||
xhr.responseType = 'json'
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (event.lengthComputable) progress?.(event.loaded / event.total)
|
||||
@@ -311,6 +322,25 @@ export const api = {
|
||||
|
||||
deleteMedia: (id: string) => request<{ ok: true }>(`/media/${id}`, { method: 'DELETE' }),
|
||||
|
||||
vaultStatus: () => request<VaultStatus>('/vault'),
|
||||
|
||||
setupVault: (password: string) =>
|
||||
request<VaultStatus>('/vault', { method: 'POST', body: body({ password }) }),
|
||||
|
||||
changeVaultPassword: (current: string, password: string) =>
|
||||
request<VaultStatus>('/vault', { method: 'PUT', body: body({ current, password }) }),
|
||||
|
||||
unlockVault: (password: string) =>
|
||||
request<VaultStatus>('/vault/unlock', { method: 'POST', body: body({ password }) }),
|
||||
|
||||
lockVault: () => request<VaultStatus>('/vault/lock', { method: 'POST' }),
|
||||
|
||||
/** Seal a file, or every file under a folder, into the vault — or take it back out. */
|
||||
seal: (id: number, encrypt: boolean) =>
|
||||
request<{ files: number; unchanged: number }>(`/nodes/${id}/${encrypt ? 'encrypt' : 'decrypt'}`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
backupStatus: () => request<BackupStatus>('/backup'),
|
||||
|
||||
backupNow: () => request<BackupStatus>('/backup', { method: 'POST' }),
|
||||
|
||||
Reference in new issue
Block a user