/// <summary>
        /// Encrypt a message.
        /// </summary>
        /// <param name="paddedPlaintext">The plaintext message bytes, optionally padded.</param>
        /// <returns>Ciphertext.</returns>
        /// <exception cref="NoSessionException"></exception>
        public byte[] encrypt(byte[] paddedPlaintext)
        {
            lock (LOCK)
            {
                try
                {
                    SenderKeyRecord  record         = senderKeyStore.loadSenderKey(senderKeyId);
                    SenderKeyState   senderKeyState = record.getSenderKeyState();
                    SenderMessageKey senderKey      = senderKeyState.getSenderChainKey().getSenderMessageKey();
                    byte[]           ciphertext     = getCipherText(senderKey.getIv(), senderKey.getCipherKey(), paddedPlaintext);

                    SenderKeyMessage senderKeyMessage = new SenderKeyMessage(senderKeyState.getKeyId(),
                                                                             senderKey.getIteration(),
                                                                             ciphertext,
                                                                             senderKeyState.getSigningKeyPrivate());

                    senderKeyState.setSenderChainKey(senderKeyState.getSenderChainKey().getNext());

                    senderKeyStore.storeSenderKey(senderKeyId, record);

                    return(senderKeyMessage.serialize());
                }
                catch (InvalidKeyIdException e)
                {
                    throw new NoSessionException(e);
                }
            }
        }
        /// <summary>
        /// Construct a group session for sending messages.
        /// </summary>
        /// <param name="senderKeyName">The (groupId, senderId, deviceId) tuple. In this case, 'senderId' should be the
        /// caller.</param>
        /// <returns>A SenderKeyDistributionMessage that is individually distributed to each member of the group.</returns>
        public SenderKeyDistributionMessage create(SenderKeyName senderKeyName)
        {
            lock (GroupCipher.LOCK)
            {
                try
                {
                    SenderKeyRecord senderKeyRecord = senderKeyStore.loadSenderKey(senderKeyName);

                    if (senderKeyRecord.isEmpty())
                    {
                        senderKeyRecord.setSenderKeyState(KeyHelper.generateSenderKeyId(),
                                                          0,
                                                          KeyHelper.generateSenderKey(),
                                                          KeyHelper.generateSenderSigningKey());
                        senderKeyStore.storeSenderKey(senderKeyName, senderKeyRecord);
                    }

                    SenderKeyState state = senderKeyRecord.getSenderKeyState();

                    return(new SenderKeyDistributionMessage(state.getKeyId(),
                                                            state.getSenderChainKey().getIteration(),
                                                            state.getSenderChainKey().getSeed(),
                                                            state.getSigningKeyPublic()));
                }
                catch (Exception e) when(e is InvalidKeyIdException || e is InvalidKeyException)
                {
                    throw new Exception(e.Message);
                }
            }
        }
        /// <summary>
        /// Decrypt a SenderKey group message.
        /// </summary>
        /// <param name="senderKeyMessageBytes">The received ciphertext.</param>
        /// <param name="callback">A callback that is triggered after decryption is complete, but before the updated
        /// session state has been committed to the session DB. This allows some implementations to store the committed
        /// plaintext to a DB first, in case they are concerned with a crash happening between the time the session
        /// state is updated but before they're able to store the plaintext to disk.</param>
        /// <returns>Plaintext</returns>
        /// <exception cref="LegacyMessageException"></exception>
        /// <exception cref="InvalidMessageException"></exception>
        /// <exception cref="DuplicateMessageException"></exception>
        /// <exception cref="NoSessionException"></exception>
        public byte[] decrypt(byte[] senderKeyMessageBytes, DecryptionCallback callback)
        {
            lock (LOCK)
            {
                try
                {
                    SenderKeyRecord record = senderKeyStore.loadSenderKey(senderKeyId);

                    if (record.isEmpty())
                    {
                        throw new NoSessionException("No sender key for: " + senderKeyId);
                    }

                    SenderKeyMessage senderKeyMessage = new SenderKeyMessage(senderKeyMessageBytes);
                    SenderKeyState   senderKeyState   = record.getSenderKeyState(senderKeyMessage.getKeyId());

                    senderKeyMessage.verifySignature(senderKeyState.getSigningKeyPublic());

                    SenderMessageKey senderKey = getSenderKey(senderKeyState, senderKeyMessage.getIteration());

                    byte[] plaintext = getPlainText(senderKey.getIv(), senderKey.getCipherKey(), senderKeyMessage.getCipherText());

                    callback.handlePlaintext(plaintext);

                    senderKeyStore.storeSenderKey(senderKeyId, record);

                    return(plaintext);
                }
                catch (Exception e) when(e is InvalidKeyException || e is InvalidKeyIdException)
                {
                    throw new InvalidMessageException(e);
                }
            }
        }