lib/io_streams/pgp.rb in iostreams-1.2.1 vs lib/io_streams/pgp.rb in iostreams-1.3.0

- old
+ new

@@ -44,11 +44,19 @@ # Highly Recommended. # To generate a good passphrase: # `SecureRandom.urlsafe_base64(128)` # # See `man gpg` for the remaining options - def self.generate_key(name:, email:, comment: nil, passphrase:, key_type: "RSA", key_length: 4096, subkey_type: "RSA", subkey_length: key_length, expire_date: nil) + def self.generate_key(name:, + email:, + comment: nil, + passphrase:, + key_type: "RSA", + key_length: 4096, + subkey_type: "RSA", + subkey_length: key_length, + expire_date: nil) version_check params = "" params << "Key-Type: #{key_type}\n" if key_type params << "Key-Length: #{key_length}\n" if key_length params << "Subkey-Type: #{subkey_type}\n" if subkey_type @@ -61,39 +69,39 @@ params << "%commit" command = "#{executable} --batch --gen-key --no-tty" out, err, status = Open3.capture3(command, binmode: true, stdin_data: params) logger&.debug { "IOStreams::Pgp.generate_key: #{command}\n#{params}\n#{err}#{out}" } - if status.success? - if match = err.match(/gpg: key ([0-9A-F]+)\s+/) - match[1] - end - else - raise(Pgp::Failure, "GPG Failed to generate key: #{err}#{out}") + + raise(Pgp::Failure, "GPG Failed to generate key: #{err}#{out}") unless status.success? + + if (match = err.match(/gpg: key ([0-9A-F]+)\s+/)) + match[1] end end # 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] Email address for 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 - def self.delete_keys(email:, public: true, private: false) + def self.delete_keys(email: nil, key_id: nil, public: true, private: false) version_check method_name = pgp_version.to_f >= 2.2 ? :delete_public_or_private_keys : :delete_public_or_private_keys_v1 status = false - status = send(method_name, email: email, private: true) if private - status = send(method_name, email: email, private: false) if public + 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 # Returns [true|false] whether their is a key for the supplied email or key_id def self.key?(email: nil, key_id: nil, private: false) @@ -148,20 +156,19 @@ version_check command = executable.to_s out, err, status = Open3.capture3(command, binmode: true, stdin_data: key) logger&.debug { "IOStreams::Pgp.key_info: #{command}\n#{err}#{out}" } - if status.success? && out.length.positive? - # Sample Output: - # - # pub 4096R/3A5456F5 2017-06-07 - # uid Joe Bloggs <j@bloggs.net> - # sub 4096R/2C9B240B 2017-06-07 - parse_list_output(out) - else - raise(Pgp::Failure, "GPG Failed extracting key details: #{err} #{out}") - end + + raise(Pgp::Failure, "GPG Failed extracting key details: #{err} #{out}") unless status.success? && out.length.positive? + + # 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 # Returns [String] containing all the public keys for the supplied email address. # # email: [String] Email address for requested key. @@ -179,15 +186,14 @@ command << (passphrase ? " #{passphrase} " : "-fd 0 ") command << (private ? "--export-secret-keys #{email}" : "--export #{email}") out, err, status = Open3.capture3(command, binmode: true) logger&.debug { "IOStreams::Pgp.export: #{command}\n#{err}" } - if status.success? && out.length.positive? - out - else - raise(Pgp::Failure, "GPG Failed reading key: #{email}: #{err}") - end + + raise(Pgp::Failure, "GPG Failed reading key: #{email}: #{err}") unless status.success? && out.length.positive? + + out end # Imports the supplied public/private key # # Returns [Array<Hash>] keys that were successfully imported. @@ -249,58 +255,60 @@ # Notes: # - If the same email address has multiple keys then only the first is currently trusted. def self.import_and_trust(key:) raise(ArgumentError, "Key cannot be empty") if key.nil? || (key == "") - email = key_info(key: key).last.fetch(:email) - raise(ArgumentError, "Recipient email cannot be extracted from supplied key") unless email + 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) + set_trust(email: email, key_id: key_id) email end # 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. # Default: 5 : Ultimate - def self.set_trust(email:, level: 5) + def self.set_trust(email: nil, key_id: nil, level: 5) version_check - fingerprint = fingerprint(email: email) + fingerprint = key_id || fingerprint(email: email) return unless fingerprint command = "#{executable} --import-ownertrust" trust = "#{fingerprint}:#{level + 1}:\n" out, err, status = Open3.capture3(command, stdin_data: trust) logger&.debug { "IOStreams::Pgp.set_trust: #{command}\n#{err}#{out}" } - if status.success? - err - else - raise(Pgp::Failure, "GPG Failed trusting key: #{err} #{out}") - end + + raise(Pgp::Failure, "GPG Failed trusting key: #{err} #{out}") unless status.success? + + err end # DEPRECATED - Use key_ids instead of fingerprints def self.fingerprint(email:) version_check Open3.popen2e("#{executable} --list-keys --fingerprint --with-colons #{email}") do |_stdin, out, waith_thr| output = out.read.chomp - if waith_thr.value.success? - output.each_line do |line| - if match = line.match(/\Afpr.*::([^\:]*):\Z/) - return match[1] - end + unless waith_thr.value.success? + unless output =~ /(public key not found|No public key)/i + raise(Pgp::Failure, "GPG Failed calling #{executable} to list keys for #{email}: #{output}") end - nil - else - return if output =~ /(public key not found|No public key)/i + end - raise(Pgp::Failure, "GPG Failed calling #{executable} to list keys for #{email}: #{output}") + output.each_line do |line| + if (match = line.match(/\Afpr.*::([^\:]*):\Z/)) + return match[1] + end end + nil end end def self.logger=(logger) @logger = logger @@ -326,11 +334,11 @@ # 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+)/) + if (match = out.lines.first.match(/(\d+\.\d+.\d+)/)) match[1] end else return [] if err =~ /(key not found|No (public|secret) key)/i @@ -346,13 +354,16 @@ def self.logger @logger end def self.version_check - if pgp_version.to_f >= 2.3 - raise(Pgp::UnsupportedVersion, "Version #{pgp_version} of #{executable} is not yet supported. You are welcome to submit a Pull Request.") - end + return unless pgp_version.to_f >= 2.3 + + raise( + Pgp::UnsupportedVersion, + "Version #{pgp_version} of #{executable} is not yet supported. Please submit a Pull Request to support it." + ) end # v2.2.1 output: # pub rsa1024 2017-10-24 [SCEA] # 18A0FC1C09C0D8AE34CE659257DC4AE323C7368C @@ -368,11 +379,11 @@ # ssb 2048R/893749EA 2016-10-05 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+(.*)/) + if (match = line.match(/(pub|sec)\s+(\D+)(\d+)\s+(\d+-\d+-\d+)\s+(.*)/)) # v2.2: pub rsa1024 2017-10-24 [SCEA] hash = { private: match[1] == "sec", key_length: match[3].to_s.to_i, key_type: match[2], @@ -380,11 +391,11 @@ 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+(.+)<(.+)>)?}) + 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, @@ -401,31 +412,39 @@ hash[:name] = match[7].strip hash[:email] = match[8].strip results << hash hash = {} end - elsif match = line.match(/uid\s+(\[(.+)\]\s+)?(.+)<(.+)>/) + 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(/([A-Z0-9]+)/) + 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(/([A-Z0-9]+)/)) # v2.2 18A0FC1C09C0D8AE34CE659257DC4AE323C7368C hash[:key_id] ||= match[1] end end results end - def self.delete_public_or_private_keys(email:, private: false) + def self.delete_public_or_private_keys(email: nil, key_id: nil, private: false) keys = private ? "secret-keys" : "keys" - list = list_keys(email: email, private: private) + 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 @@ -433,31 +452,31 @@ command = "#{executable} --batch --no-tty --yes --delete-#{keys} #{key_id}" out, err, status = Open3.capture3(command, binmode: true) logger&.debug { "IOStreams::Pgp.delete_keys: #{command}\n#{err}#{out}" } unless status.success? - raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email}: #{err}: #{out}") + 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} #{err.strip}:#{out}") if out.include?("error") + raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email || key_id} #{err.strip}:#{out}") if out.include?("error") end true end - def self.delete_public_or_private_keys_v1(email:, private: false) + def self.delete_public_or_private_keys_v1(email: nil, key_id: nil, private: false) keys = private ? "secret-keys" : "keys" - command = "for i in `#{executable} --list-#{keys} --with-colons --fingerprint #{email} | grep \"^fpr\" | cut -d: -f10`; do\n" + command = "for i in `#{executable} --list-#{keys} --with-colons --fingerprint #{email || key_id} | grep \"^fpr\" | cut -d: -f10`; do\n" command << "#{executable} --batch --no-tty --yes --delete-#{keys} \"$i\" ;\n" command << "done" out, err, status = Open3.capture3(command, binmode: true) logger&.debug { "IOStreams::Pgp.delete_keys: #{command}\n#{err}: #{out}" } return false if err =~ /(not found|no public key)/i unless status.success? - raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email}: #{err}: #{out}") + 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} #{err.strip}: #{out}") if out.include?("error") + raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email || key_id} #{err.strip}: #{out}") if out.include?("error") true end end end