fernet 1.6 → 2.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
[](http://travis-ci.org/hgmnz/fernet)
|
4
|
-
[](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
|

|
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
|