rnp 1.0.4 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rnp/misc.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # (c) 2018 Ribose Inc.
3
+ # (c) 2018-2020 Ribose Inc.
4
4
 
5
+ require "json"
5
6
  require 'ffi'
6
7
 
7
8
  require 'rnp/utils'
@@ -74,6 +75,13 @@ class Rnp
74
75
  # @param output [Output] the output to write the armored
75
76
  # data to. If nil, the result will be returned directly
76
77
  # as a String.
78
+ # @param type [String] the armor type. Valid values include:
79
+ # * message
80
+ # * public key
81
+ # * secret key
82
+ # * signature
83
+ # * cleartext
84
+ # * nil (try to guess the type)
77
85
  # @return [nil, String]
78
86
  def self.enarmor(input:, output: nil, type: nil)
79
87
  Output.default(output) do |output_|
@@ -112,8 +120,12 @@ class Rnp
112
120
  # stamps generated with {version_for}.
113
121
  #
114
122
  # @return [Integer]
115
- def self.version
116
- LibRnp.rnp_version
123
+ def self.version(str = nil)
124
+ if str.nil?
125
+ LibRnp.rnp_version
126
+ else
127
+ LibRnp.rnp_version_for(*str.split('.').map(&:to_i))
128
+ end
117
129
  end
118
130
 
119
131
  # Encode the given major, minor, and patch numbers into a version
@@ -144,5 +156,142 @@ class Rnp
144
156
  def self.version_patch(version)
145
157
  LibRnp.rnp_version_patch(version)
146
158
  end
147
- end # class
148
159
 
