sandal 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,6 +6,7 @@ module Sandal
6
6
 
7
7
  # Base implementation of the AES/GCM family of encryption algorithms.
8
8
  class AGCM
9
+ extend Sandal::Util
9
10
 
10
11
  # The JWA name of the encryption.
11
12
  attr_reader :name
@@ -22,28 +23,33 @@ module Sandal
22
23
 
23
24
  def encrypt(header, payload)
24
25
  cipher = OpenSSL::Cipher.new(@cipher_name).encrypt
25
- content_master_key = @alg.respond_to?(:cmk) ? @alg.cmk : cipher.random_key
26
- encrypted_key = @alg.encrypt_cmk(content_master_key)
26
+ cmk = @alg.respond_to?(:cmk) ? @alg.cmk : cipher.random_key
27
+ encrypted_key = @alg.encrypt_cmk(cmk)
27
28
 
28
- cipher.key = content_master_key
29
+ cipher.key = cmk
29
30
  iv = cipher.random_iv
30
31
 
31
32
  auth_parts = [MultiJson.dump(header), encrypted_key, iv]
32
- auth_data = auth_parts.map { |part| Sandal::Util.base64_encode(part) }.join('.')
33
+ auth_data = auth_parts.map { |part| jwt_base64_encode(part) }.join('.')
33
34
  cipher.auth_data = auth_data
34
35
 
35
36
  ciphertext = cipher.update(payload) + cipher.final
36
- remainder = [ciphertext, cipher.auth_tag].map { |part| Sandal::Util.base64_encode(part) }.join('.')
37
- [auth_data, remainder].join('.')
37
+ remaining_parts = [ciphertext, cipher.auth_tag]
38
+ remaining_parts.map! { |part| jwt_base64_encode(part) }
39
+ [auth_data, *remaining_parts].join('.')
38
40
  end
39
41
 
40
42
  def decrypt(parts, decoded_parts)
41
43
  cipher = OpenSSL::Cipher.new(@cipher_name).decrypt
42
- cipher.key = @alg.decrypt_cmk(decoded_parts[1])
43
- cipher.iv = decoded_parts[2]
44
- cipher.auth_tag = decoded_parts[4]
45
- cipher.auth_data = parts.take(3).join('.')
46
- cipher.update(decoded_parts[3]) + cipher.final
44
+ begin
45
+ cipher.key = @alg.decrypt_cmk(decoded_parts[1])
46
+ cipher.iv = decoded_parts[2]
47
+ cipher.auth_tag = decoded_parts[4]
48
+ cipher.auth_data = parts.take(3).join('.')
49
+ cipher.update(decoded_parts[3]) + cipher.final
50
+ rescue OpenSSL::Cipher::CipherError
51
+ raise Sandal::TokenError, 'Invalid token.'
52
+ end
47
53
  end
48
54
 
49
55
  end
@@ -4,7 +4,8 @@ module Sandal
4
4
  module Enc
5
5
  module Alg
6
6
 
7
- # The direct ("dir") key encryption mechanism, which uses a pre-shared content master key.
7
+ # The direct ("dir") key encryption mechanism, which uses a pre-shared
8
+ # content master key.
8
9
  class Direct
9
10
 
10
11
  # @return [String] The JWA name of the algorithm.
@@ -21,7 +22,8 @@ module Sandal
21
22
  @cmk = cmk
22
23
  end
23
24
 
24
- # Returns an empty string as the content master key is not included in the JWE token.
25
+ # Returns an empty string as the content master key is not included in
26
+ # the JWE token.
25
27
  #
26
28
  # @param cmk [String] This parameter is ignored.
27
29
  # @return [String] An empty string.
@@ -36,7 +38,7 @@ module Sandal
36
38
  # @raise [Sandal::TokenError] encrypted_cmk is not nil or empty.
37
39
  def decrypt_cmk(encrypted_cmk)
38
40
  unless encrypted_cmk.nil? || encrypted_cmk.empty?
39
- raise Sandal::TokenError, 'The token should not include an encrypted content master key.'
41
+ raise Sandal::TokenError, 'Token must not include encrypted CMK.'
40
42
  end
41
43
  @cmk
42
44
  end
@@ -12,10 +12,13 @@ module Sandal
12
12
 
