mortal-token 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|