gcm.ha (6642B)
1 // SPDX-License-Identifier: MPL-2.0 2 // (c) Hare authors <https://harelang.org> 3 4 use bytes; 5 use crypto::math::{xor,eqslice}; 6 use endian::{beputu64, beputu32, begetu32}; 7 use errors; 8 use io; 9 use types; 10 11 def GCMBLOCKSZ: size = 16; 12 13 export def GCMTAGSZ: size = 16; 14 15 export type gcmstream = struct { 16 stream: io::stream, 17 block: nullable *block, 18 handle: io::handle, 19 tagbuf: [GCMBLOCKSZ]u8, 20 xorbuf: [GCMBLOCKSZ]u8, 21 cipherbuf: [GCMBLOCKSZ]u8, 22 y0: [GCMBLOCKSZ]u8, 23 h: [GCMBLOCKSZ]u8, 24 y: u32, 25 xorbufpos: size, 26 adlen: u64, 27 clen: u64, 28 }; 29 30 const gcm_vtable: io::vtable = io::vtable { 31 writer = &gcm_writer, 32 reader = &gcm_reader, 33 closer = &gcm_closer, 34 ... 35 }; 36 37 // Creates a Galois Counter Mode (GCM) io::stream which can be used for 38 // encryption (by encrypting writes to the underlying handle) or decryption (or 39 // by decrypting reads from the underlying handle), but not both. [[gcm_init]] 40 // must be called to initialize the stream, before reading or writing. To 41 // authenticate the encrypted data an authentication tag must be created using 42 // [[gcm_seal]] after the encryption step. The authentication tag must be passed 43 // to [[gcm_verify]] after decryption to make sure that the encrypted and 44 // additional data were not modified. In case of a verification fail the 45 // decrypted data must not be trusted and hence discarded. 46 // 47 // A maximum of 2**36-32 bytes may be encrypted. 48 // 49 // The user must call [[io::close]] when they are done using the stream to 50 // securely erase secret information stored in the stream state. Close will 51 // also finish the 'block' provided by [[gcm_init]]. If the 'block' should 52 // not be finished, [[gcm_unlink_block]] must be called before close. 53 export fn gcm() gcmstream = { 54 return gcmstream { 55 stream = &gcm_vtable, 56 handle = 0, 57 ... 58 }; 59 }; 60 61 // Initialises the gcmstream. The data will be encrypted to or encrypted from 62 // the given 'handle' The implementation only supports a block cipher 'b' with a 63 // block size of 16 bytes. The initialization vector (nonce) 'iv' may have any 64 // size up to 2**61 bytes. 12 bytes is the recommended size, if efficiency is 65 // critical. The additional data 'ad' will be authenticated but not encrypted 66 // and may have a maximum length of 2**61 - 1 bytes. 'ad' will not be written to 67 // the underlying handle. 68 export fn gcm_init( 69 s: *gcmstream, 70 handle: io::handle, 71 b: *block, 72 iv: const []u8, 73 ad: const []u8 74 ) void = { 75 assert(blocksz(b) == GCMBLOCKSZ); 76 assert(len(iv): u64 <= (types::U64_MAX >> 3)); 77 78 s.handle = handle; 79 s.block = b; 80 s.adlen = len(ad); 81 s.xorbufpos = GCMBLOCKSZ; // to force fill xorbuf at start 82 83 encrypt(b, s.h, s.h); 84 85 if (len(iv) == 12) { 86 s.y0[..12] = iv[..]; 87 s.y0[15] |= 1; 88 } else { 89 let ivlen = s.tagbuf; 90 beputu64(ivlen[8..], len(iv) << 3); 91 ghash_ctmul64(s.y0, s.h, iv); 92 ghash_ctmul64(s.y0, s.h, ivlen); 93 bytes::zero(ivlen); 94 }; 95 96 s.y = begetu32(s.y0[12..]) + 1; 97 98 let ad = ad[..]; 99 for (len(ad) > 0) { 100 const max = if (len(ad) >= GCMBLOCKSZ) { 101 yield GCMBLOCKSZ; 102 } else { 103 yield len(ad); 104 }; 105 106 ghash_ctmul64(s.tagbuf, s.h, ad[..max]); 107 ad = ad[max..]; 108 }; 109 }; 110 111 fn gcm_writer(s: *io::stream, buf: const []u8) (size | io::error) = { 112 let s = s: *gcmstream; 113 if (len(buf) == 0) { 114 return 0z; 115 }; 116 117 if (s.xorbufpos == GCMBLOCKSZ) { 118 // current key block is depleted, prepare the next one 119 fillxorbuf(s); 120 }; 121 122 let buf = buf[..]; 123 124 let n: size = 0; 125 const max = if (s.xorbufpos + len(buf) > len(s.cipherbuf)) { 126 yield len(s.cipherbuf) - s.xorbufpos; 127 } else { 128 yield len(buf); 129 }; 130 131 let cipher = s.cipherbuf[s.xorbufpos..s.xorbufpos + max]; 132 let key = s.xorbuf[s.xorbufpos..s.xorbufpos + max]; 133 xor(cipher, key, buf[..max]); 134 135 const n = io::write(s.handle, cipher)?; 136 s.xorbufpos += n; 137 s.clen += n; 138 139 if (s.xorbufpos == GCMBLOCKSZ) { 140 ghash_ctmul64(s.tagbuf, s.h, s.cipherbuf); 141 }; 142 143 return n; 144 }; 145 146 fn fillxorbuf(s: *gcmstream) void = { 147 let y: [GCMBLOCKSZ]u8 = [0...]; 148 s.xorbuf[..] = s.y0[..]; 149 beputu32(s.xorbuf[12..], s.y); 150 encrypt(s.block as *block, s.xorbuf, s.xorbuf); 151 s.y += 1; 152 s.xorbufpos = 0; 153 }; 154 155 fn gcm_reader(s: *io::stream, buf: []u8) (size | io::EOF | io::error) = { 156 let s = s: *gcmstream; 157 158 const n = match (io::read(s.handle, buf)?) { 159 case io::EOF => 160 return io::EOF; 161 case let s: size => 162 yield s; 163 }; 164 165 for (let i = n; i > 0) { 166 if (s.xorbufpos == GCMBLOCKSZ) { 167 fillxorbuf(s); 168 }; 169 170 const max = if (s.xorbufpos + i > GCMBLOCKSZ) { 171 yield len(s.cipherbuf) - s.xorbufpos; 172 } else { 173 yield i; 174 }; 175 176 let cipher = s.cipherbuf[s.xorbufpos..s.xorbufpos + max]; 177 let key = s.xorbuf[s.xorbufpos..s.xorbufpos + max]; 178 179 cipher[..] = buf[..max]; 180 xor(buf[..max], buf[..max], key); 181 182 buf = buf[max..]; 183 i -= max; 184 185 s.xorbufpos += max; 186 s.clen += max; 187 188 if (s.xorbufpos == len(s.cipherbuf)) { 189 ghash_ctmul64(s.tagbuf, s.h, s.cipherbuf); 190 }; 191 }; 192 193 return n; 194 }; 195 196 // Finishes encryption and returns the authentication tag. After calling seal, 197 // the user must not write any more data to the stream. 198 export fn gcm_seal(s: *gcmstream, tag: []u8) void = { 199 assert(len(tag) == GCMTAGSZ); 200 if (s.xorbufpos > 0 && s.xorbufpos < GCMBLOCKSZ) { 201 // last block was is not full, therefore the content was not 202 // hashed yet. 203 ghash_ctmul64(s.tagbuf, s.h, s.cipherbuf[..s.xorbufpos]); 204 }; 205 206 beputu64(tag, s.adlen << 3); 207 beputu64(tag[8..], s.clen << 3); 208 ghash_ctmul64(s.tagbuf, s.h, tag); 209 210 // use tmp to store the resulting tag 211 encrypt(s.block as *block, tag, s.y0); 212 xor(tag, tag, s.tagbuf); 213 }; 214 215 // Verifies the authentication tag against the decrypted data. Must be called 216 // after reading all data from the stream to ensure that the data was not 217 // modified. If the data was modified, [[errors::invalid]] will be returned and 218 // the data must not be trusted. 219 export fn gcm_verify(s: *gcmstream, tag: []u8) (void | errors::invalid) = { 220 assert(len(tag) == GCMTAGSZ); 221 if (s.xorbufpos > 0 && s.xorbufpos < GCMBLOCKSZ) { 222 ghash_ctmul64(s.tagbuf, s.h, s.cipherbuf[..s.xorbufpos]); 223 }; 224 225 let tmp: [16]u8 = [0...]; 226 beputu64(tmp, s.adlen << 3); 227 beputu64(tmp[8..], s.clen << 3); 228 229 ghash_ctmul64(s.tagbuf, s.h, tmp); 230 231 encrypt(s.block as *block, tmp, s.y0); 232 xor(tmp, tmp, s.tagbuf); 233 234 if (eqslice(tag, tmp) == 0) { 235 return errors::invalid; 236 }; 237 }; 238 239 // Unlinks the 'block' provided by [[gcm_init]] to avoid finishing it when 240 // calling [[io::close]] on the stream. 241 export fn gcm_unlink_block(s: *gcmstream) void = { 242 s.block = null; 243 }; 244 245 fn gcm_closer(s: *io::stream) (void | io::error) = { 246 let s = s: *gcmstream; 247 bytes::zero(s.tagbuf); 248 bytes::zero(s.xorbuf); 249 bytes::zero(s.cipherbuf); 250 bytes::zero(s.y0); 251 bytes::zero(s.h); 252 253 if (s.block is *block) { 254 finish(s.block as *block); 255 }; 256 };