160
+ # Retrieve the commit time of the latest commit.
161
+ #
162
+ # This will return 0 for release/non-master builds.
163
+ #
164
+ # @return [Integer]
165
+ def self.commit_time
166
+ LibRnp.rnp_version_commit_timestamp
167
+ end
168
+
169
+ # Parse OpenPGP data to JSON.
170
+ #
171
+ # @param input [Input] the input to read the data
172
+ # @param mpi [Boolean] if true then MPIs will be included
173
+ # @param raw [Boolean] if true then raw bytes will be included
174
+ # @param grip [Boolean] if true then grips will be included
175
+ # @return [Array]
176
+ def self.parse(input:, mpi: false, raw: false, grip: false)
177
+ flags = 0
178
+ flags |= LibRnp::RNP_JSON_DUMP_MPI if mpi
179
+ flags |= LibRnp::RNP_JSON_DUMP_RAW if raw
180
+ flags |= LibRnp::RNP_JSON_DUMP_GRIP if grip
181
+ pptr = FFI::MemoryPointer.new(:pointer)
182
+ Rnp.call_ffi(:rnp_dump_packets_to_json, input.ptr, flags, pptr)
183
+ begin
184
+ pjson = pptr.read_pointer
185
+ JSON.parse(pjson.read_string) unless pjson.null?
186
+ ensure
187
+ LibRnp.rnp_buffer_destroy(pjson)
188
+ end
189
+ end
190
+
191
+ # Calculate s2k iterations
192
+ #
193
+ # @param hash [String] the hash algorithm to use
194
+ # @param msec [Integer] the desired number of milliseconds
195
+ # @return [Integer]
196
+ def self.s2k_iterations(hash:, msec:)
197
+ piters = FFI::MemoryPointer.new(:size_t)
198
+ Rnp.call_ffi(:rnp_calculate_iterations, hash, msec, piters)
199
+ piters.read(:size_t)
200
+ end
201
+
202
+ # Enable debugging
203
+ #
204
+ # @param file [String] the file to enable debugging for (or nil for all)
205
+ # @return [void]
206
+ def self.enable_debug(file = nil)
207
+ Rnp.call_ffi(:rnp_enable_debug, file)
208
+ end
209
+
210
+ # Disable previously-enabled debugging
211
+ #
212
+ # @return [void]
213
+ def self.disable_debug
214
+ Rnp.call_ffi(:rnp_disable_debug)
215
+ end
216
+
217
+ # Guess the contents of an input
218
+ #
219
+ # @param input [Input]
220
+ # @return [String] the guessed content type (or 'unknown'), which may be
221
+ # used with {enarmor}
222
+ def self.guess_contents(input)
223
+ pptr = FFI::MemoryPointer.new(:pointer)
224
+ Rnp.call_ffi(:rnp_guess_contents, input.ptr, pptr)
225
+ begin
226
+ presult = pptr.read_pointer
227
+ presult.read_string unless presult.null?
228
+ ensure
229
+ LibRnp.rnp_buffer_destroy(presult)
230
+ end
231
+ end
232
+
233
+ # Check if a specific feature is supported
234
+ #
235
+ # @param type [String] the type of feature ('symmetric algorithm', ...)
236
+ # @param name [String] the specific feature ('CAST5', ...)
237
+ # @return [Boolean]
238
+ def self.supports?(type, name)
239
+ presult = FFI::MemoryPointer.new(:bool)
240
+ Rnp.call_ffi(:rnp_supports_feature, type, name, presult)
241
+ presult.read(:bool)
242
+ end
243
+
244
+ # Get a list of supported features (by type)
245
+ #
246
+ # @param type [String] the type of feature ('symmetric algorithm', ...)
247
+ # @return [Array]
248
+ def self.supported_features(type)
249
+ pptr = FFI::MemoryPointer.new(:pointer)
250
+ Rnp.call_ffi(:rnp_supported_features, type, pptr)
251
+ begin
252
+ presult = pptr.read_pointer
253
+ JSON.parse(presult.read_string) unless presult.null?
254
+ ensure
255
+ LibRnp.rnp_buffer_destroy(presult)
256
+ end
257
+ end
258
+
259
+ # @api private
260
+ FEATURES = {
261
+ # Support for setting hash, creation, and expiration time for individual
262
+ # signatures in a sign operation. Older versions of rnp returned a
263
+ # "not implemented" error.
264
+ "per-signature-opts" => Rnp.version > Rnp.version("0.11.0") ||
265
+ Rnp.commit_time >= 1546035818,
266
+ # Correct grip calculation for Elgamal/DSA keys. This was actually before
267
+ # the commit timestamp API was added, so this isn't accurate in one case.
268
+ "dsa-elg-grip-calc" => Rnp.version > Rnp.version("0.11.0") ||
269
+ Rnp.commit_time >= 1538219020,
270
+ # Input reader callback signature was changed:
271
+ # ssize_t(void *app_ctx, void *buf, size_t len)
272
+ # bool(void *app_ctx, void *buf, size_t len, size_t *read)
273
+ "input-reader-cb-no-ssize_t" => Rnp.version >= Rnp.version("0.14.0") ||
274
+ Rnp.commit_time >= 1585833163,
275
+ # Behavior on primary userid retrieveing was changed:
276
+ # Now userid is not considered as primary if it is revoked/expired/etc.
277
+ "primary-userid-must-be-valid" => Rnp.version >= Rnp.version("0.14.0") ||
278
+ Rnp.commit_time >= 1605875599,
279
+ # Behavior was changed in v0.15.2 (issue #1509):
280
+ # userid is valid even if key expiration in userid expiration expires key.
281
+ "relax-userid-validity-checks" => Rnp.version >= Rnp.version("0.15.2") ||
282
+ Rnp.commit_time >= 1624526708,
283
+ # Behavior on default key expiration time was changed:
284
+ # Now default key expiration time is 2 years
285
+ "default-key-expiration-2-years" => Rnp.version >= Rnp.version("0.16.1") ||
286
+ Rnp.commit_time >= 1645578982,
287
+ # Behaviour on signature validation was changed:
288
+ # Now at least one valid signature is required for success
289
+ "require-single-valid-signature" => Rnp.version >= Rnp.version("0.16.1") ||
290
+ Rnp.commit_time >= 1661781294,
291
+ }.freeze
292
+
293
+ def self.has?(feature)
294
+ raise ArgumentError unless FEATURES.include?(feature)
295
+ FEATURES[feature]
296
+ end
297
+ end # class
@@ -43,19 +43,18 @@ class Rnp
43
43
  # Add a signer.
44
44
  #
45
45
  # @param signer [Key] the signer
