crypto-toolbox 0.1.18 → 0.1.19

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 37d4aab806c87b5df97642e506ea938733be2b79
4
- data.tar.gz: 0527ceaba2da2046e998194405f3b7d5a48394d7
3
+ metadata.gz: 5e7c5e78c91895522590c24250dced966fcfb2f6
4
+ data.tar.gz: 55532ae19e4091a32318dc423c59df4e3dc06ad4
5
5
  SHA512:
6
- metadata.gz: f5e20456aa954a252cf3c7407ecacbabb1cd09c53fbaa5773fc0468283f18a5a2f80a4ded360e83bffe84b7cd058120194fbb2191c04f915d23eaba0c5bb070d
7
- data.tar.gz: 8e41fdd52797f51022df4a97abc30320df50e95f29038ffb96e844ee74fb53d4d7aa681eaf80aa99168f05d142cc4234efbc5856559ded55ad7c7016066dcc2e
6
+ metadata.gz: 8ea2cb29c172ff16ba8f48e7d66393e41caa78a539fb48089038ff38ae8dbed5c130a061f2102522f3fcbfe0e3955a743f65e85ba53aa72142b3e20640965671
7
+ data.tar.gz: b250861b903d5bbaec6511a0bb8e734b8225916bf369fdd10e7b3d7b09d9ae7d297b97065953facafb80154a835872c58cb7d1c786bf073c00d6b395a379372b
@@ -7,5 +7,10 @@ if ARGV[0].nil?
7
7
  else
8
8
  ciphertext = ARGV[0]
9
9
 
10
- Analyzers::VigenereXor.new.analyze(ciphertext)
10
+ results = Analyzers::VigenereXor.new.analyze(ciphertext)
11
+ unless results.empty?
12
+ puts "[Success] Found valid result(s):"
13
+ puts results.map(&:str)
14
+ end
15
+
11
16
  end
@@ -4,7 +4,11 @@ require 'crypto-toolbox/crypt_buffer_input_converter.rb'
4
4
  require 'crypto-toolbox/crypt_buffer.rb'
5
5
 
6
6
  require 'crypto-toolbox/analyzers/utils/key_filter.rb'
7
+
8
+ require 'crypto-toolbox/analyzers/utils/ascii_language_detector.rb'
7
9
  require 'crypto-toolbox/analyzers/utils/spell_checker.rb'
10
+ require 'crypto-toolbox/analyzers/utils/human_language_detector.rb'
11
+
8
12
  require 'crypto-toolbox/analyzers/padding_oracle.rb'
9
13
  require 'crypto-toolbox/analyzers/cbc_mac.rb'
10
14
  require 'crypto-toolbox/analyzers/vigenere_xor.rb'
@@ -12,3 +16,5 @@ require 'crypto-toolbox/analyzers/vigenere_xor.rb'
12
16
 
13
17
  require 'crypto-toolbox/ciphers/caesar.rb'
14
18
  require 'crypto-toolbox/ciphers/rot13.rb'
19
+
20
+ require 'crypto-toolbox/crypto_challanges/solver.rb'
@@ -8,11 +8,11 @@ module Analyzers
8
8
  class Analyzer
9
9
  class FailedAnalysis < RuntimeError; end
10
10
  attr_reader :result
11
-
11
+ include ::Utils::Reporting::Console
12
12
 
13
- def initialize(oracle_class = ::Analyzers::PaddingOracle::Oracles::TcpOracle)
13
+ def initialize(oracle = ::Analyzers::PaddingOracle::Oracles::TcpOracle.new)
14
14
  @result = [ ]
15
- @oracle = oracle_class.new
15
+ @oracle = oracle
16
16
  end
17
17
 
18
18
  # start with the second to last block to manipulate the final block ( cbc xor behaviour )
@@ -24,34 +24,45 @@ module Analyzers
24
24
  # changing this byte ^- will change ^- this byte at decryption
25
25
  def analyze(cipher)
26
26
  blocks = CryptBuffer.from_hex(cipher).chunks_of(16)
27
+
28
+ # for whatever reason ranges cant be from high to low
29
+ (1..(blocks.length() -1)).reverse_each do |block_index|
30
+ result.unshift analyse_block(blocks,block_index)
31
+ end
27
32
 
