schleuder 2.2.4 → 3.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +138 -0
  3. data/Rakefile +136 -0
  4. data/bin/pinentry-clearpassphrase +72 -0
  5. data/bin/schleuder +9 -89
  6. data/bin/schleuder-api-daemon +4 -0
  7. data/db/migrate/20140501103532_create_lists.rb +39 -0
  8. data/db/migrate/20140501112859_create_subscriptions.rb +21 -0
  9. data/db/migrate/201508092100_add_language_to_lists.rb +11 -0
  10. data/db/migrate/20150812165700_change_keywords_admin_only_defaults.rb +8 -0
  11. data/db/migrate/20150813235800_add_forward_all_incoming_to_admins.rb +11 -0
  12. data/db/migrate/201508141727_change_send_encrypted_only_default.rb +8 -0
  13. data/db/migrate/201508222143_add_logfiles_to_keep_to_lists.rb +11 -0
  14. data/db/migrate/201508261723_rename_delivery_disabled_to_delivery_enabled_and_change_default.rb +14 -0
  15. data/db/migrate/201508261815_strip_gpg_passphrase.rb +11 -0
  16. data/db/migrate/201508261827_remove_default_mime.rb +9 -0
  17. data/db/migrate/20160501172700_fix_headers_to_meta_defaults.rb +8 -0
  18. data/db/migrate/20170713215059_add_internal_footer_to_list.rb +11 -0
  19. data/db/schema.rb +62 -0
  20. data/etc/init.d/schleuder-api-daemon +87 -0
  21. data/etc/list-defaults.yml +123 -0
  22. data/etc/postfix/schleuder_sqlite.cf +28 -0
  23. data/etc/schleuder-api-daemon.service +10 -0
  24. data/etc/schleuder.cron.weekly +6 -0
  25. data/etc/schleuder.yml +61 -0
  26. data/lib/schleuder-api-daemon.rb +420 -0
  27. data/lib/schleuder.rb +81 -47
  28. data/lib/schleuder/cli.rb +334 -0
  29. data/lib/schleuder/cli/cert.rb +24 -0
  30. data/lib/schleuder/cli/schleuder_cert_manager.rb +84 -0
  31. data/lib/schleuder/cli/subcommand_fix.rb +11 -0
  32. data/lib/schleuder/conf.rb +131 -0
  33. data/lib/schleuder/errors/active_model_error.rb +15 -0
  34. data/lib/schleuder/errors/base.rb +17 -0
  35. data/lib/schleuder/errors/decryption_failed.rb +16 -0
  36. data/lib/schleuder/errors/fatal_error.rb +13 -0
  37. data/lib/schleuder/errors/file_not_found.rb +14 -0
  38. data/lib/schleuder/errors/invalid_listname.rb +13 -0
  39. data/lib/schleuder/errors/key_adduid_failed.rb +13 -0
  40. data/lib/schleuder/errors/key_generation_failed.rb +16 -0
  41. data/lib/schleuder/errors/keyword_admin_only.rb +13 -0
  42. data/lib/schleuder/errors/list_exists.rb +13 -0
  43. data/lib/schleuder/errors/list_not_found.rb +14 -0
  44. data/lib/schleuder/errors/list_property_missing.rb +14 -0
  45. data/lib/schleuder/errors/listdir_problem.rb +16 -0
  46. data/lib/schleuder/errors/loading_list_settings_failed.rb +14 -0
  47. data/lib/schleuder/errors/message_empty.rb +14 -0
  48. data/lib/schleuder/errors/message_not_from_admin.rb +13 -0
  49. data/lib/schleuder/errors/message_sender_not_subscribed.rb +13 -0
  50. data/lib/schleuder/errors/message_too_big.rb +14 -0
  51. data/lib/schleuder/errors/message_unauthenticated.rb +13 -0
  52. data/lib/schleuder/errors/message_unencrypted.rb +13 -0
  53. data/lib/schleuder/errors/message_unsigned.rb +13 -0
  54. data/lib/schleuder/errors/standard_error.rb +5 -0
  55. data/lib/schleuder/errors/too_many_keys.rb +17 -0
  56. data/lib/schleuder/errors/unknown_list_option.rb +14 -0
  57. data/lib/schleuder/filters/auth_filter.rb +39 -0
  58. data/lib/schleuder/filters/bounces_filter.rb +12 -0
  59. data/lib/schleuder/filters/forward_filter.rb +17 -0
  60. data/lib/schleuder/filters/forward_incoming.rb +13 -0
  61. data/lib/schleuder/filters/hotmail_message_filter.rb +25 -0
  62. data/lib/schleuder/filters/max_message_size.rb +14 -0
  63. data/lib/schleuder/filters/request_filter.rb +26 -0
  64. data/lib/schleuder/filters/send_key_filter.rb +20 -0
  65. data/lib/schleuder/filters/strip_alternative_filter.rb +21 -0
  66. data/lib/schleuder/filters_runner.rb +83 -0
  67. data/lib/schleuder/gpgme/ctx.rb +274 -0
  68. data/lib/schleuder/gpgme/import_status.rb +27 -0
  69. data/lib/schleuder/gpgme/key.rb +212 -0
  70. data/lib/schleuder/gpgme/sub_key.rb +13 -0
  71. data/lib/schleuder/gpgme/user_id.rb +22 -0
  72. data/lib/schleuder/list.rb +318 -127
  73. data/lib/schleuder/list_builder.rb +139 -0
  74. data/lib/schleuder/listlogger.rb +31 -0
  75. data/lib/schleuder/logger.rb +23 -0
  76. data/lib/schleuder/logger_notifications.rb +69 -0
  77. data/lib/schleuder/mail/message.rb +482 -0
  78. data/lib/schleuder/mail/parts_list.rb +9 -0
  79. data/lib/schleuder/plugin_runners/base.rb +91 -0
  80. data/lib/schleuder/plugin_runners/list_plugins_runner.rb +24 -0
  81. data/lib/schleuder/plugin_runners/request_plugins_runner.rb +27 -0
  82. data/lib/schleuder/plugins/attach_listkey.rb +17 -0
  83. data/lib/schleuder/plugins/get_version.rb +7 -0
  84. data/lib/schleuder/plugins/key_management.rb +113 -0
  85. data/lib/schleuder/plugins/list_management.rb +15 -0
  86. data/lib/schleuder/plugins/resend.rb +196 -0
  87. data/lib/schleuder/plugins/sign_this.rb +46 -0
  88. data/lib/schleuder/plugins/subscription_management.rb +140 -0
  89. data/lib/schleuder/runner.rb +130 -0
  90. data/lib/schleuder/subscription.rb +98 -0
  91. data/lib/schleuder/validators/boolean_validator.rb +7 -0
  92. data/lib/schleuder/validators/email_validator.rb +7 -0
  93. data/lib/schleuder/validators/fingerprint_validator.rb +7 -0
  94. data/lib/schleuder/validators/greater_than_zero_validator.rb +7 -0
  95. data/lib/schleuder/validators/no_line_breaks_validator.rb +7 -0
  96. data/lib/schleuder/version.rb +1 -1
  97. data/locales/de.yml +179 -0
  98. data/locales/en.yml +179 -0
  99. metadata +305 -108
  100. checksums.yaml.gz.sig +0 -3
  101. data.tar.gz.sig +0 -2
  102. data/LICENSE +0 -339
  103. data/README +0 -32
  104. data/bin/schleuder-fix-gem-dependencies +0 -37
  105. data/bin/schleuder-init-setup +0 -37
  106. data/bin/schleuder-migrate-v2.1-to-v2.2 +0 -225
  107. data/bin/schleuder-newlist +0 -413
  108. data/contrib/check-expired-keys.rb +0 -60
  109. data/contrib/mutt-schleuder-colors.rc +0 -10
  110. data/contrib/mutt-schleuder-resend.vim +0 -24
  111. data/contrib/smtpserver.rb +0 -76
  112. data/ext/default-list.conf +0 -149
  113. data/ext/default-members.conf +0 -7
  114. data/ext/list.conf.example +0 -14
  115. data/ext/schleuder.conf +0 -64
  116. data/lib/schleuder/archiver.rb +0 -46
  117. data/lib/schleuder/crypt.rb +0 -210
  118. data/lib/schleuder/errors.rb +0 -5
  119. data/lib/schleuder/list_config.rb +0 -146
  120. data/lib/schleuder/log/listlogger.rb +0 -57
  121. data/lib/schleuder/log/outputter/emailoutputter.rb +0 -120
  122. data/lib/schleuder/log/outputter/metaemailoutputter.rb +0 -50
  123. data/lib/schleuder/log/schleuderlogger.rb +0 -34
  124. data/lib/schleuder/mail.rb +0 -873
  125. data/lib/schleuder/mailer.rb +0 -26
  126. data/lib/schleuder/member.rb +0 -69
  127. data/lib/schleuder/plugin.rb +0 -54
  128. data/lib/schleuder/processor.rb +0 -363
  129. data/lib/schleuder/schleuder_config.rb +0 -75
  130. data/lib/schleuder/storage.rb +0 -84
  131. data/lib/schleuder/utils.rb +0 -80
  132. data/man/schleuder-newlist.8 +0 -174
  133. data/man/schleuder.8 +0 -416
  134. data/plugins/README +0 -20
  135. data/plugins/manage_keys_plugin.rb +0 -113
  136. data/plugins/manage_members_plugin.rb +0 -156
  137. data/plugins/manage_self_plugin.rb +0 -26
  138. data/plugins/resend_plugin.rb +0 -35
  139. data/plugins/sign_this_plugin.rb +0 -14
  140. data/plugins/version_plugin.rb +0 -12
  141. 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