hashcash 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.
- 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
|