28
- (blocks.length - 1).downto(1) do |block_index|
29
- result_part = []
30
- # manipulate each byte of the 16 byte block
31
- 1.upto(blocks[block_index -1 ].length) do |pad_index|
32
- @oracle.connect
33
-
34
- jot("processing byte #{pad_index} in block: #{block_index - 1} => #{block_index}",debug: true)
35
- byte = read_byte(pad_index,result_part,blocks,block_index)
36
- result_part.unshift byte
37
-
38
- @oracle.disconnect
33
+ report_result(result)
34
+ end
35
+
36
+
37
+
38
+ private
39
+
40
+ def analyse_block(blocks,block_index)
41
+ block_result = []
42
+
43
+ # manipulate each byte of the 16 byte block
44
+ 1.upto(blocks[block_index -1].length) do |pad_index|
45
+ with_oracle_connection do
46
+ jot("processing byte #{pad_index} in block: #{block_index -1} => #{block_index}",debug: true)
47
+ byte = read_byte(pad_index,block_result,blocks,block_index)
48
+ block_result.unshift byte
39
49
  end
40
- result.unshift result_part
41
50
  end
51
+ block_result
52
+ end
53
+
54
+ def report_result(result)
42
55
  jot(CryptBuffer(result.flatten).chars.inspect,debug: false)
43
56
  jot("stripping padding!",debug: true)
44
57
  jot(CryptBuffer(result.flatten).strip_padding.str,debug: false)
45
58
  end
46
59
 
47
-
48
- private
49
- def jot(message, debug: false)
50
- if debug == false || ENV["DEBUG_ANALYSIS"]
51
- puts message
52
- end
60
+ def with_oracle_connection
61
+ @oracle.connect
62
+ yield
63
+ @oracle.disconnect
53
64
  end
54
-
65
+
55
66
  def apply_found_bytes(buf,cur_result,pad_index)
56
67
  # first we have to apply all the already found bytes
57
68
 
@@ -62,38 +73,53 @@ module Analyzers
62
73
  buf.xor(other)
63
74
  end
64
75
 
65
-
76
+ # the blocks are:
77
+ # xxxxxxxx xxxxxxxx xxxxxxxx [..]
78
+ # ^- IV ^- first ^- second ...
66
79
  def read_byte(pad_index,cur_result,blocks,block_index)
67
- #iv, first, second, last
68
80
  jot(cur_result.inspect,debug: true)
69
81
 
70
82
  # apply all the current-result bytes to the block corresponding to <block_index>
71
83
  # and store the result in a buffer we will mess with
72
- #
73
84
  forge_buf = apply_found_bytes(blocks[block_index - 1],cur_result,pad_index)
74
85
 
75
86
  1.upto 256 do |guess|
76
- # the bytes from the subset we will send to the padding oracle
77
- subset = blocks[0,block_index+1]
78
- subset[block_index -1 ] = forge_buf.xor_at([guess,pad_index], -1 * pad_index)
79
-
80
- input = subset.map(&:bytes).flatten
87
+ input = assemble_oracle_input(forge_buf,blocks,block_index,pad_index,guess)
81
88
 
82
- # skip the first correct guess on the first iteration of the first block
83
- # otherwise the resulting ciphertext would eq the original input
84
- #next if input == blocks.map(&:bytes).flatten
85
- next if guess == pad_index && guess == 1 && block_index == 2
86
-
87
- block_amount = block_index + 1
88
- if @oracle.valid_padding?(input,block_amount)
89
- return guess
90
- end
89
+ next if skip?(pad_index,block_index,guess,cur_result)
91
90
 
91
+ return guess if@oracle.valid_padding?(input,block_amount(block_index))
92
92
  end
93
93
 
94
94
  raise FailedAnalysis, "No padding found... this should neve happen..."
95
95
  end
96
+ private
96
97
 
98
+ # include the block after the index, since this
99
+ # is the one effected by our manipulation. ( due to cbc mode )
100
+ def block_amount(index)
101
+ index +1
102
+ end
103
+
104
+ # Create a subset to only send the blocks we still need to decrypt.
105
+ # manipulate the byte with a padding-index and a guess
106
+ # map the crypt buffer array to a flat array of integers ( representing bytes )
107
+ def assemble_oracle_input(buffer,blocks,block_index,pad_index,guess)
108
+ # the bytes from the subset we will send to the padding oracle
109
+ subset = blocks[0,block_index+1]
110
+ subset[block_index -1 ] = buffer.xor_at([guess,pad_index], -1 * pad_index)
111
+ subset.map(&:bytes).flatten
112
+ end
113
+
114
+ # In case of the first iteration there is a special case to skip:
115
+ # 1) No other blocks have been decrypted yet ( result.empty? )
116
+ # 2) No bytes of the current block have been processed yet ( block_result_empty? )
117
+ # 3) guess xor pad-index does not modify anything ( eq zero )
118
+ # => This would leed to the original ciphertext without any modification beeing sent
119
+ def skip?(pad_index,block_index,guess,block_result)
120
+ result.empty? && block_result.empty? && (guess ^ pad_index).zero?
121
+ end
122
+
97
123
  end