46
- # @param hash (see #hash=)
47
- # @param creation_time (see #creation_time=)
48
- # @param expiration_time (see #expiration_time=)
46
+ # @param [Hash] opts set several options in one place
47
+ # @option opts [String] :hash (see #hash=)
48
+ # @option opts [Time] :creation_time (see #creation_time=)
49
+ # @option opts [Time] :expiration_time (see #expiration_time=)
49
50
  # @return [self]
50
- def add_signer(signer, hash: nil, creation_time: nil, expiration_time: nil)
51
+ def add_signer(signer, opts = {})
51
52
  pptr = FFI::MemoryPointer.new(:pointer)
52
53
  Rnp.call_ffi(:rnp_op_encrypt_add_signature, @ptr, signer.ptr, pptr)
53
54
  psig = pptr.read_pointer
54
55
  Sign.set_signature_options(
55
56
  psig,
56
- hash: hash,
57
- creation_time: creation_time,
58
- expiration_time: expiration_time
57
+ **opts,
59
58
  )
60
59
  self
61
60
  end
@@ -88,20 +87,19 @@ class Rnp
88
87
  # @note Some options are related to signatures and will have no effect if
89
88
  # there are no signers.
90
89
  #
91
- # @param armored (see #armored=)
92
- # @param compression (see #compression=)
93
- # @param cipher (see #cipher=)
94
- # @param hash (see #hash=)
95
- # @param creation_time (see #creation_time=)
96
- # @param expiration_time (see #expiration_time=)
97
- def options=(armored: nil, compression: nil, cipher: nil, hash: nil,
98
- creation_time: nil, expiration_time: nil)
99
- self.armored = armored unless armored.nil?
100
- self.compression = compression unless compression.nil?
101
- self.cipher = cipher unless cipher.nil?
102
- self.hash = hash unless hash.nil?
103
- self.creation_time = creation_time unless creation_time.nil?
104
- self.expiration_time = expiration_time unless expiration_time.nil?
90
+ # @param [Hash] opts set several options in one place
91
+ # @option opts [Boolean] :armored (see #armored=)
92
+ # @option opts [String] :compression (see #compression=)
93
+ # @option opts [String] :cipher (see #cipher=)
94
+ # @option opts [String] :hash (see #hash=)
95
+ # @option opts [Time] :creation_time (see #creation_time=)
96
+ # @option opts [Time] :expiration_time (see #expiration_time=)
97
+ def options=(opts)
98
+ %i{armored compression cipher aead hash creation_time
99
+ expiration_time}.each do |prop|
100
+ value = opts[prop]
101
+ send("#{prop}=", value) unless value.nil?
102
+ end
105
103
  end
106
104
 
107
105
  # Set whether the output will be ASCII-armored.
@@ -135,6 +133,13 @@ class Rnp
135
133
  Rnp.call_ffi(:rnp_op_encrypt_set_cipher, @ptr, cipher)
136
134
  end
137
135
 
136
+ # Set the AEAD algorithm for encryption.
137
+ #
138
+ # @param mode [String] the AEAD algorithm to use for encryption
139
+ def aead=(mode)
140
+ Rnp.call_ffi(:rnp_op_encrypt_set_aead, @ptr, mode.to_s)
141
+ end
142
+
138
143
  # Set the hash algorithm used for calculating signatures.
139
144
  #
140
145
  # @note This is only valid when there is one or more signer.
@@ -178,4 +183,3 @@ class Rnp
178
183
  end
179
184
  end # class
180
185
  end # class
