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.
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