library impl.stream_cipher.chacha20poly1305; import 'dart:math'; import 'dart:typed_data'; import 'package:pointycastle/src/registry/registry.dart'; import '../export.dart'; import '../src/impl/base_aead_cipher.dart'; import '../src/ufixnum.dart'; import '../src/utils.dart' as utils; // ignore_for_file: non_constant_identifier_names class ChaCha20Poly1305 extends BaseAEADCipher { static final FactoryConfig factoryConfig = StaticFactoryConfig( AEADCipher, 'ChaCha20-Poly1305', () => ChaCha20Poly1305(ChaCha7539Engine(), Poly1305())); static const BUF_SIZE = 64; static const KEY_SIZE = 32; static const NONCE_SIZE = 12; static const MAC_SIZE = 16; static final MAX = pow(2, 63) - 1; static final MIN = -pow(2, 63); static final Uint8List ZEROES = Uint8List(MAC_SIZE - 1); static final int AADLimit = (MAX - MIN).toInt(); static const DATA_LIMIT = (1 << 32 - 1) * 64; final ChaCha7539Engine chacha20; final Poly1305 poly1305; final Uint8List _key = Uint8List(KEY_SIZE); final Uint8List _nonce = Uint8List(NONCE_SIZE); final Uint8List _buf = Uint8List(MAC_SIZE + BUF_SIZE); final Uint8List _mac = Uint8List(MAC_SIZE); @override String get algorithmName => 'ChaCha20-Poly1305'; @override Uint8List get mac => _mac; Uint8List? _initialAAD; late int _aadCount; late int _dataCount; int _state = State.UNINITIALIZED; late int _bufPos; ChaCha20Poly1305(this.chacha20, this.poly1305); @override void init(bool forEncryption, CipherParameters params) { KeyParameter initKeyParam; Uint8List? initNonce; CipherParameters chacha20Params; if (params is AEADParameters) { var aeadParams = params; var macSizeBits = aeadParams.macSize; if ((MAC_SIZE * 8) != macSizeBits) { throw ArgumentError('Invalid value for MAC size: $macSizeBits'); } initKeyParam = aeadParams.parameters as KeyParameter; initNonce = aeadParams.nonce; chacha20Params = ParametersWithIV(initKeyParam, initNonce); _initialAAD = aeadParams.associatedData; } else if (params is ParametersWithIV) { var ivParams = params; initKeyParam = ivParams.parameters as KeyParameter; initNonce = ivParams.iv; chacha20Params = ivParams; _initialAAD = null; } else { throw ArgumentError('invalid parameters passed to ChaCha20Poly1305'); } // Validate key if (KEY_SIZE != initKeyParam.key.length) { throw ArgumentError('Key must be 256 bits'); } // Validate nonce if (NONCE_SIZE != initNonce.length) { throw ArgumentError('Nonce must be 96 bits'); } utils.arrayCopy(initKeyParam.key, 0, _key, 0, KEY_SIZE); utils.arrayCopy(initNonce, 0, _nonce, 0, NONCE_SIZE); chacha20.init(true, chacha20Params as ParametersWithIV); _state = forEncryption ? State.ENC_INIT : State.DEC_INIT; resetBool(true, false); } @override // ignore: missing_return int getOutputSize(int len) { var total = max(0, len) + _bufPos; switch (_state) { case State.DEC_INIT: case State.DEC_AAD: case State.DEC_DATA: return max(0, total - MAC_SIZE); case State.ENC_INIT: case State.ENC_AAD: case State.ENC_DATA: return total + MAC_SIZE; default: throw StateError('state = $_state'); } } @override int getUpdateOutputSize(int len) { var total = max(0, len) + _bufPos; switch (_state) { case State.DEC_INIT: case State.DEC_AAD: case State.DEC_DATA: total = max(0, total - MAC_SIZE); break; case State.ENC_INIT: case State.ENC_AAD: case State.ENC_DATA: break; default: throw StateError(''); } return total - (total % BUF_SIZE); } @override void processAADByte(int inp) { checkAAD(); _aadCount = incrementCount(_aadCount, 1, AADLimit); poly1305.updateByte(inp); } @override void processAADBytes(Uint8List inp, int inOff, int len) { if (inOff < 0) { throw ArgumentError('\'inOff\' cannot be negative'); } if (len < 0) { throw ArgumentError('\'len\' cannot be negative'); } if (inOff > (inp.length - len)) { throw ArgumentError('Input buffer too short'); } checkAAD(); if (len > 0) { _aadCount = incrementCount(_aadCount, len, AADLimit); poly1305.update(inp, inOff, len); } } @override int processByte(int inp, Uint8List out, int outOff) { checkData(); switch (_state) { case State.DEC_DATA: { _buf[_bufPos] = inp; if (++_bufPos == _buf.length) { poly1305.update(_buf, 0, BUF_SIZE); processData(_buf, 0, BUF_SIZE, out, outOff); utils.arrayCopy(_buf, BUF_SIZE, _buf, 0, MAC_SIZE); _bufPos = MAC_SIZE; return BUF_SIZE; } return 0; } case State.ENC_DATA: { _buf[_bufPos] = inp; if (++_bufPos == BUF_SIZE) { processData(_buf, 0, BUF_SIZE, out, outOff); poly1305.update(out, outOff, BUF_SIZE); _bufPos = 0; return BUF_SIZE; } return 0; } default: throw StateError(''); } } @override int processBytes( Uint8List inp, int inOff, int len, Uint8List out, int outOff) { if (inOff < 0) { throw ArgumentError('\'inOff\' cannot be negative'); } if (len < 0) { throw ArgumentError('\'len\' cannot be negative'); } if (inOff > (inp.length - len)) { throw ArgumentError('Input buffer too short'); } if (outOff < 0) { throw ArgumentError('\'outOff\' cannot be negative'); } checkData(); var resultLen = 0; switch (_state) { case State.DEC_DATA: { for (var i = 0; i < len; ++i) { _buf[_bufPos] = inp[inOff + i]; if (++_bufPos == _buf.length) { poly1305.update(_buf, 0, BUF_SIZE); processData(_buf, 0, BUF_SIZE, out, outOff + resultLen); utils.arrayCopy(_buf, BUF_SIZE, _buf, 0, MAC_SIZE); _bufPos = MAC_SIZE; resultLen += BUF_SIZE; } } break; } case State.ENC_DATA: { if (_bufPos != 0) { while (len > 0) { --len; _buf[_bufPos] = inp[inOff++]; if (++_bufPos == BUF_SIZE) { processData(_buf, 0, BUF_SIZE, out, outOff); poly1305.update(out, outOff, BUF_SIZE); _bufPos = 0; resultLen = BUF_SIZE; break; } } } while (len >= BUF_SIZE) { processData(inp, inOff, BUF_SIZE, out, outOff + resultLen); poly1305.update(out, outOff + resultLen, BUF_SIZE); inOff += BUF_SIZE; len -= BUF_SIZE; resultLen += BUF_SIZE; } if (len > 0) { utils.arrayCopy(inp, inOff, _buf, 0, len); _bufPos = len; } break; } default: throw StateError(''); } return resultLen; } @override int doFinal(Uint8List out, int outOff) { if (outOff < 0) { throw ArgumentError('\'outOff\' cannot be negative'); } checkData(); for (var i = 0; i < _mac.length; i++) { _mac[i] = 0x00; } var resultLen = 0; switch (_state) { case State.DEC_DATA: { if (_bufPos < MAC_SIZE) { throw ArgumentError('data too short'); } resultLen = _bufPos - MAC_SIZE; if (outOff > (out.length - resultLen)) { throw ArgumentError('Output buffer too short'); } if (resultLen > 0) { poly1305.update(_buf, 0, resultLen); processData(_buf, 0, resultLen, out, outOff); } finishData(State.DEC_FINAL); if (!utils.constantTimeAreEqualOffset( MAC_SIZE, _mac, 0, _buf, resultLen)) { throw ArgumentError('mac check in ChaCha20Poly1305 failed'); } break; } case State.ENC_DATA: { resultLen = _bufPos + MAC_SIZE; // ignore: invariant_booleans if (outOff > (out.length - resultLen)) { throw ArgumentError('Output buffer too short'); } if (_bufPos > 0) { processData(_buf, 0, _bufPos, out, outOff); poly1305.update(out, outOff, _bufPos); } finishData(State.ENC_FINAL); utils.arrayCopy(_mac, 0, out, outOff + _bufPos, MAC_SIZE); break; } default: throw StateError(''); } resetBool(false, true); return resultLen; } @override void reset() { resetBool(true, true); } void resetBool(bool clearMac, bool resetCipher) { for (var i = 0; i < _buf.length; i++) { _buf[i] = 0; } if (clearMac) { for (var i = 0; i < _mac.length; i++) { _mac[i] = 0x00; } } _aadCount = 0; _dataCount = 0; _bufPos = 0; switch (_state) { case State.DEC_INIT: case State.ENC_INIT: break; case State.DEC_AAD: case State.DEC_DATA: case State.DEC_FINAL: _state = State.DEC_INIT; break; case State.ENC_AAD: case State.ENC_DATA: case State.ENC_FINAL: _state = State.ENC_FINAL; return; default: throw StateError(''); } if (resetCipher) { chacha20.reset(); } initMAC(); if (_initialAAD != null) { processAADBytes(_initialAAD!, 0, _initialAAD!.length); } } void checkAAD() { switch (_state) { case State.DEC_INIT: _state = State.DEC_AAD; break; case State.ENC_INIT: _state = State.ENC_AAD; break; case State.DEC_AAD: case State.ENC_AAD: break; case State.ENC_FINAL: throw StateError('ChaCha20Poly1305 cannot be reused for encryption'); default: throw StateError(''); } } void checkData() { switch (_state) { case State.DEC_INIT: case State.DEC_AAD: finishAAD(State.DEC_DATA); break; case State.ENC_INIT: case State.ENC_AAD: finishAAD(State.ENC_DATA); break; case State.DEC_DATA: case State.ENC_DATA: break; case State.ENC_FINAL: throw StateError('ChaCha20Poly1305 cannot be reused for encryption'); default: throw StateError(''); } } void finishAAD(int nextState) { padMAC(_aadCount); _state = nextState; } void finishData(int nextState) { padMAC(_dataCount); var lengths = Uint8List(16); pack32(_aadCount, lengths, 0, Endian.little); pack32(_dataCount, lengths, 8, Endian.little); poly1305.update(lengths, 0, 16); poly1305.doFinal(_mac, 0); _state = nextState; } int incrementCount(int count, int increment, int limit) { if (count + MIN > (limit - increment) + MIN) { throw StateError('Limit exceeded'); } return count + increment; } void initMAC() { var firstBlock = Uint8List(64); try { chacha20.processBytes(firstBlock, 0, 64, firstBlock, 0); poly1305.init(KeyParameter.offset(firstBlock, 0, 32)); } finally { for (var i = 0; i < firstBlock.length; i++) { firstBlock[i] = 0; } } } void padMAC(int count) { var partial = (count & 4294967295) & (MAC_SIZE - 1); if (0 != partial) { poly1305.update(ZEROES, 0, MAC_SIZE - partial); } } void processData( Uint8List inp, int inOff, int inLen, Uint8List out, int outOff) { if (outOff > (out.length - inLen)) { throw ArgumentError('Output buffer too short'); } chacha20.processBytes(inp, inOff, inLen, out, outOff); _dataCount = incrementCount(_dataCount, inLen, DATA_LIMIT); } } class State { static const UNINITIALIZED = 0; static const ENC_INIT = 1; static const ENC_AAD = 2; static const ENC_DATA = 3; static const ENC_FINAL = 4; static const DEC_INIT = 5; static const DEC_AAD = 6; static const DEC_DATA = 7; static const DEC_FINAL = 8; }