181
-
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ # (c) 2019 Ribose Inc.
4
+
5
+ require "ffi"
6
+
7
+ require "rnp/error"
8
+ require "rnp/ffi/librnp"
9
+ require "rnp/utils"
10
+
11
+ class Rnp
12
+ # Key generation operation
13
+ class Generate
14
+ # @api private
15
+ attr_reader :ptr
16
+
17
+ # @api private
18
+ def initialize(ptr)
19
+ raise Rnp::Error, "NULL pointer" if ptr.null?
20
+
21
+ @ptr = FFI::AutoPointer.new(ptr, self.class.method(:destroy))
22
+ end
23
+
24
+ # @api private
25
+ def self.destroy(ptr)
26
+ LibRnp.rnp_op_generate_destroy(ptr)
27
+ end
28
+
29
+ def inspect
30
+ Rnp.inspect_ptr(self)
31
+ end
32
+
33
+ # Set a group of options.
34
+ #
35
+ # @param [Hash] opts set several options in one place
36
+ # @option opts [Integer] :bits (see #bits=)
37
+ # @option opts [Integer] :qbits (see #qbits=)
38
+ # @option opts [Integer] :curve (see #curve=)
39
+ # @option opts [String] :hash (see #hash=)
40
+ # @option opts [String] :s2k_hash (see #s2k_hash=)
41
+ # @option opts [Integer] :s2k_iterations (see #s2k_iterations=)
42
+ # @option opts [String] :s2k_cipher (see #s2k_cipher=)
43
+ # @option opts [String] :password (see #password=)
44
+ # @option opts [String] :protection_mode (see #protection_mode=)
45
+ # @option opts [Integer] :lifetime (see #lifetime=)
46
+ # @option opts [String] :userid (see #userid=)
47
+ # @option opts [String] :usage (see #usage=)
48
+ def options=(opts)
49
+ %i{bits qbits curve hash s2k_hash s2k_iterations
50
+ s2k_cipher password protection_mode lifetime
51
+ userid usage preferences}.each do |prop|
52
+ value = opts[prop]
53
+ send("#{prop}=", value) unless value.nil?
54
+ end
55
+ end
56
+
57
+ # Set the bit length of the key.
58
+ #
59
+ # @param len [Integer] the desired bit length
60
+ def bits=(len)
61
+ Rnp.call_ffi(:rnp_op_generate_set_bits, @ptr, len)
62
+ end
63
+
64
+ # Set the bit length of the q parameter for a DSA key.
65
+ #
66
+ # @note This is only valid for DSA keys.
67
+ #
68
+ # @param len [Integer] the desired bit length
69
+ def qbits=(len)
70
+ Rnp.call_ffi(:rnp_op_generate_set_dsa_qbits, @ptr, len)
71
+ end
72
+
73
+ # Set the desired curve for this ECC key.
74
+ #
75
+ # @note This is only valid for ECC keys which permit specifying a curve.
76
+ #
77
+ # @param curve [String] the curve
78
+ def curve=(curve)
79
+ Rnp.call_ffi(:rnp_op_generate_set_curve, @ptr, curve.to_s)
80
+ end
81
+
82
+ # Set the hash algorithm used in the self-signature of the key.
83
+ #
84
+ # @param hash [String] the hash algorithm name
85
+ def hash=(hash)
86
+ Rnp.call_ffi(:rnp_op_generate_set_hash, @ptr, hash.to_s)
87
+ end
88
+
89
+ # Set the hash algorithm used to protect the key.
90
+ #
91
+ # @param hash [String] the hash algorithm name
92
+ def s2k_hash=(hash)
93
+ Rnp.call_ffi(:rnp_op_generate_set_protection_hash, @ptr, hash.to_s)
94
+ end
95
+
96
+ # Set the s2k iteration count used to protect the key.
97
+ #
98
+ # @param iter [Integer] the hash algorithm name
99
+ def s2k_iterations=(iter)
100
+ Rnp.call_ffi(:rnp_op_generate_set_protection_iterations, @ptr, iter)
101
+ end
102
+
103
+ # Set the cipher used to protect the key.
104
+ #
105
+ # @param cipher [String] the cipher algorithm name
106
+ def s2k_cipher=(cipher)
107
+ Rnp.call_ffi(:rnp_op_generate_set_protection_cipher, @ptr, cipher.to_s)
108
+ end
109
+
110
+ # Set the password used to protect the key.
111
+ #
112
+ # @param password [String] the password
113
+ def password=(password)
114
+ Rnp.call_ffi(:rnp_op_generate_set_protection_password, @ptr, password)
115
+ end
116
+
117
+ # Set the protection mode for this key.
118
+ #
119
+ # @note This is only valid for keys saved in the G10 format.
120
+ #
121
+ # @param mode [String] the protection mode (OCB, CBC, etc)
122
+ def protection_mode=(mode)
123
+ Rnp.call_ffi(:rnp_op_generate_set_protection_mode, @ptr, mode.to_s)
124
+ end
125
+
126
+ # Set the number of seconds for this key to remain valid.
127
+ #
128
+ # This determines the expiration time (creation time + lifetime).
129
+ #
130
+ # @param secs [Integer] the number of seconds until this key will be
131
+ # considered expired. A value of 0 indicates no expiration.
132
+ # Note that there is an upper limit of 2^32-1.
133
+ def lifetime=(secs)
134
+ Rnp.call_ffi(:rnp_op_generate_set_expiration, @ptr, secs)
135
+ end
136
+
137
+ # Set the userid for this key.
138
+ #
139
+ # @param userid [String] the userid
140
+ def userid=(userid)
141
+ Rnp.call_ffi(:rnp_op_generate_set_userid, @ptr, userid)
142
+ end
143
+
144
+ # Set the usage for this key.
145
+ #
146
+ # @param usage [Array<Symbol>,Array<String>,Symbol,String] the usage
147
+ # (:sign, etc)
148
+ def usage=(usage)
149
+ usage = [usage] unless usage.respond_to?(:each)
150
+ Rnp.call_ffi(:rnp_op_generate_clear_usage, @ptr)
151
+ usage.each do |usg|
152
+ Rnp.call_ffi(:rnp_op_generate_add_usage, @ptr, usg.to_s)
153
+ end
154
+ end
155
+
156
+ # Set the preferences for the generated key.
157
+
158
+ # @param [Hash] prefs set several preferences in one place
159
+ # @option prefs [Array<String>, Array<Symbol>] :hashes
160
+ # @option prefs [Array<String>, Array<Symbol>] :compression
161
+ # @option prefs [Array<String>, Array<Symbol>] :ciphers
162
+ # @option prefs [String] :key_server
163
+ def preferences=(prefs)
164
+ %i{hashes compression ciphers}.each do |param|
165
+ Rnp.call_ffi(pref_ffi_call(param, clear: true), @ptr)
166
+ prefs[param].each do |pref|
167
+ Rnp.call_ffi(pref_ffi_call(param, add: true),
168
+ @ptr, pref.to_s)
169
+ end
170
+ end
171
+ Rnp.call_ffi(:rnp_op_generate_set_pref_keyserver, @ptr,
172
+ prefs[:key_server])
173
+ end
174
+
175
+ # Execute the operation.
176
+ #
177
+ # This should only be called once.
178
+ #
179
+ # @return [Key] the generated key
180
+ def execute
181
+ Rnp.call_ffi(:rnp_op_generate_execute, @ptr)
182
+ key
183
+ end
184
+
185
+ # Retrieve the key.
186
+ #
187
+ # This should only be called after #execute.
188
+ #
189
+ # @return [Key]
190
+ def key
191
+ pptr = FFI::MemoryPointer.new(:pointer)
192
+ Rnp.call_ffi(:rnp_op_generate_get_key, @ptr, pptr)
193
+ pkey = pptr.read_pointer
194
+ Key.new(pkey) unless pkey.null?
195
+ end
196
+
197
+ # @api private
198
+ private
199
+
200
+ def pref_ffi_call(param, add: false, clear: false)
201
+ if add
202
+ fn = { hashes: :hash, ciphers: :cipher }.fetch(param, param)
203
+ "rnp_op_generate_add_pref_#{fn}".to_sym
204
+ elsif clear
205
+ "rnp_op_generate_clear_pref_#{param}".to_sym
206
+ else
207
+ raise ArgumentError, "add or clear must be passed"
208
+ end
209
+ end
210
+ end
211
+ end
data/lib/rnp/op/sign.rb CHANGED
@@ -34,37 +34,35 @@ class Rnp
34
34
  # @note The optional (per-signature) options here are not supported by RNP
