schleuder 2.2.4 → 3.2.2
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.
- checksums.yaml +5 -5
- data/README.md +138 -0
- data/Rakefile +136 -0
- data/bin/pinentry-clearpassphrase +72 -0
- data/bin/schleuder +9 -89
- data/bin/schleuder-api-daemon +4 -0
- data/db/migrate/20140501103532_create_lists.rb +39 -0
- data/db/migrate/20140501112859_create_subscriptions.rb +21 -0
- data/db/migrate/201508092100_add_language_to_lists.rb +11 -0
- data/db/migrate/20150812165700_change_keywords_admin_only_defaults.rb +8 -0
- data/db/migrate/20150813235800_add_forward_all_incoming_to_admins.rb +11 -0
- data/db/migrate/201508141727_change_send_encrypted_only_default.rb +8 -0
- data/db/migrate/201508222143_add_logfiles_to_keep_to_lists.rb +11 -0
- data/db/migrate/201508261723_rename_delivery_disabled_to_delivery_enabled_and_change_default.rb +14 -0
- data/db/migrate/201508261815_strip_gpg_passphrase.rb +11 -0
- data/db/migrate/201508261827_remove_default_mime.rb +9 -0
- data/db/migrate/20160501172700_fix_headers_to_meta_defaults.rb +8 -0
- data/db/migrate/20170713215059_add_internal_footer_to_list.rb +11 -0
- data/db/schema.rb +62 -0
- data/etc/init.d/schleuder-api-daemon +87 -0
- data/etc/list-defaults.yml +123 -0
- data/etc/postfix/schleuder_sqlite.cf +28 -0
- data/etc/schleuder-api-daemon.service +10 -0
- data/etc/schleuder.cron.weekly +6 -0
- data/etc/schleuder.yml +61 -0
- data/lib/schleuder-api-daemon.rb +420 -0
- data/lib/schleuder.rb +81 -47
- data/lib/schleuder/cli.rb +334 -0
- data/lib/schleuder/cli/cert.rb +24 -0
- data/lib/schleuder/cli/schleuder_cert_manager.rb +84 -0
- data/lib/schleuder/cli/subcommand_fix.rb +11 -0
- data/lib/schleuder/conf.rb +131 -0
- data/lib/schleuder/errors/active_model_error.rb +15 -0
- data/lib/schleuder/errors/base.rb +17 -0
- data/lib/schleuder/errors/decryption_failed.rb +16 -0
- data/lib/schleuder/errors/fatal_error.rb +13 -0
- data/lib/schleuder/errors/file_not_found.rb +14 -0
- data/lib/schleuder/errors/invalid_listname.rb +13 -0
- data/lib/schleuder/errors/key_adduid_failed.rb +13 -0
- data/lib/schleuder/errors/key_generation_failed.rb +16 -0
- data/lib/schleuder/errors/keyword_admin_only.rb +13 -0
- data/lib/schleuder/errors/list_exists.rb +13 -0
- data/lib/schleuder/errors/list_not_found.rb +14 -0
- data/lib/schleuder/errors/list_property_missing.rb +14 -0
- data/lib/schleuder/errors/listdir_problem.rb +16 -0
- data/lib/schleuder/errors/loading_list_settings_failed.rb +14 -0
- data/lib/schleuder/errors/message_empty.rb +14 -0
- data/lib/schleuder/errors/message_not_from_admin.rb +13 -0
- data/lib/schleuder/errors/message_sender_not_subscribed.rb +13 -0
- data/lib/schleuder/errors/message_too_big.rb +14 -0
- data/lib/schleuder/errors/message_unauthenticated.rb +13 -0
- data/lib/schleuder/errors/message_unencrypted.rb +13 -0
- data/lib/schleuder/errors/message_unsigned.rb +13 -0
- data/lib/schleuder/errors/standard_error.rb +5 -0
- data/lib/schleuder/errors/too_many_keys.rb +17 -0
- data/lib/schleuder/errors/unknown_list_option.rb +14 -0
- data/lib/schleuder/filters/auth_filter.rb +39 -0
- data/lib/schleuder/filters/bounces_filter.rb +12 -0
- data/lib/schleuder/filters/forward_filter.rb +17 -0
- data/lib/schleuder/filters/forward_incoming.rb +13 -0
- data/lib/schleuder/filters/hotmail_message_filter.rb +25 -0
- data/lib/schleuder/filters/max_message_size.rb +14 -0
- data/lib/schleuder/filters/request_filter.rb +26 -0
- data/lib/schleuder/filters/send_key_filter.rb +20 -0
- data/lib/schleuder/filters/strip_alternative_filter.rb +21 -0
- data/lib/schleuder/filters_runner.rb +83 -0
- data/lib/schleuder/gpgme/ctx.rb +274 -0
- data/lib/schleuder/gpgme/import_status.rb +27 -0
- data/lib/schleuder/gpgme/key.rb +212 -0
- data/lib/schleuder/gpgme/sub_key.rb +13 -0
- data/lib/schleuder/gpgme/user_id.rb +22 -0
- data/lib/schleuder/list.rb +318 -127
- data/lib/schleuder/list_builder.rb +139 -0
- data/lib/schleuder/listlogger.rb +31 -0
- data/lib/schleuder/logger.rb +23 -0
- data/lib/schleuder/logger_notifications.rb +69 -0
- data/lib/schleuder/mail/message.rb +482 -0
- data/lib/schleuder/mail/parts_list.rb +9 -0
- data/lib/schleuder/plugin_runners/base.rb +91 -0
- data/lib/schleuder/plugin_runners/list_plugins_runner.rb +24 -0
- data/lib/schleuder/plugin_runners/request_plugins_runner.rb +27 -0
- data/lib/schleuder/plugins/attach_listkey.rb +17 -0
- data/lib/schleuder/plugins/get_version.rb +7 -0
- data/lib/schleuder/plugins/key_management.rb +113 -0
- data/lib/schleuder/plugins/list_management.rb +15 -0
- data/lib/schleuder/plugins/resend.rb +196 -0
- data/lib/schleuder/plugins/sign_this.rb +46 -0
- data/lib/schleuder/plugins/subscription_management.rb +140 -0
- data/lib/schleuder/runner.rb +130 -0
- data/lib/schleuder/subscription.rb +98 -0
- data/lib/schleuder/validators/boolean_validator.rb +7 -0
- data/lib/schleuder/validators/email_validator.rb +7 -0
- data/lib/schleuder/validators/fingerprint_validator.rb +7 -0
- data/lib/schleuder/validators/greater_than_zero_validator.rb +7 -0
- data/lib/schleuder/validators/no_line_breaks_validator.rb +7 -0
- data/lib/schleuder/version.rb +1 -1
- data/locales/de.yml +179 -0
- data/locales/en.yml +179 -0
- metadata +305 -108
- checksums.yaml.gz.sig +0 -3
- data.tar.gz.sig +0 -2
- data/LICENSE +0 -339
- data/README +0 -32
- data/bin/schleuder-fix-gem-dependencies +0 -37
- data/bin/schleuder-init-setup +0 -37
- data/bin/schleuder-migrate-v2.1-to-v2.2 +0 -225
- data/bin/schleuder-newlist +0 -413
- data/contrib/check-expired-keys.rb +0 -60
- data/contrib/mutt-schleuder-colors.rc +0 -10
- data/contrib/mutt-schleuder-resend.vim +0 -24
- data/contrib/smtpserver.rb +0 -76
- data/ext/default-list.conf +0 -149
- data/ext/default-members.conf +0 -7
- data/ext/list.conf.example +0 -14
- data/ext/schleuder.conf +0 -64
- data/lib/schleuder/archiver.rb +0 -46
- data/lib/schleuder/crypt.rb +0 -210
- data/lib/schleuder/errors.rb +0 -5
- data/lib/schleuder/list_config.rb +0 -146
- data/lib/schleuder/log/listlogger.rb +0 -57
- data/lib/schleuder/log/outputter/emailoutputter.rb +0 -120
- data/lib/schleuder/log/outputter/metaemailoutputter.rb +0 -50
- data/lib/schleuder/log/schleuderlogger.rb +0 -34
- data/lib/schleuder/mail.rb +0 -873
- data/lib/schleuder/mailer.rb +0 -26
- data/lib/schleuder/member.rb +0 -69
- data/lib/schleuder/plugin.rb +0 -54
- data/lib/schleuder/processor.rb +0 -363
- data/lib/schleuder/schleuder_config.rb +0 -75
- data/lib/schleuder/storage.rb +0 -84
- data/lib/schleuder/utils.rb +0 -80
- data/man/schleuder-newlist.8 +0 -174
- data/man/schleuder.8 +0 -416
- data/plugins/README +0 -20
- data/plugins/manage_keys_plugin.rb +0 -113
- data/plugins/manage_members_plugin.rb +0 -156
- data/plugins/manage_self_plugin.rb +0 -26
- data/plugins/resend_plugin.rb +0 -35
- data/plugins/sign_this_plugin.rb +0 -14
- data/plugins/version_plugin.rb +0 -12
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
module GPGME
|
|
2
|
+
class Ctx
|
|
3
|
+
IMPORT_FLAGS = {
|
|
4
|
+
'new_key' => 1,
|
|
5
|
+
'new_uids' => 2,
|
|
6
|
+
'new_signatures' => 4,
|
|
7
|
+
'new_subkeys' => 8
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
def keyimport(keydata)
|
|
11
|
+
self.import_keys(GPGME::Data.new(keydata))
|
|
12
|
+
result = self.import_result
|
|
13
|
+
result.imports.map(&:set_action)
|
|
14
|
+
result
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# TODO: find solution for I18n — could be a different language in API-clients than here!
|
|
18
|
+
def interpret_import_result(import_result)
|
|
19
|
+
case import_result.imports.size
|
|
20
|
+
when 1
|
|
21
|
+
import_status = import_result.imports.first
|
|
22
|
+
if import_status.action == 'not imported'
|
|
23
|
+
[nil, "Key #{import_status.fpr} could not be imported!"]
|
|
24
|
+
else
|
|
25
|
+
[import_status.fpr, nil]
|
|
26
|
+
end
|
|
27
|
+
when 0
|
|
28
|
+
[nil, "The given key material did not contain any keys!"]
|
|
29
|
+
else
|
|
30
|
+
# TODO: report import-stati of the keys?
|
|
31
|
+
[nil, "The given key material contained more than one key, could not determine which fingerprint to use. Please set it manually!"]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def find_keys(input=nil, secret_only=nil)
|
|
36
|
+
_, input = clean_and_classify_input(input)
|
|
37
|
+
keys(input, secret_only)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def find_distinct_key(input=nil, secret_only=nil)
|
|
41
|
+
_, input = clean_and_classify_input(input)
|
|
42
|
+
keys = keys(input, secret_only)
|
|
43
|
+
if keys.size == 1
|
|
44
|
+
keys.first
|
|
45
|
+
else
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def clean_and_classify_input(input)
|
|
51
|
+
case input
|
|
52
|
+
when /.*?([^ <>]+@[^ <>]+).*?/
|
|
53
|
+
[:email, "<#{$1}>"]
|
|
54
|
+
when /^http/
|
|
55
|
+
[:url, input]
|
|
56
|
+
when Conf::FINGERPRINT_REGEXP
|
|
57
|
+
[:fingerprint, "0x#{input.gsub(/^0x/, '')}"]
|
|
58
|
+
else
|
|
59
|
+
[nil, input]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Tell gpgme to use the given binary.
|
|
64
|
+
def self.set_gpg_path_from_env
|
|
65
|
+
path = ENV['GPGBIN'].to_s
|
|
66
|
+
if ! path.empty?
|
|
67
|
+
Schleuder.logger.debug "setting gpg to use #{path}"
|
|
68
|
+
GPGME::Engine.set_info(GPGME::PROTOCOL_OpenPGP, path, ENV['GNUPGHOME'])
|
|
69
|
+
if gpg_engine.version.nil?
|
|
70
|
+
$stderr.puts "Error: The binary you specified doesn't provide a gpg-version."
|
|
71
|
+
exit 1
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.sufficient_gpg_version?(required)
|
|
77
|
+
Gem::Version.new(required) <= Gem::Version.new(gpg_engine.version)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.check_gpg_version
|
|
81
|
+
if ! sufficient_gpg_version?('2.0')
|
|
82
|
+
$stderr.puts "Error: GnuPG version >= 2.0 required.\nPlease install it and/or provide the path to the binary via the environment-variable GPGBIN.\nExample: GPGBIN=/opt/gpg2/bin/gpg ..."
|
|
83
|
+
exit 1
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.gpg_engine
|
|
88
|
+
GPGME::Engine.info.find {|e| e.protocol == GPGME::PROTOCOL_OpenPGP }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def refresh_keys(keys)
|
|
92
|
+
# reorder keys so the update pattern is random
|
|
93
|
+
output = keys.shuffle.map do |key|
|
|
94
|
+
# Sleep a short while to make traffic analysis less easy.
|
|
95
|
+
sleep rand(1.0..5.0)
|
|
96
|
+
refresh_key(key.fingerprint).presence
|
|
97
|
+
end
|
|
98
|
+
# TODO: drop version check once we killed gpg 2.0 support.
|
|
99
|
+
if GPGME::Ctx.sufficient_gpg_version?('2.1')
|
|
100
|
+
`gpgconf --kill dirmngr`
|
|
101
|
+
end
|
|
102
|
+
output.compact.join("\n")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def refresh_key(fingerprint)
|
|
106
|
+
args = "#{keyserver_arg} --refresh-keys #{fingerprint}"
|
|
107
|
+
gpgerr, gpgout, exitcode = self.class.gpgcli(args)
|
|
108
|
+
|
|
109
|
+
if exitcode > 0
|
|
110
|
+
# Return filtered error messages. Include gpgkeys-messages from stdout
|
|
111
|
+
# (gpg 2.0 does that), which could e.g. report a failure to connect to
|
|
112
|
+
# the keyserver.
|
|
113
|
+
res = [
|
|
114
|
+
refresh_key_filter_messages(gpgerr),
|
|
115
|
+
refresh_key_filter_messages(gpgout).grep(/^gpgkeys: /)
|
|
116
|
+
].flatten.compact
|
|
117
|
+
# if there was an error that we don't filter out,
|
|
118
|
+
# we better kill dirmngr, so it hopefully won't suffer
|
|
119
|
+
# from the same error during the next run.
|
|
120
|
+
# See #309 for background
|
|
121
|
+
# TODO: drop version check once we killed gpg 2.0 support.
|
|
122
|
+
if !res.empty? && GPGME::Ctx.sufficient_gpg_version?('2.1')
|
|
123
|
+
`gpgconf --kill dirmngr`
|
|
124
|
+
end
|
|
125
|
+
res.join("\n")
|
|
126
|
+
else
|
|
127
|
+
lines = translate_output('key_updated', gpgout).reject do |line|
|
|
128
|
+
# Reduce the noise a little.
|
|
129
|
+
line.match(/.* \(unchanged\):$/)
|
|
130
|
+
end
|
|
131
|
+
lines.join("\n")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def fetch_key(input)
|
|
136
|
+
arguments, error = fetch_key_gpg_arguments_for(input)
|
|
137
|
+
return error if error
|
|
138
|
+
|
|
139
|
+
gpgerr, gpgout, exitcode = self.class.gpgcli(arguments)
|
|
140
|
+
|
|
141
|
+
# Unfortunately gpg doesn't exit with code > 0 if `--fetch-key` fails.
|
|
142
|
+
if exitcode > 0 || gpgerr.grep(/ unable to fetch /).presence
|
|
143
|
+
"Fetching #{input} did not succeed:\n#{gpgerr.join("\n")}"
|
|
144
|
+
else
|
|
145
|
+
translate_output('key_fetched', gpgout).join("\n")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def fetch_key_gpg_arguments_for(input)
|
|
150
|
+
case input
|
|
151
|
+
when Conf::FINGERPRINT_REGEXP
|
|
152
|
+
"#{keyserver_arg} --recv-key #{input}"
|
|
153
|
+
when /^http/
|
|
154
|
+
"--fetch-key #{input}"
|
|
155
|
+
when /@/
|
|
156
|
+
# --recv-key doesn't work with email-addresses, so we use --locate-key
|
|
157
|
+
# restricted to keyservers.
|
|
158
|
+
"#{keyserver_arg} --auto-key-locate keyserver --locate-key #{input}"
|
|
159
|
+
else
|
|
160
|
+
[nil, I18n.t("fetch_key.invalid_input")]
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def translate_output(locale_key, gpgoutput)
|
|
165
|
+
import_states = translate_import_data(gpgoutput)
|
|
166
|
+
strings = import_states.map do |fingerprint, states|
|
|
167
|
+
key = find_distinct_key(fingerprint)
|
|
168
|
+
I18n.t(locale_key, { key_oneline: key.oneline,
|
|
169
|
+
states: states.to_sentence })
|
|
170
|
+
end
|
|
171
|
+
strings
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def translate_import_data(gpgoutput)
|
|
175
|
+
result = {}
|
|
176
|
+
gpgoutput.grep(/IMPORT_OK/) do |import_ok|
|
|
177
|
+
next if import_ok.blank?
|
|
178
|
+
|
|
179
|
+
import_status, fingerprint = import_ok.split(/\s/).slice(2, 2)
|
|
180
|
+
import_status = import_status.to_i
|
|
181
|
+
states = []
|
|
182
|
+
|
|
183
|
+
if import_status == 0
|
|
184
|
+
states << I18n.t("import_states.unchanged")
|
|
185
|
+
else
|
|
186
|
+
IMPORT_FLAGS.each do |text, int|
|
|
187
|
+
if (import_status & int) > 0
|
|
188
|
+
states << I18n.t("import_states.#{text}")
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
result[fingerprint] = states
|
|
193
|
+
end
|
|
194
|
+
result
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Unfortunately we can't distinguish between a failure to connect the
|
|
198
|
+
# keyserver, and a failure to find the key on the server. So we try to
|
|
199
|
+
# filter misleading errors to check if there are any to be reported.
|
|
200
|
+
def refresh_key_filter_messages(strings)
|
|
201
|
+
strings.reject do |line|
|
|
202
|
+
line.chomp == 'gpg: keyserver refresh failed: No data' ||
|
|
203
|
+
line.match(/^gpgkeys: key .* not found on keyserver/) ||
|
|
204
|
+
line.match(/^gpg: refreshing /) ||
|
|
205
|
+
line.match(/^gpg: requesting key /) ||
|
|
206
|
+
line.match(/^gpg: no valid OpenPGP data found/)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def self.gpgcli(args)
|
|
211
|
+
exitcode = -1
|
|
212
|
+
errors = []
|
|
213
|
+
output = []
|
|
214
|
+
base_cmd = gpg_engine.file_name
|
|
215
|
+
base_args = "--no-greeting --no-permission-warning --quiet --armor --trust-model always --no-tty --command-fd 0 --status-fd 1"
|
|
216
|
+
cmd = [base_cmd, base_args, args].flatten.join(' ')
|
|
217
|
+
Open3.popen3(cmd) do |stdin, stdout, stderr, thread|
|
|
218
|
+
if block_given?
|
|
219
|
+
output = yield(stdin, stdout, stderr)
|
|
220
|
+
else
|
|
221
|
+
output = stdout.readlines
|
|
222
|
+
end
|
|
223
|
+
stdin.close if ! stdin.closed?
|
|
224
|
+
errors = stderr.readlines
|
|
225
|
+
exitcode = thread.value.exitstatus
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
[errors, output, exitcode]
|
|
229
|
+
rescue Errno::ENOENT
|
|
230
|
+
raise 'Need gpg in $PATH or in $GPGBIN'
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def self.gpgcli_expect(args)
|
|
234
|
+
gpgcli(args) do |stdin, stdout, stderr|
|
|
235
|
+
counter = 0
|
|
236
|
+
while line = stdout.gets rescue nil
|
|
237
|
+
counter += 1
|
|
238
|
+
if counter > 1042
|
|
239
|
+
return "Too many input-lines from gpg, something went wrong"
|
|
240
|
+
end
|
|
241
|
+
output, error = yield(line.chomp)
|
|
242
|
+
if output == false
|
|
243
|
+
return error
|
|
244
|
+
elsif output
|
|
245
|
+
stdin.puts output
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def self.spawn_daemon(name, args)
|
|
252
|
+
delete_daemon_socket(name)
|
|
253
|
+
cmd = "#{name} #{args} --daemon > /dev/null 2>&1"
|
|
254
|
+
if ! system(cmd)
|
|
255
|
+
return [false, "#{name} exited with code #{$?}"]
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def self.delete_daemon_socket(name)
|
|
260
|
+
path = File.join(ENV["GNUPGHOME"], "S.#{name}")
|
|
261
|
+
if File.exist?(path)
|
|
262
|
+
File.delete(path)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def keyserver_arg
|
|
267
|
+
if Conf.keyserver.present?
|
|
268
|
+
"--keyserver #{Conf.keyserver}"
|
|
269
|
+
else
|
|
270
|
+
""
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module GPGME
|
|
2
|
+
class ImportStatus
|
|
3
|
+
attr_reader :action
|
|
4
|
+
|
|
5
|
+
# Unfortunately in initialize() @status and @result are not yet intialized.
|
|
6
|
+
def set_action
|
|
7
|
+
@action ||= if self.status > 0
|
|
8
|
+
'imported'
|
|
9
|
+
elsif self.result == 0
|
|
10
|
+
'unchanged'
|
|
11
|
+
else
|
|
12
|
+
# An error happened.
|
|
13
|
+
# TODO: Give details by going through the list of errors in
|
|
14
|
+
# "gpg-errors.h" and find out which is present here.
|
|
15
|
+
'not imported'
|
|
16
|
+
end
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Force encoding, some databases save "ASCII-8BIT" as binary data.
|
|
21
|
+
alias_method :orig_fingerprint, :fingerprint
|
|
22
|
+
def fingerprint
|
|
23
|
+
orig_fingerprint.encode(Encoding::US_ASCII)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
module GPGME
|
|
2
|
+
class Key
|
|
3
|
+
# Overwrite to specify the full fingerprint instead of the short key-ID.
|
|
4
|
+
def to_s
|
|
5
|
+
primary_subkey = subkeys[0]
|
|
6
|
+
s = sprintf("%s %4d%s/%s %s\n",
|
|
7
|
+
primary_subkey.secret? ? 'sec' : 'pub',
|
|
8
|
+
primary_subkey.length,
|
|
9
|
+
primary_subkey.pubkey_algo_letter,
|
|
10
|
+
primary_subkey.fingerprint,
|
|
11
|
+
primary_subkey.timestamp.strftime('%Y-%m-%d'))
|
|
12
|
+
uids.each do |user_id|
|
|
13
|
+
s << "uid\t\t#{user_id.name} <#{user_id.email}>\n"
|
|
14
|
+
end
|
|
15
|
+
subkeys.each do |subkey|
|
|
16
|
+
s << subkey.to_s
|
|
17
|
+
end
|
|
18
|
+
s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def generated_at
|
|
22
|
+
primary_subkey.timestamp
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def expired?
|
|
26
|
+
expired.present?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def oneline
|
|
30
|
+
@oneline ||=
|
|
31
|
+
begin
|
|
32
|
+
datefmt = '%Y-%m-%d'
|
|
33
|
+
attribs = [
|
|
34
|
+
"0x#{fingerprint}",
|
|
35
|
+
email,
|
|
36
|
+
generated_at.strftime(datefmt)
|
|
37
|
+
]
|
|
38
|
+
if usability_issue.present?
|
|
39
|
+
case usability_issue
|
|
40
|
+
when :expired
|
|
41
|
+
attribs << "[expired: #{expires.strftime(datefmt)}]"
|
|
42
|
+
when :revoked
|
|
43
|
+
# TODO: add revocation date when it's available.
|
|
44
|
+
attribs << "[revoked]"
|
|
45
|
+
else
|
|
46
|
+
attribs << "[#{usability_issue}]"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
if expires? && ! expired?
|
|
50
|
+
attribs << "[expires: #{expires.strftime(datefmt)}]"
|
|
51
|
+
end
|
|
52
|
+
attribs.join(' ')
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def armored
|
|
57
|
+
"#{self.to_s}\n\n#{export(armor: true).read}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Force encoding, some databases save "ASCII-8BIT" as binary data.
|
|
61
|
+
alias_method :orig_fingerprint, :fingerprint
|
|
62
|
+
def fingerprint
|
|
63
|
+
orig_fingerprint.encode(Encoding::US_ASCII)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def usable?
|
|
67
|
+
usability_issue.blank?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def usability_issue
|
|
71
|
+
if trust.present?
|
|
72
|
+
trust
|
|
73
|
+
elsif ! usable_for?(:encrypt)
|
|
74
|
+
"not capable of encryption"
|
|
75
|
+
else
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def set_primary_uid(email)
|
|
81
|
+
# We rely on the order of UIDs here. Seems to work.
|
|
82
|
+
index = self.uids.map(&:email).index(email)
|
|
83
|
+
uid_number = index + 1
|
|
84
|
+
primary_set = false
|
|
85
|
+
args = "--edit-key '#{self.fingerprint}' #{uid_number}"
|
|
86
|
+
errors, _ = GPGME::Ctx.gpgcli_expect(args) do |line|
|
|
87
|
+
case line.chomp
|
|
88
|
+
when /keyedit.prompt/
|
|
89
|
+
if ! primary_set
|
|
90
|
+
primary_set = true
|
|
91
|
+
"primary"
|
|
92
|
+
else
|
|
93
|
+
"save"
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
errors.join
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def adduid(uid, email)
|
|
103
|
+
# This block can be deleted once we cease to support gnupg 2.0.
|
|
104
|
+
if ! GPGME::Ctx.sufficient_gpg_version?('2.1.4')
|
|
105
|
+
return adduid_expect(uid, email)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Specifying the key via fingerprint apparently doesn't work.
|
|
109
|
+
errors, _ = GPGME::Ctx.gpgcli("--quick-adduid #{uid} '#{uid} <#{email}>'")
|
|
110
|
+
errors.join
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# This method can be deleted once we cease to support gnupg 2.0.
|
|
114
|
+
def adduid_expect(uid, email)
|
|
115
|
+
args = "--allow-freeform-uid --edit-key '#{self.fingerprint}' adduid"
|
|
116
|
+
errors, _ = GPGME::Ctx.gpgcli_expect(args) do |line|
|
|
117
|
+
case line.chomp
|
|
118
|
+
when /keygen.name/
|
|
119
|
+
uid
|
|
120
|
+
when /keygen.email/
|
|
121
|
+
email
|
|
122
|
+
when /keygen.comment/
|
|
123
|
+
''
|
|
124
|
+
when /keyedit.prompt/
|
|
125
|
+
"save"
|
|
126
|
+
else
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
errors.join
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def clearpassphrase(oldpw)
|
|
134
|
+
# This block can be deleted once we cease to support gnupg 2.0.
|
|
135
|
+
if ! GPGME::Ctx.sufficient_gpg_version?('2.1.0')
|
|
136
|
+
return clearpassphrase_v20(oldpw)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
oldpw_given = false
|
|
140
|
+
# Don't use '--passwd', it claims to fail (even though it factually doesn't).
|
|
141
|
+
args = "--pinentry-mode loopback --edit-key '#{self.fingerprint}' passwd"
|
|
142
|
+
errors, _, exitcode = GPGME::Ctx.gpgcli_expect(args) do |line|
|
|
143
|
+
case line
|
|
144
|
+
when /passphrase.enter/
|
|
145
|
+
if ! oldpw_given
|
|
146
|
+
oldpw_given = true
|
|
147
|
+
oldpw
|
|
148
|
+
else
|
|
149
|
+
""
|
|
150
|
+
end
|
|
151
|
+
when /BAD_PASSPHRASE/
|
|
152
|
+
[false, 'bad passphrase']
|
|
153
|
+
when /change_passwd.empty.okay/
|
|
154
|
+
'y'
|
|
155
|
+
when /keyedit.prompt/
|
|
156
|
+
"save"
|
|
157
|
+
else
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Only show errors if something apparently went wrong. Otherwise we might
|
|
163
|
+
# leak useless strings from gpg and make the caller report errors even
|
|
164
|
+
# though this method succeeded.
|
|
165
|
+
if exitcode > 0
|
|
166
|
+
errors.join
|
|
167
|
+
else
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# This method can be deleted once we cease to support gnupg 2.0.
|
|
173
|
+
def clearpassphrase_v20(oldpw)
|
|
174
|
+
start_gpg_agent(oldpw)
|
|
175
|
+
# Don't use '--passwd', it claims to fail (even though it factually doesn't).
|
|
176
|
+
errors, _, exitcode = GPGME::Ctx.gpgcli_expect("--edit-key '#{self.fingerprint}' passwd") do |line|
|
|
177
|
+
case line
|
|
178
|
+
when /BAD_PASSPHRASE/
|
|
179
|
+
[false, 'bad passphrase']
|
|
180
|
+
when /change_passwd.empty.okay/
|
|
181
|
+
'y'
|
|
182
|
+
when /keyedit.prompt/
|
|
183
|
+
"save"
|
|
184
|
+
else
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
stop_gpg_agent
|
|
189
|
+
|
|
190
|
+
# Only show errors if something apparently went wrong. Otherwise we might
|
|
191
|
+
# leak useless strings from gpg and make the caller report errors even
|
|
192
|
+
# though this method succeeded.
|
|
193
|
+
if exitcode > 0
|
|
194
|
+
errors.join
|
|
195
|
+
else
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# This method can be deleted once we cease to support gnupg 2.0.
|
|
201
|
+
def stop_gpg_agent
|
|
202
|
+
# gpg-agent terminates itself if its socket goes away.
|
|
203
|
+
GPGME::Ctx.delete_daemon_socket('gpg-agent')
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def start_gpg_agent(oldpw)
|
|
207
|
+
ENV['PINENTRY_USER_DATA'] = oldpw
|
|
208
|
+
pinentry = File.join(ENV['SCHLEUDER_ROOT'], 'bin', 'pinentry-clearpassphrase')
|
|
209
|
+
GPGME::Ctx.spawn_daemon('gpg-agent', "--use-standard-socket --pinentry-program #{pinentry}")
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|