Skip to content

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 →