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 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
- I found myself wanting simple, secure (never a good mix), and not necessarily encrypted auth tokens for some Sinatra apps.
7
- I wanted them to expire, but didn't want to keep track of them. Not finding anything, this library emerged. The default lifespan
8
- is two of your Earth days. This does *not* mean "48 hours from when it was created". It means that the key will be valid throughout
9
- "today" and "tomorrow". If the lifespan were one day, a key created at 23:59 would expire at 00:00.
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. *crickets*
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
- == Disclaimer
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
- This is not intended to be the most secure thing ever, but for all I know it's less secure than I think. Suggestions and
24
- pull requests are welcomed.
28
+ == Install
25
29
 
26
- Also, it is very early and the API may go through significant changes.
30
+ [sudo] gem install mortal-token
27
31
 
28
- == Use
32
+ Or add it to your Gemfile
33
+ gem "mortal-token"
29
34
 
30
- Though my example is a Sinatra app, it need not be. In fact, it needn't have anything to do with the Web. It's useful
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
- 'Welcome!'
55
+ 'Nice token!'
56
56
  else
57
- 'Go away!'
57
+ 'Your token is expired or forged!'
58
58
  end
59
59
  end
60
60
 
61
- == Checking tokens
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
- token_a == token_b
69
- token_a == token_b.to_s
70
- token_a == token_b.hash
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
- MortalCoil.rounds = 5
81
- MortalCoil.valid_across = 2
82
- MortalCoil.max_salt_length = 50
83
- MortalCoil.min_salt_length = 10
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
@@ -1,5 +1,7 @@
1
1
  require 'date'
2
+ require 'time'
2
3
  require 'digest/sha2'
3
4
  require 'mortal-token/version'
5
+ require 'mortal-token/token'
6
+ require 'mortal-token/configuration'
4
7
  require 'mortal-token/mortal-token'
5
- require 'mortal-token/defaults'
@@ -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
- # Seeds for generating random strings
4
- RAND_SEEDS = [(0..9), ('a'..'z'), ('A'..'Z')].map(&:to_a).flatten
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
- # The master secret token (Keep it secret! Keep it safe!). Changing this will invalidate all existing tokens.
8
- attr_accessor :secret
9
- # The number of encryption rounds. Defaults to 5. Changing this will invalidate existing tokens.
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
- # Alias to MortalToken::Token.hash
48
- def to_s
49
- hash
50
- end
51
-
52
- # Tests this token against another token or token hash
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
- private
20
+ alias_method :use, :config
65
21
 
66
- # Returns the end date of this token
67
- def end_date
68
- @start_date + (MortalToken.valid_across - 1)
69
- end
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
@@ -1,4 +1,4 @@
1
1
  class MortalToken
2
2
  # Library version
3
- VERSION = '0.0.1'
3
+ VERSION = '0.1.0'
4
4
  end
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
- version: 0.0.1
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-02 00:00:00 -04:00
17
+ date: 2012-08-03 00:00:00 -04:00
18
18
  default_executable:
19
19
  dependencies: []
20
20
 
21
- description: A simple library for generating self-contained, self-destructing tokens
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