thieve 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/thieve +123 -0
- data/lib/string.rb +12 -0
- data/lib/thieve/error.rb +4 -0
- data/lib/thieve/key_info.rb +162 -0
- data/lib/thieve.rb +143 -0
- metadata +70 -0
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
|
data/lib/thieve/error.rb
ADDED
@@ -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: []
|