import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; import 'package:tane/db/database.dart'; import 'package:tane/db/encrypted_executor.dart'; /// Security regression guard for "no plaintext at rest, ever" (CLAUDE.md). /// /// Writes a Variety whose label is a unique sentinel, then reads the raw /// database file bytes and asserts the sentinel does NOT appear — proving /// SQLCipher actually encrypted the data. /// /// Requires a real SQLCipher library. On a host without it (no /// `libsqlcipher.so`), the test is skipped with a clear reason; CI installs /// SQLCipher so the guard runs there. It also runs on-device via integration /// tests. void main() { test('sentinel label is absent from the raw encrypted DB file', () async { const sentinel = 'GRANDMA_TOMATO_SENTINEL_9f3a1c'; const keyHex = '00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff'; final dir = await Directory.systemTemp.createTemp('tane_enc_test'); final file = File(p.join(dir.path, 'inventory.sqlite')); addTearDown(() => dir.delete(recursive: true)); final db = AppDatabase(openEncryptedExecutor(file, keyHex)); try { await db .into(db.varieties) .insert( VarietiesCompanion.insert( id: 'sentinel-row', label: sentinel, createdAt: 0, updatedAt: '0', lastAuthor: 'test', ), ); } on Object catch (e) { // SQLCipher not available in this environment (e.g. dev host without // libsqlcipher). Skip rather than pass silently. markTestSkipped('SQLCipher unavailable, cannot verify encryption: $e'); await db.close(); return; } await db.close(); final bytes = await file.readAsBytes(); expect( _containsAscii(bytes, sentinel), isFalse, reason: 'Label leaked as plaintext — SQLCipher encryption is not active.', ); // Sanity: a real SQLCipher file does not start with the plain-SQLite header. expect( _containsAscii(bytes.sublist(0, 16), 'SQLite format 3'), isFalse, reason: 'File has a plaintext SQLite header — not encrypted.', ); }); } bool _containsAscii(Uint8List haystack, String needle) { final n = needle.codeUnits; if (n.isEmpty || haystack.length < n.length) return false; for (var i = 0; i <= haystack.length - n.length; i++) { var match = true; for (var j = 0; j < n.length; j++) { if (haystack[i + j] != n[j]) { match = false; break; } } if (match) return true; } return false; }