1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
|
require 'openssl'
##
# A Ruby implementation of AES Key Wrap, a.k.a RFC 3394, a.k.a NIST Key Wrapping
#
module AESKeyWrap
DEFAULT_IV = 0xA6A6A6A6A6A6A6A6
IV_SIZE = 8 # bytes
UnwrapFailedError = Class.new(StandardError)
class << self
##
# Wraps a key using a key-encrypting key (KEK)
#
# This is an implementation of the "index based" algorithm
# specified in section 2.2.1 of RFC 3394:
# http://www.ietf.org/rfc/rfc3394.txt
#
# @param unwrapped_key [String] The plaintext key to be wrapped, as a binary string
# @param kek [String] The key-encrypting key, as a binary_string
# @param iv [Integer, String] The "initial value", as either an unsigned
# 64-bit integer (e.g. `0xDEADBEEFC0FFEEEE`) or an 8-byte string (e.g.
# `"\xDE\xAD\xBE\xEF\xC0\xFF\xEE\xEE"`).
# @return [String] The wrapped key, as a binary string
#
def wrap(unwrapped_key, kek, iv=DEFAULT_IV)
# 1) Initialize variables.
#
# P: buffer (from unwrapped_key)
# A: buffer[0]
# R: buffer
# K: kek
# n: block_count
# AES: aes(:encrypt, _, _)
# IV: iv
buffer = [coerce_uint64(iv)] + unwrapped_key.unpack('Q>*')
block_count = buffer.size - 1
# 2) Calculate intermediate values.
# t: round
0.upto(5) do |j|
1.upto(block_count) do |i|
round = block_count*j + i
# In
data = [buffer[0], buffer[i]].pack('Q>2')
buffer[0], buffer[i] = aes(:encrypt, kek, data).unpack('Q>2')
# Enc
buffer[0] = buffer[0] ^ round
# XorT
end
end
# 3) Output the results.
buffer.pack('Q>*')
end
##
# Unwraps an encrypted key using a key-encrypting key (KEK)
#
# This is an implementation of the "index based" algorithm
# specified in section 2.2.2 of RFC 3394:
# http://www.ietf.org/rfc/rfc3394.txt
#
# @param wrapped_key [String] The wrapped key (cyphertext), as a binary string
# @param kek [String] The key-encrypting key, as a binary string
# @param expected_iv [Integer, String] The IV used to wrap the key, as either
# an unsigned 64-bit integer (e.g. `0xDEADBEEFC0FFEEEE`) or an 8-byte
# string (e.g. `"\xDE\xAD\xBE\xEF\xC0\xFF\xEE\xEE"`).
# @return [String] The unwrapped (plaintext) key as a binary string, or
# `nil` if unwrapping failed due to `expected_iv` not matching the
# decrypted IV
#
# @see #unwrap!
#
def unwrap(wrapped_key, kek, expected_iv=DEFAULT_IV)
# 1) Initialize variables.
#
# C: buffer (from wrapped_key)
# A: buffer[0]
# R: buffer
# n: block_count
# K: kek
# AES-1: aes(:decrypt, _, _)
buffer = wrapped_key.unpack('Q>*')
block_count = buffer.size - 1
# 2) Calculate intermediate values.
# t: round
5.downto(0) do |j|
block_count.downto(1) do |i|
round = block_count*j + i
# In
buffer[0] = buffer[0] ^ round
# XorT
data = [buffer[0], buffer[i]].pack('Q>2')
buffer[0], buffer[i] = aes(:decrypt, kek, data).unpack('Q>2')
# Dec
end
end
# 3) Output the results.
if buffer[0] == coerce_uint64(expected_iv)
buffer.drop(1).pack('Q>*')
else
nil
end
end
##
# Exception-throwing version of #unwrap
#
# @see #unwrap
#
def unwrap!(*args)
unwrap(*args) || raise(UnwrapFailedError, 'Unwrapped IV does not match')
end
private
MAX_UINT64 = 0xFFFFFFFFFFFFFFFF
def aes(encrypt_or_decrypt, key, data)
decipher = OpenSSL::Cipher::AES.new(key.bytesize * 8, :ECB)
decipher.send(encrypt_or_decrypt)
decipher.key = key
decipher.padding = 0
decipher.update(data) + decipher.final
end
def coerce_uint64(value)
case value
when Integer
if value > MAX_UINT64
raise ArgumentError, "IV is too large to fit in a 64-bit unsigned integer"
elsif value < 0
raise ArgumentError, "IV is not an unsigned integer (it's negative)"
else
value
end
when String
if value.bytesize == IV_SIZE
value.unpack("Q>").first
else
raise ArgumentError, "IV is not #{IV_SIZE} bytes long"
end
else
raise ArgumentError, "IV is not valid: #{value.inspect}"
end
end
end
end
|