wip: Add the basics for the Double Ratchet

This commit is contained in:
PapaTutuWawa 2022-08-02 18:13:14 +02:00
parent 56ae882aa0
commit d3c8d813a9
11 changed files with 652 additions and 11 deletions

View File

@ -6,3 +6,7 @@ linter:
use_setters_to_change_properties: false use_setters_to_change_properties: false
avoid_positional_boolean_parameters: false avoid_positional_boolean_parameters: false
avoid_bool_literals_in_conditional_expressions: false avoid_bool_literals_in_conditional_expressions: false
analyzer:
exclude:
- "lib/protobuf/*.dart"

View File

@ -35,6 +35,7 @@
flutter pinnedJDK android.platform-tools dart # Flutter/Android flutter pinnedJDK android.platform-tools dart # Flutter/Android
gitlint jq # Code hygiene gitlint jq # Code hygiene
ripgrep # General utilities ripgrep # General utilities
protobuf
]; ];
ANDROID_HOME = "${android.androidsdk}/libexec/android-sdk"; ANDROID_HOME = "${android.androidsdk}/libexec/android-sdk";
JAVA_HOME = pinnedJDK; JAVA_HOME = pinnedJDK;

263
lib/protobuf/schema.pb.dart Normal file
View File

@ -0,0 +1,263 @@
///
// Generated code. Do not modify.
// source: schema.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class OMEMOMessage extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'OMEMOMessage', createEmptyInstance: create)
..a<$core.int>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'n', $pb.PbFieldType.QU3)
..a<$core.int>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'pn', $pb.PbFieldType.QU3)
..a<$core.List<$core.int>>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'dhPub', $pb.PbFieldType.QY)
..a<$core.List<$core.int>>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'ciphertext', $pb.PbFieldType.OY)
;
OMEMOMessage._() : super();
factory OMEMOMessage({
$core.int? n,
$core.int? pn,
$core.List<$core.int>? dhPub,
$core.List<$core.int>? ciphertext,
}) {
final _result = create();
if (n != null) {
_result.n = n;
}
if (pn != null) {
_result.pn = pn;
}
if (dhPub != null) {
_result.dhPub = dhPub;
}
if (ciphertext != null) {
_result.ciphertext = ciphertext;
}
return _result;
}
factory OMEMOMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory OMEMOMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
OMEMOMessage clone() => OMEMOMessage()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
OMEMOMessage copyWith(void Function(OMEMOMessage) updates) => super.copyWith((message) => updates(message as OMEMOMessage)) as OMEMOMessage; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static OMEMOMessage create() => OMEMOMessage._();
OMEMOMessage createEmptyInstance() => create();
static $pb.PbList<OMEMOMessage> createRepeated() => $pb.PbList<OMEMOMessage>();
@$core.pragma('dart2js:noInline')
static OMEMOMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<OMEMOMessage>(create);
static OMEMOMessage? _defaultInstance;
@$pb.TagNumber(1)
$core.int get n => $_getIZ(0);
@$pb.TagNumber(1)
set n($core.int v) { $_setUnsignedInt32(0, v); }
@$pb.TagNumber(1)
$core.bool hasN() => $_has(0);
@$pb.TagNumber(1)
void clearN() => clearField(1);
@$pb.TagNumber(2)
$core.int get pn => $_getIZ(1);
@$pb.TagNumber(2)
set pn($core.int v) { $_setUnsignedInt32(1, v); }
@$pb.TagNumber(2)
$core.bool hasPn() => $_has(1);
@$pb.TagNumber(2)
void clearPn() => clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get dhPub => $_getN(2);
@$pb.TagNumber(3)
set dhPub($core.List<$core.int> v) { $_setBytes(2, v); }
@$pb.TagNumber(3)
$core.bool hasDhPub() => $_has(2);
@$pb.TagNumber(3)
void clearDhPub() => clearField(3);
@$pb.TagNumber(4)
$core.List<$core.int> get ciphertext => $_getN(3);
@$pb.TagNumber(4)
set ciphertext($core.List<$core.int> v) { $_setBytes(3, v); }
@$pb.TagNumber(4)
$core.bool hasCiphertext() => $_has(3);
@$pb.TagNumber(4)
void clearCiphertext() => clearField(4);
}
class OMEMOAuthenticatedMessage extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'OMEMOAuthenticatedMessage', createEmptyInstance: create)
..a<$core.List<$core.int>>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'mac', $pb.PbFieldType.QY)
..a<$core.List<$core.int>>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'message', $pb.PbFieldType.QY)
;
OMEMOAuthenticatedMessage._() : super();
factory OMEMOAuthenticatedMessage({
$core.List<$core.int>? mac,
$core.List<$core.int>? message,
}) {
final _result = create();
if (mac != null) {
_result.mac = mac;
}
if (message != null) {
_result.message = message;
}
return _result;
}
factory OMEMOAuthenticatedMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory OMEMOAuthenticatedMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
OMEMOAuthenticatedMessage clone() => OMEMOAuthenticatedMessage()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
OMEMOAuthenticatedMessage copyWith(void Function(OMEMOAuthenticatedMessage) updates) => super.copyWith((message) => updates(message as OMEMOAuthenticatedMessage)) as OMEMOAuthenticatedMessage; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static OMEMOAuthenticatedMessage create() => OMEMOAuthenticatedMessage._();
OMEMOAuthenticatedMessage createEmptyInstance() => create();
static $pb.PbList<OMEMOAuthenticatedMessage> createRepeated() => $pb.PbList<OMEMOAuthenticatedMessage>();
@$core.pragma('dart2js:noInline')
static OMEMOAuthenticatedMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<OMEMOAuthenticatedMessage>(create);
static OMEMOAuthenticatedMessage? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get mac => $_getN(0);
@$pb.TagNumber(1)
set mac($core.List<$core.int> v) { $_setBytes(0, v); }
@$pb.TagNumber(1)
$core.bool hasMac() => $_has(0);
@$pb.TagNumber(1)
void clearMac() => clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get message => $_getN(1);
@$pb.TagNumber(2)
set message($core.List<$core.int> v) { $_setBytes(1, v); }
@$pb.TagNumber(2)
$core.bool hasMessage() => $_has(1);
@$pb.TagNumber(2)
void clearMessage() => clearField(2);
}
class OMEMOKeyExchange extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'OMEMOKeyExchange', createEmptyInstance: create)
..a<$core.int>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'pkId', $pb.PbFieldType.QU3)
..a<$core.int>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'spkId', $pb.PbFieldType.QU3)
..a<$core.List<$core.int>>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'ik', $pb.PbFieldType.QY)
..a<$core.List<$core.int>>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'ek', $pb.PbFieldType.QY)
..aQM<OMEMOAuthenticatedMessage>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'message', subBuilder: OMEMOAuthenticatedMessage.create)
;
OMEMOKeyExchange._() : super();
factory OMEMOKeyExchange({
$core.int? pkId,
$core.int? spkId,
$core.List<$core.int>? ik,
$core.List<$core.int>? ek,
OMEMOAuthenticatedMessage? message,
}) {
final _result = create();
if (pkId != null) {
_result.pkId = pkId;
}
if (spkId != null) {
_result.spkId = spkId;
}
if (ik != null) {
_result.ik = ik;
}
if (ek != null) {
_result.ek = ek;
}
if (message != null) {
_result.message = message;
}
return _result;
}
factory OMEMOKeyExchange.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory OMEMOKeyExchange.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
OMEMOKeyExchange clone() => OMEMOKeyExchange()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
OMEMOKeyExchange copyWith(void Function(OMEMOKeyExchange) updates) => super.copyWith((message) => updates(message as OMEMOKeyExchange)) as OMEMOKeyExchange; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static OMEMOKeyExchange create() => OMEMOKeyExchange._();
OMEMOKeyExchange createEmptyInstance() => create();
static $pb.PbList<OMEMOKeyExchange> createRepeated() => $pb.PbList<OMEMOKeyExchange>();
@$core.pragma('dart2js:noInline')
static OMEMOKeyExchange getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<OMEMOKeyExchange>(create);
static OMEMOKeyExchange? _defaultInstance;
@$pb.TagNumber(1)
$core.int get pkId => $_getIZ(0);
@$pb.TagNumber(1)
set pkId($core.int v) { $_setUnsignedInt32(0, v); }
@$pb.TagNumber(1)
$core.bool hasPkId() => $_has(0);
@$pb.TagNumber(1)
void clearPkId() => clearField(1);
@$pb.TagNumber(2)
$core.int get spkId => $_getIZ(1);
@$pb.TagNumber(2)
set spkId($core.int v) { $_setUnsignedInt32(1, v); }
@$pb.TagNumber(2)
$core.bool hasSpkId() => $_has(1);
@$pb.TagNumber(2)
void clearSpkId() => clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get ik => $_getN(2);
@$pb.TagNumber(3)
set ik($core.List<$core.int> v) { $_setBytes(2, v); }
@$pb.TagNumber(3)
$core.bool hasIk() => $_has(2);
@$pb.TagNumber(3)
void clearIk() => clearField(3);
@$pb.TagNumber(4)
$core.List<$core.int> get ek => $_getN(3);
@$pb.TagNumber(4)
set ek($core.List<$core.int> v) { $_setBytes(3, v); }
@$pb.TagNumber(4)
$core.bool hasEk() => $_has(3);
@$pb.TagNumber(4)
void clearEk() => clearField(4);
@$pb.TagNumber(5)
OMEMOAuthenticatedMessage get message => $_getN(4);
@$pb.TagNumber(5)
set message(OMEMOAuthenticatedMessage v) { setField(5, v); }
@$pb.TagNumber(5)
$core.bool hasMessage() => $_has(4);
@$pb.TagNumber(5)
void clearMessage() => clearField(5);
@$pb.TagNumber(5)
OMEMOAuthenticatedMessage ensureMessage() => $_ensure(4);
}

