rnp 1.0.4 → 1.0.5

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/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]