keepass_kpscript 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/README.md +180 -0
- data/lib/keepass_kpscript.rb +22 -0
- data/lib/keepass_kpscript/database.rb +186 -0
- data/lib/keepass_kpscript/kpscript.rb +111 -0
- data/lib/keepass_kpscript/pass_encryptor.kdbx +0 -0
- data/lib/keepass_kpscript/select.rb +110 -0
- data/lib/keepass_kpscript/version.rb +5 -0
- data/spec/keepass_kpscript_test/helpers.rb +56 -0
- data/spec/keepass_kpscript_test/tests/keepass_kpscript/database_spec.rb +240 -0
- data/spec/keepass_kpscript_test/tests/keepass_kpscript/kpscript_spec.rb +117 -0
- data/spec/keepass_kpscript_test/tests/keepass_kpscript/select_spec.rb +61 -0
- data/spec/keepass_kpscript_test/tests/rubocop_spec.rb +31 -0
- data/spec/spec_helper.rb +102 -0
- metadata +129 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4d8c2308abfd17cef4c6c0c52c2ecefd0a499bf8cc7d451c9e53b50b98d53702
|
4
|
+
data.tar.gz: ae4cc14f37d7de8ed8a2003017c33503e8e686ec1a10ed2ef91ff5174dc77cb5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8bcdd547d96ea071093859910a4042ed0c129856b8999ed59d24f53893afe8f4821f1fa7506186a12ea6f89300dabb2ee724ea6e29401be8708a4ba49dd4d8cb
|
7
|
+
data.tar.gz: 3821acb14d910905dba791592d70e951db2a5f9a7bb67de4c471d0d84d9a4bcb96890a2b2057c73c233a1cdc772ad39152ff6d864372dd3f0997bb7614b055be
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
# [v0.0.1](https://github.com/Muriel-Salvan/keepass_kpscript/compare/...v0.0.1) (2021-06-30 10:50:35)
|
2
|
+
|
3
|
+
### Patches
|
4
|
+
|
5
|
+
* [Added semantic releasing](https://github.com/Muriel-Salvan/keepass_kpscript/commit/836b38f193a9bce29c1092490805a592a450c214)
|
6
|
+
* [Typo](https://github.com/Muriel-Salvan/keepass_kpscript/commit/c5e3ae4c359228d1d1bea8cef7ac80f86539aecc)
|
data/README.md
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
|
2
|
+
|
3
|
+
# keepass_kpscript - Ruby API to handle Keepass databases using KPScript
|
4
|
+
|
5
|
+
## Description
|
6
|
+
|
7
|
+
This Rubygem gives you ways to handle [KeePass databases](https://keepass.info/) using the [KPScript KeePass plugin](https://keepass.info/plugins.html#kpscript).
|
8
|
+
|
9
|
+
Other Rubygems handling KeePass databases usually handle the databases format themselves and can get obsolete if not kept up-to-date with the new file specifications.
|
10
|
+
|
11
|
+
`keepass_kpscript` uses the official KPScript plugin installed with a KeePass installation to handle databases, so that the risk of specifications' obsolescence is low (unless KPScript changes its command-line interface). However the cons of this approach is that `keepass_kpscript` needs a local installation of KeePass and KPScript to run.
|
12
|
+
|
13
|
+
Works for both Windows and Linux installations of KPScript.
|
14
|
+
|
15
|
+
## Requirements
|
16
|
+
|
17
|
+
* [KeePass](https://keepass.info/) - To be installed locally.
|
18
|
+
* [KPScript KeePass plugin](https://keepass.info/plugins.html#kpscript) - To be installed in your KeePass installation (follow the install instructions from the KPScript documentation).
|
19
|
+
|
20
|
+
## Install
|
21
|
+
|
22
|
+
Via gem command line:
|
23
|
+
|
24
|
+
```bash
|
25
|
+
gem install keepass_kpscript
|
26
|
+
```
|
27
|
+
|
28
|
+
If using `bundler`, add this in your `Gemfile`:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
gem 'keepass_kpscript'
|
32
|
+
```
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
Basically you just need to tell `keepass_kpscript` the KPScript command-line to be used (with `KeepassKpscript.use`), and the API will give you access to KPScript API to handle KeePass databases.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require 'keepass_kpscript'
|
40
|
+
|
41
|
+
# Let's use the system KeePass (under Windows, enclose it within "" for paths containing spaces like 'Program Files')
|
42
|
+
kpscript = KeepassKpscript.use('"C:\Program Files\KeePass\KPScript.exe"')
|
43
|
+
|
44
|
+
# Open a database with a simple password
|
45
|
+
database = kpscript.open('C:\Data\MyDatabase.kdbx', password: 'MyP4$sW0rD')
|
46
|
+
|
47
|
+
# Read a password for an entry
|
48
|
+
google_password = database.password_for 'Google Account'
|
49
|
+
puts "Password for 'Google Account' is #{google_password}"
|
50
|
+
```
|
51
|
+
|
52
|
+
Now that you get the basic usage, you can see the following sections for more features.
|
53
|
+
|
54
|
+
### Using key files, passwords and encrypted passwords to open databases
|
55
|
+
|
56
|
+
The [`Kpscript#open`](lib/keepass_kpscript/kpscript.rb) method accepts the following parameters while opening a database:
|
57
|
+
* `password`: The password to be used.
|
58
|
+
* `password_enc`: The encrypted password to be used (can be used in place of the password). You can use the [`Kpscript#encrypt_password`](lib/keepass_kpscript/kpscript.rb) method to generates an encrypted password from a password.
|
59
|
+
* `key_file`: Path to the key file to be used.
|
60
|
+
|
61
|
+
Example: open a database protected both by a password and a key file, and use an encrypted version of the password to open it.
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
encrypted_password = kpscript.encrypt_password('MyP4$sW0rD')
|
65
|
+
|
66
|
+
# This will not use the real password on KPScript command-line, which is better security wise.
|
67
|
+
database = kpscript.open('C:\Data\MyDatabase.kdbx', password_enc: encrypted_password, key_file: 'C:\Data\Database.key')
|
68
|
+
```
|
69
|
+
|
70
|
+
### Read entries from a database
|
71
|
+
|
72
|
+
The most versatile method to read database content is [`Database#entries_string`](lib/keepass_kpscript/database.rb), which maps directly the [`GetEntryString` KPScript method](https://keepass.info/help/v2_dev/scr_sc_index.html#getentrystring).
|
73
|
+
|
74
|
+
It uses chainable selectors to select the entries to be read, based on field names, uuids...
|
75
|
+
|
76
|
+
Example: read the URL field of all entries tagged `production` belonging to the group `Azure`
|
77
|
+
```ruby
|
78
|
+
database.entries_string(kpscript.select.tags('production').group('Azure'), 'URL').each do |url|
|
79
|
+
puts "Found URL: #{url}"
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
A more secure way to read secrets from a database is by using [`Database#with_entries_string`](lib/keepass_kpscript/database.rb) that works with a code block and the same parameters as [`Database#entries_string`](lib/keepass_kpscript/database.rb). The benefits are:
|
84
|
+
* Any variable that would have been read will be erased from memory at the end of the code execution, so that no attacker can eventually read it from memory, code injection, or memory dump on disk.
|
85
|
+
* The secret strings given to the code will be [`SecretString`](https://github.com/Muriel-Salvan/secret_string) instead of a String, that will guard the secret from being revealed in common Ruby operations (logging, screen output...), unless the `to_unprotected` method is used on it. Better way to control accessibility of your secrets!
|
86
|
+
|
87
|
+
Example: read all the passwords of entries belonging to Google's URL, and make sure those passwords are removed from memory after usage (and even try to leak the password in memory_)
|
88
|
+
```ruby
|
89
|
+
# Try to leak the password (simulating a security vulnerability here)
|
90
|
+
leaked_password = nil
|
91
|
+
|
92
|
+
database.with_entries_string(kpscript.select.fields(URL: '//google.com//'), 'Password').each do |password|
|
93
|
+
puts "Displayed password: #{password}"
|
94
|
+
# => Displayed password: XXXXX
|
95
|
+
puts "Now we REALLY want to display the password: #{password.to_unprotected}"
|
96
|
+
# => Now we REALLY want to display the password: MyP4$sW0rD
|
97
|
+
leaked_password = password
|
98
|
+
end
|
99
|
+
|
100
|
+
# Now that we are out of the code block, let's try to use the password again, hehe }:->
|
101
|
+
puts "Displayed leaked password: #{leaked_password.to_unprotected}"
|
102
|
+
# => Displayed leaked password:
|
103
|
+
```
|
104
|
+
|
105
|
+
To know more:
|
106
|
+
* The possible field references are documented in [KeePass documentation](https://keepass.info/help/base/fieldrefs.html).
|
107
|
+
* The possible selectors that can be used on the `Kpscript#select` call are methods defined in [the `Select` class](lib/keepass_kpscript/select.rb).
|
108
|
+
|
109
|
+
### Edit entries in a database
|
110
|
+
|
111
|
+
[`Database#edit_entries`](lib/keepass_kpscript/database.rb) can be used to edit entries. It maps the [`EditEntry` KPScript method](https://keepass.info/help/v2_dev/scr_sc_index.html#editentry) functionality.
|
112
|
+
|
113
|
+
The API uses the same selectors' logic as [`Database#entries_string`](lib/keepass_kpscript/database.rb).
|
114
|
+
|
115
|
+
Example: add notes and set the icon index 5 to all entries having a Google URL
|
116
|
+
```ruby
|
117
|
+
database.edit_entries(
|
118
|
+
kpscript.select.fields(URL: '//google.com//'),
|
119
|
+
fields: { Notes: 'It\'s for Google' },
|
120
|
+
icon_idx: 5
|
121
|
+
)
|
122
|
+
```
|
123
|
+
|
124
|
+
### Export a database
|
125
|
+
|
126
|
+
[`Database#export`](lib/keepass_kpscript/database.rb) maps the [`Export` KPScript method](https://keepass.info/help/v2_dev/scr_sc_index.html#export) to export databases.
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
database.export('KeePass XML (2.x)', 'my_export.xml')
|
130
|
+
```
|
131
|
+
|
132
|
+
### Detach binaries (attachments) from a database
|
133
|
+
|
134
|
+
[`Database#detach_bins`](lib/keepass_kpscript/database.rb) maps the [`DetachBins` KPScript method](https://keepass.info/help/v2_dev/scr_sc_index.html#detachbins) to extract files from databases.
|
135
|
+
|
136
|
+
Be careful that by default this method modifies your database by removing the attached files from it and writing them next to it.
|
137
|
+
If you want to keep your database intact, you can use the `copy_to_dir` option and it will extract files without removing them to antoher directory.
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
# Extract all files from database into the ./my_files sub-folder.
|
141
|
+
database.detach_bins(copy_to_dir: 'my_files')
|
142
|
+
|
143
|
+
# Extract and remove all files from database next to the database file.
|
144
|
+
database.detach_bins
|
145
|
+
```
|
146
|
+
|
147
|
+
## Change log
|
148
|
+
|
149
|
+
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
|
150
|
+
|
151
|
+
## Testing
|
152
|
+
|
153
|
+
Automated tests are done using rspec.
|
154
|
+
|
155
|
+
To execute them, first install development dependencies:
|
156
|
+
|
157
|
+
```bash
|
158
|
+
bundle install
|
159
|
+
```
|
160
|
+
|
161
|
+
Then execute rspec
|
162
|
+
|
163
|
+
```bash
|
164
|
+
bundle exec rspec
|
165
|
+
```
|
166
|
+
|
167
|
+
## Contributing
|
168
|
+
|
169
|
+
Any contribution is welcome:
|
170
|
+
* Fork the github project and create pull requests.
|
171
|
+
* Report bugs by creating tickets.
|
172
|
+
* Suggest improvements and new features by creating tickets.
|
173
|
+
|
174
|
+
## Credits
|
175
|
+
|
176
|
+
- [Muriel Salvan](https://x-aeon.com/muriel)
|
177
|
+
|
178
|
+
## License
|
179
|
+
|
180
|
+
The BSD License. Please see [License File](LICENSE.md) for more information.
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'keepass_kpscript/kpscript'
|
2
|
+
|
3
|
+
# Ruby API wrapping the KPScript CLI
|
4
|
+
module KeepassKpscript
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
# Get a KPScript instance from a given KPScript command line
|
9
|
+
#
|
10
|
+
# Parameters::
|
11
|
+
# * *cmd* (String): KPScript command line
|
12
|
+
# * *debug* (Boolean): Do we activate debugging logs? [default: false]
|
13
|
+
# Warning: Those logs can contain passwords and secrets from your database. Only use it in a local environment.
|
14
|
+
# Result::
|
15
|
+
# * Kpscript: A KPScript instance
|
16
|
+
def use(cmd, debug: false)
|
17
|
+
Kpscript.new(cmd, debug: debug)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'open3'
|
3
|
+
require 'secret_string'
|
4
|
+
require 'time'
|
5
|
+
require 'keepass_kpscript/select'
|
6
|
+
|
7
|
+
module KeepassKpscript
|
8
|
+
|
9
|
+
# KPScript API handling a KeePass database
|
10
|
+
class Database
|
11
|
+
|
12
|
+
# Constructor
|
13
|
+
#
|
14
|
+
# Parameters::
|
15
|
+
# * *kpscript* (Kpscript): The KPScript instance handling this database
|
16
|
+
# * *database_file* (String): Database file path
|
17
|
+
# * *password* (String or nil): Password opening the database, or nil if none [default: nil].
|
18
|
+
# * *password_enc* (String or nil): Encrypted password opening the database, or nil if none [default: nil].
|
19
|
+
# * *key_file* (String or nil): Key file path opening the database, or nil if none [default: nil].
|
20
|
+
def initialize(kpscript, database_file, password: nil, password_enc: nil, key_file: nil)
|
21
|
+
@kpscript = kpscript
|
22
|
+
@database_file = database_file
|
23
|
+
@password = password
|
24
|
+
@password_enc = password_enc
|
25
|
+
@key_file = key_file
|
26
|
+
end
|
27
|
+
|
28
|
+
# Securely select field values from entries.
|
29
|
+
# Using this will make sure the entries' values are then erased from memory for security when exiting client code.
|
30
|
+
# Try to not clone or extrapolate those values in other String variables, or if you have to call SecretString.erase on those variables to also erase their content.
|
31
|
+
#
|
32
|
+
# Parameters::
|
33
|
+
# * Same parameters as #entries_string
|
34
|
+
# * Proc: Code called with the entries retrieved
|
35
|
+
# * Parameters::
|
36
|
+
# * *values* (Array<String>)
|
37
|
+
def with_entries_string(*args, **kwargs)
|
38
|
+
values = []
|
39
|
+
begin
|
40
|
+
values = entries_string(*args, **kwargs).map { |str| SecretString.new(str) }
|
41
|
+
yield values
|
42
|
+
ensure
|
43
|
+
values.each(&:erase)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Get field string values from entries.
|
48
|
+
#
|
49
|
+
# Parameters::
|
50
|
+
# * *select* (Select): The entries selector
|
51
|
+
# * *field* (String): Field to be selected
|
52
|
+
# * *fail_if_not_exists* (Boolean): Do we fail if the field does not exist? [default: false]
|
53
|
+
# * *fail_if_no_entry* (Boolean): Do we fail if no entry was found? [default: false]
|
54
|
+
# * *spr* (Boolean): So we Spr-compile the value of the retrieved field? [default: false]
|
55
|
+
# Result::
|
56
|
+
# * Array<String>: List of retrieved field values
|
57
|
+
def entries_string(
|
58
|
+
select,
|
59
|
+
field,
|
60
|
+
fail_if_not_exists: false,
|
61
|
+
fail_if_no_entry: false,
|
62
|
+
spr: false
|
63
|
+
)
|
64
|
+
args = [
|
65
|
+
'-c:GetEntryString',
|
66
|
+
select.to_s,
|
67
|
+
"-Field:\"#{field}\""
|
68
|
+
]
|
69
|
+
args << '-FailIfNotExists' if fail_if_not_exists
|
70
|
+
args << '-FailIfNoEntry' if fail_if_no_entry
|
71
|
+
args << '-Spr' if spr
|
72
|
+
execute_kpscript(*args).split("\n")
|
73
|
+
end
|
74
|
+
|
75
|
+
# Edit field values from entries.
|
76
|
+
#
|
77
|
+
# Parameters::
|
78
|
+
# * *select* (Select): The entries selector
|
79
|
+
# * *fields* (Hash<String or Symbol, String>): Set of { field name => field value } to be set [default: {}]
|
80
|
+
# * *icon_idx* (Integer or nil): Set the icon index, or nil if none [default: nil]
|
81
|
+
# * *custom_icon_idx* (Integer or nil): Set the custom icon index, or nil if none [default: nil]
|
82
|
+
# * *expires* (Boolean or nil): Edit the expires flag, or nil to leave it untouched [default: nil]
|
83
|
+
# * *expiry_time* (Time or nil): Expiry time or nil to leave it untouched [default: nil]
|
84
|
+
# * *create_backup* (Boolean): Should we create backup of entries before modifying them? [default: false]
|
85
|
+
def edit_entries(
|
86
|
+
select,
|
87
|
+
fields: {},
|
88
|
+
icon_idx: nil,
|
89
|
+
custom_icon_idx: nil,
|
90
|
+
expires: nil,
|
91
|
+
expiry_time: nil,
|
92
|
+
create_backup: false
|
93
|
+
)
|
94
|
+
args = [
|
95
|
+
'-c:EditEntry',
|
96
|
+
select.to_s
|
97
|
+
] + fields.map { |field_name, field_value| "-set-#{field_name}:\"#{field_value}\"" }
|
98
|
+
args << "-setx-Icon:#{icon_idx}" if icon_idx
|
99
|
+
args << "-setx-CustomIcon:#{custom_icon_idx}" if custom_icon_idx
|
100
|
+
args << "-setx-Expires:#{expires ? 'true' : 'false'}" unless expires.nil?
|
101
|
+
args << "-setx-ExpiryTime:\"#{expiry_time.strftime('%FT%T')}\"" if expiry_time
|
102
|
+
args << '-CreateBackup' if create_backup
|
103
|
+
execute_kpscript(*args)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Retrieve a password for a given entry title
|
107
|
+
#
|
108
|
+
# Parameters::
|
109
|
+
# * *title* (String): Entry title
|
110
|
+
# Result::
|
111
|
+
# * String: Corresponding password
|
112
|
+
def password_for(title)
|
113
|
+
entries_string(@kpscript.select.fields(Title: title), 'Password').first
|
114
|
+
end
|
115
|
+
|
116
|
+
# Detach binaries.
|
117
|
+
#
|
118
|
+
# Parameters::
|
119
|
+
# * *copy_to_dir* (String or nil): Specify a directory in which binaries are extracted, or nil to extract next to the database file. [default: nil]
|
120
|
+
# If copy_to_dir is specified, then the directory will be created and a copy of the original database will be used to detach bins, leaving the original database untouched.
|
121
|
+
def detach_bins(copy_to_dir: nil)
|
122
|
+
if copy_to_dir.nil?
|
123
|
+
execute_kpscript('-c:DetachBins')
|
124
|
+
else
|
125
|
+
# Make a temporary copy of the database (as the KPScript extraction is destructive)
|
126
|
+
FileUtils.mkdir_p copy_to_dir
|
127
|
+
# Make a copy of the database in the directory first
|
128
|
+
tmp_database = "#{copy_to_dir}/#{File.basename(@database_file)}.tmp.kdbx"
|
129
|
+
FileUtils.cp @database_file, tmp_database
|
130
|
+
begin
|
131
|
+
@kpscript.open(tmp_database, password: @password, password_enc: @password_enc, key_file: @key_file).detach_bins
|
132
|
+
ensure
|
133
|
+
# Remove temporary database
|
134
|
+
File.unlink tmp_database
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Export the database
|
140
|
+
#
|
141
|
+
# Parameters::
|
142
|
+
# * *format* (String): Format to export to (see the KeePass Export dialog for possible values).
|
143
|
+
# * *file* (String): File path to export to.
|
144
|
+
# * *group_path* (Array<String> or nil): Group path to export, or nil for all [default: nil]
|
145
|
+
# * *xsl_file* (String or nil): In case of transforming using XSL, this specifies the XSL file path to be used, or nil for none. [default: nil]
|
146
|
+
def export(format, file, group_path: nil, xsl_file: nil)
|
147
|
+
args = [
|
148
|
+
'-c:Export',
|
149
|
+
"-Format:\"#{format}\"",
|
150
|
+
"-OutFile:\"#{file}\""
|
151
|
+
]
|
152
|
+
args << "-GroupPath:\"#{group_path.join('/')}\"" if group_path
|
153
|
+
args << "-XslFile:\"#{xsl_file}\"" if xsl_file
|
154
|
+
case execute_kpscript(*args)
|
155
|
+
when /E: Unknown format!/
|
156
|
+
raise "Unknown format: #{format}"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
# Execute KPScript on our database with a given list of arguments.
|
163
|
+
# Handle internally all arguments needed to open the database with the correct secrets.
|
164
|
+
#
|
165
|
+
# Parameters::
|
166
|
+
# * *args* (Array<String>): List of arguments
|
167
|
+
# Result::
|
168
|
+
# * String: stdout
|
169
|
+
def execute_kpscript(*args)
|
170
|
+
resulting_stdout = nil
|
171
|
+
begin
|
172
|
+
kdbx_args = ["\"#{@database_file}\""]
|
173
|
+
kdbx_args << SecretString.new("-pw:\"#{@password}\"", silenced_str: '-pw:"XXXXX"') if @password
|
174
|
+
kdbx_args << SecretString.new("-pw-enc:\"#{@password_enc}\"", silenced_str: '-pw-env:"XXXXX"') if @password_enc
|
175
|
+
kdbx_args << SecretString.new("-keyfile:\"#{@key_file}\"", silenced_str: '-keyfile:"XXXXX"') if @key_file
|
176
|
+
resulting_stdout = @kpscript.run(kdbx_args + args.flatten)
|
177
|
+
ensure
|
178
|
+
# Make sure we erase secrets
|
179
|
+
kdbx_args.each(&:erase)
|
180
|
+
end
|
181
|
+
resulting_stdout
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tmpdir'
|
3
|
+
require 'keepass_kpscript/select'
|
4
|
+
require 'keepass_kpscript/database'
|
5
|
+
|
6
|
+
module KeepassKpscript
|
7
|
+
|
8
|
+
# Drives an instance of KPScript
|
9
|
+
class Kpscript
|
10
|
+
|
11
|
+
# Constructor
|
12
|
+
#
|
13
|
+
# Parameters::
|
14
|
+
# * *cmd* (String): The KPScript command line
|
15
|
+
# * *debug* (Boolean): Do we activate debugging logs? [default: false]
|
16
|
+
# Warning: Those logs can contain passwords and secrets from your database. Only use it in a local environment.
|
17
|
+
def initialize(cmd, debug: false)
|
18
|
+
@cmd = cmd
|
19
|
+
@debug = debug
|
20
|
+
end
|
21
|
+
|
22
|
+
# Open a database using this KPScript instance
|
23
|
+
#
|
24
|
+
# Parameters::
|
25
|
+
# * *database_file* (String): Path to the database file
|
26
|
+
# * *password* (String or nil): Password opening the database, or nil if none [default: nil].
|
27
|
+
# * *password_enc* (String or nil): Encrypted password opening the database, or nil if none [default: nil].
|
28
|
+
# * *key_file* (String or nil): Key file path opening the database, or nil if none [default: nil].
|
29
|
+
# Result::
|
30
|
+
# * Database: The database
|
31
|
+
def open(database_file, password: nil, password_enc: nil, key_file: nil)
|
32
|
+
Database.new(self, database_file, password: password, password_enc: password_enc, key_file: key_file)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Shortcut to get easily access to selectors
|
36
|
+
#
|
37
|
+
# Result::
|
38
|
+
# * Select: A new entries selector
|
39
|
+
def select
|
40
|
+
Select.new
|
41
|
+
end
|
42
|
+
|
43
|
+
# Encrypt a password so that it can be used to open databases without using the real password
|
44
|
+
#
|
45
|
+
# Parameters:
|
46
|
+
# * *password* (String or SecretString): Password to be encrypted
|
47
|
+
# Result::
|
48
|
+
# * String: The encrypted password
|
49
|
+
def encrypt_password(password)
|
50
|
+
password_enc = nil
|
51
|
+
# We use a temporary database to encypt the password
|
52
|
+
tmp_database_file = "#{Dir.tmpdir}/keepass_kpscript.tmp.kdbx"
|
53
|
+
FileUtils.cp "#{__dir__}/pass_encryptor.kdbx", tmp_database_file
|
54
|
+
begin
|
55
|
+
tmp_database = self.open(tmp_database_file, password: 'pass_encryptor')
|
56
|
+
selector = select.fields(Title: 'pass_encryptor')
|
57
|
+
tmp_database.edit_entries(selector, fields: { Password: password.to_unprotected })
|
58
|
+
password_enc = tmp_database.entries_string(selector, 'URL', spr: true).first
|
59
|
+
ensure
|
60
|
+
File.unlink tmp_database_file
|
61
|
+
end
|
62
|
+
password_enc
|
63
|
+
end
|
64
|
+
|
65
|
+
# Run KPScript with a given list of arguments
|
66
|
+
#
|
67
|
+
# Parameters::
|
68
|
+
# * *args* (Array<String or SecretString>): List of arguments that are given to the KPScript command-line
|
69
|
+
# Result::
|
70
|
+
# * String: The stdout of the command (without the last status line)
|
71
|
+
def run(*args)
|
72
|
+
args.flatten!
|
73
|
+
resulting_stdout = nil
|
74
|
+
SecretString.protect(
|
75
|
+
"#{@cmd} #{args.map(&:to_unprotected).join(' ')}",
|
76
|
+
silenced_str: "#{@cmd} #{args.join(' ')}"
|
77
|
+
) do |cmd|
|
78
|
+
Open3.popen3(cmd.to_unprotected) do |_stdin, stdout, _stderr, wait_thr|
|
79
|
+
exit_status = wait_thr.value.exitstatus
|
80
|
+
stdout_lines = stdout.read.split("\n")
|
81
|
+
log_debug do
|
82
|
+
<<~EO_LOGDEBUG
|
83
|
+
Execute #{cmd.to_unprotected} =>
|
84
|
+
Exit status: #{exit_status}
|
85
|
+
STDOUT:
|
86
|
+
#{stdout_lines.join("\n")}
|
87
|
+
EO_LOGDEBUG
|
88
|
+
end
|
89
|
+
raise "Error while executing #{cmd} (exit status: #{exit_status})" unless exit_status.zero?
|
90
|
+
raise "Error returned by #{cmd}: #{stdout_lines.last}" unless stdout_lines.last == 'OK: Operation completed successfully.'
|
91
|
+
|
92
|
+
resulting_stdout = stdout_lines[0..-2].join("\n")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
resulting_stdout
|
96
|
+
end
|
97
|
+
|
98
|
+
# Issue some debugging logs, if debug is activated.
|
99
|
+
# Secret strings will be displayed unprotected by those logs.
|
100
|
+
#
|
101
|
+
# Parameters::
|
102
|
+
# * Proc: Code giving the message to be displayed
|
103
|
+
# * Result::
|
104
|
+
# * String: Message to display
|
105
|
+
def log_debug
|
106
|
+
puts "[ DEBUG ] - #{yield.to_unprotected}" if @debug
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|