View File

@ -0,0 +1,7 @@
///
// Generated code. Do not modify.
// source: schema.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name

View File

@ -0,0 +1,48 @@
///
// Generated code. Do not modify.
// source: schema.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
import 'dart:core' as $core;
import 'dart:convert' as $convert;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use oMEMOMessageDescriptor instead')
const OMEMOMessage$json = const {
'1': 'OMEMOMessage',
'2': const [
const {'1': 'n', '3': 1, '4': 2, '5': 13, '10': 'n'},
const {'1': 'pn', '3': 2, '4': 2, '5': 13, '10': 'pn'},
const {'1': 'dh_pub', '3': 3, '4': 2, '5': 12, '10': 'dhPub'},
const {'1': 'ciphertext', '3': 4, '4': 1, '5': 12, '10': 'ciphertext'},
],
};
/// Descriptor for `OMEMOMessage`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List oMEMOMessageDescriptor = $convert.base64Decode('CgxPTUVNT01lc3NhZ2USDAoBbhgBIAIoDVIBbhIOCgJwbhgCIAIoDVICcG4SFQoGZGhfcHViGAMgAigMUgVkaFB1YhIeCgpjaXBoZXJ0ZXh0GAQgASgMUgpjaXBoZXJ0ZXh0');
@$core.Deprecated('Use oMEMOAuthenticatedMessageDescriptor instead')
const OMEMOAuthenticatedMessage$json = const {
'1': 'OMEMOAuthenticatedMessage',
'2': const [
const {'1': 'mac', '3': 1, '4': 2, '5': 12, '10': 'mac'},
const {'1': 'message', '3': 2, '4': 2, '5': 12, '10': 'message'},
],
};
/// Descriptor for `OMEMOAuthenticatedMessage`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List oMEMOAuthenticatedMessageDescriptor = $convert.base64Decode('ChlPTUVNT0F1dGhlbnRpY2F0ZWRNZXNzYWdlEhAKA21hYxgBIAIoDFIDbWFjEhgKB21lc3NhZ2UYAiACKAxSB21lc3NhZ2U=');
@$core.Deprecated('Use oMEMOKeyExchangeDescriptor instead')
const OMEMOKeyExchange$json = const {
'1': 'OMEMOKeyExchange',
'2': const [
const {'1': 'pk_id', '3': 1, '4': 2, '5': 13, '10': 'pkId'},
const {'1': 'spk_id', '3': 2, '4': 2, '5': 13, '10': 'spkId'},
const {'1': 'ik', '3': 3, '4': 2, '5': 12, '10': 'ik'},
const {'1': 'ek', '3': 4, '4': 2, '5': 12, '10': 'ek'},
const {'1': 'message', '3': 5, '4': 2, '5': 11, '6': '.OMEMOAuthenticatedMessage', '10': 'message'},
],
};
/// Descriptor for `OMEMOKeyExchange`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List oMEMOKeyExchangeDescriptor = $convert.base64Decode('ChBPTUVNT0tleUV4Y2hhbmdlEhMKBXBrX2lkGAEgAigNUgRwa0lkEhUKBnNwa19pZBgCIAIoDVIFc3BrSWQSDgoCaWsYAyACKAxSAmlrEg4KAmVrGAQgAigMUgJlaxI0CgdtZXNzYWdlGAUgAigLMhouT01FTU9BdXRoZW50aWNhdGVkTWVzc2FnZVIHbWVzc2FnZQ==');

View File

@ -0,0 +1,9 @@
///
// Generated code. Do not modify.
// source: schema.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
export 'schema.pb.dart';

281
lib/src/double_ratchet.dart Normal file
View File

@ -0,0 +1,281 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:omemo_dart/protobuf/schema.pb.dart';
import 'package:omemo_dart/src/bundle.dart';
import 'package:omemo_dart/src/helpers.dart';
import 'package:omemo_dart/src/key.dart';
import 'package:omemo_dart/src/x3dh.dart';
class OmemoRatchetStepResult {
const OmemoRatchetStepResult(this.header, this.cipherText);
final List<int> header;
final List<int> cipherText;
}
class OmemoEncryptionResult {
const OmemoEncryptionResult(this.cipherText, this.keys);
/// The encrypted plaintext
final List<int> cipherText;
/// Mapping between Device id and the key to decrypt cipherText;
final Map<String, List<int>> keys;
}
/// The session state of one party
class AliceOmemoSession {
AliceOmemoSession(
this.dhs,
this.dhr,
this.ek,
this.rk,
this.cks,
this.ckr,
this.ns,
this.nr,
this.pn,
// this.skippedMessages,
this.ad,
);
/// The Diffie-Hellman sending key pair
final OmemoKeyPair dhs;
/// The Diffie-Hellman receiving key pair
final OmemoPublicKey dhr;
/// The EK used by X3DH
final OmemoKeyPair ek;
/// The Root Key
List<int> rk;
/// Sending Chain Key
List<int> cks;
/// Receiving Chain Key
List<int>? ckr;
/// Message number for sending
int ns;
/// Message number for receiving
int nr;
/// Number of messages in the previous sending chain
int pn;
/// The associated data from the X3DH
final List<int> ad;
// TODO(PapaTutuWawa): Track skipped over message keys
static Future<AliceOmemoSession> newSession(OmemoBundle bundle, OmemoKeyPair ik) async {
// TODO(PapaTutuWawa): Error handling
final x3dhResult = await x3dhFromBundle(bundle, ik);
final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
final dhr = bundle.ik;
final ek = x3dhResult.ek;
final sk = x3dhResult.sk;
final kdfRkResult = await kdfRk(sk, await dh(dhs, dhr, 2));
return AliceOmemoSession(
dhs,
dhr,
ek,
kdfRkResult.rk,
kdfRkResult.ck,
null,
0,
0,
0,
x3dhResult.ad,
);
}
/// The associated_data parameter is implicit as it belongs to the session
Future<List<int>> _encrypt(List<int> mk, List<int> plaintext, List<int> associatedData) async {
final algorithm = Hkdf(
hmac: Hmac(Sha256()),
outputLength: 80,
);
final hkdfResult = await algorithm.deriveKey(
secretKey: SecretKey(mk),
nonce: List<int>.filled(32, 0x00),
info: utf8.encode(encryptHkdfInfoString),
);
final bytes = await hkdfResult.extractBytes();
final encKey = bytes.sublist(0, 32);
final authKey = bytes.sublist(32, 64);
final iv = bytes.sublist(64, 82);
// TODO(PapaTutuWawa): Remove once done
assert(encKey.length == 32);
assert(authKey.length == 32);
assert(iv.length == 16);
// 32 = 256 / 8
final encodedPlaintext = pkcs7padding(plaintext, 32);
final aesAlgorithm = AesCbc.with256bits(
macAlgorithm: Hmac.sha256(),
);
final secretBox = await aesAlgorithm.encrypt(
encodedPlaintext,
secretKey: SecretKey(encKey),
nonce: iv,
);
final ad_ = associatedData.sublist(0, ad.length);
final message = OMEMOMessage.fromBuffer(associatedData.sublist(ad.length))
..ciphertext = secretBox.cipherText;
final messageBytes = message.writeToBuffer();
final input = concat([ad_, messageBytes]);
final authBytes = (await Hmac.sha256().calculateMac(
input,
secretKey: SecretKey(authKey),
)).bytes.sublist(0, 16);
final authenticatedMessage = OMEMOAuthenticatedMessage()
..mac = authBytes
..message = messageBytes;
return authenticatedMessage.writeToBuffer();
}
Future<List<int>> ratchetStep(List<int> plaintext) async {
final kdfResult = await kdfCk(cks);
final message = OMEMOMessage()
..dhPub = await dhs.pk.getBytes()
..pn = pn
..n = ns;
final header = message.writeToBuffer();
cks = kdfResult.ck;
ns++;
return _encrypt(
kdfResult.mk,
plaintext,
concat([ad, header]),
);
}
}
Future<OmemoEncryptionResult> encryptForSessions(List<AliceOmemoSession> sessions, String plaintext) async {
// TODO(PapaTutuWawa): Generate random data
final key = List<int>.filled(32, 0x0);
final algorithm = Hkdf(
hmac: Hmac(Sha256()),
outputLength: 80,
);
final result = await algorithm.deriveKey(
secretKey: SecretKey(key),
nonce: List<int>.filled(32, 0x0),
info: utf8.encode(encryptionHkdfInfoString),
);
final bytes = await result.extractBytes();
final encKey = bytes.sublist(0, 32);
final authKey = bytes.sublist(32, 64);
final iv = bytes.sublist(64, 80);
final encodedPlaintext = pkcs7padding(utf8.encode(plaintext), 32);
final aesAlgorithm = AesCbc.with256bits(
macAlgorithm: Hmac.sha256(),
);
final secretBox = await aesAlgorithm.encrypt(
encodedPlaintext,
secretKey: SecretKey(encKey),
nonce: iv,
);
final hmac = (await Hmac.sha256().calculateMac(
secretBox.cipherText,
secretKey: SecretKey(authKey),
)).bytes.sublist(0, 16);
final keyData = concat([encKey, hmac]);
final keyMap = <String, List<int>>{};
for (final session in sessions) {
final ratchetKey = await session.ratchetStep(keyData);
}
return OmemoEncryptionResult(
secretBox.cipherText,
keyMap,
);
}
/// Result of the KDF_RK function from the Double Ratchet spec.
class KdfRkResult {
const KdfRkResult(this.rk, this.ck);
/// 32 byte Root Key
final List<int> rk;
/// 32 byte Chain Key
final List<int> ck;
}
/// Result of the KDF_CK function from the Double Ratchet spec.
class KdfCkResult {
const KdfCkResult(this.ck, this.mk);
/// 32 byte Chain Key
final List<int> ck;
/// 32 byte Message Key
final List<int> mk;
}
/// Amount of messages we may skip per session
const maxSkip = 1000;
/// Info string for KDF_RK
const kdfRkInfoString = 'OMEMO Root Chain';
/// Info string for ENCRYPT
const encryptHkdfInfoString = 'OMEMO Message Key Material';
/// Info string for encrypting a message
const encryptionHkdfInfoString = 'OMEMO Payload';
/// Flags for KDF_CK
const kdfCkNextMessageKey = 0x01;
const kdfCkNextChainKey = 0x02;
Future<KdfRkResult> kdfRk(List<int> rk, List<int> dhOut) async {
final algorithm = Hkdf(
hmac: Hmac(Sha256()),
outputLength: 32,
);
final result = await algorithm.deriveKey(
secretKey: SecretKey(dhOut),
nonce: rk,
info: utf8.encode(kdfRkInfoString),
);
// TODO(PapaTutuWawa): Does the rk in the tuple (rk, ck) refer to the input rk?
return KdfRkResult(rk, await result.extractBytes());
}
Future<KdfCkResult> kdfCk(List<int> ck) async {
final hkdf = Hkdf(hmac: Hmac(Sha256()), outputLength: 32);
final newCk = await hkdf.deriveKey(
secretKey: SecretKey(ck),
nonce: [kdfCkNextChainKey],
);
final mk = await hkdf.deriveKey(
secretKey: SecretKey(ck),
nonce: [kdfCkNextMessageKey],
);
return KdfCkResult(
await newCk.extractBytes(),
await mk.extractBytes(),
);
}

