main function

Future<void> main()

Implementation

Future<void> main() async {
  print('=== Avvio demo CypherMask con socket in memoria ===\n');

  final link = InMemoryDuplex();
  final mask = CypherMask(aead: AeadAlgorithm.chacha20Poly1305);

  print('[SETUP] Alice e Bob generano le identità...');
  final alice = await Identity.generate();
  final bob = await Identity.generate();
  final (bobBundle, bobSecret) = await PreKeys.generate(identity: bob, count: 8);
  print('[SETUP] Bob pubblica un PreKeyBundle con SPK e OPK.\n');

  print('[HANDSHAKE] Alice prepara init X3DH usando il bundle di Bob...');

  final (sx, _) = await mask.startSession(me: alice, peerBundle: bobBundle);
  final initFrame = await mask.buildInitFrame(
    me: alice,
    sessionX3DH: sx,
    includeEd25519Pub: true,
    signTranscript: true,
  );

  late final Session bobSession;
  final cBobInit = Completer<void>();
  final subInitBob = link.streamToBob.listen((bytes) async {
    final m = json.decode(utf8.decode(bytes));
    if (m['type'] != 'x3dh_init') return;

    print('[HANDSHAKE] Bob ha ricevuto initFrame: verifica firma e costruisce la sessione...');
    final parsed = CypherMask.parseInitFrame(bytes);

    if (parsed.sigEd25519 != null && parsed.initiatorEd25519 != null) {
      final ok = await CypherMask.verifyInitSignature(
        initPacket: parsed.initPacket,
        signatureBytes: parsed.sigEd25519!,
        ed25519Pub: parsed.initiatorEd25519!,
      );
      print('[HANDSHAKE] Verifica firma di Alice: $ok');
      if (!ok) throw StateError('Init signature verification failed');
    }

    bobSession = await mask.acceptSession(
      me: bob,
      localPreKeys: (bobBundle, bobSecret),
      initPacket: parsed.initPacket,
      initiatorStaticX25519: parsed.initiatorIkx,
    );

    print('[HANDSHAKE] Sessione Bob inizializzata.\n');
    cBobInit.complete();
  });

  print('[HANDSHAKE] Alice invia initFrame a Bob.');
  link.sendFromAlice(initFrame);

  await cBobInit.future;
  await subInitBob.cancel();

  final aliceSession = sx.session;

  final cMsg = Completer<void>();
  late final StreamSubscription<Uint8List> subMsgBob;
  subMsgBob = link.streamToBob.listen((bytes) async {
    final m = json.decode(utf8.decode(bytes));
    if (m['type'] != 'msg') return;

    print('[MSG] Bob riceve un messaggio, decifra...');
    final p = CypherMask.parseMsgFrame(bytes);
    final text = await mask.decryptText(bobSession, p);
    print('>>> Bob ha ricevuto: "$text"');

    final reply = await mask.encryptText(bobSession, 'Ricevuto, grazie.');
    print('[MSG] Bob risponde con un ACK.');
    link.sendFromBob(CypherMask.buildMsgFrame(reply));
    cMsg.complete();
  });

  print('[MSG] Alice cifra e invia un messaggio a Bob...');
  final hello = await mask.encryptText(aliceSession, 'Ciao Bob, messaggio segreto.');
  link.sendFromAlice(CypherMask.buildMsgFrame(hello));

  await cMsg.future;
  await subMsgBob.cancel();

  final cReply = Completer<void>();
  late final StreamSubscription<Uint8List> subMsgAlice;
  subMsgAlice = link.streamToAlice.listen((bytes) async {
    final m = json.decode(utf8.decode(bytes));
    if (m['type'] != 'msg') return;

    final p = CypherMask.parseMsgFrame(bytes);
    final text = await mask.decryptText(aliceSession, p);
    print('>>> Alice ha ricevuto: "$text"\n');
    cReply.complete();
  });

  await cReply.future;
  await subMsgAlice.cancel();

  print('[CALL] Avvio demo media E2EE: Alice invia 100 frame Opus fittizi a Bob...');
  int mediaEpoch = 0;
  int mediaCounter = 0;

  final totalFrames = 100;
  final mediaDone = Completer<void>();
  int framesReceived = 0;
  int bytesReceived = 0;

  late final StreamSubscription<Uint8List> subMediaBob;
  subMediaBob = link.streamToBob.listen((bytes) async {
    final m = json.decode(utf8.decode(bytes));
    if (m['type'] != 'media_chunk') return;

    final parsed = CypherMask.parseMediaChunkFrame(bytes);
    final raw = await mask.decryptMediaFrame(
      receiver: bobSession,
      packet: parsed.packet,
      counter: parsed.counter,
      epoch: parsed.epoch,
      codec: parsed.codec,
      timestampMs: parsed.timestampMs,
    );

    framesReceived += 1;
    bytesReceived += raw.length;

    if (framesReceived % 20 == 0) {
      print('[CALL] Bob ha decifrato $framesReceived/$totalFrames frame '
            '(bytes cumulati: $bytesReceived)');
    }
    if (framesReceived == totalFrames) {
      print('[CALL] Bob ha terminato la ricezione dei frame (tot bytes: $bytesReceived).');
      mediaDone.complete();
    }
  });

  final rnd = Random(42);
  for (var i = 0; i < totalFrames; i++) {
    if (i == 50) {
      mediaEpoch += 1;
      print('[CALL] Rotazione epoch → $mediaEpoch');
    }
    final size = 60 + rnd.nextInt(61);
    final opusLike = Uint8List.fromList(List<int>.generate(size, (_) => rnd.nextInt(256)));

    mediaCounter += 1;
    final pkt = await mask.encryptMediaFrame(
      sender: aliceSession,
      encodedFrame: opusLike,
      counter: mediaCounter,
      epoch: mediaEpoch,
      codec: 'opus',
      timestampMs: DateTime.now().millisecondsSinceEpoch,
    );
    final wire = CypherMask.buildMediaChunkFrame(
      packet: pkt,
      counter: mediaCounter,
      epoch: mediaEpoch,
      codec: 'opus',
      timestampMs: DateTime.now().millisecondsSinceEpoch,
    );
    link.sendFromAlice(wire);

    await Future<void>.delayed(const Duration(milliseconds: 2));
  }

  await mediaDone.future;
  await subMediaBob.cancel();

  // ======== FILE ========
  print('[FILE] Alice prepara un file di 300KB e lo cifra in chunk...');
  final rndFile = Random(7);
  final fileBytes = Uint8List.fromList(
    List<int>.generate(300 * 1024, (_) => rndFile.nextInt(256)),
  );

  final send = await mask.encryptFile(
    sender: aliceSession,
    fileBytes: fileBytes,
    fileName: 'demo.bin',
    mimeType: 'application/octet-stream',
    chunkSize: 64 * 1024,
  );

  FileManifest? manifest;
  final received = <int, Packet>{};
  final cFile = Completer<void>();
  late final StreamSubscription<Uint8List> subFileBob;
  subFileBob = link.streamToBob.listen((bytes) async {
    final m = json.decode(utf8.decode(bytes));
    switch (m['type']) {
      case 'file_manifest':
        manifest = CypherMask.parseFileManifestFrame(bytes);
        print('[FILE] Bob riceve manifest: '
              'file="${manifest!.fileName}", size=${manifest!.fileSize}B, chunks=${manifest!.chunks}');
        break;
      case 'file_chunk':
        final part = CypherMask.parseFileChunkFrame(bytes);
        print('[FILE] Bob riceve chunk ${part.index + 1}/${part.total}');
        received[part.index] = part.packet;

        if (manifest != null && received.length == manifest!.chunks) {
          final ordered = List<Packet>.generate(
            manifest!.chunks,
            (i) => received[i]!,
          );
          final out = await mask.decryptFile(
            receiver: bobSession,
            manifest: manifest!,
            chunks: ordered,
          );
          print('[FILE] Bob ricostruisce il file: ${out.length} bytes '
                '(ok=${out.length == fileBytes.length})');
          cFile.complete();
        }
        break;
    }
  });

  print('[FILE] Alice invia il manifest a Bob.');
  link.sendFromAlice(CypherMask.buildFileManifestFrame(send));

  for (var i = 0; i < send.wire.length; i++) {
    final p = Packet.fromBytes(send.wire[i].bytes);
    print('[FILE] Alice invia chunk ${i + 1}/${send.wire.length}');
    link.sendFromAlice(CypherMask.buildFileChunkFrame(
      chunk: p,
      index: i,
      total: send.wire.length,
    ));
  }

  await cFile.future;
  await subFileBob.cancel();

  await link.close();
  print('\n=== Demo terminata con successo ===');
}