trocla-ruby2 0.4.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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.document +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +10 -0
  5. data/CHANGELOG.md +71 -0
  6. data/Gemfile +51 -0
  7. data/LICENSE.txt +15 -0
  8. data/README.md +351 -0
  9. data/Rakefile +53 -0
  10. data/bin/trocla +148 -0
  11. data/ext/redhat/rubygem-trocla.spec +120 -0
  12. data/lib/VERSION +4 -0
  13. data/lib/trocla.rb +162 -0
  14. data/lib/trocla/default_config.yaml +47 -0
  15. data/lib/trocla/encryptions.rb +54 -0
  16. data/lib/trocla/encryptions/none.rb +10 -0
  17. data/lib/trocla/encryptions/ssl.rb +51 -0
  18. data/lib/trocla/formats.rb +54 -0
  19. data/lib/trocla/formats/bcrypt.rb +7 -0
  20. data/lib/trocla/formats/md5crypt.rb +6 -0
  21. data/lib/trocla/formats/mysql.rb +6 -0
  22. data/lib/trocla/formats/pgsql.rb +7 -0
  23. data/lib/trocla/formats/plain.rb +7 -0
  24. data/lib/trocla/formats/sha1.rb +7 -0
  25. data/lib/trocla/formats/sha256crypt.rb +6 -0
  26. data/lib/trocla/formats/sha512crypt.rb +6 -0
  27. data/lib/trocla/formats/ssha.rb +9 -0
  28. data/lib/trocla/formats/sshkey.rb +46 -0
  29. data/lib/trocla/formats/x509.rb +197 -0
  30. data/lib/trocla/store.rb +80 -0
  31. data/lib/trocla/stores.rb +39 -0
  32. data/lib/trocla/stores/memory.rb +56 -0
  33. data/lib/trocla/stores/moneta.rb +58 -0
  34. data/lib/trocla/util.rb +71 -0
  35. data/lib/trocla/version.rb +22 -0
  36. data/spec/data/.keep +0 -0
  37. data/spec/spec_helper.rb +290 -0
  38. data/spec/trocla/encryptions/none_spec.rb +22 -0
  39. data/spec/trocla/encryptions/ssl_spec.rb +26 -0
  40. data/spec/trocla/formats/x509_spec.rb +375 -0
  41. data/spec/trocla/store/memory_spec.rb +6 -0
  42. data/spec/trocla/store/moneta_spec.rb +6 -0
  43. data/spec/trocla/util_spec.rb +54 -0
  44. data/spec/trocla_spec.rb +248 -0
  45. data/trocla-ruby2.gemspec +104 -0
  46. metadata +202 -0
