I have a sample client-side HTML webpage that allows a user to upload a file to a NodeJS file storage server. When a user uploads a file from the webpage, the client’s browser encrypts the file with a password before sending it to the server. The user can then download the file from that webpage by inputting the uploaded filename and the password – the browser downloads and decrypts the file. This is done using streams and CryptoJS so I can upload and download large files. (I had success using strings before, but it evidently does not work with large files).
I’ve managed to get the encryption process working: I’m able to upload a file to the server that the client’s browser encrypts – I can see the uploaded file on the server. However, I’m having trouble decrypting and downloading the file when it is being downloaded. The downloaded file always shows up with a size of 0 bytes every time with no data.
I’ve verified that the encrypted file blob size in bytes was received correctly by the client when downloading. The salt and IV are received as well. However, it appears that the value variable is always undefined when setting it to await reader.read(); – “No Value Detected” is logged. At this point, I’m not quite sure what the problem is as no errors are thrown.
I attached a small reproduceable sample below.
Browser Client Code (HTML):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Upload & Download</title>
</head>
<body>
<h1>Upload and Download Large Files</h1>
<h2>Upload File</h2>
<input type="file" id="fileInput">
<button id="uploadButton">Upload</button>
<div id="uploadStatus"></div>
<h2>Download File</h2>
<input type="text" id="downloadFilename" placeholder="Enter file name to download">
<button id="downloadButton">Download</button>
<script src="app.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
</body>
</html>
Code for client app.js:
// Utility: Derive AES Key from Password
async function deriveKey(password, salt) {
const enc = new TextEncoder();
const keyMaterial = await window.crypto.subtle.importKey(
'raw',
enc.encode(password),
'PBKDF2',
false,
['deriveKey']
);
return window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-CBC', length: 256 },
false,
['encrypt', 'decrypt']
);
}
// Utility: Generate Random Initialization Vector (IV)
function generateIV() {
return crypto.getRandomValues(new Uint8Array(16)); // 16 bytes for AES-CBC
}
// Encrypt File in Chunks
async function encryptFile(file, password) {
const chunkSize = 64 * 1024; // 64 KB
const reader = file.stream().getReader();
const enc = new TextEncoder();
// Generate salt and IV
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = generateIV();
// Derive AES key
const key = await deriveKey(password, salt);
// Create a Blob for the encrypted output
const encryptedChunks = [];
encryptedChunks.push(salt); // Store salt in the output
encryptedChunks.push(iv); // Store IV in the output
let done = false;
while (!done) {
const { value, done: isDone } = await reader.read();
if (value) {
const encryptedChunk = await crypto.subtle.encrypt(
{ name: 'AES-CBC', iv: iv },
key,
value
);
encryptedChunks.push(new Uint8Array(encryptedChunk));
}
done = isDone;
}
return new Blob(encryptedChunks, { type: 'application/octet-stream' });
}
async function decryptFile(encryptedBlob, password) {
// Check there is a blob to begin with
if (!encryptedBlob) {
throw new Error('No encrypted file provided.');
} else {
console.log('Encrypted Blob size:', encryptedBlob.size);
}
const reader = encryptedBlob.stream().getReader();
// Read the first 32 bytes: 16 for salt, 16 for IV
const { value: header } = await reader.read();
if (!header) {
throw new Error('Failed to read the file header.');
}
const salt = header.slice(0, 16); // First 16 bytes are salt
const iv = header.slice(16, 32); // Next 16 bytes are IV
console.log(`Salt:`, salt);
console.log(`IV:`, iv);
// Derive AES key
const key = await deriveKey(password, salt);
console.log(`Key: ${key}`);
const decryptedChunks = [];
let done = false;
// Start reading chunks
while (!done) {
const { value, done: isDone } = await reader.read();
done = isDone;
if (value) {
console.log("Value detected!");
try {
// Decrypt each chunk
const decryptedChunk = await crypto.subtle.decrypt(
{ name: 'AES-CBC', iv },
key,
value.buffer
);
decryptedChunks.push(new Uint8Array(decryptedChunk));
} catch (error) {
throw new Error('Decryption failed. Check the password or file integrity.');
}
} else {
console.warn('No value detected!');
}
}
// Combine all decrypted chunks into a single Blob
return new Blob(decryptedChunks, { type: 'application/octet-stream' });
}
document.getElementById('uploadButton').addEventListener('click', async () => {
const fileInput = document.getElementById('fileInput');
const uploadStatus = document.getElementById('uploadStatus');
const password = prompt('Enter a password to encrypt the file:');
if (!fileInput.files.length || !password) {
uploadStatus.textContent = 'Please select a file and enter a password.';
return;
}
const file = fileInput.files[0];
uploadStatus.textContent = 'Encrypting file...';
try {
const encryptedFile = await encryptFile(file, password);
const formData = new FormData();
formData.append('file', encryptedFile, `${file.name}.enc`);
const response = await fetch('/upload', {
method: 'POST',
body: formData,
});
if (response.ok) {
uploadStatus.textContent = 'File uploaded successfully!';
} else {
uploadStatus.textContent = 'File upload failed.';
}
} catch (err) {
console.error('Error encrypting file:', err);
uploadStatus.textContent = 'Encryption failed.';
}
});
document.getElementById('downloadButton').addEventListener('click', async () => {
const filename = document.getElementById('downloadFilename').value;
const password = prompt('Enter the password to decrypt the file:');
if (!filename || !password) {
alert('Please enter the filename and password.');
return;
}
const response = await fetch(`/download/${filename}.enc`);
if (response.ok) {
const encryptedBlob = await response.blob();
try {
const decryptedBlob = await decryptFile(encryptedBlob, password);
// Trigger download of the decrypted file
const a = document.createElement('a');
const url = URL.createObjectURL(decryptedBlob);
a.href = url;
a.download = filename.replace('.enc', '');
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
alert('File decrypted and downloaded successfully!');
} catch (err) {
alert(err);
}
} else {
alert('Failed to download the file.');
}
});
Node.JS Server Code (should not do cryptography; only stores encrypted data):
const express = require('express');
const fs = require('fs');
const multer = require('multer');
const path = require('path');
const app = express();
const PORT = 3000;
// Configure multer for file uploads
const upload = multer({
dest: 'uploads/' // Directory to store uploaded files
});
// Serve static files for the client-side HTML/JS
app.use(express.static(path.join(__dirname, 'public')));
// Endpoint to upload a file
app.post('/upload', upload.single('file'), (req, res) => {
const tempPath = req.file.path;
const targetPath = path.join(__dirname, 'uploads', req.file.originalname);
// Move the file to a permanent location
const readStream = fs.createReadStream(tempPath);
const writeStream = fs.createWriteStream(targetPath);
readStream.on('error', (err) => {
console.error('Error reading file:', err);
res.status(500).send('File upload failed.');
});
writeStream.on('error', (err) => {
console.error('Error writing file:', err);
res.status(500).send('File upload failed.');
});
writeStream.on('finish', () => {
fs.unlink(tempPath, (err) => {
if (err) console.error('Failed to delete temp file:', err);
});
res.status(200).send('File uploaded successfully!');
});
readStream.pipe(writeStream);
});
// Endpoint to download a file
app.get('/download/:filename', (req, res) => {
const filePath = path.join(__dirname, 'uploads', req.params.filename);
if (!fs.existsSync(filePath)) {
return res.status(404).send('File not found');
}
const readStream = fs.createReadStream(filePath);
res.setHeader('Content-Disposition', `attachment; filename="${req.params.filename}"`);
res.setHeader('Content-Type', 'application/octet-stream');
readStream.on('error', (err) => {
console.error('Error reading file:', err);
res.status(500).send('File download failed.');
});
readStream.pipe(res);
});
// Start the server
app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`);
});
Can anyone help me understand what I’m doing wrong?