nobi 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (8) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +48 -0
  4. data/lib/nobi.rb +166 -0
  5. data/makefile +2 -0
  6. data/nobi.gemspec +23 -0
  7. data/tests/nobi_test.rb +71 -0
  8. metadata +64 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c9b2519943abe7119d47b83b0a1ac23bba9515d1
4
+ data.tar.gz: 1a0a2902847e87bd482834c7ec396b1d49ccdcd0
5
+ SHA512:
6
+ metadata.gz: bf65dab63bde421c905181d4d43fde5f6a786010d40985b2873732b3f30df0a3b192a5239e209ace45494be102eb2163bfabd82978d65b597d448b75be01ac42
7
+ data.tar.gz: 4c920e5e013494ed9b4ce00dedbca15e0e6fb2c85e95e7e7618e339b832d300ed57ebc79b319e53c4a8ff3ea1a69701b97a0e9793c0de3e1948614d841ffa942
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2012 Cyril David
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,48 @@
1
+ nobi
2
+ ====
3
+
4
+ Ruby port of [itsdangerous][itsdangerous] python signer.
5
+
6
+ [itsdangerous]: http://pythonhosted.org/itsdangerous/
7
+
8
+ Examples
9
+ --------
10
+
11
+ Best two usecases:
12
+
13
+ 1. Creating an activation link for users
14
+ 2. Creating a password reset link
15
+
16
+ ## Creating an activation link for users
17
+
18
+ ```ruby
19
+ signer = Nobi::Signer.new('my secret')
20
+
21
+ # Let's say the user's ID is 101
22
+ signed = signer.sign('101')
23
+
24
+ # You can now email this url to your users!
25
+ url = "http://yoursite.com/activate/?key=%s" % signed
26
+ ```
27
+
28
+ ## Creating a password reset link
29
+ ```ruby
30
+ signer = Nobi::TimestampSigner.new('my secret')
31
+
32
+ # Let's say the user's ID is 101
33
+ signed = signer.sign('101')
34
+
35
+ # You can now email this url to your users!
36
+ url = "http://yoursite.com/password-reset/?key=%s" % signed
37
+
38
+ # In your code, you can verify the expiration:
39
+ signer.unsign(signed, max_age: 86400) # 1 day expiration
40
+ ```
41
+
42
+ ## Installation
43
+
44
+ As usual, you can install it using rubygems.
45
+
46
+ ```
47
+ $ gem install nobi
48
+ ```
@@ -0,0 +1,166 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+
4
+ module Nobi
5
+ BadData = Class.new(StandardError)
6
+ BadSignature = Class.new(BadData)
7
+ SignatureExpired = Class.new(BadData)
8
+ BadTimeSignature = Class.new(BadSignature)
9
+
10
+ module Utils
11
+ def self.base64_encode(string)
12
+ Base64.urlsafe_encode64(string).gsub(/=+$/, '')
13
+ end
14
+
15
+ def self.base64_decode(string)
16
+ Base64.urlsafe_decode64(string + '=' * (-string.length % 4))
17
+ end
18
+
19
+ def self.int_to_bytes(num)
20
+ raise ArgumentError unless num >= 0
21
+
22
+ rv = []
23
+
24
+ while num > 0
25
+ rv << (num & 0xff).chr
26
+ num >>= 8
27
+ end
28
+
29
+ return rv.reverse.join
30
+ end
31
+
32
+ def self.bytes_to_int(bytes)
33
+ bytes.each_byte.inject(0) do |acc, byte|
34
+ acc << 8 | byte
35
+ end
36
+ end
37
+
38
+ def self.constant_time_compare(val1, val2)
39
+ return false unless val1.length == val2.length
40
+
41
+ cmp = val2.bytes.to_a
42
+ result = 0
43
+
44
+ val1.bytes.each_with_index do |char, index|
45
+ result |= char ^ cmp[index]
46
+ end
47
+
48
+ return result == 0
49
+ end
50
+
51
+ def self.rsplit(str, sep)
52
+ if str =~ /\A(.*)#{Regexp.escape(sep)}([^#{Regexp.escape(sep)}]+)\z/
53
+ return $1, $2
54
+ end
55
+ end
56
+ end
57
+
58
+ class HMACAlgorithm
59
+ def initialize(digest_method)
60
+ @digest_method = digest_method
61
+ end
62
+
63
+ def signature(key, value)
64
+ OpenSSL::HMAC.digest(@digest_method, key, value)
65
+ end
66
+ end
67
+
68
+ class Signer
69
+ def initialize(secret,
70
+ salt: 'nobi.Signer',
71
+ sep: '.',
72
+ digest_method: 'sha1')
73
+
74
+ @secret = secret
75
+ @salt = salt
76
+ @sep = sep
77
+ @algorithm = HMACAlgorithm.new(digest_method)
78
+ end
79
+
80
+ def sign(value)
81
+ '%s%s%s' % [value, @sep, signature(value)]
82
+ end
83
+
84
+ def unsign(value)
85
+ if not value.include?(@sep)
86
+ raise BadSignature, 'No "%s" found in value' % @sep
87
+ end
88
+
89
+ value, sig = Utils.rsplit(value, @sep)
90
+
91
+ if Utils.constant_time_compare(sig, signature(value))
92
+ return value
93
+ end
94
+
95
+ raise BadSignature, 'Signature "%s" does not match' % sig
96
+ end
97
+
98
+ def derive_key
99
+ @algorithm.signature(@secret, @salt)
100
+ end
101
+
102
+ def signature(value)
103
+ key = derive_key
104
+ sig = @algorithm.signature(key, value)
105
+
106
+ Utils.base64_encode(sig)
107
+ end
108
+ end
109
+
110
+ class TimestampSigner < Signer
111
+ # 2011/01/01 in UTC
112
+ EPOCH = 1293840000
113
+
114
+ def get_timestamp
115
+ Time.now.utc.to_f - EPOCH
116
+ end
117
+
118
+ def timestamp_to_datetime(ts)
119
+ Time.at(ts + EPOCH).utc
120
+ end
121
+
122
+ def sign(value)
123
+ timestamp = Utils.base64_encode(Utils.int_to_bytes(get_timestamp.to_i))
124
+ value = '%s%s%s' % [value, @sep, timestamp]
125
+
126
+ '%s%s%s' % [value, @sep, signature(value)]
127
+ end
128
+
129
+ def unsign(value, max_age: nil, return_timestamp: nil)
130
+ sig_error = nil
131
+ result = ''
132
+
133
+ begin
134
+ result = super(value)
135
+ rescue BadSignature => e
136
+ sig_error = e
137
+ end
138
+
139
+ if not result.include?(@sep)
140
+ if sig_error
141
+ raise sig_error
142
+ else
143
+ raise BadTimeSignature, 'timestamp missing'
144
+ end
145
+ end
146
+
147
+ value, timestamp = Utils.rsplit(result, @sep)
148
+
149
+ timestamp = Utils.bytes_to_int(Utils.base64_decode(timestamp))
150
+
151
+ if max_age
152
+ age = get_timestamp - timestamp
153
+
154
+ if age > max_age
155
+ raise SignatureExpired, 'Signature age %s > %s seconds' % [age, max_age]
156
+ end
157
+ end
158
+
159
+ if return_timestamp
160
+ return value, timestamp_to_datetime(timestamp)
161
+ else
162
+ return value
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,2 @@
1
+ test:
2
+ cutest tests/*_test.rb
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "nobi"
5
+ s.version = "0.0.1"
6
+ s.summary = "Ruby port of itsdangerous python signer."
7
+ s.description = "Ruby port of itsdangerous python signer."
8
+ s.authors = ["Cyril David"]
9
+ s.email = ["cyx@cyx.is"]
10
+ s.homepage = "http://cyx.is"
11
+ s.files = Dir[
12
+ "LICENSE",
13
+ "README*",
14
+ "makefile",
15
+ "lib/**/*.rb",
16
+ "*.gemspec",
17
+ "tests/*.*",
18
+ ]
19
+
20
+ s.license = "MIT"
21
+
22
+ s.add_development_dependency "cutest"
23
+ end
@@ -0,0 +1,71 @@
1
+ require File.expand_path('../lib/nobi', File.dirname(__FILE__))
2
+ require 'cutest'
3
+
4
+ scope do
5
+ setup do
6
+ Nobi::Signer.new('foo')
7
+ end
8
+
9
+ test 'sign + unsign' do |s|
10
+ assert_equal 'bar', s.unsign(s.sign('bar'))
11
+ end
12
+
13
+ test 'sign + unsign an int' do |s|
14
+ assert_raise TypeError do
15
+ s.sign(1)
16
+ end
17
+ end
18
+
19
+ test 'no signature' do |s|
20
+ assert_raise Nobi::BadSignature do
21
+ s.unsign('nosep')
22
+ end
23
+ end
24
+
25
+ test 'bad signature' do |s|
26
+ assert_raise Nobi::BadSignature do
27
+ s.unsign('bar.whatever')
28
+ end
29
+ end
30
+ end
31
+
32
+ scope do
33
+ setup do
34
+ Nobi::TimestampSigner.new('foo')
35
+ end
36
+
37
+ test 'sign + unsign' do |ts|
38
+ assert_equal 'bar', ts.unsign(ts.sign('bar'))
39
+ end
40
+
41
+ test 'sign with no timestamp + unsign with timestamp' do |ts|
42
+ s = Nobi::Signer.new('foo')
43
+
44
+ assert_raise Nobi::BadTimeSignature do
45
+ ts.unsign(s.sign('bar'))
46
+ end
47
+ end
48
+
49
+ test 'unsign return_ timestamp' do |ts|
50
+ time = Time.now.utc
51
+ signed = ts.sign('bar')
52
+
53
+ value, timestamp = ts.unsign(signed, return_timestamp: true)
54
+
55
+ assert_equal 'bar', value
56
+
57
+ # Because we can't make the exact time down to the fractional second,
58
+ # we need to compare the time on an int level.
59
+ assert_equal time.to_i, timestamp.to_i
60
+ end
61
+
62
+ test 'signature expired' do |ts|
63
+ signed = ts.sign('bar')
64
+
65
+ sleep 0.09
66
+
67
+ assert_raise Nobi::SignatureExpired do
68
+ ts.unsign(signed, max_age: 0.1)
69
+ end
70
+ end
71
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nobi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Cyril David
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-05-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: cutest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Ruby port of itsdangerous python signer.
28
+ email:
29
+ - cyx@cyx.is
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - LICENSE
35
+ - README.md
36
+ - makefile
37
+ - lib/nobi.rb
38
+ - nobi.gemspec
39
+ - tests/nobi_test.rb
40
+ homepage: http://cyx.is
41
+ licenses:
42
+ - MIT
43
+ metadata: {}
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubyforge_project:
60
+ rubygems_version: 2.0.0
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Ruby port of itsdangerous python signer.
64
+ test_files: []