
Overview
The purpose of this article is to demonstrate how easy it is to combine the Win32 Crypto API with a simple Windows Explorer context menu shell extension to give users a way to quickly encrypt and decrypt files through the windows shell. The Source code implements an in-proc COM server that provides two interfaces:
ICrypto
: Provides general encryption/decryption and Base64 encoding/decoding methods
ICryptoShellExt
: Uses ICrypto
to extend the Windows Explorer context menu to provide four menu items shown in the screen shot above
I wrapped the Win32 cryptography behavior and Base64 encoding/decoding in a COM interface named ICrypto
(Crypto.h/Crypto.cpp in the Source). The Base64 encoding/decoding implementation was taken from the Microsoft SOAP SDK. The only modification that I made was a slight enhancement to the decoding algorithm to increase decoding performance. ICryptoShellExt
implements the shell extension interfaces necessary for extending the Windows Context menu. ICrypto
provides general methods for encrypting/decrypting files, Base64 encoding/decoding and generating digital signatures.
Supported Platforms:
Windows '95 OSR2 (Internet Explorer 3.02 and higher)
Windows '98
Windows ME
Windows NT
Windows 2000
Full support for UNICODE
Win32 Crypto API Functions Used:
CryptCreateHash
CryptEncrypt
CryptDestroyKey
CryptDestroyHash
CryptDecrypt
CryptImportKey
CryptReleaseContext
CryptAcquireContext
CryptGetUserKey
CryptGenKey
CryptExportKey
CryptSignHash
ICrypto Methods:
HRESULT
EncryptDoc( [in] BSTR bstrSrc, [in, optional] VARIANT varDestination );
HRESULT
DecryptDoc( [in] BSTR bstrSrc, [in, optional] VARIANT varDestination );
HRESULT
Base64EncodeString( [in] BSTR bstrSrc, [out, retval] BSTR* pbstrResult );
HRESULT
Base64DecodeString( [in] BSTR bstrSrc, [out, retval] BSTR* pbstrResult );
HRESULT
EncryptString( [in] BSTR bstrSrc, [out, retval] BSTR* pbstrResult );
HRESULT
DecryptString( [in] BSTR bstrSrc, [out, retval] BSTR* pbstrResult );
HRESULT
VerifyDigitalSignature( [in] BSTR bstrDigSig,
[out, retval] BOOL* pbMatches );
HRESULT
Base64EncodeFile( [in] BSTR bstrSrc,
[in, optional] VARIANT varDestination );
HRESULT
Base64DecodeFile( [in] BSTR bstrSrc,
[in, optional] VARIANT varDestination );
HRESULT
Base64EncodeFileToString( [in] BSTR bstrSrc,
[out, retval] BSTR* pbstrResult );
HRESULT
Base64DecodeFileToString( [in] BSTR bstrSrc,
[out, retval] BSTR* pbstrResult );
ICrypto Properties:
HRESULT
DigitalSignature( [out, retval] BSTR* pbstrResult );
HRESULT
get_ContainerName( [out, retval] BSTR* pbstrResult );
HRESULT
put_ContainerName( [in] BSTR bstrContainerName );
Digital Signatures
Digital signatures are calculated when the data is being encrypted using what the Crypto API documentation refers to as a hash. I maintain the hash object internally before doing any encryption or decryption. Once the encryption/decryption is complete and the digital signature is calculated,
ICrypto
provides access to it via the property named
DigitalSignature
. The
DigitalSignature
is stored internally as a BLOB. However, when a client requests the Digital Signature
ICrypto
passes it out as a Base64 encoded string (to ease use for scripting clients). Scripting clients typically have a difficult time with binary data (sometimes handled as a
SAFEARRAY
of variants).
Typically, you would perform the encryption on a file or memory, then immediate get the Digital Signature property from
ICrypto
and store it. Later when you decrypt the file or memory you would call
VerifyDigitalSignature
to determine if the digital signature matches (if the contents of the file or memory has changed since you encrypted).
ContainerName Property
The container name should be an application unique identifier for the key container. ICrypto
uses this property when acquiring a handle to the context (CryptAcquireContext). If you fail to provide a container name, then ICrypto
uses its own container name. Here's some code taken from the ICrypto implementation that shows how the container name is used during initialization:
BOOL CCrypto::InitializeContainer() const
{
USES_CONVERSION;
BOOL bSuccess = FALSE;
int nStringID= IDS_UNKNOWN_ERROR;
ATLASSERT( !m_hContext );
bSuccess = CryptAcquireContext( &m_hContext,
W2T(m_bstrContainerName.m_str), MS_DEF_PROV, PROV_RSA_FULL, 0 );
if( !bSuccess )
{
int nLastError = GetLastError();
if( nLastError == NTE_BAD_KEYSET )
{
bSuccess = CryptAcquireContext( &m_hContext,
W2T(m_bstrContainerName.m_str), MS_DEF_PROV,
PROV_RSA_FULL, CRYPT_NEWKEYSET );
if( !bSuccess )
nStringID = IDS_ERROR_INITIALIZATION_CREATE_NEW_CONTAINER_FAILED;
}
else nStringID = LookupCryptErrorStringID( nLastError );
}
if( !bSuccess )
CRYPTO_OUTPUT_DEBUGSTRING( nStringID );
return bSuccess;
}
Initialization
Once I "finished" the first revision of the implementation I decided it was time to start testing on various Windows platforms (other than my production machine). The first platform that I tried was a clean machine running Windows '98 SP1. The test harness immediately failed. After several hours of debugging and reading, I discovered an important article in MSDN regarding initialization of the key container. I found a sample in the knowledge base for "best practices" for initialization and basically cut-and-pasted that sample into ICrypto. Be sure to see how CCrypto::Initialize() works if you plan to use the Win32 Crypto API.
Samples:
The following code samples demonstrate how to use ICrypto with a
C++ and
Java Script client. See also CryptoShellExt.cpp in the
Source code for another sample in ATL/C++.
How to use ICrypto with a C++ Client:
Steps:
- Download the source, unzip it to a folder and build the project
- Be sure the CryptoAPI.dll is located somewhere in your include path
- Import the type information for ICrypto by inserting the following code in the source file in your project that is going to use ICrypto:
#import "CryptoAPI.DLL" named_guids raw_interfaces_only
- Create the instance of ICrypto and initialize the container name for your application (for more information about Cryptographic Service Provider key container names, see: http://msdn.microsoft.com/library/psdk/crypto/cryptoref1_0wvo.htm)
.
.
CComPtr<ICrypto> pCrypto;
if( SUCCEEDED( pCrypto.CoCreateInstance( CLSID_Crypto ) ) )
{
ATLASSERT( pCrypto != NULL );
if( pCrypto )
{
pCrypto->put_ContainerName( L"MY_APPLICATION_CSP_CONTAINER_NAME" );
.
.
.
- Use the functions in the API to encrypt/decrypt files:
if( SUCCEEDED( pCrypto->EncryptDoc( bstrFileName, vtMissing ) )
{
pCrypto->get_DigitalSignature( &bstrDigitalSignature );
}
else
{
IErrorInfo* pErrorInfo = NULL;
if( ::GetErrorInfo( 0, &pErrorInfo ) == S_OK && pErrorInfo != NULL)
{
CComBSTR bstrErrorMsg;
pErrorInfo->GetDescription( &bstrErrorMsg );
::MessageBox( NULL, _bstr_t(bstrErrorMsg.m_str), NULL,
MB_OK | MB_ICONEXCLAMATION );
pErrorInfo->Release();
}
}
How to use ICrypto
with a Java Script Client:
Encrypting a File
function OnEncryptFile()
{
try
{
var Crypto = new ActiveXObject( "CryptoAPI.Crypto" );
Crypto.ContainerName = "MY_APPLICATION_CSP_CONTAINER_NAME";
Crypto.EncryptDoc( g_strFileToEncrypt );
g_strDigitalSignature = Crypto.DigitalSignature;
delete Crypto;
}
catch( exception )
{
window.alert( exception.description );
}
Verifying the Digital Signature
function OnVerifySignature()
{
try
{
var Crypto = new ActiveXObject( "CryptoAPI.Crypto" );
Crypto.ContainerName = "MY_APPLICATION_CSP_CONTAINER_NAME";
Crypto.DecryptDoc( g_strFileToDecrypt );
if( !Crypto.VerifyDigitalSignature( g_strDigitalSignature ) )
{
.
.
.
}
delete Crypto;
}
catch( exception )
{
window.alert( exception.description );
}

File Encryption Binary Format

ICryptoShellExt
Guarding Against Multiple Encryptions
Guarding against the user encrypting a file more than once was an interesting problem to solve. CryptEncrypt returns NTE_DOUBLE_ENCRYPT
when you try to encrypt data more than once. However, since ICrypto
adds other information to the file (i.e. version, key size) it would need to try and extract the encrypted data and run it through CryptEncrypt to check the return code.
After coming up with a couple of other alternatives (that I didn't like), I decided to run the problem by one of my colleagues. He immediately came up with multiple solutions. One of which I used in ICryptoShellExt
. Before encrypting a file, he suggested that I attempt to decrypt the file to a temp file. If the decryption was indeed successful, then the file was already encrypted. Otherwise, the file indeed needed to be encrypted for the first time. Here is the code that handles this case in ICryptoShellExt
:
.
.
.
HCURSOR hCursor = SetCursor( LoadCursor( NULL, IDC_WAIT ) );
switch( Action )
{
case CCryptoShellExt::cryptEncrypt:
{
USES_CONVERSION;
_variant_t varDblEncryptFilename = GetDblCryptTempFilename();
if( FAILED( pCrypto->DecryptDoc( m_pbstrFiles[ nIndex ].m_str,
varDblEncryptFilename ) ) )
{
( lpctstrDestination != NULL ) ?
CHECK_HR( pCrypto->EncryptDoc(
m_pbstrFiles[ nIndex ].m_str, _variant_t(bstrDestination) ) ):
CHECK_HR( pCrypto->EncryptDoc( m_pbstrFiles[ nIndex ].m_str,
vtMissing ) );
}
_unlink( W2A(varDblEncryptFilename.bstrVal) );
break;
}
.
.
.
Further Enhancements
The shell extension could easily be extended to include a quick view feature to display the decrypted contents using the associated viewer associated with the file type using ShellExecute(...).
There are several samples in MSDN that demonstrate how to hash in a password during encryption. The shell extension and ICrypto
implementations could be enhanced to provide a way to password protect encrypted files. The examples show how to hash in a password as part of the signed hash.
Referring to the in-memory binary format, the private key is included in the encrypted results which increases the size of the result by approximately 76 bytes (typical size of the encrypted private key). Splitting the private key from the in-memory encrypted string could be accomplished by changing the ICrypto
interface to return the encrypted string and private key separately.
ICrypto
could be enhanced to include properties for setting the CSP algorithm used for hashing and encrypting block and stream ciphers. It currently uses CALG_RC2 for block encryption, CALG_RC4 for stream encryption and CALG_MD5 for hashing. See MSDN for more information about the different algorithms supported by the Win32 crypto API.
ICrypto
could be enhanced to include properties for tweaking the number of bytes read from a file at-a-time. For large files, the implementation could be altered to incorporate caching and reading larger blocks of data for enhancing performance. The current implementation reads in 512 bytes at-a-time.
Reporting Defects and Suggestions
Please report all defects to me by email:
mailto:slater_chad@hotmail.com
Please also feel free to submit suggestions and comments. I wrote this article in hopes to help others and to gain feedback from other developers. Enjoy!