simple-secrets 1.0.0

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.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in simple_secrets.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Tim Shadel
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,47 @@
1
+
2
+ # SimpleSecrets [![Build Status](https://travis-ci.org/timshadel/simple-secrets.rb.png?branch=master)](https://travis-ci.org/timshadel/simple-secrets.rb)
3
+
4
+ A Ruby client for [simple-secrets][simple-secrets], the simple, opinionated library for encrypting small packets of data securely.
5
+
6
+ [simple-secrets]: https://github.com/timshadel/simple-secrets
7
+
8
+ ## Examples
9
+
10
+ ### Basic
11
+
12
+ Send:
13
+
14
+ ```ruby
15
+ require('simple-secrets')
16
+
17
+ # Try `head /dev/urandom | shasum -a 256` to make a decent 256-bit key
18
+ master_key = new Buffer('<64-char hex string (32 bytes, 256 bits)>', 'hex');
19
+ # => <Buffer 71 c8 67 56 23 4b fd 3c 37 ... >
20
+
21
+ var sender = secrets(master_key);
22
+ var packet = sender.pack('this is a secret message');
23
+ # => 'OqlG6KVMeyFYmunboS3HIXkvN_nXKTxg2yNkQydZOhvJrZvmfov54hUmkkiZCnlhzyrlwOJkbV7XnPPbqvdzZ6TsFOO5YdmxjxRksZmeIhbhLaMiDbfsOuSY1dBn_ZgtYCw-FRIM'
24
+ ```
25
+
26
+ Receive:
27
+
28
+ ```js
29
+ var secrets = require('simple-secrets');
30
+
31
+ // Same shared key
32
+ var master_key = new Buffer('<shared-key-hex>', 'hex');
33
+ var sender = secrets(master_key);
34
+ var packet = new Buffer('<read data from somewhere>');
35
+ // => 'OqlG6KVMeyFYmunboS3HIXkvN_nXKTxg2yNkQydZOhvJrZvmfov54hUmkkiZCnlhzyrlwOJkbV7XnPPbqvdzZ6TsFOO5YdmxjxRksZmeIhbhLaMiDbfsOuSY1dBn_ZgtYCw-FRIM'
36
+ var secret_message = sender.unpack(packet);
37
+ // => 'this is a secret message'
38
+ ```
39
+
40
+
41
+ ## Can you add ...
42
+
43
+ This implementation follows [simple-secrets] for 100% compatibility.
44
+
45
+ ## License
46
+
47
+ MIT.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1 @@
1
+ require "simple_secrets"
@@ -0,0 +1,7 @@
1
+ require "simple_secrets/version"
2
+ require "simple_secrets/primitives"
3
+ require "simple_secrets/packet"
4
+
5
+ module SimpleSecrets
6
+ # Your code goes here...
7
+ end
@@ -0,0 +1,107 @@
1
+
2
+
3
+ module SimpleSecrets
4
+
5
+ class Packet
6
+
7
+ def initialize master_key
8
+ raise ArgumentError unless master_key
9
+
10
+ @master_key = hex_to_bin master_key
11
+ @identity = Primitives.identify @master_key
12
+ end
13
+
14
+ def build_body data
15
+ nonce = Primitives.nonce
16
+ bindata = Primitives.serialize data
17
+
18
+ body = "#{nonce}#{bindata}"
19
+
20
+ Primitives.zero nonce, bindata
21
+ body
22
+ end
23
+
24
+ def body_to_data body
25
+ nonce = body[0...16]
26
+ bindata = body[16..-1]
27
+
28
+ data = Primitives.deserialize bindata
29
+
30
+ Primitives.zero nonce, bindata
31
+ data
32
+ end
33
+
34
+ def encrypt_body body, master_key
35
+ key = Primitives.derive_sender_key master_key
36
+
37
+ cipher_data = Primitives.encrypt body, key
38
+
39
+ Primitives.zero key
40
+ cipher_data
41
+ end
42
+
43
+ def decrypt_body cipher_data, master_key
44
+ key = Primitives.derive_sender_key master_key
45
+ iv = cipher_data[0...16]
46
+ encrypted = cipher_data[16..-1]
47
+
48
+ body = Primitives.decrypt encrypted, key, iv
49
+
50
+ Primitives.zero key, iv, encrypted
51
+ body
52
+ end
53
+
54
+ def authenticate data, master_key, identity
55
+ hmac_key = Primitives.derive_sender_hmac master_key
56
+
57
+ auth = "#{identity}#{data}"
58
+ mac = Primitives.mac auth, hmac_key
59
+ packet = "#{auth}#{mac}"
60
+
61
+ Primitives.zero hmac_key, mac
62
+ packet
63
+ end
64
+
65
+ def verify packet, master_key, identity
66
+ packet_id = packet[0...6]
67
+ return nil unless Primitives.compare packet_id, identity
68
+
69
+ data = packet[0...-32]
70
+ packet_mac = packet[-32..-1]
71
+ hmac_key = Primitives.derive_sender_hmac master_key
72
+ mac = Primitives.mac data, hmac_key
73
+ return nil unless Primitives.compare packet_mac, mac
74
+
75
+ Primitives.zero hmac_key, mac
76
+ packet[6...-32]
77
+ end
78
+
79
+ def pack data
80
+ body = build_body data
81
+ encrypted = encrypt_body body, @master_key
82
+ packet = authenticate encrypted, @master_key, @identity
83
+ websafe = Primitives.stringify packet
84
+
85
+ Primitives.zero body, encrypted, packet
86
+ websafe
87
+ end
88
+
89
+ def unpack websafe_data
90
+ packet = Primitives.binify websafe_data
91
+ cipher_data = verify packet, @master_key, @identity
92
+ Primitives.zero packet
93
+ return nil unless cipher_data
94
+
95
+ body = decrypt_body cipher_data, @master_key
96
+ data = body_to_data body
97
+
98
+ Primitives.zero body, cipher_data
99
+ data
100
+ end
101
+
102
+ private
103
+ def hex_to_bin string
104
+ [string].pack('H*')
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,372 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+ require 'msgpack'
4
+
5
+ module SimpleSecrets
6
+
7
+
8
+ # Public: Various methods useful for performing cryptographic operations.
9
+ # WARNING: Using any of these primitives in isolation could be Bad. Take cautious.
10
+ #
11
+ # Examples
12
+ #
13
+ # Primitives.nonce
14
+ # # => "\x02\x0E\xBB\xBE\xA2\xA4\f\x80\x11N\xCDui\xEE<e"
15
+ module Primitives
16
+
17
+
18
+ # Public: Provide 16 securely random bytes.
19
+ #
20
+ # Examples
21
+ #
22
+ # nonce
23
+ # # => "\x02\x0E\xBB\xBE\xA2\xA4\f\x80\x11N\xCDui\xEE<e"
24
+ #
25
+ # Returns 16 random bytes in a binary string
26
+ def self.nonce
27
+ OpenSSL::Random.random_bytes 16
28
+ end
29
+
30
+
31
+ # Public: Generate the authentication key for messages originating
32
+ # from the channel's Sender side.
33
+ #
34
+ # Uses the ASCII string 'simple-crypto/sender-hmac-key' as the role.
35
+ #
36
+ # master_key - the 256-bit master key of this secure channel
37
+ #
38
+ # Examples
39
+ #
40
+ # Primitives.derive_sender_hmac(master_key)
41
+ # # =>
42
+ #
43
+ # Returns 256-bit sender hmac key
44
+ def self.derive_sender_hmac master_key
45
+ derive(master_key, 'simple-crypto/sender-hmac-key')
46
+ end
47
+
48
+
49
+ # Public: Generate the encryption key for messages originating from the
50
+ # channel's Sender side.
51
+ #
52
+ # Uses the ASCII string 'simple-crypto/sender-cipher-key' as the role.
53
+ #
54
+ # master_key - the 256-bit master key of this secure channel
55
+ #
56
+ # Examples
57
+ #
58
+ # Primitives.derive_sender_key(master_key)
59
+ # # =>
60
+ #
61
+ # Returns 256-bit sender encryption key
62
+ def self.derive_sender_key master_key
63
+ derive(master_key, 'simple-crypto/sender-cipher-key')
64
+ end
65
+
66
+
67
+ # Public: Generate the authentication key for messages originating
68
+ # from the channel's Receiver side.
69
+ #
70
+ # Uses the ASCII string 'simple-crypto/receiver-hmac-key' as the role.
71
+ #
72
+ # master_key - the 256-bit master key of this secure channel
73
+ #
74
+ # Examples
75
+ #
76
+ # Primitives.derive_receiver_hmac(master_key)
77
+ # # =>
78
+ #
79
+ # Returns 256-bit receiver hmac key
80
+ def self.derive_receiver_hmac master_key
81
+ derive(master_key, 'simple-crypto/receiver-hmac-key')
82
+ end
83
+
84
+
85
+ # Public: Generate the encryption key for messages originating
86
+ # from the channel's Receiver side.
87
+ #
88
+ # Uses the ASCII string 'simple-crypto/receiver-cipher-key' as the role.
89
+ #
90
+ # master_key - the 256-bit master key of this secure channel
91
+ #
92
+ # Examples
93
+ #
94
+ # Primitives.derive_receiver_key(master_key)
95
+ # # =>
96
+ #
97
+ # Returns 256-bit receiver encryption key
98
+ def self.derive_receiver_key master_key
99
+ derive(master_key, 'simple-crypto/receiver-cipher-key')
100
+ end
101
+
102
+
103
+ # Public: Encrypt buffer with the given key.
104
+ #
105
+ # Uses AES256 with a random 128-bit initialization vector.
106
+ #
107
+ # binary - the plaintext binary string
108
+ # key - the 256-bit encryption key
109
+ #
110
+ # Examples
111
+ #
112
+ # Primitives.encrypt('', '')
113
+ # # =>
114
+ #
115
+ # Returns a binary string of (IV || ciphertext)
116
+ def self.encrypt binary, key
117
+ assertBinary(binary)
118
+ assertBinary(key)
119
+ assert256BitBinary(key)
120
+
121
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
122
+ cipher.encrypt
123
+ cipher.key = key
124
+ iv = cipher.random_iv
125
+
126
+ encrypted = ''.force_encoding('BINARY')
127
+ encrypted << iv
128
+ encrypted << cipher.update(binary)
129
+ encrypted << cipher.final
130
+ encrypted
131
+ end
132
+
133
+
134
+ # Public: Decrypt buffer with the given key and initialization vector.
135
+ #
136
+ # Uses AES256.
137
+ #
138
+ # binary - ciphertext
139
+ # key - the 256-bit encryption key
140
+ # iv - the 128-bit initialization vector
141
+ #
142
+ # Examples
143
+ #
144
+ # Primitives.decrypt('', '')
145
+ # # =>
146
+ #
147
+ # Returns the plaintext binary string
148
+ def self.decrypt binary, key, iv
149
+ assertBinary(binary, key, iv)
150
+ assert256BitBinary(key)
151
+ assert128BitBinary(iv)
152
+
153
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
154
+ cipher.decrypt
155
+ cipher.key = key
156
+ cipher.iv = iv
157
+
158
+ decrypted = ''.force_encoding('BINARY')
159
+ decrypted << cipher.update(binary)
160
+ decrypted << cipher.final
161
+ decrypted
162
+ end
163
+
164
+
165
+ # Public: Create a short identifier for potentially sensitive data.
166
+ #
167
+ # binary - the data to identify
168
+ #
169
+ # Examples
170
+ #
171
+ # Primitives.identify('')
172
+ # # =>
173
+ #
174
+ # Returns a 6-byte binary string identifier
175
+ def self.identify binary
176
+ assertBinary(binary)
177
+
178
+ hash = OpenSSL::Digest::SHA256.new
179
+ hash << [binary.size].pack("C*")
180
+ hash << binary
181
+ hash.digest[0..5]
182
+ end
183
+
184
+
185
+ # Public: Create a message authentication code for the given data.
186
+ # Uses HMAC-SHA256.
187
+ #
188
+ # binary - data to authenticate
189
+ # hmac_key - the authentication key
190
+ #
191
+ # Examples
192
+ #
193
+ # Primitives.mac('','')
194
+ # # =>
195
+ #
196
+ # Returns a 32-byte MAC binary string
197
+ def self.mac binary, hmac_key
198
+ assertBinary(binary, hmac_key)
199
+ assert256BitBinary(hmac_key)
200
+
201
+ OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, hmac_key, binary)
202
+ end
203
+
204
+
205
+ # Public: Use a constant-time comparison algorithm to reduce
206
+ # side-channel attacks.
207
+ #
208
+ # Short-circuits only when the two buffers aren't the same length.
209
+ #
210
+ # a - a binary string
211
+ # b - a binary string
212
+ #
213
+ # Examples
214
+ #
215
+ # Primitives.compare('','')
216
+ # # =>
217
+ #
218
+ # Returns true if both buffer contents match
219
+ def self.compare a, b
220
+ assertBinary(a, b)
221
+
222
+ # things must be the same length to compare them.
223
+ return false if a.bytesize != b.bytesize
224
+
225
+ # constant-time compare
226
+ # hat-tip to https://github.com/freewil/scmp for |=
227
+ same = 0;
228
+ (0...a.bytesize).each do |i|
229
+ same |= a.getbyte(i) ^ b.getbyte(i)
230
+ end
231
+ same == 0
232
+ end
233
+
234
+
235
+ # Public: Turn a websafe string back into a binary string.
236
+ #
237
+ # Uses base64url encoding.
238
+ #
239
+ # string - websafe string
240
+ #
241
+ # Examples
242
+ #
243
+ # Primitives.binify('')
244
+ # # =>
245
+ #
246
+ # Returns the binary version of this string
247
+ def self.binify string
248
+ raise 'base64url string required.' unless (string.instance_of?(String) && string =~ /^[a-zA-Z0-9_\-]+$/)
249
+
250
+ string += '=' while !(string.size % 4).zero?
251
+ Base64.urlsafe_decode64(string)
252
+ end
253
+
254
+
255
+ # Public: Turn a binary buffer into a websafe string.
256
+ #
257
+ # Uses base64url encoding.
258
+ #
259
+ # binary - data which needs to be websafe
260
+ #
261
+ # Examples
262
+ #
263
+ # Primitives.stringify('')
264
+ # # =>
265
+ #
266
+ # Returns the websafe string
267
+ def self.stringify binary
268
+ assertBinary(binary)
269
+
270
+ Base64.urlsafe_encode64(binary).gsub('=','')
271
+ end
272
+
273
+
274
+ # Public: Turn a JSON-like object into a binary
275
+ # representation suitable for use in crypto functions.
276
+ # This object will possibly be deserialized in a different
277
+ # programming environment—it should be JSON-like in structure.
278
+ #
279
+ # Uses MsgPack data serialization.
280
+ #
281
+ # object - any object without cycles which responds to `to_msgpack`
282
+ #
283
+ # Examples
284
+ #
285
+ # Primitives.serialize('')
286
+ # # =>
287
+ #
288
+ # Returns the binary version of this object
289
+ def self.serialize object
290
+ object.to_msgpack
291
+ end
292
+
293
+
294
+ # Public: Turn a binary representation into a Ruby object
295
+ # suitable for use in application logic. This object
296
+ # possibly originated in a different programming
297
+ # environment—it should be JSON-like in structure.
298
+ #
299
+ # Uses MsgPack data serialization.
300
+ #
301
+ # binary - a binary string version of the object
302
+ #
303
+ # Examples
304
+ #
305
+ # Primitives.deserialize('')
306
+ # # =>
307
+ #
308
+ # Returns the Ruby object
309
+ def self.deserialize binary
310
+ assertBinary(binary)
311
+
312
+ MessagePack.unpack(binary)
313
+ end
314
+
315
+
316
+ # Public: Overwrite the contents of the buffer with zeroes.
317
+ # This is critical for removing sensitive data from memory.
318
+ #
319
+ # args - binary strings whose content should be wiped
320
+ #
321
+ # Examples
322
+ #
323
+ # Primitives.zero('','')
324
+ # # =>
325
+ #
326
+ # Returns an array of references to the strings which have been zeroed
327
+ def self.zero *args
328
+ assertBinary(*args)
329
+ args.each do |buf|
330
+ buf.gsub!(/./,"\x00")
331
+ end
332
+ end
333
+
334
+ private
335
+
336
+ # Private: Generate an encryption or hmac key from the master key and role.
337
+ # Uses SHA256(key || role). [TODO: link or citation]
338
+ #
339
+ # master_key - The 256-bit binary string master key of this secure channel.
340
+ # role - The part of the protocol in which this key will be used.
341
+ #
342
+ # Examples
343
+ #
344
+ # derive "\x0ER\xE5\x88\xC2\xBB<\xAFZ?\xA5\xCCx\xA6@AB(Bc\x962\x7F:\xF7\x0E\x1Cl\xB9\x02Y\xE4", "some-protocol/some-role"
345
+ # # => "~\x80\xB4\xC3>\xC4\xDEw\xB2\xD2\x92\xC9\x88\xA8\xD7p\xAF\xF6Y\x95\x91\xA3\xFDV\xC5qo\x80U\x19P\xB0"
346
+ #
347
+ # Returns 256-bit derived key as a 32-byte binary string
348
+
349
+ def self.derive master_key, role
350
+ assertBinary(master_key)
351
+ assert256BitBinary(master_key)
352
+ hash = OpenSSL::Digest::SHA256.new
353
+ hash << master_key
354
+ hash << role.force_encoding('BINARY')
355
+ hash.digest
356
+ end
357
+
358
+ def self.assertBinary *binaries
359
+ binaries.each { |binary| raise "Bad encoding. Binary string required." unless binary.instance_of?(String) && binary.encoding == Encoding::ASCII_8BIT }
360
+ end
361
+
362
+ def self.assert256BitBinary binary
363
+ raise '256-bit binary string required.' unless binary.size == 32
364
+ end
365
+
366
+ def self.assert128BitBinary binary
367
+ raise '128-bit binary string required.' unless binary.size == 16
368
+ end
369
+
370
+ end
371
+
372
+ end
@@ -0,0 +1,3 @@
1
+ module SimpleSecrets
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'simple_secrets/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "simple-secrets"
8
+ spec.version = SimpleSecrets::VERSION
9
+ spec.authors = ["Tim Shadel"]
10
+ spec.email = ["tim@shadelsoftware.com"]
11
+ spec.description = %q{A Ruby client for simple-secrets, the simple, opinionated library for encrypting small packets of data securely.}
12
+ spec.summary = %q{A Ruby client for simple-secrets, the simple, opinionated library for encrypting small packets of data securely.}
13
+ spec.homepage = "https://github.com/timshadel/simple-secrets.rb"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "msgpack"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec"
26
+ end
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+
3
+ include SimpleSecrets
4
+
5
+ describe Packet do
6
+
7
+ let(:master_key){ "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd".force_encoding 'BINARY' }
8
+ let(:data){ 'foobar' }
9
+ let(:nonce){ '11'.hex_to_bin 16 } # Generated with Primitives.nonce
10
+
11
+ let(:test_body){ "\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\xA6foobar".force_encoding 'BINARY' }
12
+ let(:bad_id){ 'fd'.hex_to_bin 6 }
13
+ let(:bad_mac){ 'fd'.hex_to_bin 32 }
14
+
15
+ subject{ Packet.new master_key }
16
+
17
+ describe '#initialize' do
18
+ it 'sets its master key' do
19
+ its_key = subject.instance_variable_get(:@master_key)
20
+ its_key.unpack('H*').first.should eq master_key
21
+ its_key.encoding.should eq Encoding::ASCII_8BIT
22
+ end
23
+
24
+ it 'sets its identity' do
25
+ identity = Primitives.identify subject.instance_variable_get(:@master_key)
26
+ subject.instance_variable_get(:@identity).should eq identity
27
+ end
28
+ end
29
+
30
+ describe '#build_body' do
31
+ it 'concatenates the serialized body with a nonce' do
32
+ Primitives.should_receive(:nonce){ nonce }
33
+ subject.build_body(data).should eq test_body
34
+ end
35
+ end
36
+
37
+ describe '#body_to_data' do
38
+ it 'it splits out the nonce and deserializes the body' do
39
+ subject.body_to_data(test_body).should eq data
40
+ end
41
+ end
42
+
43
+ describe '#encrypt_body and #decrypt_body' do
44
+ it 'encrypts the data and then decrypts it back' do
45
+ key = subject.instance_variable_get(:@master_key)
46
+
47
+ cipher_data = subject.encrypt_body test_body, key
48
+ decrypted_body = subject.decrypt_body cipher_data, key
49
+ test_body.should_not eq cipher_data
50
+ cipher_data.should_not eq decrypted_body
51
+ decrypted_body.should eq test_body
52
+ end
53
+ end
54
+
55
+ describe '#authenticate and #verify' do
56
+ it 'creates an authentication signature and then verifies it' do
57
+ key = subject.instance_variable_get(:@master_key)
58
+ id = subject.instance_variable_get(:@identity)
59
+
60
+ packet = subject.authenticate test_body, key, id
61
+ data = subject.verify packet, key, id
62
+ data.should eq test_body
63
+ end
64
+
65
+ it 'returns nil if the key identity does not match' do
66
+ key = subject.instance_variable_get(:@master_key)
67
+ id = subject.instance_variable_get(:@identity)
68
+
69
+ packet = subject.authenticate test_body, key, bad_id
70
+ data = subject.verify packet, key, id
71
+ data.should be_nil
72
+ end
73
+
74
+ it 'returns nil if the MAC does not match' do
75
+ key = subject.instance_variable_get(:@master_key)
76
+ id = subject.instance_variable_get(:@identity)
77
+
78
+ packet = subject.authenticate test_body, key, id
79
+ packet = "#{packet[0...-32]}#{bad_mac}"
80
+
81
+ data = subject.verify packet, key, id
82
+ data.should be_nil
83
+ end
84
+ end
85
+
86
+ describe '.pack and .unpack' do
87
+ it 'encrypts and signs data into web-safe string, then verifies and decrypts it back' do
88
+ packed_data = subject.pack data
89
+ packed_data.should_not eq data
90
+
91
+ unpacked_data = subject.unpack packed_data
92
+ unpacked_data.should eq data
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,362 @@
1
+ require 'spec_helper'
2
+
3
+ include SimpleSecrets
4
+
5
+ describe Primitives do
6
+
7
+ describe 'nonce' do
8
+ it 'should return 16 random bytes' do
9
+ expect(Primitives.nonce.size).to eq(16)
10
+ expect(Primitives.nonce).to_not eq(Primitives.nonce)
11
+ end
12
+ end
13
+
14
+ describe 'derive_sender_hmac' do
15
+ it 'should require a buffer' do
16
+ buf = "32".hex_to_bin(32)
17
+ expect { Primitives.derive_sender_hmac('') }.to raise_error(/binary string required/i)
18
+ expect { Primitives.derive_sender_hmac(buf) }.to_not raise_error
19
+ end
20
+
21
+ it 'should require a 256-bit key' do
22
+ short = "33".hex_to_bin(31)
23
+ exact = "33".hex_to_bin(32)
24
+ long = "33".hex_to_bin(33)
25
+
26
+ expect { Primitives.derive_sender_hmac(short) }.to raise_error(/256-bit/i)
27
+ expect(Primitives.derive_sender_hmac(exact).encoding).to eq(Encoding::ASCII_8BIT)
28
+ expect { Primitives.derive_sender_hmac(long) }.to raise_error(/256-bit/i)
29
+ end
30
+
31
+ it 'should derive a 256-bit hmac key from a 256-bit master key' do
32
+ master_key = 'bc'.hex_to_bin(32)
33
+
34
+ hmac_key = Primitives.derive_sender_hmac(master_key);
35
+ expect(hmac_key.size).to eq(32)
36
+ expect(hmac_key).to eq('1e2e2725f135463f05c268ffd1c1687dbc9dd7da65405697471052236b3b3088'.hex_to_bin);
37
+ end
38
+ end
39
+
40
+ describe 'derive_sender_key' do
41
+ it 'should require a buffer' do
42
+ buf = "32".hex_to_bin(32)
43
+ expect { Primitives.derive_sender_key('') }.to raise_error(/binary string required/i)
44
+ expect { Primitives.derive_sender_key(buf) }.to_not raise_error
45
+ end
46
+
47
+ it 'should require a 256-bit key' do
48
+ short = "33".hex_to_bin(31)
49
+ exact = "33".hex_to_bin(32)
50
+ long = "33".hex_to_bin(33)
51
+
52
+ expect { Primitives.derive_sender_key(short) }.to raise_error(/256-bit/i)
53
+ expect(Primitives.derive_sender_key(exact).encoding).to eq(Encoding::ASCII_8BIT)
54
+ expect { Primitives.derive_sender_key(long) }.to raise_error(/256-bit/i)
55
+ end
56
+
57
+ it 'should derive a 256-bit encryption key from a 256-bit master key' do
58
+ master_key = 'bc'.hex_to_bin(32)
59
+
60
+ hmac_key = Primitives.derive_sender_key(master_key);
61
+ expect(hmac_key.size).to eq(32)
62
+ expect(hmac_key).to eq('327b5f32d7ff0beeb0a7224166186e5f1fc2ba681092214a25b1465d1f17d837'.hex_to_bin);
63
+ end
64
+ end
65
+
66
+ describe 'derive_receiver_hmac' do
67
+ it 'should require a buffer' do
68
+ buf = "32".hex_to_bin(32)
69
+ expect { Primitives.derive_receiver_hmac('') }.to raise_error(/binary string required/i)
70
+ expect { Primitives.derive_receiver_hmac(buf) }.to_not raise_error
71
+ end
72
+
73
+ it 'should require a 256-bit key' do
74
+ short = "33".hex_to_bin(31)
75
+ exact = "33".hex_to_bin(32)
76
+ long = "33".hex_to_bin(33)
77
+
78
+ expect { Primitives.derive_receiver_hmac(short) }.to raise_error(/256-bit/i)
79
+ expect(Primitives.derive_receiver_hmac(exact).encoding).to eq(Encoding::ASCII_8BIT)
80
+ expect { Primitives.derive_receiver_hmac(long) }.to raise_error(/256-bit/i)
81
+ end
82
+
83
+ it 'should derive a 256-bit hmac key from a 256-bit master key' do
84
+ master_key = 'bc'.hex_to_bin(32)
85
+
86
+ hmac_key = Primitives.derive_receiver_hmac(master_key);
87
+ expect(hmac_key.size).to eq(32)
88
+ expect(hmac_key).to eq('375f52dff2a263f2d0e0df11d252d25ba18b2f9abae1f0cbf299bab8d8c4904d'.hex_to_bin);
89
+ end
90
+ end
91
+
92
+ describe 'derive_receiver_key' do
93
+ it 'should require a buffer' do
94
+ buf = "32".hex_to_bin(32)
95
+ expect { Primitives.derive_receiver_key('') }.to raise_error(/binary string required/i)
96
+ expect { Primitives.derive_receiver_key(buf) }.to_not raise_error
97
+ end
98
+
99
+ it 'should require a 256-bit key' do
100
+ short = "33".hex_to_bin(31)
101
+ exact = "33".hex_to_bin(32)
102
+ long = "33".hex_to_bin(33)
103
+
104
+ expect { Primitives.derive_receiver_key(short) }.to raise_error(/256-bit/i)
105
+ expect(Primitives.derive_receiver_key(exact).encoding).to eq(Encoding::ASCII_8BIT)
106
+ expect { Primitives.derive_receiver_key(long) }.to raise_error(/256-bit/i)
107
+ end
108
+
109
+ it 'should derive a 256-bit encryption key from a 256-bit master key' do
110
+ master_key = 'bc'.hex_to_bin(32)
111
+
112
+ hmac_key = Primitives.derive_receiver_key(master_key);
113
+ expect(hmac_key.size).to eq(32)
114
+ expect(hmac_key).to eq('c7e2a9660369f243aed71b0de0c49ee69719d20261778fdf39991a456566ef22'.hex_to_bin);
115
+ end
116
+ end
117
+
118
+ describe 'encrypt' do
119
+ it 'should require a buffer' do
120
+ buf = "32".hex_to_bin(32)
121
+ expect { Primitives.encrypt('', '') }.to raise_error(/binary string required/i)
122
+ expect { Primitives.encrypt('', buf) }.to raise_error(/binary string required/i)
123
+ expect { Primitives.encrypt(buf, '') }.to raise_error(/binary string required/i)
124
+ expect { Primitives.encrypt(buf, buf) }.to_not raise_error
125
+ end
126
+
127
+ it 'should encrypt data using a 256-bit key' do
128
+ key = 'cd'.hex_to_bin(32)
129
+ data = '11'.hex_to_bin(25)
130
+
131
+ binmessage = Primitives.encrypt(data, key)
132
+ iv = binmessage[0...16]
133
+ ciphertext = binmessage[16..-1]
134
+
135
+ expect(iv.size).to eq(16)
136
+ expect(ciphertext.size).to eq(32)
137
+ recovered = Primitives.decrypt(ciphertext, key, iv)
138
+ expect(recovered).to eq(data)
139
+ end
140
+
141
+ it 'should return a Buffer of (iv || ciphertext)' do
142
+ key = 'cd'.hex_to_bin(32)
143
+ data = '11'.hex_to_bin(25)
144
+
145
+ output = Primitives.encrypt(data, key)
146
+ expect(output.encoding).to eq(Encoding::ASCII_8BIT)
147
+ # 16-byte IV, 32 bytes to encrypt the 25 data bytes
148
+ expect(output.size).to eq(48)
149
+ end
150
+ end
151
+
152
+ describe 'decrypt' do
153
+ it 'should require a buffer' do
154
+ buf = "32".hex_to_bin(32)
155
+ expect { Primitives.decrypt('', '', '') }.to raise_error(/binary string required/i)
156
+ expect { Primitives.decrypt('', '', buf) }.to raise_error(/binary string required/i)
157
+ expect { Primitives.decrypt('', buf, '') }.to raise_error(/binary string required/i)
158
+ expect { Primitives.decrypt(buf, '', '') }.to raise_error(/binary string required/i)
159
+ expect { Primitives.decrypt(buf, buf, '') }.to raise_error(/binary string required/i)
160
+ expect { Primitives.decrypt(buf, '', buf) }.to raise_error(/binary string required/i)
161
+ expect { Primitives.decrypt('', buf, buf) }.to raise_error(/binary string required/i)
162
+ end
163
+
164
+ it 'should decrypt data using a 256-bit key' do
165
+ key = "cd".hex_to_bin(32)
166
+ plaintext = "11".hex_to_bin(25)
167
+ iv = 'd4a5794c81015dde3b9b0648f2b9f5b9'.hex_to_bin
168
+ ciphertext = 'cb7f804ec83617144aa261f24af07023a91a3864601a666edea98938f2702dbc'.hex_to_bin
169
+
170
+ recovered = Primitives.decrypt(ciphertext, key, iv)
171
+ expect(recovered).to eq(plaintext)
172
+ end
173
+ end
174
+
175
+ describe 'identify' do
176
+ it 'should require a buffer' do
177
+ buf = "32".hex_to_bin(10)
178
+ expect { Primitives.identify('') }.to raise_error(/binary string required/i)
179
+ expect { Primitives.identify(buf) }.to_not raise_error
180
+ end
181
+
182
+ it 'should calculate an id for a key' do
183
+ key = "ab".hex_to_bin(32)
184
+ id = Primitives.identify(key)
185
+
186
+ expect(id.size).to eq(6)
187
+ expect(id).to eq('0d081b0889d7'.hex_to_bin)
188
+ end
189
+ end
190
+
191
+ describe 'mac' do
192
+ it 'should create a message authentication code' do
193
+ key = "9f".hex_to_bin(32)
194
+ data = "11".hex_to_bin(25)
195
+ mac = Primitives.mac(data, key)
196
+
197
+ expect(mac.size).to eq(32)
198
+ expect(mac).to eq('adf1793fdef44c54a2c01513c0c7e4e71411600410edbde61558db12d0a01c65'.hex_to_bin)
199
+ end
200
+ end
201
+
202
+ describe 'compare' do
203
+ it 'should require a buffer' do
204
+ buf = "32".hex_to_bin(10)
205
+ expect { Primitives.compare('', '') }.to raise_error(/binary string required/i)
206
+ expect { Primitives.compare('', buf) }.to raise_error(/binary string required/i)
207
+ expect { Primitives.compare(buf, '') }.to raise_error(/binary string required/i)
208
+ expect { Primitives.compare(buf, buf) }.to_not raise_error
209
+ end
210
+
211
+ it 'should correctly distinguish data equality' do
212
+ a = "11".hex_to_bin
213
+ b = "12".hex_to_bin
214
+ c = "11".hex_to_bin
215
+
216
+ expect(Primitives.compare(a,a)).to be_true
217
+ expect(Primitives.compare(a,b)).to be_false
218
+ expect(Primitives.compare(a,c)).to be_true
219
+ end
220
+
221
+ # Timing test, in progress from node.js version
222
+ # # (node.js) This works fine locally, but has tons of variation on build server
223
+ # it 'should take just as long to compare different data as identical data', :skip => true do
224
+ # a = "ff".hex_to_bin(250000)
225
+ # b = "00".hex_to_bin(250000)
226
+ # c = "ff".hex_to_bin(250000)
227
+ #
228
+ # benchAA = benchmark(primitives.compare, a, a)
229
+ # benchAB = benchmark(primitives.compare, a, b)
230
+ # benchAC = benchmark(primitives.compare, a, c)
231
+ #
232
+ # naiveAA = benchmark(naiveEquals, a, a)
233
+ # naiveAB = benchmark(naiveEquals, a, b)
234
+ # naiveAC = benchmark(naiveEquals, a, c)
235
+ #
236
+ # # All constant-time comparisons should be roughly equal in time
237
+ # expect(difference(benchAA, benchAB)).to.be.greaterThan(0.95)
238
+ # expect(difference(benchAA, benchAC)).to.be.greaterThan(0.95)
239
+ # expect(difference(benchAB, benchAC)).to.be.greaterThan(0.95)
240
+ #
241
+ # # Naive comparisons of the same item with itself, or with obviously
242
+ # # different items should be ridiculously fast
243
+ # expect(difference(benchAA, naiveAA)).to.be.lessThan(0.01)
244
+ # expect(difference(benchAB, naiveAB)).to.be.lessThan(0.01)
245
+ #
246
+ # # It should take just about as long to compare identical arrays as the constant time compare
247
+ # expect(difference(benchAC, naiveAC)).to.be.greaterThan(0.90)
248
+ #
249
+ # function naiveEquals(a, b) {
250
+ # if (a === b) return true;
251
+ # for (var i = 0; i < a.length; i++) {
252
+ # if (a[i] !== b[i]) {
253
+ # return false;
254
+ # }
255
+ # }
256
+ # return true;
257
+ # }
258
+ #
259
+ # function benchmark(fn, a, b) {
260
+ # var time = process.hrtime();
261
+ # for (var i = 0; i < 100; i++) {
262
+ # fn(a, b);
263
+ # };
264
+ # var diff = process.hrtime(time);
265
+ # return diff[0] * 1e9 + diff[1];
266
+ # }
267
+ #
268
+ # function difference(first, second) {
269
+ # var smaller = Math.min(first, second);
270
+ # var larger = Math.max(first, second);
271
+ # return (smaller / larger);
272
+ # }
273
+ #
274
+ # end
275
+ end
276
+
277
+ describe 'binify' do
278
+ it 'should require a base64url string' do
279
+ expect { Primitives.binify(123) }.to raise_error(/string required/i)
280
+ expect { Primitives.binify('arstnei; another.') }.to raise_error(/base64url/i)
281
+ expect { Primitives.binify('cartinir90_-') }.to_not raise_error
282
+ end
283
+
284
+ it 'should return a Buffer' do
285
+ bin = Primitives.binify('abcd')
286
+ expect(bin.encoding).to eq(Encoding::ASCII_8BIT)
287
+ expect(bin.size).to eq(3)
288
+ end
289
+ end
290
+
291
+ describe 'stringify' do
292
+ it 'should require a buffer' do
293
+ buf = "32".hex_to_bin(10)
294
+ expect { Primitives.stringify('') }.to raise_error(/binary string required/i)
295
+ expect { Primitives.stringify(buf) }.to_not raise_error
296
+ end
297
+
298
+ it 'should return a base64url string' do
299
+ buf = "32".hex_to_bin(10)
300
+ str = Primitives.stringify(buf)
301
+ expect(str.encoding).to eq(Encoding::US_ASCII)
302
+ expect(str.size).to eq(14)
303
+ expect(str).to match(/^[a-zA-Z0-9_-]+$/)
304
+ end
305
+ end
306
+
307
+ describe 'serialize' do
308
+ it 'should accept javascript object' do
309
+ expect { Primitives.serialize(1) }.to_not raise_error
310
+ expect { Primitives.serialize('a') }.to_not raise_error
311
+ expect { Primitives.serialize([]) }.to_not raise_error
312
+ expect { Primitives.serialize({}) }.to_not raise_error
313
+ end
314
+
315
+ it 'should return a Buffer' do
316
+ bin = Primitives.serialize('abcd')
317
+ expect(bin.encoding).to eq(Encoding::ASCII_8BIT)
318
+ expect(bin.size).to eq(5)
319
+ end
320
+ end
321
+
322
+ describe 'deserialize' do
323
+ it 'should require a buffer' do
324
+ buf = "32".hex_to_bin(10)
325
+ expect { Primitives.deserialize('') }.to raise_error(/binary string required/i)
326
+ end
327
+
328
+ it 'should return a javascript primitive or object' do
329
+ expect(Primitives.deserialize(Primitives.serialize(1))).to eq(1)
330
+ expect(Primitives.deserialize(Primitives.serialize('abcd'))).to eq('abcd')
331
+ expect(Primitives.deserialize(Primitives.serialize([]))).to eq([])
332
+ expect(Primitives.deserialize(Primitives.serialize({}))).to eq({})
333
+ end
334
+ end
335
+
336
+ describe 'zero' do
337
+ it 'should require a Buffer' do
338
+ expect { Primitives.zero({}) }.to raise_error(/binary string required/i)
339
+ end
340
+
341
+ it 'should overwrite all buffer contents with zeros' do
342
+ b = '7468697320697320736f6d65'.hex_to_bin
343
+ z = '000000000000000000000000'.hex_to_bin
344
+
345
+ expect(b).to_not eq(z)
346
+ Primitives.zero(b)
347
+ expect(b).to eq(z)
348
+ end
349
+
350
+ it 'should zero multiple buffers' do
351
+ b = '7468697320697320736f6d65'.hex_to_bin
352
+ c = '697320736f6d657468697320'.hex_to_bin
353
+ z = '000000000000000000000000'.hex_to_bin
354
+
355
+ expect(b).to_not eq(z)
356
+ expect(c).to_not eq(z)
357
+ Primitives.zero(b, c)
358
+ expect(b).to eq(z)
359
+ expect(c).to eq(z)
360
+ end
361
+ end
362
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe SimpleSecrets do
4
+ it 'should have a version number' do
5
+ SimpleSecrets::VERSION.should_not be_nil
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'simple_secrets'
3
+
4
+
5
+ class String
6
+ def hex_to_bin repeat=1
7
+ [self*repeat].pack('H*')
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple-secrets
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tim Shadel
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: msgpack
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: bundler
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.3'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.3'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: A Ruby client for simple-secrets, the simple, opinionated library for
79
+ encrypting small packets of data securely.
80
+ email:
81
+ - tim@shadelsoftware.com
82
+ executables: []
83
+ extensions: []
84
+ extra_rdoc_files: []
85
+ files:
86
+ - .gitignore
87
+ - .rspec
88
+ - .travis.yml
89
+ - Gemfile
90
+ - LICENSE.txt
91
+ - README.md
92
+ - Rakefile
93
+ - lib/simple-secrets.rb
94
+ - lib/simple_secrets.rb
95
+ - lib/simple_secrets/packet.rb
96
+ - lib/simple_secrets/primitives.rb
97
+ - lib/simple_secrets/version.rb
98
+ - simple_secrets.gemspec
99
+ - spec/packet_spec.rb
100
+ - spec/primitives_spec.rb
101
+ - spec/simple_secrets_spec.rb
102
+ - spec/spec_helper.rb
103
+ homepage: https://github.com/timshadel/simple-secrets.rb
104
+ licenses:
105
+ - MIT
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ none: false
112
+ requirements:
113
+ - - ! '>='
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubyforge_project:
124
+ rubygems_version: 1.8.23
125
+ signing_key:
126
+ specification_version: 3
127
+ summary: A Ruby client for simple-secrets, the simple, opinionated library for encrypting
128
+ small packets of data securely.
129
+ test_files:
130
+ - spec/packet_spec.rb
131
+ - spec/primitives_spec.rb
132
+ - spec/simple_secrets_spec.rb
133
+ - spec/spec_helper.rb