encodrive
v2.0.0
Published
Envelope-encrypted file storage client: per-file data keys, per-user isolation, O(1) key rotation.
Downloads
346
Maintainers
Readme
Encodrive
Client-side encrypted file storage. Files are encrypted before they leave the client, so Encodrive stores ciphertext and never holds your keys.
Install
npm install encodriveUsage
import { Encodrive } from 'encodrive';
const drive = new Encodrive({
apiKey: process.env.ENCODRIVE_API_KEY,
encryptionKey: process.env.ENCODRIVE_MASTER_KEY,
context: 'user-42' // optional: scopes the key to one user
});
const { downloadUrl } = await drive.uploadFile(file);
const { blob, fileName } = await drive.downloadFile(downloadUrl);Run this from your backend.
encryptionKeyis a master secret. Shipping it in browser code lets any end user extract it and read every other user's files. For client-side apps, have your server hand the browser a short-lived per-user key instead of the master.
Per-user isolation
Pass a context and the master key is split per user via HKDF. Two users of
the same application derive different keys, so one cannot decrypt the other's
files even though both go through the same master secret.
await drive.uploadFile(file, { context: 'user-42' });The context is stored in the file header, so decryption only needs the master key — you do not have to remember which context a file used.
Key rotation
Each file is encrypted with its own random 256-bit data key (DEK). The DEK is wrapped under a key derived from your master password. Rotating the master key rewraps ~48 bytes; the encrypted body is never touched.
import { splitHeader, rewrapHeader, joinHeader } from 'encodrive';
const { header, body } = splitHeader(ciphertext);
const rotated = await rewrapHeader(header, oldKey, newKey); // body untouched
const updated = joinHeader(rotated, body);Store headers in your database, separate from the object bodies, and rotation costs one small row update per file regardless of how large the files are.
Format
EDV2: flat AES-GCM body under a per-file DEK, with the DEK wrapped by a
scrypt-derived KEK (N=16384, r=8, p=1).
[MAGIC "EDV2"(4)] [version(1)] [flags(1)] [kdfId(1)] [reserved(1)]
[kekSalt(16)] [wrapIV(12)] [wrappedDekLen(2)] [wrappedDek(48)]
[bodyIV(12)] [fileSize(8)] [contextLen(2)] [context(M)]
[bodyCipher(... + 128-bit GCM tag)]Header fields that the body depends on (version, flags, fileSize, bodyIV,
context) are bound into the body's AAD, so tampering with them fails the
integrity check. kekSalt, wrapIV and wrappedDek are deliberately left out
of that AAD — they change during rotation, and the wrap's own GCM tag already
authenticates them.
Fixed overhead is 124 bytes plus the context length.
The older segmented EDP1 format is still readable, but nothing writes it any
more.
API
| function | purpose |
|---|---|
| encryptFile(buffer, password, { context }) | encrypt to EDV2 |
| decryptFile(buffer, password) | decrypt EDV2 or legacy EDP1 |
| rewrapHeader(header, oldPw, newPw) | rotate a key, header only |
| rewrapFile(buffer, oldPw, newPw) | rotate a key, whole file |
| splitHeader(buffer) / joinHeader(h, b) | separate / rejoin header and body |
| inspectHeader(buffer) | read size, context and format without the key |
Tests
npm test