Skip to main content

Encryption Flow

This document details the cryptographic operations that secure your content in SHIELD.

Overview

SHIELD uses a symmetric encryption model where the same key encrypts and decrypts content. The key itself is never transmitted to any server—it’s embedded in the URL fragment and stays in the browser.

Encryption Algorithm

AES-GCM-256

We use AES-GCM (Galois/Counter Mode) with a 256-bit key:
  • Confidentiality: AES encryption provides strong data protection
  • Authentication: GCM mode includes built-in authentication (no separate MAC needed)
  • Performance: Hardware-accelerated in modern browsers
  • Standard: NIST-approved, widely audited
┌─────────────────────────────────────────────────────┐
│                  AES-GCM ENCRYPTION                │
├─────────────────────────────────────────────────────┤
│  Plaintext                                          │
│     │                                               │
│     ▼                                               │
│  ┌─────────────────┐    ┌──────────┐   ┌──────┐  │
│  │  Counter Mode   │◀───│   IV     │   │      │  │
│  │  (Encryption)   │    │  (96-bit)│   │      │  │
│  └────────┬────────┘    └──────────┘   │      │  │
│           │                            │      │  │
│           ▼                            │      │  │
│  ┌─────────────────┐                   │      │  │
│  │  Galois Field   │◀──────────────────│  Key │  │
│  │ Multiplication │                   │(256b)│  │
│  │   (Auth Tag)   │                   │      │  │
│  └────────┬────────┘                   │      │  │
│           │                            │      │  │
│     ┌─────┴──────┐                     └──────┘  │
│     ▼            ▼                               │
│  Ciphertext   Auth Tag (128-bit)                 │
└─────────────────────────────────────────────────────┘

Key Generation

Random Key Generation

const generateKey = async () => {
  return await crypto.subtle.generateKey(
    {
      name: 'AES-GCM',
      length: 256
    },
    true,  // extractable
    ['encrypt', 'decrypt']
  );
};
The key is generated using the browser’s cryptographically secure random number generator (CSPRNG).

IV Generation

const generateIV = () => {
  return crypto.getRandomValues(new Uint8Array(12)); // 96 bits
};
Each encryption uses a unique IV, never reused with the same key.

Encryption Process

Step-by-Step

async function encryptContent(fileData) {
  // 1. Generate random key
  const key = await crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 },
    true,
    ['encrypt', 'decrypt']
  );

  // 2. Generate random IV
  const iv = crypto.getRandomValues(new Uint8Array(12));

  // 3. Encrypt
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    fileData
  );

  // 4. Combine IV + Ciphertext + AuthTag
  const encrypted = new Uint8Array(iv.length + ciphertext.byteLength);
  encrypted.set(iv);
  encrypted.set(new Uint8Array(ciphertext), iv.length);

  // 5. Export key for URL
  const keyBuffer = await crypto.subtle.exportKey('raw', key);
  const secretKey = arrayBufferToBase64Url(keyBuffer);

  return { encrypted, secretKey };
}

Output Format

The encrypted payload sent to IPFS:
BytesContentSize
0-11IV12 bytes
12-(n-16)Ciphertextvariable
(n-15)-nAuth Tag16 bytes

Decryption Process

async function decryptContent(encryptedData, secretKey) {
  // 1. Import key from URL
  const keyBuffer = base64UrlToArrayBuffer(secretKey);
  const key = await crypto.subtle.importKey(
    'raw',
    keyBuffer,
    { name: 'AES-GCM' },
    false,
    ['decrypt']
  );

  // 2. Extract IV and ciphertext
  const iv = encryptedData.slice(0, 12);
  const ciphertext = encryptedData.slice(12);

  // 3. Decrypt
  const plaintext = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv },
    key,
    ciphertext
  );

  return plaintext;
}

URL Fragment Encoding

The secret key is encoded for the URL fragment:
https://app.shieldhq.xyz/r/{policyId}#{secretKey}

                                      └── base64url encoded

Why URL Fragment?

Never Sent to Server

Browsers don’t include the fragment in HTTP requests. The key stays client-side.

No Server Access

Even SHIELD’s servers cannot see or log the decryption key.

Base64url Encoding

// Convert ArrayBuffer to base64url string
function arrayBufferToBase64Url(buffer) {
  const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
  return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

// Convert base64url back to ArrayBuffer
function base64UrlToArrayBuffer(str) {
  const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
  const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, '=');
  const binary = atob(padded);
  return Uint8Array.from(binary, c => c.charCodeAt(0));
}

Security Properties

Confidentiality

  • 256-bit keys: Brute-force infeasible (2^256 possibilities)
  • Unique IVs: Same content encrypts differently each time
  • Authenticated encryption: Tampering is detected

Authentication

  • GCM tag: 128-bit authentication tag prevents tampering
  • Contract verification: Policy must be valid before decryption

Forward Secrecy

  • Per-content keys: Each link has a unique key
  • No key storage: Keys only exist in shared links
  • Irrecoverable: Lost links cannot be regenerated

Threat Model

ThreatMitigation
Server compromiseServer never sees keys or plaintext
Man-in-the-middleHTTPS + contract verification
Link interceptionRecipients authenticate with wallet
Brute force256-bit keys, rate limiting
Replay attacksOn-chain attempt tracking
Tampered contentGCM authentication tag