Init-Update-Final

From Crypto++ Wiki
Jump to navigation Jump to search

Many libraries, like Java and OpenSSL, provide an Init-Update-Final model for transformations. While its not readily apparent, Crypto++ provides the same interface by way of BufferedTransformation and its Put and Get functions. This page will show you how to explicitly provide an Init-Update-Final interface with Crypto++ objects.

The JavaCipher class below exposes a Java-like Init-Update-Final interface. The class is a bastardization of what Java offers, but it includes a minimally featured getInstance, init, update and final. In addition, a JavaAlgorithmParameter is provided to allow init to be called similar to Java's Cipher::init. Finally, a driver program is provided to show how the JavaCipher behaves.

Internally, JavaCipher holds two Crypto++ object pointers. The first is a SymmetricCipher and it provides the actual encryption or decryption of the data, but that's all it does. A second object is used to layer services like buffering and padding on top of the encryption and decryption. The second object is a StreamTransformationFilter.

The JavaCipher class does not track state, so you can probably produce unexpected results, if you try. For example, you can call update after final with bytes remaining in the filter from the previous encryption.

Init-Update-Final

The JavaCipher class has four functions to provide the Init-Update-Final interface. They are:

  • getInstance - requests the transformation
  • init - initializes the transformation
  • update - inputs plain text to the transformation, and retrieves cipher text from the transformation
  • final - finalizes the plain text and retrieves cipher text from the transformation

getInstance returns a pointer to a new JavaCipher object. Crypto++ cannot instantiate an object yet because it needs more information (namely, the cipher's direction). The string transformation is saved privately for use later in init.

init instantiates a concrete SymmetricCipher for encryption and decryption; and it instantiates a StreamTransformationFilter for buffering and padding. It also keys the cipher using the key and initialization vector provided in the AlgorithmParameter.

update inserts plain text bytes for encryption, and retrieves cipher text when available. If there are not enough plain text bytes to encrypt, then the StreamTransformationFilter buffers the input. If there is no output buffer to retrieve an encrypted block, then the StreamTransformationFilter buffers the output.

final flushes plain text that was buffered for encryption, adds padding and then buffers the encrypted output. If the output buffer is large enough, all the cipher text is passed back to the caller.

To ensure all remaining bytes are retrieved in final, provide a buffer that is at least 2*BLOCKSIZE. In the case of TripleDES with a 64-bit (8 byte) block size in CBC mode, use a 16 byte buffer; and for AES with a 128-bit (16 byte) block size in CBC mode, use a 32 byte buffer. CTR mode encrypts 1-for-1, and it does not have block requirements.

Driver Program

The driver program below gets an instance of an AES cipher via getInstance, initializes it using init, and then feeds plain text bytes one at a time using update. As encrypted blocks become available, the driver fetches them through update. Finally, a call to final is made to finish the cipher, add padding, and flush the remaining bytes buffered by the filter.

Though the code below pushes one byte at a time, the same technique can be used to encrypt a large number of bytes from a file. In the large file case, you will encrypt 512 or 4096 bytes at a time rather than one byte. In addition, in between calls to update, you can revise the status of a progress bar for the operation.

The HexEncoder is used to pretty print the output of the raw bytes.

byte key[32], iv[16];
OS_GenerateRandomBlock(false, key, COUNTOF(key));
OS_GenerateRandomBlock(false, iv, COUNTOF(iv));

HexEncoder encoder(new FileSink(cout));
    
JavaAlgorithmParameter params;
params.key = key;
params.ksize = COUNTOF(key);
params.iv = iv;
params.vsize = COUNTOF(iv);

JavaCipher* cipher = JavaCipher::getInstance("AES/CBC/PKCSPadding");
cipher->init(ENCRYPT_MODE, params);

cout << "Algorithm: " << cipher->getAlgorithm() << endl;

cout << "Key: ";
encoder.Put(key, COUNTOF(key));
cout << endl;

cout << "IV: ";
encoder.Put(iv, COUNTOF(iv));
cout << endl;

byte buffer[64];
size_t ready = 0;

for(unsigned int i = 0; i <= 255; i++)
{
    byte b = (byte)i;

    cout << "Put 0x";
    encoder.Put(b);
    cout << endl;

    ready = cipher->update(&b, 1, buffer, COUNTOF(buffer));
    if(ready)
    {
        cout << "Get: ";
        encoder.Put(buffer, ready);
        cout << endl;
    }
}

ready = cipher->final(buffer, COUNTOF(buffer));
if(ready)
{
    cout << "Final: ";
    encoder.Put(buffer, ready);
    cout << endl;
}    

delete cipher;

JavaCipher

The JavaCipher is shown below. The interesting function is init, which instantiates the Crypto++ objects and sets the padding mode based on Java's transformation string.

Crypto++ offers an object factory, so the if/then/else found in init can probably be cleaned up even further, if desired.

enum {ENCRYPT_MODE=1, DECRYPT_MODE=2};

struct JavaAlgorithmParameter
{
    JavaAlgorithmParameter()
        : key(NULL), ksize(0), iv(NULL), vsize(0) {}

    const byte* key;
    size_t ksize;
    const byte* iv;
    size_t vsize;
};

/////////////////////////
/////////////////////////

class JavaCipher
{
public:
    static JavaCipher* getInstance(const std::string& transformation);
    
    void init(int opmode, const JavaAlgorithmParameter& params);
    size_t update(const byte* in, size_t isize, byte* out, size_t osize);
    size_t final(byte* out, size_t osize);

    std::string getAlgorithm() const;

protected:
    JavaCipher(const std::string& transformation);

private:
    std::string m_transformation;
    member_ptr<SymmetricCipher> m_cipher;
    member_ptr<StreamTransformationFilter> m_filter;
};

/////////////////////////
/////////////////////////

JavaCipher* JavaCipher::getInstance(const std::string& transformation)
{
    return new JavaCipher(transformation);
}

JavaCipher::JavaCipher(const std::string& transformation)
    : m_transformation(transformation) { }

std::string JavaCipher::getAlgorithm() const
{
    return m_transformation;
}

/////////////////////////
/////////////////////////

size_t JavaCipher::final(byte* out, size_t osize)
{
    m_filter.get()->MessageEnd();

    if(!out || !osize || !m_filter.get()->AnyRetrievable())
        return 0;

    size_t t = STDMIN(m_filter.get()->MaxRetrievable(), (word64)osize);
    return m_filter.get()->Get(out, t);
}

/////////////////////////
/////////////////////////

size_t JavaCipher::update(const byte* in, size_t isize, byte* out, size_t osize)
{
    if(in && isize)
        m_filter.get()->Put(in, isize);

    if(!out || !osize || !m_filter.get()->AnyRetrievable())
        return 0;

    size_t t = STDMIN(m_filter.get()->MaxRetrievable(), (word64)osize);
    t = m_filter.get()->Get(out, t);
    return t;
}

/////////////////////////
/////////////////////////

void JavaCipher::init(int opmode, const JavaAlgorithmParameter& params)
{
    if(m_transformation == "AES/ECB/PKCSPadding" && opmode == ENCRYPT_MODE)
    {
        m_cipher.reset(new ECB_Mode<AES>::Encryption);
        m_cipher.get()->SetKey(params.key, params.ksize);
        m_filter.reset(new StreamTransformationFilter(*m_cipher.get(), NULL, BlockPaddingSchemeDef::PKCS_PADDING));
    }
    else if(m_transformation == "AES/ECB/PKCSPadding" && opmode == DECRYPT_MODE)
    {
        m_cipher.reset(new ECB_Mode<AES>::Decryption);
        m_cipher.get()->SetKey(params.key, params.ksize);    
        m_filter.reset(new StreamTransformationFilter(*m_cipher.get(), NULL, BlockPaddingSchemeDef::PKCS_PADDING));
    }
    else if(m_transformation == "AES/CBC/PKCSPadding" && opmode == ENCRYPT_MODE)
    {
        m_cipher.reset(new CBC_Mode<AES>::Encryption);
        m_cipher.get()->SetKeyWithIV(params.key, params.ksize, params.iv);
        m_filter.reset(new StreamTransformationFilter(*m_cipher.get(), NULL, BlockPaddingSchemeDef::PKCS_PADDING));
    }
    else if(m_transformation == "AES/CBC/PKCSPadding" && opmode == DECRYPT_MODE)
    {
        m_cipher.reset(new CBC_Mode<AES>::Decryption);
        m_cipher.get()->SetKeyWithIV(params.key, params.ksize, params.iv);        
        m_filter.reset(new StreamTransformationFilter(*m_cipher.get(), NULL, BlockPaddingSchemeDef::PKCS_PADDING));
    }
    else if(m_transformation == "AES/CTR/NoPadding" && opmode == ENCRYPT_MODE)
    {
        m_cipher.reset(new CTR_Mode<AES>::Encryption);
        m_cipher.get()->SetKeyWithIV(params.key, params.ksize, params.iv);
        m_filter.reset(new StreamTransformationFilter(*m_cipher.get(), NULL, BlockPaddingSchemeDef::NO_PADDING));
    }
    else if(m_transformation == "AES/CTR/NoPadding" && opmode == DECRYPT_MODE)
    {
        m_cipher.reset(new CTR_Mode<AES>::Decryption);
        m_cipher.get()->SetKeyWithIV(params.key, params.ksize, params.iv);        
        m_filter.reset(new StreamTransformationFilter(*m_cipher.get(), NULL, BlockPaddingSchemeDef::NO_PADDING));
    }
    else
        throw NotImplemented(m_transformation + " is not implemented");
}

Driver Output

The following shows the output of the driver program using CBC and CTR modes of operation.

CBC Mode

Below is AES operated in CBC mode. The cipher is setup with JavaCipher* cipher = JavaCipher::getInstance("AES/CBC/PKCSPadding");.

Notice bytes are fed in one at a time using update. If a full block is not available for encryption, then the Crypto++ filter buffers the input. When a full encrypted block is available, update returns it to the caller and it is displayed.

If the caller does not retrieve the encrypted block, then the filter buffers it, too.

$ ./test.exe
Algorithm: AES/ECB/PKCSPadding
Key: 15ECBA73B482F31033518A69D485585E4D2D927EC5BEF63A095023AFB64A6488
IV: 553BD62B3F5E104905A4E2CC0B2009BC

Put 0x00
Put 0x01
...
Put 0x0E
Put 0x0F
Get: 07E2F449E5A53304581017D240F81E3F
Put 0x10
Put 0x11
...
Put 0x1E
Put 0x1F
Get: 4707189A19E82EFCEC91BC3C15756EEE
Put 0x20
Put 0x21
...
Put 0x2E
Put 0x2F
Get: 746D7A3B76AD90A0C5F470C5E6C14D63
Put 0x30
Put 0x31
...
Put 0x3E
Put 0x3F
Get: B5D648C9B2E1C71DABF73F3FC59BC6CF
Put 0x40
Put 0x41
...
Put 0x4E
Put 0x4F
Get: 6C9E0F46DE3186CCD587CA7A2DD34C12
Put 0x50
Put 0x51
...
Put 0x5E
Put 0x5F
Get: F1FD85042FDA8500C1D48BC353AEB7BF
Put 0x60
Put 0x61
...
Put 0x6E
Put 0x6F
Get: AC216AB4E914835C43F8C2EB0123B775
Put 0x70
Put 0x71
...
Put 0x7E
Put 0x7F
Get: 921CDF1AC38B7407A4640A3D9112E02A
Put 0x80
Put 0x81
...
Put 0x8E
Put 0x8F
Get: 083A23BDBDB70E9E8FCCD0EC67C2986F
Put 0x90
Put 0x91
...
Put 0x9E
Put 0x9F
Get: BF9AE679C366489ED93499CFEEDF9D83
Put 0xA0
Put 0xA1
...
Put 0xAE
Put 0xAF
Get: 5DC8B090D5F241CC86E54177F7399DA2
Put 0xB0
Put 0xB1
...
Put 0xBE
Put 0xBF
Get: 45221B4D549BE380679BE1000F45075C
Put 0xC0
Put 0xC1
...
Put 0xCE
Put 0xCF
Get: 0556D818482E8B78D336DEBF59BA3D5C
Put 0xD0
Put 0xD1
...
Put 0xDE
Put 0xDF
Get: D821857AB4CB086B21E998CEC3268F91
Put 0xE0
Put 0xE1
...
Put 0xEE
Put 0xEF
Get: 1A5668DEDF0D9546CF9EE826CCC416E8
Put 0xF0
Put 0xF1
...
Put 0xFE
Put 0xFF
Get: 3E7F3B29CED905E20DAEA75EAF7A56DB
Final: 2173CEF0F16565AAE9395647960A8E74

CTR Mode

Below is AES operated in CTR mode. The cipher is setup with JavaCipher* cipher = JavaCipher::getInstance("AES/CTR/NoPadding");.

Notice a cipher text byte is available as soon as plain text byte is input because the plaintext is XOR'd with the CTR's keystream. Its not buffered like in CBC mode. final does not produce an encrypted block because padding is not used.

If the caller does not retrieve the encrypted byte, then the filter buffers it.

$ ./test.exe 
Algorithm: AES/CTR/NoPadding
Key: 6DDEDEB9EE9EED8CE3F0F8EE17C470EA205A2E36B7F271BB315F88F193664AF3
IV: 68BF4AC6CC400443248437AEB166CA63
Put 0x00
Get: 50
Put 0x01
Get: 29
Put 0x02
Get: C1
Put 0x03
Get: BB
...
Put 0xFC
Get: 56
Put 0xFD
Get: 1E
Put 0xFE
Get: 70
Put 0xFF
Get: BA

Downloads

cryptopp-init-update-final.zip - sample classes and program to demonstrate Init-Update-Final