| 18 | |
| 19 | // ==================== AES-GCM ==================== |
| 20 | class AESGCMCipher { |
| 21 | constructor(key, nonce) { |
| 22 | if (key.length !== 16) throw new Error('key must be 16 bytes'); |
| 23 | if (nonce.length !== 8) throw new Error('nonce must be 8 bytes'); |
| 24 | this.key = key; |
| 25 | this.nonce = nonce; |
| 26 | this.encryptCounter = 1; |
| 27 | this.decryptCounter = 0; |
| 28 | } |
| 29 | |
| 30 | _makeIV(counter) { |
| 31 | const iv = Buffer.alloc(12); |
| 32 | this.nonce.copy(iv, 0); |
| 33 | iv.writeUInt32LE(counter, 8); |
| 34 | return iv; |
| 35 | } |
| 36 | |
| 37 | encrypt(data) { |
| 38 | const counter = this.encryptCounter++; |
| 39 | const iv = this._makeIV(counter); |
| 40 | const cipher = crypto.createCipheriv('aes-128-gcm', this.key, iv); |
| 41 | const encrypted = Buffer.concat([cipher.update(data), cipher.final()]); |
| 42 | const tag = cipher.getAuthTag(); |
| 43 | |
| 44 | const result = Buffer.alloc(4 + encrypted.length + tag.length); |
| 45 | result.writeUInt32LE(counter, 0); |
| 46 | encrypted.copy(result, 4); |
| 47 | tag.copy(result, 4 + encrypted.length); |
| 48 | return result; |
| 49 | } |
| 50 | |
| 51 | decrypt(data) { |
| 52 | if (data.length < 20) throw new Error('Data too short'); |
| 53 | const counter = data.readUInt32LE(0); |
| 54 | if (counter <= this.decryptCounter) throw new Error('Replay attack'); |
| 55 | this.decryptCounter = counter; |
| 56 | |
| 57 | const ciphertext = data.slice(4, data.length - 16); |
| 58 | const tag = data.slice(data.length - 16); |
| 59 | const iv = this._makeIV(counter); |
| 60 | |
| 61 | const decipher = crypto.createDecipheriv('aes-128-gcm', this.key, iv); |
| 62 | decipher.setAuthTag(tag); |
| 63 | return Buffer.concat([decipher.update(ciphertext), decipher.final()]); |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | // ==================== 压缩/解压 ==================== |
| 68 | function compress(data) { |
nothing calls this directly
no outgoing calls
no test coverage detected