Qt Cryptographic Architecture: Architecture

# Architecture

Note:
  You don't need to understand any of this to use QCA - it is documented for those who are curious, and for anyone planning to extend or modify QCA.

The design of QCA is based on the Bridge design pattern. The intent of the Bridge pattern is to "Decouple an abstraction from its implementation so that the two can vary independently." [Gamma et.al, pg 151].

To understand how this decoupling works in the case of QCA, is is easiest to look at an example - a cryptographic Hash. The API is pretty simple (although I've left out some parts that aren't required for this example):

    class QCA_EXPORT Hash : public Algorithm, public BufferedComputation
    {
    public:
        Hash(const QString &type, const QString &provider);
        virtual void clear();
        virtual void update(const QCA::SecureArray &a);
        virtual QCA::SecureArray final();
    }

The implementation for the Hash class is almost as simple:

    Hash::Hash(const QString &type, const QString &provider)
    :Algorithm(type, provider)
    {
    }

    void Hash::clear()
    {
            static_cast<HashContext *>(context())->clear();
    }

    void Hash::update(const QCA::SecureArray &a)
    {
            static_cast<HashContext *>(context())->update(a);
    }

    QCA::SecureArray Hash::final()
    {
            return static_cast<HashContext *>(context())->final();
    }

The reason why it looks so simple is that the various methods in Hash just call out to equivalent routines in the context() object. The context comes from a call (getContext()) that is made as part of the Algorithm constructor. That getContext() call causes QCA to work through the list of providers (generally plugins) that it knows about, looking for a provider that can produce the right kind of context (in this case, a HashContext).

The code for a HashContext doesn't need to be linked into QCA - it can be varied in its implementation, including being changed at run-time. The application doesn't need to know how HashContext is implemented, because it just has to deal with the Hash class interface. In fact, HashContext may not be implemented, so the application should check (using QCA::isSupported()) before trying to use features that are implemented with plugins.

The code for one implementation (in this case, calling OpenSSL) is shown below.

    class opensslHashContext : public HashContext
    {
    public:
        opensslHashContext(const EVP_MD *algorithm, Provider *p, const QString &type) : HashContext(p, type)
        {
            m_algorithm = algorithm;
            EVP_DigestInit( &m_context, m_algorithm );
        };

        ~opensslHashContext()
        {
            EVP_MD_CTX_cleanup(&m_context);
        }

        void clear()
        {
            EVP_MD_CTX_cleanup(&m_context);
            EVP_DigestInit( &m_context, m_algorithm );
        }

        void update(const QCA::SecureArray &a)
        {
            EVP_DigestUpdate( &m_context, (unsigned char*)a.data(), a.size() );
        }

        QCA::SecureArray final()
        {
            QCA::SecureArray a( EVP_MD_size( m_algorithm ) );
            EVP_DigestFinal( &m_context, (unsigned char*)a.data(), 0 );
            return a;
        }

        Provider::Context *clone() const
        {
            return new opensslHashContext(*this);
        }

    protected:
        const EVP_MD *m_algorithm;
        EVP_MD_CTX m_context;
    };

This approach (using an Adapter pattern) is very common in QCA backends, because the plugins are often based on existing libraries.

In addition to the various Context objects, each provider also has a parameterised Factory class that has a createContext() method, as shown below:

            Context *createContext(const QString &type)
            {
                    //OpenSSL_add_all_digests();
                    if ( type == "sha1" )
                            return new opensslHashContext( EVP_sha1(), this, type);
                    else if ( type == "sha0" )
                            return new opensslHashContext( EVP_sha(), this, type);
                    else if ( type == "md5" )
                            return new opensslHashContext( EVP_md5(), this, type);
                    else if ( type == "aes128-cfb" )
                            return new opensslCipherContext( EVP_aes_128_cfb(), 0, this, type);
                    else if ( type == "aes128-cbc" )
                            return new opensslCipherContext( EVP_aes_128_cbc(), 0, this, type);
                    else
                            return 0;
            }

The resulting effect is that QCA can ask the provider to provide an appropriate Context object without worrying about how it is implemented.

For features that are implemented with variable algorithms (for example, HashContext can support a wide range of algorithms - MD5, SHA0, and SHA1 in the example above; and CipherContext and MACContext can also do this), we need to be able to let applications determine which algorithms are supported. This is handled through the InfoContext class. A typical example is shown below:

            class opensslInfoContext : public InfoContext
            {
                    Q_OBJECT
            public:
                    opensslInfoContext(Provider *p) : InfoContext(p)
                    {
                    }

                    Context *clone() const
                    {
                            return new opensslInfoContext(*this);
                    }

                    QStringList supportedHashTypes() const
                    {
                            QStringList list;
                            list += "sha1";
                            list += "sha0";
                            list += "md5";
                            return list;
                    }

                    // MAC and Cipher types can go in here
            };

Note that InfoContext is itself a feature, so you have to add it to the createContext() method for the provider, as shown below:

            Context *createContext(const QString &type)
            {
                    if ( type == "sha1" )
                            return new opensslHashContext( EVP_sha1(), this, type);
                    else if ( type == "sha0" )
                            return new opensslHashContext( EVP_sha(), this, type);
                    else if ( type == "md5" )
                            return new opensslHashContext( EVP_md5(), this, type);
                    else if ( type == "info" )
                            return new opensslInfoContext( this );
                    else
                            return 0;
            }