Monday, February 21, 2005

I'm working on a fairly straightforward wrapper for a customer and it occurred to me that there are a distinct lack of simple working examples of the “more difficult” marshaling techniques in the CF. A classic example is a native struct that contains a pointer to a string. On the desktop it's simple (and in CF 2.0 it's pretty easy too), but in CF 1.0, it's not straightforward.

Personally I don't mind the code required to do the marshaling - in fact I think everyone using marshaling should be required to understand these fundamentals. While that's never going to happen, some people still are interested, or their project may necessitate being able to do it, and of course having a sample never hurts.

So consider the following very simple struct. A simgle 4-byte integer followed by a pointer to a string:

typedef struct
{
 INT IntMember,
 LPSTR StringMember
} MYSTRUCT, *PMYSTRUCT;

What appears to be rather simple in C - just a mere 5 lines of code - expands to the following not-so-small 80-line code base in C#:

public class EmbeddedStringPtrStruct
{
 // offsets used internally
 private const int intMemberOffset = 0;
 private const int stringMemberOffset = 4;
 // this is the actual struct data to be passed to native methods
 protected byte[] m_bytes = new byte[8];
 // private var to hold string data pointer
 private IntPtr m_pStringMember = IntPtr.Zero;
 // default public ctor
 public EmbeddedStringPtrStruct()
 {
 }
 // dtor - call Dispose if the consumer didn't
 ~EmbeddedStringPtrStruct()
 {
  this.Dispose();
 }
 // operator to get a byte array used for marshaling
 // this allows you to pass the EmbeddedStringPtrStruct directly
 public static implicit operator byte[](EmbeddedStringPtrStruct esps)
 {
  return esps.m_bytes;
 }
 // accessor for INT member
 public int IntMember
 {
  get
  {
   return BitConverter.ToInt32(m_bytes, intMemberOffset);
  }
  set
  {
   byte[] bytes = BitConverter.GetBytes(value);
   Buffer.BlockCopy( bytes, 0, m_bytes, intMemberOffset, 4 );
  }
 }
 public string StringMember
 {
  get
  {
   IntPtr ptr = (IntPtr)BitConverter.ToInt32( m_bytes, stringMemberOffset );
   return MarshalEx.PtrToStringUni(ptr);     
  }
  set
  {
   // first see if we already have one allocated
   if(! m_pStringMember.Equals(IntPtr.Zero))
   {
    MarshalEx.FreeHGlobal(m_pStringMember);
   }
   // alloc storage for the string so we can pass the ptr
   m_pStringMember = MarshalEx.StringToHGlobalUni(value);
   // convert to an Int32 and get the bytes
   byte[] bytes = BitConverter.GetBytes( m_pStringMember.ToInt32() );
   // stuff it into the global
   Buffer.BlockCopy( bytes, 0, m_bytes, stringMemberOffset, 4 );    
  }
 }
 public void Dispose()
 {
  // clean up native memory allocations
  if(! m_pStringMember.Equals(IntPtr.Zero))
  {
   MarshalEx.FreeHGlobal(m_pStringMember);
  }
 }
}

See, not so bad, is it?

2/21/2005 10:14:39 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  | 
 Sunday, February 13, 2005

I see requests for CF encryption examples on a regular basis in the newsgroups and the Forums.  Well, here's one I used in an app for storing a password in the registry using the OpenNETCF Encryption stuff :

internal static byte[] GetIV(string keyString)
{
 byte[] bytes = Encoding.Unicode.GetBytes(keyString);
 byte[] iv = new byte[24];
 if(bytes.Length < 24)
 {
  for(int i = 0 ; i < 24 ; i++)
  {
   iv[i] = bytes[i % bytes.Length];
  }
 }
 else
 {
  Array.Copy(bytes, 0, iv, 0, 24);
 }
 return iv;
}
internal static byte[] Encrypt(string toEncrypt, byte[] iv)
{
 TripleDESCryptoServiceProvider des = null;
 try
 {
  des = new TripleDESCryptoServiceProvider();
 }
 catch(Exception)
 {
  MessageBox.Show("The high encryption pack must be installed.  Please install and try again.",
   "Crypto Failure",
   MessageBoxButtons.OK,
   MessageBoxIcon.Exclamation,
   MessageBoxDefaultButton.Button1);
  return null;
 }
 des.Key = PRIVATE_KEY;
 des.IV = iv;
 return des.EncryptValue(Encoding.Unicode.GetBytes(toEncrypt));
}
internal static string Decrypt(byte[] toDecrypt, byte[] iv)
{
 TripleDESCryptoServiceProvider des = null;
 try
 {
  des = new TripleDESCryptoServiceProvider();
 }
 catch(Exception)
 {
  MessageBox.Show("The high encryption pack must be installed.  Please install and try again.",
   "Crypto Failure",
   MessageBoxButtons.OK,
   MessageBoxIcon.Exclamation,
   MessageBoxDefaultButton.Button1);
  return null;
 }
 des.Key = PRIVATE_KEY;
 des.IV = iv;
 byte[] decBytes = des.DecryptValue(toDecrypt);
 return Encoding.Unicode.GetString(decBytes, 0, decBytes.Length);
}
Usage is simple:
byte[] encryptedPwd = AppGlobal.Encrypt(password, GetIV(PUBLIC_KEY));
string  previousPwd = Decrypt(encryptedPwd , GetIV(PUBLIC_KEY));
2/13/2005 7:13:36 AM (Eastern Standard Time, UTC-05:00)  #    Comments [1]  | 
 Wednesday, February 09, 2005

If you've done much with the CF, then you're likely aware that NDOC doen't much like any namespace that's outside of the standard FFW (like the Microsoft.WindowsCE stuff).  While that's been an annoyance for documenting parts of the SDF (leaving holes in the docs), I just ran into a place where it prevents me from generationg any docs - when targeting the TinyCLR.

So, has anyone found anything that can generate decent looking docs without exploding on stuff it doesn't see (or maybe asking for the reference)?  I know NDOC is open source and I could get it working, but I'd rather do actual work.

2/9/2005 11:18:19 PM (Eastern Standard Time, UTC-05:00)  #    Comments [2]  | 
 Tuesday, February 08, 2005

A question came up in todays CF chat about authenticating a CE device to a domain.  While I don't have ready-made CF code for it (though that might be an interesting task) here's a C version:

#include <WINDOWS.H>
#include <SECURITY.H>
#include <SSPI.H>
#define SEC_PACKAGE _T("Microsoft Unified Security Protocol Provider")
typedef struct _AUTH_SEQ {
   BOOL fInitialized;
   BOOL fHaveCredHandle;
   BOOL fHaveCtxtHandle;
   CredHandle hcred;
   struct _SecHandle hctxt;
} AUTH_SEQ, *PAUTH_SEQ;
BOOL GenClientContext(PAUTH_SEQ pAS, PSEC_WINNT_AUTH_IDENTITY pAuthIdentity,
      PVOID pIn, DWORD cbIn, PVOID pOut, PDWORD pcbOut, PBOOL pfDone);
BOOL GenServerContext(PAUTH_SEQ pAS, PVOID pIn, DWORD cbIn, PVOID pOut,
      PDWORD pcbOut, PBOOL pfDone);
BOOL WINAPI SSPLogonUser(LPTSTR szDomain, LPTSTR szUser, LPTSTR szPassword);

TCHAR *lpszUserName = _T("ctacke");
TCHAR *lpszDomainName = _T("mydomain");
TCHAR *lpszPassword = _T("mypassword");

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPWSTR lpCmdLine, int nCmShow)
{
 return SSPLogonUser(lpszDomainName, lpszUserName, lpszPassword);
}
BOOL GenClientContext(PAUTH_SEQ pAS, PSEC_WINNT_AUTH_IDENTITY pAuthIdentity,
      PVOID pIn, DWORD cbIn, PVOID pOut, PDWORD pcbOut, PBOOL pfDone) 
{
 DEBUGMSG(TRUE, (_T("+GenServerContext\r\n")));
 SECURITY_STATUS ss;
 TimeStamp       tsExpiry;
 SecBufferDesc   sbdOut;
 SecBuffer       sbOut;
 SecBufferDesc   sbdIn;
 SecBuffer       sbIn;
 ULONG           fContextAttr;
 if (!pAS->fInitialized) 
 {
  DEBUGMSG(TRUE, (_T("Calling AcquireCredentialsHandle...")));
  ss = AcquireCredentialsHandle(NULL, SEC_PACKAGE, 
   SECPKG_CRED_OUTBOUND, NULL, pAuthIdentity, NULL, NULL,
   &pAS->hcred, &tsExpiry);
  
  if (ss < 0) 
  {
   DEBUGMSG(TRUE, (_T("failed with 0x%08x\r\n"), ss));
   return FALSE;
  }
  DEBUGMSG(TRUE, (_T("ok\r\n")));
  if(ss < 0) 
  {
   fprintf(stderr, "AcquireCredentialsHandle failed with %08X\n", ss);
   return FALSE;
  }
  pAS->fHaveCredHandle = TRUE;
 }
   // Prepare output buffer
   sbdOut.ulVersion = 0;
   sbdOut.cBuffers = 1;
   sbdOut.pBuffers = &sbOut;
   sbOut.cbBuffer = *pcbOut;
   sbOut.BufferType = SECBUFFER_TOKEN;
   sbOut.pvBuffer = pOut;
   // Prepare input buffer
   if (pAS->fInitialized)  {
      sbdIn.ulVersion = 0;
      sbdIn.cBuffers = 1;
      sbdIn.pBuffers = &sbIn;
      sbIn.cbBuffer = cbIn;
      sbIn.BufferType = SECBUFFER_TOKEN;
      sbIn.pvBuffer = pIn;
   }
   ss = InitializeSecurityContext(&pAS->hcred, 
         pAS->fInitialized ? &pAS->hctxt : NULL, NULL, 0, 0, 
         SECURITY_NATIVE_DREP, pAS->fInitialized ? &sbdIn : NULL,
         0, &pAS->hctxt, &sbdOut, &fContextAttr, &tsExpiry);
   if (ss < 0)  { 
      // 
      fprintf(stderr, "InitializeSecurityContext failed with %08X\n", ss);
      return FALSE;
   }
   pAS->fHaveCtxtHandle = TRUE;
   // If necessary, complete token
   if (ss == SEC_I_COMPLETE_NEEDED || ss == SEC_I_COMPLETE_AND_CONTINUE) {
     ss = CompleteAuthToken(&pAS->hctxt, &sbdOut);
     if (ss < 0)  {
        fprintf(stderr, "CompleteAuthToken failed with %08X\n", ss);
        return FALSE;
     }
   }
   *pcbOut = sbOut.cbBuffer;
   if (!pAS->fInitialized)
      pAS->fInitialized = TRUE;
   *pfDone = !(ss == SEC_I_CONTINUE_NEEDED 
         || ss == SEC_I_COMPLETE_AND_CONTINUE );
 DEBUGMSG(TRUE, (_T("-GenServerContext\r\n")));
 return TRUE;
}

