keepass_kpscript 1.0.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,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