fernet 1.6 → 2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "spec/fernet-spec"]
2
+ path = spec/fernet-spec
3
+ url = git://github.com/kr/fernet-spec.git
data/.travis.yml CHANGED
@@ -3,7 +3,7 @@ language: ruby
3
3
  rvm:
4
4
  - "1.9.3"
5
5
  - "1.9.2"
6
- - rbx-19mode
7
- - "1.8.7"
6
+ - "2.0.0"
7
+ - "ruby-head"
8
8
 
9
9
  script: bundle exec rspec -b spec
data/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Fernet
2
2
 
3
3
  [![Build Status](https://secure.travis-ci.org/hgmnz/fernet.png)](http://travis-ci.org/hgmnz/fernet)
4
- [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/hgmnz/fernet)
4
+ [![Code Climate](https://codeclimate.com/github/hgmnz/fernet.png)](https://codeclimate.com/github/hgmnz/fernet)
5
5
 
6
6
  Fernet allows you to easily generate and verify **HMAC based authentication
7
7
  tokens** for issuing API requests between remote servers. It also **encrypts**
8
- data by default, so it can be used to transmit secure messages over the wire.
8
+ the message so it can be used to transmit secure data over the wire.
9
9
 
10
10
  ![Fernet](http://f.cl.ly/items/2d0P3d26271O3p2v253u/photo.JPG)
11
11
 
@@ -16,17 +16,9 @@ Fernet about it!
16
16
 
17
17
  ## Installation
18
18
 
19
- Add this line to your application's Gemfile:
20
-
21
- gem 'fernet'
22
-
23
- And then execute:
24
-
25
- $ bundle
26
-
27
- Or install it yourself as:
28
-
29
- $ gem install fernet
19
+ Fernet is distributed as [a rubygem](https://rubygems.org/gems/fernet), so
20
+ either add `gem 'fernet'` to your application's Gemfile or install it yourself
21
+ by running `gem install fernet`.
30
22
 
31
23
  ## Usage
32
24
 
@@ -36,53 +28,41 @@ You want to encode some data in the token as well, for example, an email
36
28
  address can be used to verify it on the other end.
37
29
 
38
30
  ```ruby
39
- token = Fernet.generate(secret) do |generator|
40
- generator.data = { email: 'harold@heroku.com' }
41
- end
31
+ token = Fernet.generate(secret, 'harold@heroku.com')
42
32
  ```
33
+
43
34
  On the server side, the receiver can use this token to verify whether it's
44
35
  legit:
45
36
 
46
37
  ```ruby
47
- verified = Fernet.verify(secret, token) do |verifier|
48
- verifier.data['email'] == 'harold@heroku.com'
38
+ verifier = Fernet.verifier(secret, token)
39
+ if verifier.valid?
40
+ operate_on(verifier.message) # the original, decrypted message
49
41
  end
50
42
  ```
51
43
 
52
- The `verified` variable will be true if:
44
+ The verifier is valid if:
53
45
 
54
- * The email encoded in the token data is `harold@heroku.com`
55
- * The token was generated in the last 60 seconds
46
+ * The token was generated in the last 60 seconds (or some configurable TTL)
56
47
  * The secret used to generate the token matches
57
48
 
58
49
  Otherwise, `verified` will be false, and you should deny the request with an
59
50
  HTTP 401, for example.
60
51
 
61
- The `Fernet.verify` method can be awkward if extracting the plain text data is
62
- required. For this case, a `verifier` can be requested that makes that
63
- use case more pleasent:
64
-
65
- ```ruby
66
- verifier = Fernet.verifier(secret, token)
67
- if verifier.valid? # signature valid, TTL verified
68
- operate_on(verifier.data) # the original, decrypted data
69
- end
70
- ```
71
-
72
52
  The specs
73
53
  ([spec/fernet_spec.rb](https://github.com/hgmnz/fernet/blob/master/spec/fernet_spec.rb))
74
54
  have more usage examples.
75
55
 
76
56
  ### Global configuration
77
57
 
78
- It's possible to configure fernet via the `Configuration` class. Put this in an initializer:
58
+ It's possible to configure fernet via the `Configuration` class. To do so, put
59
+ this in an initializer:
79
60
 
80
61
  ```ruby
81
62
  # default values shown here
82
63
  Fernet::Configuration.run do |config|
83
64
  config.enforce_ttl = true
84
65
  config.ttl = 60
85
- config.encrypt = true
86
66
  end
87
67
  ```
88
68
 
@@ -94,14 +74,42 @@ generate it using `/dev/random` in a *nix. To generate a base64-encoded 256 bit
94
74
 
95
75
  dd if=/dev/urandom bs=32 count=1 2>/dev/null | openssl base64
96
76
 
77
+ ### Ruby Compatibility
78
+
79
+ Fernet is compatible with Ruby 1.9 and above. It is tested on the rubies
80
+ available on this [Travis CI configuration
81
+ file](https://github.com/hgmnz/fernet/blob/master/.travis.yml)
82
+
97
83
  ### Attribution
98
84
 
99
85
  This library was largely made possible by [Mr. Tom
100
- Maher](http://twitter.com/#tmaher), who clearly articulated the mechanics
86
+ Maher](https://twitter.com/tmaher), who clearly articulated the mechanics
101
87
  behind this process, and further found ways to make it
102
88
  [more](https://github.com/hgmnz/fernet/commit/2bf0b4a66b49ef3fc92ef50708a2c8b401950fc2)
103
89
  [secure](https://github.com/hgmnz/fernet/commit/051161d0afb0b41480734d84bc824bdbc7f9c563).
104
90
 
91
+ Similarly, [Mr. Keith Rarick](https://twitter.com/krarick) who implemented a [Go
92
+ version](https://github.com/kr/fernet) and put together the [Fernet
93
+ spec](https://github.com/kr/fernet-spec) which is used by this project to
94
+ verify interoparability.
95
+
96
+ ### Contributing
97
+
98
+ Contributions are welcome via github pull requests.
99
+
100
+ To run the test suite:
101
+
102
+ * Clone the project
103
+ * Init submodules with `git submodule init && git submodule update`
104
+ * Run the suite: `bundle exec rspec spec`
105
+
106
+ Thanks to all [contributors](https://github.com/hgmnz/fernet/contributors).
107
+
108
+ ### Security disclosures
109
+
110
+ If you find a security issue with Fernet, please report it by emailing
111
+ the fernet security list: fernet-secure@googlegroups.com
112
+
105
113
  ## License
106
114
 
107
115
  Fernet is copyright (c) Harold Giménez and is released under the terms of the
data/fernet.gemspec CHANGED
@@ -4,7 +4,7 @@ require File.expand_path('../lib/fernet/version', __FILE__)
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ["Harold Giménez"]
6
6
  gem.email = ["harold.gimenez@gmail.com"]
7
- gem.description = %q{Delicious HMAC Digest(if) authentication and encryption}
7
+ gem.description = %q{Delicious HMAC Digest(if) authentication and AES-128-CBC encryption}
8
8
  gem.summary = %q{Easily generate and verify AES encrypted HMAC based authentication tokens}
9
9
  gem.homepage = ""
10
10
 
@@ -15,7 +15,6 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = Fernet::VERSION
17
17
 
18
- gem.add_dependency "yajl-ruby"
19
-
18
+ gem.add_runtime_dependency "valcro", "0.1"
20
19
  gem.add_development_dependency "rspec"
21
20
  end
data/lib/fernet.rb CHANGED
@@ -1,27 +1,21 @@
1
1
  require 'fernet/version'
2
+ require 'fernet/bit_packing'
3
+ require 'fernet/encryption'
4
+ require 'fernet/token'
2
5
  require 'fernet/generator'
3
6
  require 'fernet/verifier'
4
7
  require 'fernet/secret'
5
8
  require 'fernet/configuration'
6
9
 
7
- if RUBY_VERSION == '1.8.7'
8
- require 'shim/base64'
9
- end
10
-
11
10
  Fernet::Configuration.run
12
11
 
13
12
  module Fernet
14
- def self.generate(secret, encrypt = Configuration.encrypt, &block)
15
- Generator.new(secret, encrypt).generate(&block)
16
- end
17
-
18
- def self.verify(secret, token, encrypt = Configuration.encrypt, &block)
19
- Verifier.new(secret, encrypt).verify_token(token, &block)
13
+ def self.generate(secret, message = '', opts = {}, &block)
14
+ Generator.new(opts.merge({secret: secret, message: message})).
15
+ generate(&block)
20
16
  end
21
17
 
22
- def self.verifier(secret, token, encrypt = Configuration.encrypt)
23
- Verifier.new(secret, encrypt).tap do |v|
24
- v.verify_token(token)
25
- end
18
+ def self.verifier(secret, token, opts = {})
19
+ Verifier.new(opts.merge({secret: secret, token: token}))
26
20
  end
27
21
  end
@@ -0,0 +1,17 @@
1
+ module Fernet
2
+ module BitPacking
3
+ extend self
4
+
5
+ # N.B. Ruby 1.9.2 and below silently ignore endianness specifiers in
6
+ # packing/unpacking format directives; we work around it with this
7
+
8
+ def pack_int64_bigendian(value)
9
+ (0..7).map { |index| (value >> (index * 8)) & 0xFF }.reverse.map(&:chr).join
10
+ end
11
+
12
+ def unpack_int64_bigendian(bytes)
13
+ bytes.each_byte.to_a.reverse.each_with_index.
14
+ reduce(0) { |val, (byte, index)| val | (byte << (index * 8)) }
15
+ end
16
+ end
17
+ end
@@ -11,14 +11,9 @@ module Fernet
11
11
  # (an integer in seconds)
12
12
  attr_accessor :ttl
13
13
 
14
- # Whether to encrypt the payload
15
- # (true or false)
16
- attr_accessor :encrypt
17
-
18
14
  def self.run
19
15
  self.instance.enforce_ttl = true
20
16
  self.instance.ttl = 60
21
- self.instance.encrypt = true
22
17
  yield self.instance if block_given?
23
18
  end
24
19
 
@@ -0,0 +1,28 @@
1
+ require 'openssl'
2
+
3
+ module Fernet
4
+ module Encryption
5
+ AES_BLOCK_SIZE = 16.freeze
6
+
7
+ def self.encrypt(opts)
8
+ cipher = OpenSSL::Cipher.new('AES-128-CBC')
9
+ cipher.encrypt
10
+ iv = opts[:iv] || cipher.random_iv
11
+ cipher.iv = iv
12
+ cipher.key = opts[:key]
13
+ [cipher.update(opts[:message]) + cipher.final, iv]
14
+ end
15
+
16
+ def self.decrypt(opts)
17
+ decipher = OpenSSL::Cipher.new('AES-128-CBC')
18
+ decipher.decrypt
19
+ decipher.iv = opts[:iv]
20
+ decipher.key = opts[:key]
21
+ decipher.update(opts[:ciphertext]) + decipher.final
22
+ end
23
+
24
+ def self.hmac_digest(key, blob)
25
+ OpenSSL::HMAC.digest('sha256', key, blob)
26
+ end
27
+ end
28
+ end
@@ -1,71 +1,37 @@
1
+ #encoding UTF-8
1
2
  require 'base64'
2
- require 'yajl'
3
3
  require 'openssl'
4
4
  require 'date'
5
5
 
6
6
  module Fernet
7
7
  class Generator
8
- attr_accessor :data, :payload
8
+ attr_accessor :message
9
9
 
10
- def initialize(secret, encrypt)
11
- @secret = Secret.new(secret, encrypt)
12
- @encrypt = encrypt
13
- @payload = ''
14
- @data = {}
10
+ def initialize(opts)
11
+ @secret = opts.fetch(:secret)
12
+ @message = opts[:message]
13
+ @iv = opts[:iv]
14
+ @now = opts[:now]
15
15
  end
16
16
 
17
17
  def generate
18
18
  yield self if block_given?
19
- data.merge!(:issued_at => DateTime.now)
20
19
 
21
- if encrypt?
22
- iv = encrypt_data!
23
- @payload = "#{base64(data)}|#{base64(iv)}"
24
- else
25
- @payload = base64(Yajl::Encoder.encode(data))
26
- end
27
-
28
- mac = OpenSSL::HMAC.hexdigest('sha256', payload, signing_key)
29
- "#{payload}|#{mac}"
20
+ token = Token.generate(secret: @secret,
21
+ message: @message,
22
+ iv: @iv,
23
+ now: @now)
24
+ token.to_s
30
25
  end
31
26
 
32
27
  def inspect
33
- "#<Fernet::Generator @secret=[masked] @data=#{@data.inspect}>"
28
+ "#<Fernet::Generator @secret=[masked] @message=#{@message.inspect}>"
34
29
  end
35
30
  alias to_s inspect
36
31
 
37
- def data
38
- @data ||= {}
39
- end
40
-
41
- private
42
- attr_reader :secret
43
-
44
- def encrypt_data!
45
- cipher = OpenSSL::Cipher.new('AES-128-CBC')
46
- cipher.encrypt
47
- iv = cipher.random_iv
48
- cipher.iv = iv
49
- cipher.key = encryption_key
50
- @data = cipher.update(Yajl::Encoder.encode(data)) + cipher.final
51
- iv
32
+ def data=(message)
33
+ puts "[WARNING] 'data' is deprecated, use 'message' instead"
34
+ @message = message
52
35
  end
53
-
54
- def base64(chars)
55
- Base64.urlsafe_encode64(chars)
56
- end
57
-
58
- def encryption_key
59
- @secret.encryption_key
60
- end
61
-
62
- def signing_key
63
- @secret.signing_key
64
- end
65
-
66
- def encrypt?
67
- @encrypt
68
- end
69
-
70
36
  end
71
37
  end
data/lib/fernet/secret.rb CHANGED
@@ -1,20 +1,26 @@
1
+ require 'base64'
1
2
  module Fernet
2
3
  class Secret
3
- def initialize(secret, encrypt)
4
- @secret = secret
5
- @encrypt = encrypt
4
+ class InvalidSecret < RuntimeError; end
5
+
6
+ def initialize(secret)
7
+ @secret = Base64.urlsafe_decode64(secret)
8
+ unless @secret.bytesize == 32
9
+ raise InvalidSecret, "Secret must be 32 bytes, instead got #{@secret.bytesize}"
10
+ end
6
11
  end
7
12
 
8
13
  def encryption_key
9
- @secret.slice(@secret.size/2, @secret.size)
14
+ @secret.slice(16, 16)
10
15
  end
11
16
 
12
17
  def signing_key
13
- if @encrypt
14
- @secret.slice(0, @secret.size/2)
15
- else
16
- @secret
17
- end
18
+ @secret.slice(0, 16)
19
+ end
20
+
21
+ def to_s
22
+ "<Fernet::Secret [masked]>"
18
23
  end
24
+ alias to_s inspect
19
25
  end
20
26
  end
@@ -0,0 +1,165 @@
1
+ # encoding UTF-8
2
+ require 'base64'
3
+ require 'valcro'
4
+
5
+ module Fernet
6
+ class Token
7
+ include Valcro
8
+
9
+ class InvalidToken < StandardError; end
10
+
11
+ DEFAULT_VERSION = 0x80.freeze
12
+ MAX_CLOCK_SKEW = 60.freeze
13
+
14
+ def initialize(token, opts = {})
15
+ @token = token
16
+ @enforce_ttl = opts.fetch(:enforce_ttl) { Configuration.enforce_ttl }
17
+ @ttl = opts[:ttl] || Configuration.ttl
18
+ @now = opts[:now]
19
+ end
20
+
21
+ def to_s
22
+ @token
23
+ end
24
+
25
+ def secret=(secret)
26
+ @secret = Secret.new(secret)
27
+ end
28
+
29
+ def valid?
30
+ validate
31
+ super
32
+ end
33
+
34
+ def message
35
+ if valid?
36
+ begin
37
+ Encryption.decrypt(key: @secret.encryption_key,
38
+ ciphertext: encrypted_message,
39
+ iv: iv)
40
+ rescue OpenSSL::Cipher::CipherError
41
+ raise InvalidToken, "bad decrypt"
42
+ end
43
+ else
44
+ raise InvalidToken, error_messages
45
+ end
46
+ end
47
+
48
+ def self.generate(params)
49
+ unless params[:secret]
50
+ raise ArgumentError, 'Secret not provided'
51
+ end
52
+ secret = Secret.new(params[:secret])
53
+ encrypted_message, iv = Encryption.encrypt(key: secret.encryption_key,
54
+ message: params[:message],
55
+ iv: params[:iv])
56
+ issued_timestamp = (params[:now] || Time.now).to_i
57
+
58
+ payload = [DEFAULT_VERSION].pack("C") +
59
+ BitPacking.pack_int64_bigendian(issued_timestamp) +
60
+ iv +
61
+ encrypted_message
62
+ mac = OpenSSL::HMAC.digest('sha256', secret.signing_key, payload)
63
+ new(Base64.urlsafe_encode64(payload + mac))
64
+ end
65
+
66
+ private
67
+ def decoded_token
68
+ @decoded_token ||= Base64.urlsafe_decode64(@token)
69
+ end
70
+
71
+ def version
72
+ decoded_token.chr.unpack("C").first
73
+ end
74
+
75
+ def received_signature
76
+ decoded_token[(decoded_token.length - 32), 32]
77
+ end
78
+
79
+ def issued_timestamp
80
+ BitPacking.unpack_int64_bigendian(decoded_token[1, 8])
81
+ end
82
+
83
+ def iv
84
+ decoded_token[9, 16]
85
+ end
86
+
87
+ def encrypted_message
88
+ decoded_token[25..(decoded_token.length - 33)]
89
+ end
90
+
91
+ validate do
92
+ if valid_base64?
93
+ if unknown_token_version?
94
+ errors.add :version, "is unknown"
95
+ else
96
+ unless signatures_match?
97
+ errors.add :signature, "does not match"
98
+ end
99
+ if enforce_ttl? && !issued_recent_enough?
100
+ errors.add :issued_timestamp, "is too far in the past: token expired"
101
+ end
102
+ if unacceptable_clock_slew?
103
+ errors.add :issued_timestamp, "is too far in the future"
104
+ end
105
+ unless ciphertext_multiple_of_block_size?
106
+ errors.add :ciphertext, "is not a multiple of block size"
107
+ end
108
+ end
109
+ else
110
+ errors.add(:token, "invalid base64")
111
+ end
112
+ end
113
+
114
+ def regenerated_mac
115
+ Encryption.hmac_digest(@secret.signing_key, signing_blob)
116
+ end
117
+
118
+ def signing_blob
119
+ [version].pack("C") +
120
+ BitPacking.pack_int64_bigendian(issued_timestamp) +
121
+ iv +
122
+ encrypted_message
123
+ end
124
+
125
+ def valid_base64?
126
+ decoded_token
127
+ true
128
+ rescue ArgumentError
129
+ false
130
+ end
131
+
132
+ def signatures_match?
133
+ regenerated_bytes = regenerated_mac.bytes.to_a
134
+ received_bytes = received_signature.bytes.to_a
135
+ received_bytes.inject(0) do |accum, byte|
136
+ accum |= byte ^ regenerated_bytes.shift
137
+ end.zero?
138
+ end
139
+
140
+ def issued_recent_enough?
141
+ good_till = issued_timestamp + @ttl
142
+ good_till >= now.to_i
143
+ end
144
+
145
+ def unacceptable_clock_slew?
146
+ issued_timestamp >= (now.to_i + MAX_CLOCK_SKEW)
147
+ end
148
+
149
+ def ciphertext_multiple_of_block_size?
150
+ (encrypted_message.size % Encryption::AES_BLOCK_SIZE).zero?
151
+ end
152
+
153
+ def unknown_token_version?
154
+ DEFAULT_VERSION != version
155
+ end
156
+
157
+ def enforce_ttl?
158
+ @enforce_ttl
159
+ end
160
+
161
+ def now
162
+ @now || Time.now
163
+ end
164
+ end
165
+ end
@@ -1,68 +1,60 @@
1
+ #encoding UTF-8
1
2
  require 'base64'
2
- require 'yajl'
3
3
  require 'openssl'
4
4
  require 'date'
5
5
 
6
6
  module Fernet
7
7
  class Verifier
8
- attr_reader :token, :data
8
+ class UnknownTokenVersion < RuntimeError; end
9
+
10
+ attr_reader :token
9
11
  attr_accessor :ttl, :enforce_ttl
10
12
 
11
- def initialize(secret, decrypt)
12
- @secret = Secret.new(secret, decrypt)
13
- @decrypt = decrypt
14
- @ttl = Configuration.ttl
15
- @enforce_ttl = Configuration.enforce_ttl
13
+ def initialize(opts = {})
14
+ enforce_ttl = opts.has_key?(:enforce_ttl) ? opts[:enforce_ttl] : Configuration.enforce_ttl
15
+ @token = Token.new(opts.fetch(:token),
16
+ enforce_ttl: enforce_ttl,
17
+ ttl: opts[:ttl],
18
+ now: opts[:now])
19
+ @token.secret = opts.fetch(:secret)
16
20
  end
17
21
 
18
- def verify_token(token)
19
- @token = token
20
- deconstruct
21
-
22
- if block_given?
23
- custom_verification = yield self
24
- else
25
- custom_verification = true
26
- end
22
+ def valid?
23
+ @token.valid?
24
+ end
27
25
 
28
- @valid = signatures_match? && token_recent_enough? && custom_verification
26
+ def message
27
+ @token.message
29
28
  end
30
29
 
31
- def valid?
32
- @valid
30
+ def data
31
+ puts "[WARNING] data is deprected. Use message instead"
32
+ message
33
33
  end
34
34
 
35
35
  def inspect
36
- "#<Fernet::Verifier @secret=[masked] @token=#{@token} @data=#{@data.inspect} @ttl=#{@ttl}>"
36
+ "#<Fernet::Verifier @secret=[masked] @token=#{@token} @message=#{@message.inspect} @ttl=#{@ttl} @enforce_ttl=#{@enforce_ttl}>"
37
37
  end
38
38
  alias to_s inspect
39
39
 
40
40
  private
41
- attr_reader :secret
42
-
43
- def deconstruct
44
- parts = @token.split('|')
45
- if decrypt?
46
- encrypted_data, iv, @received_signature = *parts
47
- @data = Yajl::Parser.parse(decrypt!(encrypted_data, Base64.urlsafe_decode64(iv)))
48
- signing_blob = "#{encrypted_data}|#{iv}"
49
- else
50
- encoded_data, @received_signature = *parts
51
- signing_blob = encoded_data
52
- @data = Yajl::Parser.parse(Base64.urlsafe_decode64(encoded_data))
53
- end
54
- @regenerated_mac = OpenSSL::HMAC.hexdigest('sha256', signing_blob, signing_key)
41
+ def must_verify?
42
+ @must_verify || @valid.nil?
55
43
  end
56
44
 
57
45
  def token_recent_enough?
58
46
  if enforce_ttl?
59
- good_till = DateTime.parse(data['issued_at']) + (ttl.to_f / 24 / 60 / 60)
60
- good_till > now
47
+ good_till = @issued_at + (ttl.to_f / 24 / 60 / 60)
48
+ (good_till.to_i >= now.to_i) && acceptable_clock_skew?
61
49
  else
62
50
  true
63
51
  end
64
52
  end
65
53
 
54
+ def acceptable_clock_skew?
55
+ @issued_at < (now + MAX_CLOCK_SKEW)
56
+ end
57
+
66
58
  def signatures_match?
67
59
  regenerated_bytes = @regenerated_mac.bytes.to_a
68
60
  received_bytes = @received_signature.bytes.to_a
@@ -71,32 +63,12 @@ module Fernet
71
63
  end.zero?
72
64
  end
73
65
 
74
- def decrypt!(encrypted_data, iv)
75
- decipher = OpenSSL::Cipher.new('AES-128-CBC')
76
- decipher.decrypt
77
- decipher.iv = iv
78
- decipher.key = encryption_key
79
- decipher.update(Base64.urlsafe_decode64(encrypted_data)) + decipher.final
80
- end
81
-
82
- def encryption_key
83
- @secret.encryption_key
84
- end
85
-
86
- def signing_key
87
- @secret.signing_key
88
- end
89
-
90
- def decrypt?
91
- @decrypt
92
- end
93
-
94
66
  def enforce_ttl?
95
67
  @enforce_ttl
96
68
  end
97
69
 
98
70
  def now
99
- DateTime.now
71
+ @now ||= Time.now
100
72
  end
101
73
  end
102
74
  end
@@ -1,3 +1,3 @@
1
1
  module Fernet
2
- VERSION = "1.6"
2
+ VERSION = "2.0.rc1"
3
3
  end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+ require 'fernet'
3
+ require 'json'
4
+ require 'base64'
5
+
6
+ describe Fernet::Generator do
7
+ it 'generates tokens according to the spec' do
8
+ path = File.expand_path(
9
+ './../fernet-spec/generate.json', File.dirname(__FILE__)
10
+ )
11
+ generate_json = JSON.parse(File.read(path))
12
+ generate_json.each do |test_data|
13
+ message = test_data['src']
14
+ iv = test_data['iv'].pack("C*")
15
+ secret = test_data['secret']
16
+ now = DateTime.parse(test_data['now']).to_time
17
+ expected_token = test_data['token']
18
+
19
+ generator = Fernet::Generator.new(secret: secret,
20
+ message: message,
21
+ iv: iv,
22
+ now: now)
23
+
24
+ expect(generator.generate).to eq(expected_token)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+ require 'fernet'
3
+ require 'json'
4
+ require 'base64'
5
+
6
+ describe Fernet::Verifier do
7
+ it 'verifies tokens according to the spec' do
8
+ path = File.expand_path(
9
+ './../fernet-spec/verify.json', File.dirname(__FILE__)
10
+ )
11
+ verify_json = JSON.parse(File.read(path))
12
+
13
+ verify_json.each do |test_data|
14
+ token = test_data['token']
15
+ ttl = test_data['ttl_sec']
16
+ now = DateTime.parse(test_data['now']).to_time
17
+ secret = test_data['secret']
18
+ message = test_data['src']
19
+
20
+ verifier = Fernet::Verifier.new(token: token,
21
+ secret: secret,
22
+ now: now,
23
+ ttl: ttl)
24
+ expect(
25
+ verifier.message
26
+ ).to eq(message)
27
+ end
28
+ end
29
+
30
+ context 'invalid tokens' do
31
+ path = File.expand_path(
32
+ './../fernet-spec/invalid.json', File.dirname(__FILE__)
33
+ )
34
+ invalid_json = JSON.parse(File.read(path))
35
+ invalid_json.each do |test_data|
36
+ it "detects #{test_data['desc']}" do
37
+ token = test_data['token']
38
+ ttl = test_data['ttl_sec']
39
+ now = DateTime.parse(test_data['now']).to_time
40
+ secret = test_data['secret']
41
+
42
+ verifier = Fernet::Verifier.new(token: token,
43
+ secret: secret,
44
+ now: now,
45
+ ttl: ttl)
46
+
47
+ expect { verifier.message }.to raise_error(Fernet::Token::InvalidToken)
48
+ end
49
+ end
50
+ end
51
+
52
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'fernet/bit_packing'
3
+
4
+ describe Fernet::BitPacking do
5
+ VALUE_TO_BYTES = {
6
+ 0x0000000000000000 => [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ],
7
+ 0x00000000000000FF => [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF ],
8
+ 0x000000FF00000000 => [ 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00 ],
9
+ 0x00000000FF000000 => [ 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00 ],
10
+ 0xFF00000000000000 => [ 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ],
11
+ 0xFFFFFFFFFFFFFFFF => [ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF ]
12
+ }
13
+
14
+ def self.pretty(bytea)
15
+ "0x#{bytea.map { |b| sprintf("%.2x", b) }.join}"
16
+ end
17
+
18
+ def self.bytestr(bytea)
19
+ bytea.map(&:chr).join
20
+ end
21
+
22
+ VALUE_TO_BYTES.each do |value, bytes|
23
+ pretty_bytes = pretty(bytes).rjust(20)
24
+ pretty_val = value.to_s.rjust(20)
25
+ bytestr = bytestr(bytes)
26
+ it "encodes #{pretty_val} to #{pretty_bytes}" do
27
+ expect(Fernet::BitPacking.pack_int64_bigendian(value)).to eq(bytestr)
28
+ end
29
+
30
+ # N.B.: we have two extra spaces in the spec description for
31
+ # aligned formatting w.r.t. the 'encode' specs
32
+ it "decodes #{pretty_bytes} to #{pretty_val}" do
33
+ expect(Fernet::BitPacking.unpack_int64_bigendian(bytestr)).to eq(value)
34
+ end
35
+ end
36
+ end
data/spec/fernet_spec.rb CHANGED
@@ -4,90 +4,55 @@ require 'fernet'
4
4
  describe Fernet do
5
5
  after { Fernet::Configuration.run }
6
6
 
7
- let(:token_data) do
8
- { :email => 'harold@heroku.com', :id => '123', :arbitrary => 'data' }
9
- end
10
-
11
7
  let(:secret) { 'JrdICDH6x3M7duQeM8dJEMK4Y5TkBIsYDw1lPy35RiY=' }
12
8
  let(:bad_secret) { 'badICDH6x3M7duQeM8dJEMK4Y5TkBIsYDw1lPy35RiY=' }
13
9
 
14
10
  it 'can verify tokens it generates' do
15
11
  token = Fernet.generate(secret) do |generator|
16
- generator.data = token_data
12
+ generator.message = 'harold@heroku.com'
17
13
  end
18
14
 
19
- expect(
20
- Fernet.verify(secret, token) do |verifier|
21
- verifier.data['email'] == 'harold@heroku.com'
22
- end
23
- ).to be_true
15
+ verifier = Fernet.verifier(secret, token)
16
+ expect(verifier).to be_valid
17
+ expect(verifier.message).to eq('harold@heroku.com')
24
18
  end
25
19
 
26
- it 'fails with a bad secret' do
27
- token = Fernet.generate(secret) do |generator|
28
- generator.data = token_data
29
- end
30
-
31
- expect(
32
- Fernet.verify(bad_secret, token) do |verifier|
33
- verifier.data['email'] == 'harold@heroku.com'
34
- end
35
- ).to be_false
20
+ it 'can generate tokens without a block' do
21
+ token = Fernet.generate(secret, 'harold@heroku.com')
22
+ verifier = Fernet.verifier(secret, token)
23
+ expect(verifier).to be_valid
24
+ expect(verifier.message).to eq('harold@heroku.com')
36
25
  end
37
26
 
38
- it 'fails with a bad custom verification' do
27
+ it 'fails with a bad secret' do
39
28
  token = Fernet.generate(secret) do |generator|
40
- generator.data = { :email => 'harold@heroku.com' }
29
+ generator.message = 'harold@heroku.com'
41
30
  end
42
31
 
43
- expect(
44
- Fernet.verify(secret, token) do |verifier|
45
- verifier.data['email'] == 'lol@heroku.com'
46
- end
47
- ).to be_false
32
+ verifier = Fernet.verifier(bad_secret, token)
33
+ expect(verifier.valid?).to be_false
34
+ expect {
35
+ verifier.message
36
+ }.to raise_error
48
37
  end
49
38
 
50
39
  it 'fails if the token is too old' do
51
- token = Fernet.generate(secret) do |generator|
52
- generator.data = token_data
53
- end
40
+ token = Fernet.generate(secret, 'harold@heroku.com', now: (Time.now - 61))
54
41
 
55
- expect(
56
- Fernet.verify(secret, token) do |verifier|
57
- verifier.ttl = 1
58
-
59
- def verifier.now
60
- now = DateTime.now
61
- DateTime.new(now.year, now.month, now.day, now.hour,
62
- now.min, now.sec + 2, now.offset)
63
- end
64
- true
65
- end
66
- ).to be_false
67
- end
68
-
69
- it 'verifies without a custom verification' do
70
- token = Fernet.generate(secret) do |generator|
71
- generator.data = token_data
72
- end
73
-
74
- expect(Fernet.verify(secret, token)).to be_true
42
+ verifier = Fernet.verifier(secret, token)
43
+ expect(verifier.valid?).to be_false
75
44
  end
76
45
 
77
46
  it 'can ignore TTL enforcement' do
78
- token = Fernet.generate(secret) do |generator|
79
- generator.data = token_data
47
+ Fernet::Configuration.run do |config|
48
+ config.enforce_ttl = true
80
49
  end
81
50
 
82
- expect(
83
- Fernet.verify(secret, token) do |verifier|
84
- def verifier.now
85
- Time.now + 99999999999
86
- end
87
- verifier.enforce_ttl = false
88
- true
89
- end
90
- ).to be_true
51
+ token = Fernet.generate(secret, 'harold@heroku.com')
52
+
53
+ verifier = Fernet.verifier(secret, token, enforce_ttl: false,
54
+ now: Time.now + 9999)
55
+ expect(verifier.valid?).to be_true
91
56
  end
92
57
 
93
58
  it 'can ignore TTL enforcement via global config' do
@@ -95,70 +60,29 @@ describe Fernet do
95
60
  config.enforce_ttl = false
96
61
  end
97
62
 
98
- token = Fernet.generate(secret) do |generator|
99
- generator.data = token_data
100
- end
101
-
102
- expect(
103
- Fernet.verify(secret, token) do |verifier|
104
- def verifier.now
105
- Time.now + 99999999999
106
- end
107
- true
108
- end
109
- ).to be_true
110
- end
111
-
112
- it 'generates without custom data' do
113
- token = Fernet.generate(secret)
114
-
115
- expect(Fernet.verify(secret, token)).to be_true
116
- end
63
+ token = Fernet.generate(secret, 'harold@heroku.com')
117
64
 
118
- it 'can encrypt the payload' do
119
- token = Fernet.generate(secret, true) do |generator|
120
- generator.data['password'] = 'password1'
121
- end
122
-
123
- expect(Base64.decode64(token)).not_to match /password1/
124
-
125
- Fernet.verify(secret, token) do |verifier|
126
- expect(verifier.data['password']).to eq('password1')
127
- end
65
+ verifier = Fernet.verifier(secret, token, now: Time.now + 999999)
66
+ expect(verifier.valid?).to be_true
128
67
  end
129
68
 
130
- it 'does not encrypt when asked nicely' do
131
- token = Fernet.generate(secret, false) do |generator|
132
- generator.data['password'] = 'password1'
133
- end
134
-
135
- expect(Base64.decode64(token)).to match /password1/
69
+ it 'does not send the message in plain text' do
70
+ token = Fernet.generate(secret, 'password1')
136
71
 
137
- Fernet.verify(secret, token, false) do |verifier|
138
- expect(verifier.data['password']).to eq('password1')
139
- end
72
+ expect(Base64.urlsafe_decode64(token)).not_to match /password1/
140
73
  end
141
74
 
142
- it 'can disable encryption via global configuration' do
143
- Fernet::Configuration.run { |c| c.encrypt = false }
144
- token = Fernet.generate(secret) do |generator|
145
- generator.data['password'] = 'password1'
146
- end
147
-
148
- expect(Base64.decode64(token)).to match /password1/
149
-
150
- Fernet.verify(secret, token) do |verifier|
151
- expect(verifier.data['password']).to eq('password1')
75
+ it 'allows overriding enforce_ttl on a verifier' do
76
+ Fernet::Configuration.run do |config|
77
+ config.enforce_ttl = true
78
+ config.ttl = 0
152
79
  end
153
- end
154
-
155
- it 'returns the unencrypted message upon verify' do
156
80
  token = Fernet.generate(secret) do |generator|
157
- generator.data['password'] = 'password1'
81
+ generator.message = 'password1'
158
82
  end
159
-
160
83
  verifier = Fernet.verifier(secret, token)
84
+ verifier.enforce_ttl = false
161
85
  expect(verifier.valid?).to be_true
162
- expect(verifier.data['password']).to eq('password1')
86
+ expect(verifier.message).to eq('password1')
163
87
  end
164
88
  end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+ require 'fernet/secret'
3
+
4
+ describe Fernet::Secret do
5
+ it "expects base64 encoded 32 byte strings" do
6
+ secret = Base64.urlsafe_encode64("A"*32)
7
+ expect do
8
+ Fernet::Secret.new(secret)
9
+ end.to_not raise_error
10
+ end
11
+
12
+ it "extracts encryption and signing keys" do
13
+ secret = Base64.urlsafe_encode64("A"*16 + "B"*16)
14
+ fernet_secret = Fernet::Secret.new(secret)
15
+ expect(
16
+ fernet_secret.signing_key
17
+ ).to eq("A"*16)
18
+
19
+ expect(
20
+ fernet_secret.encryption_key
21
+ ).to eq("B"*16)
22
+ end
23
+
24
+ it "fails loudly when an invalid secret is provided" do
25
+ secret = Base64.urlsafe_encode64("bad")
26
+ expect do
27
+ Fernet::Secret.new(secret)
28
+ end.to raise_error(Fernet::Secret::InvalidSecret)
29
+ end
30
+ end
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+ require 'fernet'
3
+ require 'json'
4
+
5
+ describe Fernet::Token, 'validation' do
6
+ let(:secret) { 'odN/0Yu+Pwp3oIvvG8OiE5w4LsLrqfWYRb3knQtSyKI=' }
7
+ it 'is invalid with a bad MAC signature' do
8
+ generated = Fernet::Token.generate(secret: secret,
9
+ message: 'hello')
10
+
11
+ bogus_hmac = "1" * 32
12
+ Fernet::Encryption.stub(hmac_digest: bogus_hmac)
13
+
14
+ token = Fernet::Token.new(generated.to_s)
15
+ token.secret = secret
16
+
17
+ expect(token.valid?).to be_false
18
+ expect(token.errors[:signature]).to include("does not match")
19
+ end
20
+
21
+ it 'is invalid if too old' do
22
+ generated = Fernet::Token.generate(secret: secret,
23
+ message: 'hello',
24
+ now: Time.now - 61)
25
+ token = Fernet::Token.new(generated.to_s, enforce_ttl: true,
26
+ ttl: 60)
27
+ token.secret = secret
28
+
29
+ expect(token.valid?).to be_false
30
+ expect(token.errors[:issued_timestamp]).to include("is too far in the past: token expired")
31
+ end
32
+
33
+ it 'is invalid with a large clock skew' do
34
+ generated = Fernet::Token.generate(secret: secret,
35
+ message: 'hello',
36
+ now: Time.at(Time.now.to_i + 61))
37
+ token = Fernet::Token.new(generated.to_s)
38
+ token.secret = secret
39
+
40
+ expect(token.valid?).to be_false
41
+ expect(token.errors[:issued_timestamp]).to include("is too far in the future")
42
+ end
43
+
44
+ it 'is invalid with bad base64' do
45
+ token = Fernet::Token.new('bad')
46
+ token.secret = secret
47
+
48
+ expect(token.valid?).to be_false
49
+ expect(token.errors[:token]).to include("invalid base64")
50
+ end
51
+
52
+ it 'is invalid with an unknown token version' do
53
+ token = Fernet::Token.new(Base64.urlsafe_encode64("xxxxxx"))
54
+
55
+ expect(token.valid?).to be_false
56
+ expect(token.errors[:version]).to include("is unknown")
57
+ end
58
+ end
59
+
60
+ describe Fernet::Token, 'message' do
61
+ let(:secret) { 'odN/0Yu+Pwp3oIvvG8OiE5w4LsLrqfWYRb3knQtSyKI=' }
62
+ it 'refuses to decrypt if invalid' do
63
+ generated = Fernet::Token.generate(secret: secret,
64
+ message: 'hello',
65
+ now: Time.now + 61)
66
+ token = Fernet::Token.new(generated.to_s)
67
+ token.secret = secret
68
+
69
+ !token.valid? or raise "invalid token"
70
+
71
+ expect {
72
+ token.message
73
+ }.to raise_error Fernet::Token::InvalidToken,
74
+ /issued_timestamp is too far in the future/
75
+ end
76
+
77
+ it 'gives back the original message in plain text' do
78
+ token = Fernet::Token.generate(secret: secret,
79
+ message: 'hello')
80
+ token.secret = secret
81
+ token.valid? or raise "invalid token"
82
+
83
+ expect(token.message).to eq('hello')
84
+ end
85
+ end
metadata CHANGED
@@ -1,30 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fernet
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.6'
5
- prerelease:
4
+ version: 2.0.rc1
5
+ prerelease: 4
6
6
  platform: ruby
7
7
  authors:
8
8
  - Harold Giménez
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-26 00:00:00.000000000 Z
12
+ date: 2013-08-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: yajl-ruby
16
- requirement: &70306497693420 !ruby/object:Gem::Requirement
15
+ name: valcro
16
+ requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
- - - ! '>='
19
+ - - '='
20
20
  - !ruby/object:Gem::Version
21
- version: '0'
21
+ version: '0.1'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70306497693420
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: '0.1'
25
30
  - !ruby/object:Gem::Dependency
26
31
  name: rspec
27
- requirement: &70306497692980 !ruby/object:Gem::Requirement
32
+ requirement: !ruby/object:Gem::Requirement
28
33
  none: false
29
34
  requirements:
30
35
  - - ! '>='
@@ -32,8 +37,13 @@ dependencies:
32
37
  version: '0'
33
38
  type: :development
34
39
  prerelease: false
35
- version_requirements: *70306497692980
36
- description: Delicious HMAC Digest(if) authentication and encryption
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Delicious HMAC Digest(if) authentication and AES-128-CBC encryption
37
47
  email:
38
48
  - harold.gimenez@gmail.com
39
49
  executables: []
@@ -41,6 +51,7 @@ extensions: []
41
51
  extra_rdoc_files: []
42
52
  files:
43
53
  - .gitignore
54
+ - .gitmodules
44
55
  - .rspec
45
56
  - .travis.yml
46
57
  - Gemfile
@@ -49,14 +60,21 @@ files:
49
60
  - Rakefile
50
61
  - fernet.gemspec
51
62
  - lib/fernet.rb
63
+ - lib/fernet/bit_packing.rb
52
64
  - lib/fernet/configuration.rb
65
+ - lib/fernet/encryption.rb
53
66
  - lib/fernet/generator.rb
54
67
  - lib/fernet/secret.rb
68
+ - lib/fernet/token.rb
55
69
  - lib/fernet/verifier.rb
56
70
  - lib/fernet/version.rb
57
- - lib/shim/base64.rb
71
+ - spec/acceptance/generate_spec.rb
72
+ - spec/acceptance/verify_spec.rb
73
+ - spec/bit_packing_spec.rb
58
74
  - spec/fernet_spec.rb
75
+ - spec/secret_spec.rb
59
76
  - spec/spec_helper.rb
77
+ - spec/token_spec.rb
60
78
  homepage: ''
61
79
  licenses: []
62
80
  post_install_message:
@@ -72,15 +90,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
72
90
  required_rubygems_version: !ruby/object:Gem::Requirement
73
91
  none: false
74
92
  requirements:
75
- - - ! '>='
93
+ - - ! '>'
76
94
  - !ruby/object:Gem::Version
77
- version: '0'
95
+ version: 1.3.1
78
96
  requirements: []
79
97
  rubyforge_project:
80
- rubygems_version: 1.8.10
98
+ rubygems_version: 1.8.23
81
99
  signing_key:
82
100
  specification_version: 3
83
101
  summary: Easily generate and verify AES encrypted HMAC based authentication tokens
84
102
  test_files:
103
+ - spec/acceptance/generate_spec.rb
104
+ - spec/acceptance/verify_spec.rb
105
+ - spec/bit_packing_spec.rb
85
106
  - spec/fernet_spec.rb
107
+ - spec/secret_spec.rb
86
108
  - spec/spec_helper.rb
109
+ - spec/token_spec.rb
data/lib/shim/base64.rb DELETED
@@ -1,21 +0,0 @@
1
- Base64.class_eval do
2
- def strict_encode64(bin)
3
- encode64(bin).tr("\n",'')
4
- end
5
-
6
- def strict_decode64(str)
7
- unless str.include?("\n")
8
- decode64(str)
9
- else
10
- raise(ArgumentError,"invalid base64")
11
- end
12
- end
13
-
14
- def urlsafe_encode64(bin)
15
- strict_encode64(bin).tr("+/", "-_")
16
- end
17
-
18
- def urlsafe_decode64(str)
19
- strict_decode64(str.tr("-_", "+/"))
20
- end
21
- end