dropzone_ruby 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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/Drop Zone - An Anonymous Peer-To-Peer Local Contraband Marketplace.pdf +0 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +69 -0
  6. data/README.md +62 -0
  7. data/bin/dropzone +487 -0
  8. data/dropzone-screenshot.jpg +0 -0
  9. data/dropzone_ruby.gemspec +31 -0
  10. data/lib/blockrio_ext.rb +52 -0
  11. data/lib/dropzone/buyer.rb +21 -0
  12. data/lib/dropzone/command.rb +488 -0
  13. data/lib/dropzone/communication.rb +43 -0
  14. data/lib/dropzone/connection.rb +312 -0
  15. data/lib/dropzone/invoice.rb +23 -0
  16. data/lib/dropzone/item.rb +160 -0
  17. data/lib/dropzone/listing.rb +64 -0
  18. data/lib/dropzone/message_base.rb +178 -0
  19. data/lib/dropzone/payment.rb +36 -0
  20. data/lib/dropzone/profile.rb +86 -0
  21. data/lib/dropzone/record_base.rb +34 -0
  22. data/lib/dropzone/seller.rb +21 -0
  23. data/lib/dropzone/session.rb +161 -0
  24. data/lib/dropzone/state_accumulator.rb +39 -0
  25. data/lib/dropzone/version.rb +4 -0
  26. data/lib/dropzone_ruby.rb +14 -0
  27. data/lib/veto_checks.rb +74 -0
  28. data/spec/bitcoin_spec.rb +115 -0
  29. data/spec/buyer_profile_spec.rb +279 -0
  30. data/spec/buyer_spec.rb +109 -0
  31. data/spec/command_spec.rb +353 -0
  32. data/spec/config.yml +5 -0
  33. data/spec/invoice_spec.rb +129 -0
  34. data/spec/item_spec.rb +294 -0
  35. data/spec/lib/fake_connection.rb +97 -0
  36. data/spec/listing_spec.rb +150 -0
  37. data/spec/payment_spec.rb +152 -0
  38. data/spec/seller_profile_spec.rb +290 -0
  39. data/spec/seller_spec.rb +120 -0
  40. data/spec/session_spec.rb +303 -0
  41. data/spec/sham/buyer.rb +5 -0
  42. data/spec/sham/invoice.rb +5 -0
  43. data/spec/sham/item.rb +8 -0
  44. data/spec/sham/payment.rb +13 -0
  45. data/spec/sham/seller.rb +7 -0
  46. data/spec/spec_helper.rb +49 -0
  47. metadata +267 -0
@@ -0,0 +1,64 @@
1
+ module Dropzone
2
+ class Listing < RecordBase
3
+ include StateAccumulator
4
+
5
+ attr_reader :txid, :create_item
6
+
7
+ self.message_types = 'ITUPDT'
8
+
9
+ state_attr :description, :price_currency, :price_in_units, :expiration_in
10
+
11
+ def initialize(txid)
12
+ @txid = txid
13
+
14
+ item = Item.find txid
15
+ @create_item = item if item && item.valid? && item.message_type == 'ITCRTE'
16
+
17
+ if create_item
18
+ attrs_from create_item
19
+
20
+ messages(start_block: create_item.block_height).reverse.each{ |item|
21
+ attrs_from item if item.create_txid == txid}
22
+ end
23
+ end
24
+
25
+ def found?
26
+ !@create_item.nil?
27
+ end
28
+
29
+ def expiration_at
30
+ create_item.block_height+expiration_in
31
+ end
32
+
33
+ def addr; from_create :sender_addr; end
34
+ def latitude; from_create :latitude; end
35
+ def longitude; from_create :longitude; end
36
+ def radius; from_create :radius; end
37
+
38
+ def seller_profile
39
+ @seller_profile ||= SellerProfile.new addr if addr
40
+ end
41
+
42
+ private
43
+
44
+ def from_create(attr)
45
+ create_item.send attr if create_item
46
+ end
47
+ end
48
+
49
+ class Listing::Validator < ValidatorBase
50
+ validate :must_have_active_seller
51
+ validate :must_have_created_item
52
+
53
+ def must_have_active_seller(listing)
54
+ errors.add :seller_profile, "invalid or missing" if (
55
+ listing.seller_profile.nil? || !listing.seller_profile.valid? ||
56
+ !listing.seller_profile.active? )
57
+ end
58
+
59
+ def must_have_created_item(listing)
60
+ errors.add :create_item, "invalid or missing" unless (
61
+ listing.create_item && listing.create_item.valid? )
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,178 @@
1
+ module Dropzone
2
+ module MessageValidations
3
+ def self.included(base)
4
+ base.validates :receiver_addr, presence: true
5
+ end
6
+ end
7
+
8
+ module ProfileValidations
9
+ def self.included(base)
10
+ base.validates :receiver_addr, equals_attribute: { attribute: :sender_addr },
11
+ unless: "self.transfer_pkey", if: "self.sender_addr"
12
+
13
+ base.validates :transfer_pkey, equals_attribute: { attribute: :receiver_addr,
14
+ unless: "self.transfer_pkey.nil? || self.transfer_pkey == 0" }
15
+ end
16
+ end
17
+
18
+ module BillingValidations
19
+ def self.included(base)
20
+ base.validates :receiver_addr, doesnt_equal_attribute: { attribute: :sender_addr },
21
+ if: "self.sender_addr"
22
+ end
23
+ end
24
+
25
+ class MessageBase < RecordBase
26
+ DEFAULT_TIP = 20_000
27
+
28
+ attr_reader :receiver_addr, :sender_addr, :message_type, :block_height, :txid
29
+
30
+ def initialize(attrs = {})
31
+ data = attrs.delete(:data)
32
+
33
+ attrs.merge(data_hash_from_hex(data)).each do |attr, value|
34
+ instance_variable_set '@%s' % attr, value
35
+ end
36
+ end
37
+
38
+ def save!(private_key)
39
+ self.blockchain.save! to_transaction, private_key
40
+ end
41
+
42
+ def to_transaction
43
+ {receiver_addr: receiver_addr, data: data_to_hex,
44
+ tip: MessageBase.default_tip }
45
+ end
46
+
47
+ def data_to_hex
48
+ data_to_hash.inject(message_type.dup) do |ret, (key, value)|
49
+ value_hex = case
50
+ when value.nil?
51
+ nil
52
+ when self.class.is_attr_int?(key)
53
+ Bitcoin::Protocol.pack_var_int(value.to_i)
54
+ when self.class.is_attr_pkey?(key)
55
+ Bitcoin::Protocol.pack_var_string(
56
+ (value == 0) ? 0.chr :
57
+ [anynet_for_address(:hash160_from_address, value)].pack('H*'))
58
+ else
59
+ Bitcoin::Protocol.pack_var_string([value.to_s].pack('a*'))
60
+ end
61
+
62
+ (value_hex.nil?) ? ret :
63
+ ret << Bitcoin::Protocol.pack_var_string(key.to_s) << value_hex
64
+ end
65
+ end
66
+
67
+ def data_to_hash
68
+ self.class.message_attribs.inject({}) do |ret , (short, full)|
69
+ ret.merge(short => self.send(full))
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def anynet_for_address(method, addr)
76
+ start_network = Bitcoin.network_name
77
+
78
+ begin
79
+ Bitcoin.network = (/\A1/.match addr) ? :bitcoin : :testnet3
80
+
81
+ return Bitcoin.send(method, addr)
82
+ ensure
83
+ Bitcoin.network = start_network
84
+ end
85
+ end
86
+
87
+ def data_hash_from_hex(data)
88
+ return {} unless /\A(.{6})(.*)/m.match data
89
+
90
+ message_type, pairs, data = $1, $2, {}
91
+
92
+ while(pairs.length > 0) do
93
+ short_key, pairs = Bitcoin::Protocol.unpack_var_string(pairs)
94
+
95
+ value, pairs = (self.class.is_attr_int? short_key.to_sym) ?
96
+ Bitcoin::Protocol.unpack_var_int(pairs) :
97
+ Bitcoin::Protocol.unpack_var_string(pairs)
98
+
99
+ if self.class.is_attr_pkey?(short_key.to_sym) && value
100
+ value = (value == 0.chr) ? 0 :
101
+ anynet_for_address(:hash160_to_address, value.unpack('H*')[0])
102
+ end
103
+
104
+ full_key = self.class.message_attribs[short_key.to_sym]
105
+ data[full_key] = value
106
+ end
107
+
108
+ data
109
+ end
110
+
111
+ class << self
112
+ attr_writer :default_tip
113
+
114
+ def default_tip
115
+ @default_tip || DEFAULT_TIP
116
+ end
117
+
118
+ def message_attribs
119
+ @message_attribs
120
+ end
121
+
122
+ def is_attr_int?(attr)
123
+ @message_integers && @message_integers.include?(attr)
124
+ end
125
+
126
+ def is_attr_pkey?(attr)
127
+ @message_pkeys && @message_pkeys.include?(attr)
128
+ end
129
+
130
+ def message_type(type)
131
+ @types_include ||= [type]
132
+
133
+ define_method(:message_type){ type }
134
+ end
135
+
136
+ def attr_message(attribs)
137
+ @message_attribs ||= {}
138
+ @message_attribs.merge! attribs
139
+
140
+ attribs.each{ |short_attr, full_attr| attr_reader full_attr }
141
+ end
142
+
143
+ def attr_message_int(attribs)
144
+ @message_integers ||= []
145
+ @message_integers += attribs.keys
146
+
147
+ attr_message attribs
148
+ end
149
+
150
+ def attr_message_pkey(attribs)
151
+ @message_pkeys ||= []
152
+ @message_pkeys += attribs.keys
153
+
154
+ attr_message attribs
155
+ end
156
+
157
+ def find(txid)
158
+ tx = RecordBase.blockchain.tx_by_id txid
159
+ tx ? self.new(tx) : nil
160
+ end
161
+
162
+ def types_include?(type)
163
+ @types_include && @types_include.include?(type)
164
+ end
165
+
166
+ def new_message_from(tx)
167
+ @messages ||= ObjectSpace.each_object(Class).select {|klass|
168
+ klass < Dropzone::MessageBase }
169
+
170
+ if /\A([a-z0-9]{6})/i.match tx[:data]
171
+ message_klass = @messages.find{|klass| klass.types_include? $1}
172
+ (message_klass) ? message_klass.new(tx) : nil
173
+ end
174
+ end
175
+
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,36 @@
1
+ module Dropzone
2
+ class Payment < MessageBase
3
+ attr_message d: :description, t: :invoice_txid
4
+ attr_message_int q: :delivery_quality, p: :product_quality,
5
+ c: :communications_quality
6
+
7
+ def invoice
8
+ @invoice ||= Invoice.find invoice_txid if invoice_txid
9
+ end
10
+
11
+ message_type 'INPAID'
12
+ end
13
+
14
+ class Payment::Validator < ValidatorBase
15
+ include MessageValidations
16
+ include BillingValidations
17
+
18
+ validates :message_type, format: /\AINPAID\Z/
19
+
20
+ validates_if_present :description, is_string: true
21
+ validates_if_present :invoice_txid, is_string: true
22
+
23
+ [:delivery_quality,:product_quality,:communications_quality ].each do |attr|
24
+ validates_if_present attr, integer: true, inclusion: 0..8
25
+ end
26
+
27
+ validate :must_have_corresponding_invoice
28
+
29
+ def must_have_corresponding_invoice(payment)
30
+ invoice = payment.invoice
31
+
32
+ errors.add :invoice_txid, "can't be found" if ( invoice.nil? ||
33
+ !invoice.valid? || (invoice.sender_addr != payment.receiver_addr) )
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,86 @@
1
+ module Dropzone
2
+ class Profile < RecordBase
3
+ include StateAccumulator
4
+
5
+ attr_reader :addr, :transfer_pkey, :prior_profile
6
+
7
+ def initialize(addr)
8
+ @addr = addr
9
+
10
+ messages.reverse.each_with_index do |seller, i|
11
+ # There is a bit of extra logic if the seller profile was transferred
12
+ # from elsewhere
13
+ if i == 0 && seller.transfer_pkey
14
+ # Load the profile from the prior address and pop it off the stack
15
+ @prior_profile = self.class.new seller.sender_addr
16
+
17
+ # It's possible the prior profile was invalid
18
+ break unless @prior_profile.valid?
19
+
20
+ # And it's possible the prior profile was deactivated or not
21
+ # transferred to us:
22
+ break unless @prior_profile.transfer_pkey == addr
23
+
24
+ attrs_from @prior_profile
25
+ else
26
+ # This prevents a second inbound transfer from happening:
27
+ next if seller.transfer_pkey == addr
28
+
29
+ # In case they transferred away :
30
+ @transfer_pkey = seller.transfer_pkey
31
+
32
+ attrs_from seller
33
+ end
34
+
35
+ break if @transfer_pkey
36
+ end
37
+ end
38
+
39
+ def closed?; (@transfer_pkey == 0); end
40
+ def active?; @transfer_pkey.nil? end
41
+ def found?; messages.length > 0; end
42
+ end
43
+
44
+ module ValidateProfile
45
+ def self.included(base)
46
+ base.validate :must_have_declaration
47
+ base.validate :prior_profile_is_valid
48
+ base.validate :prior_profile_transferred_to_us
49
+ end
50
+
51
+ def must_have_declaration(profile)
52
+ errors.add :addr, "profile not found" unless profile.messages.length > 0
53
+ end
54
+
55
+ def prior_profile_is_valid(profile)
56
+ if profile.prior_profile && !profile.prior_profile.valid?
57
+ errors.add :prior_profile, "invalid"
58
+ end
59
+ end
60
+
61
+ def prior_profile_transferred_to_us(profile)
62
+ if profile.prior_profile && profile.prior_profile.transfer_pkey != profile.addr
63
+ errors.add :prior_profile, "invalid transfer or closed"
64
+ end
65
+ end
66
+ end
67
+
68
+ # A profile is different than a Seller message, as it's the concatenation of
69
+ # Seller messages, and is missing the transfer and sender_addr.
70
+ class SellerProfile < Profile
71
+ self.message_types = 'SLUPDT'
72
+
73
+ state_attr :description, :alias, :communications_pkey
74
+
75
+ class Validator < ValidatorBase; include ValidateProfile; end
76
+ end
77
+
78
+ class BuyerProfile < Profile
79
+ self.message_types = 'BYUPDT'
80
+
81
+ state_attr :description, :alias
82
+
83
+ class Validator < ValidatorBase; include ValidateProfile; end
84
+ end
85
+
86
+ end
@@ -0,0 +1,34 @@
1
+ module Dropzone
2
+ class ValidatorBase
3
+ IS_STRING = /\A.+\Z/
4
+
5
+ include Veto.validator
6
+
7
+ def self.validates_if_present(attr, options)
8
+ validates attr, options.merge({unless: "self.%s.nil?" % attr.to_s})
9
+ end
10
+ end
11
+
12
+ # This lets us set connection parameters across the entire library.
13
+ # A cattr_inheritable-esque implementation might be worth adding at some point.
14
+ class RecordBase
15
+ def blockchain;
16
+ RecordBase.blockchain
17
+ end
18
+
19
+ def valid?; validator.valid? self; end
20
+
21
+ def errors
22
+ validator.valid? self
23
+ validator.errors
24
+ end
25
+
26
+ private
27
+
28
+ def validator; @validator ||= self.class.const_get(:Validator).new; end
29
+
30
+ class << self
31
+ attr_accessor :blockchain
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ module Dropzone
2
+ class Seller < MessageBase
3
+ message_type 'SLUPDT'
4
+
5
+ attr_message d: :description, a: :alias
6
+ attr_message_pkey t: :transfer_pkey, p: :communications_pkey
7
+ end
8
+
9
+ class Seller::Validator < ValidatorBase
10
+ include MessageValidations
11
+ include ProfileValidations
12
+
13
+ validates :message_type, format: /\ASLUPDT\Z/
14
+
15
+ validates_if_present :description, is_string: true
16
+ validates_if_present :alias, is_string: true
17
+
18
+ validates_if_present :communications_pkey, is_pkey: true
19
+ validates_if_present :transfer_pkey, is_pkey: true
20
+ end
21
+ end
@@ -0,0 +1,161 @@
1
+ module Dropzone
2
+ class Session
3
+ CIPHER_ALGORITHM = 'AES-256-CBC'
4
+
5
+ class Unauthenticated < StandardError; end
6
+ class MissingReceiver < StandardError; end
7
+ class InvalidWithReceiver < StandardError; end
8
+ class DerAlreadyExists < StandardError; end
9
+ class InvalidCommunication < StandardError; end
10
+ class SessionInvalid < StandardError; end
11
+
12
+ attr_accessor :priv_key, :receiver_addr, :session_key, :with, :end_block
13
+
14
+ def initialize(priv_key, session_secret, options = {})
15
+ @priv_key, @session_key = priv_key, OpenSSL::BN.new(session_secret, 16)
16
+ @end_block = options[:end_block] if options.has_key? :end_block
17
+
18
+ # Either you attach to an existing session, or create a new one
19
+ case
20
+ when options.has_key?(:receiver_addr)
21
+ # New Session:
22
+ @receiver_addr = options[:receiver_addr]
23
+ when options.has_key?(:with)
24
+ # Existing Session:
25
+ raise InvalidWithReceiver unless options[:with].receiver_addr == sender_addr
26
+ @with = options[:with]
27
+ @receiver_addr = @with.sender_addr
28
+ else
29
+ raise MissingReceiver
30
+ end
31
+ end
32
+
33
+ def blockchain; self.class.blockchain; end
34
+ def sender_addr; blockchain.privkey_to_addr priv_key; end
35
+
36
+ # Iv passing is supported only for the purpose of making tests completely
37
+ # deterministic
38
+ def send(contents, iv = nil)
39
+ raise Unauthenticated unless authenticated?
40
+
41
+ # Cipher Setup:
42
+ aes = OpenSSL::Cipher::Cipher.new CIPHER_ALGORITHM
43
+ aes.encrypt
44
+
45
+ iv ||= aes.random_iv
46
+ aes.iv = iv
47
+
48
+ aes.key = symm_key
49
+
50
+ # Encrypt Time:
51
+ cipher = aes.update contents
52
+ cipher << aes.final
53
+
54
+ communicate! contents: cipher.to_s, iv: iv
55
+ end
56
+
57
+ alias :<< :send
58
+
59
+ def authenticate!(der = nil)
60
+ is_init = (communication_init.nil? || authenticated?)
61
+
62
+ # If we're already authenticated, we'll try to re-initialize. Presumably
63
+ # one would want to do this if they lost a secret key, or that key were
64
+ # somehow compromised
65
+ if is_init
66
+ dh = OpenSSL::PKey::DH.new(der || 1024)
67
+ else
68
+ raise DerAlreadyExists unless der.nil?
69
+ dh = OpenSSL::PKey::DH.new with.der
70
+ end
71
+
72
+ dh.priv_key = session_key
73
+ dh.generate_key!
74
+
75
+ communicate! session_pkey: [dh.pub_key.to_s(16)].pack('H*'),
76
+ der: (is_init) ? dh.public_key.to_der : nil
77
+ end
78
+
79
+ def symm_key
80
+ return @symm_key if @symm_key
81
+
82
+ # If we can't compute, then it's ok to merely indicate this:
83
+ return nil unless communication_init && communication_auth
84
+
85
+ dh = OpenSSL::PKey::DH.new communication_init.der
86
+ dh.priv_key = session_key
87
+ dh.generate_key!
88
+
89
+ @symm_key = dh.compute_key OpenSSL::BN.new(
90
+ their_pkey.session_pkey.unpack('H*').first, 16)
91
+ end
92
+
93
+ def authenticated?
94
+ communication_init && communication_auth
95
+ end
96
+
97
+ def communication_init
98
+ # NOTE that this returns the newest initialization
99
+ commun_messages.find(&:is_init?)
100
+ end
101
+
102
+ # This is the response to the init
103
+ def communication_auth
104
+ # NOTE that this returns the newest auth, or nil if we encounter an init
105
+ commun_messages.find{|c|
106
+ break if c.is_init?
107
+ c.is_auth? }
108
+ end
109
+
110
+ def their_pkey
111
+ [communication_init, communication_auth].find{|c|
112
+ c.sender_addr == receiver_addr && c.receiver_addr == sender_addr }
113
+ end
114
+
115
+ def communications
116
+ if authenticated?
117
+ communications = commun_messages(
118
+ start_block: communication_init.block_height ).reject(&:is_auth?)
119
+ communications.each{|c| c.symm_key = symm_key}
120
+ communications
121
+ else
122
+ []
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ # Addr's of who this conversation is between
129
+ def between
130
+ [sender_addr, receiver_addr]
131
+ end
132
+
133
+ def commun_messages(options = {})
134
+ options[:type] = 'COMMUN'
135
+ options[:end_block] = @end_block if @end_block
136
+ options[:between] = between
137
+ blockchain.messages_by_addr(sender_addr, options)
138
+ end
139
+
140
+ def communicate!(attrs)
141
+ comm = Communication.new( {receiver_addr: receiver_addr,
142
+ sender_addr: sender_addr}.merge(attrs) )
143
+
144
+ raise InvalidCommunication unless comm.valid?
145
+
146
+ comm.save! priv_key
147
+ end
148
+
149
+ class << self
150
+ attr_writer :blockchain
151
+
152
+ def blockchain
153
+ @blockchain || Dropzone::RecordBase.blockchain
154
+ end
155
+
156
+ def all(addr)
157
+ blockchain.messages_by_addr(addr, type: 'COMMUN').find_all(&:is_init?)
158
+ end
159
+ end
160
+ end
161
+ end