bosh-core 1.5.0.pre.1113

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -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
data/bosh-core.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ version = File.read(File.expand_path('../../BOSH_VERSION', __FILE__)).strip
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = 'bosh-core'
6
+ spec.version = version
7
+ spec.authors = 'Pivotal'
8
+ spec.email = 'support@cloudfoundry.com'
9
+ spec.description = 'Bosh::Core provides things BOSH needs to exist'
10
+ spec.summary = 'Bosh::Core provides things BOSH needs to exist'
11
+ spec.homepage = 'https://github.com/cloudfoundry/bosh'
12
+ spec.license = 'Apache 2.0'
13
+
14
+ spec.required_ruby_version = Gem::Requirement.new('>= 1.9.3')
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 = %w[lib]
20
+
21
+ spec.add_dependency 'gibberish', '~>1.2.0'
22
+ spec.add_dependency 'yajl-ruby', '~>1.1.0'
23
+
24
+ spec.add_development_dependency 'rake'
25
+ spec.add_development_dependency 'rspec'
26
+ spec.add_development_dependency 'rspec-fire'
27
+ end
@@ -0,0 +1,149 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ require 'securerandom'
4
+ require 'gibberish'
5
+ require 'securerandom'
6
+ require 'yajl'
7
+
8
+ module Bosh::Core
9
+ # Utility class for decrypting/encrypting Director/Agent message exchanges
10
+ class EncryptionHandler
11
+ class CryptError < StandardError
12
+ end
13
+
14
+ class SessionError < CryptError
15
+ end
16
+
17
+ class SequenceNumberError < CryptError
18
+ end
19
+
20
+ class SignatureError < CryptError
21
+ end
22
+
23
+ class DecryptionError < CryptError
24
+ end
25
+
26
+ attr_reader :session_id
27
+
28
+ def initialize(id, credentials)
29
+ @id = id
30
+ crypt_key = credentials['crypt_key']
31
+ @cipher = Gibberish::AES.new(crypt_key)
32
+ @sign_key = credentials['sign_key']
33
+ @session_id = nil
34
+ @session_sequence_number = 0
35
+
36
+ initiate_sequence_number
37
+ end
38
+
39
+ def initiate_sequence_number
40
+ @sequence_number = Time.now.to_i + SecureRandom.random_number(1 << 32)
41
+ end
42
+
43
+ def encrypt(data)
44
+ raise ArgumentError unless data.is_a?(Hash)
45
+
46
+ start_session unless @session_id
47
+
48
+ encapsulated_data = data.dup
49
+ # Add encrytpion related metadata before signing and encrypting
50
+ @sequence_number += 1
51
+ encapsulated_data['sequence_number'] = @sequence_number
52
+ encapsulated_data['client_id'] = @id
53
+ encapsulated_data['session_id'] = @session_id
54
+
55
+ signed_data = sign(encapsulated_data)
56
+ encrypted_data = @cipher.encrypt(encode(signed_data))
57
+ encrypted_data
58
+ end
59
+
60
+ def sign(encapsulated_data)
61
+ data_json = encode(encapsulated_data)
62
+ hmac = signature(data_json)
63
+ signed_data = { 'hmac' => hmac, 'json_data' => data_json }
64
+ signed_data
65
+ end
66
+
67
+ def decrypt(encrypted_data)
68
+ begin
69
+ decrypted_data = @cipher.decrypt(encrypted_data)
70
+ # rubocop:disable RescueException
71
+ rescue Exception => e
72
+ # rubocop:enable RescueException
73
+
74
+ raise DecryptionError, e.inspect
75
+ end
76
+
77
+ data = Yajl::Parser.new.parse(decrypted_data)
78
+
79
+ verify_signature(data)
80
+ decoded_data = decode(data['json_data'])
81
+ verify_session(decoded_data)
82
+ decoded_data
83
+ end
84
+
85
+ def start_session
86
+ @session_id = SecureRandom.uuid
87
+ end
88
+
89
+ def verify_signature(data)
90
+ hmac = data['hmac']
91
+ json_data = data['json_data']
92
+
93
+ json_hmac = signature(json_data)
94
+ unless constant_time_comparison(hmac, json_hmac)
95
+ raise SignatureError, "Expected hmac (#{hmac}), got (#{json_hmac})"
96
+ end
97
+ end
98
+
99
+ # constant time comparison snagged from activesupport
100
+ def constant_time_comparison(a, b)
101
+ return false unless a.bytesize == b.bytesize
102
+ l = a.unpack "C#{a.bytesize}"
103
+ res = 0
104
+ b.each_byte { |byte| res |= byte ^ l.shift }
105
+ res == 0
106
+ end
107
+
108
+ def verify_session(decrypted_data)
109
+ # If you are the receiver of a session - use session_id from payload
110
+ if @session_id.nil?
111
+ if !decrypted_data['session_id'].nil?
112
+ @session_id = decrypted_data['session_id']
113
+ else
114
+ raise SessionError, 'no session_id'
115
+ end
116
+ end
117
+
118
+ unless decrypted_data['session_id'] == @session_id
119
+ raise SessionError, 'session_id mismatch'
120
+ end
121
+
122
+ sender_sequence_number = decrypted_data['sequence_number'].to_i
123
+ if sender_sequence_number > @session_sequence_number
124
+ @session_sequence_number = sender_sequence_number
125
+ else
126
+ raise SequenceNumberError, 'invalid sequence number'
127
+ end
128
+ end
129
+
130
+ def signature(sign_data)
131
+ Gibberish.HMAC(@sign_key, sign_data, { digest: :sha256 })
132
+ end
133
+
134
+ def encode(data)
135
+ Yajl::Encoder.encode(data)
136
+ end
137
+
138
+ def decode(json)
139
+ Yajl::Parser.new.parse(json)
140
+ end
141
+
142
+ def self.generate_credentials
143
+ %w(crypt_key sign_key).inject({}) do |credentials, key|
144
+ credentials[key] = SecureRandom.base64(48)
145
+ credentials
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,54 @@
1
+ module Bosh::Core
2
+ class Shell
3
+ def initialize(stdout = $stdout)
4
+ @stdout = stdout
5
+ end
6
+
7
+ def run(command, options = {})
8
+ output_lines = run_command(command, options)
9
+ output_lines = tail(output_lines, options)
10
+
11
+ command_output = output_lines.join("\n")
12
+ report(command, command_output, options)
13
+ command_output
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :stdout
19
+
20
+ def run_command(command, options)
21
+ stdout.puts command if options[:output_command]
22
+ lines = []
23
+
24
+ IO.popen(command).each do |line|
25
+ stdout.puts line.chomp
26
+ lines << line.chomp
27
+ end.close
28
+
29
+ lines
30
+ end
31
+
32
+ def tail(lines, options)
33
+ line_number = options[:last_number]
34
+ line_number ? lines.last(line_number) : lines
35
+ end
36
+
37
+ def report(cmd, command_output, options)
38
+ return if command_exited_successfully?
39
+
40
+ err_msg = "Failed: '#{cmd}' from #{pwd}, with exit status #{$?.to_i}\n\n #{command_output}"
41
+ options[:ignore_failures] ? stdout.puts("#{err_msg}, continuing anyway") : raise(err_msg)
42
+ end
43
+
44
+ def command_exited_successfully?
45
+ $?.success?
46
+ end
47
+
48
+ def pwd
49
+ Dir.pwd
50
+ rescue Errno::ENOENT
51
+ 'a deleted directory'
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,5 @@
1
+ module Bosh
2
+ module Core
3
+ VERSION = '1.5.0.pre.1113'
4
+ end
5
+ end
data/lib/bosh/core.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Bosh
2
+ module Core
3
+ end
4
+ end
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+ require 'bosh/core/shell'
3
+
4
+ module Bosh::Core
5
+ describe Shell do
6
+ let(:stdout) { StringIO.new }
7
+
8
+ subject do
9
+ Shell.new(stdout)
10
+ end
11
+
12
+ describe '#run' do
13
+ it 'shells out, prints and returns the output of the command' do
14
+ expect(subject.run('echo hello; echo world')).to eq("hello\nworld")
15
+ expect(stdout.string).to eq("hello\nworld\n")
16
+ end
17
+
18
+ context 'when "output_command" is specified' do
19
+ it 'outputs the command' do
20
+ cmd = 'echo 1;echo 2;echo 3;echo 4;echo 5'
21
+ subject.run(cmd, output_command: true)
22
+
23
+ expect(stdout.string).to include('echo 1;echo 2;echo 3;echo 4;echo 5')
24
+ end
25
+ end
26
+
27
+ context 'when "last_number" is specified' do
28
+ it 'tails "last_number" lines of output' do
29
+ cmd = 'echo 1;echo 2;echo 3;echo 4;echo 5'
30
+ expect(subject.run(cmd, last_number: 3)).to eq("3\n4\n5")
31
+ expect(stdout.string).to eq("1\n2\n3\n4\n5\n")
32
+ end
33
+
34
+ it 'outputs the entire output if more lines are requested than generated' do
35
+ cmd = 'echo 1;echo 2;echo 3;echo 4;echo 5'
36
+ expect(subject.run(cmd, last_number: 6)).to eq("1\n2\n3\n4\n5")
37
+ expect(stdout.string).to eq("1\n2\n3\n4\n5\n")
38
+ end
39
+ end
40
+
41
+ context 'when the command fails' do
42
+ it 'raises an error' do
43
+ expect {
44
+ subject.run('false')
45
+ }.to raise_error /Failed: 'false' from /
46
+ end
47
+
48
+ context 'because the working directory has gone missing' do
49
+ it 'fails gracefully with a slightly helpful error message' do
50
+ Dir.stub(:pwd).and_raise(Errno::ENOENT, 'No such file or directory - getcwd')
51
+ expect {
52
+ subject.run('false')
53
+ }.to raise_error /from a deleted directory/
54
+ end
55
+ end
56
+
57
+ context 'and ignoring failures' do
58
+ it 'raises an error' do
59
+ subject.run('false', ignore_failures: true)
60
+ expect(stdout.string).to match(/continuing anyway/)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+ require 'bosh/core/version'
3
+
4
+ module Bosh::Core
5
+ describe VERSION do
6
+ let(:bosh_version_file) do
7
+ File.expand_path('../../../../BOSH_VERSION', File.dirname(__FILE__))
8
+ end
9
+
10
+ it { should eq(File.read(bosh_version_file).strip) }
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+ require 'bosh/core'
3
+
4
+ describe Bosh::Core do
5
+ it { should be_a(Module) }
6
+ end
@@ -0,0 +1,135 @@
1
+ require 'spec_helper'
2
+ require 'bosh/core/encryption_handler'
3
+
4
+ module Bosh::Core
5
+ describe EncryptionHandler do
6
+ before(:each) do
7
+ @credentials = EncryptionHandler.generate_credentials
8
+ @cipher = Gibberish::AES.new(@credentials['crypt_key'])
9
+ @sign_key = @credentials['sign_key']
10
+ end
11
+
12
+ it 'should encrypt data' do
13
+ handler = EncryptionHandler.new('client_id', @credentials)
14
+ encrypted_data = handler.encrypt('hubba' => 'bubba')
15
+
16
+ # double decode is not an error - data need to be serialized before it is
17
+ # signed and then serialized again to be encrypted
18
+ decrypted_data = handler.decode(handler.decode(@cipher.decrypt(encrypted_data))['json_data'])
19
+ decrypted_data['hubba'].should eq 'bubba'
20
+ end
21
+
22
+ it 'should be signed' do
23
+ handler = EncryptionHandler.new('client_id', @credentials)
24
+ encrypted_data = handler.encrypt('hubba' => 'bubba')
25
+
26
+ decrypted_data = handler.decode(@cipher.decrypt(encrypted_data))
27
+ signature = decrypted_data['hmac']
28
+ json_data = decrypted_data['json_data']
29
+
30
+ signature.should eq Gibberish.HMAC(@sign_key, json_data, { digest: :sha256 })
31
+ end
32
+
33
+ it 'should decrypt' do
34
+ handler = EncryptionHandler.new('client_id', @credentials)
35
+
36
+ encrypted_data = handler.encrypt('hubba' => 'bubba')
37
+ handler.decrypt(encrypted_data)['hubba'].should eq 'bubba'
38
+ end
39
+
40
+ it 'should verify signature' do
41
+ handler = EncryptionHandler.new('client_id', @credentials)
42
+
43
+ encrypted_data = handler.encrypt('hubba' => 'bubba')
44
+
45
+ # build bad data
46
+ manipulated_data = handler.decode(@cipher.decrypt(encrypted_data))
47
+ manipulated_data['hmac'] = 'foo'
48
+ encrypted_manipulated_data = @cipher.encrypt(handler.encode(manipulated_data))
49
+
50
+ lambda {
51
+ handler.decrypt(encrypted_manipulated_data)
52
+ }.should raise_error(EncryptionHandler::SignatureError,
53
+ /Expected hmac \(foo\)/)
54
+ end
55
+
56
+ it 'should verify session' do
57
+ handler = EncryptionHandler.new('client_id', @credentials)
58
+
59
+ encrypted_data = handler.encrypt('knife' => 'fork')
60
+
61
+ # build bad data
62
+ decrypted_data = handler.decode(@cipher.decrypt(encrypted_data))
63
+
64
+ bad_data = handler.decode(decrypted_data['json_data'])
65
+ bad_data['session_id'] = 'bad_session_data'
66
+
67
+ bad_json_data = handler.encode(bad_data)
68
+
69
+ manipulated_data = {
70
+ 'hmac' => handler.signature(bad_json_data),
71
+ 'json_data' => bad_json_data
72
+ }
73
+
74
+ encrypted_manipulated_data = @cipher.encrypt(handler.encode(manipulated_data))
75
+
76
+ lambda {
77
+ handler.decrypt(encrypted_manipulated_data)
78
+ }.should raise_error(EncryptionHandler::SessionError)
79
+ end
80
+
81
+ it 'should decrypt for multiple messages' do
82
+ h1 = EncryptionHandler.new('client_id', @credentials)
83
+ h2 = EncryptionHandler.new('client_id', @credentials)
84
+ encrypted_data1 = h1.encrypt('hubba' => 'bubba')
85
+ encrypted_data2 = h1.encrypt('bubba' => 'hubba')
86
+
87
+ h2.decrypt(encrypted_data1)['hubba'].should eq 'bubba'
88
+ h2.decrypt(encrypted_data2)['bubba'].should eq 'hubba'
89
+ end
90
+
91
+ it 'should exchange messages' do
92
+ h1 = EncryptionHandler.new('client_id', @credentials)
93
+ h2 = EncryptionHandler.new('client_id', @credentials)
94
+
95
+ encrypted_data1 = h1.encrypt('hubba' => 'bubba')
96
+ h2.decrypt(encrypted_data1)['hubba'].should eq 'bubba'
97
+
98
+ encrypted_data2 = h2.encrypt('kermit' => 'frog')
99
+ h1.decrypt(encrypted_data2)['kermit'].should eq 'frog'
100
+
101
+ encrypted_data3 = h1.encrypt('frank' => 'zappa')
102
+ h2.decrypt(encrypted_data3)['frank'].should eq 'zappa'
103
+ end
104
+
105
+ it 'should fail when sequence number is out of order' do
106
+ handler = EncryptionHandler.new('client_id', @credentials)
107
+ encrypted_data1 = handler.encrypt('foo' => 'bar')
108
+ encrypted_data2 = handler.encrypt('baz' => 'bus')
109
+
110
+ handler.decrypt(encrypted_data2)
111
+
112
+ lambda {
113
+ handler.decrypt(encrypted_data1)
114
+ }.should raise_error(EncryptionHandler::SequenceNumberError)
115
+ end
116
+
117
+ it 'should handle garbage encrypt args' do
118
+ handler = EncryptionHandler.new('client_id', @credentials)
119
+ lambda {
120
+ handler.encrypt('bleh')
121
+ }.should raise_error(ArgumentError)
122
+ end
123
+
124
+ it 'should handle garbage decrypt args' do
125
+ handler = EncryptionHandler.new('client_id', @credentials)
126
+ lambda {
127
+ handler.decrypt('f')
128
+ }.should raise_error(EncryptionHandler::DecryptionError, /TypeError/)
129
+
130
+ lambda {
131
+ handler.decrypt('fddddddddddddddddddddddddddddddddddddddddddddddddd')
132
+ }.should raise_error(EncryptionHandler::DecryptionError, /CipherError/)
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,5 @@
1
+ require 'rspec'
2
+
3
+ Dir.glob(File.expand_path('support/**/*.rb', File.dirname(__FILE__))).each do |support|
4
+ require support
5
+ end
metadata ADDED
@@ -0,0 +1,142 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bosh-core
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.5.0.pre.1113
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - Pivotal
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-10-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: gibberish
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.2.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: 1.2.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: yajl-ruby
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 1.1.0
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 1.1.0
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
+ - !ruby/object:Gem::Dependency
79
+ name: rspec-fire
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: Bosh::Core provides things BOSH needs to exist
95
+ email: support@cloudfoundry.com
96
+ executables: []
97
+ extensions: []
98
+ extra_rdoc_files: []
99
+ files:
100
+ - .gitignore
101
+ - .rspec
102
+ - bosh-core.gemspec
103
+ - lib/bosh/core.rb
104
+ - lib/bosh/core/encryption_handler.rb
105
+ - lib/bosh/core/shell.rb
106
+ - lib/bosh/core/version.rb
107
+ - spec/bosh/core/shell_spec.rb
108
+ - spec/bosh/core/version_spec.rb
109
+ - spec/bosh/core_spec.rb
110
+ - spec/bosh/encryption_handler_spec.rb
111
+ - spec/spec_helper.rb
112
+ homepage: https://github.com/cloudfoundry/bosh
113
+ licenses:
114
+ - Apache 2.0
115
+ post_install_message:
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ none: false
121
+ requirements:
122
+ - - ! '>='
123
+ - !ruby/object:Gem::Version
124
+ version: 1.9.3
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ! '>'
129
+ - !ruby/object:Gem::Version
130
+ version: 1.3.1
131
+ requirements: []
132
+ rubyforge_project:
133
+ rubygems_version: 1.8.23
134
+ signing_key:
135
+ specification_version: 3
136
+ summary: Bosh::Core provides things BOSH needs to exist
137
+ test_files:
138
+ - spec/bosh/core/shell_spec.rb
139
+ - spec/bosh/core/version_spec.rb
140
+ - spec/bosh/core_spec.rb
141
+ - spec/bosh/encryption_handler_spec.rb
142
+ - spec/spec_helper.rb