|
| 1 | +class ECE |
| 2 | + |
| 3 | + KEY_LENGTH=16 |
| 4 | + TAG_LENGTH=16 |
| 5 | + NONCE_LENGTH=12 |
| 6 | + SHA256_LENGTH=32 |
| 7 | + |
| 8 | + def self.hmac_hash(key, input) |
| 9 | + digest = OpenSSL::Digest.new('sha256') |
| 10 | + OpenSSL::HMAC.digest(digest, key, input) |
| 11 | + end |
| 12 | + |
| 13 | + def self.hkdf_extract(salt, ikm) #ikm stays for input keying material |
| 14 | + hmac_hash(salt,ikm) |
| 15 | + end |
| 16 | + |
| 17 | + def self.get_info(type, client_public, server_public) |
| 18 | + cl_len_no = [client_public.size].pack('n') |
| 19 | + sv_len_no = [server_public.size].pack('n') |
| 20 | + "Content-Encoding: #{type}\x00P-256\x00#{cl_len_no}#{client_public}#{sv_len_no}#{server_public}" |
| 21 | + end |
| 22 | + |
| 23 | + def self.extract_key(params) |
| 24 | + raise "Salt must be 16-bytes long" unless params[:salt].length==16 |
| 25 | + |
| 26 | + input_key = params[:key] |
| 27 | + auth = false |
| 28 | + if params.has_key?(:auth) # Encrypted Content Encoding, March 11 2016, http://httpwg.org/http-extensions/draft-ietf-httpbis-encryption-encoding.html |
| 29 | + auth = true |
| 30 | + input = HKDF.new(input_key, {salt: params[:auth] , algorithm: 'sha256', info: "Content-Encoding: auth\x00"}) |
| 31 | + input_key = input.next_bytes(SHA256_LENGTH) |
| 32 | + secret = HKDF.new(input_key, {salt: params[:salt], algorithm: 'sha256', info: get_info("aesgcm", params[:user_public_key], params[:server_public_key])}) |
| 33 | + nonce = HKDF.new(input_key, salt: params[:salt], algorithm: 'sha256', info: get_info("nonce", params[:user_public_key], params[:server_public_key])) |
| 34 | + else |
| 35 | + secret = HKDF.new(input_key, {salt: params[:salt], algorithm: 'sha256', info: "Content-Encoding: aesgcm128"}) |
| 36 | + nonce = HKDF.new(input_key, salt: params[:salt], algorithm: 'sha256', info: "Content-Encoding: nonce") |
| 37 | + end |
| 38 | + |
| 39 | + {key: secret.next_bytes(KEY_LENGTH), nonce: nonce.next_bytes(NONCE_LENGTH), auth: auth} |
| 40 | + end |
| 41 | + |
| 42 | + def self.generate_nonce(nonce, counter) |
| 43 | + raise "Nonce must be #{NONCE_LENGTH} bytes long." unless nonce.length == NONCE_LENGTH |
| 44 | + output = nonce.dup |
| 45 | + integer = nonce[-6..-1].unpack('B*')[0].to_i(2) #taking last 6 bytes, treating as integer |
| 46 | + x = ((integer ^ counter) & 0xffffff) + ((((integer / 0x1000000) ^ (counter / 0x1000000)) & 0xffffff) * 0x1000000) |
| 47 | + bytestring = x.to_s(16).length < 12 ? "0"*(12-x.to_s(16).length)+x.to_s(16) : x.to_s(16) #it's for correct handling of cases when generated integer is less than 6 bytes |
| 48 | + output[-6..-1] = [bytestring].pack('H*') #without it packing would produce less than 6 bytes |
| 49 | + output #I didn't find pack directive for such usage, so there is a such solution |
| 50 | + end |
| 51 | + |
| 52 | + def self.encrypt(data, params) |
| 53 | + key = extract_key(params) |
| 54 | + rs = params[:rs] ? params [:rs] : 4096 |
| 55 | + padsize = params[:padsize] ? params [:padsize] : 0 |
| 56 | + raise "The rs parameter must be greater than 1." if rs <= 1 |
| 57 | + rs -=1 #this ensures encrypted data cannot be truncated |
| 58 | + result = "" |
| 59 | + pad_bytes = 1 |
| 60 | + if params[:auth] # old spec used 1 byte for padding, latest one always uses 2 bytes |
| 61 | + pad_bytes = 2 |
| 62 | + end |
| 63 | + |
| 64 | + counter = 0 |
| 65 | + (0..data.length).step(rs-pad_bytes+1) do |i| |
| 66 | + block = encrypt_record(key, counter, data[i..i+rs-pad_bytes], padsize) |
| 67 | + result += block |
| 68 | + counter +=1 |
| 69 | + end |
| 70 | + result |
| 71 | + end |
| 72 | + |
| 73 | + def self.decrypt(data, params) |
| 74 | + key = extract_key(params) |
| 75 | + rs = params[:rs] ? params [:rs] : 4096 |
| 76 | + raise "The rs parameter must be greater than 1." if rs <= 1 |
| 77 | + rs += TAG_LENGTH |
| 78 | + raise "Message is truncated" if data.length % rs == 0 |
| 79 | + result = "" |
| 80 | + counter = 0 |
| 81 | + (0..data.length).step(rs) do |i| |
| 82 | + block = decrypt_record(key, counter, data[i..i+rs-1]) |
| 83 | + result += block |
| 84 | + counter +=1 |
| 85 | + end |
| 86 | + result |
| 87 | + end |
| 88 | + |
| 89 | + def self.decrypt_record(params, counter, buffer, pad=0) |
| 90 | + gcm = OpenSSL::Cipher.new('aes-128-gcm') |
| 91 | + gcm.decrypt |
| 92 | + gcm.key = params[:key] |
| 93 | + gcm.iv = generate_nonce(params[:nonce], counter) |
| 94 | + pad_bytes = 1 |
| 95 | + if params[:auth] # old spec used 1 byte for padding, latest one always uses 2 bytes |
| 96 | + pad_bytes = 2 |
| 97 | + end |
| 98 | + raise "Block is too small" if buffer.length <= TAG_LENGTH+pad_bytes |
| 99 | + gcm.auth_tag = buffer[-TAG_LENGTH..-1] |
| 100 | + decrypted = gcm.update(buffer[0..-TAG_LENGTH-1]) + gcm.final |
| 101 | + |
| 102 | + if params[:auth] |
| 103 | + padding_length = decrypted[0..1].unpack("n")[0] |
| 104 | + raise "Padding is too big" if padding_length+2 > decrypted.length |
| 105 | + padding = decrypted[2..padding_length] |
| 106 | + raise "Wrong padding" unless padding = "\x00"*padding_length |
| 107 | + return decrypted[2+padding_length..-1] |
| 108 | + else |
| 109 | + padding_length = decrypted[0].unpack("C")[0] |
| 110 | + raise "Padding is too big" if padding_length+1 > decrypted.length |
| 111 | + padding = decrypted[1..padding_length] |
| 112 | + raise "Wrong padding" unless padding = "\x00"*padding_length |
| 113 | + return decrypted[1..-1] |
| 114 | + end |
| 115 | + end |
| 116 | + |
| 117 | + def self.encrypt_record(params, counter, buffer, pad=0) |
| 118 | + gcm = OpenSSL::Cipher.new('aes-128-gcm') |
| 119 | + gcm.encrypt |
| 120 | + gcm.key = params[:key] |
| 121 | + gcm.iv = generate_nonce(params[:nonce], counter) |
| 122 | + gcm.auth_data = "" |
| 123 | + padding = "" |
| 124 | + if params[:auth] |
| 125 | + padding = [pad].pack('n') + "\x00"*pad # 2 bytes, big endian, then n zero bytes of padding |
| 126 | + buf = padding+buffer |
| 127 | + record = gcm.update(buf) |
| 128 | + else |
| 129 | + record = gcm.update("\x00"+buffer) # 1 padding byte, not fully implemented |
| 130 | + end |
| 131 | + enc = record + gcm.final + gcm.auth_tag |
| 132 | + enc |
| 133 | + end |
| 134 | + |
| 135 | + |
| 136 | +end |
0 commit comments