Wednesday 21 February 2018

C# - ECDSA - a common curve between OpenSSL, C# and C++ Microsoft CNG

So in the previous post we saw how to generate a ECDSA key in C#, then export its public representation to be used in another C# program. Ultimately, I'd want a Linux server probably using OpenSSL to sign licence certificates, for C# programs we use the .Net managed classes to verify hashes and for C++ programs we'd using the Microsoft CNG C++ API. I know I want to use 384-bit ECDSA and use SHA256 hash twice because this is what BitCoin uses and it would be nice to say my encryption is as strong a BitCoin (buzzword advertising!).

Here is an example of the C# export for a key, I have added line breaks

<ECDSAKeyValue xmlns="http://www.w3.org/2001/04/xmldsig-more#">
  <DomainParameters>
    <NamedCurve URN="urn:oid:1.3.132.0.34" />
  </DomainParameters>
  <PublicKey>
    <X Value="1902202729747667972037907690858726927598735515
9453131739849441348197140389889215510822928340340114265083966753527920" 
xsi:type="PrimeFieldElemType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />
    <Y Value="2166460856334825178504234528741952408073634514
9497343748736063144545323191375900071111148263795957487183518893659845" 
xsi:type="PrimeFieldElemType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />
  </PublicKey>
</ECDSAKeyValue>


