crypto_toolchain 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/.gitignore +51 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +3 -0
- data/Guardfile +15 -0
- data/LICENSE +21 -0
- data/README.md +95 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/crypto_toolchain.gemspec +33 -0
- data/exe/crypto +7 -0
- data/lib/crypto_toolchain/black_boxes/aes_ctr_editor.rb +25 -0
- data/lib/crypto_toolchain/black_boxes/cbc_bitflip_target.rb +33 -0
- data/lib/crypto_toolchain/black_boxes/cbc_iv_equals_key_target.rb +35 -0
- data/lib/crypto_toolchain/black_boxes/cbc_padding_oracle.rb +44 -0
- data/lib/crypto_toolchain/black_boxes/ctr_bitflip_target.rb +32 -0
- data/lib/crypto_toolchain/black_boxes/dsa_keypair.rb +50 -0
- data/lib/crypto_toolchain/black_boxes/ecb_cut_and_paste_target.rb +50 -0
- data/lib/crypto_toolchain/black_boxes/ecb_interpolate_chosen_plaintext_oracle.rb +28 -0
- data/lib/crypto_toolchain/black_boxes/ecb_or_cbc_encryptor.rb +47 -0
- data/lib/crypto_toolchain/black_boxes/ecb_prepend_chosen_plaintext_oracle.rb +23 -0
- data/lib/crypto_toolchain/black_boxes/md4_mac.rb +20 -0
- data/lib/crypto_toolchain/black_boxes/mt_19937_stream_cipher.rb +47 -0
- data/lib/crypto_toolchain/black_boxes/netcat_cbc_padding_oracle.rb +33 -0
- data/lib/crypto_toolchain/black_boxes/rsa_keypair.rb +83 -0
- data/lib/crypto_toolchain/black_boxes/rsa_parity_oracle.rb +14 -0
- data/lib/crypto_toolchain/black_boxes/rsa_unpadded_message_recovery_oracle.rb +24 -0
- data/lib/crypto_toolchain/black_boxes/sha1_mac.rb +20 -0
- data/lib/crypto_toolchain/black_boxes.rb +22 -0
- data/lib/crypto_toolchain/diffie_hellman/messages.rb +53 -0
- data/lib/crypto_toolchain/diffie_hellman/mitm.rb +52 -0
- data/lib/crypto_toolchain/diffie_hellman/peer.rb +130 -0
- data/lib/crypto_toolchain/diffie_hellman/peer_info.rb +43 -0
- data/lib/crypto_toolchain/diffie_hellman/received_message.rb +17 -0
- data/lib/crypto_toolchain/diffie_hellman.rb +10 -0
- data/lib/crypto_toolchain/extensions/integer_extensions.rb +90 -0
- data/lib/crypto_toolchain/extensions/object_extensions.rb +24 -0
- data/lib/crypto_toolchain/extensions/string_extensions.rb +263 -0
- data/lib/crypto_toolchain/extensions.rb +8 -0
- data/lib/crypto_toolchain/srp/client.rb +51 -0
- data/lib/crypto_toolchain/srp/framework.rb +55 -0
- data/lib/crypto_toolchain/srp/server.rb +38 -0
- data/lib/crypto_toolchain/srp/simple_client.rb +32 -0
- data/lib/crypto_toolchain/srp/simple_server.rb +68 -0
- data/lib/crypto_toolchain/srp.rb +14 -0
- data/lib/crypto_toolchain/tools/aes_ctr_recoverer.rb +30 -0
- data/lib/crypto_toolchain/tools/cbc_bitflip_attack.rb +30 -0
- data/lib/crypto_toolchain/tools/cbc_iv_equals_key_attack.rb +30 -0
- data/lib/crypto_toolchain/tools/cbc_padding_oracle_attack.rb +51 -0
- data/lib/crypto_toolchain/tools/ctr_bitflip_attack.rb +24 -0
- data/lib/crypto_toolchain/tools/determine_blocksize.rb +20 -0
- data/lib/crypto_toolchain/tools/dsa_recover_nonce_from_signatures.rb +53 -0
- data/lib/crypto_toolchain/tools/dsa_recover_private_key_from_nonce.rb +39 -0
- data/lib/crypto_toolchain/tools/ecb_cut_and_paste_attack.rb +47 -0
- data/lib/crypto_toolchain/tools/ecb_interpolate_chosen_plaintext_attack.rb +72 -0
- data/lib/crypto_toolchain/tools/ecb_prepend_chosen_plaintext_attack.rb +42 -0
- data/lib/crypto_toolchain/tools/interactive_xor.rb +51 -0
- data/lib/crypto_toolchain/tools/low_exponent_rsa_signature_forgery.rb +27 -0
- data/lib/crypto_toolchain/tools/md4_length_extension_attack.rb +30 -0
- data/lib/crypto_toolchain/tools/mt_19937_seed_recoverer.rb +27 -0
- data/lib/crypto_toolchain/tools/mt_19937_stream_cipher_seed_recoverer.rb +40 -0
- data/lib/crypto_toolchain/tools/rsa_broadcast_attack.rb +21 -0
- data/lib/crypto_toolchain/tools/rsa_parity_oracle_attack.rb +33 -0
- data/lib/crypto_toolchain/tools/rsa_unpadded_message_recovery_attack.rb +49 -0
- data/lib/crypto_toolchain/tools/sha1_length_extension_attack.rb +30 -0
- data/lib/crypto_toolchain/tools.rb +31 -0
- data/lib/crypto_toolchain/utilities/hmac.rb +73 -0
- data/lib/crypto_toolchain/utilities/md4.rb +106 -0
- data/lib/crypto_toolchain/utilities/mt_19937.rb +218 -0
- data/lib/crypto_toolchain/utilities/sha1.rb +95 -0
- data/lib/crypto_toolchain/utilities.rb +9 -0
- data/lib/crypto_toolchain/version.rb +3 -0
- data/lib/crypto_toolchain.rb +34 -0
- metadata +232 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
module CryptoToolchain
|
2
|
+
module BlackBoxes
|
3
|
+
class MT19937StreamCipher
|
4
|
+
MAX_SEED = 0x0000ffff
|
5
|
+
class << self
|
6
|
+
def max_seed
|
7
|
+
@max_seed ||= MAX_SEED
|
8
|
+
end
|
9
|
+
|
10
|
+
def max_seed=(val)
|
11
|
+
@max_seed = val
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.generate_token(length: 32, seed: Time.now.to_i)
|
16
|
+
new("A" * length, seed: seed).keystream.to_base64
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(plaintext, seed: rand(0..(self.class.max_seed)))
|
20
|
+
@seed = seed & self.class.max_seed
|
21
|
+
@prng = CryptoToolchain::Utilities::MT19937.new(@seed)
|
22
|
+
@plaintext = plaintext
|
23
|
+
end
|
24
|
+
|
25
|
+
def encrypt(str = plaintext)
|
26
|
+
str ^ keystream
|
27
|
+
end
|
28
|
+
|
29
|
+
def decrypt(str)
|
30
|
+
str ^ keystream
|
31
|
+
end
|
32
|
+
|
33
|
+
def keystream
|
34
|
+
return @keystream if defined? @keystream
|
35
|
+
_keystream = (0..(plaintext.bytesize / 4)).each_with_object("") do |_, memo|
|
36
|
+
memo << [prng.extract].pack("L")
|
37
|
+
end
|
38
|
+
@keystream = _keystream[0...(plaintext.bytesize)]
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :plaintext, :prng, :seed
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module CryptoToolchain
|
2
|
+
module BlackBoxes
|
3
|
+
class NetcatCbcPaddingOracle
|
4
|
+
|
5
|
+
attr_reader :ciphertext
|
6
|
+
|
7
|
+
def initialize(key: Random.new.bytes(16), iv: Random.new.bytes(16))
|
8
|
+
@key = key
|
9
|
+
@iv = iv
|
10
|
+
@ciphertext = Base64.strict_decode64('SNXIDUFQW0Ul6GXI4NyU/LMHl+vRlVIYp4pvFstfpP1n1C9Xhbl/bNip6mK5l7TMPS+vw247XTYK3LKIGT4AZVh6zUB97fN3fOamkLvzpmA=')
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute(str)
|
14
|
+
handle = IO.popen(["nc", "bufferoverflow.disappointedmama.com", '6767'], "r+")
|
15
|
+
handle.puts(Base64.strict_encode64(str))
|
16
|
+
resp = handle.readpartial(1024).strip
|
17
|
+
handle.close
|
18
|
+
case resp
|
19
|
+
when "Failed to decrypt the message"
|
20
|
+
false
|
21
|
+
when "Successfully received and decrypted the message"
|
22
|
+
true
|
23
|
+
else
|
24
|
+
raise StandardError.new, "Unknown response `#{res}`"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :key, :iv
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
module CryptoToolchain
|
3
|
+
module BlackBoxes
|
4
|
+
class RSAKeypair
|
5
|
+
PrivateKey = Struct.new(:d, :n)
|
6
|
+
PublicKey = Struct.new(:e, :n)
|
7
|
+
|
8
|
+
def initialize(bits: 1024)
|
9
|
+
@bits = bits
|
10
|
+
@p = OpenSSL::BN::generate_prime(bits/2).to_i
|
11
|
+
@q = OpenSSL::BN::generate_prime(bits/2).to_i
|
12
|
+
@n = @p * @q
|
13
|
+
et = (@p-1) * (@q-1)
|
14
|
+
@e = 3
|
15
|
+
@d = @e.invmod(et)
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :e, :bits
|
19
|
+
|
20
|
+
def encrypt(m, to: )
|
21
|
+
raise ArgumentError.new("Message should be a string") unless m.is_a?(String)
|
22
|
+
m.
|
23
|
+
to_number.
|
24
|
+
modpow(to.e, to.n).
|
25
|
+
to_bin_string
|
26
|
+
end
|
27
|
+
|
28
|
+
def decrypt(m)
|
29
|
+
raise ArgumentError.new("Message should be a string") unless m.is_a?(String)
|
30
|
+
m.
|
31
|
+
to_number.
|
32
|
+
modpow(private_key.d, private_key.n).
|
33
|
+
to_bin_string
|
34
|
+
end
|
35
|
+
|
36
|
+
def sign(plaintext)
|
37
|
+
blocksize = bits / 8
|
38
|
+
digest = CryptoToolchain::Utilities::SHA1.digest(plaintext)
|
39
|
+
asn = asn1(:sha1)
|
40
|
+
# the 4 is the mandatory 0x00 0x01 0xff 0x00
|
41
|
+
pad_num = blocksize - ( digest.bytesize + asn.bytesize + 4)
|
42
|
+
block = "\x00\x01\xff" + (0xff.chr * pad_num) + "\x00" + asn + digest
|
43
|
+
decrypt(block)
|
44
|
+
end
|
45
|
+
|
46
|
+
def verify(message, signature: , lazy: true)
|
47
|
+
raise("I can't not be lazy") unless lazy
|
48
|
+
enc = encrypt(signature, to: pubkey)
|
49
|
+
asn = ASN1.fetch(:sha1)
|
50
|
+
regex = /(?<padding>\x01\xff+\x00)(?<asn>.{#{asn.bytesize}})(?<hash>.{20})/m
|
51
|
+
begin
|
52
|
+
_padding, potential_asn, hash = enc.match(regex).captures
|
53
|
+
rescue
|
54
|
+
return false
|
55
|
+
end
|
56
|
+
potential_asn == asn && hash == CryptoToolchain::Utilities::SHA1.digest(message)
|
57
|
+
end
|
58
|
+
|
59
|
+
def public_key
|
60
|
+
@public_key ||= PublicKey.new(@e, @n)
|
61
|
+
end
|
62
|
+
alias_method :pubkey, :public_key
|
63
|
+
|
64
|
+
def private_key
|
65
|
+
@private_key ||= PrivateKey.new(@d, @n)
|
66
|
+
end
|
67
|
+
alias_method :privkey, :private_key
|
68
|
+
|
69
|
+
# Values from
|
70
|
+
# https://stackoverflow.com/questions/3713774/c-sharp-how-to-calculate-asn-1-der-encoding-of-a-particular-hash-algorithm
|
71
|
+
def asn1(hash_type)
|
72
|
+
raise ArgumentError.new("Only sha1 is supported") unless hash_type == :sha1
|
73
|
+
{
|
74
|
+
md5: "0 0\f\x06\b*\x86H\x86\xF7\r\x02\x05\x05\x00\x04\x10",
|
75
|
+
sha1: "0!0\t\x06\x05+\x0E\x03\x02\x1A\x05\x00\x04\x14",
|
76
|
+
sha256: "010\r\x06\t`\x86H\x01e\x03\x04\x02\x01\x05\x00\x04 ",
|
77
|
+
sha384: "0A0\r\x06\t`\x86H\x01e\x03\x04\x02\x02\x05\x00\x040",
|
78
|
+
sha512: "0Q0\r\x06\t`\x86H\x01e\x03\x04\x02\x03\x05\x00\x04@"
|
79
|
+
}.fetch(hash_type)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module CryptoToolchain
|
2
|
+
module BlackBoxes
|
3
|
+
class RSAUnpaddedMessageRecoveryOracle
|
4
|
+
|
5
|
+
attr_reader :keypair
|
6
|
+
|
7
|
+
def initialize(keypair: CryptoToolchain::BlackBoxes::RSAKeypair.new)
|
8
|
+
@keypair = keypair
|
9
|
+
@seen = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute(ciphertext)
|
13
|
+
hsh = Digest::SHA256.hexdigest(ciphertext)
|
14
|
+
raise ArgumentError.new("Already decrypted") if @seen.include?(hsh)
|
15
|
+
@seen << hsh
|
16
|
+
keypair.decrypt(ciphertext)
|
17
|
+
end
|
18
|
+
|
19
|
+
def encrypt(ciphertext)
|
20
|
+
keypair.encrypt(ciphertext, to: keypair.public_key)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module CryptoToolchain
|
2
|
+
module BlackBoxes
|
3
|
+
class SHA1Mac
|
4
|
+
attr_reader :key
|
5
|
+
|
6
|
+
def initialize(key: Random.new.bytes(16))
|
7
|
+
@key = key
|
8
|
+
end
|
9
|
+
|
10
|
+
def mac(str)
|
11
|
+
concat = key + str
|
12
|
+
CryptoToolchain::Utilities::SHA1.hexdigest(concat)
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid?(message: , mac: )
|
16
|
+
self.mac(message) == mac
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "crypto_toolchain/black_boxes/ecb_or_cbc_encryptor"
|
2
|
+
require "crypto_toolchain/black_boxes/ecb_prepend_chosen_plaintext_oracle"
|
3
|
+
require "crypto_toolchain/black_boxes/ecb_interpolate_chosen_plaintext_oracle"
|
4
|
+
require "crypto_toolchain/black_boxes/ecb_cut_and_paste_target"
|
5
|
+
require "crypto_toolchain/black_boxes/cbc_bitflip_target"
|
6
|
+
require "crypto_toolchain/black_boxes/cbc_padding_oracle"
|
7
|
+
require "crypto_toolchain/black_boxes/netcat_cbc_padding_oracle"
|
8
|
+
require "crypto_toolchain/black_boxes/mt_19937_stream_cipher"
|
9
|
+
require "crypto_toolchain/black_boxes/aes_ctr_editor"
|
10
|
+
require "crypto_toolchain/black_boxes/ctr_bitflip_target"
|
11
|
+
require "crypto_toolchain/black_boxes/cbc_iv_equals_key_target"
|
12
|
+
require "crypto_toolchain/black_boxes/sha1_mac"
|
13
|
+
require "crypto_toolchain/black_boxes/md4_mac"
|
14
|
+
require "crypto_toolchain/black_boxes/rsa_keypair"
|
15
|
+
require "crypto_toolchain/black_boxes/rsa_unpadded_message_recovery_oracle"
|
16
|
+
require "crypto_toolchain/black_boxes/rsa_parity_oracle"
|
17
|
+
require "crypto_toolchain/black_boxes/dsa_keypair"
|
18
|
+
|
19
|
+
module CryptoToolchain
|
20
|
+
module BlackBoxes
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module CryptoToolchain
|
2
|
+
module DiffieHellman
|
3
|
+
module Messages
|
4
|
+
class Die ; end
|
5
|
+
|
6
|
+
class PeerAddress
|
7
|
+
def initialize(peer: , channel: , initial: false)
|
8
|
+
@peer = peer
|
9
|
+
@channel = channel
|
10
|
+
@initial = initial
|
11
|
+
end
|
12
|
+
attr_reader :peer, :channel, :initial
|
13
|
+
alias_method :initial?, :initial
|
14
|
+
attr_accessor :pubkey, :shared_secret
|
15
|
+
end
|
16
|
+
|
17
|
+
class KeyExchange
|
18
|
+
def initialize(peer: , pubkey: , p: nil, g: nil, initial: false)
|
19
|
+
if initial && (p.nil? || g.nil?)
|
20
|
+
raise ArgumentError.new("Initial message must provide p and g")
|
21
|
+
end
|
22
|
+
@p = p
|
23
|
+
@g = g
|
24
|
+
@pubkey = pubkey
|
25
|
+
@peer = peer
|
26
|
+
@initial = initial
|
27
|
+
end
|
28
|
+
attr_reader :p, :g, :peer, :pubkey, :initial
|
29
|
+
alias_method :initial?, :initial
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
"PEER: #{peer.name} P: #{p} G: #{g} PUBKEY: #{pubkey % 1000}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Datum
|
37
|
+
def initialize(peer: , contents: , initial: false)
|
38
|
+
@peer = peer
|
39
|
+
@contents = contents
|
40
|
+
@initial = initial
|
41
|
+
end
|
42
|
+
|
43
|
+
def decrypt(key: )
|
44
|
+
iv = contents[0..15]
|
45
|
+
contents[16..-1].decrypt_cbc(key: key, iv: iv)
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :peer, :contents, :initial
|
49
|
+
alias_method :initial?, :initial
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
Thread.abort_on_exception = true
|
2
|
+
module CryptoToolchain
|
3
|
+
module DiffieHellman
|
4
|
+
class MITM < Peer
|
5
|
+
def initialize(debug: false, name: "MITM", p: NIST_P, g: NIST_G, peer_a: , peer_b: , pubkey: nil)
|
6
|
+
@peer_a = peer_a
|
7
|
+
@peer_b = peer_b
|
8
|
+
@pubkey = pubkey
|
9
|
+
super(debug: debug, name: name, p: p, g: g)
|
10
|
+
[peer_a, peer_b].each do |peer|
|
11
|
+
puts "Adding #{peer.name} to #{name} at startup" if debug
|
12
|
+
add_address(peer)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def peer_address_response(msg)
|
17
|
+
send_msg other_peer(msg.peer), my_address_message(initial: msg.initial)
|
18
|
+
end
|
19
|
+
|
20
|
+
def do_key_exchange
|
21
|
+
msg = Messages::KeyExchange.new(peer: self, pubkey: pubkey, p: p, g: g, initial: true)
|
22
|
+
[peer_a, peer_b].each do |peer|
|
23
|
+
info_for(peer).update(p: p, g: g)
|
24
|
+
send_msg(peer, msg)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def key_exchange_response(msg)
|
29
|
+
info = info_for(msg.peer)
|
30
|
+
info.update(pubkey: msg.pubkey)
|
31
|
+
secret_override = invalid_pubkey? ? 0 : nil
|
32
|
+
info.set_shared_secret(privkey, override: secret_override)
|
33
|
+
puts "#{name} generated secret #{info.shared_secret} for #{msg.peer.name}" if debug
|
34
|
+
end
|
35
|
+
|
36
|
+
def datum_response(msg)
|
37
|
+
data = msg.decrypt(key: info_for(msg.peer).session_key)
|
38
|
+
puts "#{name} got message containing #{data} from #{msg.peer.name}" if debug
|
39
|
+
other = other_peer(msg.peer)
|
40
|
+
encrypted = encrypted_message_for(other, message: data, initial: msg.initial)
|
41
|
+
send_msg(other, encrypted)
|
42
|
+
@received_messages << ReceivedMessage.new(from: msg.peer.name, contents: data)
|
43
|
+
end
|
44
|
+
|
45
|
+
def other_peer(peer)
|
46
|
+
peer == peer_a ? peer_b : peer_a
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_reader :peer_a, :peer_b
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module CryptoToolchain
|
2
|
+
module DiffieHellman
|
3
|
+
class Peer
|
4
|
+
def initialize(debug: false, name: SecureRandom.uuid, p: NIST_P, g: NIST_G)
|
5
|
+
@addresses = {}
|
6
|
+
@channel = Queue.new
|
7
|
+
@name = name
|
8
|
+
@debug = debug
|
9
|
+
@p = p
|
10
|
+
@g = g
|
11
|
+
@received_messages = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def process!
|
15
|
+
when_ready do
|
16
|
+
msg = channel.pop
|
17
|
+
message_type = msg.class.to_s.split(':').last
|
18
|
+
if msg.respond_to?(:peer)
|
19
|
+
puts "#{name} got #{message_type} from #{msg.peer.name}" if debug
|
20
|
+
end
|
21
|
+
method = "#{message_type}_response".snakecase
|
22
|
+
unless self.respond_to?(method)
|
23
|
+
raise ArgumentError.new("Don't know how to process method :#{method}")
|
24
|
+
end
|
25
|
+
begin
|
26
|
+
send(method, msg)
|
27
|
+
rescue ReceivedDie
|
28
|
+
break
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
alias_method :process, :process!
|
33
|
+
|
34
|
+
def die_response(msg)
|
35
|
+
raise ReceivedDie
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_address(peer)
|
39
|
+
@addresses[peer.name] ||= PeerInfo.new(peer: peer, channel: peer.channel )
|
40
|
+
end
|
41
|
+
|
42
|
+
def peer_address_response(msg)
|
43
|
+
add_address(msg.peer)
|
44
|
+
if msg.initial?
|
45
|
+
send_msg msg.peer, my_address_message
|
46
|
+
end
|
47
|
+
puts "#{name} added #{msg.peer.name}" if debug
|
48
|
+
end
|
49
|
+
|
50
|
+
def key_exchange_response(msg)
|
51
|
+
info = info_for(msg.peer)
|
52
|
+
if msg.initial?
|
53
|
+
@p = msg.p
|
54
|
+
@g = msg.g
|
55
|
+
end
|
56
|
+
info.update(p: p, g: g, pubkey: msg.pubkey)
|
57
|
+
info.set_shared_secret(privkey)
|
58
|
+
if debug
|
59
|
+
puts "#{name} will use p = #{p}"
|
60
|
+
puts "#{name} will use g = #{g}"
|
61
|
+
puts "#{name} thinks #{msg.peer.name} has pubkey #{msg.pubkey}"
|
62
|
+
puts "#{name} generated secret #{info.shared_secret} for #{msg.peer.name}"
|
63
|
+
end
|
64
|
+
my_pubkey_msg = Messages::KeyExchange.new(peer: self, pubkey: pubkey, initial: false)
|
65
|
+
send_msg msg.peer, my_pubkey_msg if msg.initial?
|
66
|
+
end
|
67
|
+
|
68
|
+
def datum_response(msg)
|
69
|
+
data = msg.decrypt(key: info_for(msg.peer).session_key)
|
70
|
+
puts "#{name} got message containing #{data} from #{msg.peer.name}" if debug
|
71
|
+
if msg.initial?
|
72
|
+
encrypted = encrypted_message_for(msg.peer, message: data, initial: false)
|
73
|
+
send_msg(msg.peer, encrypted)
|
74
|
+
end
|
75
|
+
@received_messages << ReceivedMessage.new(from: msg.peer.name, contents: data)
|
76
|
+
end
|
77
|
+
|
78
|
+
def when_ready
|
79
|
+
loop do
|
80
|
+
while(channel.empty?)
|
81
|
+
sleep 0.001
|
82
|
+
end
|
83
|
+
yield
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def send_msg(peer, message)
|
88
|
+
puts "#{name} sends #{message.class.to_s.split(':').last} to #{peer.name}" if debug
|
89
|
+
peer.channel.enq(message)
|
90
|
+
end
|
91
|
+
|
92
|
+
def info_for(peer)
|
93
|
+
addresses.fetch(peer.name)
|
94
|
+
end
|
95
|
+
|
96
|
+
def pubkey
|
97
|
+
raise RuntimeError.new("Can't generate public key until p has been set") if p.nil?
|
98
|
+
raise RuntimeError.new("Can't generate public key until g has been set") if g.nil?
|
99
|
+
@pubkey ||= g.modexp(privkey, p)
|
100
|
+
end
|
101
|
+
|
102
|
+
def privkey
|
103
|
+
raise RuntimeError.new("Can't generate private key until p has been set") if p.nil?
|
104
|
+
@privkey ||= rand(1..0xffffffff) % p
|
105
|
+
end
|
106
|
+
|
107
|
+
def my_address_message(initial: false)
|
108
|
+
Messages::PeerAddress.new(peer: self, channel: self.channel, initial: initial)
|
109
|
+
end
|
110
|
+
|
111
|
+
def encrypted_message_for(peer, message: , initial: false)
|
112
|
+
key = info_for(peer).session_key
|
113
|
+
iv = Random.new.bytes(16)
|
114
|
+
encrypted = (iv + message.encrypt_cbc(key: key, iv: iv))
|
115
|
+
Messages::Datum.new(peer: self, contents: encrypted, initial: initial)
|
116
|
+
end
|
117
|
+
|
118
|
+
def valid_pubkey?
|
119
|
+
pubkey < p
|
120
|
+
end
|
121
|
+
|
122
|
+
def invalid_pubkey?
|
123
|
+
!valid_pubkey?
|
124
|
+
end
|
125
|
+
|
126
|
+
attr_reader :addresses, :channel, :name, :debug, :p, :g, :received_messages
|
127
|
+
alias_method :debug?, :debug
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module CryptoToolchain
|
2
|
+
module DiffieHellman
|
3
|
+
class PeerInfo
|
4
|
+
def initialize(peer: , channel: , pubkey: nil, p: nil, g: nil)
|
5
|
+
@peer = peer
|
6
|
+
@channel = channel
|
7
|
+
@pubkey = pubkey
|
8
|
+
@p = p
|
9
|
+
@g = g
|
10
|
+
end
|
11
|
+
attr_reader :peer, :channel, :shared_secret
|
12
|
+
attr_accessor :p, :g, :pubkey
|
13
|
+
|
14
|
+
def to_h
|
15
|
+
{
|
16
|
+
name: peer.name,
|
17
|
+
p: p,
|
18
|
+
g: g,
|
19
|
+
pubkey: pubkey,
|
20
|
+
secret: shared_secret
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def set_shared_secret(privkey, override: nil)
|
25
|
+
@shared_secret = override ? override : pubkey.modexp(privkey, p)
|
26
|
+
end
|
27
|
+
|
28
|
+
def session_key
|
29
|
+
if shared_secret.nil?
|
30
|
+
raise ArgumentError.new("Session key requires a shared secret")
|
31
|
+
end
|
32
|
+
@session_key ||= CryptoToolchain::Utilities::SHA1.bindigest(shared_secret.to_s)[0..15]
|
33
|
+
end
|
34
|
+
|
35
|
+
def update(hsh)
|
36
|
+
hsh.each do |k, v|
|
37
|
+
self.send("#{k}=", v) unless v.nil?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module CryptoToolchain
|
2
|
+
module DiffieHellman
|
3
|
+
class ReceivedMessage
|
4
|
+
def initialize(from: , contents: )
|
5
|
+
@from = from
|
6
|
+
@contents = contents
|
7
|
+
end
|
8
|
+
|
9
|
+
def ==(other)
|
10
|
+
unless other.is_a?(CryptoToolchain::DiffieHellman::ReceivedMessage)
|
11
|
+
raise ArgumentError.new("Cannot coerce #{other.clas} to ReceivedMessage")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
attr_reader :from, :contents
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require "crypto_toolchain/diffie_hellman/messages"
|
2
|
+
require "crypto_toolchain/diffie_hellman/peer_info"
|
3
|
+
require "crypto_toolchain/diffie_hellman/peer"
|
4
|
+
require "crypto_toolchain/diffie_hellman/mitm"
|
5
|
+
require "crypto_toolchain/diffie_hellman/received_message"
|
6
|
+
module CryptoToolchain
|
7
|
+
module DiffieHellman
|
8
|
+
ReceivedDie = Class.new(RuntimeError)
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
class Integer
|
3
|
+
def to_hex_string
|
4
|
+
ret = to_s(16)
|
5
|
+
if ret.length.odd?
|
6
|
+
ret = "0#{ret}"
|
7
|
+
end
|
8
|
+
ret
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_bin_string
|
12
|
+
to_hex_string.from_hex
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_bits(pack_arg = "L>")
|
16
|
+
[self].pack(pack_arg).to_bits
|
17
|
+
end
|
18
|
+
|
19
|
+
def lrot(num)
|
20
|
+
((self << num) & 0xffffffff) |
|
21
|
+
((self & 0xffffffff) >> (32 - num))
|
22
|
+
end
|
23
|
+
|
24
|
+
def rrot(num)
|
25
|
+
((self & 0xffffffff) >> num) |
|
26
|
+
((self << (32 - num)) & 0xffffffff)
|
27
|
+
end
|
28
|
+
|
29
|
+
# From Wikipedia:
|
30
|
+
# https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Pseudocode
|
31
|
+
def invmod(n)
|
32
|
+
a = self
|
33
|
+
t = 0
|
34
|
+
new_t = 1
|
35
|
+
r = n
|
36
|
+
new_r = a
|
37
|
+
while new_r != 0
|
38
|
+
quotient = r / new_r
|
39
|
+
t, new_t = new_t, (t - quotient * new_t)
|
40
|
+
r, new_r = new_r, (r - quotient * new_r)
|
41
|
+
end
|
42
|
+
raise ArgumentError.new("#{self} is not invertible") if r > 1
|
43
|
+
t += n if t < 0
|
44
|
+
t
|
45
|
+
end
|
46
|
+
alias_method :mod_inverse, :invmod
|
47
|
+
alias_method :modinv, :invmod
|
48
|
+
|
49
|
+
# https://rosettacode.org/wiki/Nth_root#Ruby
|
50
|
+
# (with modifications)
|
51
|
+
ROUNDING = %i(up down none)
|
52
|
+
def root(n, round: :down)
|
53
|
+
raise ArgumentError.new("round must be in [#{ROUNDING.join(', ')}]") unless ROUNDING.include?(round)
|
54
|
+
raise ArgumentError.new("Can't be called on 0") if self == 0
|
55
|
+
x = self
|
56
|
+
loop do
|
57
|
+
prev = x
|
58
|
+
x = ((n - 1) * prev) + (self / (prev ** (n - 1)))
|
59
|
+
x /= n
|
60
|
+
break if (prev - x) <= 0
|
61
|
+
end
|
62
|
+
if x**n == self
|
63
|
+
x
|
64
|
+
else
|
65
|
+
case round
|
66
|
+
when :up
|
67
|
+
x+1
|
68
|
+
when :down
|
69
|
+
x
|
70
|
+
when :none
|
71
|
+
raise ArgumentError.new("#{self} has no #{n}th root")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def modexp(exponent, mod)
|
77
|
+
raise ArgumentError.new("Exponent must be non-negative") if exponent < 0
|
78
|
+
product = 1
|
79
|
+
base = self % mod
|
80
|
+
while exponent > 0
|
81
|
+
if exponent & 0x01 == 1
|
82
|
+
product = (product * base) % mod
|
83
|
+
end
|
84
|
+
exponent = exponent >> 1
|
85
|
+
base = (base**2) % mod
|
86
|
+
end
|
87
|
+
product
|
88
|
+
end
|
89
|
+
alias_method :modpow, :modexp
|
90
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
class Object
|
3
|
+
# Transcribed from http://cryptopals.com/sets/5/challenges/40
|
4
|
+
def chinese_remainder(residues, mods)
|
5
|
+
mod_product = ->(without) { mods.inject(:*) / without }
|
6
|
+
sum = 0
|
7
|
+
residues.zip(mods) do |(residue, mod)|
|
8
|
+
mp = mod_product.call(mod)
|
9
|
+
sum += residue * mp * mp.invmod(mod)
|
10
|
+
end
|
11
|
+
sum % mods.inject(:*)
|
12
|
+
end
|
13
|
+
|
14
|
+
def numberize(n)
|
15
|
+
if n.respond_to?(:to_number)
|
16
|
+
n.to_number
|
17
|
+
elsif n.is_a?(Numeric)
|
18
|
+
n
|
19
|
+
else
|
20
|
+
raise ArgumentError, "#{n} cannot be numberized"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|