thieve 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.
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: []