Tuesday, November 23, 2010

Compute Google Chrome Extension IDs in .NET C#

In order to programmatically load a .crx Chrome extension as an external extension, the ID of the extension is required. The algorithm for computing it is:
  1. make a SHA256 hash of the public key embedded in the .crx file
  2. take the first 128bits (16 bytes) and encode them in base16
  3. use characters a-p instead of the customary 0-9,A-F


For this we need, obviously, the public key. Reading from the CRX Package Format page, we can determine we need a 4 byte (Int32) value of the public key length and the public key itself. The length is found at position 8 in the file, the public key starts at position 16. Here is the code:

private byte[] getPublicKey(FileInfo fi)
{
using (
FileStream stream = File.Open(fi.FullName,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite))
{
byte[] arr = new byte[4];
stream.Seek(8, SeekOrigin.Begin);
stream.Read(arr, 0, arr.Length);
var publicKeyLength = BitConverter.ToInt32(arr, 0);
arr = new byte[publicKeyLength];
stream.Seek(16, SeekOrigin.Begin);
stream.Read(arr, 0, arr.Length);
return arr;
}
}


The code to create the id is now simple:

private string getExtensionId(byte[] publicKey)
{
SHA256 sha = SHA256.Create();
publicKey = sha.ComputeHash(publicKey);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 16; i++)
{
byte b = publicKey[i];
char ch = (char)('a' + (b >> 4));
sb.Append(ch);
ch = (char)('a' + (b & 0xF));
sb.Append(ch);
}
return sb.ToString();
}


Just in case you want to get a complete class that handles .crx files, here it is:

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Web.Script.Serialization;
using SevenZip;

namespace ChromeExtensionInstaller
{
internal class CrxPack
{
#region Instance fields

private byte[] mContent;
private SevenZipExtractor mExtractor;
private dynamic mManifest;
private Uri mUri;
private string mPath;

#endregion

#region Properties

public Exception InvalidReason
{
get;
private set;
}

public bool IsValid
{
get;
private set;
}

private dynamic Manifest
{
get
{
if (mManifest == null)
{
FileInfo fi = new FileInfo(mUri.AbsolutePath);
mManifest = getManifest(fi);
}
return mManifest;
}
}

public string Id
{
get
{
return getExtensionID();
}
}

public string Name
{
get
{
return Manifest.name as string;
}
}

public string Version
{
get
{
return Manifest.version as string;
}
}

public string Path
{
get
{
return mPath;
}
}

#endregion

#region Constructors

public CrxPack(string path)
{
mPath = path;
try
{
checkPath(path);
IsValid = true;
}
catch (Exception ex)
{
IsValid = false;
InvalidReason = ex;
}
}

#endregion

#region Private Methods

private void checkPath(string path)
{
mUri = ExtensionHelper.GetUri(path);
if (mUri == null)
{
throw new Exception(string.Format("Parameter is not a valid URI ({0})", mPath));
}
mPath = mUri.AbsolutePath;
if (!mUri.IsFile && !mUri.IsUnc)
{
throw new Exception(string.Format("Only file and local network paths are acceptable ({0})",
mPath));
}
DirectoryInfo di = new DirectoryInfo(mPath);
if (di.Exists)
{
throw new Exception(string.Format(
"Loading extensions from folders is not implemented ({0})", mPath));
}
FileInfo fi = new FileInfo(mPath);
if (!fi.Exists)
{
throw new Exception(string.Format("The file does not exist ({0})", mPath));
}
if (fi.Extension.ToLower() != ".crx")
{
throw new Exception(string.Format("The file extension must be a .crx file ({0})", mPath));
}
try
{
mExtractor = getExtractor(fi);
if (mExtractor.Check())
{
return;
}
}
catch (Exception ex)
{
throw new Exception(
string.Format("The file could not be read as a valid .crx file ({0})", mPath), ex);
}
throw new Exception(string.Format("The file could not be read as a valid .crx file ({0})",
mPath));
}


private SevenZipExtractor getExtractor(FileInfo fi)
{
byte[] arr;
using (
FileStream stream = File.Open(fi.FullName, FileMode.Open, FileAccess.Read,
FileShare.ReadWrite))
{
arr = new byte[fi.Length];
mContent = arr;
stream.Read(arr, 0, arr.Length);
}
// force PkZip signature
arr[0] = 0x50;
arr[1] = 0x4B;
arr[2] = 0x03;
arr[3] = 0x04;
MemoryStream ms = new MemoryStream(arr);
return new SevenZipExtractor(ms);
}

private string getExtensionID()
{
int length = readInt(8);
byte[] bytes = readBytes(16, length);
SHA256 sha = SHA256.Create();
bytes = sha.ComputeHash(bytes);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 16; i++)
{
byte b = bytes[i];
char ch = (char) ('a' + (b >> 4));
sb.Append(ch);
ch = (char) ('a' + (b & 0xF));
sb.Append(ch);
}
return sb.ToString();
}

private int readInt(int index)
{
byte[] bytes = readBytes(index, 4);
return BitConverter.ToInt32(bytes, 0);
}

private byte[] readBytes(int index, int length)
{
byte[] bytes = new byte[length];
Array.Copy(mContent, index, bytes, 0, length);
return bytes;
}

private object getManifest(FileInfo fi)
{
SevenZipExtractor extractor = getExtractor(fi);
string json;
using (MemoryStream ms = new MemoryStream())
{
extractor.ExtractFile("manifest.json", ms);
ms.Seek(0, SeekOrigin.Begin);
StreamReader sr = new StreamReader(ms);
json = sr.ReadToEnd();
}
JavaScriptSerializer serializer = new JavaScriptSerializer();
serializer.RegisterConverters(new[] {new DynamicJsonConverter()});
return serializer.Deserialize(json, typeof (object));
}

#endregion
}
}


You need to reference the SevenZipSharp library and place 7z.dll (from the 7-Zip archiver) in the same folder with the application using this class.

3 comments:

Tim said...

You are a champ. Thank you so much for writing this up. I was about to embark on exactly this journey when I found your post.

Siderite said...

You are welcome. Please share of your experiences when you are done or suggest any modification to the post, if necessary.

Anonymous said...

This seems to be precisely what I'm after!

Unfortunately, I'm no .NET C# coder.
All I need is a command line executable to be called
foo.exe bar.crx
which writes the extension's id to stdout (or a file, respectively).

Do you happen to have a compiled binary (x86/x64)?
That would be marvellous!