klacointe-openpgp 0.0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +19 -0
- data/README +104 -0
- data/Rakefile +19 -0
- data/VERSION +1 -0
- data/bin/openpgp +3 -0
- data/lib/openpgp/algorithm.rb +57 -0
- data/lib/openpgp/armor.rb +120 -0
- data/lib/openpgp/buffer.rb +100 -0
- data/lib/openpgp/cipher/3des.rb +9 -0
- data/lib/openpgp/cipher/aes.rb +26 -0
- data/lib/openpgp/cipher/blowfish.rb +9 -0
- data/lib/openpgp/cipher/cast5.rb +9 -0
- data/lib/openpgp/cipher/idea.rb +9 -0
- data/lib/openpgp/cipher/twofish.rb +9 -0
- data/lib/openpgp/cipher.rb +117 -0
- data/lib/openpgp/client/gnupg.rb +583 -0
- data/lib/openpgp/digest/md5.rb +7 -0
- data/lib/openpgp/digest/rmd160.rb +7 -0
- data/lib/openpgp/digest/sha1.rb +7 -0
- data/lib/openpgp/digest/sha2.rb +19 -0
- data/lib/openpgp/digest.rb +60 -0
- data/lib/openpgp/engine/gnupg.rb +194 -0
- data/lib/openpgp/engine/openssl.rb +47 -0
- data/lib/openpgp/engine.rb +46 -0
- data/lib/openpgp/message.rb +106 -0
- data/lib/openpgp/packet.rb +507 -0
- data/lib/openpgp/random.rb +31 -0
- data/lib/openpgp/s2k.rb +186 -0
- data/lib/openpgp/util.rb +65 -0
- data/lib/openpgp/version.rb +14 -0
- data/lib/openpgp.rb +17 -0
- metadata +96 -0
@@ -0,0 +1,194 @@
|
|
1
|
+
module OpenPGP class Engine
|
2
|
+
##
|
3
|
+
# GNU Privacy Guard (GnuPG) wrapper.
|
4
|
+
#
|
5
|
+
# @see http://www.gnupg.org/
|
6
|
+
class GnuPG < Engine
|
7
|
+
class Error < IOError; end
|
8
|
+
|
9
|
+
def self.available?
|
10
|
+
self.new.available?
|
11
|
+
end
|
12
|
+
|
13
|
+
OPTIONS = {
|
14
|
+
:batch => true,
|
15
|
+
:quiet => true,
|
16
|
+
:no_verbose => true,
|
17
|
+
:no_tty => true,
|
18
|
+
:no_permission_warning => true,
|
19
|
+
:no_random_seed_file => true,
|
20
|
+
}
|
21
|
+
|
22
|
+
attr_accessor :where
|
23
|
+
attr_accessor :options
|
24
|
+
|
25
|
+
def initialize(options = {})
|
26
|
+
@where = '/usr/bin/env gpg' # FIXME
|
27
|
+
@options = OPTIONS.merge!(options)
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Determines if GnuPG is available.
|
32
|
+
def available?
|
33
|
+
!!version
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Returns the GnuPG version number.
|
38
|
+
def version
|
39
|
+
exec(:version).readline =~ /^gpg \(GnuPG\) (.*)$/ ? $1 : nil
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Generates a new OpenPGP keypair and stores it GnuPG's keyring.
|
44
|
+
def gen_key(info = {})
|
45
|
+
stdin, stdout, stderr = exec3(:gen_key) do |stdin, stdout, stderr|
|
46
|
+
stdin.puts "Key-Type: #{info[:key_type]}" if info[:key_type]
|
47
|
+
stdin.puts "Key-Length: #{info[:key_length]}" if info[:key_length]
|
48
|
+
stdin.puts "Subkey-Type: #{info[:subkey_type]}" if info[:subkey_type]
|
49
|
+
stdin.puts "Subkey-Length: #{info[:subkey_length]}" if info[:subkey_length]
|
50
|
+
stdin.puts "Name-Real: #{info[:name]}" if info[:name]
|
51
|
+
stdin.puts "Name-Comment: #{info[:comment]}" if info[:comment]
|
52
|
+
stdin.puts "Name-Email: #{info[:email]}" if info[:email]
|
53
|
+
stdin.puts "Expire-Date: #{info[:expire_date]}" if info[:expire_date]
|
54
|
+
stdin.puts "Passphrase: #{info[:passphrase]}" if info[:passphrase]
|
55
|
+
stdin.puts "%commit"
|
56
|
+
end
|
57
|
+
stderr.each_line do |line|
|
58
|
+
if (line = line.chomp) =~ /^gpg: key ([0-9A-F]+) marked as ultimately trusted/
|
59
|
+
return $1.to_i(16) # the key ID
|
60
|
+
end
|
61
|
+
end
|
62
|
+
return nil
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Exports a specified key from the GnuPG keyring.
|
67
|
+
def export(key_id = nil, opts = {})
|
68
|
+
OpenPGP::Message.parse(exec([:export, *[key_id].flatten], opts ).read)
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
##
|
73
|
+
# Imports a specified keyfile into the GnuPG keyring.
|
74
|
+
def import()
|
75
|
+
# TODO
|
76
|
+
end
|
77
|
+
|
78
|
+
def delete_secret_and_public_key(key_id)
|
79
|
+
opts = {:batch => true}
|
80
|
+
OpenPGP::Message.parse(exec([:delete_secret_and_public_key, key_fingerprint(key_id)], opts ).read)
|
81
|
+
end
|
82
|
+
|
83
|
+
def key_fingerprint(key_id, opts = {})
|
84
|
+
message = exec([:fingerprint, *[key_id].flatten], opts ).read
|
85
|
+
if message =~ /Key fingerprint = (.*)\n/
|
86
|
+
return $1.delete(" ")
|
87
|
+
end
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# Returns an array of key IDs/titles of the keys in the public keyring.
|
93
|
+
def list_keys()
|
94
|
+
# TODO
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Encrypts the given plaintext to the specified recipients.
|
99
|
+
def encrypt(plaintext, options = {})
|
100
|
+
# TODO
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
# Decrypts the given ciphertext using the specified key ID.
|
105
|
+
def decrypt(ciphertext, options = {})
|
106
|
+
# TODO
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# Makes an OpenPGP signature.
|
111
|
+
def sign()
|
112
|
+
# TODO
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Makes an OpenPGP signature.
|
117
|
+
def sign_file(key_id, file, passphrase)
|
118
|
+
OpenPGP::Message.parse(exec([:sign, file],{ :local_user => key_id, :passphrase => passphrase}).read)
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Makes a clear text OpenPGP signature.
|
123
|
+
def clearsign()
|
124
|
+
# TODO
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
# Makes a detached OpenPGP signature.
|
129
|
+
def detach_sign()
|
130
|
+
# TODO
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Verifies an OpenPGP signature.
|
135
|
+
def verify(key_id, file)
|
136
|
+
OpenPGP::Message.parse(exec([:verify, file],{ :local_user => key_id}).read)
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# Executes a GnuPG command, yielding the standard input and returning
|
141
|
+
# the standard output.
|
142
|
+
def exec(command, options = {}, &block) #:yields: stdin
|
143
|
+
exec4(command, options) do |pid, stdin, stdout, stderr|
|
144
|
+
block.call(stdin) if block_given?
|
145
|
+
stdin.close_write
|
146
|
+
pid, status = Process.waitpid2(pid)
|
147
|
+
raise Error, stderr.read.chomp if status.exitstatus.nonzero?
|
148
|
+
stdout
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
# Executes a GnuPG command, yielding and returning the standard input,
|
154
|
+
# output and error.
|
155
|
+
def exec3(command, options = {}, &block) #:yields: stdin, stdout, stderr
|
156
|
+
exec4(command, options) do |pid, stdin, stdout, stderr|
|
157
|
+
block.call(stdin, stdout, stderr) if block_given?
|
158
|
+
stdin.close_write
|
159
|
+
pid, status = Process.waitpid2(pid)
|
160
|
+
raise Error, stderr.read.chomp if status.exitstatus.nonzero?
|
161
|
+
[stdin, stdout, stderr]
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
##
|
166
|
+
# Executes a GnuPG command, yielding the process identifier as well as
|
167
|
+
# the standard input, output and error.
|
168
|
+
def exec4(command, options = {}, &block) #:yields: pid, stdin, stdout, stderr
|
169
|
+
require 'rubygems'
|
170
|
+
require 'open4'
|
171
|
+
p Open4.popen4(cmdline(command, options))
|
172
|
+
block.call(*Open4.popen4(cmdline(command, options)))
|
173
|
+
end
|
174
|
+
|
175
|
+
protected
|
176
|
+
|
177
|
+
##
|
178
|
+
# Constructs the GnuPG command-line for use with +exec+.
|
179
|
+
def cmdline(command, options = {})
|
180
|
+
command = [command].flatten
|
181
|
+
cmdline = [where]
|
182
|
+
cmdline += @options.merge(options).map { |k, v| !v ? nil : "#{option(k)} #{v == true ? '' : v.to_s}".rstrip }.compact
|
183
|
+
cmdline << option(command.shift)
|
184
|
+
cmdline += command
|
185
|
+
cmdline.flatten.join(' ').strip
|
186
|
+
end
|
187
|
+
|
188
|
+
##
|
189
|
+
# Translates Ruby symbols into GnuPG option arguments.
|
190
|
+
def option(option)
|
191
|
+
"--" << option.to_s.gsub('_', '-')
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module OpenPGP
|
2
|
+
class Engine
|
3
|
+
class OpenSSL < Engine
|
4
|
+
def self.load!(reload = false)
|
5
|
+
require 'openssl' unless defined?(::OpenSSL) || reload
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.install!
|
9
|
+
load!
|
10
|
+
[Random, Digest].each { |mod| install_extensions! mod }
|
11
|
+
end
|
12
|
+
|
13
|
+
module Random #:nodoc:
|
14
|
+
def number(bits = 32, options = {})
|
15
|
+
::OpenSSL::BN.rand(bits)
|
16
|
+
end
|
17
|
+
|
18
|
+
def prime(bits, options = {})
|
19
|
+
::OpenSSL::BN.generate_prime(bits, options[:safe])
|
20
|
+
end
|
21
|
+
|
22
|
+
def bytes(count, &block)
|
23
|
+
::OpenSSL::Random.random_bytes(count)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module Digest #:nodoc:
|
28
|
+
def size
|
29
|
+
::OpenSSL::Digest.new(algorithm.to_s).digest_length
|
30
|
+
end
|
31
|
+
|
32
|
+
def hexdigest(data)
|
33
|
+
::OpenSSL::Digest.hexdigest(algorithm.to_s, data).upcase
|
34
|
+
end
|
35
|
+
|
36
|
+
def digest(data)
|
37
|
+
::OpenSSL::Digest.digest(algorithm.to_s, data)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module Cipher #:nodoc:
|
42
|
+
# TODO
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module OpenPGP
|
2
|
+
class Engine
|
3
|
+
autoload :GnuPG, 'openpgp/engine/gnupg'
|
4
|
+
autoload :OpenSSL, 'openpgp/engine/openssl'
|
5
|
+
|
6
|
+
def self.available?
|
7
|
+
begin
|
8
|
+
load!(true)
|
9
|
+
return true
|
10
|
+
rescue LoadError => e
|
11
|
+
return false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.load!(reload = false)
|
16
|
+
raise LoadError
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.install!
|
20
|
+
load!
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.use(&block)
|
24
|
+
load!
|
25
|
+
block.call(self)
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def self.install_extensions!(extension)
|
31
|
+
name = extension.name.split('::').last.to_sym
|
32
|
+
|
33
|
+
klass = OpenPGP.const_get(name)
|
34
|
+
extension.constants.each do |const|
|
35
|
+
klass.send(:remove_const, const)
|
36
|
+
klass.const_set(const, extension.const_get(const))
|
37
|
+
end
|
38
|
+
|
39
|
+
target = (class << klass; self; end)
|
40
|
+
extension.instance_methods(false).each do |method|
|
41
|
+
target.send(:remove_method, method)
|
42
|
+
target.send(:include, extension)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module OpenPGP
|
2
|
+
##
|
3
|
+
# OpenPGP message.
|
4
|
+
#
|
5
|
+
# @see http://tools.ietf.org/html/rfc4880#section-4.1
|
6
|
+
# @see http://tools.ietf.org/html/rfc4880#section-11
|
7
|
+
# @see http://tools.ietf.org/html/rfc4880#section-11.3
|
8
|
+
class Message
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
attr_accessor :packets
|
12
|
+
|
13
|
+
##
|
14
|
+
# Creates an encrypted OpenPGP message.
|
15
|
+
def self.encrypt(data, options = {}, &block)
|
16
|
+
if options[:symmetric]
|
17
|
+
key = (options[:key] || S2K::DEFAULT.new(options[:passphrase]))
|
18
|
+
cipher = (options[:cipher] || Cipher::DEFAULT).new(key)
|
19
|
+
|
20
|
+
msg = self.new do |msg|
|
21
|
+
msg << Packet::SymmetricSessionKey.new(:algorithm => cipher.identifier, :s2k => key)
|
22
|
+
msg << Packet::EncryptedData.new do |packet|
|
23
|
+
plaintext = self.write do |msg|
|
24
|
+
case data
|
25
|
+
when Message then data.each { |packet| msg << packet }
|
26
|
+
when Packet then msg << data
|
27
|
+
else msg << Packet::LiteralData.new(:data => data)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
packet.data = cipher.encrypt(plaintext)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
block_given? ? block.call(msg) : msg
|
35
|
+
else
|
36
|
+
raise NotImplementedError # TODO
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
def self.decrypt(data, options = {}, &block)
|
42
|
+
raise NotImplementedError # TODO
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Parses an OpenPGP message.
|
47
|
+
#
|
48
|
+
# @see http://tools.ietf.org/html/rfc4880#section-4.1
|
49
|
+
# @see http://tools.ietf.org/html/rfc4880#section-4.2
|
50
|
+
def self.parse(data)
|
51
|
+
data = Buffer.new(data.to_str) if data.respond_to?(:to_str)
|
52
|
+
|
53
|
+
msg = self.new
|
54
|
+
until data.eof?
|
55
|
+
if packet = OpenPGP::Packet.parse(data)
|
56
|
+
msg << packet
|
57
|
+
else
|
58
|
+
raise "Invalid OpenPGP message data at position #{data.pos}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
msg
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.write(io = nil, &block)
|
65
|
+
data = self.new(&block).to_s
|
66
|
+
io.respond_to?(:write) ? io.write(data) : data
|
67
|
+
end
|
68
|
+
|
69
|
+
def initialize(*packets, &block)
|
70
|
+
@packets = packets.flatten
|
71
|
+
block.call(self) if block_given?
|
72
|
+
end
|
73
|
+
|
74
|
+
def each(&block) # :yields: packet
|
75
|
+
packets.each(&block)
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_a
|
79
|
+
packets.to_a
|
80
|
+
end
|
81
|
+
|
82
|
+
def <<(packet)
|
83
|
+
packets << packet
|
84
|
+
end
|
85
|
+
|
86
|
+
def empty?
|
87
|
+
packets.empty?
|
88
|
+
end
|
89
|
+
|
90
|
+
def size
|
91
|
+
inject(0) { |sum, packet| sum + packet.size }
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_s
|
95
|
+
Buffer.write do |buffer|
|
96
|
+
packets.each do |packet|
|
97
|
+
if body = packet.body
|
98
|
+
buffer.write_byte(packet.class.tag | 0xC0)
|
99
|
+
buffer.write_byte(body.size)
|
100
|
+
buffer.write_bytes(body)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|