98
124
  end
99
125
  end
@@ -0,0 +1,72 @@
1
+ module Analyzers
2
+ module Utils
3
+ class AsciiLanguageDetector
4
+ ASCII_BASE_RANGE=(32..127).freeze
5
+ ASCII_BLACKLIST = [40,41,42,43,47,60,61,62,91,92,93,94,95,96,35,59].freeze
6
+ ASCII_WHITELIST = [10]
7
+ ASCII_CHARACTERS = ( ASCII_BASE_RANGE.to_a + ASCII_WHITELIST - ASCII_BLACKLIST ).to_ary.freeze # 10 == \n is now allowed!
8
+ =begin
9
+ NOTE: This is the output of the benchmark script contained in this gem
10
+ see: benchmarks/language_detection.rb
11
+ It compares many ways of filtering bytes to check if only "plain" language
12
+ characters are contained. Result:
13
+
14
+ Comparison:
15
+ ascii_range_check: 1773.5 i/s <- use range.cover? and then blacklist.include
16
+ ascii_lingual_byte?: 1494.8 i/s - 1.19x slower <- now uses range.cover? internally
17
+ ascii_lingual_bytes?: 1459.2 i/s - 1.22x slower <- see prev. but get the entire byte array
18
+ ascii_lingual?: 1420.1 i/s - 1.25x slower <- see prev. but works on crypt buffers
19
+ ascii_lingual_and_human_language: 1413.6 i/s - 1.25x slower <- use human_languge?, but apply 0 < byte < 127 first
20
+ ascii_shift_check: 634.4 i/s - 2.80x slower <- uses & (1 << 5).zero? but has to do slow additional checks
21
+ ascii_whitelist.bsearch?: 483.8 i/s - 3.50x slower <- whitelist lookup using bsearch
22
+ hunspell.human_language?: 212.3 i/s - 8.35x slower <- use human_languge?
23
+ ascii_whitelist.include?: 90.2 i/s - 19.67x slower <- use (whitelist - blacklist).include?
24
+ hunspell_human_language_without_dict: 0.2 i/s - 10013.62x slower <- instanciating the dict seems to be very very slow...
25
+
26
+ NOTE:
27
+ Normally the shift solution would be the fastes, but we have to convert back and forth,
28
+ thus the range.cover? check still seems to be the best soution. It is also more readable
29
+
30
+ (We need the chr.downcase.ord conversion to support upper case letters)
31
+ byte < 127 && !(byte.chr.downcase.ord & (1 << 5)).zero?
32
+ =end
33
+ def ascii_lingual_byte?(byte)
34
+ # check how fast bsearch is, if range.cover is no longer needed we can nicely add 10 to the array
35
+ (ascii_base_range.cover?(byte) && !ascii_blacklist.include?(byte)) || ( ascii_whitelist.bsearch{|i| i == byte} )
36
+ end
37
+
38
+ def ascii_lingual_bytes?(bytes)
39
+ bytes.all?{|b| ascii_lingual_byte?(b) }
40
+ end
41
+
42
+ def ascii_lingual_chars
43
+ ASCII_CHARACTERS
44
+ end
45
+
46
+ def ascii_lingual?(buf)
47
+ ascii_lingual_bytes?(buf.bytes)
48
+ end
49
+
50
+ def ascii_lingual_bytes
51
+ ascii_whitelist.to_ary
52
+ end
53
+
54
+ private
55
+
56
+ # building up the range is too slow, thus we cache
57
+ def ascii_base_range
58
+ ASCII_BASE_RANGE
59
+ end
60
+
61
+ def ascii_whitelist
62
+ ASCII_WHITELIST
63
+ end
64
+
65
+ def ascii_blacklist
66
+ ASCII_BLACKLIST
67
+ end
68
+
69
+ end
70
+ end
71
+ end
72
+
@@ -0,0 +1,37 @@
1
+ module Analyzers
2
+ module Utils
3
+ # NOTE the implementation decisions are based on the result of
4
+ # benchmarks/language_detector.rb
5
+ class HumanLanguageDetector
6
+ def initialize
7
+ @spell_checker = ::Analyzers::Utils::SpellChecker.new
8
+ @ascii_checker = ::Analyzers::Utils::AsciiLanguageDetector.new
9
+ end
10
+
11
+ # NOTE: we dont use the human_language? method
12
+ # to be faster at processing and more idiomatic
13
+ def human_language_entries(buffers,spellcheck: true )
14
+ filtered = buffers.select{|b| ascii_valid?(b) }
15
+ if spellcheck
16
+ buffers.select{|b| spell_valid?(b) }
17
+ else
18
+ filtered
19
+ end
20
+ end
21
+
22
+ def human_language?(buffer)
23
+ ascii_valid?(buffer) && spell_valid?(buffer)
24
+ end
25
+
26
+ private
27
+
28
+ def ascii_valid?(buf)
29
+ @ascii_checker.ascii_lingual?(buf)
30
+ end
31
+
32
+ def spell_valid?(buf)
33
+ @spell_checker.human_language?(buf.str)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -11,6 +11,9 @@ module Analyzers
11
11
  # }
