Skip to content

Text Encryptor — C++ source

Encrypt or decrypt text with a password using AES-256-GCM. Share the ciphertext safely - only someone with the password can read it.

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

// text-encryptor — password-based text encryption (AES-256-GCM + PBKDF2).
//
// Language: C++ (C++17, standard library only; crypto primitives implemented below)
// Ported from src/lib/text-encryptor.ts (the canonical TypeScript implementation).
// display source — part of CosmoDev's polyglot tool pages.
//
// Format: PBKDF2 (SHA-256, 100k iterations, 16-byte random salt) derives an
// AES-256 key from the password; AES-GCM encrypts with a 12-byte random IV.
// The Base64 output packs salt + IV + ciphertext (+ GCM tag), so every
// encryption is unique and self-contained - decrypt needs only the string and
// the password.
//
// The TS reference uses the browser's SubtleCrypto; C++ has no standard crypto
// facility, so the primitives are implemented here in portable C++.

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

namespace text_encryptor {

using Bytes = std::vector<uint8_t>;

constexpr uint32_t PBKDF2_ITERATIONS = 100000;
constexpr std::size_t SALT_BYTES = 16;
constexpr std::size_t IV_BYTES = 12;
/// Smallest valid packed payload: salt + IV + one AES-GCM block (tag).
constexpr std::size_t MIN_BYTES = SALT_BYTES + IV_BYTES + 16;

const char* const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

// --- Base64 codec (pure - no platform btoa, so it runs anywhere) ---------------

/** Bytes -> canonical Base64. */
std::string bytesToBase64(const Bytes& bytes) {
  std::string out;
  out.reserve((bytes.size() + 2) / 3 * 4);
  for (std::size_t i = 0; i < bytes.size(); i += 3) {
    uint8_t b0 = bytes[i];
    uint8_t b1 = i + 1 < bytes.size() ? bytes[i + 1] : 0;
    uint8_t b2 = i + 2 < bytes.size() ? bytes[i + 2] : 0;
    out += ALPHABET[b0 >> 2];
    out += ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)];
    out += i + 1 < bytes.size() ? ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)] : '=';
    out += i + 2 < bytes.size() ? ALPHABET[b2 & 0x3f] : '=';
  }
  return out;
}

static int b64Value(char c) {
  const char* p = std::strchr(ALPHABET, c);
  return c == '+' || (p && p - ALPHABET < 64) ? int(p - ALPHABET) : -1;
}

/** Base64 -> bytes. Accepts surrounding whitespace; throws on any other
 *  non-alphabet character, wrong length, or misplaced padding. */
Bytes base64ToBytes(const std::string& b64) {
  std::string clean;
  for (char c : b64) {
    if (!std::isspace(static_cast<unsigned char>(c))) clean += c;
  }
  if (clean.empty()) throw std::runtime_error("Invalid Base64: input is empty");
  if (clean.size() % 4 != 0) {
    throw std::runtime_error("Invalid Base64: length must be a multiple of 4");
  }
  for (char c : clean) {
    bool ok = std::isalnum(static_cast<unsigned char>(c)) || c == '+' || c == '/';
    if (!ok && c != '=') {
      throw std::runtime_error(std::string("Invalid Base64: unexpected character \"") + c + "\"");
    }
  }
  std::size_t outLength = (clean.size() / 4) * 3;
  if (clean.size() >= 2 && clean.compare(clean.size() - 2, 2, "==") == 0) outLength -= 2;
  else if (!clean.empty() && clean.back() == '=') outLength -= 1;
  Bytes bytes(outLength);
  std::size_t p = 0;
  for (std::size_t i = 0; i < clean.size(); i += 4) {
    int c0 = b64Value(clean[i]);
    int c1 = b64Value(clean[i + 1]);
    int c2 = clean[i + 2] == '=' ? -1 : b64Value(clean[i + 2]);
    int c3 = clean[i + 3] == '=' ? -1 : b64Value(clean[i + 3]);
    if (p < outLength) bytes[p++] = uint8_t((c0 << 2) | (c1 >> 4));
    if (c2 != -1 && p < outLength) bytes[p++] = uint8_t(((c1 & 0x0f) << 4) | (c2 >> 2));
    if (c2 != -1 && c3 != -1 && p < outLength) bytes[p++] = uint8_t(((c2 & 0x03) << 6) | c3);
  }
  return bytes;
}

// --- SHA-256 / HMAC / PBKDF2 ----------------------------------------------------

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 = uint64_t(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;
  uint64_t len_ = 0;
};

static 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;
}

/** PBKDF2-SHA256 (100k iterations) -> 32-byte AES-256 key. */
Bytes deriveKey(const std::string& password, const Bytes& salt) {
  Bytes out;
  uint32_t block = 1;
  while (out.size() < 32) {
    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 < PBKDF2_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(32);
  return out;
}

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

class Aes256 {
 public:
  explicit Aes256(const Bytes& key) {
    for (int i = 0; i < 8; i++) {
      roundKeys_[i] = (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 = 1;
    for (int i = 8; i < 60; i++) {
      uint32_t temp = roundKeys_[i - 1];
      if (i % 8 == 0) {
        temp = (temp << 8) | (temp >> 24);
        temp = subWord(temp) ^ (rcon << 24);
        rcon = uint32_t(((rcon << 1) & 0xff) ^ ((rcon & 0x80) ? 0x1b : 0));
      } else if (i % 8 == 4) {
        temp = subWord(temp);
      }
      roundKeys_[i] = roundKeys_[i - 8] ^ 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 rotl8(uint8_t v, int n) { return uint8_t((v << n) | (v >> (8 - n))); }
  static uint8_t sbox(uint8_t x) {
    static uint8_t table[256];
    static bool init = false;
    if (!init) {
      uint8_t p = 1, q = 1;
      do {
        p = uint8_t(p ^ ((p << 1) & 0xff) ^ ((p & 0x80) ? 0x1b : 0));
        q ^= uint8_t(q << 1);
        q ^= uint8_t(q << 2);
        q ^= uint8_t(q << 4);
        if (q & 0x80) q ^= 0x09;
        table[p] = q ^ rotl8(q, 1) ^ rotl8(q, 2) ^ rotl8(q, 3) ^ rotl8(q, 4) ^ 0x63;
      } while (p != 1);
      table[0] = 0x63;
      init = true;
    }
    return table[x];
  }
  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];
      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;
    t = s[1]; s[1] = s[5]; s[5] = s[9]; s[9] = s[13]; s[13] = t;
    t = s[2]; s[2] = s[10]; s[10] = t; t = s[6]; s[6] = s[14]; s[14] = t;
    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_{};
};

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);
}

/** CTR-encrypt (also decrypts) + GHASH + tag: ciphertext || 16-byte tag. */
Bytes aesGcmEncrypt(const Bytes& key, const Bytes& iv, const Bytes& plaintext) {
  Aes256 aes(key);
  uint8_t H[16] = {0};
  aes.encryptBlock(H, H);
  uint8_t J0[16] = {0};
  std::memcpy(J0, iv.data(), IV_BYTES);
  J0[15] = 1;

  Bytes out = plaintext;
  uint8_t counter[16];
  std::memcpy(counter, J0, 16);
  for (std::size_t off = 0; off < plaintext.size(); off += 16) {
    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[off + j] ^= ks[j];
  }

  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.size(); off += 16) {
    uint8_t block[16] = {0};
    std::memcpy(block, out.data() + off, std::min(std::size_t(16), out.size() - off));
    ghashBlock(block);
  }
  uint8_t lenBlock[16] = {0};
  uint64_t cBits = uint64_t(out.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.insert(out.end(), tag, tag + 16);
  return out;
}

static Bytes aesGcmDecrypt(const Bytes& key, const Bytes& iv, const Bytes& tagged) {
  if (tagged.size() < 16) {
    throw std::runtime_error("Decryption failed - wrong password or corrupted ciphertext");
  }
  std::size_t cLen = tagged.size() - 16;
  Bytes body(tagged.begin(), tagged.begin() + cLen);
  Bytes re = aesGcmEncrypt(key, iv, body);
  for (std::size_t i = 0; i < 16; i++) {
    if (re[cLen + i] != tagged[cLen + i]) {
      throw std::runtime_error("Decryption failed - wrong password or corrupted ciphertext");
    }
  }
  re.resize(cLen);
  return re;
}

// --- CSPRNG + UTF-8 ---------------------------------------------------------------

static Bytes randomBytes(std::size_t n) {
  Bytes out(n);
  std::random_device rd;  // OS entropy (getrandom/arc4random) on mainstream platforms
  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;
}

// std::string already carries arbitrary bytes; treat UTF-8 text as bytes
// (std::string::size() counts bytes, matching the TS TextEncoder output).

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

/**
 * Encrypt UTF-8 `plaintext` under `password` and return the Base64 string
 * packing salt + IV + AES-256-GCM ciphertext. Throws if either input is empty.
 */
std::string encryptText(const std::string& plaintext, const std::string& password) {
  if (plaintext.empty()) throw std::runtime_error("Nothing to encrypt: input is empty");
  if (password.empty()) throw std::runtime_error("Password must not be empty");
  Bytes salt = randomBytes(SALT_BYTES);
  Bytes iv = randomBytes(IV_BYTES);
  Bytes key = deriveKey(password, salt);
  Bytes plain(plaintext.begin(), plaintext.end());
  Bytes tagged = aesGcmEncrypt(key, iv, plain);
  Bytes packed;
  packed.reserve(SALT_BYTES + IV_BYTES + tagged.size());
  packed.insert(packed.end(), salt.begin(), salt.end());
  packed.insert(packed.end(), iv.begin(), iv.end());
  packed.insert(packed.end(), tagged.begin(), tagged.end());
  return bytesToBase64(packed);
}

/**
 * Decrypt a Base64 string produced by `encryptText` back to plaintext. Throws
 * on empty input/password, invalid Base64, a payload too short to be one of
 * ours, or a GCM authentication failure (wrong password / tampered data).
 */
std::string decryptText(const std::string& ciphertext, const std::string& password) {
  auto isBlank = [](const std::string& s) {
    for (char c : s) {
      if (!std::isspace(static_cast<unsigned char>(c))) return false;
    }
    return true;
  };
  if (isBlank(ciphertext)) throw std::runtime_error("Nothing to decrypt: input is empty");
  if (password.empty()) throw std::runtime_error("Password must not be empty");
  std::string trimmed;
  for (char c : ciphertext) {
    if (!std::isspace(static_cast<unsigned char>(c))) trimmed += c;
  }
  Bytes packed = base64ToBytes(trimmed);
  if (packed.size() < MIN_BYTES) {
    throw std::runtime_error("Ciphertext too short - not a valid salt + IV + AES-GCM payload");
  }
  Bytes salt(packed.begin(), packed.begin() + SALT_BYTES);
  Bytes iv(packed.begin() + SALT_BYTES, packed.begin() + SALT_BYTES + IV_BYTES);
  Bytes data(packed.begin() + SALT_BYTES + IV_BYTES, packed.end());
  Bytes key = deriveKey(password, salt);
  Bytes plain = aesGcmDecrypt(key, iv, data);
  return std::string(plain.begin(), plain.end());
}

}  // namespace text_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 →