hashcash 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +49 -0
- data/lib/hashcash.rb +143 -0
- data/test/test_hashcash.rb +50 -0
- metadata +69 -0
data/README
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
== Description
|
2
|
+
A library for creating and verifying so-called »hash cash stamps«, i.e.
|
3
|
+
proof of work as defined on hashcash.org.
|
4
|
+
|
5
|
+
== Prerequisites
|
6
|
+
This package requires Ruby 1.8 or later.
|
7
|
+
|
8
|
+
== Installation instructions
|
9
|
+
rake test (optional)
|
10
|
+
rake install (non-gem) or rake install_gem (gem)
|
11
|
+
|
12
|
+
== Synopsis
|
13
|
+
require 'hashcash'
|
14
|
+
|
15
|
+
# Create a new hash cash stamp
|
16
|
+
s = HashCash::Stamp.new(:resource => 'hashcash@alech.de')
|
17
|
+
puts s
|
18
|
+
|
19
|
+
# Verify a given stamp
|
20
|
+
s = HashCash::Stamp.new(:stamp => '1:20:060408:adam@cypherspace.org::1QTjaYd7niiQA/sc:ePa')
|
21
|
+
s.verify('adam@cypherspace.org)
|
22
|
+
|
23
|
+
== Known bugs
|
24
|
+
Stamp creation is an order of magnitude slower than using the
|
25
|
+
standalone hash cash program. This might not be fixed as I
|
26
|
+
mainly use the library for verification.
|
27
|
+
|
28
|
+
== Copyright
|
29
|
+
(c) 2010 Alexander Klink
|
30
|
+
|
31
|
+
== License
|
32
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
33
|
+
you may not use this file except in compliance with the License.
|
34
|
+
You may obtain a copy of the License at
|
35
|
+
|
36
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
37
|
+
|
38
|
+
== Warranty
|
39
|
+
Unless required by applicable law or agreed to in writing, software
|
40
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
41
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
42
|
+
See the License for the specific language governing permissions and
|
43
|
+
limitations under the License.
|
44
|
+
|
45
|
+
== Author
|
46
|
+
Alexander Klink
|
47
|
+
hashcash@alech.de
|
48
|
+
http://www.alech.de
|
49
|
+
@alech on Twitter
|
data/lib/hashcash.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'openssl'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module HashCash
|
7
|
+
# The HashCash::Stamp class can be used to create and verify proof of work, so
|
8
|
+
# called hash cash, as defined on hashcash.org.
|
9
|
+
#
|
10
|
+
# Basically, it creates a 'stamp', which when hashed with SHA-1 has a
|
11
|
+
# certain amount of 0 bytes at the top.
|
12
|
+
#
|
13
|
+
# To create a new stamp, call the constructor with the :resource parameter
|
14
|
+
# to specify a resource for which this stamp will be valid (e.g. an email
|
15
|
+
# address).
|
16
|
+
#
|
17
|
+
# To verify a stamp, call it with a string representation of the stamp as
|
18
|
+
# the :stamp parameter and call verify with a resource on it.
|
19
|
+
class Stamp
|
20
|
+
attr_reader :version, :bits, :resource, :date, :stamp_string
|
21
|
+
|
22
|
+
STAMP_VERSION = 1
|
23
|
+
|
24
|
+
# To construct a new HashCash::Stamp object, pass the :resource parameter
|
25
|
+
# to it, e.g.
|
26
|
+
#
|
27
|
+
# s = HashCash::Stamp.new(:resource => 'hashcash@alech.de')
|
28
|
+
#
|
29
|
+
# This creates a 20 bit hash cash stamp, which can be retrieved using
|
30
|
+
# the stamp_string() attribute reader method.
|
31
|
+
#
|
32
|
+
# Optionally, the parameters :bits and :date can be passed to the
|
33
|
+
# method to change the number of bits the stamp is worth and the issuance
|
34
|
+
# date (which is checked on the server for an expiry with a default
|
35
|
+
# deviance of 2 days, pass a Time object).
|
36
|
+
#
|
37
|
+
# Alternatively, a stamp can be passed to the constructor by passing
|
38
|
+
# it as a string to the :stamp parameter, e.g.
|
39
|
+
#
|
40
|
+
# s = HashCash::Stamp.new(:stamp => '1:20:060408:adam@cypherspace.org::1QTjaYd7niiQA/sc:ePa')
|
41
|
+
def initialize(args)
|
42
|
+
if ! args || (! args[:stamp] && ! args[:resource]) then
|
43
|
+
raise ArgumentError, 'either stamp or stamp parameters needed'
|
44
|
+
end
|
45
|
+
# existing stamp in string format
|
46
|
+
if args[:stamp] then
|
47
|
+
@stamp_string = args[:stamp]
|
48
|
+
(@version, @bits, @date, @resource, ext, @rand, @counter) \
|
49
|
+
= args[:stamp].split(':')
|
50
|
+
@bits = @bits.to_i
|
51
|
+
if @version.to_i != STAMP_VERSION then
|
52
|
+
raise ArgumentError, "incorrect stamp version #{@version}"
|
53
|
+
end
|
54
|
+
@date = parse_date(@date)
|
55
|
+
# new stamp to be created
|
56
|
+
elsif args[:resource] then
|
57
|
+
@resource = args[:resource]
|
58
|
+
# optional parameters: bits and date
|
59
|
+
@bits = args[:bits] || 20
|
60
|
+
@bits = @bits.to_i
|
61
|
+
if args[:date] && ! args[:date].class == Time then
|
62
|
+
raise ArgumentError, 'date needs to be a Time object'
|
63
|
+
end
|
64
|
+
@date = args[:date] || Time.now
|
65
|
+
|
66
|
+
# create first part of stamp string
|
67
|
+
random_string = Base64.encode64(OpenSSL::Random.random_bytes(12)).chomp
|
68
|
+
first_part = "#{STAMP_VERSION}:#{@bits}:" + \
|
69
|
+
"#{date_to_str(@date)}:#{@resource}" + \
|
70
|
+
"::#{random_string}:"
|
71
|
+
ctr = 0
|
72
|
+
@stamp_string = nil
|
73
|
+
while ! @stamp_string do
|
74
|
+
test_stamp = first_part + ctr.to_s(36)
|
75
|
+
if Digest::SHA1.digest(test_stamp).unpack('B*')[0][0,@bits].to_i == 0
|
76
|
+
@stamp_string = test_stamp
|
77
|
+
end
|
78
|
+
ctr += 1
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Verify a stamp for a given resource or resources and a number of bits.
|
84
|
+
# The resources parameter can either be a string for a single resource
|
85
|
+
# or an array of strings for more than one possible resource (for example
|
86
|
+
# if you have different email addresses and want the stamp to verify against
|
87
|
+
# one of them).
|
88
|
+
#
|
89
|
+
# The method checks the resource, the time of issuance and the number of
|
90
|
+
# 0 bits when the stamp is SHA1-hashed. It returns true if all checks
|
91
|
+
# are successful and raises an exception otherwise.
|
92
|
+
def verify(resources, bits = 20)
|
93
|
+
# check for correct resource
|
94
|
+
if resources.class != String && resources.class != Array then
|
95
|
+
raise ArgumentError, "resource must be either String or Array"
|
96
|
+
end
|
97
|
+
if resources.class == String then
|
98
|
+
resources = [ resources ]
|
99
|
+
end
|
100
|
+
if ! resources.include? @resource then
|
101
|
+
raise "Stamp is not valid for the given resource(s)."
|
102
|
+
end
|
103
|
+
# check if difference is greater than 2 days
|
104
|
+
if (Time.now - @date).to_i.abs > 2*24*60*60 then
|
105
|
+
raise "Stamp is expired/not yet valid"
|
106
|
+
end
|
107
|
+
# check 0 bits in stamp
|
108
|
+
if (Digest::SHA1.hexdigest(@stamp_string).hex >> (160-bits) != 0) then
|
109
|
+
raise "Invalid stamp, not enough 0 bits"
|
110
|
+
end
|
111
|
+
true
|
112
|
+
end
|
113
|
+
|
114
|
+
# A string representation of the stamp
|
115
|
+
def to_s
|
116
|
+
@stamp_string
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
# Parse the date contained in the stamp string.
|
121
|
+
def parse_date(date)
|
122
|
+
year = date[0,2].to_i
|
123
|
+
month = date[2,2].to_i
|
124
|
+
day = date[4,2].to_i
|
125
|
+
# Those may not exist, but it is irrelevant as ''.to_i is 0
|
126
|
+
hour = date[6,2].to_i
|
127
|
+
min = date[8,2].to_i
|
128
|
+
sec = date[10,2].to_i
|
129
|
+
Time.utc(year, month, day, hour, min, sec)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Convert a date to the string format used in the stamps
|
133
|
+
def date_to_str(date)
|
134
|
+
if (date.sec == 0) && (date.hour == 0) && (date.min == 0) then
|
135
|
+
date.strftime("%y%m%d")
|
136
|
+
elsif (date.sec == 0) then
|
137
|
+
date.strftime("%y%m%d%H%M")
|
138
|
+
else
|
139
|
+
date.strftime("%y%m%d%H%M%S")
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'lib/hashcash'
|
3
|
+
require 'digest/sha1'
|
4
|
+
|
5
|
+
class TestHashCash < Test::Unit::TestCase
|
6
|
+
WIKIPEDIA_EXAMPLE = '1:20:060408:adam@cypherspace.org::1QTjaYd7niiQA/sc:ePa'
|
7
|
+
|
8
|
+
def test_instantiation
|
9
|
+
assert_raise( ArgumentError ) { HashCash::Stamp.new }
|
10
|
+
assert(HashCash::Stamp.new(:resource => 'testhashcash'))
|
11
|
+
assert(HashCash::Stamp.new(:resource => 'foobar', :bits => 15))
|
12
|
+
assert(HashCash::Stamp.new(:stamp => WIKIPEDIA_EXAMPLE))
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_parsing
|
16
|
+
s = HashCash::Stamp.new(:stamp => WIKIPEDIA_EXAMPLE)
|
17
|
+
assert_equal(1, s.version.to_i)
|
18
|
+
assert_equal(20, s.bits)
|
19
|
+
assert_equal(2006, s.date.year)
|
20
|
+
assert_equal(4, s.date.month)
|
21
|
+
assert_equal(8, s.date.day)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_verify_errors
|
25
|
+
s = HashCash::Stamp.new(:stamp => WIKIPEDIA_EXAMPLE)
|
26
|
+
# wrong resource
|
27
|
+
assert_raise( RuntimeError ) { s.verify('foo@example.org') }
|
28
|
+
# expired
|
29
|
+
assert_raise( RuntimeError ) { s.verify('adam@cypherspace.org') }
|
30
|
+
# not enough 'cash'
|
31
|
+
stamp = "1:32:" + Time.now.strftime('%y%m%d') + ':testhashcash::foobar1234:1'
|
32
|
+
# make sure it is really not enough
|
33
|
+
while (Digest::SHA1.hexdigest(stamp).hex >> (160-32) == 0) do
|
34
|
+
stamp += '1'
|
35
|
+
end
|
36
|
+
s2 = HashCash::Stamp.new(:stamp => stamp)
|
37
|
+
assert_raise( RuntimeError ) { s.verify('testhashcash') }
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_create_and_verify
|
41
|
+
s = HashCash::Stamp.new(:resource => 'testhashcash')
|
42
|
+
assert(s.verify('testhashcash'))
|
43
|
+
|
44
|
+
s2 = HashCash::Stamp.new(:resource => 'testhashcash', :bits => 10)
|
45
|
+
assert(s2.verify('testhashcash', 10))
|
46
|
+
|
47
|
+
s3 = HashCash::Stamp.new(:resource => 'testhashcash', :bits => 10, :date => Time.now)
|
48
|
+
assert(s3.verify('testhashcash', 10))
|
49
|
+
end
|
50
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hashcash
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 9
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: "0.1"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Alexander Klink
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-09-24 00:00:00 +02:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: "A library for creating and verifying so-called \xC2\xBBhash cash stamps\xC2\xAB, i.e.\n\
|
22
|
+
proof of work as defined on hashcash.org.\n"
|
23
|
+
email: hashcash@alech.de
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- README
|
30
|
+
files:
|
31
|
+
- lib/hashcash.rb
|
32
|
+
- test/test_hashcash.rb
|
33
|
+
- README
|
34
|
+
has_rdoc: true
|
35
|
+
homepage:
|
36
|
+
licenses: []
|
37
|
+
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
hash: 3
|
49
|
+
segments:
|
50
|
+
- 0
|
51
|
+
version: "0"
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.3.7
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: A library to create hash cash stamps as defined on hashcash.org.
|
68
|
+
test_files:
|
69
|
+
- test/test_hashcash.rb
|