module NetPGP require 'forwardable' require_relative 'publickey' require_relative 'utils' # Secret key # class SecretKey extend Forwardable delegate [:creation_time, :expiration_time, :expiration_time=, :fingerprint, :fingerprint_hex, :key_id, :key_id_hex] => :@public_key attr_accessor :public_key, :string_to_key_usage, :string_to_key_specifier, :symmetric_key_algorithm, :hash_algorithm, :iv, :check_hash, :mpi, :userids, :parent, :subkeys, :raw_subpackets, :encrypted, :passphrase def initialize @public_key = nil @string_to_key_usage = nil @string_to_key_specifier = nil @symmetric_key_algorithm = nil @hash_algorithm = nil @iv = nil @check_hash = nil @mpi = {} @userids = [] @parent = nil @subkeys = [] @raw_subpackets = [] @encrypted = false @passphrase = '' end # Checks if a key is encrypted. An encrypted key requires a # passphrase for signing/decrypting/etc and will have nil values # for key material/mpis. # # @return [Boolean] def encrypted? @encrypted end # Decrypts data using this secret key. # # Note: {#passphrase} must be set to the correct passphrase prior # to this call. If no passphrase is required, it should be set to # '' (not nil). # # @param data [String] the encrypted data to be decrypted. # @param armored [Boolean] whether the encrypted data is ASCII armored. def decrypt(data, armored=true) begin rd, wr = IO.pipe wr.write(@passphrase + "\n") native_keyring_ptr = LibC::calloc(1, LibNetPGP::PGPKeyring.size) native_keyring = LibNetPGP::PGPKeyring.new(native_keyring_ptr) NetPGP::keys_to_native_keyring([self], native_keyring) pgpio = create_pgpio data_ptr = FFI::MemoryPointer.new(:uint8, data.bytesize) data_ptr.write_bytes(data) passfp = LibC::fdopen(rd.to_i, 'r') mem_ptr = LibNetPGP::pgp_decrypt_buf(pgpio, data_ptr, data_ptr.size, native_keyring, nil, armored ? 1 : 0, 0, passfp, 1, nil) return nil if mem_ptr.null? mem = LibNetPGP::PGPMemory.new(mem_ptr) mem[:buf].read_bytes(mem[:length]) ensure rd.close wr.close end end # Signs data using this secret key. # # Note: {#passphrase} must be set to the correct passphrase prior # to this call. If no passphrase is required, it should be set to ''. # # @param data [String] the data to be signed. # @param armored [Boolean] whether the output should be ASCII armored. # @param options [Hash] less-often used options that override defaults. # * :from [Time] (defaults to Time.now) - signature creation time # * :duration [Numeric] (defaults to 0) - signature duration/expiration # * :hash_algorithm [NetPGP::HashAlgorithm] (defaults to SHA1) - # hash algorithm to use # * :cleartext [Boolean] (defaults to false) - whether this should be # a cleartext/clearsign signature, which includes the original # data in cleartext in the same document. # @return [String] the signed data, or nil on error. def sign(data, armored=true, options={}) valid_options = [:from, :duration, :hash_algorithm, :cleartext] for option in options.keys raise if not valid_options.include?(option) end armored = armored ? 1 : 0 from = options[:from] || Time.now duration = options[:duration] || 0 hashalg = options[:hash_algorithm] || HashAlgorithm::SHA1 cleartext = options[:cleartext] ? 1 : 0 from = from.to_i hashname = HashAlgorithm::to_s(hashalg) pgpio = create_pgpio data_buf = FFI::MemoryPointer.new(:uint8, data.bytesize) data_buf.write_bytes(data) seckey = decrypted_seckey return nil if not seckey memory = nil begin memory_ptr = LibNetPGP::pgp_sign_buf(pgpio, data_buf, data_buf.size, seckey, from, duration, hashname, armored, cleartext) return nil if not memory_ptr or memory_ptr.null? memory = LibNetPGP::PGPMemory.new(memory_ptr) signed_data = memory[:buf].read_bytes(memory[:length]) signed_data ensure LibNetPGP::pgp_memory_free(memory) if memory LibNetPGP::pgp_seckey_free(seckey) if seckey end end # Cleartext signs data using this secret key. # This is a shortcut for {#sign}. # # Note: {#passphrase} must be set to the correct passphrase prior # to this call. If no passphrase is required, it should be set to ''. # # @param data [String] the data to be signed. # @param armored [Boolean] whether the output should be ASCII armored. # @param options [Hash] less-often used options that override defaults. # * :from [Time] (defaults to Time.now) - signature creation time # * :duration [Integer] (defaults to 0) - signature duration/expiration # * :hash_algorithm [{NetPGP::HashAlgorithm}] (defaults to SHA1) - # hash algorithm to use # @return [String] the signed data, or nil on error. def clearsign(data, armored=true, options={}) options[:cleartext] = true sign(data, armored, options) end # Creates a detached signature of a file. # # Note: {#passphrase} must be set to the correct passphrase prior # to this call. If no passphrase is required, it should be set to ''. # # @param infile [String] the path to the input file for which a # signature will be created. # @param sigfile [String] the path to the signature file that will # be created. # # This can be nil, in which case the filename will be the infile # parameter with '.asc' appended. # # @param armored [Boolean] whether the output should be ASCII armored. # @param options [Hash] less-often used options that override defaults. # * :from [Time] (defaults to Time.now) - signature creation time # * :duration [Integer] (defaults to 0) - signature duration/expiration # * :hash_algorithm [{NetPGP::HashAlgorithm}] (defaults to SHA1) - # hash algorithm to use # @return [Boolean] whether the signing was successful. def detached_sign(infile, sigfile=nil, armored=true, options={}) valid_options = [:from, :duration, :hash_algorithm] for option in options.keys raise if not valid_options.include?(option) end armored = armored ? 1 : 0 from = options[:from] || Time.now duration = options[:duration] || 0 hashalg = options[:hash_algorithm] || HashAlgorithm::SHA1 hashname = HashAlgorithm::to_s(hashalg) from = from.to_i pgpio = create_pgpio # Note: pgp_sign_detached calls pgp_seckey_free for us seckey = decrypted_seckey return false if not seckey ret = LibNetPGP::pgp_sign_detached(pgpio, infile, sigfile, seckey, hashname, from, duration, armored, 1) return ret == 1 end def add_subkey(subkey) raise if subkey.subkeys.any? subkey.parent = self subkey.userids = @userids @subkeys.push(subkey) end def self.generate(passphrase, options={}) valid_options = [:key_length, :public_key_algorithm, :algorithm_params, :hash_algorithm, :symmetric_key_algorithm] for option in options.keys raise if not valid_options.include?(option) end key_length = options[:key_length] || 4096 pkalg = options[:public_key_algorithm] || PublicKeyAlgorithm::RSA pkalg_params = options[:algorithm_params] || {e: 65537} hashalg = options[:hash_algorithm] || HashAlgorithm::SHA1 skalg = options[:symmetric_key_algorithm] || SymmetricKeyAlgorithm::CAST5 hashalg_s = HashAlgorithm::to_s(hashalg) skalg_s = SymmetricKeyAlgorithm::to_s(skalg) native_key = nil begin native_key = LibNetPGP::pgp_rsa_new_key(key_length, pkalg_params[:e], hashalg_s, skalg_s) key = SecretKey::from_native(native_key[:key][:seckey]) key.passphrase = passphrase key ensure LibNetPGP::pgp_keydata_free(native_key) if native_key end end def self.from_native(sk, encrypted=false) seckey = SecretKey.new seckey.public_key = PublicKey::from_native(sk[:pubkey]) seckey.string_to_key_usage = LibNetPGP::enum_value(sk[:s2k_usage]) seckey.string_to_key_specifier = LibNetPGP::enum_value(sk[:s2k_specifier]) seckey.symmetric_key_algorithm = LibNetPGP::enum_value(sk[:alg]) seckey.hash_algorithm = LibNetPGP::enum_value(sk[:hash_alg]) || HashAlgorithm::SHA1 seckey.iv = sk[:iv].to_ptr.read_bytes(sk[:iv].size) if not sk[:checkhash].null? seckey.check_hash = sk[:checkhash].read_bytes(LibNetPGP::PGP_CHECKHASH_SIZE) end seckey.mpi = NetPGP::mpis_from_native(sk[:pubkey][:alg], sk) seckey.encrypted = encrypted seckey end def to_native(native) @public_key.to_native(native[:pubkey]) native[:s2k_usage] = @string_to_key_usage native[:s2k_specifier] = @string_to_key_specifier native[:alg] = @symmetric_key_algorithm native[:hash_alg] = @hash_algorithm # zero IV, then copy native[:iv].to_ptr.write_bytes("\x00" * native[:iv].size) native[:iv].to_ptr.write_bytes(@iv) if @iv NetPGP::mpis_to_native(PublicKeyAlgorithm::to_native(@public_key.public_key_algorithm), @mpi, native) # note: this has to come after mpis_to_native because that frees ptrs if @check_hash native[:checkhash] = LibC::calloc(1, LibNetPGP::PGP_CHECKHASH_SIZE) native[:checkhash].write_bytes(@check_hash) end end def to_native_key(native_key) raise if not native_key[:packets].null? native_key[:type] = :PGP_PTAG_CT_SECRET_KEY native_key[:sigid] = @public_key.key_id to_native(native_key[:key][:seckey]) if not @parent @userids.each {|userid| LibNetPGP::dynarray_append_item(native_key, 'uid', :string, userid) } end @raw_subpackets.each {|bytes| packet = LibNetPGP::PGPSubPacket.new length = bytes.bytesize packet[:length] = length packet[:raw] = LibC::calloc(1, length) packet[:raw].write_bytes(bytes) LibNetPGP::dynarray_append_item(native_key, 'packet', LibNetPGP::PGPSubPacket, packet) } end def decrypted_seckey if encrypted? native_ptr = LibC::calloc(1, LibNetPGP::PGPKey.size) native = LibNetPGP::PGPKey.new(native_ptr) native_auto = FFI::AutoPointer.new(native_ptr, LibNetPGP::PGPKey.method(:release)) to_native_key(native) rd, wr = IO.pipe wr.write(@passphrase + "\n") wr.close passfp = LibC::fdopen(rd.to_i, 'r') decrypted = LibNetPGP::pgp_decrypt_seckey(native, passfp) rd.close LibC::fclose(passfp) return nil if not decrypted or decrypted.null? LibNetPGP::PGPSecKey.new(decrypted) else native_ptr = LibC::calloc(1, LibNetPGP::PGPSecKey.size) native = LibNetPGP::PGPSecKey.new(native_ptr) to_native(native) native end end private def create_pgpio pgpio = LibNetPGP::PGPIO.new pgpio[:outs] = LibC::fdopen($stdout.to_i, 'w') pgpio[:errs] = LibC::fdopen($stderr.to_i, 'w') pgpio[:res] = pgpio[:errs] pgpio end end end # module NetPGP