strongboxio 0.1.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.
@@ -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