12
12
  include ::Utils::Reporting::Console
13
13
 
14
+ def initialize
15
+ @lang_detector = Analyzers::Utils::AsciiLanguageDetector.new
16
+ end
14
17
  # factory method for easy use
15
18
  def self.create(input_buf,keylen)
16
19
  new.run(input_buf,keylen)
@@ -22,9 +25,19 @@ module Analyzers
22
25
  # 3) xor any possible byte value (guess) with all nth's bytes
23
26
  # 4) select those guesses that decipher the nth-byte stream to only english plain ascii chars
24
27
  def run(input_buf,keylen)
25
- candidate_map ={}
26
- (0..(keylen-1)).each do |key_byte_pos|
27
-
28
+ #return run2(input_buf,keylen)
29
+ detector = Analyzers::Utils::HumanLanguageDetector.new
30
+
31
+ candidate_map = (0..(keylen-1)).each_with_object({}) do |key_byte_pos,hsh|
32
+ =begin
33
+ # Letter frquency testing
34
+ freqs = letter_freq(nth_byte_stream.xor_all_with(guess).str)
35
+ diff = FREQUENCIES.keys - freqs.keys
36
+ binding.pry if nth_byte_stream.xor_all_with(guess).bytes.all?{|byte| acceptable_char?(byte) } &&
37
+ ((diff.map{|c| FREQUENCIES[c]}.reduce(&:+)||0) > 16)
38
+ =end
39
+
40
+
28
41
  # create an array of every nth byte of the input. ( thus a pseudo stream of the nth bytes )
29
42
  # 1) create an enumerator of the nth positions. e.g for iteration 0: [0,7,14,...]
30
43
  # 2) Next: Map the positions to bytes of the input buffer
@@ -34,9 +47,9 @@ module Analyzers
34
47
  # nth_byte_stream2 = CryptBuffer.new(nth_stream)
35
48
 
36
49
  nth_byte_stream = input_buf.nth_bytes(keylen,offset: key_byte_pos)
37
- candidate_map[key_byte_pos] = 0.upto(255).select{|guess| nth_byte_stream.xor_all_with(guess).bytes.all?{|byte| acceptable_char?(byte) } }
50
+ hsh[key_byte_pos] = 0.upto(255).select{|guess| nth_byte_stream.xor_all_with(guess).bytes.all?{|byte| acceptable_char?(byte) } }
38
51
 
39
- jot("found #{candidate_map[key_byte_pos].inspect} bytes for position: #{key_byte_pos}",debug: true)
52
+ jot("found #{hsh[key_byte_pos].inspect} bytes for position: #{key_byte_pos}",debug: true)
40
53
  end
41
54
  candidate_map
42
55
  end
@@ -45,10 +58,8 @@ module Analyzers
45
58
 
46
59
  # Checks if a given byte maps to a reasonable english language character
47
60
  def acceptable_char?(byte)
48
- (byte > 31 && byte < 123) && (byte != 60 && byte !=64)
61
+ @lang_detector.ascii_lingual_byte?(byte)
49
62
  end
50
-
51
-
52
63
  end
53
64
  end
54
65
  end
