Commit 32b1872b authored by johan's avatar johan

Chain key indexes(Ns,Nr,Pn) on 2 bytes instead of 4

- save 4 bytes in DR message header
parent d1f7795e
......@@ -235,7 +235,7 @@ namespace lime {
* @throws when we try to overpass the maximum number of key derivation since last valid message
*/
template <typename Curve>
void DR<Curve>::skipMessageKeys(const uint32_t until, const uint32_t limit) {
void DR<Curve>::skipMessageKeys(const uint16_t until, const int limit) {
if (m_Nr==until) return; // just to be sure we actually have MK to derive and store
// check if there are not too much message keys to derive in this chain
......@@ -359,7 +359,7 @@ namespace lime {
DRMKey MK;
int32_t maxAllowedDerivation = lime::settings::maxMessageSkip;
int maxAllowedDerivation = lime::settings::maxMessageSkip;
m_dirty = DRSessionDbStatus::dirty_decrypt; // we're about to modify the DR session, it will not be in sync anymore with local storage
if (!m_DHr_valid) { // it's the first message arriving after the initialisation of the chain in receiver mode, we have no existing history in this chain
DHRatchet(header.DHs()); // just perform the DH ratchet step
......@@ -391,7 +391,7 @@ namespace lime {
}
// in the derivation limit we remove the derivation done in the previous DH rachet key chain
skipMessageKeys(header.Ns(), static_cast<uint32_t>(maxAllowedDerivation)); // maxAllowedDerivation cannot actually be negative or an exception is raised in previous call to skipMessageKeys
skipMessageKeys(header.Ns(), maxAllowedDerivation); // maxAllowedDerivation cannot actually be negative or an exception is raised in previous call to skipMessageKeys
// generate key material for decryption(and derive Chain key)
KDF_CK(m_CKr, MK);
......
......@@ -42,11 +42,11 @@ namespace lime {
using DRMKey = std::array<uint8_t, lime::settings::DRMessageKeySize+lime::settings::DRMessageIVSize>;
// Shared Associated Data : stored at session initialisation, given by upper level(X3DH), shall be derived from Identity and Identity keys of sender and recipient, fixed size for storage convenience
using SharedADBuffer = std::array<uint8_t, lime::settings::DRSessionSharedADSize>;
// Chain storing the DH and MKs associated with Nr(uint32_t map index)
// Chain storing the DH and MKs associated with Nr(uint16_t map index)
template <typename Curve>
struct receiverKeyChain {
X<Curve> DHr;
std::unordered_map<std::uint32_t, DRMKey> messageKeys;
std::unordered_map<std::uint16_t, DRMKey> messageKeys;
receiverKeyChain(X<Curve> key) :DHr{std::move(key)}, messageKeys{} {};
};
......@@ -66,15 +66,15 @@ namespace lime {
DRChainKey m_RK; // 32 bytes root key
DRChainKey m_CKs; // 32 bytes key chain for sending
DRChainKey m_CKr; // 32 bytes key chain for receiving
std::uint32_t m_Ns,m_Nr; // Message index in sending and receiving chain
std::uint32_t m_PN; // Number of messages in previous sending chain
std::uint16_t m_Ns,m_Nr; // Message index in sending and receiving chain
std::uint16_t m_PN; // Number of messages in previous sending chain
SharedADBuffer m_sharedAD; // Associated Data derived from self and peer device Identity key, set once at session creation, given by X3DH
std::vector<lime::receiverKeyChain<Curve>> m_mkskipped; // list of skipped message indexed by DH receiver public key and Nr, store MK generated during on-going decrypt, lookup is done directly in DB.
/* helpers variables */
bctbx_rng_context_t *m_RNG; // Random Number Generator context
long int m_dbSessionId; // used to store row id from Database Storage
uint32_t m_usedNr; // store the index of message key used for decryption if it came from mkskipped db
uint16_t m_usedNr; // store the index of message key used for decryption if it came from mkskipped db
long m_usedDHid; // store the index of DHr message key used for decryption if it came from mkskipped db(not zero only if used)
lime::Db *m_localStorage; // enable access to the database holding sessions and skipped message keys, no need to use smart pointers here, Db is not owned by DRsession, it must persist even if no session exists
DRSessionDbStatus m_dirty; // status of the object regarding its instance in local storage, could be: clean, dirty_encrypt, dirty_decrypt or dirty
......@@ -83,12 +83,12 @@ namespace lime {
std::vector<uint8_t> m_X3DH_initMessage; // store the X3DH init message to be able to prepend it to any message until we got a first response from peer so we're sure he was able to init the session on his side
/*helpers functions */
void skipMessageKeys(const uint32_t until, const uint32_t limit); /* check if we skipped some messages in current receiving chain, generate and store in session intermediate message keys */
void skipMessageKeys(const uint16_t until, const int limit); /* check if we skipped some messages in current receiving chain, generate and store in session intermediate message keys */
void DHRatchet(const X<Curve> &headerDH); /* perform a Diffie-Hellman ratchet using the given peer public key */
/* local storage related implemented in lime_localStorage.cpp */
bool session_save(); /* save/update session in database : updated component depends m_dirty value */
bool session_load(); /* load session in database */
bool trySkippedMessageKeys(const uint32_t Nr, const X<Curve> &DHr, DRMKey &MK); /* check in DB if we have a message key matching public DH and Ns */
bool trySkippedMessageKeys(const uint16_t Nr, const X<Curve> &DHr, DRMKey &MK); /* check in DB if we have a message key matching public DH and Ns */
public:
DR() = delete; // make sure the Double Ratchet is not initialised without parameters
......
......@@ -37,7 +37,7 @@ namespace lime {
* Supported version description :
*
* Version 0x01:
* DRHeader is: Protocol Version Number<1 byte> || Packet Type <1 byte> || curveId <1 byte> || [X3DH Init message <variable>] || Ns<4 bytes> || PN<4 bytes> || DHs<...>
* DRHeader is: Protocol Version Number<1 byte> || Packet Type <1 byte> || curveId <1 byte> || [X3DH Init message <variable>] || Ns<2 bytes> || PN<2 bytes> || DHs<...>
* Message is : DRheaer<...> || cipherMessageKeyK<48 bytes> || Key auth tag<16 bytes> || cipherText<...> || Message auth tag<16 bytes>
*
* Associated Data are transmitted separately: ADk for the Key auth tag, and ADm for the Message auth tag
......@@ -59,12 +59,12 @@ namespace lime {
/**
* @brief return the size of the double ratchet packet header
* header is: Protocol Version Number<1 byte> || Packet Type <1 byte> || curveId <1 byte> || [X3DH Init message <variable>] || Ns<4 bytes> || PN<4 bytes> || DHs<...>
* header is: Protocol Version Number<1 byte> || Packet Type <1 byte> || curveId <1 byte> || [X3DH Init message <variable>] || Ns<2 bytes> || PN<2 bytes> || DHs<...>
* This function return the size without optionnal X3DH init packet
*/
template <typename Curve>
constexpr size_t headerSize() {
return 11 + X<Curve>::keyLength();
return 7 + X<Curve>::keyLength();
}
/**
......@@ -149,7 +149,7 @@ namespace lime {
template <typename Curve>
bool parseMessage_get_X3DHinit(const std::vector<uint8_t> &message, std::vector<uint8_t> &X3DH_initMessage) noexcept {
// we need to parse the first 4 bytes of the packet to determine if we have a valid one and an X3DH init in it
if (message.size()<4) {
if (message.size()<headerSize<Curve>()) {
return false;
}
......@@ -183,7 +183,7 @@ namespace lime {
/**
* @brief Build a header string from needed info
* header is: Protocol Version Number<1 byte> || Packet Type <1 byte> || curveId <1 byte> || [X3DH Init message <variable>] || Ns<4 bytes> || PN<4 bytes> || DHs<...>
* header is: Protocol Version Number<1 byte> || Packet Type <1 byte> || curveId <1 byte> || [X3DH Init message <variable>] || Ns<2 bytes> || PN<2 bytes> || DHs<...>
*
* @param[out] header output buffer
* @param[in] Ns Index of sending chain
......@@ -192,9 +192,9 @@ namespace lime {
* @param[in] X3DH_initMessage A buffer holding an X3DH init message to be inserted in header, if empty packet type is set to regular
*/
template <typename Curve>
void buildMessage_header(std::vector<uint8_t> &header, const uint32_t Ns, const uint32_t PN, const X<Curve> &DHs, const std::vector<uint8_t> X3DH_initMessage) noexcept {
void buildMessage_header(std::vector<uint8_t> &header, const uint16_t Ns, const uint16_t PN, const X<Curve> &DHs, const std::vector<uint8_t> X3DH_initMessage) noexcept {
// Header is one buffer composed of:
// Version Number<1 byte> || packet Type <1 byte> || curve Id <1 byte> || [<x3d init <variable>] || Ns <4 bytes> || PN <4 bytes> || Key type byte Id(1 byte) || self public key<DHKey::size bytes>
// Version Number<1 byte> || packet Type <1 byte> || curve Id <1 byte> || [<x3d init <variable>] || Ns <2 bytes> || PN <2 bytes> || Key type byte Id(1 byte) || self public key<DHKey::size bytes>
header.assign(1, static_cast<uint8_t>(double_ratchet_protocol::DR_v01));
if (X3DH_initMessage.size()>0) { // we do have an X3DH init message to insert in the header
header.push_back(static_cast<uint8_t>(lime::double_ratchet_protocol::DR_message_type::x3dhinit));
......@@ -204,12 +204,8 @@ namespace lime {
header.push_back(static_cast<uint8_t>(lime::double_ratchet_protocol::DR_message_type::regular));
header.push_back(static_cast<uint8_t>(Curve::curveId()));
}
header.push_back((uint8_t)((Ns>>24)&0xFF));
header.push_back((uint8_t)((Ns>>16)&0xFF));
header.push_back((uint8_t)((Ns>>8)&0xFF));
header.push_back((uint8_t)(Ns&0xFF));
header.push_back((uint8_t)((PN>>24)&0xFF));
header.push_back((uint8_t)((PN>>16)&0xFF));
header.push_back((uint8_t)((PN>>8)&0xFF));
header.push_back((uint8_t)(PN&0xFF));
header.insert(header.end(), DHs.begin(), DHs.end());
......@@ -236,14 +232,13 @@ namespace lime {
// header is : Version<1 byte> ||
// packet type <1 byte> ||
// curve id <1 byte> ||
// Ns<4 bytes> ||
// PN <4 bytes> ||
// Ns<2 bytes> || PN <2 bytes> ||
// DHs < X<Curve>::keyLength() >
m_size = headerSize<Curve>(); // headerSize is the size when no X3DJ init is present
if (header.size() >= m_size) { //header shall be actually longer because buffer pass is the whole message
m_Ns = header[3]<<24|header[4]<<16|header[5]<<8|header[6];
m_PN = header[7]<<24|header[8]<<16|header[9]<<8|header[10];
m_DHs = X<Curve>{header.data()+11}; // DH key start after header other infos
m_Ns = header[3]<<8|header[4];
m_PN = header[5]<<8|header[6];
m_DHs = X<Curve>{header.data()+7}; // DH key start after header other infos
m_valid = true;
}
break;
......@@ -253,7 +248,7 @@ namespace lime {
// packet type <1 byte> ||
// curve id <1 byte> ||
// x3dh init message <variable> ||
// Ns<4 bytes> || PN <4 bytes> ||
// Ns<2 bytes> || PN <2 bytes> ||
// DHs < X<Curve>::keyLength() >
//
// x3dh init is : haveOPk <flag 1 byte : 0 no OPk, 1 OPk > ||
......@@ -269,9 +264,9 @@ namespace lime {
// X3DH init message is processed separatly, just take care of the DR header values
if (header.size() >= m_size) { //header shall be actually longer because buffer pass is the whole message
m_Ns = header[3+x3dh_initMessageSize]<<24|header[4+x3dh_initMessageSize]<<16|header[5+x3dh_initMessageSize]<<8|header[6+x3dh_initMessageSize];
m_PN = header[7+x3dh_initMessageSize]<<24|header[8+x3dh_initMessageSize]<<16|header[9+x3dh_initMessageSize]<<8|header[10+x3dh_initMessageSize];
m_DHs = X<Curve>{header.data()+11+x3dh_initMessageSize}; // DH key start after header other infos
m_Ns = header[3+x3dh_initMessageSize]<<8|header[4+x3dh_initMessageSize];
m_PN = header[5+x3dh_initMessageSize]<<8|header[6+x3dh_initMessageSize];
m_DHs = X<Curve>{header.data()+7+x3dh_initMessageSize}; // DH key start after header other infos
m_valid = true;
}
}
......@@ -291,7 +286,7 @@ namespace lime {
template void buildMessage_X3DHinit<C255>(std::vector<uint8_t> &message, const ED<C255> &Ik, const X<C255> &Ek, const uint32_t SPk_id, const uint32_t OPk_id, const bool OPk_flag) noexcept;
template void parseMessage_X3DHinit<C255>(const std::vector<uint8_t>message, ED<C255> &Ik, X<C255> &Ek, uint32_t &SPk_id, uint32_t &OPk_id, bool &OPk_flag) noexcept;
template bool parseMessage_get_X3DHinit<C255>(const std::vector<uint8_t> &message, std::vector<uint8_t> &X3DH_initMessage) noexcept;
template void buildMessage_header<C255>(std::vector<uint8_t> &header, const uint32_t Ns, const uint32_t PN, const X<C255> &DHs, const std::vector<uint8_t> X3DH_initMessage) noexcept;
template void buildMessage_header<C255>(std::vector<uint8_t> &header, const uint16_t Ns, const uint16_t PN, const X<C255> &DHs, const std::vector<uint8_t> X3DH_initMessage) noexcept;
template class DRHeader<C255>;
#endif
......@@ -299,7 +294,7 @@ namespace lime {
template void buildMessage_X3DHinit<C448>(std::vector<uint8_t> &message, const ED<C448> &Ik, const X<C448> &Ek, const uint32_t SPk_id, const uint32_t OPk_id, const bool OPk_flag) noexcept;
template void parseMessage_X3DHinit<C448>(const std::vector<uint8_t>message, ED<C448> &Ik, X<C448> &Ek, uint32_t &SPk_id, uint32_t &OPk_id, bool &OPk_flag) noexcept;
template bool parseMessage_get_X3DHinit<C448>(const std::vector<uint8_t> &message, std::vector<uint8_t> &X3DH_initMessage) noexcept;
template void buildMessage_header<C448>(std::vector<uint8_t> &header, const uint32_t Ns, const uint32_t PN, const X<C448> &DHs, const std::vector<uint8_t> X3DH_initMessage) noexcept;
template void buildMessage_header<C448>(std::vector<uint8_t> &header, const uint16_t Ns, const uint16_t PN, const X<C448> &DHs, const std::vector<uint8_t> X3DH_initMessage) noexcept;
template class DRHeader<C448>;
#endif
......
......@@ -77,7 +77,7 @@ namespace lime {
* @param[in] X3DH_initMessage A buffer holding an X3DH init message to be inserted in header, if empty packet type is set to regular
*/
template <typename Curve>
void buildMessage_header(std::vector<uint8_t> &header, const uint32_t Ns, const uint32_t PN, const X<Curve> &DHs, const std::vector<uint8_t> X3DH_initMessage) noexcept;
void buildMessage_header(std::vector<uint8_t> &header, const uint16_t Ns, const uint16_t PN, const X<Curve> &DHs, const std::vector<uint8_t> X3DH_initMessage) noexcept;
/**
* DR message header: helper class and functions to parse message header and access its components
......@@ -86,7 +86,7 @@ namespace lime {
template <typename Curve>
class DRHeader {
private:
uint32_t m_Ns,m_PN; // Sender chain and Previous Sender chain indexes.
uint16_t m_Ns,m_PN; // Sender chain and Previous Sender chain indexes.
X<Curve> m_DHs; // Public key
bool m_valid; // is this header valid?
size_t m_size; // store the size of parsed header
......@@ -110,7 +110,7 @@ namespace lime {
extern template void buildMessage_X3DHinit<C255>(std::vector<uint8_t> &message, const ED<C255> &Ik, const X<C255> &Ek, const uint32_t SPk_id, const uint32_t OPk_id, const bool OPk_flag) noexcept;
extern template void parseMessage_X3DHinit<C255>(const std::vector<uint8_t>message, ED<C255> &Ik, X<C255> &Ek, uint32_t &SPk_id, uint32_t &OPk_id, bool &OPk_flag) noexcept;
extern template bool parseMessage_get_X3DHinit<C255>(const std::vector<uint8_t> &message, std::vector<uint8_t> &X3DH_initMessage) noexcept;
extern template void buildMessage_header<C255>(std::vector<uint8_t> &header, const uint32_t Ns, const uint32_t PN, const X<C255> &DHs, const std::vector<uint8_t> X3DH_initMessage) noexcept;
extern template void buildMessage_header<C255>(std::vector<uint8_t> &header, const uint16_t Ns, const uint16_t PN, const X<C255> &DHs, const std::vector<uint8_t> X3DH_initMessage) noexcept;
extern template class DRHeader<C255>;
#endif
......@@ -118,7 +118,7 @@ namespace lime {
extern template void buildMessage_X3DHinit<C448>(std::vector<uint8_t> &message, const ED<C448> &Ik, const X<C448> &Ek, const uint32_t SPk_id, const uint32_t OPk_id, const bool OPk_flag) noexcept;
extern template void parseMessage_X3DHinit<C448>(const std::vector<uint8_t>message, ED<C448> &Ik, X<C448> &Ek, uint32_t &SPk_id, uint32_t &OPk_id, bool &OPk_flag) noexcept;
extern template bool parseMessage_get_X3DHinit<C448>(const std::vector<uint8_t> &message, std::vector<uint8_t> &X3DH_initMessage) noexcept;
extern template void buildMessage_header<C448>(std::vector<uint8_t> &header, const uint32_t Ns, const uint32_t PN, const X<C448> &DHs, const std::vector<uint8_t> X3DH_initMessage) noexcept;
extern template void buildMessage_header<C448>(std::vector<uint8_t> &header, const uint16_t Ns, const uint16_t PN, const X<C448> &DHs, const std::vector<uint8_t> X3DH_initMessage) noexcept;
extern template class DRHeader<C448>;
#endif
/* These constants are needed only for tests purpose, otherwise their usage is internal only */
......
......@@ -328,7 +328,7 @@ bool DR<DHKey>::session_save() {
m_localStorage->sql<<"select last_insert_rowid()",into(DHid); // WARNING: unportable code, sqlite3 only, see above for more details on similar issue
}
// insert all the skipped key in the chain
uint32_t Nr;
uint16_t Nr;
blob MK(m_localStorage->sql);
statement st = (m_localStorage->sql.prepare << "INSERT INTO DR_MSk_MK(DHid,Nr,MK) VALUES(:DHid,:Nr,:Mk)", use(DHid), use(Nr), use(MK));
......@@ -341,7 +341,7 @@ bool DR<DHKey>::session_save() {
// Now do the cleaning(remove unused row from DR_MKs_DHr table) if needed
if (MSk_DHr_Clean == true) {
uint32_t Nr;
uint16_t Nr;
m_localStorage->sql<<"SELECT Nr from DR_MSk_MK WHERE DHid = :DHid LIMIT 1;", into(Nr), use(m_usedDHid);
if (!m_localStorage->sql.got_data()) { // no more MK with this DHid, remove it
m_localStorage->sql<<"DELETE from DR_MSk_DHr WHERE DHid = :DHid;", use(m_usedDHid);
......@@ -393,7 +393,7 @@ bool DR<DHKey>::session_load() {
};
template <typename Curve>
bool DR<Curve>::trySkippedMessageKeys(const uint32_t Nr, const X<Curve> &DHr, DRMKey &MK) {
bool DR<Curve>::trySkippedMessageKeys(const uint16_t Nr, const X<Curve> &DHr, DRMKey &MK) {
blob MK_blob(m_localStorage->sql);
blob DHr_blob(m_localStorage->sql);
DHr_blob.write(0, (char *)(DHr.data()), DHr.size());
......@@ -415,13 +415,13 @@ bool DR<Curve>::trySkippedMessageKeys(const uint32_t Nr, const X<Curve> &DHr, DR
#ifdef EC25519_ENABLED
template bool DR<C255>::session_load();
template bool DR<C255>::session_save();
template bool DR<C255>::trySkippedMessageKeys(const uint32_t Nr, const X<C255> &DHr, DRMKey &MK);
template bool DR<C255>::trySkippedMessageKeys(const uint16_t Nr, const X<C255> &DHr, DRMKey &MK);
#endif
#ifdef EC448_ENABLED
template bool DR<C448>::session_load();
template bool DR<C448>::session_save();
template bool DR<C448>::trySkippedMessageKeys(const uint32_t Nr, const X<C448> &DHr, DRMKey &MK);
template bool DR<C448>::trySkippedMessageKeys(const uint16_t Nr, const X<C448> &DHr, DRMKey &MK);
#endif
/******************************************************************************/
......
......@@ -46,11 +46,16 @@ namespace settings {
constexpr size_t DRMessageAuthTagSize=16;
// Each session stores a shared AD given at built and derived from Identity keys of sender and receiver
// SharedAD is computed by X3DH as SHA256(Identity Key Sender|Identity Key Receiver)
// SharedAD is computed by X3DH HKDF(session Initiator Ik || session receiver Ik || session Initiator device Id || session receiver device Id)
constexpr size_t DRSessionSharedADSize=32;
// Maximum number of Message we can skip(and store their keys) at reception of one message
constexpr std::uint32_t maxMessageSkip=1024;
constexpr std::uint16_t maxMessageSkip=1024;
// Maximum length of Sending chain: is this count is reached without any return from peer,
// the DR session is set to stale and we must create another one to send messages
// Can't be more than 2^16 as message number is send on 2 bytes
constexpr std::uint16_t maxSendingChain=1000;
/******************************************************************************/
/* */
......
......@@ -151,22 +151,22 @@ bool DR_message_holdsX3DHInit(std::vector<uint8_t> &message) {
if (message[1] !=static_cast<uint8_t>(lime::double_ratchet_protocol::DR_message_type::x3dhinit)) return false;
// check packet length, packet is :
// header<3 bytes>, X3DH init packet, Ns+PN<8 bytes>, DHs<X<Curve>::keyLength>, Cipher message Key+tag: DRMessageKey + DRMessageIV <48 bytes>, key auth tag<16 bytes> = <75 + X<Curve>::keyLengh + X3DH init size>
// header<3 bytes>, X3DH init packet, Ns+PN<4 bytes>, DHs<X<Curve>::keyLength>, Cipher message Key+tag: DRMessageKey + DRMessageIV <48 bytes>, key auth tag<16 bytes> = <71 + X<Curve>::keyLengh + X3DH init size>
// X3DH init size = OPk_flag<1 byte> + selfIK<ED<Curve>::keyLength> + EK<X<Curve>::keyLenght> + SPk id<4 bytes> + OPk id (if flag is 1)<4 bytes>
switch (message[2]) {
case static_cast<uint8_t>(lime::CurveId::c25519):
if (message[3] == 0x00) { // no OPk in the X3DH init message
if (message.size() != (75 + X<C255>::keyLength() + 5 + ED<C255>::keyLength() + X<C255>::keyLength())) return false;
if (message.size() != (71 + X<C255>::keyLength() + 5 + ED<C255>::keyLength() + X<C255>::keyLength())) return false;
} else { // OPk present in the X3DH init message
if (message.size() != (75 + X<C255>::keyLength() + 9 + ED<C255>::keyLength() + X<C255>::keyLength())) return false;
if (message.size() != (71 + X<C255>::keyLength() + 9 + ED<C255>::keyLength() + X<C255>::keyLength())) return false;
}
return true;
break;
case static_cast<uint8_t>(lime::CurveId::c448):
if (message[3] == 0x00) { // no OPk in the X3DH init message
if (message.size() != (75 + X<C448>::keyLength() + 5 + ED<C448>::keyLength() + X<C448>::keyLength())) return false;
if (message.size() != (71 + X<C448>::keyLength() + 5 + ED<C448>::keyLength() + X<C448>::keyLength())) return false;
} else { // OPk present in the X3DH init message
if (message.size() != (75 + X<C448>::keyLength() + 9 + ED<C448>::keyLength() + X<C448>::keyLength())) return false;
if (message.size() != (71 + X<C448>::keyLength() + 9 + ED<C448>::keyLength() + X<C448>::keyLength())) return false;
}
return true;
......@@ -193,7 +193,7 @@ bool DR_message_extractX3DHInit(std::vector<uint8_t> &message, std::vector<uint8
}
// copy it in buffer
X3DH_initMessage.assign(message.begin()+4, message.begin()+4+X3DH_length);
X3DH_initMessage.assign(message.begin()+3, message.begin()+3+X3DH_length);
return true;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment