smaak 0.0.10 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/smaak/server.rb CHANGED
@@ -5,7 +5,7 @@ require 'smaak/auth_message'
5
5
  module Smaak
6
6
  class Server < Associate
7
7
  attr_accessor :nonce_store
8
- attr_accessor :public_key
8
+ attr_reader :private_key
9
9
 
10
10
  def initialize
11
11
  super
@@ -16,6 +16,10 @@ module Smaak
16
16
  set_key(key)
17
17
  end
18
18
 
19
+ def set_private_key(key)
20
+ @private_key = adapt_rsa_key(key)
21
+ end
22
+
19
23
  def auth_message_unique?(auth_message)
20
24
  if nonce_store[auth_message.nonce].nil?
21
25
  nonce_store[auth_message.nonce] = 1
@@ -24,26 +28,39 @@ module Smaak
24
28
  false
25
29
  end
26
30
 
27
- def get_signed_auth_message_from_request(request)
28
- authorization = request.env['HTTP_AUTHORIZATION']
29
- auth_body = JSON.parse(authorization)
30
- message = auth_body['message']
31
- signature = auth_body['signature']
32
- return AuthMessage.new(message), Base64.decode64(signature)
31
+ def build_auth_message_from_request(adaptor)
32
+ recipient_public_key = Base64.decode64(adaptor.header("x-smaak-recipient"))
33
+ psk = adaptor.header("x-smaak-psk")
34
+ expires = adaptor.header("x-smaak-expires")
35
+ identifier = adaptor.header("x-smaak-identifier")
36
+ nonce = adaptor.header("x-smaak-nonce")
37
+ encrypt = adaptor.header("x-smaak-encrypt")
38
+ auth_message = Smaak::AuthMessage.build(recipient_public_key, psk, expires, identifier, nonce, encrypt)
39
+ end
40
+
41
+ def verify_auth_message(auth_message)
42
+ return false if not auth_message_unique?(auth_message)
43
+ return false if not auth_message.intended_for_recipient?(@key.export)
44
+ identifier = auth_message.identifier
45
+ psk = @association_store[identifier]['psk']
46
+ return false if not auth_message.verify(psk)
47
+ true
33
48
  end
34
49
 
35
50
  def verify_signed_request(request)
36
- auth_message, signature = get_signed_auth_message_from_request(request)
37
- verify_auth_message(auth_message, signature)
51
+ adaptor = Smaak::create_adaptor(request)
52
+ auth_message = build_auth_message_from_request(adaptor)
53
+ return false if not verify_auth_message(auth_message)
54
+ pubkey = @association_store[auth_message.identifier]['public_key']
55
+ body = Smaak::Crypto::sink(adaptor.body)
56
+ body = Smaak::Crypto::decrypt(body, @private_key) if auth_message.encrypt
57
+ return false, nil if not Smaak::verify_authorization_headers(adaptor, pubkey)
58
+ return auth_message, body # TBD return ID from cert
38
59
  end
39
60
 
40
- def verify_auth_message(auth_message, signature)
41
- return false if not auth_message_unique?(auth_message)
42
- return false if not auth_message.intended_for_recipient?(@key.export)
43
- identity = auth_message.identity
44
- pubkey = @association_store[identity]['public_key']
45
- psk = @association_store[identity]['psk']
46
- auth_message.verify(signature, pubkey, psk)
61
+ def compile_response(auth_message, data)
62
+ return Smaak::Crypto::encrypt(data, @association_store[auth_message.identifier]['public_key']) if auth_message.encrypt
63
+ data
47
64
  end
48
65
  end
49
66
  end
@@ -0,0 +1,29 @@
1
+ require 'smaak.rb'
2
+ require 'smaak/server.rb'
3
+
4
+ module Smaak
5
+ class SmaakService
6
+ @@mutex = Mutex.new
7
+ @@instance = nil
8
+ attr_reader :smaak_server
9
+
10
+ def self.get_instance
11
+ @@mutex.synchronize do
12
+ if (@@instance.nil?)
13
+ @@instance = self.new
14
+ end
15
+ @@instance
16
+ end
17
+ end
18
+
19
+ def initialize
20
+ @smaak_server = Smaak::Server.new
21
+ configure_service
22
+ end
23
+
24
+ def configure_service
25
+ # @smaak_server.set_public_key(File.read('/service-provider-pub.pem'))
26
+ # @smaak_server.add_association('service-client-01', File.read('service-client-01-public.pem'), 'pre-shared-key')
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,10 @@
1
+ module Smaak
2
+ class Utils
3
+ def self.non_blank_string?(s)
4
+ return false if s.nil?
5
+ return false if not s.is_a? String
6
+ return false if s.strip == ""
7
+ return true
8
+ end
9
+ end
10
+ end
data/lib/smaak/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Smaak
2
- VERSION = "0.0.10"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/smaak.rb CHANGED
@@ -1,34 +1,74 @@
1
- require "smaak/version"
2
-
3
1
  require 'openssl'
2
+ require 'time'
3
+
4
+ require "smaak/version"
5
+ require "smaak/cavage_04"
6
+ require "smaak/adaptors/net_http_adaptor"
7
+ require "smaak/adaptors/rack_adaptor"
8
+ require "smaak/crypto"
4
9
 
5
10
  module Smaak
6
11
  DEFAULT_TOKEN_LIFE = 2 unless defined? DEFAULT_TOKEN_LIFE; DEFAULT_TOKEN_LIFE.freeze
7
12
 
8
- def self.obfuscate_psk(psk)
9
- Digest::MD5.hexdigest(psk.reverse)
13
+ @@adaptors = { Net::HTTPRequest => NetHttpAdaptor, Rack::Request => RackAdaptor}
14
+
15
+ def self.headers_to_be_signed
16
+ [ "x-smaak-recipient",
17
+ "x-smaak-identifier",
18
+ "x-smaak-psk",
19
+ "x-smaak-expires",
20
+ "x-smaak-nonce",
21
+ "x-smaak-encrypt" ]
22
+ end
23
+
24
+ def self.adaptors
25
+ @@adaptors
26
+ end
27
+
28
+ def self.add_request_adaptor(request_clazz, adaptor_clazz)
29
+ @@adaptors[request_clazz] = adaptor_clazz
30
+ end
31
+
32
+ def self.create_adaptor(request)
33
+ @@adaptors.each do |r, a|
34
+ return a.new(request) if request.is_a? r
35
+ end
36
+ raise ArgumentError.new("Unknown request class #{request.class}. Add an adaptor using Smaak::add_request_adaptor.")
10
37
  end
11
38
 
12
- def self.build_message(message_data)
13
- Base64.encode64(message_data.to_json)
39
+ def self.select_specification(adaptor, specification)
40
+ raise ArgumentError.new("Adaptor must be provided") if adaptor.nil?
41
+ return Cavage04.new(adaptor) if specification == Smaak::Cavage04::SPECIFICATION
42
+ raise ArgumentError.new("Unknown specification")
14
43
  end
15
44
 
16
- def self.sign_message_data(message_data, private_key)
17
- digest = OpenSSL::Digest::SHA256.new
18
- private_key.sign(digest, Smaak::build_message(message_data))
45
+ def self.sign_authorization_headers(key, auth_message, adaptor, specification = Smaak::Cavage04::SPECIFICATION)
46
+ specification = Smaak::select_specification(adaptor, specification)
47
+
48
+ signature_headers = specification.compile_signature_headers(auth_message)
49
+ signature_data = Smaak::Crypto::sign_data(signature_headers, key)
50
+ signature = Smaak::Crypto::encode64(signature_data)
51
+ specification.compile_auth_header(signature)
52
+ specification.adaptor
19
53
  end
20
54
 
21
- def self.generate_nonce
22
- SecureRandom::random_number(10000000000)
55
+ def self.verify_authorization_headers(adaptor, pubkey)
56
+ raise ArgumentError.new("Key is required") if pubkey.nil?
57
+ signature_headers, signature = Smaak::get_signature_data_from_request(adaptor)
58
+ return false if signature.nil?
59
+ return false if signature_headers.nil?
60
+ Smaak::Crypto::verify_signature(signature, Smaak::Crypto::encode64(signature_headers), pubkey)
23
61
  end
24
62
 
25
- def self.compile_auth_message_data(recipient_public_key, recipient_psk, token_life, identity, request_signing_data)
26
- { 'recipient' => recipient_public_key.export,
27
- 'identity' => identity,
28
- 'psk' => Smaak::obfuscate_psk(recipient_psk),
29
- 'expires' => Time.now.to_i + token_life,
30
- 'nonce' => Smaak::generate_nonce,
31
- 'data' => request_signing_data }
63
+ private
64
+
65
+ def self.get_signature_data_from_request(adaptor, specification = Smaak::Cavage04::SPECIFICATION)
66
+ specification = Smaak::select_specification(adaptor, specification)
67
+
68
+ signature_headers = specification.extract_signature_headers
69
+ signature = specification.extract_signature
70
+
71
+ return signature_headers, Base64.decode64(signature)
32
72
  end
33
73
  end
34
74
 
data/smaak.gemspec CHANGED
@@ -27,4 +27,5 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency 'simplecov'
28
28
  spec.add_development_dependency 'simplecov-rcov'
29
29
  spec.add_development_dependency 'rspec'
30
+ spec.add_development_dependency 'rack'
30
31
  end
@@ -0,0 +1,99 @@
1
+ require './spec/spec_helper.rb'
2
+
3
+ describe Smaak::NetHttpAdaptor do
4
+ before :each do
5
+ @request = Net::HTTP::Post.new("http://rubygems.org:80/gems/smaak")
6
+ @request.body = "body-data"
7
+ @iut = Smaak::NetHttpAdaptor.new(@request)
8
+ end
9
+
10
+ context "when initialized" do
11
+ it "should remember the request provided" do
12
+ expect(@iut.request).to eq(@request)
13
+ end
14
+
15
+ it "should raise an ArgumentError if request is not a Net::HTTPRequest" do
16
+ expect{
17
+ Smaak::NetHttpAdaptor.new("notarequest")
18
+ }.to raise_error ArgumentError, "Must provide a Net::HTTPRequest"
19
+ expect{
20
+ Smaak::NetHttpAdaptor.new(nil)
21
+ }.to raise_error ArgumentError, "Must provide a Net::HTTPRequest"
22
+ end
23
+ end
24
+
25
+ context "when asked to set a header" do
26
+ it "should raise an ArgumentError if the header is not a non-blank string" do
27
+ expect {
28
+ @iut.set_header("", "value")
29
+ }.to raise_error ArgumentError, "Header must be a non-blank string"
30
+ expect {
31
+ @iut.set_header(" ", "value")
32
+ }.to raise_error ArgumentError, "Header must be a non-blank string"
33
+ end
34
+
35
+ it "should raise and ArgumentError if the header is not a string" do
36
+ expect {
37
+ @iut.set_header({:a => 'A'}, "value")
38
+ }.to raise_error ArgumentError, "Header must be a non-blank string"
39
+ end
40
+
41
+ it "should set the header on the request" do
42
+ @iut.set_header("one", "1")
43
+ @iut.set_header("two", "2")
44
+ expect(@iut.request["one"]).to eq("1")
45
+ expect(@iut.request["two"]).to eq("2")
46
+ end
47
+ end
48
+
49
+ context "when asked to iterate all headers" do
50
+ it "should iterate each header if there are headers set" do
51
+ @iut.set_header("one", "1")
52
+ @iut.set_header("two", "2")
53
+ count = 0
54
+ one = false
55
+ two = false
56
+ @iut.each_header do |header, value|
57
+ count = count + 1
58
+ one = true if header == "one" and value == "1"
59
+ two = true if header == "two" and value == "2"
60
+ end
61
+ expect(count).to eq(5) #some default headers come with a vanilla request
62
+ expect(one).to eq(true)
63
+ expect(two).to eq(true)
64
+ end
65
+
66
+ it "should not iterate any headers if there are no headers set" do
67
+ count = 0
68
+ @iut.each_header do |header, value|
69
+ count = count + 1
70
+ end
71
+ expect(count).to eq(3) #some default headers come with a vanilla request
72
+ end
73
+ end
74
+
75
+ context "when asked for request attributes" do
76
+ it "should provide an accessor to the host" do
77
+ expect(@iut.host).to eq("rubygems.org")
78
+ end
79
+
80
+ it "should provide an accessor to the path" do
81
+ expect(@iut.path).to eq("/gems/smaak")
82
+ end
83
+
84
+ it "should provide an accessor to the method" do
85
+ expect(@iut.method).to eq("POST")
86
+ end
87
+
88
+ it "should provide an accessor to the body" do
89
+ expect(@iut.body).to eq("body-data")
90
+ end
91
+ end
92
+
93
+ context "when setting the body" do
94
+ it "should set the request's body attribute" do
95
+ @iut.body = "new body"
96
+ expect(@iut.body).to eq("new body")
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,83 @@
1
+ require './spec/spec_helper.rb'
2
+
3
+ describe Smaak::RackAdaptor do
4
+ before :each do
5
+ @env = {"CONTENT_LENGTH" => "25", "REQUEST_METHOD" => "POST", "PATH_INFO" => "/gems/smaak", "HTTP_X_SMAAK_ENCRYPT" => "false"}
6
+ @request = Rack::Request.new(@env)
7
+
8
+ @iut = Smaak::RackAdaptor.new(@request)
9
+ end
10
+
11
+ context "when initialized" do
12
+ it "should remember the request provided" do
13
+ expect(@iut.request).to eq(@request)
14
+ end
15
+
16
+ it "should raise an ArgumentError if request is not a Rack::Request" do
17
+ expect{
18
+ Smaak::NetHttpAdaptor.new("notarequest")
19
+ }.to raise_error ArgumentError, "Must provide a Net::HTTPRequest"
20
+ expect{
21
+ Smaak::NetHttpAdaptor.new(nil)
22
+ }.to raise_error ArgumentError, "Must provide a Net::HTTPRequest"
23
+ end
24
+ end
25
+
26
+ context "when asked for a header" do
27
+ it "should return an ArgumentError if the header is blank" do
28
+ expect {
29
+ @iut.header("")
30
+ }.to raise_error ArgumentError, "Header must be a non-blank string"
31
+ expect {
32
+ @iut.header(" ")
33
+ }.to raise_error ArgumentError, "Header must be a non-blank string"
34
+ end
35
+
36
+ it "should raise and ArgumentError if the header is not a string" do
37
+ expect {
38
+ @iut.header({:a => 'A'})
39
+ }.to raise_error ArgumentError, "Header must be a non-blank string"
40
+ end
41
+
42
+ it "return the value of the requested header" do
43
+ expect(@iut.header("content-length")).to eq("25")
44
+ expect(@iut.header("x-smaak-encrypt")).to eq("false")
45
+ end
46
+
47
+ it "should translate the header from the Rack HTTP_header index in the Rack upcase" do
48
+ expect(@iut.header("x-smaak-encrypt")).to eq("false")
49
+ end
50
+
51
+ it "should translate the header from the Rack underscore index" do
52
+ expect(@iut.header("x-smaak-encrypt")).to eq("false")
53
+ end
54
+
55
+ it "should ignore header case" do
56
+ expect(@iut.header("x-SMAAK-encrypt")).to eq("false")
57
+ end
58
+
59
+ it "should understand that content-length does not have HTTP_ prepended in the rack env" do
60
+ expect(@iut.header("content-length")).to eq("25")
61
+ end
62
+
63
+ it "should understand that request-method does not have HTTP_ prepended in the rack env" do
64
+ expect(@iut.header("request-method")).to eq("POST")
65
+ end
66
+ end
67
+
68
+ context "when asked for request attributes" do
69
+ it "should provide an accessor to the path" do
70
+ expect(@iut.path).to eq("/gems/smaak")
71
+ end
72
+
73
+ it "should provide an accessor to the method" do
74
+ expect(@iut.method).to eq("POST")
75
+ end
76
+ end
77
+
78
+ context "when asked for the body" do
79
+ it "should return the request body" do
80
+ @request.body
81
+ end
82
+ end
83
+ end