slosilo 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +32 -0
- data/Rakefile +17 -0
- data/lib/slosilo/adapters/abstract_adapter.rb +19 -0
- data/lib/slosilo/adapters/mock_adapter.rb +8 -0
- data/lib/slosilo/adapters/sequel_adapter/migration.rb +49 -0
- data/lib/slosilo/adapters/sequel_adapter.rb +34 -0
- data/lib/slosilo/attr_encrypted.rb +59 -0
- data/lib/slosilo/http_request.rb +59 -0
- data/lib/slosilo/key.rb +95 -0
- data/lib/slosilo/keystore.rb +61 -0
- data/lib/slosilo/rack/middleware.rb +123 -0
- data/lib/slosilo/random.rb +11 -0
- data/lib/slosilo/symmetric.rb +33 -0
- data/lib/slosilo/version.rb +3 -0
- data/lib/slosilo.rb +13 -0
- data/lib/tasks/slosilo.rake +22 -0
- data/slosilo.gemspec +25 -0
- data/spec/http_request_spec.rb +107 -0
- data/spec/http_stack_spec.rb +44 -0
- data/spec/key_spec.rb +115 -0
- data/spec/keystore_spec.rb +13 -0
- data/spec/rack_middleware_spec.rb +109 -0
- data/spec/random_spec.rb +9 -0
- data/spec/sequel_adapter_spec.rb +60 -0
- data/spec/slosilo_spec.rb +58 -0
- data/spec/spec_helper.rb +62 -0
- data/spec/symmetric_spec.rb +51 -0
- metadata +185 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
namespace :slosilo do
|
2
|
+
desc "Dump a public key"
|
3
|
+
task :dump, [:name] => :environment do |t, args|
|
4
|
+
args.with_defaults(:name => :own)
|
5
|
+
puts Slosilo[args[:name]]
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "Enroll a key"
|
9
|
+
task :enroll, [:name] => :environment do |t, args|
|
10
|
+
key = Slosilo::Key.new STDIN.read
|
11
|
+
Slosilo[args[:name]] = key
|
12
|
+
puts key
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "Generate a key pair"
|
16
|
+
task :generate, [:name] => :environment do |t, args|
|
17
|
+
args.with_defaults(:name => :own)
|
18
|
+
key = Slosilo::Key.new
|
19
|
+
Slosilo[args[:name]] = key
|
20
|
+
puts key
|
21
|
+
end
|
22
|
+
end
|
data/slosilo.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/slosilo/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Rafa\305\202 Rzepecki"]
|
6
|
+
gem.email = ["divided.mind@gmail.com"]
|
7
|
+
gem.description = %q{This gem provides an easy way of storing and retrieving encryption keys in the database.}
|
8
|
+
gem.summary = %q{Store SSL keys in a database}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "slosilo"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Slosilo::VERSION
|
17
|
+
gem.required_ruby_version = '~> 1.9.3'
|
18
|
+
|
19
|
+
gem.add_development_dependency 'rake'
|
20
|
+
gem.add_development_dependency 'rspec'
|
21
|
+
gem.add_development_dependency 'ci_reporter'
|
22
|
+
gem.add_development_dependency 'simplecov'
|
23
|
+
gem.add_development_dependency 'sequel' # for sequel tests
|
24
|
+
gem.add_development_dependency 'sqlite3' # for sequel tests
|
25
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Slosilo::HTTPRequest do
|
4
|
+
let(:keyname) { :bacon }
|
5
|
+
let(:encrypt) { subject.encrypt! }
|
6
|
+
subject { Hash.new }
|
7
|
+
before do
|
8
|
+
subject.extend Slosilo::HTTPRequest
|
9
|
+
subject.keyname = keyname
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#sign!" do
|
13
|
+
let(:own_key) { double "own key" }
|
14
|
+
before { Slosilo.stub(:[]).with(:own).and_return own_key }
|
15
|
+
|
16
|
+
let(:signed_data) { "this is the truest truth" }
|
17
|
+
before { subject.stub signed_data: signed_data }
|
18
|
+
let(:timestamp) { "long time ago" }
|
19
|
+
let(:signature) { "seal of approval" }
|
20
|
+
let(:token) { { "data" => signed_data, "timestamp" => timestamp, "signature" => signature } }
|
21
|
+
|
22
|
+
it "makes a token out of the data to sign and inserts headers" do
|
23
|
+
own_key.stub(:signed_token).with(signed_data).and_return token
|
24
|
+
subject.should_receive(:[]=).with 'Timestamp', timestamp
|
25
|
+
subject.should_receive(:[]=).with 'X-Slosilo-Signature', signature
|
26
|
+
subject.sign!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#signed_data" do
|
31
|
+
before { subject.stub path: :path, body: 'body' }
|
32
|
+
context "when X-Slosilo-Key not present" do
|
33
|
+
its(:signed_data) { should == { "path" => :path, "body" => "Ym9keQ==" } }
|
34
|
+
end
|
35
|
+
|
36
|
+
context "when X-Slosilo-Key is present" do
|
37
|
+
before { subject.merge! 'X-Slosilo-Key' => :key }
|
38
|
+
its(:signed_data) { should == { "path" => :path, "body" => "Ym9keQ==", "key" => :key } }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#encrypt!" do
|
43
|
+
context "when key not set" do
|
44
|
+
before { subject.keyname = nil }
|
45
|
+
it "does nothing" do
|
46
|
+
subject.should_not_receive(:body=)
|
47
|
+
encrypt
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when requested key does not exist" do
|
52
|
+
before { Slosilo.stub(:[]).and_return nil }
|
53
|
+
it "raises error" do
|
54
|
+
expect{ encrypt }.to raise_error
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "when the key exists" do
|
59
|
+
let(:key) { double "key" }
|
60
|
+
context "when the body is not empty" do
|
61
|
+
let(:plaintext) { "Keep your solutions close, and your problems closer." }
|
62
|
+
let(:ciphertext) { "And, when you want something, all the universe conspires in helping you to achieve it." }
|
63
|
+
let(:skey) { "make me sound like a fool instead" }
|
64
|
+
before do
|
65
|
+
subject.stub body: plaintext
|
66
|
+
key.stub(:encrypt).with(plaintext).and_return([ciphertext, skey])
|
67
|
+
Slosilo.stub(:[]).with(keyname).and_return key
|
68
|
+
end
|
69
|
+
|
70
|
+
it "encrypts the message body and adds the X-Slosilo-Key header" do
|
71
|
+
subject.should_receive(:body=).with ciphertext
|
72
|
+
subject.should_receive(:[]=).with 'X-Slosilo-Key', Base64::urlsafe_encode64(skey)
|
73
|
+
encrypt
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context "when the body is empty" do
|
78
|
+
before { subject.stub body: "" }
|
79
|
+
it "doesn't set the key header" do
|
80
|
+
subject.should_not_receive(:[]=).with 'X-Slosilo-Key'
|
81
|
+
encrypt
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "#exec" do
|
88
|
+
class Subject
|
89
|
+
def exec *a
|
90
|
+
"ok, got it"
|
91
|
+
end
|
92
|
+
|
93
|
+
def initialize keyname
|
94
|
+
extend Slosilo::HTTPRequest
|
95
|
+
self.keyname = keyname
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
subject { Subject.new keyname }
|
100
|
+
|
101
|
+
it "encrypts, then signs and delegates to the superclass" do
|
102
|
+
subject.should_receive(:encrypt!).once.ordered
|
103
|
+
subject.should_receive(:sign!).once.ordered
|
104
|
+
subject.exec(:foo).should == "ok, got it"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "http request stack" do
|
4
|
+
include_context "with example key"
|
5
|
+
include_context "with mock adapter"
|
6
|
+
before { Slosilo[:own] = key }
|
7
|
+
|
8
|
+
class MockRequest < Hash
|
9
|
+
def exec *a
|
10
|
+
end
|
11
|
+
|
12
|
+
def [] name
|
13
|
+
name = name.sub(/^HTTP_/,'').gsub('_', '-').split(/(\W)/).map(&:capitalize).join
|
14
|
+
result = super name
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
extend Slosilo::HTTPRequest
|
19
|
+
self['Authorization'] = "Simon says it's fine"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
subject { MockRequest.new }
|
24
|
+
let(:path) { '/some/path' }
|
25
|
+
|
26
|
+
context "with authorization header" do
|
27
|
+
it "works" do
|
28
|
+
mw = Slosilo::Rack::Middleware.new lambda{|_|:ok}, signature_required: true
|
29
|
+
subject.stub path: path, body: ''
|
30
|
+
mw.stub path: path
|
31
|
+
subject.send :exec
|
32
|
+
mw.call(subject).should == :ok
|
33
|
+
end
|
34
|
+
|
35
|
+
it "detects tampering" do
|
36
|
+
mw = Slosilo::Rack::Middleware.new lambda{|_|:ok}, signature_required: true
|
37
|
+
subject.stub path: path, body: ''
|
38
|
+
mw.stub path: path
|
39
|
+
subject.send :exec
|
40
|
+
subject['Authorization'] = "Simon changed his mind"
|
41
|
+
mw.call(subject).should_not == :ok
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/spec/key_spec.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Slosilo::Key do
|
4
|
+
include_context "with example key"
|
5
|
+
|
6
|
+
subject { key }
|
7
|
+
its(:to_der) { should == rsa.to_der }
|
8
|
+
its(:to_s) { should == rsa.public_key.to_pem }
|
9
|
+
|
10
|
+
let(:plaintext) { 'quick brown fox jumped over the lazy dog' }
|
11
|
+
describe '#encrypt' do
|
12
|
+
it "generates a symmetric encryption key and encrypts the plaintext with the public key" do
|
13
|
+
ctxt, skey = subject.encrypt plaintext
|
14
|
+
pskey = rsa.private_decrypt skey
|
15
|
+
Slosilo::Symmetric.new.decrypt(ctxt, key: pskey).should == plaintext
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#decrypt' do
|
20
|
+
let(:ciphertext) { "\x8B\xE6\xEC\x8C\xAB\xA4\xC0\x8EF\"\x0F\xD5Yh\xA1\aq\x00\xF5\xAC\xAB\v\a\xEC\xF6G\xA6\e\x14N\xFF\x11\x98\xDA\x19\xB5\x8994_:\xA0\xF8\x06l\xDC\x9B\xB1\xE8z\x83\xCC\x9A\x02E\x02tOhu\x92F]h" }
|
21
|
+
let(:skey) { "\x15\xFF\xD4>\x9C\xF4\x0F\xB6\x04C\x18\xC1\x96\xC3\xE0T\xA3\xF5\xE8\x17\xA6\xE0\x86~rrw\xC3\xDF\x11)$\x9B\r@\x0E\xE4Zv\rw-\xFC\x1C\x84\x17\xBD\xDB\x12u\xCD\r\xFEo\xD5\xC8[\x8FEA\\\xA9\xA2F#\x8BH -\xFA\xF7\xA9\xEBf\x97\xAAT}\x8E*\xC0r\x944p\xD0\x9A\xE7\xBD\a;O\f\xEF\xE4B.y\xFA\xB4e@O\xBB\x15>y\xC9\t=\x01\xE1J\xF3X\xA9\x9E3\x04^H\x1F\xFF\x19C\x93ve)@\xF7\t_\xCF\xDE\xF1\xB7]\x83lL\xB8%A\x93p{\xE9Y\xBAu\xCE\x99T\xDC\xDF\xE7\x0FD%\xB9AXb\x1CW\x94$P\xBB\xE1g\xDEE\t\xC4\x92\x9E\xFEt\xDF\xD0\xEA\x03\xC4\x12\xA9\x02~u\xF4\x92 ;\xA0\xCE.\b+)\x05\xEDo\xA5cF\xF8\x12\xD7F\x97\xE44\xBF\xF1@\xA5\xC5\xA8\xE5\a\xE8ra<\x04\xB5\xA2\f\t\xF7T\x97\e\xF5(^\xAB\xA5%84\x10\xD1\x13e<\xCA/\xBF}%\xF6\xB1%\xB2" }
|
22
|
+
|
23
|
+
it "decrypts the symmetric key and then uses it to decrypt the ciphertext" do
|
24
|
+
subject.decrypt(ciphertext, skey).should == plaintext
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#initialize' do
|
29
|
+
context "when no argument given" do
|
30
|
+
subject { Slosilo::Key.new }
|
31
|
+
let (:rsa) { double "key" }
|
32
|
+
it "generates a new key pair" do
|
33
|
+
OpenSSL::PKey::RSA.should_receive(:new).with(2048).and_return(rsa)
|
34
|
+
subject.key.should == rsa
|
35
|
+
end
|
36
|
+
end
|
37
|
+
context "when given an armored key" do
|
38
|
+
subject { Slosilo::Key.new rsa.to_der }
|
39
|
+
its(:to_der) { should == rsa.to_der }
|
40
|
+
end
|
41
|
+
context "when given a key instance" do
|
42
|
+
subject { Slosilo::Key.new rsa }
|
43
|
+
its(:to_der) { should == rsa.to_der }
|
44
|
+
end
|
45
|
+
context "when given something else" do
|
46
|
+
subject { Slosilo::Key.new "foo" }
|
47
|
+
it "fails early" do
|
48
|
+
expect { subject }.to raise_error
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "#sign" do
|
54
|
+
context "when given a hash" do
|
55
|
+
it "converts to a sorted array and signs that" do
|
56
|
+
key.should_receive(:sign_string).with '[["a",3],["b",42]]'
|
57
|
+
key.sign b: 42, a: 3
|
58
|
+
end
|
59
|
+
end
|
60
|
+
context "when given an array" do
|
61
|
+
it "signs a JSON representation instead" do
|
62
|
+
key.should_receive(:sign_string).with '[2,[42,2]]'
|
63
|
+
key.sign [2, [42, 2]]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
context "when given a string" do
|
67
|
+
let(:expected_signature) { "d[\xA4\x00\x02\xC5\x17\xF5P\x1AD\x91\xF9\xC1\x00P\x0EG\x14,IN\xDE\x17\xE1\xA2a\xCC\xABR\x99'\xB0A\xF5~\x93M/\x95-B\xB1\xB6\x92!\x1E\xEA\x9C\v\xC2O\xA8\x91\x1C\xF9\x11\x92a\xBFxm-\x93\x9C\xBBoM\x92%\xA9\xD06$\xC1\xBC.`\xF8\x03J\x16\xE1\xB0c\xDD\xBF\xB0\xAA\xD7\xD4\xF4\xFC\e*\xAB\x13A%-\xD3\t\xA5R\x18\x01let6\xC8\xE9\"\x7F6O\xC7p\x82\xAB\x04J(IY\xAA]b\xA4'\xD6\x873`\xAB\x13\x95g\x9C\x17\xCAB\xF8\xB9\x85B:^\xC5XY^\x03\xEA\xB6V\x17b2\xCA\xF5\xD6\xD4\xD2\xE3u\x11\xECQ\x0Fb\x14\xE2\x04\xE1<a\xC5\x01eW-\x15\x01X\x81K\x1A\xE5A\vVj\xBF\xFC\xFE#\xD5\x93y\x16\xDC\xB4\x8C\xF0\x02Y\xA8\x87i\x01qC\xA7#\xE8\f\xA5\xF0c\xDEJ\xB0\xDB BJ\x87\xA4\xB0\x92\x80\x03\x95\xEE\xE9\xB8K\xC0\xE3JbE-\xD4\xCBP\\\x13S\"\eZ\xE1\x93\xFDa pinch of salt" }
|
68
|
+
it "signs it" do
|
69
|
+
key.stub salt: 'a pinch of salt'
|
70
|
+
key.sign("this sentence is not this sentence").should == expected_signature
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#signed_token" do
|
76
|
+
let(:time) { Time.new(2012,1,1,1,1,1,0) }
|
77
|
+
let(:data) { { "foo" => :bar } }
|
78
|
+
let(:token_to_sign) { { "data" => data, "timestamp" => "2012-01-01 01:01:01 UTC" } }
|
79
|
+
let(:signature) { "signature" }
|
80
|
+
let(:salt) { 'a pinch of salt' }
|
81
|
+
let(:expected_signature) { Base64::urlsafe_encode64 "\xB0\xCE{\x9FP\xEDV\x9C\xE7b\x8B[\xFAil\x87^\x96\x17Z\x97\x1D\xC2?B\x96\x9C\x8Ep-\xDF_\x8F\xC21\xD9^\xBC\n\x16\x04\x8DJ\xF6\xAF-\xEC\xAD\x03\xF9\xEE:\xDF\xB5\x8F\xF9\xF6\x81m\xAB\x9C\xAB1\x1E\x837\x8C\xFB\xA8P\xA8<\xEA\x1Dx\xCEd\xED\x84f\xA7\xB5t`\x96\xCC\x0F\xA9t\x8B\x9Fo\xBF\x92K\xFA\xFD\xC5?\x8F\xC68t\xBC\x9F\xDE\n$\xCA\xD2\x8F\x96\x0EtX2\x8Cl\x1E\x8Aa\r\x8D\xCAi\x86\x1A\xBD\x1D\xF7\xBC\x8561j\x91YlO\xFA(\x98\x10iq\xCC\xAF\x9BV\xC6\v\xBC\x10Xm\xCD\xFE\xAD=\xAA\x95,\xB4\xF7\xE8W\xB8\x83;\x81\x88\xE6\x01\xBA\xA5F\x91\x17\f\xCE\x80\x8E\v\x83\x9D<\x0E\x83\xF6\x8D\x03\xC0\xE8A\xD7\x90i\x1D\x030VA\x906D\x10\xA0\xDE\x12\xEF\x06M\xD8\x8B\xA9W\xC8\x9DTc\x8AJ\xA4\xC0\xD3!\xFA\x14\x89\xD1p\xB4J7\xA5\x04\xC2l\xDC8<\x04Y\xD8\xA4\xFB[\x89\xB1\xEC\xDA\xB8\xD7\xEA\x03Ja pinch of salt" }
|
82
|
+
let(:expected_token) { { "data" => data, "timestamp" => "2012-01-01 01:01:01 UTC", "signature" => expected_signature } }
|
83
|
+
before do
|
84
|
+
key.stub salt: salt
|
85
|
+
Time.stub new: time
|
86
|
+
end
|
87
|
+
subject { key.signed_token data }
|
88
|
+
it { should == expected_token }
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#token_valid?" do
|
92
|
+
let(:data) { { "foo" => :bar } }
|
93
|
+
let(:signature) { Base64::urlsafe_encode64 "\xB0\xCE{\x9FP\xEDV\x9C\xE7b\x8B[\xFAil\x87^\x96\x17Z\x97\x1D\xC2?B\x96\x9C\x8Ep-\xDF_\x8F\xC21\xD9^\xBC\n\x16\x04\x8DJ\xF6\xAF-\xEC\xAD\x03\xF9\xEE:\xDF\xB5\x8F\xF9\xF6\x81m\xAB\x9C\xAB1\x1E\x837\x8C\xFB\xA8P\xA8<\xEA\x1Dx\xCEd\xED\x84f\xA7\xB5t`\x96\xCC\x0F\xA9t\x8B\x9Fo\xBF\x92K\xFA\xFD\xC5?\x8F\xC68t\xBC\x9F\xDE\n$\xCA\xD2\x8F\x96\x0EtX2\x8Cl\x1E\x8Aa\r\x8D\xCAi\x86\x1A\xBD\x1D\xF7\xBC\x8561j\x91YlO\xFA(\x98\x10iq\xCC\xAF\x9BV\xC6\v\xBC\x10Xm\xCD\xFE\xAD=\xAA\x95,\xB4\xF7\xE8W\xB8\x83;\x81\x88\xE6\x01\xBA\xA5F\x91\x17\f\xCE\x80\x8E\v\x83\x9D<\x0E\x83\xF6\x8D\x03\xC0\xE8A\xD7\x90i\x1D\x030VA\x906D\x10\xA0\xDE\x12\xEF\x06M\xD8\x8B\xA9W\xC8\x9DTc\x8AJ\xA4\xC0\xD3!\xFA\x14\x89\xD1p\xB4J7\xA5\x04\xC2l\xDC8<\x04Y\xD8\xA4\xFB[\x89\xB1\xEC\xDA\xB8\xD7\xEA\x03Ja pinch of salt" }
|
94
|
+
let(:token) { { "data" => data, "timestamp" => "2012-01-01 01:01:01 UTC", "signature" => signature } }
|
95
|
+
before { Time.stub now: Time.new(2012,1,1,1,2,1,0) }
|
96
|
+
subject { key.token_valid? token }
|
97
|
+
it { should be_true }
|
98
|
+
context "when token is 1 hour old" do
|
99
|
+
before { Time.stub now: Time.new(2012,1,1,2,1,1,0) }
|
100
|
+
it { should be_false }
|
101
|
+
context "when timestamp in the token is changed accordingly" do
|
102
|
+
let(:token) { { "data" => data, "timestamp" => "2012-01-01 02:00:01 UTC", "signature" => signature } }
|
103
|
+
it { should be_false }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
context "when the data is changed" do
|
107
|
+
let(:data) { { "foo" => :baz } }
|
108
|
+
it { should be_false }
|
109
|
+
end
|
110
|
+
context "when RSA decrypt raises an error" do
|
111
|
+
before { OpenSSL::PKey::RSA.any_instance.should_receive(:public_decrypt).and_raise(OpenSSL::PKey::RSAError) }
|
112
|
+
it { should be_false }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Slosilo::Keystore do
|
4
|
+
include_context "with example key"
|
5
|
+
include_context "with mock adapter"
|
6
|
+
|
7
|
+
describe '#put' do
|
8
|
+
it "handles Slosilo::Keys too" do
|
9
|
+
subject.put(:test, key)
|
10
|
+
adapter['test'].should == rsa.to_der
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Slosilo::Rack::Middleware do
|
4
|
+
include_context "with example key"
|
5
|
+
mock_own_key
|
6
|
+
|
7
|
+
let(:app) { double "app" }
|
8
|
+
subject { Slosilo::Rack::Middleware.new app }
|
9
|
+
|
10
|
+
describe '#path' do
|
11
|
+
context "when QUERY_STRING is empty" do
|
12
|
+
let(:env) { { 'SCRIPT_NAME' => '/foo', 'PATH_INFO' => '/bar', 'QUERY_STRING' => '' } }
|
13
|
+
before { subject.stub env: env }
|
14
|
+
its(:path) { should == '/foo/bar' }
|
15
|
+
end
|
16
|
+
context "when QUERY_STRING is not" do
|
17
|
+
let(:env) { { 'SCRIPT_NAME' => '/foo', 'PATH_INFO' => '/bar', 'QUERY_STRING' => 'baz' } }
|
18
|
+
before { subject.stub env: env }
|
19
|
+
its(:path) { should == '/foo/bar?baz' }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#call' do
|
24
|
+
let(:call) { subject.call(env) }
|
25
|
+
let(:path) { "/this/is/the/path" }
|
26
|
+
before { subject.stub path: path }
|
27
|
+
context "when no X-Slosilo-Key is given" do
|
28
|
+
let(:env) { {} }
|
29
|
+
let(:result) { double "result" }
|
30
|
+
it "passes the env verbatim" do
|
31
|
+
app.should_receive(:call).with(env).and_return(result)
|
32
|
+
call.should == result
|
33
|
+
end
|
34
|
+
|
35
|
+
context "and X-Slosilo-Signature is given" do
|
36
|
+
let(:body) { "the body" }
|
37
|
+
let(:timestamp) { "long time ago" }
|
38
|
+
let(:signature) { "in blood" }
|
39
|
+
let(:env) { {'rack.input' => StringIO.new(body), 'HTTP_TIMESTAMP' => timestamp, 'HTTP_X_SLOSILO_SIGNATURE' => signature } }
|
40
|
+
let(:token) { { "data" => { "path" => path, "body" => "dGhlIGJvZHk=" }, "timestamp" => timestamp, "signature" => signature } }
|
41
|
+
context "when the signature is valid" do
|
42
|
+
before { Slosilo.stub(:token_valid?).with(token).and_return true }
|
43
|
+
it "passes the env verbatim" do
|
44
|
+
app.should_receive(:call).with(env).and_return(result)
|
45
|
+
call.should == result
|
46
|
+
end
|
47
|
+
end
|
48
|
+
context "when the signature is invalid" do
|
49
|
+
before { Slosilo.stub(:token_valid?).with(token).and_return false }
|
50
|
+
it "returns 401" do
|
51
|
+
call[0].should == 401
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "but encryption is required" do
|
57
|
+
subject { Slosilo::Rack::Middleware.new app, encryption_required: true }
|
58
|
+
context "and the body is not empty" do
|
59
|
+
let(:env) { {'rack.input' => StringIO.new('foo') } }
|
60
|
+
it "returns 403" do
|
61
|
+
status, headers, body = call
|
62
|
+
status.should == 403
|
63
|
+
end
|
64
|
+
end
|
65
|
+
context "but the body is empty" do
|
66
|
+
subject { Slosilo::Rack::Middleware.new app, encryption_required: true, signature_required: false }
|
67
|
+
let(:body) { "" }
|
68
|
+
let(:timestamp) { "long time ago" }
|
69
|
+
let(:env) { {'rack.input' => StringIO.new(body) } }
|
70
|
+
it "passes the env verbatim" do
|
71
|
+
app.should_receive(:call).with(env).and_return(result)
|
72
|
+
call.should == result
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "when no X-Slosilo-Signature is given" do
|
79
|
+
context "but signature is required" do
|
80
|
+
let(:env) {{}}
|
81
|
+
subject { Slosilo::Rack::Middleware.new app, signature_required: true }
|
82
|
+
it "returns 401" do
|
83
|
+
status, headers, body = call
|
84
|
+
status.should == 401
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
let(:plaintext) { "If you were taught that elves caused rain, every time it rained, you'd see the proof of elves." }
|
90
|
+
let(:skey) { "Eiho7xIoFj-Qwqc0swcQQJzJyM1sSv_b6VdRIoHCPRUwemB0v5MNyOirU_5dQ_bNzlmSlo8HDvfAnMgapwpIBH__uDUV_3nCkzrzQVV3-bSp6owJnqebeSQxJMoVMKEWqqek3ZCBPo0OB63A8mkYGu9955gDEDOnlxLkETGb3SmDQIVJtiMmAkUWN0fh9z1M9Ycw9FfworaHKQXRLw6z6Rl-Yoe_TDaiKVlGIYjQKpCz8h_I5lRdrhPJaP53d0yQuKMK3PBHMzE77IikZyQ3VZdoqI9XqzUJF27KehxJ_BCx0oAcPaxG6I7WWe3Xb7K7MhE4HgzqVZACDLhYfm_0XA==" }
|
91
|
+
let(:ciphertext) { "0\xDE\xE1\xBA=\x06+K\xE0\xCAD\xC6\xE3 d\xC7kx\x90\r\ni\xDCXmS!EP\xAB\xEF\xAA\x13{\x85f\x8FU,\xB3zO\x1F\x85\f\x0E\xAE\xF8\x10`\x1C\x94\xAB@\xFA\xBC\xC0/\x1F\xA6nX\xFF-m\xF4\xC3f\xBB\xCA\x05\xC82\x18l\xC3\xF0v\x96\v\x8F\xFC\xB2\xC7wX;\xF6v\xDCX:\xCC\xF8\xD7\x99\xC8\x1A\xBA\x9F\xDB\xE7\x0F\xF2\xC9f\aaGs\xEFc" }
|
92
|
+
context "when X-Slosilo-Key is given" do
|
93
|
+
context "when the key decrypts cleanly" do
|
94
|
+
let(:env) { {'HTTP_X_SLOSILO_KEY' => skey, 'rack.input' => StringIO.new(ciphertext) } }
|
95
|
+
it "passes the decrypted contents" do
|
96
|
+
app.should_receive(:call).with(rack_environment_with_input(plaintext)).and_return(:result)
|
97
|
+
call.should == :result
|
98
|
+
end
|
99
|
+
end
|
100
|
+
context "when the key is invalid" do
|
101
|
+
let(:env) { {'HTTP_X_SLOSILO_KEY' => "broken #{skey}", 'rack.input' => StringIO.new(ciphertext) } }
|
102
|
+
it "returns 403 status" do
|
103
|
+
status, headers, body = call
|
104
|
+
status.should == 403
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/spec/random_spec.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'slosilo/adapters/sequel_adapter'
|
4
|
+
|
5
|
+
describe Slosilo::Adapters::SequelAdapter do
|
6
|
+
let(:model) { double "model" }
|
7
|
+
before { subject.stub create_model: model }
|
8
|
+
|
9
|
+
describe "#get_key" do
|
10
|
+
context "when given key does not exist" do
|
11
|
+
before { model.stub :[] => nil }
|
12
|
+
it "returns nil" do
|
13
|
+
subject.get_key(:whatever).should_not be
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#put_key" do
|
19
|
+
let(:id) { "id" }
|
20
|
+
let(:key) { "key" }
|
21
|
+
it "creates the key" do
|
22
|
+
model.should_receive(:create).with id: id, key: key
|
23
|
+
subject.put_key id, key
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
let(:adapter) { subject }
|
28
|
+
describe "#each" do
|
29
|
+
let(:one) { double("one", id: :one, key: :onek) }
|
30
|
+
let(:two) { double("two", id: :two, key: :twok) }
|
31
|
+
before { model.stub(:each).and_yield(one).and_yield(two) }
|
32
|
+
|
33
|
+
it "iterates over each key" do
|
34
|
+
results = []
|
35
|
+
adapter.each { |id,k| results << { id => k } }
|
36
|
+
results.should == [ { one: :onek}, {two: :twok } ]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#model' do
|
41
|
+
let(:db) { Sequel.sqlite }
|
42
|
+
before do
|
43
|
+
Slosilo::encryption_key = Slosilo::Symmetric.new.random_key
|
44
|
+
subject.unstub :create_model
|
45
|
+
require 'sequel'
|
46
|
+
Sequel::Model.db = db
|
47
|
+
Sequel.extension :migration
|
48
|
+
require 'slosilo/adapters/sequel_adapter/migration'
|
49
|
+
Sequel::Migration::descendants.first.apply db, :up
|
50
|
+
end
|
51
|
+
|
52
|
+
let(:key) { 'fake key' }
|
53
|
+
let(:id) { 'some id' }
|
54
|
+
it "transforms (encrypts) the key" do
|
55
|
+
subject.model.create id: id, key: key
|
56
|
+
db[:slosilo_keystore][id: id][:key].should_not == key
|
57
|
+
subject.model[id].key.should == key
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Slosilo do
|
4
|
+
include_context "with mock adapter"
|
5
|
+
let(:key) { OpenSSL::PKey::RSA.new 512 }
|
6
|
+
before { adapter['test'] = key.to_der }
|
7
|
+
|
8
|
+
describe '[]' do
|
9
|
+
it "returns a Slosilo::Key" do
|
10
|
+
Slosilo[:test].should be_instance_of Slosilo::Key
|
11
|
+
end
|
12
|
+
|
13
|
+
context "when the requested key does not exist" do
|
14
|
+
it "returns nil instead of creating a new key" do
|
15
|
+
Slosilo[:aether].should_not be
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '.sign' do
|
21
|
+
let(:own_key) { double "own key" }
|
22
|
+
before { Slosilo.stub(:[]).with(:own).and_return own_key }
|
23
|
+
let (:argument) { double "thing to sign" }
|
24
|
+
it "fetches the own key and signs using that" do
|
25
|
+
own_key.should_receive(:sign).with(argument)
|
26
|
+
Slosilo.sign argument
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '.token_valid?' do
|
31
|
+
before { adapter['test'].stub token_valid?: false }
|
32
|
+
let(:key2) { double "key 2", token_valid?: false }
|
33
|
+
let(:key3) { double "key 3", token_valid?: false }
|
34
|
+
before do
|
35
|
+
adapter[:key2] = key2
|
36
|
+
adapter[:key3] = key3
|
37
|
+
end
|
38
|
+
|
39
|
+
let(:token) { double "token" }
|
40
|
+
subject { Slosilo.token_valid? token }
|
41
|
+
|
42
|
+
context "when no key validates the token" do
|
43
|
+
before { Slosilo::Key.stub new: (double "key", token_valid?: false) }
|
44
|
+
it { should be_false }
|
45
|
+
end
|
46
|
+
|
47
|
+
context "when a key validates the token" do
|
48
|
+
let(:valid_key) { double token_valid?: true }
|
49
|
+
let(:invalid_key) { double token_valid?: true }
|
50
|
+
before do
|
51
|
+
Slosilo::Key.stub new: invalid_key
|
52
|
+
Slosilo::Key.stub(:new).with(key2).and_return(valid_key)
|
53
|
+
end
|
54
|
+
|
55
|
+
it { should be_true }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require "simplecov"
|
2
|
+
SimpleCov.start
|
3
|
+
|
4
|
+
require 'slosilo'
|
5
|
+
|
6
|
+
shared_context "with mock adapter" do
|
7
|
+
require 'slosilo/adapters/mock_adapter'
|
8
|
+
|
9
|
+
let(:adapter) { Slosilo::Adapters::MockAdapter.new }
|
10
|
+
before { Slosilo::adapter = adapter }
|
11
|
+
end
|
12
|
+
|
13
|
+
shared_context "with example key" do
|
14
|
+
let(:rsa) { OpenSSL::PKey::RSA.new """
|
15
|
+
-----BEGIN RSA PRIVATE KEY-----
|
16
|
+
MIIEpQIBAAKCAQEAtTG/SQhW9QawP+GL6EZ5Al9gscCr7HiRO7MuQqFkaXIJD6+3
|
17
|
+
prdHRrb0qqjNlGFgDBGAuswZ2AYqhBt7eekup+/vIpI5n04b0w+is3WwZAFco4uP
|
18
|
+
ojDeM0aY65Ar3Zgra2vWUJXRwBumroZjVBVoLJSgVfwIhwU6ORbS2oJflbtqxpuS
|
19
|
+
zkPDqS6RwEzI/DHuHTOI26fe+vfuDqGOuSR6iVI16lfvTbWwccpDwU0W9vSlyjjD
|
20
|
+
LIw0MnoKL3DHyzO66s+oNNRleMvjghQtJk/xg1kRuHReJ5/ygt2zyzdKSLeqU+T+
|
21
|
+
TCWw/F65jrFElftexiS+g+lZC467VLCaMe1fJQIDAQABAoIBAQCiNWzXRr4CEQDL
|
22
|
+
z3Deeehu9U+tEZ1Xzv/FgD0TrUQlGc9+2YIBn+YRKkySUxfnk9zWMP0bPQiN2cdK
|
23
|
+
CQhbNSNteGCOhHVNZjGGm2K+YceNX6K9Tn1BZ5okMTlI+QIsGMQWIK316omh/58S
|
24
|
+
coCNj7R45H09PKmtpkJfRU1yDHDhqypjPDpb9/7U5mt3g2BdXYi+1hilfonHoDrC
|
25
|
+
yy3eRdf7Tlij9O3UeM+Z7pZrKATcvpDkYbNWizDITvKMYy6Ss+ajM5v7lt6QN5LP
|
26
|
+
MHjwX8Ilrxkxl0jeopr4f94tR7rNDZbLC457j8gns7cUeODtF7pPZqlrlk4KOq8Q
|
27
|
+
DvEMt2ZpAoGBAOLNUiO1SwRo75Y8ukuMVQev8O8WuzEEGINoM1lQiYlbUw3HmVp3
|
28
|
+
iUvv58ANmKSzTXpOEZ1L8pZHSp435FrzD2WZmCAoXhNdfAXtmZA7Y46iE6BF4qrr
|
29
|
+
UegtLPhVgwpO74Y+4w2YwfDknzCOhWE4sxCbukuSvxz2pz1Vm31eFB6jAoGBAMyF
|
30
|
+
VxfYq9WhmLNsHqR+qfhb1EC5FfpSq23z/o4ryiKqCaWHimVtOO7DL7x2SK3mVNNZ
|
31
|
+
X8b4+vnJpAQ3nOxeg8fpmBaLAWYRna2AN/CYVIMKYusawhsGAlZZTu2mtJKLiOPS
|
32
|
+
8/z5dK55xJWlG5JalUB+n/4vd3WmXiT/XJj3qU+XAoGBALyHzLXeKCPcTvzmMj5G
|
33
|
+
wxAG0xMMJEMUkoP5hGXEKvBBOAMGXpXzM/Ap1s2w/6g5XDhE2SOWVGtTi9WFxI9N
|
34
|
+
6Qid6vUgWUNjvIr4/WQF2jZgyEu8jDVkM8v6cZ1lB+7zuuwvLnLI/r6ObT3h20H7
|
35
|
+
7e3qZawYqkEbT94OYZiPMc5dAoGAHmIQtjIyFOKU1NLTGozWo1bBCXx1j2KIpSUC
|
36
|
+
RAytUsj/9d9U6Ax50L6ecNkBoxP8tgko+V4zqrgR7a51WYgQ+7nwJikwZAFp80SB
|
37
|
+
CvUWWQFKALNQ8sLJxhouZ4/Ec6DXDUFhjcthUio00iZdGjjqw1IMYq6aiJfWlJh7
|
38
|
+
IR5pwLECgYEAyjlguks/3cjrHRF+yjonxT4tLuBI/n3TAQUPqmtkJtcwZSAJas/1
|
39
|
+
c/twlAJ7F6n3ZroF3lgPxMJRRHZl4Z4dJsDatIxVShf3nSGg9Mi5C25obxahbv5/
|
40
|
+
Dg1ikwi8GUF4HPZe9DyhXgDhg19wM/qcpjX8bSypsUWHWP+FanhjdWU=
|
41
|
+
-----END RSA PRIVATE KEY-----
|
42
|
+
""" }
|
43
|
+
let (:key) { Slosilo::Key.new rsa.to_der }
|
44
|
+
|
45
|
+
def self.mock_own_key
|
46
|
+
before { Slosilo.stub(:[]).with(:own).and_return key }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class RackEnvironmentInputMatcher
|
51
|
+
def initialize expected
|
52
|
+
@expected = expected
|
53
|
+
end
|
54
|
+
|
55
|
+
def == env
|
56
|
+
env['rack.input'].read.should == @expected
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def rack_environment_with_input expected
|
61
|
+
RackEnvironmentInputMatcher.new expected
|
62
|
+
end
|