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