keepass_kpscript 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,110 @@
1
+ module KeepassKpscript
2
+
3
+ # Define rules to select entries from a Keepass database.
4
+ # Those rules are taken from https://keepass.info/help/v2_dev/scr_sc_index.html#editentry
5
+ class Select
6
+
7
+ # Constructor
8
+ def initialize
9
+ @selectors = []
10
+ end
11
+
12
+ # Select a set of given field values
13
+ #
14
+ # Parameters::
15
+ # * *selection* (Hash<String or Symbol, String>): Set of { field_name => field_value } to be selected.
16
+ # Result::
17
+ # * self: The selector itself, useful for chaining
18
+ def fields(selection)
19
+ selection.each do |field_name, field_value|
20
+ @selectors << "-ref-#{field_name}:\"#{field_value}\""
21
+ end
22
+ self
23
+ end
24
+
25
+ # Select a UUID
26
+ #
27
+ # Parameters::
28
+ # * *id* (String): The UUID
29
+ # Result::
30
+ # * self: The selector itself, useful for chaining
31
+ def uuid(id)
32
+ @selectors << "-refx-UUID:#{id}"
33
+ self
34
+ end
35
+
36
+ # Select a list of tags
37
+ #
38
+ # Parameters::
39
+ # * *lst_tags* (Array<String>): List of tags to select
40
+ # Result::
41
+ # * self: The selector itself, useful for chaining
42
+ def tags(*lst_tags)
43
+ @selectors << "-refx-Tags:\"#{lst_tags.flatten.join(',')}\""
44
+ self
45
+ end
46
+
47
+ # Select entries that expire
48
+ #
49
+ # Parameters:
50
+ # * *switch* (Boolean): Do we select entries that expire? [default = true]
51
+ # Result::
52
+ # * self: The selector itself, useful for chaining
53
+ def expires(switch = true)
54
+ @selectors << "-refx-Expires:#{switch ? 'true' : 'false'}"
55
+ self
56
+ end
57
+
58
+ # Select entries that have expired
59
+ #
60
+ # Parameters:
61
+ # * *switch* (Boolean): Do we select entries that have expired? [default = true]
62
+ # Result::
63
+ # * self: The selector itself, useful for chaining
64
+ def expired(switch = true)
65
+ @selectors << "-refx-Expired:#{switch ? 'true' : 'false'}"
66
+ self
67
+ end
68
+
69
+ # Select entries that have a given parent group
70
+ #
71
+ # Parameters:
72
+ # * *group_name* (String): Name of the parent group
73
+ # Result::
74
+ # * self: The selector itself, useful for chaining
75
+ def group(group_name)
76
+ @selectors << "-refx-Group:\"#{group_name}\""
77
+ self
78
+ end
79
+
80
+ # Select entries that have a given group path
81
+ #
82
+ # Parameters:
83
+ # * *group_path_entries* (Array<String>): Group path
84
+ # Result::
85
+ # * self: The selector itself, useful for chaining
86
+ def group_path(*group_path_entries)
87
+ @selectors << "-refx-GroupPath:\"#{group_path_entries.flatten.join('/')}\""
88
+ self
89
+ end
90
+
91
+ # Select all entries
92
+ #
93
+ # Result::
94
+ # * self: The selector itself, useful for chaining
95
+ def all
96
+ @selectors << '-refx-All'
97
+ self
98
+ end
99
+
100
+ # Return the command-line string selecting the entries
101
+ #
102
+ # Result::
103
+ # * String: The command-line string
104
+ def to_s
105
+ @selectors.join(' ')
106
+ end
107
+
108
+ end
109
+
110
+ end
@@ -0,0 +1,5 @@
1
+ module KeepassKpscript
2
+
3
+ VERSION = '1.0.0'
4
+
5
+ end
@@ -0,0 +1,56 @@
1
+ require 'stringio'
2
+ require 'keepass_kpscript'
3
+
4
+ module KeepassKpscriptTest
5
+
6
+ module Helpers
7
+
8
+ # Expect some calls to be done on KPScript
9
+ #
10
+ # Parameters::
11
+ # * *expected_calls* (Array<[String, String or Hash]>): The list of calls and their corresponding mocked response:
12
+ # * String: Mocked stdout
13
+ # * Hash<Symbol,Object>: More complete structure defining the mocked response:
14
+ # * *exit_status* (Integer): The command exit status [default: 0]
15
+ # * *stdout* (String): The command stdout
16
+ def expect_calls_to_kpscript(expected_calls)
17
+ if expected_calls.empty?
18
+ expect(Open3).not_to receive(:popen3)
19
+ else
20
+ expected_calls.each do |(expected_call, mocked_call)|
21
+ mocked_call = { stdout: mocked_call } if mocked_call.is_a?(String)
22
+ mocked_call[:exit_status] = 0 unless mocked_call.key?(:exit_status)
23
+ expect(Open3).to receive(:popen3).with(expected_call) do |_cmd, &block|
24
+ wait_thr_double = instance_double(Process::Waiter)
25
+ allow(wait_thr_double).to receive(:value) do
26
+ wait_thr_value_double = instance_double(Process::Status)
27
+ allow(wait_thr_value_double).to receive(:exitstatus) do
28
+ mocked_call[:exit_status]
29
+ end
30
+ wait_thr_value_double
31
+ end
32
+ block.call(
33
+ StringIO.new,
34
+ StringIO.new(mocked_call[:stdout]),
35
+ StringIO.new,
36
+ wait_thr_double
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+
47
+ RSpec.configure do |config|
48
+ config.include KeepassKpscriptTest::Helpers
49
+
50
+ config.before do
51
+ # Make sure log debugs are not output on screen during tests
52
+ # rubocop:disable RSpec/AnyInstance
53
+ allow_any_instance_of(KeepassKpscript::Kpscript).to receive(:log_debug).and_yield
54
+ # rubocop:enable RSpec/AnyInstance
55
+ end
56
+ end
@@ -0,0 +1,240 @@
1
+ describe KeepassKpscript::Database do
2
+
3
+ shared_examples 'a database' do
4
+
5
+ subject(:database) { kpscript.open('/path/to/my_db.kdbx', password: 'MyPassword') }
6
+
7
+ let(:kpscript) { KeepassKpscript.use('/path/to/KPScript.exe', debug: debug) }
8
+
9
+ it 'gets a simple password for an entry title' do
10
+ expect_calls_to_kpscript [
11
+ [
12
+ '/path/to/KPScript.exe "/path/to/my_db.kdbx" -pw:"MyPassword" -c:GetEntryString -ref-Title:"MyEntryTitle" -Field:"Password"',
13
+ <<~EO_STDOUT
14
+ MyEntryPassword
15
+ OK: Operation completed successfully.
16
+ EO_STDOUT
17
+ ]
18
+ ]
19
+ expect(database.password_for('MyEntryTitle')).to eq 'MyEntryPassword'
20
+ end
21
+
22
+ it 'fails with an error silencing secrets when KPScript returns a non-zero exit status' do
23
+ expect_calls_to_kpscript [
24
+ [
25
+ '/path/to/KPScript.exe "/path/to/my_db.kdbx" -pw:"MyPassword" -c:GetEntryString -ref-Title:"MyEntryTitle" -Field:"Password"',
26
+ {
27
+ stdout: '',
28
+ exit_status: 1
29
+ }
30
+ ]
31
+ ]
32
+ expect { database.password_for('MyEntryTitle') }.to raise_error 'Error while executing /path/to/KPScript.exe "/path/to/my_db.kdbx" -pw:"XXXXX" -c:GetEntryString -ref-Title:"MyEntryTitle" -Field:"Password" (exit status: 1)'
33
+ end
34
+
35
+ it 'fails with an error silencing secrets when KPScript returns an error message' do
36
+ expect_calls_to_kpscript [
37
+ [
38
+ '/path/to/KPScript.exe "/path/to/my_db.kdbx" -pw:"MyPassword" -c:GetEntryString -ref-Title:"MyEntryTitle" -Field:"Password"',
39
+ 'E: Error reading database.'
40
+ ]
41
+ ]
42
+ expect { database.password_for('MyEntryTitle') }.to raise_error 'Error returned by /path/to/KPScript.exe "/path/to/my_db.kdbx" -pw:"XXXXX" -c:GetEntryString -ref-Title:"MyEntryTitle" -Field:"Password": E: Error reading database.'
43
+ end
44
+
45
+ it 'reads entries string' do
46
+ expect_calls_to_kpscript [
47
+ [
48
+ '/path/to/KPScript.exe "/path/to/my_db.kdbx" -pw:"MyPassword" -c:GetEntryString -refx-All -Field:"Field"',
49
+ <<~EO_STDOUT
50
+ Value1
51
+ Value2
52
+ Value3
53
+ OK: Operation completed successfully.
54
+ EO_STDOUT
55
+ ]
56
+ ]
57
+ expect(database.entries_string(kpscript.select.all, 'Field')).to eq %w[Value1 Value2 Value3]
58
+ end
59
+
60
+ it 'reads entries string in a secured way with secret strings' do
61
+ expect_calls_to_kpscript [
62
+ [
63
+ '/path/to/KPScript.exe "/path/to/my_db.kdbx" -pw:"MyPassword" -c:GetEntryString -refx-All -Field:"Field"',
64
+ <<~EO_STDOUT
65
+ Value1
66
+ Value2
67
+ Value3
68
+ OK: Operation completed successfully.
69
+ EO_STDOUT
70
+ ]
71
+ ]
72
+ leaked_values = nil
73
+ expect do
74
+ database.with_entries_string(kpscript.select.all, 'Field') do |values|
75
+ expect(values.map(&:to_s)).to eq %w[XXXXX XXXXX XXXXX]
76
+ expect(values.map(&:to_unprotected)).to eq %w[Value1 Value2 Value3]
77
+ leaked_values = values
78
+ end
79
+ end.not_to raise_error
80
+ expect(leaked_values.map(&:to_s)).to eq %w[XXXXX XXXXX XXXXX]
81
+ expect(leaked_values.map(&:to_unprotected)).to eq ["\x00\x00\x00\x00\x00\x00", "\x00\x00\x00\x00\x00\x00", "\x00\x00\x00\x00\x00\x00"]
82
+ end
83
+
84
+ {
85
+ fail_if_not_exists: '-FailIfNotExists',
86
+ fail_if_no_entry: '-FailIfNoEntry',
87
+ spr: '-Spr'
88
+ }.each do |keyword, kpscript_flag|
89
+
90
+ it "reads entries string with #{keyword} flag" do
91
+ expect_calls_to_kpscript [
92
+ [
93
+ "/path/to/KPScript.exe \"/path/to/my_db.kdbx\" -pw:\"MyPassword\" -c:GetEntryString -refx-All -Field:\"Field\" #{kpscript_flag}",
94
+ <<~EO_STDOUT
95
+ Value1
96
+ Value2
97
+ Value3
98
+ OK: Operation completed successfully.
99
+ EO_STDOUT
100
+ ]
101
+ ]
102
+ expect(database.entries_string(kpscript.select.all, 'Field', **{ keyword => true })).to eq %w[Value1 Value2 Value3]
103
+ end
104
+
105
+ it "reads entries string in a secured way with secret strings with #{keyword} flag" do
106
+ expect_calls_to_kpscript [
107
+ [
108
+ "/path/to/KPScript.exe \"/path/to/my_db.kdbx\" -pw:\"MyPassword\" -c:GetEntryString -refx-All -Field:\"Field\" #{kpscript_flag}",
109
+ <<~EO_STDOUT
110
+ Value1
111
+ Value2
112
+ Value3
113
+ OK: Operation completed successfully.
114
+ EO_STDOUT
115
+ ]
116
+ ]
117
+ leaked_values = nil
118
+ expect do
119
+ database.with_entries_string(kpscript.select.all, 'Field', **{ keyword => true }) do |values|
120
+ expect(values.map(&:to_s)).to eq %w[XXXXX XXXXX XXXXX]
121
+ expect(values.map(&:to_unprotected)).to eq %w[Value1 Value2 Value3]
122
+ leaked_values = values
123
+ end
124
+ end.not_to raise_error
125
+ expect(leaked_values.map(&:to_s)).to eq %w[XXXXX XXXXX XXXXX]
126
+ expect(leaked_values.map(&:to_unprotected)).to eq ["\x00\x00\x00\x00\x00\x00", "\x00\x00\x00\x00\x00\x00", "\x00\x00\x00\x00\x00\x00"]
127
+ end
128
+
129
+ end
130
+
131
+ # All edit entries test cases
132
+ {
133
+ { fields: { Field: 'Value' } } => '-set-Field:"Value"',
134
+ { fields: { Field1: 'Value1', Field2: 'Value2' } } => '-set-Field1:"Value1" -set-Field2:"Value2"',
135
+ { icon_idx: 7 } => '-setx-Icon:7',
136
+ { custom_icon_idx: 11 } => '-setx-CustomIcon:11',
137
+ { expires: true } => '-setx-Expires:true',
138
+ { expiry_time: Time.parse('2021-06-30 15:12:11') } => '-setx-ExpiryTime:"2021-06-30T15:12:11"',
139
+ { fields: { Field: 'Value' }, create_backup: true } => '-set-Field:"Value" -CreateBackup',
140
+ {
141
+ fields: { Field1: 'Value1', Field2: 'Value2' },
142
+ expiry_time: Time.parse('2021-06-30 15:12:11'),
143
+ icon_idx: 7,
144
+ create_backup: true
145
+ } => '-set-Field1:"Value1" -set-Field2:"Value2" -setx-Icon:7 -setx-ExpiryTime:"2021-06-30T15:12:11" -CreateBackup'
146
+ }.each do |kwargs, expected_args|
147
+
148
+ it "edit entries using #{kwargs}" do
149
+ expect_calls_to_kpscript [
150
+ [
151
+ "/path/to/KPScript.exe \"/path/to/my_db.kdbx\" -pw:\"MyPassword\" -c:EditEntry -refx-All #{expected_args}",
152
+ 'OK: Operation completed successfully.'
153
+ ]
154
+ ]
155
+ expect { database.edit_entries(kpscript.select.all, **kwargs) }.not_to raise_error
156
+ end
157
+
158
+ end
159
+
160
+ it 'detaches binaries' do
161
+ expect_calls_to_kpscript [
162
+ [
163
+ '/path/to/KPScript.exe "/path/to/my_db.kdbx" -pw:"MyPassword" -c:DetachBins',
164
+ 'OK: Operation completed successfully.'
165
+ ]
166
+ ]
167
+ expect { database.detach_bins }.not_to raise_error
168
+ end
169
+
170
+ it 'detaches binaries in another directory' do
171
+ tmpdir = "#{Dir.tmpdir}/keepass_kpscript_test"
172
+ FileUtils.mkdir_p tmpdir
173
+ begin
174
+ database_file = "#{tmpdir}/my_db.kdbx"
175
+ File.write(database_file, 'Dummy database')
176
+ bins_dir = "#{tmpdir}/bins_dir"
177
+ expect_calls_to_kpscript [
178
+ [
179
+ "/path/to/KPScript.exe \"#{bins_dir}/my_db.kdbx.tmp.kdbx\" -pw:\"MyPassword\" -c:DetachBins",
180
+ 'OK: Operation completed successfully.'
181
+ ]
182
+ ]
183
+ expect { kpscript.open(database_file, password: 'MyPassword').detach_bins(copy_to_dir: bins_dir) }.not_to raise_error
184
+ expect(File.exist?(bins_dir)).to eq true
185
+ # Check that no database copy is remaining
186
+ expect(Dir.glob("#{bins_dir}/*")).to eq []
187
+ ensure
188
+ FileUtils.rm_rf tmpdir
189
+ end
190
+ end
191
+
192
+ it 'exports the database' do
193
+ expect_calls_to_kpscript [
194
+ [
195
+ '/path/to/KPScript.exe "/path/to/my_db.kdbx" -pw:"MyPassword" -c:Export -Format:"Export Format" -OutFile:"/path/to/export_file"',
196
+ 'OK: Operation completed successfully.'
197
+ ]
198
+ ]
199
+ expect { database.export('Export Format', '/path/to/export_file') }.not_to raise_error
200
+ end
201
+
202
+ it 'exports the database with a group path selected' do
203
+ expect_calls_to_kpscript [
204
+ [
205
+ '/path/to/KPScript.exe "/path/to/my_db.kdbx" -pw:"MyPassword" -c:Export -Format:"Export Format" -OutFile:"/path/to/export_file" -GroupPath:"Group1/Group2/Group3"',
206
+ 'OK: Operation completed successfully.'
207
+ ]
208
+ ]
209
+ expect { database.export('Export Format', '/path/to/export_file', group_path: %w[Group1 Group2 Group3]) }.not_to raise_error
210
+ end
211
+
212
+ it 'exports the database with an XSL file specified' do
213
+ expect_calls_to_kpscript [
214
+ [
215
+ '/path/to/KPScript.exe "/path/to/my_db.kdbx" -pw:"MyPassword" -c:Export -Format:"Export Format" -OutFile:"/path/to/export_file" -XslFile:"/path/to/xsl_file.xsl"',
216
+ 'OK: Operation completed successfully.'
217
+ ]
218
+ ]
219
+ expect { database.export('Export Format', '/path/to/export_file', xsl_file: '/path/to/xsl_file.xsl') }.not_to raise_error
220
+ end
221
+
222
+ end
223
+
224
+ context 'without debug' do
225
+
226
+ it_behaves_like 'a database' do
227
+ let(:debug) { false }
228
+ end
229
+
230
+ end
231
+
232
+ context 'with debug' do
233
+
234
+ it_behaves_like 'a database' do
235
+ let(:debug) { true }
236
+ end
237
+
238
+ end
239
+
240
+ end
@@ -0,0 +1,117 @@
1
+ describe KeepassKpscript::Kpscript do
2
+
3
+ shared_examples 'a kpscript instance' do
4
+
5
+ subject(:kpscript) { KeepassKpscript.use('/path/to/KPScript.exe', debug: debug) }
6
+
7
+ it 'gives an instance wrapping a KPScript installation' do
8
+ expect_calls_to_kpscript [['/path/to/KPScript.exe -example-arg', 'OK: Operation completed successfully.']]
9
+ kpscript.run('-example-arg')
10
+ end
11
+
12
+ it 'encrypts passwords' do
13
+ expect_calls_to_kpscript [
14
+ [
15
+ '/path/to/KPScript.exe "/tmp/keepass_kpscript.tmp.kdbx" -pw:"pass_encryptor" -c:EditEntry -ref-Title:"pass_encryptor" -set-Password:"MyPassword"',
16
+ 'OK: Operation completed successfully.'
17
+ ],
18
+ [
19
+ '/path/to/KPScript.exe "/tmp/keepass_kpscript.tmp.kdbx" -pw:"pass_encryptor" -c:GetEntryString -ref-Title:"pass_encryptor" -Field:"URL" -Spr',
20
+ <<~EO_STDOUT
21
+ ENCRYPTED_PASSWORD
22
+ OK: Operation completed successfully.
23
+ EO_STDOUT
24
+ ]
25
+ ]
26
+ expect(kpscript.encrypt_password('MyPassword')).to eq 'ENCRYPTED_PASSWORD'
27
+ end
28
+
29
+ it 'opens a database with a password' do
30
+ expect_calls_to_kpscript [
31
+ [
32
+ '/path/to/KPScript.exe "/path/to/my_db.kdbx" -pw:"MyPassword" -c:GetEntryString -ref-Title:"MyEntryTitle" -Field:"Password"',
33
+ <<~EO_STDOUT
34
+ MyEntryPassword
35
+ OK: Operation completed successfully.
36
+ EO_STDOUT
37
+ ]
38
+ ]
39
+ expect(kpscript.open('/path/to/my_db.kdbx', password: 'MyPassword').password_for('MyEntryTitle')).to eq 'MyEntryPassword'
40
+ end
41
+
42
+ it 'opens a database with an encrypted password' do
43
+ expect_calls_to_kpscript [
44
+ [
45
+ '/path/to/KPScript.exe "/path/to/my_db.kdbx" -pw-enc:"MyEncryptedPassword" -c:GetEntryString -ref-Title:"MyEntryTitle" -Field:"Password"',
46
+ <<~EO_STDOUT
47
+ MyEntryPassword
48
+ OK: Operation completed successfully.
49
+ EO_STDOUT
50
+ ]
51
+ ]
52
+ expect(kpscript.open('/path/to/my_db.kdbx', password_enc: 'MyEncryptedPassword').password_for('MyEntryTitle')).to eq 'MyEntryPassword'
53
+ end
54
+
55
+ it 'opens a database with a key file' do
56
+ expect_calls_to_kpscript [
57
+ [
58
+ '/path/to/KPScript.exe "/path/to/my_db.kdbx" -keyfile:"/path/to/key_file" -c:GetEntryString -ref-Title:"MyEntryTitle" -Field:"Password"',
59
+ <<~EO_STDOUT
60
+ MyEntryPassword
61
+ OK: Operation completed successfully.
62
+ EO_STDOUT
63
+ ]
64
+ ]
65
+ expect(kpscript.open('/path/to/my_db.kdbx', key_file: '/path/to/key_file').password_for('MyEntryTitle')).to eq 'MyEntryPassword'
66
+ end
67
+
68
+ it 'opens a database with a key file and password' do
69
+ expect_calls_to_kpscript [
70
+ [
71
+ '/path/to/KPScript.exe "/path/to/my_db.kdbx" -pw:"MyPassword" -keyfile:"/path/to/key_file" -c:GetEntryString -ref-Title:"MyEntryTitle" -Field:"Password"',
72
+ <<~EO_STDOUT
73
+ MyEntryPassword
74
+ OK: Operation completed successfully.
75
+ EO_STDOUT
76
+ ]
77
+ ]
78
+ expect(kpscript.open('/path/to/my_db.kdbx', password: 'MyPassword', key_file: '/path/to/key_file').password_for('MyEntryTitle')).to eq 'MyEntryPassword'
79
+ end
80
+
81
+ it 'opens a database with a key file and encrypted password' do
82
+ expect_calls_to_kpscript [
83
+ [
84
+ '/path/to/KPScript.exe "/path/to/my_db.kdbx" -pw-enc:"MyEncryptedPassword" -keyfile:"/path/to/key_file" -c:GetEntryString -ref-Title:"MyEntryTitle" -Field:"Password"',
85
+ <<~EO_STDOUT
86
+ MyEntryPassword
87
+ OK: Operation completed successfully.
88
+ EO_STDOUT
89
+ ]
90
+ ]
91
+ expect(kpscript.open('/path/to/my_db.kdbx', password_enc: 'MyEncryptedPassword', key_file: '/path/to/key_file').password_for('MyEntryTitle')).to eq 'MyEntryPassword'
92
+ end
93
+
94
+ it 'gives a selector' do
95
+ expect_calls_to_kpscript []
96
+ expect(kpscript.select.fields(Title: 'MyEntryTitle').to_s).to eq '-ref-Title:"MyEntryTitle"'
97
+ end
98
+
99
+ end
100
+
101
+ context 'without debug' do
102
+
103
+ it_behaves_like 'a kpscript instance' do
104
+ let(:debug) { false }
105
+ end
106
+
107
+ end
108
+
109
+ context 'with debug' do
110
+
111
+ it_behaves_like 'a kpscript instance' do
112
+ let(:debug) { true }
113
+ end
114
+
115
+ end
116
+
117
+ end