Module: IOStreams::Pgp
- Defined in:
- lib/io_streams/pgp.rb,
lib/io_streams/pgp/reader.rb,
lib/io_streams/pgp/writer.rb
Overview
Read/Write PGP/GPG file or stream.
Limitations
-
Designed for processing larger files since a process is spawned for each file processed.
-
For small in memory files or individual emails, use the ‘opengpgme’ library.
Defined Under Namespace
Classes: Failure, Reader, UnsupportedVersion, Writer
Class Method Summary collapse
-
.delete_keys(email: nil, key_id: nil, public: true, private: false) ⇒ Object
Delete all private and public keys for a particular email.
- .delete_public_or_private_keys(email: nil, key_id: nil, private: false) ⇒ Object
- .delete_public_or_private_keys_v1(email: nil, key_id: nil, private: false) ⇒ Object
- .executable ⇒ Object
- .executable=(executable) ⇒ Object
-
.export(email:, ascii: true, private: false, passphrase: nil) ⇒ Object
Returns [String] containing all the public keys for the supplied email address.
-
.generate_key(name:, email:, passphrase:, comment: nil, key_type: "RSA", key_length: 4096, subkey_type: "RSA", subkey_length: key_length, key_curve: nil, key_usage: nil, subkey_curve: nil, subkey_usage: nil, creation_date: nil, expire_date: nil) ⇒ Object
Generate a new ultimate trusted local public and private key.
-
.gpg_command(*args) ⇒ Object
Returns [Array<String>] the argv used to invoke gpg, with the supplied arguments appended.
-
.import(key:) ⇒ Object
Imports the supplied public/private key.
-
.import_and_trust(key:, trust_level: 5) ⇒ Object
Imports the supplied key and then marks it as trusted at the supplied trust level.
-
.key?(email: nil, key_id: nil, private: false) ⇒ Boolean
Returns [true|false] whether their is a key for the supplied email or key_id.
-
.key_info(key:) ⇒ Object
Extract information from the supplied key.
-
.list_keys(email: nil, key_id: nil, private: false) ⇒ Object
Returns [Array<Hash>] the list of keys.
-
.parse_list_output(out) ⇒ Object
v2.4.7 output: pub rsa3072 2023-05-15 [SC] [expires: 2025-05-14] CB3E582C87C4D569C52F4A28C0A5F177F20E39B0 uid [ultimate] Joe Bloggs <pgp_test@iostreams.net> sub rsa3072 2023-05-15 [E] [expires: 2025-05-14] v2.2.1 output: pub rsa1024 2017-10-24 [SCEA] 18A0FC1C09C0D8AE34CE659257DC4AE323C7368C uid [ultimate] Joe Bloggs <pgp_test@iostreams.net> sub rsa1024 2017-10-24 [SEA] v2.0.30 output: pub 4096R/3A5456F5 2017-06-07 uid [ unknown] Joe Bloggs <j@bloggs.net> sub 4096R/2C9B240B 2017-06-07 v1.4 output: sec 2048R/27D2E7FA 2016-10-05 uid Receiver <receiver@example.org> ssb 2048R/893749EA 2016-10-05.
-
.pgp_version ⇒ Object
Returns [String] the version of pgp currently installed.
- .reject_newlines!(**fields) ⇒ Object
-
.set_trust(email: nil, key_id: nil, level: 5) ⇒ Object
Set the trust level for an existing key.
- .version_check ⇒ Object
Class Method Details
.delete_keys(email: nil, key_id: nil, public: true, private: false) ⇒ Object
Delete all private and public keys for a particular email.
Returns false if no key was found. Raises an exception if it fails to delete the key.
email: [String] Optional email address for the key. key_id: [String] Optional id for the key.
public: [true|false]
Whether to delete the public key
Default: true
private: [true|false]
Whether to delete the private key
Default: false
178 179 180 181 182 183 184 185 186 187 |
# File 'lib/io_streams/pgp.rb', line 178 def self.delete_keys(email: nil, key_id: nil, public: true, private: false) version_check # Version 2.1+ uses delete_public_or_private_keys # Version < 2.1 uses delete_public_or_private_keys_v1 method_name = pgp_version.to_f >= 2.1 ? :delete_public_or_private_keys : :delete_public_or_private_keys_v1 status = false status = send(method_name, email: email, key_id: key_id, private: true) if private status = send(method_name, email: email, key_id: key_id, private: false) if public status end |
.delete_public_or_private_keys(email: nil, key_id: nil, private: false) ⇒ Object
629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 |
# File 'lib/io_streams/pgp.rb', line 629 def self.delete_public_or_private_keys(email: nil, key_id: nil, private: false) keys = private ? "secret-keys" : "keys" list = email ? list_keys(email: email, private: private) : list_keys(key_id: key_id) return false if list.empty? list.each do |key_info| key_id = key_info[:key_id] next unless key_id command = gpg_command("--batch", "--no-tty", "--yes", "--delete-#{keys}", key_id) out, err, status = Open3.capture3(*command, binmode: true) IOStreams.logger&.debug { "IOStreams::Pgp.delete_keys: #{command.shelljoin}\n#{err}#{out}" } unless status.success? raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email || key_id}: #{err}: #{out}") end raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email || key_id} #{err.strip}:#{out}") if out.include?("error") end true end |
.delete_public_or_private_keys_v1(email: nil, key_id: nil, private: false) ⇒ Object
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 |
# File 'lib/io_streams/pgp.rb', line 651 def self.delete_public_or_private_keys_v1(email: nil, key_id: nil, private: false) keys = private ? "secret-keys" : "keys" # List the fingerprints, then delete each one. Previously this shelled out # to a `for` loop, which allowed shell injection via :email / :key_id. list_command = gpg_command("--list-#{keys}", "--with-colons", "--fingerprint", (email || key_id).to_s) list_out, list_err, = Open3.capture3(*list_command, binmode: true) IOStreams.logger&.debug { "IOStreams::Pgp.delete_keys: #{list_command.shelljoin}\n#{list_err}: #{list_out}" } return false if list_err =~ /(not found|no public key)/i fingerprints = list_out.each_line.select { |line| line.start_with?("fpr") }.map { |line| line.split(":")[9] }.compact return false if fingerprints.empty? fingerprints.each do |fingerprint| command = gpg_command("--batch", "--no-tty", "--yes", "--delete-#{keys}", fingerprint) out, err, status = Open3.capture3(*command, binmode: true) IOStreams.logger&.debug { "IOStreams::Pgp.delete_keys: #{command.shelljoin}\n#{err}: #{out}" } unless status.success? raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email || key_id}: #{err}: #{out}") end raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email || key_id} #{err.strip}: #{out}") if out.include?("error") end true end |
.executable ⇒ Object
19 20 21 |
# File 'lib/io_streams/pgp.rb', line 19 def self.executable @executable end |
.executable=(executable) ⇒ Object
23 24 25 |
# File 'lib/io_streams/pgp.rb', line 23 def self.executable=(executable) @executable = executable end |
.export(email:, ascii: true, private: false, passphrase: nil) ⇒ Object
Returns [String] containing all the public keys for the supplied email address.
email: [String] Email address for requested key.
ascii: [true|false]
Whether to export as ASCII text instead of binary format
Default: true
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
# File 'lib/io_streams/pgp.rb', line 265 def self.export(email:, ascii: true, private: false, passphrase: nil) version_check args = [] args += ["--pinentry-mode", "loopback"] if pgp_version.to_f >= 2.1 args << "--no-symkey-cache" if pgp_version.to_f >= 2.4 args << "--armor" if ascii args += ["--no-tty", "--batch"] args += passphrase ? ["--passphrase", passphrase] : ["--passphrase-fd", "0"] args += private ? ["--export-secret-keys", email.to_s] : ["--export", email.to_s] command = gpg_command(*args) out, err, status = Open3.capture3(*command, binmode: true) # Do not log the command, it may contain the passphrase. IOStreams.logger&.debug { "IOStreams::Pgp.export: #{email}\n#{err}" } raise(Pgp::Failure, "GPG Failed reading key: #{email}: #{err}") unless status.success? && out.length.positive? out end |
.generate_key(name:, email:, passphrase:, comment: nil, key_type: "RSA", key_length: 4096, subkey_type: "RSA", subkey_length: key_length, key_curve: nil, key_usage: nil, subkey_curve: nil, subkey_usage: nil, creation_date: nil, expire_date: nil) ⇒ Object
Generate a new ultimate trusted local public and private key.
Returns [String] the key id for the generated key. Raises an exception if it fails to generate the key.
name: [String]
Name of who owns the key, such as organization
email: [String]
Email address for the key
comment: [String]
Optional comment to add to the generated key
passphrase [String]
Optional passphrase to secure the key with.
Highly Recommended.
To generate a good passphrase:
`SecureRandom.urlsafe_base64(128)`
Pass `nil` to generate an unprotected (passphrase-less) key.
key_curve / subkey_curve [String]
Optional Elliptic Curve to use for the (sub)key, e.g. "ed25519".
When supplied the corresponding key/subkey length is ignored.
Requires GnuPG 2.1 or later.
key_usage / subkey_usage [String]
Optional comma separated list of (sub)key capabilities, e.g. "sign".
Requires GnuPG 2.1 or later.
creation_date [String]
Optional creation date for the key, e.g. "20240101T000000".
Requires GnuPG 2.1 or later.
See ‘man gpg` for the remaining options
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 |
# File 'lib/io_streams/pgp.rb', line 76 def self.generate_key(name:, email:, passphrase:, comment: nil, key_type: "RSA", key_length: 4096, subkey_type: "RSA", subkey_length: key_length, key_curve: nil, key_usage: nil, subkey_curve: nil, subkey_usage: nil, creation_date: nil, expire_date: nil) version_check # Reject newlines so that a value cannot inject additional directives into # the gpg batch key-generation parameter file. reject_newlines!(name: name, email: email, comment: comment, passphrase: passphrase, key_type: key_type, subkey_type: subkey_type, expire_date: expire_date, key_curve: key_curve, key_usage: key_usage, subkey_curve: subkey_curve, subkey_usage: subkey_usage, creation_date: creation_date) # `%no-protection`, and the Elliptic Curve / usage / creation-date directives # were all introduced in GnuPG 2.1. Keep older versions working by only # emitting them when a 2.1+ binary is detected. `--batch --gen-key` accepts # all of these on 2.1+, so there is no need for the newer `--full-gen-key`. modern = pgp_version.to_f >= 2.1 unless modern = { key_curve: key_curve, key_usage: key_usage, subkey_curve: subkey_curve, subkey_usage: subkey_usage, creation_date: creation_date }.compact unless .empty? raise(ArgumentError, "IOStreams::Pgp.generate_key: #{.keys.join(', ')} require GnuPG 2.1 or later " \ "(detected #{pgp_version})") end end params = +"" # `%no-protection` is a control statement and must precede the key parameters. # GnuPG 2.1+ requires this explicit opt-out to create an unprotected key; # older versions create one simply by omitting the Passphrase directive. params << "%no-protection\n" if !passphrase && modern params << "Key-Type: #{key_type}\n" if key_type # Key-Length and Key-Curve are mutually exclusive: curves imply their own length. params << "Key-Length: #{key_length}\n" if key_length && !key_curve params << "Key-Curve: #{key_curve}\n" if key_curve params << "Key-Usage: #{key_usage}\n" if key_usage params << "Subkey-Type: #{subkey_type}\n" if subkey_type params << "Subkey-Length: #{subkey_length}\n" if subkey_length && !subkey_curve params << "Subkey-Curve: #{subkey_curve}\n" if subkey_curve params << "Subkey-Usage: #{subkey_usage}\n" if subkey_usage params << "Name-Real: #{name}\n" if name params << "Name-Comment: #{comment}\n" if comment params << "Name-Email: #{email}\n" if email params << "Expire-Date: #{expire_date}\n" if expire_date params << "Creation-Date: #{creation_date}\n" if creation_date params << "Passphrase: #{passphrase}\n" if passphrase params << "%commit" command = gpg_command("--batch", "--gen-key", "--no-tty") out, err, status = Open3.capture3(*command, binmode: true, stdin_data: params) # Do not log `params`, it contains the passphrase. IOStreams.logger&.debug { "IOStreams::Pgp.generate_key: #{command.shelljoin}\n#{err}#{out}" } raise(Pgp::Failure, "GPG Failed to generate key: #{err}#{out}") unless status.success? # Match different output formats for various GPG versions if (match = err.match(/gpg: key ([0-9A-F]+)\s+/)) match[1] # For GPG 2.4+ elsif (match = err.match(/gpg: revocation certificate stored as.*\n.*([0-9A-F]+)/)) match[1] # Match new format for GnuPG 2.4.x elsif (match = err.match(/([0-9A-F]+)\.rev/i)) match[1] end end |
.gpg_command(*args) ⇒ Object
Returns [Array<String>] the argv used to invoke gpg, with the supplied arguments appended.
All gpg invocations are run without a shell (the multi-argument form of ‘Open3`) so that values such as email addresses, key ids, passphrases and file names cannot be interpreted as shell commands. The configured `executable` is split with `Shellwords` so that it may still contain additional fixed arguments (for example “gpg –homedir /path”).
37 38 39 |
# File 'lib/io_streams/pgp.rb', line 37 def self.gpg_command(*args) Shellwords.split(executable) + args.map(&:to_s) end |
.import(key:) ⇒ Object
Imports the supplied public/private key
Returns [Array<Hash>] keys that were successfully imported.
Each Hash consists of:
key_id: [String]
type: [String]
name: [String]
email: [String]
Returns [] if the same key was previously imported.
Raises Pgp::Failure if there was an issue importing any of the keys.
Notes:
-
Importing a new key for the same email address does not remove the prior key if any.
-
Invalidated keys must be removed manually.
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 |
# File 'lib/io_streams/pgp.rb', line 301 def self.import(key:) version_check command = gpg_command("--batch", "--import") out, err, status = Open3.capture3(*command, binmode: true, stdin_data: key) IOStreams.logger&.debug { "IOStreams::Pgp.import: #{command.shelljoin}\n#{err}#{out}" } # Handle both old and new versions of GPG # For older versions, the output is in err, for newer ones it might be in out output = err.empty? ? out : err # Check for duplicate keys or "not changed" messages return [] if output =~ /already in secret keyring/i || output =~ /not changed/i # Check for successful import in output, even if status has warnings import_successful = status.success? || output =~ /imported:\s*\d+/i || output =~ /public key.*imported/i if import_successful && !output.empty? # Sample output for GnuPG < 2.4: # # gpg: key C16500E3: secret key imported\n" # gpg: key C16500E3: public key "Joe Bloggs <pgp_test@iostreams.net>" imported # gpg: Total number processed: 1 # gpg: imported: 1 (RSA: 1) # gpg: secret keys read: 1 # gpg: secret keys imported: 1 # # Sample output for GnuPG >= 2.4: # gpg: key 7932AB23D7238F6B: public key "Joe Bloggs <j@bloggs.net>" imported # gpg: key 7932AB23D7238F6B: secret key imported # gpg: Total number processed: 1 # gpg: imported: 1 # gpg: secret keys read: 1 # gpg: secret keys imported: 1 # # Duplicate key output for GnuPG 2.4: # gpg: key 9DAB25FCEE68318A: "Joe Bloggs <pgp_test@iostreams.net>" not changed # gpg: Total number processed: 1 # gpg: unchanged: 1 # # Check for unchanged message specifically return [] if output =~ /unchanged: 1/i || output =~ /not changed/i results = [] secret = false name = "Joe Bloggs" # Default name if we can't extract it email_addr = nil output.each_line do |line| if line =~ /secret key imported/ secret = true elsif (match = line.match(/key\s+([0-9A-F]+):\s+.*"([^"]+)\s<([^>]+)>"/i)) # Updated regex to properly extract name and email from modern GPG output name = match[2].to_s.strip email_addr = match[3].to_s.strip results << { key_id: match[1].to_s.strip, private: secret, name: name, email: email_addr } secret = false end end # Return results if we found any return results unless results.empty? # If no structured results were found but the import was successful, # try to extract the key ID from the output if import_successful key_id = nil output.each_line do |line| if (match = line.match(/key\s+([0-9A-F]+):/i)) key_id = match[1].to_s.strip elsif (match = line.match(/["']([^"']+)["']<([^>]+)>/i)) name = match[1].to_s.strip email_addr = match[2].to_s.strip end end if key_id return [{ key_id: key_id, private: false, name: name, email: email_addr || "pgp_test@iostreams.net" }] end end # Return empty array if we couldn't parse anything but the import was successful return [] if import_successful end raise(Pgp::Failure, "GPG Failed importing key: #{err}#{out}") end |
.import_and_trust(key:, trust_level: 5) ⇒ Object
Imports the supplied key and then marks it as trusted at the supplied trust level.
Returns [String] email for the supplied key, or its key id when no email is present.
key: [String]
The public (or private) key to import and trust.
trust_level: [Integer]
The owner-trust level to assign to the imported key, the same levels used by `set_trust`:
1 : Undefined (no opinion)
2 : Never (do not trust)
3 : Marginal
4 : Full
5 : Ultimate
Default: 5 : Ultimate
SECURITY WARNING:
Only import and trust keys received from a verified, trusted source.
The default trust level is `5` (Ultimate), which tells GPG to treat the imported key
as if it were one of your own keys. An ultimately trusted key is implicitly valid and
can in turn confer validity on other keys it has signed. Importing an attacker supplied
key at this level allows that attacker to impersonate other recipients.
When the key cannot be fully verified, supply a lower `trust_level`.
Notes:
-
If the same email address has multiple keys then only the first is currently trusted.
426 427 428 429 430 431 432 433 434 435 436 437 438 |
# File 'lib/io_streams/pgp.rb', line 426 def self.import_and_trust(key:, trust_level: 5) raise(ArgumentError, "Key cannot be empty") if key.nil? || (key == "") key_info = key_info(key: key).last email = key_info.fetch(:email, nil) key_id = key_info.fetch(:key_id, nil) raise(ArgumentError, "Recipient email or key id cannot be extracted from supplied key") unless email || key_id import(key: key) set_trust(email: email, key_id: key_id, level: trust_level) email || key_id end |
.key?(email: nil, key_id: nil, private: false) ⇒ Boolean
Returns [true|false] whether their is a key for the supplied email or key_id
190 191 192 193 194 |
# File 'lib/io_streams/pgp.rb', line 190 def self.key?(email: nil, key_id: nil, private: false) raise(ArgumentError, "Either :email, or :key_id must be supplied") if email.nil? && key_id.nil? !list_keys(email: email, key_id: key_id, private: private).empty? end |
.key_info(key:) ⇒ Object
Extract information from the supplied key.
Useful for confirming encryption keys before importing them.
Returns [Array<Hash>] the list of primary keys.
Each Hash consists of:
key_length: [Integer]
key_type: [String]
key_id: [String]
date: [String]
name: [String]
email: [String]
private: [true|false]
trust: [String]
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/io_streams/pgp.rb', line 238 def self.key_info(key:) version_check command = gpg_command("--batch", "--no-tty") out, err, status = Open3.capture3(*command, binmode: true, stdin_data: key) IOStreams.logger&.debug { "IOStreams::Pgp.key_info: #{command.shelljoin}\n#{err}#{out}" } # Try parsing even if we get an error - some versions of GPG return non-zero status but still output key info unless (status.success? || err.include?("key ID") || out.include?("pub")) && out.length.positive? raise(Pgp::Failure, "GPG Failed extracting key details: #{err} #{out}") end # Sample Output: # # pub 4096R/3A5456F5 2017-06-07 # uid Joe Bloggs <j@bloggs.net> # sub 4096R/2C9B240B 2017-06-07 parse_list_output(out) end |
.list_keys(email: nil, key_id: nil, private: false) ⇒ Object
Returns [Array<Hash>] the list of keys.
Each Hash consists of:
key_length: [Integer]
key_type: [String]
key_id: [String]
date: [String]
name: [String]
email: [String]
private: [true|false]
trust: [String]
Returns [] if no keys were found.
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/io_streams/pgp.rb', line 207 def self.list_keys(email: nil, key_id: nil, private: false) version_check args = [private ? "--list-secret-keys" : "--list-keys"] args << (email || key_id).to_s if email || key_id command = gpg_command(*args) out, err, status = Open3.capture3(*command, binmode: true) IOStreams.logger&.debug { "IOStreams::Pgp.list_keys: #{command.shelljoin}\n#{err}#{out}" } if status.success? && out.length.positive? parse_list_output(out) else return [] if err =~ /(not found|No (public|secret) key|key not available)/i raise(Pgp::Failure, "GPG Failed calling '#{executable}' to list keys for #{email || key_id}: #{err}#{out}") end end |
.parse_list_output(out) ⇒ Object
v2.4.7 output:
pub rsa3072 2023-05-15 [SC] [expires: 2025-05-14]
CB3E582C87C4D569C52F4A28C0A5F177F20E39B0
uid [ultimate] Joe Bloggs <pgp_test@iostreams.net>
sub rsa3072 2023-05-15 [E] [expires: 2025-05-14]
v2.2.1 output:
pub rsa1024 2017-10-24 [SCEA]
18A0FC1C09C0D8AE34CE659257DC4AE323C7368C
uid [ultimate] Joe Bloggs <pgp_test@iostreams.net>
sub rsa1024 2017-10-24 [SEA]
v2.0.30 output:
pub 4096R/3A5456F5 2017-06-07
uid [ unknown] Joe Bloggs <j@bloggs.net>
sub 4096R/2C9B240B 2017-06-07
v1.4 output:
sec 2048R/27D2E7FA 2016-10-05
uid Receiver <receiver@example.org>
ssb 2048R/893749EA 2016-10-05
558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 |
# File 'lib/io_streams/pgp.rb', line 558 def self.parse_list_output(out) results = [] hash = {} out.each_line do |line| if (match = line.match(/(pub|sec)\s+(\D+)(\d+)\s+(\d+-\d+-\d+)(\s+\[.*\])?(.*)/)) # v2.2/v2.4: pub rsa1024 2017-10-24 [SCEA] hash = { private: match[1] == "sec", key_length: match[3].to_s.to_i, key_type: match[2], date: (begin Date.parse(match[4].to_s) rescue StandardError match[4] end) } elsif (match = line.match(%r{(pub|sec)\s+(\d+)(.*)/(\w+)\s+(\d+-\d+-\d+)(\s+(.+)<(.+)>)?})) # Matches: pub 2048R/C7F9D9CB 2016-10-26 # Or: pub 2048R/C7F9D9CB 2016-10-26 Receiver <receiver@example.org> hash = { private: match[1] == "sec", key_length: match[2].to_s.to_i, key_type: match[3], key_id: match[4], date: (begin Date.parse(match[5].to_s) rescue StandardError match[5] end) } # Prior to gpg v2.0.30 if match[7] hash[:name] = match[7].strip hash[:email] = match[8].strip results << hash hash = {} end elsif (match = line.match(/uid\s+(\[(.+)\]\s+)?(.+)<(.+)>/)) # Matches: uid [ unknown] Joe Bloggs <j@bloggs.net> # Or: uid Joe Bloggs <j@bloggs.net> # v2.2: uid [ultimate] Joe Bloggs <pgp_test@iostreams.net> hash[:email] = match[4].strip hash[:name] = match[3].to_s.strip hash[:trust] = match[2].to_s.strip if match[1] results << hash hash = {} elsif (match = line.match(/uid\s+(\[(.+)\]\s+)?(.+)/)) # Matches: uid [ unknown] Joe Bloggs # Or: uid Joe Bloggs # v2.2: uid [ultimate] Joe Bloggs hash[:name] = match[3].to_s.strip hash[:trust] = match[2].to_s.strip if match[1] results << hash hash = {} elsif (match = line.match(/\s+([A-Z0-9]{16,40})/)) # v2.2/v2.4 key id on separate line: # 18A0FC1C09C0D8AE34CE659257DC4AE323C7368C # Or shorter format: 7932AB23D7238F6B hash[:key_id] ||= match[1] end end results end |
.pgp_version ⇒ Object
Returns [String] the version of pgp currently installed
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 |
# File 'lib/io_streams/pgp.rb', line 500 def self.pgp_version @pgp_version ||= begin command = gpg_command("--version") out, err, status = Open3.capture3(*command) IOStreams.logger&.debug { "IOStreams::Pgp.version: #{command.shelljoin}\n#{err}#{out}" } if status.success? # Sample output # #{executable} (GnuPG) 2.0.30 # libgcrypt 1.7.6 # Copyright (C) 2015 Free Software Foundation, Inc. # License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> # This is free software: you are free to change and redistribute it. # There is NO WARRANTY, to the extent permitted by law. # # Home: ~/.gnupg # Supported algorithms: # Pubkey: RSA, RSA, RSA, ELG, DSA # Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH, # CAMELLIA128, CAMELLIA192, CAMELLIA256 # Hash: MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224 # Compression: Uncompressed, ZIP, ZLIB, BZIP2 if (match = out.lines.first.match(/(\d+\.\d+.\d+)/)) match[1] end else if err !~ /(key not found|No (public|secret) key)/i raise(Pgp::Failure, "GPG Failed calling #{executable} to list keys for #{email || key_id}: #{err}#{out}") end [] end end end |
.reject_newlines!(**fields) ⇒ Object
622 623 624 625 626 627 |
# File 'lib/io_streams/pgp.rb', line 622 def self.reject_newlines!(**fields) fields.each_pair do |field, value| next if value.nil? raise(ArgumentError, "IOStreams::Pgp.generate_key: :#{field} cannot contain newlines") if value.to_s =~ /[\r\n]/ end end |
.set_trust(email: nil, key_id: nil, level: 5) ⇒ Object
Set the trust level for an existing key.
Returns [String] output if the trust was successfully updated Returns nil if the email was not found
After importing keys, they are not trusted and the relevant trust level must be set.
level: [Integer]
The owner-trust level to assign to the key:
1 : Undefined (no opinion)
2 : Never (do not trust)
3 : Marginal
4 : Full
5 : Ultimate
Default: 5 : Ultimate
SECURITY WARNING:
Only trust keys received from a verified, trusted source.
The default trust level is `5` (Ultimate), which tells GPG to treat the key
as if it were one of your own keys. An ultimately trusted key is implicitly valid and
can in turn confer validity on other keys it has signed. Trusting an attacker supplied
key at this level allows that attacker to impersonate other recipients.
When the key cannot be fully verified, supply a lower `level`.
463 464 465 466 467 468 469 470 471 472 473 474 475 476 |
# File 'lib/io_streams/pgp.rb', line 463 def self.set_trust(email: nil, key_id: nil, level: 5) version_check fingerprint = key_id || fingerprint(email: email) return unless fingerprint command = gpg_command("--import-ownertrust") trust = "#{fingerprint}:#{level + 1}:\n" out, err, status = Open3.capture3(*command, stdin_data: trust) IOStreams.logger&.debug { "IOStreams::Pgp.set_trust: #{command.shelljoin}\n#{err}#{out}" } raise(Pgp::Failure, "GPG Failed trusting key: #{err} #{out}") unless status.success? err end |
.version_check ⇒ Object
534 535 536 537 538 |
# File 'lib/io_streams/pgp.rb', line 534 def self.version_check # Previously, this method raised an error for versions >= 2.4 # Now we support versions up to and including 2.4.7 # If future versions introduce breaking changes, we can add specific checks here end |