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 +3 -0
- data/.travis.yml +2 -2
- data/README.md +43 -35
- data/fernet.gemspec +2 -3
- data/lib/fernet.rb +8 -14
- data/lib/fernet/bit_packing.rb +17 -0
- data/lib/fernet/configuration.rb +0 -5
- data/lib/fernet/encryption.rb +28 -0
- data/lib/fernet/generator.rb +16 -50
- data/lib/fernet/secret.rb +15 -9
- data/lib/fernet/token.rb +165 -0
- data/lib/fernet/verifier.rb +29 -57
- data/lib/fernet/version.rb +1 -1
- data/spec/acceptance/generate_spec.rb +27 -0
- data/spec/acceptance/verify_spec.rb +52 -0
- data/spec/bit_packing_spec.rb +36 -0
- data/spec/fernet_spec.rb +39 -115
- data/spec/secret_spec.rb +30 -0
- data/spec/token_spec.rb +85 -0
- metadata +38 -15
- data/lib/shim/base64.rb +0 -21
data/.gitmodules
ADDED
data/.travis.yml
CHANGED
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/
|
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
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
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)
|
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
|
-
|
48
|
-
|
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
|
44
|
+
The verifier is valid if:
|
53
45
|
|
54
|
-
* The
|
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.
|
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](
|
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.
|
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,
|
15
|
-
Generator.new(secret,
|
16
|
-
|
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,
|
23
|
-
Verifier.new(secret,
|
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
|
data/lib/fernet/configuration.rb
CHANGED
@@ -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
|
data/lib/fernet/generator.rb
CHANGED
@@ -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 :
|
8
|
+
attr_accessor :message
|
9
9
|
|
10
|
-
def initialize(
|
11
|
-
@secret =
|
12
|
-
@
|
13
|
-
@
|
14
|
-
@
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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] @
|
28
|
+
"#<Fernet::Generator @secret=[masked] @message=#{@message.inspect}>"
|
34
29
|
end
|
35
30
|
alias to_s inspect
|
36
31
|
|
37
|
-
def data
|
38
|
-
|
39
|
-
|
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
|
-
|
4
|
-
|
5
|
-
|
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(
|
14
|
+
@secret.slice(16, 16)
|
10
15
|
end
|
11
16
|
|
12
17
|
def signing_key
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
data/lib/fernet/token.rb
ADDED
@@ -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
|
data/lib/fernet/verifier.rb
CHANGED
@@ -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
|
-
|
8
|
+
class UnknownTokenVersion < RuntimeError; end
|
9
|
+
|
10
|
+
attr_reader :token
|
9
11
|
attr_accessor :ttl, :enforce_ttl
|
10
12
|
|
11
|
-
def initialize(
|
12
|
-
|
13
|
-
@
|
14
|
-
|
15
|
-
|
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
|
19
|
-
@token
|
20
|
-
|
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
|
-
|
26
|
+
def message
|
27
|
+
@token.message
|
29
28
|
end
|
30
29
|
|
31
|
-
def
|
32
|
-
|
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} @
|
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
|
-
|
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 =
|
60
|
-
good_till
|
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
|
-
|
71
|
+
@now ||= Time.now
|
100
72
|
end
|
101
73
|
end
|
102
74
|
end
|
data/lib/fernet/version.rb
CHANGED
@@ -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.
|
12
|
+
generator.message = 'harold@heroku.com'
|
17
13
|
end
|
18
14
|
|
19
|
-
|
20
|
-
|
21
|
-
|
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 '
|
27
|
-
token = Fernet.generate(secret)
|
28
|
-
|
29
|
-
|
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
|
27
|
+
it 'fails with a bad secret' do
|
39
28
|
token = Fernet.generate(secret) do |generator|
|
40
|
-
generator.
|
29
|
+
generator.message = 'harold@heroku.com'
|
41
30
|
end
|
42
31
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
52
|
-
generator.data = token_data
|
53
|
-
end
|
40
|
+
token = Fernet.generate(secret, 'harold@heroku.com', now: (Time.now - 61))
|
54
41
|
|
55
|
-
|
56
|
-
|
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
|
-
|
79
|
-
|
47
|
+
Fernet::Configuration.run do |config|
|
48
|
+
config.enforce_ttl = true
|
80
49
|
end
|
81
50
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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)
|
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
|
-
|
119
|
-
|
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
|
131
|
-
token = Fernet.generate(secret,
|
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
|
-
|
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 '
|
143
|
-
Fernet::Configuration.run
|
144
|
-
|
145
|
-
|
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.
|
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.
|
86
|
+
expect(verifier.message).to eq('password1')
|
163
87
|
end
|
164
88
|
end
|
data/spec/secret_spec.rb
ADDED
@@ -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
|
data/spec/token_spec.rb
ADDED
@@ -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:
|
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:
|
12
|
+
date: 2013-08-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
16
|
-
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:
|
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:
|
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:
|
36
|
-
|
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
|
-
-
|
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:
|
95
|
+
version: 1.3.1
|
78
96
|
requirements: []
|
79
97
|
rubyforge_project:
|
80
|
-
rubygems_version: 1.8.
|
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
|