simple-secrets 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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