Skip to content

File Encryptor — C++ source

Encrypt or decrypt any file with AES-256-GCM in your browser. Password-based, zero server contact. Drag, drop, done.

This is the C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.

// file-encryptor — password-based AES-256-GCM file encryption.
//
// Language: C++ (C++17, standard library only; crypto primitives implemented below)
// Ported from src/lib/file-encryptor.ts (the canonical TypeScript implementation).
// display source — part of CosmoDev's polyglot tool pages.
//
// Wire format: salt (16 B) || IV (12 B) || AES-256-GCM ciphertext + tag.
// The 256-bit key is derived from the password with PBKDF2-SHA256 and a fresh
// random salt per encryption, so the same file + password never encrypts to
// the same bytes, and the password itself is never stored or derivable from
// the output.
//
// The TS reference uses the browser's Web Crypto (SubtleCrypto); C++ has no
// standard crypto facility, so SHA-256, HMAC, PBKDF2, AES-256 and GCM are
// implemented here in portable C++ with the same test vectors in mind.

#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <random>
#include <stdexcept>
#include <string>
#include <vector>

namespace file_encryptor {

using Bytes = std::vector<uint8_t>;

constexpr std::size_t SALT_LENGTH = 16;
constexpr std::size_t IV_LENGTH = 12;
constexpr uint32_t ITERATIONS = 100000;

// GCM appends a 16-byte auth tag to the ciphertext; the smallest possible
// encrypted payload is therefore salt + IV + tag = 44 bytes.
constexpr std::size_t TAG_LENGTH = 16;
constexpr std::size_t MIN_LENGTH = SALT_LENGTH + IV_LENGTH + TAG_LENGTH;

// --- SHA-256 -----------------------------------------------------------------

class Sha256 {
 public:
  Sha256() { reset(); }
  void reset() {
    h_ = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
          0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
    len_ = 0;
    buffered_ = 0;
  }
  void update(const uint8_t* data, std::size_t n) {
    len_ += n;
    while (n > 0) {
      std::size_t take = std::min(n, std::size_t(64) - buffered_);
      std::memcpy(buf_.data() + buffered_, data, take);
      buffered_ += take;
      data += take;
      n -= take;
      if (buffered_ == 64) {
        transform(buf_.data());
        buffered_ = 0;
      }
    }
  }
  void final(uint8_t out[32]) {
    uint64_t bitLen = len_ * 8;
    uint8_t pad = 0x80;
    update(&pad, 1);
    uint8_t zero = 0;
    while (buffered_ != 56) update(&zero, 1);
    uint8_t lenBytes[8];
    for (int i = 0; i < 8; i++) lenBytes[i] = uint8_t(bitLen >> (56 - 8 * i));
    update(lenBytes, 8);
    for (int i = 0; i < 8; i++) {
      out[i * 4] = uint8_t(h_[i] >> 24);
      out[i * 4 + 1] = uint8_t(h_[i] >> 16);
      out[i * 4 + 2] = uint8_t(h_[i] >> 8);
      out[i * 4 + 3] = uint8_t(h_[i]);
    }
  }

 private:
  static uint32_t rotr(uint32_t x, int n) { return (x >> n) | (x << (32 - n)); }
  void transform(const uint8_t block[64]) {
    static const uint32_t K[64] = {
        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
        0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
        0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
        0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
        0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
        0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
        0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
        0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
        0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
        0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
        0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};
    uint32_t w[64];
    for (int i = 0; i < 16; i++) {
      w[i] = (uint32_t(block[i * 4]) << 24) | (uint32_t(block[i * 4 + 1]) << 16) |
             (uint32_t(block[i * 4 + 2]) << 8) | uint32_t(block[i * 4 + 3]);
    }
    for (int i = 16; i < 64; i++) {
      uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3);
      uint32_t s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10);
      w[i] = w[i - 16] + s0 + w[i - 7] + s1;
    }
    uint32_t a = h_[0], b = h_[1], c = h_[2], d = h_[3];
    uint32_t e = h_[4], f = h_[5], g = h_[6], h = h_[7];
    for (int i = 0; i < 64; i++) {
      uint32_t S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
      uint32_t ch = (e & f) ^ (~e & g);
      uint32_t t1 = h + S1 + ch + K[i] + w[i];
      uint32_t S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
      uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
      uint32_t t2 = S0 + maj;
      h = g; g = f; f = e; e = d + t1;
      d = c; c = b; b = a; a = t1 + t2;
    }
    h_[0] += a; h_[1] += b; h_[2] += c; h_[3] += d;
    h_[4] += e; h_[5] += f; h_[6] += g; h_[7] += h;
  }
  std::array<uint32_t, 8> h_;
  std::array<uint8_t, 64> buf_;
  std::size_t buffered_ = 0;
  std::size_t len_ = 0;
};

// --- HMAC-SHA256 + PBKDF2 ----------------------------------------------------

Bytes hmacSha256(const Bytes& key, const Bytes& data) {
  uint8_t k[64] = {0};
  Bytes kCopy = key;
  if (kCopy.size() > 64) {
    Sha256 big;
    big.update(kCopy.data(), kCopy.size());
    Bytes digest(32);
    big.final(digest.data());
    kCopy = digest;
  }
  std::memcpy(k, kCopy.data(), kCopy.size());
  uint8_t ipad[64], opad[64];
  for (int i = 0; i < 64; i++) {
    ipad[i] = k[i] ^ 0x36;
    opad[i] = k[i] ^ 0x5c;
  }
  Sha256 inner;
  inner.update(ipad, 64);
  inner.update(data.data(), data.size());
  uint8_t idigest[32];
  inner.final(idigest);
  Sha256 outer;
  outer.update(opad, 64);
  outer.update(idigest, 32);
  Bytes out(32);
  outer.final(out.data());
  return out;
}

Bytes pbkdf2Sha256(const std::string& password, const Bytes& salt,
                   uint32_t iterations, std::size_t outLen) {
  Bytes out;
  uint32_t block = 1;
  while (out.size() < outLen) {
    Bytes saltBlock = salt;
    saltBlock.push_back(uint8_t(block >> 24));
    saltBlock.push_back(uint8_t(block >> 16));
    saltBlock.push_back(uint8_t(block >> 8));
    saltBlock.push_back(uint8_t(block));
    Bytes u = hmacSha256(Bytes(password.begin(), password.end()), saltBlock);
    Bytes t = u;
    for (uint32_t i = 1; i < iterations; i++) {
      u = hmacSha256(Bytes(password.begin(), password.end()), u);
      for (std::size_t j = 0; j < 32; j++) t[j] ^= u[j];
    }
    out.insert(out.end(), t.begin(), t.end());
    block++;
  }
  out.resize(outLen);
  return out;
}

// --- AES-256 (encrypt-only block cipher — all GCM needs) ---------------------

class Aes256 {
 public:
  explicit Aes256(const Bytes& key) {
    static const uint32_t RCON = 0x01;
    for (int i = 0; i < 8; i++) {
      roundKeys_[i][0] = (uint32_t(key[i * 4]) << 24) | (uint32_t(key[i * 4 + 1]) << 16) |
                         (uint32_t(key[i * 4 + 2]) << 8) | uint32_t(key[i * 4 + 3]);
    }
    uint32_t rcon = RCON;
    for (int i = 8; i < 60; i++) {
      uint32_t temp = roundKeys_[i - 1][0];
      if (i % 8 == 0) {
        temp = (temp << 8) | (temp >> 24);  // RotWord
        temp = subWord(temp) ^ (rcon << 24);
        rcon = (rcon << 1) ^ ((rcon & 0x80) ? 0x11b : 0);
      } else if (i % 8 == 4) {
        temp = subWord(temp);
      }
      roundKeys_[i][0] = roundKeys_[i - 8][0] ^ temp;
    }
  }
  void encryptBlock(const uint8_t in[16], uint8_t out[16]) const {
    uint8_t s[16];
    std::memcpy(s, in, 16);
    addRoundKey(s, 0);
    for (int round = 1; round < 14; round++) {
      subBytes(s);
      shiftRows(s);
      mixColumns(s);
      addRoundKey(s, round);
    }
    subBytes(s);
    shiftRows(s);
    addRoundKey(s, 14);
    std::memcpy(out, s, 16);
  }

 private:
  static uint8_t sbox(uint8_t x) {
    // Computed S-box via multiplicative inverse in GF(2^8) would be slow to
    // spell out; the table is the standard FIPS-197 one, generated compactly
    // below by affine transform of the inverse (still constant work).
    static uint8_t table[256];
    static bool init = false;
    if (!init) {
      uint8_t p = 1, q = 1;
      do {
        // p advances by 3 (generator), q by its inverse so p*q == 1
        p = uint8_t(p ^ ((p << 1) & 0xff) ^ ((p & 0x80) ? 0x1b : 0));
        q ^= uint8_t(q << 1);        // q advances by inverse-generator steps
        q ^= uint8_t(q << 2);
        q ^= uint8_t(q << 4);
        if (q & 0x80) q ^= 0x09;
        uint8_t xformed = q ^ rotl8(q, 1) ^ rotl8(q, 2) ^ rotl8(q, 3) ^ rotl8(q, 4);
        table[p] = xformed ^ 0x63;
      } while (p != 1);
      table[0] = 0x63;
      init = true;
    }
    return table[x];
  }
  static uint8_t rotl8(uint8_t v, int n) { return uint8_t((v << n) | (v >> (8 - n))); }
  static uint32_t subWord(uint32_t w) {
    return (uint32_t(sbox(uint8_t(w >> 24))) << 24) | (uint32_t(sbox(uint8_t(w >> 16))) << 16) |
           (uint32_t(sbox(uint8_t(w >> 8))) << 8) | sbox(uint8_t(w));
  }
  void addRoundKey(uint8_t s[16], int round) const {
    for (int col = 0; col < 4; col++) {
      uint32_t rk = roundKeys_[round * 4 + col][0];
      s[col * 4] ^= uint8_t(rk >> 24);
      s[col * 4 + 1] ^= uint8_t(rk >> 16);
      s[col * 4 + 2] ^= uint8_t(rk >> 8);
      s[col * 4 + 3] ^= uint8_t(rk);
    }
  }
  void subBytes(uint8_t s[16]) const {
    for (int i = 0; i < 16; i++) s[i] = sbox(s[i]);
  }
  static void shiftRows(uint8_t s[16]) {
    uint8_t t;
    // Row 1: rotate left by 1 (indices 1,5,9,13)
    t = s[1]; s[1] = s[5]; s[5] = s[9]; s[9] = s[13]; s[13] = t;
    // Row 2: rotate left by 2
    t = s[2]; s[2] = s[10]; s[10] = t; t = s[6]; s[6] = s[14]; s[14] = t;
    // Row 3: rotate left by 3 (== right by 1)
    t = s[15]; s[15] = s[11]; s[11] = s[7]; s[7] = s[3]; s[3] = t;
  }
  static uint8_t xtime(uint8_t a) {
    return uint8_t((a << 1) ^ ((a & 0x80) ? 0x1b : 0));
  }
  static void mixColumns(uint8_t s[16]) {
    for (int c = 0; c < 4; c++) {
      uint8_t* col = s + c * 4;
      uint8_t a0 = col[0], a1 = col[1], a2 = col[2], a3 = col[3];
      uint8_t all = a0 ^ a1 ^ a2 ^ a3;
      col[0] ^= all ^ xtime(a0 ^ a1);
      col[1] ^= all ^ xtime(a1 ^ a2);
      col[2] ^= all ^ xtime(a2 ^ a3);
      col[3] ^= all ^ xtime(a3 ^ a0);
    }
  }
  std::array<uint32_t, 60> roundKeys_{};
};

// --- AES-256-GCM ---------------------------------------------------------------

// Multiply a by b in GF(2^128) with the GCM polynomial (bit-by-bit, MSB first).
static void gcmMul(uint8_t a[16], const uint8_t bIn[16]) {
  uint8_t b[16], p[16] = {0};
  std::memcpy(b, bIn, 16);
  for (int i = 0; i < 128; i++) {
    if (a[i / 8] & (0x80 >> (i % 8))) {
      for (int j = 0; j < 16; j++) p[j] ^= b[j];
    }
    bool lsb = b[15] & 1;
    for (int j = 15; j > 0; j--) b[j] = uint8_t((b[j] >> 1) | (b[j - 1] << 7));
    b[0] >>= 1;
    if (lsb) b[0] ^= 0xe1;
  }
  std::memcpy(a, p, 16);
}

struct GcmResult {
  Bytes ciphertext;  // ciphertext + 16-byte tag appended
};

static GcmResult aesGcmEncrypt(const Bytes& key, const Bytes& iv, const Bytes& plaintext) {
  Aes256 aes(key);
  uint8_t H[16] = {0};
  aes.encryptBlock(H, H);

  // J0 = IV || 0^31 || 1 for the 96-bit IV this format always uses.
  uint8_t J0[16] = {0};
  std::memcpy(J0, iv.data(), IV_LENGTH);
  J0[15] = 1;

  // CTR mode encryption starting at inc32(J0).
  GcmResult out;
  out.ciphertext = plaintext;
  uint8_t counter[16];
  std::memcpy(counter, J0, 16);
  for (std::size_t off = 0; off < plaintext.size(); off += 16) {
    // inc32
    for (int i = 15; i >= 12; i--) {
      if (++counter[i] != 0) break;
    }
    uint8_t ks[16];
    aes.encryptBlock(counter, ks);
    std::size_t n = std::min(std::size_t(16), plaintext.size() - off);
    for (std::size_t j = 0; j < n; j++) out.ciphertext[off + j] ^= ks[j];
  }

  // GHASH over (empty AAD, C) with 64-bit length blocks (bit lengths).
  uint8_t X[16] = {0};
  auto ghashBlock = [&](const uint8_t block[16]) {
    for (int i = 0; i < 16; i++) X[i] ^= block[i];
    gcmMul(X, H);
  };
  for (std::size_t off = 0; off < out.ciphertext.size(); off += 16) {
    uint8_t block[16] = {0};
    std::memcpy(block, out.ciphertext.data() + off,
                std::min(std::size_t(16), out.ciphertext.size() - off));
    ghashBlock(block);
  }
  uint8_t lenBlock[16] = {0};
  uint64_t cBits = uint64_t(out.ciphertext.size()) * 8;
  for (int i = 0; i < 8; i++) lenBlock[8 + i] = uint8_t(cBits >> (56 - 8 * i));
  ghashBlock(lenBlock);

  uint8_t tag[16];
  aes.encryptBlock(J0, tag);
  for (int i = 0; i < 16; i++) tag[i] ^= X[i];
  out.ciphertext.insert(out.ciphertext.end(), tag, tag + 16);
  return out;
}

