saltpack 0.1.0.pre.20200506181314

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'e2mmap'
5
+
6
+ require 'saltpack' unless defined?( Saltpack )
7
+
8
+
9
+ module Saltpack
10
+ extend Exception2MessageMapper
11
+
12
+ def_exception :Error, "saltpack error"
13
+
14
+ def_exception :KeyError, "missing/malformed key error"
15
+
16
+ def_exception :MalformedMessage, "malformed saltpack message", Saltpack::Error
17
+ def_exception :HMACError, "HMAC mismatch", Saltpack::Error
18
+
19
+ def_exception :UnsupportedFormat, "unrecognized format name", Saltpack::Error
20
+ def_exception :UnsupportedVersion, "incompatible version", Saltpack::Error
21
+
22
+ end # module Saltpack
23
+
@@ -0,0 +1,299 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'msgpack'
5
+ require 'rbnacl'
6
+ require 'loggability'
7
+
8
+ require 'saltpack' unless defined?( Saltpack )
9
+ require 'saltpack/refinements'
10
+
11
+
12
+ using Saltpack::Refinements
13
+
14
+
15
+ # Header for a saltpack message.
16
+ #
17
+ # Refs:
18
+ # - https://saltpack.org/encryption-format-v2
19
+ class Saltpack::Header
20
+ extend Loggability
21
+
22
+
23
+ # The header format name field value
24
+ FORMAT_NAME = 'saltpack'
25
+
26
+ # The header format major version field value
27
+ FORMAT_MAJOR_VERSION = 2
28
+
29
+ # The header format minor version field value
30
+ FORMAT_MINOR_VERSION = 0
31
+
32
+ # Version header Array
33
+ FORMAT_VERSION = [ FORMAT_MAJOR_VERSION, FORMAT_MINOR_VERSION ].freeze
34
+
35
+ # Mode names to numeric values
36
+ MODES = {
37
+ encryption: 0,
38
+ attached_signing: 1,
39
+ detached_signing: 2,
40
+ signcryption: 4
41
+ }.freeze
42
+ MODE_NAMES = MODES.invert.freeze
43
+
44
+ # The nonce used to create the sender key secret box
45
+ SENDER_KEY_SECRETBOX_NONCE = "saltpack_sender_key_sbox".b
46
+
47
+ # The nonce prefix used to create the recipients list
48
+ PAYLOAD_KEY_BOX_NONCE_PREFIX = "saltpack_recipsb".b
49
+
50
+ # The nonce prefix used for the payload packets
51
+ PAYLOAD_NONCE_PREFIX = "saltpack_ploadsb".b
52
+
53
+
54
+ # Log to the Saltpack logger
55
+ log_to :saltpack
56
+
57
+
58
+ # [
59
+ # format name,
60
+ # version,
61
+ # mode,
62
+ # ephemeral public key,
63
+ # sender secretbox,
64
+ # recipients list,
65
+ # ]
66
+
67
+ ### Parse the (already once-decoded) data in the specified +source+ as a
68
+ ### Saltpack header and return it as a Saltpack::Header. Raises a
69
+ ### Saltpack::Error if the +source+ cannot be parsed.
70
+ def self::parse( source, recipient_key )
71
+ source = StringIO.new( source ) unless
72
+ source.respond_to?( :read ) || source.respond_to?( :readpartial )
73
+ unpacker = MessagePack::Unpacker.new( source )
74
+ self.log.debug "Unpacker is: %p" % [ unpacker ]
75
+
76
+ encoded_header = unpacker.read
77
+ header_hash = RbNaCl::Hash.sha512( encoded_header )
78
+ parts = MessagePack.unpack( encoded_header )
79
+
80
+ raise Saltpack::MalformedMessage, "header is not an Array" unless parts.is_a?( Array )
81
+ raise Saltpack::UnsupportedFormat, parts[0] unless
82
+ parts[0] == FORMAT_NAME
83
+ raise Saltpack::UnsupportedVersion, parts[1] unless
84
+ parts[1] == FORMAT_VERSION
85
+
86
+ return new( *parts, header_hash: header_hash )
87
+ rescue MessagePack::MalformedFormatError => err
88
+ self.log.error "%p while parsing the header: %s" % [ err.class, err.message ]
89
+ raise Saltpack::MalformedMessage, "malformed msgpack data: %s" % [ err.message ]
90
+ end
91
+
92
+
93
+ ### Generate a header
94
+ def self::generate( sender_key, *recipient_public_keys, hide_recipients: false )
95
+
96
+
97
+ end
98
+
99
+
100
+ ### Create a new header with the given
101
+ def initialize( **fields )
102
+ @format_name = FORMAT_NAME
103
+ @format_version = FORMAT_VERSION
104
+
105
+ @mode = :encryption
106
+
107
+ @payload_key = RbNaCl::Random.random_bytes( RbNaCl::SecretBox.key_bytes )
108
+ @ephemeral_key = RbNaCl::PrivateKey.generate
109
+ @sender_key = nil
110
+
111
+ @recipients = []
112
+ @hide_recipients = true
113
+
114
+ @recipient_mac_keys = nil
115
+ @hash = nil
116
+ @data = nil
117
+
118
+ fields.each do |name, value|
119
+ self.public_send( "#{name}=", value )
120
+ end
121
+ end
122
+
123
+
124
+ ######
125
+ public
126
+ ######
127
+
128
+ ##
129
+ # The format name used in the header
130
+ attr_reader :format_name
131
+
132
+ ##
133
+ # The [major, minor] version tuple used in the header
134
+ attr_reader :format_version
135
+
136
+ ##
137
+ # The mode being used; one of the keys of MODES
138
+ attr_reader :mode
139
+
140
+ ##
141
+ # The random payload key
142
+ attr_accessor :payload_key
143
+
144
+ ##
145
+ # The RbNaCl::PrivateKey used only for this message to encrypt/sign its
146
+ # internals
147
+ attr_accessor :ephemeral_key
148
+
149
+ ##
150
+ # The RbNaCl::PrivateKey/PublicKey of the sender
151
+ attr_accessor :sender_key
152
+
153
+ ##
154
+ # The public keys of each of the message's recipients.
155
+ attr_reader :recipients
156
+
157
+ ##
158
+ # Whether to include the recipients' public key in the recipients tuples of the
159
+ # message.
160
+ attr_accessor :hide_recipients
161
+
162
+
163
+ ### Set the mode as either a Symbol or as an Integer.
164
+ def mode=( new_mode )
165
+ if MODES.key?( new_mode )
166
+ @mode = new_mode
167
+ elsif MODE_NAMES.key?( new_mode )
168
+ @mode = MODE_NAMES[ new_mode ]
169
+ else
170
+ raise ArgumentError, "invalid mode %p" % [ new_mode ]
171
+ end
172
+ end
173
+
174
+
175
+ ### Return the mode as an Integer.
176
+ def numeric_mode
177
+ return MODES[ self.mode ]
178
+ end
179
+
180
+
181
+ ### Return the #sender_key after checking to be sure the PrivateKey is
182
+ ### available.
183
+ def sender_private_key
184
+ key = self.sender_key or raise Saltpack::KeyError, "sender key is not set"
185
+ raise Saltpack::KeyError, "sender private key not available" unless
186
+ key && key.respond_to?( :public_key )
187
+ return key
188
+ end
189
+
190
+
191
+ ### Return either the #sender_key, or the public half of the #send_key if it's a
192
+ ### PrivateKey.
193
+ def sender_public_key
194
+ key = self.sender_key or raise Saltpack::KeyError, "sender key is not set"
195
+ return key.public_key if key.respond_to?( :public_key )
196
+ return key
197
+ end
198
+
199
+
200
+ ### Calculate all the header values and freeze it.
201
+ def finalize
202
+ return if self.frozen?
203
+
204
+ # 5. Collect the format name, version, and mode into a list, followed by the
205
+ # ephemeral public key, the sender secretbox, and the nested recipients list.
206
+ header_parts = [
207
+ self.format_name,
208
+ self.format_version,
209
+ self.numeric_mode,
210
+ self.ephemeral_key.public_key.to_bytes,
211
+ self.sender_secretbox,
212
+ self.recipient_tuples( hide_recipients: self.hide_recipients ),
213
+ ]
214
+
215
+ # 6. Serialize the list from #5 into a MessagePack array object.
216
+ header_bytes = MessagePack.pack( header_parts )
217
+
218
+ # 7. Take the crypto_hash (SHA512) of the bytes from #6. This is the header hash.
219
+ @hash = RbNaCl::Hash.sha512( header_bytes )
220
+
221
+ # 8. Serialize the bytes from #6 again into a MessagePack bin object. These
222
+ # twice-encoded bytes are the header packet.
223
+ @data = MessagePack.pack( header_bytes )
224
+
225
+ # After generating the header, the sender computes each recipient's MAC key,
226
+ # which will be used below to authenticate the payload:
227
+ @recipient_mac_keys = self.recipients.map.with_index do |recipient_key, i|
228
+ Saltpack.calculate_recipient_hash(
229
+ @hash, i,
230
+ [recipient_key, self.sender_private_key],
231
+ [recipient_key, self.ephemeral_key]
232
+ )
233
+ end
234
+
235
+ self.freeze
236
+ end
237
+
238
+
239
+ ### Overloaded -- also freeze the recipients when the header is frozen.
240
+ def freeze
241
+ @recipients.freeze
242
+ super
243
+ end
244
+
245
+
246
+ ### Return the SHA612 hash of the single-messagepacked header.
247
+ def hash
248
+ self.finalize
249
+ return @hash
250
+ end
251
+
252
+
253
+ ### Return the header as a binary String.
254
+ def data
255
+ self.finalize
256
+ return @data
257
+ end
258
+ alias_method :to_s, :data
259
+
260
+
261
+ ### The MAC keys used to hash/validate message parts.
262
+ def recipient_mac_keys
263
+ self.finalize
264
+ return @recipient_mac_keys
265
+ end
266
+
267
+
268
+ ### Generate an Array of header tuples from the #recipients keys and return it.
269
+ ### If +hide_recipients+ is true, don't include the public keys in the tuples.
270
+ def recipient_tuples( hide_recipients: true )
271
+ # 4. For each recipient, encrypt the payload key using crypto_box with the
272
+ # recipient's public key, the ephemeral private key, and the nonce
273
+ # saltpack_recipsbXXXXXXXX. XXXXXXXX is 8-byte big-endian unsigned recipient
274
+ # index, where the first recipient is index zero. Pair these with the
275
+ # recipients' public keys, or null for anonymous recipients, and collect the
276
+ # pairs into the recipients list.
277
+ return self.recipients.map.with_index do |recipient_key, i|
278
+ box = RbNaCl::Box.new( recipient_key, self.ephemeral_key )
279
+ nonce = PAYLOAD_KEY_BOX_NONCE_PREFIX + [ i ].pack( 'Q>' )
280
+ encrypted_key = box.encrypt( nonce, self.payload_key )
281
+
282
+ [ hide_recipients ? nil : recipient_key, encrypted_key ]
283
+ end
284
+ end
285
+
286
+
287
+ ### Return the sender secretbox
288
+ def sender_secretbox
289
+ # 1. Generate a random 32-byte payload key
290
+ # 2. Generate a random ephemeral keypair
291
+ # 3. Encrypt the sender's long-term public key using crypto_secretbox with the
292
+ # payload key and the nonce saltpack_sender_key_sbox, to create the sender
293
+ # secretbox.
294
+ box = RbNaCl::SecretBox.new( self.payload_key )
295
+ return box.encrypt( SENDER_KEY_SECRETBOX_NONCE, self.sender_public_key )
296
+ end
297
+
298
+ end # class Saltpack::Header
299
+
@@ -0,0 +1,101 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'loggability'
5
+ require 'msgpack'
6
+ require 'rbnacl'
7
+
8
+ require 'saltpack' unless defined?( Saltpack )
9
+ require 'saltpack/header'
10
+ require 'saltpack/payload'
11
+
12
+
13
+ # An encrypted message.
14
+ class Saltpack::Message
15
+ extend Loggability
16
+
17
+
18
+ # Loggability -- log to the Saltpack module's logger
19
+ log_to :saltpack
20
+
21
+
22
+ ### Read a Saltpack::Message from the given +source+ and +recipient_key+ and
23
+ ### return it.
24
+ def self::read( source, recipient_key )
25
+ header = Saltpack::Header.parse( source, recipient_key )
26
+ self.log.debug( header )
27
+
28
+ # Try to open each of the payload key boxes in the recipients list using
29
+ # crypto_box_open_afternm, the precomputed secret from #5, and the nonce
30
+ # saltpack_recipsbXXXXXXXX. XXXXXXXX is 8-byte big-endian unsigned recipient
31
+ # index, where the first recipient is index 0. Successfully opening one gives
32
+ # the payload key.
33
+ if ( recipient = recipients.assoc(recipient_key.public_key) )
34
+ index = recipients.index( recipient ) or
35
+ raise "Recipient %p not present in the recipients list?!"
36
+ nonce = PAYLOAD_KEY_BOX_NONCE_PREFIX + [index].pack( 'Q>' )
37
+ box = RbNaCl::Box.new( ephemeral_pubkey, recipient_key )
38
+ payload_key = box.decrypt( nonce, recipient[1] )
39
+ else
40
+ ephemeral_beforenm = RbNaCl::Box.beforenm( ephemeral_pubkey, recipient_key ) or
41
+ raise "Failed to extract the ephemeral shared key."
42
+ recipients.each_with_index do |(_, encrypted_key), index|
43
+ nonce = self.payload_key_nonce( header.version, index )
44
+ payload_key = RbNaCl::Box.open_afternm( encrypted_key,
45
+ ephemeral_beforenm, nonce, RbNaCl::SecretBox.key_bytes )
46
+ break if payload_key
47
+ end
48
+ end
49
+
50
+ raise "Failed to extract the payload key" unless payload_key
51
+ sender_public = RbNaCl::SecretBox.new( payload_key ).
52
+ decrypt( SENDER_KEY_SECRETBOX_NONCE, sender_secretbox )
53
+
54
+ recipient_mac = Saltpack.calculate_recipient_hash( header_hash, index,
55
+ [sender_public, recipient_key],
56
+ [ephemeral_pubkey, recipient_key]
57
+ )
58
+
59
+ self.log.debug "recipient index: %p" % [ recipient_index ]
60
+ self.log.debug "sender key: %p" % [ sender_public ]
61
+ self.log.debug "payload key: %p" % [ payload_key ]
62
+ self.log.debug "mac key: %p" % [ mac_key ]
63
+ end
64
+
65
+
66
+ ### (Undocumented)
67
+ def self::payload_key_nonce( version, index )
68
+
69
+ return PAYLOAD_KEY_BOX_NONCE_PREFIX + [index].pack( 'Q>' )
70
+ end
71
+
72
+ ### Create a new Message for the given +recipients+ using the specified
73
+ ### +sender_key+.
74
+ def initialize( data, header=nil, from: nil, to: [], **options )
75
+ @data = readable_source( data )
76
+ @header = header
77
+ @sender_key = from
78
+ @recipients = to
79
+ @options = options
80
+ end
81
+
82
+
83
+ ### (Undocumented)
84
+ def decrypt
85
+
86
+ end
87
+
88
+
89
+ #######
90
+ private
91
+ #######
92
+
93
+ ### Convert the given +data+ source into an object that supports #read or #readpartial if it
94
+ ### isn't already one.
95
+ def readable_source( data )
96
+ return data if data.respond_to?( :read ) || data.respond_to?( :readpartial )
97
+ return StringIO.new( data ) if data.respond_to?( :to_str )
98
+ raise ArgumentError, "don't know how to read from a %p" % [ data.class ]
99
+ end
100
+
101
+ end # class Saltpack::Message
@@ -0,0 +1,24 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'loggability'
5
+ require 'rbnacl'
6
+
7
+ require 'saltpack' unless defined?( Saltpack )
8
+
9
+
10
+ # A payload packet of a Saltpack message.
11
+ class Saltpack::Payload
12
+
13
+ #################################################################
14
+ ### I N S T A N C E M E T H O D S
15
+ #################################################################
16
+
17
+ ### Create a new Saltpack::Payload
18
+ def initialize( payload, *authenticators, final: false )
19
+
20
+ end
21
+
22
+
23
+ end # class Saltpack::Payload
24
+