sandal 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,103 +1,148 @@
1
- require 'openssl'
2
- require 'sandal/util'
1
+ require "openssl"
2
+ require "sandal/util"
3
3
 
4
4
  module Sandal
5
5
  module Enc
6
6
 
7
- # Base implementation of the AES/CBC+HMAC-SHA family of encryption
8
- # algorithms.
7
+ # Base implementation of the A*CBC-HS* family of encryption methods.
9
8
  class ACBC_HS
10
9
  include Sandal::Util
11
10
 
12
- # The JWA name of the encryption.
11
+ # The JWA name of the encryption method.
13
12
  attr_reader :name
14
13
 
15
14
  # The JWA algorithm used to encrypt the content master key.
16
15
  attr_reader :alg
17
16
 
18
- # Creates a new instance; it's probably easier to use one of the subclass
19
- # constructors.
17
+ # Initialises a new instance; it's probably easier to use one of the subclass constructors.
20
18
  #
21
- # @param aes_size [Integer] The size of the AES algorithm.
22
- # @param sha_size [Integer] The size of the SHA algorithm.
23
- # @param alg [#name, #encrypt_cmk, #decrypt_cmk] The algorithm to use to
24
- # encrypt and/or decrypt the AES key.
25
- def initialize(aes_size, sha_size, alg)
19
+ # @param name [String] The JWA name of the encryption method.
20
+ # @param aes_size [Integer] The size of the AES algorithm, in bits.
21
+ # @param sha_size [Integer] The size of the SHA algorithm, in bits.
22
+ # @param alg [#name, #encrypt_key, #decrypt_key] The algorithm to use to encrypt and/or decrypt the AES key.
23
+ def initialize(name, aes_size, sha_size, alg)
24
+ @name = name
26
25
  @aes_size = aes_size
27
26
  @sha_size = sha_size
28
- @name = "A#{aes_size}CBC+HS#{@sha_size}"
29
27
  @cipher_name = "aes-#{aes_size}-cbc"
30
28
  @alg = alg
31
29
  @digest = OpenSSL::Digest.new("sha#{@sha_size}")
32
30
  end
33
31
 
32
+ # Encrypts a token payload.
33
+ #
34
+ # @param header [String] The header string.
35
+ # @param payload [String] The payload.
36
+ # @return [String] An encrypted JSON Web Token.
34
37
  def encrypt(header, payload)
35
- cipher = OpenSSL::Cipher.new(@cipher_name).encrypt
36
- cmk = @alg.respond_to?(:cmk) ? @alg.cmk : cipher.random_key
37
- encrypted_key = @alg.encrypt_cmk(cmk)
38
+ key = get_encryption_key
39
+ mac_key, enc_key = derive_keys(key)
40
+ encrypted_key = @alg.encrypt_key(key)
38
41
 
39
- cipher.key = derive_encryption_key(cmk)
40
- iv = cipher.random_iv
42
+ cipher = OpenSSL::Cipher.new(@cipher_name).encrypt
43
+ cipher.key = enc_key
44
+ cipher.iv = iv = SecureRandom.random_bytes(16)
41
45
  ciphertext = cipher.update(payload) + cipher.final
42
46
 
43
- sec_parts = [MultiJson.dump(header), encrypted_key, iv, ciphertext]
44
- sec_input = sec_parts.map { |part| jwt_base64_encode(part) }.join('.')
45
- cik = derive_integrity_key(cmk)
46
- integrity_value = compute_integrity_value(cik, sec_input)
47
+ auth_data = jwt_base64_encode(header)
48
+ auth_data_length = [auth_data.length * 8].pack("Q>")
49
+ mac_input = [auth_data, iv, ciphertext, auth_data_length].join
50
+ mac = OpenSSL::HMAC.digest(@digest, mac_key, mac_input)
51
+ auth_tag = mac[0...(mac.length / 2)]
47
52
 
48
- sec_input << '.' << jwt_base64_encode(integrity_value)
53
+ remainder = [encrypted_key, iv, ciphertext, auth_tag].map { |part| jwt_base64_encode(part) }
54
+ [auth_data, *remainder].join(".")
49
55
  end
50
56
 
