lib/secure_mailing/pgp/tool/parse.rb
# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
module SecureMailing::PGP::Tool::Parse
extend ActiveSupport::Concern
include SecureMailing::PGP::Tool::Exec
PGP_KEY_INFO = Struct.new(:fingerprint, :uids, :created_at, :expires_at, :secret)
PGP_KEY_INFO_EXPIRES_AT_TIMESTAMP = 6
PGP_KEY_INFO_CREATED_AT_TIMESTAMP = 5
PGP_KEY_INFO_UID = 9
PGP_KEY_INFO_UID_VALIDITY = 1
PGP_KEY_INFO_UID_INVALID_STATE = %w[i d r n].freeze
included do # rubocop:disable Metrics/BlockLength
def info(key)
result = gpg('show-key', options: %w[--with-colons], stdin: key)
parse_info(result.stdout)
end
private
def parse_info(data)
# https://github.com/gpg/gnupg/blob/master/doc/DETAILS
info = {
fingerprint: nil,
uids: [],
created_at: nil,
expires_at: nil,
secret: false
}
data.split("\n").tap do |chunks|
# We assume all relevant subkeys [SCE] have the same expiration date.
dates = chunks.find { |chunk| chunk.start_with?(%r{pub|sec}) }
info[:expires_at] = expires_at(dates)
info[:created_at] = created_at(dates)
info[:secret] = secret?(chunks)
fpr = chunks.find { |chunk| chunk.start_with?('fpr') }
info[:fingerprint] = fingerprint(fpr)
uids = chunks.select { |chunk| chunk.start_with?('uid') }
uids = uids.map { |uid| uid(uid) }
info[:uids] = uids.compact
end
PGP_KEY_INFO.new(*info.values)
end
def created_at(chunk)
timestamp = chunk.split(':').fetch(PGP_KEY_INFO_CREATED_AT_TIMESTAMP)
return nil if timestamp == '0'
Time.zone.at(timestamp.to_i)
end
def expires_at(chunk)
timestamp = chunk.split(':').fetch(PGP_KEY_INFO_EXPIRES_AT_TIMESTAMP)
return nil if timestamp.blank? || timestamp == '0'
Time.zone.at(timestamp.to_i)
end
def fingerprint(chunk)
chunk.split(':').last
end
def uid(chunk)
hunks = chunk.split(':')
return nil if PGP_KEY_INFO_UID_INVALID_STATE.include?(hunks.fetch(PGP_KEY_INFO_UID_VALIDITY))
hunks.fetch(PGP_KEY_INFO_UID)
end
def secret?(chunks)
chunks.any? { |chunk| chunk.start_with?('sec') }
end
end
end