@@ -0,0 +1,36 @@
1
+ module Analyzers
2
+ module Utils
3
+ class LetterFrequency
4
+
5
+ FREQUENCIES={
6
+ ' ' => 20, # ??
7
+ 'e' => 12.02,
8
+ 't' => 9.10,
9
+ 'a' => 8.12,
10
+ 'o' => 7.68,
11
+ 'i' => 7.31,
12
+ 'n' => 6.95,
13
+ 's' => 6.28,
14
+ 'r' => 6.02,
15
+ 'h' => 5.92,
16
+ 'd' => 4.32,
17
+ 'l' => 3.98,
18
+ 'u' => 2.88,
19
+ 'c' => 2.71
20
+ }
21
+ def letter_count(str)
22
+ str.downcase.each_char.with_object({}) do |c,h|
23
+ h[c] = (h.fetch(c,0) + 1) if c =~ /[A-Za-z ]/
24
+ end
25
+ end
26
+
27
+ def letter_freq(str)
28
+ counts = letter_count(str)
29
+ quotient = counts.values.reduce(&:+).to_f
30
+ counts.sort_by{|k,v| v}.reverse.to_h.each_with_object({}){|(k,v),hsh| hsh[k] = (v/quotient) }
31
+ end
32
+
33
+
34
+ end
35
+ end
36
+ end
@@ -1,10 +1,8 @@
1
1
  require 'ffi/hunspell'
2
2
 
3
-
4
3
  module Analyzers
5
4
  module Utils
6
5
  class SpellChecker
7
-
8
6
 
9
7
  def initialize(dict_lang="en_GB")
10
8
  @dict = FFI::Hunspell.dict(dict_lang)
@@ -23,15 +21,18 @@ Some statistics about it:
23
21
  8 13 9 534 1319 2809
24
22
  (0.84,0.88] (0.88,0.92] (0.92,0.96] (0.96,1]
25
23
  10581 46598 198477 1440651
24
+
25
+ NOTE: There is ony caveat: Short messages with < 5 words may have 33 or 50% error rates
26
+ if numbers or single char words are taken into account
26
27
  =end
27
28
  def known_words(str)
28
- words = str.split(" ").select{|w| @dict.check?(w) }
29
+ words = str.split(" ").select{|w| check?(w) }
29
30
  end
30
31
 
31
32
  def human_word?(str)
32
- @dict.check?(str)
33
+ check?(str)
33
34
  end
34
-
35
+
35
36
  def human_phrase?(string)
36
37
  string.split(" ").all?{|part| human_word?(part)}
37
38
  end
@@ -46,19 +47,34 @@ Some statistics about it:
46
47
  # Using shell instead of hunspell ffi causes lots of escaping errors, even with shellwords.escape
47
48
  # errors = Float(`echo '#{Shellwords.escape(str)}' |hunspell -l |wc -l `.split.first)
48
49
  def human_language?(str)
49
- words = str.split(" ").length
50
- errors = str.split(" ").map{|e| @dict.check?(e) }.count{|e| e == false}
51
-
52
- error_rate = errors.to_f/words
53
-
54
- $stderr.puts error_rate.round(4) if ENV["CRYPTO_TOOBOX_PRINT_ERROR_RATES"]
50
+ #NOTE should be reject 1char numbers or all 1 char symbols
51
+ words = str.split(" ").reject{|w| (w.length < 2 || w =~ /^[0-9]+$/) }
52
+ word_amount = words.length
53
+ errors = words.map{|e| check?(e) }.count{|e| e == false}
55
54
 
55
+ error_rate = errors.to_f/word_amount
56
+
57
+ report_error_rate(str,error_rate) if ENV["DEBUG_ANALYSIS"]
58
+
56
59
  error_rate_sufficient?(error_rate)
57
60
  end
58
61
 
59
62
  private
63
+
64
+ def report_error_rate(str,error_rate)
65
+ if ENV["DEBUG_ANALYSIS"]
66
+ $stderr.puts "=================================================="
67
+ $stderr.puts "str: #{str} has error rate: #{error_rate.round(4)}"
68
+ $stderr.puts "=================================================="
69
+ end
70
+ end
71
+
72
+ def check?(input)
73
+ @dict.check?(input) rescue false
74
+ end
75
+
60
76
  def error_rate_sufficient?(rate)
61
- rate < 0.5
77
+ rate < 0.20
62
78
  end
63
79
  end
64
80
  end
@@ -27,77 +27,128 @@ module Analyzers
27
27
  # the error rate of the candidate plaintext using hunspell