So one can see the very long co-ordinate pair, X & Y but also what matters is the curve name/id. In the above export from (C#) it says Named Curve "urn:oid:1.3.132.0.34" and this tallies to secp384r1 because IBM say so here. Here is a fuller list
  • NIST recommended curves
    • secp192r1 – {1.2.840.10045.3.1.1}
    • secp224r1 – {1.3.132.0.33}
    • secp256r1 – {1.2.840.10045.3.1.7}
    • secp384r1 – {1.3.132.0.34}
    • secp521r1 – {1.3.132.0.35}

This matters because OpenSSL lists its curves using the text name and not the oid. Like so ...

C:\OpenSSL-Win64\bin>openssl ecparam -list_curves
  secp112r1 : SECG/WTLS curve over a 112 bit prime field
  secp112r2 : SECG curve over a 112 bit prime field
  secp128r1 : SECG curve over a 128 bit prime field
  secp128r2 : SECG curve over a 128 bit prime field
  secp160k1 : SECG curve over a 160 bit prime field
  secp160r1 : SECG curve over a 160 bit prime field
  secp160r2 : SECG/WTLS curve over a 160 bit prime field
  secp192k1 : SECG curve over a 192 bit prime field
  secp224k1 : SECG curve over a 224 bit prime field
  secp224r1 : NIST/SECG curve over a 224 bit prime field
  secp256k1 : SECG curve over a 256 bit prime field
  secp384r1 : NIST/SECG curve over a 384 bit prime field

C# -Elliptic Curve Digital Signatures

So the prior post showed an example of using RSA asymmetric hashing and signing to implement a digital signature but the latest trend is to use Elliptic Curve Digital Signature Algorithm (ECDSA) because they use shorter keys. So I have given another chunk of source code to demonstrate this.

Also in this code base I am using SHA256 for the hash and I am hashing twice just like BitCoin.

So for the Form1.cs code use this


// Based on code by Adnan Samuel, Code Project,
// https://www.codeproject.com/script/Membership/View.aspx?mid=1307566

using System;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        // instance of class ServerSide (Bob)
        public ServerSide mySender; // = new ServerSide();

        // instance of class ClientSide (Alice)
        public ClientSide myReceiver = new ClientSide();

        public Form1()
        {
            InitializeComponent();
        }

        private void btnExit_Click(object sender, EventArgs e)
        {
            myReceiver = null;
            mySender = null;
            Application.Exit();
        }

        private void btn2_GenerateSignatureBlock_Click(object sender, EventArgs e)
        {

            int keySize = Int32.Parse(maskTxtKeySize.Text);
            mySender = new ServerSide(keySize);

            //* Hash and Sign only
            Byte[] nonencryptedSignature;
            nonencryptedSignature = mySender.HashAndSign(Encoding.UTF8.GetBytes(
                                                              txtPlainText.Text));

            txtNonEncryptedSignatureBlock.Text = Convert.ToBase64String(
                                                           nonencryptedSignature);

            //C# 6 grpPublicKey.Text = $"Public Key ({mySender.KeySize} bit)";
            grpPublicKey.Text = "Public Key (" + mySender.KeySize + " bit)";
            txtPublicKey.Text = mySender.ToString2();

            btn3b_VerifySignature.Enabled = true;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            btn3b_VerifySignature.Enabled = false;

            // build the link
            LinkLabel.Link link = new LinkLabel.Link();
            link.LinkData =
            "https://www.codeproject.com/Articles/8868/Implementing-Digital-Signing-in-NET";
            linkLabel1.Links.Add(link);
        }

        private void btn3b_VerifySignature_Click(object sender, EventArgs e)
        {
            Byte[] signature;
            signature = Convert.FromBase64String(
                                              txtNonEncryptedSignatureBlock.Text);

            Byte[] plainTextAsBytes;
            plainTextAsBytes = Encoding.UTF8.GetBytes(txtPlainText.Text);

            if (myReceiver.VerifyHash(txtPublicKey.Text, plainTextAsBytes,
                                                                       signature))
            {
                txtPlainTextReceiver.Text = txtPlainText.Text;
                MessageBox.Show("Signature Valid", "Verification Results",
                                MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            else
            {
                txtPlainTextReceiver.Text = "";
                MessageBox.Show("Invalid Signature", "Verification Results",
                                MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
        }

        private void linkLabel1_LinkClicked(object sender,
                                                  LinkLabelLinkClickedEventArgs e)
        {
            // Send the URL to the operating system.
            Process.Start(e.Link.LinkData as string);
        }

        private void maskTxtKeySize_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Enter)
            {
                // enter key has been pressed
                btn2_GenerateSignatureBlock_Click(sender, e);
            }
        }
    }

    public class ServerSide //Bob
    {
        //========================================================================
        //  Bob  is the Server who will hash and sign license documents
        //========================================================================
        private ECDsaCng ecDsaCngserverSide = null;

        private ServerSide()
        {
            ecDsaCngserverSide = new ECDsaCng();
        }

        public ServerSide(int keySize)
        {
            try
            {
                ecDsaCngserverSide = new ECDsaCng(keySize);
            }
            catch (Exception ex)
            {
                
                if (ex.HResult == -2146233296)
                {
                    MessageBox.Show("Key size should be 256, 384 or 521", 
                           "Key initialisation error", 
                           MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                }
                else
                {
                    //C# 6.0 string errMsg = $"Unhandled error {ex.HResult} : {ex.Message} ";
                    string errMsg = "Unhandled error " + "ex.HResult" + " : " + ex.Message + " ";
                    MessageBox.Show(errMsg, "Key initialisation error", 
                           MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                }
            }

        }

        public string PublicParametersAsXml
        {
            get
            {
                return ecDsaCngserverSide.ToXmlString(false);
            }
        }

        public int KeySize
        {
            get
            {
                return ecDsaCngserverSide.KeySize;
            }
        }

        public byte[] HashAndSign(byte[] input)
        {


            // a byte array to store hash value
            byte[] hashedData = null;

            byte[] results = null;

            // create new instance of SHA256 hash algorithm to compute hash
            HashAlgorithm hashAlgo = new SHA256Managed();

            // compute hash with algorithm 
            hashedData = hashAlgo.ComputeHash(input);

            // hasgh again, like BitCoin
            hashedData = hashAlgo.ComputeHash(hashedData);

            // sign Data using private key 
            results = ecDsaCngserverSide.SignHash(hashedData);

            return results;
        }

        public string ToString2()
        {
            return ecDsaCngserverSide.ToXmlString(ECKeyXmlFormat.Rfc4050);
        }
    }

    public class ClientSide
    {
        //'========================================================================
        //' Alice is Client who will import Public Key and verify signed hash
        //'========================================================================

        public Boolean VerifyHash(String ecDsaCngParamsXml, byte[] signedData,
                                                                  byte[] signature)
        {
            // create new instance 
            ECDsaCng client = new ECDsaCng();

            bool bOk = true;
            try
            {
                client.FromXmlString(ecDsaCngParamsXml, ECKeyXmlFormat.Rfc4050);
            }
            catch (Exception ex)
            {
                bOk = false;
                if (ex.HResult == -2146893785)
                {
                    MessageBox.Show("Key has been tampered with", 
                               "Key initialisation error", 
                               MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                }
                else
                {
                    //C# 6.0 string errMsg = $"Unhandled error {ex.HResult} : {ex.Message} ";
                    string errMsg = "Unhandled error " + "ex.HResult" + " : " + ex.Message + " ";

                    MessageBox.Show(errMsg, "Key initialisation error", 
                                 MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                }
            }

            // a byte array to store hash value
            byte[] hashedData = null;

            HashAlgorithm hashAlgo = new SHA256Managed();

            if (bOk && hashAlgo != null)
            {
                // compute hash with algorithm specified 
                hashedData = hashAlgo.ComputeHash(signedData);

                // hash it twice
                hashedData = hashAlgo.ComputeHash(hashedData);

                return client.VerifyHash(hashedData, signature);
            }
            else
            {
                return false;
            }
        }
    }
}

and ther Form1.Designer.cs code is here

namespace WindowsFormsApp1
{
    partial class Form1
    {
        /// 
        /// Required designer variable.
        /// 
        private System.ComponentModel.IContainer components = null;


        /// 
        /// Clean up any resources being used.
        /// 
        /// true if managed resources should be disposed; 
        /// otherwise, false.
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// 
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// 
        private void InitializeComponent()
        {
            this.grpServer = new System.Windows.Forms.GroupBox();
            this.grpMetrics = new System.Windows.Forms.GroupBox();
            this.maskTxtKeySize = new System.Windows.Forms.MaskedTextBox();
            this.lblKeySize = new System.Windows.Forms.Label();
            this.txtNonEncryptedSignatureBlock = new System.Windows.Forms.TextBox();
            this.btn2_GenerateSignatureBlock = new System.Windows.Forms.Button();
            this.txtPlainText = new System.Windows.Forms.TextBox();
            this.lblEnterPlaintextForSigning = new System.Windows.Forms.Label();
            this.lblTitle = new System.Windows.Forms.Label();
            this.grpClientSide = new System.Windows.Forms.GroupBox();
            this.txtPlainTextReceiver = new System.Windows.Forms.TextBox();
            this.btn3b_VerifySignature = new System.Windows.Forms.Button();
            this.btnExit = new System.Windows.Forms.Button();
            this.grpPublicKey = new System.Windows.Forms.GroupBox();
            this.txtPublicKey = new System.Windows.Forms.TextBox();
            this.lblIOriginalAuthor = new System.Windows.Forms.Label();
            this.linkLabel1 = new System.Windows.Forms.LinkLabel();
            this.grpServer.SuspendLayout();
            this.grpMetrics.SuspendLayout();
            this.grpClientSide.SuspendLayout();
            this.grpPublicKey.SuspendLayout();
            this.SuspendLayout();
            // 
            // grpServer
            // 
            this.grpServer.BackColor = System.Drawing.SystemColors.Control;
            this.grpServer.Controls.Add(this.grpMetrics);
            this.grpServer.Controls.Add(this.txtNonEncryptedSignatureBlock);
            this.grpServer.Controls.Add(this.btn2_GenerateSignatureBlock);
            this.grpServer.Controls.Add(this.txtPlainText);
            this.grpServer.Controls.Add(this.lblEnterPlaintextForSigning);
            this.grpServer.ForeColor = System.Drawing.SystemColors.WindowText;
            this.grpServer.Location = new System.Drawing.Point(12, 39);
            this.grpServer.Name = "grpServer";
            this.grpServer.Size = new System.Drawing.Size(826, 184);
            this.grpServer.TabIndex = 9;
            this.grpServer.TabStop = false;
            this.grpServer.Text = "ServerSide (Bob)";
            // 
            // grpMetrics
            // 
            this.grpMetrics.Controls.Add(this.maskTxtKeySize);
            this.grpMetrics.Controls.Add(this.lblKeySize);
            this.grpMetrics.Location = new System.Drawing.Point(11, 43);
            this.grpMetrics.Name = "grpMetrics";
            this.grpMetrics.Size = new System.Drawing.Size(159, 80);
            this.grpMetrics.TabIndex = 19;
            this.grpMetrics.TabStop = false;
            this.grpMetrics.Text = "Metrics";
            // 
            // maskTxtKeySize
            // 
            this.maskTxtKeySize.BackColor = System.Drawing.SystemColors.Control;
            this.maskTxtKeySize.Location = new System.Drawing.Point(84, 13);
            this.maskTxtKeySize.Mask = "00000";
            this.maskTxtKeySize.Name = "maskTxtKeySize";
            this.maskTxtKeySize.Size = new System.Drawing.Size(61, 20);
            this.maskTxtKeySize.TabIndex = 18;
            this.maskTxtKeySize.Text = "384";
            this.maskTxtKeySize.ValidatingType = typeof(int);
            this.maskTxtKeySize.KeyDown += new System.Windows.Forms.KeyEventHandler(this.maskTxtKeySize_KeyDown);
            // 
            // lblKeySize
            // 
            this.lblKeySize.BackColor = System.Drawing.SystemColors.Control;
            this.lblKeySize.ForeColor = System.Drawing.SystemColors.WindowText;
            this.lblKeySize.Location = new System.Drawing.Point(6, 17);
            this.lblKeySize.Name = "lblKeySize";
            this.lblKeySize.Size = new System.Drawing.Size(62, 16);
            this.lblKeySize.TabIndex = 17;
            this.lblKeySize.Text = "Key Size :";
            // 
            // txtNonEncryptedSignatureBlock
            // 
            this.txtNonEncryptedSignatureBlock.BackColor = System.Drawing.SystemColors.Control;
            this.txtNonEncryptedSignatureBlock.ForeColor = System.Drawing.SystemColors.WindowText;
            this.txtNonEncryptedSignatureBlock.Location = new System.Drawing.Point(192, 130);
            this.txtNonEncryptedSignatureBlock.Multiline = true;
            this.txtNonEncryptedSignatureBlock.Name = "txtNonEncryptedSignatureBlock";
            this.txtNonEncryptedSignatureBlock.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
            this.txtNonEncryptedSignatureBlock.Size = new System.Drawing.Size(628, 48);
            this.txtNonEncryptedSignatureBlock.TabIndex = 15;
            // 
            // btn2_GenerateSignatureBlock
            // 
            this.btn2_GenerateSignatureBlock.BackColor = System.Drawing.SystemColors.Control;
            this.btn2_GenerateSignatureBlock.ForeColor = System.Drawing.SystemColors.WindowText;
            this.btn2_GenerateSignatureBlock.Location = new System.Drawing.Point(11, 130);
            this.btn2_GenerateSignatureBlock.Name = "btn2_GenerateSignatureBlock";
            this.btn2_GenerateSignatureBlock.Size = new System.Drawing.Size(160, 48);
            this.btn2_GenerateSignatureBlock.TabIndex = 10;
            this.btn2_GenerateSignatureBlock.Text = "Generate &Signature Block using Sender\'s (Bob) Private Key";
            this.btn2_GenerateSignatureBlock.UseVisualStyleBackColor = false;
            this.btn2_GenerateSignatureBlock.Click += new System.EventHandler(this.btn2_GenerateSignatureBlock_Click);
            // 
            // txtPlainText
            // 
            this.txtPlainText.BackColor = System.Drawing.SystemColors.Control;
            this.txtPlainText.ForeColor = System.Drawing.SystemColors.WindowText;
            this.txtPlainText.Location = new System.Drawing.Point(192, 20);
            this.txtPlainText.Multiline = true;
            this.txtPlainText.Name = "txtPlainText";
            this.txtPlainText.Size = new System.Drawing.Size(628, 104);
            this.txtPlainText.TabIndex = 8;
            this.txtPlainText.Text = "Hello";
            // 
            // lblEnterPlaintextForSigning
            // 
            this.lblEnterPlaintextForSigning.BackColor = System.Drawing.SystemColors.Control;
            this.lblEnterPlaintextForSigning.ForeColor = System.Drawing.SystemColors.WindowText;
            this.lblEnterPlaintextForSigning.Location = new System.Drawing.Point(54, 23);
            this.lblEnterPlaintextForSigning.Name = "lblEnterPlaintextForSigning";
            this.lblEnterPlaintextForSigning.Size = new System.Drawing.Size(132, 20);
            this.lblEnterPlaintextForSigning.TabIndex = 7;
            this.lblEnterPlaintextForSigning.Text = "Enter Plaintext for signing :";
            // 
            // lblTitle
            // 
            this.lblTitle.BackColor = System.Drawing.SystemColors.Control;
            this.lblTitle.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
            this.lblTitle.ForeColor = System.Drawing.SystemColors.WindowText;
            this.lblTitle.Location = new System.Drawing.Point(107, 4);
            this.lblTitle.Name = "lblTitle";
            this.lblTitle.Size = new System.Drawing.Size(566, 32);
            this.lblTitle.TabIndex = 8;
            this.lblTitle.Text = "Cryptography - Elliptic Curve Digital Signature Algorithm (ECDSA)";
            this.lblTitle.TextAlign = System.Drawing.ContentAlignment.TopCenter;
            // 
            // grpClientSide
            // 
            this.grpClientSide.BackColor = System.Drawing.SystemColors.Control;
            this.grpClientSide.Controls.Add(this.txtPlainTextReceiver);
            this.grpClientSide.Controls.Add(this.btn3b_VerifySignature);
            this.grpClientSide.ForeColor = System.Drawing.SystemColors.WindowText;
            this.grpClientSide.Location = new System.Drawing.Point(12, 424);
            this.grpClientSide.Name = "grpClientSide";
            this.grpClientSide.Size = new System.Drawing.Size(826, 119);
            this.grpClientSide.TabIndex = 11;
            this.grpClientSide.TabStop = false;
            this.grpClientSide.Text = "ClientSide (Alice)";
            // 
            // txtPlainTextReceiver
            // 
            this.txtPlainTextReceiver.BackColor = System.Drawing.SystemColors.Control;
            this.txtPlainTextReceiver.ForeColor = System.Drawing.SystemColors.WindowText;
            this.txtPlainTextReceiver.Location = new System.Drawing.Point(167, 20);
            this.txtPlainTextReceiver.Multiline = true;
            this.txtPlainTextReceiver.Name = "txtPlainTextReceiver";
            this.txtPlainTextReceiver.ReadOnly = true;
            this.txtPlainTextReceiver.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
            this.txtPlainTextReceiver.Size = new System.Drawing.Size(643, 93);
            this.txtPlainTextReceiver.TabIndex = 13;
            // 
            // btn3b_VerifySignature
            // 
            this.btn3b_VerifySignature.BackColor = System.Drawing.SystemColors.Control;
            this.btn3b_VerifySignature.ForeColor = System.Drawing.SystemColors.WindowText;
            this.btn3b_VerifySignature.Location = new System.Drawing.Point(6, 20);
            this.btn3b_VerifySignature.Name = "btn3b_VerifySignature";
            this.btn3b_VerifySignature.Size = new System.Drawing.Size(155, 61);
            this.btn3b_VerifySignature.TabIndex = 0;
            this.btn3b_VerifySignature.Text = "Verify Signature block using sender\'s (Bob) Public Key";
            this.btn3b_VerifySignature.UseVisualStyleBackColor = false;
            this.btn3b_VerifySignature.Click += new System.EventHandler(this.btn3b_VerifySignature_Click);
            // 
            // btnExit
            // 
            this.btnExit.BackColor = System.Drawing.SystemColors.Control;
            this.btnExit.ForeColor = System.Drawing.SystemColors.WindowText;
            this.btnExit.Location = new System.Drawing.Point(696, 549);
            this.btnExit.Name = "btnExit";
            this.btnExit.Size = new System.Drawing.Size(136, 24);
            this.btnExit.TabIndex = 10;
            this.btnExit.Text = "E&xit";
            this.btnExit.UseVisualStyleBackColor = false;
            this.btnExit.Click += new System.EventHandler(this.btnExit_Click);
            // 
            // grpPublicKey
            // 
            this.grpPublicKey.BackColor = System.Drawing.SystemColors.Control;
            this.grpPublicKey.Controls.Add(this.txtPublicKey);
            this.grpPublicKey.ForeColor = System.Drawing.SystemColors.WindowText;
            this.grpPublicKey.Location = new System.Drawing.Point(12, 243);
            this.grpPublicKey.Name = "grpPublicKey";
            this.grpPublicKey.Size = new System.Drawing.Size(826, 175);
            this.grpPublicKey.TabIndex = 12;
            this.grpPublicKey.TabStop = false;
            this.grpPublicKey.Text = "Public Key";
            // 
            // txtPublicKey
            // 
            this.txtPublicKey.BackColor = System.Drawing.SystemColors.Control;
            this.txtPublicKey.ForeColor = System.Drawing.SystemColors.WindowText;
            this.txtPublicKey.Location = new System.Drawing.Point(11, 20);
            this.txtPublicKey.Multiline = true;
            this.txtPublicKey.Name = "txtPublicKey";
            this.txtPublicKey.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
            this.txtPublicKey.Size = new System.Drawing.Size(809, 149);
            this.txtPublicKey.TabIndex = 13;
            // 
            // lblIOriginalAuthor
            // 
            this.lblIOriginalAuthor.AutoSize = true;
            this.lblIOriginalAuthor.Location = new System.Drawing.Point(13, 559);
            this.lblIOriginalAuthor.Name = "lblIOriginalAuthor";
            this.lblIOriginalAuthor.Size = new System.Drawing.Size(165, 13);
            this.lblIOriginalAuthor.TabIndex = 13;
            this.lblIOriginalAuthor.Text = "Based on code by Adnan Samuel";
            // 
            // linkLabel1
            // 
            this.linkLabel1.AutoSize = true;
            this.linkLabel1.Location = new System.Drawing.Point(184, 559);
            this.linkLabel1.Name = "linkLabel1";
            this.linkLabel1.Size = new System.Drawing.Size(248, 13);
            this.linkLabel1.TabIndex = 14;
            this.linkLabel1.TabStop = true;
            this.linkLabel1.Text = "Code Project - Implementing Digital Signing in .NET";
            this.linkLabel1.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkLabel1_LinkClicked);
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(857, 593);
            this.Controls.Add(this.linkLabel1);
            this.Controls.Add(this.lblIOriginalAuthor);
            this.Controls.Add(this.grpServer);
            this.Controls.Add(this.lblTitle);
            this.Controls.Add(this.grpClientSide);
            this.Controls.Add(this.btnExit);
            this.Controls.Add(this.grpPublicKey);
            this.Name = "Form1";
            this.Text = "Form1";
            this.Load += new System.EventHandler(this.Form1_Load);
            this.grpServer.ResumeLayout(false);
            this.grpServer.PerformLayout();
            this.grpMetrics.ResumeLayout(false);
            this.grpMetrics.PerformLayout();
            this.grpClientSide.ResumeLayout(false);
            this.grpClientSide.PerformLayout();
            this.grpPublicKey.ResumeLayout(false);
            this.grpPublicKey.PerformLayout();
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        internal System.Windows.Forms.GroupBox grpServer;
        internal System.Windows.Forms.TextBox txtNonEncryptedSignatureBlock;
        internal System.Windows.Forms.Button btn2_GenerateSignatureBlock;
        internal System.Windows.Forms.TextBox txtPlainText;
        internal System.Windows.Forms.Label lblEnterPlaintextForSigning;
        internal System.Windows.Forms.Label lblTitle;
        internal System.Windows.Forms.GroupBox grpClientSide;
        internal System.Windows.Forms.TextBox txtPlainTextReceiver;
        internal System.Windows.Forms.Button btn3b_VerifySignature;
        internal System.Windows.Forms.Button btnExit;
        internal System.Windows.Forms.GroupBox grpPublicKey;
        internal System.Windows.Forms.TextBox txtPublicKey;
        private System.Windows.Forms.Label lblIOriginalAuthor;
        private System.Windows.Forms.LinkLabel linkLabel1;
        private System.Windows.Forms.MaskedTextBox maskTxtKeySize;
        internal System.Windows.Forms.Label lblKeySize;
        private System.Windows.Forms.GroupBox grpMetrics;
    }
}



C# - RSA Digital Signatures

I am grateful to Adnan Samuel who has penned a Code Project article called Implementing Digital Signing in .NET which uses .NET cryptography classes to show example of encrypting and digitally signing a message. I am indebted to this code base but because I don't need encryption I simplified the code, also I folded all the code from separate class files into one file so it is easier to paste in the reader's instance of Visual Studio.

Another change I made is to segment the logic because actually in my use case one half on the code will run on a server and the other half will run on the client and the given code base tended to keep global variables hanging around and pass them between the two halves and I thought this unrealistic. I have added an extra textbox on the form to show the public key being exported.

Also I changed some of the textboxes to be writeable and so tamperable, so that one can tamper with the values and the signature report to provoke a verification fail.

Also another change is to convert to C# because since the time of writing of the article (2004) C# has stretched its lead over VB (IMHO). [Also I like the partial class split way the Forms are defined in C#].

Also, I have updated the SHA hash from SHA1 to SHA256 more in line with current standards of FIPS 140/2 Suite B

Here is amended code, create a C# Windows Forms (.NET Framework) project and then paste in the following code into Form1.cs


// Based on code by Adnan Samuel, Code Project,
// https://www.codeproject.com/script/Membership/View.aspx?mid=1307566

using System;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        // instance of class ServerSide (Bob)
        public ServerSide mySender = new ServerSide();

        // instance of class ClientSide (Alice)
        public ClientSide myReceiver = new ClientSide();


        public Form1()
        {
            InitializeComponent();
        }

        private void btnExit_Click(object sender, EventArgs e)
        {
            myReceiver = null;
            mySender = null;
            Application.Exit();
        }

        private void btn2_GenerateSignatureBlock_Click(object sender, EventArgs e)
        {
            //* Hash and Sign only
            Byte[] nonencryptedSignature;
            nonencryptedSignature = mySender.HashAndSign(Encoding.UTF8.GetBytes(
                                                              txtPlainText.Text));

            txtNonEncryptedSignatureBlock.Text = Convert.ToBase64String(
                                                           nonencryptedSignature);

            grpPublicKey.Text = $"Public Key ({mySender.KeySize} bit)";
            txtPublicKey.Text = mySender.ToString2();

            btn3b_VerifySignature.Enabled = true;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            btn3b_VerifySignature.Enabled = false;

            // build the link
            LinkLabel.Link link = new LinkLabel.Link();
            link.LinkData = 
            "https://www.codeproject.com/Articles/8868/Implementing-Digital-Signing-in-NET";
            linkLabel1.Links.Add(link);
        }

        private void btn3b_VerifySignature_Click(object sender, EventArgs e)
        {
            Byte[] signature;
            signature = Convert.FromBase64String(
                                              txtNonEncryptedSignatureBlock.Text);

            Byte[] plainTextAsBytes;
            plainTextAsBytes = Encoding.UTF8.GetBytes(txtPlainText.Text);

            if (myReceiver.VerifyHash(txtPublicKey.Text, plainTextAsBytes, 
                                                                       signature))
            {
                txtPlainTextReceiver.Text = txtPlainText.Text;
                MessageBox.Show("Signature Valid", "Verification Results", 
                                MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            else
            {
                txtPlainTextReceiver.Text = "";
                MessageBox.Show("Invalid Signature", "Verification Results", 
                                MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
        }

        private void linkLabel1_LinkClicked(object sender, 
                                                  LinkLabelLinkClickedEventArgs e)
        {
            // Send the URL to the operating system.
            Process.Start(e.Link.LinkData as string);
        }
    }

    public class ServerSide //Bob
    {
        //========================================================================
        //  Bob  is the Server who will hash and sign license documents
        //========================================================================
        private RSACryptoServiceProvider rsaCSPServerSide = 
                                                   new RSACryptoServiceProvider(); 

        public RSAParameters PublicParameters
        {
            get
            {
                return rsaCSPServerSide.ExportParameters(false);
            }
        }

        public int KeySize
        {
            get
            {
                return rsaCSPServerSide.KeySize;
            }
        }

        public byte[] HashAndSign(byte[] input)
        {
            // create new instance of SHA256 hash algorithm to compute hash
            SHA256Managed hash = new SHA256Managed();

            // a byte array to store hash value
            byte[] hashedData;

            // compute hash with algorithm specified as here we have SHA256
            hashedData = hash.ComputeHash(input);

            // sign Data using private key and  OID is simple name 
            // of the algorithm for which to get the object identifier (OID)
            return rsaCSPServerSide.SignHash(hashedData, 
                                                CryptoConfig.MapNameToOID("SHA256"));
        }

        public string ToString2()
        {
            return rsaCSPServerSide.ToXmlString(false);
        }

    }


    public class ClientSide
    {
        //'========================================================================
        //' Alice is Client who will import Public Key and verify signed hash
        //'========================================================================

        // Manually performs hash and then verifies hashed value.
        public Boolean VerifyHash(String rsaParamsXml, byte[] signedData, 
                                                                  byte[] signature)
        {
            // create new instance of RSACryptoServiceProvider
            RSACryptoServiceProvider rsaCSP = new RSACryptoServiceProvider();

            // create new instance of SHA256 hash algorithm to compute hash
            SHA256Managed hash = new SHA256Managed();

            // a byte array to store hash value
            byte[] hashedData;

            // import  public key params into instance of RSACryptoServiceProvider
            rsaCSP.FromXmlString(rsaParamsXml);


            // compute hash with algorithm specified as here we have SHA256
            hashedData = hash.ComputeHash(signedData);

            // Sign Data using public key and  OID is simple name of the algorithm 
            // for which to get the object identifier (OID)
            return rsaCSP.VerifyHash(hashedData, CryptoConfig.MapNameToOID("SHA256"), 
                                                                        signature);
        }
    }
}


and the code behind the designer, Form1.Designer.cs. Sorry for small font, designer generated code tends to spill over the column of my blog. There's nothing much to see here though.

namespace WindowsFormsApp1
{
    partial class Form1
    {
        /// 
        /// Required designer variable.
        /// 
        private System.ComponentModel.IContainer components = null;


        /// 
        /// Clean up any resources being used.
        /// 
        /// true if managed resources should be disposed; 
        /// otherwise, false.
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// 
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// 
        private void InitializeComponent()
        {
            this.grpServer = new System.Windows.Forms.GroupBox();
            this.txtNonEncryptedSignatureBlock = new System.Windows.Forms.TextBox();
            this.btn2_GenerateSignatureBlock = new System.Windows.Forms.Button();
            this.txtPlainText = new System.Windows.Forms.TextBox();
            this.lblEnterPlaintextForSigning = new System.Windows.Forms.Label();
            this.lblTitle = new System.Windows.Forms.Label();
            this.grpClientSide = new System.Windows.Forms.GroupBox();
            this.txtPlainTextReceiver = new System.Windows.Forms.TextBox();
            this.btn3b_VerifySignature = new System.Windows.Forms.Button();
            this.btnExit = new System.Windows.Forms.Button();
            this.grpPublicKey = new System.Windows.Forms.GroupBox();
            this.txtPublicKey = new System.Windows.Forms.TextBox();
            this.lblIOriginalAuthor = new System.Windows.Forms.Label();
            this.linkLabel1 = new System.Windows.Forms.LinkLabel();
            this.grpServer.SuspendLayout();
            this.grpClientSide.SuspendLayout();
            this.grpPublicKey.SuspendLayout();
            this.SuspendLayout();
            // 
            // grpServer
            // 
            this.grpServer.BackColor = System.Drawing.SystemColors.Control;
            this.grpServer.Controls.Add(this.txtNonEncryptedSignatureBlock);
            this.grpServer.Controls.Add(this.btn2_GenerateSignatureBlock);
            this.grpServer.Controls.Add(this.txtPlainText);
            this.grpServer.Controls.Add(this.lblEnterPlaintextForSigning);
            this.grpServer.ForeColor = System.Drawing.SystemColors.WindowText;
            this.grpServer.Location = new System.Drawing.Point(12, 39);
            this.grpServer.Name = "grpServer";
            this.grpServer.Size = new System.Drawing.Size(826, 184);
            this.grpServer.TabIndex = 9;
            this.grpServer.TabStop = false;
            this.grpServer.Text = "ServerSide (Bob)";
            // 
            // txtNonEncryptedSignatureBlock
            // 
            this.txtNonEncryptedSignatureBlock.BackColor = System.Drawing.SystemColors.Control;
            this.txtNonEncryptedSignatureBlock.ForeColor = System.Drawing.SystemColors.WindowText;
            this.txtNonEncryptedSignatureBlock.Location = new System.Drawing.Point(192, 130);
            this.txtNonEncryptedSignatureBlock.Multiline = true;
            this.txtNonEncryptedSignatureBlock.Name = "txtNonEncryptedSignatureBlock";
            this.txtNonEncryptedSignatureBlock.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
            this.txtNonEncryptedSignatureBlock.Size = new System.Drawing.Size(628, 48);
            this.txtNonEncryptedSignatureBlock.TabIndex = 15;
            // 
            // btn2_GenerateSignatureBlock
            // 
            this.btn2_GenerateSignatureBlock.BackColor = System.Drawing.SystemColors.Control;
            this.btn2_GenerateSignatureBlock.ForeColor = System.Drawing.SystemColors.WindowText;
            this.btn2_GenerateSignatureBlock.Location = new System.Drawing.Point(11, 130);
            this.btn2_GenerateSignatureBlock.Name = "btn2_GenerateSignatureBlock";
            this.btn2_GenerateSignatureBlock.Size = new System.Drawing.Size(160, 48);
            this.btn2_GenerateSignatureBlock.TabIndex = 10;
            this.btn2_GenerateSignatureBlock.Text = "Generate &Signature Block using Sender\'s (Bob) Private Key";
            this.btn2_GenerateSignatureBlock.UseVisualStyleBackColor = false;
            this.btn2_GenerateSignatureBlock.Click += new System.EventHandler(this.btn2_GenerateSignatureBlock_Click);
            // 
            // txtPlainText
            // 
            this.txtPlainText.BackColor = System.Drawing.SystemColors.Control;
            this.txtPlainText.ForeColor = System.Drawing.SystemColors.WindowText;
            this.txtPlainText.Location = new System.Drawing.Point(192, 20);
            this.txtPlainText.Multiline = true;
            this.txtPlainText.Name = "txtPlainText";
            this.txtPlainText.Size = new System.Drawing.Size(628, 104);
            this.txtPlainText.TabIndex = 8;
            this.txtPlainText.Text = "Hello";
            // 
            // lblEnterPlaintextForSigning
            // 
            this.lblEnterPlaintextForSigning.BackColor = System.Drawing.SystemColors.Control;
            this.lblEnterPlaintextForSigning.ForeColor = System.Drawing.SystemColors.WindowText;
            this.lblEnterPlaintextForSigning.Location = new System.Drawing.Point(8, 24);
            this.lblEnterPlaintextForSigning.Name = "lblEnterPlaintextForSigning";
            this.lblEnterPlaintextForSigning.Size = new System.Drawing.Size(168, 32);
            this.lblEnterPlaintextForSigning.TabIndex = 7;
            this.lblEnterPlaintextForSigning.Text = "Enter Plaintext for signing :";
            // 
            // lblTitle
            // 
            this.lblTitle.BackColor = System.Drawing.SystemColors.Control;
            this.lblTitle.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
            this.lblTitle.ForeColor = System.Drawing.SystemColors.WindowText;
            this.lblTitle.Location = new System.Drawing.Point(286, 4);
            this.lblTitle.Name = "lblTitle";
            this.lblTitle.Size = new System.Drawing.Size(176, 32);
            this.lblTitle.TabIndex = 8;
            this.lblTitle.Text = "Digital Signatures";
            this.lblTitle.TextAlign = System.Drawing.ContentAlignment.TopCenter;
            // 
            // grpClientSide
            // 
            this.grpClientSide.BackColor = System.Drawing.SystemColors.Control;
            this.grpClientSide.Controls.Add(this.txtPlainTextReceiver);
            this.grpClientSide.Controls.Add(this.btn3b_VerifySignature);
            this.grpClientSide.ForeColor = System.Drawing.SystemColors.WindowText;
            this.grpClientSide.Location = new System.Drawing.Point(12, 424);
            this.grpClientSide.Name = "grpClientSide";
            this.grpClientSide.Size = new System.Drawing.Size(826, 119);
            this.grpClientSide.TabIndex = 11;
            this.grpClientSide.TabStop = false;
            this.grpClientSide.Text = "ClientSide (Alice)";
            // 
            // txtPlainTextReceiver
            // 
            this.txtPlainTextReceiver.BackColor = System.Drawing.SystemColors.Control;
            this.txtPlainTextReceiver.ForeColor = System.Drawing.SystemColors.WindowText;
            this.txtPlainTextReceiver.Location = new System.Drawing.Point(167, 20);
            this.txtPlainTextReceiver.Multiline = true;
            this.txtPlainTextReceiver.Name = "txtPlainTextReceiver";
            this.txtPlainTextReceiver.ReadOnly = true;
            this.txtPlainTextReceiver.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
            this.txtPlainTextReceiver.Size = new System.Drawing.Size(643, 93);
            this.txtPlainTextReceiver.TabIndex = 13;
            // 
            // btn3b_VerifySignature
            // 
            this.btn3b_VerifySignature.BackColor = System.Drawing.SystemColors.Control;
            this.btn3b_VerifySignature.ForeColor = System.Drawing.SystemColors.WindowText;
            this.btn3b_VerifySignature.Location = new System.Drawing.Point(6, 20);
            this.btn3b_VerifySignature.Name = "btn3b_VerifySignature";
            this.btn3b_VerifySignature.Size = new System.Drawing.Size(155, 61);
            this.btn3b_VerifySignature.TabIndex = 0;
            this.btn3b_VerifySignature.Text = "Verify Signature block using sender\'s (Bob) Public Key";
            this.btn3b_VerifySignature.UseVisualStyleBackColor = false;
            this.btn3b_VerifySignature.Click += new System.EventHandler(this.btn3b_VerifySignature_Click);
            // 
            // btnExit
            // 
            this.btnExit.BackColor = System.Drawing.SystemColors.Control;
            this.btnExit.ForeColor = System.Drawing.SystemColors.WindowText;
            this.btnExit.Location = new System.Drawing.Point(696, 549);
            this.btnExit.Name = "btnExit";
            this.btnExit.Size = new System.Drawing.Size(136, 24);
            this.btnExit.TabIndex = 10;
            this.btnExit.Text = "E&xit";
            this.btnExit.UseVisualStyleBackColor = false;
            this.btnExit.Click += new System.EventHandler(this.btnExit_Click);
            // 
            // grpPublicKey
            // 
            this.grpPublicKey.BackColor = System.Drawing.SystemColors.Control;
            this.grpPublicKey.Controls.Add(this.txtPublicKey);
            this.grpPublicKey.ForeColor = System.Drawing.SystemColors.WindowText;
            this.grpPublicKey.Location = new System.Drawing.Point(12, 243);
            this.grpPublicKey.Name = "grpPublicKey";
            this.grpPublicKey.Size = new System.Drawing.Size(826, 175);
            this.grpPublicKey.TabIndex = 12;
            this.grpPublicKey.TabStop = false;
            this.grpPublicKey.Text = "Public Key";
            // 
            // txtPublicKey
            // 
            this.txtPublicKey.BackColor = System.Drawing.SystemColors.Control;
            this.txtPublicKey.ForeColor = System.Drawing.SystemColors.WindowText;
            this.txtPublicKey.Location = new System.Drawing.Point(11, 20);
            this.txtPublicKey.Multiline = true;
            this.txtPublicKey.Name = "txtPublicKey";
            this.txtPublicKey.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
            this.txtPublicKey.Size = new System.Drawing.Size(809, 149);
            this.txtPublicKey.TabIndex = 13;
            // 
            // lblIOriginalAuthor
            // 
            this.lblIOriginalAuthor.AutoSize = true;
            this.lblIOriginalAuthor.Location = new System.Drawing.Point(13, 559);
            this.lblIOriginalAuthor.Name = "lblIOriginalAuthor";
            this.lblIOriginalAuthor.Size = new System.Drawing.Size(165, 13);
            this.lblIOriginalAuthor.TabIndex = 13;
            this.lblIOriginalAuthor.Text = "Based on code by Adnan Samuel";
            // 
            // linkLabel1
            // 
            this.linkLabel1.AutoSize = true;
            this.linkLabel1.Location = new System.Drawing.Point(184, 559);
            this.linkLabel1.Name = "linkLabel1";
            this.linkLabel1.Size = new System.Drawing.Size(248, 13);
            this.linkLabel1.TabIndex = 14;
            this.linkLabel1.TabStop = true;
            this.linkLabel1.Text = "Code Project - Implementing Digital Signing in .NET";
            this.linkLabel1.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkLabel1_LinkClicked);
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(857, 583);
            this.Controls.Add(this.linkLabel1);
            this.Controls.Add(this.lblIOriginalAuthor);
            this.Controls.Add(this.grpServer);
            this.Controls.Add(this.lblTitle);
            this.Controls.Add(this.grpClientSide);
            this.Controls.Add(this.btnExit);
            this.Controls.Add(this.grpPublicKey);
            this.Name = "Form1";
            this.Text = "Form1";
            this.Load += new System.EventHandler(this.Form1_Load);
            this.grpServer.ResumeLayout(false);
            this.grpServer.PerformLayout();
            this.grpClientSide.ResumeLayout(false);
            this.grpClientSide.PerformLayout();
            this.grpPublicKey.ResumeLayout(false);
            this.grpPublicKey.PerformLayout();
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        internal System.Windows.Forms.GroupBox grpServer;
        internal System.Windows.Forms.TextBox txtNonEncryptedSignatureBlock;
        internal System.Windows.Forms.Button btn2_GenerateSignatureBlock;
        internal System.Windows.Forms.TextBox txtPlainText;
        internal System.Windows.Forms.Label lblEnterPlaintextForSigning;
        internal System.Windows.Forms.Label lblTitle;
        internal System.Windows.Forms.GroupBox grpClientSide;
        internal System.Windows.Forms.TextBox txtPlainTextReceiver;
        internal System.Windows.Forms.Button btn3b_VerifySignature;
        internal System.Windows.Forms.Button btnExit;
        internal System.Windows.Forms.GroupBox grpPublicKey;
        internal System.Windows.Forms.TextBox txtPublicKey;
        private System.Windows.Forms.Label lblIOriginalAuthor;
        private System.Windows.Forms.LinkLabel linkLabel1;
    }
}



C# - Sadly VBA's GetObject's Custom Activation Syntax is not implemented in C#/.NET

For a while I have been curious as to a syntax of VBA's GetObject which has become quite prevalent.  GetObject is used to get an object that have been registered in the RunningObjectTable, C# has the equivalent of Marshal.GetActiveObject.  VBA's GetObject has an extra use cases in that if called with a filename and the file is not loaded into an application then it is loaded on demand, I have yet to establish if C# can do this.

The real extra use case of GetObject that caught my eye is when using either WMI or LDAP.  Here is some sample code instantiating LDAP

    Set objUser = GetObject("LDAP://" & strUserDN) 

And here is some code instantiating WMI

    Set oWMIService = GetObject("winmgmts:\\.\root\cimv2")

And third and final case is a Windows Communication Foundation WCF Moniker

Set typedServiceMoniker = GetObject(  
"service4:address=http://localhost/ServiceModelSamples/service.svc, binding=wsHttpBinding,   
contractType={9213C6D2-5A6F-3D26-839B-3BA9B82228D3}") 

So you can see some very different syntaxes beyond the plain COM Server's ProgId of "Excel.Application" and filenames then. There is some pattern, they all start with a text string and then a colon ":", after the colon they can be custom. This is a custom activation syntax. Sadly, it is not callable/useable from C#. This is ironic because WCF services are written in C# (though C# to C# code should clearly avoid VBA patterns).

I'm afraid to say I've wasted some time investigating this. I will post my interim findings here but not to a conclusion.

A key resource is a good book by Guy and Henry Eddon titled Essential COM, fortunately there is an online version. The relevant section is The MkParseDisplayName Function .  Here is a quote

MkParseDisplayName accepts two primary string formats. The first ... The second string format is the more general and thus more important of the two formats. In this format, MkParseDisplayName accepts any string in the form ProgID:ObjectName, where ProgID is a registered program identifier. This architecture allows anyone to write a custom moniker that hooks into the COM+ namespace simply by creating a program identifier (ProgID) entry in the registry.  
The following steps are executed when MkParseDisplayName encounters a string that has the ProgID:ObjectName format: 

  1. The ProgID is converted to a CLSID using the CLSIDFromProgID function. The result is the CLSID of the moniker. 
  2. CoGetClassObject is called to instantiate the moniker. 
  3. IUnknown::QueryInterface is called to request the IParseDisplayName interface. 
  4. The IParseDisplayName::ParseDisplayName method is called to parse the string passed to MkParseDisplayName. 
  5. In the moniker's IParseDisplayName::ParseDisplayName method, a moniker that names the > object identified by the string is created. 
  6. The resulting IMoniker pointer is returned to the client. 

For example, if the string "Hello:Maya" is passed to MkParseDisplayName, the HKEY_CLASSES_ROOT section of the registry is searched for the ProgID Hello. If Hello is found, the CLSID subkey below the ProgID is used to locate and load the moniker. The moniker's IParseDisplayName::ParseDisplayName method is then called to create a moniker object that names the Maya object. Figure 11-2 shows the registry entries involved in this hypothetical example; the numbered labels indicate the order in which the information is obtained from the registry.

So I found this fascinating and I knew of LDAP, WMI and a WCF service moniker which I had developed much earlier I wondered what other COM servers followed this pattern.  I wrote some VBA code which scanned the registry looking for the above pattern and write to a file.  I need a separate C++ program to scan through that file and instantiate the COM server candidates and QueryInterface for IParseDisplayName.  The C++ program is given here.

// C++LookingForCustomActivation.cpp : Defines the entry point for the console application.
//

#include <iostream>
#include <vector>
#include <fstream>
#include <sstream> 
#include "stdafx.h"
#include "Objbase.h"  // required for CoInitialize
#include "atlbase.h"  // required for CComBSTR

using namespace std;

bool ClassImplementsInterface(CLSID clsid1, IID interfaceId, std::string progidInfo);

int _tmain(int argc, _TCHAR* argv[])
{
	::CoInitialize(0);

	std::string idsFilename = "N:\\ProgIdsClassIds.txt";
	std::ifstream idsFileStream(idsFilename, ios_base::in);

	std::string progid;
	std::string clsid;
	std::vector<std::pair<std::string, std::string>> ids;

	while (idsFileStream >> progid >> clsid) {
		bool goodClsId = false ;
		if (clsid.size() == 38) {
			if (clsid[0] == '{' && clsid[37] == '}') {
				goodClsId = true;
			}
		}

		if (goodClsId) {
			
			auto id = std::make_pair(progid, clsid);
			ids.push_back(id);
		}
		else
		{
			cout << "Problem!";
		}
	}

	CLSID iParseDisplayName;
	CLSIDFromString(CComBSTR("{0000011A-0000-0000-C000-000000000046}"), 
                   &iParseDisplayName);

	for (std::vector<std::pair<std::string, std::string>>::iterator
                          it = ids.begin(); it != ids.end(); ++it) {
		progid = it->first;
		clsid = it->second;

		std::wstring stemp = std::wstring(clsid.begin(), clsid.end());
		LPCWSTR sw = stemp.c_str();

		CLSID clsidLoop;
		CLSIDFromString(sw, &clsidLoop);

		if (ClassImplementsInterface(clsidLoop, iParseDisplayName, progid)) {
			std::cout << progid << endl;
		}
		else
		{
			//std::cout << "Nope.";
		}
	}

	int wait;
	std::cin >> wait;

	::CoUninitialize();
	return 0;
}

bool ClassImplementsInterface(CLSID clsid1, IID interfaceId, 
                                                    std::string progidInfo)
{
	CLSID iUnknown;
	CLSIDFromString(CComBSTR("{00000000-0000-0000-C000-000000000046}"), 
                                                               &iUnknown);

	IUnknown* pUnk = NULL;
	HRESULT hr;
	bool abort = false;
	try {
		hr = CoCreateInstance(clsid1,
			NULL,
			CLSCTX_INPROC_SERVER,
			iUnknown,
			reinterpret_cast<void**>(&pUnk));
	}
	catch (const std::exception& e)
	{
		cout << "failed to CoCreateInstance " << progidInfo;
		abort = true;
	}
	if (!abort)
	{
		if (hr == S_OK) {
			void* pItf = NULL;
			hr = pUnk->QueryInterface(interfaceId, &pItf);
			if (hr == S_OK) {
				return true;
			}
			else
			{
				return false;
			}
		}
	}
}

Unfortunately this threw out hundreds of false positives so I was stuck with my three examples of LDAP, WMI and WCF. At this point I turned to writing my own example and I got it working though it does little. Here is the C# code.

    using System;
    using System.IO;
    using System.Runtime.InteropServices;
    using System.Runtime.InteropServices.ComTypes;

    namespace MonikerParseDisplayName
    {
        // https://www.microsoft.com/msj/1199/wicked/wicked1199.aspx

        // In Project Properties->Build->Check 'Interop for COM'
        // In AssemblyInfo.cs [assembly: ComVisible(true)]
        // compile with /unsafe 
        // In Project Properties->Build->Check 'Allow unsafe code'

        public static class Win32PInvoke
        {
            [DllImport("ole32.dll")]
            public static extern int CreateClassMoniker([In] ref Guid rclsid, 
                                                    out IMoniker ppmk);
        }

        [Guid("0FD50B85-CE66-47E2-9C71-2E780EBB8D54")]
        public interface ICalculator
        {
            double add(double a, double b);
            double mult(double a, double b);
        }

        [ComImport]
        [System.Security.SuppressUnmanagedCodeSecurity]
        [Guid("0000011a-0000-0000-C000-000000000046")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        internal interface IParseDisplayName
        {
            void ParseDisplayName(IBindCtx pbc,
                [MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName,
                IntPtr pchEaten, IntPtr ppmkOut);
            //void ParseDisplayName(IBindCtx pbc,
            //    [MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName,
            //    out int  pchEaten, out IMoniker  ppmkOut);
        }

        [Guid("30194303-435D-44E1-9FB2-A625CEDB8B68")]
        [ClassInterface(ClassInterfaceType.None)]
        [ComDefaultInterface(typeof(ICalculator))]
        public class Calculator : ICalculator, IParseDisplayName,
            IMoniker
        {
            [ComVisible(true)]
            [ComRegisterFunction()]
            public static void DllRegisterServer(string sKey)
            {

                Microsoft.Win32.RegistryKey key;
                key = Microsoft.Win32.Registry.ClassesRoot.CreateSubKey 
                     ("SimonsCalc");
                key.SetValue("", 
                   "Simon's experiment with GetObject(\"SimonsCalc:adder\")");
                Microsoft.Win32.RegistryKey subkey;
                subkey = key.CreateSubKey("Clsid");
                subkey.SetValue("", "{30194303-435D-44E1-9FB2-A625CEDB8B68}");
                subkey.Close();
                key.Close();

            }
            [ComVisible(true)]
            [ComUnregisterFunction()]
            public static void DllUnregisterServer(string sKey)
            {

                try
                {
                    Microsoft.Win32.Registry.ClassesRoot.DeleteSubKeyTree(
                           "SimonsCalc");
                }
                catch (Exception)
                {}
            }

            public static void Log(string logMessage, TextWriter w)
            {
                w.Write("\r\nLog Entry : ");
                w.WriteLine("{0} {1}", DateTime.Now.ToLongTimeString(),
                    DateTime.Now.ToLongDateString());
                w.WriteLine("  :");
                w.WriteLine("  :{0}", logMessage);
                w.WriteLine("-------------------------------");
            }

            public double add(double a, double b)
            {
                return a + b;
            }
            public double mult(double a, double b)
            {
                return a * b;
            }


            void IParseDisplayName.ParseDisplayName(IBindCtx pbc, 
                string pszDisplayName, IntPtr pchEaten, IntPtr ppmkOut)
            {
                using (StreamWriter w = File.AppendText("n:\\log.txt"))
                {
                    IMoniker mon=null;
                    int retVal=0;

                    try { 
                        //consume the whole lot
                        System.Runtime.InteropServices.Marshal.WriteInt32(pchEaten, 
                               pszDisplayName.Length);
                        
                    }
                    catch (Exception ex)
                    {
                        Log("Unsuccessful attempt to populate pchEaten:" + 
                              ex.Message , w);
                    }

                    try
                    {
                        Guid rclsid = new 
                              Guid("30194303-435D-44E1-9FB2-A625CEDB8B68");
                        retVal = Win32PInvoke.CreateClassMoniker(ref rclsid, 
                              out mon );
                    }
                    catch (Exception ex)
                    {
                        Log("Unsuccessful attempt to call CreateClassMoniker:" + 
                               ex.Message, w);
                    }
                    const int S_OK = 0;
                    if (retVal==S_OK)
                    {
                        //unsafe { 
                        //void* pvMon = (void*)mon;
                        //ppmkOut = new IntPtr(pvMon);
                        //} [StructLayout(LayoutKind.Explicit)]
                        try
                        {
                            //ppmkOut=mon;
                            //Marshal.StructureToPtr(mon, ppmkOut, true);
                            //https://searchcode.com/file/115732569/mcs/ ...
                            // ...class/referencesource/System.ServiceModel/ ...
                            // ...System/ServiceModel/ComIntegration/ ...
                            // ...ServiceMoniker.cs#l-159
                            
                            IntPtr ppv = InterfaceHelper.GetInterfacePtrForObject(
                                typeof(IMoniker).GUID, this);

                            System.Runtime.InteropServices.Marshal.WriteIntPtr(
                                  ppmkOut, ppv);
                            
                        }
                        catch (Exception ex)
                        {
                            Log("Unsuccessful attempt to " +
                                "call Marshal.StructureToPtr:" 
                                + ex.Message, w);
                        }
                    }
                }
            }


            void IMoniker.BindToObject(IBindCtx pbc, IMoniker pmkToLeft, 
                ref Guid riidResult, out object ppvResult)
            {
                ppvResult = this;
                //throw new NotImplementedException();
            }

            void IMoniker.BindToStorage(IBindCtx pbc, IMoniker pmkToLeft, 
                ref Guid riid, out object ppvObj)
            {
                throw new NotImplementedException();
            }

            void IMoniker.CommonPrefixWith(IMoniker pmkOther, 
                out IMoniker ppmkPrefix)
            {
                throw new NotImplementedException();
            }

            void IMoniker.ComposeWith(IMoniker pmkRight, bool fOnlyIfNotGeneric, 
                out IMoniker ppmkComposite)
            {
                throw new NotImplementedException();
            }

            void IMoniker.Enum(bool fForward, out IEnumMoniker ppenumMoniker)
            {
                throw new NotImplementedException();
            }

            void IMoniker.GetClassID(out Guid pClassID)
            {
                throw new NotImplementedException();
            }

            void IMoniker.GetDisplayName(IBindCtx pbc, IMoniker pmkToLeft, 
                out string ppszDisplayName)
            {
                throw new NotImplementedException();
            }

            void IMoniker.GetSizeMax(out long pcbSize)
            {
                throw new NotImplementedException();
            }

            void IMoniker.GetTimeOfLastChange(IBindCtx pbc, IMoniker pmkToLeft, 
                out System.Runtime.InteropServices.ComTypes.FILETIME pFileTime)
            {
                throw new NotImplementedException();
            }

            void IMoniker.Hash(out int pdwHash)
            {
                throw new NotImplementedException();
            }

            void IMoniker.Inverse(out IMoniker ppmk)
            {
                throw new NotImplementedException();
            }

            int IMoniker.IsDirty()
            {
                throw new NotImplementedException();
            }

            int IMoniker.IsEqual(IMoniker pmkOtherMoniker)
            {
                throw new NotImplementedException();
            }

            int IMoniker.IsRunning(IBindCtx pbc, IMoniker pmkToLeft, 
                                                 IMoniker pmkNewlyRunning)
            {
                throw new NotImplementedException();
            }

            int IMoniker.IsSystemMoniker(out int pdwMksys)
            {
                throw new NotImplementedException();
            }

            void IMoniker.Load(IStream pStm)
            {
                throw new NotImplementedException();
            }

            void IMoniker.ParseDisplayName(IBindCtx pbc, IMoniker pmkToLeft, 
                    string pszDisplayName, out int pchEaten, out IMoniker ppmkOut)
            {
                throw new NotImplementedException();
            }

            void IMoniker.Reduce(IBindCtx pbc, int dwReduceHowFar, 
                          ref IMoniker ppmkToLeft, out IMoniker ppmkReduced)
            {
                throw new NotImplementedException();
            }

            void IMoniker.RelativePathTo(IMoniker pmkOther, 
                                      out IMoniker ppmkRelPath)
            {
                throw new NotImplementedException();
            }

            void IMoniker.Save(IStream pStm, bool fClearDirty)
            {
                throw new NotImplementedException();
            }
        }

        internal static class InterfaceHelper
        {
            // only use this helper to get interfaces that 
            // are guaranteed to be supported
            internal static IntPtr GetInterfacePtrForObject(Guid iid, object obj)
            {
                IntPtr pUnk = Marshal.GetIUnknownForObject(obj);
                if (IntPtr.Zero == pUnk)
                {
                    //throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(
                    //new ArgumentException(SR.GetString(SR.UnableToRetrievepUnk)));
                }

                IntPtr ppv = IntPtr.Zero;
                int hr = Marshal.QueryInterface(pUnk, ref iid, out ppv);

                Marshal.Release(pUnk);

                if (hr != 0)
                {
                    throw new Exception("QueryInterface should succeed");
                }

                return ppv;
            }
        }    
    }

A shout out has to be made to a code search resoure called searchcode.com which helped me find sample fragments simply not anywhere on StackOverflow or on official Microsoft documentation.

On reflection there is nothing in this custom activation syntax that cannot be shipped in a method call after a COM server has been instantiated by New or CreateObject or the C# equivalents. If the COM server is remote then I suppose one saves a network round trip. LDAP would be a remote call in an enterprise. WMI could query remote resources. WCF is a remote-ing technology. So you can see the use cases. For same machine calls custom activation syntax's absence is no great loss.

C# - Embed Html page as resource in C# Winforms

So in previous posts I have shown a Winforms WebBrowser control navigating to a local html file.  It is possible to set the WebBrowser's DocumentStream property to a System.IO.Stream.  With the following code it is possible to create a System.IO.Stream from a string


        public static System.IO.Stream GenerateStreamFromString(string s)
        {
            MemoryStream stream = new MemoryStream();
            StreamWriter writer = 
                  new StreamWriter(stream,System.Text.Encoding.Unicode);
            writer.Write(s);
            writer.Flush();
            stream.Position = 0;
            return stream;
        }


The string would be an Html string but we want to avoid files so it looks like we can add the file as an embedded resource.

I named mine HTMLPage1 and then it can be referred to in code with


            string myFile2 = WebBrowserInWinForms.Properties.Resources.HTMLPage1;

            System.IO.Stream s = GenerateStreamFromString(myFile2);


But beware, the document needs to complete loading and whilst that is happening the stream needs to kept alive so no early disposal. To solve define stream variable at class level and dispose later. So the code becomes

using System;
using System.Windows.Forms;
using System.IO;

namespace WebBrowserInWinForms
{
    public partial class Form1 : Form
    {
        Stream s = null;

        public Form1()
        {
            
            InitializeComponent();

            string myFile2 = 
               WebBrowserInWinForms.Properties.Resources.HTMLPage1;

            this.webBrowser1.DocumentCompleted += webBrowser1_DocumentCompleted;
            s = GenerateStreamFromString(myFile2);
            this.webBrowser1.DocumentStream = s;

        }

        private void webBrowser1_DocumentCompleted(object sender, 
                                   WebBrowserDocumentCompletedEventArgs e)
        {
            s.Dispose(); // finished with this now
            System.Windows.Forms.HtmlElement clickMe = 
                              this.webBrowser1.Document.Body.All["clickMe"];
            clickMe.Click += clickMe_Click;
        }

        public static Stream GenerateStreamFromString(string s)
        {
            MemoryStream stream = new MemoryStream();
            StreamWriter writer = 
                      new StreamWriter(stream,System.Text.Encoding.Unicode);
            writer.Write(s);
            writer.Flush();
            stream.Position = 0;
            return stream;
        }
        
        void clickMe_Click(object sender, HtmlElementEventArgs e)
        {
            //...
        }
    }
}

Copying the HTML string into the resource editor is clunky, the resource string editor is probably just for words and phrases when one ships an edition in a foreign language. It would be nice if in Debug environment we could use the file navigate method and then for Release we'd do a custom build step and pack the files into the embedded resources with code. We'd need conditional compilation.

The embedded resources can be found in <$SolutionDirector$>/Properties/Resources.resx which is Xml so they are entitised, this looks possible for one day.

C# - Creating a simple Winforms and WebBrowser application to shows disk drives

Ok, so I've seen enough to be able to give a series of instructions as to how to build a simple Winforms app that uses the WebBrowser control and an HTML Gui to display a list of disk drives.

To begin we need an HTML file with a clickable anchor and the shell of a table into which we will writes rows (and delete rows when re-running).

1. Create a new Winforms project called WinformsBrowserShowDrives.
2. In Solution Explorer for the C# project select menu 'Add->New Item' and select an HTML Page, HTMLPage1.html.

Add the following HTML to HTMLPage1.html and save


<!DOCTYPE html>
<html>

<head>
    <title>Disk Drive List</title>
    <style>
        table {
            border: 1px solid black;
        }

        th, td {
            padding: 5px;
            text-align: left;
            font-family: Arial;
            font-size: 10pt;
        }

        th {
            background-color: darkblue;
            color: white;
        }

        tr:nth-child(even) {
            background-color: #f2f2f2;
        }

        a#clickMe:hover {
            background-color: #1A365B;
            color: white;
        }

        a#clickMe {
            background-color: #E0F1EA;
            color: #000000;
        }
    </style>
</head>

<body>
    <a id="clickMe">Click Me</a>
    <table>
        <tbody id="diskDrives">
            <tr>
                <th>Disk Drive</th>
                <th>Interface Type</th>
                <th>DeviceDesc</th>
                <th>DeviceSize</th>
            </tr>
        </tbody>
    </table>
</body>
</html>

3. Add a WebBrowser control to the WinForms.Form1. Expand the form to something close to a browser window's size.
4. Add the following code to the Form1.cs class

    public Form1()
    {
        InitializeComponent();

        Assembly myExe = Assembly.GetExecutingAssembly();
        string filePath = Path.GetDirectoryName(new Uri(myExe.CodeBase).LocalPath);
        string myFile = Path.Combine(filePath, @"..\..\HTMLPage1.html");

        if (File.Exists(myFile))
        {
            //this.webBrowser1.DocumentCompleted +
            this.webBrowser1.Navigate(myFile);
        }
    }

5. In the above code, right-click on "Assembly" and Resolve to get the correct Using.
6. In the above code, right-click on "Path" and Resolve to get the correct Using.
7. Build the Project, it should compile then run the project it should run and show the HTML page, if not diagnose file locations.

OK, so at this point the project should build and run and show the HTML in the form, yay! But it doesn't do much as the moment so let's add some active content. First thing we need to do is write an event handler for the DocumentComplete event because we have to wait for completion before accessing the Dom.

8. In the above code un-comment the commented line go to the plus sign at the end and type an equals sign this should bring up hints.
9. Follow the hint to press TAB and repeat. The C# IDE should create an event handler for you.

The code should now look like this


    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            Assembly myExe = Assembly.GetExecutingAssembly();
            string filePath = Path.GetDirectoryName(new Uri(myExe.CodeBase).LocalPath);
            string myFile = Path.Combine(filePath, @"..\..\HTMLPage1.html");

            if (File.Exists(myFile))
            {
                this.webBrowser1.DocumentCompleted += webBrowser1_DocumentCompleted;
                this.webBrowser1.Navigate(myFile);
            }
        }

        void webBrowser1_DocumentCompleted(object sender, 
                                         WebBrowserDocumentCompletedEventArgs e)
        {
            throw new NotImplementedException();
        }
    }

10. In the newly created code block replace with the following

    void webBrowser1_DocumentCompleted(object sender, 
                WebBrowserDocumentCompletedEventArgs e)
    {
        System.Windows.Forms.HtmlElement clickMe = 
            this.webBrowser1.Document.Body.All["clickMe"];
        //clickMe.Click +
    }

11. Use same trick as steps 8. and 9. to create a click handler for the clickMe anchor element. Code should look like this

    public Form1()
    {
        InitializeComponent();

        Assembly myExe = Assembly.GetExecutingAssembly();
        string filePath = Path.GetDirectoryName(new Uri(myExe.CodeBase).LocalPath);
        string myFile = Path.Combine(filePath, @"..\..\HTMLPage1.html");

        if (File.Exists(myFile))
        {
            this.webBrowser1.DocumentCompleted += webBrowser1_DocumentCompleted;
            this.webBrowser1.Navigate(myFile);
        }
    }

    void webBrowser1_DocumentCompleted(object sender, 
                WebBrowserDocumentCompletedEventArgs e)
    {
        System.Windows.Forms.HtmlElement clickMe = 
            this.webBrowser1.Document.Body.All["clickMe"];
        clickMe.Click += clickMe_Click;
    }

    void clickMe_Click(object sender, HtmlElementEventArgs e)
    {
        throw new NotImplementedException();
    }
}

Next we need to write a class that gives us some data, we'll Wiindows Management Instrumentation (WMI) to provide something interesting to look at.

11. In the Solution Explorer, on the C# Project node right-click and take 'Add->New Itm' and add a Class file called WMIDriveDeviceLister.cs
12. Add using directive "using System.Management;" at top of class
13. Add Reference to System.Management.dll otherwise you'll get compilation errors
14. Copy in the following code

using System;
using System.Collections.Generic;
using System.Text;
using System.Management;           // Add Reference to System.Management.dll

namespace WinformsBrowserShowDrives
{
    internal class WMIDriveDeviceLister
    {
        internal string ListDiskDevicesAsHTML()
        {
            List<Tuple<int, string, string, string>> driveDetails;
            driveDetails = this.ListDiskDevices();

            StringBuilder rows = new StringBuilder();
            foreach (Tuple<int, string, string, string> driveDetail in driveDetails)
            {
                rows.Append("<tr>"
                        + "<td>" + driveDetail.Item1 + "</td>"
                        + "<td>" + driveDetail.Item2 + "</td>"
                        + "<td>" + driveDetail.Item3 + "</td>"
                        + "<td>" + driveDetail.Item4 + "</td>"
                        + "</tr>");
            }

            return rows.ToString();
        }

        internal List<Tuple<int, string, string, string>> ListDiskDevices()
        {
            // Add Reference to System.Management.dll
            System.Management.ManagementScope scope = new
                System.Management.ManagementScope(@"\\.");
            System.Management.ObjectQuery query = new
                System.Management.ObjectQuery("Select * from Win32_DiskDrive");
            ManagementObjectSearcher searcher = 
                      new ManagementObjectSearcher(scope, query);

            Dictionary<Int32, ManagementObject> dicUnSort = 
                      new Dictionary<Int32, ManagementObject>();

            ManagementObjectCollection queryCollection = searcher.Get();

            foreach (ManagementObject m in queryCollection)
            {
                int driveIndex = Int32.Parse(m["Index"].ToString());
                dicUnSort.Add(driveIndex, m);
            }

            List<Tuple<int, string, string, string>> sortedList = 
                                 new List<Tuple<int, string, string, string>>();
            for (int i = 0; i < dicUnSort.Count; i++)
            {
                ManagementObject m = dicUnSort[i];
                string s1 = m["InterfaceType"].ToString();
                string s2 = m["Caption"].ToString();
                string s3 = (m["Size"] != null) ? m["Size"].ToString() : "";
                Tuple<int, string, string, string> driveDetails =
                    new Tuple<int, string, string, string>(i,
                    m["InterfaceType"].ToString(),
                    m["Caption"].ToString(),
                    (m["Size"] != null) ? 
                      (Int64.Parse(m["Size"].ToString()) / (1000000)).ToString()
                      : "");

                sortedList.Add(driveDetails);
            }
            return sortedList;
        }
    }
}


Now we're going to write a class to handle the population of the disk drive HTML table

15. In the Solution Explorer, on the C# Project node right-click and take 'Add->New Item' and add a Class file called DrivesTable.cs
16. Add using directive "using System.Windows.Forms;" at top of class
17. Copy in code below

using System;
using System.Collections.Generic;
using System.Windows.Forms;

namespace WinformsBrowserShowDrives
{
    public static class MyHTMLExtensions
    {
        public static HtmlElement CreateElementWithInnerText(
                   this HtmlDocument doc, string elementTag, string innerText)
        {
            HtmlElement ele = doc.CreateElement(elementTag);
            ele.InnerText=innerText;
            return ele;
        }
    }

    internal class DrivesTable
    {
        protected HtmlDocument doc = null;
        protected HtmlElement tBody = null;

        internal DrivesTable(HtmlDocument doc, string tBodyId)
        {
            this.doc = doc;
            this.tBody = doc.Body.All[tBodyId];
        }

        internal void AddRows()
        {
            WMIDriveDeviceLister lister=new WMIDriveDeviceLister();
            List<Tuple<int, string, string, string>> driveDetails;
            driveDetails = lister.ListDiskDevices();

            foreach (Tuple<int, string, string, string> driveDetail in driveDetails)
            {
                HtmlElement tr = doc.CreateElement("tr");
                tr.Id = "diskDriveEntry";

                tr.AppendChild(doc.CreateElementWithInnerText("td", 
                    driveDetail.Item1.ToString()));

                tr.AppendChild(doc.CreateElementWithInnerText("td", 
                    driveDetail.Item2));

                tr.AppendChild(doc.CreateElementWithInnerText("td", 
                    driveDetail.Item3));

                tr.AppendChild(doc.CreateElementWithInnerText("td", 
                    driveDetail.Item4));

                tBody.AppendChild(tr);
            }
        }
    }
}


//internal void RemoveRows()
//{
//    HTMLDocument htmlDoc = (HTMLDocument)doc.DomDocument;
//    bool skippedFirst = false;
//    foreach (HtmlElement row in rotTBody.Children)
//    {
//        if (skippedFirst)
//        {
//            IHTMLDOMNode node = htmlDoc.getElementById(row.Id) as IHTMLDOMNode;
//            node.parentNode.removeChild(node);
//        }
//        skippedFirst = true;
//    }
//}


The final step is make the anchor click handler call into the new classes. So in the Form1.cs change button handler code to this below.

        void clickMe_Click(object sender, HtmlElementEventArgs e)
        {
            DrivesTable table = new DrivesTable(webBrowser1.Document, "diskDrives");
            table.AddRows();
        }

So the code should run and you should see a "ClickMe anchor" (sadly hover CSS does not work at the moment, SO Q pending) and if you clik on it the C# code will query WMI and then write out an HTML table. Press it again and it add more rows because currently remove rows is commented out to simplify this blog entry.

C# - Microsoft Tutorial: missing code for webBrowser in winforms

So a very good StackOverflow question asks about using HTML as the GUI technology for a WinForms desktop application. I can sympathise with this because the Excel worksheet is already a rich GUI and so for forms do I really need to keep current with WinForms and WPF etc. can't I write forms in HTML please?

So just working this tutorial from Microsoft but sadly there are a few lines missing from the Form1.Designer.cs class where all the controls are defined to be members of the Form1 class.

Here is the missing code which you should place at bottom of Form1.Designer.cs


        private void InitializeComponent()
        {
            // ... Designer authored code goes here do not modify...  
        }

        #endregion

        // Here the new code begins
        private System.Windows.Forms.WebBrowser webBrowser1;
        private System.Windows.Forms.MenuStrip menuStrip1; 

        private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem;
        private System.Windows.Forms.ToolStripMenuItem saveAsToolStripMenuItem;

        private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;

        private System.Windows.Forms.ToolStripMenuItem printToolStripMenuItem;
        private System.Windows.Forms.ToolStripMenuItem printPreviewToolStripMenuItem;

        private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;

        private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
        private System.Windows.Forms.ToolStripMenuItem pageSetupToolStripMenuItem;
        private System.Windows.Forms.ToolStripMenuItem propertiesToolStripMenuItem;

        private System.Windows.Forms.ToolStrip toolStrip1;
        private System.Windows.Forms.ToolStripButton goButton;
        private System.Windows.Forms.ToolStripButton backButton;
        private System.Windows.Forms.ToolStripButton forwardButton;
        private System.Windows.Forms.ToolStripButton stopButton;
        private System.Windows.Forms.ToolStripButton refreshButton;
        private System.Windows.Forms.ToolStripButton homeButton; 
        private System.Windows.Forms.ToolStripButton searchButton; 
        private System.Windows.Forms.ToolStripButton printButton;

        private System.Windows.Forms.ToolStrip toolStrip2; 
        private System.Windows.Forms.ToolStripTextBox toolStripTextBox1; 

        private System.Windows.Forms.StatusStrip statusStrip1;  
        private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabel1; 
        // Here the new code ends
    }
}


Early experiments show this browser control doesn't like JSON parsing as found in JQuery. I wonder how much Javascript is compromised. I also wonder if in fact one could write a pure HTML web page and handle events in C#.

C# - .net versions of COM interfaces

Currently poking around in C# and COM development and one great resource I have discovered is that compiler errors can detail the C# versions of COM interfaces.  So, open a C# Class library project and declare the default class (Class1) as implementing a classic COM interface, e.g. IAdviseSink so my code looks like this

namespace ComInterfacesInCSharp
{
    public class Class1 : System.Runtime.InteropServices.ComTypes.IAdviseSink 
    {
    }
}

Then attempt to compile, you will get the following error messages ...

error CS0535: 'Class1' does not implement interface member ...
  ... System.Runtime.InteropServices.ComTypes.IAdviseSink ...
OnDataChange(ref System.Runtime.InteropServices.ComTypes.FORMATETC, 
              ref System.Runtime.InteropServices.ComTypes.STGMEDIUM)
OnViewChange(int, int)
OnRename(System.Runtime.InteropServices.ComTypes.IMoniker)
OnSave()
OnClose()

So now you can dig out these method signatures and put them in your class, I used full qualification whilst I progressed through compilation errors. I used void as the return type and supplied some parameter identifiers to get to this

namespace ComInterfacesInCSharp
{
    public class Class1 : System.Runtime.InteropServices.ComTypes.IAdviseSink 
    {
        void System.Runtime.InteropServices.ComTypes.IAdviseSink.OnDataChange(
                      ref System.Runtime.InteropServices.ComTypes.FORMATETC form, 
                      ref System.Runtime.InteropServices.ComTypes.STGMEDIUM medium) { }
        void System.Runtime.InteropServices.ComTypes.IAdviseSink.OnViewChange(
                      int i1, int i2) {}
        void System.Runtime.InteropServices.ComTypes.IAdviseSink.OnRename(
                         System.Runtime.InteropServices.ComTypes.IMoniker mon) {}
        void System.Runtime.InteropServices.ComTypes.IAdviseSink.OnSave() {}
        void System.Runtime.InteropServices.ComTypes.IAdviseSink.OnClose() {}
    }
}

And that compiled! Yay! Of course we can then economise

using System.Runtime.InteropServices.ComTypes;

namespace ComInterfacesInCSharp
{
    public class Class1 : IAdviseSink 
    {
        void IAdviseSink.OnDataChange(ref FORMATETC form, 
                      ref STGMEDIUM medium) { }
        void IAdviseSink.OnViewChange(int i1, int i2) {}
        void IAdviseSink.OnRename(IMoniker mon) {}
        void IAdviseSink.OnSave() {}
        void IAdviseSink.OnClose() {}
    }
}

Ok, so I'm not claiming there won't be problems ahead going down this path ut it does give a starting point and then we can use Google and StackOverflow to iron out difficulties.

*** EDIT *** EDIT *** EDIT *** EDIT ***

So I asked about another interface on StackOverflow that wasn't playing ball and my new favourite Stackoverflow user gave both an excellent link to Microsoft Reference Source site and also a new gif to remind me that the C# IDE can be very very helpful and write out the interface for you from the menu. Here is the gif ...

C# - COM - Running Object Table Viewer - Example of C# COM Server scriptable from VBA

Summary: Migrated from our sister Excel blog, this source code builds to a .NET class library that also serves as a COM Server. The logic inspects and reports on the Running Object Table.

So COM has a machine wide table for registering objects that can be acquired by other processes, called the Running Object Table. Processes that are COM-enabled like Excel, Word and Internet Explorer can both deposit and fetch objects from the ROT. There used to be a ROT viewer that shipped with earlier versions of Visual Studio but it went away. So I wrote a new one, or at least a core dll, COM enabled assembly that can be scripted from Excel VBA and other COM clients. I put the code on StackOverflow here but I've had some of my questions deleted so I'm placing a copy here as well. And there was a bug to fix so that is fixed here.

Also I am including some sample VBA client script.

This code base is actually a good example of a C# Com server. We define interfaces separately from the classes. Use this project as a template for your own COM Dlls.

The C# .NET Framework assembly

Start a .NET Framework assembly project, in AssemblyInfo.cs change to [assembly: ComVisible(true)] and in the Project Properties->Build tab under the Output section ensure the 'Register for COM interop' checkbox is checked.

You will need to run Visual Studio with admin rights so it can register the dll.


using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Xml;

// Courteously curated and posted to Stack Overflow by S Meaden from MSDN samples and documentation

namespace RotViewerScriptable
{
    internal static class RotNativeMethods
    {
        [DllImport("ole32.dll", PreserveSig = false)]
        internal static extern void CreateBindCtx(uint reserved, out IBindCtx ppbc);

        [DllImport("ole32.dll")]
        internal static extern int GetRunningObjectTable(int reserved,
            out IRunningObjectTable prot);

        [DllImport("ole32.dll")]
        internal static extern int CreateFileMoniker([MarshalAs(UnmanagedType.LPWStr)] string lpszPathName, out System.Runtime.InteropServices.ComTypes.IMoniker ppmk);

        [DllImport("oleaut32.dll")]
        internal static extern int RevokeActiveObject(int register, IntPtr reserved);

        [DllImport("kernel32.dll")]
        internal static extern bool FileTimeToLocalFileTime([In] ref System.Runtime.InteropServices.ComTypes.FILETIME lpFileTime,
           out System.Runtime.InteropServices.ComTypes.FILETIME lpLocalFileTime);

        [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)]
        [return: MarshalAs(UnmanagedType.LPWStr)]
        internal static extern string StringFromCLSID([MarshalAs(UnmanagedType.LPStruct)] Guid rclsid);

        [DllImport("ole32.dll")]
        internal static extern int IIDFromString([MarshalAs(UnmanagedType.LPWStr)] string lpsz,
           out Guid lpiid);

    }


    public interface IMonikerDetails
    {
        string monikerType { get; set; }
        string monikerClassId { get; set; }

    }
    [ClassInterface(ClassInterfaceType.None)]
    [ComDefaultInterface(typeof(IMonikerDetails))]
    public class MonikerDetails : IMonikerDetails
    {
        public string monikerType { get; set; }
        public string monikerClassId { get; set; }
    }

    /// 
    /// We have to make this a class instead of a struct so VBA can script against it
    /// 
    public interface IRotTableEntry
    {
        string displayName { get; set; }
        DateTime lastChange { get; set; }
        string className { get; set; }
        MonikerDetails monikerDetails { get; set; }

    }

    [ClassInterface(ClassInterfaceType.None)]
    [ComDefaultInterface(typeof(IRotTableEntry))]
    public class RotTableEntry : IRotTableEntry
    {
        public string displayName { get; set; }
        public DateTime lastChange { get; set; }
        public string className { get; set; }
        public MonikerDetails monikerDetails { get; set; }
    }
    /// 
    /// Be a good COM citizen and define the interface separately.
    /// In AssemblyInfo.cs we have [assembly: ComVisible(true)].
    /// In Project Properties->Build tab under the Output section we have checked the 'Register for COM interop' checkbox.
    /// 
    public interface IRotViewer
    {
        RotTableEntry[] DetailedTableAsArray();
        string DetailedTableAsXml();
        string DetailedTableAsJson();
        string[] TableAsStringArray();
        string TableAsXml();
        string TableAsJson();
        object GetObject(string sRotEntry);
        bool IsExcelFileName(string filename);
        int[] RegisterObject(object obj, string stringId);
        int RevokeActiveObject(int register);

    }

    /// 
    /// A single class that reads the Running Object Table and is instantiable/creatable from VBA, JScript and scripting clients.
    /// 
    [ClassInterface(ClassInterfaceType.None)]
    [ComDefaultInterface(typeof(IRotViewer))]
    public class RotViewer : IRotViewer
    {

        /// 
        /// This is the equivalent of VBA's GetObject where one supplies a filename or other moniker to retrieve an interface pointer to object.
        /// 
        /// 
        /// A COM interface pointer to object in running object table specified by entry.
        public object GetObject(string sRotEntry)
        {
            Object retVal = null;
            IBindCtx ctx;
            IRunningObjectTable table;
            IEnumMoniker mon;
            IMoniker[] lst = new IMoniker[1];

            RotNativeMethods.CreateBindCtx(0, out ctx);
            ctx.GetRunningObjectTable(out table);
            table.EnumRunning(out mon);
            while (mon.Next(1, lst, IntPtr.Zero) == 0)
            {
                string displayName;
                lst[0].GetDisplayName(ctx, lst[0], out displayName);
                if (displayName == sRotEntry)
                {
                    table.GetObject(lst[0], out retVal);
                }
            }
            return retVal;
        }

        /// 
        /// For VBA Clients we can use COM interop compatible types to return table as string array.
        /// 
        /// An string array of all the entries in the Running Object Table.
        public string[] TableAsStringArray()
        {
            List itemsList = new List();

            IBindCtx ctx;
            IRunningObjectTable table;
            IEnumMoniker mon;
            IMoniker[] lst = new IMoniker[1];

            RotNativeMethods.CreateBindCtx(0, out ctx);
            ctx.GetRunningObjectTable(out table);
            table.EnumRunning(out mon);
            while (mon.Next(1, lst, IntPtr.Zero) == 0)
            {
                string displayName;
                lst[0].GetDisplayName(ctx, lst[0], out displayName);
                itemsList.Add(displayName);
            }
            return itemsList.ToArray();
        }

        /// 
        /// For clients that prefer to work with Xml documents call this.
        /// 
        /// An Xml string of a document of all the entries in the Running Object Table.
        public string TableAsXml()
        {
            string[] table = this.TableAsStringArray();
            XmlDocument dom = new XmlDocument();
            dom.LoadXml("");
            foreach (string s in table)
            {
                XmlElement rte = dom.CreateElement("RotTableEntry");
                rte.InnerText = s;
                dom.DocumentElement.AppendChild(rte);
            }
            return dom.OuterXml;
        }

        /// 
        /// Call this from Javascript and then use JSON.parse() to get the entries as an array.
        /// 
        /// A JSON string of all the entries in the Running Object Table.
        public string TableAsJson()
        {
            string[] table = this.TableAsStringArray();
            string json = "";
            foreach (string s in table)
            {
                string text = s.Replace(@"\", @"\\");
                if (json.Length > 0) { json = json + ","; }
                json = json + "\"" + text + "\"";
            }
            return "[" + json + "]";
        }

        /// 
        /// Utility method to help clients filter items.
        /// 
        /// An entry for the Running Object Table.
        /// True if it is an Excel file.
        public bool IsExcelFileName(string filename)
        {
            string extension = Path.GetExtension(filename).ToUpper();
            return (extension == ".XLS" || extension == ".XLSX" || extension == ".XLSM");
        }

        /// 
        /// This allows us to register an object in the Running Object Table
        /// 
        /// A Com interface pointer owned by the object
        /// A display name for the object
        /// Returns two integers as an array, the first is the HRESULT of system calls and if successful the second is the registration id (regid).  You'll need the regid to revoke from the ROT when tidying up.
        public int[] RegisterObject(object obj, string stringId)
        {
            int[] retval = new int[] { 0, 0 };
            int regId = -1;

            System.Runtime.InteropServices.ComTypes.IRunningObjectTable pROT = null;
            System.Runtime.InteropServices.ComTypes.IMoniker pMoniker = null;

            int hr;

            if ((hr = RotNativeMethods.GetRunningObjectTable((int)0, out pROT)) != 0)
            {
                retval[0] = hr;
                return retval; //(hr);
            }

            // File Moniker has to be used because in VBS GetObject only works with file monikers in the ROT
            if ((hr = RotNativeMethods.CreateFileMoniker(stringId, out pMoniker)) != 0)
            {

                retval[0] = hr;
                return retval; //(hr);
            }

            int ROTFLAGS_REGISTRATIONKEEPSALIVE = 1;
            regId = pROT.Register(ROTFLAGS_REGISTRATIONKEEPSALIVE, obj, pMoniker);


            retval[0] = 0;
            retval[1] = regId;
            return retval;
        }

        /// 
        /// Removes an object previously registered with RegisterObject(,).
        /// 
        /// The registration id (regid) returned from RegisterObject().
        /// 
        public int RevokeActiveObject(int register)
        {
            int hr;
            hr = RotNativeMethods.RevokeActiveObject(register, IntPtr.Zero);
            return hr;
        }

        public RotTableEntry[] DetailedTableAsArray()
        {
            List itemsList = new List();

            IBindCtx ctx;
            IRunningObjectTable table;
            IEnumMoniker mon;
            IMoniker[] lst = new IMoniker[1];

            RotNativeMethods.CreateBindCtx(0, out ctx);
            ctx.GetRunningObjectTable(out table);
            table.EnumRunning(out mon);
            while (mon.Next(1, lst, IntPtr.Zero) == 0)
            {
                RotTableEntry item = new RotTableEntry();
                item.monikerDetails = new MonikerDetails();

                {
                    Guid guid;
                    lst[0].GetClassID(out guid);

                    item.monikerDetails.monikerClassId = RotNativeMethods.StringFromCLSID(guid);

                    switch (item.monikerDetails.monikerClassId)
                    {
                        case "{00000303-0000-0000-C000-000000000046}":
                            item.monikerDetails.monikerType = "FileMoniker";
                            break;
                        case "{00000304-0000-0000-C000-000000000046}":
                            item.monikerDetails.monikerType = "ItemMoniker";
                            break;
                        case "{00000305-0000-0000-C000-000000000046}":
                            item.monikerDetails.monikerType = "AntiMoniker";
                            break;
                        case "{00000306-0000-0000-C000-000000000046}":
                            item.monikerDetails.monikerType = "PointerMoniker";
                            break;
                        case "{00000308-0000-0000-C000-000000000046}":
                            item.monikerDetails.monikerType = "PackageMoniker";
                            break;
                        case "{00000309-0000-0000-C000-000000000046}":
                            item.monikerDetails.monikerType = "CompositeMoniker";
                            break;
                        case "{0000031A-0000-0000-C000-000000000046}":
                            item.monikerDetails.monikerType = "ClassMoniker";
                            break;
                        default:
                            {
                                RegistryKey monikerClassKey = Registry.ClassesRoot.OpenSubKey("CLSID\\" + item.monikerDetails.monikerClassId);
                                if (monikerClassKey == null)
                                {
                                    item.monikerDetails.monikerType = "Failed to identify moniker";
                                }
                                else
                                {
                                    item.monikerDetails.monikerType = monikerClassKey.GetValue(null).ToString();
                                }
                            }
                            break;
                    }

                }

                {
                    string displayName;
                    lst[0].GetDisplayName(ctx, lst[0], out displayName);
                    item.displayName = displayName;
                    item.className = "";
                }

                {
                    if (item.monikerDetails.monikerType == "FileMoniker")
                    {
                        System.Runtime.InteropServices.ComTypes.FILETIME ft;
                        table.GetTimeOfLastChange(lst[0], out ft);


                        long hFT2 = (((long)ft.dwHighDateTime) << 32) + ft.dwLowDateTime;

                        DateTime dte = DateTime.FromFileTime(hFT2);
                        item.lastChange = dte;
                        //http://snipplr.com/view/32409/
                    }
                    if (item.monikerDetails.monikerType == "ItemMoniker")
                    {
                        string coreGuid = "";
                        {
                            if ((item.displayName.Substring(0, 1) == "!") && (item.displayName.Length >= 38))
                            {
                                coreGuid = item.displayName.Substring(1, 38);
                                RegistryKey key = null;
                                key = Registry.ClassesRoot.OpenSubKey("CLSID\\" + coreGuid);
                                if (key == null)
                                {
                                    key = Registry.ClassesRoot.OpenSubKey("Wow6432Node\\CLSID\\" + coreGuid);
                                }
                                if (key != null)
                                {
                                    item.className = key.GetValue(null).ToString();
                                }
                            }
                        }

                    }
                }

                itemsList.Add(item);
            }
            return itemsList.ToArray();
        }


        /// 
        /// For clients that prefer to work with Xml documents call this.
        /// 
        /// An Xml string of a document of all the detailed entries in the Running Object Table.
        public string DetailedTableAsXml()
        {
            RotTableEntry[] table = this.DetailedTableAsArray();
            XmlDocument dom = new XmlDocument();
            dom.LoadXml("");
            foreach (RotTableEntry tb in table)
            {
                XmlElement rte = dom.CreateElement("RotTableEntry");

                XmlElement dn = dom.CreateElement("DisplayName");
                dn.InnerText = tb.displayName;
                rte.AppendChild(dn);

                XmlElement lcd = dom.CreateElement("LastChange");
                DateTime.UtcNow.ToString("o");
                lcd.InnerText = tb.lastChange.ToString("s", System.Globalization.CultureInfo.InvariantCulture);
                rte.AppendChild(lcd);

                XmlElement monType = dom.CreateElement("MonikerType");
                monType.InnerText = tb.monikerDetails.monikerType;
                rte.AppendChild(monType);

                XmlElement clsNam = dom.CreateElement("ClassName");
                clsNam.InnerText = tb.className;
                rte.AppendChild(clsNam);

                dom.DocumentElement.AppendChild(rte);
            }
            return dom.OuterXml;
        }

        public string DetailedTableAsJson()
        {
            RotTableEntry[] table = this.DetailedTableAsArray();
            string wholeJson = "";
            foreach (RotTableEntry tbe in table)
            {
                string jsonTE = "";
                {
                    string displayNameString = tbe.displayName.Replace(@"\", @"\\");
                    jsonTE = jsonTE + "\"displayName\":\"" + displayNameString + "\",";
                }
                string jsonTEMonDetails = "";
                {
                    jsonTEMonDetails = jsonTEMonDetails + "\"monikerType\":\"" + tbe.monikerDetails.monikerType + "\",";
                    jsonTEMonDetails = jsonTEMonDetails + "\"monikerClassId\":\"" + tbe.monikerDetails.monikerClassId + "\"";
                    jsonTEMonDetails = "{" + jsonTEMonDetails + "}";
                    jsonTE = jsonTE + "\"monikerDetails\":" + jsonTEMonDetails + "";
                }
                {
                    if (tbe.lastChange != DateTime.MinValue)
                    {
                        string lastChangeString = tbe.lastChange.ToString("s", System.Globalization.CultureInfo.InvariantCulture);
                        jsonTE = jsonTE + ",\"lastChange\":\"" + lastChangeString + "\"";
                    }
                    else
                    {
                        jsonTE = jsonTE + ",\"lastChange\":\"\"";
                    }
                }
                {
                    jsonTE = jsonTE + ",\"className\":\"" + tbe.className + "\"";
                }

                jsonTE = "{" + jsonTE + "}";

                if (wholeJson.Length > 0) { wholeJson = wholeJson + ","; }
                wholeJson = wholeJson + jsonTE;
            }
            return "[" + wholeJson + "]";
        }
    }
}


The client VBA code

Once the C# project is built (with admin rights) then its type library should be visible in the VBA IDE Tools->References so you should now be able to run the following code...


Function FindARunningExcel() As Excel.Application
    Dim oROT As RunningObjectTable.RotViewer
    Set oROT = New RunningObjectTable.RotViewer
    
    'Dim sTableAsStringArray() As String
    'sTableAsStringArray() = oROT.TableAsStringArray
    
    'Dim sTableAsJson As String
    'sTableAsJson = oROT.TableAsJson
    
    'Dim sTableAsXml As String
    'sTableAsXml = oROT.TableAsXml
    
    Dim v() As RunningObjectTable.RotTableEntry
    v() = oROT.DetailedTableAsArray()
    
    Dim lLoop As Long
    For lLoop = LBound(v) To UBound(v)
        Dim oEntryLoop As RunningObjectTable.RotTableEntry
        Set oEntryLoop = v(lLoop)
        If oROT.IsExcelFileName(oEntryLoop.DisplayName) Then
            Debug.Print oEntryLoop.DisplayName
            
            If TypeName(GetObject(oEntryLoop.DisplayName).Parent) = "Application" Then
                Dim xlApp As Excel.Application
                Set xlApp = GetObject(oEntryLoop.DisplayName).Parent
                
            End If
        End If
    Next lLoop
    Set FindARunningExcel = xlApp

End Function