thieve 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9de5bc6aa2ca34e66cfc680f9f4f82ff12b32528
4
+ data.tar.gz: fccccdb67a5393b0367feb04f67179e4206e990d
5
+ SHA512:
6
+ metadata.gz: a06e491e0ccf98c1de44e5b8735aca3d8fc877e7420338f1db15fcd57d24aaba8e0f4246e9fe945b2e5f04f8c275c00a26021c9477445d67551dd5a822b4ae6d
7
+ data.tar.gz: 87fbcbe90eb592f8ffa029db2c70e87fedc2ef9f74333588ef65401bbf078591333d57cdc5ca2514c42ca5fbb74635798761e8b966dd8fe4013a4420ce2f341e
data/bin/thieve ADDED
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "io/wait"
4
+ require "optparse"
5
+ require "string"
6
+ require "thieve"
7
+
8
+ class ThieveExit
9
+ GOOD = 0
10
+ INVALID_OPTION = 1
11
+ INVALID_ARGUMENT = 2
12
+ MISSING_ARGUMENT = 3
13
+ EXTRA_ARGUMENTS = 4
14
+ EXCEPTION = 5
15
+ end
16
+
17
+ def parse(args)
18
+ options = Hash.new
19
+ options["export"] = nil
20
+ options["verbose"] = false
21
+
22
+ info = "Searches through provided directories, looking for " \
23
+ "private/public keys and certs. Then extracts, " \
24
+ "fingerprints, and attempts to match keys with certs."
25
+
26
+ parser = OptionParser.new do |opts|
27
+ opts.banner = "Usage: #{File.basename($0)} [OPTIONS] <dir>..."
28
+
29
+ opts.on(
30
+ "-e",
31
+ "--export=DIRECTORY",
32
+ "Export keys to specified directory"
33
+ ) do |directory|
34
+ options["export"] = Pathname.new(directory).expand_path
35
+ end
36
+
37
+ opts.on("-h", "--help", "Display this help message") do
38
+ puts opts
39
+ exit ThieveExit::GOOD
40
+ end
41
+
42
+ opts.on("--nocolor", "Disable colorized output") do
43
+ String.disable_colorization = true
44
+ end
45
+
46
+ opts.on(
47
+ "-v",
48
+ "--verbose",
49
+ "Show backtrace when error occurs"
50
+ ) do
51
+ options["verbose"] = true
52
+ end
53
+
54
+ opts.on("", info.word_wrap)
55
+ end
56
+
57
+ begin
58
+ parser.parse!
59
+ rescue OptionParser::InvalidOption => e
60
+ puts e.message
61
+ puts parser
62
+ exit ThieveExit::INVALID_OPTION
63
+ rescue OptionParser::InvalidArgument => e
64
+ puts e.message
65
+ puts parser
66
+ exit ThieveExit::INVALID_ARGUMENT
67
+ rescue OptionParser::MissingArgument => e
68
+ puts e.message
69
+ puts parser
70
+ exit ThieveExit::MISSING_ARGUMENT
71
+ end
72
+
73
+ if (args.length < 1)
74
+ puts parser
75
+ exit ThieveExit::MISSING_ARGUMENT
76
+ end
77
+
78
+ options["dirs"] = args
79
+
80
+ return options
81
+ end
82
+
83
+ options = parse(ARGV)
84
+
85
+ begin
86
+ thieve = Thieve.new(!String.disable_colorization)
87
+ options["dirs"].each do |dir|
88
+ thieve.steal_from(dir)
89
+ end
90
+ thieve.find_matches
91
+
92
+ export_thread = nil
93
+ if (options["export"])
94
+ export_thread = Thread.new do
95
+ thieve.export_loot(options["export"])
96
+ end
97
+ end
98
+
99
+ puts thieve.to_s
100
+ export_thread.join if (export_thread)
101
+ rescue Interrupt
102
+ # ^C
103
+ # Exit gracefully
104
+ rescue Errno::EPIPE
105
+ # Do nothing. This can happen if piping to another program such as
106
+ # less. Usually if less is closed before we're done with STDOUT.
107
+ rescue Thieve::Error => e
108
+ puts e.message
109
+ exit ThieveExit::EXCEPTION
110
+ rescue Exception => e
111
+ $stderr.puts "Oops! Looks like an error has occured! Maybe the " \
112
+ "message below will help. If not, you can use the " \
113
+ "--verbose flag to get a backtrace.".word_wrap
114
+
115
+ $stderr.puts e.message.white.on_red
116
+ if (options["verbose"])
117
+ e.backtrace.each do |line|
118
+ $stderr.puts line.light_yellow
119
+ end
120
+ end
121
+ exit ThieveExit::EXCEPTION
122
+ end
123
+ exit ThieveExit::GOOD
data/lib/string.rb ADDED
@@ -0,0 +1,12 @@
1
+ # Modify String class to allow for rsplit and word wrap
2
+ class String
3
+ def rsplit(pattern)
4
+ ret = rpartition(pattern)
5
+ ret.delete_at(1)
6
+ return ret
7
+ end
8
+
9
+ def word_wrap(width = 80)
10
+ return scan(/\S.{0,#{width}}\S(?=\s|$)|\S+/).join("\n")
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ class Thieve::Error < RuntimeError
2
+ end
3
+
4
+ # require "thieve/error/executable_not_found_error"
@@ -0,0 +1,162 @@
1
+ require "digest"
2
+ require "fileutils"
3
+ require "openssl"
4
+
5
+ class Thieve::KeyInfo
6
+ # File extension to use when exporting
7
+ attr_reader :ext
8
+
9
+ # File that the key was found in
10
+ attr_reader :file
11
+
12
+ # The fingerprint
13
+ attr_reader :fingerprint
14
+
15
+ # The actual key
16
+ attr_reader :key
17
+
18
+ # The matching cert/key
19
+ attr_accessor :match
20
+
21
+ # The OpenSSL object
22
+ attr_reader :openssl
23
+
24
+ # Type of key/cert
25
+ attr_reader :type
26
+
27
+ def colorize_file(file = @file)
28
+ return file if (!@colorize)
29
+ return file.to_s.light_blue
30
+ end
31
+ private :colorize_file
32
+
33
+ def colorize_key(key = @key)
34
+ return key if (!@colorize)
35
+ return key.split("\n").map do |line|
36
+ line.light_white
37
+ end.join("\n")
38
+ end
39
+ private :colorize_key
40
+
41
+ def colorize_match(match = @match)
42
+ return "" if (match.nil?)
43
+ return "Matches #{match}" if (!@colorize)
44
+ return [
45
+ "Matches".light_blue,
46
+ match.light_green
47
+ ].join(" ")
48
+ end
49
+ private :colorize_match
50
+
51
+ def export(directory)
52
+ FileUtils.mkdir_p(directory)
53
+ File.open("#{directory}/#{@fingerprint}.#{@ext}", "w") do |f|
54
+ f.write(@key)
55
+ end
56
+ end
57
+
58
+ def initialize(file, type, key, colorize)
59
+ @colorize = colorize
60
+ @ext = type.gsub(/ +/, ".").downcase
61
+ @file = file
62
+ @key = key
63
+ @match = nil
64
+ @type = type
65
+
66
+ case @type
67
+ when "CERTIFICATE"
68
+ @openssl = OpenSSL::X509::Certificate.new(@key)
69
+ @fingerprint = OpenSSL::Digest::SHA1.hexdigest(
70
+ @openssl.to_der
71
+ ).to_s
72
+ when "CERTIFICATE REQUEST"
73
+ @openssl = OpenSSL::X509::Request.new(@key)
74
+ @fingerprint = OpenSSL::Digest::SHA1.hexdigest(
75
+ @openssl.to_der
76
+ ).to_s
77
+ when "DH PARAMETERS"
78
+ @openssl = OpenSSL::PKey::DH.new(@key)
79
+ @fingerprint = OpenSSL::Digest::SHA1.hexdigest(
80
+ @openssl.public_key.to_der
81
+ ).to_s
82
+ when "DH PRIVATE KEY"
83
+ @openssl = OpenSSL::PKey::DH.new(@key)
84
+ @fingerprint = OpenSSL::Digest::SHA1.hexdigest(
85
+ @openssl.public_key.to_der
86
+ ).to_s
87
+ when "DSA PRIVATE KEY"
88
+ @openssl = OpenSSL::PKey::DSA.new(@key)
89
+ @fingerprint = OpenSSL::Digest::SHA1.hexdigest(
90
+ @openssl.public_key.to_der
91
+ ).to_s
92
+ when "EC PARAMETERS"
93
+ @openssl = OpenSSL::PKey::EC.new(@key)
94
+ @fingerprint = OpenSSL::Digest::SHA1.hexdigest(
95
+ @openssl.public_key.to_der
96
+ ).to_s
97
+ when "EC PRIVATE KEY"
98
+ @openssl = OpenSSL::PKey::EC.new(@key)
99
+ @fingerprint = OpenSSL::Digest::SHA1.hexdigest(
100
+ @openssl.public_key.to_der
101
+ ).to_s
102
+ when "PGP PRIVATE KEY BLOCK"
103
+ command = "gpg --with-fingerprint << EOF\n#{@key}\nEOF"
104
+ %x(#{command}).each_line do |line|
105
+ line.match(/Key fingerprint = (.*)/) do |m|
106
+ @fingerprint = m[1].gsub(" ", "").downcase
107
+ end
108
+ end
109
+ @openssl = nil
110
+ when "PGP PUBLIC KEY BLOCK"
111
+ command = "gpg --with-fingerprint << EOF\n#{@key}\nEOF"
112
+ %x(#{command}).each_line do |line|
113
+ line.match(/Key fingerprint = (.*)/) do |m|
114
+ @fingerprint = m[1].gsub(" ", "").downcase
115
+ end
116
+ end
117
+ @openssl = nil
118
+ when "PRIVATE KEY"
119
+ @openssl = OpenSSL::PKey::RSA.new(@key)
120
+ @fingerprint = OpenSSL::Digest::SHA1.hexdigest(
121
+ @openssl.public_key.to_der
122
+ ).to_s
123
+ when "PUBLIC KEY"
124
+ @openssl = OpenSSL::PKey::RSA.new(@key)
125
+ @fingerprint = OpenSSL::Digest::SHA1.hexdigest(
126
+ @openssl.public_key.to_der
127
+ ).to_s
128
+ when "RSA PRIVATE KEY"
129
+ @openssl = OpenSSL::PKey::RSA.new(@key)
130
+ @fingerprint = OpenSSL::Digest::SHA1.hexdigest(
131
+ @openssl.public_key.to_der
132
+ ).to_s
133
+ when "X509 CRL"
134
+ @openssl = OpenSSL::X509::CRL.new(@key)
135
+ @fingerprint = OpenSSL::Digest::SHA1.new(
136
+ @openssl.to_der
137
+ ).to_s
138
+ else
139
+ @ext = "unknown"
140
+ @fingerprint = Digest::SHA256.hexdigest(@file.to_s + @key)
141
+ @openssl = nil
142
+ end
143
+ end
144
+
145
+ def to_json
146
+ return {
147
+ "file" => file,
148
+ "fingerprint" => fingerprint,
149
+ "key" => key,
150
+ "match" => match || "",
151
+ "type" => type
152
+ }
153
+ end
154
+
155
+ def to_s
156
+ ret = Array.new
157
+ ret.push(colorize_file)
158
+ ret.push(colorize_key)
159
+ ret.push(colorize_match) if (@match)
160
+ return ret.join("\n")
161
+ end
162
+ end
data/lib/thieve.rb ADDED
@@ -0,0 +1,143 @@
1
+ require "colorize"
2
+ require "io/wait"
3
+ require "json"
4
+ require "pathname"
5
+
6
+ class Thieve
7
+ attr_accessor :loot
8
+
9
+ def colorize_type(type)
10
+ return type if (!@colorize)
11
+ return type.light_cyan
12
+ end
13
+ private :colorize_type
14
+
15
+ def export_loot(dir)
16
+ exported = Hash.new
17
+ @loot.each do |type, keys|
18
+ keys.each do |key|
19
+ key.export(dir)
20
+ exported[key.type] ||= Hash.new
21
+ exported[key.type]["#{key.fingerprint}.#{key.ext}"] =
22
+ key.to_json
23
+ end
24
+ end
25
+ File.open("#{dir}/loot.json", "w") do |f|
26
+ f.write(JSON.pretty_generate(exported))
27
+ end
28
+ end
29
+
30
+ def extract_from(file)
31
+ start = false
32
+ key = ""
33
+
34
+ File.open(file).each do |line|
35
+ if (line.include?("BEGIN"))
36
+ start = true
37
+ end
38
+
39
+ if (start)
40
+ key += line.unpack("C*").pack("U*").lstrip.rstrip
41
+ if (key.end_with?("\\n\\"))
42
+ key = key[0..-4]
43
+ end
44
+ key += "\\n"
45
+ end
46
+
47
+ if (line.include?("END"))
48
+ key.scan(/(-----BEGIN(.*)[^-]+-----END\2)/) do |m, t|
49
+ keydata = m.gsub(/\\+n/, "\n").chomp
50
+ type = t.gsub(/-----.*/, "").strip
51
+
52
+ @loot[type] ||= Array.new
53
+ begin
54
+ @loot[type].push(
55
+ Thieve::KeyInfo.new(
56
+ file,
57
+ type,
58
+ keydata,
59
+ @colorize
60
+ )
61
+ )
62
+ rescue Exception => e
63
+ if (@colorize)
64
+ $stderr.puts file.to_s.light_blue
65
+ keydata.each_line do |line|
66
+ $stderr.puts line.strip.light_yellow
67
+ end
68
+ $stderr.puts e.message.white.on_red
69
+ else
70
+ $stderr.puts file
71
+ $stderr.puts keydata
72
+ $stderr.puts e.message
73
+ end
74
+ $stderr.puts
75
+ end
76
+ end
77
+
78
+ start = false
79
+ key = ""
80
+ end
81
+ end
82
+ end
83
+ private :extract_from
84
+
85
+ def find_matches
86
+ @loot["CERTIFICATE"].each do |cert|
87
+ next if (cert.openssl.nil?)
88
+ @loot.each do |type, keys|
89
+ next if (type == "CERTIFICATE")
90
+ keys.each do |key|
91
+ next if (key.openssl.nil?)
92
+ if (cert.openssl.check_private_key(key.openssl))
93
+ cert.match = "#{key.fingerprint}.#{key.ext}"
94
+ key.match = "#{cert.fingerprint}.#{cert.ext}"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ def initialize(colorize = false)
102
+ @colorize = colorize
103
+ @loot = Hash.new
104
+ end
105
+
106
+ def steal_from(filename)
107
+ file = Pathname.new(filename).expand_path
108
+
109
+ if (file.directory?)
110
+ files = Dir[File.join(file, "**", "*")].reject do |f|
111
+ Pathname.new(f).directory? || Pathname.new(f).symlink?
112
+ end
113
+
114
+ files.each do |f|
115
+ extract_from(Pathname.new(f).expand_path)
116
+ end
117
+ else
118
+ extract_from(file)
119
+ end
120
+
121
+ return @loot
122
+ end
123
+
124
+ def summarize_loot
125
+ ret = Array.new
126
+ @loot.each do |type, keys|
127
+ ret.push(colorize_type(type))
128
+ keys.each do |key|
129
+ ret.push("#{key.to_s}\n")
130
+ end
131
+ end
132
+
133
+ return ret.join("\n")
134
+ end
135
+ private :summarize_loot
136
+
137
+ def to_s
138
+ return summarize_loot
139
+ end
140
+ end
141
+
142
+ require "thieve/error"
143
+ require "thieve/key_info"
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thieve
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Miles Whittaker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-03-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: colorize
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.7'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.7.7
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.7'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.7.7
33
+ description: This ruby gem will extract, fingerprint, and match-up keys and/or certs
34
+ from source code trees.
35
+ email: mjwhitta@gmail.com
36
+ executables:
37
+ - thieve
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - bin/thieve
42
+ - lib/string.rb
43
+ - lib/thieve.rb
44
+ - lib/thieve/error.rb
45
+ - lib/thieve/key_info.rb
46
+ homepage: https://mjwhitta.github.io/thieve
47
+ licenses:
48
+ - GPL-3.0
49
+ metadata: {}
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubyforge_project:
66
+ rubygems_version: 2.5.1
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: Extract, fingerprint, and match-up keys and/or certs
70
+ test_files: []