<%@ Page Title="Encryption-at-rest" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" %>
<asp:Content ID="TitleContent" ContentPlaceHolderID="TitleContent" runat="server">Encryption-at-rest - AjaxUploader v5.3 Demos</asp:Content>
<asp:Content ID="HeaderContent" ContentPlaceHolderID="HeaderTitle" runat="server">Client-side encryption-at-rest (AES-GCM-256)</asp:Content>
<asp:Content ID="BodyContent" ContentPlaceHolderID="MainContent" runat="server"> 
<div class="demo-card"> <div class="demo-card-header">Encrypt files in the browser before they ever leave the device</div> <div class="demo-card-body"> <p class="demo-description"> <strong>Zero-knowledge upload.</strong> Every queued file is encrypted with AES-GCM-256 before upload begins; your server only ever sees ciphertext. Decryption requires the password and the per-file salt + IV emitted as headers. Compliance-friendly default for HIPAA / GDPR confidentiality workflows. <em>None of Uppy, FilePond, Dropzone, or FineUploader offer this built-in.</em> </p> <div class="demo-expect"><strong>What you should see:</strong> files encrypted in the browser (AES-GCM) before a byte leaves the page - the server only ever stores ciphertext. Auto-upload is off so the encryption step is visible.</div> <div class="card p-3 mt-3" style="background:#fff;border:1px solid #e2e8f0;"> <label class="form-label fw-semibold small" for="encryptPw">Encryption password (used to derive the AES key via PBKDF2)</label> <input id="encryptPw" type="password" class="form-control" style="max-width:320px" placeholder="any passphrase" value="demo-passphrase" autocomplete="off" /> <p class="text-muted small mt-2 mb-0">The password never leaves the browser. The server only sees ciphertext + the salt &amp; IV needed to decrypt on retrieval.</p> <div class="d-flex gap-2 mb-2 mt-3">
 <button type="button" class="btn btn-primary" id="startBtn_encryptUploader">Start upload</button>
 <button type="button" class="btn btn-outline-secondary" id="clearBtn_encryptUploader">Clear queue</button>
 </div>
 <div id="encryptUploader"></div> </div> <script> (function () {
 function init() {
 if (!window.AjaxUploader || typeof AjaxUploader.create !== 'function') return;
 var uploader = AjaxUploader.create('#encryptUploader', {
 uploadUrl: '<%= ResolveUrl("~/ajaxupload.axd/upload") %>',
 multiple: true,
 autoUpload: false,
 encrypt: true,
 encryptPassword: function () {
 var passwordInput = document.getElementById('encryptPw');
 return passwordInput ? passwordInput.value : '';
 }
 });
 var startButton = document.getElementById('startBtn_encryptUploader');
 var clearButton = document.getElementById('clearBtn_encryptUploader');
 if (startButton) {
 startButton.addEventListener('click', function () {
 if (uploader && typeof uploader.upload === 'function') uploader.upload();
 });
 }
 if (clearButton) {
 clearButton.addEventListener('click', function () {
 if (uploader && typeof uploader.clear === 'function') uploader.clear();
 });
 }
 }
 if (document.readyState !== 'loading') init();
 else document.addEventListener('DOMContentLoaded', init);
 })();
 </script> <div class="alert alert-info mt-3" role="alert"> <strong>Try it:</strong> drop a file, hit upload, then inspect the request body in DevTools -> Network. The payload is unintelligible binary, and request-side headers carry the decryption metadata:
 <code>X-Mu-Encryption-Algo</code>, <code>X-Mu-Encryption-Salt</code>, <code>X-Mu-Encryption-IV</code> - record these alongside the ciphertext on the server.
 </div> <h5 class="mt-4">Configuration</h5> <div class="code-panel">