13
13
  # Creates a new instance.
14
14
  #
15
- # @param key [OpenSSL::PKey::RSA] The RSA public key used to protect the content master key.
15
+ # @param key [OpenSSL::PKey::RSA or String] The key to use for CMK
16
+ # encryption (public) or decryption (private). If the value is a String
17
+ # 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.
16
19
  def initialize(key)
17
20
  @name = 'RSA1_5'
18
- @key = key
21
+ @key = key.is_a?(String) ? OpenSSL::PKey::RSA.new(key) : key
19
22
  end
20
23
 
21
24
  # Encrypts the content master key.
@@ -30,11 +33,11 @@ module Sandal
30
33
  #
31
34
  # @param encrypted_cmk [String] The encrypted content master key.
32
35
  # @return [String] The pre-shared content master key.
33
- # @raise [Sandal::TokenError] The content master key cannot be decrypted.
36
+ # @raise [Sandal::TokenError] The content master key can't be decrypted.
34
37
  def decrypt_cmk(encrypted_cmk)
35
38
  @key.private_decrypt(encrypted_cmk)
36
39
  rescue
37
- raise Sandal::TokenError, 'Failed to decrypt the content master key.'
40
+ raise Sandal::TokenError, 'Cannot decrypt content master key.'
38
41
  end
39
42
 
40
43
  end
@@ -12,10 +12,13 @@ module Sandal
12
12
 
13
13
  # Creates a new instance.
14
14
  #
15
- # @param key [OpenSSL::PKey::RSA] The RSA public key used to protect the content master key.
15
+ # @param key [OpenSSL::PKey::RSA or String] The key to use for CMK
16
+ # encryption (public) or decryption (private). If the value is a String
17
+ # 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.
16
19
  def initialize(key)
17
20
  @name = 'RSA-OAEP'
18
- @key = key
21
+ @key = key.is_a?(String) ? OpenSSL::PKey::RSA.new(key) : key
19
22
  @padding = OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
20
23
  end
21
24
 
@@ -31,11 +34,11 @@ module Sandal
31
34
  #
32
35
  # @param encrypted_cmk [String] The encrypted content master key.
33
36
  # @return [String] The pre-shared content master key.
34
- # @raise [Sandal::TokenError] The content master key cannot be decrypted.
37
+ # @raise [Sandal::TokenError] The content master key can't be decrypted.
35
38
  def decrypt_cmk(encrypted_cmk)
36
39
  @key.private_decrypt(encrypted_cmk, @padding)
37
40
  rescue
38
- raise Sandal::TokenError, 'Failed to decrypt the content master key.'
41
+ raise Sandal::TokenError, 'Cannot decrypt content master key.'
39
42
  end
40
43
 
41
44
  end
data/lib/sandal/sig.rb CHANGED
@@ -28,13 +28,16 @@ module Sandal
28
28
  #
29
29
  # @param signature [String] The signature to verify.
30
30
  # @param payload [String] This parameter is ignored.
31
- # @return [Boolean] true if the signature is nil or empty; otherwise false.
31
+ # @return [Boolean] true if the signature is nil/empty; otherwise false.
32
32
  def valid?(signature, payload)
33
33
  signature.nil? || signature.empty?
34
34
  end
35
35
 
36
36
  end
37
37
 
38
+ # The singleton instance of the Sandal::Sig::None signature method.
39
+ NONE = Sandal::Sig::None.instance
40
+
38
41
  end
39
42
  end
40
43
 
data/lib/sandal/sig/es.rb CHANGED
@@ -9,11 +9,13 @@ module Sandal
9
9
  # @return [String] The JWA name of the algorithm.
10
10
  attr_reader :name
11
11
 
12
- # Creates a new instance; it's probably easier to use one of the subclass constructors.
12
+ # Creates a new instance; it's probably easier to use one of the subclass
13
+ # constructors.
13
14
  #
14
15
  # @param sha_size [Integer] The size of the SHA algorithm.
15
16
  # @param prime_size [Integer] The size of the ECDSA primes.
16
- # @param key [OpenSSL::PKey::EC] The key to use for signing (private) or validation (public).
17
+ # @param key [OpenSSL::PKey::EC] The key to use for signing (private) or
18
+ # validation (public).
17
19
  def initialize(sha_size, prime_size, key)
