strongboxio 0.1.0 → 0.2.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.
- data/.gitignore +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +19 -0
- data/README.md +97 -0
- data/Rakefile +23 -0
- data/bin/strongbox.rb +38 -0
- data/lib/strongboxio.rb +184 -191
- data/lib/strongboxio/version.rb +4 -0
- data/strongboxio.gemspec +28 -0
- data/test/test_strongboxio.rb +28 -20
- metadata +37 -5
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2012 Aric Batkovic
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
Strongbox
|
2
|
+
=========
|
3
|
+
|
4
|
+
Ruby gem and command-line interface for decrypting and reading www.Strongbox.io files.
|
5
|
+
|
6
|
+
Description
|
7
|
+
-----------
|
8
|
+
|
9
|
+
**Strongbox** (https://www.strongbox.io/) provides a simple way to effectively
|
10
|
+
organize, secure, and share sensitive textual data, the kind that has no other
|
11
|
+
home: passwords, credentials, credit card & account numbers, encryption keys,
|
12
|
+
certificates, etc. - essentially, anything we're not comfortable simply putting
|
13
|
+
into an unencrypted text document file or sharing via email, Skype, etc.
|
14
|
+
|
15
|
+
**This gem** enables decrypting and reading Strongbox files using the following
|
16
|
+
Ruby Standard Libraries:
|
17
|
+
|
18
|
+
* `openssl` (for decryption)
|
19
|
+
* `zlib` (for decompression)
|
20
|
+
* `base64` (for decoding)
|
21
|
+
|
22
|
+
and the following third-party libraries:
|
23
|
+
|
24
|
+
* `nokogiri` (for xml parsing)
|
25
|
+
* `highline` (for password input)
|
26
|
+
|
27
|
+
**This gem** also ships with a command-line interface (an executable Ruby
|
28
|
+
program). See *command-line usage* below.
|
29
|
+
|
30
|
+
Examples
|
31
|
+
--------
|
32
|
+
|
33
|
+
Given a Strongbox file and password, `Strongbox.decrypt` will handle everything
|
34
|
+
from opening the Strongbox file (extension `.sbox`), reading the XML content,
|
35
|
+
extracting the XML's Data node, decoding from Base64, decrypting
|
36
|
+
(`AES-256-CBC`), decompressing, and returning the raw content (which itself is
|
37
|
+
XML structured data, represented below as variable `d`).
|
38
|
+
|
39
|
+
At this point, the data just needs to be rendered (displayed); so we create a
|
40
|
+
strongbox object (`sb`) and call `render` on it.
|
41
|
+
|
42
|
+
d = Strongbox.decrypt(filename, password)
|
43
|
+
sb = Strongbox.new(d)
|
44
|
+
sb.render
|
45
|
+
|
46
|
+
Command-line usage
|
47
|
+
==================
|
48
|
+
|
49
|
+
RVM users: you probably want to install this gem into your default Ruby's global gemset.
|
50
|
+
|
51
|
+
Examples
|
52
|
+
--------
|
53
|
+
|
54
|
+
These examples assume `strongbox.rb` is executable and in a directory in `PATH`.
|
55
|
+
|
56
|
+
$ strongbox.rb
|
57
|
+
Usage /Users/abatko/bin/strongbox.rb input.sbox
|
58
|
+
|
59
|
+
$ strongbox.rb the_case_of_everything.sbox
|
60
|
+
Enter the password to unlock this box: ********
|
61
|
+
2012-11-27T23:29:23.18873Z
|
62
|
+
|
63
|
+
VanCity bank info
|
64
|
+
credit card, debit card, online banking
|
65
|
+
Credit, Debit, Card, Online, Banking
|
66
|
+
credit card number:
|
67
|
+
1234 5678 9012 3456
|
68
|
+
credit card expiration:
|
69
|
+
11/2012
|
70
|
+
credit card verification value:
|
71
|
+
123
|
72
|
+
debit card PIN:
|
73
|
+
1234
|
74
|
+
online banking password:
|
75
|
+
qwerty
|
76
|
+
|
77
|
+
fake Gmail account
|
78
|
+
used for Stackoverflow
|
79
|
+
Fake Gmail Stackoverflow
|
80
|
+
username:
|
81
|
+
ima_fake@gmail.com
|
82
|
+
password:
|
83
|
+
password1
|
84
|
+
gender:
|
85
|
+
other
|
86
|
+
backup:
|
87
|
+
bart_simpson@gmail.com
|
88
|
+
|
89
|
+
Contributing
|
90
|
+
============
|
91
|
+
|
92
|
+
1. Fork it
|
93
|
+
2. Create your feature branch (`git checkout -b my_new_feature`)
|
94
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
95
|
+
4. Push to the branch (`git push origin my_new_feature`)
|
96
|
+
5. Create new Pull Request
|
97
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
lib = File.expand_path('../lib/', __FILE__)
|
2
|
+
$:.unshift lib unless $:.include?(lib)
|
3
|
+
require 'strongboxio/version'
|
4
|
+
|
5
|
+
require 'rake/testtask'
|
6
|
+
|
7
|
+
Rake::TestTask.new do |t|
|
8
|
+
t.libs << 'test'
|
9
|
+
end
|
10
|
+
|
11
|
+
desc 'Run tests'
|
12
|
+
task :default => :test
|
13
|
+
|
14
|
+
desc 'Build gem'
|
15
|
+
task :build do
|
16
|
+
system 'gem build strongboxio.gemspec'
|
17
|
+
end
|
18
|
+
|
19
|
+
desc 'Release gem'
|
20
|
+
task :release => :build do
|
21
|
+
system "gem push strongboxio-#{Strongboxio::VERSION}.gem"
|
22
|
+
end
|
23
|
+
|
data/bin/strongbox.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems' if defined?RUBY_VERSION && RUBY_VERSION =~ /^1.8/ # for requiring gem dependency in Ruby 1.8
|
4
|
+
require 'highline/import' # ask
|
5
|
+
require 'strongboxio'
|
6
|
+
|
7
|
+
def get_filename(filename=ARGV[0])
|
8
|
+
if filename.nil?
|
9
|
+
puts "Usage #{$0} input.sbox" ; exit
|
10
|
+
else
|
11
|
+
if File.exist?(filename)
|
12
|
+
if !File.readable?(filename)
|
13
|
+
puts "file #{filename} is not readable!" ; exit
|
14
|
+
end
|
15
|
+
else
|
16
|
+
puts "file #{filename} does not exist!" ; exit
|
17
|
+
end
|
18
|
+
end
|
19
|
+
filename
|
20
|
+
end
|
21
|
+
|
22
|
+
filename = get_filename
|
23
|
+
|
24
|
+
password = ask('Enter the password to unlock this box: ') { |q| q.echo = '*' }
|
25
|
+
|
26
|
+
d = Strongboxio.decrypt(filename, password)
|
27
|
+
|
28
|
+
begin
|
29
|
+
sbox = Strongboxio.new(d)
|
30
|
+
rescue => e
|
31
|
+
puts "Error: #{e}"
|
32
|
+
yn = ask('Continue anyway? [yN] ')
|
33
|
+
exit unless yn =~ /^[yY]\Z/
|
34
|
+
sbox = Strongboxio.new(d, true)
|
35
|
+
end
|
36
|
+
|
37
|
+
sbox.render
|
38
|
+
|
data/lib/strongboxio.rb
CHANGED
@@ -4,204 +4,197 @@ require 'base64' # for decoding
|
|
4
4
|
require 'rubygems' if defined?RUBY_VERSION && RUBY_VERSION =~ /^1.8/ # for requiring gem dependency in Ruby 1.8
|
5
5
|
require 'nokogiri' # for xml parsing
|
6
6
|
|
7
|
+
require 'strongboxio/version'
|
8
|
+
|
7
9
|
class Strongboxio
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
d = OpenSSL::Cipher.new(CIPHER)
|
54
|
-
d.decrypt
|
55
|
-
d.key = key
|
56
|
-
d.iv = iv
|
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
|
+
attr_accessor :sbox
|
22
|
+
|
23
|
+
def self.decrypt(sbox_filename, password)
|
24
|
+
# open the xml file
|
25
|
+
f = File.open(sbox_filename)
|
26
|
+
data = Nokogiri::XML(f)
|
27
|
+
f.close
|
28
|
+
|
29
|
+
# extract the Data node
|
30
|
+
data = data.xpath('//Data').text
|
31
|
+
base64_error_msg = 'expected Base64 encoded byte string from the StrongBox.Payload.Data element of the xml of a Strongbox file'
|
32
|
+
raise "#{base64_error_msg}, but got nothing" if data.length == 0
|
33
|
+
raise "#{base64_error_msg}, but it does not resemble Base64" unless self.resembles_base64?(data)
|
34
|
+
|
35
|
+
data = Base64.decode64(data)
|
36
|
+
|
37
|
+
#version = data.getbyte(0) # ruby 1.9
|
38
|
+
version = data.bytes.to_a[0] # ruby 1.8 friendly
|
39
|
+
raise "expected version number #{STRONGBOX_VERSION}, but got #{version}" unless version == STRONGBOX_VERSION
|
40
|
+
|
41
|
+
salt = data.slice(1, SALT_LENGTH)
|
42
|
+
raise "expected salt length #{SALT_LENGTH}, but got #{salt.length}" unless salt.length == SALT_LENGTH
|
43
|
+
|
44
|
+
iv = data.bytes.to_a.slice((1+64), IV_LENGTH).pack('C*')
|
45
|
+
raise "expected iv length #{IV_LENGTH}, but got #{iv.length}" unless iv.length == IV_LENGTH
|
46
|
+
|
47
|
+
key = Digest::SHA256.digest(salt + password)
|
48
|
+
raise "expected key length #{KEY_LENGTH}, but got #{key.length}" unless key.length == KEY_LENGTH
|
49
|
+
|
50
|
+
# prepare for decryption
|
51
|
+
d = OpenSSL::Cipher.new(CIPHER)
|
52
|
+
d.decrypt
|
53
|
+
d.key = key
|
54
|
+
d.iv = iv
|
57
55
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
56
|
+
# decrypt the portion beyond the header
|
57
|
+
begin
|
58
|
+
data = '' << d.update(data.slice((VERSION_LENGTH+SALT_LENGTH+IV_LENGTH)..-1)) << d.final
|
59
|
+
rescue => e
|
60
|
+
raise "Error decrypting. You probably entered the password incorrectly. Specific error: #{e}"
|
61
|
+
end
|
62
|
+
|
63
|
+
# decompress the portion beyond the random value
|
64
|
+
#z = Zlib::Inflate.new
|
65
|
+
#z = Zlib::Inflate.new(-Zlib::BEST_COMPRESSION) # works for Strongbox
|
66
|
+
z = Zlib::Inflate.new(-Zlib::MAX_WBITS) # works for Strongbox!
|
67
|
+
#z = Zlib::Inflate.new(Zlib::MAX_WBITS) # works for roundtrip
|
68
|
+
data = z.inflate(data.slice(RANDOM_VALUE_LENGTH..-1))
|
69
|
+
z.finish
|
70
|
+
z.close
|
71
|
+
|
72
|
+
data
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.render(decrypted_sbox, continue_despite_unexpected_payload_schema_version=false)
|
76
|
+
data = Nokogiri::XML(decrypted_sbox)
|
77
|
+
|
78
|
+
payload_schema_version = data.xpath('//Payload').xpath('SchemaVersion').text
|
79
|
+
unless payload_schema_version == PAYLOAD_SCHEMA_VERSION
|
80
|
+
raise "expected schema version #{PAYLOAD_SCHEMA_VERSION}, but got #{payload_schema_version}" unless continue_despite_unexpected_payload_schema_version
|
81
|
+
end
|
82
|
+
|
83
|
+
puts data.xpath('//Payload').xpath('PayloadInfo').xpath('MT').text
|
84
|
+
|
85
|
+
data.xpath('//PayloadData').each { |payload_data|
|
86
|
+
payload_data.xpath('//SBE').each { |entity|
|
87
|
+
puts
|
88
|
+
name = entity.xpath('N').text
|
89
|
+
puts "#{name}" if name.length > 0
|
90
|
+
description = entity.xpath('D').text
|
91
|
+
puts "#{description}" if description.length > 0
|
92
|
+
tags = entity.xpath('T').text
|
93
|
+
puts "#{tags}" if tags.length > 0
|
94
|
+
ce = entity.xpath('CE')
|
95
|
+
ce.xpath('TFE').each { |tfe|
|
96
|
+
name = tfe.xpath('N').text
|
97
|
+
puts "#{name}:" if name.length > 0
|
98
|
+
content = tfe.xpath('C').text
|
99
|
+
puts "#{content}" if content.length > 0
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
0
|
104
|
+
end
|
105
|
+
|
106
|
+
def initialize(decrypted_sbox, continue_despite_unexpected_payload_schema_version=false)
|
107
|
+
super()
|
108
|
+
|
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
|
+
end
|
165
|
+
|
166
|
+
def render(verbose=false)
|
167
|
+
puts sbox['MT']
|
168
|
+
|
169
|
+
sbox['PayloadData'].each { |payload_data|
|
170
|
+
puts
|
171
|
+
puts payload_data['N'] unless payload_data['N'].nil?
|
172
|
+
puts payload_data['D'] unless payload_data['D'].nil?
|
173
|
+
puts payload_data['T'] unless payload_data['T'].nil?
|
174
|
+
payload_data['CE'].each { |strongbox_entity|
|
175
|
+
puts strongbox_entity['N'] + ': ' unless strongbox_entity['N'].nil?
|
176
|
+
puts strongbox_entity['C'] unless strongbox_entity['C'].nil?
|
177
|
+
}
|
178
|
+
puts "Access Count: #{payload_data['AC']}" if !payload_data['AC'].nil? && verbose
|
179
|
+
puts "#{convert_time_from_dot_net_epoch(payload_data['MT'].to_i)} (modify time)" if !payload_data['MT'].nil? && verbose
|
180
|
+
puts "#{convert_time_from_dot_net_epoch(payload_data['CT'].to_i)} (create time)" if !payload_data['CT'].nil? && verbose
|
181
|
+
}
|
182
|
+
0
|
183
|
+
end
|
195
184
|
|
196
185
|
private
|
197
186
|
|
198
|
-
|
199
|
-
|
200
|
-
|
187
|
+
def self.resembles_base64?(string)
|
188
|
+
string.length % 4 == 0 && string =~ /^[A-Za-z0-9+\/=]+\Z/
|
189
|
+
end
|
190
|
+
|
191
|
+
def convert_time_from_dot_net_epoch(t)
|
192
|
+
Time.at((t-UNIX_EPOCH_IN_100NS_INTERVALS)*1e-7).utc.getlocal
|
193
|
+
end
|
201
194
|
end
|
202
195
|
|
203
196
|
#class String
|
204
|
-
#
|
205
|
-
#
|
206
|
-
#
|
197
|
+
# def resembles_base64?
|
198
|
+
# self.length % 4 == 0 && self =~ /^[A-Za-z0-9+\/=]+\Z/
|
199
|
+
# end
|
207
200
|
#end
|
data/strongboxio.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
lib = File.expand_path('../lib/', __FILE__)
|
2
|
+
$:.unshift lib unless $:.include?(lib)
|
3
|
+
|
4
|
+
require 'strongboxio/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'strongboxio'
|
8
|
+
s.version = Strongboxio::VERSION
|
9
|
+
|
10
|
+
s.summary = 'Decrypt and read www.Strongbox.io files'
|
11
|
+
s.description = "#{s.summary}. This is a combination gem and command-line utility."
|
12
|
+
|
13
|
+
s.authors = ['Alex Batko']
|
14
|
+
s.email = ['alexbatko@gmail.com']
|
15
|
+
|
16
|
+
s.homepage = 'https://github.com/abatko/strongboxio'
|
17
|
+
|
18
|
+
s.add_runtime_dependency 'nokogiri'
|
19
|
+
s.add_runtime_dependency 'highline'
|
20
|
+
|
21
|
+
s.files = `git ls-files`.split($/)
|
22
|
+
s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
23
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
24
|
+
s.require_paths = ['lib']
|
25
|
+
|
26
|
+
s.license = 'MIT'
|
27
|
+
end
|
28
|
+
|
data/test/test_strongboxio.rb
CHANGED
@@ -3,26 +3,34 @@ require 'test_helper'
|
|
3
3
|
|
4
4
|
class StrongboxioTest < Test::Unit::TestCase
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
+
d = Strongboxio.decrypt(FILENAME, PASSWORD)
|
12
|
+
assert_equal expect, d
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_static_render
|
16
|
+
d = Strongboxio.decrypt(FILENAME, PASSWORD)
|
17
|
+
assert_equal 0, Strongboxio.render(d) # not checking puts (and don't want to waste memory on creating string)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_strongboxio_instantiation
|
21
|
+
# #<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"}>
|
22
|
+
|
23
|
+
d = Strongboxio.decrypt(FILENAME, PASSWORD)
|
24
|
+
sbox = Strongboxio.new(d)
|
25
|
+
assert_equal 'Strongboxio', sbox.class.to_s
|
26
|
+
assert_equal '2012-11-27T23:29:23.18873Z', sbox.sbox['MT']
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_instance_render
|
30
|
+
d = Strongboxio.decrypt(FILENAME, PASSWORD)
|
31
|
+
sbox = Strongboxio.new(d)
|
32
|
+
assert_equal 0, sbox.render # not checking puts (and don't want to waste memory on creating string)
|
33
|
+
end
|
26
34
|
|
27
35
|
end
|
28
36
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: strongboxio
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,16 +9,48 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
13
|
-
dependencies:
|
14
|
-
|
12
|
+
date: 2013-01-03 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: nokogiri
|
16
|
+
requirement: &2153457760 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2153457760
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: highline
|
27
|
+
requirement: &2153457340 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2153457340
|
36
|
+
description: Decrypt and read www.Strongbox.io files. This is a combination gem and
|
37
|
+
command-line utility.
|
15
38
|
email:
|
16
39
|
- alexbatko@gmail.com
|
17
|
-
executables:
|
40
|
+
executables:
|
41
|
+
- strongbox.rb
|
18
42
|
extensions: []
|
19
43
|
extra_rdoc_files: []
|
20
44
|
files:
|
45
|
+
- .gitignore
|
46
|
+
- Gemfile
|
47
|
+
- LICENSE.txt
|
48
|
+
- README.md
|
49
|
+
- Rakefile
|
50
|
+
- bin/strongbox.rb
|
21
51
|
- lib/strongboxio.rb
|
52
|
+
- lib/strongboxio/version.rb
|
53
|
+
- strongboxio.gemspec
|
22
54
|
- test/2fe22f4d-bd41-476a-8159-c6e0b6487f7d.sbox
|
23
55
|
- test/test_helper.rb
|
24
56
|
- test/test_strongboxio.rb
|