Skip to content

Instantly share code, notes, and snippets.

@pch
Created June 1, 2020 10:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pch/b036ea1d74faaf20e5cc05f8f28e9926 to your computer and use it in GitHub Desktop.
Save pch/b036ea1d74faaf20e5cc05f8f28e9926 to your computer and use it in GitHub Desktop.
Encrypted Tokens Between Rails and Node

Encrypted Tokens Between Rails and Node

If you need to pass data safely between Rails and Node, you can use the built-in Rails encryption.

The script below wraps ActiveSupport::MessageEncryptor, allowing to create expiring JSON tokens that can be decoded in Node using a shared secret.

secret = "866b914a169d3969849966febafe8057bec6b82ea477e64682a11a2e61096797"
payload = { user_id: 1, name: "John Doe", role: "admin" }
EncryptedToken.encode(payload: payload, expires_at: 10.minutes.from_now, secret: secret)
# => "OGIwM2FkYjZjMzNkNGUyOGVjZGE2NWY1OGVlZDdhYWYwY2VlODcxMTE1MDRkZTE3NTFmMDU4MzMxZDU3NzUwMg==--bAliRJeMe0AL5qR5w6eWxCthlia2UQZgBlO9+EqPL6zevnNqzZesEt6NignGmf3hiE7b7490OWWqNKjqcuS8--GcrE9Y1djaPyS4Bv--iGnyNEuZXazbSrn7/p2JUg=="
node node-decrypt.js "OGIwM2FkYjZjMzNkNGUyOGVjZGE2NWY1OGVlZDdhYWYwY2VlODcxMTE1MDRkZTE3NTFmMDU4MzMxZDU3NzUwMg==--bAliRJeMe0AL5qR5w6eWxCthlia2UQZgBlO9+EqPL6zevnNqzZesEt6NignGmf3hiE7b7490OWWqNKjqcuS8--GcrE9Y1djaPyS4Bv--iGnyNEuZXazbSrn7/p2JUg=="
{"exp":1591005069,"user_id":1,"name":"John Doe","role":"admin"}
class EncryptedToken
class InvalidSignature < StandardError; end
class ExpiredSignature < StandardError; end
class Message
class << self
def wrap(payload, expires_at)
ActiveSupport::JSON.encode new(payload, expires_at)
end
def verify(message)
extract_metadata(message).verify
end
private
def extract_metadata(message)
data = ActiveSupport::JSON.decode(message) rescue nil
expired_at = data.delete("exp")
new(data, expired_at)
end
end
def initialize(payload, expires_at)
@payload = payload
@expires_at = expires_at.to_i
end
def payload
@payload
end
def as_json(options = {})
{ exp: @expires_at }.merge(@payload)
end
def verify
raise(ExpiredSignature) unless fresh?
self
end
def fresh?
Time.now.utc.to_i < @expires_at
end
end
# Wrapper for ActiveSupport::MessageEncryptor
#
# For portability, we avoid the built-in `expires_at` mechanism
# to avoid the Rails-specific metadata hash (`{ _rails: { exp: 12345 } }`)
class Encryptor
CIPHER = "aes-256-gcm"
ITERATIONS = 2**16
SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
def initialize(secret)
@secret = secret
@key_len = ActiveSupport::MessageEncryptor.key_len
end
def encrypt_and_sign(data, expires_at)
salt = SecureRandom.hex(@key_len)
crypt = init_crypt(salt)
encrypted_data = crypt.encrypt_and_sign(Message.wrap(data, expires_at))
"#{Base64.strict_encode64(salt)}--#{encrypted_data}"
end
def decrypt_and_verify(data)
salt, data = data.split("--", 2)
salt = Base64.strict_decode64(salt)
crypt = init_crypt(salt)
Message.verify(crypt.decrypt_and_verify(data))
rescue ActiveSupport::MessageEncryptor::InvalidMessage
raise InvalidSignature
end
private
def init_crypt(salt)
key_gen = ActiveSupport::KeyGenerator.new(@secret, iterations: ITERATIONS)
key = key_gen.generate_key(salt, @key_len)
ActiveSupport::MessageEncryptor.new(key, cipher: CIPHER, serializer: SERIALIZER)
end
end
def self.encode(payload:, expires_at:, secret:)
encryptor = Encryptor.new(secret)
encryptor.encrypt_and_sign(payload, expires_at)
end
def self.decode(encrypted_token:, secret:)
encryptor = Encryptor.new(secret)
encryptor.decrypt_and_verify(encrypted_token)
end
end
const secret = "866b914a169d3969849966febafe8057bec6b82ea477e64682a11a2e61096797";
let crypto = require('crypto'),
algorithm = 'aes-256-gcm',
iterations = Math.pow(2, 16),
keySize = 32,
hmacAlg = 'sha1';
// Based on: https://www.bryanculver.com/2020/01/13/secure-message-rails-nodejs.html
function decryptToken(data, key) {
let [salt, encryptedValue, iv, authTag] = data.split('--');
let derivedKey = crypto.pbkdf2Sync(key, Buffer.from(salt, 'base64'), iterations, keySize, hmacAlg);
let decipher = crypto.createDecipheriv(algorithm, derivedKey, Buffer.from(iv, 'base64'));
decipher.setAuthTag(Buffer.from(authTag, 'base64'));
let dec = decipher.update(encryptedValue, 'base64', 'utf8');
dec += decipher.final('utf8');
return dec;
}
const token = process.argv[2];
console.log(decryptToken(token, secret));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment