Last active
October 8, 2021 10:45
-
-
Save jsanders/6002502 to your computer and use it in GitHub Desktop.
Implementation of AES with counter (CTR) and cipher-block-chaining (CBC) modes. Based on spec at http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf. ** Disclaimer: For educational purposes only. Obviously, nobody should ever use a hacky un-vetted non-standard (not to mention, non-optimized) implementation of crypto like this **
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require File.expand_path('../../utilities', __FILE__) | |
require 'openssl' | |
# Set to true to see debug output | |
DEBUG = false | |
def debug_puts(s=nil); puts(s) if DEBUG; end | |
def debug_print(s=nil); print(s) if DEBUG; end | |
# Encrypt data using given `mode`, `key_b`, `iv_b` and `data_b`, all as byte arrays | |
# Only uses padding in CBC mode | |
def openssl_aes_128(encrypt_or_decrypt, mode, key_b, iv_b, data_b) | |
key_s, data_s = b2s(key_b), b2s(data_b) | |
aes = OpenSSL::Cipher::AES.new(128, mode) | |
aes.send(encrypt_or_decrypt) | |
aes.padding = mode == 'CBC' ? 1 : 0 | |
aes.key = key_s | |
aes.iv = b2s(iv_b) if iv_b | |
encrypted_s = aes.update(data_s) + aes.final | |
s2b(encrypted_s) | |
end | |
SBOX = { | |
:encrypt => [ | |
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, | |
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, | |
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, | |
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, | |
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, | |
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, | |
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, | |
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, | |
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, | |
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB, | |
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, | |
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, | |
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, | |
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, | |
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, | |
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16 | |
], | |
:decrypt => [ | |
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB, | |
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB, | |
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E, | |
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25, | |
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92, | |
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84, | |
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06, | |
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B, | |
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73, | |
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E, | |
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B, | |
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4, | |
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F, | |
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF, | |
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61, | |
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D | |
] | |
} | |
def sub_bytes(state, encrypt_or_decrypt) | |
state.each_with_index do | column, column_index | | |
column.each_with_index do | byte, row_index | | |
state[column_index][row_index] = SBOX[encrypt_or_decrypt][byte] | |
end | |
end | |
print_aes_state('subbytes', state) | |
state | |
end | |
def shift_rows(state, encrypt_or_decrypt) | |
direction = encrypt_or_decrypt == :encrypt ? 1 : -1 | |
state = state.transpose.each_with_index.map do | row, index | | |
# Rotate the i'th row by i in the proper direction | |
row.rotate(index * direction) | |
end.transpose | |
print_aes_state('shiftrows', state) | |
state | |
end | |
# Multiplication of `a` and `b` in GF(2^n) for given `n` and | |
# characteristic polynomial `characteristic` given in binary | |
def galois_2_mult(a, b, n, characteristic) | |
p = 0 | |
low_mask = 1 | |
high_mask = (1 << (n-1)) | |
n.times do | |
break if a.zero? or b.zero? | |
if (b & low_mask) == low_mask | |
# Add a to product | |
p ^= a | |
end | |
# Divide b by x | |
b >>= 1 | |
# If the coefficient of x^(n-1) is 1, the multiplication below will overflow | |
carry = a & high_mask | |
# Multiply a by x | |
a <<= 1 | |
if carry == high_mask | |
# Modulo the characteristic polynomial | |
a ^= characteristic | |
end | |
end | |
p | |
end | |
def aes_gmult(a, b) | |
galois_2_mult(a, b, 8, 0x11b) | |
end | |
# AES rcon value for i. This is 2^(i-1) in GF(2^8) with characteristic 0x11b | |
def rcon(i) | |
return 0 if i == 0 | |
exp = 1 | |
while i > 1 | |
exp = aes_gmult(exp, 2) | |
i -= 1 | |
end | |
exp | |
end | |
def mix_column(column, encrypt_or_decrypt) | |
matrix = if encrypt_or_decrypt == :encrypt | |
[[ 0x02, 0x03, 0x01, 0x01 ], | |
[ 0x01, 0x02, 0x03, 0x01 ], | |
[ 0x01, 0x01, 0x02, 0x03 ], | |
[ 0x03, 0x01, 0x01, 0x02 ]] | |
else | |
[[ 0x0e, 0x0b, 0x0d, 0x09 ], | |
[ 0x09, 0x0e, 0x0b, 0x0d ], | |
[ 0x0d, 0x09, 0x0e, 0x0b ], | |
[ 0x0b, 0x0d, 0x09, 0x0e ]] | |
end | |
matrix.reduce([]) do | result, row | | |
result << row.each_with_index.reduce(0) do | acc, (coeff, index) | | |
acc ^= aes_gmult(coeff, column[index]) | |
end | |
end | |
end | |
def mix_columns(state, encrypt_or_decrypt) | |
state.each_with_index do | column, index | | |
state[index] = mix_column(column, encrypt_or_decrypt) | |
end | |
print_aes_state('mixcolumns', state) | |
state | |
end | |
def print_word(which, word) | |
debug_puts "#{which}: #{b2h(word)}" | |
end | |
# Core key expansion transformation | |
def transform_word(word_b, round) | |
temp = word_b | |
temp = temp.rotate | |
print_word('rotword', temp) | |
temp = temp.map { | b | SBOX[:encrypt][b] } | |
print_word('subword', temp) | |
rcon = rcon(round) | |
print_word('rcon', [rcon, 0, 0, 0]) | |
temp[0] ^= rcon | |
print_word('afterrcon', temp) | |
temp | |
end | |
# Takes 16-byte key and expands to 10 16-byte round keys | |
def expand_key(key_b) | |
debug_puts "--- KEY EXPANSION ---" | |
key_words = key_b.each_slice(4).to_a | |
# First four words of expansion is the cipher key | |
expanded = key_words | |
(1..10).each do | round | | |
"--- KEY #{round} ---" | |
# Start with transformation of previous word | |
temp = expanded[-1] | |
print_word('temp', temp) | |
temp = transform_word(temp, round) | |
4.times do | |
# Append four words | |
prev = expanded[-4] | |
print_word('prev', prev) | |
current = xorb(temp, prev) | |
print_word('current', current) | |
expanded << current | |
temp = current | |
end | |
end | |
# Return 11 16-byte blocks | |
expanded.each_slice(4).map(&:flatten) | |
end | |
def add_round_key(state, round_key) | |
round_key_state = round_key.each_slice(4).to_a | |
print_aes_state('roundkey', round_key_state) | |
state.each_with_index do | column, index | | |
state[index] = xorb(column, round_key_state[index]) | |
end | |
print_aes_state('addroundkey', state) | |
state | |
end | |
def print_aes_state(which, state) | |
debug_puts "#{which}:" | |
(0...4).each { | row | (0...4).each { | column | debug_print "%02x " % state[column][row] }; debug_puts };nil | |
end | |
def custom_aes_128_decrypt(rounds, state, key_schedule) | |
debug_puts "--- START DECRYPTION ---" | |
# Run keys in reverse order for decryption | |
key_schedule = key_schedule.reverse | |
add_round_key(state, key_schedule[0]) | |
(1..rounds).each do | round | | |
debug_puts "--- ROUND #{round} ---" | |
state = shift_rows(state, :decrypt) | |
state = sub_bytes(state, :decrypt) | |
state = add_round_key(state, key_schedule[round]) | |
state = mix_columns(state, :decrypt) unless round == rounds # No MixColumns on last round | |
end | |
print_aes_state('final', state) | |
state.flatten | |
end | |
def custom_aes_128_encrypt(rounds, state, key_schedule) | |
debug_puts "--- START ENCRYPTION ---" | |
add_round_key(state, key_schedule[0]) | |
(1..rounds).each do | round | | |
debug_puts "--- ROUND #{round} ---" | |
state = sub_bytes(state, :encrypt) | |
state = shift_rows(state, :encrypt) | |
state = mix_columns(state, :encrypt) unless round == rounds # No MixColumns on last round | |
state = add_round_key(state, key_schedule[round]) | |
end | |
print_aes_state('final', state) | |
state.flatten | |
end | |
# Encrypt or decrypt one block of data using AES without openssl | |
def custom_aes_128(encrypt_or_decrypt, key_b, block_b) | |
debug_puts "--- START ---" | |
# AES-128 uses 10 rounds | |
rounds = 10 | |
# First index into `state` is column, second is row | |
state = block_b.each_slice(4).to_a | |
print_aes_state('start', state) | |
key_schedule = expand_key(key_b) | |
case encrypt_or_decrypt | |
when :encrypt | |
custom_aes_128_encrypt(rounds, state, key_schedule) | |
when :decrypt | |
custom_aes_128_decrypt(rounds, state, key_schedule) | |
else | |
raise "Must either encrypt or decrypt" | |
end | |
end | |
# Encrypt or decrypt one AES block using `strategy` of either openssl or custom | |
def aes_128_block(encrypt_or_decrypt, key_b, block_b, strategy = :custom) | |
raise "16-byte block required" unless block_b.size == 16 | |
raise "16-byte key required" unless key_b.size == 16 | |
case strategy | |
when :openssl | |
openssl_aes_128(encrypt_or_decrypt, 'ECB', key_b, nil, block_b) | |
when :custom | |
custom_aes_128(encrypt_or_decrypt, key_b, block_b) | |
else | |
raise "Unknown AES strategy #{strategy}" | |
end | |
end | |
# Generate a stream cipher key of length `length` bytes | |
# using given AES `key_b` and initialization vector `iv_b` | |
def aes_ctr_stream_key(key_b, iv_b, length) | |
iv_i = b2i(iv_b) | |
stream_key_b = [] | |
while stream_key_b.length < length | |
stream_key_b += aes_128_block(:encrypt, key_b, i2b(iv_i)) | |
iv_i += 1 | |
end | |
stream_key_b.take(length) | |
end | |
def do_ctr(key_b, iv_b, data_b) | |
# Get a properly sized stream cipher key using AES-CTR | |
stream_key_b = aes_ctr_stream_key(key_b, iv_b, data_b.length) | |
# Stream-cipher decryption - simple xor | |
xorb(stream_key_b, data_b) | |
end | |
# Encrypt plaintext with AES-128-CTR using given key and IV | |
# Input: 16-byte AES key `key_h` as hex string, | |
# 16-byte initialization vector `iv_h` as hex string, | |
# Arbitrary length plaintext `pt_s` as ASCII text | |
# Output: Encrypted text with prepended initialization vector as hex string | |
def encrypt_ctr(key_h, iv_h, pt_s) | |
key_b, iv_b, pt_b = h2b(key_h), h2b(iv_h), s2b(pt_s) | |
encrypted_b = do_ctr(key_b, iv_b, pt_b) | |
b2h(iv_b + encrypted_b) | |
end | |
# Decrypt AES-128-CTR encrypted ciphertext | |
# Input: 16-byte AES hex string `key_h`, | |
# Arbitrary length ciphertext hex string `ct_h` | |
# Output: Plain text decryption | |
def decrypt_ctr(key_h, ct_h) | |
key_b, ct_b = h2b(key_h), h2b(ct_h) | |
# Pull off initialization vector in first 16 bytes of ciphertext | |
iv_b, ct_b = ct_b.take(16), ct_b.drop(16) | |
plaintext_b = do_ctr(key_b, iv_b, ct_b) | |
b2s(plaintext_b) | |
end | |
# Encrypt plaintext with AES-128-CBC using given key and IV | |
# Input: 16-byte AES key `key_h` as hex string, | |
# 16-byte initialization vector `iv_h` as hex string, | |
# Arbitrary length plaintext `pt_s` as ASCII text | |
# Output: Encrypted text with prepended initialization vector as hex string | |
def encrypt_cbc(key_h, iv_h, pt_s) | |
key_b, iv_b, pt_b = h2b(key_h), h2b(iv_h), s2b(pt_s) | |
ct_b = [] | |
last_block_b = iv_b | |
padded = false | |
pt_b.each_slice(16) do | block_b | | |
if (pad_amount = (16 - block_b.size)) > 0 | |
padded = true | |
block_b += [ pad_amount ] * pad_amount | |
end | |
encrypted_block_b = aes_128_block(:encrypt, key_b, xorb(last_block_b, block_b)) | |
ct_b += encrypted_block_b | |
last_block_b = encrypted_block_b | |
end | |
# Add dummy block if needed | |
ct_b += [ 16 ] * 16 unless padded | |
b2h(iv_b + ct_b) | |
end | |
# Decrypt AES-128-CBC encrypted ciphertext | |
# Input: 16-byte AES hex string `key_h`, | |
# Arbitrary length ciphertext hex string `ct_h` | |
# Output: Plain text decryption | |
def decrypt_cbc(key_h, ct_h) | |
key_b, ct_b = h2b(key_h), h2b(ct_h) | |
last_block_b, *blocks_b = ct_b.each_slice(16).to_a | |
plaintext_b = [] | |
blocks_b.each do | block_b | | |
plaintext_b += xorb(aes_128_block(:decrypt, key_b, block_b), last_block_b) | |
last_block_b = block_b | |
end | |
# Strip padding | |
pad_amount = plaintext_b.last | |
unpadded_b = plaintext_b[0...(-pad_amount)] | |
b2s(unpadded_b) | |
end | |
# TESTS | |
FAIL = RuntimeError.new('Failed assertion') | |
def test_ctr | |
pt_s = 'CTR mode lets you build a stream cipher from a block cipher.'; pt_b = s2b(pt_s) | |
key_h = '36f18357be4dbd77f050515c73fcf9f2'; key_b = h2b(key_h) | |
ct_h = '69dda8455c7dd4254bf353b773304eec0ec7702330098ce7f7520d1cbbb20fc388d1b0adb5054dbd7370849dbf0b88d393f252e764f1f5f7ad97ef79d59ce29f5f51eeca32eabedd9afa9329' | |
iv_h = ct_h[0...32]; iv_b = h2b(iv_h) # 16 bytes | |
openssl_decrypted_s = b2s(openssl_aes_128(:decrypt, 'CTR', key_b, iv_b, h2b(ct_h[32..-1]))) | |
our_decrypted_s = decrypt_ctr(key_h, ct_h) | |
raise FAIL unless our_decrypted_s == openssl_decrypted_s and our_decrypted_s == pt_s | |
openssl_encrypted_h = (iv_h + b2h(openssl_aes_128(:encrypt, 'CTR', key_b, iv_b, pt_b))) | |
our_encrypted_h = encrypt_ctr(key_h, iv_h, pt_s) | |
raise FAIL unless our_encrypted_h == openssl_encrypted_h and our_encrypted_h == ct_h | |
raise FAIL unless decrypt_ctr(key_h, encrypt_ctr(key_h, iv_h, 'some other string')) == 'some other string' | |
"CTR works!" | |
end | |
def test_cbc | |
pt_s = 'Basic CBC mode encryption needs padding.'; pt_b = s2b(pt_s) | |
key_h = '140b41b22a29beb4061bda66b6747e14'; key_b = h2b(key_h) | |
ct_h = '4ca00ff4c898d61e1edbf1800618fb2828a226d160dad07883d04e008a7897ee2e4b7465d5290d0c0e6c6822236e1daafb94ffe0c5da05d9476be028ad7c1d81' | |
iv_h = ct_h[0...32]; iv_b = h2b(iv_h) # 16 bytes | |
openssl_decrypted_s = b2s(openssl_aes_128(:decrypt, 'CBC', key_b, iv_b, h2b(ct_h[32..-1]))) | |
our_decrypted_s = decrypt_cbc(key_h, ct_h) | |
raise FAIL unless our_decrypted_s == openssl_decrypted_s and our_decrypted_s == pt_s | |
openssl_encrypted_h = (iv_h + b2h(openssl_aes_128(:encrypt, 'CBC', key_b, iv_b, pt_b))) | |
our_encrypted_h = encrypt_cbc(key_h, iv_h, pt_s) | |
raise FAIL unless our_encrypted_h == openssl_encrypted_h and our_encrypted_h == ct_h | |
raise FAIL unless decrypt_cbc(key_h, encrypt_cbc(key_h, iv_h, 'some other string')) == 'some other string' | |
"CBC works!" | |
end | |
puts test_ctr | |
puts test_cbc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Turn a byte array into a number | |
def b2i(b) | |
# This bugs me but it seems like the easiest way to do this is to go to hex first | |
h2i(b2h(b)) | |
end | |
# Turn a number into a byte array | |
def i2b(i) | |
# This bugs me but it seems like the easiest way to do this is to go to hex first | |
h2b(i2h(i)) | |
end | |
# Turn a byte array into a string | |
def b2s(b) | |
b.pack('C*') | |
end | |
# Turn a string into a byte array | |
def s2b(s) | |
s.unpack('C*') | |
end | |
# Turn a hex string into a number | |
def h2i(h) | |
h.hex | |
end | |
# Turn a number into a hex string | |
def i2h(i) | |
"%02x" % i | |
end | |
# Turn a hex string into a byte buffer | |
def h2b(h) | |
h.scan(/../).map { | h | h2i(h) } | |
end | |
# Turn a byte buffer into a hex string | |
def b2h(b) | |
b.map { | i | i2h(i) }.join | |
end | |
# Turn a byte array into a byte string | |
def b2s(b) | |
b.pack('C*') | |
end | |
# Turn a byte string into a byte array | |
def s2b(s) | |
s.unpack('C*') | |
end | |
# Turn a hex string into a byte string | |
def h2s(h) | |
[h].pack('H*') | |
end | |
# Turn a byte string into a hex string | |
def s2h(s) | |
s.unpack('H*').first | |
end | |
# Input byte arrays to be xor'd | |
# Output byte array of the result | |
def xorb(buf1, buf2) | |
buf1.zip(buf2).map { | b1, b2 | (b1 ^ b2) if b1 && b2 }.compact | |
end | |
# Input two hex strings | |
# Output their xor as hex string | |
def xorh(h1, h2) | |
b2h(xorb(h2b(h1), h2b(h2))) | |
end | |
# Input actual text for pt, ct as hex string | |
# Output xor as hex string | |
def xorsh(pt, ct) | |
xorh(s2h(pt), ct) | |
end | |
# Return `howmany` hex "bytes" from `h`, starting at the `which`th | |
def subh(h, which, howmany = nil) | |
if howmany | |
h[which*2...howmany*2] | |
else | |
h[which*2..-1] | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment