gentle_brute 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/Gemfile +1 -0
- data/bin/GentleBrute +297 -0
- data/gentle_brute.gemspec +12 -0
- data/lib/gentle_brute/cpa_analyzer.rb +70 -0
- data/lib/gentle_brute/odometer.rb +167 -0
- data/lib/gentle_brute/pattern_finder.rb +64 -0
- data/lib/gentle_brute/resources/dictionary +24005 -0
- data/lib/gentle_brute/resources/neighbors.json +1 -0
- data/lib/gentle_brute/resources/suffixes +56 -0
- data/lib/gentle_brute/word_analyzer.rb +163 -0
- data/lib/gentle_brute.rb +30 -0
- data/passwords.txt +15 -0
- metadata +60 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
gemspec
|
data/bin/GentleBrute
ADDED
@@ -0,0 +1,297 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'digest/md5'
|
4
|
+
require 'slop'
|
5
|
+
require 'gentle_brute'
|
6
|
+
require 'curses'
|
7
|
+
include Curses
|
8
|
+
|
9
|
+
def pretty_time_string time
|
10
|
+
hours = (time/3600).to_i
|
11
|
+
minutes = (time/60 - hours * 60).to_i
|
12
|
+
seconds = (time - (minutes * 60 + hours * 3600))
|
13
|
+
"%02d:%02d:%02d" % [hours, minutes, seconds]
|
14
|
+
end
|
15
|
+
|
16
|
+
def crack_md5_list file_path
|
17
|
+
if not File.exists? file_path
|
18
|
+
puts "[!] ERROR: '#{file_path}' does not exist."
|
19
|
+
return
|
20
|
+
end
|
21
|
+
|
22
|
+
target_hashes = File.read(file_path).split("\n")
|
23
|
+
unbroken_hashes = target_hashes.dup
|
24
|
+
cracked_hashes = {}
|
25
|
+
b = GentleBrute::BruteForcer.new
|
26
|
+
start_time = Time.now.to_f
|
27
|
+
|
28
|
+
Curses.init_screen()
|
29
|
+
Curses.noecho()
|
30
|
+
Curses.stdscr.nodelay = 1
|
31
|
+
Curses.curs_set(1)
|
32
|
+
|
33
|
+
Curses.start_color
|
34
|
+
# Determines the colors in the 'attron' below
|
35
|
+
Curses.init_pair(COLOR_GREEN,COLOR_GREEN,COLOR_BLACK)
|
36
|
+
Curses.init_pair(COLOR_RED,COLOR_RED,COLOR_BLACK)
|
37
|
+
|
38
|
+
win = Curses::Window.new(0, 0, 0, 0)
|
39
|
+
win.setpos(1, 0)
|
40
|
+
win.addstr("== Attempting to crack target hashes using heuristic brute forcing ==".center(Curses.cols))
|
41
|
+
win.setpos(2, 0)
|
42
|
+
win.addstr("(Press 'q' to quit at any time)".center(Curses.cols))
|
43
|
+
win.setpos(4, 0)
|
44
|
+
header = "Phrase | MD5 Hash (Phrase) | MD5 Hash (Target)"
|
45
|
+
divider = ("-" * 77) + " "
|
46
|
+
while header.length < divider.length
|
47
|
+
header += " "
|
48
|
+
end
|
49
|
+
win.addstr(header.rjust(Curses.cols))
|
50
|
+
win.setpos(5, 0)
|
51
|
+
win.addstr(divider.rjust(Curses.cols))
|
52
|
+
|
53
|
+
while unbroken_hashes.length > 0
|
54
|
+
break if Curses.getch == ?q
|
55
|
+
phrase = b.next_valid_phrase
|
56
|
+
attempt_hash = Digest::MD5.hexdigest(phrase)
|
57
|
+
output_phrase = phrase
|
58
|
+
|
59
|
+
row = 6
|
60
|
+
target_hashes.each do | target_hash |
|
61
|
+
if cracked_hashes.key? target_hash
|
62
|
+
new_phrase = cracked_hashes[target_hash]
|
63
|
+
new_hash = target_hash
|
64
|
+
line = "#{new_phrase} | #{new_hash} | #{target_hash} "
|
65
|
+
offset = (line.rjust(Curses.cols).length) - line.length
|
66
|
+
win.setpos(row, offset)
|
67
|
+
win.attron(color_pair(COLOR_GREEN)|A_NORMAL){
|
68
|
+
win.addstr(new_phrase)
|
69
|
+
}
|
70
|
+
win.addstr(" | ")
|
71
|
+
win.attron(color_pair(COLOR_GREEN)|A_NORMAL){
|
72
|
+
win.addstr(new_hash)
|
73
|
+
}
|
74
|
+
win.addstr(" | ")
|
75
|
+
win.attron(color_pair(COLOR_GREEN)|A_NORMAL){
|
76
|
+
win.addstr(target_hash)
|
77
|
+
}
|
78
|
+
else
|
79
|
+
line = "#{output_phrase} | #{attempt_hash} | #{target_hash} "
|
80
|
+
win.setpos(row, 0)
|
81
|
+
win.addstr(line.rjust(Curses.cols))
|
82
|
+
end
|
83
|
+
|
84
|
+
if attempt_hash == target_hash
|
85
|
+
unbroken_hashes.delete target_hash
|
86
|
+
cracked_hashes[target_hash] = output_phrase
|
87
|
+
end
|
88
|
+
|
89
|
+
row += 1
|
90
|
+
end
|
91
|
+
|
92
|
+
# Add time elapsed information
|
93
|
+
win.setpos(row, 0)
|
94
|
+
win.addstr(divider.rjust(Curses.cols))
|
95
|
+
row += 1
|
96
|
+
time_elapsed = Time.now.to_f - start_time
|
97
|
+
time_elapsed_string = "Time Elapsed: #{pretty_time_string time_elapsed} "
|
98
|
+
win.setpos(row, 0)
|
99
|
+
win.addstr(time_elapsed_string.rjust(Curses.cols))
|
100
|
+
row += 1
|
101
|
+
win.setpos(row, 0)
|
102
|
+
win.addstr(divider.rjust(Curses.cols))
|
103
|
+
|
104
|
+
win.refresh
|
105
|
+
end
|
106
|
+
win.refresh
|
107
|
+
win.close
|
108
|
+
|
109
|
+
puts
|
110
|
+
puts "[+] Password cracking finished"
|
111
|
+
if cracked_hashes.keys.length > 0
|
112
|
+
formatted_date = Time.now.strftime("%m_%d_%Y_%H_%M")
|
113
|
+
file_name = "passwords-#{formatted_date}.txt"
|
114
|
+
results = []
|
115
|
+
cracked_hashes.keys.each do | cracked_hash |
|
116
|
+
result = "#{cracked_hashes[cracked_hash]}\t#{cracked_hash}"
|
117
|
+
results << result
|
118
|
+
end
|
119
|
+
File.open(file_name, "w") { |f| f.write results.join "\n" }
|
120
|
+
puts "[+] Saved results to #{file_name}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
Slop.parse :help => true do | opts |
|
125
|
+
opts.banner "GentleBrute [options]"
|
126
|
+
|
127
|
+
opts.on "word-list=", "Generate a word list of valid English-like words and phrases for a given length" do | length |
|
128
|
+
length = length.to_i
|
129
|
+
start_time = Time.now.to_f
|
130
|
+
|
131
|
+
puts "[+] Generating valid words with a length #{length} characters."
|
132
|
+
words = []
|
133
|
+
b = GentleBrute::BruteForcer.new(start_length=length)
|
134
|
+
print "\r[+] Words generated: #{words.length}"
|
135
|
+
while true
|
136
|
+
phrase = b.next_valid_phrase
|
137
|
+
break if phrase.length > length
|
138
|
+
words << phrase
|
139
|
+
print "\r[+] Words generated: #{words.length}"
|
140
|
+
end
|
141
|
+
puts ", Finished!"
|
142
|
+
puts "-" * 60
|
143
|
+
|
144
|
+
puts "[+] Generated #{words.length} potential passphrases"
|
145
|
+
chars = ('a'..'z').to_a + ["'"]
|
146
|
+
potential_combinations = chars.length ** length
|
147
|
+
puts "[+] There are #{potential_combinations} total potential combinations for the length you provided"
|
148
|
+
percent = 100 - ((words.length * 100)/potential_combinations)
|
149
|
+
puts "[+] Gentle-Brute reduced the number of hashes you would need to try by #{percent}%"
|
150
|
+
puts "-" * 50
|
151
|
+
|
152
|
+
puts "[+] Saved generated words to the file, words-#{length}.txt"
|
153
|
+
File.open("words-#{length}.txt", "w") { |f| f.write words.join "\n" }
|
154
|
+
|
155
|
+
puts "[+] Total time spent: #{Time.now.to_f-start_time}"
|
156
|
+
end
|
157
|
+
|
158
|
+
opts.on "cross-compare-crack=", "Cross compare brute force cracking times for a given md5 hash between GentleBrute, and regular brute forcing." do | target_hash |
|
159
|
+
start_length = 1
|
160
|
+
puts "-" * 75
|
161
|
+
puts "[+] Attempting to crack target hash using heuristic brute forcing."
|
162
|
+
start_time = Time.now.to_f
|
163
|
+
b = GentleBrute::BruteForcer.new(start_length)
|
164
|
+
|
165
|
+
puts
|
166
|
+
puts " Phrase | MD5 Hash (Phrase) | MD5 Hash (Target)"
|
167
|
+
puts " " + ("-" * 75)
|
168
|
+
while true
|
169
|
+
phrase = b.next_valid_phrase
|
170
|
+
attempt_hash = Digest::MD5.hexdigest(phrase)
|
171
|
+
output_phrase = phrase
|
172
|
+
while output_phrase.length < 8
|
173
|
+
output_phrase = " " + output_phrase
|
174
|
+
end
|
175
|
+
print "\r#{output_phrase} | #{attempt_hash} | #{target_hash}"
|
176
|
+
break if attempt_hash == target_hash
|
177
|
+
end
|
178
|
+
time_difference = Time.now.to_f - start_time
|
179
|
+
puts
|
180
|
+
puts
|
181
|
+
puts "[+] Crack Succeeded in #{pretty_time_string time_difference}"
|
182
|
+
puts "-" * 75
|
183
|
+
puts
|
184
|
+
|
185
|
+
puts "-" * 75
|
186
|
+
puts "[+] Attempting to crack target hash using standard brute forcing"
|
187
|
+
start_time = Time.now.to_f
|
188
|
+
odometer = GentleBrute::Odometer.new(start_length, heuristic=false)
|
189
|
+
puts
|
190
|
+
puts " Phrase | MD5 Hash (Phrase) | MD5 Hash (Target)"
|
191
|
+
puts " " + ("-" * 75)
|
192
|
+
while true
|
193
|
+
odometer.increment
|
194
|
+
phrase = odometer.string_for_odometer
|
195
|
+
attempt_hash = Digest::MD5.hexdigest(phrase)
|
196
|
+
output_phrase = phrase
|
197
|
+
while output_phrase.length < 8
|
198
|
+
output_phrase = " " + output_phrase
|
199
|
+
end
|
200
|
+
print "\r#{output_phrase} | #{attempt_hash} | #{target_hash}"
|
201
|
+
break if attempt_hash == target_hash
|
202
|
+
end
|
203
|
+
time_difference1 = Time.now.to_f - start_time
|
204
|
+
puts
|
205
|
+
puts
|
206
|
+
puts "[+] Crack Succeeded in #{pretty_time_string time_difference1}"
|
207
|
+
puts "-" * 75
|
208
|
+
puts
|
209
|
+
|
210
|
+
puts "-" * 75
|
211
|
+
puts "[+] GentleBrute was #{pretty_time_string(time_difference1-time_difference)} faster"
|
212
|
+
percent = 100 - ((time_difference*100)/time_difference1)
|
213
|
+
puts "[+] That's a speed improvement of %d%% compared to standard brute forcing!" % percent
|
214
|
+
puts "-" * 75
|
215
|
+
end
|
216
|
+
|
217
|
+
opts.on "validate=", "Test whether a given word or phrase is considered valid" do | word |
|
218
|
+
cpa_threshold = 24
|
219
|
+
word_analyzer = GentleBrute::WordAnalyzer.new cpa_threshold
|
220
|
+
if word_analyzer.is_valid_word? word.dup
|
221
|
+
puts "[+] \"#{word}\" is a valid English-like word or phrase."
|
222
|
+
else
|
223
|
+
puts "[+] \"#{word}\" is NOT a valid English-like word or phrase."
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
opts.on "crack-md5=", "Crack a single MD5 password hash" do | target_hash |
|
228
|
+
start_length = 1
|
229
|
+
puts "-" * 75
|
230
|
+
puts "[+] Attempting to crack target hash using heuristic brute forcing."
|
231
|
+
start_time = Time.now.to_f
|
232
|
+
b = GentleBrute::BruteForcer.new(start_length)
|
233
|
+
|
234
|
+
puts
|
235
|
+
puts " Phrase | MD5 Hash (Phrase) | MD5 Hash (Target)"
|
236
|
+
puts " " + ("-" * 75)
|
237
|
+
while true
|
238
|
+
phrase = b.next_valid_phrase
|
239
|
+
attempt_hash = Digest::MD5.hexdigest(phrase)
|
240
|
+
output_phrase = phrase
|
241
|
+
while output_phrase.length < 8
|
242
|
+
output_phrase = " " + output_phrase
|
243
|
+
end
|
244
|
+
print "\r#{output_phrase} | #{attempt_hash} | #{target_hash}"
|
245
|
+
break if attempt_hash == target_hash
|
246
|
+
end
|
247
|
+
time_difference = Time.now.to_f - start_time
|
248
|
+
puts
|
249
|
+
puts
|
250
|
+
puts "[+] Crack Succeeded in #{pretty_time_string time_difference}"
|
251
|
+
puts "-" * 75
|
252
|
+
puts
|
253
|
+
end
|
254
|
+
|
255
|
+
opts.on "crack-md5-list=", "Crack a series of MD5 password hashes in a given file." do | file_path |
|
256
|
+
crack_md5_list file_path
|
257
|
+
end
|
258
|
+
|
259
|
+
opts.on "rainbow-table", "Build MD5 hash rainbow table." do
|
260
|
+
start_length = 1
|
261
|
+
puts "-" * 75
|
262
|
+
puts "[+] Creating rainbow table using heuristic brute force phrase creation."
|
263
|
+
puts "[+] Saving results to 'rainbow_table.txt'"
|
264
|
+
puts "[+] Press Ctrl-C to stop at any time."
|
265
|
+
start_time = Time.now.to_f
|
266
|
+
b = GentleBrute::BruteForcer.new(start_length)
|
267
|
+
|
268
|
+
table_file = File.open("rainbow_table.txt", "w")
|
269
|
+
|
270
|
+
puts
|
271
|
+
puts " Phrase | MD5 Hash (Phrase) | Time Elapsed"
|
272
|
+
puts " " + ("-" * 75)
|
273
|
+
begin
|
274
|
+
while true
|
275
|
+
phrase = b.next_valid_phrase
|
276
|
+
attempt_hash = Digest::MD5.hexdigest(phrase)
|
277
|
+
output_phrase = phrase
|
278
|
+
while output_phrase.length < 8
|
279
|
+
output_phrase = " " + output_phrase
|
280
|
+
end
|
281
|
+
time_elapsed = Time.now.to_f - start_time
|
282
|
+
print "\r#{output_phrase} | #{attempt_hash} | #{pretty_time_string time_elapsed}"
|
283
|
+
table_file.write("#{phrase}\t#{attempt_hash}\n")
|
284
|
+
end
|
285
|
+
rescue Interrupt
|
286
|
+
puts
|
287
|
+
puts
|
288
|
+
puts "[+] Table generating stopped."
|
289
|
+
end
|
290
|
+
|
291
|
+
table_file.close()
|
292
|
+
end
|
293
|
+
|
294
|
+
opts.on_empty do
|
295
|
+
puts opts.help
|
296
|
+
end
|
297
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'gentle_brute'
|
3
|
+
s.version = '0.0.1'
|
4
|
+
s.summary = 'The better brute force algorithm'
|
5
|
+
s.description = 'A heuristic brute forcing algorithm for Ruby that generates brute force passphrases that adhere to the rules of English-like words and phrases.'
|
6
|
+
s.authors = ["Brandon Smith"]
|
7
|
+
s.email = 'brandon.smith@studiobebop.net'
|
8
|
+
s.files = `git ls-files`.split("\n")
|
9
|
+
s.homepage = 'http://github.com/jamespenguin/gentle-brute'
|
10
|
+
s.executables << 'GentleBrute'
|
11
|
+
s.requirements << 'slop >= 2.3.1'
|
12
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module GentleBrute
|
2
|
+
class CPAAnalyzer
|
3
|
+
NEIGHBORS_PATH = File.expand_path('../resources/neighbors.json', __FILE__)
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
build_cpa_tables if not File.exists? NEIGHBORS_PATH
|
7
|
+
lattices = JSON.parse File.read(NEIGHBORS_PATH)
|
8
|
+
@starters = lattices["starters"]
|
9
|
+
@neighbors = lattices["neighbors"]
|
10
|
+
@enders = lattices["enders"]
|
11
|
+
end
|
12
|
+
|
13
|
+
# Analyze a dictionary word list ('lib/gentle_brute/resources/dictionary') for repetition patterns
|
14
|
+
# @param [String] dictionary_path path to dictionary word list file
|
15
|
+
def build_cpa_tables(dictionary_path=File.expand_path("../resources/dictionary", __FILE__))
|
16
|
+
# Create empty pattern occurce lattices
|
17
|
+
lattice = Hash.new { |h, k| h[k] = {} }
|
18
|
+
starter_lattice = Hash.new { |h, k| h[k] = {} }
|
19
|
+
ender_lattice = Hash.new { |h, k| h[k] = {} }
|
20
|
+
chars = ('a'..'z').to_a
|
21
|
+
chars.each do | char |
|
22
|
+
chars.each do | char2 |
|
23
|
+
lattice[char][char2] = [0, 0]
|
24
|
+
starter_lattice[char][char2] = [0, 0]
|
25
|
+
ender_lattice[char][char2] = [0, 0]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Analyze each wrod in dictionary list
|
30
|
+
words = File.read(dictionary_path)
|
31
|
+
words.each_line do | word |
|
32
|
+
word.chomp!
|
33
|
+
word.downcase!
|
34
|
+
for i in 0..word.length
|
35
|
+
char = word[i] # current char
|
36
|
+
char_r = word[i+1] # char one index to the right of the current char
|
37
|
+
char_rr = word[i+2] # char two indicies to the right of the current char
|
38
|
+
begin
|
39
|
+
starter_lattice[char][char_r][0] += 1 if char_r and i == 0
|
40
|
+
starter_lattice[char][char_rr][1] += 1 if char_rr and i == 0
|
41
|
+
ender_lattice[char][char_r][0] += 1 if char_r and i == word.length-3
|
42
|
+
ender_lattice[char][char_rr][1] += 1 if char_rr and i == word.length-3
|
43
|
+
lattice[char][char_r][0] += 1 if char_r
|
44
|
+
lattice[char][char_rr][1] += 1 if char_rr
|
45
|
+
rescue
|
46
|
+
break
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Write neighbors file
|
52
|
+
output_lattice = {"starters" => starter_lattice,
|
53
|
+
"neighbors" => lattice,
|
54
|
+
"enders" => ender_lattice}
|
55
|
+
File.open(NEIGHBORS_PATH, "w") {|f| f.write output_lattice.to_json }
|
56
|
+
end
|
57
|
+
|
58
|
+
def get_starter_neighbor_score(char, char2)
|
59
|
+
@starters[char][char2]
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_neighbor_score(char, char2)
|
63
|
+
@neighbors[char][char2]
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_ender_neighbor_score(char, char2)
|
67
|
+
@enders[char][char2]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
module GentleBrute
|
2
|
+
class Odometer
|
3
|
+
def initialize(start_length=1, heuristic=true)
|
4
|
+
@letters = ('a'..'z').to_a
|
5
|
+
@chars = @letters + [" ", "'"]
|
6
|
+
@odometer = Array.new(start_length, 0)
|
7
|
+
@heuristic = heuristic
|
8
|
+
@cpa_analyzer = CPAAnalyzer.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def has_triple_char_pattern?
|
12
|
+
return false if not @heuristic
|
13
|
+
indexes = []
|
14
|
+
odometer_length = @odometer.length
|
15
|
+
(odometer_length-1).downto 0 do | i |
|
16
|
+
index = @odometer[i]
|
17
|
+
if indexes.length > 0
|
18
|
+
char1 = @chars[index] # figure out what char the current index is
|
19
|
+
char2 = @chars[@odometer[indexes[0]]] # get the char of our first element in the indexes list
|
20
|
+
indexes = [] if char1 != char2
|
21
|
+
end
|
22
|
+
indexes << i
|
23
|
+
break if indexes.length == 3
|
24
|
+
end
|
25
|
+
return false if indexes.length < 3
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
#def break_up_triplet_patterns
|
30
|
+
# return if not @heuristic
|
31
|
+
#
|
32
|
+
# word = string_for_odometer
|
33
|
+
# pattern_data = PatternFinder.patterns_in_strintg word
|
34
|
+
# return if pattern_data == nil
|
35
|
+
# return if pattern_data[3] < 3
|
36
|
+
#
|
37
|
+
# index = pattern_data[4][1]
|
38
|
+
# index.downto 0 do | i |
|
39
|
+
# element = @odometer[i]
|
40
|
+
# element += 1
|
41
|
+
# if element != @chars.length
|
42
|
+
# @odometer[i] = element
|
43
|
+
# break
|
44
|
+
# end
|
45
|
+
# @odometer[i] = 0
|
46
|
+
# @odometer << 0 if i == 0
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# break_up_triplet_patterns
|
50
|
+
#end
|
51
|
+
|
52
|
+
def rotate_out_bad_start_pairs
|
53
|
+
return if not @heuristic
|
54
|
+
return if @odometer.length < 3
|
55
|
+
|
56
|
+
char1 = @chars[@odometer[0]]
|
57
|
+
char2 = @chars[@odometer[1]]
|
58
|
+
char3 = @chars[@odometer[2]]
|
59
|
+
return if not @letters.include? char1
|
60
|
+
return if not @letters.include? char2
|
61
|
+
return if not @letters.include? char3
|
62
|
+
|
63
|
+
if @cpa_analyzer.get_starter_neighbor_score(char1, char3)[1] == 0
|
64
|
+
2.downto 0 do | i |
|
65
|
+
element = @odometer[i]
|
66
|
+
element += 1
|
67
|
+
if element != @chars.length
|
68
|
+
@odometer[i] = element
|
69
|
+
break
|
70
|
+
end
|
71
|
+
@odometer[i] = 0
|
72
|
+
@odometer << 0 if i == 0
|
73
|
+
end
|
74
|
+
rotate_out_bad_start_pairs
|
75
|
+
end
|
76
|
+
|
77
|
+
if @cpa_analyzer.get_starter_neighbor_score(char1, char2)[0] == 0
|
78
|
+
1.downto 0 do | i |
|
79
|
+
element = @odometer[i]
|
80
|
+
element += 1
|
81
|
+
if element != @chars.length
|
82
|
+
@odometer[i] = element
|
83
|
+
break
|
84
|
+
end
|
85
|
+
@odometer[i] = 0
|
86
|
+
@odometer << 0 if i == 0
|
87
|
+
end
|
88
|
+
rotate_out_bad_start_pairs
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def rotate_out_bad_end_pairs
|
93
|
+
return if not @heuristic
|
94
|
+
|
95
|
+
return if @odometer.length < 3
|
96
|
+
|
97
|
+
char1 = @chars[@odometer[-1]]
|
98
|
+
char2 = @chars[@odometer[-2]]
|
99
|
+
char3 = @chars[@odometer[-3]]
|
100
|
+
return if not @letters.include? char1
|
101
|
+
return if not @letters.include? char2
|
102
|
+
return if not @letters.include? char3
|
103
|
+
|
104
|
+
last = @odometer.length-1
|
105
|
+
|
106
|
+
if @cpa_analyzer.get_ender_neighbor_score(char1, char3)[1] == 0
|
107
|
+
last.downto 0 do | i |
|
108
|
+
element = @odometer[i]
|
109
|
+
element += 1
|
110
|
+
if element != @chars.length
|
111
|
+
@odometer[i] = element
|
112
|
+
break
|
113
|
+
end
|
114
|
+
@odometer[i] = 0
|
115
|
+
@odometer << 0 if i == 0
|
116
|
+
end
|
117
|
+
rotate_out_bad_end_pairs
|
118
|
+
end
|
119
|
+
|
120
|
+
if @cpa_analyzer.get_ender_neighbor_score(char1, char2)[0] == 0
|
121
|
+
last.downto 0 do | i |
|
122
|
+
element = @odometer[i]
|
123
|
+
element += 1
|
124
|
+
if element != @chars.length
|
125
|
+
@odometer[i] = element
|
126
|
+
break
|
127
|
+
end
|
128
|
+
@odometer[i] = 0
|
129
|
+
@odometer << 0 if i == 0
|
130
|
+
end
|
131
|
+
rotate_out_bad_end_pairs
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Increment the odometer
|
136
|
+
# @param [Number] total_steps the number of steps to increment the odometer by (default is 1)
|
137
|
+
def increment(total_steps=1)
|
138
|
+
steps_taken = 0
|
139
|
+
while steps_taken < total_steps
|
140
|
+
odometer_length = @odometer.length
|
141
|
+
(odometer_length-1).downto 0 do | i |
|
142
|
+
element = @odometer[i]
|
143
|
+
element += 1
|
144
|
+
if element != @chars.length
|
145
|
+
@odometer[i] = element
|
146
|
+
break
|
147
|
+
end
|
148
|
+
@odometer[i] = 0
|
149
|
+
@odometer << 0 if i == 0
|
150
|
+
end
|
151
|
+
rotate_out_bad_end_pairs
|
152
|
+
rotate_out_bad_start_pairs
|
153
|
+
next if has_triple_char_pattern?
|
154
|
+
steps_taken += 1
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# @return [String] string representation of the odometer, with character indexes mapped to their respective characters
|
159
|
+
def string_for_odometer
|
160
|
+
return @odometer.map {|i| @chars[i]}.join ""
|
161
|
+
end
|
162
|
+
|
163
|
+
def to_s
|
164
|
+
string_for_odometer
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module GentleBrute
|
2
|
+
module PatternFinder
|
3
|
+
module_function
|
4
|
+
|
5
|
+
# Analyzes a given string for different patterns
|
6
|
+
# @param [String] string the string to analyze
|
7
|
+
# @return [Array] a whole bunch of patterns :s
|
8
|
+
def patterns_in_strintg string
|
9
|
+
pattern_string = ""
|
10
|
+
pattern_length = 0
|
11
|
+
pattern_count = 0
|
12
|
+
pattern_bounds = []
|
13
|
+
patterns = {}
|
14
|
+
|
15
|
+
for i in 0...string.length
|
16
|
+
(string.length-1).downto i do | ii |
|
17
|
+
slice = string[i..ii]
|
18
|
+
slice_length = slice.length
|
19
|
+
slice2 = string[i+slice_length..ii+slice_length]
|
20
|
+
if slice == slice2
|
21
|
+
if not patterns.has_key? slice
|
22
|
+
patterns[slice] = {}
|
23
|
+
patterns[slice]["count"] = 1
|
24
|
+
patterns[slice]["start"] = i
|
25
|
+
else
|
26
|
+
patterns[slice]["count"] += 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
return nil if patterns == {}
|
33
|
+
|
34
|
+
highest_count = 0
|
35
|
+
patterns.keys.reverse.each do | key |
|
36
|
+
if patterns[key]["count"] > highest_count
|
37
|
+
pattern_string = key
|
38
|
+
highest_count = patterns[key]["count"]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
start = patterns[pattern_string]["start"]
|
43
|
+
index = start
|
44
|
+
length = pattern_string.length
|
45
|
+
pattern_length = length
|
46
|
+
pattern_count += 1
|
47
|
+
|
48
|
+
while true
|
49
|
+
slice = string[index...index+length]
|
50
|
+
slice2 = string[index+length...index+(length*2)]
|
51
|
+
break if slice != slice2
|
52
|
+
|
53
|
+
pattern_bounds = [start, index+(length*2)-1]
|
54
|
+
pattern_length += length
|
55
|
+
pattern_count += 1
|
56
|
+
|
57
|
+
index += length
|
58
|
+
end
|
59
|
+
full_pattern = pattern_string * pattern_count
|
60
|
+
|
61
|
+
[pattern_string, full_pattern, pattern_length, pattern_count, pattern_bounds]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|