/////////////////////////////////////////////////////////////////////////////// 

BOOL GenServerContext(PAUTH_SEQ pAS, PVOID pIn, DWORD cbIn, PVOID pOut,
      PDWORD pcbOut, PBOOL pfDone) 
{
 DEBUGMSG(TRUE, (_T("+GenServerContext\r\n")));
 SECURITY_STATUS ss;
 TimeStamp       tsExpiry;
 SecBufferDesc   sbdOut;
 SecBuffer       sbOut;
 SecBufferDesc   sbdIn;
 SecBuffer       sbIn;
 ULONG           fContextAttr;
 if (!pAS->fInitialized)
 {
  DEBUGMSG(TRUE, (_T("Calling AcquireCredentialsHandle...")));
  ss = AcquireCredentialsHandle(NULL, SEC_PACKAGE, 
   SECPKG_CRED_INBOUND, NULL, NULL, NULL, NULL, &pAS->hcred, 
   &tsExpiry);
  
  if (ss < 0) 
  {
   DEBUGMSG(TRUE, (_T("failed with 0x%08x\r\n"), ss));
   return FALSE;
  }
  DEBUGMSG(TRUE, (_T("ok\r\n")));
  pAS->fHaveCredHandle = TRUE;
 }
 // Prepare output buffer
 sbdOut.ulVersion = 0;
 sbdOut.cBuffers = 1;
 sbdOut.pBuffers = &sbOut;
 sbOut.cbBuffer = *pcbOut;
 sbOut.BufferType = SECBUFFER_TOKEN;
 sbOut.pvBuffer = pOut;
 // Prepare input buffer
 sbdIn.ulVersion = 0;
 sbdIn.cBuffers = 1;
 sbdIn.pBuffers = &sbIn;
 sbIn.cbBuffer = cbIn;
 sbIn.BufferType = SECBUFFER_TOKEN;
 sbIn.pvBuffer = pIn;
 DEBUGMSG(TRUE, (_T("Calling AcceptSecurityContext...")));
 ss = AcceptSecurityContext(&pAS->hcred, 
   pAS->fInitialized ? &pAS->hctxt : NULL, &sbdIn, 0, 
   SECURITY_NATIVE_DREP, &pAS->hctxt, &sbdOut, &fContextAttr, 
   &tsExpiry);
 if (ss < 0) 
 {
  DEBUGMSG(TRUE, (_T("failed with 0x%08x\r\n"), ss));
  return FALSE;
 }
 DEBUGMSG(TRUE, (_T("ok\r\n")));
 pAS->fHaveCtxtHandle = TRUE;
 // If necessary, complete token
 if (ss == SEC_I_COMPLETE_NEEDED || ss == SEC_I_COMPLETE_AND_CONTINUE) 
 {
  DEBUGMSG(TRUE, (_T("Calling CompleteAuthToken...")));
  ss = CompleteAuthToken(&pAS->hctxt, &sbdOut);
  if (ss < 0) 
  {
   DEBUGMSG(TRUE, (_T("failed with 0x%08x\r\n"), ss));
   return FALSE;
  }
  DEBUGMSG(TRUE, (_T("ok\r\n")));
 }
 *pcbOut = sbOut.cbBuffer;
 if (!pAS->fInitialized)
  pAS->fInitialized = TRUE;
 *pfDone = !(ss = SEC_I_CONTINUE_NEEDED 
   || ss == SEC_I_COMPLETE_AND_CONTINUE);
 DEBUGMSG(TRUE, (_T("-GenServerContext\r\n")));
 return TRUE;
}

