dropzone_ruby 0.1

Sign up to get free protection for your applications and to get access to all the features.
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