15
lib/src/helpers.dart Normal file
View File

@ -0,0 +1,15 @@
/// Flattens [inputs] and concatenates the elements.
List<int> concat(List<List<int>> inputs) {
final tmp = List<int>.empty(growable: true);
for (final input in inputs) {
tmp.addAll(input);
}
return tmp;
}
List<int> pkcs7padding(List<int> input, int size) {
final paddingLength = size - input.length % size;
final padding = List<int>.filled(paddingLength, 0x0);
return concat([input, padding]);
}

View File

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:cryptography/cryptography.dart'; import 'package:cryptography/cryptography.dart';
import 'package:omemo_dart/src/bundle.dart'; import 'package:omemo_dart/src/bundle.dart';
import 'package:omemo_dart/src/errors.dart'; import 'package:omemo_dart/src/errors.dart';
import 'package:omemo_dart/src/helpers.dart';
import 'package:omemo_dart/src/key.dart'; import 'package:omemo_dart/src/key.dart';
/// The overarching assumption is that we use Ed25519 keys for the identity keys /// The overarching assumption is that we use Ed25519 keys for the identity keys
@ -88,16 +89,6 @@ Future<List<int>> kdf(List<int> km) async {
return output.extractBytes(); return output.extractBytes();
} }
/// Flattens [inputs] and concatenates the elements.
List<int> concat(List<List<int>> inputs) {
final tmp = List<int>.empty(growable: true);
for (final input in inputs) {
tmp.addAll(input);
}
return tmp;
}
/// Alice builds a session with Bob using his bundle [bundle] and Alice's identity key /// Alice builds a session with Bob using his bundle [bundle] and Alice's identity key
/// pair [ik]. /// pair [ik].
Future<X3DHAliceResult> x3dhFromBundle(OmemoBundle bundle, OmemoKeyPair ik) async { Future<X3DHAliceResult> x3dhFromBundle(OmemoBundle bundle, OmemoKeyPair ik) async {

20
protobuf/schema.proto Normal file
View File

@ -0,0 +1,20 @@
// Taken from https://xmpp.org/extensions/xep-0384.html#protobuf-schema
message OMEMOMessage {
required uint32 n = 1;
required uint32 pn = 2;
required bytes dh_pub = 3;
optional bytes ciphertext = 4;
}
message OMEMOAuthenticatedMessage {
required bytes mac = 1;
required bytes message = 2; // Byte-encoding of an OMEMOMessage
}
message OMEMOKeyExchange {
required uint32 pk_id = 1;
required uint32 spk_id = 2;
required bytes ik = 3;
required bytes ek = 4;
required OMEMOAuthenticatedMessage message = 5;
}

View File

@ -5,12 +5,14 @@ version: 0.1.0
environment: environment:
sdk: '>=2.17.0 <3.0.0' sdk: '>=2.17.0 <3.0.0'
dependencies: dependencies:
cryptography: ^2.0.5 cryptography: ^2.0.5
pinenacl: ^0.5.1 pinenacl: ^0.5.1
protobuf: ^2.1.0
dev_dependencies: dev_dependencies:
lints: ^2.0.0 lints: ^2.0.0
protoc_plugin: ^20.0.1
test: ^1.21.0 test: ^1.21.0
very_good_analysis: ^3.0.1 very_good_analysis: ^3.0.1