diff --git a/.lock b/.lock new file mode 100644 index 00000000..e69de29b diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/crates.js b/crates.js new file mode 100644 index 00000000..a72b8967 --- /dev/null +++ b/crates.js @@ -0,0 +1 @@ +window.ALL_CRATES = ["vodozemac"]; \ No newline at end of file diff --git a/help.html b/help.html new file mode 100644 index 00000000..478c8ecf --- /dev/null +++ b/help.html @@ -0,0 +1 @@ +
Ed25519SecretKey
from a base64 encoded …","Instantiate a Ed25519PublicKey public key from an unpadded …","Try to create a Ed25519Signature
from an unpadded base64 …","Create a Curve25519PublicKey
from a byte array.","Create a Curve25519SecretKey
from the given slice of bytes.","Try to create a Curve25519PublicKey
from a slice of bytes.","Try to create a Ed25519SecretKey
from a slice of bytes.","Try to create a Ed25519PublicKey
from a slice of bytes.","Try to create a Ed25519Signature
from a slice of bytes.","","","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","An implementation of the Megolm ratchet.","Generate a new, random, Curve25519SecretKey.","Create a new, random, Ed25519Keypair
.","Create a new random Ed25519SecretKey
.","An implementation of the Olm double ratchet.","","","","","","","","","Get the public Ed25519 key of this keypair.","Get the public key that matches this Ed25519SecretKey
.","User-friendly key verification using short authentication …","","","","","","","Sign the given message with our secret key.","Sign the given slice of bytes with this Ed25519SecretKey
.","","","","","","Serialize a Curve25519 public key to an unpadded base64 …","Convert the secret key to a base64 encoded string.","Serialize a Ed25519PublicKey public key to an unpadded …","Serialize an Ed25519Signature
to an unpadded base64 …","","Convert this shared secret to a byte array.","Convert the Curve25519SecretKey
to a byte array.","Convert this public key to a byte array.","Get the byte representation of the secret key.","Convert the Ed25519Signature
to a byte array.","","","","","","","","","","","","","","","","","","","Convert the public key to a vector of bytes.","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Verify that the provided signature for a given message has …","","","","","","","","","","","","","","","","Ensure in constant-time that this shared secret did not …","","","","","The encoded session key wasn’t valid base64.","The first session has a better initial message index than …","","Error type for Megolm-based decryption failures.","The sessions are the same.","The exported session key.","A Megolm group session represents a single sending …","A format suitable for serialization which implements …","","A format suitable for serialization which implements …","The message authentication code of the message was invalid.","The length of the message authentication code of the …","The ciphertext of the message isn’t padded correctly.","An encrypted Megolm message.","The encoded session key contains an invalid public key.","The encoded session key didn’t contain enough data to be …","A struct to configure how Megolm sessions should work …","The session key, can be used to create a …","Error type describing failure modes for the SessionKey
and …","The result of a comparison between two InboundGroupSession
…","The signature on the message was invalid.","The signature on the session key was invalid.","The sessions are not the same, they can’t be compared.","The session is missing the correct message key to decrypt …","The encoded session key had a unsupported version.","The first session has a worse initial message index than …","Permanently advance the session to the given index.","","","","","","","","","","","","","","","","","","","","","","","","","The actual ciphertext of the message.","","","","","","","","","Compare the InboundGroupSession
with the given other …","Check if two InboundGroupSession
s are the same.","","","","","","","","","","","","","Encrypt the plaintext
with the group session.","Serialize and encrypt the pickle using the given key.","Serialize and encrypt the pickle using the given key.","","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","","Returns the argument unchanged.","","","Returns the argument unchanged.","","","Returns the argument unchanged.","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","","","","Returns the argument unchanged.","","Deserialize the ExportedSessionKey
from base64 encoded …","Deserialize the SessionKey
from base64 encoded string.","Try to decode the given string as a MegolmMessage
.","Deserialize the ExportedSessionKey
from a byte slice.","Deserialize the SessionKey
from a byte slice.","Try to decode the given byte slice as a MegolmMessage
.","Obtain a pickle from a ciphertext by decrypting and …","Obtain a pickle from a ciphertext by decrypting and …","","","Restore a GroupSession
from a previously saved …","Restore an InboundGroupSession
from a previously saved …","","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Get the megolm message’s mac.","Merge the session with the given other session, picking …","Return the current message index.","The index of the message that was used when the message …","","Construct a new group session, with a random ratchet state …","","Convert the group session into a struct which implements …","Convert the inbound group session into a struct which …","","","","","","","","","","","Returns the globally unique session ID, in base64-encoded …","","Export the group session into a session key.","Get a reference to the megolm message’s signature.","","","Serialize the ExportedSessionKey
to a base64 encoded …","Serialize the SessionKey
to a base64 encoded string.","Encode the MegolmMessage
as a string.","Serialize the ExportedSessionKey
to a byte vector.","Serialize the SessionKey
to a byte vector.","Encode the MegolmMessage
as an array of bytes.","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Get the numeric version of this SessionConfig
.","Create a SessionConfig
for the Megolm version 1. This …","Create a SessionConfig
for the Megolm version 2. This …","","","","","","","","","","","","","","","An Olm account manages all cryptographic keys used on a …","A format suitable for serialization which implements …","The pre-key message that was used to establish the Session …","Error type for Olm-based decryption failures.","Struct holding the two public identity keys of an Account
.","Return type for the creation of inbound Session
objects.","The message authentication code of the message was invalid.","The length of the message authentication code of the …","The ciphertext of the message isn’t padded correctly.","An encrypted Olm message.","An enum over the two supported message types.","The pre-key message contains a curve25519 identity key …","The session is missing the correct message key to decrypt …","The pre-key message contained an unknown one-time key. …","A normal message, contains only the ciphertext and …","The normal message type.","Enum over the different Olm message types.","The result type for the one-time key generation operation.","A pre-key message, contains metadata to establish a Session
…","The pre-key message type.","An encrypted Olm pre-key message.","","An Olm session represents one end of an encrypted …","A struct to configure how Olm sessions should work under …","Error describing failure modes when creating a Olm Session …","The set of keys that were used to establish the Olm …","A format suitable for serialization which implements …","Too many messages have been skipped to attempt decrypting …","","The base key, a single use key that was created just in …","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","The index of the chain that was used when the message was …","The actual ciphertext of the message.","","","","","","","","","","","","","","","","","Create a Session
from the given pre-key message and …","Create a Session
with the given identity key and one-time …","The public part of the one-time keys that were newly …","The curve25519 key, used for to establish shared secrets.","Get a reference to the account’s public Curve25519 key","","Try to decrypt an Olm message, which will either return …","","","","","","","","","","","The ed25519 key, used for signing.","Get a reference to the account’s public Ed25519 key","Encrypt the plaintext
and construct an OlmMessage
.","Serialize and encrypt the pickle using the given key.","Serialize and encrypt the pickle using the given key.","","","","","","","","","","","","","","","","","Get the currently unpublished fallback key.","","","","","","","","","","","","","","","The Account
stores at most two private parts of the …","Returns the argument unchanged.","Returns the argument unchanged.","","Returns the argument unchanged.","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","","Returns the argument unchanged.","","Returns the argument unchanged.","Returns the argument unchanged.","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Try to decode the given string as a Olm Message
.","Try to decode the given string as a Olm PreKeyMessage
.","Try to decode the given byte slice as a Olm Message
.","Try to decode the given byte slice as a Olm Message
.","Obtain a pickle from a ciphertext by decrypting and …","Obtain a pickle from a ciphertext by decrypting and …","Create an Account
object by unpickling an account pickle …","Create a Session
object by unpickling a session pickle in …","Create a OlmMessage
from a message type and a ciphertext.","Restore an Account
from a previously saved AccountPickle
.","Restore a Session
from a previously saved SessionPickle
.","Generate a single new fallback key.","Generates the supplied number of one time keys. Returns …","Have we ever received and decrypted a message from the …","The long term identity key of the sender of the message. …","","Get the IdentityKeys of this Account","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Has the MAC been truncated in this Olm message.","Mark all currently unpublished one-time and fallback keys …","Get the maximum number of one-time keys the client should …","The actual message that contains the ciphertext.","Get the message as a byte array.","Get the type of the message.","Create a new Account with new random identity keys.","The single-use key that was uploaded to a public key …","","Get the currently unpublished one-time keys.","Convert the account into a struct which implements …","Convert the session into a struct which implements …","The plaintext of the pre-key message.","","","The public part of the ratchet key, that was used when the …","The public part of the one-time keys that had to be …","","","","","","","","","The Session
that was created from a pre-key message.","","Returns the globally unique session ID, in base64-encoded …","Returns the globally unique session ID, in base64-encoded …","Returns the globally unique session ID which these …","Get the keys associated with this session.","The collection of all keys required for establishing an …","Sign the given message using our Ed25519 fingerprint key.","","","","Encode the Message
as a string.","Encode the PreKeyMessage
as a string.","Encode the Message
as an array of bytes.","Encode the PreKeyMessage
as an array of bytes.","","","","","","","","","Convert the OlmMessage
into a message type, and base64 …","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","The version of the Olm message.","Get the numeric version of this SessionConfig
.","Create a SessionConfig
for the Olm version 1. This version …","Create a SessionConfig
for the Olm version 2. This version …","","","","","","","","","","","","","","","","","A struct representing a short auth string verification …","Error type for the case when we try to generate too many …","The output type for the SAS MAC calculation.","The MAC failed to be validated.","A struct representing a short auth string verification …","Bytes generated from an shared secret that can be used as …","Error type describing failures that can happen during the …","Get the byte slice of the MAC.","Get the raw bytes of the short auth string that can be …","","","","","","","","","","","","","Generate SasBytes
using HKDF with the shared secret as the …","Generate the given number of bytes using HKDF with the …","Calculate a MAC for the given input using the info string …","Calculate a MAC for the given input using the info string …","","","","","Get the three decimal numbers that can be presented to …","","Establishes a SAS secret by performing a DH handshake with …","Establishes a SAS secret by performing a DH handshake with …","Get the index of 7 emojis that can be presented to users …","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","","Returns the argument unchanged.","Create a new Mac
object from a base64 encoded string.","Create a new Mac
object from a byte slice.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Create a new random verification object","Get the public key that was created by us, that was used …","","","Get the public key that can be used to establish a shared …","","Get the public key that was created by the other party, …","Convert the MAC to a base64 encoded string.","","","","","","","","","","","","","","","","","","","","","","","Verify a MAC that was previously created using the …","","","","","",""],"i":[25,27,28,29,0,26,0,0,28,0,27,28,0,0,0,0,7,29,26,7,7,29,7,28,29,0,0,4,19,5,13,0,29,29,28,29,26,0,0,29,28,27,0,25,26,29,0,0,28,1,4,5,1,1,10,7,11,4,25,12,19,5,13,14,26,27,28,29,1,10,7,11,4,25,12,19,5,13,14,26,27,28,29,7,10,7,11,4,12,5,13,14,10,7,11,4,12,5,13,14,14,4,11,12,19,11,4,12,19,5,14,11,10,7,4,5,13,14,10,7,4,5,13,14,10,10,7,7,4,4,25,25,5,5,13,13,14,26,26,27,27,28,28,29,29,1,10,7,11,4,4,4,4,4,25,25,25,12,19,5,13,14,26,26,26,27,27,27,28,28,28,28,29,29,29,29,29,4,19,5,13,4,11,4,19,5,13,4,14,1,10,7,11,4,25,12,19,5,13,14,26,27,28,29,0,11,12,19,0,14,10,7,25,26,27,28,29,12,19,0,11,4,12,19,5,14,12,19,25,26,27,28,29,4,19,5,13,14,1,11,4,19,13,10,7,11,4,12,5,13,14,10,7,4,25,5,13,26,27,28,29,4,1,10,7,11,4,25,12,19,5,13,14,26,27,28,29,1,10,7,11,4,25,12,19,5,13,14,26,27,28,29,1,10,7,11,4,25,12,19,5,13,14,26,27,28,29,5,1,10,7,11,4,25,12,19,5,13,14,26,27,28,29,1,1,84,84,84,55,45,0,0,45,0,0,0,0,0,48,48,48,0,55,55,0,0,0,0,48,55,45,48,55,45,42,49,50,51,52,45,48,42,46,53,44,47,55,49,50,51,52,45,48,42,46,53,44,47,55,44,45,46,44,47,45,46,44,47,42,42,42,49,47,50,51,52,42,53,44,47,50,51,49,52,53,45,46,44,47,45,46,44,47,42,42,42,45,48,48,46,44,47,55,55,49,49,50,51,52,45,48,48,48,48,42,42,42,46,53,53,44,47,55,55,55,55,55,50,51,44,50,51,44,52,53,49,42,49,42,42,49,50,51,52,45,48,42,46,53,44,47,55,44,42,49,44,46,49,42,49,42,46,48,55,50,51,52,53,44,47,49,49,42,49,44,48,55,50,51,44,50,51,44,45,46,44,47,48,55,49,50,50,50,51,51,51,52,45,48,42,46,53,44,44,44,44,47,55,49,50,51,52,45,48,42,46,53,44,47,55,49,50,51,52,45,48,42,46,53,44,47,55,47,47,47,49,50,51,52,45,48,42,46,53,44,47,55,50,51,0,0,70,0,0,0,72,72,72,0,0,70,72,70,64,65,0,0,64,65,0,0,0,0,0,0,0,72,59,60,67,77,68,71,70,63,69,73,61,60,64,65,59,72,74,66,67,77,68,71,70,63,69,73,61,60,64,65,59,72,74,66,67,61,61,63,61,60,64,65,59,66,67,63,61,60,64,65,59,66,67,68,68,77,63,68,67,71,68,66,63,73,61,60,64,74,66,67,63,68,71,73,74,63,61,60,64,65,59,66,67,63,61,60,64,65,59,66,67,68,71,70,70,63,69,61,60,64,65,59,72,72,66,67,68,77,68,68,71,71,70,70,63,69,73,61,60,64,64,64,65,59,59,72,72,72,74,66,67,61,60,61,60,73,74,68,71,64,68,71,68,68,71,60,67,68,77,68,71,70,63,69,73,61,60,64,65,59,72,74,66,67,61,68,68,60,64,64,68,60,67,68,68,71,69,70,72,61,77,63,73,61,60,64,74,66,67,69,71,71,60,67,71,60,68,70,72,68,61,60,61,60,63,61,60,64,65,59,66,67,64,70,72,77,68,71,70,63,69,73,61,61,61,61,60,60,60,60,64,65,65,59,72,74,66,67,77,68,71,70,63,69,73,61,60,64,65,59,72,74,66,67,77,68,71,70,63,69,73,61,60,64,65,59,72,74,66,67,61,66,66,66,77,68,71,70,63,69,73,61,60,64,65,59,72,74,66,67,0,0,0,83,0,0,0,78,79,78,82,80,81,83,79,78,82,80,81,83,79,80,80,80,80,81,79,81,79,79,82,82,82,79,79,79,80,81,81,83,83,79,78,82,80,81,83,83,79,78,78,78,82,80,81,83,79,82,80,81,83,82,83,80,78,81,79,81,83,78,82,80,81,83,79,78,82,80,81,83,79,78,82,80,81,83,79,80,78,82,80,81,83,79],"f":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[1,[[3,[2]]]],[4,[[3,[2]]]],[5,[[3,[2]]]],[1,[[6,[2]]]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[7,[[9,[8]]]],[10,10],[7,7],[11,11],[4,4],[12,12],[5,5],[13,13],[14,14],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[14,14],15],[16,[[18,[4,17]]]],[[],11],[[],12],[[],19],[20,[[18,[11]]]],[20,[[18,[4]]]],[20,[[18,[12]]]],[20,[[18,[19]]]],[20,[[18,[5]]]],[20,[[18,[14]]]],[[11,4],1],[[10,10],21],[[7,7],21],[[4,4],21],[[5,5],21],[[13,13],21],[[14,14],21],[[],21],[[],21],[[],21],[[],21],[[],21],[[],21],[[10,22],[[18,[23]]]],[[10,22],[[18,[23]]]],[[7,22],[[18,[23]]]],[[7,22],[[18,[23]]]],[[4,22],24],[[4,22],24],[[25,22],24],[[25,22],24],[[5,22],24],[[5,22],24],[[13,22],24],[[13,22],24],[[14,22],24],[[26,22],24],[[26,22],24],[[27,22],24],[[27,22],24],[[28,22],24],[[28,22],24],[[29,22],24],[[29,22],24],[[]],[[]],[[]],[[]],[[]],[11,4],[30,4],[31,4],[[[3,[2]]],4],[7,25],[[]],[32,25],[[]],[[]],[[]],[[]],[[]],[[]],[25,26],[7,26],[7,27],[33,27],[[]],[[]],[17,28],[26,28],[7,28],[10,29],[25,29],[26,29],[7,29],[[]],[34,[[18,[4,26]]]],[34,[[18,[19,26]]]],[34,[[18,[5,26]]]],[34,[[18,[13,25]]]],[[[3,[2]]],4],[[[3,[2]]],11],[[[6,[2]]],[[18,[4,26]]]],[[[3,[2]]],19],[[[3,[2]]],[[18,[5,26]]]],[[[6,[2]]],[[18,[13,25]]]],[[4,35]],[[14,35]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],0,[[],11],[[],12],[[],19],0,[[14,14],[[9,[15]]]],[36],[36],[36],[36],[36],[36],[36],[12,5],[19,5],0,[[11,37],18],[[4,37],18],[[12,37],18],[[19,37],18],[[5,37],18],[[14,37],18],[[12,[6,[2]]],13],[[19,[6,[2]]],13],[25,[[9,[8]]]],[26,[[9,[8]]]],[27,[[9,[8]]]],[28,[[9,[8]]]],[29,[[9,[8]]]],[4,38],[19,38],[5,38],[13,38],[14,38],[1,[[3,[2]]]],[11,[[3,[2]]]],[4,[[3,[2]]]],[19,[[39,[[3,[2]]]]]],[13,[[3,[2]]]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[],38],[[],38],[[],38],[[],38],[[],38],[[],38],[[],38],[[],38],[[],38],[[],38],[4,[[40,[2]]]],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[5,[6,[2]],13],[[18,[25]]]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[1,21],[1],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[[42,43],21],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[44,[[6,[2]]]],[45,45],[46,46],[44,44],[47,47],[[]],[[]],[[]],[[]],[[42,42],45],[[42,42],21],[[42,44],[[18,[46,48]]]],[[],49],[[],47],[20,[[18,[50]]]],[20,[[18,[51]]]],[20,[[18,[52]]]],[20,[[18,[42]]]],[20,[[18,[53]]]],[20,[[18,[44]]]],[20,[[18,[47]]]],[50],[51],[[49,[54,[[6,[2]]]]],44],[[52,[3,[2]]],38],[[53,[3,[2]]],38],[[45,45],21],[[46,46],21],[[44,44],21],[[47,47],21],[[],21],[[],21],[[],21],[[],21],[[42,43],[[9,[50]]]],[42,50],[42,43],[[45,22],24],[[48,22],24],[[48,22],24],[[46,22],24],[[44,22],24],[[47,22],24],[[55,22],24],[[55,22],24],[52,49],[[]],[[]],[[]],[[]],[[]],[56,48],[[]],[57,48],[25,48],[[]],[53,42],[49,42],[[]],[42,53],[[]],[[]],[[]],[26,55],[58,55],[25,55],[[]],[7,55],[34,[[18,[50,55]]]],[34,[[18,[51,55]]]],[34,[[18,[44,29]]]],[[[6,[2]]],[[18,[50,55]]]],[[[6,[2]]],[[18,[51,55]]]],[[[6,[2]]],[[18,[44,29]]]],[[34,[3,[2]]],[[18,[52,27]]]],[[34,[3,[2]]],[[18,[53,27]]]],[[34,[6,[2]]],[[18,[49,28]]]],[[34,[6,[2]]],[[18,[42,28]]]],[52,49],[53,42],[[50,47],42],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[44,[[6,[2]]]],[[42,42],[[9,[42]]]],[49,43],[44,43],0,[47,49],[[51,47],42],[49,52],[42,53],0,[36],[36],[[50,37],18],[[51,37],18],[[52,37],18],[[53,37],18],[[44,37],18],[[47,37],18],[49,47],[49,38],[42,38],[49,51],[44,13],[48,[[9,[8]]]],[55,[[9,[8]]]],[50,38],[51,38],[44,38],[50,[[40,[2]]]],[51,[[40,[2]]]],[44,[[40,[2]]]],[[]],[[]],[[]],[[]],[[],38],[[],38],[[],18],[[[6,[2]]],[[18,[50]]]],[[],18],[34,[[18,[50]]]],[[[6,[2]]],[[18,[51]]]],[34,[[18,[51]]]],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[[40,[2]]],[[18,[44]]]],[34,[[18,[44]]]],[[],18],[[[6,[2]]],[[18,[44]]]],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[47,2],[[],47],[[],47],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[50],[51],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[59,4],[60,4],0,[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[61,62],[61,[[6,[2]]]],[63,63],[61,61],[60,60],[64,64],[65,65],[59,59],[66,66],[67,67],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[68,4,60],[[18,[69,70]]]],[[68,66,4,4],71],0,0,[68,4],[16,[[18,[67,17]]]],[[71,64],[[18,[[40,[2]],72]]]],[[],68],[[],66],[20,[[18,[63]]]],[20,[[18,[73]]]],[20,[[18,[61]]]],[20,[[18,[60]]]],[20,[[18,[64]]]],[20,[[18,[74]]]],[20,[[18,[66]]]],[20,[[18,[67]]]],0,[68,5],[[71,[54,[[6,[2]]]]],64],[[73,[3,[2]]],38],[[74,[3,[2]]],38],[[63,63],21],[[61,61],21],[[60,60],21],[[64,64],21],[[65,65],21],[[59,59],21],[[66,66],21],[[67,67],21],[[],21],[[],21],[[],21],[[],21],[[],21],[[],21],[[],21],[[],21],[68,[[75,[14,4]]]],[[71,22],24],[[70,22],24],[[70,22],24],[[63,22],24],[[69,22],24],[[61,22],24],[[60,22],24],[[64,22],24],[[65,22],24],[[59,22],24],[[72,22],24],[[72,22],24],[[66,22],24],[[67,22],24],[68,21],[[]],[[]],[73,68],[[]],[74,71],[72,70],[[]],[[]],[[]],[[]],[[]],[[]],[60,64],[[]],[61,64],[[]],[[]],[[[3,[2]]],59],[57,72],[56,72],[[]],[[]],[[]],[[]],[34,[[18,[61,29]]]],[34,[[18,[60,29]]]],[[[6,[2]]],[[18,[61,29]]]],[[[6,[2]]],[[18,[60,29]]]],[[34,[3,[2]]],[[18,[73,27]]]],[[34,[3,[2]]],[[18,[74,27]]]],[[34,[6,[2]]],[[18,[68,28]]]],[[34,[6,[2]]],[[18,[71,28]]]],[[76,34],[[18,[64,29]]]],[73,68],[74,71],[68,[[9,[4]]]],[[68,76],77],[71,21],[60,4],0,[68,63],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[61,21],[68],[68,76],[60,61],[64,[[6,[2]]]],[64,65],[[],68],[60,4],0,[68,[[75,[14,4]]]],[68,73],[71,74],0,[36],[36],[61,4],0,[[63,37],18],[[73,37],18],[[61,37],18],[[60,37],18],[[64,37],18],[[74,37],18],[[66,37],18],[[67,37],18],0,[71,66],[71,38],[60,38],[67,38],[71,67],[60,67],[[68,34],13],[70,[[9,[8]]]],[72,[[9,[8]]]],[68,76],[61,38],[60,38],[61,[[40,[2]]]],[60,[[40,[2]]]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[64],[[],38],[[],38],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[[40,[2]]],[[18,[61]]]],[34,[[18,[61]]]],[[[6,[2]]],[[18,[61]]]],[[],18],[34,[[18,[60]]]],[[[6,[2]]],[[18,[60]]]],[[[40,[2]]],[[18,[60]]]],[[],18],[[],18],[76,[[18,[65]]]],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[61,2],[66,2],[[],66],[[],66],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],0,0,0,0,0,0,0,[78,[[6,[2]]]],[79,[[3,[2]]]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[80,34],79],[[80,34,76],[[18,[[40,[2]],81]]]],[[80,34,34],78],[[80,34,34],38],[81,81],[79,79],[[]],[[]],[79],[[],82],[[82,4],[[18,[80,26]]]],[[82,34],[[18,[80,26]]]],[79,[[3,[2]]]],[[79,79],21],[[],21],[[80,22],24],[[81,22],24],[[81,22],24],[[83,22],24],[[83,22],24],[[79,22],24],[[]],[[]],[[]],[[]],[[]],[56,83],[[]],[34,[[18,[78,7]]]],[[[6,[2]]],78],[[]],[[]],[[]],[[]],[[]],[[]],[[],82],[80,4],[36],[36],[82,4],[83,[[9,[8]]]],[80,4],[78,38],[[]],[[]],[[],38],[[],38],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],18],[[],41],[[],41],[[],41],[[],41],[[],41],[[],41],[[80,34,34,78],[[18,[83]]]],[[]],[[]],[[]],[[]],[[]],[[]]],"c":[],"p":[[3,"SharedSecret"],[15,"u8"],[15,"array"],[3,"Curve25519PublicKey"],[3,"Ed25519PublicKey"],[15,"slice"],[4,"Base64DecodeError"],[8,"Error"],[4,"Option"],[3,"ProtoBufDecodeError"],[3,"Curve25519SecretKey"],[3,"Ed25519Keypair"],[3,"Ed25519Signature"],[3,"KeyId"],[4,"Ordering"],[8,"Read"],[4,"DecodeError"],[4,"Result"],[3,"Ed25519SecretKey"],[8,"Deserializer"],[15,"bool"],[3,"Formatter"],[3,"Error"],[6,"Result"],[4,"SignatureError"],[4,"KeyError"],[4,"PickleError"],[4,"LibolmPickleError"],[4,"DecodeError"],[3,"EphemeralSecret"],[3,"ReusableSecret"],[6,"SignatureError"],[3,"Error"],[15,"str"],[8,"Hasher"],[3,"Demand"],[8,"Serializer"],[3,"String"],[3,"Box"],[3,"Vec"],[3,"TypeId"],[3,"InboundGroupSession"],[15,"u32"],[3,"MegolmMessage"],[4,"SessionOrdering"],[3,"DecryptedMessage"],[3,"SessionConfig"],[4,"DecryptionError"],[3,"GroupSession"],[3,"ExportedSessionKey"],[3,"SessionKey"],[3,"GroupSessionPickle"],[3,"InboundGroupSessionPickle"],[8,"AsRef"],[4,"SessionKeyDecodeError"],[3,"MacError"],[3,"UnpadError"],[3,"Error"],[3,"RatchetPublicKey"],[3,"PreKeyMessage"],[3,"Message"],[15,"u64"],[3,"IdentityKeys"],[4,"OlmMessage"],[4,"MessageType"],[3,"SessionConfig"],[3,"SessionKeys"],[3,"Account"],[3,"InboundCreationResult"],[4,"SessionCreationError"],[3,"Session"],[4,"DecryptionError"],[3,"AccountPickle"],[3,"SessionPickle"],[3,"HashMap"],[15,"usize"],[3,"OneTimeKeyGenerationResult"],[3,"Mac"],[3,"SasBytes"],[3,"EstablishedSas"],[3,"InvalidCount"],[3,"Sas"],[4,"SasError"],[13,"InvalidKeyLength"]]}\
+}');
+if (typeof window !== 'undefined' && window.initSearch) {window.initSearch(searchIndex)};
+if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex};
diff --git a/settings.html b/settings.html
new file mode 100644
index 00000000..92c26369
--- /dev/null
+++ b/settings.html
@@ -0,0 +1 @@
+1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +
// Copyright 2021 The Matrix.org Foundation C.I.C.
+// Copyright 2021 Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use aes::{
+ cipher::{generic_array::GenericArray, IvSizeUser, KeySizeUser},
+ Aes256,
+};
+use hkdf::Hkdf;
+use sha2::Sha256;
+use zeroize::Zeroize;
+
+use super::Aes256CbcEnc;
+
+type Aes256Key = GenericArray<u8, <Aes256 as KeySizeUser>::KeySize>;
+type Aes256Iv = GenericArray<u8, <Aes256CbcEnc as IvSizeUser>::IvSize>;
+type HmacSha256Key = [u8; 32];
+
+#[derive(Zeroize)]
+#[zeroize(drop)]
+struct ExpandedKeys(Box<[u8; 80]>);
+
+impl ExpandedKeys {
+ const OLM_HKDF_INFO: &'static [u8] = b"OLM_KEYS";
+ const MEGOLM_HKDF_INFO: &'static [u8] = b"MEGOLM_KEYS";
+
+ fn new(message_key: &[u8; 32]) -> Self {
+ Self::new_helper(message_key, Self::OLM_HKDF_INFO)
+ }
+
+ fn new_megolm(message_key: &[u8; 128]) -> Self {
+ Self::new_helper(message_key, Self::MEGOLM_HKDF_INFO)
+ }
+
+ #[cfg(feature = "libolm-compat")]
+ fn new_pickle(pickle_key: &[u8]) -> Self {
+ Self::new_helper(pickle_key, b"Pickle")
+ }
+
+ fn new_helper(message_key: &[u8], info: &[u8]) -> Self {
+ let mut expanded_keys = [0u8; 80];
+
+ let hkdf: Hkdf<Sha256> = Hkdf::new(Some(&[0]), message_key);
+
+ hkdf.expand(info, &mut expanded_keys).expect("Can't expand message key");
+
+ Self(Box::new(expanded_keys))
+ }
+}
+
+#[derive(Zeroize)]
+#[zeroize(drop)]
+pub(super) struct CipherKeys {
+ aes_key: Box<[u8; 32]>,
+ aes_iv: Box<[u8; 16]>,
+ mac_key: Box<[u8; 32]>,
+}
+
+impl CipherKeys {
+ pub fn new(message_key: &[u8; 32]) -> Self {
+ let expanded_keys = ExpandedKeys::new(message_key);
+
+ Self::from_expanded_keys(expanded_keys)
+ }
+
+ pub fn new_megolm(message_key: &[u8; 128]) -> Self {
+ let expanded_keys = ExpandedKeys::new_megolm(message_key);
+
+ Self::from_expanded_keys(expanded_keys)
+ }
+
+ #[cfg(feature = "libolm-compat")]
+ pub fn new_pickle(pickle_key: &[u8]) -> Self {
+ let expanded_keys = ExpandedKeys::new_pickle(pickle_key);
+
+ Self::from_expanded_keys(expanded_keys)
+ }
+
+ fn from_expanded_keys(expanded_keys: ExpandedKeys) -> Self {
+ let mut aes_key = Box::new([0u8; 32]);
+ let mut mac_key = Box::new([0u8; 32]);
+ let mut aes_iv = Box::new([0u8; 16]);
+
+ aes_key.copy_from_slice(&expanded_keys.0[0..32]);
+ mac_key.copy_from_slice(&expanded_keys.0[32..64]);
+ aes_iv.copy_from_slice(&expanded_keys.0[64..80]);
+
+ Self { aes_key, aes_iv, mac_key }
+ }
+
+ pub fn aes_key(&self) -> &Aes256Key {
+ Aes256Key::from_slice(self.aes_key.as_slice())
+ }
+
+ pub fn mac_key(&self) -> &HmacSha256Key {
+ &self.mac_key
+ }
+
+ pub fn iv(&self) -> &Aes256Iv {
+ Aes256Iv::from_slice(self.aes_iv.as_slice())
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +
// Copyright 2021 The Matrix.org Foundation C.I.C.
+// Copyright 2021 Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+mod key;
+
+use aes::{
+ cipher::{
+ block_padding::{Pkcs7, UnpadError},
+ BlockDecryptMut, BlockEncryptMut, KeyIvInit,
+ },
+ Aes256,
+};
+use hmac::{digest::MacError, Hmac, Mac as MacT};
+use key::CipherKeys;
+use sha2::Sha256;
+use thiserror::Error;
+
+type Aes256CbcEnc = cbc::Encryptor<Aes256>;
+type Aes256CbcDec = cbc::Decryptor<Aes256>;
+type HmacSha256 = Hmac<Sha256>;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Mac(pub(crate) [u8; Self::LENGTH]);
+
+impl Mac {
+ pub const LENGTH: usize = 32;
+ pub const TRUNCATED_LEN: usize = 8;
+
+ pub fn truncate(&self) -> [u8; Self::TRUNCATED_LEN] {
+ let mut truncated = [0u8; Self::TRUNCATED_LEN];
+ truncated.copy_from_slice(&self.0[0..Self::TRUNCATED_LEN]);
+
+ truncated
+ }
+
+ pub fn as_bytes(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) enum MessageMac {
+ Truncated([u8; Mac::TRUNCATED_LEN]),
+ Full(Mac),
+}
+
+impl MessageMac {
+ pub fn as_bytes(&self) -> &[u8] {
+ match self {
+ MessageMac::Truncated(m) => m.as_ref(),
+ MessageMac::Full(m) => m.as_bytes(),
+ }
+ }
+}
+
+impl From<Mac> for MessageMac {
+ fn from(m: Mac) -> Self {
+ Self::Full(m)
+ }
+}
+
+impl From<[u8; Mac::TRUNCATED_LEN]> for MessageMac {
+ fn from(m: [u8; Mac::TRUNCATED_LEN]) -> Self {
+ Self::Truncated(m)
+ }
+}
+
+#[derive(Debug, Error)]
+pub enum DecryptionError {
+ #[error("Failed decrypting, invalid padding")]
+ InvalidPadding(#[from] UnpadError),
+ #[error("The MAC of the ciphertext didn't pass validation {0}")]
+ Mac(#[from] MacError),
+ #[allow(dead_code)]
+ #[error("The ciphertext didn't contain a valid MAC")]
+ MacMissing,
+}
+
+pub struct Cipher {
+ keys: CipherKeys,
+}
+
+impl Cipher {
+ pub fn new(key: &[u8; 32]) -> Self {
+ let keys = CipherKeys::new(key);
+
+ Self { keys }
+ }
+
+ pub fn new_megolm(&key: &[u8; 128]) -> Self {
+ let keys = CipherKeys::new_megolm(&key);
+
+ Self { keys }
+ }
+
+ #[cfg(feature = "libolm-compat")]
+ pub fn new_pickle(key: &[u8]) -> Self {
+ let keys = CipherKeys::new_pickle(key);
+
+ Self { keys }
+ }
+
+ fn get_hmac(&self) -> HmacSha256 {
+ // We don't use HmacSha256::new() here because it expects a 64-byte
+ // large HMAC key while the Olm spec defines a 32-byte one instead.
+ //
+ // https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md#version-1
+ HmacSha256::new_from_slice(self.keys.mac_key()).expect("Invalid HMAC key size")
+ }
+
+ pub fn encrypt(&self, plaintext: &[u8]) -> Vec<u8> {
+ let cipher = Aes256CbcEnc::new(self.keys.aes_key(), self.keys.iv());
+ cipher.encrypt_padded_vec_mut::<Pkcs7>(plaintext)
+ }
+
+ pub fn mac(&self, message: &[u8]) -> Mac {
+ let mut hmac = self.get_hmac();
+ hmac.update(message);
+
+ let mac_bytes = hmac.finalize().into_bytes();
+
+ let mut mac = [0u8; 32];
+ mac.copy_from_slice(&mac_bytes);
+
+ Mac(mac)
+ }
+
+ pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, UnpadError> {
+ let cipher = Aes256CbcDec::new(self.keys.aes_key(), self.keys.iv());
+ cipher.decrypt_padded_vec_mut::<Pkcs7>(ciphertext)
+ }
+
+ pub fn decrypt_pickle(&self, ciphertext: &[u8]) -> Result<Vec<u8>, DecryptionError> {
+ if ciphertext.len() < Mac::TRUNCATED_LEN + 1 {
+ Err(DecryptionError::MacMissing)
+ } else {
+ let (ciphertext, mac) = ciphertext.split_at(ciphertext.len() - Mac::TRUNCATED_LEN);
+ self.verify_truncated_mac(ciphertext, mac)?;
+
+ Ok(self.decrypt(ciphertext)?)
+ }
+ }
+
+ pub fn encrypt_pickle(&self, plaintext: &[u8]) -> Vec<u8> {
+ let mut ciphertext = self.encrypt(plaintext);
+ let mac = self.mac(&ciphertext);
+
+ ciphertext.extend(mac.truncate());
+
+ ciphertext
+ }
+
+ #[cfg(not(fuzzing))]
+ pub fn verify_mac(&self, message: &[u8], tag: &Mac) -> Result<(), MacError> {
+ let mut hmac = self.get_hmac();
+
+ hmac.update(message);
+ hmac.verify_slice(tag.as_bytes())
+ }
+
+ #[cfg(not(fuzzing))]
+ pub fn verify_truncated_mac(&self, message: &[u8], tag: &[u8]) -> Result<(), MacError> {
+ let mut hmac = self.get_hmac();
+
+ hmac.update(message);
+ hmac.verify_truncated_left(tag)
+ }
+
+ /// A verify_mac method that always succeeds.
+ ///
+ /// Useful if we're fuzzing vodozemac, since MAC verification discards a lot
+ /// of inputs right away.
+ #[cfg(fuzzing)]
+ pub fn verify_mac(&self, _: &[u8], _: &Mac) -> Result<(), MacError> {
+ Ok(())
+ }
+
+ #[cfg(fuzzing)]
+ pub fn verify_truncated_mac(&self, _: &[u8], _: &[u8]) -> Result<(), MacError> {
+ Ok(())
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +
// Copyright 2021 Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! A Rust implementation of Olm and Megolm
+//!
+//! vodozemac is a Rust reimplementation of
+//! [libolm](https://gitlab.matrix.org/matrix-org/olm), a cryptographic library
+//! used for end-to-end encryption in [Matrix](https://matrix.org). At its core, it
+//! is an implementation of the Olm and Megolm cryptographic ratchets, along
+//! with a high-level API to easily establish cryptographic communication
+//! channels employing those ratchets with other parties. It also implements
+//! some other miscellaneous cryptographic functionality which is useful for
+//! building Matrix clients, such as [SAS][sas].
+//!
+//! [sas]:
+//! <https://spec.matrix.org/v1.2/client-server-api/#short-authentication-string-sas-verification>
+//!
+//! # Olm
+//!
+//! Olm is an implementation of the [Double Ratchet
+//! algorithm](https://whispersystems.org/docs/specifications/doubleratchet/), very
+//! similar to that employed by the Signal Protocol. It allows the establishment
+//! of a 1-to-1 private communication channel, with perfect forward secrecy and
+//! self-healing properties.
+//!
+//! A detailed technical specification can be found at
+//! <https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md>.
+//!
+//! For more information on using vodozemac for Olm, see the [`olm`] module.
+//!
+//! # Megolm
+//!
+//! Megolm is an AES-based single ratchet for group conversations with a large
+//! number of participants, where using Olm would be cost prohibitive because it
+//! would imply encrypting each message individually for each participant.
+//! Megolm sidesteps this by encrypting messages with a symmetric ratchet,
+//! shared once with each participant and then reused for a sequence of messages
+//! before rotating.
+//!
+//! This is a trade-off in which we lose Olm's self-healing properties, because
+//! someone in possession of a Megolm session at a particular state can derive
+//! all future states. However, if the attacker is only able to obtain the
+//! session in a ratcheted state, they cannot use it to decrypt messages
+//! encrypted with an earlier state. This preserves forward secrecy.
+//!
+//! A detailed technical specification can be found at
+//! <https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/megolm.md>.
+//!
+//! For more information on using vodozemac for Megolm, see the [`megolm`]
+//! module.
+//!
+//! # Features
+//!
+//! ## Supported
+//!
+//! - [Olm](https://matrix-org.github.io/vodozemac/vodozemac/olm/index.html)
+//! - [Megolm](https://matrix-org.github.io/vodozemac/vodozemac/megolm/index.html)
+//! - [libolm pickle format](#legacy-pickles) (read-only)
+//! - [Modern pickle format](#modern-pickles)
+//! - [SAS (Short Authentication Strings)](https://matrix-org.github.io/vodozemac/vodozemac/sas/index.html)
+//!
+//! ## Unsupported
+//!
+//! - Creating asymmetric [server-side message key
+//! backups][legacy-message-key-backup], since they are slated to be replaced
+//! with symmetric backups.
+//!
+//! ## Planned
+//!
+//! - Symmetric [server-side message key backups][symmetric-message-key-backup]
+//! - Importing asymmetric [server-side message key
+//! backups][legacy-message-key-backup], for compatibility with existing
+//! backups created by libolm.
+//!
+//! [legacy-message-key-backup]:
+//! <https://spec.matrix.org/v1.2/client-server-api/#server-side-key-backups>
+//!
+//! [symmetric-message-key-backup]:
+//! https://github.com/uhoreg/matrix-doc/blob/symmetric-backups/proposals/3270-symmetric-megolm-backup.md
+//!
+//! # Feature flags
+//!
+//! ## Low-level API
+//!
+//! Feature: `low-level-api` (default: off)
+//!
+//! Vodozemac exposes some lower-level structs and functions that are only
+//! useful in very advanced use cases. These should *not* be needed by the vast
+//! majority of users.
+//!
+//! Extreme care must be taken when using such APIs, as incorrect usage can lead
+//! to broken sessions.
+//!
+//! # Pickling
+//!
+//! vodozemac supports serializing its entire internal state into a form
+//! a "pickle". The state can subsequently be restored from such a pickle
+//! ("unpickled") in order to continue operation. This is used to support some
+//! Matrix features like device dehydration.
+//!
+//! ## Legacy pickles
+//!
+//! The legacy pickle format is a simple binary format used by libolm.
+//! Implemented for interoperability with current clients which are using
+//! libolm. Only *unpickling* is supported.
+//!
+//! ## Modern pickles
+//!
+//! The crate also implements a modern pickling mechanism using
+//! [Serde](https://serde.rs/). The exact serialization format is not mandated
+//! nor specified by this crate, but you can serialize to and deserialize from
+//! any format supported by Serde.
+//!
+//! The following structs support pickling:
+//!
+//! - [`olm::Account`]
+//! - [`olm::Session`]
+//! - [`megolm::GroupSession`]
+//! - [`megolm::InboundGroupSession`]
+//!
+//! For example, the following will print out the JSON representing the
+//! serialized `Account` and will leave no new copies of the account's secrets
+//! in memory:
+//!
+//! ```rust
+//! use anyhow::Result;
+//! use vodozemac::olm::{Account, AccountPickle};
+//!
+//! const PICKLE_KEY: [u8; 32] = [0u8; 32];
+//!
+//! fn main() -> Result<()>{
+//! let mut account = Account::new();
+//!
+//! account.generate_one_time_keys(10);
+//! account.generate_fallback_key();
+//!
+//! let pickle = account.pickle().encrypt(&PICKLE_KEY);
+//!
+//! let account2: Account = AccountPickle::from_encrypted(&pickle, &PICKLE_KEY)?.into();
+//!
+//! assert_eq!(account.identity_keys(), account2.identity_keys());
+//!
+//! Ok(())
+//! }
+//! ```
+//!
+//! You can unpickle a pickle-able struct directly from its serialized form:
+//!
+//! ```rust
+//! # use anyhow::Result;
+//! # use vodozemac::olm::{Account, AccountPickle};
+//! # use zeroize::Zeroize;
+//! #
+//! # fn main() -> Result<()> {
+//! # let some_account = Account::new();
+//! let mut json_str = serde_json::to_string(&some_account.pickle())?;
+//! // This will produce an account which is identical to `some_account`.
+//! let account: Account = serde_json::from_str::<AccountPickle>(&json_str)?.into();
+//!
+//! json_str.zeroize();
+//! #
+//! # Ok(())
+//! # }
+//! ```
+//!
+//! However, the pickle-able structs do not implement `serde::Serialize`
+//! themselves. If you want to serialize to a format other than JSON, you should
+//! instead call the `.pickle()` method to obtain a special serializable struct.
+//! This struct *does* implement `Serialize` and can therefore be serialized
+//! into any format supported by `serde`. To get back to the original struct
+//! from such as serializeable struct, just call `.unpickle()`.
+//!
+//! ```rust
+//! use anyhow::Result;
+//! use vodozemac::olm::Account;
+//!
+//! fn main() -> Result<()> {
+//! let account = Account::new();
+//! let account: Account = account.pickle().into(); // this is identity
+//!
+//! Ok(())
+//! }
+//! ```
+
+#![deny(
+ clippy::mem_forget,
+ clippy::unwrap_used,
+ dead_code,
+ trivial_casts,
+ trivial_numeric_casts,
+ unsafe_code,
+ unsafe_op_in_unsafe_fn,
+ unused_extern_crates,
+ unused_import_braces,
+ unused_qualifications,
+ rust_2018_idioms
+)]
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
+
+mod cipher;
+mod types;
+mod utilities;
+
+pub mod hazmat;
+pub mod megolm;
+pub mod olm;
+pub mod sas;
+
+pub use base64::DecodeError as Base64DecodeError;
+pub use prost::DecodeError as ProtoBufDecodeError;
+pub use types::{
+ Curve25519PublicKey, Curve25519SecretKey, Ed25519Keypair, Ed25519PublicKey, Ed25519SecretKey,
+ Ed25519Signature, KeyError, KeyId, SharedSecret, SignatureError,
+};
+
+/// Error type describing the various ways Vodozemac pickles can fail to be
+/// decoded.
+#[derive(Debug, thiserror::Error)]
+pub enum PickleError {
+ /// The pickle wasn't valid base64.
+ #[error("The pickle wasn't valid base64: {0}")]
+ Base64(#[from] base64::DecodeError),
+ /// The encrypted pickle could not have been decrypted.
+ #[error("The pickle couldn't be decrypted: {0}")]
+ Decryption(#[from] crate::cipher::DecryptionError),
+ /// The serialized Vodozemac object couldn't be deserialized.
+ #[error("The pickle couldn't be deserialized: {0}")]
+ Serialization(#[from] serde_json::Error),
+}
+
+/// Error type describing the various ways libolm pickles can fail to be
+/// decoded.
+#[cfg(feature = "libolm-compat")]
+#[derive(Debug, thiserror::Error)]
+pub enum LibolmPickleError {
+ /// The pickle is missing a valid version.
+ #[error("The pickle doesn't contain a version")]
+ MissingVersion,
+ /// The pickle has a unsupported version.
+ #[error("The pickle uses an unsupported version, expected {0}, got {1}")]
+ Version(u32, u32),
+ /// The pickle wasn't valid base64.
+ #[error("The pickle wasn't valid base64: {0}")]
+ Base64(#[from] Base64DecodeError),
+ /// The pickle could not have been decrypted.
+ #[error("The pickle couldn't be decrypted: {0}")]
+ Decryption(#[from] crate::cipher::DecryptionError),
+ /// The pickle contains an invalid public key.
+ #[error("The pickle contained an invalid ed25519 public key {0}")]
+ PublicKey(#[from] KeyError),
+ /// The pickle does not contain a valid receiving or sending chain. A valid
+ /// Olm session needs to have at least one of them.
+ #[error("The pickle didn't contain a valid Olm session")]
+ InvalidSession,
+ /// The payload of the pickle could not be decoded.
+ #[error(transparent)]
+ Decode(#[from] matrix_pickle::DecodeError),
+}
+
+/// Error type describing the different ways message decoding can fail.
+#[derive(Debug, thiserror::Error)]
+pub enum DecodeError {
+ /// The Olm message has an invalid type.
+ #[error("The message has an invalid type, expected 0 or 1, got {0}")]
+ MessageType(usize),
+ /// The message is missing a valid version.
+ #[error("The message didn't contain a version")]
+ MissingVersion,
+ /// The message doesn't have enough data to be correctly decoded.
+ #[error("The message was too short, it didn't contain a valid payload")]
+ MessageTooShort(usize),
+ /// The message has a unsupported version.
+ #[error("The message didn't have a valid version, expected {0}, got {1}")]
+ InvalidVersion(u8, u8),
+ /// An embedded public key couldn't be decoded.
+ #[error("The message contained an invalid public key: {0}")]
+ InvalidKey(#[from] KeyError),
+ /// The embedded message authentication code couldn't be decoded.
+ #[error("The message contained a MAC with an invalid size, expected {0}, got {1}")]
+ InvalidMacLength(usize, usize),
+ /// An embedded signature couldn't be decoded.
+ #[error("The message contained an invalid Signature: {0}")]
+ Signature(#[from] SignatureError),
+ /// The message couldn't be decoded as a valid protocol buffer message.
+ #[error(transparent)]
+ ProtoBufError(#[from] ProtoBufDecodeError),
+ /// The message wasn't valid base64.
+ #[error("The message wasn't valid base64: {0}")]
+ Base64(#[from] Base64DecodeError),
+}
+
+/// The version of vodozemac that is being used.
+pub static VERSION: &str = env!("CARGO_PKG_VERSION");
+
+#[cfg(test)]
+fn corpus_data_path(fuzz_target: &str) -> std::path::PathBuf {
+ let manifest_dir =
+ std::env::var("CARGO_MANIFEST_DIR").expect("Cargo always sets the manifest dir");
+
+ let mut afl_dir = std::path::PathBuf::from(manifest_dir);
+ afl_dir.push("afl");
+ afl_dir.push(fuzz_target);
+ afl_dir.push("in");
+
+ afl_dir
+}
+
+#[cfg(test)]
+fn run_corpus<F>(fuzz_target: &str, method: F)
+where
+ F: FnOnce(&[u8]) + Copy,
+{
+ let dir = corpus_data_path(fuzz_target);
+ let corpus = std::fs::read_dir(dir).expect("Couldn't read the corpus directory");
+
+ for input in corpus {
+ let input = input.expect("Couldn't read the input file");
+ let data = std::fs::read(input.path()).expect("Couldn't read the input file");
+ method(&data)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +
// Copyright 2021 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use serde::{Deserialize, Serialize};
+
+use super::{
+ default_config, message::MegolmMessage, ratchet::Ratchet, session_config::Version,
+ session_keys::SessionKey, SessionConfig,
+};
+use crate::{
+ cipher::Cipher,
+ types::Ed25519Keypair,
+ utilities::{pickle, unpickle},
+ PickleError,
+};
+
+/// A Megolm group session represents a single sending participant in an
+/// encrypted group communication context containing multiple receiving parties.
+///
+/// A group session consists of a ratchet, used for encryption, and an Ed25519
+/// signing key pair, used for authenticity.
+///
+/// A group session containing the signing key pair is also known as an
+/// "outbound" group session. We differentiate this from an *inbound* group
+/// session where this key pair has been removed and which can be used solely
+/// for receipt and decryption of messages.
+///
+/// Such an inbound group session is typically sent by the outbound group
+/// session owner to each of the receiving parties via a secure peer-to-peer
+/// channel (e.g. an Olm channel).
+pub struct GroupSession {
+ ratchet: Ratchet,
+ signing_key: Ed25519Keypair,
+ config: SessionConfig,
+}
+
+impl Default for GroupSession {
+ fn default() -> Self {
+ Self::new(Default::default())
+ }
+}
+
+impl GroupSession {
+ /// Construct a new group session, with a random ratchet state and signing
+ /// key pair.
+ pub fn new(config: SessionConfig) -> Self {
+ let signing_key = Ed25519Keypair::new();
+ Self { signing_key, ratchet: Ratchet::new(), config }
+ }
+
+ /// Returns the globally unique session ID, in base64-encoded form.
+ ///
+ /// A session ID is the public part of the Ed25519 key pair associated with
+ /// the group session. Due to the construction, every session ID is
+ /// (probabilistically) globally unique.
+ pub fn session_id(&self) -> String {
+ self.signing_key.public_key().to_base64()
+ }
+
+ /// Return the current message index.
+ ///
+ /// The message index is incremented each time a message is encrypted with
+ /// the group session.
+ pub fn message_index(&self) -> u32 {
+ self.ratchet.index()
+ }
+
+ pub fn session_config(&self) -> SessionConfig {
+ self.config
+ }
+
+ /// Encrypt the `plaintext` with the group session.
+ ///
+ /// The resulting ciphertext is MAC-ed, then signed with the group session's
+ /// Ed25519 key pair and finally base64-encoded.
+ pub fn encrypt(&mut self, plaintext: impl AsRef<[u8]>) -> MegolmMessage {
+ let cipher = Cipher::new_megolm(self.ratchet.as_bytes());
+
+ let message = match self.config.version {
+ Version::V1 => MegolmMessage::encrypt_truncated_mac(
+ self.message_index(),
+ &cipher,
+ &self.signing_key,
+ plaintext.as_ref(),
+ ),
+ Version::V2 => MegolmMessage::encrypt_full_mac(
+ self.message_index(),
+ &cipher,
+ &self.signing_key,
+ plaintext.as_ref(),
+ ),
+ };
+
+ self.ratchet.advance();
+
+ message
+ }
+
+ /// Export the group session into a session key.
+ ///
+ /// The session key contains the key version constant, the current message
+ /// index, the ratchet state and the *public* part of the signing key pair.
+ /// It is signed by the signing key pair for authenticity.
+ ///
+ /// The session key is in a portable format, suitable for sending over the
+ /// network. It is typically sent to other group participants so that they
+ /// can reconstruct an inbound group session in order to decrypt messages
+ /// sent by this group session.
+ pub fn session_key(&self) -> SessionKey {
+ let mut session_key = SessionKey::new(&self.ratchet, self.signing_key.public_key());
+ let signature = self.signing_key.sign(&session_key.to_signature_bytes());
+ session_key.signature = signature;
+
+ session_key
+ }
+
+ /// Convert the group session into a struct which implements
+ /// [`serde::Serialize`] and [`serde::Deserialize`].
+ pub fn pickle(&self) -> GroupSessionPickle {
+ GroupSessionPickle {
+ ratchet: self.ratchet.clone(),
+ signing_key: self.signing_key.clone(),
+ config: self.config,
+ }
+ }
+
+ /// Restore a [`GroupSession`] from a previously saved
+ /// [`GroupSessionPickle`].
+ pub fn from_pickle(pickle: GroupSessionPickle) -> Self {
+ pickle.into()
+ }
+
+ #[cfg(feature = "libolm-compat")]
+ pub fn from_libolm_pickle(
+ pickle: &str,
+ pickle_key: &[u8],
+ ) -> Result<Self, crate::LibolmPickleError> {
+ use matrix_pickle::Decode;
+ use zeroize::Zeroize;
+
+ use crate::{
+ megolm::libolm::LibolmRatchetPickle,
+ utilities::{unpickle_libolm, LibolmEd25519Keypair},
+ };
+
+ #[derive(Zeroize, Decode)]
+ #[zeroize(drop)]
+ struct Pickle {
+ version: u32,
+ ratchet: LibolmRatchetPickle,
+ ed25519_keypair: LibolmEd25519Keypair,
+ }
+
+ impl TryFrom<Pickle> for GroupSession {
+ type Error = crate::LibolmPickleError;
+
+ fn try_from(pickle: Pickle) -> Result<Self, Self::Error> {
+ // Removing the borrow doesn't work and clippy complains about
+ // this on nightly.
+ #[allow(clippy::needless_borrow)]
+ let ratchet = (&pickle.ratchet).into();
+ let signing_key =
+ Ed25519Keypair::from_expanded_key(&pickle.ed25519_keypair.private_key)?;
+
+ Ok(Self { ratchet, signing_key, config: SessionConfig::version_1() })
+ }
+ }
+
+ const PICKLE_VERSION: u32 = 1;
+
+ unpickle_libolm::<Pickle, _>(pickle, pickle_key, PICKLE_VERSION)
+ }
+}
+
+/// A format suitable for serialization which implements [`serde::Serialize`]
+/// and [`serde::Deserialize`]. Obtainable by calling [`GroupSession::pickle`].
+#[derive(Serialize, Deserialize)]
+pub struct GroupSessionPickle {
+ ratchet: Ratchet,
+ signing_key: Ed25519Keypair,
+ #[serde(default = "default_config")]
+ config: SessionConfig,
+}
+
+impl GroupSessionPickle {
+ /// Serialize and encrypt the pickle using the given key.
+ ///
+ /// This is the inverse of [`GroupSessionPickle::from_encrypted`].
+ pub fn encrypt(self, pickle_key: &[u8; 32]) -> String {
+ pickle(&self, pickle_key)
+ }
+
+ /// Obtain a pickle from a ciphertext by decrypting and deserializing using
+ /// the given key.
+ ///
+ /// This is the inverse of [`GroupSessionPickle::encrypt`].
+ pub fn from_encrypted(ciphertext: &str, pickle_key: &[u8; 32]) -> Result<Self, PickleError> {
+ unpickle(ciphertext, pickle_key)
+ }
+}
+
+impl From<GroupSessionPickle> for GroupSession {
+ fn from(pickle: GroupSessionPickle) -> Self {
+ Self { ratchet: pickle.ratchet, signing_key: pickle.signing_key, config: pickle.config }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +
// Copyright 2021 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::cmp::Ordering;
+
+use aes::cipher::block_padding::UnpadError;
+use hmac::digest::MacError;
+use serde::{Deserialize, Serialize};
+use subtle::ConstantTimeEq;
+use thiserror::Error;
+use zeroize::Zeroize;
+
+use super::{
+ default_config,
+ message::MegolmMessage,
+ ratchet::Ratchet,
+ session_config::Version,
+ session_keys::{ExportedSessionKey, SessionKey},
+ GroupSession, SessionConfig,
+};
+use crate::{
+ cipher::{Cipher, Mac, MessageMac},
+ types::{Ed25519PublicKey, SignatureError},
+ utilities::{base64_encode, pickle, unpickle},
+ PickleError,
+};
+
+/// The result of a comparison between two [`InboundGroupSession`] types.
+///
+/// Tells us if one session can be considered to be better than another one.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum SessionOrdering {
+ /// The sessions are the same.
+ Equal,
+ /// The first session has a better initial message index than the second
+ /// one.
+ Better,
+ /// The first session has a worse initial message index than the second one.
+ Worse,
+ /// The sessions are not the same, they can't be compared.
+ Unconnected,
+}
+
+/// Error type for Megolm-based decryption failures.
+#[derive(Debug, Error)]
+pub enum DecryptionError {
+ /// The signature on the message was invalid.
+ #[error("The signature on the message was invalid: {0}")]
+ Signature(#[from] SignatureError),
+
+ /// The message authentication code of the message was invalid.
+ #[error("Failed decrypting Megolm message, invalid MAC: {0}")]
+ InvalidMAC(#[from] MacError),
+
+ /// The length of the message authentication code of the message did not
+ /// match our expected length.
+ #[error("Failed decrypting Olm message, invalid MAC length: expected {0}, got {1}")]
+ InvalidMACLength(usize, usize),
+
+ /// The ciphertext of the message isn't padded correctly.
+ #[error("Failed decrypting Megolm message, invalid padding")]
+ InvalidPadding(#[from] UnpadError),
+
+ /// The session is missing the correct message key to decrypt the message,
+ /// The Session has been ratcheted forwards and the message key isn't
+ /// available anymore.
+ #[error(
+ "The message was encrypted using an unknown message index, \
+ first known index {0}, index of the message {1}"
+ )]
+ UnknownMessageIndex(u32, u32),
+}
+
+#[derive(Deserialize)]
+#[serde(try_from = "InboundGroupSessionPickle")]
+pub struct InboundGroupSession {
+ initial_ratchet: Ratchet,
+ latest_ratchet: Ratchet,
+ signing_key: Ed25519PublicKey,
+ signing_key_verified: bool,
+ config: SessionConfig,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DecryptedMessage {
+ pub plaintext: Vec<u8>,
+ pub message_index: u32,
+}
+
+impl InboundGroupSession {
+ pub fn new(key: &SessionKey, session_config: SessionConfig) -> Self {
+ let initial_ratchet =
+ Ratchet::from_bytes(key.session_key.ratchet.clone(), key.session_key.ratchet_index);
+ let latest_ratchet = initial_ratchet.clone();
+
+ Self {
+ initial_ratchet,
+ latest_ratchet,
+ signing_key: key.session_key.signing_key,
+ signing_key_verified: true,
+ config: session_config,
+ }
+ }
+
+ pub fn import(session_key: &ExportedSessionKey, session_config: SessionConfig) -> Self {
+ let initial_ratchet =
+ Ratchet::from_bytes(session_key.ratchet.clone(), session_key.ratchet_index);
+ let latest_ratchet = initial_ratchet.clone();
+
+ Self {
+ initial_ratchet,
+ latest_ratchet,
+ signing_key: session_key.signing_key,
+ signing_key_verified: false,
+ config: session_config,
+ }
+ }
+
+ pub fn session_id(&self) -> String {
+ base64_encode(self.signing_key.as_bytes())
+ }
+
+ /// Check if two `InboundGroupSession`s are the same.
+ ///
+ /// An `InboundGroupSession` could be received multiple times with varying
+ /// degrees of trust and first known message indices.
+ ///
+ /// This method checks if the underlying ratchets of the two
+ /// `InboundGroupSession`s are actually the same ratchet, potentially at
+ /// a different ratcheting index. That is, if the sessions are *connected*,
+ /// then ratcheting one of the ratchets to the index of the other should
+ /// yield the same ratchet value, byte-for-byte. This will only be the case
+ /// if the `InboundGroupSession`s were created from the same
+ /// [`GroupSession`].
+ ///
+ /// If the sessions are connected, the session with the lower message index
+ /// can safely replace the one with the higher message index.
+ pub fn connected(&mut self, other: &mut InboundGroupSession) -> bool {
+ // This method attempts to bring the two `Ratchet` values, one from each
+ // session, to the same message index.
+ //
+ // We first try to ratchet our own ratchets towards the initial ratchet
+ // of the other session. If that fails we try to ratchet the other
+ // session's ratchets towards our initial ratchet.
+ //
+ // After that we compare the raw ratchet bytes in constant time.
+
+ if self.config != other.config || self.signing_key != other.signing_key {
+ // Short circuit if session configs differ or the signing keys
+ // differ. This is comparing public key material.
+ false
+ } else if let Some(ratchet) = self.find_ratchet(other.first_known_index()) {
+ ratchet.ct_eq(&other.initial_ratchet).into()
+ } else if let Some(ratchet) = other.find_ratchet(self.first_known_index()) {
+ self.initial_ratchet.ct_eq(ratchet).into()
+ } else {
+ unreachable!("Either index A >= index B, or vice versa. There is no third option.")
+ }
+ }
+
+ /// Compare the `InboundGroupSession` with the given other
+ /// `InboundGroupSession`.
+ ///
+ /// Returns a `SessionOrdering` describing how the two sessions relate to
+ /// each other.
+ pub fn compare(&mut self, other: &mut InboundGroupSession) -> SessionOrdering {
+ if self.connected(other) {
+ match self.first_known_index().cmp(&other.first_known_index()) {
+ Ordering::Less => SessionOrdering::Better,
+ Ordering::Equal => SessionOrdering::Equal,
+ Ordering::Greater => SessionOrdering::Worse,
+ }
+ } else {
+ // If we're not connected to other, other can't be better.
+ SessionOrdering::Unconnected
+ }
+ }
+
+ /// Merge the session with the given other session, picking the best parts
+ /// from each of them.
+ ///
+ /// This method is useful when you receive multiple sessions with
+ /// the same session ID but potentially different ratchet indices and
+ /// authenticity properties.
+ ///
+ /// For example, imagine you receive a `SessionKey` S1 with ratchet index
+ /// A from a fully-trusted source and an `ExportedSessionKey` S2 with
+ /// ratchet state B from a less trusted source. If A > B, then S1 is better
+ /// because it's fully trusted, but worse because it's ratcheted further
+ /// than S2.
+ ///
+ /// This method allows you to merge S1 and S2 safely into a fully-trusted S3
+ /// with ratchet state B, provided S1 and S2 connect with each other
+ /// (meaning they are the same session, just at different ratchet indices).
+ ///
+ /// Returns `Some(session)` if the sessions could be merged, i.e. they are
+ /// considered to be connected and `None` otherwise.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use vodozemac::megolm::{GroupSession, InboundGroupSession, SessionOrdering};
+ ///
+ /// let session = GroupSession::new(Default::default());
+ /// let session_key = session.session_key();
+ ///
+ /// let mut first_session = InboundGroupSession::new(&session_key, Default::default());
+ /// let mut second_session = InboundGroupSession::import(&first_session.export_at(10).unwrap(), Default::default());
+ ///
+ /// assert_eq!(first_session.compare(&mut second_session), SessionOrdering::Better);
+ ///
+ /// let mut merged = second_session.merge(&mut first_session).unwrap();
+ ///
+ /// assert_eq!(merged.compare(&mut second_session), SessionOrdering::Better);
+ /// assert_eq!(merged.compare(&mut first_session), SessionOrdering::Equal);
+ /// ```
+ pub fn merge(&mut self, other: &mut InboundGroupSession) -> Option<InboundGroupSession> {
+ let best_ratchet = match self.compare(other) {
+ SessionOrdering::Equal | SessionOrdering::Better => Some(self.initial_ratchet.clone()),
+ SessionOrdering::Worse => Some(other.initial_ratchet.clone()),
+ SessionOrdering::Unconnected => None,
+ }?;
+
+ Some(InboundGroupSession {
+ initial_ratchet: best_ratchet.clone(),
+ latest_ratchet: best_ratchet,
+ signing_key: self.signing_key,
+ signing_key_verified: self.signing_key_verified || other.signing_key_verified,
+ config: self.config,
+ })
+ }
+
+ pub fn first_known_index(&self) -> u32 {
+ self.initial_ratchet.index()
+ }
+
+ /// Permanently advance the session to the given index.
+ ///
+ /// This will remove the ability to decrypt messages that were encrypted
+ /// with a lower message index than what is given as the argument.
+ ///
+ /// Returns true if the ratchet has been advanced, false if the ratchet was
+ /// already advanced past the given index.
+ pub fn advance_to(&mut self, index: u32) -> bool {
+ if self.first_known_index() < index {
+ self.initial_ratchet.advance_to(index);
+
+ if self.latest_ratchet.index() < index {
+ self.latest_ratchet = self.initial_ratchet.clone();
+ }
+
+ true
+ } else {
+ false
+ }
+ }
+
+ /// Returns a copy of the [`Cipher`] at the given message index, without
+ /// advancing the internal ratchets.
+ #[cfg(feature = "low-level-api")]
+ pub fn get_cipher_at(&self, message_index: u32) -> Option<Cipher> {
+ if self.initial_ratchet.index() <= message_index {
+ let mut ratchet = self.initial_ratchet.clone();
+ if self.initial_ratchet.index() < message_index {
+ ratchet.advance_to(message_index);
+ }
+ Some(Cipher::new_megolm(ratchet.as_bytes()))
+ } else {
+ None
+ }
+ }
+
+ fn find_ratchet(&mut self, message_index: u32) -> Option<&Ratchet> {
+ if self.initial_ratchet.index() == message_index {
+ Some(&self.initial_ratchet)
+ } else if self.latest_ratchet.index() == message_index {
+ Some(&self.latest_ratchet)
+ } else if self.latest_ratchet.index() < message_index {
+ self.latest_ratchet.advance_to(message_index);
+ Some(&self.latest_ratchet)
+ } else if self.initial_ratchet.index() < message_index {
+ self.latest_ratchet = self.initial_ratchet.clone();
+ self.latest_ratchet.advance_to(message_index);
+ Some(&self.latest_ratchet)
+ } else {
+ None
+ }
+ }
+
+ fn verify_mac(&self, cipher: &Cipher, message: &MegolmMessage) -> Result<(), DecryptionError> {
+ match self.config.version {
+ Version::V1 => {
+ if let MessageMac::Truncated(m) = &message.mac {
+ Ok(cipher.verify_truncated_mac(&message.to_mac_bytes(), m)?)
+ } else {
+ Err(DecryptionError::InvalidMACLength(Mac::TRUNCATED_LEN, Mac::LENGTH))
+ }
+ }
+ Version::V2 => {
+ if let MessageMac::Full(m) = &message.mac {
+ Ok(cipher.verify_mac(&message.to_mac_bytes(), m)?)
+ } else {
+ Err(DecryptionError::InvalidMACLength(Mac::LENGTH, Mac::TRUNCATED_LEN))
+ }
+ }
+ }
+ }
+
+ pub fn decrypt(
+ &mut self,
+ message: &MegolmMessage,
+ ) -> Result<DecryptedMessage, DecryptionError> {
+ self.signing_key.verify(&message.to_signature_bytes(), &message.signature)?;
+
+ if let Some(ratchet) = self.find_ratchet(message.message_index) {
+ let cipher = Cipher::new_megolm(ratchet.as_bytes());
+
+ self.verify_mac(&cipher, message)?;
+
+ let plaintext = cipher.decrypt(&message.ciphertext)?;
+
+ Ok(DecryptedMessage { plaintext, message_index: message.message_index })
+ } else {
+ Err(DecryptionError::UnknownMessageIndex(
+ self.initial_ratchet.index(),
+ message.message_index,
+ ))
+ }
+ }
+
+ pub fn export_at(&mut self, index: u32) -> Option<ExportedSessionKey> {
+ let signing_key = self.signing_key;
+
+ self.find_ratchet(index).map(|ratchet| ExportedSessionKey::new(ratchet, signing_key))
+ }
+
+ pub fn export_at_first_known_index(&self) -> ExportedSessionKey {
+ ExportedSessionKey::new(&self.initial_ratchet, self.signing_key)
+ }
+
+ /// Convert the inbound group session into a struct which implements
+ /// [`serde::Serialize`] and [`serde::Deserialize`].
+ pub fn pickle(&self) -> InboundGroupSessionPickle {
+ InboundGroupSessionPickle {
+ initial_ratchet: self.initial_ratchet.clone(),
+ signing_key: self.signing_key,
+ signing_key_verified: self.signing_key_verified,
+ config: self.config,
+ }
+ }
+
+ /// Restore an [`InboundGroupSession`] from a previously saved
+ /// [`InboundGroupSessionPickle`].
+ pub fn from_pickle(pickle: InboundGroupSessionPickle) -> Self {
+ Self::from(pickle)
+ }
+
+ #[cfg(feature = "libolm-compat")]
+ pub fn from_libolm_pickle(
+ pickle: &str,
+ pickle_key: &[u8],
+ ) -> Result<Self, crate::LibolmPickleError> {
+ use matrix_pickle::Decode;
+
+ use super::libolm::LibolmRatchetPickle;
+ use crate::utilities::unpickle_libolm;
+
+ #[derive(Zeroize, Decode)]
+ #[zeroize(drop)]
+ struct Pickle {
+ version: u32,
+ initial_ratchet: LibolmRatchetPickle,
+ latest_ratchet: LibolmRatchetPickle,
+ signing_key: [u8; 32],
+ signing_key_verified: bool,
+ }
+
+ impl TryFrom<Pickle> for InboundGroupSession {
+ type Error = crate::LibolmPickleError;
+
+ fn try_from(pickle: Pickle) -> Result<Self, Self::Error> {
+ // Removing the borrow doesn't work and clippy complains about
+ // this on nightly.
+ #[allow(clippy::needless_borrow)]
+ let initial_ratchet = (&pickle.initial_ratchet).into();
+ #[allow(clippy::needless_borrow)]
+ let latest_ratchet = (&pickle.latest_ratchet).into();
+ let signing_key = Ed25519PublicKey::from_slice(&pickle.signing_key)?;
+ let signing_key_verified = pickle.signing_key_verified;
+
+ Ok(Self {
+ initial_ratchet,
+ latest_ratchet,
+ signing_key,
+ signing_key_verified,
+ config: SessionConfig::version_1(),
+ })
+ }
+ }
+
+ const PICKLE_VERSION: u32 = 2;
+
+ unpickle_libolm::<Pickle, _>(pickle, pickle_key, PICKLE_VERSION)
+ }
+}
+
+/// A format suitable for serialization which implements [`serde::Serialize`]
+/// and [`serde::Deserialize`]. Obtainable by calling
+/// [`InboundGroupSession::pickle`].
+#[derive(Serialize, Deserialize)]
+pub struct InboundGroupSessionPickle {
+ initial_ratchet: Ratchet,
+ signing_key: Ed25519PublicKey,
+ #[allow(dead_code)]
+ signing_key_verified: bool,
+ #[serde(default = "default_config")]
+ config: SessionConfig,
+}
+
+impl InboundGroupSessionPickle {
+ /// Serialize and encrypt the pickle using the given key.
+ ///
+ /// This is the inverse of [`InboundGroupSessionPickle::from_encrypted`].
+ pub fn encrypt(self, pickle_key: &[u8; 32]) -> String {
+ pickle(&self, pickle_key)
+ }
+
+ /// Obtain a pickle from a ciphertext by decrypting and deserializing using
+ /// the given key.
+ ///
+ /// This is the inverse of [`InboundGroupSessionPickle::encrypt`].
+ pub fn from_encrypted(ciphertext: &str, pickle_key: &[u8; 32]) -> Result<Self, PickleError> {
+ unpickle(ciphertext, pickle_key)
+ }
+}
+
+impl From<&InboundGroupSession> for InboundGroupSessionPickle {
+ fn from(session: &InboundGroupSession) -> Self {
+ session.pickle()
+ }
+}
+
+impl From<InboundGroupSessionPickle> for InboundGroupSession {
+ fn from(pickle: InboundGroupSessionPickle) -> Self {
+ Self {
+ initial_ratchet: pickle.initial_ratchet.clone(),
+ latest_ratchet: pickle.initial_ratchet,
+ signing_key: pickle.signing_key,
+ signing_key_verified: pickle.signing_key_verified,
+ config: pickle.config,
+ }
+ }
+}
+
+impl From<&GroupSession> for InboundGroupSession {
+ fn from(session: &GroupSession) -> Self {
+ Self::new(&session.session_key(), session.session_config())
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::InboundGroupSession;
+ use crate::megolm::{GroupSession, SessionConfig, SessionOrdering};
+
+ #[test]
+ fn advance_inbound_session() {
+ let mut session = InboundGroupSession::from(&GroupSession::new(Default::default()));
+
+ assert_eq!(session.first_known_index(), 0);
+ assert_eq!(session.latest_ratchet.index(), 0);
+
+ assert!(session.advance_to(10));
+ assert_eq!(session.first_known_index(), 10);
+ assert_eq!(session.latest_ratchet.index(), 10);
+
+ assert!(!session.advance_to(10));
+
+ assert!(session.advance_to(20));
+ assert_eq!(session.first_known_index(), 20);
+ assert_eq!(session.latest_ratchet.index(), 20);
+ }
+
+ #[test]
+ fn connecting() {
+ let outbound = GroupSession::new(Default::default());
+ let mut session = InboundGroupSession::from(&outbound);
+ let mut clone = InboundGroupSession::from(&outbound);
+
+ assert!(session.connected(&mut clone));
+ assert!(clone.connected(&mut session));
+
+ clone.advance_to(10);
+
+ assert!(session.connected(&mut clone));
+ assert!(clone.connected(&mut session));
+
+ let mut other = InboundGroupSession::from(&GroupSession::new(Default::default()));
+
+ assert!(!session.connected(&mut other));
+ assert!(!clone.connected(&mut other));
+
+ other.signing_key = session.signing_key;
+
+ assert!(!session.connected(&mut other));
+ assert!(!clone.connected(&mut other));
+
+ let session_key = session.export_at_first_known_index();
+ let mut different_config =
+ InboundGroupSession::import(&session_key, SessionConfig::version_1());
+
+ assert!(!session.connected(&mut different_config));
+ assert!(!different_config.connected(&mut session));
+ }
+
+ #[test]
+ fn comparison() {
+ let outbound = GroupSession::new(Default::default());
+ let mut session = InboundGroupSession::from(&outbound);
+ let mut clone = InboundGroupSession::from(&outbound);
+
+ assert_eq!(session.compare(&mut clone), SessionOrdering::Equal);
+ assert_eq!(clone.compare(&mut session), SessionOrdering::Equal);
+
+ clone.advance_to(10);
+
+ assert_eq!(session.compare(&mut clone), SessionOrdering::Better);
+ assert_eq!(clone.compare(&mut session), SessionOrdering::Worse);
+
+ let mut other = InboundGroupSession::from(&GroupSession::new(Default::default()));
+
+ assert_eq!(session.compare(&mut other), SessionOrdering::Unconnected);
+ assert_eq!(clone.compare(&mut other), SessionOrdering::Unconnected);
+
+ other.signing_key = session.signing_key;
+
+ assert_eq!(session.compare(&mut other), SessionOrdering::Unconnected);
+ assert_eq!(clone.compare(&mut other), SessionOrdering::Unconnected);
+ }
+
+ #[test]
+ fn upgrade() {
+ let session = GroupSession::new(Default::default());
+ let session_key = session.session_key();
+
+ let mut first_session = InboundGroupSession::new(&session_key, Default::default());
+
+ // This one is less trusted because it's imported from an `ExportedSessionKey`.
+ let mut second_session =
+ InboundGroupSession::import(&first_session.export_at(10).unwrap(), Default::default());
+ assert!(!second_session.signing_key_verified);
+
+ assert_eq!(first_session.compare(&mut second_session), SessionOrdering::Better);
+
+ let mut merged = second_session.merge(&mut first_session).unwrap();
+
+ assert!(merged.signing_key_verified);
+ assert_eq!(merged.compare(&mut second_session), SessionOrdering::Better);
+ assert_eq!(merged.compare(&mut first_session), SessionOrdering::Equal);
+ }
+
+ /// Test that [`InboundGroupSession::get_cipher_at`] correctly handles the
+ /// correct range of message indices.`
+ #[cfg(feature = "low-level-api")]
+ #[test]
+ fn get_cipher_at() {
+ let mut group_session = GroupSession::new(Default::default());
+
+ // Advance the ratchet a few times by calling `encrypt`.
+ group_session.encrypt("test1");
+ group_session.encrypt("test2");
+
+ let session = InboundGroupSession::from(&group_session);
+
+ // The inbound session will only be able to decrypt messages from
+ // indices starting at 2 (as we advanced the ratchet twice before
+ // creating the inbound session)
+ assert!(session.get_cipher_at(0).is_none());
+ assert!(session.get_cipher_at(1).is_none());
+ assert!(session.get_cipher_at(2).is_some());
+ assert!(session.get_cipher_at(1000).is_some());
+
+ // Now check that we actually *do* advance the ratchet. We do this by
+ // checking that the ratchet changes.
+ assert_ne!(
+ session.get_cipher_at(2).unwrap().encrypt(b""),
+ session.get_cipher_at(3).unwrap().encrypt(b"")
+ );
+ assert_ne!(
+ session.get_cipher_at(3).unwrap().encrypt(b""),
+ session.get_cipher_at(1000).unwrap().encrypt(b"")
+ );
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +
// Copyright 2021 The Matrix.org Foundation C.I.C.
+// Copyright 2022 Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::fmt::Debug;
+
+use prost::Message;
+use serde::{Deserialize, Serialize};
+
+use crate::{
+ cipher::{Cipher, Mac, MessageMac},
+ types::{Ed25519Keypair, Ed25519Signature},
+ utilities::{base64_decode, base64_encode, extract_mac, VarInt},
+ DecodeError,
+};
+#[cfg(feature = "low-level-api")]
+use crate::{Ed25519PublicKey, SignatureError};
+
+const MAC_TRUNCATED_VERSION: u8 = 3;
+const VERSION: u8 = 4;
+
+/// An encrypted Megolm message.
+///
+/// Contains metadata that is required to find the correct ratchet state of a
+/// [`InboundGroupSession`] necessary to decryp the message.
+///
+/// [`InboundGroupSession`]: crate::megolm::InboundGroupSession
+#[derive(Clone, PartialEq, Eq)]
+pub struct MegolmMessage {
+ pub(super) version: u8,
+ pub(super) ciphertext: Vec<u8>,
+ pub(super) message_index: u32,
+ pub(super) mac: MessageMac,
+ pub(super) signature: Ed25519Signature,
+}
+
+impl MegolmMessage {
+ const MESSAGE_TRUNCATED_SUFFIX_LENGTH: usize = Mac::TRUNCATED_LEN + Ed25519Signature::LENGTH;
+ const MESSAGE_SUFFIX_LENGTH: usize = Mac::LENGTH + Ed25519Signature::LENGTH;
+
+ /// The actual ciphertext of the message.
+ pub fn ciphertext(&self) -> &[u8] {
+ &self.ciphertext
+ }
+
+ /// The index of the message that was used when the message was encrypted.
+ pub fn message_index(&self) -> u32 {
+ self.message_index
+ }
+
+ /// Get the megolm message's mac.
+ pub fn mac(&self) -> &[u8] {
+ self.mac.as_bytes()
+ }
+
+ /// Get a reference to the megolm message's signature.
+ pub fn signature(&self) -> &Ed25519Signature {
+ &self.signature
+ }
+
+ /// Try to decode the given byte slice as a [`MegolmMessage`].
+ ///
+ /// The expected format of the byte array is described in the
+ /// [`MegolmMessage::to_bytes()`] method.
+ pub fn from_bytes(message: &[u8]) -> Result<Self, DecodeError> {
+ Self::try_from(message)
+ }
+
+ /// Encode the [`MegolmMessage`] as an array of bytes.
+ ///
+ /// Megolm messages consist of a one byte version, followed by a variable
+ /// length payload, a fixed length message authentication code, and a fixed
+ /// length signature.
+ ///
+ /// ```text
+ /// +---+------------------------------------+-----------+------------------+
+ /// | V | Payload Bytes | MAC Bytes | Signature Bytes |
+ /// +---+------------------------------------+-----------+------------------+
+ /// 0 1 N N+8 N+72 bytes
+ /// ```
+ ///
+ /// The payload uses a format based on the Protocol Buffers encoding. It
+ /// consists of the following key-value pairs:
+ ///
+ /// **Name** |**Tag**|**Type**| **Meaning**
+ /// :-----------:|:-----:|:------:|:---------------------------------------:
+ /// Message-Index| 0x08 | Integer|The index of the ratchet, i
+ /// Cipher-Text | 0x12 | String |The cipher-text, Xi, of the message
+ pub fn to_bytes(&self) -> Vec<u8> {
+ let mut message = self.encode_message();
+
+ message.extend(self.mac.as_bytes());
+ message.extend(self.signature.to_bytes());
+
+ message
+ }
+
+ /// Try to decode the given string as a [`MegolmMessage`].
+ ///
+ /// The string needs to be a base64 encoded byte array that follows the
+ /// format described in the [`MegolmMessage::to_bytes()`] method.
+ pub fn from_base64(message: &str) -> Result<Self, DecodeError> {
+ Self::try_from(message)
+ }
+
+ /// Encode the [`MegolmMessage`] as a string.
+ ///
+ /// This method first calls [`MegolmMessage::to_bytes()`] and then encodes
+ /// the resulting byte array as a string using base64 encoding.
+ pub fn to_base64(&self) -> String {
+ base64_encode(self.to_bytes())
+ }
+
+ /// Set the signature of the message, verifying that the signature matches
+ /// the signing key.
+ #[cfg(feature = "low-level-api")]
+ pub fn add_signature(
+ &mut self,
+ signature: Ed25519Signature,
+ signing_key: Ed25519PublicKey,
+ ) -> Result<(), SignatureError> {
+ signing_key.verify(&self.to_signature_bytes(), &signature)?;
+
+ self.signature = signature;
+
+ Ok(())
+ }
+
+ fn encode_message(&self) -> Vec<u8> {
+ let message = ProtobufMegolmMessage {
+ message_index: self.message_index,
+ ciphertext: self.ciphertext.clone(),
+ };
+
+ message.encode_manual(self.version)
+ }
+
+ fn set_mac(&mut self, mac: Mac) {
+ match self.mac {
+ MessageMac::Truncated(_) => self.mac = mac.truncate().into(),
+ MessageMac::Full(_) => self.mac = mac.into(),
+ }
+ }
+
+ /// Create a new [`MegolmMessage`] with the given plaintext and keys.
+ #[cfg(feature = "low-level-api")]
+ pub fn encrypt(
+ message_index: u32,
+ cipher: &Cipher,
+ signing_key: &Ed25519Keypair,
+ plaintext: &[u8],
+ ) -> Self {
+ MegolmMessage::encrypt_truncated_mac(message_index, cipher, signing_key, plaintext)
+ }
+
+ /// Implementation of [`MegolmMessage::encrypt`] that is used by rest of the
+ /// crate.
+ pub(super) fn encrypt_full_mac(
+ message_index: u32,
+ cipher: &Cipher,
+ signing_key: &Ed25519Keypair,
+ plaintext: &[u8],
+ ) -> Self {
+ let ciphertext = cipher.encrypt(plaintext);
+
+ let message = Self {
+ version: VERSION,
+ ciphertext,
+ message_index,
+ mac: Mac([0u8; Mac::LENGTH]).into(),
+ signature: Ed25519Signature::from_slice(&[0; Ed25519Signature::LENGTH])
+ .expect("Can't create an empty signature"),
+ };
+
+ Self::encrypt_helper(cipher, signing_key, message)
+ }
+
+ pub(super) fn encrypt_truncated_mac(
+ message_index: u32,
+ cipher: &Cipher,
+ signing_key: &Ed25519Keypair,
+ plaintext: &[u8],
+ ) -> Self {
+ let ciphertext = cipher.encrypt(plaintext);
+
+ let message = Self {
+ version: MAC_TRUNCATED_VERSION,
+ ciphertext,
+ message_index,
+ mac: [0u8; Mac::TRUNCATED_LEN].into(),
+ signature: Ed25519Signature::from_slice(&[0; Ed25519Signature::LENGTH])
+ .expect("Can't create an empty signature"),
+ };
+
+ Self::encrypt_helper(cipher, signing_key, message)
+ }
+
+ fn encrypt_helper(
+ cipher: &Cipher,
+ signing_key: &Ed25519Keypair,
+ mut message: MegolmMessage,
+ ) -> Self {
+ let mac = cipher.mac(&message.to_mac_bytes());
+ message.set_mac(mac);
+
+ let signature = signing_key.sign(&message.to_signature_bytes());
+ message.signature = signature;
+
+ message
+ }
+
+ pub(super) fn to_mac_bytes(&self) -> Vec<u8> {
+ self.encode_message()
+ }
+
+ pub(super) fn to_signature_bytes(&self) -> Vec<u8> {
+ let mut message = self.encode_message();
+ message.extend(self.mac.as_bytes());
+
+ message
+ }
+}
+
+impl Serialize for MegolmMessage {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let message = self.to_base64();
+ serializer.serialize_str(&message)
+ }
+}
+
+impl<'de> Deserialize<'de> for MegolmMessage {
+ fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
+ let ciphertext = String::deserialize(d)?;
+ Self::from_base64(&ciphertext).map_err(serde::de::Error::custom)
+ }
+}
+
+impl TryFrom<&str> for MegolmMessage {
+ type Error = DecodeError;
+
+ fn try_from(message: &str) -> Result<Self, Self::Error> {
+ let decoded = base64_decode(message)?;
+
+ Self::try_from(decoded)
+ }
+}
+
+impl TryFrom<Vec<u8>> for MegolmMessage {
+ type Error = DecodeError;
+
+ fn try_from(message: Vec<u8>) -> Result<Self, Self::Error> {
+ Self::try_from(message.as_slice())
+ }
+}
+
+impl TryFrom<&[u8]> for MegolmMessage {
+ type Error = DecodeError;
+
+ fn try_from(message: &[u8]) -> Result<Self, Self::Error> {
+ let version = *message.first().ok_or(DecodeError::MissingVersion)?;
+
+ let suffix_length = match version {
+ VERSION => Self::MESSAGE_SUFFIX_LENGTH,
+ MAC_TRUNCATED_VERSION => Self::MESSAGE_TRUNCATED_SUFFIX_LENGTH,
+ _ => return Err(DecodeError::InvalidVersion(VERSION, version)),
+ };
+
+ if message.len() < suffix_length + 2 {
+ Err(DecodeError::MessageTooShort(message.len()))
+ } else {
+ let inner = ProtobufMegolmMessage::decode(
+ message
+ .get(1..message.len() - suffix_length)
+ .ok_or_else(|| DecodeError::MessageTooShort(message.len()))?,
+ )?;
+
+ let signature_location = message.len() - Ed25519Signature::LENGTH;
+ let signature_slice = &message[signature_location..];
+ let signature = Ed25519Signature::from_slice(signature_slice)?;
+
+ let mac_slice = &message[message.len() - suffix_length..];
+ let mac = extract_mac(mac_slice, version == MAC_TRUNCATED_VERSION);
+
+ Ok(MegolmMessage {
+ version,
+ ciphertext: inner.ciphertext,
+ message_index: inner.message_index,
+ mac,
+ signature,
+ })
+ }
+ }
+}
+
+impl Debug for MegolmMessage {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self { version, ciphertext: _, message_index, mac: _, signature: _ } = self;
+
+ f.debug_struct("MegolmMessage")
+ .field("version", version)
+ .field("message_index", message_index)
+ .finish_non_exhaustive()
+ }
+}
+
+#[derive(Clone, Message, PartialEq, Eq)]
+struct ProtobufMegolmMessage {
+ #[prost(uint32, tag = "1")]
+ pub message_index: u32,
+ #[prost(bytes, tag = "2")]
+ pub ciphertext: Vec<u8>,
+}
+
+impl ProtobufMegolmMessage {
+ const INDEX_TAG: &'static [u8; 1] = b"\x08";
+ const CIPHER_TAG: &'static [u8; 1] = b"\x12";
+
+ fn encode_manual(&self, version: u8) -> Vec<u8> {
+ // Prost optimizes away the message index if it's 0, libolm can't decode
+ // this, so encode our messages the pedestrian way instead.
+ let index = self.message_index.to_var_int();
+ let ciphertext_len = self.ciphertext.len().to_var_int();
+
+ [
+ [version].as_ref(),
+ Self::INDEX_TAG.as_ref(),
+ &index,
+ Self::CIPHER_TAG.as_ref(),
+ &ciphertext_len,
+ &self.ciphertext,
+ ]
+ .concat()
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +
// Copyright 2021 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! An implementation of the Megolm ratchet.
+
+mod group_session;
+mod inbound_group_session;
+pub(crate) mod message;
+mod ratchet;
+mod session_config;
+mod session_keys;
+
+pub use group_session::{GroupSession, GroupSessionPickle};
+pub use inbound_group_session::{
+ DecryptedMessage, DecryptionError, InboundGroupSession, InboundGroupSessionPickle,
+ SessionOrdering,
+};
+pub use message::MegolmMessage;
+pub use session_config::SessionConfig;
+pub use session_keys::{ExportedSessionKey, SessionKey, SessionKeyDecodeError};
+
+fn default_config() -> SessionConfig {
+ SessionConfig::version_1()
+}
+
+#[cfg(feature = "libolm-compat")]
+mod libolm {
+ use matrix_pickle::Decode;
+ use zeroize::Zeroize;
+
+ use super::ratchet::Ratchet;
+
+ #[derive(Zeroize, Decode)]
+ #[zeroize(drop)]
+ pub(crate) struct LibolmRatchetPickle {
+ #[secret]
+ ratchet: Box<[u8; 128]>,
+ index: u32,
+ }
+
+ impl From<&LibolmRatchetPickle> for Ratchet {
+ fn from(pickle: &LibolmRatchetPickle) -> Self {
+ Ratchet::from_bytes(pickle.ratchet.clone(), pickle.index)
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use anyhow::Result;
+ use olm_rs::{
+ inbound_group_session::OlmInboundGroupSession,
+ outbound_group_session::OlmOutboundGroupSession,
+ };
+
+ use super::{ExportedSessionKey, GroupSession, InboundGroupSession, MegolmMessage};
+ use crate::{
+ megolm::{GroupSessionPickle, InboundGroupSessionPickle, SessionConfig, SessionKey},
+ run_corpus,
+ };
+
+ const PICKLE_KEY: [u8; 32] = [0u8; 32];
+
+ #[test]
+ fn encrypting() -> Result<()> {
+ let mut session = GroupSession::new(SessionConfig::version_1());
+ let session_key = session.session_key();
+
+ let olm_session = OlmInboundGroupSession::new(&session_key.to_base64())?;
+
+ let plaintext = "It's a secret to everybody";
+ let message = session.encrypt(plaintext).to_base64();
+
+ let (decrypted, _) = olm_session.decrypt(message)?;
+
+ assert_eq!(decrypted, plaintext);
+
+ let plaintext = "Another secret";
+ let message = session.encrypt(plaintext).to_base64();
+
+ let (decrypted, _) = olm_session.decrypt(message)?;
+ assert_eq!(decrypted, plaintext);
+
+ let plaintext = "And another secret";
+ let message = session.encrypt(plaintext).to_base64();
+ let (decrypted, _) = olm_session.decrypt(message)?;
+
+ assert_eq!(decrypted, plaintext);
+
+ let plaintext = "Last secret";
+
+ for _ in 1..2000 {
+ session.encrypt(plaintext);
+ }
+
+ let message = session.encrypt(plaintext).to_base64();
+ let (decrypted, _) = olm_session.decrypt(message)?;
+
+ assert_eq!(decrypted, plaintext);
+
+ Ok(())
+ }
+
+ #[test]
+ fn decrypting() -> Result<()> {
+ let olm_session = OlmOutboundGroupSession::new();
+
+ let session_key = SessionKey::from_base64(&olm_session.session_key())?;
+
+ let mut session = InboundGroupSession::new(&session_key, SessionConfig::version_1());
+
+ let plaintext = "Hello";
+ let message = olm_session.encrypt(plaintext).as_str().try_into()?;
+
+ let decrypted = session.decrypt(&message)?;
+
+ assert_eq!(decrypted.plaintext, plaintext.as_bytes());
+ assert_eq!(decrypted.message_index, 0);
+
+ let plaintext = "Another secret";
+ let message = olm_session.encrypt(plaintext).as_str().try_into()?;
+
+ let decrypted = session.decrypt(&message)?;
+
+ assert_eq!(decrypted.plaintext, plaintext.as_bytes());
+ assert_eq!(decrypted.message_index, 1);
+
+ let third_plaintext = "And another secret";
+ let third_message = olm_session.encrypt(third_plaintext).as_str().try_into()?;
+ let decrypted = session.decrypt(&third_message)?;
+
+ assert_eq!(decrypted.plaintext, third_plaintext.as_bytes());
+ assert_eq!(decrypted.message_index, 2);
+
+ let plaintext = "Last secret";
+
+ for _ in 1..2000 {
+ olm_session.encrypt(plaintext);
+ }
+
+ let message = olm_session.encrypt(plaintext).as_str().try_into()?;
+ let decrypted = session.decrypt(&message)?;
+
+ assert_eq!(decrypted.plaintext, plaintext.as_bytes());
+ assert_eq!(decrypted.message_index, 2002);
+
+ let decrypted = session.decrypt(&third_message)?;
+
+ assert_eq!(decrypted.plaintext, third_plaintext.as_bytes());
+ assert_eq!(decrypted.message_index, 2);
+
+ Ok(())
+ }
+
+ #[test]
+ fn exporting() -> Result<()> {
+ let mut session = GroupSession::new(Default::default());
+ let mut inbound =
+ InboundGroupSession::new(&session.session_key(), session.session_config());
+
+ assert_eq!(session.session_id(), inbound.session_id());
+
+ let first_plaintext = "It's a secret to everybody".as_bytes();
+ let first_message = session.encrypt(first_plaintext);
+ let second_plaintext = "It's dangerous to go alone. Take this!".as_bytes();
+ let second_message = session.encrypt(second_plaintext);
+
+ let decrypted = inbound.decrypt(&first_message)?;
+
+ assert_eq!(decrypted.plaintext, first_plaintext);
+ assert_eq!(decrypted.message_index, 0);
+
+ let export = inbound.export_at(1).expect("Can export at the initial index.");
+ let mut imported = InboundGroupSession::import(&export, session.session_config());
+
+ assert_eq!(session.session_id(), imported.session_id());
+
+ imported.decrypt(&first_message).expect_err("Can't decrypt at the initial index.");
+ let second_decrypted =
+ imported.decrypt(&second_message).expect("Can decrypt at the next index.");
+ assert_eq!(
+ second_plaintext, second_decrypted.plaintext,
+ "Decrypted plaintext differs from original."
+ );
+ assert_eq!(1, second_decrypted.message_index, "Expected message index to be 1.");
+
+ assert!(imported.export_at(0).is_none(), "Can't export at the initial index.");
+ assert!(imported.export_at(1).is_some(), "Can export at the next index.");
+
+ Ok(())
+ }
+
+ #[test]
+ fn group_session_pickling_roundtrip_is_identity() -> Result<()> {
+ let session = GroupSession::new(Default::default());
+
+ let pickle = session.pickle().encrypt(&PICKLE_KEY);
+
+ let decrypted_pickle = GroupSessionPickle::from_encrypted(&pickle, &PICKLE_KEY)?;
+ let unpickled_group_session = GroupSession::from_pickle(decrypted_pickle);
+ let repickle = unpickled_group_session.pickle();
+
+ assert_eq!(session.session_id(), unpickled_group_session.session_id());
+
+ let decrypted_pickle = GroupSessionPickle::from_encrypted(&pickle, &PICKLE_KEY)?;
+ let pickle = serde_json::to_value(decrypted_pickle)?;
+ let repickle = serde_json::to_value(repickle)?;
+
+ assert_eq!(pickle, repickle);
+
+ Ok(())
+ }
+
+ #[test]
+ fn inbound_group_session_pickling_roundtrip_is_identity() -> Result<()> {
+ let session = GroupSession::new(Default::default());
+ let session = InboundGroupSession::from(&session);
+
+ let pickle = session.pickle().encrypt(&PICKLE_KEY);
+
+ let decrypted_pickle = InboundGroupSessionPickle::from_encrypted(&pickle, &PICKLE_KEY)?;
+ let unpickled_group_session = InboundGroupSession::from_pickle(decrypted_pickle);
+ let repickle = unpickled_group_session.pickle();
+
+ assert_eq!(session.session_id(), unpickled_group_session.session_id());
+
+ let decrypted_pickle = InboundGroupSessionPickle::from_encrypted(&pickle, &PICKLE_KEY)?;
+ let pickle = serde_json::to_value(decrypted_pickle)?;
+ let repickle = serde_json::to_value(repickle)?;
+
+ assert_eq!(pickle, repickle);
+
+ Ok(())
+ }
+
+ #[test]
+ #[cfg(feature = "libolm-compat")]
+ fn libolm_inbound_unpickling() -> Result<()> {
+ let session = GroupSession::new(SessionConfig::version_1());
+ let session_key = session.session_key();
+
+ let olm = OlmInboundGroupSession::new(&session_key.to_base64())?;
+
+ let key = b"DEFAULT_PICKLE_KEY";
+ let pickle = olm.pickle(olm_rs::PicklingMode::Encrypted { key: key.to_vec() });
+
+ let unpickled = InboundGroupSession::from_libolm_pickle(&pickle, key)?;
+
+ assert_eq!(olm.session_id(), unpickled.session_id());
+ assert_eq!(olm.first_known_index(), unpickled.first_known_index());
+
+ Ok(())
+ }
+
+ #[test]
+ #[cfg(feature = "libolm-compat")]
+ fn libolm_unpickling() -> Result<()> {
+ let olm = OlmOutboundGroupSession::new();
+ let session_key = SessionKey::from_base64(&olm.session_key())?;
+ let mut inbound_session =
+ InboundGroupSession::new(&session_key, SessionConfig::version_1());
+
+ let key = b"DEFAULT_PICKLE_KEY";
+ let pickle = olm.pickle(olm_rs::PicklingMode::Encrypted { key: key.to_vec() });
+
+ let mut unpickled = GroupSession::from_libolm_pickle(&pickle, key)?;
+
+ assert_eq!(olm.session_id(), unpickled.session_id());
+ assert_eq!(olm.session_message_index(), unpickled.message_index());
+
+ let plaintext = "It's a secret to everybody".as_bytes();
+ let message = unpickled.encrypt(plaintext);
+
+ let decrypted = inbound_session
+ .decrypt(&message)
+ .expect("We should be able to decrypt a message with a migrated Megolm session");
+
+ assert_eq!(decrypted.plaintext, plaintext);
+ assert_eq!(decrypted.message_index, 0);
+
+ Ok(())
+ }
+
+ #[test]
+ fn fuzz_corpus_decoding() {
+ run_corpus("megolm-decoding", |data| {
+ let _ = MegolmMessage::from_bytes(data);
+ });
+ }
+
+ #[test]
+ fn fuzz_corpus_session_creation() {
+ run_corpus("megolm-session-creation", |data| {
+ if let Ok(session_key) = SessionKey::from_bytes(data) {
+ let _ = InboundGroupSession::new(&session_key, Default::default());
+ }
+ });
+ }
+
+ #[test]
+ fn fuzz_corpus_session_import() {
+ run_corpus("megolm-session-import", |data| {
+ if let Ok(session_key) = ExportedSessionKey::from_bytes(data) {
+ let _ = InboundGroupSession::import(&session_key, Default::default());
+ }
+ });
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +
// Copyright 2016 OpenMarket Ltd
+// Copyright 2021 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use hmac::{Hmac, Mac as _};
+use rand::{thread_rng, RngCore};
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use sha2::{digest::CtOutput, Sha256};
+use subtle::{Choice, ConstantTimeEq};
+use thiserror::Error;
+use zeroize::Zeroize;
+
+const ADVANCEMENT_SEEDS: [&[u8; 1]; Ratchet::RATCHET_PART_COUNT] =
+ [b"\x00", b"\x01", b"\x02", b"\x03"];
+
+#[derive(Serialize, Deserialize, Zeroize, Clone)]
+#[zeroize(drop)]
+pub(super) struct Ratchet {
+ inner: RatchetBytes,
+ counter: u32,
+}
+
+impl ConstantTimeEq for Ratchet {
+ fn ct_eq(&self, other: &Self) -> Choice {
+ // Short circuit on the counter, not on the contents of the ratchet.
+ if self.counter != other.counter {
+ Choice::from(0)
+ } else {
+ self.as_bytes().ct_eq(other.as_bytes())
+ }
+ }
+}
+
+#[derive(Zeroize, Clone)]
+#[zeroize(drop)]
+struct RatchetBytes(Box<[u8; Ratchet::RATCHET_LENGTH]>);
+
+impl RatchetBytes {
+ fn from_bytes(bytes: &[u8]) -> Result<Self, RatchetBytesError> {
+ let length = bytes.len();
+
+ if length != Ratchet::RATCHET_LENGTH {
+ Err(RatchetBytesError::InvalidLength(length))
+ } else {
+ let mut ratchet = Self(Box::new([0u8; Ratchet::RATCHET_LENGTH]));
+ ratchet.0.copy_from_slice(bytes);
+
+ Ok(ratchet)
+ }
+ }
+}
+
+impl Serialize for RatchetBytes {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let bytes = &self.0;
+ bytes.serialize(serializer)
+ }
+}
+
+impl<'d> Deserialize<'d> for RatchetBytes {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'d>,
+ {
+ let mut bytes = <Vec<u8>>::deserialize(deserializer)?;
+ let ratchet = Self::from_bytes(bytes.as_ref()).map_err(serde::de::Error::custom)?;
+
+ bytes.zeroize();
+
+ Ok(ratchet)
+ }
+}
+
+struct RatchetPart<'a>(&'a mut [u8]);
+
+impl<'a> RatchetPart<'a> {
+ fn hash(&self, seed: &[u8]) -> CtOutput<Hmac<Sha256>> {
+ let mut hmac = Hmac::<Sha256>::new_from_slice(self.0).expect("Can't create a HMAC object");
+ hmac.update(seed);
+
+ hmac.finalize()
+ }
+
+ fn update(&mut self, new_part: &[u8]) {
+ self.0.copy_from_slice(new_part);
+ }
+}
+
+struct RatchetParts<'a> {
+ r_0: RatchetPart<'a>,
+ r_1: RatchetPart<'a>,
+ r_2: RatchetPart<'a>,
+ r_3: RatchetPart<'a>,
+}
+
+impl<'a> RatchetParts<'a> {
+ fn update(&'a mut self, from: usize, to: usize) {
+ let from = match from {
+ 0 => &self.r_0,
+ 1 => &self.r_1,
+ 2 => &self.r_2,
+ 3 => &self.r_3,
+ _ => unreachable!(),
+ };
+
+ let result = from.hash(ADVANCEMENT_SEEDS[to]);
+
+ let to = match to {
+ 0 => &mut self.r_0,
+ 1 => &mut self.r_1,
+ 2 => &mut self.r_2,
+ 3 => &mut self.r_3,
+ _ => unreachable!(),
+ };
+
+ to.update(&result.into_bytes());
+ }
+}
+
+impl Ratchet {
+ pub const RATCHET_LENGTH: usize = 128;
+ const RATCHET_PART_COUNT: usize = 4;
+ const LAST_RATCHET_INDEX: usize = Self::RATCHET_PART_COUNT - 1;
+
+ pub fn new() -> Self {
+ let mut rng = thread_rng();
+
+ let mut ratchet =
+ Self { inner: RatchetBytes(Box::new([0u8; Self::RATCHET_LENGTH])), counter: 0 };
+
+ rng.fill_bytes(ratchet.inner.0.as_mut());
+
+ ratchet
+ }
+
+ pub fn from_bytes(bytes: Box<[u8; Self::RATCHET_LENGTH]>, counter: u32) -> Self {
+ Self { inner: RatchetBytes(bytes), counter }
+ }
+
+ pub fn index(&self) -> u32 {
+ self.counter
+ }
+
+ pub fn as_bytes(&self) -> &[u8; Self::RATCHET_LENGTH] {
+ &self.inner.0
+ }
+
+ fn as_parts(&mut self) -> RatchetParts<'_> {
+ let (top, bottom) = self.inner.0.split_at_mut(64);
+
+ let (r_0, r_1) = top.split_at_mut(32);
+ let (r_2, r_3) = bottom.split_at_mut(32);
+
+ let r_0 = RatchetPart(r_0);
+ let r_1 = RatchetPart(r_1);
+ let r_2 = RatchetPart(r_2);
+ let r_3 = RatchetPart(r_3);
+
+ RatchetParts { r_0, r_1, r_2, r_3 }
+ }
+
+ pub fn advance(&mut self) {
+ let mut mask: u32 = 0x00FFFFFF;
+
+ // The index of the "slowest" part of the ratchet that needs to be
+ // advanced.
+ let mut h = 0;
+
+ self.counter += 1;
+
+ // Figure out which parts of the ratchet need to be advanced.
+ while h < Self::RATCHET_PART_COUNT {
+ if (self.counter & mask) == 0 {
+ break;
+ }
+
+ h += 1;
+ mask >>= 8;
+ }
+
+ let parts_to_advance = (h..=Self::LAST_RATCHET_INDEX).rev();
+
+ // Now advance R(h)...R(3) based on R(h).
+ for i in parts_to_advance {
+ let mut parts = self.as_parts();
+ parts.update(h, i);
+ }
+ }
+
+ pub fn advance_to(&mut self, advance_to: u32) {
+ for j in 0..Self::RATCHET_PART_COUNT {
+ let shift = (Self::LAST_RATCHET_INDEX - j) * 8;
+ let mask: u32 = !0u32 << shift;
+
+ // How many times do we need to rehash this part? `& 0xff` ensures
+ // we handle integer wrap-around correctly.
+ let mut steps = ((advance_to >> shift) - (self.counter >> shift)) & 0xff;
+
+ if steps == 0 {
+ // Deal with the edge case where the ratchet counter is slightly
+ // larger than the index we need to advance to. This should only
+ // happen for R(0) and implies that advance_to has wrapped
+ // around and we need to advance R(0) 256 times.
+ if advance_to < self.counter {
+ steps = 0x100;
+ } else {
+ continue;
+ }
+ }
+
+ // For all but the last step, we can just bump R(j) without regard
+ // to R(j+1)...R(3).
+ while steps > 1 {
+ let mut parts = self.as_parts();
+ parts.update(j, j);
+ steps -= 1;
+ }
+
+ // On the last step we also need to bump R(j+1)...R(3).
+ // (Theoretically, we could skip bumping R(j+2) if we're going to
+ // bump R(j+1) again, but the code to figure that out is a bit
+ // baroque and doesn't save us much).
+
+ let parts_to_update = (j..=Self::LAST_RATCHET_INDEX).rev();
+
+ for k in parts_to_update {
+ let mut parts = self.as_parts();
+ parts.update(j, k);
+ }
+
+ self.counter = advance_to & mask;
+ }
+ }
+}
+
+#[derive(Error, Debug)]
+enum RatchetBytesError {
+ #[error("Invalid Megolm ratchet length: expected 128, got {0}")]
+ InvalidLength(usize),
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn advancing_high_counter_ratchet_doesnt_panic() {
+ let mut ratchet = Ratchet::new();
+ ratchet.counter = 0x00FFFFFF;
+ ratchet.advance();
+ }
+
+ #[test]
+ fn advance_to_with_high_counter_doesnt_panic() {
+ let mut ratchet = Ratchet::new();
+ ratchet.counter = (1 << 24) - 1;
+ ratchet.advance_to(1 << 24);
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +
// Copyright 2022 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use serde::{Deserialize, Serialize};
+
+/// A struct to configure how Megolm sessions should work under the hood.
+/// Currently only the MAC truncation behaviour can be configured.
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub struct SessionConfig {
+ pub(super) version: Version,
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub(super) enum Version {
+ V1 = 1,
+ V2 = 2,
+}
+
+impl SessionConfig {
+ /// Get the numeric version of this `SessionConfig`.
+ pub fn version(&self) -> u8 {
+ self.version as u8
+ }
+
+ /// Create a `SessionConfig` for the Megolm version 1. This version of
+ /// Megolm uses AES-256 and HMAC with a truncated MAC to encrypt individual
+ /// messages. The MAC will be truncated to 8 bytes.
+ pub fn version_1() -> Self {
+ SessionConfig { version: Version::V1 }
+ }
+
+ /// Create a `SessionConfig` for the Megolm version 2. This version of
+ /// Megolm uses AES-256 and HMAC to encrypt individual messages. The MAC
+ /// won't be truncated.
+ pub fn version_2() -> Self {
+ SessionConfig { version: Version::V2 }
+ }
+}
+
+impl Default for SessionConfig {
+ fn default() -> Self {
+ Self::version_2()
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +
// Copyright 2022 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::io::{Cursor, Read};
+
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+use zeroize::Zeroize;
+
+use super::ratchet::Ratchet;
+use crate::{
+ utilities::{base64_decode, base64_encode},
+ Ed25519PublicKey, Ed25519Signature, SignatureError,
+};
+
+/// Error type describing failure modes for the `SessionKey` and
+/// `ExportedSessionKey` decoding.
+#[derive(Debug, Error)]
+pub enum SessionKeyDecodeError {
+ /// The encoded session key had a unsupported version.
+ #[error("The session key had a invalid version, expected {0}, got {1}")]
+ Version(u8, u8),
+ /// The encoded session key didn't contain enough data to be decoded.
+ #[error("The session key was too short {0}")]
+ Read(#[from] std::io::Error),
+ /// The encoded session key wasn't valid base64.
+ #[error("The session key wasn't valid base64: {0}")]
+ Base64(#[from] base64::DecodeError),
+ /// The signature on the session key was invalid.
+ #[error("The signature on the session key was invalid: {0}")]
+ Signature(#[from] SignatureError),
+ /// The encoded session key contains an invalid public key.
+ #[error("The public key of session was invalid: {0}")]
+ PublicKey(#[from] crate::KeyError),
+}
+
+/// The exported session key.
+///
+/// This uses the same format as the `SessionKey` minus the signature at the
+/// end.
+pub struct ExportedSessionKey {
+ pub(crate) ratchet_index: u32,
+ pub(crate) ratchet: Box<[u8; 128]>,
+ pub(crate) signing_key: Ed25519PublicKey,
+}
+
+impl ExportedSessionKey {
+ const VERSION: u8 = 1;
+
+ pub(super) fn new(ratchet: &Ratchet, signing_key: Ed25519PublicKey) -> Self {
+ let ratchet_index = ratchet.index();
+ let mut ratchet_bytes = Box::new([0u8; Ratchet::RATCHET_LENGTH]);
+
+ ratchet_bytes.copy_from_slice(ratchet.as_bytes());
+
+ Self { ratchet_index, ratchet: ratchet_bytes, signing_key }
+ }
+
+ fn to_bytes_with_version(&self, version: u8) -> Vec<u8> {
+ let index = self.ratchet_index.to_be_bytes();
+
+ [[version].as_ref(), index.as_ref(), self.ratchet.as_ref(), self.signing_key.as_bytes()]
+ .concat()
+ }
+
+ /// Serialize the `ExportedSessionKey` to a byte vector.
+ pub fn to_bytes(&self) -> Vec<u8> {
+ self.to_bytes_with_version(Self::VERSION)
+ }
+
+ /// Deserialize the `ExportedSessionKey` from a byte slice.
+ pub fn from_bytes(bytes: &[u8]) -> Result<Self, SessionKeyDecodeError> {
+ let mut cursor = Cursor::new(bytes);
+ Self::decode_key(Self::VERSION, &mut cursor)
+ }
+
+ /// Serialize the `ExportedSessionKey` to a base64 encoded string.
+ ///
+ /// This method will first use the [`ExportedSessionKey::to_bytes()`] to
+ /// convert the session key to a byte vector and then encode the byte vector
+ /// to a string using unpadded base64 as the encoding.
+ pub fn to_base64(&self) -> String {
+ let mut bytes = self.to_bytes();
+
+ let ret = base64_encode(&bytes);
+
+ bytes.zeroize();
+
+ ret
+ }
+
+ /// Deserialize the `ExportedSessionKey` from base64 encoded string.
+ pub fn from_base64(key: &str) -> Result<Self, SessionKeyDecodeError> {
+ let mut bytes = base64_decode(key)?;
+ let ret = Self::from_bytes(&bytes);
+
+ bytes.zeroize();
+
+ ret
+ }
+
+ fn decode_key(
+ expected_version: u8,
+ cursor: &mut Cursor<&[u8]>,
+ ) -> Result<ExportedSessionKey, SessionKeyDecodeError> {
+ let mut version = [0u8; 1];
+ let mut index = [0u8; 4];
+ let mut ratchet = Box::new([0u8; 128]);
+ let mut public_key = [0u8; Ed25519PublicKey::LENGTH];
+
+ cursor.read_exact(&mut version)?;
+
+ if version[0] != expected_version {
+ Err(SessionKeyDecodeError::Version(expected_version, version[0]))
+ } else {
+ cursor.read_exact(&mut index)?;
+ cursor.read_exact(ratchet.as_mut_slice())?;
+ cursor.read_exact(&mut public_key)?;
+
+ let signing_key = Ed25519PublicKey::from_slice(&public_key)?;
+ let ratchet_index = u32::from_be_bytes(index);
+
+ Ok(ExportedSessionKey { ratchet_index, ratchet, signing_key })
+ }
+ }
+}
+
+impl Zeroize for ExportedSessionKey {
+ fn zeroize(&mut self) {
+ self.ratchet_index.zeroize();
+ self.ratchet.zeroize();
+ }
+}
+
+impl Drop for ExportedSessionKey {
+ fn drop(&mut self) {
+ self.zeroize()
+ }
+}
+
+impl TryFrom<&[u8]> for ExportedSessionKey {
+ type Error = SessionKeyDecodeError;
+
+ fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
+ Self::from_bytes(value)
+ }
+}
+
+impl TryFrom<&str> for ExportedSessionKey {
+ type Error = SessionKeyDecodeError;
+
+ fn try_from(value: &str) -> Result<Self, Self::Error> {
+ Self::from_base64(value)
+ }
+}
+
+impl Serialize for ExportedSessionKey {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut encoded = self.to_base64();
+ let ret = encoded.serialize(serializer);
+
+ encoded.zeroize();
+
+ ret
+ }
+}
+
+impl<'de> Deserialize<'de> for ExportedSessionKey {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let mut session_key = String::deserialize(deserializer)?;
+ let ret = Self::from_base64(&session_key).map_err(serde::de::Error::custom);
+
+ session_key.zeroize();
+
+ ret
+ }
+}
+
+/// The session key, can be used to create a [`InboundGroupSession`].
+///
+/// Uses the session-sharing format described in the [Olm spec].
+///
+/// +---+----+--------+--------+--------+--------+------+-----------+
+/// | V | i | R(i,0) | R(i,1) | R(i,2) | R(i,3) | Kpub | Signature |
+/// +---+----+--------+--------+--------+--------+------+-----------+
+/// 0 1 5 37 69 101 133 165 229 bytes
+///
+/// The version byte, V, is "\x02".
+/// This is followed by the ratchet index, iii, which is encoded as a
+/// big-endian 32-bit integer; the 128 bytes of the ratchet; and the public
+/// part of the Ed25519 keypair.
+///
+/// The data is then signed using the Ed25519 key, and the 64-byte signature is
+/// appended.
+///
+/// [`InboundGroupSession`]: #crate.megolm.InboundGroupSession
+/// [Olm spec]: https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md#session-sharing-format
+pub struct SessionKey {
+ pub(super) session_key: ExportedSessionKey,
+ pub(super) signature: Ed25519Signature,
+}
+
+impl SessionKey {
+ const VERSION: u8 = 2;
+
+ pub(super) fn new(ratchet: &Ratchet, signing_key: Ed25519PublicKey) -> Self {
+ let session_key = ExportedSessionKey::new(ratchet, signing_key);
+
+ Self {
+ session_key,
+ signature: Ed25519Signature::from_slice(&[0; Ed25519Signature::LENGTH])
+ .expect("Can't create an empty signature"),
+ }
+ }
+
+ pub(crate) fn to_signature_bytes(&self) -> Vec<u8> {
+ self.session_key.to_bytes_with_version(Self::VERSION)
+ }
+
+ /// Serialize the `SessionKey` to a byte vector.
+ pub fn to_bytes(&self) -> Vec<u8> {
+ let mut bytes = self.to_signature_bytes();
+ bytes.extend(self.signature.to_bytes());
+
+ bytes
+ }
+
+ /// Deserialize the `SessionKey` from a byte slice.
+ pub fn from_bytes(bytes: &[u8]) -> Result<Self, SessionKeyDecodeError> {
+ let mut cursor = Cursor::new(bytes);
+ let session_key = ExportedSessionKey::decode_key(Self::VERSION, &mut cursor)?;
+
+ let mut signature = [0u8; Ed25519Signature::LENGTH];
+
+ cursor.read_exact(&mut signature)?;
+ let signature = Ed25519Signature::from_slice(&signature)?;
+
+ let decoded = cursor.into_inner();
+
+ session_key
+ .signing_key
+ .verify(&decoded[..decoded.len() - Ed25519Signature::LENGTH], &signature)?;
+
+ Ok(Self { session_key, signature })
+ }
+
+ /// Serialize the `SessionKey` to a base64 encoded string.
+ ///
+ /// This method will first use the [`SessionKey::to_bytes()`] to
+ /// convert the session key to a byte vector and then encode the byte vector
+ /// to a string using unpadded base64 as the encoding.
+ pub fn to_base64(&self) -> String {
+ let mut bytes = self.to_bytes();
+ let ret = base64_encode(&bytes);
+
+ bytes.zeroize();
+
+ ret
+ }
+
+ /// Deserialize the `SessionKey` from base64 encoded string.
+ pub fn from_base64(key: &str) -> Result<Self, SessionKeyDecodeError> {
+ let mut bytes = base64_decode(key)?;
+ let ret = Self::from_bytes(&bytes);
+
+ bytes.zeroize();
+
+ ret
+ }
+}
+
+impl Zeroize for SessionKey {
+ fn zeroize(&mut self) {
+ self.session_key.zeroize();
+ }
+}
+
+impl Drop for SessionKey {
+ fn drop(&mut self) {
+ self.zeroize()
+ }
+}
+
+impl TryFrom<&[u8]> for SessionKey {
+ type Error = SessionKeyDecodeError;
+
+ fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
+ Self::from_bytes(value)
+ }
+}
+
+impl TryFrom<&str> for SessionKey {
+ type Error = SessionKeyDecodeError;
+
+ fn try_from(value: &str) -> Result<Self, Self::Error> {
+ Self::from_base64(value)
+ }
+}
+
+impl Serialize for SessionKey {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut encoded = self.to_base64();
+ let ret = encoded.serialize(serializer);
+
+ encoded.zeroize();
+
+ ret
+ }
+}
+
+impl<'de> Deserialize<'de> for SessionKey {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let mut session_key = String::deserialize(deserializer)?;
+ let ret = Self::from_base64(&session_key).map_err(serde::de::Error::custom);
+
+ session_key.zeroize();
+
+ ret
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::megolm::{ExportedSessionKey, GroupSession, InboundGroupSession, SessionKey};
+
+ #[test]
+ fn session_key_serialization() -> Result<(), anyhow::Error> {
+ let session = GroupSession::new(Default::default());
+
+ let key = session.session_key();
+
+ let serialized = serde_json::to_string(&key)?;
+ let deserialized: SessionKey = serde_json::from_str(&serialized)?;
+
+ assert_eq!(key.session_key.ratchet, deserialized.session_key.ratchet);
+ assert_eq!(key.session_key.ratchet_index, deserialized.session_key.ratchet_index);
+ assert_eq!(key.session_key.signing_key, deserialized.session_key.signing_key);
+ assert_eq!(key.signature, deserialized.signature);
+
+ Ok(())
+ }
+
+ #[test]
+ fn exported_session_key_serialization() -> Result<(), anyhow::Error> {
+ let session = GroupSession::new(Default::default());
+ let mut session = InboundGroupSession::from(&session);
+
+ let key = session.export_at(0).expect(
+ "A freshly created inbound session can always be exported at the initial index",
+ );
+
+ let serialized = serde_json::to_string(&key)?;
+ let deserialized: ExportedSessionKey = serde_json::from_str(&serialized)?;
+
+ assert_eq!(key.ratchet, deserialized.ratchet);
+ assert_eq!(key.ratchet_index, deserialized.ratchet_index);
+ assert_eq!(key.signing_key, deserialized.signing_key);
+
+ Ok(())
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +
// Copyright 2021 Damir Jelić, Denis Kasak
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use serde::{Deserialize, Serialize};
+
+use crate::{
+ types::{Curve25519SecretKey, KeyId},
+ Curve25519PublicKey,
+};
+
+#[derive(Serialize, Deserialize, Clone)]
+pub(super) struct FallbackKey {
+ pub key_id: KeyId,
+ pub key: Curve25519SecretKey,
+ pub published: bool,
+}
+
+impl FallbackKey {
+ fn new(key_id: KeyId) -> Self {
+ let key = Curve25519SecretKey::new();
+
+ Self { key_id, key, published: false }
+ }
+
+ pub fn public_key(&self) -> Curve25519PublicKey {
+ Curve25519PublicKey::from(&self.key)
+ }
+
+ pub fn secret_key(&self) -> &Curve25519SecretKey {
+ &self.key
+ }
+
+ pub fn key_id(&self) -> KeyId {
+ self.key_id
+ }
+
+ pub fn mark_as_published(&mut self) {
+ self.published = true;
+ }
+
+ pub fn published(&self) -> bool {
+ self.published
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub(super) struct FallbackKeys {
+ pub key_id: u64,
+ pub fallback_key: Option<FallbackKey>,
+ pub previous_fallback_key: Option<FallbackKey>,
+}
+
+impl FallbackKeys {
+ pub fn new() -> Self {
+ Self { key_id: 0, fallback_key: None, previous_fallback_key: None }
+ }
+
+ pub fn mark_as_published(&mut self) {
+ if let Some(f) = self.fallback_key.as_mut() {
+ f.mark_as_published()
+ }
+ }
+
+ pub fn generate_fallback_key(&mut self) -> Option<Curve25519PublicKey> {
+ let key_id = KeyId(self.key_id);
+ self.key_id += 1;
+
+ let ret = self.previous_fallback_key.take().map(|f| f.public_key());
+
+ self.previous_fallback_key = self.fallback_key.take();
+ self.fallback_key = Some(FallbackKey::new(key_id));
+
+ ret
+ }
+
+ pub fn get_secret_key(&self, public_key: &Curve25519PublicKey) -> Option<&Curve25519SecretKey> {
+ self.fallback_key
+ .as_ref()
+ .filter(|f| f.public_key() == *public_key)
+ .or_else(|| {
+ self.previous_fallback_key.as_ref().filter(|f| f.public_key() == *public_key)
+ })
+ .map(|f| f.secret_key())
+ }
+
+ pub fn forget_previous_fallback_key(&mut self) -> Option<FallbackKey> {
+ self.previous_fallback_key.take()
+ }
+
+ pub fn unpublished_fallback_key(&self) -> Option<&FallbackKey> {
+ self.fallback_key.as_ref().filter(|f| !f.published())
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::FallbackKeys;
+
+ #[test]
+ fn fallback_key_fetching() {
+ let err = "Missing fallback key";
+ let mut fallback_keys = FallbackKeys::new();
+
+ fallback_keys.generate_fallback_key();
+
+ let public_key = fallback_keys.fallback_key.as_ref().expect(err).public_key();
+ let secret_bytes = fallback_keys.fallback_key.as_ref().expect(err).key.to_bytes();
+
+ let fetched_key = fallback_keys.get_secret_key(&public_key).expect(err);
+
+ assert_eq!(secret_bytes, fetched_key.to_bytes());
+
+ fallback_keys.generate_fallback_key();
+
+ let fetched_key = fallback_keys.get_secret_key(&public_key).expect(err);
+ assert_eq!(secret_bytes, fetched_key.to_bytes());
+
+ let public_key = fallback_keys.fallback_key.as_ref().expect(err).public_key();
+ let secret_bytes = fallback_keys.fallback_key.as_ref().expect(err).key.to_bytes();
+
+ let fetched_key = fallback_keys.get_secret_key(&public_key).expect(err);
+
+ assert_eq!(secret_bytes, fetched_key.to_bytes());
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +
// Copyright 2021 Damir Jelić, Denis Kasak
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+mod fallback_keys;
+mod one_time_keys;
+
+use std::collections::HashMap;
+
+use rand::thread_rng;
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+use x25519_dalek::ReusableSecret;
+
+pub use self::one_time_keys::OneTimeKeyGenerationResult;
+use self::{
+ fallback_keys::FallbackKeys,
+ one_time_keys::{OneTimeKeys, OneTimeKeysPickle},
+};
+use super::{
+ messages::PreKeyMessage,
+ session::{DecryptionError, Session},
+ session_keys::SessionKeys,
+ shared_secret::{RemoteShared3DHSecret, Shared3DHSecret},
+ SessionConfig,
+};
+use crate::{
+ types::{
+ Curve25519Keypair, Curve25519KeypairPickle, Curve25519PublicKey, Curve25519SecretKey,
+ Ed25519Keypair, Ed25519KeypairPickle, Ed25519PublicKey, KeyId,
+ },
+ utilities::{pickle, unpickle},
+ Ed25519Signature, PickleError,
+};
+
+const PUBLIC_MAX_ONE_TIME_KEYS: usize = 50;
+
+/// Error describing failure modes when creating a Olm Session from an incoming
+/// Olm message.
+#[derive(Error, Debug)]
+pub enum SessionCreationError {
+ /// The pre-key message contained an unknown one-time key. This happens
+ /// either because we never had such a one-time key, or because it has
+ /// already been used up.
+ #[error("The pre-key message contained an unknown one-time key: {0}")]
+ MissingOneTimeKey(Curve25519PublicKey),
+ /// The pre-key message contains a curve25519 identity key that doesn't
+ /// match to the identity key that was given.
+ #[error(
+ "The given identity key doesn't match the one in the pre-key message: \
+ expected {0}, got {1}"
+ )]
+ MismatchedIdentityKey(Curve25519PublicKey, Curve25519PublicKey),
+ /// The pre-key message that was used to establish the Session couldn't be
+ /// decrypted. The message needs to be decryptable, otherwise we will have
+ /// created a Session that wasn't used to encrypt the pre-key message.
+ #[error("The message that was used to establish the Session couldn't be decrypted")]
+ Decryption(#[from] DecryptionError),
+}
+
+/// Struct holding the two public identity keys of an [`Account`].
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub struct IdentityKeys {
+ /// The ed25519 key, used for signing.
+ pub ed25519: Ed25519PublicKey,
+ /// The curve25519 key, used for to establish shared secrets.
+ pub curve25519: Curve25519PublicKey,
+}
+
+/// Return type for the creation of inbound [`Session`] objects.
+#[derive(Debug)]
+pub struct InboundCreationResult {
+ /// The [`Session`] that was created from a pre-key message.
+ pub session: Session,
+ /// The plaintext of the pre-key message.
+ pub plaintext: Vec<u8>,
+}
+
+/// An Olm account manages all cryptographic keys used on a device.
+pub struct Account {
+ /// A permanent Ed25519 key used for signing. Also known as the fingerprint
+ /// key.
+ signing_key: Ed25519Keypair,
+ /// The permanent Curve25519 key used for 3DH. Also known as the sender key
+ /// or the identity key.
+ diffie_hellman_key: Curve25519Keypair,
+ /// The ephemeral (one-time) Curve25519 keys used as part of the 3DH.
+ one_time_keys: OneTimeKeys,
+ /// The ephemeral Curve25519 keys used in lieu of a one-time key as part of
+ /// the 3DH, in case we run out of those. We keep track of both the current
+ /// and the previous fallback key in any given moment.
+ fallback_keys: FallbackKeys,
+}
+
+impl Account {
+ /// Create a new Account with new random identity keys.
+ pub fn new() -> Self {
+ Self {
+ signing_key: Ed25519Keypair::new(),
+ diffie_hellman_key: Curve25519Keypair::new(),
+ one_time_keys: OneTimeKeys::new(),
+ fallback_keys: FallbackKeys::new(),
+ }
+ }
+
+ /// Get the IdentityKeys of this Account
+ pub fn identity_keys(&self) -> IdentityKeys {
+ IdentityKeys { ed25519: self.ed25519_key(), curve25519: self.curve25519_key() }
+ }
+
+ /// Get a reference to the account's public Ed25519 key
+ pub fn ed25519_key(&self) -> Ed25519PublicKey {
+ self.signing_key.public_key()
+ }
+
+ /// Get a reference to the account's public Curve25519 key
+ pub fn curve25519_key(&self) -> Curve25519PublicKey {
+ self.diffie_hellman_key.public_key()
+ }
+
+ /// Sign the given message using our Ed25519 fingerprint key.
+ pub fn sign(&self, message: &str) -> Ed25519Signature {
+ self.signing_key.sign(message.as_bytes())
+ }
+
+ /// Get the maximum number of one-time keys the client should keep on the
+ /// server.
+ ///
+ /// **Note**: this differs from the libolm method of the same name, the
+ /// libolm method returned the maximum amount of one-time keys the `Account`
+ /// could hold and only half of those should be uploaded.
+ pub fn max_number_of_one_time_keys(&self) -> usize {
+ // We tell clients to upload a limited amount of one-time keys, this
+ // amount is smaller than what we can store.
+ //
+ // We do this because a client might receive the count of uploaded keys
+ // from the server before they receive all the pre-key messages that
+ // used some of our one-time keys. This would mean that we would forget
+ // private one-time keys, since we're generating new ones, while we
+ // didn't yet receive the pre-key messages that used those one-time
+ // keys.
+ PUBLIC_MAX_ONE_TIME_KEYS
+ }
+
+ /// Create a `Session` with the given identity key and one-time key.
+ pub fn create_outbound_session(
+ &self,
+ session_config: SessionConfig,
+ identity_key: Curve25519PublicKey,
+ one_time_key: Curve25519PublicKey,
+ ) -> Session {
+ let rng = thread_rng();
+
+ let base_key = ReusableSecret::random_from_rng(rng);
+ let public_base_key = Curve25519PublicKey::from(&base_key);
+
+ let shared_secret = Shared3DHSecret::new(
+ self.diffie_hellman_key.secret_key(),
+ &base_key,
+ &identity_key,
+ &one_time_key,
+ );
+
+ let session_keys = SessionKeys {
+ identity_key: self.curve25519_key(),
+ base_key: public_base_key,
+ one_time_key,
+ };
+
+ Session::new(session_config, shared_secret, session_keys)
+ }
+
+ fn find_one_time_key(&self, public_key: &Curve25519PublicKey) -> Option<&Curve25519SecretKey> {
+ self.one_time_keys
+ .get_secret_key(public_key)
+ .or_else(|| self.fallback_keys.get_secret_key(public_key))
+ }
+
+ /// Remove a one-time key that has previously been published but not yet
+ /// used.
+ ///
+ /// **Note**: This function is only rarely useful and you'll know if you
+ /// need it. Notably, you do *not* need to call it manually when using up
+ /// a key via [`Account::create_inbound_session`] since the key is
+ /// automatically removed in that case.
+ #[cfg(feature = "low-level-api")]
+ pub fn remove_one_time_key(
+ &mut self,
+ public_key: Curve25519PublicKey,
+ ) -> Option<Curve25519SecretKey> {
+ self.remove_one_time_key_helper(public_key)
+ }
+
+ fn remove_one_time_key_helper(
+ &mut self,
+ public_key: Curve25519PublicKey,
+ ) -> Option<Curve25519SecretKey> {
+ self.one_time_keys.remove_secret_key(&public_key)
+ }
+
+ /// Create a [`Session`] from the given pre-key message and identity key
+ pub fn create_inbound_session(
+ &mut self,
+ their_identity_key: Curve25519PublicKey,
+ pre_key_message: &PreKeyMessage,
+ ) -> Result<InboundCreationResult, SessionCreationError> {
+ if their_identity_key != pre_key_message.identity_key() {
+ Err(SessionCreationError::MismatchedIdentityKey(
+ their_identity_key,
+ pre_key_message.identity_key(),
+ ))
+ } else {
+ // Find the matching private part of the OTK that the message claims
+ // was used to create the session that encrypted it.
+ let public_otk = pre_key_message.one_time_key();
+ let private_otk = self
+ .find_one_time_key(&public_otk)
+ .ok_or(SessionCreationError::MissingOneTimeKey(public_otk))?;
+
+ // Construct a 3DH shared secret from the various curve25519 keys.
+ let shared_secret = RemoteShared3DHSecret::new(
+ self.diffie_hellman_key.secret_key(),
+ private_otk,
+ &pre_key_message.identity_key(),
+ &pre_key_message.base_key(),
+ );
+
+ // These will be used to uniquely identify the Session.
+ let session_keys = SessionKeys {
+ identity_key: pre_key_message.identity_key(),
+ base_key: pre_key_message.base_key(),
+ one_time_key: pre_key_message.one_time_key(),
+ };
+
+ let config = if pre_key_message.message.mac_truncated() {
+ SessionConfig::version_1()
+ } else {
+ SessionConfig::version_2()
+ };
+
+ // Create a Session, AKA a double ratchet, this one will have an
+ // inactive sending chain until we decide to encrypt a message.
+ let mut session = Session::new_remote(
+ config,
+ shared_secret,
+ pre_key_message.message.ratchet_key,
+ session_keys,
+ );
+
+ // Decrypt the message to check if the Session is actually valid.
+ let plaintext = session.decrypt_decoded(&pre_key_message.message)?;
+
+ // We only drop the one-time key now, this is why we can't use a
+ // one-time key type that takes `self`. If we didn't do this,
+ // someone could maliciously pretend to use up our one-time key and
+ // make us drop the private part. Unsuspecting users that actually
+ // try to use such an one-time key won't be able to commnuicate with
+ // us. This is strictly worse than the one-time key exhaustion
+ // scenario.
+ self.remove_one_time_key_helper(pre_key_message.one_time_key());
+
+ Ok(InboundCreationResult { session, plaintext })
+ }
+ }
+
+ /// Generates the supplied number of one time keys.
+ /// Returns the public parts of the one-time keys that were created and
+ /// discarded.
+ ///
+ /// Our one-time key store inside the [`Account`] has a limited amount of
+ /// places for one-time keys, If we try to generate new ones while the store
+ /// is completely populated, the oldest one-time keys will get discarded
+ /// to make place for new ones.
+ pub fn generate_one_time_keys(&mut self, count: usize) -> OneTimeKeyGenerationResult {
+ self.one_time_keys.generate(count)
+ }
+
+ pub fn stored_one_time_key_count(&self) -> usize {
+ self.one_time_keys.private_keys.len()
+ }
+
+ /// Get the currently unpublished one-time keys.
+ ///
+ /// The one-time keys should be published to a server and marked as
+ /// published using the `mark_keys_as_published()` method.
+ pub fn one_time_keys(&self) -> HashMap<KeyId, Curve25519PublicKey> {
+ self.one_time_keys
+ .unpublished_public_keys
+ .iter()
+ .map(|(key_id, key)| (*key_id, *key))
+ .collect()
+ }
+
+ /// Generate a single new fallback key.
+ ///
+ /// The fallback key will be used by other users to establish a `Session` if
+ /// all the one-time keys on the server have been used up.
+ ///
+ /// Returns the public Curve25519 key of the *previous* fallback key, that
+ /// is, the one that will get removed from the [`Account`] when this method
+ /// is called. This return value is mostly useful for logging purposes.
+ pub fn generate_fallback_key(&mut self) -> Option<Curve25519PublicKey> {
+ self.fallback_keys.generate_fallback_key()
+ }
+
+ /// Get the currently unpublished fallback key.
+ ///
+ /// The fallback key should be published just like the one-time keys, after
+ /// it has been successfully published it needs to be marked as published
+ /// using the `mark_keys_as_published()` method as well.
+ pub fn fallback_key(&self) -> HashMap<KeyId, Curve25519PublicKey> {
+ let fallback_key = self.fallback_keys.unpublished_fallback_key();
+
+ if let Some(fallback_key) = fallback_key {
+ HashMap::from([(fallback_key.key_id(), fallback_key.public_key())])
+ } else {
+ HashMap::new()
+ }
+ }
+
+ /// The `Account` stores at most two private parts of the fallback key. This
+ /// method lets us forget the previously used fallback key.
+ pub fn forget_fallback_key(&mut self) -> bool {
+ self.fallback_keys.forget_previous_fallback_key().is_some()
+ }
+
+ /// Mark all currently unpublished one-time and fallback keys as published.
+ pub fn mark_keys_as_published(&mut self) {
+ self.one_time_keys.mark_as_published();
+ self.fallback_keys.mark_as_published();
+ }
+
+ /// Convert the account into a struct which implements [`serde::Serialize`]
+ /// and [`serde::Deserialize`].
+ pub fn pickle(&self) -> AccountPickle {
+ AccountPickle {
+ signing_key: self.signing_key.clone().into(),
+ diffie_hellman_key: self.diffie_hellman_key.clone().into(),
+ one_time_keys: self.one_time_keys.clone().into(),
+ fallback_keys: self.fallback_keys.clone(),
+ }
+ }
+
+ /// Restore an [`Account`] from a previously saved [`AccountPickle`].
+ pub fn from_pickle(pickle: AccountPickle) -> Self {
+ pickle.into()
+ }
+
+ /// Create an [`Account`] object by unpickling an account pickle in libolm
+ /// legacy pickle format.
+ ///
+ /// Such pickles are encrypted and need to first be decrypted using
+ /// `pickle_key`.
+ #[cfg(feature = "libolm-compat")]
+ pub fn from_libolm_pickle(
+ pickle: &str,
+ pickle_key: &[u8],
+ ) -> Result<Self, crate::LibolmPickleError> {
+ use self::libolm::Pickle;
+ use crate::utilities::unpickle_libolm;
+
+ const PICKLE_VERSION: u32 = 4;
+ unpickle_libolm::<Pickle, _>(pickle, pickle_key, PICKLE_VERSION)
+ }
+
+ #[cfg(all(any(fuzzing, test), feature = "libolm-compat"))]
+ pub fn from_decrypted_libolm_pickle(pickle: &[u8]) -> Result<Self, crate::LibolmPickleError> {
+ use std::io::Cursor;
+
+ use matrix_pickle::Decode;
+
+ use self::libolm::Pickle;
+
+ let mut cursor = Cursor::new(&pickle);
+ let pickle = Pickle::decode(&mut cursor)?;
+
+ pickle.try_into()
+ }
+}
+
+impl Default for Account {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// A format suitable for serialization which implements [`serde::Serialize`]
+/// and [`serde::Deserialize`]. Obtainable by calling [`Account::pickle`].
+#[derive(Serialize, Deserialize)]
+pub struct AccountPickle {
+ signing_key: Ed25519KeypairPickle,
+ diffie_hellman_key: Curve25519KeypairPickle,
+ one_time_keys: OneTimeKeysPickle,
+ fallback_keys: FallbackKeys,
+}
+
+/// A format suitable for serialization which implements [`serde::Serialize`]
+/// and [`serde::Deserialize`]. Obtainable by calling [`Account::pickle`].
+impl AccountPickle {
+ /// Serialize and encrypt the pickle using the given key.
+ ///
+ /// This is the inverse of [`AccountPickle::from_encrypted`].
+ pub fn encrypt(self, pickle_key: &[u8; 32]) -> String {
+ pickle(&self, pickle_key)
+ }
+
+ /// Obtain a pickle from a ciphertext by decrypting and deserializing using
+ /// the given key.
+ ///
+ /// This is the inverse of [`AccountPickle::encrypt`].
+ pub fn from_encrypted(ciphertext: &str, pickle_key: &[u8; 32]) -> Result<Self, PickleError> {
+ unpickle(ciphertext, pickle_key)
+ }
+}
+
+impl From<AccountPickle> for Account {
+ fn from(pickle: AccountPickle) -> Self {
+ Self {
+ signing_key: pickle.signing_key.into(),
+ diffie_hellman_key: pickle.diffie_hellman_key.into(),
+ one_time_keys: pickle.one_time_keys.into(),
+ fallback_keys: pickle.fallback_keys,
+ }
+ }
+}
+
+#[cfg(feature = "libolm-compat")]
+mod libolm {
+ use matrix_pickle::{Decode, DecodeError};
+ use zeroize::Zeroize;
+
+ use super::{
+ fallback_keys::{FallbackKey, FallbackKeys},
+ one_time_keys::OneTimeKeys,
+ Account,
+ };
+ use crate::{
+ types::{Curve25519Keypair, Curve25519SecretKey},
+ utilities::LibolmEd25519Keypair,
+ Ed25519Keypair, KeyId,
+ };
+
+ #[derive(Debug, Zeroize, Decode)]
+ #[zeroize(drop)]
+ struct OneTimeKey {
+ key_id: u32,
+ published: bool,
+ public_key: [u8; 32],
+ private_key: Box<[u8; 32]>,
+ }
+
+ impl From<&OneTimeKey> for FallbackKey {
+ fn from(key: &OneTimeKey) -> Self {
+ FallbackKey {
+ key_id: KeyId(key.key_id.into()),
+ key: Curve25519SecretKey::from_slice(&key.private_key),
+ published: key.published,
+ }
+ }
+ }
+
+ #[derive(Debug, Zeroize)]
+ #[zeroize(drop)]
+ struct FallbackKeysArray {
+ fallback_key: Option<OneTimeKey>,
+ previous_fallback_key: Option<OneTimeKey>,
+ }
+
+ impl Decode for FallbackKeysArray {
+ fn decode(reader: &mut impl std::io::Read) -> Result<Self, DecodeError> {
+ let count = u8::decode(reader)?;
+
+ let (fallback_key, previous_fallback_key) = if count >= 1 {
+ let fallback_key = OneTimeKey::decode(reader)?;
+
+ let previous_fallback_key =
+ if count >= 2 { Some(OneTimeKey::decode(reader)?) } else { None };
+
+ (Some(fallback_key), previous_fallback_key)
+ } else {
+ (None, None)
+ };
+
+ Ok(Self { fallback_key, previous_fallback_key })
+ }
+ }
+
+ #[derive(Zeroize, Decode)]
+ #[zeroize(drop)]
+ pub(super) struct Pickle {
+ version: u32,
+ ed25519_keypair: LibolmEd25519Keypair,
+ public_curve25519_key: [u8; 32],
+ private_curve25519_key: Box<[u8; 32]>,
+ one_time_keys: Vec<OneTimeKey>,
+ fallback_keys: FallbackKeysArray,
+ next_key_id: u32,
+ }
+
+ impl TryFrom<Pickle> for Account {
+ type Error = crate::LibolmPickleError;
+
+ fn try_from(pickle: Pickle) -> Result<Self, Self::Error> {
+ let mut one_time_keys = OneTimeKeys::new();
+
+ for key in &pickle.one_time_keys {
+ let secret_key = Curve25519SecretKey::from_slice(&key.private_key);
+ let key_id = KeyId(key.key_id.into());
+ one_time_keys.insert_secret_key(key_id, secret_key, key.published);
+ }
+
+ one_time_keys.next_key_id = pickle.next_key_id.into();
+
+ let fallback_keys = FallbackKeys {
+ key_id: pickle
+ .fallback_keys
+ .fallback_key
+ .as_ref()
+ .map(|k| k.key_id.wrapping_add(1))
+ .unwrap_or(0) as u64,
+ fallback_key: pickle.fallback_keys.fallback_key.as_ref().map(|k| k.into()),
+ previous_fallback_key: pickle
+ .fallback_keys
+ .previous_fallback_key
+ .as_ref()
+ .map(|k| k.into()),
+ };
+
+ Ok(Self {
+ signing_key: Ed25519Keypair::from_expanded_key(
+ &pickle.ed25519_keypair.private_key,
+ )?,
+ diffie_hellman_key: Curve25519Keypair::from_secret_key(
+ &pickle.private_curve25519_key,
+ ),
+ one_time_keys,
+ fallback_keys,
+ })
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use anyhow::{bail, Context, Result};
+ use olm_rs::{account::OlmAccount, session::OlmMessage as LibolmOlmMessage};
+
+ use super::{Account, InboundCreationResult, SessionConfig, SessionCreationError};
+ use crate::{
+ cipher::Mac,
+ olm::{
+ messages::{OlmMessage, PreKeyMessage},
+ AccountPickle,
+ },
+ run_corpus, Curve25519PublicKey as PublicKey,
+ };
+
+ const PICKLE_KEY: [u8; 32] = [0u8; 32];
+
+ #[test]
+ fn vodozemac_libolm_communication() -> Result<()> {
+ // vodozemac account
+ let alice = Account::new();
+ // libolm account
+ let bob = OlmAccount::new();
+
+ bob.generate_one_time_keys(1);
+
+ let one_time_key = bob
+ .parsed_one_time_keys()
+ .curve25519()
+ .values()
+ .next()
+ .cloned()
+ .expect("Didn't find a valid one-time key");
+
+ bob.mark_keys_as_published();
+
+ let identity_keys = bob.parsed_identity_keys();
+ let curve25519_key = PublicKey::from_base64(identity_keys.curve25519())?;
+ let one_time_key = PublicKey::from_base64(&one_time_key)?;
+ let mut alice_session =
+ alice.create_outbound_session(SessionConfig::version_1(), curve25519_key, one_time_key);
+
+ let message = "It's a secret to everybody";
+ let olm_message: LibolmOlmMessage = alice_session.encrypt(message).into();
+
+ if let LibolmOlmMessage::PreKey(m) = olm_message.clone() {
+ let libolm_session =
+ bob.create_inbound_session_from(&alice.curve25519_key().to_base64(), m)?;
+ assert_eq!(alice_session.session_id(), libolm_session.session_id());
+
+ let plaintext = libolm_session.decrypt(olm_message)?;
+ assert_eq!(message, plaintext);
+
+ let second_text = "Here's another secret to everybody";
+ let olm_message = alice_session.encrypt(second_text).into();
+
+ let plaintext = libolm_session.decrypt(olm_message)?;
+ assert_eq!(second_text, plaintext);
+
+ let reply_plain = "Yes, take this, it's dangerous out there";
+ let reply = libolm_session.encrypt(reply_plain).into();
+ let plaintext = alice_session.decrypt(&reply)?;
+
+ assert_eq!(plaintext, reply_plain.as_bytes());
+
+ let another_reply = "Last one";
+ let reply = libolm_session.encrypt(another_reply).into();
+ let plaintext = alice_session.decrypt(&reply)?;
+ assert_eq!(plaintext, another_reply.as_bytes());
+
+ let last_text = "Nope, I'll have the last word";
+ let olm_message = alice_session.encrypt(last_text).into();
+
+ let plaintext = libolm_session.decrypt(olm_message)?;
+ assert_eq!(last_text, plaintext);
+ } else {
+ bail!("Received a invalid message type {:?}", olm_message);
+ }
+
+ Ok(())
+ }
+
+ #[test]
+ fn vodozemac_vodozemac_communication() -> Result<()> {
+ // Both of these are vodozemac accounts.
+ let alice = Account::new();
+ let mut bob = Account::new();
+
+ bob.generate_one_time_keys(1);
+
+ let mut alice_session = alice.create_outbound_session(
+ SessionConfig::version_2(),
+ bob.curve25519_key(),
+ *bob.one_time_keys()
+ .iter()
+ .next()
+ .context("Failed getting bob's OTK, which should never happen here.")?
+ .1,
+ );
+
+ bob.mark_keys_as_published();
+
+ let message = "It's a secret to everybody";
+ let olm_message = alice_session.encrypt(message);
+
+ if let OlmMessage::PreKey(m) = olm_message {
+ assert_eq!(m.session_keys(), alice_session.session_keys());
+
+ let InboundCreationResult { session: mut bob_session, plaintext } =
+ bob.create_inbound_session(alice.curve25519_key(), &m)?;
+ assert_eq!(alice_session.session_id(), bob_session.session_id());
+ assert_eq!(m.session_keys(), bob_session.session_keys());
+
+ assert_eq!(message.as_bytes(), plaintext);
+
+ let second_text = "Here's another secret to everybody";
+ let olm_message = alice_session.encrypt(second_text);
+
+ let plaintext = bob_session.decrypt(&olm_message)?;
+ assert_eq!(second_text.as_bytes(), plaintext);
+
+ let reply_plain = "Yes, take this, it's dangerous out there";
+ let reply = bob_session.encrypt(reply_plain);
+ let plaintext = alice_session.decrypt(&reply)?;
+
+ assert_eq!(plaintext, reply_plain.as_bytes());
+
+ let another_reply = "Last one";
+ let reply = bob_session.encrypt(another_reply);
+ let plaintext = alice_session.decrypt(&reply)?;
+ assert_eq!(plaintext, another_reply.as_bytes());
+
+ let last_text = "Nope, I'll have the last word";
+ let olm_message = alice_session.encrypt(last_text);
+
+ let plaintext = bob_session.decrypt(&olm_message)?;
+ assert_eq!(last_text.as_bytes(), plaintext);
+ }
+
+ Ok(())
+ }
+
+ #[test]
+ fn inbound_session_creation() -> Result<()> {
+ let alice = OlmAccount::new();
+ let mut bob = Account::new();
+
+ bob.generate_one_time_keys(1);
+
+ let one_time_key =
+ bob.one_time_keys().values().next().cloned().expect("Didn't find a valid one-time key");
+
+ let alice_session = alice.create_outbound_session(
+ &bob.curve25519_key().to_base64(),
+ &one_time_key.to_base64(),
+ )?;
+
+ let text = "It's a secret to everybody";
+ let message = alice_session.encrypt(text).into();
+
+ let identity_key = PublicKey::from_base64(alice.parsed_identity_keys().curve25519())?;
+
+ let InboundCreationResult { session, plaintext } = if let OlmMessage::PreKey(m) = &message {
+ bob.create_inbound_session(identity_key, m)?
+ } else {
+ bail!("Got invalid message type from olm_rs {:?}", message);
+ };
+
+ assert_eq!(alice_session.session_id(), session.session_id());
+ assert!(bob.one_time_keys.private_keys.is_empty());
+
+ assert_eq!(text.as_bytes(), plaintext);
+
+ Ok(())
+ }
+
+ #[test]
+ fn inbound_session_creation_using_fallback_keys() -> Result<()> {
+ let alice = OlmAccount::new();
+ let mut bob = Account::new();
+
+ bob.generate_fallback_key();
+
+ let one_time_key =
+ bob.fallback_key().values().next().cloned().expect("Didn't find a valid fallback key");
+ assert!(bob.one_time_keys.private_keys.is_empty());
+
+ let alice_session = alice.create_outbound_session(
+ &bob.curve25519_key().to_base64(),
+ &one_time_key.to_base64(),
+ )?;
+
+ let text = "It's a secret to everybody";
+
+ let message = alice_session.encrypt(text).into();
+ let identity_key = PublicKey::from_base64(alice.parsed_identity_keys().curve25519())?;
+
+ if let OlmMessage::PreKey(m) = &message {
+ let InboundCreationResult { session, plaintext } =
+ bob.create_inbound_session(identity_key, m)?;
+
+ assert_eq!(m.session_keys(), session.session_keys());
+ assert_eq!(alice_session.session_id(), session.session_id());
+ assert!(bob.fallback_keys.fallback_key.is_some());
+
+ assert_eq!(text.as_bytes(), plaintext);
+ } else {
+ bail!("Got invalid message type from olm_rs");
+ };
+
+ Ok(())
+ }
+
+ #[test]
+ fn account_pickling_roundtrip_is_identity() -> Result<()> {
+ let mut account = Account::new();
+
+ account.generate_one_time_keys(50);
+
+ // Generate two fallback keys so the previous fallback key field gets populated.
+ account.generate_fallback_key();
+ account.generate_fallback_key();
+
+ let pickle = account.pickle().encrypt(&PICKLE_KEY);
+
+ let decrypted_pickle = AccountPickle::from_encrypted(&pickle, &PICKLE_KEY)?;
+ let unpickled_account = Account::from_pickle(decrypted_pickle);
+ let repickle = unpickled_account.pickle();
+
+ assert_eq!(account.identity_keys(), unpickled_account.identity_keys());
+
+ let decrypted_pickle = AccountPickle::from_encrypted(&pickle, &PICKLE_KEY)?;
+ let pickle = serde_json::to_value(decrypted_pickle)?;
+ let repickle = serde_json::to_value(repickle)?;
+
+ assert_eq!(pickle, repickle);
+
+ Ok(())
+ }
+
+ #[test]
+ #[cfg(feature = "libolm-compat")]
+ fn libolm_unpickling() -> Result<()> {
+ let olm = OlmAccount::new();
+ olm.generate_one_time_keys(10);
+ olm.generate_fallback_key();
+
+ let key = b"DEFAULT_PICKLE_KEY";
+ let pickle = olm.pickle(olm_rs::PicklingMode::Encrypted { key: key.to_vec() });
+
+ let unpickled = Account::from_libolm_pickle(&pickle, key)?;
+
+ assert_eq!(olm.parsed_identity_keys().ed25519(), unpickled.ed25519_key().to_base64());
+ assert_eq!(olm.parsed_identity_keys().curve25519(), unpickled.curve25519_key().to_base64());
+
+ let mut olm_one_time_keys: Vec<_> =
+ olm.parsed_one_time_keys().curve25519().values().map(|k| k.to_owned()).collect();
+ let mut one_time_keys: Vec<_> =
+ unpickled.one_time_keys().values().map(|k| k.to_base64()).collect();
+
+ // We generated 10 one-time keys on the libolm side, we expect the next key id
+ // to be 11.
+ assert_eq!(unpickled.one_time_keys.next_key_id, 11);
+
+ olm_one_time_keys.sort();
+ one_time_keys.sort();
+ assert_eq!(olm_one_time_keys, one_time_keys);
+
+ let olm_fallback_key =
+ olm.parsed_fallback_key().expect("libolm should have a fallback key");
+ assert_eq!(
+ olm_fallback_key.curve25519(),
+ unpickled
+ .fallback_key()
+ .values()
+ .next()
+ .expect("We should have a fallback key")
+ .to_base64()
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ #[cfg(feature = "libolm-compat")]
+ fn signing_with_expanded_key() -> Result<()> {
+ let olm = OlmAccount::new();
+ olm.generate_one_time_keys(10);
+ olm.generate_fallback_key();
+
+ let key = b"DEFAULT_PICKLE_KEY";
+ let pickle = olm.pickle(olm_rs::PicklingMode::Encrypted { key: key.to_vec() });
+
+ let account_with_expanded_key = Account::from_libolm_pickle(&pickle, key)?;
+
+ // The clone is needed since we're later on using the account.
+ #[allow(clippy::redundant_clone)]
+ let signing_key_clone = account_with_expanded_key.signing_key.clone();
+ signing_key_clone.sign("You met with a terrible fate, haven’t you?".as_bytes());
+ account_with_expanded_key.sign("You met with a terrible fate, haven’t you?");
+
+ Ok(())
+ }
+
+ #[test]
+ fn invalid_session_creation_does_not_remove_otk() -> Result<()> {
+ let mut alice = Account::new();
+ let malory = Account::new();
+ alice.generate_one_time_keys(1);
+
+ let mut session = malory.create_outbound_session(
+ SessionConfig::default(),
+ alice.curve25519_key(),
+ *alice.one_time_keys().values().next().expect("Should have one-time key"),
+ );
+
+ let message = session.encrypt("Test");
+
+ if let OlmMessage::PreKey(m) = message {
+ let mut message = m.to_bytes();
+ let message_len = message.len();
+
+ // We mangle the MAC so decryption fails but creating a Session
+ // succeeds.
+ message[message_len - Mac::TRUNCATED_LEN..message_len]
+ .copy_from_slice(&[0u8; Mac::TRUNCATED_LEN]);
+
+ let message = PreKeyMessage::try_from(message)?;
+
+ match alice.create_inbound_session(malory.curve25519_key(), &message) {
+ Err(SessionCreationError::Decryption(_)) => {}
+ e => bail!("Expected a decryption error, got {:?}", e),
+ }
+ assert!(
+ !alice.one_time_keys.private_keys.is_empty(),
+ "The one-time key was removed when it shouldn't"
+ );
+
+ Ok(())
+ } else {
+ bail!("Invalid message type");
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "libolm-compat")]
+ fn fuzz_corpus_unpickling() {
+ run_corpus("olm-account-unpickling", |data| {
+ let _ = Account::from_decrypted_libolm_pickle(data);
+ });
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +
// Copyright 2021 Damir Jelić, Denis Kasak
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::collections::{BTreeMap, HashMap};
+
+use serde::{Deserialize, Serialize};
+
+use super::PUBLIC_MAX_ONE_TIME_KEYS;
+use crate::{
+ types::{Curve25519SecretKey, KeyId},
+ Curve25519PublicKey,
+};
+
+#[derive(Serialize, Deserialize, Clone)]
+#[serde(from = "OneTimeKeysPickle")]
+#[serde(into = "OneTimeKeysPickle")]
+pub(super) struct OneTimeKeys {
+ pub next_key_id: u64,
+ pub unpublished_public_keys: BTreeMap<KeyId, Curve25519PublicKey>,
+ pub private_keys: BTreeMap<KeyId, Curve25519SecretKey>,
+ pub key_ids_by_key: HashMap<Curve25519PublicKey, KeyId>,
+}
+
+/// The result type for the one-time key generation operation.
+pub struct OneTimeKeyGenerationResult {
+ /// The public part of the one-time keys that were newly generated.
+ pub created: Vec<Curve25519PublicKey>,
+ /// The public part of the one-time keys that had to be removed to make
+ /// space for the new ones.
+ pub removed: Vec<Curve25519PublicKey>,
+}
+
+impl OneTimeKeys {
+ const MAX_ONE_TIME_KEYS: usize = 100 * PUBLIC_MAX_ONE_TIME_KEYS;
+
+ pub fn new() -> Self {
+ Self {
+ next_key_id: 0,
+ unpublished_public_keys: Default::default(),
+ private_keys: Default::default(),
+ key_ids_by_key: Default::default(),
+ }
+ }
+
+ pub fn mark_as_published(&mut self) {
+ self.unpublished_public_keys.clear();
+ }
+
+ pub fn get_secret_key(&self, public_key: &Curve25519PublicKey) -> Option<&Curve25519SecretKey> {
+ self.key_ids_by_key.get(public_key).and_then(|key_id| self.private_keys.get(key_id))
+ }
+
+ pub fn remove_secret_key(
+ &mut self,
+ public_key: &Curve25519PublicKey,
+ ) -> Option<Curve25519SecretKey> {
+ self.key_ids_by_key.remove(public_key).and_then(|key_id| {
+ self.unpublished_public_keys.remove(&key_id);
+ self.private_keys.remove(&key_id)
+ })
+ }
+
+ pub(super) fn insert_secret_key(
+ &mut self,
+ key_id: KeyId,
+ key: Curve25519SecretKey,
+ published: bool,
+ ) -> (Curve25519PublicKey, Option<Curve25519PublicKey>) {
+ // If we hit the max number of one-time keys we'd like to keep, first remove one
+ // before we create a new one.
+ let removed = if self.private_keys.len() >= Self::MAX_ONE_TIME_KEYS {
+ if let Some(key_id) = self.private_keys.keys().next().copied() {
+ let public_key = if let Some(private_key) = self.private_keys.remove(&key_id) {
+ let public_key = Curve25519PublicKey::from(&private_key);
+ self.key_ids_by_key.remove(&public_key);
+
+ Some(public_key)
+ } else {
+ None
+ };
+
+ self.unpublished_public_keys.remove(&key_id);
+
+ public_key
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+
+ let public_key = Curve25519PublicKey::from(&key);
+
+ self.private_keys.insert(key_id, key);
+ self.key_ids_by_key.insert(public_key, key_id);
+
+ if !published {
+ self.unpublished_public_keys.insert(key_id, public_key);
+ }
+
+ (public_key, removed)
+ }
+
+ fn generate_one_time_key(&mut self) -> (Curve25519PublicKey, Option<Curve25519PublicKey>) {
+ let key_id = KeyId(self.next_key_id);
+ let key = Curve25519SecretKey::new();
+ self.insert_secret_key(key_id, key, false)
+ }
+
+ pub fn generate(&mut self, count: usize) -> OneTimeKeyGenerationResult {
+ let mut removed_keys = Vec::new();
+ let mut created_keys = Vec::new();
+
+ for _ in 0..count {
+ let (created, removed) = self.generate_one_time_key();
+
+ created_keys.push(created);
+ if let Some(removed) = removed {
+ removed_keys.push(removed);
+ }
+
+ self.next_key_id = self.next_key_id.wrapping_add(1);
+ }
+
+ OneTimeKeyGenerationResult { created: created_keys, removed: removed_keys }
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub(super) struct OneTimeKeysPickle {
+ #[serde(alias = "key_id")]
+ next_key_id: u64,
+ public_keys: BTreeMap<KeyId, Curve25519PublicKey>,
+ private_keys: BTreeMap<KeyId, Curve25519SecretKey>,
+}
+
+impl From<OneTimeKeysPickle> for OneTimeKeys {
+ fn from(pickle: OneTimeKeysPickle) -> Self {
+ let mut key_ids_by_key = HashMap::new();
+
+ for (k, v) in pickle.private_keys.iter() {
+ key_ids_by_key.insert(v.into(), *k);
+ }
+
+ Self {
+ next_key_id: pickle.next_key_id,
+ unpublished_public_keys: pickle.public_keys.iter().map(|(&k, &v)| (k, v)).collect(),
+ private_keys: pickle.private_keys,
+ key_ids_by_key,
+ }
+ }
+}
+
+impl From<OneTimeKeys> for OneTimeKeysPickle {
+ fn from(keys: OneTimeKeys) -> Self {
+ OneTimeKeysPickle {
+ next_key_id: keys.next_key_id,
+ public_keys: keys.unpublished_public_keys.iter().map(|(&k, &v)| (k, v)).collect(),
+ private_keys: keys.private_keys,
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::OneTimeKeys;
+ use crate::types::KeyId;
+
+ #[test]
+ fn store_limit() {
+ let mut store = OneTimeKeys::new();
+
+ assert!(store.private_keys.is_empty());
+
+ store.generate(OneTimeKeys::MAX_ONE_TIME_KEYS);
+ assert_eq!(store.private_keys.len(), OneTimeKeys::MAX_ONE_TIME_KEYS);
+ assert_eq!(store.unpublished_public_keys.len(), OneTimeKeys::MAX_ONE_TIME_KEYS);
+ assert_eq!(store.key_ids_by_key.len(), OneTimeKeys::MAX_ONE_TIME_KEYS);
+
+ store.mark_as_published();
+ assert!(store.unpublished_public_keys.is_empty());
+ assert_eq!(store.private_keys.len(), OneTimeKeys::MAX_ONE_TIME_KEYS);
+ assert_eq!(store.key_ids_by_key.len(), OneTimeKeys::MAX_ONE_TIME_KEYS);
+
+ let oldest_key_id =
+ store.private_keys.keys().next().copied().expect("Couldn't get the first key ID");
+ assert_eq!(oldest_key_id, KeyId(0));
+
+ store.generate(10);
+ assert_eq!(store.unpublished_public_keys.len(), 10);
+ assert_eq!(store.private_keys.len(), OneTimeKeys::MAX_ONE_TIME_KEYS);
+ assert_eq!(store.key_ids_by_key.len(), OneTimeKeys::MAX_ONE_TIME_KEYS);
+
+ let oldest_key_id =
+ store.private_keys.keys().next().copied().expect("Couldn't get the first key ID");
+
+ assert_eq!(oldest_key_id, KeyId(10));
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +
// Copyright 2021 Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::fmt::Debug;
+
+use prost::Message as ProstMessage;
+use serde::{Deserialize, Serialize};
+
+use crate::{
+ cipher::{Mac, MessageMac},
+ utilities::{base64_decode, base64_encode, extract_mac, VarInt},
+ Curve25519PublicKey, DecodeError,
+};
+
+const MAC_TRUNCATED_VERSION: u8 = 3;
+const VERSION: u8 = 4;
+
+/// An encrypted Olm message.
+///
+/// Contains metadata that is required to find the correct ratchet state of a
+/// [`Session`] necessary to decrypt the message.
+///
+/// [`Session`]: crate::olm::Session
+#[derive(Clone, PartialEq, Eq)]
+pub struct Message {
+ pub(crate) version: u8,
+ pub(crate) ratchet_key: Curve25519PublicKey,
+ pub(crate) chain_index: u64,
+ pub(crate) ciphertext: Vec<u8>,
+ pub(crate) mac: MessageMac,
+}
+
+impl Message {
+ /// The public part of the ratchet key, that was used when the message was
+ /// encrypted.
+ pub fn ratchet_key(&self) -> Curve25519PublicKey {
+ self.ratchet_key
+ }
+
+ /// The index of the chain that was used when the message was encrypted.
+ pub fn chain_index(&self) -> u64 {
+ self.chain_index
+ }
+
+ /// The actual ciphertext of the message.
+ pub fn ciphertext(&self) -> &[u8] {
+ &self.ciphertext
+ }
+
+ /// The version of the Olm message.
+ pub fn version(&self) -> u8 {
+ self.version
+ }
+
+ /// Has the MAC been truncated in this Olm message.
+ pub fn mac_truncated(&self) -> bool {
+ self.version == MAC_TRUNCATED_VERSION
+ }
+
+ /// Try to decode the given byte slice as a Olm [`Message`].
+ ///
+ /// The expected format of the byte array is described in the
+ /// [`Message::to_bytes()`] method.
+ pub fn from_bytes(bytes: &[u8]) -> Result<Self, DecodeError> {
+ Self::try_from(bytes)
+ }
+
+ /// Encode the `Message` as an array of bytes.
+ ///
+ /// Olm `Message`s consist of a one-byte version, followed by a variable
+ /// length payload and a fixed length message authentication code.
+ ///
+ /// ```text
+ /// +--------------+------------------------------------+-----------+
+ /// | Version Byte | Payload Bytes | MAC Bytes |
+ /// +--------------+------------------------------------+-----------+
+ /// ```
+ ///
+ /// The payload uses a format based on the Protocol Buffers encoding. It
+ /// consists of the following key-value pairs:
+ ///
+ /// **Name** |**Tag**|**Type**| **Meaning**
+ /// :---------:|:-----:|:------:|:-----------------------------------------:
+ /// Ratchet-Key| 0x0A | String |The public part of the ratchet key
+ /// Chain-Index| 0x10 | Integer|The chain index, of the message
+ /// Cipher-Text| 0x22 | String |The cipher-text of the message
+ pub fn to_bytes(&self) -> Vec<u8> {
+ let mut message = self.encode();
+ message.extend(self.mac.as_bytes());
+
+ message
+ }
+
+ /// Try to decode the given string as a Olm [`Message`].
+ ///
+ /// The string needs to be a base64 encoded byte array that follows the
+ /// format described in the [`Message::to_bytes()`] method.
+ pub fn from_base64(message: &str) -> Result<Self, DecodeError> {
+ Self::try_from(message)
+ }
+
+ /// Encode the [`Message`] as a string.
+ ///
+ /// This method first calls [`Message::to_bytes()`] and then encodes the
+ /// resulting byte array as a string using base64 encoding.
+ pub fn to_base64(&self) -> String {
+ base64_encode(self.to_bytes())
+ }
+
+ pub(crate) fn new(
+ ratchet_key: Curve25519PublicKey,
+ chain_index: u64,
+ ciphertext: Vec<u8>,
+ ) -> Self {
+ Self {
+ version: VERSION,
+ ratchet_key,
+ chain_index,
+ ciphertext,
+ mac: Mac([0u8; Mac::LENGTH]).into(),
+ }
+ }
+
+ pub(crate) fn new_truncated_mac(
+ ratchet_key: Curve25519PublicKey,
+ chain_index: u64,
+ ciphertext: Vec<u8>,
+ ) -> Self {
+ Self {
+ version: MAC_TRUNCATED_VERSION,
+ ratchet_key,
+ chain_index,
+ ciphertext,
+ mac: [0u8; Mac::TRUNCATED_LEN].into(),
+ }
+ }
+
+ fn encode(&self) -> Vec<u8> {
+ ProtoBufMessage {
+ ratchet_key: self.ratchet_key.to_bytes().to_vec(),
+ chain_index: self.chain_index,
+ ciphertext: self.ciphertext.clone(),
+ }
+ .encode_manual(self.version)
+ }
+
+ pub(crate) fn to_mac_bytes(&self) -> Vec<u8> {
+ self.encode()
+ }
+
+ pub(crate) fn set_mac(&mut self, mac: Mac) {
+ match self.mac {
+ MessageMac::Truncated(_) => self.mac = mac.truncate().into(),
+ MessageMac::Full(_) => self.mac = mac.into(),
+ }
+ }
+}
+
+impl Serialize for Message {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let message = self.to_base64();
+ serializer.serialize_str(&message)
+ }
+}
+
+impl<'de> Deserialize<'de> for Message {
+ fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
+ let ciphertext = String::deserialize(d)?;
+ Message::from_base64(&ciphertext).map_err(serde::de::Error::custom)
+ }
+}
+
+impl TryFrom<&str> for Message {
+ type Error = DecodeError;
+
+ fn try_from(value: &str) -> Result<Self, Self::Error> {
+ let decoded = base64_decode(value)?;
+
+ Self::try_from(decoded)
+ }
+}
+
+impl TryFrom<Vec<u8>> for Message {
+ type Error = DecodeError;
+
+ fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
+ Self::try_from(value.as_slice())
+ }
+}
+
+impl TryFrom<&[u8]> for Message {
+ type Error = DecodeError;
+
+ fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
+ let version = *value.first().ok_or(DecodeError::MissingVersion)?;
+
+ let mac_length = match version {
+ VERSION => Mac::LENGTH,
+ MAC_TRUNCATED_VERSION => Mac::TRUNCATED_LEN,
+ _ => return Err(DecodeError::InvalidVersion(VERSION, version)),
+ };
+
+ if value.len() < mac_length + 2 {
+ Err(DecodeError::MessageTooShort(value.len()))
+ } else {
+ let inner = ProtoBufMessage::decode(
+ value
+ .get(1..value.len() - mac_length)
+ .ok_or_else(|| DecodeError::MessageTooShort(value.len()))?,
+ )?;
+
+ let mac_slice = &value[value.len() - mac_length..];
+
+ if mac_slice.len() != mac_length {
+ Err(DecodeError::InvalidMacLength(mac_length, mac_slice.len()))
+ } else {
+ let mac = extract_mac(mac_slice, version == MAC_TRUNCATED_VERSION);
+
+ let chain_index = inner.chain_index;
+ let ciphertext = inner.ciphertext;
+ let ratchet_key = Curve25519PublicKey::from_slice(&inner.ratchet_key)?;
+
+ let message = Message { version, ratchet_key, chain_index, ciphertext, mac };
+
+ Ok(message)
+ }
+ }
+ }
+}
+
+impl Debug for Message {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self { version, ratchet_key, chain_index, ciphertext: _, mac: _ } = self;
+
+ f.debug_struct("Message")
+ .field("version", version)
+ .field("ratchet_key", ratchet_key)
+ .field("chain_index", chain_index)
+ .finish_non_exhaustive()
+ }
+}
+
+#[derive(ProstMessage, PartialEq, Eq)]
+struct ProtoBufMessage {
+ #[prost(bytes, tag = "1")]
+ ratchet_key: Vec<u8>,
+ #[prost(uint64, tag = "2")]
+ chain_index: u64,
+ #[prost(bytes, tag = "4")]
+ ciphertext: Vec<u8>,
+}
+
+impl ProtoBufMessage {
+ const RATCHET_TAG: &'static [u8; 1] = b"\x0A";
+ const INDEX_TAG: &'static [u8; 1] = b"\x10";
+ const CIPHER_TAG: &'static [u8; 1] = b"\x22";
+
+ fn encode_manual(&self, version: u8) -> Vec<u8> {
+ let index = self.chain_index.to_var_int();
+ let ratchet_len = self.ratchet_key.len().to_var_int();
+ let ciphertext_len = self.ciphertext.len().to_var_int();
+
+ [
+ [version].as_ref(),
+ Self::RATCHET_TAG.as_ref(),
+ &ratchet_len,
+ &self.ratchet_key,
+ Self::INDEX_TAG.as_ref(),
+ &index,
+ Self::CIPHER_TAG.as_ref(),
+ &ciphertext_len,
+ &self.ciphertext,
+ ]
+ .concat()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::Message;
+ use crate::Curve25519PublicKey;
+
+ #[test]
+ fn encode() {
+ let message = b"\x03\n\x20ratchetkeyhereprettyplease123456\x10\x01\"\nciphertext";
+ let message_mac =
+ b"\x03\n\x20ratchetkeyhereprettyplease123456\x10\x01\"\nciphertextMACHEREE";
+
+ let ratchet_key = Curve25519PublicKey::from(*b"ratchetkeyhereprettyplease123456");
+ let ciphertext = b"ciphertext";
+
+ let mut encoded = Message::new_truncated_mac(ratchet_key, 1, ciphertext.to_vec());
+ encoded.mac = (*b"MACHEREE").into();
+
+ assert_eq!(encoded.to_mac_bytes(), message.as_ref());
+ assert_eq!(encoded.to_bytes(), message_mac.as_ref());
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +
// Copyright 2021 Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+mod message;
+mod pre_key;
+
+pub use message::Message;
+pub use pre_key::PreKeyMessage;
+use serde::{Deserialize, Serialize};
+
+use crate::DecodeError;
+
+/// Enum over the different Olm message types.
+///
+/// Olm uses two types of messages. The underlying transport protocol must
+/// provide a means for recipients to distinguish between them.
+///
+/// [`OlmMessage`] provides [`Serialize`] and [`Deserialize`] implementations
+/// that are compatible with [Matrix].
+///
+/// [Matrix]: https://spec.matrix.org/latest/client-server-api/#molmv1curve25519-aes-sha2
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum OlmMessage {
+ /// A normal message, contains only the ciphertext and metadata to decrypt
+ /// it.
+ Normal(Message),
+ /// A pre-key message, contains metadata to establish a [`Session`] as well
+ /// as a [`Message`].
+ ///
+ /// [`Session`]: crate::olm::Session
+ PreKey(PreKeyMessage),
+}
+
+impl From<Message> for OlmMessage {
+ fn from(m: Message) -> Self {
+ Self::Normal(m)
+ }
+}
+
+impl From<PreKeyMessage> for OlmMessage {
+ fn from(m: PreKeyMessage) -> Self {
+ Self::PreKey(m)
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+struct MessageSerdeHelper {
+ #[serde(rename = "type")]
+ message_type: usize,
+ #[serde(rename = "body")]
+ ciphertext: String,
+}
+
+impl Serialize for OlmMessage {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let (message_type, ciphertext) = self.clone().to_parts();
+
+ let message = MessageSerdeHelper { message_type, ciphertext };
+
+ message.serialize(serializer)
+ }
+}
+
+impl<'de> Deserialize<'de> for OlmMessage {
+ fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
+ let value = MessageSerdeHelper::deserialize(d)?;
+
+ OlmMessage::from_parts(value.message_type, &value.ciphertext)
+ .map_err(serde::de::Error::custom)
+ }
+}
+
+impl OlmMessage {
+ /// Create a `OlmMessage` from a message type and a ciphertext.
+ pub fn from_parts(message_type: usize, ciphertext: &str) -> Result<Self, DecodeError> {
+ match message_type {
+ 0 => Ok(Self::PreKey(PreKeyMessage::try_from(ciphertext)?)),
+ 1 => Ok(Self::Normal(Message::try_from(ciphertext)?)),
+ m => Err(DecodeError::MessageType(m)),
+ }
+ }
+
+ /// Get the message as a byte array.
+ pub fn message(&self) -> &[u8] {
+ match self {
+ OlmMessage::Normal(m) => &m.ciphertext,
+ OlmMessage::PreKey(m) => &m.message.ciphertext,
+ }
+ }
+
+ /// Get the type of the message.
+ pub fn message_type(&self) -> MessageType {
+ match self {
+ OlmMessage::Normal(_) => MessageType::Normal,
+ OlmMessage::PreKey(_) => MessageType::PreKey,
+ }
+ }
+
+ /// Convert the `OlmMessage` into a message type, and base64 encoded message
+ /// tuple.
+ pub fn to_parts(self) -> (usize, String) {
+ let message_type = self.message_type();
+
+ match self {
+ OlmMessage::Normal(m) => (message_type.into(), m.to_base64()),
+ OlmMessage::PreKey(m) => (message_type.into(), m.to_base64()),
+ }
+ }
+}
+
+/// An enum over the two supported message types.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum MessageType {
+ /// The pre-key message type.
+ PreKey = 0,
+ /// The normal message type.
+ Normal = 1,
+}
+
+impl TryFrom<usize> for MessageType {
+ type Error = ();
+
+ fn try_from(value: usize) -> Result<Self, Self::Error> {
+ match value {
+ 0 => Ok(MessageType::PreKey),
+ 1 => Ok(MessageType::Normal),
+ _ => Err(()),
+ }
+ }
+}
+
+impl From<MessageType> for usize {
+ fn from(value: MessageType) -> usize {
+ value as usize
+ }
+}
+
+#[cfg(test)]
+use olm_rs::session::OlmMessage as LibolmMessage;
+
+#[cfg(test)]
+impl From<LibolmMessage> for OlmMessage {
+ fn from(other: LibolmMessage) -> Self {
+ let (message_type, ciphertext) = other.to_tuple();
+
+ Self::from_parts(message_type.into(), &ciphertext).expect("Can't decode a libolm message")
+ }
+}
+
+#[cfg(test)]
+impl From<OlmMessage> for LibolmMessage {
+ fn from(value: OlmMessage) -> LibolmMessage {
+ match value {
+ OlmMessage::Normal(m) => LibolmMessage::from_type_and_ciphertext(1, m.to_base64())
+ .expect("Can't create a valid libolm message"),
+ OlmMessage::PreKey(m) => LibolmMessage::from_type_and_ciphertext(0, m.to_base64())
+ .expect("Can't create a valid libolm pre-key message"),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use anyhow::Result;
+ use assert_matches::assert_matches;
+ use serde_json::json;
+
+ use super::*;
+ use crate::run_corpus;
+
+ const PRE_KEY_MESSAGE: &str = "AwoghAEuxPZ+w7M3pgUae4tDNiggUpOsQ/zci457VAti\
+ AEYSIO3xOKRDBWKicIfxjSmYCYZ9DD4RMLjvvclbMlE5\
+ yIEWGiApLrCr853CKlPpW4Bi7S8ykRcejJ0lq7AfYLXK\
+ CjKdHSJPAwoghw3+P+cajhWj9Qzp5g87h+tbpiuh5wEa\
+ eUppqmWqug4QASIgRhZ2cgZcIWQbIa23R7U4y1Mo1R/t\
+ LCaMU+xjzRV5smGsCrJ6AHwktg";
+
+ const MESSAGE: &str = "AwogI7JhE/UsMZqXKb3xV6kUZWoJc6jTm2+AIgWYmaETIR0QASIQ\
+ +X2zb7kEX/3JvoLspcNBcLWOFXYpV0nS";
+
+ #[test]
+ fn message_type_from_usize() {
+ assert_eq!(
+ MessageType::try_from(0),
+ Ok(MessageType::PreKey),
+ "0 should denote a pre-key Olm message"
+ );
+ assert_eq!(
+ MessageType::try_from(1),
+ Ok(MessageType::Normal),
+ "1 should denote a normal Olm message"
+ );
+ assert!(
+ MessageType::try_from(2).is_err(),
+ "2 should be recognized as an unknown Olm message type"
+ );
+ }
+
+ #[test]
+ fn from_json() -> Result<()> {
+ let value = json!({
+ "type": 0u8,
+ "body": PRE_KEY_MESSAGE,
+ });
+
+ let message: OlmMessage = serde_json::from_value(value.clone())?;
+ assert_matches!(message, OlmMessage::PreKey(_));
+
+ let serialized = serde_json::to_value(message)?;
+ assert_eq!(value, serialized, "The serialization cycle isn't a noop");
+
+ let value = json!({
+ "type": 1u8,
+ "body": MESSAGE,
+ });
+
+ let message: OlmMessage = serde_json::from_value(value.clone())?;
+ assert_matches!(message, OlmMessage::Normal(_));
+
+ let serialized = serde_json::to_value(message)?;
+ assert_eq!(value, serialized, "The serialization cycle isn't a noop");
+
+ Ok(())
+ }
+
+ #[test]
+ fn from_parts() -> Result<()> {
+ let message = OlmMessage::from_parts(0, PRE_KEY_MESSAGE)?;
+ assert_matches!(message, OlmMessage::PreKey(_));
+ assert_eq!(
+ message.message_type(),
+ MessageType::PreKey,
+ "Expected message to be recognized as a pre-key Olm message."
+ );
+
+ assert_eq!(message.to_parts(), (0, PRE_KEY_MESSAGE.to_string()), "Roundtrip not identity.");
+
+ let message = OlmMessage::from_parts(1, MESSAGE)?;
+ assert_eq!(
+ message.message_type(),
+ MessageType::Normal,
+ "Expected message to be recognized as a normal Olm message."
+ );
+ assert_eq!(message.to_parts(), (1, MESSAGE.to_string()), "Roundtrip not identity.");
+
+ OlmMessage::from_parts(3, PRE_KEY_MESSAGE)
+ .expect_err("Unknown message types can't be parsed");
+
+ Ok(())
+ }
+
+ #[test]
+ fn fuzz_corpus_decoding() {
+ run_corpus("olm-message-decoding", |data| {
+ let _ = PreKeyMessage::from_bytes(data);
+ });
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +
// Copyright 2021 Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::fmt::Debug;
+
+use prost::Message as ProstMessage;
+use serde::{Deserialize, Serialize};
+
+use super::Message;
+use crate::{
+ olm::SessionKeys,
+ utilities::{base64_decode, base64_encode},
+ Curve25519PublicKey, DecodeError,
+};
+
+/// An encrypted Olm pre-key message.
+///
+/// Contains metadata that is required to establish a [`Session`] and a normal
+/// Olm [`Message`].
+///
+/// [`Session`]: crate::olm::Session
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct PreKeyMessage {
+ pub(crate) session_keys: SessionKeys,
+ pub(crate) message: Message,
+}
+
+impl PreKeyMessage {
+ const VERSION: u8 = 3;
+
+ /// The single-use key that was uploaded to a public key directory by the
+ /// receiver of the message. Should be used to establish a [`Session`].
+ ///
+ /// [`Session`]: crate::olm::Session
+ pub fn one_time_key(&self) -> Curve25519PublicKey {
+ self.session_keys.one_time_key
+ }
+
+ /// The base key, a single use key that was created just in time by the
+ /// sender of the message. Should be used to establish a [`Session`].
+ ///
+ /// [`Session`]: crate::olm::Session
+ pub fn base_key(&self) -> Curve25519PublicKey {
+ self.session_keys.base_key
+ }
+
+ /// The long term identity key of the sender of the message. Should be used
+ /// to establish a [`Session`]
+ ///
+ /// [`Session`]: crate::olm::Session
+ pub fn identity_key(&self) -> Curve25519PublicKey {
+ self.session_keys.identity_key
+ }
+
+ /// The collection of all keys required for establishing an Olm [`Session`]
+ /// from this pre-key message.
+ ///
+ /// Other methods on this struct (like [`PreKeyMessage::identity_key()`])
+ /// can be used to retrieve individual keys from this collection.
+ ///
+ /// [`Session`]: crate::olm::Session
+ pub fn session_keys(&self) -> SessionKeys {
+ self.session_keys
+ }
+
+ /// Returns the globally unique session ID, in base64-encoded form.
+ ///
+ /// This is a shorthand helper of the [`SessionKeys::session_id()`] method.
+ pub fn session_id(&self) -> String {
+ self.session_keys.session_id()
+ }
+
+ /// The actual message that contains the ciphertext.
+ pub fn message(&self) -> &Message {
+ &self.message
+ }
+
+ /// Try to decode the given byte slice as a Olm [`Message`].
+ ///
+ /// The expected format of the byte array is described in the
+ /// [`PreKeyMessage::to_bytes()`] method.
+ pub fn from_bytes(message: &[u8]) -> Result<Self, DecodeError> {
+ Self::try_from(message)
+ }
+
+ /// Encode the `PreKeyMessage` as an array of bytes.
+ ///
+ /// Olm `PreKeyMessage`s consist of a one-byte version, followed by a
+ /// variable length payload.
+ ///
+ /// ```text
+ /// +--------------+------------------------------------+
+ /// | Version Byte | Payload Bytes |
+ /// +--------------+------------------------------------+
+ /// ```
+ ///
+ /// The payload uses a format based on the Protocol Buffers encoding. It
+ /// consists of the following key-value pairs:
+ ///
+ /// **Name** |**Tag**|**Type**| **Meaning**
+ /// :----------:|:-----:|:------:|:----------------------------------------:
+ /// One-Time-Key| 0x0A | String |The public part of Bob's single-use key
+ /// Base-Key | 0x12 | String |The public part of Alice's single-use key
+ /// Identity-Key| 0x1A | String |The public part of Alice's identity key
+ /// Message | 0x22 | String |An embedded Olm message
+ ///
+ /// The last key/value pair in a [`PreKeyMessage`] is a normal Olm
+ /// [`Message`].
+ pub fn to_bytes(&self) -> Vec<u8> {
+ let message = ProtoBufPreKeyMessage {
+ one_time_key: self.session_keys.one_time_key.as_bytes().to_vec(),
+ base_key: self.session_keys.base_key.as_bytes().to_vec(),
+ identity_key: self.session_keys.identity_key.as_bytes().to_vec(),
+ message: self.message.to_bytes(),
+ };
+
+ let mut output: Vec<u8> = vec![0u8; message.encoded_len() + 1];
+ output[0] = Self::VERSION;
+
+ message
+ .encode(&mut output[1..].as_mut())
+ .expect("Couldn't encode our message into a protobuf");
+
+ output
+ }
+
+ /// Try to decode the given string as a Olm [`PreKeyMessage`].
+ ///
+ /// The string needs to be a base64 encoded byte array that follows the
+ /// format described in the [`PreKeyMessage::to_bytes()`] method.
+ pub fn from_base64(message: &str) -> Result<Self, DecodeError> {
+ Self::try_from(message)
+ }
+
+ /// Encode the [`PreKeyMessage`] as a string.
+ ///
+ /// This method first calls [`PreKeyMessage::to_bytes()`] and then encodes
+ /// the resulting byte array as a string using base64 encoding.
+ pub fn to_base64(&self) -> String {
+ base64_encode(self.to_bytes())
+ }
+
+ /// Create a new pre-key message from the session keys and standard message.
+ #[cfg(feature = "low-level-api")]
+ pub fn wrap(session_keys: SessionKeys, message: Message) -> Self {
+ PreKeyMessage::new(session_keys, message)
+ }
+
+ pub(crate) fn new(session_keys: SessionKeys, message: Message) -> Self {
+ Self { session_keys, message }
+ }
+}
+
+impl Serialize for PreKeyMessage {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let message = self.to_base64();
+ serializer.serialize_str(&message)
+ }
+}
+
+impl<'de> Deserialize<'de> for PreKeyMessage {
+ fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
+ let ciphertext = String::deserialize(d)?;
+ PreKeyMessage::from_base64(&ciphertext).map_err(serde::de::Error::custom)
+ }
+}
+
+impl TryFrom<&str> for PreKeyMessage {
+ type Error = DecodeError;
+
+ fn try_from(value: &str) -> Result<Self, Self::Error> {
+ let decoded = base64_decode(value)?;
+
+ Self::try_from(decoded)
+ }
+}
+
+impl TryFrom<Vec<u8>> for PreKeyMessage {
+ type Error = DecodeError;
+
+ fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
+ Self::try_from(value.as_slice())
+ }
+}
+
+impl TryFrom<&[u8]> for PreKeyMessage {
+ type Error = DecodeError;
+
+ fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
+ let version = *value.first().ok_or(DecodeError::MissingVersion)?;
+
+ if version != Self::VERSION {
+ Err(DecodeError::InvalidVersion(Self::VERSION, version))
+ } else {
+ let decoded = ProtoBufPreKeyMessage::decode(&value[1..value.len()])?;
+ let one_time_key = Curve25519PublicKey::from_slice(&decoded.one_time_key)?;
+ let base_key = Curve25519PublicKey::from_slice(&decoded.base_key)?;
+ let identity_key = Curve25519PublicKey::from_slice(&decoded.identity_key)?;
+
+ let message = decoded.message.try_into()?;
+
+ let session_keys = SessionKeys { one_time_key, identity_key, base_key };
+
+ Ok(Self { session_keys, message })
+ }
+ }
+}
+
+#[derive(Clone, ProstMessage)]
+struct ProtoBufPreKeyMessage {
+ #[prost(bytes, tag = "1")]
+ one_time_key: Vec<u8>,
+ #[prost(bytes, tag = "2")]
+ base_key: Vec<u8>,
+ #[prost(bytes, tag = "3")]
+ identity_key: Vec<u8>,
+ #[prost(bytes, tag = "4")]
+ message: Vec<u8>,
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +
// Copyright 2021 The Matrix.org Foundation C.I.C.
+// Copyright 2021 Damir Jelić, Denis Kasak
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! An implementation of the Olm double ratchet.
+//!
+//! ## Overview
+//!
+//! The core component of the crate is the `Account`, representing a single Olm
+//! participant. An Olm `Account` consists of a collection of key pairs, though
+//! often documentation will shorten this to just "keys". These are:
+//!
+//! 1. An Ed25519 *signing key pair* representing the stable cryptographic
+//! identity of the participant (the participant's "fingerprint").
+//! 2. A Curve25519 *sender key pair* (also sometimes called the *identity key
+//! pair*, somewhat confusingly).
+//! 3. A number of one-time key pairs.
+//! 4. A current and previous (if any) "fallback" key pair.
+//!
+//! While the key in 1 is used for signing but not encryption, the keys in 2-4
+//! participate in a triple Diffie-Hellman key exchange (3DH) with another Olm
+//! participant, thereby establishing an Olm session on each side of the
+//! communication channel. Ultimately, this session is used for deriving the
+//! concrete encryption keys for a particular message.
+//!
+//! Olm sessions are represented by the `Session` struct. Such a session is
+//! created by calling `Account::create_outbound_session` on one of the
+//! participating accounts, passing it the Curve25519 sender key and one
+//! Curve25519 one-time key of the other side. The protocol is asynchronous, so
+//! the participant can start sending messages to the other side even before the
+//! other side has created a session, producing so-called pre-key messages (see
+//! `PreKeyMessage`).
+//!
+//! Once the other participant receives such a pre-key message, they can create
+//! their own matching session by calling `Account::create_inbound_session` and
+//! passing it the pre-key message they received and the Curve25519 sender key
+//! of the other side. This completes the establishment of the Olm communication
+//! channel.
+//!
+//! ```rust
+//! use anyhow::Result;
+//! use vodozemac::olm::{Account, InboundCreationResult, OlmMessage, SessionConfig};
+//!
+//! fn main() -> Result<()> {
+//! let alice = Account::new();
+//! let mut bob = Account::new();
+//!
+//! bob.generate_one_time_keys(1);
+//! let bob_otk = *bob.one_time_keys().values().next().unwrap();
+//!
+//! let mut alice_session = alice
+//! .create_outbound_session(SessionConfig::version_2(), bob.curve25519_key(), bob_otk);
+//!
+//! bob.mark_keys_as_published();
+//!
+//! let message = "Keep it between us, OK?";
+//! let alice_msg = alice_session.encrypt(message);
+//!
+//! if let OlmMessage::PreKey(m) = alice_msg.clone() {
+//! let result = bob.create_inbound_session(alice.curve25519_key(), &m)?;
+//!
+//! let mut bob_session = result.session;
+//! let what_bob_received = result.plaintext;
+//!
+//! assert_eq!(alice_session.session_id(), bob_session.session_id());
+//!
+//! assert_eq!(message.as_bytes(), what_bob_received);
+//!
+//! let bob_reply = "Yes. Take this, it's dangerous out there!";
+//! let bob_encrypted_reply = bob_session.encrypt(bob_reply).into();
+//!
+//! let what_alice_received = alice_session
+//! .decrypt(&bob_encrypted_reply)?;
+//! assert_eq!(what_alice_received, bob_reply.as_bytes());
+//! }
+//!
+//! Ok(())
+//! }
+//! ```
+//!
+//! ## Sending messages
+//!
+//! To encrypt a message, just call `Session::encrypt(msg_content)`. This will
+//! either produce an `OlmMessage::PreKey(..)` or `OlmMessage::Normal(..)`
+//! depending on whether the session is fully established. A session is fully
+//! established once you receive (and decrypt) at least one message from the
+//! other side.
+
+mod account;
+mod messages;
+pub(crate) mod session;
+mod session_config;
+mod session_keys;
+mod shared_secret;
+
+pub use account::{
+ Account, AccountPickle, IdentityKeys, InboundCreationResult, OneTimeKeyGenerationResult,
+ SessionCreationError,
+};
+pub use messages::{Message, MessageType, OlmMessage, PreKeyMessage};
+pub use session::{ratchet::RatchetPublicKey, DecryptionError, Session, SessionPickle};
+pub use session_config::SessionConfig;
+pub use session_keys::SessionKeys;
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +
// Copyright 2021 Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use hmac::{Hmac, Mac};
+use serde::{Deserialize, Serialize};
+use sha2::{digest::CtOutput, Sha256};
+use zeroize::Zeroize;
+
+use super::{
+ message_key::{MessageKey, RemoteMessageKey},
+ ratchet::RatchetPublicKey,
+};
+
+const MESSAGE_KEY_SEED: &[u8; 1] = b"\x01";
+const ADVANCEMENT_SEED: &[u8; 1] = b"\x02";
+
+fn expand_chain_key(key: &[u8; 32]) -> Box<[u8; 32]> {
+ let mut mac =
+ Hmac::<Sha256>::new_from_slice(key).expect("Can't create HmacSha256 from the key");
+ mac.update(MESSAGE_KEY_SEED);
+
+ let mut output = mac.finalize().into_bytes();
+
+ let mut key = Box::new([0u8; 32]);
+ key.copy_from_slice(output.as_slice());
+
+ output.zeroize();
+
+ key
+}
+
+fn advance(key: &[u8; 32]) -> CtOutput<Hmac<Sha256>> {
+ let mut mac = Hmac::<Sha256>::new_from_slice(key)
+ .expect("Couldn't create a valid Hmac object to advance the ratchet");
+ mac.update(ADVANCEMENT_SEED);
+
+ mac.finalize()
+}
+
+#[derive(Clone, Zeroize, Serialize, Deserialize)]
+#[zeroize(drop)]
+pub(super) struct ChainKey {
+ key: Box<[u8; 32]>,
+ index: u64,
+}
+
+#[derive(Clone, Zeroize, Serialize, Deserialize)]
+#[zeroize(drop)]
+pub(super) struct RemoteChainKey {
+ key: Box<[u8; 32]>,
+ index: u64,
+}
+
+impl RemoteChainKey {
+ pub fn new(bytes: Box<[u8; 32]>) -> Self {
+ Self { key: bytes, index: 0 }
+ }
+
+ pub fn chain_index(&self) -> u64 {
+ self.index
+ }
+
+ #[cfg(feature = "libolm-compat")]
+ pub fn from_bytes_and_index(bytes: Box<[u8; 32]>, index: u32) -> Self {
+ Self { key: bytes, index: index.into() }
+ }
+
+ pub fn advance(&mut self) {
+ let output = advance(&self.key).into_bytes();
+ self.key.copy_from_slice(output.as_slice());
+ self.index += 1;
+ }
+
+ pub fn create_message_key(&mut self) -> RemoteMessageKey {
+ let key = expand_chain_key(&self.key);
+ let message_key = RemoteMessageKey::new(key, self.index);
+
+ self.advance();
+
+ message_key
+ }
+}
+
+impl ChainKey {
+ pub fn new(bytes: Box<[u8; 32]>) -> Self {
+ Self { key: bytes, index: 0 }
+ }
+
+ #[cfg(feature = "libolm-compat")]
+ pub fn from_bytes_and_index(bytes: Box<[u8; 32]>, index: u32) -> Self {
+ Self { key: bytes, index: index.into() }
+ }
+
+ pub fn advance(&mut self) {
+ let output = advance(&self.key).into_bytes();
+ self.key.copy_from_slice(output.as_slice());
+ self.index += 1;
+ }
+
+ pub fn index(&self) -> u64 {
+ self.index
+ }
+
+ pub fn create_message_key(&mut self, ratchet_key: RatchetPublicKey) -> MessageKey {
+ let key = expand_chain_key(&self.key);
+ let message_key = MessageKey::new(key, ratchet_key, self.index);
+
+ self.advance();
+
+ message_key
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +
// Copyright 2021 Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use serde::{Deserialize, Serialize};
+
+use super::{
+ chain_key::ChainKey,
+ message_key::MessageKey,
+ ratchet::{Ratchet, RatchetPublicKey, RemoteRatchetKey},
+ receiver_chain::ReceiverChain,
+ root_key::{RemoteRootKey, RootKey},
+};
+use crate::olm::{messages::Message, shared_secret::Shared3DHSecret};
+
+#[derive(Serialize, Deserialize, Clone)]
+#[serde(transparent)]
+pub(super) struct DoubleRatchet {
+ inner: DoubleRatchetState,
+}
+
+impl DoubleRatchet {
+ pub fn chain_index(&self) -> Option<u64> {
+ match &self.inner {
+ DoubleRatchetState::Inactive(_) => None,
+ DoubleRatchetState::Active(r) => Some(r.symmetric_key_ratchet.index()),
+ }
+ }
+
+ pub fn next_message_key(&mut self) -> MessageKey {
+ match &mut self.inner {
+ DoubleRatchetState::Inactive(ratchet) => {
+ let mut ratchet = ratchet.activate();
+
+ let message_key = ratchet.next_message_key();
+ self.inner = DoubleRatchetState::Active(ratchet);
+
+ message_key
+ }
+ DoubleRatchetState::Active(ratchet) => ratchet.next_message_key(),
+ }
+ }
+
+ pub fn encrypt(&mut self, plaintext: &[u8]) -> Message {
+ self.next_message_key().encrypt(plaintext)
+ }
+
+ pub fn encrypt_truncated_mac(&mut self, plaintext: &[u8]) -> Message {
+ self.next_message_key().encrypt_truncated_mac(plaintext)
+ }
+
+ pub fn active(shared_secret: Shared3DHSecret) -> Self {
+ let (root_key, chain_key) = shared_secret.expand();
+
+ let root_key = RootKey::new(root_key);
+ let chain_key = ChainKey::new(chain_key);
+
+ let ratchet = ActiveDoubleRatchet {
+ active_ratchet: Ratchet::new(root_key),
+ symmetric_key_ratchet: chain_key,
+ };
+
+ Self { inner: ratchet.into() }
+ }
+
+ #[cfg(feature = "libolm-compat")]
+ pub fn from_ratchet_and_chain_key(ratchet: Ratchet, chain_key: ChainKey) -> Self {
+ Self {
+ inner: ActiveDoubleRatchet {
+ active_ratchet: ratchet,
+ symmetric_key_ratchet: chain_key,
+ }
+ .into(),
+ }
+ }
+
+ pub fn inactive(root_key: RemoteRootKey, ratchet_key: RemoteRatchetKey) -> Self {
+ let ratchet = InactiveDoubleRatchet { root_key, ratchet_key };
+
+ Self { inner: ratchet.into() }
+ }
+
+ pub fn advance(&mut self, ratchet_key: RemoteRatchetKey) -> (DoubleRatchet, ReceiverChain) {
+ let (ratchet, receiver_chain) = match &self.inner {
+ DoubleRatchetState::Active(r) => r.advance(ratchet_key),
+ DoubleRatchetState::Inactive(r) => {
+ let ratchet = r.activate();
+ // Advancing an inactive ratchet shouldn't be possible since the
+ // other side did not yet receive our new ratchet key.
+ //
+ // This will likely end up in a decryption error but for
+ // consistency sake and avoiding the leakage of our internal
+ // state it's better to error out there.
+ let ret = ratchet.advance(ratchet_key);
+
+ self.inner = ratchet.into();
+
+ ret
+ }
+ };
+
+ (Self { inner: DoubleRatchetState::Inactive(ratchet) }, receiver_chain)
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+#[serde(rename_all = "snake_case")]
+#[serde(tag = "type")]
+enum DoubleRatchetState {
+ Inactive(InactiveDoubleRatchet),
+ Active(ActiveDoubleRatchet),
+}
+
+impl From<InactiveDoubleRatchet> for DoubleRatchetState {
+ fn from(r: InactiveDoubleRatchet) -> Self {
+ Self::Inactive(r)
+ }
+}
+
+impl From<ActiveDoubleRatchet> for DoubleRatchetState {
+ fn from(r: ActiveDoubleRatchet) -> Self {
+ Self::Active(r)
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+struct InactiveDoubleRatchet {
+ root_key: RemoteRootKey,
+ ratchet_key: RemoteRatchetKey,
+}
+
+impl InactiveDoubleRatchet {
+ fn activate(&self) -> ActiveDoubleRatchet {
+ let (root_key, chain_key, ratchet_key) = self.root_key.advance(&self.ratchet_key);
+ let active_ratchet = Ratchet::new_with_ratchet_key(root_key, ratchet_key);
+
+ ActiveDoubleRatchet { active_ratchet, symmetric_key_ratchet: chain_key }
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+struct ActiveDoubleRatchet {
+ active_ratchet: Ratchet,
+ symmetric_key_ratchet: ChainKey,
+}
+
+impl ActiveDoubleRatchet {
+ fn advance(&self, ratchet_key: RemoteRatchetKey) -> (InactiveDoubleRatchet, ReceiverChain) {
+ let (root_key, remote_chain) = self.active_ratchet.advance(ratchet_key);
+
+ let ratchet = InactiveDoubleRatchet { root_key, ratchet_key };
+ let receiver_chain = ReceiverChain::new(ratchet_key, remote_chain);
+
+ (ratchet, receiver_chain)
+ }
+
+ fn ratchet_key(&self) -> RatchetPublicKey {
+ RatchetPublicKey::from(self.active_ratchet.ratchet_key())
+ }
+
+ fn next_message_key(&mut self) -> MessageKey {
+ self.symmetric_key_ratchet.create_message_key(self.ratchet_key())
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +
// Copyright 2021 Damir Jelić, Denis Kasak
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::fmt::Debug;
+
+use serde::{Deserialize, Serialize};
+use zeroize::Zeroize;
+
+use super::{ratchet::RatchetPublicKey, DecryptionError};
+use crate::{
+ cipher::{Cipher, Mac},
+ olm::messages::Message,
+};
+
+pub struct MessageKey {
+ key: Box<[u8; 32]>,
+ ratchet_key: RatchetPublicKey,
+ index: u64,
+}
+
+impl Drop for MessageKey {
+ fn drop(&mut self) {
+ self.key.zeroize()
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub(super) struct RemoteMessageKey {
+ pub key: Box<[u8; 32]>,
+ pub index: u64,
+}
+
+impl Debug for RemoteMessageKey {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self { key: _, index } = self;
+
+ f.debug_struct("RemoteMessageKey").field("index", index).finish()
+ }
+}
+
+impl Drop for RemoteMessageKey {
+ fn drop(&mut self) {
+ self.key.zeroize()
+ }
+}
+
+impl MessageKey {
+ pub fn new(key: Box<[u8; 32]>, ratchet_key: RatchetPublicKey, index: u64) -> Self {
+ Self { key, ratchet_key, index }
+ }
+
+ pub fn encrypt_truncated_mac(self, plaintext: &[u8]) -> Message {
+ let cipher = Cipher::new(&self.key);
+
+ let ciphertext = cipher.encrypt(plaintext);
+
+ let mut message =
+ Message::new_truncated_mac(*self.ratchet_key.as_ref(), self.index, ciphertext);
+
+ let mac = cipher.mac(&message.to_mac_bytes());
+ message.set_mac(mac);
+
+ message
+ }
+
+ pub fn encrypt(self, plaintext: &[u8]) -> Message {
+ let cipher = Cipher::new(&self.key);
+
+ let ciphertext = cipher.encrypt(plaintext);
+
+ let mut message = Message::new(*self.ratchet_key.as_ref(), self.index, ciphertext);
+
+ let mac = cipher.mac(&message.to_mac_bytes());
+ message.set_mac(mac);
+
+ message
+ }
+
+ /// Get a reference to the message key's key.
+ #[cfg(feature = "low-level-api")]
+ pub fn key(&self) -> &[u8; 32] {
+ self.key.as_ref()
+ }
+
+ /// Get the message key's ratchet key.
+ #[cfg(feature = "low-level-api")]
+ pub fn ratchet_key(&self) -> RatchetPublicKey {
+ self.ratchet_key
+ }
+
+ /// Get the message key's index.
+ #[cfg(feature = "low-level-api")]
+ pub fn index(&self) -> u64 {
+ self.index
+ }
+}
+
+impl RemoteMessageKey {
+ pub fn new(key: Box<[u8; 32]>, index: u64) -> Self {
+ Self { key, index }
+ }
+
+ pub fn chain_index(&self) -> u64 {
+ self.index
+ }
+
+ pub fn decrypt_truncated_mac(&self, message: &Message) -> Result<Vec<u8>, DecryptionError> {
+ let cipher = Cipher::new(&self.key);
+
+ if let crate::cipher::MessageMac::Truncated(m) = &message.mac {
+ cipher.verify_truncated_mac(&message.to_mac_bytes(), m)?;
+ Ok(cipher.decrypt(&message.ciphertext)?)
+ } else {
+ Err(DecryptionError::InvalidMACLength(Mac::TRUNCATED_LEN, Mac::LENGTH))
+ }
+ }
+
+ pub fn decrypt(&self, message: &Message) -> Result<Vec<u8>, DecryptionError> {
+ let cipher = Cipher::new(&self.key);
+
+ if let crate::cipher::MessageMac::Full(m) = &message.mac {
+ cipher.verify_mac(&message.to_mac_bytes(), m)?;
+ Ok(cipher.decrypt(&message.ciphertext)?)
+ } else {
+ Err(DecryptionError::InvalidMACLength(Mac::LENGTH, Mac::TRUNCATED_LEN))
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +
// Copyright 2021 Damir Jelić
+// Copyright 2021 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+mod chain_key;
+mod double_ratchet;
+pub mod message_key;
+pub mod ratchet;
+mod receiver_chain;
+mod root_key;
+
+use std::fmt::Debug;
+
+use aes::cipher::block_padding::UnpadError;
+use arrayvec::ArrayVec;
+use chain_key::RemoteChainKey;
+use double_ratchet::DoubleRatchet;
+use hmac::digest::MacError;
+use ratchet::RemoteRatchetKey;
+use receiver_chain::ReceiverChain;
+use root_key::RemoteRootKey;
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+use zeroize::Zeroize;
+
+use super::{
+ session_config::Version,
+ session_keys::SessionKeys,
+ shared_secret::{RemoteShared3DHSecret, Shared3DHSecret},
+ SessionConfig,
+};
+#[cfg(feature = "low-level-api")]
+use crate::hazmat::olm::MessageKey;
+use crate::{
+ olm::messages::{Message, OlmMessage, PreKeyMessage},
+ utilities::{pickle, unpickle},
+ Curve25519PublicKey, PickleError,
+};
+
+const MAX_RECEIVING_CHAINS: usize = 5;
+
+/// Error type for Olm-based decryption failures.
+#[derive(Error, Debug)]
+pub enum DecryptionError {
+ /// The message authentication code of the message was invalid.
+ #[error("Failed decrypting Olm message, invalid MAC: {0}")]
+ InvalidMAC(#[from] MacError),
+ /// The length of the message authentication code of the message did not
+ /// match our expected length.
+ #[error("Failed decrypting Olm message, invalid MAC length: expected {0}, got {1}")]
+ InvalidMACLength(usize, usize),
+ /// The ciphertext of the message isn't padded correctly.
+ #[error("Failed decrypting Olm message, invalid padding")]
+ InvalidPadding(#[from] UnpadError),
+ /// The session is missing the correct message key to decrypt the message,
+ /// either because it was already used up, or because the Session has been
+ /// ratcheted forwards and the message key has been discarded.
+ #[error("The message key with the given key can't be created, message index: {0}")]
+ MissingMessageKey(u64),
+ /// Too many messages have been skipped to attempt decrypting this message.
+ #[error("The message gap was too big, got {0}, max allowed {1}")]
+ TooBigMessageGap(u64, u64),
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+struct ChainStore {
+ inner: ArrayVec<ReceiverChain, MAX_RECEIVING_CHAINS>,
+}
+
+impl ChainStore {
+ fn new() -> Self {
+ Self { inner: ArrayVec::new() }
+ }
+
+ fn push(&mut self, ratchet: ReceiverChain) {
+ if self.inner.is_full() {
+ self.inner.pop_at(0);
+ }
+
+ self.inner.push(ratchet)
+ }
+
+ fn is_empty(&self) -> bool {
+ self.inner.is_empty()
+ }
+
+ #[cfg(test)]
+ pub fn len(&self) -> usize {
+ self.inner.len()
+ }
+
+ #[cfg(feature = "libolm-compat")]
+ pub fn get(&self, index: usize) -> Option<&ReceiverChain> {
+ self.inner.get(index)
+ }
+
+ fn find_ratchet(&mut self, ratchet_key: &RemoteRatchetKey) -> Option<&mut ReceiverChain> {
+ self.inner.iter_mut().find(|r| r.belongs_to(ratchet_key))
+ }
+}
+
+impl Default for ChainStore {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// An Olm session represents one end of an encrypted communication channel
+/// between two participants.
+///
+/// A session enables enables the session owner to encrypt messages intended
+/// for, and decrypt messages sent by, the other participant of the channel.
+///
+/// Olm sessions have two important properties:
+///
+/// 1. They are based on a double ratchet algorithm which continuously
+/// introduces new entropy into the channel as messages are sent and
+/// received. This imbues the channel with *self-healing* properties,
+/// allowing it to recover from a momentary loss of confidentiality in the event
+/// of a key compromise.
+/// 2. They are *asynchronous*, allowing the participant to start sending
+/// messages to the other side even if the other participant is not online at
+/// the moment.
+///
+/// An Olm [`Session`] is acquired from an [`Account`], by calling either
+///
+/// - [`Account::create_outbound_session`], if you are the first participant to
+/// send a message in
+/// this channel, or
+/// - [`Account::create_inbound_session`], if the other participant initiated
+/// the channel by
+/// sending you a message.
+///
+/// [`Account`]: crate::olm::Account
+/// [`Account::create_outbound_session`]: crate::olm::Account::create_outbound_session
+/// [`Account::create_inbound_session`]: crate::olm::Account::create_inbound_session
+pub struct Session {
+ session_keys: SessionKeys,
+ sending_ratchet: DoubleRatchet,
+ receiving_chains: ChainStore,
+ config: SessionConfig,
+}
+
+impl Debug for Session {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self { session_keys: _, sending_ratchet, receiving_chains, config } = self;
+
+ f.debug_struct("Session")
+ .field("session_id", &self.session_id())
+ .field("sending_chain_index", &sending_ratchet.chain_index())
+ .field("receiving_chains", &receiving_chains.inner)
+ .field("config", config)
+ .finish_non_exhaustive()
+ }
+}
+
+impl Session {
+ pub(super) fn new(
+ config: SessionConfig,
+ shared_secret: Shared3DHSecret,
+ session_keys: SessionKeys,
+ ) -> Self {
+ let local_ratchet = DoubleRatchet::active(shared_secret);
+
+ Self {
+ session_keys,
+ sending_ratchet: local_ratchet,
+ receiving_chains: Default::default(),
+ config,
+ }
+ }
+
+ pub(super) fn new_remote(
+ config: SessionConfig,
+ shared_secret: RemoteShared3DHSecret,
+ remote_ratchet_key: Curve25519PublicKey,
+ session_keys: SessionKeys,
+ ) -> Self {
+ let (root_key, remote_chain_key) = shared_secret.expand();
+
+ let remote_ratchet_key = RemoteRatchetKey::from(remote_ratchet_key);
+ let root_key = RemoteRootKey::new(root_key);
+ let remote_chain_key = RemoteChainKey::new(remote_chain_key);
+
+ let local_ratchet = DoubleRatchet::inactive(root_key, remote_ratchet_key);
+ let remote_ratchet = ReceiverChain::new(remote_ratchet_key, remote_chain_key);
+
+ let mut ratchet_store = ChainStore::new();
+ ratchet_store.push(remote_ratchet);
+
+ Self {
+ session_keys,
+ sending_ratchet: local_ratchet,
+ receiving_chains: ratchet_store,
+ config,
+ }
+ }
+
+ /// Returns the globally unique session ID, in base64-encoded form.
+ ///
+ /// This is a shorthand helper of the [`SessionKeys::session_id()`] method.
+ pub fn session_id(&self) -> String {
+ self.session_keys.session_id()
+ }
+
+ /// Have we ever received and decrypted a message from the other side?
+ ///
+ /// Used to decide if outgoing messages should be sent as normal or pre-key
+ /// messages.
+ pub fn has_received_message(&self) -> bool {
+ !self.receiving_chains.is_empty()
+ }
+
+ /// Encrypt the `plaintext` and construct an [`OlmMessage`].
+ ///
+ /// The message will either be a pre-key message or a normal message,
+ /// depending on whether the session is fully established. A session is
+ /// fully established once you receive (and decrypt) at least one
+ /// message from the other side.
+ pub fn encrypt(&mut self, plaintext: impl AsRef<[u8]>) -> OlmMessage {
+ let message = match self.config.version {
+ Version::V1 => self.sending_ratchet.encrypt_truncated_mac(plaintext.as_ref()),
+ Version::V2 => self.sending_ratchet.encrypt(plaintext.as_ref()),
+ };
+
+ if self.has_received_message() {
+ OlmMessage::Normal(message)
+ } else {
+ let message = PreKeyMessage::new(self.session_keys, message);
+
+ OlmMessage::PreKey(message)
+ }
+ }
+
+ /// Get the keys associated with this session.
+ pub fn session_keys(&self) -> SessionKeys {
+ self.session_keys
+ }
+
+ pub fn session_config(&self) -> SessionConfig {
+ self.config
+ }
+
+ /// Get the [`MessageKey`] to encrypt the next message.
+ ///
+ /// **Note**: Each key obtained in this way should be used to encrypt
+ /// a message and the message must then be sent to the recipient.
+ ///
+ /// Failing to do so will increase the number of out-of-order messages on
+ /// the recipient side. Given that a `Session` can only support a limited
+ /// number of out-of-order messages, this will eventually lead to
+ /// undecryptable messages.
+ #[cfg(feature = "low-level-api")]
+ pub fn next_message_key(&mut self) -> MessageKey {
+ self.sending_ratchet.next_message_key()
+ }
+
+ /// Try to decrypt an Olm message, which will either return the plaintext or
+ /// result in a [`DecryptionError`].
+ ///
+ /// [`DecryptionError`]: self::DecryptionError
+ pub fn decrypt(&mut self, message: &OlmMessage) -> Result<Vec<u8>, DecryptionError> {
+ let decrypted = match message {
+ OlmMessage::Normal(m) => self.decrypt_decoded(m)?,
+ OlmMessage::PreKey(m) => self.decrypt_decoded(&m.message)?,
+ };
+
+ Ok(decrypted)
+ }
+
+ pub(super) fn decrypt_decoded(
+ &mut self,
+ message: &Message,
+ ) -> Result<Vec<u8>, DecryptionError> {
+ let ratchet_key = RemoteRatchetKey::from(message.ratchet_key);
+
+ if let Some(ratchet) = self.receiving_chains.find_ratchet(&ratchet_key) {
+ ratchet.decrypt(message, &self.config)
+ } else {
+ let (sending_ratchet, mut remote_ratchet) = self.sending_ratchet.advance(ratchet_key);
+
+ let plaintext = remote_ratchet.decrypt(message, &self.config)?;
+
+ self.sending_ratchet = sending_ratchet;
+ self.receiving_chains.push(remote_ratchet);
+
+ Ok(plaintext)
+ }
+ }
+
+ /// Convert the session into a struct which implements [`serde::Serialize`]
+ /// and [`serde::Deserialize`].
+ pub fn pickle(&self) -> SessionPickle {
+ SessionPickle {
+ session_keys: self.session_keys,
+ sending_ratchet: self.sending_ratchet.clone(),
+ receiving_chains: self.receiving_chains.clone(),
+ config: self.config,
+ }
+ }
+
+ /// Restore a [`Session`] from a previously saved [`SessionPickle`].
+ pub fn from_pickle(pickle: SessionPickle) -> Self {
+ pickle.into()
+ }
+
+ /// Create a [`Session`] object by unpickling a session pickle in libolm
+ /// legacy pickle format.
+ ///
+ /// Such pickles are encrypted and need to first be decrypted using
+ /// `pickle_key`.
+ #[cfg(feature = "libolm-compat")]
+ pub fn from_libolm_pickle(
+ pickle: &str,
+ pickle_key: &[u8],
+ ) -> Result<Self, crate::LibolmPickleError> {
+ use chain_key::ChainKey;
+ use matrix_pickle::Decode;
+ use message_key::RemoteMessageKey;
+ use ratchet::{Ratchet, RatchetKey};
+ use root_key::RootKey;
+
+ use crate::{types::Curve25519SecretKey, utilities::unpickle_libolm};
+
+ #[derive(Debug, Decode, Zeroize)]
+ #[zeroize(drop)]
+ struct SenderChain {
+ public_ratchet_key: [u8; 32],
+ #[secret]
+ secret_ratchet_key: Box<[u8; 32]>,
+ chain_key: Box<[u8; 32]>,
+ chain_key_index: u32,
+ }
+
+ #[derive(Debug, Decode, Zeroize)]
+ #[zeroize(drop)]
+ struct ReceivingChain {
+ public_ratchet_key: [u8; 32],
+ #[secret]
+ chain_key: Box<[u8; 32]>,
+ chain_key_index: u32,
+ }
+
+ impl From<&ReceivingChain> for ReceiverChain {
+ fn from(chain: &ReceivingChain) -> Self {
+ let ratchet_key = RemoteRatchetKey::from(chain.public_ratchet_key);
+ let chain_key = RemoteChainKey::from_bytes_and_index(
+ chain.chain_key.clone(),
+ chain.chain_key_index,
+ );
+
+ ReceiverChain::new(ratchet_key, chain_key)
+ }
+ }
+
+ #[derive(Debug, Decode, Zeroize)]
+ #[zeroize(drop)]
+ struct MessageKey {
+ ratchet_key: [u8; 32],
+ #[secret]
+ message_key: Box<[u8; 32]>,
+ index: u32,
+ }
+
+ impl From<&MessageKey> for RemoteMessageKey {
+ fn from(key: &MessageKey) -> Self {
+ RemoteMessageKey { key: key.message_key.clone(), index: key.index.into() }
+ }
+ }
+
+ #[derive(Decode)]
+ struct Pickle {
+ #[allow(dead_code)]
+ version: u32,
+ #[allow(dead_code)]
+ received_message: bool,
+ session_keys: SessionKeys,
+ #[secret]
+ root_key: Box<[u8; 32]>,
+ sender_chains: Vec<SenderChain>,
+ receiver_chains: Vec<ReceivingChain>,
+ message_keys: Vec<MessageKey>,
+ }
+
+ impl Drop for Pickle {
+ fn drop(&mut self) {
+ self.root_key.zeroize();
+ self.sender_chains.zeroize();
+ self.receiver_chains.zeroize();
+ self.message_keys.zeroize();
+ }
+ }
+
+ impl TryFrom<Pickle> for Session {
+ type Error = crate::LibolmPickleError;
+
+ fn try_from(pickle: Pickle) -> Result<Self, Self::Error> {
+ let mut receiving_chains = ChainStore::new();
+
+ for chain in &pickle.receiver_chains {
+ receiving_chains.push(chain.into())
+ }
+
+ for key in &pickle.message_keys {
+ let ratchet_key =
+ RemoteRatchetKey::from(Curve25519PublicKey::from(key.ratchet_key));
+
+ if let Some(receiving_chain) = receiving_chains.find_ratchet(&ratchet_key) {
+ receiving_chain.insert_message_key(key.into())
+ }
+ }
+
+ if let Some(chain) = pickle.sender_chains.get(0) {
+ // XXX: Passing in secret array as value.
+ let ratchet_key = RatchetKey::from(Curve25519SecretKey::from_slice(
+ chain.secret_ratchet_key.as_ref(),
+ ));
+ let chain_key = ChainKey::from_bytes_and_index(
+ chain.chain_key.clone(),
+ chain.chain_key_index,
+ );
+
+ let root_key = RootKey::new(pickle.root_key.clone());
+
+ let ratchet = Ratchet::new_with_ratchet_key(root_key, ratchet_key);
+ let sending_ratchet =
+ DoubleRatchet::from_ratchet_and_chain_key(ratchet, chain_key);
+
+ Ok(Self {
+ session_keys: pickle.session_keys,
+ sending_ratchet,
+ receiving_chains,
+ config: SessionConfig::version_1(),
+ })
+ } else if let Some(chain) = receiving_chains.get(0) {
+ let sending_ratchet = DoubleRatchet::inactive(
+ RemoteRootKey::new(pickle.root_key.clone()),
+ chain.ratchet_key(),
+ );
+
+ Ok(Self {
+ session_keys: pickle.session_keys,
+ sending_ratchet,
+ receiving_chains,
+ config: SessionConfig::version_1(),
+ })
+ } else {
+ Err(crate::LibolmPickleError::InvalidSession)
+ }
+ }
+ }
+
+ const PICKLE_VERSION: u32 = 1;
+ unpickle_libolm::<Pickle, _>(pickle, pickle_key, PICKLE_VERSION)
+ }
+}
+
+/// A format suitable for serialization which implements [`serde::Serialize`]
+/// and [`serde::Deserialize`]. Obtainable by calling [`Session::pickle`].
+#[derive(Deserialize, Serialize)]
+pub struct SessionPickle {
+ session_keys: SessionKeys,
+ sending_ratchet: DoubleRatchet,
+ receiving_chains: ChainStore,
+ #[serde(default = "default_config")]
+ config: SessionConfig,
+}
+
+fn default_config() -> SessionConfig {
+ SessionConfig::version_1()
+}
+
+impl SessionPickle {
+ /// Serialize and encrypt the pickle using the given key.
+ ///
+ /// This is the inverse of [`SessionPickle::from_encrypted`].
+ pub fn encrypt(self, pickle_key: &[u8; 32]) -> String {
+ pickle(&self, pickle_key)
+ }
+
+ /// Obtain a pickle from a ciphertext by decrypting and deserializing using
+ /// the given key.
+ ///
+ /// This is the inverse of [`SessionPickle::encrypt`].
+ pub fn from_encrypted(ciphertext: &str, pickle_key: &[u8; 32]) -> Result<Self, PickleError> {
+ unpickle(ciphertext, pickle_key)
+ }
+}
+
+impl From<SessionPickle> for Session {
+ fn from(pickle: SessionPickle) -> Self {
+ Self {
+ session_keys: pickle.session_keys,
+ sending_ratchet: pickle.sending_ratchet,
+ receiving_chains: pickle.receiving_chains,
+ config: pickle.config,
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use anyhow::{bail, Result};
+ use olm_rs::{
+ account::OlmAccount,
+ session::{OlmMessage, OlmSession},
+ };
+
+ use super::Session;
+ use crate::{
+ olm::{Account, SessionConfig, SessionPickle},
+ Curve25519PublicKey,
+ };
+
+ const PICKLE_KEY: [u8; 32] = [0u8; 32];
+
+ fn sessions() -> Result<(Account, OlmAccount, Session, OlmSession)> {
+ let alice = Account::new();
+ let bob = OlmAccount::new();
+ bob.generate_one_time_keys(1);
+
+ let one_time_key = bob
+ .parsed_one_time_keys()
+ .curve25519()
+ .values()
+ .next()
+ .cloned()
+ .expect("Couldn't find a one-time key");
+
+ let identity_keys = bob.parsed_identity_keys();
+ let curve25519_key = Curve25519PublicKey::from_base64(identity_keys.curve25519())?;
+ let one_time_key = Curve25519PublicKey::from_base64(&one_time_key)?;
+ let mut alice_session =
+ alice.create_outbound_session(SessionConfig::version_1(), curve25519_key, one_time_key);
+
+ let message = "It's a secret to everybody";
+
+ let olm_message = alice_session.encrypt(message);
+ bob.mark_keys_as_published();
+
+ if let OlmMessage::PreKey(m) = olm_message.into() {
+ let session =
+ bob.create_inbound_session_from(&alice.curve25519_key().to_base64(), m)?;
+
+ Ok((alice, bob, alice_session, session))
+ } else {
+ bail!("Invalid message type");
+ }
+ }
+
+ #[test]
+ fn out_of_order_decryption() -> Result<()> {
+ let (_, _, mut alice_session, bob_session) = sessions()?;
+
+ let message_1 = bob_session.encrypt("Message 1").into();
+ let message_2 = bob_session.encrypt("Message 2").into();
+ let message_3 = bob_session.encrypt("Message 3").into();
+
+ assert_eq!("Message 3".as_bytes(), alice_session.decrypt(&message_3)?);
+ assert_eq!("Message 2".as_bytes(), alice_session.decrypt(&message_2)?);
+ assert_eq!("Message 1".as_bytes(), alice_session.decrypt(&message_1)?);
+
+ Ok(())
+ }
+
+ #[test]
+ fn more_out_of_order_decryption() -> Result<()> {
+ let (_, _, mut alice_session, bob_session) = sessions()?;
+
+ let message_1 = bob_session.encrypt("Message 1").into();
+ let message_2 = bob_session.encrypt("Message 2").into();
+ let message_3 = bob_session.encrypt("Message 3").into();
+
+ assert_eq!("Message 1".as_bytes(), alice_session.decrypt(&message_1)?);
+
+ assert_eq!(alice_session.receiving_chains.len(), 1);
+
+ let message_4 = alice_session.encrypt("Message 4").into();
+ assert_eq!("Message 4", bob_session.decrypt(message_4)?);
+
+ let message_5 = bob_session.encrypt("Message 5").into();
+ assert_eq!("Message 5".as_bytes(), alice_session.decrypt(&message_5)?);
+ assert_eq!("Message 3".as_bytes(), alice_session.decrypt(&message_3)?);
+ assert_eq!("Message 2".as_bytes(), alice_session.decrypt(&message_2)?);
+
+ assert_eq!(alice_session.receiving_chains.len(), 2);
+
+ Ok(())
+ }
+
+ #[test]
+ #[cfg(feature = "libolm-compat")]
+ fn libolm_unpickling() -> Result<()> {
+ let (_, _, mut session, olm) = sessions()?;
+
+ let plaintext = "It's a secret to everybody";
+ let old_message = session.encrypt(plaintext);
+
+ for _ in 0..9 {
+ session.encrypt("Hello");
+ }
+
+ let message = session.encrypt("Hello");
+ olm.decrypt(message.into())?;
+
+ let key = b"DEFAULT_PICKLE_KEY";
+ let pickle = olm.pickle(olm_rs::PicklingMode::Encrypted { key: key.to_vec() });
+
+ let mut unpickled = Session::from_libolm_pickle(&pickle, key)?;
+
+ assert_eq!(olm.session_id(), unpickled.session_id());
+
+ assert_eq!(unpickled.decrypt(&old_message)?, plaintext.as_bytes());
+
+ let message = unpickled.encrypt(plaintext);
+
+ assert_eq!(session.decrypt(&message)?, plaintext.as_bytes());
+
+ Ok(())
+ }
+
+ #[test]
+ fn session_pickling_roundtrip_is_identity() -> Result<()> {
+ let (_, _, session, _) = sessions()?;
+
+ let pickle = session.pickle().encrypt(&PICKLE_KEY);
+
+ let decrypted_pickle = SessionPickle::from_encrypted(&pickle, &PICKLE_KEY)?;
+ let unpickled_group_session = Session::from_pickle(decrypted_pickle);
+ let repickle = unpickled_group_session.pickle();
+
+ assert_eq!(session.session_id(), unpickled_group_session.session_id());
+
+ let decrypted_pickle = SessionPickle::from_encrypted(&pickle, &PICKLE_KEY)?;
+ let pickle = serde_json::to_value(decrypted_pickle)?;
+ let repickle = serde_json::to_value(repickle)?;
+
+ assert_eq!(pickle, repickle);
+
+ Ok(())
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +
// Copyright 2021 Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::fmt::Debug;
+
+use matrix_pickle::Decode;
+use serde::{Deserialize, Serialize};
+use x25519_dalek::SharedSecret;
+
+use super::{
+ chain_key::RemoteChainKey,
+ root_key::{RemoteRootKey, RootKey},
+};
+use crate::{types::Curve25519SecretKey, Curve25519PublicKey};
+
+#[derive(Serialize, Deserialize, Clone)]
+#[serde(transparent)]
+pub(super) struct RatchetKey(Curve25519SecretKey);
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct RatchetPublicKey(Curve25519PublicKey);
+
+#[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Decode)]
+#[serde(transparent)]
+pub struct RemoteRatchetKey(Curve25519PublicKey);
+
+impl Debug for RemoteRatchetKey {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl RatchetKey {
+ pub fn new() -> Self {
+ Self(Curve25519SecretKey::new())
+ }
+
+ pub fn diffie_hellman(&self, other: &RemoteRatchetKey) -> SharedSecret {
+ self.0.diffie_hellman(&other.0)
+ }
+}
+
+impl Default for RatchetKey {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl From<Curve25519SecretKey> for RatchetKey {
+ fn from(key: Curve25519SecretKey) -> Self {
+ Self(key)
+ }
+}
+
+impl From<[u8; 32]> for RatchetPublicKey {
+ fn from(bytes: [u8; 32]) -> Self {
+ RatchetPublicKey(Curve25519PublicKey::from(bytes))
+ }
+}
+
+impl From<[u8; 32]> for RemoteRatchetKey {
+ fn from(bytes: [u8; 32]) -> Self {
+ RemoteRatchetKey(Curve25519PublicKey::from(bytes))
+ }
+}
+
+impl From<Curve25519PublicKey> for RemoteRatchetKey {
+ fn from(key: Curve25519PublicKey) -> Self {
+ RemoteRatchetKey(key)
+ }
+}
+
+impl AsRef<Curve25519PublicKey> for RatchetPublicKey {
+ fn as_ref(&self) -> &Curve25519PublicKey {
+ &self.0
+ }
+}
+
+impl From<&RatchetKey> for RatchetPublicKey {
+ fn from(r: &RatchetKey) -> Self {
+ RatchetPublicKey(Curve25519PublicKey::from(&r.0))
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub(super) struct Ratchet {
+ root_key: RootKey,
+ ratchet_key: RatchetKey,
+}
+
+impl Ratchet {
+ pub fn new(root_key: RootKey) -> Self {
+ let ratchet_key = RatchetKey::new();
+
+ Self { root_key, ratchet_key }
+ }
+
+ pub fn new_with_ratchet_key(root_key: RootKey, ratchet_key: RatchetKey) -> Self {
+ Self { root_key, ratchet_key }
+ }
+
+ pub fn advance(&self, remote_key: RemoteRatchetKey) -> (RemoteRootKey, RemoteChainKey) {
+ let (remote_root_key, remote_chain_key) =
+ self.root_key.advance(&self.ratchet_key, &remote_key);
+
+ (remote_root_key, remote_chain_key)
+ }
+
+ pub fn ratchet_key(&self) -> &RatchetKey {
+ &self.ratchet_key
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +
// Copyright 2021 Damir Jelić, Denis Kasak
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::fmt::Debug;
+
+use arrayvec::ArrayVec;
+use serde::{Deserialize, Serialize};
+
+use super::{
+ chain_key::RemoteChainKey, message_key::RemoteMessageKey, ratchet::RemoteRatchetKey,
+ DecryptionError,
+};
+use crate::olm::{messages::Message, session_config::Version, SessionConfig};
+
+const MAX_MESSAGE_GAP: u64 = 2000;
+const MAX_MESSAGE_KEYS: usize = 40;
+
+#[derive(Serialize, Deserialize, Clone)]
+struct MessageKeyStore {
+ inner: ArrayVec<RemoteMessageKey, MAX_MESSAGE_KEYS>,
+}
+
+impl MessageKeyStore {
+ fn new() -> Self {
+ Self { inner: ArrayVec::new() }
+ }
+
+ fn push(&mut self, message_key: RemoteMessageKey) {
+ if self.inner.is_full() {
+ self.inner.pop_at(0);
+ }
+
+ self.inner.push(message_key)
+ }
+
+ fn merge(&mut self, mut store: MessageKeyStore) {
+ for key in store.inner.drain(..) {
+ self.push(key);
+ }
+ }
+
+ fn get_message_key(&self, chain_index: u64) -> Option<&RemoteMessageKey> {
+ self.inner.iter().find(|k| k.chain_index() == chain_index)
+ }
+
+ fn remove_message_key(&mut self, chain_index: u64) {
+ self.inner.retain(|k| k.chain_index() != chain_index);
+ }
+}
+
+impl Default for MessageKeyStore {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+enum FoundMessageKey<'a> {
+ Existing(&'a RemoteMessageKey),
+ New(Box<(RemoteChainKey, MessageKeyStore, RemoteMessageKey)>),
+}
+
+impl FoundMessageKey<'_> {
+ fn decrypt(
+ &self,
+ message: &Message,
+ config: &SessionConfig,
+ ) -> Result<Vec<u8>, DecryptionError> {
+ let message_key = match self {
+ FoundMessageKey::Existing(m) => m,
+ FoundMessageKey::New(m) => &m.2,
+ };
+
+ match config.version {
+ Version::V1 => message_key.decrypt_truncated_mac(message),
+ Version::V2 => message_key.decrypt(message),
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub(super) struct ReceiverChain {
+ ratchet_key: RemoteRatchetKey,
+ hkdf_ratchet: RemoteChainKey,
+ skipped_message_keys: MessageKeyStore,
+}
+
+impl Debug for ReceiverChain {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self { ratchet_key: _, hkdf_ratchet, skipped_message_keys } = self;
+
+ f.debug_struct("ReceiverChain")
+ .field("chain_index", &hkdf_ratchet.chain_index())
+ .field("skipped_message_keys", &skipped_message_keys.inner)
+ .finish_non_exhaustive()
+ }
+}
+
+impl ReceiverChain {
+ pub fn new(ratchet_key: RemoteRatchetKey, chain_key: RemoteChainKey) -> Self {
+ ReceiverChain {
+ ratchet_key,
+ hkdf_ratchet: chain_key,
+ skipped_message_keys: Default::default(),
+ }
+ }
+
+ fn find_message_key(&self, chain_index: u64) -> Result<FoundMessageKey<'_>, DecryptionError> {
+ let message_gap = chain_index.saturating_sub(self.hkdf_ratchet.chain_index());
+
+ if message_gap > MAX_MESSAGE_GAP {
+ Err(DecryptionError::TooBigMessageGap(message_gap, MAX_MESSAGE_GAP))
+ } else if self.hkdf_ratchet.chain_index() > chain_index {
+ self.skipped_message_keys
+ .get_message_key(chain_index)
+ .map(FoundMessageKey::Existing)
+ .ok_or(DecryptionError::MissingMessageKey(chain_index))
+ } else {
+ let mut ratchet = self.hkdf_ratchet.clone();
+ let mut skipped_keys = MessageKeyStore::new();
+
+ // Advance the ratchet up until our desired point.
+ while ratchet.chain_index() < chain_index {
+ if chain_index - ratchet.chain_index() > MAX_MESSAGE_KEYS as u64 {
+ ratchet.advance();
+ } else {
+ let key = ratchet.create_message_key();
+ skipped_keys.push(key);
+ }
+ }
+
+ // Create now our desired message key
+ let message_key = ratchet.create_message_key();
+
+ Ok(FoundMessageKey::New(Box::new((ratchet, skipped_keys, message_key))))
+ }
+ }
+
+ pub fn decrypt(
+ &mut self,
+ message: &Message,
+ config: &SessionConfig,
+ ) -> Result<Vec<u8>, DecryptionError> {
+ let chain_index = message.chain_index;
+ let message_key = self.find_message_key(chain_index)?;
+
+ let plaintext = message_key.decrypt(message, config)?;
+
+ match message_key {
+ FoundMessageKey::Existing(m) => {
+ let chain_index = m.chain_index();
+ self.skipped_message_keys.remove_message_key(chain_index)
+ }
+ FoundMessageKey::New(m) => {
+ let (ratchet, skipped_keys, _) = *m;
+
+ self.hkdf_ratchet = ratchet;
+ self.skipped_message_keys.merge(skipped_keys);
+ }
+ }
+
+ Ok(plaintext)
+ }
+
+ #[cfg(feature = "libolm-compat")]
+ pub fn ratchet_key(&self) -> RemoteRatchetKey {
+ self.ratchet_key
+ }
+
+ #[cfg(feature = "libolm-compat")]
+ pub fn insert_message_key(&mut self, message_key: RemoteMessageKey) {
+ self.skipped_message_keys.push(message_key)
+ }
+
+ pub fn belongs_to(&self, ratchet_key: &RemoteRatchetKey) -> bool {
+ &self.ratchet_key == ratchet_key
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +
// Copyright 2021 Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use hkdf::Hkdf;
+use serde::{Deserialize, Serialize};
+use sha2::Sha256;
+use zeroize::Zeroize;
+
+use super::{
+ chain_key::{ChainKey, RemoteChainKey},
+ ratchet::{RatchetKey, RemoteRatchetKey},
+};
+
+const ADVANCEMENT_SEED: &[u8; 11] = b"OLM_RATCHET";
+
+#[derive(Serialize, Deserialize, Clone, Zeroize)]
+#[serde(transparent)]
+#[zeroize(drop)]
+pub(crate) struct RootKey {
+ pub key: Box<[u8; 32]>,
+}
+
+#[derive(Serialize, Deserialize, Clone, Zeroize)]
+#[zeroize(drop)]
+pub(crate) struct RemoteRootKey {
+ pub key: Box<[u8; 32]>,
+}
+
+fn kdf(
+ root_key: &[u8; 32],
+ ratchet_key: &RatchetKey,
+ remote_ratchet_key: &RemoteRatchetKey,
+) -> Box<[u8; 64]> {
+ let shared_secret = ratchet_key.diffie_hellman(remote_ratchet_key);
+ let hkdf: Hkdf<Sha256> = Hkdf::new(Some(root_key.as_ref()), shared_secret.as_bytes());
+ let mut output = Box::new([0u8; 64]);
+
+ hkdf.expand(ADVANCEMENT_SEED, output.as_mut_slice()).expect("Can't expand");
+
+ output
+}
+
+impl RemoteRootKey {
+ pub(super) fn new(bytes: Box<[u8; 32]>) -> Self {
+ Self { key: bytes }
+ }
+
+ pub(super) fn advance(
+ &self,
+ remote_ratchet_key: &RemoteRatchetKey,
+ ) -> (RootKey, ChainKey, RatchetKey) {
+ let ratchet_key = RatchetKey::new();
+ let output = kdf(&self.key, &ratchet_key, remote_ratchet_key);
+
+ let mut chain_key = Box::new([0u8; 32]);
+ let mut root_key = Box::new([0u8; 32]);
+
+ chain_key.copy_from_slice(&output[32..]);
+ root_key.copy_from_slice(&output[..32]);
+
+ let chain_key = ChainKey::new(chain_key);
+ let root_key = RootKey::new(root_key);
+
+ (root_key, chain_key, ratchet_key)
+ }
+}
+
+impl RootKey {
+ pub(super) fn new(bytes: Box<[u8; 32]>) -> Self {
+ Self { key: bytes }
+ }
+
+ pub(super) fn advance(
+ &self,
+ old_ratchet_key: &RatchetKey,
+ remote_ratchet_key: &RemoteRatchetKey,
+ ) -> (RemoteRootKey, RemoteChainKey) {
+ let output = kdf(&self.key, old_ratchet_key, remote_ratchet_key);
+
+ let mut chain_key = Box::new([0u8; 32]);
+ let mut root_key = Box::new([0u8; 32]);
+
+ root_key.copy_from_slice(&output[..32]);
+ chain_key.copy_from_slice(&output[32..]);
+
+ let root_key = RemoteRootKey::new(root_key);
+ let chain_key = RemoteChainKey::new(chain_key);
+
+ (root_key, chain_key)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +
// Copyright 2022 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use serde::{Deserialize, Serialize};
+
+/// A struct to configure how Olm sessions should work under the hood.
+/// Currently only the MAC truncation behaviour can be configured.
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub struct SessionConfig {
+ pub(super) version: Version,
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub(super) enum Version {
+ V1 = 1,
+ V2 = 2,
+}
+
+impl SessionConfig {
+ /// Get the numeric version of this `SessionConfig`.
+ pub fn version(&self) -> u8 {
+ self.version as u8
+ }
+
+ /// Create a `SessionConfig` for the Olm version 1. This version of Olm will
+ /// use AES-256 and HMAC with a truncated MAC to encrypt individual
+ /// messages. The MAC will be truncated to 8 bytes.
+ pub fn version_1() -> Self {
+ SessionConfig { version: Version::V1 }
+ }
+
+ /// Create a `SessionConfig` for the Olm version 2. This version of Olm will
+ /// use AES-256 and HMAC to encrypt individual messages. The MAC won't be
+ /// truncated.
+ pub fn version_2() -> Self {
+ SessionConfig { version: Version::V2 }
+ }
+}
+
+impl Default for SessionConfig {
+ fn default() -> Self {
+ Self::version_2()
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +
// Copyright 2021 The Matrix.org Foundation C.I.C.
+// Copyright 2021 Damir Jelić, Denis Kasak
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use matrix_pickle::Decode;
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+
+use crate::{utilities::base64_encode, Curve25519PublicKey};
+
+/// The set of keys that were used to establish the Olm Session,
+#[derive(Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Decode)]
+pub struct SessionKeys {
+ pub identity_key: Curve25519PublicKey,
+ pub base_key: Curve25519PublicKey,
+ pub one_time_key: Curve25519PublicKey,
+}
+
+impl SessionKeys {
+ /// Returns the globally unique session ID which these [`SessionKeys`] will
+ /// produce.
+ ///
+ /// A session ID is the SHA256 of the concatenation of three `SessionKeys`,
+ /// the account's identity key, the ephemeral base key and the one-time
+ /// key which is used to establish the session.
+ ///
+ /// Due to the construction, every session ID is (probabilistically)
+ /// globally unique.
+ pub fn session_id(&self) -> String {
+ let sha = Sha256::new();
+
+ let digest = sha
+ .chain_update(self.identity_key.as_bytes())
+ .chain_update(self.base_key.as_bytes())
+ .chain_update(self.one_time_key.as_bytes())
+ .finalize();
+
+ base64_encode(digest)
+ }
+}
+
+impl std::fmt::Debug for SessionKeys {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("SessionKeys")
+ .field("identity_key", &self.identity_key.to_base64())
+ .field("base_key", &self.base_key.to_base64())
+ .field("one_time_key", &self.one_time_key.to_base64())
+ .finish()
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +
// Copyright 2021 Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! A 3DH implementation following the Olm [spec].
+//!
+//! The setup takes four Curve25519 inputs: Identity keys for Alice and Bob,
+//! (Ia, Ib), and one-time keys for Alice and Bob (Ea, Eb).
+//!
+//! A shared secret S is generated via Triple Diffie-Hellman using the above
+//! inputs. The initial 256-bit root key R0 and a 256-bit chain key C0,0 are
+//! derived from the shared secret using an HMAC-based Key Derivation Function
+//! with SHA-256 as the hash function (HKDF-SHA-256), the default salt and
+//! "OLM_ROOT" as the info.
+//!
+//! ```text
+//! S = ECDH(Ia, Eb) || ECDH(Ea, Ib) || ECDH (Ea, Eb)
+//!
+//! R0, C0,0 = HKDF(0, S, "OLM_ROOT", 64)
+//! ```
+//!
+//! [spec]: https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md#initial-setup
+
+use hkdf::Hkdf;
+use sha2::Sha256;
+use x25519_dalek::{ReusableSecret, SharedSecret};
+use zeroize::Zeroize;
+
+use crate::{types::Curve25519SecretKey as StaticSecret, Curve25519PublicKey as PublicKey};
+
+#[derive(Zeroize)]
+#[zeroize(drop)]
+pub struct Shared3DHSecret(Box<[u8; 96]>);
+
+#[derive(Zeroize)]
+#[zeroize(drop)]
+pub struct RemoteShared3DHSecret(Box<[u8; 96]>);
+
+fn expand(shared_secret: &[u8; 96]) -> (Box<[u8; 32]>, Box<[u8; 32]>) {
+ let hkdf: Hkdf<Sha256> = Hkdf::new(Some(&[0]), shared_secret);
+ let mut root_key = Box::new([0u8; 32]);
+ let mut chain_key = Box::new([0u8; 32]);
+
+ let mut expanded_keys = [0u8; 64];
+
+ hkdf.expand(b"OLM_ROOT", &mut expanded_keys)
+ .expect("Can't expand the shared 3DH secret into the Olm root");
+
+ root_key.copy_from_slice(&expanded_keys[0..32]);
+ chain_key.copy_from_slice(&expanded_keys[32..64]);
+
+ expanded_keys.zeroize();
+
+ (root_key, chain_key)
+}
+
+fn merge_secrets(
+ first_secret: SharedSecret,
+ second_secret: SharedSecret,
+ third_secret: SharedSecret,
+) -> Box<[u8; 96]> {
+ let mut secret = Box::new([0u8; 96]);
+
+ secret[0..32].copy_from_slice(first_secret.as_bytes());
+ secret[32..64].copy_from_slice(second_secret.as_bytes());
+ secret[64..96].copy_from_slice(third_secret.as_bytes());
+
+ secret
+}
+
+impl RemoteShared3DHSecret {
+ pub(crate) fn new(
+ identity_key: &StaticSecret,
+ one_time_key: &StaticSecret,
+ remote_identity_key: &PublicKey,
+ remote_one_time_key: &PublicKey,
+ ) -> Self {
+ let first_secret = one_time_key.diffie_hellman(remote_identity_key);
+ let second_secret = identity_key.diffie_hellman(remote_one_time_key);
+ let third_secret = one_time_key.diffie_hellman(remote_one_time_key);
+
+ Self(merge_secrets(first_secret, second_secret, third_secret))
+ }
+
+ pub fn expand(self) -> (Box<[u8; 32]>, Box<[u8; 32]>) {
+ expand(&self.0)
+ }
+}
+
+impl Shared3DHSecret {
+ pub(crate) fn new(
+ identity_key: &StaticSecret,
+ one_time_key: &ReusableSecret,
+ remote_identity_key: &PublicKey,
+ remote_one_time_key: &PublicKey,
+ ) -> Self {
+ let first_secret = identity_key.diffie_hellman(remote_one_time_key);
+ let second_secret = one_time_key.diffie_hellman(&remote_identity_key.inner);
+ let third_secret = one_time_key.diffie_hellman(&remote_one_time_key.inner);
+
+ Self(merge_secrets(first_secret, second_secret, third_secret))
+ }
+
+ pub fn expand(self) -> (Box<[u8; 32]>, Box<[u8; 32]>) {
+ expand(&self.0)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use rand::thread_rng;
+ use x25519_dalek::ReusableSecret;
+
+ use super::{RemoteShared3DHSecret, Shared3DHSecret};
+ use crate::{types::Curve25519SecretKey as StaticSecret, Curve25519PublicKey as PublicKey};
+
+ #[test]
+ fn triple_diffie_hellman() {
+ let rng = thread_rng();
+
+ let alice_identity = StaticSecret::new();
+ let alice_one_time = ReusableSecret::random_from_rng(rng);
+
+ let bob_identity = StaticSecret::new();
+ let bob_one_time = StaticSecret::new();
+
+ let alice_secret = Shared3DHSecret::new(
+ &alice_identity,
+ &alice_one_time,
+ &PublicKey::from(&bob_identity),
+ &PublicKey::from(&bob_one_time),
+ );
+
+ let bob_secret = RemoteShared3DHSecret::new(
+ &bob_identity,
+ &bob_one_time,
+ &PublicKey::from(&alice_identity),
+ &PublicKey::from(&alice_one_time),
+ );
+
+ assert_eq!(alice_secret.0, bob_secret.0);
+
+ let alice_result = alice_secret.expand();
+ let bob_result = bob_secret.expand();
+
+ assert_eq!(alice_result, bob_result);
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +
// Copyright 2021 Damir Jelić, Denis Kasak
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! User-friendly key verification using short authentication strings (SAS).
+//!
+//! The verification process is heavily inspired by Phil Zimmermann’s [ZRTP]
+//! key agreement handshake. A core part of key agreement in [ZRTP] is the
+//! *hash commitment*: the party that begins the key sharing process sends
+//! a *hash* of their part of the Diffie-Hellman exchange but does not send the
+//! part itself exchange until they had received the other party’s part.
+//!
+//! The verification process can be used to verify the Ed25519 identity key of
+//! an [`Account`].
+//!
+//! # Examples
+//!
+//! ```rust
+//! use vodozemac::sas::Sas;
+//! # use anyhow::Result;
+//! # fn main() -> Result<()> {
+//! let alice = Sas::new();
+//! let bob = Sas::new();
+//!
+//! let bob_public_key = bob.public_key();
+//!
+//! let bob = bob.diffie_hellman(alice.public_key())?;
+//! let alice = alice.diffie_hellman(bob_public_key)?;
+//!
+//! let alice_bytes = alice.bytes("AGREED_INFO");
+//! let bob_bytes = bob.bytes("AGREED_INFO");
+//!
+//! let alice_emojis = alice_bytes.emoji_indices();
+//! let bob_emojis = bob_bytes.emoji_indices();
+//!
+//! assert_eq!(alice_emojis, bob_emojis);
+//! # Ok(())
+//! # }
+//! ```
+//!
+//! [`Account`]: crate::olm::Account
+//! [ZRTP]: https://tools.ietf.org/html/rfc6189#section-4.4.1
+
+use hkdf::Hkdf;
+use hmac::{digest::MacError, Hmac, Mac as _};
+use rand::thread_rng;
+use sha2::Sha256;
+use thiserror::Error;
+use x25519_dalek::{EphemeralSecret, SharedSecret};
+
+use crate::{
+ utilities::{base64_decode, base64_encode},
+ Curve25519PublicKey, KeyError,
+};
+
+type HmacSha256Key = Box<[u8; 32]>;
+
+/// The output type for the SAS MAC calculation.
+pub struct Mac(Vec<u8>);
+
+impl Mac {
+ /// Convert the MAC to a base64 encoded string.
+ pub fn to_base64(&self) -> String {
+ base64_encode(&self.0)
+ }
+
+ /// Get the byte slice of the MAC.
+ pub fn as_bytes(&self) -> &[u8] {
+ &self.0
+ }
+
+ /// Create a new `Mac` object from a byte slice.
+ pub fn from_slice(bytes: &[u8]) -> Self {
+ Self(bytes.to_vec())
+ }
+
+ /// Create a new `Mac` object from a base64 encoded string.
+ pub fn from_base64(mac: &str) -> Result<Self, base64::DecodeError> {
+ let bytes = base64_decode(mac)?;
+
+ Ok(Self(bytes))
+ }
+}
+
+/// Error type for the case when we try to generate too many SAS bytes.
+#[derive(Debug, Clone, Error)]
+#[error("The given count of bytes was too large")]
+pub struct InvalidCount;
+
+/// Error type describing failures that can happen during the key verification.
+#[derive(Debug, Error)]
+pub enum SasError {
+ /// The MAC failed to be validated.
+ #[error("The SAS MAC validation didn't succeed: {0}")]
+ Mac(#[from] MacError),
+}
+
+/// A struct representing a short auth string verification object.
+///
+/// This object can be used to establish a shared secret to perform the short
+/// auth string based key verification.
+pub struct Sas {
+ secret_key: EphemeralSecret,
+ public_key: Curve25519PublicKey,
+}
+
+/// A struct representing a short auth string verification object where the
+/// shared secret has been established.
+///
+/// This object can be used to generate the short auth string and calculate and
+/// verify a MAC that protects information about the keys being verified.
+pub struct EstablishedSas {
+ shared_secret: SharedSecret,
+ our_public_key: Curve25519PublicKey,
+ their_public_key: Curve25519PublicKey,
+}
+
+impl std::fmt::Debug for EstablishedSas {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("EstablishedSas")
+ .field("our_public_key", &self.our_public_key.to_base64())
+ .field("their_public_key", &self.their_public_key.to_base64())
+ .finish_non_exhaustive()
+ }
+}
+
+/// Bytes generated from an shared secret that can be used as the short auth
+/// string.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct SasBytes {
+ bytes: [u8; 6],
+}
+
+impl SasBytes {
+ /// Get the index of 7 emojis that can be presented to users to perform the
+ /// key verification
+ ///
+ /// The table that maps the index to an emoji can be found in the [spec].
+ ///
+ /// [spec]: https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji
+ pub fn emoji_indices(&self) -> [u8; 7] {
+ Self::bytes_to_emoji_index(&self.bytes)
+ }
+
+ /// Get the three decimal numbers that can be presented to users to perform
+ /// the key verification, as described in the [spec]
+ ///
+ /// [spec]: https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji
+ pub fn decimals(&self) -> (u16, u16, u16) {
+ Self::bytes_to_decimal(&self.bytes)
+ }
+
+ /// Get the raw bytes of the short auth string that can be converted to an
+ /// emoji, or decimal representation.
+ pub fn as_bytes(&self) -> &[u8; 6] {
+ &self.bytes
+ }
+
+ /// Split the first 42 bits of our 6 bytes into 7 groups of 6 bits. The 7
+ /// groups of 6 bits represent an emoji index from the [spec].
+ ///
+ /// [spec]: https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji
+ fn bytes_to_emoji_index(bytes: &[u8; 6]) -> [u8; 7] {
+ let bytes: Vec<u64> = bytes.iter().map(|b| *b as u64).collect();
+ // Join the 6 bytes into one 64 bit unsigned int. This u64 will contain 48
+ // bits from our 6 bytes.
+ let mut num: u64 = bytes[0] << 40;
+ num += bytes[1] << 32;
+ num += bytes[2] << 24;
+ num += bytes[3] << 16;
+ num += bytes[4] << 8;
+ num += bytes[5];
+
+ // Take the top 42 bits of our 48 bits from the u64 and convert each 6 bits
+ // into a 6 bit number.
+ [
+ ((num >> 42) & 63) as u8,
+ ((num >> 36) & 63) as u8,
+ ((num >> 30) & 63) as u8,
+ ((num >> 24) & 63) as u8,
+ ((num >> 18) & 63) as u8,
+ ((num >> 12) & 63) as u8,
+ ((num >> 6) & 63) as u8,
+ ]
+ }
+
+ /// Convert the given bytes into three decimals. The 6th byte is ignored,
+ /// it's used for the emoji index conversion.
+ fn bytes_to_decimal(bytes: &[u8; 6]) -> (u16, u16, u16) {
+ let bytes: Vec<u16> = bytes.iter().map(|b| *b as u16).collect();
+
+ // This bitwise operation is taken from the [spec]
+ // [spec]: https://matrix.org/docs/spec/client_server/latest#sas-method-decimal
+ let first = bytes[0] << 5 | bytes[1] >> 3;
+ let second = (bytes[1] & 0x7) << 10 | bytes[2] << 2 | bytes[3] >> 6;
+ let third = (bytes[3] & 0x3F) << 7 | bytes[4] >> 1;
+
+ (first + 1000, second + 1000, third + 1000)
+ }
+}
+
+impl Default for Sas {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Sas {
+ /// Create a new random verification object
+ ///
+ /// This creates an ephemeral curve25519 keypair that can be used to
+ /// establish a shared secret.
+ pub fn new() -> Self {
+ let rng = thread_rng();
+
+ let secret_key = EphemeralSecret::random_from_rng(rng);
+ let public_key = Curve25519PublicKey::from(&secret_key);
+
+ Self { secret_key, public_key }
+ }
+
+ /// Get the public key that can be used to establish a shared secret.
+ pub fn public_key(&self) -> Curve25519PublicKey {
+ self.public_key
+ }
+
+ /// Establishes a SAS secret by performing a DH handshake with another
+ /// public key.
+ ///
+ /// Returns an [`EstablishedSas`] object which can be used to generate
+ /// [`SasBytes`] if the given public key was valid, otherwise `None`.
+ pub fn diffie_hellman(
+ self,
+ their_public_key: Curve25519PublicKey,
+ ) -> Result<EstablishedSas, KeyError> {
+ let shared_secret = self.secret_key.diffie_hellman(&their_public_key.inner);
+
+ if shared_secret.was_contributory() {
+ Ok(EstablishedSas { shared_secret, our_public_key: self.public_key, their_public_key })
+ } else {
+ Err(KeyError::NonContributoryKey)
+ }
+ }
+
+ /// Establishes a SAS secret by performing a DH handshake with another
+ /// public key in "raw", base64-encoded form.
+ ///
+ /// Returns an [`EstablishedSas`] object which can be used to generate
+ /// [`SasBytes`] if the received public key is valid, otherwise `None`.
+ pub fn diffie_hellman_with_raw(
+ self,
+ other_public_key: &str,
+ ) -> Result<EstablishedSas, KeyError> {
+ let other_public_key = Curve25519PublicKey::from_base64(other_public_key)?;
+ self.diffie_hellman(other_public_key)
+ }
+}
+
+impl EstablishedSas {
+ /// Generate [`SasBytes`] using HKDF with the shared secret as the input key
+ /// material.
+ ///
+ /// The info string should be agreed upon beforehand, both parties need to
+ /// use the same info string.
+ pub fn bytes(&self, info: &str) -> SasBytes {
+ let mut bytes = [0u8; 6];
+ let byte_vec =
+ self.bytes_raw(info, 6).expect("HKDF should always be able to generate 6 bytes");
+
+ bytes.copy_from_slice(&byte_vec);
+
+ SasBytes { bytes }
+ }
+
+ /// Generate the given number of bytes using HKDF with the shared secret
+ /// as the input key material.
+ ///
+ /// The info string should be agreed upon beforehand, both parties need to
+ /// use the same info string.
+ ///
+ /// The number of bytes we can generate is limited, we can generate up to
+ /// 32 * 255 bytes. The function will not fail if the given count is smaller
+ /// than the limit.
+ pub fn bytes_raw(&self, info: &str, count: usize) -> Result<Vec<u8>, InvalidCount> {
+ let mut output = vec![0u8; count];
+ let hkdf = self.get_hkdf();
+
+ hkdf.expand(info.as_bytes(), &mut output[0..count]).map_err(|_| InvalidCount)?;
+
+ Ok(output)
+ }
+
+ /// Calculate a MAC for the given input using the info string as additional
+ /// data.
+ ///
+ ///
+ /// This should be used to calculate a MAC of the ed25519 identity key of an
+ /// [`Account`]
+ ///
+ /// The MAC is returned as a base64 encoded string.
+ ///
+ /// [`Account`]: crate::olm::Account
+ pub fn calculate_mac(&self, input: &str, info: &str) -> Mac {
+ let mut mac = self.get_mac(info);
+
+ mac.update(input.as_ref());
+
+ Mac(mac.finalize().into_bytes().to_vec())
+ }
+
+ /// Calculate a MAC for the given input using the info string as additional
+ /// data, the MAC is returned as an invalid base64 encoded string.
+ ///
+ /// **Warning**: This method should never be used unless you require libolm
+ /// compatibility. Libolm used to incorrectly encode their MAC because the
+ /// input buffer was reused as the output buffer. This method replicates the
+ /// buggy behaviour.
+ #[cfg(feature = "libolm-compat")]
+ pub fn calculate_mac_invalid_base64(&self, input: &str, info: &str) -> String {
+ // First calculate the MAC as usual.
+ let mac = self.calculate_mac(input, info);
+
+ // Since the input buffer is reused as an output buffer, and base64
+ // operates on 3 input bytes to generate 4 output bytes, the input
+ // buffer gets overrun by the output.
+ //
+ // Only 6 bytes of the MAC get to be used before the output overwrites
+ // the input.
+
+ // All three bytes of the first input chunk are used successfully.
+ let mut out = base64_encode(&mac.as_bytes()[0..3]);
+
+ // For the next input chunk, only two bytes are sourced from the actual
+ // MAC, since the first byte gets overwritten by the output.
+ let mut bytes_from_mac = 2;
+
+ // Subsequent input chunks get progressively more overwritten by the
+ // output, so that after two iterations, none of the original input
+ // bytes remain.
+ for i in (6..10).step_by(3) {
+ let from_mac = &mac.as_bytes()[i - bytes_from_mac..i];
+ let from_out = &out.as_bytes()[out.len() - (3 - bytes_from_mac)..];
+
+ let bytes = [from_out, from_mac].concat();
+ let encoded = base64_encode(bytes);
+ bytes_from_mac -= 1;
+
+ out = out + &encoded;
+ }
+
+ // At this point, the rest of our input will be completely sourced from
+ // the previous output. The MAC has a size of 32, so we abort before we
+ // get to the remainder calculation.
+ for i in (9..30).step_by(3) {
+ let next = &out.as_bytes()[i..i + 3];
+ let next_four = base64_encode(next);
+ out = out + &next_four;
+ }
+
+ // Finally, use the remainder to get the last 3 bytes of output. No
+ // padding is used.
+ let next = &out.as_bytes()[30..32];
+ let next = base64_encode(next);
+
+ out + &next
+ }
+
+ /// Verify a MAC that was previously created using the
+ /// [`EstablishedSas::calculate_mac()`] method.
+ ///
+ /// Users should calculate a MAC and send it to the other side, they should
+ /// then verify each other's MAC using this method.
+ pub fn verify_mac(&self, input: &str, info: &str, tag: &Mac) -> Result<(), SasError> {
+ let mut mac = self.get_mac(info);
+ mac.update(input.as_bytes());
+
+ Ok(mac.verify_slice(&tag.0)?)
+ }
+
+ /// Get the public key that was created by us, that was used to establish
+ /// the shared secret.
+ pub fn our_public_key(&self) -> Curve25519PublicKey {
+ self.our_public_key
+ }
+
+ /// Get the public key that was created by the other party, that was used to
+ /// establish the shared secret.
+ pub fn their_public_key(&self) -> Curve25519PublicKey {
+ self.their_public_key
+ }
+
+ fn get_hkdf(&self) -> Hkdf<Sha256> {
+ Hkdf::new(None, self.shared_secret.as_bytes())
+ }
+
+ fn get_mac_key(&self, info: &str) -> HmacSha256Key {
+ let mut mac_key = Box::new([0u8; 32]);
+ let hkdf = self.get_hkdf();
+
+ hkdf.expand(info.as_bytes(), mac_key.as_mut_slice()).expect("Can't expand the MAC key");
+
+ mac_key
+ }
+
+ fn get_mac(&self, info: &str) -> Hmac<Sha256> {
+ let mac_key = self.get_mac_key(info);
+ Hmac::<Sha256>::new_from_slice(mac_key.as_slice()).expect("Can't create a HMAC object")
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use anyhow::Result;
+ use olm_rs::sas::OlmSas;
+ use proptest::prelude::*;
+
+ use super::{Mac, Sas, SasBytes};
+
+ const ALICE_MXID: &str = "@alice:example.com";
+ const ALICE_DEVICE_ID: &str = "AAAAAAAAAA";
+ const BOB_MXID: &str = "@bob:example.com";
+ const BOB_DEVICE_ID: &str = "BBBBBBBBBB";
+
+ #[test]
+ fn mac_from_slice_as_bytes_is_identity() {
+ let bytes = "ABCDEFGH".as_bytes();
+ assert_eq!(
+ Mac::from_slice(bytes).as_bytes(),
+ bytes,
+ "as_bytes() after from_slice() is not identity"
+ );
+ }
+
+ #[test]
+ fn libolm_and_vodozemac_generate_same_bytes() -> Result<()> {
+ let mut olm = OlmSas::new();
+ let dalek = Sas::new();
+
+ olm.set_their_public_key(dalek.public_key().to_base64())
+ .expect("Couldn't set the public key for libolm");
+ let established = dalek.diffie_hellman_with_raw(&olm.public_key())?;
+
+ assert_eq!(
+ olm.generate_bytes("TEST", 10).expect("libolm couldn't generate SAS bytes"),
+ established.bytes_raw("TEST", 10)?
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn vodozemac_and_vodozemac_generate_same_bytes() -> Result<()> {
+ let alice = Sas::default();
+ let bob = Sas::default();
+
+ let alice_public_key_encoded = alice.public_key().to_base64();
+ let alice_public_key = alice.public_key().to_owned();
+ let bob_public_key_encoded = bob.public_key().to_base64();
+ let bob_public_key = bob.public_key();
+
+ let alice_established = alice.diffie_hellman_with_raw(&bob_public_key_encoded)?;
+ let bob_established = bob.diffie_hellman_with_raw(&alice_public_key_encoded)?;
+
+ assert_eq!(alice_established.our_public_key(), alice_public_key);
+ assert_eq!(alice_established.their_public_key(), bob_public_key);
+ assert_eq!(bob_established.our_public_key(), bob_public_key);
+ assert_eq!(bob_established.their_public_key(), alice_public_key);
+
+ let alice_bytes = alice_established.bytes("TEST");
+ let bob_bytes = bob_established.bytes("TEST");
+
+ assert_eq!(alice_bytes, bob_bytes, "The two sides calculated different bytes.");
+ assert_eq!(
+ alice_bytes.emoji_indices(),
+ bob_bytes.emoji_indices(),
+ "The two sides calculated different emoji indices."
+ );
+ assert_eq!(
+ alice_bytes.decimals(),
+ bob_bytes.decimals(),
+ "The two sides calculated different decimals."
+ );
+ assert_eq!(alice_bytes.as_bytes(), bob_bytes.as_bytes());
+
+ Ok(())
+ }
+
+ #[test]
+ fn calculate_mac_vodozemac_vodozemac() -> Result<()> {
+ let alice = Sas::new();
+ let bob = Sas::new();
+
+ let alice_public_key = alice.public_key().to_base64();
+ let bob_public_key = bob.public_key().to_base64();
+
+ let message = format!("ed25519:{BOB_DEVICE_ID}");
+ let extra_info = format!(
+ "MATRIX_KEY_VERIFICATION_MAC\
+ {BOB_MXID}{BOB_DEVICE_ID}\
+ {ALICE_MXID}{ALICE_DEVICE_ID}\
+ $1234567890\
+ KEY_IDS",
+ );
+
+ let alice_established = alice.diffie_hellman_with_raw(&bob_public_key)?;
+ let bob_established = bob.diffie_hellman_with_raw(&alice_public_key)?;
+
+ let alice_mac = alice_established.calculate_mac(&message, &extra_info);
+ let bob_mac = bob_established.calculate_mac(&message, &extra_info);
+
+ assert_eq!(
+ alice_mac.to_base64(),
+ bob_mac.to_base64(),
+ "Two vodozemac devices calculated different SAS MACs."
+ );
+
+ alice_established.verify_mac(&message, &extra_info, &bob_mac)?;
+ bob_established.verify_mac(&message, &extra_info, &alice_mac)?;
+
+ Ok(())
+ }
+
+ #[test]
+ fn calculate_mac_vodozemac_libolm() -> Result<()> {
+ let alice_on_dalek = Sas::new();
+ let mut bob_on_libolm = OlmSas::new();
+
+ let alice_public_key = alice_on_dalek.public_key().to_base64();
+ let bob_public_key = bob_on_libolm.public_key();
+
+ let message = format!("ed25519:{BOB_DEVICE_ID}");
+ let extra_info = format!(
+ "MATRIX_KEY_VERIFICATION_MAC\
+ {BOB_MXID}{BOB_DEVICE_ID}\
+ {ALICE_MXID}{ALICE_DEVICE_ID}\
+ $1234567890\
+ KEY_IDS",
+ );
+
+ bob_on_libolm
+ .set_their_public_key(alice_public_key)
+ .expect("Couldn't set the public key for libolm");
+ let established = alice_on_dalek.diffie_hellman_with_raw(&bob_public_key)?;
+
+ let olm_mac = bob_on_libolm
+ .calculate_mac_fixed_base64(&message, &extra_info)
+ .expect("libolm couldn't calculate SAS MAC.");
+ assert_eq!(olm_mac, established.calculate_mac(&message, &extra_info).to_base64());
+
+ let olm_mac =
+ Mac::from_base64(&olm_mac).expect("SAS MAC generated by libolm wasn't valid base64.");
+
+ established.verify_mac(&message, &extra_info, &olm_mac)?;
+
+ Ok(())
+ }
+
+ #[test]
+ fn calculate_mac_invalid_base64() -> Result<()> {
+ let mut olm = OlmSas::new();
+ let dalek = Sas::new();
+
+ olm.set_their_public_key(dalek.public_key().to_base64())
+ .expect("Couldn't set the public key for libolm");
+ let established = dalek.diffie_hellman_with_raw(&olm.public_key())?;
+
+ let olm_mac = olm.calculate_mac("", "").expect("libolm couldn't calculate a MAC");
+ assert_eq!(olm_mac, established.calculate_mac_invalid_base64("", ""));
+
+ Ok(())
+ }
+
+ #[test]
+ fn emoji_generation() {
+ let bytes: [u8; 6] = [0, 0, 0, 0, 0, 0];
+ let index: [u8; 7] = [0, 0, 0, 0, 0, 0, 0];
+ assert_eq!(SasBytes::bytes_to_emoji_index(&bytes), index.as_ref());
+
+ let bytes: [u8; 6] = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
+ let index: [u8; 7] = [63, 63, 63, 63, 63, 63, 63];
+ assert_eq!(SasBytes::bytes_to_emoji_index(&bytes), index.as_ref());
+ }
+
+ #[test]
+ fn decimal_generation() {
+ let bytes: [u8; 6] = [0, 0, 0, 0, 0, 0];
+ let result = SasBytes::bytes_to_decimal(&bytes);
+
+ assert_eq!(result, (1000, 1000, 1000));
+
+ let bytes: [u8; 6] = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
+ let result = SasBytes::bytes_to_decimal(&bytes);
+ assert_eq!(result, (9191, 9191, 9191));
+ }
+
+ proptest! {
+ #[test]
+ fn proptest_emoji(bytes in prop::array::uniform6(0u8..)) {
+ let numbers = SasBytes::bytes_to_emoji_index(&bytes);
+
+ for number in numbers.iter() {
+ prop_assert!(*number < 64);
+ }
+ }
+ }
+
+ proptest! {
+ #[test]
+ fn proptest_decimals(bytes in prop::array::uniform6(0u8..)) {
+ let (first, second, third) = SasBytes::bytes_to_decimal(&bytes);
+
+ prop_assert!((1000..=9191).contains(&first));
+ prop_assert!((1000..=9191).contains(&second));
+ prop_assert!((1000..=9191).contains(&third));
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +
// Copyright 2021 Denis Kasak, Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::fmt::Display;
+
+use base64::decoded_len_estimate;
+use matrix_pickle::{Decode, DecodeError};
+use rand::thread_rng;
+use serde::{Deserialize, Serialize};
+use x25519_dalek::{EphemeralSecret, PublicKey, ReusableSecret, SharedSecret, StaticSecret};
+
+use super::KeyError;
+use crate::utilities::{base64_decode, base64_encode};
+
+/// Struct representing a Curve25519 secret key.
+#[derive(Clone, Deserialize, Serialize)]
+#[serde(transparent)]
+pub struct Curve25519SecretKey(Box<StaticSecret>);
+
+impl Curve25519SecretKey {
+ /// Generate a new, random, Curve25519SecretKey.
+ pub fn new() -> Self {
+ let rng = thread_rng();
+
+ Self(Box::new(StaticSecret::random_from_rng(rng)))
+ }
+
+ /// Create a `Curve25519SecretKey` from the given slice of bytes.
+ pub fn from_slice(bytes: &[u8; 32]) -> Self {
+ // XXX: Passing in secret array as value.
+ Self(Box::new(StaticSecret::from(*bytes)))
+ }
+
+ /// Perform a Diffie-Hellman key exchange between the given
+ /// `Curve25519PublicKey` and this `Curve25519SecretKey` and return a shared
+ /// secret.
+ pub fn diffie_hellman(&self, their_public_key: &Curve25519PublicKey) -> SharedSecret {
+ self.0.diffie_hellman(&their_public_key.inner)
+ }
+
+ /// Convert the `Curve25519SecretKey` to a byte array.
+ ///
+ /// **Note**: This creates a copy of the key which won't be zeroized, the
+ /// caller of the method needs to make sure to zeroize the returned array.
+ pub fn to_bytes(&self) -> [u8; 32] {
+ self.0.to_bytes()
+ }
+}
+
+impl Default for Curve25519SecretKey {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+#[serde(from = "Curve25519KeypairPickle")]
+#[serde(into = "Curve25519KeypairPickle")]
+pub(crate) struct Curve25519Keypair {
+ pub secret_key: Curve25519SecretKey,
+ pub public_key: Curve25519PublicKey,
+}
+
+impl Curve25519Keypair {
+ pub fn new() -> Self {
+ let secret_key = Curve25519SecretKey::new();
+ let public_key = Curve25519PublicKey::from(&secret_key);
+
+ Self { secret_key, public_key }
+ }
+
+ #[cfg(feature = "libolm-compat")]
+ pub fn from_secret_key(key: &[u8; 32]) -> Self {
+ let secret_key = Curve25519SecretKey::from_slice(key);
+ let public_key = Curve25519PublicKey::from(&secret_key);
+
+ Curve25519Keypair { secret_key, public_key }
+ }
+
+ pub fn secret_key(&self) -> &Curve25519SecretKey {
+ &self.secret_key
+ }
+
+ pub fn public_key(&self) -> Curve25519PublicKey {
+ self.public_key
+ }
+}
+
+/// Struct representing a Curve25519 public key.
+#[derive(PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct Curve25519PublicKey {
+ pub(crate) inner: PublicKey,
+}
+
+impl Decode for Curve25519PublicKey {
+ fn decode(reader: &mut impl std::io::Read) -> Result<Self, DecodeError> {
+ let key = <[u8; 32]>::decode(reader)?;
+
+ Ok(Curve25519PublicKey::from(key))
+ }
+}
+
+impl Curve25519PublicKey {
+ /// The number of bytes a Curve25519 public key has.
+ pub const LENGTH: usize = 32;
+
+ const BASE64_LENGTH: usize = 43;
+ const PADDED_BASE64_LENGTH: usize = 44;
+
+ /// Convert this public key to a byte array.
+ #[inline]
+ pub fn to_bytes(&self) -> [u8; Self::LENGTH] {
+ self.inner.to_bytes()
+ }
+
+ /// View this public key as a byte array.
+ #[inline]
+ pub fn as_bytes(&self) -> &[u8; Self::LENGTH] {
+ self.inner.as_bytes()
+ }
+
+ /// Convert the public key to a vector of bytes.
+ pub fn to_vec(&self) -> Vec<u8> {
+ self.inner.as_bytes().to_vec()
+ }
+
+ /// Create a `Curve25519PublicKey` from a byte array.
+ pub fn from_bytes(bytes: [u8; 32]) -> Self {
+ Self { inner: PublicKey::from(bytes) }
+ }
+
+ /// Instantiate a Curve25519 public key from an unpadded base64
+ /// representation.
+ pub fn from_base64(input: &str) -> Result<Curve25519PublicKey, KeyError> {
+ if input.len() != Self::BASE64_LENGTH && input.len() != Self::PADDED_BASE64_LENGTH {
+ Err(KeyError::InvalidKeyLength {
+ key_type: "Curve25519",
+ expected_length: Self::LENGTH,
+ length: decoded_len_estimate(input.len()),
+ })
+ } else {
+ let key = base64_decode(input)?;
+ Self::from_slice(&key)
+ }
+ }
+
+ /// Try to create a `Curve25519PublicKey` from a slice of bytes.
+ pub fn from_slice(slice: &[u8]) -> Result<Curve25519PublicKey, KeyError> {
+ let key_len = slice.len();
+
+ if key_len == Self::LENGTH {
+ let mut key = [0u8; Self::LENGTH];
+ key.copy_from_slice(slice);
+
+ Ok(Self::from(key))
+ } else {
+ Err(KeyError::InvalidKeyLength {
+ key_type: "Curve25519",
+ expected_length: Self::LENGTH,
+ length: key_len,
+ })
+ }
+ }
+
+ /// Serialize a Curve25519 public key to an unpadded base64 representation.
+ pub fn to_base64(&self) -> String {
+ base64_encode(self.inner.as_bytes())
+ }
+}
+
+impl Display for Curve25519PublicKey {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.to_base64())
+ }
+}
+
+impl std::fmt::Debug for Curve25519PublicKey {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let s = format!("curve25519:{self}");
+ <str as std::fmt::Debug>::fmt(&s, f)
+ }
+}
+
+impl From<[u8; Self::LENGTH]> for Curve25519PublicKey {
+ fn from(bytes: [u8; Self::LENGTH]) -> Curve25519PublicKey {
+ Curve25519PublicKey { inner: PublicKey::from(bytes) }
+ }
+}
+
+impl<'a> From<&'a Curve25519SecretKey> for Curve25519PublicKey {
+ fn from(secret: &'a Curve25519SecretKey) -> Curve25519PublicKey {
+ Curve25519PublicKey { inner: PublicKey::from(secret.0.as_ref()) }
+ }
+}
+
+impl<'a> From<&'a EphemeralSecret> for Curve25519PublicKey {
+ fn from(secret: &'a EphemeralSecret) -> Curve25519PublicKey {
+ Curve25519PublicKey { inner: PublicKey::from(secret) }
+ }
+}
+
+impl<'a> From<&'a ReusableSecret> for Curve25519PublicKey {
+ fn from(secret: &'a ReusableSecret) -> Curve25519PublicKey {
+ Curve25519PublicKey { inner: PublicKey::from(secret) }
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(transparent)]
+pub(crate) struct Curve25519KeypairPickle(Curve25519SecretKey);
+
+impl From<Curve25519KeypairPickle> for Curve25519Keypair {
+ fn from(pickle: Curve25519KeypairPickle) -> Self {
+ let secret_key = pickle.0;
+ let public_key = Curve25519PublicKey::from(&secret_key);
+
+ Self { secret_key, public_key }
+ }
+}
+
+impl From<Curve25519Keypair> for Curve25519KeypairPickle {
+ fn from(key: Curve25519Keypair) -> Self {
+ Curve25519KeypairPickle(key.secret_key)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::Curve25519PublicKey;
+ use crate::{utilities::DecodeError, KeyError};
+
+ #[test]
+ fn decoding_invalid_base64_fails() {
+ let base64_payload = "a";
+ assert!(matches!(
+ Curve25519PublicKey::from_base64(base64_payload),
+ Err(KeyError::InvalidKeyLength { .. })
+ ));
+
+ let base64_payload = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ";
+ assert!(matches!(
+ Curve25519PublicKey::from_base64(base64_payload),
+ Err(KeyError::Base64Error(DecodeError::InvalidByte(..)))
+ ));
+
+ let base64_payload = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZ";
+ assert!(matches!(
+ Curve25519PublicKey::from_base64(base64_payload),
+ Err(KeyError::Base64Error(DecodeError::InvalidLastSymbol(..)))
+ ));
+ }
+
+ #[test]
+ fn decoding_incorrect_num_of_bytes_fails() {
+ let base64_payload = "aaaa";
+ assert!(matches!(
+ Curve25519PublicKey::from_base64(base64_payload),
+ Err(KeyError::InvalidKeyLength { .. })
+ ));
+ }
+
+ #[test]
+ fn decoding_of_correct_num_of_bytes_succeeds() {
+ let base64_payload = "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA";
+ assert!(matches!(Curve25519PublicKey::from_base64(base64_payload), Ok(..)));
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +
// Copyright 2021 Denis Kasak, Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::fmt::Display;
+
+use base64::decoded_len_estimate;
+use curve25519_dalek::EdwardsPoint;
+#[cfg(not(fuzzing))]
+use ed25519_dalek::Verifier;
+use ed25519_dalek::{
+ Signature, Signer, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH,
+};
+use rand::thread_rng;
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use serde_bytes::{ByteBuf as SerdeByteBuf, Bytes as SerdeBytes};
+use sha2::Sha512;
+use thiserror::Error;
+use zeroize::Zeroize;
+
+use crate::utilities::{base64_decode, base64_encode};
+
+/// Error type describing signature verification failures.
+#[derive(Debug, Error)]
+pub enum SignatureError {
+ /// The signature wasn't valid base64.
+ #[error("The signature couldn't be decoded: {0}")]
+ Base64(#[from] base64::DecodeError),
+ /// The signature failed to be verified.
+ #[error("The signature was invalid: {0}")]
+ Signature(#[from] ed25519_dalek::SignatureError),
+}
+
+/// A struct collecting both a public, and a secret, Ed25519 key.
+#[derive(Deserialize, Serialize)]
+#[serde(try_from = "Ed25519KeypairPickle")]
+#[serde(into = "Ed25519KeypairPickle")]
+pub struct Ed25519Keypair {
+ secret_key: SecretKeys,
+ public_key: Ed25519PublicKey,
+}
+
+struct ExpandedSecretKey {
+ source: Box<[u8; 64]>,
+ inner: Box<ed25519_dalek::hazmat::ExpandedSecretKey>,
+}
+
+impl ExpandedSecretKey {
+ fn from_bytes(bytes: &[u8; 64]) -> Result<Self, ed25519_dalek::SignatureError> {
+ let mut source = Box::new([0u8; 64]);
+ source.copy_from_slice(bytes);
+
+ Ok(Self {
+ source,
+ inner: ed25519_dalek::hazmat::ExpandedSecretKey::from_bytes(bytes).into(),
+ })
+ }
+
+ fn as_bytes(&self) -> &[u8; 64] {
+ &self.source
+ }
+
+ fn sign(&self, message: &[u8]) -> Signature {
+ ed25519_dalek::hazmat::raw_sign::<Sha512>(&self.inner, message, &self.public_key().0)
+ }
+
+ fn public_key(&self) -> Ed25519PublicKey {
+ let point = EdwardsPoint::mul_base(&self.inner.scalar);
+ Ed25519PublicKey(VerifyingKey::from(point))
+ }
+}
+
+impl Clone for ExpandedSecretKey {
+ fn clone(&self) -> Self {
+ let source = self.source.clone();
+ Self {
+ source,
+ inner: ed25519_dalek::hazmat::ExpandedSecretKey::from_bytes(&self.source).into(),
+ }
+ }
+}
+
+impl Serialize for ExpandedSecretKey {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let bytes = self.as_bytes();
+ SerdeBytes::new(bytes).serialize(serializer)
+ }
+}
+
+impl<'d> Deserialize<'d> for ExpandedSecretKey {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'d>,
+ {
+ let mut bytes = <SerdeByteBuf>::deserialize(deserializer)?;
+ let length = bytes.len();
+
+ if bytes.len() != 64 {
+ bytes.zeroize();
+
+ Err(serde::de::Error::custom(format!(
+ "Invalid secret key length: expected 64 bytes, got {length}"
+ )))
+ } else {
+ let mut slice = [0u8; 64];
+ slice.copy_from_slice(&bytes);
+
+ let ret = ExpandedSecretKey::from_bytes(&slice);
+
+ slice.zeroize();
+ bytes.zeroize();
+
+ ret.map_err(serde::de::Error::custom)
+ }
+ }
+}
+
+impl Ed25519Keypair {
+ /// Create a new, random, `Ed25519Keypair`.
+ pub fn new() -> Self {
+ let mut rng = thread_rng();
+ let signing_key = SigningKey::generate(&mut rng);
+
+ Self {
+ public_key: Ed25519PublicKey(signing_key.verifying_key()),
+ secret_key: signing_key.into(),
+ }
+ }
+
+ #[cfg(feature = "libolm-compat")]
+ pub(crate) fn from_expanded_key(secret_key: &[u8; 64]) -> Result<Self, crate::KeyError> {
+ let secret_key = ExpandedSecretKey::from_bytes(secret_key).map_err(SignatureError::from)?;
+ let public_key = secret_key.public_key();
+
+ Ok(Self { secret_key: secret_key.into(), public_key })
+ }
+
+ /// Get the public Ed25519 key of this keypair.
+ pub fn public_key(&self) -> Ed25519PublicKey {
+ self.public_key
+ }
+
+ /// Sign the given message with our secret key.
+ pub fn sign(&self, message: &[u8]) -> Ed25519Signature {
+ self.secret_key.sign(message)
+ }
+}
+
+impl Default for Ed25519Keypair {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// An Ed25519 secret key, used to create digital signatures.
+#[derive(Deserialize, Serialize)]
+#[serde(transparent)]
+pub struct Ed25519SecretKey(Box<SigningKey>);
+
+impl Ed25519SecretKey {
+ /// The number of bytes a Ed25519 secret key has.
+ pub const LENGTH: usize = ed25519_dalek::SECRET_KEY_LENGTH;
+
+ const BASE64_LENGTH: usize = 43;
+ const PADDED_BASE64_LENGTH: usize = 44;
+
+ /// Create a new random `Ed25519SecretKey`.
+ pub fn new() -> Self {
+ let mut rng = thread_rng();
+ let signing_key = SigningKey::generate(&mut rng);
+ let key = Box::new(signing_key);
+
+ Self(key)
+ }
+
+ /// Get the byte representation of the secret key.
+ pub fn to_bytes(&self) -> Box<[u8; 32]> {
+ Box::new(self.0.to_bytes())
+ }
+
+ /// Try to create a `Ed25519SecretKey` from a slice of bytes.
+ pub fn from_slice(bytes: &[u8; 32]) -> Self {
+ Self(Box::new(SigningKey::from_bytes(bytes)))
+ }
+
+ /// Convert the secret key to a base64 encoded string.
+ ///
+ /// This can be useful if the secret key needs to be sent over the network
+ /// or persisted.
+ ///
+ /// **Warning**: The string should be zeroized after it has been used,
+ /// otherwise an unintentional copy of the key might exist in memory.
+ pub fn to_base64(&self) -> String {
+ let mut bytes = self.to_bytes();
+ let ret = base64_encode(bytes.as_ref());
+
+ bytes.zeroize();
+
+ ret
+ }
+
+ /// Try to create a `Ed25519SecretKey` from a base64 encoded string.
+ pub fn from_base64(input: &str) -> Result<Self, crate::KeyError> {
+ if input.len() != Self::BASE64_LENGTH && input.len() != Self::PADDED_BASE64_LENGTH {
+ Err(crate::KeyError::InvalidKeyLength {
+ key_type: "Ed25519",
+ expected_length: ed25519_dalek::SECRET_KEY_LENGTH,
+ length: decoded_len_estimate(input.len()),
+ })
+ } else {
+ let mut bytes = base64_decode(input)?;
+ let mut key_bytes = [0u8; 32];
+
+ key_bytes.copy_from_slice(&bytes);
+ let key = Self::from_slice(&key_bytes);
+
+ bytes.zeroize();
+ key_bytes.zeroize();
+
+ Ok(key)
+ }
+ }
+
+ /// Get the public key that matches this `Ed25519SecretKey`.
+ pub fn public_key(&self) -> Ed25519PublicKey {
+ Ed25519PublicKey(self.0.verifying_key())
+ }
+
+ /// Sign the given slice of bytes with this `Ed25519SecretKey`.
+ ///
+ /// The signature can be verified using the public key.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use vodozemac::{Ed25519SecretKey, Ed25519PublicKey};
+ ///
+ /// let secret = Ed25519SecretKey::new();
+ /// let message = "It's dangerous to go alone";
+ ///
+ /// let signature = secret.sign(message.as_bytes());
+ ///
+ /// let public_key = secret.public_key();
+ ///
+ /// public_key.verify(message.as_bytes(), &signature).expect("The signature has to be valid");
+ /// ```
+ pub fn sign(&self, message: &[u8]) -> Ed25519Signature {
+ Ed25519Signature(self.0.sign(message))
+ }
+}
+
+impl Default for Ed25519SecretKey {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+enum SecretKeys {
+ Normal(Box<SigningKey>),
+ Expanded(Box<ExpandedSecretKey>),
+}
+
+impl SecretKeys {
+ fn public_key(&self) -> Ed25519PublicKey {
+ match &self {
+ SecretKeys::Normal(k) => Ed25519PublicKey(k.verifying_key()),
+ SecretKeys::Expanded(k) => k.public_key(),
+ }
+ }
+
+ fn sign(&self, message: &[u8]) -> Ed25519Signature {
+ let signature = match &self {
+ SecretKeys::Normal(k) => k.sign(message),
+ SecretKeys::Expanded(k) => k.sign(message),
+ };
+
+ Ed25519Signature(signature)
+ }
+}
+
+/// An Ed25519 public key, used to verify digital signatures.
+#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
+#[serde(transparent)]
+pub struct Ed25519PublicKey(VerifyingKey);
+
+impl Ed25519PublicKey {
+ /// The number of bytes a Ed25519 public key has.
+ pub const LENGTH: usize = PUBLIC_KEY_LENGTH;
+
+ const BASE64_LENGTH: usize = 43;
+ const PADDED_BASE64_LENGTH: usize = 44;
+
+ /// Try to create a `Ed25519PublicKey` from a slice of bytes.
+ pub fn from_slice(bytes: &[u8; 32]) -> Result<Self, crate::KeyError> {
+ Ok(Self(VerifyingKey::from_bytes(bytes).map_err(SignatureError::from)?))
+ }
+
+ /// View this public key as a byte array.
+ pub fn as_bytes(&self) -> &[u8; Self::LENGTH] {
+ self.0.as_bytes()
+ }
+
+ /// Instantiate a Ed25519PublicKey public key from an unpadded base64
+ /// representation.
+ pub fn from_base64(input: &str) -> Result<Self, crate::KeyError> {
+ if input.len() != Self::BASE64_LENGTH && input.len() != Self::PADDED_BASE64_LENGTH {
+ Err(crate::KeyError::InvalidKeyLength {
+ key_type: "Ed25519",
+ expected_length: Self::LENGTH,
+ length: decoded_len_estimate(input.len()),
+ })
+ } else {
+ let mut bytes = base64_decode(input)?;
+ let mut key_bytes = [0u8; 32];
+
+ key_bytes.copy_from_slice(&bytes);
+ let key = Self::from_slice(&key_bytes);
+
+ bytes.zeroize();
+ key_bytes.zeroize();
+
+ key
+ }
+ }
+
+ /// Serialize a Ed25519PublicKey public key to an unpadded base64
+ /// representation.
+ pub fn to_base64(&self) -> String {
+ base64_encode(self.as_bytes())
+ }
+
+ /// Verify that the provided signature for a given message has been signed
+ /// by the private key matching this public one.
+ ///
+ /// By default this performs an [RFC8032] compatible signature check. A
+ /// stricter version of the signature check can be enabled with the
+ /// `strict-signatures` feature flag.
+ ///
+ /// The stricter variant is compatible with libsodium 0.16 and under the
+ /// hood uses the [`ed25519_dalek::PublicKey::verify_strict()`] method.
+ ///
+ /// For more info, see the ed25519_dalek [README] and [this] post.
+ ///
+ /// [RFC8032]: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.7
+ /// [README]: https://github.com/dalek-cryptography/ed25519-dalek#a-note-on-signature-malleability
+ /// [this]: https://hdevalence.ca/blog/2020-10-04-its-25519am
+ #[cfg(not(fuzzing))]
+ pub fn verify(
+ &self,
+ message: &[u8],
+ signature: &Ed25519Signature,
+ ) -> Result<(), SignatureError> {
+ if cfg!(feature = "strict-signatures") {
+ Ok(self.0.verify_strict(message, &signature.0)?)
+ } else {
+ Ok(self.0.verify(message, &signature.0)?)
+ }
+ }
+
+ #[cfg(fuzzing)]
+ pub fn verify(
+ &self,
+ _message: &[u8],
+ _signature: &Ed25519Signature,
+ ) -> Result<(), SignatureError> {
+ Ok(())
+ }
+}
+
+impl Display for Ed25519PublicKey {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.to_base64())
+ }
+}
+
+impl std::fmt::Debug for Ed25519PublicKey {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let s = format!("ed25519:{self}");
+ <str as std::fmt::Debug>::fmt(&s, f)
+ }
+}
+
+/// An Ed25519 digital signature, can be used to verify the authenticity of a
+/// message.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct Ed25519Signature(pub(crate) Signature);
+
+impl Ed25519Signature {
+ /// The number of bytes a Ed25519 signature has.
+ pub const LENGTH: usize = SIGNATURE_LENGTH;
+
+ /// Try to create a `Ed25519Signature` from a slice of bytes.
+ pub fn from_slice(bytes: &[u8]) -> Result<Self, SignatureError> {
+ Ok(Self(Signature::try_from(bytes)?))
+ }
+
+ /// Try to create a `Ed25519Signature` from an unpadded base64
+ /// representation.
+ pub fn from_base64(signature: &str) -> Result<Self, SignatureError> {
+ Ok(Self(Signature::try_from(base64_decode(signature)?.as_slice())?))
+ }
+
+ /// Serialize an `Ed25519Signature` to an unpadded base64 representation.
+ pub fn to_base64(&self) -> String {
+ base64_encode(self.0.to_bytes())
+ }
+
+ /// Convert the `Ed25519Signature` to a byte array.
+ pub fn to_bytes(&self) -> [u8; Self::LENGTH] {
+ self.0.to_bytes()
+ }
+}
+
+impl Display for Ed25519Signature {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.to_base64())
+ }
+}
+
+impl std::fmt::Debug for Ed25519Signature {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let s = format!("ed25519:{self}");
+ <str as std::fmt::Debug>::fmt(&s, f)
+ }
+}
+
+impl Clone for Ed25519Keypair {
+ fn clone(&self) -> Self {
+ let secret_key: SecretKeys = match &self.secret_key {
+ SecretKeys::Normal(k) => SecretKeys::Normal(k.clone()),
+ SecretKeys::Expanded(k) => SecretKeys::Expanded(k.clone()),
+ };
+
+ Self { secret_key, public_key: self.public_key }
+ }
+}
+
+impl From<Ed25519Keypair> for Ed25519KeypairPickle {
+ fn from(key: Ed25519Keypair) -> Self {
+ Self(key.secret_key)
+ }
+}
+
+impl From<SigningKey> for SecretKeys {
+ fn from(key: SigningKey) -> Self {
+ Self::Normal(Box::new(key))
+ }
+}
+
+impl From<ExpandedSecretKey> for SecretKeys {
+ fn from(key: ExpandedSecretKey) -> Self {
+ Self::Expanded(Box::new(key))
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct Ed25519KeypairPickle(SecretKeys);
+
+impl From<Ed25519KeypairPickle> for Ed25519Keypair {
+ fn from(pickle: Ed25519KeypairPickle) -> Self {
+ let secret_key = pickle.0;
+ let public_key = secret_key.public_key();
+
+ Self { secret_key, public_key }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +
// Copyright 2021 Denis Kasak, Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+mod curve25519;
+mod ed25519;
+
+pub(crate) use curve25519::{Curve25519Keypair, Curve25519KeypairPickle};
+pub use curve25519::{Curve25519PublicKey, Curve25519SecretKey};
+pub use ed25519::{
+ Ed25519Keypair, Ed25519KeypairPickle, Ed25519PublicKey, Ed25519SecretKey, Ed25519Signature,
+ SignatureError,
+};
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+pub use x25519_dalek::SharedSecret;
+
+#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct KeyId(pub(super) u64);
+
+impl From<KeyId> for String {
+ fn from(value: KeyId) -> String {
+ value.to_base64()
+ }
+}
+
+impl KeyId {
+ pub fn to_base64(self) -> String {
+ crate::utilities::base64_encode(self.0.to_be_bytes())
+ }
+}
+
+/// Error type describing failures that can happen when we try decode or use a
+/// cryptographic key.
+#[derive(Error, Debug)]
+pub enum KeyError {
+ #[error("Failed decoding a public key from base64: {}", .0)]
+ Base64Error(#[from] base64::DecodeError),
+ #[error(
+ "Failed decoding {key_type} key from base64: \
+ Invalid number of bytes for {key_type}, expected {expected_length}, got {length}."
+ )]
+ InvalidKeyLength { key_type: &'static str, expected_length: usize, length: usize },
+ #[error(transparent)]
+ Signature(#[from] SignatureError),
+ /// At least one of the keys did not have contributory behaviour and the
+ /// resulting shared secret would have been insecure.
+ #[error("At least one of the keys did not have contributory behaviour")]
+ NonContributoryKey,
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +
// Copyright 2021 Damir Jelić
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::io::Cursor;
+
+use matrix_pickle::Decode;
+use zeroize::Zeroize;
+
+use super::base64_decode;
+use crate::{cipher::Cipher, LibolmPickleError};
+
+/// Decrypt and decode the given pickle with the given pickle key.
+///
+/// # Arguments
+///
+/// * pickle - The base64-encoded and encrypted libolm pickle string
+/// * pickle_key - The key that was used to encrypt the libolm pickle
+/// * pickle_version - The expected version of the pickle. Unpickling will fail
+/// if the version in the pickle doesn't match this one.
+pub(crate) fn unpickle_libolm<P: Decode, T: TryFrom<P, Error = LibolmPickleError>>(
+ pickle: &str,
+ pickle_key: &[u8],
+ pickle_version: u32,
+) -> Result<T, LibolmPickleError> {
+ /// Fetch the pickle version from the given pickle source.
+ fn get_version(source: &[u8]) -> Option<u32> {
+ // Pickle versions are always u32 encoded as a fixed sized integer in
+ // big endian encoding.
+ let version = source.get(0..4)?;
+ Some(u32::from_be_bytes(version.try_into().ok()?))
+ }
+
+ // libolm pickles are always base64 encoded, so first try to decode.
+ let decoded = base64_decode(pickle)?;
+
+ // The pickle is always encrypted, even if a zero key is given. Try to
+ // decrypt next.
+ let cipher = Cipher::new_pickle(pickle_key);
+ let mut decrypted = cipher.decrypt_pickle(&decoded)?;
+
+ // A pickle starts with a version, which will decide how we need to decode.
+ // We only support the latest version so bail out if it isn't the expected
+ // pickle version.
+ let version = get_version(&decrypted).ok_or(LibolmPickleError::MissingVersion)?;
+
+ if version == pickle_version {
+ let mut cursor = Cursor::new(&decrypted);
+ let pickle = P::decode(&mut cursor)?;
+
+ decrypted.zeroize();
+ pickle.try_into()
+ } else {
+ Err(LibolmPickleError::Version(pickle_version, version))
+ }
+}
+
+#[derive(Zeroize, Decode)]
+#[zeroize(drop)]
+pub(crate) struct LibolmEd25519Keypair {
+ pub public_key: [u8; 32],
+ #[secret]
+ pub private_key: Box<[u8; 64]>,
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +
// Copyright 2021 The Matrix.org Foundation C.I.C.
+// Copyright 2021 Damir Jelić, Denis Kasak
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#[cfg(feature = "libolm-compat")]
+mod libolm_compat;
+
+pub use base64::DecodeError;
+use base64::{
+ alphabet,
+ engine::{general_purpose, GeneralPurpose},
+ Engine,
+};
+#[cfg(feature = "libolm-compat")]
+pub(crate) use libolm_compat::{unpickle_libolm, LibolmEd25519Keypair};
+
+const STANDARD_NO_PAD: GeneralPurpose = GeneralPurpose::new(
+ &alphabet::STANDARD,
+ general_purpose::NO_PAD
+ .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent),
+);
+
+/// Decode the input as base64 with no padding.
+pub fn base64_decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
+ STANDARD_NO_PAD.decode(input)
+}
+
+/// Encode the input as base64 with no padding.
+pub fn base64_encode(input: impl AsRef<[u8]>) -> String {
+ STANDARD_NO_PAD.encode(input)
+}
+
+pub(crate) fn unpickle<T: for<'b> serde::Deserialize<'b>>(
+ ciphertext: &str,
+ pickle_key: &[u8; 32],
+) -> Result<T, crate::PickleError> {
+ use zeroize::Zeroize;
+
+ let cipher = crate::cipher::Cipher::new_pickle(pickle_key);
+ let decoded = base64_decode(ciphertext)?;
+ let mut plaintext = cipher.decrypt_pickle(&decoded)?;
+
+ let pickle = serde_json::from_slice(&plaintext)?;
+
+ plaintext.zeroize();
+
+ Ok(pickle)
+}
+
+pub(crate) fn pickle<T: serde::Serialize>(thing: &T, pickle_key: &[u8; 32]) -> String {
+ use zeroize::Zeroize;
+
+ let mut json = serde_json::to_vec(&thing).expect("Can't serialize a pickled object");
+ let cipher = crate::cipher::Cipher::new_pickle(pickle_key);
+
+ let ciphertext = cipher.encrypt_pickle(json.as_slice());
+
+ json.zeroize();
+
+ base64_encode(ciphertext)
+}
+
+pub(crate) fn extract_mac(slice: &[u8], truncated: bool) -> crate::cipher::MessageMac {
+ use crate::cipher::Mac;
+
+ if truncated {
+ let mac_slice = &slice[0..Mac::TRUNCATED_LEN];
+
+ let mut mac = [0u8; Mac::TRUNCATED_LEN];
+ mac.copy_from_slice(mac_slice);
+ mac.into()
+ } else {
+ let mac_slice = &slice[0..Mac::LENGTH];
+
+ let mut mac = [0u8; Mac::LENGTH];
+ mac.copy_from_slice(mac_slice);
+ Mac(mac).into()
+ }
+}
+
+// The integer encoding logic here has been taken from the integer-encoding[1]
+// crate and is under the MIT license.
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2016 Google Inc. (lewinb@google.com) -- though not an official
+// Google product or in any way related!
+// Copyright (c) 2018-2020 Lewin Bormann (lbo@spheniscida.de)
+//
+// [1]: https://github.com/dermesser/integer-encoding-rs
+pub(crate) trait VarInt {
+ fn to_var_int(self) -> Vec<u8>;
+}
+
+/// Most-significant byte, == 0x80
+const MSB: u8 = 0b1000_0000;
+
+/// How many bytes an integer uses when being encoded as a VarInt.
+#[inline]
+fn required_encoded_space_unsigned(mut v: u64) -> usize {
+ if v == 0 {
+ return 1;
+ }
+
+ let mut logcounter = 0;
+ while v > 0 {
+ logcounter += 1;
+ v >>= 7;
+ }
+ logcounter
+}
+
+impl VarInt for usize {
+ fn to_var_int(self) -> Vec<u8> {
+ (self as u64).to_var_int()
+ }
+}
+
+impl VarInt for u32 {
+ fn to_var_int(self) -> Vec<u8> {
+ (self as u64).to_var_int()
+ }
+}
+
+impl VarInt for u64 {
+ #[inline]
+ fn to_var_int(self) -> Vec<u8> {
+ let mut v = Vec::new();
+ v.resize(required_encoded_space_unsigned(self), 0);
+
+ let mut n = self;
+ let mut i = 0;
+
+ while n >= 0x80 {
+ v[i] = MSB | (n as u8);
+ i += 1;
+ n >>= 7;
+ }
+
+ v[i] = n as u8;
+
+ v
+ }
+}
+
fn:
) to \
+ restrict the search to a given item kind.","Accepted kinds are: fn
, mod
, struct
, \
+ enum
, trait
, type
, macro
, \
+ and const
.","Search functions by type signature (e.g., vec -> usize
or \
+ -> vec
or String, enum:Cow -> bool
)","You can look for items with an exact name by putting double quotes around \
+ your request: \"string\"
","Look for functions that accept or return \
+ slices and \
+ arrays by writing \
+ square brackets (e.g., -> [u8]
or [] -> Option
)","Look for items inside another one by searching for a path: vec::Vec
",].map(x=>""+x+"
").join("");const div_infos=document.createElement("div");addClass(div_infos,"infos");div_infos.innerHTML="${value.replaceAll(" ", " ")}
`}else{error[index]=value}});output+=`pub enum Base64DecodeError {
+ InvalidByte(usize, u8),
+ InvalidLength,
+ InvalidLastSymbol(usize, u8),
+ InvalidPadding,
+}
Errors that can occur while decoding.
+An invalid byte was found in the input. The offset and offending byte are provided.
+Padding characters (=
) interspersed in the encoded form will be treated as invalid bytes.
The length of the input is invalid.
+A typical cause of this is stray trailing whitespace or other separator bytes.
+In the case where excess trailing bytes have produced an invalid length and the last byte
+is also an invalid base64 symbol (as would be the case for whitespace, etc), InvalidByte
+will be emitted instead of InvalidLength
to make the issue easier to debug.
The last non-padding input symbol’s encoded 6 bits have nonzero bits that will be discarded.
+This is indicative of corrupted or truncated Base64.
+Unlike InvalidByte
, which reports symbols that aren’t in the alphabet, this error is for
+symbols that are in the alphabet but represent nonsensical encodings.
The nature of the padding was not as configured: absent or incorrect when it must be +canonical, or present when it must be absent, etc.
+source
. Read morestd
only.self
and other
values to be equal, and is used
+by ==
.pub enum DecodeError {
+ MessageType(usize),
+ MissingVersion,
+ MessageTooShort(usize),
+ InvalidVersion(u8, u8),
+ InvalidKey(KeyError),
+ InvalidMacLength(usize, usize),
+ Signature(SignatureError),
+ ProtoBufError(ProtoBufDecodeError),
+ Base64(Base64DecodeError),
+}
Error type describing the different ways message decoding can fail.
+The Olm message has an invalid type.
+The message is missing a valid version.
+The message doesn’t have enough data to be correctly decoded.
+The message has a unsupported version.
+An embedded public key couldn’t be decoded.
+The embedded message authentication code couldn’t be decoded.
+An embedded signature couldn’t be decoded.
+The message couldn’t be decoded as a valid protocol buffer message.
+The message wasn’t valid base64.
+pub enum KeyError {
+ Base64Error(DecodeError),
+ InvalidKeyLength {
+ key_type: &'static str,
+ expected_length: usize,
+ length: usize,
+ },
+ Signature(SignatureError),
+ NonContributoryKey,
+}
Error type describing failures that can happen when we try decode or use a +cryptographic key.
+At least one of the keys did not have contributory behaviour and the +resulting shared secret would have been insecure.
+pub enum LibolmPickleError {
+ MissingVersion,
+ Version(u32, u32),
+ Base64(Base64DecodeError),
+ Decryption(DecryptionError),
+ PublicKey(KeyError),
+ InvalidSession,
+ Decode(DecodeError),
+}
libolm-compat
only.Error type describing the various ways libolm pickles can fail to be +decoded.
+The pickle is missing a valid version.
+The pickle has a unsupported version.
+The pickle wasn’t valid base64.
+The pickle could not have been decrypted.
+The pickle contains an invalid public key.
+The pickle does not contain a valid receiving or sending chain. A valid +Olm session needs to have at least one of them.
+The payload of the pickle could not be decoded.
+pub enum PickleError {
+ Base64(DecodeError),
+ Decryption(DecryptionError),
+ Serialization(Error),
+}
Error type describing the various ways Vodozemac pickles can fail to be +decoded.
+The pickle wasn’t valid base64.
+The encrypted pickle could not have been decrypted.
+The serialized Vodozemac object couldn’t be deserialized.
+pub enum SignatureError {
+ Base64(DecodeError),
+ Signature(SignatureError),
+}
Error type describing signature verification failures.
+The signature wasn’t valid base64.
+The signature failed to be verified.
+A Rust implementation of Olm and Megolm
+vodozemac is a Rust reimplementation of +libolm, a cryptographic library +used for end-to-end encryption in Matrix. At its core, it +is an implementation of the Olm and Megolm cryptographic ratchets, along +with a high-level API to easily establish cryptographic communication +channels employing those ratchets with other parties. It also implements +some other miscellaneous cryptographic functionality which is useful for +building Matrix clients, such as SAS.
+Olm is an implementation of the Double Ratchet +algorithm, very +similar to that employed by the Signal Protocol. It allows the establishment +of a 1-to-1 private communication channel, with perfect forward secrecy and +self-healing properties.
+A detailed technical specification can be found at +https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md.
+For more information on using vodozemac for Olm, see the olm
module.
Megolm is an AES-based single ratchet for group conversations with a large +number of participants, where using Olm would be cost prohibitive because it +would imply encrypting each message individually for each participant. +Megolm sidesteps this by encrypting messages with a symmetric ratchet, +shared once with each participant and then reused for a sequence of messages +before rotating.
+This is a trade-off in which we lose Olm’s self-healing properties, because +someone in possession of a Megolm session at a particular state can derive +all future states. However, if the attacker is only able to obtain the +session in a ratcheted state, they cannot use it to decrypt messages +encrypted with an earlier state. This preserves forward secrecy.
+A detailed technical specification can be found at +https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/megolm.md.
+For more information on using vodozemac for Megolm, see the megolm
+module.
Feature: low-level-api
(default: off)
Vodozemac exposes some lower-level structs and functions that are only +useful in very advanced use cases. These should not be needed by the vast +majority of users.
+Extreme care must be taken when using such APIs, as incorrect usage can lead +to broken sessions.
+vodozemac supports serializing its entire internal state into a form +a “pickle”. The state can subsequently be restored from such a pickle +(“unpickled”) in order to continue operation. This is used to support some +Matrix features like device dehydration.
+The legacy pickle format is a simple binary format used by libolm. +Implemented for interoperability with current clients which are using +libolm. Only unpickling is supported.
+The crate also implements a modern pickling mechanism using +Serde. The exact serialization format is not mandated +nor specified by this crate, but you can serialize to and deserialize from +any format supported by Serde.
+The following structs support pickling:
+ +For example, the following will print out the JSON representing the
+serialized Account
and will leave no new copies of the account’s secrets
+in memory:
use anyhow::Result;
+use vodozemac::olm::{Account, AccountPickle};
+
+const PICKLE_KEY: [u8; 32] = [0u8; 32];
+
+fn main() -> Result<()>{
+ let mut account = Account::new();
+
+ account.generate_one_time_keys(10);
+ account.generate_fallback_key();
+
+ let pickle = account.pickle().encrypt(&PICKLE_KEY);
+
+ let account2: Account = AccountPickle::from_encrypted(&pickle, &PICKLE_KEY)?.into();
+
+ assert_eq!(account.identity_keys(), account2.identity_keys());
+
+ Ok(())
+}
You can unpickle a pickle-able struct directly from its serialized form:
+ + let mut json_str = serde_json::to_string(&some_account.pickle())?;
+ // This will produce an account which is identical to `some_account`.
+ let account: Account = serde_json::from_str::<AccountPickle>(&json_str)?.into();
+
+ json_str.zeroize();
However, the pickle-able structs do not implement serde::Serialize
+themselves. If you want to serialize to a format other than JSON, you should
+instead call the .pickle()
method to obtain a special serializable struct.
+This struct does implement Serialize
and can therefore be serialized
+into any format supported by serde
. To get back to the original struct
+from such as serializeable struct, just call .unpickle()
.
use anyhow::Result;
+use vodozemac::olm::Account;
+
+fn main() -> Result<()> {
+ let account = Account::new();
+ let account: Account = account.pickle().into(); // this is identity
+
+ Ok(())
+}
libolm-compat
pub enum DecryptionError {
+ Signature(SignatureError),
+ InvalidMAC(MacError),
+ InvalidMACLength(usize, usize),
+ InvalidPadding(UnpadError),
+ UnknownMessageIndex(u32, u32),
+}
Error type for Megolm-based decryption failures.
+The signature on the message was invalid.
+The message authentication code of the message was invalid.
+The length of the message authentication code of the message did not +match our expected length.
+The ciphertext of the message isn’t padded correctly.
+The session is missing the correct message key to decrypt the message, +The Session has been ratcheted forwards and the message key isn’t +available anymore.
+pub enum SessionKeyDecodeError {
+ Version(u8, u8),
+ Read(Error),
+ Base64(DecodeError),
+ Signature(SignatureError),
+ PublicKey(KeyError),
+}
Error type describing failure modes for the SessionKey
and
+ExportedSessionKey
decoding.
The encoded session key had a unsupported version.
+The encoded session key didn’t contain enough data to be decoded.
+The encoded session key wasn’t valid base64.
+The signature on the session key was invalid.
+The encoded session key contains an invalid public key.
+pub enum SessionOrdering {
+ Equal,
+ Better,
+ Worse,
+ Unconnected,
+}
The result of a comparison between two InboundGroupSession
types.
Tells us if one session can be considered to be better than another one.
+The sessions are the same.
+The first session has a better initial message index than the second +one.
+The first session has a worse initial message index than the second one.
+The sessions are not the same, they can’t be compared.
+source
. Read moreself
and other
values to be equal, and is used
+by ==
.Redirecting to ../../../vodozemac/megolm/struct.GroupSession.html...
+ + + \ No newline at end of file diff --git a/vodozemac/megolm/group_session/struct.GroupSessionPickle.html b/vodozemac/megolm/group_session/struct.GroupSessionPickle.html new file mode 100644 index 00000000..ad1c5ac5 --- /dev/null +++ b/vodozemac/megolm/group_session/struct.GroupSessionPickle.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/megolm/struct.GroupSessionPickle.html...
+ + + \ No newline at end of file diff --git a/vodozemac/megolm/inbound_group_session/enum.DecryptionError.html b/vodozemac/megolm/inbound_group_session/enum.DecryptionError.html new file mode 100644 index 00000000..2a788eea --- /dev/null +++ b/vodozemac/megolm/inbound_group_session/enum.DecryptionError.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/megolm/enum.DecryptionError.html...
+ + + \ No newline at end of file diff --git a/vodozemac/megolm/inbound_group_session/enum.SessionOrdering.html b/vodozemac/megolm/inbound_group_session/enum.SessionOrdering.html new file mode 100644 index 00000000..fbd26a56 --- /dev/null +++ b/vodozemac/megolm/inbound_group_session/enum.SessionOrdering.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/megolm/enum.SessionOrdering.html...
+ + + \ No newline at end of file diff --git a/vodozemac/megolm/inbound_group_session/struct.DecryptedMessage.html b/vodozemac/megolm/inbound_group_session/struct.DecryptedMessage.html new file mode 100644 index 00000000..cf8a3d0a --- /dev/null +++ b/vodozemac/megolm/inbound_group_session/struct.DecryptedMessage.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/megolm/struct.DecryptedMessage.html...
+ + + \ No newline at end of file diff --git a/vodozemac/megolm/inbound_group_session/struct.InboundGroupSession.html b/vodozemac/megolm/inbound_group_session/struct.InboundGroupSession.html new file mode 100644 index 00000000..0b8ca53b --- /dev/null +++ b/vodozemac/megolm/inbound_group_session/struct.InboundGroupSession.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/megolm/struct.InboundGroupSession.html...
+ + + \ No newline at end of file diff --git a/vodozemac/megolm/inbound_group_session/struct.InboundGroupSessionPickle.html b/vodozemac/megolm/inbound_group_session/struct.InboundGroupSessionPickle.html new file mode 100644 index 00000000..28290125 --- /dev/null +++ b/vodozemac/megolm/inbound_group_session/struct.InboundGroupSessionPickle.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/megolm/struct.InboundGroupSessionPickle.html...
+ + + \ No newline at end of file diff --git a/vodozemac/megolm/index.html b/vodozemac/megolm/index.html new file mode 100644 index 00000000..da6dbfaa --- /dev/null +++ b/vodozemac/megolm/index.html @@ -0,0 +1,8 @@ +An implementation of the Megolm ratchet.
+serde::Serialize
+and serde::Deserialize
. Obtainable by calling GroupSession::pickle
.serde::Serialize
+and serde::Deserialize
. Obtainable by calling
+InboundGroupSession::pickle
.InboundGroupSession
.SessionKey
and
+ExportedSessionKey
decoding.InboundGroupSession
types.Redirecting to ../../../vodozemac/megolm/struct.MegolmMessage.html...
+ + + \ No newline at end of file diff --git a/vodozemac/megolm/session_config/struct.SessionConfig.html b/vodozemac/megolm/session_config/struct.SessionConfig.html new file mode 100644 index 00000000..4cb4a66c --- /dev/null +++ b/vodozemac/megolm/session_config/struct.SessionConfig.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/megolm/struct.SessionConfig.html...
+ + + \ No newline at end of file diff --git a/vodozemac/megolm/session_keys/enum.SessionKeyDecodeError.html b/vodozemac/megolm/session_keys/enum.SessionKeyDecodeError.html new file mode 100644 index 00000000..0193bf09 --- /dev/null +++ b/vodozemac/megolm/session_keys/enum.SessionKeyDecodeError.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/megolm/enum.SessionKeyDecodeError.html...
+ + + \ No newline at end of file diff --git a/vodozemac/megolm/session_keys/struct.ExportedSessionKey.html b/vodozemac/megolm/session_keys/struct.ExportedSessionKey.html new file mode 100644 index 00000000..9b39fa93 --- /dev/null +++ b/vodozemac/megolm/session_keys/struct.ExportedSessionKey.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/megolm/struct.ExportedSessionKey.html...
+ + + \ No newline at end of file diff --git a/vodozemac/megolm/session_keys/struct.SessionKey.html b/vodozemac/megolm/session_keys/struct.SessionKey.html new file mode 100644 index 00000000..d9d14b6b --- /dev/null +++ b/vodozemac/megolm/session_keys/struct.SessionKey.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/megolm/struct.SessionKey.html...
+ + + \ No newline at end of file diff --git a/vodozemac/megolm/sidebar-items.js b/vodozemac/megolm/sidebar-items.js new file mode 100644 index 00000000..70c35234 --- /dev/null +++ b/vodozemac/megolm/sidebar-items.js @@ -0,0 +1 @@ +window.SIDEBAR_ITEMS = {"enum":["DecryptionError","SessionKeyDecodeError","SessionOrdering"],"struct":["DecryptedMessage","ExportedSessionKey","GroupSession","GroupSessionPickle","InboundGroupSession","InboundGroupSessionPickle","MegolmMessage","SessionConfig","SessionKey"]}; \ No newline at end of file diff --git a/vodozemac/megolm/struct.DecryptedMessage.html b/vodozemac/megolm/struct.DecryptedMessage.html new file mode 100644 index 00000000..5cf72b8d --- /dev/null +++ b/vodozemac/megolm/struct.DecryptedMessage.html @@ -0,0 +1,18 @@ +pub struct DecryptedMessage {
+ pub plaintext: Vec<u8>,
+ pub message_index: u32,
+}
plaintext: Vec<u8>
§message_index: u32
source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub struct ExportedSessionKey { /* private fields */ }
The exported session key.
+This uses the same format as the SessionKey
minus the signature at the
+end.
Deserialize the ExportedSessionKey
from a byte slice.
Serialize the ExportedSessionKey
to a base64 encoded string.
This method will first use the ExportedSessionKey::to_bytes()
to
+convert the session key to a byte vector and then encode the byte vector
+to a string using unpadded base64 as the encoding.
Deserialize the ExportedSessionKey
from base64 encoded string.
pub struct GroupSession { /* private fields */ }
A Megolm group session represents a single sending participant in an +encrypted group communication context containing multiple receiving parties.
+A group session consists of a ratchet, used for encryption, and an Ed25519 +signing key pair, used for authenticity.
+A group session containing the signing key pair is also known as an +“outbound” group session. We differentiate this from an inbound group +session where this key pair has been removed and which can be used solely +for receipt and decryption of messages.
+Such an inbound group session is typically sent by the outbound group +session owner to each of the receiving parties via a secure peer-to-peer +channel (e.g. an Olm channel).
+Construct a new group session, with a random ratchet state and signing +key pair.
+Returns the globally unique session ID, in base64-encoded form.
+A session ID is the public part of the Ed25519 key pair associated with +the group session. Due to the construction, every session ID is +(probabilistically) globally unique.
+Return the current message index.
+The message index is incremented each time a message is encrypted with +the group session.
+Encrypt the plaintext
with the group session.
The resulting ciphertext is MAC-ed, then signed with the group session’s +Ed25519 key pair and finally base64-encoded.
+Export the group session into a session key.
+The session key contains the key version constant, the current message +index, the ratchet state and the public part of the signing key pair. +It is signed by the signing key pair for authenticity.
+The session key is in a portable format, suitable for sending over the +network. It is typically sent to other group participants so that they +can reconstruct an inbound group session in order to decrypt messages +sent by this group session.
+Convert the group session into a struct which implements
+serde::Serialize
and serde::Deserialize
.
Restore a GroupSession
from a previously saved
+GroupSessionPickle
.
libolm-compat
only.pub struct GroupSessionPickle { /* private fields */ }
A format suitable for serialization which implements serde::Serialize
+and serde::Deserialize
. Obtainable by calling GroupSession::pickle
.
Serialize and encrypt the pickle using the given key.
+This is the inverse of GroupSessionPickle::from_encrypted
.
Obtain a pickle from a ciphertext by decrypting and deserializing using +the given key.
+This is the inverse of GroupSessionPickle::encrypt
.
pub struct InboundGroupSession { /* private fields */ }
Check if two InboundGroupSession
s are the same.
An InboundGroupSession
could be received multiple times with varying
+degrees of trust and first known message indices.
This method checks if the underlying ratchets of the two
+InboundGroupSession
s are actually the same ratchet, potentially at
+a different ratcheting index. That is, if the sessions are connected,
+then ratcheting one of the ratchets to the index of the other should
+yield the same ratchet value, byte-for-byte. This will only be the case
+if the InboundGroupSession
s were created from the same
+GroupSession
.
If the sessions are connected, the session with the lower message index +can safely replace the one with the higher message index.
+Compare the InboundGroupSession
with the given other
+InboundGroupSession
.
Returns a SessionOrdering
describing how the two sessions relate to
+each other.
Merge the session with the given other session, picking the best parts +from each of them.
+This method is useful when you receive multiple sessions with +the same session ID but potentially different ratchet indices and +authenticity properties.
+For example, imagine you receive a SessionKey
S1 with ratchet index
+A from a fully-trusted source and an ExportedSessionKey
S2 with
+ratchet state B from a less trusted source. If A > B, then S1 is better
+because it’s fully trusted, but worse because it’s ratcheted further
+than S2.
This method allows you to merge S1 and S2 safely into a fully-trusted S3 +with ratchet state B, provided S1 and S2 connect with each other +(meaning they are the same session, just at different ratchet indices).
+Returns Some(session)
if the sessions could be merged, i.e. they are
+considered to be connected and None
otherwise.
use vodozemac::megolm::{GroupSession, InboundGroupSession, SessionOrdering};
+
+let session = GroupSession::new(Default::default());
+let session_key = session.session_key();
+
+let mut first_session = InboundGroupSession::new(&session_key, Default::default());
+let mut second_session = InboundGroupSession::import(&first_session.export_at(10).unwrap(), Default::default());
+
+assert_eq!(first_session.compare(&mut second_session), SessionOrdering::Better);
+
+let mut merged = second_session.merge(&mut first_session).unwrap();
+
+assert_eq!(merged.compare(&mut second_session), SessionOrdering::Better);
+assert_eq!(merged.compare(&mut first_session), SessionOrdering::Equal);
Permanently advance the session to the given index.
+This will remove the ability to decrypt messages that were encrypted +with a lower message index than what is given as the argument.
+Returns true if the ratchet has been advanced, false if the ratchet was +already advanced past the given index.
+Convert the inbound group session into a struct which implements
+serde::Serialize
and serde::Deserialize
.
Restore an InboundGroupSession
from a previously saved
+InboundGroupSessionPickle
.
libolm-compat
only.pub struct InboundGroupSessionPickle { /* private fields */ }
A format suitable for serialization which implements serde::Serialize
+and serde::Deserialize
. Obtainable by calling
+InboundGroupSession::pickle
.
Serialize and encrypt the pickle using the given key.
+This is the inverse of InboundGroupSessionPickle::from_encrypted
.
Obtain a pickle from a ciphertext by decrypting and deserializing using +the given key.
+This is the inverse of InboundGroupSessionPickle::encrypt
.
pub struct MegolmMessage { /* private fields */ }
An encrypted Megolm message.
+Contains metadata that is required to find the correct ratchet state of a
+InboundGroupSession
necessary to decryp the message.
The actual ciphertext of the message.
+The index of the message that was used when the message was encrypted.
+Get a reference to the megolm message’s signature.
+Try to decode the given byte slice as a MegolmMessage
.
The expected format of the byte array is described in the
+MegolmMessage::to_bytes()
method.
Encode the MegolmMessage
as an array of bytes.
Megolm messages consist of a one byte version, followed by a variable +length payload, a fixed length message authentication code, and a fixed +length signature.
++---+------------------------------------+-----------+------------------+
+| V | Payload Bytes | MAC Bytes | Signature Bytes |
++---+------------------------------------+-----------+------------------+
+0 1 N N+8 N+72 bytes
+
The payload uses a format based on the Protocol Buffers encoding. It +consists of the following key-value pairs:
+Name | Tag | Type | Meaning |
---|---|---|---|
Message-Index | 0x08 | Integer | The index of the ratchet, i |
Cipher-Text | 0x12 | String | The cipher-text, Xi, of the message |
Try to decode the given string as a MegolmMessage
.
The string needs to be a base64 encoded byte array that follows the
+format described in the MegolmMessage::to_bytes()
method.
Encode the MegolmMessage
as a string.
This method first calls MegolmMessage::to_bytes()
and then encodes
+the resulting byte array as a string using base64 encoding.
source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub struct SessionConfig { /* private fields */ }
A struct to configure how Megolm sessions should work under the hood. +Currently only the MAC truncation behaviour can be configured.
+source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub struct SessionKey { /* private fields */ }
The session key, can be used to create a InboundGroupSession
.
Uses the session-sharing format described in the Olm spec.
++—+––+––––+––––+––––+––––+——+———–+ +| V | i | R(i,0) | R(i,1) | R(i,2) | R(i,3) | Kpub | Signature | ++—+––+––––+––––+––––+––––+——+———–+ +0 1 5 37 69 101 133 165 229 bytes
+The version byte, V, is “\x02”. +This is followed by the ratchet index, iii, which is encoded as a +big-endian 32-bit integer; the 128 bytes of the ratchet; and the public +part of the Ed25519 keypair.
+The data is then signed using the Ed25519 key, and the 64-byte signature is +appended.
+Deserialize the SessionKey
from a byte slice.
Serialize the SessionKey
to a base64 encoded string.
This method will first use the SessionKey::to_bytes()
to
+convert the session key to a byte vector and then encode the byte vector
+to a string using unpadded base64 as the encoding.
Deserialize the SessionKey
from base64 encoded string.
Redirecting to ../../../vodozemac/olm/enum.SessionCreationError.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/account/one_time_keys/struct.OneTimeKeyGenerationResult.html b/vodozemac/olm/account/one_time_keys/struct.OneTimeKeyGenerationResult.html new file mode 100644 index 00000000..11b26b6b --- /dev/null +++ b/vodozemac/olm/account/one_time_keys/struct.OneTimeKeyGenerationResult.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../../vodozemac/olm/struct.OneTimeKeyGenerationResult.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/account/struct.Account.html b/vodozemac/olm/account/struct.Account.html new file mode 100644 index 00000000..ae067822 --- /dev/null +++ b/vodozemac/olm/account/struct.Account.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/olm/struct.Account.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/account/struct.AccountPickle.html b/vodozemac/olm/account/struct.AccountPickle.html new file mode 100644 index 00000000..088e62fc --- /dev/null +++ b/vodozemac/olm/account/struct.AccountPickle.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/olm/struct.AccountPickle.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/account/struct.IdentityKeys.html b/vodozemac/olm/account/struct.IdentityKeys.html new file mode 100644 index 00000000..8516157d --- /dev/null +++ b/vodozemac/olm/account/struct.IdentityKeys.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/olm/struct.IdentityKeys.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/account/struct.InboundCreationResult.html b/vodozemac/olm/account/struct.InboundCreationResult.html new file mode 100644 index 00000000..fde91318 --- /dev/null +++ b/vodozemac/olm/account/struct.InboundCreationResult.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/olm/struct.InboundCreationResult.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/enum.DecryptionError.html b/vodozemac/olm/enum.DecryptionError.html new file mode 100644 index 00000000..f418d192 --- /dev/null +++ b/vodozemac/olm/enum.DecryptionError.html @@ -0,0 +1,30 @@ +pub enum DecryptionError {
+ InvalidMAC(MacError),
+ InvalidMACLength(usize, usize),
+ InvalidPadding(UnpadError),
+ MissingMessageKey(u64),
+ TooBigMessageGap(u64, u64),
+}
Error type for Olm-based decryption failures.
+The message authentication code of the message was invalid.
+The length of the message authentication code of the message did not +match our expected length.
+The ciphertext of the message isn’t padded correctly.
+The session is missing the correct message key to decrypt the message, +either because it was already used up, or because the Session has been +ratcheted forwards and the message key has been discarded.
+Too many messages have been skipped to attempt decrypting this message.
+pub enum MessageType {
+ PreKey,
+ Normal,
+}
An enum over the two supported message types.
+source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub enum OlmMessage {
+ Normal(Message),
+ PreKey(PreKeyMessage),
+}
Enum over the different Olm message types.
+Olm uses two types of messages. The underlying transport protocol must +provide a means for recipients to distinguish between them.
+OlmMessage
provides Serialize
and Deserialize
implementations
+that are compatible with Matrix.
A normal message, contains only the ciphertext and metadata to decrypt +it.
+Create a OlmMessage
from a message type and a ciphertext.
Get the type of the message.
+source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub enum SessionCreationError {
+ MissingOneTimeKey(Curve25519PublicKey),
+ MismatchedIdentityKey(Curve25519PublicKey, Curve25519PublicKey),
+ Decryption(DecryptionError),
+}
Error describing failure modes when creating a Olm Session from an incoming +Olm message.
+The pre-key message contained an unknown one-time key. This happens +either because we never had such a one-time key, or because it has +already been used up.
+The pre-key message contains a curve25519 identity key that doesn’t +match to the identity key that was given.
+The pre-key message that was used to establish the Session couldn’t be +decrypted. The message needs to be decryptable, otherwise we will have +created a Session that wasn’t used to encrypt the pre-key message.
+An implementation of the Olm double ratchet.
+The core component of the crate is the Account
, representing a single Olm
+participant. An Olm Account
consists of a collection of key pairs, though
+often documentation will shorten this to just “keys”. These are:
While the key in 1 is used for signing but not encryption, the keys in 2-4 +participate in a triple Diffie-Hellman key exchange (3DH) with another Olm +participant, thereby establishing an Olm session on each side of the +communication channel. Ultimately, this session is used for deriving the +concrete encryption keys for a particular message.
+Olm sessions are represented by the Session
struct. Such a session is
+created by calling Account::create_outbound_session
on one of the
+participating accounts, passing it the Curve25519 sender key and one
+Curve25519 one-time key of the other side. The protocol is asynchronous, so
+the participant can start sending messages to the other side even before the
+other side has created a session, producing so-called pre-key messages (see
+PreKeyMessage
).
Once the other participant receives such a pre-key message, they can create
+their own matching session by calling Account::create_inbound_session
and
+passing it the pre-key message they received and the Curve25519 sender key
+of the other side. This completes the establishment of the Olm communication
+channel.
use anyhow::Result;
+use vodozemac::olm::{Account, InboundCreationResult, OlmMessage, SessionConfig};
+
+fn main() -> Result<()> {
+ let alice = Account::new();
+ let mut bob = Account::new();
+
+ bob.generate_one_time_keys(1);
+ let bob_otk = *bob.one_time_keys().values().next().unwrap();
+
+ let mut alice_session = alice
+ .create_outbound_session(SessionConfig::version_2(), bob.curve25519_key(), bob_otk);
+
+ bob.mark_keys_as_published();
+
+ let message = "Keep it between us, OK?";
+ let alice_msg = alice_session.encrypt(message);
+
+ if let OlmMessage::PreKey(m) = alice_msg.clone() {
+ let result = bob.create_inbound_session(alice.curve25519_key(), &m)?;
+
+ let mut bob_session = result.session;
+ let what_bob_received = result.plaintext;
+
+ assert_eq!(alice_session.session_id(), bob_session.session_id());
+
+ assert_eq!(message.as_bytes(), what_bob_received);
+
+ let bob_reply = "Yes. Take this, it's dangerous out there!";
+ let bob_encrypted_reply = bob_session.encrypt(bob_reply).into();
+
+ let what_alice_received = alice_session
+ .decrypt(&bob_encrypted_reply)?;
+ assert_eq!(what_alice_received, bob_reply.as_bytes());
+ }
+
+ Ok(())
+}
To encrypt a message, just call Session::encrypt(msg_content)
. This will
+either produce an OlmMessage::PreKey(..)
or OlmMessage::Normal(..)
+depending on whether the session is fully established. A session is fully
+established once you receive (and decrypt) at least one message from the
+other side.
serde::Serialize
+and serde::Deserialize
. Obtainable by calling Account::pickle
.Account
.Session
objects.serde::Serialize
+and serde::Deserialize
. Obtainable by calling Session::pickle
.Redirecting to ../../../vodozemac/olm/enum.MessageType.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/messages/enum.OlmMessage.html b/vodozemac/olm/messages/enum.OlmMessage.html new file mode 100644 index 00000000..9aba00a2 --- /dev/null +++ b/vodozemac/olm/messages/enum.OlmMessage.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/olm/enum.OlmMessage.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/messages/message/struct.Message.html b/vodozemac/olm/messages/message/struct.Message.html new file mode 100644 index 00000000..7c65a4b9 --- /dev/null +++ b/vodozemac/olm/messages/message/struct.Message.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../../vodozemac/olm/struct.Message.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/messages/pre_key/struct.PreKeyMessage.html b/vodozemac/olm/messages/pre_key/struct.PreKeyMessage.html new file mode 100644 index 00000000..882fd9b5 --- /dev/null +++ b/vodozemac/olm/messages/pre_key/struct.PreKeyMessage.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../../vodozemac/olm/struct.PreKeyMessage.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/session/enum.DecryptionError.html b/vodozemac/olm/session/enum.DecryptionError.html new file mode 100644 index 00000000..64c3f723 --- /dev/null +++ b/vodozemac/olm/session/enum.DecryptionError.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/olm/enum.DecryptionError.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/session/ratchet/struct.RatchetPublicKey.html b/vodozemac/olm/session/ratchet/struct.RatchetPublicKey.html new file mode 100644 index 00000000..0bf8b973 --- /dev/null +++ b/vodozemac/olm/session/ratchet/struct.RatchetPublicKey.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../../vodozemac/olm/struct.RatchetPublicKey.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/session/struct.Session.html b/vodozemac/olm/session/struct.Session.html new file mode 100644 index 00000000..44238f12 --- /dev/null +++ b/vodozemac/olm/session/struct.Session.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/olm/struct.Session.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/session/struct.SessionPickle.html b/vodozemac/olm/session/struct.SessionPickle.html new file mode 100644 index 00000000..5c0db13c --- /dev/null +++ b/vodozemac/olm/session/struct.SessionPickle.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/olm/struct.SessionPickle.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/session_config/struct.SessionConfig.html b/vodozemac/olm/session_config/struct.SessionConfig.html new file mode 100644 index 00000000..d8251a70 --- /dev/null +++ b/vodozemac/olm/session_config/struct.SessionConfig.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/olm/struct.SessionConfig.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/session_keys/struct.SessionKeys.html b/vodozemac/olm/session_keys/struct.SessionKeys.html new file mode 100644 index 00000000..13a001a3 --- /dev/null +++ b/vodozemac/olm/session_keys/struct.SessionKeys.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/olm/struct.SessionKeys.html...
+ + + \ No newline at end of file diff --git a/vodozemac/olm/sidebar-items.js b/vodozemac/olm/sidebar-items.js new file mode 100644 index 00000000..0b77a43a --- /dev/null +++ b/vodozemac/olm/sidebar-items.js @@ -0,0 +1 @@ +window.SIDEBAR_ITEMS = {"enum":["DecryptionError","MessageType","OlmMessage","SessionCreationError"],"struct":["Account","AccountPickle","IdentityKeys","InboundCreationResult","Message","OneTimeKeyGenerationResult","PreKeyMessage","RatchetPublicKey","Session","SessionConfig","SessionKeys","SessionPickle"]}; \ No newline at end of file diff --git a/vodozemac/olm/struct.Account.html b/vodozemac/olm/struct.Account.html new file mode 100644 index 00000000..7221cad0 --- /dev/null +++ b/vodozemac/olm/struct.Account.html @@ -0,0 +1,70 @@ +pub struct Account { /* private fields */ }
An Olm account manages all cryptographic keys used on a device.
+Get the IdentityKeys of this Account
+Get a reference to the account’s public Ed25519 key
+Get a reference to the account’s public Curve25519 key
+Sign the given message using our Ed25519 fingerprint key.
+Get the maximum number of one-time keys the client should keep on the +server.
+Note: this differs from the libolm method of the same name, the
+libolm method returned the maximum amount of one-time keys the Account
+could hold and only half of those should be uploaded.
Create a Session
with the given identity key and one-time key.
Create a Session
from the given pre-key message and identity key
Generates the supplied number of one time keys. +Returns the public parts of the one-time keys that were created and +discarded.
+Our one-time key store inside the Account
has a limited amount of
+places for one-time keys, If we try to generate new ones while the store
+is completely populated, the oldest one-time keys will get discarded
+to make place for new ones.
Get the currently unpublished one-time keys.
+The one-time keys should be published to a server and marked as
+published using the mark_keys_as_published()
method.
Generate a single new fallback key.
+The fallback key will be used by other users to establish a Session
if
+all the one-time keys on the server have been used up.
Returns the public Curve25519 key of the previous fallback key, that
+is, the one that will get removed from the Account
when this method
+is called. This return value is mostly useful for logging purposes.
Get the currently unpublished fallback key.
+The fallback key should be published just like the one-time keys, after
+it has been successfully published it needs to be marked as published
+using the mark_keys_as_published()
method as well.
The Account
stores at most two private parts of the fallback key. This
+method lets us forget the previously used fallback key.
Mark all currently unpublished one-time and fallback keys as published.
+Convert the account into a struct which implements serde::Serialize
+and serde::Deserialize
.
Restore an Account
from a previously saved AccountPickle
.
libolm-compat
only.Create an Account
object by unpickling an account pickle in libolm
+legacy pickle format.
Such pickles are encrypted and need to first be decrypted using
+pickle_key
.
pub struct AccountPickle { /* private fields */ }
A format suitable for serialization which implements serde::Serialize
+and serde::Deserialize
. Obtainable by calling Account::pickle
.
A format suitable for serialization which implements serde::Serialize
+and serde::Deserialize
. Obtainable by calling Account::pickle
.
Serialize and encrypt the pickle using the given key.
+This is the inverse of AccountPickle::from_encrypted
.
Obtain a pickle from a ciphertext by decrypting and deserializing using +the given key.
+This is the inverse of AccountPickle::encrypt
.
pub struct IdentityKeys {
+ pub ed25519: Ed25519PublicKey,
+ pub curve25519: Curve25519PublicKey,
+}
Struct holding the two public identity keys of an Account
.
ed25519: Ed25519PublicKey
The ed25519 key, used for signing.
+curve25519: Curve25519PublicKey
The curve25519 key, used for to establish shared secrets.
+source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub struct InboundCreationResult {
+ pub session: Session,
+ pub plaintext: Vec<u8>,
+}
Return type for the creation of inbound Session
objects.
session: Session
The Session
that was created from a pre-key message.
plaintext: Vec<u8>
The plaintext of the pre-key message.
+pub struct Message { /* private fields */ }
An encrypted Olm message.
+Contains metadata that is required to find the correct ratchet state of a
+Session
necessary to decrypt the message.
The public part of the ratchet key, that was used when the message was +encrypted.
+The index of the chain that was used when the message was encrypted.
+The actual ciphertext of the message.
+Has the MAC been truncated in this Olm message.
+Try to decode the given byte slice as a Olm Message
.
The expected format of the byte array is described in the
+Message::to_bytes()
method.
Encode the Message
as an array of bytes.
Olm Message
s consist of a one-byte version, followed by a variable
+length payload and a fixed length message authentication code.
+--------------+------------------------------------+-----------+
+| Version Byte | Payload Bytes | MAC Bytes |
++--------------+------------------------------------+-----------+
+
The payload uses a format based on the Protocol Buffers encoding. It +consists of the following key-value pairs:
+Name | Tag | Type | Meaning |
---|---|---|---|
Ratchet-Key | 0x0A | String | The public part of the ratchet key |
Chain-Index | 0x10 | Integer | The chain index, of the message |
Cipher-Text | 0x22 | String | The cipher-text of the message |
Try to decode the given string as a Olm Message
.
The string needs to be a base64 encoded byte array that follows the
+format described in the Message::to_bytes()
method.
Encode the Message
as a string.
This method first calls Message::to_bytes()
and then encodes the
+resulting byte array as a string using base64 encoding.
pub struct OneTimeKeyGenerationResult {
+ pub created: Vec<Curve25519PublicKey>,
+ pub removed: Vec<Curve25519PublicKey>,
+}
The result type for the one-time key generation operation.
+created: Vec<Curve25519PublicKey>
The public part of the one-time keys that were newly generated.
+removed: Vec<Curve25519PublicKey>
The public part of the one-time keys that had to be removed to make +space for the new ones.
+pub struct PreKeyMessage { /* private fields */ }
The single-use key that was uploaded to a public key directory by the
+receiver of the message. Should be used to establish a Session
.
The base key, a single use key that was created just in time by the
+sender of the message. Should be used to establish a Session
.
The long term identity key of the sender of the message. Should be used
+to establish a Session
The collection of all keys required for establishing an Olm Session
+from this pre-key message.
Other methods on this struct (like PreKeyMessage::identity_key()
)
+can be used to retrieve individual keys from this collection.
Returns the globally unique session ID, in base64-encoded form.
+This is a shorthand helper of the SessionKeys::session_id()
method.
Try to decode the given byte slice as a Olm Message
.
The expected format of the byte array is described in the
+PreKeyMessage::to_bytes()
method.
Encode the PreKeyMessage
as an array of bytes.
Olm PreKeyMessage
s consist of a one-byte version, followed by a
+variable length payload.
+--------------+------------------------------------+
+| Version Byte | Payload Bytes |
++--------------+------------------------------------+
+
The payload uses a format based on the Protocol Buffers encoding. It +consists of the following key-value pairs:
+Name | Tag | Type | Meaning |
---|---|---|---|
One-Time-Key | 0x0A | String | The public part of Bob’s single-use key |
Base-Key | 0x12 | String | The public part of Alice’s single-use key |
Identity-Key | 0x1A | String | The public part of Alice’s identity key |
Message | 0x22 | String | An embedded Olm message |
The last key/value pair in a PreKeyMessage
is a normal Olm
+Message
.
Try to decode the given string as a Olm PreKeyMessage
.
The string needs to be a base64 encoded byte array that follows the
+format described in the PreKeyMessage::to_bytes()
method.
Encode the PreKeyMessage
as a string.
This method first calls PreKeyMessage::to_bytes()
and then encodes
+the resulting byte array as a string using base64 encoding.
source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub struct RatchetPublicKey(_);
source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub struct Session { /* private fields */ }
An Olm session represents one end of an encrypted communication channel +between two participants.
+A session enables enables the session owner to encrypt messages intended +for, and decrypt messages sent by, the other participant of the channel.
+Olm sessions have two important properties:
+An Olm Session
is acquired from an Account
, by calling either
Account::create_outbound_session
, if you are the first participant to
+send a message in
+this channel, orAccount::create_inbound_session
, if the other participant initiated
+the channel by
+sending you a message.Returns the globally unique session ID, in base64-encoded form.
+This is a shorthand helper of the SessionKeys::session_id()
method.
Have we ever received and decrypted a message from the other side?
+Used to decide if outgoing messages should be sent as normal or pre-key +messages.
+Encrypt the plaintext
and construct an OlmMessage
.
The message will either be a pre-key message or a normal message, +depending on whether the session is fully established. A session is +fully established once you receive (and decrypt) at least one +message from the other side.
+Get the keys associated with this session.
+Try to decrypt an Olm message, which will either return the plaintext or
+result in a DecryptionError
.
Convert the session into a struct which implements serde::Serialize
+and serde::Deserialize
.
Restore a Session
from a previously saved SessionPickle
.
libolm-compat
only.Create a Session
object by unpickling a session pickle in libolm
+legacy pickle format.
Such pickles are encrypted and need to first be decrypted using
+pickle_key
.
pub struct SessionConfig { /* private fields */ }
A struct to configure how Olm sessions should work under the hood. +Currently only the MAC truncation behaviour can be configured.
+source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub struct SessionKeys {
+ pub identity_key: Curve25519PublicKey,
+ pub base_key: Curve25519PublicKey,
+ pub one_time_key: Curve25519PublicKey,
+}
The set of keys that were used to establish the Olm Session,
+identity_key: Curve25519PublicKey
§base_key: Curve25519PublicKey
§one_time_key: Curve25519PublicKey
Returns the globally unique session ID which these SessionKeys
will
+produce.
A session ID is the SHA256 of the concatenation of three SessionKeys
,
+the account’s identity key, the ephemeral base key and the one-time
+key which is used to establish the session.
Due to the construction, every session ID is (probabilistically) +globally unique.
+source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub struct SessionPickle { /* private fields */ }
A format suitable for serialization which implements serde::Serialize
+and serde::Deserialize
. Obtainable by calling Session::pickle
.
Serialize and encrypt the pickle using the given key.
+This is the inverse of SessionPickle::from_encrypted
.
Obtain a pickle from a ciphertext by decrypting and deserializing using +the given key.
+This is the inverse of SessionPickle::encrypt
.
pub enum SasError {
+ Mac(MacError),
+}
Error type describing failures that can happen during the key verification.
+The MAC failed to be validated.
+User-friendly key verification using short authentication strings (SAS).
+The verification process is heavily inspired by Phil Zimmermann’s ZRTP +key agreement handshake. A core part of key agreement in ZRTP is the +hash commitment: the party that begins the key sharing process sends +a hash of their part of the Diffie-Hellman exchange but does not send the +part itself exchange until they had received the other party’s part.
+The verification process can be used to verify the Ed25519 identity key of
+an Account
.
use vodozemac::sas::Sas;
+let alice = Sas::new();
+let bob = Sas::new();
+
+let bob_public_key = bob.public_key();
+
+let bob = bob.diffie_hellman(alice.public_key())?;
+let alice = alice.diffie_hellman(bob_public_key)?;
+
+let alice_bytes = alice.bytes("AGREED_INFO");
+let bob_bytes = bob.bytes("AGREED_INFO");
+
+let alice_emojis = alice_bytes.emoji_indices();
+let bob_emojis = bob_bytes.emoji_indices();
+
+assert_eq!(alice_emojis, bob_emojis);
pub struct EstablishedSas { /* private fields */ }
A struct representing a short auth string verification object where the +shared secret has been established.
+This object can be used to generate the short auth string and calculate and +verify a MAC that protects information about the keys being verified.
+Generate SasBytes
using HKDF with the shared secret as the input key
+material.
The info string should be agreed upon beforehand, both parties need to +use the same info string.
+Generate the given number of bytes using HKDF with the shared secret +as the input key material.
+The info string should be agreed upon beforehand, both parties need to +use the same info string.
+The number of bytes we can generate is limited, we can generate up to +32 * 255 bytes. The function will not fail if the given count is smaller +than the limit.
+Calculate a MAC for the given input using the info string as additional +data.
+This should be used to calculate a MAC of the ed25519 identity key of an
+Account
The MAC is returned as a base64 encoded string.
+libolm-compat
only.Calculate a MAC for the given input using the info string as additional +data, the MAC is returned as an invalid base64 encoded string.
+Warning: This method should never be used unless you require libolm +compatibility. Libolm used to incorrectly encode their MAC because the +input buffer was reused as the output buffer. This method replicates the +buggy behaviour.
+Verify a MAC that was previously created using the
+EstablishedSas::calculate_mac()
method.
Users should calculate a MAC and send it to the other side, they should +then verify each other’s MAC using this method.
+Get the public key that was created by us, that was used to establish +the shared secret.
+Get the public key that was created by the other party, that was used to +establish the shared secret.
+pub struct InvalidCount;
Error type for the case when we try to generate too many SAS bytes.
+source
. Read morepub struct Mac(_);
The output type for the SAS MAC calculation.
+Create a new Mac
object from a byte slice.
Create a new Mac
object from a base64 encoded string.
pub struct Sas { /* private fields */ }
A struct representing a short auth string verification object.
+This object can be used to establish a shared secret to perform the short +auth string based key verification.
+Create a new random verification object
+This creates an ephemeral curve25519 keypair that can be used to +establish a shared secret.
+Get the public key that can be used to establish a shared secret.
+Establishes a SAS secret by performing a DH handshake with another +public key.
+Returns an EstablishedSas
object which can be used to generate
+SasBytes
if the given public key was valid, otherwise None
.
Establishes a SAS secret by performing a DH handshake with another +public key in “raw”, base64-encoded form.
+Returns an EstablishedSas
object which can be used to generate
+SasBytes
if the received public key is valid, otherwise None
.
pub struct SasBytes { /* private fields */ }
Bytes generated from an shared secret that can be used as the short auth +string.
+Get the index of 7 emojis that can be presented to users to perform the +key verification
+The table that maps the index to an emoji can be found in the spec.
+pub struct Curve25519PublicKey { /* private fields */ }
Struct representing a Curve25519 public key.
+Create a Curve25519PublicKey
from a byte array.
Instantiate a Curve25519 public key from an unpadded base64 +representation.
+Try to create a Curve25519PublicKey
from a slice of bytes.
source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub struct Curve25519SecretKey(_);
Struct representing a Curve25519 secret key.
+Create a Curve25519SecretKey
from the given slice of bytes.
Perform a Diffie-Hellman key exchange between the given
+Curve25519PublicKey
and this Curve25519SecretKey
and return a shared
+secret.
source
. Read morepub struct Ed25519Keypair { /* private fields */ }
A struct collecting both a public, and a secret, Ed25519 key.
+Get the public Ed25519 key of this keypair.
+Sign the given message with our secret key.
+pub struct Ed25519PublicKey(_);
An Ed25519 public key, used to verify digital signatures.
+Try to create a Ed25519PublicKey
from a slice of bytes.
Instantiate a Ed25519PublicKey public key from an unpadded base64 +representation.
+Serialize a Ed25519PublicKey public key to an unpadded base64 +representation.
+fuzzing
only.Verify that the provided signature for a given message has been signed +by the private key matching this public one.
+By default this performs an RFC8032 compatible signature check. A
+stricter version of the signature check can be enabled with the
+strict-signatures
feature flag.
The stricter variant is compatible with libsodium 0.16 and under the
+hood uses the [ed25519_dalek::PublicKey::verify_strict()
] method.
source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub struct Ed25519SecretKey(_);
An Ed25519 secret key, used to create digital signatures.
+Try to create a Ed25519SecretKey
from a slice of bytes.
Convert the secret key to a base64 encoded string.
+This can be useful if the secret key needs to be sent over the network +or persisted.
+Warning: The string should be zeroized after it has been used, +otherwise an unintentional copy of the key might exist in memory.
+Try to create a Ed25519SecretKey
from a base64 encoded string.
Get the public key that matches this Ed25519SecretKey
.
Sign the given slice of bytes with this Ed25519SecretKey
.
The signature can be verified using the public key.
+use vodozemac::{Ed25519SecretKey, Ed25519PublicKey};
+
+let secret = Ed25519SecretKey::new();
+let message = "It's dangerous to go alone";
+
+let signature = secret.sign(message.as_bytes());
+
+let public_key = secret.public_key();
+
+public_key.verify(message.as_bytes(), &signature).expect("The signature has to be valid");
pub struct Ed25519Signature(_);
An Ed25519 digital signature, can be used to verify the authenticity of a +message.
+Try to create a Ed25519Signature
from a slice of bytes.
Try to create a Ed25519Signature
from an unpadded base64
+representation.
source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub struct KeyId(_);
self
and other
) and is used by the <=
+operator. Read morepub struct ProtoBufDecodeError { /* private fields */ }
A Protobuf message decoding error.
+DecodeError
indicates that the input buffer does not contain a valid
+Protobuf message. The error details should be considered ‘best effort’: in
+general it is not possible to exactly pinpoint why data is malformed.
source
. Read morestd
only.self
and other
values to be equal, and is used
+by ==
.pub struct SharedSecret(_);
The result of a Diffie-Hellman key exchange.
+Each party computes this using their [EphemeralSecret
] or [StaticSecret
] and their
+counterparty’s [PublicKey
].
Ensure in constant-time that this shared secret did not result from a +key exchange with non-contributory behaviour.
+In some more exotic protocols which need to guarantee “contributory” +behaviour for both parties, that is, that each party contributed a public +value which increased the security of the resulting shared secret. +To take an example protocol attack where this could lead to undesirable +results from Thái “thaidn” Dương:
+++If Mallory replaces Alice’s and Bob’s public keys with zero, which is +a valid Curve25519 public key, he would be able to force the ECDH +shared value to be zero, which is the encoding of the point at infinity, +and thus get to dictate some publicly known values as the shared +keys. It still requires an active man-in-the-middle attack to pull the +trick, after which, however, not only Mallory can decode Alice’s data, +but everyone too! It is also impossible for Alice and Bob to detect the +intrusion, as they still share the same keys, and can communicate with +each other as normal.
+
The original Curve25519 specification argues that checks for +non-contributory behaviour are “unnecessary for Diffie-Hellman”. +Whether this check is necessary for any particular given protocol is +often a matter of debate, which we will not re-hash here, but simply +cite some of the relevant public discussions.
+Returns true
if the key exchange was contributory (good), and false
+otherwise (can be bad for some protocols).
Redirecting to ../../../vodozemac/struct.Curve25519PublicKey.html...
+ + + \ No newline at end of file diff --git a/vodozemac/types/curve25519/struct.Curve25519SecretKey.html b/vodozemac/types/curve25519/struct.Curve25519SecretKey.html new file mode 100644 index 00000000..2c2c5e06 --- /dev/null +++ b/vodozemac/types/curve25519/struct.Curve25519SecretKey.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/struct.Curve25519SecretKey.html...
+ + + \ No newline at end of file diff --git a/vodozemac/types/ed25519/enum.SignatureError.html b/vodozemac/types/ed25519/enum.SignatureError.html new file mode 100644 index 00000000..1221b611 --- /dev/null +++ b/vodozemac/types/ed25519/enum.SignatureError.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/enum.SignatureError.html...
+ + + \ No newline at end of file diff --git a/vodozemac/types/ed25519/struct.Ed25519Keypair.html b/vodozemac/types/ed25519/struct.Ed25519Keypair.html new file mode 100644 index 00000000..59371dc1 --- /dev/null +++ b/vodozemac/types/ed25519/struct.Ed25519Keypair.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/struct.Ed25519Keypair.html...
+ + + \ No newline at end of file diff --git a/vodozemac/types/ed25519/struct.Ed25519PublicKey.html b/vodozemac/types/ed25519/struct.Ed25519PublicKey.html new file mode 100644 index 00000000..b647a68d --- /dev/null +++ b/vodozemac/types/ed25519/struct.Ed25519PublicKey.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/struct.Ed25519PublicKey.html...
+ + + \ No newline at end of file diff --git a/vodozemac/types/ed25519/struct.Ed25519SecretKey.html b/vodozemac/types/ed25519/struct.Ed25519SecretKey.html new file mode 100644 index 00000000..d92b0092 --- /dev/null +++ b/vodozemac/types/ed25519/struct.Ed25519SecretKey.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/struct.Ed25519SecretKey.html...
+ + + \ No newline at end of file diff --git a/vodozemac/types/ed25519/struct.Ed25519Signature.html b/vodozemac/types/ed25519/struct.Ed25519Signature.html new file mode 100644 index 00000000..b0ef1372 --- /dev/null +++ b/vodozemac/types/ed25519/struct.Ed25519Signature.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../vodozemac/struct.Ed25519Signature.html...
+ + + \ No newline at end of file diff --git a/vodozemac/types/enum.KeyError.html b/vodozemac/types/enum.KeyError.html new file mode 100644 index 00000000..d12c3b14 --- /dev/null +++ b/vodozemac/types/enum.KeyError.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../vodozemac/enum.KeyError.html...
+ + + \ No newline at end of file diff --git a/vodozemac/types/struct.KeyId.html b/vodozemac/types/struct.KeyId.html new file mode 100644 index 00000000..618927e7 --- /dev/null +++ b/vodozemac/types/struct.KeyId.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../vodozemac/struct.KeyId.html...
+ + + \ No newline at end of file diff --git a/vodozemac/types/struct.SharedSecret.html b/vodozemac/types/struct.SharedSecret.html new file mode 100644 index 00000000..0809c86f --- /dev/null +++ b/vodozemac/types/struct.SharedSecret.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../vodozemac/struct.SharedSecret.html...
+ + + \ No newline at end of file diff --git a/vodozemac/utilities/enum.DecodeError.html b/vodozemac/utilities/enum.DecodeError.html new file mode 100644 index 00000000..c6e77d3e --- /dev/null +++ b/vodozemac/utilities/enum.DecodeError.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../vodozemac/enum.Base64DecodeError.html...
+ + + \ No newline at end of file