|
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 |