File Encryptor — Ruby source
Encrypt or decrypt any file with AES-256-GCM in your browser. Password-based, zero server contact. Drag, drop, done.
This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# File Encryptor — password-based AES-256-GCM file encryption via OpenSSL.
#
# Language: Ruby (3.1+, standard library only)
# Source: CosmoDev polyglot showcase port of the File Encryptor tool,
# ported from src/lib/file-encryptor.ts (the canonical TypeScript
# implementation).
# License: 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.
require 'openssl'
require 'securerandom'
module FileEncryptor
SALT_LENGTH = 16
IV_LENGTH = 12
ITERATIONS = 100_000
# GCM appends a 16-byte auth tag to the ciphertext; the smallest possible
# encrypted payload is therefore salt + IV + tag = 44 bytes.
TAG_LENGTH = 16
MIN_LENGTH = SALT_LENGTH + IV_LENGTH + TAG_LENGTH
module_function
# PBKDF2-SHA256 (100k iterations) -> 256-bit AES key (binary String).
def derive_key(password, salt)
OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: ITERATIONS,
length: 32, hash: 'SHA-256')
end
# Encrypt +data+ (binary String) under +password+.
# Returns salt || IV || ciphertext + tag as a binary String.
def encrypt_file(data, password)
raise ArgumentError, 'Password must not be empty.' if password.empty?
raise ArgumentError, 'Input data is empty - nothing to encrypt.' if data.empty?
salt = SecureRandom.random_bytes(SALT_LENGTH)
iv = SecureRandom.random_bytes(IV_LENGTH)
key = derive_key(password, salt)
cipher = OpenSSL::Cipher.new('aes-256-gcm')
cipher.encrypt
cipher.key = key
cipher.iv = iv
ciphertext = cipher.update(data) + cipher.final
tag = cipher.auth_tag # 16 bytes; WebCrypto appends it to the ciphertext
salt + iv + ciphertext + tag
end
# Decrypt a payload produced by #encrypt_file. Raises when the password is
# wrong or the payload was corrupted/tampered (GCM auth-tag failure).
def decrypt_file(data, password)
raise ArgumentError, 'Password must not be empty.' if password.empty?
if data.bytesize < MIN_LENGTH
raise ArgumentError,
"Input is too short to be an encrypted file (needs at least " \
"#{MIN_LENGTH} bytes: salt + IV + auth tag)."
end
salt = data.byteslice(0, SALT_LENGTH)
iv = data.byteslice(SALT_LENGTH, IV_LENGTH)
body = data.byteslice(SALT_LENGTH + IV_LENGTH, data.bytesize)
tag = body.byteslice(body.bytesize - TAG_LENGTH, TAG_LENGTH)
ciphertext = body.byteslice(0, body.bytesize - TAG_LENGTH)
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(ciphertext) + decipher.final
rescue OpenSSL::Cipher::CipherError
# A GCM auth-tag failure means the key did not match (wrong password) or
# the payload was modified after encryption.
raise 'Decryption failed: wrong password or corrupted file.'
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 →