35
35
  # internally at the time of this writing.
36
36
  #
37
- # @param signer [Key] the signer
38
- # @param hash [String] (see #hash=)
39
- # @param creation_time (see #creation_time=)
40
- # @param expiration_time (see #expiration_time=)
37
+ # @param [Hash] opts set several options in one place
38
+ # @option opts [Key] :signer the signer
39
+ # @option opts [String] :hash (see #hash=)
40
+ # @option opts [Time] :creation_time (see #creation_time=)
41
+ # @option opts [Time] :expiration_time (see #expiration_time=)
41
42
  # @return [self]
42
- def add_signer(signer, hash: nil, creation_time: nil, expiration_time: nil)
43
+ def add_signer(signer, opts = {})
43
44
  pptr = FFI::MemoryPointer.new(:pointer)
44
45
  Rnp.call_ffi(:rnp_op_sign_add_signature, @ptr, signer.ptr, pptr)
45
46
  psig = pptr.read_pointer
46
47
  self.class.set_signature_options(
47
48
  psig,
48
- hash: hash,
49
- creation_time: creation_time,
50
- expiration_time: expiration_time
49
+ **opts,
51
50
  )
52
51
  end
53
52
 
54
53
  # Set a group of options.
55
54
  #
56
- # @param armored see {#armored=}
57
- # @param compression see {#compression=}
58
- # @param hash see {#hash=}
59
- # @param creation_time see {#creation_time=}
60
- # @param expiration_time see {#expiration_time=}
61
- def options=(armored: nil, compression: nil, hash: nil,
62
- creation_time: nil, expiration_time: nil)
63
- self.armored = armored unless armored.nil?
64
- self.compression = compression unless compression.nil?
65
- self.hash = hash unless hash.nil?
66
- self.creation_time = creation_time unless creation_time.nil?
67
- self.expiration_time = expiration_time unless expiration_time.nil?
55
+ # @param [Hash] opts set several options in one place
56
+ # @option opts [String] :armored see {#armored=}
57
+ # @option opts [String] :compression see {#compression=}
58
+ # @option opts [String] :hash see {#hash=}
59
+ # @option opts [String] :creation_time see {#creation_time=}
60
+ # @option opts [String] :expiration_time see {#expiration_time=}
61
+ def options=(opts)
62
+ %i{armored compression hash creation_time expiration_time}.each do |prop|
63
+ value = opts[prop]
64
+ send("#{prop}=", value) unless value.nil?
65
+ end
68
66
  end