18
20
  @name = "ES#{sha_size}"
19
21
  @digest = OpenSSL::Digest.new("sha#{sha_size}")
@@ -87,16 +89,20 @@ module Sandal
87
89
  n_to_s.call(r) + n_to_s.call(s)
88
90
  end
89
91
 
90
- protected
92
+ private
91
93
 
92
- # Ensures that a key has a specified curve name.
94
+ # Makes an EC key and ensures that it has the right curve name.
93
95
  #
94
- # @param key [OpenSSL::PKey::EC] The key.
96
+ # @param key [OpenSSL::PKey::EC or String] The key.
95
97
  # @param curve_name [String] The curve name.
96
- # @return [void].
97
- # @raise [ArgumentError] The key has a different curve name.
98
- def ensure_curve(key, curve_name)
99
- raise ArgumentError, "The key must be in the #{curve_name} group." unless key.group.curve_name == curve_name
98
+ # @return [OpenSSL::PKey::EC] The key.
99
+ # @raise [ArgumentError] The key has the wrong curve name.
100
+ def make_key(key, curve_name)
101
+ key = OpenSSL::PKey::EC.new(key) if key.is_a?(String)
102
+ unless key.group.curve_name == curve_name
103
+ raise ArgumentError, "The key must be in the #{curve_name} group."
104
+ end
105
+ key
100
106
  end
101
107
 
102
108
  end
@@ -105,11 +111,12 @@ module Sandal
105
111
  class ES256 < Sandal::Sig::ES
106
112
  # Creates a new instance.
107
113
  #
108
- # @param key [OpenSSL::PKey::EC] The key to use for signing (private) or validation (public).
114
+ # @param key [OpenSSL::PKey::EC or String] The key to use for signing
115
+ # (private) or validation (public). If the value is a String then it
116
+ # will be passed to the constructor of the EC class.
109
117
  # @raise [ArgumentError] The key is not in the "prime256v1" group.
110
118
  def initialize(key)
111
- ensure_curve(key, 'prime256v1')
112
- super(256, 256, key)
119
+ super(256, 256, make_key(key, 'prime256v1'))
113
120
  end
114
121
  end
115
122
 
@@ -117,11 +124,12 @@ module Sandal
117
124
  class ES384 < Sandal::Sig::ES
118
125
  # Creates a new instance.
119
126
  #
120
- # @param key [OpenSSL::PKey::EC] The key to use for signing (private) or validation (public).
127
+ # @param key [OpenSSL::PKey::EC or String] The key to use for signing
128
+ # (private) or validation (public). If the value is a String then it
129
+ # will be passed to the constructor of the EC class.
121
130
  # @raise [ArgumentError] The key is not in the "secp384r1" group.
122
131
  def initialize(key)
123
- ensure_curve(key, 'secp384r1')
124
- super(384, 384, key)
132
+ super(384, 384, make_key(key, 'secp384r1'))
125
133
  end
126
134
  end
127
135
 
@@ -129,11 +137,12 @@ module Sandal
129
137
  class ES512 < Sandal::Sig::ES
130
138
  # Creates a new instance.
131
139
  #
132
- # @param key [OpenSSL::PKey::EC] The key to use for signing (private) or validation (public).
140
+ # @param key [OpenSSL::PKey::EC or String] The key to use for signing
141
+ # (private) or validation (public). If the value is a String then it
142
+ # will be passed to the constructor of the EC class.
133
143
  # @raise [ArgumentError] The key is not in the "secp521r1" group.
134
144
  def initialize(key)
135
- ensure_curve(key, 'secp521r1')
136
- super(512, 521, key)
145
+ super(512, 521, make_key(key, 'secp521r1'))
137
146
  end
138
147
  end
139
148
 
data/lib/sandal/sig/hs.rb CHANGED
@@ -5,11 +5,13 @@ module Sandal
5
5
 
6
6
  # Base implementation of the HMAC-SHA family of signature algorithms.
7
7
  class HS
8
+ extend Sandal::Util
8
9
 
9
10
  # @return [String] The JWA name of the algorithm.
10
11
  attr_reader :name
11
12
 
12
- # Creates a new instance; it's probably easier to use one of the subclass constructors.
13
+ # Creates a new instance; it's probably easier to use one of the subclass
14
+ # constructors.
13
15
  #