/////////////////////////////////////////////////////////////////////////////// 

BOOL WINAPI SSPLogonUser(LPTSTR szDomain, LPTSTR szUser, LPTSTR szPassword) 
{
 DEBUGMSG(TRUE, (_T("\r\n+SSPLogonUser\r\n")));
 AUTH_SEQ    asServer   = {0};
 AUTH_SEQ    asClient   = {0};
 BOOL        fDone      = FALSE;
 BOOL        fResult    = FALSE;
 DWORD       cbOut      = 0;
 DWORD       cbIn       = 0;
 DWORD       cbMaxToken = 0;
 PVOID       pClientBuf = NULL;
 PVOID       pServerBuf = NULL;
 SecPkgInfo *pSPI       = NULL;
 HMODULE     hModule    = NULL;
 SECURITY_STATUS ss;
 SEC_WINNT_AUTH_IDENTITY ai;
 ULONG packages = 0;
 PSecPkgInfo pPackageInfo = NULL;
 __try 
 {
  EnumerateSecurityPackages(&packages, &pPackageInfo);
  DEBUGMSG(TRUE, (_T("  Available security packages:\r\n")));
  for(UINT i = 0 ; i < packages ; i++)
  {
   DEBUGMSG(TRUE, (_T("\t%s\r\n"), pPackageInfo[i].Name));
  }
  DEBUGMSG(TRUE, (_T("\r\n")));

  DEBUGMSG(TRUE, (_T("Calling FreeContextBuffer...")));
  ss =FreeContextBuffer(pPackageInfo);
  if(ss != SEC_E_OK)
  {
   DEBUGMSG(TRUE, (_T("failed with 0x%08x\r\n"), ss));
   __leave;
  }
  DEBUGMSG(TRUE, (_T("ok\r\n")));
  // Get max token size
  DEBUGMSG(TRUE, (_T("Calling QuerySecurityPackageInfo...")));
  ss = QuerySecurityPackageInfo(SEC_PACKAGE, &pSPI);
  if(ss != SEC_E_OK)
  {
   DEBUGMSG(TRUE, (_T("failed with 0x%08x\r\n"), ss));
   __leave;
  }
  DEBUGMSG(TRUE, (_T("ok\r\n")));
  cbMaxToken = pSPI->cbMaxToken;
  DEBUGMSG(TRUE, (_T("Calling FreeContextBuffer...")));
  ss =FreeContextBuffer(pSPI);
  if(ss != SEC_E_OK)
  {
   DEBUGMSG(TRUE, (_T("failed with 0x%08x\r\n"), ss));
   __leave;
  }
  DEBUGMSG(TRUE, (_T("ok\r\n")));
  // Allocate buffers for client and server messages
  DEBUGMSG(TRUE, (_T("Allocating heaps...")));
  pClientBuf = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cbMaxToken);
  pServerBuf = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cbMaxToken);
  DEBUGMSG(TRUE, (_T("ok\r\n")));
  // Initialize auth identity structure
  ZeroMemory(&ai, sizeof(ai));
  ai.Domain = szDomain;
  ai.DomainLength = _tcslen(szDomain);
  ai.User = szUser;
  ai.UserLength = _tcslen(szUser);
  ai.Password = szPassword;
  ai.PasswordLength = _tcslen(szPassword);
  ai.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
  // Prepare client message (negotiate) .
  cbOut = cbMaxToken;
  DEBUGMSG(TRUE, (_T("Calling GenClientContext\r\n")));
  if (!GenClientContext(&asClient, &ai, NULL, 0, pClientBuf, &cbOut, &fDone))
   __leave;
  // Prepare server message (challenge) .
  cbIn = cbOut;
  cbOut = cbMaxToken;
  DEBUGMSG(TRUE, (_T("Calling GenServerContext\r\n")));
  if (!GenServerContext(&asServer, pClientBuf, cbIn, pServerBuf, &cbOut, &fDone))
   __leave;
  // Most likely failure: AcceptServerContext fails with SEC_E_LOGON_DENIED
   // in the case of bad szUser or szPassword.
   // Unexpected Result: Logon will succeed if you pass in a bad szUser and 
   // the guest account is enabled in the specified domain.
  // Prepare client message (authenticate) .
  cbIn = cbOut;
  cbOut = cbMaxToken;
  DEBUGMSG(TRUE, (_T("Calling GenClientContext\r\n")));
  if (!GenClientContext(&asClient, &ai, pServerBuf, cbIn, pClientBuf, &cbOut, &fDone))
   __leave;
  // Prepare server message (authentication) .
  cbIn = cbOut;
  cbOut = cbMaxToken;
  DEBUGMSG(TRUE, (_T("Calling GenServerContext\r\n")));
  if (!GenServerContext(&asServer, pClientBuf, cbIn, pServerBuf, &cbOut, &fDone))
   __leave;
  fResult = TRUE;
 } 
 __finally 
 {
  // Clean up resources
  if (asClient.fHaveCtxtHandle)
  {
   DEBUGMSG(TRUE, (_T("Calling DeleteSecurityContext\r\n")));
   DeleteSecurityContext(&asClient.hctxt);
  }
  if (asClient.fHaveCredHandle)
  {
   DEBUGMSG(TRUE, (_T("Calling FreeCredentialsHandle\r\n")));
   FreeCredentialsHandle(&asClient.hcred);
  }
  if (asServer.fHaveCtxtHandle)
  {
   DEBUGMSG(TRUE, (_T("Calling DeleteSecurityContext\r\n")));
   DeleteSecurityContext(&asServer.hctxt);
  }
  if (asServer.fHaveCredHandle)
  {
   DEBUGMSG(TRUE, (_T("Calling FreeCredentialsHandle\r\n")));
   FreeCredentialsHandle(&asServer.hcred);
  }
  DEBUGMSG(TRUE, (_T("Freeing heaps...")));
  HeapFree(GetProcessHeap(), 0, pClientBuf);
  HeapFree(GetProcessHeap(), 0, pServerBuf);
  DEBUGMSG(TRUE, (_T("ok\r\n")));
 }
 DEBUGMSG(TRUE, (_T("-SSPLogonUser\r\n")));
 return fResult;
}
2/8/2005 1:29:26 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  | 
 Friday, February 04, 2005

So I started a amanged wrapper for Pocket Excel some time ago - probably a year and a half now.  I made good progress - got it so you can real and write workbooks, worksheets, cells - just no support for the formulas yet.  Then I got sidetracked with one thing after another. 

Initially I was going to use it as a commercial product to get revenue to support OpenNETCF, but I honestly don't have the time for it.  If I can find someone, or a group of someones willing to put in a final push to finish it (maybe an industrious student looking for thesis material?) I'll donate it to the community at large as an OpenNETCF namespace.

Any takers?

2/4/2005 9:07:39 PM (Eastern Standard Time, UTC-05:00)  #    Comments [1]  |