import 'dart:convert'; import 'package:cryptography/cryptography.dart'; /// local imports import 'package:letusmsg/ccc/ccc_kdf.dart'; import 'package:letusmsg/data/ccc_data.dart'; /// Phase 1 key schedule helper. /// /// This utility derives deterministic channel-root and per-message key material /// with a selectable KDF/hash option so message/attachment flows can share one /// key-schedule contract before full ratchet rollout. class CccKeySchedule { static const String version = 'phase1-v1'; /// Derive the deterministic 32-byte root key for a channel. /// /// This is the shared secret that all channel members can independently /// compute from the channel UUID and CCCData configuration. Used both /// by the Phase 1 static key schedule and as the seed for Phase 2 /// ratchet initialization via [RatchetEngine.initializeFromRoot]. /// /// Returns a 32-byte root key (truncated from the full hash output). static Future> deriveRootKey({ required String channelUuid, required CCCData? cccData, }) async { final combo = cccData?.combo ?? 0; final iterations = cccData?.iterrations ?? 0; final kdfFunctionValue = normalizeCccKdfFunctionValue( cccData?.kdfFunction, ); final kdfFunction = cccKdfFunctionFromInt(kdfFunctionValue); final fullHash = await _hashUtf8( 'lum|$version|root|$channelUuid|combo:$combo|iter:$iterations', kdfFunction, ); return fullHash.sublist(0, 32); } /// Build Phase 1 key-schedule params for one channel/message operation. /// /// The returned params are additive and safe to merge with existing CCC params. static Future buildMessageParams({ required String channelUuid, required CCCData? cccData, required Map baseCipherParams, required bool isUserMessage, int? messageSequence, }) async { final combo = cccData?.combo ?? 0; final iterations = cccData?.iterrations ?? 0; final kdfFunctionValue = normalizeCccKdfFunctionValue( cccData?.kdfFunction, ); final kdfFunction = cccKdfFunctionFromInt(kdfFunctionValue); final effectiveSequence = messageSequence ?? 0; final rootKey = await _hashUtf8( 'lum|$version|root|$channelUuid|combo:$combo|iter:$iterations', kdfFunction, ); final direction = isUserMessage ? 'out' : 'in'; final messageKey = await _hashBytes( [ ...rootKey, ...utf8.encode('|msg|$direction|seq:$effectiveSequence'), ], kdfFunction, ); // Note: direction and message_sequence are included in the AD map for // key-schedule metadata completeness, but are stripped by the isolate // worker's _buildAssociatedDataBytes() before AEAD binding. This is // intentional for Phase 1: the sender does not know the relay-assigned // server_seq at encryption time, and direction differs between sender // ('out') and receiver ('in'). Phase 2 ratchet will re-enable sequence // binding once both sides share agreed-upon counters. final associatedData = { 'v': version, 'channel_uuid': channelUuid, 'ccc_combo': combo, 'ccc_iterations': iterations, 'message_sequence': effectiveSequence, 'direction': direction, 'kdf_function': kdfFunction.name, 'kdf_function_value': kdfFunction.value, }; return CccPhase1KeyMaterial( effectiveMessageSequence: effectiveSequence, cipherParams: { ...baseCipherParams, 'phase1_key_schedule_version': version, 'phase1_kdf_function': kdfFunction.name, 'phase1_kdf_function_value': kdfFunction.value, 'phase1_effective_sequence': effectiveSequence, 'phase1_root_key_b64': _toBase64UrlNoPad(rootKey), 'phase1_message_key_b64': _toBase64UrlNoPad(messageKey), 'phase1_associated_data': associatedData, }, ); } /// Hash UTF-8 text to bytes using selected KDF/hash function. static Future> _hashUtf8( String value, CccKdfFunction kdfFunction, ) async { return _hashBytes(utf8.encode(value), kdfFunction); } /// Hash bytes to bytes using selected KDF/hash function. static Future> _hashBytes( List bytes, CccKdfFunction kdfFunction, ) async { switch (kdfFunction) { case CccKdfFunction.sha256: return (await Sha256().hash(bytes)).bytes; case CccKdfFunction.sha384: return (await Sha384().hash(bytes)).bytes; case CccKdfFunction.sha512: return (await Sha512().hash(bytes)).bytes; case CccKdfFunction.blake2b512: return (await Blake2b().hash(bytes)).bytes; } } static String _toBase64UrlNoPad(List bytes) { return base64UrlEncode(bytes).replaceAll('=', ''); } } /// Derived key material metadata for Phase 1 message encryption scheduling. class CccPhase1KeyMaterial { /// Effective message sequence used for this derivation. final int effectiveMessageSequence; /// Parameters merged into CCC cipher params for runtime usage. final Map cipherParams; const CccPhase1KeyMaterial({ required this.effectiveMessageSequence, required this.cipherParams, }); }