Symmetric ciphers -- HowTo

How do I use the symmetric cipher framework?

An introduction to the symmetric cipher framework

The symmetric cipher framework collates the behaviour of all symmetric ciphers under one interface: CSymmetricCipher. This interface is intended to represent one direction of one instance of any symmetric cipher. One direction means either encryption or decryption, but not both.

CSymmetricCipher interface basics

The following code illustrates the creation of a buffered AES ECB encryptor and an ARC4 stream cipher.


CBlockTransformation* block = 0;
block = CAESEncryptor::NewLC(aKey);
CPadding* padding = CPaddingSSLv3::NewLC(KAESBlockSize); //The blocksize of AES (16 bytes)
CSymmetricCipher* cipher = CBufferedEncryptor::NewL(block, padding);
CleanupStack::Pop(2, block); //padding, block -> both owned by cipher

CSymmetricCipher* cipher = new(ELeave)CARC4(aKey);
CleanupStack::PushL(cipher):

After creation, both examples are usable through the CSymmetricCipher interface. So, to encrypt with either of the above ciphers one could do:


HBufC8* output = HBufC8::NewLC(cipher->MaxOutputLength(input.Size()));
cipher->Process(input, output);
HBufC8* output2 = HBufC8::NewLC(cipher->MaxFinalOutputLength(input2.Size()));
cipher->ProcessFinalL(input2, output2);

In this example, input and input2 are two arbitrary, finite length descriptors. The derived implementations of CSymmetricCipher (CBufferedEncryptor / CBufferedDecryptor and CStreamCipher) are responsible for handling what to do in each specific case. For example, in the case of an encrypting block cipher, ProcessFinalL() will call the underlying padding system, which will in turn ensure that the overall length of input plaintext is of a suitable length for encryption. For more information on how the values returned from MaxOutputLength() and MaxFinalOutputLength() are calculated see How does buffering work within the symmetric cipher framework?.

Example code for a symmetric factory class

To simplify the process of creating symmetric encryptors and decryptors, it is strongly recommended that applications create a factory that automates the process for them. The following code gives a sample factory that applications might like to use as a reference. It is not supplied as part of the framework as every application has different ways of identifying symmetric cipher suites.


CSymmetricCipher* CCipherFactory::BuildEncryptorL(
    TSymmetricCipherType aType,const TDesC8& aKey,const TDesC8& aIV)
    {
    CSymmetricCipher* cipher = NULL;

    if (aType==ERc4)
        {
        cipher = new(ELeave) CARC4(aKey);
        }
    else
        {
        CBlockTransformation* bT = NULL;
        switch (aType)
            {
            case EDes_cbc:
                bT = CDESEncryptor::NewL(aKey);
                break;
            case EDes_ede3_cbc:
                bT = C3DESEncryptor::NewL(aKey);
                break;
            case ERc2_cbc:
                bT = CRC2Encryptor::NewL(aKey);
                break;
            default:
                User::Leave(KErrNotSupported);
            };
        CleanupStack::PushL(bT);
        CBlockTransformation* mode = CModeCBCEncryptor::NewL(bT, aIV);
        CleanupStack::Pop(bT);    //    owned by mode
        CleanupStack::PushL(mode);
     
        CPadding* padding = CPaddingSSLv3::NewLC(KBlockSize); //All of these ciphers use 8 byte blocks
        cipher = CBufferedEncryptor::NewL(mode, padding);
        CleanupStack::Pop(2, mode);    //padding, mode    now owned by cipher
        }
    return cipher;
    }

Applications creating these factories need to supply an equivalent to the TSymmetricCipherType enum, which contains the list of identifiers representing the cipher, padding, and mode requirements of the application.

Note that a similar BuildDecryptorL() will also have to be created.

Good naming conventions dictate that applications should not pollute the global namespace and either use their own namespace or prefix their factory classes with identifiers that associated it with that specific application.

Symmetric Modes

When the amount of plaintext to be encrypted is larger than a single block, some method must be employed to specify how subsequent blocks are dependent on previous blocks. The simplest method, known as ECB (Electronic CodeBook), specifies that subsequent blocks are completely independent. Therefore, two identical blocks of plaintext will encrypt to two identical blocks of ciphertext. ECB has significant security drawbacks, thus most applications use more advanced modes in which subsequent blocks are dependent on the ciphertext of previous blocks. The symmetric framework handles these modes through the CBlockChainingMode class, which is a specialization of CBlockTransformation. The idea is that one gives an implementation of a CBlockChainingMode another CBlockTransformation (CAESEncryptor, for instance) and then performs all operations on the CBlockChainingMode instance. When CBlockTransformation::Transform() is called on the mode, it is responsible for calling Transform() on the underlying transformation that it owns and then applying its own chaining mode transformation.

The following example shows how to create a buffered AES CBC encryptor.


CBlockTransformation* basicAesBlock = 0;
CBlockTransformation* cbcBlock = 0;
basicAesBlock = CAESEncryptor::NewLC(aKey);
cbcBlock = CModeCBCEncryptor::NewL(basicAesBlock, iv);
CleanupStack::Pop(basicAesBlock); //owned by cbcBlock
CleanupStack::PushL(cbcBlock);
CPadding* padding = CPaddingSSLv3::NewLC(KAESBlockSize); //The blocksize of AES (16 bytes)
CSymmetricCipher* cipher = CBufferedEncryptor::NewL(cbcBlock, padding);
CleanupStack::Pop(2, cbcBlock); //padding, cbcBlock -> both owned by cipher

Which symmetric cipher should I use?

Generally, when implementing secure comms protocols, the cipher you use will be dictated by the protocol specification. However, if you are writing your own application, you should consider the use of AES (CAESEncryptor); this is the cipher recommended by NIST.

How does buffering work within the symmetric cipher framework?

  • Stream ciphers consume all content they are given. That is, the value returned from CSymmetricCipher::MaxOutputLength() is always the same as the aInputLength parameter passed in.

  • Block ciphers controlled through a CBufferedTransformation operate under the following rules:

    • Process()

      • Any previously buffered data is (logically) prepended to aInput.

      • All whole blocks are transformed and appended to aOutput.

      • Any remaining partial blocks, orphaned by the above rule, are buffered.

    • ProcessFinalL()

      • Encryption

        1. Any previously buffered data is (logically) prepended to aInput.

        2. All whole block are transformed and appended to aOutput.

        3. Any remaining partial blocks are padded with underlying padding system to be block aligned to the padding block size. (In the vast majority of cases, the padding block size is equal to the block cipher block size).

        4. The resulting block(s) are transformed and appended to aOutput.

      • Decryption

        1. The input must be a multiple of the block size.

        2. Data is decrypted and unpadded using underlying padding system.

        3. Decrypted, unpadded data is appended to aOutput.

In all cases CSymmetricCipher::MaxOutputLength() returns as tight an upper bound as possible on the number of bytes that will be returned by a call to CSymmetricCipher::Process() with a specified number of input bytes. Correspondingly, CSymmetricCipher::MaxFinalOutputLength() returns a similar bound but for a pending call to CSymmetricCipher::ProcessFinalL().