14
16
  # @param sha_size [Integer] The size of the SHA algorithm.
15
17
  # @param key [String] The key to use for signing or validation.
@@ -33,7 +35,7 @@ module Sandal
33
35
  # @param payload [String] The payload of the token.
34
36
  # @return [Boolean] true if the signature is correct; otherwise false.
35
37
  def valid?(signature, payload)
36
- Sandal::Util.secure_equals(sign(payload), signature)
38
+ jwt_strings_equal?(sign(payload), signature)
37
39
  end
38
40
 
39
41
  end
data/lib/sandal/sig/rs.rb CHANGED
@@ -9,11 +9,13 @@ module Sandal
9
9
  # @return [String] The JWA name of the algorithm.
10
10
  attr_reader :name
11
11
 
12
- # Creates a new instance; it's probably easier to use one of the subclass constructors.
12
+ # Creates a new instance; it's probably easier to use one of the subclass
13
+ # constructors.
13
14
  #
14
15
  # @param sha_size [Integer] The size of the SHA algorithm.
15
- # @param key [OpenSSL::PKey::RSA] The key to use for signing (private) or validation (public). This must
16
- # be at least 2048 bits to be compliant with the JWA specification.
16
+ # @param key [OpenSSL::PKey::RSA] The key to use for signing (private) or
17
+ # validation (public). This must be at least 2048 bits to be compliant
18
+ # with the JWA specification.
17
19
  def initialize(sha_size, key)
18
20
  @name = "RS#{sha_size}"
19
21
  @digest = OpenSSL::Digest.new("sha#{sha_size}")
@@ -37,16 +39,28 @@ module Sandal
37
39
  @key.verify(@digest, signature, payload)
38
40
  end
39
41
 
42
+ private
43
+
44
+ # Makes an RSA key.
45
+ #
46
+ # @param key [OpenSSL::PKey::RSA or String] The key.
47
+ # @return [OpenSSL::PKey::RSA] The key.
48
+ def make_key(key)
49
+ key.is_a?(String) ? OpenSSL::PKey::RSA.new(key) : key
50
+ end
51
+
40
52
  end
41
53
 
42
54
  # The RSA-SHA256 signing algorithm.
43
55
  class RS256 < Sandal::Sig::RS
44
56
  # Creates a new instance.
45
57
  #
46
- # @param key [OpenSSL::PKey::RSA] The key to use for signing (private) or validation (public). This must
47
- # be at least 2048 bits to be compliant with the JWA specification.
58
+ # @param key [OpenSSL::PKey::RSA or String] The key to use for signing
59
+ # (private) or validation (public). If the value is a String then it
60
+ # will be passed to the constructor of the RSA class. This must be at
61
+ # least 2048 bits to be compliant with the JWA specification.
48
62
  def initialize(key)
49
- super(256, key)
63
+ super(256, make_key(key))
50
64
  end
51
65
  end
52
66
 
@@ -54,10 +68,12 @@ module Sandal
54
68
  class RS384 < Sandal::Sig::RS
55
69
  # Creates a new instance.
56
70
  #
57
- # @param key [OpenSSL::PKey::RSA] The key to use for signing (private) or validation (public). This must
58
- # be at least 2048 bits to be compliant with the JWA specification.
71
+ # @param key [OpenSSL::PKey::RSA or String] The key to use for signing
72
+ # (private) or validation (public). If the value is a String then it
73
+ # will be passed to the constructor of the RSA class. This must be at
74
+ # least 2048 bits to be compliant with the JWA specification.
59
75
  def initialize(key)
60
- super(384, key)
76
+ super(384, make_key(key))
61
77
  end
62
78
  end
63
79
 
@@ -65,10 +81,12 @@ module Sandal
65
81
  class RS512 < Sandal::Sig::RS
66
82
  # Creates a new instance.
67
83
  #
68
- # @param key [OpenSSL::PKey::RSA] The key to use for signing (private) or validation (public). This must
69
- # be at least 2048 bits to be compliant with the JWA specification.
84
+ # @param key [OpenSSL::PKey::RSA or String] The key to use for signing
85
+ # (private) or validation (public). If the value is a String then it
86
+ # will be passed to the constructor of the RSA class. This must be at
87
+ # least 2048 bits to be compliant with the JWA specification.
70
88
  def initialize(key)