// Returns the plaintext, or throws on auth failure (wrong password / tamper).
static Bytes aesGcmDecrypt(const Bytes& key, const Bytes& iv, const Bytes& ciphertextWithTag) {
  if (ciphertextWithTag.size() < TAG_LENGTH) {
    throw std::runtime_error("Decryption failed: wrong password or corrupted file.");
  }
  std::size_t cLen = ciphertextWithTag.size() - TAG_LENGTH;
  Bytes body(ciphertextWithTag.begin(), ciphertextWithTag.begin() + cLen);
  GcmResult re = aesGcmEncrypt(key, iv, body);  // GHASH is the same operation
  for (std::size_t i = 0; i < TAG_LENGTH; i++) {
    if (re.ciphertext[cLen + i] != ciphertextWithTag[cLen + i]) {
      // A GCM auth-tag failure means the key did not match (wrong password) or
      // the payload was modified after encryption.
      throw std::runtime_error("Decryption failed: wrong password or corrupted file.");
    }
  }
  return re.ciphertext;  // CTR encryption with the same keystream decrypts
}

// --- CSPRNG --------------------------------------------------------------------

// std::random_device is backed by the OS entropy pool on every mainstream
// desktop/server platform (getrandom/arc4random) — the C++ stand-in for the
// browser's crypto.getRandomValues.
static Bytes randomBytes(std::size_t n) {
  Bytes out(n);
  std::random_device rd;
  for (std::size_t i = 0; i < n; i += sizeof(unsigned)) {
    unsigned v = rd();
    for (std::size_t j = 0; j < sizeof(unsigned) && i + j < n; j++) {
      out[i + j] = uint8_t(v >> (8 * j));
    }
  }
  return out;
}

// --- Public API ------------------------------------------------------------------

/** Encrypt `data` under `password`. Returns salt || IV || ciphertext+tag. */
Bytes encryptFile(const Bytes& data, const std::string& password) {
  if (password.empty()) throw std::runtime_error("Password must not be empty.");
  if (data.empty()) throw std::runtime_error("Input data is empty - nothing to encrypt.");
  Bytes salt = randomBytes(SALT_LENGTH);
  Bytes iv = randomBytes(IV_LENGTH);
  Bytes key = pbkdf2Sha256(password, salt, ITERATIONS, 32);
  GcmResult enc = aesGcmEncrypt(key, iv, data);
  Bytes out;
  out.reserve(SALT_LENGTH + IV_LENGTH + enc.ciphertext.size());
  out.insert(out.end(), salt.begin(), salt.end());
  out.insert(out.end(), iv.begin(), iv.end());
  out.insert(out.end(), enc.ciphertext.begin(), enc.ciphertext.end());
  return out;
}

/**
 * Decrypt a payload produced by `encryptFile`. Throws when the password is
 * wrong or the payload was corrupted/tampered (GCM auth-tag failure).
 */
Bytes decryptFile(const Bytes& data, const std::string& password) {
  if (password.empty()) throw std::runtime_error("Password must not be empty.");
  if (data.size() < MIN_LENGTH) {
    throw std::runtime_error("Input is too short to be an encrypted file (needs at least " +
                             std::to_string(MIN_LENGTH) +
                             " bytes: salt + IV + auth tag).");
  }
  Bytes salt(data.begin(), data.begin() + SALT_LENGTH);
  Bytes iv(data.begin() + SALT_LENGTH, data.begin() + SALT_LENGTH + IV_LENGTH);
  Bytes ciphertext(data.begin() + SALT_LENGTH + IV_LENGTH, data.end());
  Bytes key = pbkdf2Sha256(password, salt, ITERATIONS, 32);
  return aesGcmDecrypt(key, iv, ciphertext);
}

}  // namespace file_encryptor

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →