keepass_kpscript 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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