69
67
 
70
68
  # Set whether the output will be ASCII-armored.
@@ -127,12 +125,14 @@ class Rnp
127
125
  end
128
126
 
129
127
  # @api private
130
- def self.set_signature_options(psig, hash:, creation_time:,
131
- expiration_time:)
132
- Rnp.call_ffi(:rnp_op_sign_signature_set_hash, psig, value) unless hash.nil?
128
+ def self.set_signature_options(psig, hash: nil, creation_time: nil,
129
+ expiration_time: nil)
130
+ Rnp.call_ffi(:rnp_op_sign_signature_set_hash, psig, hash) unless hash.nil?
133
131
  creation_time = creation_time.to_i if creation_time.is_a?(::Time)
134
- Rnp.call_ffi(:rnp_op_sign_signature_set_creation_time, psig, value) unless creation_time.nil?
135
- Rnp.call_ffi(:rnp_op_sign_signature_set_expiration_time, psig, value) unless expiration_time.nil?
132
+ Rnp.call_ffi(:rnp_op_sign_signature_set_creation_time, psig,
133
+ creation_time) unless creation_time.nil?
134
+ Rnp.call_ffi(:rnp_op_sign_signature_set_expiration_time, psig,
135
+ expiration_time) unless expiration_time.nil?
136
136
  end
137
137
  end # class
138
138
  end # class
data/lib/rnp/output.rb CHANGED
@@ -86,6 +86,20 @@ class Rnp
86
86
  to_callback(io.method(:write))
87
87
  end
88
88
 
89
+ # Write to the output.
90
+ #
91
+ # @param strings [String]
92
+ # @return [Integer] the number of bytes written
93
+ def write(*strings)
94
+ total_written = 0
95
+ pwritten = FFI::MemoryPointer.new(:size_t)
96
+ strings.each do |string|
97
+ Rnp.call_ffi(:rnp_output_write, @ptr, string, string.size, pwritten)
98
+ total_written += pwritten.read(:size_t)
99
+ end
100
+ total_written
101
+ end
102
+
89
103
  # Retrieve the data written. Only valid for #{to_string}.
90
104
  #
91
105
  # @return [String, nil]