import crypto from 'crypto';
// Helper: derive a unique IV for each chunk (or filename)
function deriveChunkIv(baseIv, chunkIndex) {
const iv = Buffer.alloc(12);
baseIv.copy(iv, 0, 0, 8); // First 8 bytes from base IV
iv.writeUInt32BE(chunkIndex, 8); // Chunk index as big-endian u32
return iv;
}
// 1. Generate encryption key, drop metadata IV, and per-file IV
const key = crypto.randomBytes(32); // 256-bit AES key
const dropIv = crypto.randomBytes(12); // 96-bit IV for title/message metadata
const fileIv = crypto.randomBytes(12); // 96-bit base IV for this file
// 2. Encrypt file data (chunk index 0 for single-chunk files)
const chunkIv = deriveChunkIv(fileIv, 0);
const cipher = crypto.createCipheriv('aes-256-gcm', key, chunkIv);
const encrypted = Buffer.concat([cipher.update(fileBuffer), cipher.final()]);
const authTag = cipher.getAuthTag();
const encryptedData = Buffer.concat([encrypted, authTag]);
// 3. Encrypt filename (reserved chunk index 0xFFFFFFFF)
const filenameIv = deriveChunkIv(fileIv, 0xFFFFFFFF);
const nameCipher = crypto.createCipheriv('aes-256-gcm', key, filenameIv);
const encryptedName = Buffer.concat([
nameCipher.update(filename, 'utf8'),
nameCipher.final(),
nameCipher.getAuthTag()
]).toString('base64url');
// 4. Create drop
const createResponse = await fetch('https://anon.li/api/v1/drop', {
method: 'POST',
headers: {
'Authorization': 'Bearer ak_...',
'Content-Type': 'application/json'
},
body: JSON.stringify({
iv: dropIv.toString('base64url'),
fileCount: 1,
expiry: 7,
// ownerKey: {
// wrappedKey: vaultWrappedOwnerKey,
// vaultId,
// vaultGeneration
// }
})
}).then(r => r.json());
const drop = createResponse.data;
// 5. Add file and get presigned upload URLs
const file = await fetch(`https://anon.li/api/v1/drop/${drop.drop_id}/file`, {
method: 'POST',
headers: {
'Authorization': 'Bearer ak_...',
'Content-Type': 'application/json'
},
body: JSON.stringify({
size: encryptedData.length,
encryptedName,
iv: fileIv.toString('base64url'),
mimeType: 'application/octet-stream',
chunkCount: 1,
chunkSize: encryptedData.length
})
}).then(r => r.json());
// 6. Upload encrypted data to presigned URL
const uploadResponse = await fetch(file.uploadUrls[1], {
method: 'PUT',
body: encryptedData
});
const etag = uploadResponse.headers.get('ETag');
if (!etag) throw new Error('Storage did not return an ETag');
// 7. Finish the upload by recording chunk ETags and marking the drop ready
await fetch(`https://anon.li/api/v1/drop/${drop.drop_id}?action=finish`, {
method: 'PATCH',
headers: {
'Authorization': 'Bearer ak_...',
'Content-Type': 'application/json'
},
body: JSON.stringify({
files: [{
fileId: file.fileId,
chunks: [{ chunkIndex: 0, etag }]
}]
})
});
// 8. Build share URL with key in fragment
const shareUrl = `https://anon.li/d/${drop.drop_id}#${key.toString('base64url')}`;
console.log('Share URL:', shareUrl);