/* $Id: XmlEncDecrypt.cs $
* Last updated:
* $Date: 2020-11-26 12:29:00 $
* $Version: 2.0.0 $
*/
#define TRACE
using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using Xmlsq;
using Pki = CryptoSysPKI;
/******************************* LICENSE ***********************************
* Copyright (C) 2020 David Ireland, DI Management Services Pty Limited.
* All rights reserved. <www.di-mgt.com.au> <www.cryptosys.net>
* The code in this module is licensed under the terms of the MIT license.
* For a copy, see <http://opensource.org/licenses/MIT>
****************************************************************************
*/
/* This shows how to extract and decrypt the cipher value from an XMLENC document.
* It uses an heuristic approach to extract the required values from the XMLENC example documents we use here.
* It expects a single <EncryptedData> element with a single child <KeyInfo> element.
* This code is intended to demonstrate principles you might use in your own programs.
* NOTE: these examples do not use any prefixes: all elements are in the default namespace.
* Other XML documents may use the "xenc:" and "ds:" prefixes. You will need to modify this code to cope with that.
* If prefixes are used they need to be "hardcoded" in the xmlsq search queries (or use the XPath local-name() function).
* */
/* PSEUDOCODE:
* Confirm EncryptedData element exists; else stop.
* Get DataEncryption algorithm, expecting "*-cbc" or "*-gcm".
* Get CipherValue.
* Get the key - this may be a shared symmetric key referenced by a KeyName or an EncryptedKey.
* Use key and the DataEncryption algorithm to decrypt the cipher value.
* Return the decrypted text - we expect UTF-8 to be returned as a Unicode string.
* */
/* [2020-11] Added support for rsa-oaep-mgf1p and rsa-oaep */
namespace TestXmlsqPki
{
class TestXmlsqPki
{
/* Lookup tables mapping W3C XMLENC algorithm identifiers to CryptoSys PKI options */
// W3C XMLENC Block Cipher CBC algorithms (all use Mode.CBC, Padding.W3CPadding and Cipher.Opts.PrefixIV)
private static readonly Dictionary<string, Pki.CipherAlgorithm> W3C_CipherAlgs = new Dictionary<string, Pki.CipherAlgorithm>()
{
{ "http://www.w3.org/2001/04/xmlenc#tripledes-cbc", Pki.CipherAlgorithm.Tdea },
{ "http://www.w3.org/2001/04/xmlenc#aes128-cbc", Pki.CipherAlgorithm.Aes128 },
{ "http://www.w3.org/2001/04/xmlenc#aes192-cbc", Pki.CipherAlgorithm.Aes192 },
{ "http://www.w3.org/2001/04/xmlenc#aes256-cbc", Pki.CipherAlgorithm.Aes256 },
};
// W3C XMLENC Block Cipher GCM algorithms (all use Cipher.Opts.PrefixIV)
private static readonly Dictionary<string, Pki.AeadAlgorithm> W3C_GcmAlgs = new Dictionary<string, Pki.AeadAlgorithm>()
{
{ "http://www.w3.org/2009/xmlenc11#aes128-gcm", Pki.AeadAlgorithm.Aes_128_Gcm },
{ "http://www.w3.org/2009/xmlenc11#aes192-gcm", Pki.AeadAlgorithm.Aes_192_Gcm },
{ "http://www.w3.org/2009/xmlenc11#aes256-gcm", Pki.AeadAlgorithm.Aes_256_Gcm },
};
// W3C XMLENC Symmetric Key Wrap algorithms
private static readonly Dictionary<string, Pki.CipherAlgorithm> W3C_KeyWrapAlgs = new Dictionary<string, Pki.CipherAlgorithm>()
{
{ "http://www.w3.org/2001/04/xmlenc#kw-tripledes", Pki.CipherAlgorithm.Tdea },
{ "http://www.w3.org/2001/04/xmlenc#kw-aes128", Pki.CipherAlgorithm.Aes128 },
{ "http://www.w3.org/2001/04/xmlenc#kw-aes192", Pki.CipherAlgorithm.Aes192 },
{ "http://www.w3.org/2001/04/xmlenc#kw-aes256", Pki.CipherAlgorithm.Aes256 },
};
// W3C XMLENC RSA Key Transport algorithms
private static readonly Dictionary<string, Pki.Rsa.EME> W3C_RsaAlgs = new Dictionary<string, Pki.Rsa.EME>()
{
{ "http://www.w3.org/2001/04/xmlenc#rsa-1_5", Pki.Rsa.EME.PKCSv1_5 },
{ "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p", Pki.Rsa.EME.OAEP },
{ "http://www.w3.org/2009/xmlenc11#rsa-oaep", Pki.Rsa.EME.OAEP },
};
// W3C XMLENC Message Digest algorithms for the RSAES-OAEP-ENCRYPT algorithm
// (expected as a parameter for rsa-oaep or rsa-oaep-mgfp1)
private static readonly Dictionary<string, Pki.Rsa.HashAlg> W3C_HashAlgs = new Dictionary<string, Pki.Rsa.HashAlg>()
{
{ "http://www.w3.org/2000/09/xmldsig#sha1", Pki.Rsa.HashAlg.Sha1 },
{ "http://www.w3.org/2001/04/xmlenc#sha256", Pki.Rsa.HashAlg.Sha256 },
{ "http://www.w3.org/2001/04/xmlenc#sha384", Pki.Rsa.HashAlg.Sha384 },
{ "http://www.w3.org/2001/04/xmlenc#sha512", Pki.Rsa.HashAlg.Sha512 },
};
// W3C XMLENC mask generation function URI values for the RSAES-OAEP-ENCRYPT algorithm
// (expected as a parameter for rsa-oaep)
private static readonly Dictionary<string, Pki.Rsa.HashAlg> W3C_MGF1Algs = new Dictionary<string, Pki.Rsa.HashAlg>()
{
{ "http://www.w3.org/2009/xmlenc11#mgf1sha1", Pki.Rsa.HashAlg.Sha1 },
{ "http://www.w3.org/2009/xmlenc11#mgf1sha224", Pki.Rsa.HashAlg.Sha224 },
{ "http://www.w3.org/2009/xmlenc11#mgf1sha256", Pki.Rsa.HashAlg.Sha256 },
{ "http://www.w3.org/2009/xmlenc11#mgf1sha384", Pki.Rsa.HashAlg.Sha384 },
{ "http://www.w3.org/2009/xmlenc11#mgf1sha512", Pki.Rsa.HashAlg.Sha512 },
};
// Shared secret keys identified by key name (keys in ASCII string form)
// CAUTION: not a secure way to store secret keys!!
private static readonly Dictionary<string, string> MySecretKeys = new Dictionary<string, string>()
{
{ "bob", "abcdefghijklmnopqrstuvwx"},
{ "job", "abcdefghijklmnop"},
{ "jeb", "abcdefghijklmnopqrstuvwx"},
{ "jed", "abcdefghijklmnopqrstuvwxyz012345"},
};
// Index of RSA key files (all unencrypted)
private static readonly Dictionary<string, string> MyRsaKeyFiles = new Dictionary<string, string>()
{
{ "default", "rsa-w3c.p8"},
{ "bob@smime.example", "bob-smime.p8"},
};
static byte[] getKeyUsingKeyName(string keyname)
{
// Lookup key using KeyName
byte[] key = null;
// CAUTION: you may need a more secure way to do this :-)
if (MySecretKeys.ContainsKey(keyname)) {
string keystr = MySecretKeys[keyname];
// keystr is an ASCII string; we need a byte array.
key = System.Text.Encoding.Default.GetBytes(keystr);
} else {
Console.WriteLine("**ERROR: Cannot find matching key for " + keyname);
}
return key;
}
static string getRsaKeyFileName(string keyname)
{
string keyfilename = MyRsaKeyFiles["default"];
if (keyname.Length == 0) return keyfilename;
if (MyRsaKeyFiles.ContainsKey(keyname)) {
keyfilename = MyRsaKeyFiles[keyname];
}
return keyfilename;
}
static byte[] getKey(string xmlfile)
{
byte[] key = null;
string query;
string s;
int n;
string keyname;
string enckey_alg;
string enckey_keyname;
string enckey_ciphervalue;
string rsa_keyfile;
byte[] kek;
Pki.CipherAlgorithm cipherAlg;
Pki.Rsa.HashAlg digestAlg;
Pki.Rsa.HashAlg mgfDigAlg;
Pki.Rsa.AdvOptions mgfOpt;
/* In our simple example here, we either have
* (1) a KeyInfo/KeyName element which we can use to lookup a symmetric block cipher key
* (2) an EncryptedKey using one of
* (a) a key wrap algorithm
* (b) rsa-1_5
* (c) rsa-oaep-mgf1p
* (d) rsa-oaep
*
* */
// (1) Do we have a simple key name?
query = "//EncryptedData/KeyInfo/KeyName";
s = Xmlsq.Query.GetText(xmlfile, query);
Trace.WriteLine(query + " => '" + s + "'");
if (s.Length > 0) {
keyname = s;
key = getKeyUsingKeyName(keyname);
return key;
}
// (2) Do we have an EncryptedKey?
query = "//EncryptedData/KeyInfo/EncryptedKey";
n = Xmlsq.Query.Count(xmlfile, query);
Trace.WriteLine("COUNT: '" + query + "' = " + n);
if (n < 1)
return key;
query = "//EncryptedData/KeyInfo/EncryptedKey/EncryptionMethod/@Algorithm";
s = Xmlsq.Query.GetText(xmlfile, query);
Trace.WriteLine(query + " => '" + s + "'");
enckey_alg = s;
query = "//EncryptedData/KeyInfo/EncryptedKey/CipherData/CipherValue";
s = Xmlsq.Query.GetText(xmlfile, query, Query.Opts.Trim);
enckey_ciphervalue = s;
Trace.WriteLine("ciphervalue='" + enckey_ciphervalue + "'");
// Do we have a KeyName
query = "//EncryptedData/KeyInfo/EncryptedKey/KeyInfo/KeyName";
s = Xmlsq.Query.GetText(xmlfile, query);
Trace.WriteLine(query + " => '" + s + "'");
enckey_keyname = s;
if (enckey_alg.EndsWith("rsa-1_5")) {
Trace.WriteLine("Using rsa-1_5");
// RSA Version 1.5
rsa_keyfile = getRsaKeyFileName(enckey_keyname); // no password
key = Pki.Rsa.Decrypt(Pki.Cnv.FromBase64(enckey_ciphervalue), rsa_keyfile, "");
Trace.WriteLine("key=0x" + Pki.Cnv.ToHex(key));
}
else if (enckey_alg.EndsWith("#rsa-oaep-mgf1p")) {
Trace.WriteLine("Using rsa-oaep-mgf1p");
// RSA-OAEP using default MGF1withSHA1
rsa_keyfile = getRsaKeyFileName(enckey_keyname); // no password
// The message digest function SHOULD be specified using the Algorithm attribute of the ds:DigestMethod child
// element of the xenc:EncryptionMethod element. If it is not specified, the default value of SHA1 is to be used.
query = "//EncryptedData/KeyInfo/EncryptedKey/EncryptionMethod/DigestMethod/@Algorithm";
s = Xmlsq.Query.GetText(xmlfile, query);
Trace.WriteLine(query + " => '" + s + "'");
if (s.Length > 0 && W3C_HashAlgs.ContainsKey(s)) {
digestAlg = W3C_HashAlgs[s];
}
else {
digestAlg = Pki.Rsa.HashAlg.Sha1;
}
mgfOpt = Pki.Rsa.AdvOptions.Mgf1_Sha1; // Default
key = Pki.Rsa.Decrypt(Pki.Cnv.FromBase64(enckey_ciphervalue), rsa_keyfile, "", Pki.Rsa.EME.OAEP, digestAlg, mgfOpt);
Trace.WriteLine("key=0x" + Pki.Cnv.ToHex(key));
} else if (enckey_alg.EndsWith("#rsa-oaep")) {
Trace.WriteLine("Using rsa-oaep");
// RSA-OAEP
rsa_keyfile = getRsaKeyFileName(enckey_keyname); // no password
// The message digest function SHOULD be specified using the Algorithm attribute of the ds:DigestMethod child
// element of the xenc:EncryptionMethod element. If it is not specified, the default value of SHA1 is to be used.
query = "//EncryptedData/KeyInfo/EncryptedKey/EncryptionMethod/DigestMethod/@Algorithm";
s = Xmlsq.Query.GetText(xmlfile, query);
Trace.WriteLine(query + " => '" + s + "'");
if (s.Length > 0 && W3C_HashAlgs.ContainsKey(s)) {
digestAlg = W3C_HashAlgs[s];
} else {
digestAlg = Pki.Rsa.HashAlg.Sha1;
}
// We only support MGF1Alg = same as DigestMethod (PKI default) or MGF1Alg = SHA-1 (strict default)
query = "//EncryptedData/KeyInfo/EncryptedKey/EncryptionMethod/MGF/@Algorithm";
s = Xmlsq.Query.GetText(xmlfile, query);
Trace.WriteLine(query + " => '" + s + "'");
if (s.Length > 0 && W3C_MGF1Algs.ContainsKey(s)) {
mgfDigAlg = W3C_MGF1Algs[s];
if (mgfDigAlg == digestAlg) {
mgfOpt = Pki.Rsa.AdvOptions.Default;
} else if (mgfDigAlg == Pki.Rsa.HashAlg.Sha1) {
mgfOpt = Pki.Rsa.AdvOptions.Mgf1_Sha1;
} else {
Console.WriteLine("**ERROR: Unsupported digest algorithm for MGF1 " + s);
return null;
}
} else {
mgfOpt = Pki.Rsa.AdvOptions.Default;
}
key = Pki.Rsa.Decrypt(Pki.Cnv.FromBase64(enckey_ciphervalue), rsa_keyfile, "", Pki.Rsa.EME.OAEP, digestAlg, mgfOpt);
Trace.WriteLine("key=0x" + Pki.Cnv.ToHex(key));
} else if (enckey_alg.Contains("#kw-")) {
Trace.WriteLine("Using Key Wrap");
// Lookup block cipher algorithm
if (W3C_KeyWrapAlgs.ContainsKey(enckey_alg)) {
cipherAlg = W3C_KeyWrapAlgs[enckey_alg];
} else {
Console.WriteLine("**ERROR: Cannot find matching cipher algorithm for " + enckey_alg);
return null;
}
// Uses Key Wrap, expecting KeyName
kek = getKeyUsingKeyName(enckey_keyname);
key = Pki.Cipher.KeyUnwrap(Pki.Cnv.FromBase64(enckey_ciphervalue), kek, cipherAlg);
}
return key;
}
/// <summary>
/// Extract and decrypt cipher text from XMLENC document using block cipher.
/// </summary>
/// <param name="xmlfile">File path to XML file or string containing XML.</param>
/// <returns>Decrypted text or empty string if error occurred.</returns>
static string getXmlEncDecryptedText(string xmlfile)
{
string s;
int n;
string query;
string encalgstr;
string ciphervalue;
Pki.CipherAlgorithm cipherAlg;
Pki.Mode mode;
Pki.Padding pad;
Pki.Cipher.Opts opts;
Pki.AeadAlgorithm aeadAlg;
byte[] key, ct;
byte[] pt = null;
string data;
// Confirm EncryptedData existence
Trace.WriteLine("Confirm EncryptedData exists using Xmlsq.Query.Count (expecting > 0)");
query = "//EncryptedData";
n = Xmlsq.Query.Count(xmlfile, query);
Trace.WriteLine("COUNT: '" + query + "' = " + n);
if (n < 1) {
Console.WriteLine("**ERROR: EncryptedData element not found");
return "";
}
// Get data encryption algorithm: expecting "xmlenc#*-cbc" or "xmlenc#*-gcm"
Trace.WriteLine("Get data encryption algorithm...");
query = "//EncryptedData/EncryptionMethod/@Algorithm";
Trace.WriteLine("Query: " + query);
encalgstr = Xmlsq.Query.GetText(xmlfile, query);
Trace.WriteLine("encalg=" + encalgstr);
if (encalgstr.Length == 0) {
Console.WriteLine("**ERROR: Cannot find EncryptionMethod");
return "";
}
// Show EncryptedData attribute details, if present
query = "//EncryptedData/@Type";
s = Xmlsq.Query.GetText(xmlfile, query);
Trace.WriteLine(query + " => '" + s + "'");
query = "//EncryptedData/@MimeType";
s = Xmlsq.Query.GetText(xmlfile, query);
Trace.WriteLine(query + " => '" + s + "'");
// Get cipher value
Trace.WriteLine("Get cipher value...");
query = "//EncryptedData/CipherData/CipherValue";
Trace.WriteLine("Query: " + query);
ciphervalue = Xmlsq.Query.GetText(xmlfile, query, Query.Opts.Trim); // NB Trim whitespace here
Trace.WriteLine("ciphervalue='" + ciphervalue + "'");
if (ciphervalue.Length == 0) {
Console.WriteLine("**ERROR: Cannot find CipherValue");
return "";
}
// Get the key for symmetric encryption
key = getKey(xmlfile);
if (null == key) {
Console.WriteLine("**ERROR: Failed to get a valid key");
return "";
}
// Decrypt cipher value using key and data encryption algorithm...
// Expecting algorithm string to end with either "-cbc" or "-gcm"
if (encalgstr.EndsWith("-cbc")) {
// Lookup block cipher algorithm
if (W3C_CipherAlgs.ContainsKey(encalgstr)) {
cipherAlg = W3C_CipherAlgs[encalgstr];
} else {
Console.WriteLine("**ERROR: Cannot find matching cipher algorithm for " + encalgstr);
return "";
}
// These are always the same for "-cbc" algorithms
mode = Pki.Mode.CBC;
pad = Pki.Padding.W3CPadding;
opts = Pki.Cipher.Opts.PrefixIV;
// Do block cipher decryption
ct = Pki.Cnv.FromBase64(ciphervalue);
pt = Pki.Cipher.Decrypt(ct, key, null, cipherAlg, mode, pad, opts);
}
else if (encalgstr.EndsWith("-gcm")) {
// Lookup block cipher GCM algorithm
if (W3C_GcmAlgs.ContainsKey(encalgstr)) {
aeadAlg = W3C_GcmAlgs[encalgstr];
} else {
Console.WriteLine("**ERROR: Cannot find matching cipher algorithm for " + encalgstr);
return "";
}
opts = Pki.Cipher.Opts.PrefixIV;
// Do AEAD decryption
ct = Pki.Cnv.FromBase64(ciphervalue);
pt = Pki.Cipher.DecryptAEAD(ct, key, null, null, aeadAlg, opts);
}
else {
Console.WriteLine("**ERROR: expecting -cbc or -gcm in Algorithm");
return "";
}
// Convert bytes to string: we assume UTF-8 encoded text
data = System.Text.Encoding.UTF8.GetString(pt);
return data;
}
static void test_XmlEncDecrypt(string xmlfile)
{
string data;
Console.WriteLine("\nFILE: {0}", xmlfile);
data = getXmlEncDecryptedText(xmlfile);
Debug.Assert(data.Length > 0, "Failed to find any data");
Console.WriteLine("plaintext=[{0}]", data);
}
static void Main(string[] args)
{
// Setup to display Trace info in console
// Comment out the next line to turn off Trace
Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));
// Show versions of core DLLs...
Trace.WriteLine(string.Format("Xmlsq Version={0:D5}", Xmlsq.Gen.Version()));
Trace.WriteLine(string.Format("CryptoSys PKI Version={0:D5}", Pki.General.Version()));
/* These example files are from (or adapted from)
* W3C "XML Encryption Implementation and Interoperability Report"
* https://www.w3.org/Encryption/2002/02-xenc-interop.html
* merlin-xmlenc-five.tar.gz
* https://lists.w3.org/Archives/Public/xml-encryption/2002Mar/0008.html
* */
// Demonstrate decrypting data using block ciphers
test_XmlEncDecrypt("encrypt-data-aes128-cbc.xml");
test_XmlEncDecrypt("encrypt-content-tripledes-cbc.xml");
test_XmlEncDecrypt("encrypt-content-aes256-cbc-prop.xml");
test_XmlEncDecrypt("encrypt-element-aes128-gcm.xml");
test_XmlEncDecrypt("encrypt-data-aes256-gcm.xml");
// Using key wrap
test_XmlEncDecrypt("encrypt-content-aes128-cbc-kw-aes192.xml");
test_XmlEncDecrypt("encrypt-data-aes192-cbc-kw-aes256.xml");
// Using RSA key transport
test_XmlEncDecrypt("encrypt-element-aes128-cbc-rsa-1_5.xml");
test_XmlEncDecrypt("encrypt-data-tripledes-cbc-rsa-oaep-mgf1p.xml");
test_XmlEncDecrypt("encrypt-content-aes128-gcm-rsa-oaep-sha256.xml");
test_XmlEncDecrypt("root-bob-oaep-gcm.xml");
}
}
}