mortal-token 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +42 -36
- data/lib/mortal-token.rb +3 -1
- data/lib/mortal-token/configuration.rb +52 -0
- data/lib/mortal-token/mortal-token.rb +16 -65
- data/lib/mortal-token/token.rb +66 -0
- data/lib/mortal-token/version.rb +1 -1
- metadata +7 -6
- data/lib/mortal-token/defaults.rb +0 -10
data/README.rdoc
CHANGED
@@ -3,33 +3,38 @@
|
|
3
3
|
MortalToken is a library for creating tokens that self-destruct after a specified time. No need to store and look up the
|
4
4
|
token; it is self-verifying and self-expiring.
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
The default lifespan is two of your Earth days. This does *not* mean "48 hours from when it was created". It means that
|
7
|
+
the key will be valid throughout "today" and "tomorrow". If the lifespan were one day, a key created at 23:59 would expire
|
8
|
+
in one minute at 00:00.
|
9
|
+
|
10
|
+
My original use case was login auth tokens for some simple Sinatra apps. I can also see it being useful for API auth.
|
11
|
+
Of course there are potential uses outside of HTTP, too.
|
10
12
|
|
11
13
|
Steps:
|
12
14
|
|
13
15
|
1. Generate a new token
|
14
16
|
2. Give the client the resulting hash and salt
|
15
|
-
3.
|
17
|
+
3. Time passes
|
16
18
|
4. Receive a hash and salt from client
|
17
|
-
5. Reconstitute the token from the salt, and test if the hashes match
|
19
|
+
5. Reconstitute the token from the salt, and test if the hashes match. If not, the token has expired (or was forged).
|
18
20
|
|
19
21
|
Read the full documentation at {jordanhollinger.com/docs/mortal-token/}[http://jordanhollinger.com/docs/mortal-token/].
|
20
22
|
|
21
|
-
==
|
23
|
+
== Warning
|
24
|
+
|
25
|
+
This is, at best, alpha-quality software. It "works" in that it doesn't usually blow up, but it is not guaranteed to be "right".
|
26
|
+
Don't use it in production, or at least not in any important production. May contain traces of peanut oil.
|
22
27
|
|
23
|
-
|
24
|
-
pull requests are welcomed.
|
28
|
+
== Install
|
25
29
|
|
26
|
-
|
30
|
+
[sudo] gem install mortal-token
|
27
31
|
|
28
|
-
|
32
|
+
Or add it to your Gemfile
|
33
|
+
gem "mortal-token"
|
29
34
|
|
30
|
-
|
31
|
-
anytime you want a self-contained, self-expiring token (w/ salt).
|
35
|
+
== Example with Sinatra
|
32
36
|
|
37
|
+
require 'sinatra'
|
33
38
|
require 'mortal-token'
|
34
39
|
|
35
40
|
# You MUST set a secret key! Otherwise, anyone who looks at the source code will be able to forge tokens.
|
@@ -37,50 +42,51 @@ anytime you want a self-contained, self-expiring token (w/ salt).
|
|
37
42
|
|
38
43
|
post '/login' do
|
39
44
|
if login_ok?
|
40
|
-
# Create a brand new token. Store the resulting hash and salt.
|
41
45
|
token = MortalToken.new
|
42
46
|
session[:token] = token.hash
|
43
47
|
session[:salt] = token.salt
|
44
|
-
|
45
48
|
redirect '/secret'
|
46
49
|
end
|
47
50
|
end
|
48
51
|
|
49
52
|
get '/secret' do
|
50
|
-
# Attempt to reconstitute the original token, using the salt
|
51
53
|
token = MortalToken.new(session[:salt])
|
52
|
-
|
53
|
-
# Test if the token still is (or ever was) valid
|
54
54
|
if token == session[:token]
|
55
|
-
'
|
55
|
+
'Nice token!'
|
56
56
|
else
|
57
|
-
'
|
57
|
+
'Your token is expired or forged!'
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
-
== Checking
|
62
|
-
|
63
|
-
These are all valid means of checking a token's validity. In this case, they will all return true. == and === are treated the same.
|
64
|
-
|
65
|
-
token_a = MortalToken.new
|
66
|
-
token_b = MortalToken.new(token_a.salt)
|
61
|
+
== Checking token validity
|
67
62
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
token_a.to_s == token_b.to_s
|
72
|
-
token_a.hash == token_b.hash
|
73
|
-
token_a.to_s == token_b.hash
|
63
|
+
A token's == and === methods accept another token or a hash. In the example above, an attempt has been made to reconstitute
|
64
|
+
the original token (using it's salt) on the left. If successful, it will be able to regenerate the hash on the right. But if
|
65
|
+
too much time has passed, the hashes will differ and we can conclude that the token has expired.
|
74
66
|
|
75
67
|
== Tweak token parameters
|
76
68
|
|
77
69
|
You may tweak certain parameters of the library in order to make it more secure, less, faster, etc.
|
78
70
|
These are the defaults (see the MortalToken class for documentation about each parameter):
|
79
71
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
72
|
+
MortalToken.rounds = 5 # rounds of hashing
|
73
|
+
MortalToken.units = :days # or :hours, :minutes
|
74
|
+
MortalToken.valid_across = 2 # tokens are valid across N units
|
75
|
+
MortalToken.max_salt_length = 50
|
76
|
+
MortalToken.min_salt_length = 10
|
77
|
+
|
78
|
+
== Multiple configurations
|
79
|
+
|
80
|
+
Your application may want to use MortalTokens in various contexts, where the same parameters may not make sense (probably units and valid_across).
|
81
|
+
You may define different scopes and give them each their own config. Always define the default scope first (as above). Other scopes will inherit
|
82
|
+
its secret.
|
83
|
+
|
84
|
+
MortalToken.config(:foo) do |config|
|
85
|
+
config.units = :minutes
|
86
|
+
config.valid_across = 10
|
87
|
+
end
|
88
|
+
|
89
|
+
token = MortalToken.use(:foo).token
|
84
90
|
|
85
91
|
== License
|
86
92
|
Copyright 2012 Jordan Hollinger
|
data/lib/mortal-token.rb
CHANGED
@@ -0,0 +1,52 @@
|
|
1
|
+
class MortalToken
|
2
|
+
# Holds a specific a configuration of parameters
|
3
|
+
class Configuration
|
4
|
+
RAND_SEEDS = [(0..9), ('a'..'z'), ('A'..'Z')].map(&:to_a).flatten # :nodoc;
|
5
|
+
|
6
|
+
# The configuration's name
|
7
|
+
attr_reader :name
|
8
|
+
# The master secret token (Keep it secret! Keep it safe!). Changing this will invalidate all existing tokens.
|
9
|
+
attr_accessor :secret
|
10
|
+
# The number of encryption rounds. Defaults to 5. Changing this will invalidate existing tokens.
|
11
|
+
attr_accessor :rounds
|
12
|
+
# The units that life is measured in - :days, :hours or :minutes
|
13
|
+
attr_reader :units
|
14
|
+
# The number of units tokens will be valid across. Defaults to 2, which (for :days) prevents a
|
15
|
+
# token generated at 23:59 from expiring at 00:00. Changing this will invalidate existing tokens.
|
16
|
+
attr_accessor :valid_across
|
17
|
+
# The maximum salt length, defaults to 50
|
18
|
+
attr_accessor :max_salt_length
|
19
|
+
# The minimum salt length, defaults to 10
|
20
|
+
attr_accessor :min_salt_length
|
21
|
+
|
22
|
+
# Instantiates a new Configuration object default values
|
23
|
+
def initialize(name)
|
24
|
+
@name = name
|
25
|
+
@rounds = 5
|
26
|
+
@units = :days
|
27
|
+
@valid_across = 2
|
28
|
+
@max_salt_length = 50
|
29
|
+
@min_salt_length = 10
|
30
|
+
# A temporary secret key. Use your own!!
|
31
|
+
@secret = CONFIGS[:default] ? CONFIGS[:default].salt : self.salt
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return a new token using this configuration
|
35
|
+
def token(salt=nil, time=nil)
|
36
|
+
Token.new(salt, time, self)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Set the units that life is measured in - :days, :hours or :minutes
|
40
|
+
def units=(unit)
|
41
|
+
raise ArgumentError, "MortalToken.units must be one of #{UNITS.keys.join(', ')}" unless UNITS.keys.include? unit
|
42
|
+
@units = unit
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns a random string of between min_salt_length and max_salt_length alphanumeric charachters
|
46
|
+
def salt
|
47
|
+
max_length = [self.min_salt_length, rand(self.max_salt_length + 1)].max
|
48
|
+
pool_size = RAND_SEEDS.size
|
49
|
+
(0..max_length).map { RAND_SEEDS[rand(pool_size)] }.join('')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -1,76 +1,27 @@
|
|
1
1
|
# A token hash that "self-destructs" after a certain time.
|
2
2
|
class MortalToken
|
3
|
-
|
4
|
-
|
3
|
+
CONFIGS = {} # :nodoc:
|
4
|
+
UNITS = {days: {increment: 1, format: '%Y-%m-%d'}, hours: {increment: 3600, format: '%Y-%m-%d_%H'}, minutes: {increment: 60, format: '%Y-%m-%d_%H:%M'}} # :nodoc:
|
5
5
|
|
6
6
|
class << self
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
attr_accessor :rounds
|
11
|
-
# The number of days tokens will be valid across. Defaults to 2, which prevents a
|
12
|
-
# token generated at 23:59 from expiring at 00:00. Changing this will invalidate existing tokens.
|
13
|
-
attr_accessor :valid_across
|
14
|
-
# The maximum salt length, defaults to 50
|
15
|
-
attr_accessor :max_salt_length
|
16
|
-
# The minimum salt length, defaults to 10
|
17
|
-
attr_accessor :min_salt_length
|
18
|
-
|
19
|
-
# Returns a random string of between min_salt_length and max_salt_length alphanumeric charachters
|
20
|
-
def salt
|
21
|
-
max_length = rand(self.max_salt_length + 1)
|
22
|
-
max_length = self.min_salt_length if max_length < self.min_salt_length
|
23
|
-
pool_size = RAND_SEEDS.size
|
24
|
-
(0..max_length).map { RAND_SEEDS[rand(pool_size)] }.join('')
|
7
|
+
# Returns a new Token. Also alised to #token.
|
8
|
+
def new(salt=nil, time=nil)
|
9
|
+
config.token(salt, time)
|
25
10
|
end
|
26
|
-
end
|
27
|
-
|
28
|
-
# The token's salt
|
29
|
-
attr_reader :salt
|
30
|
-
|
31
|
-
# To create a brand new token, do *not* pass any arguments.
|
32
|
-
#
|
33
|
-
# If you want to validate a hash from an existing token, pass
|
34
|
-
# the existing token's salt. You should usually leave "start_date" alone.
|
35
|
-
def initialize(salt=nil, start_date=nil)
|
36
|
-
@salt = salt || MortalToken.salt
|
37
|
-
@start_date = start_date || Date.today
|
38
|
-
end
|
39
|
-
|
40
|
-
# Returns the hash value of the token
|
41
|
-
def hash
|
42
|
-
val = [MortalToken.secret, salt, @start_date, end_date].join('_')
|
43
|
-
MortalToken.rounds.times { val = Digest::SHA512.hexdigest(val) }
|
44
|
-
val
|
45
|
-
end
|
46
11
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
def ==(other_token_or_hash)
|
54
|
-
other_hash = other_token_or_hash.to_s
|
55
|
-
(earliest_date).upto(@start_date) do |date|
|
56
|
-
guess = MortalToken.new(salt, date)
|
57
|
-
return true if guess.to_s == other_hash
|
12
|
+
# Returns a new or existing MortalToken::Configuration. If you pass a block, it will pass it the config object.
|
13
|
+
# If it's a new config, it will inherit the default settings.
|
14
|
+
def config(name=:default)
|
15
|
+
config = (CONFIGS[name] ||= Configuration.new(name))
|
16
|
+
yield config if block_given?
|
17
|
+
config
|
58
18
|
end
|
59
|
-
return false
|
60
|
-
end
|
61
|
-
|
62
|
-
alias_method :===, :==
|
63
19
|
|
64
|
-
|
20
|
+
alias_method :use, :config
|
65
21
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
# Returns the earliest possible date this token could have been valid.
|
72
|
-
# Only useful when checking for validity.
|
73
|
-
def earliest_date
|
74
|
-
@start_date - (MortalToken.valid_across - 1)
|
22
|
+
# Alias all other methods to the default Configuration
|
23
|
+
def method_missing(method, *args, &block)
|
24
|
+
config.send(method, *args, &block)
|
25
|
+
end
|
75
26
|
end
|
76
27
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
class MortalToken
|
2
|
+
class Token
|
3
|
+
# The token's salt
|
4
|
+
attr_reader :salt
|
5
|
+
attr_accessor :config
|
6
|
+
|
7
|
+
# To create a brand new token, do *not* pass any arguments. To validate a hash from
|
8
|
+
# an existing token, pass the existing token's salt.
|
9
|
+
def initialize(salt=nil, start_time=nil, config=nil)
|
10
|
+
@config = config || MortalToken.config
|
11
|
+
@salt = salt || config.salt
|
12
|
+
@start_time = start_time || (config.units == :days ? Date.today : Time.now.utc)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the hash value of the token
|
16
|
+
def hash
|
17
|
+
@hash ||= (0..config.rounds-1).inject("#{config.secret}_#{salt}_#{end_time.strftime(strfmt)}") do |val, i|
|
18
|
+
Digest::SHA512.hexdigest(val)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
alias_method :to_s, :hash
|
23
|
+
|
24
|
+
# Tests this token against another token or token hash
|
25
|
+
def ==(other_token_or_hash)
|
26
|
+
each_time do |time|
|
27
|
+
guess = config.token(salt, time)
|
28
|
+
return true if guess.to_s == other_token_or_hash.to_s
|
29
|
+
end
|
30
|
+
return false
|
31
|
+
end
|
32
|
+
|
33
|
+
alias_method :===, :==
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Iterates over each possible time unit and passes it to the block
|
38
|
+
def each_time(&block)
|
39
|
+
time = earliest_time
|
40
|
+
while time <= end_time
|
41
|
+
block.call(time)
|
42
|
+
time += increment
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the end date/time of this token
|
47
|
+
def end_time
|
48
|
+
@start_time + (config.valid_across - 1) * increment
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the earliest possible date/time this token could have been valid.
|
52
|
+
def earliest_time
|
53
|
+
@start_time - (config.valid_across - 1) * increment
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the incrementor for the configured unit
|
57
|
+
def increment
|
58
|
+
UNITS[config.units][:increment]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the string formater for the configured unit
|
62
|
+
def strfmt
|
63
|
+
UNITS[config.units][:format]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/mortal-token/version.rb
CHANGED
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
- 0
|
8
7
|
- 1
|
9
|
-
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Jordan Hollinger
|
@@ -14,11 +14,11 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2012-08-
|
17
|
+
date: 2012-08-03 00:00:00 -04:00
|
18
18
|
default_executable:
|
19
19
|
dependencies: []
|
20
20
|
|
21
|
-
description:
|
21
|
+
description: An EXPERIMENTAL library for generating self-contained, self-destructing tokens
|
22
22
|
email: jordan@jordanhollinger.com
|
23
23
|
executables: []
|
24
24
|
|
@@ -28,9 +28,10 @@ extra_rdoc_files: []
|
|
28
28
|
|
29
29
|
files:
|
30
30
|
- lib/mortal-token.rb
|
31
|
-
- lib/mortal-token/defaults.rb
|
32
31
|
- lib/mortal-token/mortal-token.rb
|
32
|
+
- lib/mortal-token/token.rb
|
33
33
|
- lib/mortal-token/version.rb
|
34
|
+
- lib/mortal-token/configuration.rb
|
34
35
|
- README.rdoc
|
35
36
|
- LICENSE
|
36
37
|
has_rdoc: true
|
@@ -64,6 +65,6 @@ rubyforge_project:
|
|
64
65
|
rubygems_version: 1.3.7
|
65
66
|
signing_key:
|
66
67
|
specification_version: 3
|
67
|
-
summary: Generate self-destructing tokens
|
68
|
+
summary: Generate self-destructing tokens (experimental)
|
68
69
|
test_files: []
|
69
70
|
|
@@ -1,10 +0,0 @@
|
|
1
|
-
class MortalToken
|
2
|
-
# Set defaults
|
3
|
-
self.rounds = 5
|
4
|
-
self.valid_across = 2
|
5
|
-
self.max_salt_length = 50
|
6
|
-
self.min_salt_length = 10
|
7
|
-
# Set a temporary secret key. You should set your own consistent key.
|
8
|
-
# Otherwise, existing tokens will be invalidated each time the library is loaded.
|
9
|
-
self.secret = self.salt
|
10
|
-
end
|