File: aes_key_wrap.rb

package info (click to toggle)
ruby-aes-key-wrap 1.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 108 kB
  • sloc: ruby: 77; makefile: 6; sh: 3
file content (155 lines) | stat: -rw-r--r-- 4,580 bytes parent folder | download
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