51
- def decrypt(parts, decoded_parts)
52
- cmk = @alg.decrypt_cmk(decoded_parts[1])
53
-
54
- cik = derive_integrity_key(cmk)
55
- integrity_value = compute_integrity_value(cik, parts.take(4).join('.'))
56
- unless jwt_strings_equal?(decoded_parts[4], integrity_value)
57
- raise Sandal::TokenError, 'Invalid integrity value.'
57
+ # Decrypts an encrypted JSON Web Token.
58
+ #
59
+ # @param token [String or Array] The token, or token parts, to decrypt.
60
+ # @return [String] The token payload.
61
+ def decrypt(token)
62
+ parts, decoded_parts = Sandal::Enc.token_parts(token)
63
+ header, encrypted_key, iv, ciphertext, auth_tag = *decoded_parts
64
+
65
+ key = @alg.decrypt_key(encrypted_key)
66
+ mac_key, enc_key = derive_keys(key)
67
+
68
+ auth_data = parts[0]
69
+ auth_data_length = [auth_data.length * 8].pack("Q>")
70
+ mac_input = [auth_data, iv, ciphertext, auth_data_length].join
71
+ mac = OpenSSL::HMAC.digest(@digest, mac_key, mac_input)
72
+ unless auth_tag == mac[0...(mac.length / 2)]
73
+ raise Sandal::InvalidTokenError, "Invalid authentication tag."
58
74
  end
59
75
 
60
76
  cipher = OpenSSL::Cipher.new(@cipher_name).decrypt
61
77
  begin
62
- cipher.key = derive_encryption_key(cmk)
78
+ cipher.key = enc_key
63
79
  cipher.iv = decoded_parts[2]
64
80
  cipher.update(decoded_parts[3]) + cipher.final
65
- rescue OpenSSL::Cipher::CipherError
66
- raise Sandal::TokenError, 'Invalid token.'
81
+ rescue OpenSSL::Cipher::CipherError => e
82
+ raise Sandal::InvalidTokenError, "Cannot decrypt token: #{e.message}"
67
83
  end
68
84
  end
69
85
 
70
- private
71
-
72
- # Computes the integrity value.
73
- def compute_integrity_value(cik, sec_input)
74
- OpenSSL::HMAC.digest(@digest, cik, sec_input)
75
- end
76
-
77
- # Derives the content encryption key from the content master key.
78
- def derive_encryption_key(cmk)
79
- Sandal::Enc.concat_kdf(@digest, cmk, @aes_size, @name, 0, 0, 'Encryption')
86
+ private
87
+
88
+ # Gets the key to use for mac and encryption
89
+ def get_encryption_key
90
+ key_bytes = @sha_size / 8
91
+ if @alg.respond_to?(:preshared_key)
92
+ key = @alg.preshared_key
93
+ unless key.size == key_bytes
94
+ raise Sandal::KeyError, "The pre-shared content key must be #{@sha_size} bits."
95
+ end
96
+ key
97
+ else
98
+ SecureRandom.random_bytes(key_bytes)
99
+ end
80
100
  end
81
101
 
82
- # Derives the content integrity key from the content master key.
83
- def derive_integrity_key(cmk)
84
- Sandal::Enc.concat_kdf(@digest, cmk, @sha_size, @name, 0, 0, 'Integrity')
102
+ # Derives the mac key and encryption key
103
+ def derive_keys(key)
104
+ derived_key_size = key.size / 2
105
+ mac_key = key[0...derived_key_size]
106
+ enc_key = key[derived_key_size..-1]
107
+ return mac_key, enc_key
85
108
  end
86
109
 
87
110
  end
88
111
 
89
- # The AES-128-CBC+HMAC-SHA256 encryption algorithm.
112
+ # The A128CBC-HS256 encryption method.
90
113
  class A128CBC_HS256 < Sandal::Enc::ACBC_HS
91
- def initialize(key)
92
- super(128, 256, key)
114
+
115
+ # The JWA name of the algorithm.
116
+ NAME = "A128CBC-HS256"
117
+
118
+ # The size of key that is required, in bits.
119
+ KEY_SIZE = 256
120
+
121
+ # Initialises a new instance.
122
+ #
123
+ # @param alg [#name, #encrypt_key, #decrypt_key] The algorithm to use to encrypt and/or decrypt the AES key.
124
+ def initialize(alg)
125
+ super(NAME, KEY_SIZE / 2, KEY_SIZE, alg)
93
126
  end
127
+
94
128
  end
95
129
 
96
- # The AES-256-CBC+HMAC-SHA512 encryption algorithm.
130
+ # The A256CBC-HS512 encryption method.
97
131
  class A256CBC_HS512 < Sandal::Enc::ACBC_HS
