crypto-toolbox 0.1.18 → 0.1.19

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 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: