nobi 0.0.1

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.
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: []