98
- def initialize(key)
99
- super(256, 512, key)
132
+
133
+ # The JWA name of the algorithm.
134
+ NAME = "A256CBC-HS512"
135
+
136
+ # The size of key that is required, in bits.
137
+ KEY_SIZE = 512
138
+
139
+ # Initialises a new instance.
140
+ #
141
+ # @param alg [#name, #encrypt_key, #decrypt_key] The algorithm to use to encrypt and/or decrypt the AES key.
142
+ def initialize(alg)
143
+ super(NAME, KEY_SIZE / 2, KEY_SIZE, alg)
100
144
  end
145
+
101
146
  end
102
147
 
103
148
  end
@@ -1,71 +1,109 @@
1
- require 'openssl'
2
- require 'sandal/util'
1
+ require "openssl"
2
+ require "sandal/util"
3
3
 
4
4
  module Sandal
5
5
  module Enc
6
6
 
7
- # Base implementation of the AES/GCM family of encryption algorithms.
7
+ # Base implementation of the A*GCM family of encryption methods.
8
8
  class AGCM
9
9
  include Sandal::Util
10
10
 
11
- # The JWA name of the encryption.
11
+ @@iv_size = 96
12
+ @@auth_tag_size = 128
13
+
14
+ # The JWA name of the encryption method.
12
15
  attr_reader :name
13
16
 
14
- # The JWA algorithm used to encrypt the content master key.
17
+ # The JWA algorithm used to encrypt the content encryption key.
15
18
  attr_reader :alg
16
19
 
17
- def initialize(aes_size, alg)
20
+ # Initialises a new instance; it's probably easier to use one of the subclass constructors.
21
+ #
22
+ # @param aes_size [Integer] The size of the AES algorithm, in bits.
23
+ # @param alg [#name, #encrypt_key, #decrypt_key] The algorithm to use to encrypt and/or decrypt the AES key.
24
+ def initialize(name, aes_size, alg)
25
+ @name = name
18
26
  @aes_size = aes_size
19
- @name = "A#{aes_size}GCM"
20
27
  @cipher_name = "aes-#{aes_size}-gcm"
21
28
  @alg = alg
22
29
  end
23
30
 
31
+ # Encrypts a token payload.
32
+ #
33
+ # @param header [String] The header string.
34
+ # @param payload [String] The payload.
35
+ # @return [String] An encrypted JSON Web Token.
24
36
  def encrypt(header, payload)
25
37
  cipher = OpenSSL::Cipher.new(@cipher_name).encrypt
26
- cmk = @alg.respond_to?(:cmk) ? @alg.cmk : cipher.random_key
27
- encrypted_key = @alg.encrypt_cmk(cmk)
38
+ key = @alg.respond_to?(:preshared_key) ? @alg.preshared_key : cipher.random_key
39
+ encrypted_key = @alg.encrypt_key(key)
28
40
 
29
- cipher.key = cmk
30
- iv = cipher.random_iv
41
+ cipher.key = key
42
+ cipher.iv = iv = SecureRandom.random_bytes(@@iv_size / 8)
31
43
 
32
- auth_parts = [MultiJson.dump(header), encrypted_key, iv]
33
- auth_data = auth_parts.map { |part| jwt_base64_encode(part) }.join('.')
44
+ auth_data = jwt_base64_encode(header)
34
45
  cipher.auth_data = auth_data
35
46
 
36
47
  ciphertext = cipher.update(payload) + cipher.final
37
- remaining_parts = [ciphertext, cipher.auth_tag]
48
+ remaining_parts = [encrypted_key, iv, ciphertext, cipher.auth_tag(@@auth_tag_size / 8)]
38
49
  remaining_parts.map! { |part| jwt_base64_encode(part) }
39
- [auth_data, *remaining_parts].join('.')
50
+ [auth_data, *remaining_parts].join(".")
40
51
  end
41
52
 
42
- def decrypt(parts, decoded_parts)
53
+ # Decrypts an encrypted JSON Web Token.
54
+ #
55
+ # @param token [String or Array] The token, or token parts, to decrypt.
56
+ # @return [String] The token payload.
57
+ def decrypt(token)
58
+ parts, decoded_parts = Sandal::Enc.token_parts(token)
43
59
  cipher = OpenSSL::Cipher.new(@cipher_name).decrypt
44
60
  begin
