Skip to content

Instantly share code, notes, and snippets.

@GuyPaddock
Last active January 25, 2021 16:57
Show Gist options
  • Save GuyPaddock/346b8a7f945a535bc1ba77166ed27b0a to your computer and use it in GitHub Desktop.
Save GuyPaddock/346b8a7f945a535bc1ba77166ed27b0a to your computer and use it in GitHub Desktop.
How to use AES/CBC/PKCS5Padding and RSA/ECB/OAEPWithSHA-1AndMGF1Padding with Ruby 2.0.0 and Java
##
# The ONLY example on the web of using Ruby 2.0.0 to encrypt a password with the
# hybrid encryption required for interoperability with ForgeRock OpenIDM / Wren
# Security Wren:IDM.
#
# In this example, a password is first encrypted with a symmetric,
# 128-bit AES cipher in cipher-block-chaining (CBC) mode. The symmetric cipher
# is initialized with a random "session key" (i.e. a random symmetric encryption
# key). Then, the RSA public key of an SSL certificate is used to encrypt
# that encryption key.
#
# RSA is used with OAEP padding and MGF1 masking, with SHA-1 being used as the
# digest algorithm for both of those steps.
#
# The resulting JSON payload matches the structure needed for a Wren:IDM
# "x-simple-encryption" encrypted password field. If wrapped appropriately in
# a "$crypto" field, you can send it in a "patch" action request to OpenIDM
# to update a user's password field. As long as the SSL certificate being
# referenced exists in the key store for the IDM install, IDM will be able to
# decrypt the password immediately, and then re-encrypt it using whatever key
# or hashing algorithm IDM has been configured to use for longer time storage.
#
# See this section of the IDM Integrator's Guide for examples of what this
# structure looks like:
# https://backstage.forgerock.com/docs/idm/5.5/integrators-guide/#cli-encrypt
#
# This code does not contain any HTTP post logic to actually perform this type
# of patch action -- that exercise is left up to the reader (and is, quite
# frankly, the easy part). You will likely also need to setup mutual trust
# authentication and tweak some authorization policies to ensure that the
# Ruby system can obtain the necessary access to update passwords for arbitrary
# users.
#
# Tricky gotchas when working with OpenSSL in Ruby for this:
# - OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING automatically implies OAEP with
# SHA-1 hash and SHA-1 MGF1 mask. This is NOT obvious from any of the
# available documentation (especially on the Ruby docs site) -- all they
# explicitly mention is the OAEP padding.
#
# - If you wanted to use SHA-256 instead, note that:
# - "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" is a well-known misnomer,
# since RSA cannot be used in ECB mode -- it only works with a single
# block. So the correct name is actually
# "RSA/None/OAEPWithSHA-256AndMGF1Padding" when working with other JCE
# providers (like BouncyCastle).
#
# - The BouncyCastle and Go implementations of
# "RSA/None/OAEPWithSHA-256AndMGF1Padding" and
# "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" use SHA-256 for both the hash
# and masking.
#
# - The SunJCE implementation of "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"
# uses SHA-256 for the hash, but retains SHA-1 for the MGF1 masking. As
# expected, this causes no end of frustration.
#
# - You would have to take a peek at a library like JOSE::JWA to see
# how to implement the SHA-256 approach, since it's not natively
# supported by OpenSSL. See "JOSE::JWA::PKCS1.rsaes_oaep_encrypt".
#
# - PKCS7 padding for the symmetric key is still known as PKCS5 padding in
# Java, for legacy reasons.
#
# - Although there is some overlap in ciphers between the JOSE JWE library
# and what's required for this operation, JWE uses "Base 64 URL Encoding"
# for the intermediate steps (including the symmetric key), while
# Wren:IDM uses regular Base 64 (since they not try to comply to a spec like
# JWE). Consequently, output from JWE -- even if re-constituted into a
# payload that matches what the IDM decryptor needs -- can't be used for
# this use case (it fails during symmetric the decryption step).
#
require 'openssl'
require 'base64'
require 'json'
def base64(data)
Base64.encode64(data).gsub(/\n/, '')
end
password = "Rutabega8675309"
cert_pem = <<-END
-----BEGIN CERTIFICATE-----
MIIEIzCCAwugAwIBAgIJAJFhYiyUJGaIMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYD
VQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxDzANBgNVBAcMBkl0aGFjYTEgMB4G
A1UECgwXUm9zaWUgQXBwbGljYXRpb25zIEluYy4xFDASBgNVBAsMC0RldmVsb3Bt
ZW50MRswGQYDVQQDDBJwYXNzd29yZC1lbmNyeXB0b3IxHzAdBgkqhkiG9w0BCQEW
EGRldkByb3NpZWFwcC5jb20wHhcNMTgwNDI0MDExMjA0WhcNMjMwNDIzMDExMjA0
WjCBpzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMQ8wDQYDVQQHDAZJ
dGhhY2ExIDAeBgNVBAoMF1Jvc2llIEFwcGxpY2F0aW9ucyBJbmMuMRQwEgYDVQQL
DAtEZXZlbG9wbWVudDEbMBkGA1UEAwwScGFzc3dvcmQtZW5jcnlwdG9yMR8wHQYJ
KoZIhvcNAQkBFhBkZXZAcm9zaWVhcHAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEA0rRhDildLteqVXE2Jv3KT3LigbYp4FJz51/jYtCIt1QQAfbJ
p5mJfZKl61N2prW4KIgUkWtdfU0aMJTRrhFIr4iKROTFq3yhmHaeuNKhXn/n5Woo
5fe0hfcLufp/p8IZHT6m1F8/s7+zo4+GAFRUYjubKWIqdKFAWCY6UVcuVZtF+fJ6
CnkhgNDSq67vwb6m22GC0zVbs6gVEX6OSqdwuVlTDU1jigHbiqSd0kPPpkyKtxcX
2sqW6HVXVyEbdYL+q7OY0FPyhOP5Kr5X0PG80RyOOg7lN7woJnQ0jkSSjk127O9X
Gt+uW1hhibR0W9tuK3eEYClU75ocx80gJtzw2QIDAQABo1AwTjAdBgNVHQ4EFgQU
6s9FKMy85YUP52eRj4KadfSGbgwwHwYDVR0jBBgwFoAU6s9FKMy85YUP52eRj4Ka
dfSGbgwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAKtgYDECtWuP2
pxGoJQp2yutsn+EeHeqoqr50aWjgYOqrcAkZRchrO2uzdwTfBOiRcA+nfVb9GyOM
IYb6Ab3WORePN6CmlDKd66f3kSCX9mEaiW0tFz7Cp7dDTlQanie1HrJSTZF1qL6z
tVN6+lSooZe02FzFhluuo6Rd444dNXkwK7T1YmqWZE5aJtGibygBLsvjmf89m9/O
1Trl1kUIhoAkPfQ/jSbcL8bIVOfrwaH7ZrF/HwmcyO3NFLAl9Sgax99tOYcZ6dbU
lDI8PwLEkhqkQmWBE8pUyqqKL3ip7lkSsiiQKZku+7R/Hhy0n5Udn5qYKpay3GOj
g5jU7ZidXw==
-----END CERTIFICATE-----
END
cert = OpenSSL::X509::Certificate.new(cert_pem)
# Symmetric Cipher: AES 128-bit in Cipher Block Chaining Operating Mode
# (aka "AES/CBC/PKCS5Padding").
#
# AES/ECB/PKCS5Padding also works, if you change the "CBC" to "ECB", omit
# code that generates an initialization vector (iv), and omit the iv from the
# payload. But, CBC mode should be more secure.
#
# OpenSSL uses PKCS7 padding by default (what SunJCE still calls "PKCS5").
# Source: https://wiki.openssl.org/index.php/EVP_Symmetric_Encryption_and_Decryption#Padding
symmetric_cipher = OpenSSL::Cipher::AES.new(128, :CBC)
# Initialize for encryption mode
symmetric_cipher.encrypt
session_key = symmetric_cipher.random_key
password_iv = symmetric_cipher.random_iv
password_iv_encoded = base64(password_iv)
# ForgeRock must have the password as a valid JSON value, which means we have
# to enclose it in quotes.
password_json = "\"#{password}\""
password_encrypted = symmetric_cipher.update(password_json) + symmetric_cipher.final
password_encoded = base64(password_encrypted)
# Asymmetric Cipher: RSA/ECB/OAEPWithSHA-1AndMGF1Padding
asymmetric_cipher = cert.public_key
session_key_encrypted = asymmetric_cipher.public_encrypt(session_key, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
session_key_encoded = base64(session_key_encrypted)
payload = {
cipher: "AES/CBC/PKCS5Padding",
key: {
cipher: "RSA/ECB/OAEPWithSHA-1AndMGF1Padding",
key: "password-encryptor",
data: session_key_encoded
},
iv: password_iv_encoded,
data: password_encoded
}
puts JSON::generate(payload)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment