strongboxio 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,207 @@
1
+ require 'openssl' # for decryption
2
+ require 'zlib' # for decompression
3
+ require 'base64' # for decoding
4
+ require 'rubygems' if defined?RUBY_VERSION && RUBY_VERSION =~ /^1.8/ # for requiring gem dependency in Ruby 1.8
5
+ require 'nokogiri' # for xml parsing
6
+
7
+ class Strongboxio
8
+
9
+ VERSION = '0.1.0'
10
+
11
+ STRONGBOX_VERSION = 3
12
+ VERSION_LENGTH = 1
13
+ SALT_LENGTH = 64
14
+ IV_LENGTH = 16
15
+ KEY_LENGTH = 32
16
+ RANDOM_VALUE_LENGTH = 64
17
+ CIPHER = 'AES-256-CBC'
18
+ PAYLOAD_SCHEMA_VERSION = '2.4'
19
+ UNIX_EPOCH_IN_100NS_INTERVALS = 621355968000000000 # .NET time format: number of 100-nanosecond intervals since .NET epoch: January 1, 0001 at 00:00:00.000 (midnight)
20
+
21
+ def self.resembles_base64?(string)
22
+ string.length % 4 == 0 && string =~ /^[A-Za-z0-9+\/=]+\Z/
23
+ end
24
+
25
+ def self.decrypt(sbox_filename, password)
26
+ # open the xml file
27
+ f = File.open(sbox_filename)
28
+ data = Nokogiri::XML(f)
29
+ f.close
30
+
31
+ # extract the Data node
32
+ data = data.xpath('//Data').text
33
+ base64_error_msg = 'expected Base64 encoded byte string from the StrongBox.Payload.Data element of the xml of a Strongbox file'
34
+ raise "#{base64_error_msg}, but got nothing" if data.length == 0
35
+ raise "#{base64_error_msg}, but it does not resemble Base64" unless self.resembles_base64?(data)
36
+
37
+ data = Base64.decode64(data)
38
+
39
+ #version = data.getbyte(0) # ruby 1.9
40
+ version = data.bytes.to_a[0] # ruby 1.8 friendly
41
+ raise "expected version number #{STRONGBOX_VERSION}, but got #{version}" unless version == STRONGBOX_VERSION
42
+
43
+ salt = data.slice(1, SALT_LENGTH)
44
+ raise "expected salt length #{SALT_LENGTH}, but got #{salt.length}" unless salt.length == SALT_LENGTH
45
+
46
+ iv = data.bytes.to_a.slice((1+64), IV_LENGTH).pack('C*')
47
+ raise "expected iv length #{IV_LENGTH}, but got #{iv.length}" unless iv.length == IV_LENGTH
48
+
49
+ key = Digest::SHA256.digest(salt + password)
50
+ raise "expected key length #{KEY_LENGTH}, but got #{key.length}" unless key.length == KEY_LENGTH
51
+
52
+ # prepare for decryption
53
+ d = OpenSSL::Cipher.new(CIPHER)
54
+ d.decrypt
55
+ d.key = key
56
+ d.iv = iv
57
+
58
+ # decrypt the portion beyond the header
59
+ begin
60
+ data = '' << d.update(data.slice((VERSION_LENGTH+SALT_LENGTH+IV_LENGTH)..-1)) << d.final
61
+ rescue => e
62
+ raise "Error decrypting. You probably entered the password incorrectly. Specific error: #{e}"
63
+ end
64
+
65
+ # decompress the portion beyond the random value
66
+ #z = Zlib::Inflate.new
67
+ #z = Zlib::Inflate.new(-Zlib::BEST_COMPRESSION) # works for Strongbox
68
+ z = Zlib::Inflate.new(-Zlib::MAX_WBITS) # works for Strongbox!
69
+ #z = Zlib::Inflate.new(Zlib::MAX_WBITS) # works for roundtrip
70
+ data = z.inflate(data.slice(RANDOM_VALUE_LENGTH..-1))
71
+ z.finish
72
+ z.close
73
+
74
+ data
75
+ end
76
+
77
+ def self.render(decrypted_sbox, continue_despite_unexpected_payload_schema_version=false)
78
+ data = Nokogiri::XML(decrypted_sbox)
79
+
80
+ payload_schema_version = data.xpath('//Payload').xpath('SchemaVersion').text
81
+ unless payload_schema_version == PAYLOAD_SCHEMA_VERSION
82
+ raise "expected schema version #{PAYLOAD_SCHEMA_VERSION}, but got #{payload_schema_version}" unless continue_despite_unexpected_payload_schema_version
83
+ end
84
+
85
+ mt = data.xpath('//Payload').xpath('PayloadInfo').xpath('MT').text
86
+ puts mt
87
+
88
+ data.xpath('//PayloadData').each { |payload_data|
89
+ payload_data.xpath('//SBE').each { |entity|
90
+ puts
91
+ name = entity.xpath('N').text
92
+ puts "#{name}" if name.length > 0
93
+ description = entity.xpath('D').text
94
+ puts "#{description}" if description.length > 0
95
+ tags = entity.xpath('T').text
96
+ puts "#{tags}" if tags.length > 0
97
+ ce = entity.xpath('CE')
98
+ ce.xpath('TFE').each { |tfe|
99
+ name = tfe.xpath('N').text
100
+ puts "#{name}:" if name.length > 0
101
+ content = tfe.xpath('C').text
102
+ puts "#{content}" if content.length > 0
103
+ }
104
+ }
105
+ }
106
+ end
107
+
108
+ def assemble(decrypted_sbox, continue_despite_unexpected_payload_schema_version=false)
109
+ data = Nokogiri::XML(decrypted_sbox)
110
+
111
+ payload_schema_version = data.xpath('//Payload').xpath('SchemaVersion').text
112
+ unless payload_schema_version == PAYLOAD_SCHEMA_VERSION
113
+ raise "expected schema version #{PAYLOAD_SCHEMA_VERSION}, but got #{payload_schema_version}" unless continue_despite_unexpected_payload_schema_version
114
+ end
115
+
116
+ sbox = {}
117
+
118
+ mt = data.xpath('//Payload').xpath('PayloadInfo').xpath('MT').text
119
+ sbox['MT'] = mt
120
+
121
+ data.xpath('//PayloadData').each { |payload_data|
122
+
123
+ sbox['PayloadData'] = []
124
+
125
+ payload_data.xpath('//SBE').each_with_index { |strongbox_entity, sbe_index|
126
+
127
+ sbox['PayloadData'][sbe_index] = {}
128
+
129
+ sbe_mt = strongbox_entity.attr('MT') # ModifiedTimestamp.Ticks
130
+ sbox['PayloadData'][sbe_index]['MT'] = sbe_mt if sbe_mt.length > 0
131
+
132
+ sbe_ct = strongbox_entity.attr('CT') # CreatedTimestamp.Ticks
133
+ sbox['PayloadData'][sbe_index]['CT'] = sbe_ct if sbe_ct.length > 0
134
+
135
+ sbe_ac = strongbox_entity.attr('AC') # accessCount
136
+ sbox['PayloadData'][sbe_index]['AC'] = sbe_ac if sbe_ac.length > 0
137
+
138
+ sbe_name = strongbox_entity.xpath('N').text
139
+ sbox['PayloadData'][sbe_index]['N'] = sbe_name if sbe_name.length > 0
140
+
141
+ sbe_description = strongbox_entity.xpath('D').text
142
+ sbox['PayloadData'][sbe_index]['D'] = sbe_description if sbe_description.length > 0
143
+
144
+ sbe_tags = strongbox_entity.xpath('T').text
145
+ sbox['PayloadData'][sbe_index]['T'] = sbe_tags if sbe_tags.length > 0
146
+
147
+ child_entity = strongbox_entity.xpath('CE')
148
+ if child_entity.length > 0
149
+ sbox['PayloadData'][sbe_index]['CE'] = []
150
+
151
+ child_entity.xpath('TFE').each_with_index { |text_field_entity, ce_index|
152
+
153
+ sbox['PayloadData'][sbe_index]['CE'][ce_index] = {}
154
+
155
+ tfe_name = text_field_entity.xpath('N').text
156
+ sbox['PayloadData'][sbe_index]['CE'][ce_index]['N'] = tfe_name if tfe_name.length > 0
157
+
158
+ tfe_content = text_field_entity.xpath('C').text
159
+ sbox['PayloadData'][sbe_index]['CE'][ce_index]['C'] = tfe_content if tfe_content.length > 0
160
+ }
161
+ end
162
+ }
163
+ }
164
+
165
+ sbox
166
+ end
167
+
168
+ def render(verbose=false)
169
+ puts sbox['MT']
170
+
171
+ sbox['PayloadData'].each { |payload_data|
172
+ puts
173
+ puts payload_data['N'] unless payload_data['N'].nil?
174
+ puts payload_data['D'] unless payload_data['D'].nil?
175
+ puts payload_data['T'] unless payload_data['T'].nil?
176
+ payload_data['CE'].each { |strongbox_entity|
177
+ puts strongbox_entity['N'] + ': ' unless strongbox_entity['N'].nil?
178
+ puts strongbox_entity['C'] unless strongbox_entity['C'].nil?
179
+ }
180
+ puts "Access Count: #{payload_data['AC']}" if !payload_data['AC'].nil? && verbose
181
+ puts "#{convert_time_from_dot_net_epoch(payload_data['MT'].to_i)} (modify time)" if !payload_data['MT'].nil? && verbose
182
+ puts "#{convert_time_from_dot_net_epoch(payload_data['CT'].to_i)} (create time)" if !payload_data['CT'].nil? && verbose
183
+ }
184
+ end
185
+
186
+ #attr_accessor :decrypted_sbox
187
+ attr_accessor :sbox
188
+
189
+ # create an instance of Strongbox
190
+ def initialize(decrypted_sbox, continue_despite_unexpected_payload_schema_version=false)
191
+ super()
192
+ #self.decrypted_sbox = decrypted_sbox
193
+ self.sbox = assemble(decrypted_sbox, continue_despite_unexpected_payload_schema_version)
194
+ end
195
+
196
+ private
197
+
198
+ def convert_time_from_dot_net_epoch(t)
199
+ Time.at((t-UNIX_EPOCH_IN_100NS_INTERVALS)*1e-7).utc.getlocal
200
+ end
201
+ end
202
+
203
+ #class String
204
+ # def resembles_base64?
205
+ # self.length % 4 == 0 && self =~ /^[A-Za-z0-9+\/=]+\Z/
206
+ # end
207
+ #end
@@ -0,0 +1,24 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <StrongBox>
3
+ <SchemaVersion>2.1</SchemaVersion>
4
+ <Metadata>
5
+ <SchemaVersion>2.2</SchemaVersion>
6
+ <SBOXID>2fe22f4d-bd41-476a-8159-c6e0b6487f7d</SBOXID>
7
+ <SID>1aefeb7f-87fb-4c12-9587-78f2354c3a70</SID>
8
+ <CreateDate>129985312023925270</CreateDate>
9
+ <ModifiedDate>129985323759179228</ModifiedDate>
10
+ <Name>strongbox gem test</Name>
11
+ <Description>Strongbox Ruby gem test box</Description>
12
+ <PassphaseHint />
13
+ <LocalModifiedDate>129985312023925280</LocalModifiedDate>
14
+ <PayloadIdentity>sQgzfRSQEkCJo25tVi47Q</PayloadIdentity>
15
+ <PayloadSize>1217</PayloadSize>
16
+ <PayloadType />
17
+ <PayloadItemCount>4</PayloadItemCount>
18
+ <PayloadBaseRevisionTag>129985312024788056-129985312024788056</PayloadBaseRevisionTag>
19
+ </Metadata>
20
+ <Payload>
21
+ <SchemaVersion>2.1</SchemaVersion>
22
+ <Data>AxHLk57HTQaFzAHatF+lFg++t44Dfwr78cKGHzQB1+eWbKL9HVTZK9T/omI7P9az8gvJA//5CkYGz6HcQS7Sg/U5PTNqU4u2EZhkdaRUK6KooHux/u9HBF27RXy/8shiJbS6FN7EPew/8zzuZRe00WzOcoKELmHvbeTgZG0Te2Mtox+PfZMYEvRPjbA2eI70KwzlzSuoHbmyECKo36x9RTAHTeQTEXMfkcMU6D0ONsbMmNoJkC3TqIX5bcYLQ1FxQpwvB/AbtfyCLPk9Zxhmm0ihEsBfcPqxU9kaVkCBOfOyxFyvueEdFgEs1PxYJ571huJpyprCcQKcarr3yvMIzkn3rJKO6s9F0gR8PqdrBBO3PxKeZO6sJnpEq0WIuSYaY2wFGXr0UvK/Hid3hUHwVEjfbiVVJmXFmRHmEnJD7Q2EctX5Ju20ESYSXy3f3r/W3RTaR8ZwLVPiyroV2EjOZrhI0pMhXLZaOhszVA4alC7djphKRZkh1zg39pnMaPNYACW0rlAnFz5zk74Uez64xiWI352i89jfjj5R16blBlxq8FjJJVSlUxJJNJwnlbG744AXxd1ygI56koztmbwY1VbAOmr4FiX9JtVLq6a6gVKv3sy3eKlxKXd41jhOTNIKnu0Nh36TxXs64Q2BdozKnQvCN9BYelawWPQOp3viQ95LlruiQQEnzzmcGuZw33ny1QfxVFuz/Gd+i50eg++urf/woprI74OtXANWWhN6chSP29Gz8y76/zYZoYAerZxAVPuh+elW9Xw3fjCwHk33/tkOvVcKQb/LCcFZdaawh6gdYfRJTRetQYKIWdKn7QuYhZLKYXMcJ1BBrDnJx0QKCN8kwNggzRlfYtgzz07t1BQXL+5F9m18KQfXbGR4HpI3INwWMFA61mFKRMih0cORjbVwfISJIZfRp/oe1Ut8r4+cZbTwdmPEnUrMiOSQLk0gUk7DknnCfEPKLjOFz+DAwhqSfosNKUkV1AB+GDzI2JfR5P3SMLh77/CLN1Adx/tXH46A2K6D5JXPVGUR9mhMD9hCH6v34dRNkfBSD0EN2VHMAkVAWrDm4UiKf3uO6k51Q7IZ0vcp+QSZdcV98rhAVgpx79S3MPd8+riamxuZFHe/5Ao1p4JW1FajPxbZnh4LhQNdN8vTSuywOaefN7Mph4X3WEJ5cOYcJm5MdYQDx9t1DSdY1l+tuDHW+8LWpqicVN0JWNADPFCyQwoAQMzX1ax3zq8zXofI+MaJOxaxiEu/pJDPce07fmf/W6Xxcw8Uugm4bZE7pxcTRrrhXZ4WYBmHyxRfNtGZyw8K385S8wRW93FPUnwHRDUyDoRN+w9FwAxWPmywM2Z8RyfyN6frQ0j4roB9YD2Yt1YuY6/yoUDiDxsLdaBYEfre+A26VlkC3zUy/M64yPe6J/CO1hBDVkwxJ8hmkiw2QtHROIz80hDvprecVWjzwLNT+ddT+oXf5nEFREZP8IMxzDNdrRcydQjpJHViWvn4fLxhF72KcTLKM0yfokpbsGnC3X63dcHFHG0KTiTcfnBNqqdi9WyhD12Jm62bAfgJVV+GFJHXSHdTGCa1REmqfuKeSIVBCxaCPSNG1M/WbEdAfJNjGj980ZQ=</Data>
23
+ </Payload>
24
+ </StrongBox>
@@ -0,0 +1,3 @@
1
+ require 'test/unit'
2
+ require 'strongboxio'
3
+
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+ require 'test_helper'
3
+
4
+ class StrongboxioTest < Test::Unit::TestCase
5
+
6
+ FILENAME = 'test/2fe22f4d-bd41-476a-8159-c6e0b6487f7d.sbox'
7
+ PASSWORD = 'pWY4ic9q'
8
+
9
+ def test_decrypt_from_file
10
+ expect='<Payload><SchemaVersion>2.4</SchemaVersion><PayloadInfo><SchemaVersion>1.0</SchemaVersion><IP>PyOQP3GmNEA3T5BjBOiPg,DU4Qm4btjyejD1TYGoUjQ,nC9jzQ4Tv3RsrfxHzghe4A,YlVFCZO1FqBT91H0M3XmQA,WIVON6o61vebxLKJhXKQ,sQgzfRSQEkCJo25tVi47Q</IP><MT>2012-11-27T23:29:23.18873Z</MT><SD>xJ2VL0VourNV3a7cOJkinAMkVVEJJahV17MlbE5x8w</SD></PayloadInfo><PayloadData><T /><CE><SBE ID="7ijIF8xabBZjKsEKpUFnag" II="4" MT="634896555753103810" CT="634896555753102130" AC="0"><N>VanCity bank info</N><D>credit card, debit card, online banking</D><T>Credit, Debit, Card, Online, Banking</T><CE><TFE ID="K87JOu1lge4X7SSKE9elDQ" II="4" MT="634896555753103820"><N>credit card number</N><C>1234 5678 9012 3456</C></TFE><TFE ID="PTsGYmJb7f1B8nIquZfAMQ" II="4" MT="634896555753104130"><N>credit card expiration</N><C>11/2012</C></TFE><TFE ID="sfRgujycbbKIad58FkC1vg" II="4" MT="634896555753104380"><N>credit card verification value</N><C>123</C></TFE><TFE ID="bK7Vba0t2oupLiRGjEkt5Q" II="4" MT="634896555753104690"><N>debit card PIN</N><C>1234</C></TFE><TFE ID="2j9hKQJqCvbrVpmfRqJVA" II="4" MT="634896555753104970"><N>online banking password</N><C>qwerty</C></TFE></CE></SBE><SBE ID="L35bmTNw34lQnA70hmEVmg" II="3" MT="634896550747551430" CT="634896550747549900" AC="0"><N>fake Gmail account</N><D>used for Stackoverflow</D><T>Fake Gmail Stackoverflow</T><CE><TFE ID="Zmjyh5cMoj5s1AuH18EJA" II="3" MT="634896550747551440"><N>username</N><C>ima_fake@gmail.com</C></TFE><TFE ID="bOudR7nlwP40bo6V23jw" II="3" MT="634896550747551750"><N>password</N><C>password1</C></TFE><TFE ID="klbjFvsyyDEs956gkduKg" II="3" MT="634896550747552080"><N>gender</N><C>other</C></TFE><TFE ID="ckdoh9oM478UliFoeg" II="3" MT="634896550747552370"><N>backup</N><C>bart_simpson@gmail.com</C></TFE></CE></SBE><SBE ID="SwH6NoXPhg7qXLOUlDQyQ" II="2" MT="634896545666557670" CT="634896545666556630" AC="0"><N>just a name</N><D></D><T /><CE><TFE ID="u9LEZ2H2jQlzqc7a5vD2iA" II="2" MT="634896545666557680"><N /><C /></TFE></CE></SBE><SBE ID="ZoPVBFNFdOlRLT0FLO9BVQ" II="5" MT="634896557631887530" CT="634896545082402010" AC="0"><N>test item name</N><D>test item description</D><T>Test, Item, Tag1, Tag2, Tag3</T><CE><TFE ID="NyRRL7tmBMr0VrDoyzfwg" II="1" MT="634896545082426060"><N>test field name</N><C>test secure text</C></TFE></CE></SBE></CE></PayloadData></Payload>'
11
+
12
+ assert_equal expect,
13
+ Strongboxio.decrypt(FILENAME, PASSWORD)
14
+ end
15
+
16
+ def test_strongboxio_instantiation
17
+ # #<Strongboxio:0x1011b2e28 @sbox={"PayloadData"=>[{"CE"=>[{"C"=>"1234 5678 9012 3456", "N"=>"credit card number"}, {"C"=>"11/2012", "N"=>"credit card expiration"}, {"C"=>"123", "N"=>"credit card verification value"}, {"C"=>"1234", "N"=>"debit card PIN"}, {"C"=>"qwerty", "N"=>"online banking password"}], "AC"=>"0", "N"=>"VanCity bank info", "D"=>"credit card, debit card, online banking", "CT"=>"634896555753102130", "MT"=>"634896555753103810", "T"=>"Credit, Debit, Card, Online, Banking"}, {"CE"=>[{"C"=>"ima_fake@gmail.com", "N"=>"username"}, {"C"=>"password1", "N"=>"password"}, {"C"=>"other", "N"=>"gender"}, {"C"=>"bart_simpson@gmail.com", "N"=>"backup"}], "AC"=>"0", "N"=>"fake Gmail account", "D"=>"used for Stackoverflow", "CT"=>"634896550747549900", "MT"=>"634896550747551430", "T"=>"Fake Gmail Stackoverflow"}, {"CE"=>[{}], "AC"=>"0", "N"=>"just a name", "CT"=>"634896545666556630", "MT"=>"634896545666557670"}, {"CE"=>[{"C"=>"test secure text", "N"=>"test field name"}], "AC"=>"0", "N"=>"test item name", "D"=>"test item description", "CT"=>"634896545082402010", "MT"=>"634896557631887530", "T"=>"Test, Item, Tag1, Tag2, Tag3"}], "MT"=>"2012-11-27T23:29:23.18873Z"}>
18
+
19
+ d = Strongboxio.decrypt(FILENAME, PASSWORD)
20
+ sbox = Strongboxio.new(d)
21
+ assert_equal 'Strongboxio',
22
+ sbox.class.to_s
23
+ assert_equal '2012-11-27T23:29:23.18873Z',
24
+ sbox.sbox['MT']
25
+ end
26
+
27
+ end
28
+
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: strongboxio
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alex Batko
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-29 00:00:00.000000000Z
13
+ dependencies: []
14
+ description: Decrypt and read www.Strongbox.io files.
15
+ email:
16
+ - alexbatko@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/strongboxio.rb
22
+ - test/2fe22f4d-bd41-476a-8159-c6e0b6487f7d.sbox
23
+ - test/test_helper.rb
24
+ - test/test_strongboxio.rb
25
+ homepage: https://github.com/abatko/strongboxio
26
+ licenses:
27
+ - MIT
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubyforge_project:
46
+ rubygems_version: 1.8.10
47
+ signing_key:
48
+ specification_version: 3
49
+ summary: Decrypt and read www.Strongbox.io files
50
+ test_files:
51
+ - test/2fe22f4d-bd41-476a-8159-c6e0b6487f7d.sbox
52
+ - test/test_helper.rb
53
+ - test/test_strongboxio.rb