149 lines
5.1 KiB
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,
|
|
});
|
|
}
|