smaak 0.0.10 → 0.1.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.
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