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.

Example Setup:

1. Install OpenPGP
   Mac OSX (homebrew) : `brew install gpg2`
   Redhat Linux: `rpm install gpg2`

2. # Generate senders private and public key
   IOStreams::Pgp.generate_key(name: 'Sender', email: '[email protected]', passphrase: 'sender_passphrase')

3. # Generate receivers private and public key
   IOStreams::Pgp.generate_key(name: 'Receiver', email: '[email protected]', passphrase: 'receiver_passphrase')

Example 1:

# Generate encrypted file for a specific recipient and sign it with senders credentials
data = %w(this is some data that should be encrypted using pgp)
IOStreams::Pgp::Writer.open('secure.gpg', recipient: '[email protected]', signer: '[email protected]', signer_passphrase: 'sender_passphrase') do |output|
  data.each { |word| output.puts(word) }
end

# Decrypt the file sent to `[email protected]` using its private key
# Recipient must also have the senders public key to verify the signature
IOStreams::Pgp::Reader.open('secure.gpg', passphrase: 'receiver_passphrase') do |stream|
  while !stream.eof?
    ap stream.read(10)
    puts
  end
end

Example 2:

# Default user and passphrase to sign the output file:
IOStreams::Pgp::Writer.default_signer            = '[email protected]'
IOStreams::Pgp::Writer.default_signer_passphrase = 'sender_passphrase'

# Default passphrase for decrypting recipients files.
# Note: Usually this would be the senders passphrase, but in this example
#       it is decrypting the file intended for the recipient.
IOStreams::Pgp::Reader.default_passphrase = 'receiver_passphrase'

# Generate encrypted file for a specific recipient and sign it with senders credentials
data = %w(this is some data that should be encrypted using pgp)
IOStreams.writer('secure.gpg', pgp: {recipient: '[email protected]'}) do |output|
  data.each { |word| output.puts(word) }
end

# Decrypt the file sent to `[email protected]` using its private key
# Recipient must also have the senders public key to verify the signature
IOStreams.reader('secure.gpg') do |stream|
  while data = stream.read(10)
    ap data
  end
end

FAQ:

Delete test keys:

IOStreams::Pgp.delete_keys(email: '[email protected]', secret: true)
IOStreams::Pgp.delete_keys(email: '[email protected]', secret: true)

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.

Compression Performance:

Running tests on an Early 2015 Macbook Pro Dual Core with Ruby v2.3.1

Input file: test.log 3.6GB
  :none:  size: 3.6GB  write:  52s  read:  45s
  :zip:   size: 411MB  write:  75s  read:  31s
  :zlib:  size: 241MB  write:  66s  read:  23s  ( 756KB Memory )
  :bzip2: size: 129MB  write: 430s  read: 130s  ( 5MB Memory )

Defined Under Namespace

Classes: Failure, Reader, Writer

Class Method Summary collapse

Class Method Details

.delete_keys(email:, public: true, secret: false) ⇒ Object

Delete a secret and public keys using its 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

public: [true|false]

Whether to delete the public key
Default: true

secret: [true|false]

Whether to delete the secret key
Default: false


147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/io_streams/pgp.rb', line 147

def self.delete_keys(email:, public: true, secret: false)
  cmd = "for i in `gpg --with-colons --fingerprint #{email} | grep \"^fpr\" | cut -d: -f10`; do\n"
  cmd << "gpg --batch --delete-secret-keys \"$i\" ;\n" if secret
  cmd << "gpg --batch --delete-keys \"$i\" ;\n" if public
  cmd << 'done'
  Open3.popen2e(cmd) do |stdin, out, waith_thr|
    output = out.read.chomp
    if waith_thr.value.success?
      return false if output =~ /(public key not found|No public key)/i
      raise(Pgp::Failure, "GPG Failed to delete keys for #{email}: #{output}") if output.include?('error')
      true
    else
      raise(Pgp::Failure, "GPG Failed calling gpg to delete secret keys for #{email}: #{output}")
    end
  end
end

.export(email:, ascii: true, secret: false) ⇒ Object

Returns [String] the key 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

secret: [true|false]

Whether to export the private key
Default: false


190
191
192
193
194
195
196
197
198
199
# File 'lib/io_streams/pgp.rb', line 190

def self.export(email:, ascii: true, secret: false)
  armor            = ascii ? ' --armor' : nil
  cmd              = secret ? '--export-secret-keys' : '--export'
  out, err, status = Open3.capture3("gpg#{armor} #{cmd} #{email}", binmode: true)
  if status.success? && out.length > 0
    out
  else
    raise(Pgp::Failure, "GPG Failed reading key: #{email}: #{err} #{out}")
  end
end

.generate_key(name:, email:, comment: nil, passphrase: nil, key_type: 'RSA', key_length: 4096, subkey_type: 'RSA', subkey_length: key_length, 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)`

See ‘man gpg` for the remaining options



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
# File 'lib/io_streams/pgp.rb', line 107

def self.generate_key(name:, email:, comment: nil, passphrase: nil, key_type: 'RSA', key_length: 4096, subkey_type: 'RSA', subkey_length: key_length, expire_date: nil)
  Open3.popen2e('gpg --batch --gen-key') do |stdin, out, waith_thr|
    stdin.puts "Key-Type: #{key_type}" if key_type
    stdin.puts "Key-Length: #{key_length}" if key_length
    stdin.puts "Subkey-Type: #{subkey_type}" if subkey_type
    stdin.puts "Subkey-Length: #{subkey_length}" if subkey_length
    stdin.puts "Name-Real: #{name}" if name
    stdin.puts "Name-Comment: #{comment}" if comment
    stdin.puts "Name-Email: #{email}" if email
    stdin.puts "Expire-Date: #{expire_date}" if expire_date
    stdin.puts "Passphrase: #{passphrase}" if passphrase
    stdin.puts '%commit'
    stdin.close
    if waith_thr.value.success?
      key_id = nil
      out.each_line do |line|
        if (line = line.chomp) =~ /^gpg: key ([0-9A-F]+) marked as ultimately trusted/
          key_id = $1.to_i(16)
        end
      end
      key_id
    else
      raise(Pgp::Failure, "GPG Failed to generate key: #{out.read.chomp}")
    end
  end
end

.has_key?(email:) ⇒ Boolean

Returns:

  • (Boolean)


164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/io_streams/pgp.rb', line 164

def self.has_key?(email:)
  Open3.popen2e("gpg --list-keys --with-colons #{email}") do |stdin, out, waith_thr|
    output = out.read.chomp
    if waith_thr.value.success?
      output.each_line do |line|
        return true if line.match(/\Auid.*::([^\:]*):\Z/)
      end
      false
    else
      return false if output =~ /(public key not found|No public key)/i
      raise(Pgp::Failure, "GPG Failed calling gpg to list keys for #{email}: #{output}")
    end
  end
end

.import(key) ⇒ Object

Imports the supplied public/private key Returns [String] the output returned from the import command



203
204
205
206
207
208
209
210
# File 'lib/io_streams/pgp.rb', line 203

def self.import(key)
  out, err, status = Open3.capture3('gpg --import', binmode: true, stdin_data: key)
  if status.success? && out.length > 0
    out
  else
    raise(Pgp::Failure, "GPG Failed importing key: #{err} #{out}")
  end
end