45
- cipher.key = @alg.decrypt_cmk(decoded_parts[1])
61
+ cipher.key = @alg.decrypt_key(decoded_parts[1])
46
62
  cipher.iv = decoded_parts[2]
47
63
  cipher.auth_tag = decoded_parts[4]
48
- cipher.auth_data = parts.take(3).join('.')
64
+ cipher.auth_data = parts[0]
49
65
  cipher.update(decoded_parts[3]) + cipher.final
50
- rescue OpenSSL::Cipher::CipherError
51
- raise Sandal::TokenError, 'Invalid token.'
66
+ rescue OpenSSL::Cipher::CipherError => e
67
+ raise Sandal::InvalidTokenError, "Cannot decrypt token: #{e.message}"
52
68
  end
53
69
  end
54
70
 
55
71
  end
56
72
 
57
- # The AES-128-GCM encryption algorithm.
73
+ # The A128GCM encryption method.
58
74
  class A128GCM < Sandal::Enc::AGCM
59
- def initialize(key)
60
- super(128, key)
75
+
76
+ # The JWA name of the algorithm.
77
+ NAME = "A128GCM"
78
+
79
+ # The size of key that is required, in bits.
80
+ KEY_SIZE = 128
81
+
82
+ # Initialises a new instance.
83
+ #
84
+ # @param alg [#name, #encrypt_key, #decrypt_key] The algorithm to use to encrypt and/or decrypt the AES key.
85
+ def initialize(alg)
86
+ super(NAME, KEY_SIZE, alg)
61
87
  end
88
+
62
89
  end
63
90
 
64
- # The AES-256-GCM encryption algorithm.
91
+ # The A256GCM encryption method.
65
92
  class A256GCM < Sandal::Enc::AGCM
66
- def initialize(key)
67
- super(256, key)
93
+
94
+ # The JWA name of the algorithm.
95
+ NAME = "A256GCM"
96
+
97
+ # The size of key that is required, in bits.
98
+ KEY_SIZE = 256
99
+
100
+ # Initialises a new instance.
101
+ #
102
+ # @param alg [#name, #encrypt_key, #decrypt_key] The algorithm to use to encrypt and/or decrypt the AES key.
103
+ def initialize(alg)
104
+ super(NAME, KEY_SIZE, alg)
68
105
  end
106
+
69
107
  end
70
108
 
71
109
  end
@@ -6,6 +6,5 @@ module Sandal
6
6
  end
7
7
  end
8
8
 
9
- require 'sandal/enc/alg/direct'
10
- require 'sandal/enc/alg/rsa1_5'
11
- require 'sandal/enc/alg/rsa_oaep'
9
+ require "sandal/enc/alg/direct"
10
+ require "sandal/enc/alg/rsa"
@@ -1,46 +1,48 @@
1
- require 'openssl'
1
+ require "openssl"
2
2
 
3
3
  module Sandal
4
4
  module Enc
5
5
  module Alg
6
6
 
7
- # The direct ("dir") key encryption mechanism, which uses a pre-shared
8
- # content master key.
7
+ # The direct ("dir") key encryption algorithm, which uses a pre-shared symmetric key.
9
8
  class Direct
10
9
 
11
- # @return [String] The JWA name of the algorithm.
12
- attr_reader :name
10
+ # The JWA name of the algorithm.
11
+ NAME = "dir"
13
12
 
14
- # @return [String] The pre-shared content master key key.
15
- attr_reader :cmk
13
+ # @return [String] The pre-shared symmetric key.
14
+ attr_reader :preshared_key
16
15
 
17
- # Creates a new instance.
16
+ # Initialises a new instance.
18
17
  #
19
- # @param cmk [String] The pre-shared content master key.
20
- def initialize(cmk)
21
- @name = 'dir'
22
- @cmk = cmk
18
+ # @param preshared_key [String] The pre-shared symmetric key.
19
+ def initialize(preshared_key)
20
+ @preshared_key = preshared_key
23
21
  end
24
22
 
25
- # Returns an empty string as the content master key is not included in
26
- # the JWE token.
23
+ # The JWA name of the algorithm.
24
+ def name
25
+ NAME
26
+ end
27
+
28
+ # Returns an empty string as the key is not included in JWE tokens using direct key exchange.
27
29
  #
28
- # @param cmk [String] This parameter is ignored.
30
+ # @param key [String] This parameter is ignored.
29
31
  # @return [String] An empty string.
30
- def encrypt_cmk(cmk)
31
- ''
32
+ def encrypt_key(key)
33
+ ""
32
34
  end
33
35
 
34
- # Returns the pre-shared content master key.
36
+ # Returns the pre-shared content key.
35
37
  #
36
- # @param encrypted_cmk [String] The encrypted content master key.
37
- # @return [String] The pre-shared content master key.
38
- # @raise [Sandal::TokenError] encrypted_cmk is not nil or empty.
39
- def decrypt_cmk(encrypted_cmk)
40
- unless encrypted_cmk.nil? || encrypted_cmk.empty?
41
- raise Sandal::TokenError, 'Token must not include encrypted CMK.'
38
+ # @param encrypted_key [String] The encrypted key.
39
+ # @return [String] The pre-shared symmetric key.
40
+ # @raise [Sandal::InvalidTokenError] encrypted_key is not nil or empty.
41
+ def decrypt_key(encrypted_key)
42
+ unless encrypted_key.nil? || encrypted_key.empty?
43
+ raise Sandal::InvalidTokenError, "Tokens using direct key exchange must not include a content key."
42
44
  end
43
- @cmk
45
+ @preshared_key
44
46
  end
45
47
 
46
48
  end
@@ -0,0 +1,82 @@
1
+ require "openssl"
2
+
3
+ module Sandal
4
+ module Enc
5
+ module Alg
6
+
7
+ # Base class for RSA key encryption algorithm.
8
+ class RSA
9
+
10
+ # The JWA name of the algorithm.
11
+ attr_reader :name
12
+
13
+ # Initialises a new instance.
14
+ #
15
+ # @param name [String] The JWA name of the algorithm.
16
+ # @param rsa_key [OpenSSL::PKey::RSA or String] The RSA key to use for key encryption (public) or decryption
17
+ # (private). If the value is a String then it will be passed to the constructor of the RSA class. This must
18
+ # be at least 2048 bits to be compliant with the JWA specification.
19
+ def initialize(name, rsa_key, padding)
20
+ @name = name
21
+ @rsa_key = rsa_key.is_a?(String) ? OpenSSL::PKey::RSA.new(rsa_key) : rsa_key
22
+ @padding = padding
23
+ end
24
+
25
+ # Encrypts the content key.
26
+ #
27
+ # @param key [String] The content key.
28
+ # @return [String] The encrypted content key.
29
+ def encrypt_key(key)
30
+ @rsa_key.public_encrypt(key, @padding)
31
+ end
32
+
33
+ # Decrypts the content key.
34
+ #
35
+ # @param encrypted_key [String] The encrypted content key.
36
+ # @return [String] The pre-shared content key.
37
+ # @raise [Sandal::TokenError] The content key can't be decrypted.
38
+ def decrypt_key(encrypted_key)
39
+ @rsa_key.private_decrypt(encrypted_key, @padding)
40
+ rescue => e
41
+ raise Sandal::InvalidTokenError, "Cannot decrypt content key: #{e.message}"
42
+ end
43
+
44
+ end
45
+
46
+ # The RSA1_5 key encryption algorithm.
47
+ class RSA1_5 < RSA
48
+
49
+ # The JWA name of the algorithm.
50
+ NAME = "RSA1_5"
51
+
52
+ # Initialises a new instance.
53
+ #
54
+ # @param rsa_key [OpenSSL::PKey::RSA or String] The RSA key to use for key encryption (public) or decryption
55
+ # (private). If the value is a String then it will be passed to the constructor of the RSA class. This must
56
+ # be at least 2048 bits to be compliant with the JWA specification.
57
+ def initialize(rsa_key)
58
+ super(NAME, rsa_key, OpenSSL::PKey::RSA::PKCS1_PADDING)
59
+ end
60
+
61
+ end
62
+
63
+ # The RSA-OAEP key encryption algorithm.
64
+ class RSA_OAEP < RSA
65
+
66
+ # The JWA name of the algorithm.
67
+ NAME = "RSA-OAEP"
68
+
69
+ # Initialises a new instance.
70
+ #
71
+ # @param rsa_key [OpenSSL::PKey::RSA or String] The RSA key to use for key encryption (public) or decryption
72
+ # (private). If the value is a String then it will be passed to the constructor of the RSA class. This must
73
+ # be at least 2048 bits to be compliant with the JWA specification.
74
+ def initialize(rsa_key)
75
+ super(NAME, rsa_key, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
76
+ end
77
+
78
+ end
79
+
80
+ end
81
+ end
82
+ end