???: Move code around

This commit is contained in:
PapaTutuWawa 2022-08-03 15:13:03 +02:00
parent d3c8d813a9
commit 4e3e20f08c
6 changed files with 414 additions and 241 deletions

View File

@ -1,6 +1,8 @@
library omemo_dart;
export 'src/bundle.dart';
export 'src/double_ratchet.dart';
export 'src/errors.dart';
export 'src/helpers.dart';
export 'src/key.dart';
export 'src/x3dh.dart';

View File

@ -1,281 +1,196 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:meta/meta.dart';
import 'package:omemo_dart/protobuf/schema.pb.dart';
import 'package:omemo_dart/src/bundle.dart';
import 'package:omemo_dart/src/double_ratchet/crypto.dart';
import 'package:omemo_dart/src/double_ratchet/kdf.dart';
import 'package:omemo_dart/src/helpers.dart';
import 'package:omemo_dart/src/key.dart';
import 'package:omemo_dart/src/x3dh.dart';
class OmemoRatchetStepResult {
/// Amount of messages we may skip per session
const maxSkip = 1000;
const OmemoRatchetStepResult(this.header, this.cipherText);
final List<int> header;
final List<int> cipherText;
class RatchetStep {
const RatchetStep(this.header, this.ciphertext);
final OMEMOMessage header;
final List<int> ciphertext;
}
class OmemoEncryptionResult {
@immutable
class SkippedKey {
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;
const SkippedKey(this.dh, this.n);
final OmemoPublicKey dh;
final int n;
@override
bool operator ==(Object other) {
return other is SkippedKey && other.dh == dh && other.n == n;
}
/// The session state of one party
class AliceOmemoSession {
@override
int get hashCode => dh.hashCode ^ n.hashCode;
}
AliceOmemoSession(
this.dhs,
this.dhr,
this.ek,
this.rk,
this.cks,
this.ckr,
this.ns,
this.nr,
this.pn,
// this.skippedMessages,
this.ad,
class OmemoDoubleRatchet {
OmemoDoubleRatchet(
this.dhs, // DHs
this.dhr, // DHr
this.rk, // RK
this.cks, // CKs
this.ckr, // CKr
this.ns, // Ns
this.nr, // Nr
this.pn, // Pn
this.sessionAd,
);
/// The Diffie-Hellman sending key pair
final OmemoKeyPair dhs;
/// Sending DH keypair
OmemoKeyPair dhs;
/// The Diffie-Hellman receiving key pair
final OmemoPublicKey dhr;
/// Receiving Public key
OmemoPublicKey? dhr;
/// The EK used by X3DH
final OmemoKeyPair ek;
/// The Root Key
/// 32 byte Root Key
List<int> rk;
/// Sending Chain Key
List<int> cks;
/// Receiving Chain Key
/// Sending and receiving Chain Keys
List<int>? cks;
List<int>? ckr;
/// Message number for sending
/// Sending and receiving message numbers
int ns;
/// Message number for receiving
int nr;
/// Number of messages in the previous sending chain
/// Previous sending chain number
int pn;
/// The associated data from the X3DH
final List<int> ad;
final List<int> sessionAd;
// TODO(PapaTutuWawa): Track skipped over message keys
final Map<SkippedKey, List<int>> mkSkipped = {};
static Future<AliceOmemoSession> newSession(OmemoBundle bundle, OmemoKeyPair ik) async {
// TODO(PapaTutuWawa): Error handling
final x3dhResult = await x3dhFromBundle(bundle, ik);
/// This is performed by the initiating entity
static Future<OmemoDoubleRatchet> initiateNewSession(OmemoPublicKey spk, List<int> sk, List<int> ad) async {
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));
final dhr = spk;
final rk = await kdfRk(sk, await dh(dhs, dhr, 0));
final cks = rk;
return AliceOmemoSession(
return OmemoDoubleRatchet(
dhs,
dhr,
ek,
kdfRkResult.rk,
kdfRkResult.ck,
rk,
cks,
null,
0,
0,
0,
x3dhResult.ad,
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,
/// This is performed by the accepting entity
static Future<OmemoDoubleRatchet> acceptNewSession(OmemoKeyPair spk, List<int> sk, List<int> ad) async {
final dhs = spk;
return OmemoDoubleRatchet(
dhs,
null,
sk,
null,
null,
0,
0,
0,
ad,
);
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()
Future<RatchetStep> ratchetEncrypt(List<int> plaintext) async {
final newCks = await kdfCk(cks!, kdfCkNextChainKey);
final mk = await kdfCk(cks!, kdfCkNextMessageKey);
cks = newCks;
final header = OMEMOMessage()
..n = ns
..pn = pn
..n = ns;
final header = message.writeToBuffer();
..dhPub = await dhs.pk.getBytes();
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,
return RatchetStep(
header,
await encrypt(mk, plaintext, concat([sessionAd, header.writeToBuffer()]), sessionAd),
);
}
/// Result of the KDF_RK function from the Double Ratchet spec.
class KdfRkResult {
Future<List<int>?> trySkippedMessageKeys(OMEMOMessage header, List<int> ciphertext) async {
final key = SkippedKey(
// TODO(PapaTutuWawa): Is this correct
OmemoPublicKey.fromBytes(header.dhPub, KeyPairType.ed25519),
header.n,
);
if (mkSkipped.containsKey(key)) {
final mk = mkSkipped[key]!;
mkSkipped.remove(key);
const KdfRkResult(this.rk, this.ck);
/// 32 byte Root Key
final List<int> rk;
/// 32 byte Chain Key
final List<int> ck;
return decrypt(mk, ciphertext, concat([sessionAd, header.writeToBuffer()]), sessionAd);
}
/// 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;
return null;
}
/// 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<void> skipMessageKeys(int until) async {
if (nr + maxSkip < until) {
// TODO(PapaTutuWawa): Custom exception
throw Exception();
}
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(),
);
if (ckr != null) {
while (nr < until) {
final newCkr = await kdfCk(ckr!, kdfCkNextChainKey);
final mk = await kdfCk(ckr!, kdfCkNextMessageKey);
ckr = newCkr;
mkSkipped[SkippedKey(dhr!, nr)] = mk;
nr++;
}
}
}
Future<void> dhRatchet(OMEMOMessage header) async {
pn = header.n;
ns = 0;
nr = 0;
dhr = OmemoPublicKey.fromBytes(header.dhPub, KeyPairType.ed25519);
final newRk = await kdfRk(rk, await dh(dhs, dhr!, 2));
rk = newRk;
ckr = newRk;
dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
final newNewRk = await kdfRk(rk, await dh(dhs, dhr!, 2));
rk = newNewRk;
cks = newNewRk;
}
Future<List<int>> ratchetDecrypt(OMEMOMessage header, List<int> ciphertext) async {
// Check if we skipped too many messages
final plaintext = await trySkippedMessageKeys(header, ciphertext);
if (plaintext != null) {
return plaintext;
}
if (header.dhPub != await dhr?.getBytes()) {
await skipMessageKeys(header.pn);
await dhRatchet(header);
}
await skipMessageKeys(header.n);
final newCkr = await kdfCk(ckr!, kdfCkNextChainKey);
final mk = await kdfCk(ckr!, kdfCkNextMessageKey);
ckr = newCkr;
nr++;
return decrypt(mk, ciphertext, concat([sessionAd, header.writeToBuffer()]), sessionAd);
}
}

View File

@ -0,0 +1,109 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:omemo_dart/protobuf/schema.pb.dart';
import 'package:omemo_dart/src/helpers.dart';
/// Info string for ENCRYPT
const encryptHkdfInfoString = 'OMEMO Message Key Material';
/// cryptography _really_ wants to check the MAC output from AES-256-CBC. Since
/// we don't have it, we need the MAC check to always "pass".
class NoMacSecretBox extends SecretBox {
NoMacSecretBox(super.cipherText, { required super.nonce }) : super(mac: Mac.empty);
@override
Future<void> checkMac({
required MacAlgorithm macAlgorithm,
required SecretKey secretKey,
required List<int> aad,
}) async {}
}
/// Signals ENCRYPT function as specified by OMEMO 0.8.0.
/// Encrypt [plaintext] using the message key [mk], given associated_data [associatedData]
/// and the AD output from the X3DH [sessionAd].
Future<List<int>> encrypt(List<int> mk, List<int> plaintext, List<int> associatedData, List<int> sessionAd) async {
final hkdf = Hkdf(
hmac: Hmac(Sha256()),
outputLength: 80,
);
final hkdfResult = await hkdf.deriveKey(
secretKey: SecretKey(mk),
nonce: List<int>.filled(32, 0x0),
info: utf8.encode(encryptHkdfInfoString),
);
final hkdfBytes = await hkdfResult.extractBytes();
// Split hkdfBytes into encryption, authentication key and IV
final encryptionKey = hkdfBytes.sublist(0, 32);
final authenticationKey = hkdfBytes.sublist(32, 64);
final iv = hkdfBytes.sublist(64, 80);
final aesResult = await AesCbc.with256bits(
macAlgorithm: MacAlgorithm.empty,
).encrypt(
plaintext,
secretKey: SecretKey(encryptionKey),
nonce: iv,
);
final header = OMEMOMessage.fromBuffer(associatedData.sublist(sessionAd.length))
..ciphertext = aesResult.cipherText;
final headerBytes = header.writeToBuffer();
final hmacInput = concat([sessionAd, headerBytes]);
final hmacResult = (await Hmac.sha256().calculateMac(
hmacInput,
secretKey: SecretKey(authenticationKey),
)).bytes.sublist(0, 16);
final message = OMEMOAuthenticatedMessage()
..mac = hmacResult
..message = headerBytes;
return message.writeToBuffer();
}
/// Signals DECRYPT function as specified by OMEMO 0.8.0.
/// Decrypt [ciphertext] with the message key [mk], given the associated_data [associatedData]
/// and the AD output from the X3DH.
Future<List<int>> decrypt(List<int> mk, List<int> ciphertext, List<int> associatedData, List<int> sessionAd) async {
// Generate the keys and iv from mk
final hkdf = Hkdf(
hmac: Hmac(Sha256()),
outputLength: 80,
);
final hkdfResult = await hkdf.deriveKey(
secretKey: SecretKey(mk),
nonce: List<int>.filled(32, 0x0),
info: utf8.encode(encryptHkdfInfoString),
);
final hkdfBytes = await hkdfResult.extractBytes();
// Split hkdfBytes into encryption, authentication key and IV
final encryptionKey = hkdfBytes.sublist(0, 32);
final authenticationKey = hkdfBytes.sublist(32, 64);
final iv = hkdfBytes.sublist(64, 80);
// Assumption ciphertext is a OMEMOAuthenticatedMessage
final message = OMEMOAuthenticatedMessage.fromBuffer(ciphertext);
final header = OMEMOMessage.fromBuffer(message.message);
final hmacInput = concat([sessionAd, header.writeToBuffer()]);
final hmacResult = (await Hmac.sha256().calculateMac(
hmacInput,
secretKey: SecretKey(authenticationKey),
)).bytes.sublist(0, 16);
// TODO(PapaTutuWawa): Check the HMAC result
final plaintext = await AesCbc.with256bits(
macAlgorithm: MacAlgorithm.empty,
).decrypt(
NoMacSecretBox(
header.ciphertext,
nonce: iv,
),
secretKey: SecretKey(encryptionKey),
);
return plaintext;
}

View File

@ -0,0 +1,35 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
/// Info string for KDF_RK
const kdfRkInfoString = 'OMEMO Root Chain';
/// Flags for KDF_CK
const kdfCkNextMessageKey = 0x01;
const kdfCkNextChainKey = 0x02;
/// Signals KDF_CK function as specified by OMEMO 0.8.0.
Future<List<int>> kdfCk(List<int> ck, int constant) async {
final hkdf = Hkdf(hmac: Hmac(Sha256()), outputLength: 32);
final result = await hkdf.deriveKey(
secretKey: SecretKey(ck),
nonce: [constant],
);
return result.extractBytes();
}
/// Signals KDF_RK function as specified by OMEMO 0.8.0.
Future<List<int>> 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),
);
return result.extractBytes();
}

View File

@ -59,6 +59,9 @@ Future<List<int>> dh(OmemoKeyPair kp, OmemoPublicKey pk, int identityKey) async
ckp = await kp.toCurve25519();
} else if (identityKey == 2) {
cpk = await pk.toCurve25519();
} else if (identityKey == 3) {
ckp = await kp.toCurve25519();
cpk = await pk.toCurve25519();
}
final shared = await Cryptography.instance.x25519().sharedSecretKey(

View File

@ -0,0 +1,109 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:omemo_dart/omemo_dart.dart';
import 'package:omemo_dart/protobuf/schema.pb.dart';
import 'package:omemo_dart/src/double_ratchet/crypto.dart';
import 'package:test/test.dart';
void main() {
test('Test encrypting and decrypting', () async {
final sessionAd = List<int>.filled(32, 0x0);
final mk = List<int>.filled(32, 0x1);
final plaintext = utf8.encode('Hallo');
final header = OMEMOMessage()
..n = 0
..pn = 0
..dhPub = List<int>.empty();
final asd = concat([sessionAd, header.writeToBuffer()]);
final ciphertext = await encrypt(
mk,
plaintext,
asd,
sessionAd,
);
final decrypted = await decrypt(
mk,
ciphertext,
asd,
sessionAd,
);
expect(decrypted, plaintext);
});
/*
test('Test the Double Ratchet', () async {
// Generate keys
final ikAlice = await OmemoKeyPair.generateNewPair(KeyPairType.ed25519);
final ikBob = await OmemoKeyPair.generateNewPair(KeyPairType.ed25519);
final spkBob = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
final opkBob = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
final bundleBob = OmemoBundle(
'1',
await spkBob.pk.asBase64(),
'3',
base64Encode(
await sig(ikBob, await spkBob.pk.getBytes()),
),
//'Q5in+/L4kJixEX692h6mJkPMyp4I3SlQ84L0E7ipPzqfPHOMiraUlqG2vG/O8wvFjLsKYZpPBraga9IvwhqVDA==',
await ikBob.pk.asBase64(),
{
'2': await opkBob.pk.asBase64(),
},
);
// Alice does X3DH
final resultAlice = await x3dhFromBundle(bundleBob, ikAlice);
// Alice sends the inital message to Bob
// ...
// Bob does X3DH
final resultBob = await x3dhFromInitialMessage(
X3DHMessage(
ikAlice.pk,
resultAlice.ek.pk,
'2',
),
spkBob,
opkBob,
ikBob,
);
print('X3DH key exchange done');
// Alice and Bob now share sk as a common secret and ad
final alicesRatchet = await OmemoDoubleRatchet.initiateNewSession(
spkBob.pk,
resultAlice.sk,
resultAlice.ad,
);
final bobsRatchet = await OmemoDoubleRatchet.acceptNewSession(
spkBob,
resultBob.sk,
resultBob.ad,
);
expect(alicesRatchet.sessionAd, bobsRatchet.sessionAd);
//expect(await alicesRatchet.dhr.getBytes(), await ikBob.pk.getBytes());
// Alice encrypts a message
final aliceRatchetResult = await alicesRatchet.ratchetEncrypt(utf8.encode('Hello Bob'));
print('Alice sent the message');
// Alice sends it to Bob
// ...
// Bob tries to decrypt it
final bobRatchetResult = await bobsRatchet.ratchetDecrypt(
aliceRatchetResult.header,
aliceRatchetResult.ciphertext,
);
print('Bob decrypted the message');
expect(utf8.encode('Hello Bob'), bobRatchetResult);
});
*/
}