saltpack 0.1.0.pre.20200506181314
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.rdoc_options +16 -0
- data/.simplecov +9 -0
- data/ChangeLog +55 -0
- data/History.md +4 -0
- data/LICENSE.txt +19 -0
- data/Manifest.txt +25 -0
- data/README.md +89 -0
- data/Rakefile +9 -0
- data/examples/post_signed_message.rb +21 -0
- data/lib/saltpack.rb +112 -0
- data/lib/saltpack/armor.rb +211 -0
- data/lib/saltpack/errors.rb +23 -0
- data/lib/saltpack/header.rb +299 -0
- data/lib/saltpack/message.rb +101 -0
- data/lib/saltpack/payload.rb +24 -0
- data/lib/saltpack/recipient.rb +33 -0
- data/lib/saltpack/refinements.rb +38 -0
- data/spec/data/msg1-ciphertext.txt +1 -0
- data/spec/data/msg1.txt +16 -0
- data/spec/saltpack/armor_spec.rb +57 -0
- data/spec/saltpack/header_spec.rb +87 -0
- data/spec/saltpack/recipient_spec.rb +25 -0
- data/spec/saltpack_spec.rb +86 -0
- data/spec/spec_helper.rb +43 -0
- metadata +193 -0
@@ -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
|
+
|