@@ -0,0 +1,47 @@
1
+ ---
2
+ store: :moneta
3
+ store_options:
4
+ adapter: :YAML
5
+ adapter_options:
6
+ :file: '/tmp/trocla.yaml'
7
+
8
+ encryption: :none
9
+ options:
10
+ random: true
11
+ length: 16
12
+ charset: default
13
+
14
+ profiles:
15
+ rootpw:
16
+ charset: consolesafe
17
+ length: 32
18
+ mysql:
19
+ charset: shellsafe
20
+ length: 32
21
+ login:
22
+ charset: consolesafe
23
+ length: 16
24
+ x509veryverylong:
25
+ # 15 years
26
+ days: 5475
27
+ # 5475 days
28
+ expires: 466560000
29
+ x509verylong:
30
+ # 10 years
31
+ days: 3650
32
+ # 3600 days
33
+ expires: 311040000
34
+ x509long:
35
+ # 5 years
36
+ days: 1825
37
+ # 1800 days
38
+ expires: 155520000
39
+ x509auto:
40
+ days: 40
41
+ # 30 days
42
+ expires: 2592000
43
+ x509short:
44
+ days: 2
45
+ # 1 day
46
+ expires: 86400
47
+
@@ -0,0 +1,54 @@
1
+ class Trocla::Encryptions
2
+
3
+ class Base
4
+ attr_reader :trocla, :config
5
+ def initialize(config, trocla)
6
+ @trocla = trocla
7
+ @config = config
8
+ end
9
+
10
+ def encrypt(value)
11
+ raise NoMethodError.new("#{self.class.name} needs to implement 'encrypt()'")
12
+ end
13
+
14
+ def decrypt(value)
15
+ raise NoMethodError.new("#{self.class.name} needs to implement 'decrypt()'")
16
+ end
17
+ end
18
+
19
+ class << self
20
+ def [](enc)
21
+ encryptions[enc.to_s.downcase]
22
+ end
23
+
24
+ def all
25
+ Dir[ path '*' ].collect do |enc|
26
+ File.basename(enc, '.rb').downcase
27
+ end
28
+ end
29
+
30
+ def available?(encryption)
31
+ all.include?(encryption.to_s.downcase)
32
+ end
33
+
34
+ private
35
+ def encryptions
36
+ @@encryptions ||= Hash.new do |hash, encryption|
37
+ encryption = encryption.to_s.downcase
38
+ if File.exists?( path encryption )
39
+ require "trocla/encryptions/#{encryption}"
40
+ class_name = "Trocla::Encryptions::#{encryption.capitalize}"
41
+ hash[encryption] = (eval class_name)
42
+ else
43
+ raise "Encryption #{encryption} is not supported!"
44
+ end
45
+ end
46
+ end
47
+
48
+ def path(encryption)
49
+ File.expand_path(
50
+ File.join(File.dirname(__FILE__), 'encryptions', "#{encryption}.rb")
51
+ )
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,10 @@
1
+ class Trocla::Encryptions::None < Trocla::Encryptions::Base
2
+ def encrypt(value)
3
+ value
4
+ end
5
+
6
+ def decrypt(value)
7
+ value
8
+ end
9
+ end
10
+
@@ -0,0 +1,51 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ class Trocla::Encryptions::Ssl < Trocla::Encryptions::Base
5
+ def encrypt(value)
6
+ ciphertext = ''
7
+ value.scan(/.{0,#{chunksize}}/m).each do |chunk|
8
+ ciphertext += Base64.encode64(public_key.public_encrypt(chunk)).gsub("\n",'')+"\n" if chunk
9
+ end
10
+ ciphertext
11
+ end
12
+
13
+ def decrypt(value)
14
+ plaintext = ''
15
+ value.split(/\n/).each do |line|
16
+ plaintext += private_key.private_decrypt(Base64.decode64(line)) if line
17
+ end
18
+ plaintext
19
+ end
20
+
21
+ private
22
+
23
+ def chunksize
24
+ public_key.n.num_bytes - 11
25
+ end
26
+
27
+ def private_key
28
+ @private_key ||= begin
29
+ file = require_option(:private_key)
30
+ OpenSSL::PKey::RSA.new(File.read(file), nil)
31
+ end
32
+ end
33
+
34
+ def public_key
35
+ @public_key ||= begin
36
+ file = require_option(:public_key)
37
+ OpenSSL::PKey::RSA.new(File.read(file), nil)
38
+ end
39
+ end
40
+
41
+ def option(key)
42
+ config[key]
43
+ end
44
+
45
+ def require_option(key)
46
+ val = option(key)
47
+ raise "Config error: 'ssl_options' => :#{key} is not defined" if val.nil?
48
+ val
49
+ end
50
+ end
51
+
@@ -0,0 +1,54 @@
1
+ class Trocla::Formats
2
+
3
+ class Base
4
+ attr_reader :trocla
5
+ def initialize(trocla)
6
+ @trocla = trocla
7
+ end
8
+ def render(output,render_options={})
9
+ output
10
+ end
11
+ def expensive?
12
+ self.class.expensive?
13
+ end
14
+ class << self
15
+ def expensive(is_expensive)
16
+ @expensive = is_expensive
17
+ end
18
+ def expensive?
19
+ @expensive == true
20
+ end
21
+ end
22
+ end
23
+
24
+ class << self
25
+ def [](format)
26
+ formats[format.downcase]
27
+ end
28
+
29
+ def all
30
+ Dir[File.expand_path(File.join(File.dirname(__FILE__),'formats','*.rb'))].collect{|f| File.basename(f,'.rb').downcase }
31
+ end
32
+
33
+ def available?(format)
34
+ all.include?(format.downcase)
35
+ end
36
+
37
+ private
38
+ def formats
39
+ @@formats ||= Hash.new do |hash, format|
40
+ format = format.downcase
41
+ if File.exists?(path(format))
42
+ require "trocla/formats/#{format}"
43
+ hash[format] = (eval "Trocla::Formats::#{format.capitalize}")
44
+ else
45
+ raise "Format #{format} is not supported!"
46
+ end
47
+ end
48
+ end
49
+
50
+ def path(format)
51
+ File.expand_path(File.join(File.dirname(__FILE__),'formats',"#{format}.rb"))
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,7 @@
1
+ class Trocla::Formats::Bcrypt < Trocla::Formats::Base
2
+ expensive true
3
+ require 'bcrypt'
4
+ def format(plain_password,options={})
5
+ BCrypt::Password.create(plain_password, :cost => options['cost']||BCrypt::Engine.cost).to_s
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ # salted crypt
2
+ class Trocla::Formats::Md5crypt < Trocla::Formats::Base
3
+ def format(plain_password,options={})
4
+ plain_password.crypt('$1$' << Trocla::Util.salt << '$')
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class Trocla::Formats::Mysql < Trocla::Formats::Base
2
+ require 'digest/sha1'
3
+ def format(plain_password,options={})
4
+ "*" + Digest::SHA1.hexdigest(Digest::SHA1.digest(plain_password)).upcase
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ class Trocla::Formats::Pgsql < Trocla::Formats::Base
2
+ require 'digest/md5'
3
+ def format(plain_password,options={})
4
+ raise "You need pass the username as an option to use this format" unless options['username']
5
+ "md5" + Digest::MD5.hexdigest(plain_password + options['username'])
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ class Trocla::Formats::Plain < Trocla::Formats::Base
2
+
3
+ def format(plain_password,options={})
4
+ plain_password
5
+ end
6
+
7
+ end
@@ -0,0 +1,7 @@
1
+ class Trocla::Formats::Sha1 < Trocla::Formats::Base
2
+ require 'digest/sha1'
3
+ require 'base64'
4
+ def format(plain_password,options={})
5
+ '{SHA}' + Base64.encode64(Digest::SHA1.digest(plain_password))
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ # salted crypt
2
+ class Trocla::Formats::Sha256crypt < Trocla::Formats::Base
3
+ def format(plain_password,options={})
4
+ plain_password.crypt('$5$' << Trocla::Util.salt << '$')
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # salted crypt
2
+ class Trocla::Formats::Sha512crypt < Trocla::Formats::Base
3
+ def format(plain_password,options={})
4
+ plain_password.crypt('$6$' << Trocla::Util.salt << '$')
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ # salted crypt
2
+ require 'base64'
3
+ require 'digest'
4
+ class Trocla::Formats::Ssha < Trocla::Formats::Base
5
+ def format(plain_password,options={})
6
+ salt = options['salt'] || Trocla::Util.salt(16)
7
+ "{SSHA}"+Base64.encode64("#{Digest::SHA1.digest("#{plain_password}#{salt}")}#{salt}").chomp
8
+ end
9
+ end
@@ -0,0 +1,46 @@
1
+ require 'sshkey'
2
+
3
+ class Trocla::Formats::Sshkey < Trocla::Formats::Base
4
+
5
+ expensive true
6
+
7
+ def format(plain_password,options={})
8
+
9
+ if plain_password.match(/-----BEGIN RSA PRIVATE KEY-----.*-----END RSA PRIVATE KEY/m)
10
+ # Import, validate ssh key
11
+ begin
12
+ sshkey = ::SSHKey.new(plain_password)
13
+ rescue Exception => e
14
+ raise "SSH key import failed: #{e.message}"
15
+ end
16
+ return sshkey.private_key + sshkey.ssh_public_key
17
+ end
18
+
19
+ type = options['type'] || 'rsa'
20
+ bits = options['bits'] || 2048
21
+
22
+ begin
23
+ sshkey = ::SSHKey.generate(
24
+ type: type,
25
+ bits: bits,
26
+ comment: options['comment'],
27
+ passphrase: options['passphrase']
28
+ )
29
+ rescue Exception => e
30
+ raise "SSH key creation failed: #{e.message}"
31
+ end
32
+
33
+ sshkey.private_key + sshkey.ssh_public_key
34
+ end
35
+
36
+ def render(output,render_options={})
37
+ if render_options['privonly']
38
+ ::SSHKey.new(output).private_key
39
+ elsif render_options['pubonly']
40
+ ::SSHKey.new(output).ssh_public_key
41
+ else
42
+ super(output,render_options)
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,197 @@
1
+ require 'openssl'
2
+ class Trocla::Formats::X509 < Trocla::Formats::Base
3
+ expensive true
4
+ def format(plain_password,options={})
5
+
6
+ if plain_password.match(/-----BEGIN RSA PRIVATE KEY-----.*-----END RSA PRIVATE KEY-----.*-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----/m)
7
+ # just an import, don't generate any new keys
8
+ return plain_password
9
+ end
10
+
11
+ cn = nil
12
+ if options['subject']
13
+ subject = options['subject']
14
+ if cna = OpenSSL::X509::Name.parse(subject).to_a.find{|e| e[0] == 'CN' }
15
+ cn = cna[1]
16
+ end
17
+ elsif options['CN']
18
+ subject = ''
19
+ cn = options['CN']
20
+ ['C','ST','L','O','OU','CN','emailAddress'].each do |field|
21
+ subject << "/#{field}=#{options[field]}" if options[field]
22
+ end
23
+ else
24
+ raise "You need to pass \"subject\" or \"CN\" as an option to use this format"
25
+ end
26
+ hash = options['hash'] || 'sha2'
27
+ sign_with = options['ca']
28
+ become_ca = options['become_ca'] || false
29
+ keysize = options['keysize'] || 4096
30
+ days = options['days'].nil? ? 365 : options['days'].to_i
31
+ name_constraints = Array(options['name_constraints'])
32
+ key_usages = options['key_usages']
33
+ key_usages = Array(key_usages) if key_usages
34
+
35
+ altnames = if become_ca || (an = options['altnames']) && Array(an).empty?
36
+ []
37
+ else
38
+ # ensure that we have the CN with us, but only if it
39
+ # it's like a hostname.
40
+ # This might have to be improved.
41
+ if cn.include?(' ')
42
+ Array(an).collect { |v| "DNS:#{v}" }.join(', ')
43
+ else
44
+ (["DNS:#{cn}"] + Array(an).collect { |v| "DNS:#{v}" }).uniq.join(', ')
45
+ end
46
+ end
47
+
48
+ begin
49
+ key = mkkey(keysize)
50
+ rescue Exception => e
51
+ puts e.backtrace
52
+ raise "Private key for #{subject} creation failed: #{e.message}"
53
+ end
54
+
55
+ cert = nil
56
+ if sign_with # certificate signed with CA
57
+ begin
58
+ ca_str = trocla.get_password(sign_with,'x509')
59
+ ca = OpenSSL::X509::Certificate.new(ca_str)
60
+ cakey = OpenSSL::PKey::RSA.new(ca_str)
61
+ caserial = getserial(sign_with)
62
+ rescue Exception => e
63
+ raise "Value of #{sign_with} can't be loaded as CA: #{e.message}"
64
+ end
65
+
66
+ begin
67
+ subj = OpenSSL::X509::Name.parse(subject)
68
+ request = mkreq(subj, key.public_key)
69
+ request.sign(key, signature(hash))
70
+ rescue Exception => e
71
+ raise "Certificate request #{subject} creation failed: #{e.message}"
72
+ end
73
+
74
+ begin
75
+ cert = mkcert(caserial, request.subject, ca, request.public_key, days,
76
+ altnames, key_usages, name_constraints, become_ca)
77
+ cert.sign(cakey, signature(hash))
78
+ addserial(sign_with, caserial)
79
+ rescue Exception => e
80
+ raise "Certificate #{subject} signing failed: #{e.message}"
81
+ end
82
+ else # self-signed certificate
83
+ begin
84
+ subj = OpenSSL::X509::Name.parse(subject)
85
+ cert = mkcert(getserial(subj), subj, nil, key.public_key, days,
86
+ altnames, key_usages, name_constraints, become_ca)
87
+ cert.sign(key, signature(hash))
88
+ rescue Exception => e
89
+ raise "Self-signed certificate #{subject} creation failed: #{e.message}"
90
+ end
91
+ end
92
+ key.to_pem + cert.to_pem
93
+ end
94
+
95
+ def render(output,render_options={})
96
+ if render_options['keyonly']
97
+ OpenSSL::PKey::RSA.new(output).to_pem
98
+ elsif render_options['certonly']
99
+ OpenSSL::X509::Certificate.new(output).to_pem
100
+ else
101
+ super(output,render_options)
102
+ end
103
+ end
104
+
105
+ private
106
+ # nice help: https://gist.github.com/mitfik/1922961
107
+
108
+ def signature(hash = 'sha2')
109
+ if hash == 'sha1'
110
+ OpenSSL::Digest::SHA1.new
111
+ elsif hash == 'sha224'
112
+ OpenSSL::Digest::SHA224.new
113
+ elsif hash == 'sha2' || hash == 'sha256'
114
+ OpenSSL::Digest::SHA256.new
115
+ elsif hash == 'sha384'
116
+ OpenSSL::Digest::SHA384.new
117
+ elsif hash == 'sha512'
118
+ OpenSSL::Digest::SHA512.new
119
+ else
120
+ raise "Unrecognized hash: #{hash}"
121
+ end
122
+ end
123
+
124
+ def mkkey(len)
125
+ OpenSSL::PKey::RSA.generate(len)
126
+ end
127
+
128
+ def mkreq(subject,public_key)
129
+ request = OpenSSL::X509::Request.new
130
+ request.subject = subject
131
+ request.public_key = public_key
132
+
133
+ request
134
+ end
135
+
136
+ def mkcert(serial,subject,issuer,public_key,days,altnames, key_usages = nil, name_constraints = [], become_ca = false)
137
+ cert = OpenSSL::X509::Certificate.new
138
+ issuer = cert if issuer == nil
139
+ cert.subject = subject
140
+ cert.issuer = issuer.subject
141
+ cert.not_before = Time.now
142
+ cert.not_after = Time.now + days * 24 * 60 * 60
143
+ cert.public_key = public_key
144
+ cert.serial = serial
145
+ cert.version = 2
146
+
147
+ ef = OpenSSL::X509::ExtensionFactory.new
148
+ ef.subject_certificate = cert
149
+ ef.issuer_certificate = issuer
150
+ cert.extensions = [ ef.create_extension("subjectKeyIdentifier", "hash") ]
151
+
152
+ if become_ca
153
+ cert.add_extension ef.create_extension("basicConstraints","CA:TRUE", true)
154
+ unless (ku = key_usages || ca_key_usages).empty?
155
+ cert.add_extension ef.create_extension("keyUsage", ku.join(', '), true)
156
+ end
157
+ if name_constraints && !name_constraints.empty?
158
+ cert.add_extension ef.create_extension("nameConstraints","permitted;DNS:#{name_constraints.join(',permitted;DNS:')}",true)
159
+ end
160
+ else
161
+ cert.add_extension ef.create_extension("subjectAltName", altnames, true) unless altnames.empty?
162
+ cert.add_extension ef.create_extension("basicConstraints","CA:FALSE", true)
163
+ unless (ku = key_usages || cert_key_usages).empty?
164
+ cert.add_extension ef.create_extension("keyUsage", ku.join(', '), true)
165
+ end
166
+ end
167
+ cert.add_extension ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")
168
+
169
+ cert
170
+ end
171
+
172
+ def getserial(ca)
173
+ newser = Trocla::Util.random_str(20,'hexadecimal').to_i(16)
174
+ all_serials(ca).include?(newser) ? getserial(ca) : newser
175
+ end
176
+
177
+ def all_serials(ca)
178
+ if allser = trocla.get_password("#{ca}_all_serials",'plain')
179
+ YAML.load(allser)
180
+ else
181
+ []
182
+ end
183
+ end
184
+
185
+ def addserial(ca,serial)
186
+ serials = all_serials(ca) << serial
187
+ trocla.set_password("#{ca}_all_serials",'plain',YAML.dump(serials))
188
+ end
189
+
190
+ def cert_key_usages
191
+ ['nonRepudiation', 'digitalSignature', 'keyEncipherment']
192
+ end
193
+ def ca_key_usages
194
+ ['keyCertSign', 'cRLSign', 'nonRepudiation',
195
+ 'digitalSignature', 'keyEncipherment' ]
196
+ end
197
+ end