crypto-toolbox 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/crypto-toolbox.rb +1 -0
- data/lib/crypto-toolbox/break_vigenere.rb +97 -0
- data/lib/crypto-toolbox/crypt_buffer.rb +147 -0
- data/lib/crypto-toolbox/key_filter.rb +41 -0
- data/lib/crypto-toolbox/spell_checker.rb +48 -0
- metadata +77 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: dff3731a08e88906d365f475991967f6f29800ce
|
4
|
+
data.tar.gz: 622f4078f5a13226208ab4aff323b911bebb9d82
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6601d0c7bff0a3904557992cefbd36935b103e666f3c7d4616066ce33c6715b17d657b59527ad9e69e5043f09878efe5f19779117cb1f11d7a66b30e420cf609
|
7
|
+
data.tar.gz: af42b570b9ef6eb7240f23134290d4b0854fc4d2c5a03911449bdd7acf22491d570532755501ac0da0a8fab7d89b40c4fa76cce81716909610ef87e0079f9ead
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'crypto-toolbox/crypt_buffer.rb'
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require_relative './crypt_buffer.rb'
|
3
|
+
require_relative './key_filter.rb'
|
4
|
+
require 'shellwords'
|
5
|
+
require 'ffi/hunspell'
|
6
|
+
|
7
|
+
##
|
8
|
+
# http://www.ulduzsoft.com/2015/03/breaking-the-vigenere-cipher/
|
9
|
+
# https://github.com/trekawek/vigenere/blob/master/vig.rb
|
10
|
+
|
11
|
+
def find_pattern(buf)
|
12
|
+
bitstring = buf.bits.map{|b| b[0]}.join("")
|
13
|
+
1.upto([buf.bytes.length,62].min).map do |ksize|
|
14
|
+
parts = bitstring.scan(/.{#{ksize}}/)
|
15
|
+
if parts.uniq.length == 1
|
16
|
+
parts.first
|
17
|
+
else
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end.compact.first
|
21
|
+
end
|
22
|
+
|
23
|
+
input = ARGV[0] || "F96DE8C227A259C87EE1DA2AED57C93FE5DA36ED4EC87EF2C63AAE5B9A7EFFD673BE4ACF7BE8923CAB1ECE7AF2DA3DA44FCF7AE29235A24C963FF0DF3CA3599A70E5DA36BF1ECE77F8DC34BE129A6CF4D126BF5B9A7CFEDF3EB850D37CF0C63AA2509A76FF9227A55B9A6FE3D720A850D97AB1DD35ED5FCE6BF0D138A84CC931B1F121B44ECE70F6C032BD56C33FF9D320ED5CDF7AFF9226BE5BDE3FF7DD21ED56CF71F5C036A94D963FF8D473A351CE3FE5DA3CB84DDB71F5C17FED51DC3FE8D732BF4D963FF3C727ED4AC87EF5DB27A451D47EFD9230BF47CA6BFEC12ABE4ADF72E29224A84CDF3FF5D720A459D47AF59232A35A9A7AE7D33FB85FCE7AF5923AA31EDB3FF7D33ABF52C33FF0D673A551D93FFCD33DA35BC831B1F43CBF1EDF67F0DF23A15B963FE5DA36ED68D378F4DC36BF5B9A7AFFD121B44ECE76FEDC73BE5DD27AFCD773BA5FC93FE5DA3CB859D26BB1C63CED5CDF3FE2D730B84CDF3FF7DD21ED5ADF7CF0D636BE1EDB79E5D721ED57CE3FE6D320ED57D469F4DC27A85A963FF3C727ED49DF3FFFDD24ED55D470E69E73AC50DE3FE5DA3ABE1EDF67F4C030A44DDF3FF5D73EA250C96BE3D327A84D963FE5DA32B91ED36BB1D132A31ED87AB1D021A255DF71B1C436BF479A7AF0C13AA14794"
|
24
|
+
|
25
|
+
buf = CryptBuffer.new(input)
|
26
|
+
result = find_pattern(buf)
|
27
|
+
|
28
|
+
if result.nil?
|
29
|
+
$stderr.puts "failed to find keylength by ASCII-8-Bit anlysis"
|
30
|
+
exit(1)
|
31
|
+
end
|
32
|
+
|
33
|
+
keylen = result.length
|
34
|
+
puts "Found recurring key pattern: #{result}"
|
35
|
+
puts "Detected key length: #{keylen}"
|
36
|
+
|
37
|
+
candidate_map ={}
|
38
|
+
(0..(keylen-1)).each do |key_byte|
|
39
|
+
|
40
|
+
nth_stream = (key_byte).step(buf.bytes.length() -1, keylen).map{|i| buf.bytes[i]}
|
41
|
+
smart_buf = CryptBuffer.new(nth_stream)
|
42
|
+
|
43
|
+
candidate_map[key_byte]=[]
|
44
|
+
1.upto(255).each do |possible_key_value|
|
45
|
+
if smart_buf.xor_all_with(possible_key_value).bytes.all?{|e| e > 31 && e < 123 && e != 60 && e !=64}
|
46
|
+
#puts "YES: " + smart_buf.xor_all_with(possible_key_value).to_s
|
47
|
+
candidate_map[key_byte] << possible_key_value
|
48
|
+
else
|
49
|
+
#puts "NO: " + smart_buf.xor_all_with(possible_key_value).to_s
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
|
56
|
+
head,*tail = candidate_map.map{|k,v|v}
|
57
|
+
|
58
|
+
puts "Amount of candidate keys: #{candidate_map.map{|k,v| v.length}.reduce(&:*)}. Starting Permutation (RAM intensive)"
|
59
|
+
|
60
|
+
combinations = head.product(*tail)
|
61
|
+
# make sure all permutations are still according to the bytes per position map
|
62
|
+
#x = combinations.select do |arr|
|
63
|
+
# #binding.pry
|
64
|
+
# arr.map.with_index{|e,i| candidate_map[i].include?(e) }.all?{|e| e ==true}
|
65
|
+
#end
|
66
|
+
|
67
|
+
# printout for debugging. (Manual analysis of the characters)
|
68
|
+
puts "======= Candidate decryption result of first #{keylen} bytes ======="
|
69
|
+
(0..keylen-1).each do|i|
|
70
|
+
candidate_map[i].each do |byte|
|
71
|
+
print CryptBuffer.new(buf.bytes[i,keylen]).xor(byte).to_s + " "
|
72
|
+
end
|
73
|
+
print "\n"
|
74
|
+
end
|
75
|
+
puts "====================================================================="
|
76
|
+
|
77
|
+
|
78
|
+
result = KeySearch::Filter::AsciiPlain.new(combinations,buf).filter
|
79
|
+
unless result.empty?
|
80
|
+
puts "[Success] Found valid result(s)"
|
81
|
+
result.each do |r|
|
82
|
+
puts r.xor(buf).str
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
=begin
|
87
|
+
NOTE: we may at digram and trigram support?
|
88
|
+
#trigram="the "
|
89
|
+
#x = CryptBuffer.new(trigram)
|
90
|
+
=end
|
91
|
+
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'pry'
|
3
|
+
require 'pp'
|
4
|
+
require 'aes'
|
5
|
+
|
6
|
+
class CryptBuffer
|
7
|
+
attr_accessor :bytes
|
8
|
+
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
def initialize(input)
|
12
|
+
@bytes = bytes_from_any(input)
|
13
|
+
end
|
14
|
+
|
15
|
+
def each(&block)
|
16
|
+
@bytes.each(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
alias_method :b, :bytes
|
20
|
+
|
21
|
+
def hex
|
22
|
+
bytes2hex(bytes).upcase
|
23
|
+
end
|
24
|
+
alias_method :h, :hex
|
25
|
+
|
26
|
+
def chars
|
27
|
+
map{|b| b.to_i.chr}
|
28
|
+
end
|
29
|
+
alias_method :c, :chars
|
30
|
+
|
31
|
+
def str
|
32
|
+
chars.join
|
33
|
+
end
|
34
|
+
alias_method :s, :str
|
35
|
+
|
36
|
+
def bits
|
37
|
+
map{|b| "%08d" % b.to_s(2) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def xor(input,expand_input: false)
|
41
|
+
if expand_input
|
42
|
+
xor_all_with(input)
|
43
|
+
else
|
44
|
+
xor_bytes(bytes_from_any(input))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def xor_all_with(input)
|
49
|
+
expanded = expand_bytes(bytes_from_any(input),self.bytes.length)
|
50
|
+
xor_bytes(expanded)
|
51
|
+
end
|
52
|
+
|
53
|
+
def pp
|
54
|
+
puts pretty_hexstr
|
55
|
+
end
|
56
|
+
|
57
|
+
def xor_space
|
58
|
+
xor(0x20,expand_input: true)
|
59
|
+
end
|
60
|
+
|
61
|
+
def ==(other)
|
62
|
+
bytes == bytes_from_any(other)
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_s
|
66
|
+
str
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def expand_bytes(input,total)
|
71
|
+
if input.length >= total
|
72
|
+
input
|
73
|
+
else
|
74
|
+
n = total / input.length
|
75
|
+
rest = total % input.length
|
76
|
+
|
77
|
+
# expand the input to the full length of the internal data
|
78
|
+
(input * n) + input[0,rest]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
def bytes_from_any(input)
|
82
|
+
case input
|
83
|
+
when Array
|
84
|
+
input
|
85
|
+
when String
|
86
|
+
if input.match(/^(0x)?[0-9a-fA-F]+$/).nil?
|
87
|
+
str2bytes(input)
|
88
|
+
else
|
89
|
+
hex2bytes(normalize_hex(input))
|
90
|
+
end
|
91
|
+
when CryptBuffer
|
92
|
+
input.b
|
93
|
+
when Fixnum
|
94
|
+
# integers as strings dont have a 0x prefix
|
95
|
+
if input.to_s(16).match(/^[0-9a-fA-F]+$/)
|
96
|
+
# assume 0x prefixed integer
|
97
|
+
hex2bytes(normalize_hex(input.to_s(16)))
|
98
|
+
else
|
99
|
+
# regular number
|
100
|
+
[input].pack('C*').bytes
|
101
|
+
end
|
102
|
+
else
|
103
|
+
raise "Unsupported input: #{input.inspect} of class #{input.class}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def normalize_hex(str)
|
108
|
+
tmp = (str.length == 1) ? "0#{str}" : "#{str}"
|
109
|
+
tmp.gsub(/(^0x|\s)/,"").upcase
|
110
|
+
end
|
111
|
+
|
112
|
+
def strip_hex_prefix(hex)
|
113
|
+
raise "remove 0x from hexinput"
|
114
|
+
end
|
115
|
+
|
116
|
+
def xor_bytes(byt)
|
117
|
+
len = [self.bytes.size,byt.size].min
|
118
|
+
result = self.bytes[0...len].map.with_index{|b,i| b ^ byt[i] } + self.bytes[len,self.bytes.length - len]
|
119
|
+
CryptBuffer.new(result)
|
120
|
+
end
|
121
|
+
|
122
|
+
def xor_hex(hex)
|
123
|
+
x = hex2bytes(hex)
|
124
|
+
xor_bytes(x)
|
125
|
+
end
|
126
|
+
|
127
|
+
def hex2bytes(hexstr)
|
128
|
+
hexstr.scan(/../).map{|h| h.to_i(16) }
|
129
|
+
end
|
130
|
+
|
131
|
+
def str2bytes(str)
|
132
|
+
str.bytes.to_a
|
133
|
+
end
|
134
|
+
|
135
|
+
def bytes2hex(bytes)
|
136
|
+
bytes.map{|b| b.to_s(16)}.map{|hs| hs.length == 1 ? "0#{hs}" : hs }.join
|
137
|
+
end
|
138
|
+
|
139
|
+
def pretty_hexstr
|
140
|
+
str = h.scan(/.{2}/).to_a.join(" ")
|
141
|
+
"0x#{h.upcase} (#{str.upcase})"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def CryptBuffer(input)
|
146
|
+
CryptBuffer.new(input)
|
147
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative './crypt_buffer.rb'
|
2
|
+
require_relative './spell_checker.rb'
|
3
|
+
require 'pry'
|
4
|
+
|
5
|
+
module KeySearch
|
6
|
+
module Filter
|
7
|
+
class AsciiPlain
|
8
|
+
def initialize(keys,ciphertext,dict_lang="en_GB")
|
9
|
+
@keys = keys
|
10
|
+
@c = @ciphertext = ciphertext
|
11
|
+
@keylen = keys.first.length
|
12
|
+
@dict = FFI::Hunspell.dict(dict_lang)
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def filter
|
17
|
+
# how often is the key repeated
|
18
|
+
reps = @c.bytes.length / @keylen
|
19
|
+
result =[]
|
20
|
+
spell_checker = SpellChecker.new("en_GB")
|
21
|
+
|
22
|
+
@keys.each_with_index do |key,i| # i is used as a simple counter only !
|
23
|
+
test = CryptBuffer.new(@c.bytes[0,@keylen]).xor(key).str
|
24
|
+
repkey = CryptBuffer.new((key*reps) + key[0,(@c.bytes.length % reps).to_i])
|
25
|
+
str = @c.xor(repkey).to_s
|
26
|
+
|
27
|
+
if spell_checker.human_language?(str)
|
28
|
+
result << repkey
|
29
|
+
break
|
30
|
+
else
|
31
|
+
if (i % 50000).zero?
|
32
|
+
puts "[Progress] #{i}/#{@keys.length} (#{(i.to_f/@keys.length*100).round(4)}%)"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
return result
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'ffi/hunspell'
|
2
|
+
class SpellChecker
|
3
|
+
def initialize(dict_lang="en_GB")
|
4
|
+
@dict = FFI::Hunspell.dict(dict_lang)
|
5
|
+
end
|
6
|
+
=begin
|
7
|
+
NOTE: About spelling error rates and language detection:
|
8
|
+
|
9
|
+
missing punctuation support may lead to > 2% errors on valid texts, thus we use a high value .
|
10
|
+
invalid decryptions tend to have spell error rates > 70
|
11
|
+
Some statistics about it:
|
12
|
+
> summary(invalids)
|
13
|
+
Min. 1st Qu. Median Mean 3rd Qu. Max.
|
14
|
+
0.6000 1.0000 1.0000 0.9878 1.0000 1.0000
|
15
|
+
> summary(cut(invalids,10))
|
16
|
+
(0.6,0.64] (0.64,0.68] (0.68,0.72] (0.72,0.76] (0.76,0.8] (0.8,0.84]
|
17
|
+
8 13 9 534 1319 2809
|
18
|
+
(0.84,0.88] (0.88,0.92] (0.92,0.96] (0.96,1]
|
19
|
+
10581 46598 198477 1440651
|
20
|
+
=end
|
21
|
+
def known_words(str)
|
22
|
+
words = str.split(" ").select{|w| @dict.check?(w) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def suggest(str)
|
26
|
+
@dict.suggest(str)
|
27
|
+
end
|
28
|
+
|
29
|
+
def human_language?(str)
|
30
|
+
words = str.split(" ").length
|
31
|
+
errors = str.split(" ").map{|e| @dict.check?(e) }.count{|e| e == false}
|
32
|
+
# using shell instead of hunspell ffi causes lots of escaping errors, even with shellwords.escape
|
33
|
+
#errors = Float(`echo '#{Shellwords.escape(str)}' |hunspell -l |wc -l `.split.first)
|
34
|
+
|
35
|
+
error_rate = errors.to_f/words
|
36
|
+
|
37
|
+
$stderr.puts error_rate.round(4) if ENV["CRYPTO_TOOBOX_PRINT_ERROR_RATES"]
|
38
|
+
|
39
|
+
if error_rate < 0.5
|
40
|
+
puts "[Success] Found valid result (spell error_rate: #{error_rate*100}% is below threshold: 20%)"
|
41
|
+
return true
|
42
|
+
else
|
43
|
+
return false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: crypto-toolbox
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dennis Sivia
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-04-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: aes
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.5'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ffi-hunspell
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.3'
|
41
|
+
description: The Crypto Toolbox offers some tools to easily work with cryptographic
|
42
|
+
primitives like arrays of Bytes or hextrings
|
43
|
+
email: dev@d-coded.de
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- lib/crypto-toolbox.rb
|
49
|
+
- lib/crypto-toolbox/break_vigenere.rb
|
50
|
+
- lib/crypto-toolbox/crypt_buffer.rb
|
51
|
+
- lib/crypto-toolbox/key_filter.rb
|
52
|
+
- lib/crypto-toolbox/spell_checker.rb
|
53
|
+
homepage: https://github.com/scepticulous/crypto-toolbox
|
54
|
+
licenses:
|
55
|
+
- GPLv3
|
56
|
+
metadata: {}
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
requirements: []
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 2.4.6
|
74
|
+
signing_key:
|
75
|
+
specification_version: 4
|
76
|
+
summary: Toolbox for crypto analysis
|
77
|
+
test_files: []
|