71
- super(512, key)
89
+ super(512, make_key(key))
72
90
  end
73
91
  end
74
92
 
data/lib/sandal/util.rb CHANGED
@@ -1,43 +1,62 @@
1
1
  require 'base64'
2
2
 
3
3
  module Sandal
4
- # Implements some JWT utility functions. Shouldn't be needed by most people but may
5
- # be useful if you're developing an extension to the library.
4
+ # @private
5
+ # Implements some JWT utility functions. Shouldn't be needed by most people
6
+ # but may be useful if you're developing an extension to the library.
6
7
  module Util
8
+
9
+ private
7
10
 
8
- # A string equality function which doesn't short-circuit the equality check to help
9
- # protect against timing attacks.
11
+ # A string equality function that compares Unicode codepoints, and also
12
+ # doesn't short-circuit the equality check to help protect against timing
13
+ # attacks.
10
14
  #--
11
- # See http://rdist.root.org/2009/05/28/timing-attack-in-google-keyczar-library/
12
- def self.secure_equals(a, b)
13
- if a.nil? && b.nil?
14
- true
15
- elsif a.nil? || b.nil? || a.bytesize != b.bytesize
16
- false
17
- else
18
- result = a.bytes.zip(b.bytes).reduce(0) { |memo, (b1, b2)| memo |= (b1 ^ b2) }
19
- result == 0
20
- end
15
+ # http://rdist.root.org/2009/05/28/timing-attack-in-google-keyczar-library/
16
+ # for more info about timing attacks.
17
+ #++
18
+ #
19
+ # @param a [String] The first string.
20
+ # @param b [String] The second string.
21
+ # @return [Boolean] true if the strings are equal; otherwise false.
22
+ def jwt_strings_equal?(a, b)
23
+ return true if a.object_id == b.object_id
24
+ return false if a.nil? || b.nil? || a.length != b.length
25
+ a.codepoints.zip(b.codepoints).reduce(0) { |r, (a, b)| r |= a ^ b } == 0
21
26
  end
22
27
 
23
28
  # Base64 encodes a string, in compliance with the JWT specification.
24
29
  #
25
30
  # @param s [String] The string to encode.
26
31
  # @return [String] The encoded base64 string.
27
- def self.base64_encode(s)
28
- Base64.urlsafe_encode64(s).gsub(%r{=+$}, '')
32
+ def jwt_base64_encode(s)
33
+ Base64.urlsafe_encode64(s).gsub(/=+$/, '')
29
34
  end
30
35
 
31
36
  # Base64 decodes a string, in compliance with the JWT specification.
32
37
  #
33
38
  # @param s [String] The base64 string to decode.
34
39
  # @return [String] The decoded string.
35
- # @raise [Sandal::TokenError] The base64 string contains padding.
36
- def self.base64_decode(s)
37
- raise Sandal::TokenError, 'Base64 strings cannot contain padding.' if s.end_with?('=')
40
+ # @raise [ArgumentError] The base64 string is invalid or contains padding.
41
+ def jwt_base64_decode(s)
42
+ if s.end_with?('=')
43
+ raise ArgumentError, 'Base64 strings must not contain padding.'
44
+ end
45
+
38
46
  padding_length = (4 - (s.length % 4)) % 4
39
47
  padding = '=' * padding_length
40
- Base64.urlsafe_decode64(s + padding)
48
+ input = s + padding
49
+ result = Base64.urlsafe_decode64(input)
50
+
51
+ # this bit is primarily for jruby which does a 'best effort' decode of
52
+ # whatever data it can if the input is invalid rather than raising an
53
+ # ArgumentError - as that could be a security issue we'll check that the
54
+ # result contains all the data that was in the input string
55
+ unless input.length == (((result.length - 1) / 3) * 4) + 4
56
+ raise ArgumentError, 'Invalid base64.'
57
+ end
58
+
59
+ result
41
60
  end
42
61
 
43
62
  end
@@ -1,4 +1,4 @@
1
1
  module Sandal
2
2
  # The semantic version of the library.
3
- VERSION = '0.2.0'
3
+ VERSION = '0.3.0'
4
4
  end