Text Encryptor — Ruby 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 Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# Text Encryptor — password-based text encryption via OpenSSL.
#
# Language: Ruby (3.1+, standard library only)
# Source: CosmoDev polyglot showcase port of the Text Encryptor tool,
# ported from src/lib/text-encryptor.ts (the canonical TypeScript
# implementation).
# License: 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.
#
# Mirrors the PBKDF2 + AES-256-GCM pattern the file-encryptor uses, applied
# to UTF-8 strings.
require 'openssl'
require 'securerandom'
module TextEncryptor
PBKDF2_ITERATIONS = 100_000
SALT_BYTES = 16
IV_BYTES = 12
# Smallest valid packed payload: salt + IV + one AES-GCM block (tag).
MIN_BYTES = SALT_BYTES + IV_BYTES + 16
ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
BASE64_RE = /\A[A-Za-z0-9+/]+={0,2}\z/
module_function
# Binary String -> canonical Base64 (pack 'm0' emits the same alphabet,
# padding, and 3-byte grouping as the hand-rolled TS codec).
def bytes_to_base64(bytes)
[bytes].pack('m0')
end
# Base64 -> binary String. Accepts surrounding whitespace; raises on any
# other non-alphabet character, wrong length, or misplaced padding.
def base64_to_bytes(b64)
clean = b64.gsub(/\s+/, '')
raise ArgumentError, 'Invalid Base64: input is empty' if clean.empty?
unless clean.size % 4 == 0
raise ArgumentError, 'Invalid Base64: length must be a multiple of 4'
end
unless clean.match?(BASE64_RE)
bad = clean.chars.find { |c| !ALPHABET.include?(c) && c != '=' }
raise ArgumentError, %(Invalid Base64: unexpected character "#{bad || '?'}")
end
clean.unpack1('m0')
end
# PBKDF2-SHA256 (100k iterations) -> 256-bit AES-GCM key (binary String).
def derive_key(password, salt)
OpenSSL::KDF.pbkdf2_hmac(password, salt: salt,
iterations: PBKDF2_ITERATIONS,
length: 32, hash: 'SHA-256')
end
# Encrypt UTF-8 +plaintext+ under +password+ and return the Base64 string
# packing salt + IV + AES-256-GCM ciphertext. Raises if either input is
# empty.
def encrypt_text(plaintext, password)
raise ArgumentError, 'Nothing to encrypt: input is empty' if plaintext.empty?
raise ArgumentError, 'Password must not be empty' if password.empty?
salt = SecureRandom.random_bytes(SALT_BYTES)
iv = SecureRandom.random_bytes(IV_BYTES)
key = derive_key(password, salt)
cipher = OpenSSL::Cipher.new('aes-256-gcm')
cipher.encrypt
cipher.key = key
cipher.iv = iv
ciphertext = cipher.update(plaintext.dup.force_encoding('BINARY')) + cipher.final
tag = cipher.auth_tag
bytes_to_base64(salt + iv + ciphertext + tag)
end
# Decrypt a Base64 string produced by #encrypt_text back to plaintext.
# Raises on empty input/password, invalid Base64, a payload too short to be
# one of ours, or a GCM authentication failure (wrong password / tampered
# data).
def decrypt_text(ciphertext, password)
if ciphertext.strip.empty?
raise ArgumentError, 'Nothing to decrypt: input is empty'
end
raise ArgumentError, 'Password must not be empty' if password.empty?
packed = base64_to_bytes(ciphertext.strip)
if packed.bytesize < MIN_BYTES
raise ArgumentError,
'Ciphertext too short - not a valid salt + IV + AES-GCM payload'
end
salt = packed.byteslice(0, SALT_BYTES)
iv = packed.byteslice(SALT_BYTES, IV_BYTES)
body = packed.byteslice(SALT_BYTES + IV_BYTES, packed.bytesize)
tag = body.byteslice(body.bytesize - 16, 16)
data = body.byteslice(0, body.bytesize - 16)
key = derive_key(password, salt)
decipher = OpenSSL::Cipher.new('aes-256-gcm')
decipher.decrypt
decipher.key = key
decipher.iv = iv
decipher.auth_tag = tag
begin
(decipher.update(data) + decipher.final).force_encoding(Encoding::UTF_8)
rescue OpenSSL::Cipher::CipherError
raise 'Decryption failed - wrong password or corrupted ciphertext'
end
end
end
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 →