Last active
January 25, 2021 16:57
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## | |
# 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