import 'dart:math'; import 'dart:typed_data'; import 'package:pointycastle/export.dart'; /// AES-256 in counter mode, so any byte range of a song can be decrypted on /// its own (seeking without reading the whole file). Encrypting and decrypting /// are the same operation. /// /// File layout: 8-byte random nonce, then the encrypted song. class SongCipher { SongCipher(Uint8List key) : _aes = AESEngine()..init(true, KeyParameter(key)); static const headerLength = 8; static const _block = 16; final AESEngine _aes; static Uint8List newNonce() { final r = Random.secure(); return Uint8List.fromList(List.generate(headerLength, (_) => r.nextInt(256))); } static Uint8List newKey() { final r = Random.secure(); return Uint8List.fromList(List.generate(32, (_) => r.nextInt(256))); } /// XORs [data] with the key stream for song position [offset] (bytes after the header), in place. void apply(Uint8List data, int offset, Uint8List nonce) { final counter = Uint8List(_block)..setRange(0, headerLength, nonce); final stream = Uint8List(_block); var blockIndex = -1; for (var i = 0; i < data.length; i++) { final pos = offset + i; final b = pos ~/ _block; if (b != blockIndex) { blockIndex = b; _writeCounter(counter, b); _aes.processBlock(counter, 0, stream, 0); } data[i] ^= stream[pos % _block]; } } static void _writeCounter(Uint8List counter, int blockIndex) { var v = blockIndex; for (var i = _block - 1; i >= headerLength; i--) { counter[i] = v & 0xff; v >>= 8; } } }