cryptoconditions_ruby 0.5.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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.gitmodules +3 -0
- data/.rspec +2 -0
- data/.rubocop.yml +26 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +201 -0
- data/README.md +22 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/cryptoconditions_ruby.gemspec +34 -0
- data/lib/cryptoconditions_ruby/condition.rb +129 -0
- data/lib/cryptoconditions_ruby/crypto.rb +168 -0
- data/lib/cryptoconditions_ruby/exceptions.rb +8 -0
- data/lib/cryptoconditions_ruby/fulfillment.rb +138 -0
- data/lib/cryptoconditions_ruby/type_registry.rb +27 -0
- data/lib/cryptoconditions_ruby/types/base_sha_256_fulfillment.rb +15 -0
- data/lib/cryptoconditions_ruby/types/ed25519_fulfillment.rb +79 -0
- data/lib/cryptoconditions_ruby/types/inverted_threshold_sha_256_fulfillment.rb +15 -0
- data/lib/cryptoconditions_ruby/types/preimage_sha_256_fulfillment.rb +64 -0
- data/lib/cryptoconditions_ruby/types/threshold_sha_256_fulfillment.rb +343 -0
- data/lib/cryptoconditions_ruby/types/timeout_fulfillment.rb +46 -0
- data/lib/cryptoconditions_ruby/utils/base16.rb +28 -0
- data/lib/cryptoconditions_ruby/utils/base58.rb +53 -0
- data/lib/cryptoconditions_ruby/utils/byte_array.rb +16 -0
- data/lib/cryptoconditions_ruby/utils/bytes.rb +13 -0
- data/lib/cryptoconditions_ruby/utils/hasher.rb +27 -0
- data/lib/cryptoconditions_ruby/utils/hexlify.rb +13 -0
- data/lib/cryptoconditions_ruby/utils/predictor.rb +58 -0
- data/lib/cryptoconditions_ruby/utils/reader.rb +167 -0
- data/lib/cryptoconditions_ruby/utils/writer.rb +81 -0
- data/lib/cryptoconditions_ruby/version.rb +3 -0
- data/lib/cryptoconditions_ruby.rb +28 -0
- metadata +191 -0
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'base32'
|
3
|
+
require 'rbnacl'
|
4
|
+
|
5
|
+
module CryptoconditionsRuby
|
6
|
+
module Crypto
|
7
|
+
def self.get_encoder(encoding)
|
8
|
+
case encoding
|
9
|
+
when 'base58' then Base58Encoder
|
10
|
+
when 'base64' then Base64Encoder
|
11
|
+
when 'base32' then Base32Encoder
|
12
|
+
when 'base16' then Base16Encoder
|
13
|
+
when 'hex' then HexEncoder
|
14
|
+
when 'bytes' then RawEncoder
|
15
|
+
else
|
16
|
+
raise Exceptions::UnknownEncodingError, 'Unknown or unsupported encoding'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module Helpers
|
21
|
+
def ed25519_generate_key_pair
|
22
|
+
sk = Ed25519SigningKey.generate
|
23
|
+
private_value_base58 = sk.encode('base58')
|
24
|
+
public_value_compressed_base58 = sk.get_verifying_key.encode('base58')
|
25
|
+
[private_value_base58, public_value_compressed_base58]
|
26
|
+
end
|
27
|
+
|
28
|
+
def base64_add_padding(data)
|
29
|
+
data = data.encode('utf-8') if data.is_a?(String)
|
30
|
+
missing_padding = (4 - data.length) % 4
|
31
|
+
data += '=' * missing_padding if missing_padding
|
32
|
+
data
|
33
|
+
end
|
34
|
+
|
35
|
+
def base64_remove_padding(data)
|
36
|
+
data = data.encode('utf-8') if data.is_a?(String)
|
37
|
+
data.sub(/=+\Z/, '')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Base58Encoder
|
42
|
+
def encode(data)
|
43
|
+
CryptoconditionsRuby::Utils::Base58.encode(data)
|
44
|
+
end
|
45
|
+
|
46
|
+
def decode(data)
|
47
|
+
CryptoconditionsRuby::Utils::Base58.decode(data)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Base64Encoder
|
52
|
+
def encode(data)
|
53
|
+
Base64.encode64(data).strip
|
54
|
+
end
|
55
|
+
|
56
|
+
def decode(data)
|
57
|
+
Base64.decode64(data)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Base32Encoder
|
62
|
+
def encode(data)
|
63
|
+
Base32.encode(data)
|
64
|
+
end
|
65
|
+
|
66
|
+
def decode(data)
|
67
|
+
Base32.decode(data)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Base16Encoder
|
72
|
+
def encode(data)
|
73
|
+
CryptoconditionsRuby::Utils::Base16.encode(data)
|
74
|
+
end
|
75
|
+
|
76
|
+
def decode(data)
|
77
|
+
CryptoconditionsRuby::Utils::Base16.decode(data)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class HexEncoder
|
82
|
+
def encode(data)
|
83
|
+
data.to_s.unpack('H*')[0]
|
84
|
+
end
|
85
|
+
|
86
|
+
def decode(data)
|
87
|
+
[data].pack('H*')
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class RawEncoder
|
92
|
+
def encode(data)
|
93
|
+
data
|
94
|
+
end
|
95
|
+
|
96
|
+
def decode(data)
|
97
|
+
data
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class Ed25519SigningKey < ::RbNaCl::SigningKey
|
102
|
+
CryptoKeypair = Struct.new(:private_key, :public_key)
|
103
|
+
|
104
|
+
attr_accessor :key, :encoder, :encoding
|
105
|
+
private :key, :encoder, :encoding
|
106
|
+
|
107
|
+
def initialize(key = nil, encoding = nil)
|
108
|
+
@key = key
|
109
|
+
@encoding = encoding || 'base58'
|
110
|
+
@encoder = Crypto.get_encoder(@encoding)
|
111
|
+
super(@encoder.new.decode(@key))
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.generate
|
115
|
+
encoder = Base58Encoder.new
|
116
|
+
new(encoder.encode(::RbNaCl::Random.random_bytes(RbNaCl::Signatures::Ed25519::SEEDBYTES)))
|
117
|
+
end
|
118
|
+
|
119
|
+
def get_verifying_key
|
120
|
+
Ed25519VerifyingKey.new(encoder.new.encode(verify_key.to_s), encoding)
|
121
|
+
end
|
122
|
+
|
123
|
+
def sign(data, encoding = nil)
|
124
|
+
encoder = Crypto.get_encoder(encoding || 'base58')
|
125
|
+
encoder.new.encode(super(data))
|
126
|
+
end
|
127
|
+
|
128
|
+
def encode(encoding = 'base58')
|
129
|
+
encoder = Crypto.get_encoder(encoding).new
|
130
|
+
encoder.encode(self.to_s)
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def generate_signing_key
|
136
|
+
if key
|
137
|
+
::RbNaCl::SigningKey.new(key)
|
138
|
+
else
|
139
|
+
::RbNaCl::SigningKey.generate
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class Ed25519VerifyingKey < ::RbNaCl::VerifyKey
|
145
|
+
attr_accessor :key, :encoder
|
146
|
+
private :key, :encoder
|
147
|
+
|
148
|
+
def initialize(key = nil, encoding = nil)
|
149
|
+
@key = key
|
150
|
+
encoding = encoding || 'base58'
|
151
|
+
@encoder = Crypto.get_encoder(encoding)
|
152
|
+
super(@encoder.new.decode(@key))
|
153
|
+
end
|
154
|
+
|
155
|
+
def verify(signature, data, encoding = 'base58')
|
156
|
+
encoder = Crypto.get_encoder(encoding)
|
157
|
+
super(encoder.new.decode(signature), data)
|
158
|
+
rescue ::RbNaCl::BadSignatureError
|
159
|
+
false
|
160
|
+
end
|
161
|
+
|
162
|
+
def encode(encoding = 'base58')
|
163
|
+
encoder = Crypto.get_encoder(encoding).new
|
164
|
+
encoder.encode(self.to_s)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module CryptoconditionsRuby
|
4
|
+
FULFILLMENT_REGEX = /^cf:([1-9a-f][0-9a-f]{0,3}|0):[a-zA-Z0-9_-]*$/
|
5
|
+
|
6
|
+
class Fulfillment
|
7
|
+
extend Crypto::Helpers
|
8
|
+
include Crypto::Helpers
|
9
|
+
TYPE_ID = nil
|
10
|
+
REGEX = FULFILLMENT_REGEX
|
11
|
+
FEATURE_BITMASK = nil
|
12
|
+
|
13
|
+
def self.from_uri(serialized_fulfillment)
|
14
|
+
return serialized_fulfillment if serialized_fulfillment.is_a?(Fulfillment)
|
15
|
+
|
16
|
+
unless serialized_fulfillment.is_a?(String)
|
17
|
+
raise TypeError, 'Serialized fulfillment must be a string'
|
18
|
+
end
|
19
|
+
|
20
|
+
pieces = serialized_fulfillment.split(':', -1)
|
21
|
+
|
22
|
+
unless pieces.first == 'cf'
|
23
|
+
raise TypeError, 'Serialized fulfillment must start with "cf:"'
|
24
|
+
end
|
25
|
+
|
26
|
+
unless serialized_fulfillment.match(Fulfillment::REGEX)
|
27
|
+
raise TypeError, 'Invalid fulfillment format'
|
28
|
+
end
|
29
|
+
|
30
|
+
type_id = pieces[1].to_i(16)
|
31
|
+
payload = Base64.urlsafe_decode64(base64_add_padding(pieces[2]))
|
32
|
+
|
33
|
+
cls = TypeRegistry.get_class_from_type_id(type_id)
|
34
|
+
fulfillment = cls.new
|
35
|
+
|
36
|
+
fulfillment.parse_payload(Utils::Reader.from_source(payload), payload.length)
|
37
|
+
fulfillment
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.from_binary(reader)
|
41
|
+
reader = Utils::Reader.from_source(reader)
|
42
|
+
|
43
|
+
cls_type = reader.read_uint16
|
44
|
+
cls = TypeRegistry.get_class_from_type_id(cls_type)
|
45
|
+
|
46
|
+
fulfillment = cls.new
|
47
|
+
payload_length = reader.read_length_prefix
|
48
|
+
fulfillment.parse_payload(reader, payload_length)
|
49
|
+
fulfillment
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.from_dict(data)
|
53
|
+
cls_type = data['type_id']
|
54
|
+
cls = TypeRegistry.get_class_from_type_id(cls_type)
|
55
|
+
fulfillment = cls.new
|
56
|
+
fulfillment.parse_dict(data)
|
57
|
+
fulfillment
|
58
|
+
end
|
59
|
+
|
60
|
+
def type_id
|
61
|
+
self.class::TYPE_ID
|
62
|
+
end
|
63
|
+
|
64
|
+
def bitmask
|
65
|
+
self.class::FEATURE_BITMASK
|
66
|
+
end
|
67
|
+
|
68
|
+
def condition
|
69
|
+
condition = Condition.new
|
70
|
+
condition.type_id = type_id
|
71
|
+
condition.bitmask = bitmask
|
72
|
+
condition.hash = generate_hash
|
73
|
+
condition.max_fulfillment_length = calculate_max_fulfillment_length
|
74
|
+
condition
|
75
|
+
end
|
76
|
+
|
77
|
+
def condition_uri
|
78
|
+
condition.serialize_uri
|
79
|
+
end
|
80
|
+
|
81
|
+
def condition_binary
|
82
|
+
condition.serialize_binary
|
83
|
+
end
|
84
|
+
|
85
|
+
def generate_hash
|
86
|
+
raise 'Implement me'
|
87
|
+
end
|
88
|
+
|
89
|
+
def calculate_max_fulfillment_length
|
90
|
+
predictor = Utils::Predictor.new
|
91
|
+
write_payload(predictor)
|
92
|
+
predictor.size
|
93
|
+
end
|
94
|
+
|
95
|
+
def serialize_uri
|
96
|
+
format(
|
97
|
+
'cf:%x:%s',
|
98
|
+
type_id,
|
99
|
+
base64_remove_padding(
|
100
|
+
Base64.urlsafe_encode64(serialize_payload)
|
101
|
+
)
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
def serialize_binary
|
106
|
+
writer = Utils::Writer.new
|
107
|
+
writer.write_uint16(type_id)
|
108
|
+
writer.write_var_octet_string(serialize_payload)
|
109
|
+
writer.buffer
|
110
|
+
end
|
111
|
+
|
112
|
+
def serialize_payload
|
113
|
+
writer = Utils::Writer.new
|
114
|
+
write_payload(writer)
|
115
|
+
writer.buffer
|
116
|
+
end
|
117
|
+
|
118
|
+
def write_payload(_writer)
|
119
|
+
raise 'Implement me'
|
120
|
+
end
|
121
|
+
|
122
|
+
def parse_payload(_reader, *_args)
|
123
|
+
raise 'Implement me'
|
124
|
+
end
|
125
|
+
|
126
|
+
def to_dict
|
127
|
+
raise 'Implement me'
|
128
|
+
end
|
129
|
+
|
130
|
+
def parse_dict(_data)
|
131
|
+
raise 'Implement me'
|
132
|
+
end
|
133
|
+
|
134
|
+
def validate(**_kwargs)
|
135
|
+
raise 'Implement me'
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class CryptoconditionsRuby::TypeRegistry
|
2
|
+
MAX_SAFE_INTEGER_JS = 2 ** 53 - 1
|
3
|
+
|
4
|
+
def self.registered_types
|
5
|
+
@registered_types ||=[]
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.get_class_from_type_id(type_id)
|
9
|
+
if type_id > MAX_SAFE_INTEGER_JS
|
10
|
+
raise TypeError.new("Type #{type_id} is not supported")
|
11
|
+
end
|
12
|
+
|
13
|
+
type = registered_types.find do |registered_type|
|
14
|
+
type_id == registered_type['type_id']
|
15
|
+
end
|
16
|
+
|
17
|
+
if type
|
18
|
+
type['class']
|
19
|
+
else
|
20
|
+
raise TypeError.new("Type #{type_id} is not supported")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.register_type(cls)
|
25
|
+
registered_types.push({'type_id' => cls::TYPE_ID, 'class' => cls})
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module CryptoconditionsRuby
|
2
|
+
module Types
|
3
|
+
class BaseSha256Fulfillment < Fulfillment
|
4
|
+
def generate_hash
|
5
|
+
hasher = Utils::Hasher.new('sha256')
|
6
|
+
write_hash_payload(hasher)
|
7
|
+
hasher.digest
|
8
|
+
end
|
9
|
+
|
10
|
+
def write_hash_payload(_hasher)
|
11
|
+
raise 'Implement me'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module CryptoconditionsRuby
|
2
|
+
module Types
|
3
|
+
class Ed25519Fulfillment < Fulfillment
|
4
|
+
TYPE_ID = 4
|
5
|
+
FEATURE_BITMASK = 0x20
|
6
|
+
PUBKEY_LENGTH = 32
|
7
|
+
SIGNATURE_LENGTH = 64
|
8
|
+
FULFILLMENT_LENGTH = PUBKEY_LENGTH + SIGNATURE_LENGTH
|
9
|
+
|
10
|
+
attr_accessor :public_key, :signature
|
11
|
+
private :signature
|
12
|
+
def initialize(public_key = nil)
|
13
|
+
if public_key
|
14
|
+
public_key = Crypto::Ed25519VerifyingKey.new(public_key) if public_key.is_a?(String)
|
15
|
+
raise TypeError unless public_key.is_a?(Crypto::Ed25519VerifyingKey)
|
16
|
+
end
|
17
|
+
@public_key = public_key
|
18
|
+
@signature = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def write_common_header(writer)
|
22
|
+
writer.write_var_octet_string(public_key)
|
23
|
+
end
|
24
|
+
|
25
|
+
def sign(message, private_key)
|
26
|
+
sk = private_key
|
27
|
+
vk = sk.get_verifying_key
|
28
|
+
|
29
|
+
self.public_key = vk
|
30
|
+
|
31
|
+
self.signature = sk.sign(message, 'bytes')
|
32
|
+
end
|
33
|
+
|
34
|
+
def generate_hash
|
35
|
+
raise StandardError, 'Requires a public publicKey' unless public_key
|
36
|
+
public_key.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_payload(reader, *_args)
|
40
|
+
self.public_key = Crypto::Ed25519VerifyingKey.new(
|
41
|
+
Utils::Base58.encode(reader.read_octet_string(Ed25519Fulfillment::PUBKEY_LENGTH))
|
42
|
+
)
|
43
|
+
self.signature = reader.read_octet_string(Ed25519Fulfillment::SIGNATURE_LENGTH)
|
44
|
+
end
|
45
|
+
|
46
|
+
def write_payload(writer)
|
47
|
+
writer.tap do |w|
|
48
|
+
w.write_octet_string(public_key.to_s, Ed25519Fulfillment::PUBKEY_LENGTH)
|
49
|
+
w.write_octet_string(signature, Ed25519Fulfillment::SIGNATURE_LENGTH)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def calculate_max_fulfillment_length
|
54
|
+
Ed25519Fulfillment::FULFILLMENT_LENGTH
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_dict
|
58
|
+
{
|
59
|
+
'type' => 'fulfillment',
|
60
|
+
'type_id' => TYPE_ID,
|
61
|
+
'bitmask' => bitmask,
|
62
|
+
'public_key' => Utils::Base58.encode(public_key.to_s),
|
63
|
+
'signature' => (Utils::Base58.encode(signature) if signature)
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_dict(data)
|
68
|
+
self.public_key = Crypto::Ed25519VerifyingKey.new(data['public_key'])
|
69
|
+
self.signature = (Utils::Base58.decode(data['signature']) if data['signature'])
|
70
|
+
end
|
71
|
+
|
72
|
+
def validate(message: nil, **_kwargs)
|
73
|
+
return false unless message && signature
|
74
|
+
|
75
|
+
public_key.verify(signature, message, 'bytes')
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module CryptoconditionsRuby
|
2
|
+
CONDITION = 'condition'.freeze
|
3
|
+
FULFILLMENT = 'fulfillment'.freeze
|
4
|
+
|
5
|
+
module Types
|
6
|
+
class InvertedThresholdSha256Fulfillment < ThresholdSha256Fulfillment
|
7
|
+
TYPE_ID = 98
|
8
|
+
FEATURE_BITMASK = 0x09
|
9
|
+
|
10
|
+
def validate(message: nil, **kwargs)
|
11
|
+
!super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module CryptoconditionsRuby
|
2
|
+
module Types
|
3
|
+
class PreimageSha256Fulfillment < BaseSha256Fulfillment
|
4
|
+
TYPE_ID = 0
|
5
|
+
FEATURE_BITMASK = 0x03
|
6
|
+
|
7
|
+
attr_accessor :preimage
|
8
|
+
private :preimage
|
9
|
+
def initialize(preimage = nil)
|
10
|
+
if preimage && !preimage.respond_to?(:bytes)
|
11
|
+
raise TypeError, "Preimage must be bytes, was #{preimage.class.name}"
|
12
|
+
end
|
13
|
+
@preimage = preimage
|
14
|
+
end
|
15
|
+
|
16
|
+
def bitmask
|
17
|
+
FEATURE_BITMASK
|
18
|
+
end
|
19
|
+
|
20
|
+
def write_hash_payload(hasher)
|
21
|
+
unless hasher.is_a?(Utils::Hasher)
|
22
|
+
raise TypeError, 'hasher must be a Hasher instance'
|
23
|
+
end
|
24
|
+
unless preimage
|
25
|
+
raise TypeError, 'Could not calculate hash, no preimage provided'
|
26
|
+
end
|
27
|
+
hasher.write(preimage)
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_payload(reader, payload_size)
|
31
|
+
unless reader.is_a?(Utils::Reader)
|
32
|
+
raise TypeError, 'reader must be a Reader instance'
|
33
|
+
end
|
34
|
+
self.preimage = reader.read(payload_size)
|
35
|
+
end
|
36
|
+
|
37
|
+
def write_payload(writer)
|
38
|
+
unless [Utils::Writer, Utils::Predictor].include?(writer.class)
|
39
|
+
raise TypeError, 'writer must be a Writer instance'
|
40
|
+
end
|
41
|
+
raise TypeError, 'Preimage must be specified' unless preimage
|
42
|
+
writer.write(preimage)
|
43
|
+
writer
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_dict
|
47
|
+
{
|
48
|
+
'type' => 'fulfillment',
|
49
|
+
'type_id' => TYPE_ID,
|
50
|
+
'bitmask' => bitmask,
|
51
|
+
'preimage' => preimage
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_dict(data)
|
56
|
+
self.preimage = data['preimage'].encode
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate(**_kwargs)
|
60
|
+
true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|