28
28
 
29
29
  include ::Utils::Reporting::Console
30
+
31
+ class HammingDistanceKeyLengthFinder
32
+ def keylen_for(buffer)
33
+ offset = 2
34
+ distances = ((0+offset)..64).map do |keysize|
35
+ # take the first 4 blocks of keysize length, generate all combinations (6),
36
+ # map than to normalized hamming distance and take mean
37
+ buffer.chunks_of(keysize)[0,4].combination(2).map{|a,b| a.hdist(b,normalize: true)}.reduce(&:+) / 6.0
38
+ end
39
+ # get the min distance, find its index, convert the keylen
40
+ distances.min(4).map{|m| distances.index(m)}.map{|i| i + offset }.uniq
41
+ end
42
+ end
30
43
 
31
- def find_pattern(buf)
32
- bitstring = buf.nth_bits(7).join("")
44
+ class EightBitPatternFinder
45
+ include ::Utils::Reporting::Console
46
+ def keylen_for(buf)
47
+ # Example: "100100" || nil
48
+ key_pattern = find_pattern(buf)
49
+
50
+ assert_key_pattern!(key_pattern)
51
+
52
+ report_pattern_info(key_pattern)
53
+
54
+ [key_pattern.length]
55
+ end
33
56
 
34
- 1.upto(buf.bytes.length).map do |ksize|
35
- parts = bitstring.scan(/.{#{ksize}}/)
36
- if parts.uniq.length == 1
37
- parts.first
38
- else
39
- nil
57
+ private
58
+
59
+ def assert_key_pattern!(key_pattern)
60
+ if key_pattern.nil?
61
+ $stderr.puts "failed to find keylength by ASCII-8-Bit anlysis"
62
+ exit(1)
40
63
  end
41
- end.compact.first
64
+ end
65
+
66
+ def report_pattern_info(key_pattern)
67
+ jot "Found recurring key pattern: #{key_pattern}"
68
+ jot "Detected key length: #{key_pattern.length}"
69
+ end
70
+
71
+ def find_pattern(buf)
72
+ bitstring = buf.nth_bits(7).join("")
73
+
74
+ 1.upto(buf.bytes.length).map do |ksize|
75
+ parts = bitstring.scan(/.{#{ksize}}/)
76
+ if parts.uniq.length == 1
77
+ parts.first
78
+ else
79
+ nil
80
+ end
81
+ end.compact.first
82
+ end
83
+ end
84
+
85
+ class StaticKeylength
86
+ def initialize(keylength)
87
+ @keylength = keylength
88
+ end
89
+ def keylen_for(dummy)
90
+ [@keylength]
91
+ end
42
92
  end
93
+
94
+
43
95
 
44
- def analyze(input)
96
+ def analyze(input, keylength_strategy=EightBitPatternFinder.new)
45
97
  buf = CryptBuffer.from_hex(input)
46
- ## === Should this be extracted into a dedicated class ?
47
- # Example: "100100" || nil
48
- key_pattern = find_pattern(buf)
49
98
 
50
- assert_key_pattern!(key_pattern)
51
-
52
- report_pattern_info(key_pattern)
99
+ keylength_strategy.keylen_for(buf).map do |keylen|
100
+ analyse_single(buf,keylen)
101
+ end.flatten
102
+ end
103
+
104
+
105
+
106
+ def analyse_single(buf,key_length)
107
+ candidate_map = Analyzers::Utils::KeyCandidateMap.create(buf,key_length)
108
+
109
+
110
+ candidate_amount = candidate_map.map{|k,v| v.length}.reduce(&:*)
111
+ if candidate_amount.zero?
112
+ jot("no combinations for keylen #{key_length} (at least one byte has no candidates)",debug: true)
113
+ return []
114
+ end
115
+ jot "Amount of candidate keys: #{candidate_map.map{|k,v| v.length}.reduce(&:*)}. Starting Permutation (RAM intensive)",debug: true
53
116
 
54
- ##====
55
-
56
- candidate_map = Analyzers::Utils::KeyCandidateMap.create(buf,key_pattern.length)
57
- jot "Amount of candidate keys: #{candidate_map.map{|k,v| v.length}.reduce(&:*)}. Starting Permutation (RAM intensive)"
58
117
 
59
118
  # split the candidate map into head and*tail to create the prduct of all combinations
60
119
  head,*tail = candidate_map.map{|k,v|v}
61
- combinations = head.product(*tail)
120
+ begin
121
+ combinations = head.product(*tail)
122
+ # we simply skip too big products
123
+ rescue RangeError => ex
124
+ jot "keylen: #{key_length}: #{ex}"
125
+ return []
126
+ end
62
127
 
128
+
129
+
63
130
  if ENV["DEBUG_ANALYSIS"]
64
131
  ensure_consistent_result!(combinations,candidate_map)
65
- print_candidate_decryptions(candidate_map,key_pattern.length,buf)
132
+ print_candidate_decryptions(candidate_map,key_length,buf)
66
133
  end
134
+
67
135
 
68
- results = Analyzers::Utils::KeyFilter::AsciiPlain.new(combinations,buf).filter
69
- report_result(results,buf)
70
- end
71
- private
72
- def assert_key_pattern!(key_pattern)
73
- if key_pattern.nil?
74
- $stderr.puts "failed to find keylength by ASCII-8-Bit anlysis"
75
- exit(1)
136
+ keys = Analyzers::Utils::KeyFilter::AsciiPlain.new(combinations,buf).filter.reject(&:empty?)
137
+
138
+ # return the result, not the key
139
+ keys.map do|key|
140
+ key.xor(buf)
76
141
  end
77
142
  end
143
+ private
78
144
 
79
- def ensure_consistent_result!(combinations,condidate_map)
145
+ def ensure_consistent_result!(combinations,candidate_map)
80
146
  # NOTE Consistency check ( enable if you dont trust the generation anymore )
81
147
  # make sure all permutations are still according to the bytes per position map
82
148
  combinations.select do |arr|
83
149
  raise "Inconsistent key candidate combinations" unless arr.map.with_index{|e,i| candidate_map[i].include?(e) }.all?{|e| e ==true}
84
150
  end
85
151
  end
86
-
87
- def report_pattern_info(key_pattern)
88
- jot "Found recurring key pattern: #{key_pattern}"
89
- jot "Detected key length: #{key_pattern.length}"
90
- end
91
-
92
-
93
- def report_result(results,buf)
94
- unless results.empty?
95
- jot "[Success] Found valid result(s):"
96
- results.each do |r|
97
- jot r.xor(buf).str
98
- end
99
- end
100
- end
101
152
 
102
153
  def print_candidate_decryptions(candidate_map,keylen,buf)
103
154
  # printout for debugging. (Manual analysis of the characters)
@@ -51,6 +51,10 @@ class CryptBuffer
51
51
  CryptBufferInputConverter.new.from_hex(input)
52
52
  end
53
53
 
54
+ def self.from_base64(input)
55
+ CryptBufferInputConverter.new.from_base64(input)
56
+ end
57
+
54
58
  def nth_bytes(n,offset: 0)
55
59
  return CryptBuffer([]) if n.nil? || n < 1
56
60
 
@@ -27,7 +27,19 @@ module CryptBufferConcern
27
27
  CryptBuffer(tmp)
28
28
  end
29
29
 
30
+ def hdist(other,normalize: false)
31
+ if normalize
32
+ hamming_distance(other) / length.to_f
33
+ else
34
+ hamming_distance(other)
35
+ end
36
+ end
37
+
30
38
  private
39
+
40
+ def hamming_distance(other)
41
+ (self ^ other).bits.join.count("1")
42
+ end
31
43
 
32
44
  def sanitize_modulus(mod)
33
45
  (mod > 0) ? mod : 256
@@ -22,8 +22,8 @@ module CryptBufferConcern
22
22
  CryptBuffer(bytes.last(n))
23
23
  end
24
24
 
25
- def [](anything)
26
- CryptBuffer(bytes[anything])
25
+ def [](*things)
26
+ CryptBuffer(bytes[*things])
27
27
  end
28
28
 
29
29
  end
@@ -23,6 +23,11 @@ module CryptBufferConcern
23
23
  def to_s
24
24
  str
25
25
  end
26
+
27
+ def base64(strict: true)
28
+ strict ? Base64.strict_encode64(str) : Base64.encode64(str)
29
+ end
30
+
26
31
  private
27
32
  def bytes2hex(bytes)
28
33
  bytes.map{|b| b.to_s(16)}.map{|hs| hs.length == 1 ? "0#{hs}" : hs }.join
@@ -16,6 +16,11 @@ class CryptBufferInputConverter
16
16
  CryptBuffer.new(hex2bytes(hexstr))
17
17
  end
18
18
 
19
+ def from_base64(input)
20
+ string = Base64.decode64(input)
21
+ CryptBuffer.new(str2bytes(string))
22
+ end
23
+
19
24
  private
20
25
  def bytes_from_any(input)
21
26
  case input
@@ -0,0 +1,56 @@
1
+
2
+ module CryptoChallanges
3
+ class Solver
4
+ def solve1(input)
5
+ #CryptoChallanges::Set1::Challange1::Solver.run(input)
6
+ CryptBuffer.from_hex(input).base64
7
+ end
8
+ def solve2(c1,c2)
9
+ (CryptBuffer.from_hex(c1) ^ CryptBuffer.from_hex(c2)).hex.downcase
10
+ end
11
+
12
+ def letter_count(str)
13
+ str.downcase.each_char.with_object({}) do |c,h|
14
+ h[c] = (h.fetch(c,0) + 1) if c =~ /[A-Za-z ]/
15
+ end
16
+ end
17
+
18
+ def letter_freq(str)
19
+ counts = letter_count(str)
20
+ quotient = counts.values.reduce(&:+).to_f
21
+ counts.sort_by{|k,v| v}.reverse.to_h.each_with_object({}){|(k,v),hsh| hsh[k] = (v/quotient) }
22
+ end
23
+
24
+ def solve3(input)
25
+ candidates = (1..256).map{ |guess| CryptBuffer.from_hex(input).xor_all_with(guess) }
26
+ detector = Analyzers::Utils::HumanLanguageDetector.new
27
+
28
+ detector.human_language_entries(candidates).first.to_s
29
+ end
30
+
31
+ def solve4(hexstrings)
32
+ detector = Analyzers::Utils::HumanLanguageDetector.new
33
+ result = hexstrings.map{|h| CryptBuffer.from_hex(h)}.map.with_index do |c,i|
34
+ candidates = (1..256).map{ |guess| c.xor_all_with(guess) }
35
+ matches = detector.human_language_entries(candidates)
36
+
37
+ matches.empty? ? nil : matches
38
+ end
39
+ result.flatten.compact.map(&:str).first
40
+ end
41
+
42
+ def solve5(input,key)
43
+ CryptBuffer(input).xor(key,expand_input: true).hex
44
+ end
45
+
46
+ def solve6(input)
47
+ buffer = CryptBuffer.from_base64(input)
48
+ Analyzers::VigenereXor.new.analyze(buffer.hex,Analyzers::VigenereXor::HammingDistanceKeyLengthFinder.new)
49
+ end
50
+
51
+ def solve7(input,key)
52
+ end
53
+
54
+
55
+ end
56
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crypto-toolbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.18
4
+ version: 0.1.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dennis Sivia
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-07 00:00:00.000000000 Z
11
+ date: 2015-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aes
@@ -59,8 +59,11 @@ files:
59
59
  - lib/crypto-toolbox/analyzers/padding_oracle/analyzer.rb
60
60
  - lib/crypto-toolbox/analyzers/padding_oracle/oracles/http_oracle.rb
61
61
  - lib/crypto-toolbox/analyzers/padding_oracle/oracles/tcp_oracle.rb
62
+ - lib/crypto-toolbox/analyzers/utils/ascii_language_detector.rb
63
+ - lib/crypto-toolbox/analyzers/utils/human_language_detector.rb
62
64
  - lib/crypto-toolbox/analyzers/utils/key_candidate_map.rb
63
65
  - lib/crypto-toolbox/analyzers/utils/key_filter.rb
66
+ - lib/crypto-toolbox/analyzers/utils/letter_frequency.rb
64
67
  - lib/crypto-toolbox/analyzers/utils/spell_checker.rb
65
68
  - lib/crypto-toolbox/analyzers/vigenere_xor.rb
66
69
  - lib/crypto-toolbox/ciphers/caesar.rb
@@ -76,6 +79,7 @@ files:
76
79
  - lib/crypto-toolbox/crypt_buffer/concerns/random.rb
77
80
  - lib/crypto-toolbox/crypt_buffer/concerns/xor.rb
78
81
  - lib/crypto-toolbox/crypt_buffer_input_converter.rb
82
+ - lib/crypto-toolbox/crypto_challanges/solver.rb
79
83
  - lib/crypto-toolbox/utils/reporting/console.rb
80
84
  homepage: https://github.com/scepticulous/crypto-toolbox
81
85
  licenses: