? 如果你使用了上篇文章技术,在客户端中成功实现了端到端加密。那么恭喜你!这是真正值得骄傲的一步,也是值得庆祝的一步!但是,您可能已经注意到,有些事情仍处于粗糙边缘:您必须通过比较公钥来手动验证所有其他设备,解密密钥不会在您的设备之间共享等。如果您想在自己的客户端中实现更进一步这些功能,那么本指南适合您
?这是实现以下所有内容所需的一般路线图:
- 表情符号验证(SAS 或短验证字符串)
- 根据交叉签名状态显示验证状态
- SSSS(安全秘密存储和共享)
- 其他密钥的签名
- 房间内验证(通过房间内的消息进行验证)
- 杂项交叉签名的东西
- 引导 SSSS 和交叉签名
- 在线密钥备份
??1.实施表情符号验证(SAS)
? 表情符号验证是称为 SAS 的验证模型的一部分,用于短验证字符串。除了表情符号验证,还有数字验证。预计所有支持 SAS 的客户端都至少支持数字验证。这是为了确保仍然可以验证可能无法显示表情符号的客户端(可能是某些 CLI 应用程序或嵌入式设备显示)
验证过程概览
验证通过两个设备相互发送消息,通过 to_device 消息或通过房间中的消息进行。想要验证另一个设备 (Bob) 的设备 (Alice) 会发送一条m.key.verification.request ?消息及其支持的方法。Bob 用 回答m.key.verification.ready ,同时通知 Alice 它支持哪些方法。在客户端 UI 中,Bob 在收到m.key.verification.request ?消息后会弹出一个新的验证请求。在双方都接受开始后,任何一方(Alice 和/或 Bob)都可以发送一个m.key.verification.start 特定于所使用的验证方法的 。如果 Alice 和 Bob 都发送 a?m.key.verification.start 且验证方式不匹配,则取消验证请求。如果验证匹配,则m.key.verification.start 使用按字典顺序较小的用户 ID。如果它们也匹配(例如,如果您正在验证自己的设备),则使用字典顺序较小的设备 ID。这确保了m.key.verification.start ?使用哪个是明确的。从现在开始,使用的设备m.key.verification.start 就是发起请求的设备(这很重要,因为 SAS 中的某些事情取决于谁发起了请求)。之后,会发生一些验证过程,即特定的验证方法。如果一切都成功,则双方m.key.verification.done 互相发送一个。
为了使所有这些工作,每个验证请求都有一个唯一的交易 ID,这是一个不透明的字符串,用于标识特定的验证请求。在房间验证的情况下,这是第一个发送消息的事件 ID (?m.key.verification.request )。任何一方都可以随时取消验证请求,方法是发送一个m.key.verification.cancel 以及有关取消原因的一些信息。
send 由于发送的所有消息都会添加一些通用元数据,因此编写一个包装函数来发送密钥验证数据可能是一个好主意。此函数还可以将发送事件处理为 to_device 消息或房间事件。to_device 和房间验证的消息内容略有不同:
对于 to_device 消息,transaction_id 和from_device 被简单地添加到发送的对象中。?transaction_id 表示交易的唯一 ID,而from_device 只是一个包含您自己的设备 ID 的字符串。有效载荷,例如
因此看起来像:
{
"foo": "bar",
"transaction_id": "some-awesome-id",
"from_device": "DXKKF"
}
对于房间消息,事务ID,即第一个事件的事件ID,被添加到?m.relates_to 事件的部分。设备 ID 与 to_device 消息相同;它应该设置为发送设备的 ID。所以上面的payload就变成了:
{
"foo": "bar",
"from_device": "DXKKF",
"m.relates_to": {
"rel_type": "m.reference",
"event_id": "$firstEventId"
}
}
为简单起见,下面提到的所有有效负载都将在没有这些额外键的情况下显示。
发送的 to_devices 不必加密,因为验证请求的整个想法是它们可以公开。然而,客户端无论如何都可以选择这样做,例如,如果它们发送 to_device 消息的功能默认为加密发送它们。房间消息是否加密通常取决于发送它们的房间是否加密。
m.key.verification.request
发送这个会启动一个验证请求。它会发送您知道的所有验证方法(在 SAS 的情况下仅为m.sas.v1 ),以及当前时间戳(以毫秒为单位)。
{
"methods": ["m.sas.v1"],
"timestamp": 1590314157821
}
如果过去超过 10 分钟或未来超过 5 分钟,接收客户端将拒绝该请求。
m.key.verification.ready
发送此信息表示您接受密钥验证请求,并另外显示您自己支持的方法。这样,两个验证合作伙伴都将能够弄清楚他们有哪些共同的方法。
{
"methods": ["m.sas.v1"],
}
m.key.verification.start
发送此信息表示您正在使用特定方法开始验证。确切的有效载荷取决于验证方法。
{
"method": "m.sas.v1",
// additional keys specific to m.sas.v1
}
m.key.verification.done
发送此信息表示验证过程已完全完成。
m.key.verification.cancel
发送此消息会取消验证(超时、密钥不匹配、用户取消等)。它有一个人类可读的原因,以及一个取消代码。
{
"reason": "The verification timed out",
"code": "m.timeout"
}
回顾
回顾一下,Alice 想要验证 Bob的一般验证过程如下:
- Alice 向 Bob 发送
m.key.verification.request 了她支持的方法。 - Bob 收到该请求。他的设备询问他是否想与 Alice 进行验证。他点击接受。
m.key.verification.ready Bob连同他支持的方法一起发送。- Alice 收到 Bob 的方法,并根据该方法和她自己的
m.key.verification.start 方法,发送一个他们共有的方法。 - Bob 也发送了一个
m.key.verification.start ,这恰好和 alices 是同一个方法 - 由于 Alice 的用户 ID 在字典上较小(
@alice:example.org vs?@bob:example.org ),所以使用 Alice 发送的启动命令。 - 具体验证方法如下。
- 当 Alice 和 Bob 完全完成验证后,他们会互相发送一个
m.key.verification.done .
看看SAS
好的,现在我们已经了解了验证过程框架的一般工作原理,让我们具体看看 SAS。
由于遗留原因,不幸的是,有些客户直接希望使用 开始 SAS 验证?m.key.verification.start ,因此需要进行处理。如果有这样的启动请求进来,并且是 SAS,提示用户是否接受验证。然后照常继续 SAS 流程。
SAS 验证的一般思想是希望相互验证的双方 Alice 和 Bob 生成一个临时的公钥/私钥对并交换公钥。使用??ECDH,只要中间没有人,它们都会生成相同的密钥。然后使用表情符号或数字对生成的密钥进行比较。在验证它们在任一侧都匹配之后,就知道ECDH的输出是相同的。因此,相互发送的 ECDH 的临时公钥得到了成功验证。使用这些密钥,可以形成一个安全通道,发送想要相互验证的实际设备密钥。收到后,验证过程完成!
在实践中,它的工作原理如下:Alice 向 Bob 发送 am.key.verification.start 以及她支持的一堆不同参数(哈希方法、表情符号/数字验证、要使用的特定 ECDH 等),Bob 将回复 a?m.key.verification.accept ,这将基于根据 Alice 所宣传的能力,并根据 Bob 知道他自己可以做的事情,确定使用哪种确切的方法。之后,他们都生成并向对方发送 ECDH 的临时公钥,并且表情符号或数字会显示在屏幕上。一旦他们都使用某种安全的第三方渠道(例如亲自见面)验证他们匹配,他们将互相发送一个m.key.verification.mac ,其中包含他们应该验证的密钥的 MAC。验证后,他们都互相发送a?m.key.verification.done 并且他们被验证了!
m.key.verification.start
这会发送一堆发送设备支持的参数。请注意,在执行承诺检查(将在下一节中解释)时,您必须记住该对象,该对象包含transaction_id 和的元数据from_device ,在这些示例中省略。
{
"method": "m.sas.v1",
"key_agreement_protocols": ["curve25519-hkdf-sha256"],
"hashes": ["sha256"],
"message_authentication_codes": ["hkdf-hmac-sha256"],
"short_authentication_string": ["emoji", "decimal"]
}
承诺生成
承诺是一个额外的验证过程。它是您生成的连接临时公钥和?m.key.verification.start 正文的规范 json 的哈希值(基于指定的哈希方法)。Libolm 提供散列方法和公钥/私钥生成。计算承诺的代码如下所示:
var sas = olm.SAS(); // save for later. This will generate our ephemeral public/private keypair
var canonicalJson = ""; // the canonical json of the `m.key.verification.start` request
var commitment = "";
if (hashMethod == "sha256") {
var olmutil = olm.Utility();
commitment = olmutil.sha256(sas.get_pubkey() + canonicalJson);
olmutil.free();
} else {
throw "Unknown hash method";
}
m.key.verification.accept
这接受m.key.verification.start 请求,并发送双方支持的参数。除此之外,它还发送一个commitment ,如上定义。
{
"method": "m.sas.v1",
"key_agreement_protocol": "curve25519-hkdf-sha256",
"hash": "sha256",
"message_authentication_code": "hkdf-hmac-sha256",
"short_authentication_string": ["emoji", "decimal"],
"commitment": "the-commitment-calculated"
}
m.key.verification.key
这会将临时公钥相互发送。如果您尚未生成它(由于尚未计算承诺),请立即var sas = olm.SAS(); 使用sas.get_pubkey(); .?确保如果您收到了承诺,则使用收到的公钥来验证您之前收到的承诺。公钥既被添加到sas 对象中,sas.set_their_key(payload["key"]); ?又被保存在一个单独的变量中,因为稍后您将需要它,不幸的是,该sas 对象不允许再次检索它。
{
"key": "your-public-key"
}
显示表情符号/数字
根据商定的key_agreement_protocol 和short_authentication_string 支持的 s,现在应该提示用户使用表情符号/数字,以便他们可以验证它们是否匹配。为此,您必须生成字节以从中派生表情符号以进行比较。为此,创建了“SAS 信息”。对于密钥协商协议curve25519-hkdf-sha256 ,它的工作方式如下:
SAS 信息与开始验证的用户 ID 加上开始验证的设备 ID 加上开始验证的 SAS 公钥加上接收验证的用户 ID 加上接收验证的设备 IDMATRIX_KEY_VERIFICATION_SAS| 连接在一起。验证加上接收验证的 SAS 公钥加上交易 ID。在可能如下所示的代码中:
var ourInfo = '${client.userID}|${client.deviceID}|${sas.get_pubkey()}|';
var theirInfo = '${request.userId}|${request.deviceId}|${theirPublicKey}|';
var sasInfo = 'MATRIX_KEY_VERIFICATION_SAS|' +
(request.startedVerification
? ourInfo + theirInfo
: theirInfo + ourInfo) +
request.transactionId;
密钥协商协议curve25519 已弃用,不应实施。因此,这里没有概述。
生成字节
获得该 SAS 信息后,您可以使用它通过sas.generate_bytes(sasInfo, bytes) .?表情符号验证需要 6 个字节,数字验证需要 5 个字节。但是,您可以始终生成 6 个字节,并且对于数字验证,只使用前 5 个字节。
如果双方都说他们可以显示表情符号,那就显示那些!如果一方只支持号码,则显示号码。以防万一,最好有一个按钮在表情符号和数字之间来回切换。
数字
要获得您比较的数字,您需要使用上述方法生成 5 个字节。将它们分成三块,每块 13 位,丢弃最后一位。然后将 1000 添加到每个数字,这意味着它们可以在 1000 到 9191 的范围内。预计显示的数字之间带有一些分隔符,例如1337-4242-9001 。
表情符号
要让表情符号进行比较,您需要使用上述方法生成 6 个字节。将它们分成 7 块,每块 6 位,丢弃最后 6 位。
表情符号/数字匹配:计算 MAC
一旦用户说他们的表情符号/数字匹配,这意味着他们有一个安全的渠道!然后根据您希望对方验证的设备 ID 和公钥计算 MAC。目前,仅使用该方法发送您自己的公钥。在本指南的后面部分,您还将发送您的主交叉签名密钥。为此,创建了一个基本信息,它是连接的字符串?MATRIX_KEY_VERIFICATION_MAC ,您自己的用户 ID,您自己的设备 ID,其他用户的 ID,其他设备的 ID,以及交易 ID。请注意,与 SAS 不同,它不| 使用分隔符,并且顺序不取决于谁开始验证:
var baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' +
client.userID +
client.deviceID +
request.userId +
request.deviceId +
request.transactionId;
接下来计算您的设备 ID 的 MAC 和您要发送的指纹:
String _calculateMac(String input, String info) {
if (messageAuthenticationCode == "hkdf-hmac-sha256") { // this is from the m.key.verification.accept call
return sas.calculate_mac(input, info); // the same sas object we used previously, from libolm
} else {
throw "Unknown message authentication code";
}
}
Map<String, String> mac = {}; // create the map for the MACs
List<String> keyList = []; // the list / array holding all the key Ids to verify
// now add all the keys we want to verify
var deviceKeyId = "ed25519:${client.deviceID}";
mac[deviceKeyId] = _calculateMac(client.fingerprintKey, baseInfo + deviceKeyId); // client.fingerprintKey is here the public key string for our device
keyList.add(deviceKeyId);
// now we still need to sort the key list
keyList.sort();
var keys = _calculateMac(keyList.join(","), baseInfo + "KEY_IDS");
m.key.verification.mac
将密钥的 MAC 发送给对方以进行验证。
{
"mac": { // is the "mac" object calculated above
"ed25519:CCKDF": "<calculated MAC here>"
},
"keys": "<MAC for keys here>" // is the calculated "keys" string above
}
接收/验证 MAC
在收到 MAC 并让用户验证表情符号匹配(因此也发送您自己的 MAC)后,您必须验证收到的 MAC 是否有效,并且仅在 MAC 有效时验证密钥。为此,从对方的角度生成基础信息;本质上是把别人的信息放在你自己之前:
final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' +
request.userId +
request.deviceId +
client.userID +
client.deviceID +
request.transactionId;
然后从接收到的对象的字典键中生成键列表mac 并验证 MAC 是否匹配:
final keyList = payload["mac"].keys.toList();
keyList.sort();
if (payload["keys"] != _calculateMac(keyList.join(","), baseInfo + "KEY_IDS")) {
await request.cancel("m.key_mismatch");
return;
}
然后通过迭代对象来验证密钥本身的 MAC,并如上所述再次计算密钥的 MAC。您自己应该已经拥有公钥,因为您只是在验证密钥是否匹配。如果您收到了您不知道的密钥的 MAC,请忽略它。它可能是您还不知道如何处理的交叉签名密钥。但是,如果只有一个MAC 不匹配,则不要验证任何设备并取消整个请求。
最后发送一个m.key.verification.done ,你就完成了!
回顾
所以,整个 SAS 验证流程简而言之如下:
- (可选)Alice 向 Bob a 发送
m.key.verification.request 她支持的方法,包括?m.sas.v1 . - (可选)Bob 收到该请求。他的设备询问他是否想与 Alice 进行验证。他点击接受。
- (可选)Bob 发送一个
m.key.verification.ready 连同他支持的方法,包括m.sas.v1 . - Alice 收到 Bob 的方法,并基于此,以及她自己知道如何做的方法,她发送一个
m.key.verification.start for?m.sas.v1 。这将包含一系列关于 SAS 如何具体工作的参数。Alice 的设备记下请求的规范 json,以供以后的承诺验证。 - Bob 也可以发送一个
m.key.verification.start for?m.sas.v1 。但是,由于 Alice 用户 ID 在字典上较小,因此被丢弃。 - Bob 收到
m.key.verification.start ,并使用 生成他自己的临时密钥对var sas = olm.SAS(); 。然后,他使用请求的规范 json 来计算承诺m.key.verification.start ,然后将?m.key.verification.accept 回传给 Alice,以及用于此 SAS 验证的特定参数。 - Alice 收到
m.key.verification.accept 并存储承诺以供以后验证。她现在创建自己的临时密钥对,var sas = olm.SAS(); 并通过 向 Bob 发送公钥m.key.verification.key 。 - Bob 从 Alice 那里收到公钥并发送他自己的公钥。Bob 的设备现在显示表情符号/数字以供验证。
- Alice 收到 Bob 的公钥,最终可以验证她之前保存的承诺。如果全部匹配,Alice 的设备现在将显示表情符号/数字以进行验证。
- 如果表情符号/数字都匹配,它们将相互发送一个
m.key.verification.mac 带有 MAC 的应验证密钥信息。 - 最后,如果全部check out,则互相发送a?
m.key.verification.done ,验证过程结束。
需要注意的事项
- 始终验证传入消息是否具有有效的已知事务 ID。
- 始终验证传入消息不超过过去 10 分钟。
m.key.verification.cancel 无论出于何种原因,一旦收到 ,请务必立即停止验证过程。- 不要只是盲目地假设方法
message_authentication_code 等如这里所述。未来可能会添加更多内容,因此始终验证您实际支持哪些漏洞并采取相应措施。 sas.free(); 在验证结束或超时或取消时,不要忘记生成的 SAS 对象。- 不要忘记计算和验证承诺!
- 概括处理验证流程的代码可能是一个好主意,以便您可以在
m.sas.v1 以后轻松添加其他方法。
|