<pre><code>AjaxUploader.create('#uploader', {
 uploadUrl: '<%= ResolveUrl("~/ajaxupload.axd/upload") %>',
 encrypt: <span style="color:#94a3b8">true</span>, <span style="color:#94a3b8">// or 'aes-gcm-256'</span> encryptPassword: 'shhh',
 <span style="color:#94a3b8">// OR for a prompt-driven UI:</span> <span style="color:#94a3b8">// encryptPassword: () =&gt; promptUser(),</span> <span style="color:#94a3b8">// OR for a pre-derived key:</span> <span style="color:#94a3b8">// encryptKey: cryptoKey,</span> encryptIterations: <span style="color:#94a3b8">200000</span> <span style="color:#94a3b8">// PBKDF2 iterations (default)</span>
});</code></pre> </div> <h5 class="mt-4">Wire-protocol headers (sent on every chunk / single upload)</h5> <div class="code-panel">
<pre><code>X-Mu-Encryption-Algo: aes-gcm-256
X-Mu-Encryption-Salt: <span style="color:#94a3b8">&lt;16 random bytes, base64url&gt;</span>
X-Mu-Encryption-IV: <span style="color:#94a3b8">&lt;12 random bytes, base64url&gt;</span>
X-Mu-Encryption-Iter: 200000
X-Mu-Encryption-Original-Name: <span style="color:#94a3b8">&lt;URL-encoded cleartext filename&gt;</span>
X-Mu-Encryption-Original-Size: <span style="color:#94a3b8">&lt;cleartext byte count&gt;</span></code></pre> </div> <h5 class="mt-4">Decryption (browser-side download viewer)</h5> <div class="code-panel">
<pre><code><span style="color:#94a3b8">// Static helper: pass the ciphertext blob, password, salt, iv</span>
const cleartext = await AjaxUploader.decryptFile(
 ciphertextBlob,
 password,
 saltFromHeader,
 ivFromHeader,
 200000 <span style="color:#94a3b8">// iterations, must match upload</span>
);
<span style="color:#94a3b8">// cleartext is a Blob - show it, save it, or pipe it to a viewer</span></code></pre> </div> <h5 class="mt-4">Decryption (server-side, e.g. .NET 8+)</h5> <div class="code-panel">
<pre><code><span style="color:#94a3b8">// PBKDF2 + AES-GCM are first-class in .NET 8's System.Security.Cryptography.</span>
<span style="color:#94a3b8">using</span> var pbkdf2 = <span style="color:#94a3b8">new</span> Rfc2898DeriveBytes(password, salt, 200000, HashAlgorithmName.SHA256);
<span style="color:#94a3b8">var</span> key = pbkdf2.GetBytes(32);
<span style="color:#94a3b8">using</span> var aes = <span style="color:#94a3b8">new</span> AesGcm(key, 16);
<span style="color:#94a3b8">var</span> ciphertext = encryptedFile.AsSpan(0, encryptedFile.Length - 16);
<span style="color:#94a3b8">var</span> tag = encryptedFile.AsSpan(encryptedFile.Length - 16);
<span style="color:#94a3b8">var</span> plaintext = <span style="color:#94a3b8">new</span> byte[ciphertext.Length];
aes.Decrypt(iv, ciphertext, tag, plaintext);</code></pre> </div> <h5 class="mt-4">Algorithm details</h5> <ul class="mb-3"> <li><strong>Cipher:</strong> AES-256-GCM (authenticated encryption - tamper-evident).</li> <li><strong>Key derivation:</strong> PBKDF2-HMAC-SHA256, 200&nbsp;000 iterations by default (tunable via <code>encryptIterations</code>).</li> <li><strong>Salt:</strong> 128-bit random per file. Stored alongside ciphertext; never reused.</li> <li><strong>IV (nonce):</strong> 96-bit random per file. Stored alongside ciphertext; never reused with the same key.</li> <li><strong>Auth tag:</strong> 128-bit GCM tag appended to ciphertext (handled automatically by SubtleCrypto).</li> </ul> <h5 class="mt-4">Failure modes &amp; safety</h5> <ul class="mb-3"> <li><strong>Insecure context (http://):</strong> <code>SubtleCrypto</code> is unavailable and the encrypt task fails fast with <code>encryption_failed: SubtleCrypto unavailable</code>. <strong>No silent fallback to cleartext</strong> - that's the right default for compliance use.</li> <li><strong>Missing password:</strong> queued files fail with a clear error before any byte leaves the browser.</li> <li><strong>Resume after reload:</strong> when paired with <code>persistState</code>, encryption is preserved (the ciphertext file is what's persisted, not the cleartext).</li> </ul> </div>
</div> 
</asp:Content>
