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 +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: []
|