mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 07:04:07 +00:00
start with sealed sender
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
This commit is contained in:
parent
b98a434a8e
commit
e2c616ff21
10 changed files with 1022 additions and 30 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit b874b9cc309822d90b940fdfbc8fd1ad3afc2450
|
||||
Subproject commit 76a36c4d3da1fd0da04da3aa5a6c2b3b9c984925
|
||||
|
|
@ -21,6 +21,291 @@ export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
|
|||
|
||||
export 'messages.pbenum.dart';
|
||||
|
||||
class EncryptedMessageEnvelope extends $pb.GeneratedMessage {
|
||||
factory EncryptedMessageEnvelope({
|
||||
$core.List<$core.int>? ephemeralPublicKey,
|
||||
$core.List<$core.int>? nonce,
|
||||
$core.List<$core.int>? ciphertext,
|
||||
}) {
|
||||
final result = create();
|
||||
if (ephemeralPublicKey != null)
|
||||
result.ephemeralPublicKey = ephemeralPublicKey;
|
||||
if (nonce != null) result.nonce = nonce;
|
||||
if (ciphertext != null) result.ciphertext = ciphertext;
|
||||
return result;
|
||||
}
|
||||
|
||||
EncryptedMessageEnvelope._();
|
||||
|
||||
factory EncryptedMessageEnvelope.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory EncryptedMessageEnvelope.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'EncryptedMessageEnvelope',
|
||||
createEmptyInstance: create)
|
||||
..a<$core.List<$core.int>>(
|
||||
1, _omitFieldNames ? '' : 'ephemeralPublicKey', $pb.PbFieldType.OY)
|
||||
..a<$core.List<$core.int>>(
|
||||
2, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OY)
|
||||
..a<$core.List<$core.int>>(
|
||||
3, _omitFieldNames ? '' : 'ciphertext', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
EncryptedMessageEnvelope clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
EncryptedMessageEnvelope copyWith(
|
||||
void Function(EncryptedMessageEnvelope) updates) =>
|
||||
super.copyWith((message) => updates(message as EncryptedMessageEnvelope))
|
||||
as EncryptedMessageEnvelope;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EncryptedMessageEnvelope create() => EncryptedMessageEnvelope._();
|
||||
@$core.override
|
||||
EncryptedMessageEnvelope createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EncryptedMessageEnvelope getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<EncryptedMessageEnvelope>(create);
|
||||
static EncryptedMessageEnvelope? _defaultInstance;
|
||||
|
||||
/// Fresh 32-byte X25519 public key generated for this envelope. The sender
|
||||
/// derives the encryption key from this ephemeral key pair and the recipient's
|
||||
/// public identity key using HKDF-SHA-256 with an empty salt and a 32-byte
|
||||
/// output. HKDF info must be the UTF-8 bytes of
|
||||
/// "twonly-message-envelope-encryption-v1" followed by ephemeral_public_key
|
||||
/// and the recipient's serialized public identity key.
|
||||
@$pb.TagNumber(1)
|
||||
$core.List<$core.int> get ephemeralPublicKey => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set ephemeralPublicKey($core.List<$core.int> value) => $_setBytes(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasEphemeralPublicKey() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearEphemeralPublicKey() => $_clearField(1);
|
||||
|
||||
/// Fresh 24-byte XChaCha20-Poly1305 nonce.
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get nonce => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set nonce($core.List<$core.int> value) => $_setBytes(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasNonce() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearNonce() => $_clearField(2);
|
||||
|
||||
/// Serialized MessageEnvelope encrypted with XChaCha20-Poly1305. This includes
|
||||
/// the 16-byte Poly1305 authentication tag. The AEAD associated data is exactly
|
||||
/// the UTF-8 bytes of "twonly-message-envelope-encryption-v1".
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<$core.int> get ciphertext => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set ciphertext($core.List<$core.int> value) => $_setBytes(2, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasCiphertext() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearCiphertext() => $_clearField(3);
|
||||
}
|
||||
|
||||
class MessageEnvelope extends $pb.GeneratedMessage {
|
||||
factory MessageEnvelope({
|
||||
$core.List<$core.int>? signedPayload,
|
||||
$core.List<$core.int>? signature,
|
||||
}) {
|
||||
final result = create();
|
||||
if (signedPayload != null) result.signedPayload = signedPayload;
|
||||
if (signature != null) result.signature = signature;
|
||||
return result;
|
||||
}
|
||||
|
||||
MessageEnvelope._();
|
||||
|
||||
factory MessageEnvelope.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory MessageEnvelope.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'MessageEnvelope',
|
||||
createEmptyInstance: create)
|
||||
..a<$core.List<$core.int>>(
|
||||
1, _omitFieldNames ? '' : 'signedPayload', $pb.PbFieldType.OY)
|
||||
..a<$core.List<$core.int>>(
|
||||
2, _omitFieldNames ? '' : 'signature', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
MessageEnvelope clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
MessageEnvelope copyWith(void Function(MessageEnvelope) updates) =>
|
||||
super.copyWith((message) => updates(message as MessageEnvelope))
|
||||
as MessageEnvelope;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static MessageEnvelope create() => MessageEnvelope._();
|
||||
@$core.override
|
||||
MessageEnvelope createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static MessageEnvelope getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<MessageEnvelope>(create);
|
||||
static MessageEnvelope? _defaultInstance;
|
||||
|
||||
/// Exact serialized MessageEnvelopePayload bytes. The signature is calculated
|
||||
/// directly over these bytes. Keeping the serialized payload allows verification
|
||||
/// without relying on canonical protobuf serialization.
|
||||
@$pb.TagNumber(1)
|
||||
$core.List<$core.int> get signedPayload => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set signedPayload($core.List<$core.int> value) => $_setBytes(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasSignedPayload() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearSignedPayload() => $_clearField(1);
|
||||
|
||||
/// Signature created with the sender's identity key. For a known contact, use
|
||||
/// the pinned public identity key. For an unknown sender, fetch the public key
|
||||
/// from the trusted server, verify this signature, and only then pin the key and
|
||||
/// process the raw message.
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get signature => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set signature($core.List<$core.int> value) => $_setBytes(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasSignature() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearSignature() => $_clearField(2);
|
||||
}
|
||||
|
||||
class MessageEnvelopePayload extends $pb.GeneratedMessage {
|
||||
factory MessageEnvelopePayload({
|
||||
$core.String? magic,
|
||||
$fixnum.Int64? fromUserId,
|
||||
$fixnum.Int64? recipientUserId,
|
||||
Message? message,
|
||||
$fixnum.Int64? createdAtUnixSeconds,
|
||||
}) {
|
||||
final result = create();
|
||||
if (magic != null) result.magic = magic;
|
||||
if (fromUserId != null) result.fromUserId = fromUserId;
|
||||
if (recipientUserId != null) result.recipientUserId = recipientUserId;
|
||||
if (message != null) result.message = message;
|
||||
if (createdAtUnixSeconds != null)
|
||||
result.createdAtUnixSeconds = createdAtUnixSeconds;
|
||||
return result;
|
||||
}
|
||||
|
||||
MessageEnvelopePayload._();
|
||||
|
||||
factory MessageEnvelopePayload.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory MessageEnvelopePayload.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'MessageEnvelopePayload',
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, _omitFieldNames ? '' : 'magic')
|
||||
..aInt64(2, _omitFieldNames ? '' : 'fromUserId')
|
||||
..aInt64(3, _omitFieldNames ? '' : 'recipientUserId')
|
||||
..aOM<Message>(4, _omitFieldNames ? '' : 'message',
|
||||
subBuilder: Message.create)
|
||||
..aInt64(5, _omitFieldNames ? '' : 'createdAtUnixSeconds')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
MessageEnvelopePayload clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
MessageEnvelopePayload copyWith(
|
||||
void Function(MessageEnvelopePayload) updates) =>
|
||||
super.copyWith((message) => updates(message as MessageEnvelopePayload))
|
||||
as MessageEnvelopePayload;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static MessageEnvelopePayload create() => MessageEnvelopePayload._();
|
||||
@$core.override
|
||||
MessageEnvelopePayload createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static MessageEnvelopePayload getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<MessageEnvelopePayload>(create);
|
||||
static MessageEnvelopePayload? _defaultInstance;
|
||||
|
||||
/// Domain-separation and protocol-version marker. The receiver must reject the
|
||||
/// payload unless this is exactly "twonly-message-envelope-v1".
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get magic => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set magic($core.String value) => $_setString(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasMagic() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearMagic() => $_clearField(1);
|
||||
|
||||
/// The sender is protected by the encryption of the outer MessageEnvelope.
|
||||
@$pb.TagNumber(2)
|
||||
$fixnum.Int64 get fromUserId => $_getI64(1);
|
||||
@$pb.TagNumber(2)
|
||||
set fromUserId($fixnum.Int64 value) => $_setInt64(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasFromUserId() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearFromUserId() => $_clearField(2);
|
||||
|
||||
/// Bind the sender's signature to the intended recipient. This prevents a
|
||||
/// recipient from re-encrypting a valid signed envelope for another user. The
|
||||
/// binding is required because raw_message is not always a Signal ciphertext
|
||||
/// and therefore is not always independently bound to a Signal session. The
|
||||
/// receiver must reject an envelope when this ID does not match its own user ID.
|
||||
@$pb.TagNumber(3)
|
||||
$fixnum.Int64 get recipientUserId => $_getI64(2);
|
||||
@$pb.TagNumber(3)
|
||||
set recipientUserId($fixnum.Int64 value) => $_setInt64(2, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasRecipientUserId() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearRecipientUserId() => $_clearField(3);
|
||||
|
||||
/// The mandatory UUIDv4 receipt_id is covered by the envelope signature and is
|
||||
/// used by the receiver to reject replayed messages before processing them.
|
||||
@$pb.TagNumber(4)
|
||||
Message get message => $_getN(3);
|
||||
@$pb.TagNumber(4)
|
||||
set message(Message value) => $_setField(4, value);
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasMessage() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearMessage() => $_clearField(4);
|
||||
@$pb.TagNumber(4)
|
||||
Message ensureMessage() => $_ensure(3);
|
||||
|
||||
/// UTC Unix timestamp in seconds when the envelope was created. The receiver
|
||||
/// must reject envelopes older than 45 days or more than five minutes in the
|
||||
/// future. This field is covered by the envelope signature.
|
||||
@$pb.TagNumber(5)
|
||||
$fixnum.Int64 get createdAtUnixSeconds => $_getI64(4);
|
||||
@$pb.TagNumber(5)
|
||||
set createdAtUnixSeconds($fixnum.Int64 value) => $_setInt64(4, value);
|
||||
@$pb.TagNumber(5)
|
||||
$core.bool hasCreatedAtUnixSeconds() => $_has(4);
|
||||
@$pb.TagNumber(5)
|
||||
void clearCreatedAtUnixSeconds() => $_clearField(5);
|
||||
}
|
||||
|
||||
class Message extends $pb.GeneratedMessage {
|
||||
factory Message({
|
||||
Message_Type? type,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,67 @@ import 'dart:convert' as $convert;
|
|||
import 'dart:core' as $core;
|
||||
import 'dart:typed_data' as $typed_data;
|
||||
|
||||
@$core.Deprecated('Use encryptedMessageEnvelopeDescriptor instead')
|
||||
const EncryptedMessageEnvelope$json = {
|
||||
'1': 'EncryptedMessageEnvelope',
|
||||
'2': [
|
||||
{
|
||||
'1': 'ephemeral_public_key',
|
||||
'3': 1,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'10': 'ephemeralPublicKey'
|
||||
},
|
||||
{'1': 'nonce', '3': 2, '4': 1, '5': 12, '10': 'nonce'},
|
||||
{'1': 'ciphertext', '3': 3, '4': 1, '5': 12, '10': 'ciphertext'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `EncryptedMessageEnvelope`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List encryptedMessageEnvelopeDescriptor = $convert.base64Decode(
|
||||
'ChhFbmNyeXB0ZWRNZXNzYWdlRW52ZWxvcGUSMAoUZXBoZW1lcmFsX3B1YmxpY19rZXkYASABKA'
|
||||
'xSEmVwaGVtZXJhbFB1YmxpY0tleRIUCgVub25jZRgCIAEoDFIFbm9uY2USHgoKY2lwaGVydGV4'
|
||||
'dBgDIAEoDFIKY2lwaGVydGV4dA==');
|
||||
|
||||
@$core.Deprecated('Use messageEnvelopeDescriptor instead')
|
||||
const MessageEnvelope$json = {
|
||||
'1': 'MessageEnvelope',
|
||||
'2': [
|
||||
{'1': 'signed_payload', '3': 1, '4': 1, '5': 12, '10': 'signedPayload'},
|
||||
{'1': 'signature', '3': 2, '4': 1, '5': 12, '10': 'signature'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `MessageEnvelope`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List messageEnvelopeDescriptor = $convert.base64Decode(
|
||||
'Cg9NZXNzYWdlRW52ZWxvcGUSJQoOc2lnbmVkX3BheWxvYWQYASABKAxSDXNpZ25lZFBheWxvYW'
|
||||
'QSHAoJc2lnbmF0dXJlGAIgASgMUglzaWduYXR1cmU=');
|
||||
|
||||
@$core.Deprecated('Use messageEnvelopePayloadDescriptor instead')
|
||||
const MessageEnvelopePayload$json = {
|
||||
'1': 'MessageEnvelopePayload',
|
||||
'2': [
|
||||
{'1': 'magic', '3': 1, '4': 1, '5': 9, '10': 'magic'},
|
||||
{'1': 'from_user_id', '3': 2, '4': 1, '5': 3, '10': 'fromUserId'},
|
||||
{'1': 'recipient_user_id', '3': 3, '4': 1, '5': 3, '10': 'recipientUserId'},
|
||||
{'1': 'message', '3': 4, '4': 1, '5': 11, '6': '.Message', '10': 'message'},
|
||||
{
|
||||
'1': 'created_at_unix_seconds',
|
||||
'3': 5,
|
||||
'4': 1,
|
||||
'5': 3,
|
||||
'10': 'createdAtUnixSeconds'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `MessageEnvelopePayload`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List messageEnvelopePayloadDescriptor = $convert.base64Decode(
|
||||
'ChZNZXNzYWdlRW52ZWxvcGVQYXlsb2FkEhQKBW1hZ2ljGAEgASgJUgVtYWdpYxIgCgxmcm9tX3'
|
||||
'VzZXJfaWQYAiABKANSCmZyb21Vc2VySWQSKgoRcmVjaXBpZW50X3VzZXJfaWQYAyABKANSD3Jl'
|
||||
'Y2lwaWVudFVzZXJJZBIiCgdtZXNzYWdlGAQgASgLMgguTWVzc2FnZVIHbWVzc2FnZRI1Chdjcm'
|
||||
'VhdGVkX2F0X3VuaXhfc2Vjb25kcxgFIAEoA1IUY3JlYXRlZEF0VW5peFNlY29uZHM=');
|
||||
|
||||
@$core.Deprecated('Use messageDescriptor instead')
|
||||
const Message$json = {
|
||||
'1': 'Message',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,64 @@
|
|||
syntax = "proto3";
|
||||
|
||||
message EncryptedMessageEnvelope {
|
||||
// Fresh 32-byte X25519 public key generated for this envelope. The sender
|
||||
// derives the encryption key from this ephemeral key pair and the recipient's
|
||||
// public identity key using HKDF-SHA-256 with an empty salt and a 32-byte
|
||||
// output. HKDF info must be the UTF-8 bytes of
|
||||
// "twonly-message-envelope-encryption-v1" followed by ephemeral_public_key
|
||||
// and the recipient's serialized public identity key.
|
||||
bytes ephemeral_public_key = 1;
|
||||
|
||||
// Fresh 24-byte XChaCha20-Poly1305 nonce.
|
||||
bytes nonce = 2;
|
||||
|
||||
// Serialized MessageEnvelope encrypted with XChaCha20-Poly1305. This includes
|
||||
// the 16-byte Poly1305 authentication tag. The AEAD associated data is exactly
|
||||
// the UTF-8 bytes of "twonly-message-envelope-encryption-v1".
|
||||
bytes ciphertext = 3;
|
||||
}
|
||||
|
||||
message MessageEnvelope {
|
||||
// The serialized MessageEnvelope is encrypted into EncryptedMessageEnvelope
|
||||
// before it is sent to the server.
|
||||
|
||||
// Exact serialized MessageEnvelopePayload bytes. The signature is calculated
|
||||
// directly over these bytes. Keeping the serialized payload allows verification
|
||||
// without relying on canonical protobuf serialization.
|
||||
bytes signed_payload = 1;
|
||||
|
||||
// Signature created with the sender's identity key. For a known contact, use
|
||||
// the pinned public identity key. For an unknown sender, fetch the public key
|
||||
// from the trusted server, verify this signature, and only then pin the key and
|
||||
// process the raw message.
|
||||
bytes signature = 2;
|
||||
}
|
||||
|
||||
message MessageEnvelopePayload {
|
||||
// Domain-separation and protocol-version marker. The receiver must reject the
|
||||
// payload unless this is exactly "twonly-message-envelope-v1".
|
||||
string magic = 1;
|
||||
|
||||
// The sender is protected by the encryption of the outer MessageEnvelope.
|
||||
int64 from_user_id = 2;
|
||||
|
||||
// Bind the sender's signature to the intended recipient. This prevents a
|
||||
// recipient from re-encrypting a valid signed envelope for another user. The
|
||||
// binding is required because raw_message is not always a Signal ciphertext
|
||||
// and therefore is not always independently bound to a Signal session. The
|
||||
// receiver must reject an envelope when this ID does not match its own user ID.
|
||||
int64 recipient_user_id = 3;
|
||||
|
||||
// The mandatory UUIDv4 receipt_id is covered by the envelope signature and is
|
||||
// used by the receiver to reject replayed messages before processing them.
|
||||
Message message = 4;
|
||||
|
||||
// UTC Unix timestamp in seconds when the envelope was created. The receiver
|
||||
// must reject envelopes older than 45 days or more than five minutes in the
|
||||
// future. This field is covered by the envelope signature.
|
||||
int64 created_at_unix_seconds = 5;
|
||||
}
|
||||
|
||||
message Message {
|
||||
enum Type {
|
||||
SENDER_DELIVERY_RECEIPT = 0;
|
||||
|
|
@ -38,29 +97,29 @@ message EncryptedContent {
|
|||
/// This can be added, so the receiver can check weather he is up to date with the current profile
|
||||
optional int64 sender_profile_counter = 4;
|
||||
optional bytes sender_user_discovery_version = 21;
|
||||
optional bool ask_for_friend_promotions = 25;
|
||||
optional bool ask_for_friend_promotions = 25;
|
||||
|
||||
optional MessageUpdate message_update = 5;
|
||||
optional Media media = 6;
|
||||
optional MediaUpdate media_update = 7;
|
||||
optional ContactUpdate contact_update = 8;
|
||||
optional ContactRequest contact_request = 9;
|
||||
optional FlameSync flame_sync = 10;
|
||||
optional PushKeys push_keys = 11;
|
||||
optional Reaction reaction = 12;
|
||||
optional TextMessage text_message = 13;
|
||||
optional GroupCreate group_create = 14;
|
||||
optional GroupJoin group_join = 15;
|
||||
optional GroupUpdate group_update = 16;
|
||||
optional ResendGroupPublicKey resend_group_public_key = 17;
|
||||
optional ErrorMessages error_messages = 18;
|
||||
optional AdditionalDataMessage additional_data_message = 19;
|
||||
optional TypingIndicator typing_indicator = 20;
|
||||
optional UserDiscoveryRequest user_discovery_request = 22;
|
||||
optional UserDiscoveryUpdate user_discovery_update = 23;
|
||||
optional KeyVerificationProof key_verification_proof = 24;
|
||||
optional PasswordLessRecovery passwordless_recovery = 26;
|
||||
optional PasswordLessRecoveryHeartbeat passwordless_recovery_heartbeat = 27;
|
||||
optional MessageUpdate message_update = 5;
|
||||
optional Media media = 6;
|
||||
optional MediaUpdate media_update = 7;
|
||||
optional ContactUpdate contact_update = 8;
|
||||
optional ContactRequest contact_request = 9;
|
||||
optional FlameSync flame_sync = 10;
|
||||
optional PushKeys push_keys = 11;
|
||||
optional Reaction reaction = 12;
|
||||
optional TextMessage text_message = 13;
|
||||
optional GroupCreate group_create = 14;
|
||||
optional GroupJoin group_join = 15;
|
||||
optional GroupUpdate group_update = 16;
|
||||
optional ResendGroupPublicKey resend_group_public_key = 17;
|
||||
optional ErrorMessages error_messages = 18;
|
||||
optional AdditionalDataMessage additional_data_message = 19;
|
||||
optional TypingIndicator typing_indicator = 20;
|
||||
optional UserDiscoveryRequest user_discovery_request = 22;
|
||||
optional UserDiscoveryUpdate user_discovery_update = 23;
|
||||
optional KeyVerificationProof key_verification_proof = 24;
|
||||
optional PasswordLessRecovery passwordless_recovery = 26;
|
||||
optional PasswordLessRecoveryHeartbeat passwordless_recovery_heartbeat = 27;
|
||||
|
||||
message ErrorMessages {
|
||||
enum Type {
|
||||
|
|
@ -75,9 +134,9 @@ message EncryptedContent {
|
|||
|
||||
message GroupCreate {
|
||||
// key for the state stored on the server
|
||||
bytes state_key = 3;
|
||||
bytes group_public_key = 4;
|
||||
optional string group_name = 5;
|
||||
bytes state_key = 3;
|
||||
bytes group_public_key = 4;
|
||||
optional string group_name = 5;
|
||||
}
|
||||
|
||||
message GroupJoin {
|
||||
|
|
@ -220,12 +279,12 @@ message EncryptedContent {
|
|||
|
||||
message PasswordLessRecovery {
|
||||
optional bytes recoverySecretShare = 1;
|
||||
bool delete = 2;
|
||||
int64 threshold = 3;
|
||||
bool delete = 2;
|
||||
int64 threshold = 3;
|
||||
}
|
||||
|
||||
message PasswordLessRecoveryHeartbeat {
|
||||
bytes hash = 1;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
39
rust/Cargo.lock
generated
39
rust/Cargo.lock
generated
|
|
@ -381,6 +381,17 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher 0.4.4",
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.1"
|
||||
|
|
@ -392,6 +403,19 @@ dependencies = [
|
|||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chacha20poly1305"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"chacha20 0.9.1",
|
||||
"cipher 0.4.4",
|
||||
"poly1305",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
|
|
@ -414,6 +438,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
|||
dependencies = [
|
||||
"crypto-common 0.1.7",
|
||||
"inout 0.1.4",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2196,6 +2221,17 @@ version = "0.3.33"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
||||
dependencies = [
|
||||
"cpufeatures 0.2.17",
|
||||
"opaque-debug",
|
||||
"universal-hash 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.6.2"
|
||||
|
|
@ -2422,7 +2458,7 @@ version = "0.10.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"chacha20 0.10.1",
|
||||
"getrandom 0.4.3",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
|
@ -2539,6 +2575,7 @@ dependencies = [
|
|||
"async-trait",
|
||||
"base64",
|
||||
"blahaj",
|
||||
"chacha20poly1305",
|
||||
"chrono",
|
||||
"flutter_rust_bridge",
|
||||
"hex",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ hmac = "0.13.0"
|
|||
hkdf = "0.12.4"
|
||||
sha2 = "0.10.8"
|
||||
aes-gcm = "0.10.3"
|
||||
chacha20poly1305 = "0.10.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-appender = "0.2.5"
|
||||
paste = "1.0.15"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
use std::io::Result;
|
||||
fn main() -> Result<()> {
|
||||
prost_build::compile_protos(&["src/user_discovery/types.proto"], &["src/"])?;
|
||||
prost_build::Config::new()
|
||||
.include_file("client_messages.rs")
|
||||
.compile_protos(
|
||||
&["../lib/src/model/protobuf/client/messages.proto"],
|
||||
&["../lib/src/model/protobuf/client/"],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use crate::{
|
|||
log::init_tracing,
|
||||
utils::Shared,
|
||||
};
|
||||
use libsignal_protocol::IdentityKey;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use tokio::sync::{Mutex, OnceCell, RwLock};
|
||||
use zeroize::Zeroize;
|
||||
|
|
@ -253,6 +254,21 @@ impl Context {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_identity(&self, user_id: i64) -> Result<Option<IdentityKey>> {
|
||||
let database = self.get_rust_database().await;
|
||||
let identity_key: Option<Vec<u8>> =
|
||||
sqlx::query_scalar("SELECT identity_key FROM signal_identities WHERE name = ?")
|
||||
.bind(user_id.to_string())
|
||||
.fetch_optional(&database.pool)
|
||||
.await?;
|
||||
|
||||
identity_key
|
||||
.map(|bytes| {
|
||||
IdentityKey::decode(&bytes).map_err(|error| TwonlyError::Signal(error.to_string()))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub(crate) async fn replace_rust_database(
|
||||
&self,
|
||||
database: Database,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ mod error;
|
|||
mod frb_generated;
|
||||
mod keys;
|
||||
mod log;
|
||||
pub mod sealed_sender;
|
||||
mod secure_storage;
|
||||
pub mod signal;
|
||||
mod user_discovery;
|
||||
|
|
|
|||
526
rust/src/sealed_sender.rs
Normal file
526
rust/src/sealed_sender.rs
Normal file
|
|
@ -0,0 +1,526 @@
|
|||
use chacha20poly1305::{
|
||||
aead::{Aead, Payload},
|
||||
KeyInit, XChaCha20Poly1305, XNonce,
|
||||
};
|
||||
use hkdf::Hkdf;
|
||||
use libsignal_protocol::{IdentityKeyPair, KeyPair, PublicKey};
|
||||
use prost::Message as ProstMessage;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use sha2::Sha256;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::context::Context;
|
||||
|
||||
pub mod proto {
|
||||
include!(concat!(env!("OUT_DIR"), "/client_messages.rs"));
|
||||
}
|
||||
|
||||
const PAYLOAD_MAGIC: &str = "twonly-message-envelope-v1";
|
||||
const ENCRYPTION_CONTEXT: &[u8] = b"twonly-message-envelope-encryption-v1";
|
||||
const X25519_PUBLIC_KEY_SIZE: usize = 32;
|
||||
const XCHACHA20_NONCE_SIZE: usize = 24;
|
||||
const POLY1305_TAG_SIZE: usize = 16;
|
||||
const ENVELOPE_VALIDITY_SECONDS: i64 = 45 * 24 * 60 * 60;
|
||||
const MAX_FUTURE_CLOCK_SKEW_SECONDS: i64 = 5 * 60;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, SealedSenderError>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SealedSenderError {
|
||||
#[error("invalid protobuf: {0}")]
|
||||
InvalidProtobuf(#[from] prost::DecodeError),
|
||||
#[error("invalid ephemeral public key")]
|
||||
InvalidEphemeralPublicKey,
|
||||
#[error("invalid XChaCha20-Poly1305 nonce")]
|
||||
InvalidNonce,
|
||||
#[error("invalid XChaCha20-Poly1305 ciphertext")]
|
||||
InvalidCiphertext,
|
||||
#[error("X25519 key agreement failed")]
|
||||
KeyAgreement,
|
||||
#[error("sender signature generation failed")]
|
||||
SignatureGeneration,
|
||||
#[error("HKDF key derivation failed")]
|
||||
KeyDerivation,
|
||||
#[error("XChaCha20-Poly1305 encryption failed")]
|
||||
EncryptionFailed,
|
||||
#[error("XChaCha20-Poly1305 authentication failed")]
|
||||
AuthenticationFailed,
|
||||
#[error("invalid sealed-sender payload magic")]
|
||||
InvalidMagic,
|
||||
#[error("message was addressed to user {actual}, not user {expected}")]
|
||||
WrongRecipient { expected: i64, actual: i64 },
|
||||
#[error("sealed-sender payload does not contain a message")]
|
||||
MissingMessage,
|
||||
#[error("sealed-sender message has an empty receipt ID")]
|
||||
MissingReceiptId,
|
||||
#[error("no identity key is available for sender {0}")]
|
||||
UnknownSender(i64),
|
||||
#[error("invalid sender signature")]
|
||||
InvalidSignature,
|
||||
#[error("sealed-sender context error: {0}")]
|
||||
Context(String),
|
||||
#[error("the local user ID is unavailable")]
|
||||
MissingLocalUserId,
|
||||
#[error("the local Signal identity is unavailable")]
|
||||
MissingLocalIdentity,
|
||||
#[error("invalid local Signal identity")]
|
||||
InvalidLocalIdentity,
|
||||
#[error("system clock is before the Unix epoch")]
|
||||
InvalidSystemTime,
|
||||
#[error("sealed-sender envelope has expired")]
|
||||
ExpiredEnvelope,
|
||||
#[error("sealed-sender envelope timestamp is too far in the future")]
|
||||
FutureEnvelope,
|
||||
}
|
||||
|
||||
/// Encrypts and decrypts twonly sealed-sender envelopes.
|
||||
///
|
||||
/// The outer encryption authenticates the ciphertext but intentionally does not
|
||||
/// authenticate the sender. Sender authentication is provided by the XEdDSA
|
||||
/// identity-key signature inside the encrypted envelope.
|
||||
pub(crate) struct SealedSender;
|
||||
|
||||
impl SealedSender {
|
||||
pub(crate) fn encrypt<R>(
|
||||
from_user_id: i64,
|
||||
recipient_user_id: i64,
|
||||
message: proto::Message,
|
||||
sender_identity: &IdentityKeyPair,
|
||||
recipient_identity_key: &PublicKey,
|
||||
rng: &mut R,
|
||||
) -> Result<Vec<u8>>
|
||||
where
|
||||
R: Rng + CryptoRng + ?Sized,
|
||||
{
|
||||
Self::encrypt_at(
|
||||
from_user_id,
|
||||
recipient_user_id,
|
||||
message,
|
||||
unix_time_seconds()?,
|
||||
sender_identity,
|
||||
recipient_identity_key,
|
||||
rng,
|
||||
)
|
||||
}
|
||||
|
||||
fn encrypt_at<R>(
|
||||
from_user_id: i64,
|
||||
recipient_user_id: i64,
|
||||
message: proto::Message,
|
||||
created_at_unix_seconds: i64,
|
||||
sender_identity: &IdentityKeyPair,
|
||||
recipient_identity_key: &PublicKey,
|
||||
rng: &mut R,
|
||||
) -> Result<Vec<u8>>
|
||||
where
|
||||
R: Rng + CryptoRng + ?Sized,
|
||||
{
|
||||
if message.receipt_id.is_empty() {
|
||||
return Err(SealedSenderError::MissingReceiptId);
|
||||
}
|
||||
let payload = proto::MessageEnvelopePayload {
|
||||
magic: PAYLOAD_MAGIC.to_owned(),
|
||||
from_user_id,
|
||||
recipient_user_id,
|
||||
message: Some(message),
|
||||
created_at_unix_seconds,
|
||||
};
|
||||
let signed_payload = payload.encode_to_vec();
|
||||
let signature = sender_identity
|
||||
.private_key()
|
||||
.calculate_signature(&signed_payload, rng)
|
||||
.map_err(|_| SealedSenderError::SignatureGeneration)?;
|
||||
let envelope = proto::MessageEnvelope {
|
||||
signed_payload,
|
||||
signature: signature.into_vec(),
|
||||
}
|
||||
.encode_to_vec();
|
||||
|
||||
let ephemeral_key_pair = KeyPair::generate(rng);
|
||||
let ephemeral_public_key = ephemeral_key_pair.public_key.public_key_bytes().to_vec();
|
||||
let shared_secret = ephemeral_key_pair
|
||||
.private_key
|
||||
.calculate_agreement(recipient_identity_key)
|
||||
.map_err(|_| SealedSenderError::KeyAgreement)?;
|
||||
let key = derive_encryption_key(
|
||||
&shared_secret,
|
||||
&ephemeral_public_key,
|
||||
recipient_identity_key,
|
||||
)?;
|
||||
|
||||
let mut nonce = [0_u8; XCHACHA20_NONCE_SIZE];
|
||||
rng.fill_bytes(&mut nonce);
|
||||
let ciphertext = XChaCha20Poly1305::new((&key).into())
|
||||
.encrypt(
|
||||
XNonce::from_slice(&nonce),
|
||||
Payload {
|
||||
msg: &envelope,
|
||||
aad: ENCRYPTION_CONTEXT,
|
||||
},
|
||||
)
|
||||
.map_err(|_| SealedSenderError::EncryptionFailed)?;
|
||||
|
||||
Ok(proto::EncryptedMessageEnvelope {
|
||||
ephemeral_public_key,
|
||||
nonce: nonce.to_vec(),
|
||||
ciphertext,
|
||||
}
|
||||
.encode_to_vec())
|
||||
}
|
||||
|
||||
/// Decrypts an envelope with the local identity from `context` and verifies
|
||||
/// the signature using the sender identity stored by Signal.
|
||||
pub(crate) async fn decrypt(
|
||||
encrypted_envelope: &[u8],
|
||||
context: &Context,
|
||||
) -> Result<proto::MessageEnvelopePayload> {
|
||||
let (recipient_user_id, recipient_identity) = {
|
||||
let key_manager = context.get_key_manager().await.map_err(context_error)?;
|
||||
let recipient_user_id = key_manager
|
||||
.user_id
|
||||
.ok_or(SealedSenderError::MissingLocalUserId)?;
|
||||
let serialized_identity = &key_manager
|
||||
.signal_identity
|
||||
.as_ref()
|
||||
.ok_or(SealedSenderError::MissingLocalIdentity)?
|
||||
.identity_key_pair_structure;
|
||||
let recipient_identity = IdentityKeyPair::try_from(serialized_identity.as_slice())
|
||||
.map_err(|_| SealedSenderError::InvalidLocalIdentity)?;
|
||||
(recipient_user_id, recipient_identity)
|
||||
};
|
||||
|
||||
let (envelope, payload) =
|
||||
Self::decrypt_envelope(encrypted_envelope, recipient_user_id, &recipient_identity)?;
|
||||
let sender_identity = context
|
||||
.get_identity(payload.from_user_id)
|
||||
.await
|
||||
.map_err(context_error)?
|
||||
.ok_or(SealedSenderError::UnknownSender(payload.from_user_id))?;
|
||||
verify_sender_and_message(&envelope, &payload, sender_identity.public_key())?;
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
fn decrypt_envelope(
|
||||
encrypted_envelope: &[u8],
|
||||
recipient_user_id: i64,
|
||||
recipient_identity: &IdentityKeyPair,
|
||||
) -> Result<(proto::MessageEnvelope, proto::MessageEnvelopePayload)> {
|
||||
let encrypted = proto::EncryptedMessageEnvelope::decode(encrypted_envelope)?;
|
||||
if encrypted.ephemeral_public_key.len() != X25519_PUBLIC_KEY_SIZE {
|
||||
return Err(SealedSenderError::InvalidEphemeralPublicKey);
|
||||
}
|
||||
if encrypted.nonce.len() != XCHACHA20_NONCE_SIZE {
|
||||
return Err(SealedSenderError::InvalidNonce);
|
||||
}
|
||||
if encrypted.ciphertext.len() < POLY1305_TAG_SIZE {
|
||||
return Err(SealedSenderError::InvalidCiphertext);
|
||||
}
|
||||
|
||||
let ephemeral_public_key =
|
||||
PublicKey::from_djb_public_key_bytes(&encrypted.ephemeral_public_key)
|
||||
.map_err(|_| SealedSenderError::InvalidEphemeralPublicKey)?;
|
||||
let shared_secret = recipient_identity
|
||||
.private_key()
|
||||
.calculate_agreement(&ephemeral_public_key)
|
||||
.map_err(|_| SealedSenderError::KeyAgreement)?;
|
||||
let key = derive_encryption_key(
|
||||
&shared_secret,
|
||||
&encrypted.ephemeral_public_key,
|
||||
recipient_identity.identity_key().public_key(),
|
||||
)?;
|
||||
let plaintext = XChaCha20Poly1305::new((&key).into())
|
||||
.decrypt(
|
||||
XNonce::from_slice(&encrypted.nonce),
|
||||
Payload {
|
||||
msg: &encrypted.ciphertext,
|
||||
aad: ENCRYPTION_CONTEXT,
|
||||
},
|
||||
)
|
||||
.map_err(|_| SealedSenderError::AuthenticationFailed)?;
|
||||
|
||||
let envelope = proto::MessageEnvelope::decode(plaintext.as_slice())?;
|
||||
let payload = proto::MessageEnvelopePayload::decode(envelope.signed_payload.as_slice())?;
|
||||
if payload.magic != PAYLOAD_MAGIC {
|
||||
return Err(SealedSenderError::InvalidMagic);
|
||||
}
|
||||
if payload.recipient_user_id != recipient_user_id {
|
||||
return Err(SealedSenderError::WrongRecipient {
|
||||
expected: recipient_user_id,
|
||||
actual: payload.recipient_user_id,
|
||||
});
|
||||
}
|
||||
Ok((envelope, payload))
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_sender_and_message(
|
||||
envelope: &proto::MessageEnvelope,
|
||||
payload: &proto::MessageEnvelopePayload,
|
||||
sender_key: &PublicKey,
|
||||
) -> Result<()> {
|
||||
if !sender_key.verify_signature(&envelope.signed_payload, &envelope.signature) {
|
||||
return Err(SealedSenderError::InvalidSignature);
|
||||
}
|
||||
let message = payload
|
||||
.message
|
||||
.as_ref()
|
||||
.ok_or(SealedSenderError::MissingMessage)?;
|
||||
if message.receipt_id.is_empty() {
|
||||
return Err(SealedSenderError::MissingReceiptId);
|
||||
}
|
||||
let now = unix_time_seconds()?;
|
||||
if payload.created_at_unix_seconds < now.saturating_sub(ENVELOPE_VALIDITY_SECONDS) {
|
||||
return Err(SealedSenderError::ExpiredEnvelope);
|
||||
}
|
||||
if payload.created_at_unix_seconds > now.saturating_add(MAX_FUTURE_CLOCK_SKEW_SECONDS) {
|
||||
return Err(SealedSenderError::FutureEnvelope);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn context_error(error: impl std::fmt::Display) -> SealedSenderError {
|
||||
SealedSenderError::Context(error.to_string())
|
||||
}
|
||||
|
||||
fn unix_time_seconds() -> Result<i64> {
|
||||
let seconds = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|_| SealedSenderError::InvalidSystemTime)?
|
||||
.as_secs();
|
||||
i64::try_from(seconds).map_err(|_| SealedSenderError::InvalidSystemTime)
|
||||
}
|
||||
|
||||
fn derive_encryption_key(
|
||||
shared_secret: &[u8],
|
||||
ephemeral_public_key: &[u8],
|
||||
recipient_identity_key: &PublicKey,
|
||||
) -> Result<[u8; 32]> {
|
||||
let mut info = Vec::with_capacity(
|
||||
ENCRYPTION_CONTEXT.len()
|
||||
+ ephemeral_public_key.len()
|
||||
+ recipient_identity_key.serialize().len(),
|
||||
);
|
||||
info.extend_from_slice(ENCRYPTION_CONTEXT);
|
||||
info.extend_from_slice(ephemeral_public_key);
|
||||
info.extend_from_slice(&recipient_identity_key.serialize());
|
||||
|
||||
let mut key = [0_u8; 32];
|
||||
Hkdf::<Sha256>::new(None, shared_secret)
|
||||
.expand(&info, &mut key)
|
||||
.map_err(|_| SealedSenderError::KeyDerivation)?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::{rngs::StdRng, SeedableRng};
|
||||
|
||||
fn test_message() -> proto::Message {
|
||||
proto::Message {
|
||||
r#type: proto::message::Type::PlaintextContent as i32,
|
||||
receipt_id: "1f58417a-5cd4-4bb6-9f38-2f499d9cfa8e".to_owned(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt_with_identity(
|
||||
encrypted: &[u8],
|
||||
recipient_user_id: i64,
|
||||
recipient: &IdentityKeyPair,
|
||||
sender_key: &PublicKey,
|
||||
) -> Result<proto::MessageEnvelopePayload> {
|
||||
let (envelope, payload) =
|
||||
SealedSender::decrypt_envelope(encrypted, recipient_user_id, recipient)?;
|
||||
verify_sender_and_message(&envelope, &payload, sender_key)?;
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn encrypts_decrypts_and_authenticates_a_sealed_sender_message() {
|
||||
let mut rng = StdRng::seed_from_u64(7);
|
||||
let sender = IdentityKeyPair::generate(&mut rng);
|
||||
let recipient = IdentityKeyPair::generate(&mut rng);
|
||||
let message = test_message();
|
||||
let temporary_directory = tempfile::tempdir().unwrap();
|
||||
let context = Context::init_for_testing(
|
||||
temporary_directory.path().join("database"),
|
||||
temporary_directory.path().join("data"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let mut key_manager = context.get_key_manager().await.unwrap();
|
||||
key_manager.user_id = Some(42);
|
||||
key_manager.signal_identity = Some(crate::keys::SignalIdentityKey {
|
||||
identity_key_pair_structure: recipient.serialize().to_vec(),
|
||||
registration_id: 1,
|
||||
pre_key_store: Default::default(),
|
||||
});
|
||||
}
|
||||
let database = context.get_rust_database().await;
|
||||
sqlx::query(
|
||||
"INSERT INTO signal_identities (name, identity_key, timestamp) VALUES (?, ?, 0)",
|
||||
)
|
||||
.bind("41")
|
||||
.bind(sender.identity_key().serialize().as_ref())
|
||||
.execute(&database.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let encrypted = SealedSender::encrypt(
|
||||
41,
|
||||
42,
|
||||
message.clone(),
|
||||
&sender,
|
||||
recipient.identity_key().public_key(),
|
||||
&mut rng,
|
||||
)
|
||||
.unwrap();
|
||||
let decrypted = SealedSender::decrypt(&encrypted, &context).await.unwrap();
|
||||
|
||||
assert_eq!(decrypted.magic, PAYLOAD_MAGIC);
|
||||
assert_eq!(decrypted.from_user_id, 41);
|
||||
assert_eq!(decrypted.recipient_user_id, 42);
|
||||
assert_eq!(decrypted.message, Some(message));
|
||||
assert!(decrypted.created_at_unix_seconds > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_tampered_ciphertext() {
|
||||
let mut rng = StdRng::seed_from_u64(8);
|
||||
let sender = IdentityKeyPair::generate(&mut rng);
|
||||
let recipient = IdentityKeyPair::generate(&mut rng);
|
||||
let encrypted = SealedSender::encrypt(
|
||||
41,
|
||||
42,
|
||||
test_message(),
|
||||
&sender,
|
||||
recipient.identity_key().public_key(),
|
||||
&mut rng,
|
||||
)
|
||||
.unwrap();
|
||||
let mut wrapper = proto::EncryptedMessageEnvelope::decode(encrypted.as_slice()).unwrap();
|
||||
wrapper.ciphertext[0] ^= 1;
|
||||
|
||||
let error = decrypt_with_identity(
|
||||
&wrapper.encode_to_vec(),
|
||||
42,
|
||||
&recipient,
|
||||
sender.identity_key().public_key(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(error, SealedSenderError::AuthenticationFailed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_a_signature_from_another_identity() {
|
||||
let mut rng = StdRng::seed_from_u64(9);
|
||||
let sender = IdentityKeyPair::generate(&mut rng);
|
||||
let impostor = IdentityKeyPair::generate(&mut rng);
|
||||
let recipient = IdentityKeyPair::generate(&mut rng);
|
||||
let encrypted = SealedSender::encrypt(
|
||||
41,
|
||||
42,
|
||||
test_message(),
|
||||
&sender,
|
||||
recipient.identity_key().public_key(),
|
||||
&mut rng,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let error = decrypt_with_identity(
|
||||
&encrypted,
|
||||
42,
|
||||
&recipient,
|
||||
impostor.identity_key().public_key(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(error, SealedSenderError::InvalidSignature));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_an_envelope_for_another_recipient_before_resolving_sender() {
|
||||
let mut rng = StdRng::seed_from_u64(10);
|
||||
let sender = IdentityKeyPair::generate(&mut rng);
|
||||
let recipient = IdentityKeyPair::generate(&mut rng);
|
||||
let encrypted = SealedSender::encrypt(
|
||||
41,
|
||||
42,
|
||||
test_message(),
|
||||
&sender,
|
||||
recipient.identity_key().public_key(),
|
||||
&mut rng,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let error = SealedSender::decrypt_envelope(&encrypted, 43, &recipient).unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
SealedSenderError::WrongRecipient {
|
||||
expected: 43,
|
||||
actual: 42
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_an_expired_envelope() {
|
||||
let mut rng = StdRng::seed_from_u64(11);
|
||||
let sender = IdentityKeyPair::generate(&mut rng);
|
||||
let recipient = IdentityKeyPair::generate(&mut rng);
|
||||
let expired_at = unix_time_seconds().unwrap() - ENVELOPE_VALIDITY_SECONDS - 1;
|
||||
let encrypted = SealedSender::encrypt_at(
|
||||
41,
|
||||
42,
|
||||
test_message(),
|
||||
expired_at,
|
||||
&sender,
|
||||
recipient.identity_key().public_key(),
|
||||
&mut rng,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let error = decrypt_with_identity(
|
||||
&encrypted,
|
||||
42,
|
||||
&recipient,
|
||||
sender.identity_key().public_key(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(error, SealedSenderError::ExpiredEnvelope));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_an_envelope_too_far_in_the_future() {
|
||||
let mut rng = StdRng::seed_from_u64(12);
|
||||
let sender = IdentityKeyPair::generate(&mut rng);
|
||||
let recipient = IdentityKeyPair::generate(&mut rng);
|
||||
let future_at = unix_time_seconds().unwrap() + MAX_FUTURE_CLOCK_SKEW_SECONDS + 1;
|
||||
let encrypted = SealedSender::encrypt_at(
|
||||
41,
|
||||
42,
|
||||
test_message(),
|
||||
future_at,
|
||||
&sender,
|
||||
recipient.identity_key().public_key(),
|
||||
&mut rng,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let error = decrypt_with_identity(
|
||||
&encrypted,
|
||||
42,
|
||||
&recipient,
|
||||
sender.identity_key().public_key(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(error, SealedSenderError::FutureEnvelope));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue