Understanding CAPICOM EncryptedData


CAPICOM provides scripters (VBS, ASP, ASP.NET etc.) with an easy to use approach for encrypting data based on secure underlying Windows CryptoAPI functionality. Encryption of data to recipients having valid X509 certificates (CMS/PKCS#7 Enveloped Data) is supported in the CAPICOM EnvelopedData object which is compatible with most CMS/PKCS#7 Enveloped Data implementations (e.g. S/MIME email clients).

For some applications however, it is more convenient to implement data encryption based on a password-derived symmetric key approach. CAPICOM supports password-derived data encryption with the EncryptedData object. However the format of CAPICOM EncryptedData uses a non-standard ASN format (different from the PKCS#7 EncryptedData content type), the details are not public and currently decryption of the CAPICOM EncryptedData is only supported in CAPICOM. In some cases it is desirable to be able to decrypt a CAPICOM EncryptedData structure without requiring CAPICOM to be installed locally. This requires a basic understanding of some details of the CAPICOM EncryptedData ASN format and how the symmetric key is derived from the CAPICOM EncryptedData contents. This note provides the information necessary to decode and decrypt a CAPICOM EncryptedData using .NET or CryptoAPI alone.

ASN.1 dump of CAPICOM EncryptedData:
The following script code snippet, based on the CAPICOM sample Encrypt.vbs, reads a content file, specifies RC2 128 bit encryption, and encrypts the data file read into the Content string:

  Set EncryptedData = CreateObject("CAPICOM.EncryptedData")
  LoadFile InFile, Content
  EncryptedData.Content = Content
  EncryptedData.SetSecret "mypswd"
  EncryptedData.Algorithm.Name = CAPICOM_ENCRYPTION_ALGORITHM_RC2
  EncryptedData.Algorithm.KeyLength = CAPICOM_ENCRYPTION_KEY_LENGTH_128_BITS
  Message = EncryptedData.Encrypt

The following is a dump of the CAPICOM EncryptedData blob for RC2 128 bit encryption, in binary form, generated by the sample code above:


   0 30  572: SEQUENCE {
   4 06    9:   OBJECT IDENTIFIER '1 3 6 1 4 1 311 88 3'
  15 A0  557:   [0] {
  19 30  553:     SEQUENCE {
  23 06   10:       OBJECT IDENTIFIER '1 3 6 1 4 1 311 88 3 1'
  35 A0  537:       [0] {
  39 30  533:         SEQUENCE {
  43 02    3:           INTEGER 131072
  48 02    2:           INTEGER 26114
  52 02    2:           INTEGER 128
  56 04    8:           OCTET STRING
            :             08 6D D8 A7 80 09 41 9A
  66 04   16:           OCTET STRING
            :             95 F1 7C 8C CC FA 62 18 DF 55 65 F0 04 C7 63 5A
  84 04  488:           OCTET STRING
            :             24 CD 4C 1A FA CF 89 A4 B2 AA B0 B8 A8 E7 C6 C6
            :             45 04 84 B6 0A 31 AD BB 84 45 8B EE 51 F7 F9 6E
		          .... remaining encrypted blob  ......
The first two OIDs are Microsoft-specific OIDs referring to CAPICOM EncryptedData. The 1.3.6.1.4.1.311.88.3.1 "CAPICOM content of encrypted data" has sequence members which describe the encryption algorithm, key size, IV for symmetric encryption, and the Salt value used for key derivation. So, for the example above:
  131072  - CAPICOM version information ??
  26114   - RC2 algorithm identifier  (CryptoAPI  CALG_RC2)
  128     - key size (128 bits)
  08 6D D8 A7 80 09 41 9A   - Initialization Vector for RC2 encryption
  95 F1 7C 8C CC FA 62 18 DF 55 65 F0 04 C7 63 5A   - Salt value for key derivation
  24 CD 4C 1A FA CF 89 A4 B2 AA B0 ...............  - Cipher text


How the CAPICOM EncryptedData symmetric key is derived:
The symmetric key used to encrypt data in EncryptedData is derived from a supplied password as well as a randomly generated Salt value (which is embedded in the EncryptedData blob). [Note that while .NET provides a PasswordDeriveBytes(String pswd, byte[] salt) object, the method used by CAPICOM to derive a symmetric key cannot be simulated directly with the current .NET implemention. Also, the CryptoAPI compatibility method PasswordDeriveBytes.CryptDeriveKey() does not use the salt parameter.] The CAPICOM password, initialized with oEncryptedData.SetSecret is internally passed as a UNICODE byte array and is hashed first, followed by the random Salt bytes (extracted from EncryptedData blob in the same byte-order). Then, for RC2, the first 16 bytes of the resultant 20 byte SHA1 hash are extracted as the encryption key:


  byte[] mysalt = {0x95, 0xF1, 0x7C, 0x8C, 0xCC, 0xFA, 0x62, 0x18, 
		   0xDF, 0x55, 0x65, 0xF0, 0x04, 0xC7, 0x63, 0x5A};
  String capicompswd = "mypswd";  //CAPICOM pswd (hashed as UNICODE by EncryptedData)

  byte[] pswd = (new UnicodeEncoding()).GetBytes(capicompswd);
  byte[] sha1hash = CHashSaltPswd(mysalt, pswd) ; //hash of salt+pswd
  byte[] pbeKey = new byte[16];   //For RC2 128bit, derived key is first 16 bytes of SHA1 hash
  Array.Copy(sha1hash, pbeKey, pbeKey.Length);
  ...
  ...
  private static byte[] CHashSaltPswd(byte[] salt, byte[] pswd)
  {
   SHA1 sha1 = SHA1.Create();
   CryptoStream cs = new CryptoStream(Stream.Null, sha1, CryptoStreamMode.Write);
   cs.Write(pswd, 0, pswd.Length);
   cs.Write(salt, 0, salt.Length);
   cs.FlushFinalBlock();
   return sha1.Hash;
  }

How CAPICOM EncryptedData encrypts data:
Having derived the symmetric key based on supplied password and Salt, CAPICOM EncryptedData.Encrypt performs standard encryption (CBC mode) using a randomly generated (underlying CryptoAPI CryptGenRandom()) IV vector. For decryption, this IV is extracted from the EncryptedData blob, again in the same byte order (no byte reversal) for use in either .NET Crypto classes or CryptoAPI. Note however that in CAPICOM, data is often initialized as a text string. When the data is passed to CAPICOM in this way via oEncryptedData.Content = <string data>, the actual content encrypted is the UNICODE representation of the data. The distribution CAPICOM script samples which read text files have the data encrypted as UNICODE (meaning that the cipher block size is roughly twice the size of the plain text file). The following .NET encryption sample shows RC2 encryption in .NET which generates the identical cipher text block as CAPICOM EncryptedData, in the same byte order, using the same derived key and IV as CAPICOM. In this sample, the RC2 encryption block is written to a binary file:

 byte[] IV = {0x08, 0x6D, 0xD8, 0xA7, 0x80, 0x09, 0x41, 0x9A} ;
 Rc2EncryptData("sigline.txt", "netencrypted", pbeKey, IV);

 private static void Rc2EncryptData(String contentFile, String outName, byte[] rc2Key, byte[] rc2IV)
 {    
    byte[] contentbytes = GetFileBytes(contentFile);
    String content	= (new ASCIIEncoding()).GetString(contentbytes);
    byte[] unicontent	= (new UnicodeEncoding()).GetBytes(content);

    FileStream fout = new FileStream(outName, FileMode.OpenOrCreate, FileAccess.Write);
    RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider();          
    CryptoStream encStream = new CryptoStream(fout, rc2.CreateEncryptor(rc2Key, rc2IV), CryptoStreamMode.Write);
    encStream.Write(unicontent, 0, unicontent.Length);
    encStream.Close();                     
 }



 private static byte[] GetFileBytes(String filename){
	if(!File.Exists(filename))
	 return null;
	Stream stream=new FileStream(filename,FileMode.Open);
	int datalen = (int)stream.Length;
	byte[] filebytes =new byte[datalen];
	stream.Seek(0,SeekOrigin.Begin);
	stream.Read(filebytes,0,datalen);
	stream.Close();
	return filebytes;
  }

A hex display of the .NET generated RC2 encryption block is shown below and is identical to that displayed in the original CAPICOM EncryptedData blob:


   24 CD 4C 1A FA CF 89 A4 B2 AA B0 B8 A8 E7 C6 C6 
   45 04 84 B6 0A 31 AD BB 84 45 8B EE 51 F7 F9 6E 
   D8 C0 A9 39 D3 DB 83 B9 DE C2 BA 57 B1 0D C7 D3 
   .............

3DES Case: How the CAPICOM EncryptedData symmetric key is derived:
The above example demonstrated RC2 128 bit (16 bytes) key derivation, where the symmetric key material was a subset of the SHA1 hash of the contatenated password and salt. Internally, CAPICOM EncryptedData invokes CryptoAPI methods such CryptCreateHash(), CryptHashData(), CryptDeriveKey(), CryptSetKeyParam(). For 3DES keys with a key length of 192 bits (24 bytes), which includes the parity bits, the algorithm used by CryptDeriveKey() is more complex. The following example explicitly demonstrates this algorithm in C# code. Of course P/Invoke in .NET could also be used directly to call the CryptoAPI functions.

The following is a dump of the CAPICOM EncryptedData blob for 3DES 192 bit encryption, in binary form, generated by sample code similar to that above:

0000 30   7A: SEQUENCE {
0002 06    9:   OBJECT IDENTIFIER '1 3 6 1 4 1 311 88 3'
000D A0   6D:   [0] {
000F 30   6B:     SEQUENCE {
0011 06    A:       OBJECT IDENTIFIER '1 3 6 1 4 1 311 88 3 1'
001D A0   5D:       [0] {
001F 30   5B:         SEQUENCE {
0021 02    3:           INTEGER 131073
0026 02    2:           INTEGER 26115
002A 02    2:           INTEGER 192
002E 04    8:           OCTET STRING
            :             EE A8 74 F7 CE BF 76 A0
0038 04   10:           OCTET STRING
            :             4B B0 EF FF 03 30 ED C2 B1 BF FF E2 8C E5 16 2B
004A 04   30:           OCTET STRING
            :             6F DA 47 06 B2 78 7E C7 14 E1 86 43 56 5D 5A A9
            :             52 50 EB 8F B7 42 D3 4D E5 89 6E 46 23 8B B5 3D
            :             6E B0 42 B1 4E EE 74 12 AB 26 B7 E0 5F 2D 71 71
            :           }
            :         }
            :       }
            :     }
            :   }
Similar to the RC2 case,
  131073  - CAPICOM version information
  26115   - 3DES algorithm identifier 
  192     - key size (192 bits or 24 bytes)
  EE A8 74 F7 CE BF 76 A0   - Initialization Vector for 3DES encryption
  4B B0 EF FF 03 30 ED C2 B1 BF FF E2 8C E5 16 2B   - Salt value for key derivation
  6F DA 47 06 B2 78 7E C7 14 E1 86 43 56 5D 5A A9   - Cipher text (48 bytes)
  52 50 EB 8F B7 42 D3 4D E5 89 6E 46 23 8B B5 3D
  6E B0 42 B1 4E EE 74 12 AB 26 B7 E0 5F 2D 71 71

The following code snippet demonstrates the algorithm used by CryptDeriveKey() to derive a 3DES key:


 byte[] iv  = {0xEE,  0xA8,  0x74,  0xF7,  0xCE,  0xBF,  0x76,  0xA0} ;
 byte[] mysalt = {0x4B,  0xB0,  0xEF,  0xFF,  0x03,  0x30,  0xED,  0xC2,  
	0xB1,  0xBF,  0xFF,  0xE2,  0x8C,  0xE5,  0x16,  0x2B};

 String capicompswd = "mypassword";  //CAPICOM pswd (hashed as UNICODE by EncryptedData)
 byte[] pswd = (new UnicodeEncoding()).GetBytes(capicompswd);
 byte[] tdeskey = new byte[24] ;
 byte[] buff1 = new byte[64];
 byte[] buff2 = new byte[64];
 for (int i = 0; i<64; i++)
 {
	buff1[i] = 0x36;
	buff2[i] = 0x5C;
 }

  byte[] psalthash = CHashSaltPswd(mysalt, pswd) ; //hash of pswd + salt

  byte[] concathash = new byte[40] ;  //to hold contatenated SHA1 hash data

 // ----- Step 1; XOR first 24 bytes of buff1 with SHA1 hash
	for (int i = 0; i<20; i++)
	  buff1[i] = (byte) (buff1[i] ^ psalthash[i]) ;


 // ----- Step 2; XOR first 24 bytes of buff2 with SHA1 hash
	for (int i = 0; i<20; i++)
	  buff2[i] = (byte) (buff2[i] ^ psalthash[i]) ;
 
// ----- Step 3; hash the entire first buffer
   byte[] hash3 = sha1hash(buff1);

// ----- Step 4; hash the entire second buffer
   byte[] hash4 = sha1hash(buff2);


// ----- Step 5: Concatenate the two hashes --------
 Array.Copy(hash3, 0, concathash, 0, hash3.Length);
 Array.Copy(hash4, 0, concathash, hash3.Length, hash4.Length);
 
// ----- Step 6: Use the first 24 bytes of the contatenated hash as the 3DES key
 Array.Copy(concathash, 0, tdeskey, 0, tdeskey.Length);

 // Encrypt or decrypt with this derived 3DES key and IV:
----- 
Summary:
This note has provided some information required to decode and decrypt CAPICOM EncryptedData blobs using either .NET or CryptoAPI alone. It is fairly easy to write a decoder to easily extract the Salt, IV, key algorithm and keysize from an EncryptedData structure completely in .NET.

CAPICOM EncryptedData

CryptDeriveKey()

PKCS #7 - Cryptographic Message Syntax Standard


Michel I. Gallant
neutron@istar.ca