How to encrypt/decrypt data?


I am having trouble figuring out how to use OpenSSL::Cipher to decrypt data.
I keep getting this error:

Unhandled exception: EVP_CipherFinal_ex: Unknown or no error (OpenSSL::Cipher::Error)
  from ../../../../../snap/crystal/397/share/crystal/src/openssl/ in 'final'
  from src/ in 'decrypt'
  from src/ in '__crystal_main'
  from ../../../../../snap/crystal/397/share/crystal/src/crystal/ in 'main_user_code'
  from ../../../../../snap/crystal/397/share/crystal/src/crystal/ in 'main'
  from ../../../../../snap/crystal/397/share/crystal/src/crystal/ in 'main'
  from __libc_start_main
  from _start
  from ???

I would really appreciate it if someone could spare some time to look at the code and let me know what I am doing wrong.

Similar example from here is working fine with Bytes size 16 and “aes-128-gcm” encryption.

But in your example, it is failing here

Got it working by using single instance of cipher and passing it as parameter.

password = "password"
salt = Random::Secure.random_bytes(32)
data = "Secret data that needs to be encrypted"

key = key_derivation password, salt

cipher ="aes-256-gcm")
encrypted_data = encrypt cipher, data, key
puts "Encrypted data: #{encrypted_data.to_s}"
puts "--------------------"
decrypted_data = decrypt cipher, encrypted_data, key
puts "Decrypted data: #{decrypted_data.to_s}"

I recently had to do the same. Here’s a shard I made to help me out Feel free to pull some ideas from that.

Also note that the Crystal OpenSSL::Cipher may be missing some methods just in case you run in to that.


A single instance of Cipher works because some state is being kept between encryption/decryption. Notably, it breaks if you do the encryption/decryption independently.

To demonstrate this I have separated the encryption/decryption into separate files that writes/reads the encrypted data to a file.

$ crystal src/
$ cat encrypted_data
$ crystal src/

I noticed that you are using “aes-256-cbc” in your code, and that does indeed work whereas the “aes-256-gcm” that I was using does not. And yeah, looks like those missing methods are required for gcm.

The main difference between gcm and cbc as far as I understand is that gcm does data integrity checks and cbc does not.

Switching my example to use cbc the effect of data corruption can be seen like this:

encrypted_data[33] = 1 # data corruption
decrypted_data = decrypt encrypted_data, key
puts "Decrypted data: #{decrypted_data.to_s}"

Could you explain why you picked the numbers that you did for the salt/iv (8/16 bytes)?
I thought that the iv needed to match the key length.

1 Like

A single instance of Cipher works because some state is being kept between encryption/decryption. Notably, it breaks if you do the encryption/decryption independently.

Totally makes sense. They should not depend on each other.

I was trying to port this lib which uses the gcm, and those missing methods. It also uses 12 bytes for the iv, but when I used 12 with cbc, Crystal throws an error and said it had to be 16.

I don’t know anything about cryptography, or encryption. So I can’t say that my was is a good way of doing, but it works for what I needed (for now). It seems like the Crystal Crypto stuff needs a lot of love. I’m finding a few instances where there’s some missing areas like the Crystal version of ecdsa, elliptic and rsa. So if you find a better way to handle this, then I’m all ears!

Yeah, the crypto stuff is a bit rough compared to the rest of Crystal.

I do wish there were a simple to use API for encryption built in, something like this.

text = "secret stuff..."

key, salt = Crypto::PBKDF2.key_derivation "password" #(optional) provide salt : Bytes

encrypted_bytes = text.bytes.encrypt key #(optional), algorithm: Crypto::AES256CBC
# encrypted_text, iv = ...  (maybe return the iv, but I think I  would prefer it to just be baked in to the result)

decrypted_bytes = encrypted_bytes.decrypt key #(optional), provide the algorithm
decrypted_text = decrypted_bytes.to_s

I don’t know, just would be nice if crypto was easy to use and hard to get wrong by default instead of the other way around. Also good examples in the documentation would be great.

Guess I will use cbc for now so I can get on with finishing this application.


I also broke my teeth a bit with the crypto in Crystal. But the std is rather well provided at low level. I just published this shard to simplify the Crypto in Crystal:

Also, other crypto algo in Crystal:


Thanks, @nico,
Thats awesome!

1 Like

That’s what sodium/nacl were designed for. “The design choices emphasize security and ease of use. But despite the emphasis on high security, primitives are faster across-the-board than most implementations.”

There are crystal bindings which I maintain and a list of questions to help you decide which API to use.

The encrypted data is cross language/platform and validated against the python/ruby sodium bindings so you can pass the data to other applications easily.

Internally it uses ECDSA & argon2 as recommended by @nico. Argon2 is a big step up from PBKDF2. There’s even a program to help you choose security parameters. Or just look at the table.

There are several other features like using mlocked memory so keys don’t get paged to disk, wiping the keys when no longer used and nonce reuse detection. All of it tested with -Dpreview_mt.

Many cryptographers recommend not rolling your own crypto unless very experienced and even then having it validated.

1 Like

From the gibberish project page:

Note: It’s 2017 and if you’re looking for a modern and actively maintained Ruby encryption library you should do yourself a favor and check out RbNaCl. Gibberish was started in 2011 when encryption on Ruby was not a trivial matter, however thanks to projects like NaCl and LibSodium that’s no longer the case.

Thanks @Didactic.Drunk, I will have to try it out.
I tend to write CLI apps that automate things across multiple systems so my use case is basically an integrated password manager.

1 Like

There 3 classes are designed with your use case in mind:

All of them fit together to encrypt a password database.

  1. Create helps you choose encryption strength by timing the users system.
  2. Key takes the params from Create with the users password and derives a unique key.
  3. Kdf is used to derive subkeys. This class produces the encryption keys used by SecretBox or XChaCha20Poly1305Ietf (recommended).


Helps you choose parameters. There are several strategies in choosing key strength. Some recommendations:

  1. Use fixed parameters after timing them on the slowest system likely to be used (easy)
  2. Let Create choose them based on a given time estimate. (easy)
  3. Display different values to the user letting them decide by using Create or copying one of the examples (harder)

When choosing max mem be sure you take multiple systems and other programs in to account. Don’t set it to the systems max memory. Maybe use the lesser of 1/4, 1/8 system memory with a max of 256M-1024M.

It’s ok to use the defaults. They have fairly conservative values.


Takes the password with the provided params and gives a high quality master key. Only use this key to derive subkeys. Never store it.

Password can directly give you a Kdf so you never need to touch this.


Derives subkeys from a master key. Use this to create individual encryption keys for each file or possibly each database record.

If you encrypt your password database as a single file you probably need 1 subkey.
If you want different databases or each entry to have it’s own encryption key then you’ll need multiple subkeys.

Convenience methods exist to create CryptoBox, Sign or other Sodium objects. Prefer these methods to using subkeys directly. It’s less effort.


# First use - creating a password key and saving an initial database
pwkc =
pwkc.tcost = 0.5 # Up to 1/2 second to create key.
kdf, params = pwkc.create_kdf password

save params for later

# use unique values for each file/record
# string portion must be 8 chars and may be a fixed value or possibly table name
# the int portion may be a fixed value for single file, db record id or other
cipher = kdf.aead_xchacha20poly_ietf "8charsub", 0
data = cipher.encrypt database.to_json
File.write("db", data)

# Loading a password database
pwkey = Sodium::Password::Key.from_params hash
kdf = pwkey.derive_kdf pass
cipher = kdf.aead_xchacha20poly_ietf "8charsub", 0
database = cipher.decrypt"db").from_json

See examples for further information

1 Like