lum_ccc_rust/flutter_src/ccc/ccc_key_schedule.dart

149 lines
5.1 KiB
Dart

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<List<int>> 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<CccPhase1KeyMaterial> buildMessageParams({
required String channelUuid,
required CCCData? cccData,
required Map<String, dynamic> 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 = <String, dynamic>{
'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<List<int>> _hashUtf8(
String value,
CccKdfFunction kdfFunction,
) async {
return _hashBytes(utf8.encode(value), kdfFunction);
}
/// Hash bytes to bytes using selected KDF/hash function.
static Future<List<int>> _hashBytes(
List<int> 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<int> 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<String, dynamic> cipherParams;
const CccPhase1KeyMaterial({
required this.effectiveMessageSequence,
required this.cipherParams,
});
}