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.

Comments

Anonymous

<p>This seems to be precisely what I'm after!<br><br>Unfortunately, I'm no .NET C# coder.<br>All I need is a command line executable to be called<br><i>foo.exe bar.crx</i><br>which writes the extension's id to stdout (or a file, respectively).<br><br>Do you happen to have a compiled binary (x86/x64)?<br>That would be marvellous!</p>

Anonymous

Anonymous

This seems to be precisely what I&#39;m after!<br><br>Unfortunately, I&#39;m no .NET C# coder.<br>All I need is a command line executable to be called<br><i>foo.exe bar.crx</i><br>which writes the extension&#39;s id to stdout (or a file, respectively).<br><br>Do you happen to have a compiled binary (x86/x64)?<br>That would be marvellous!

Anonymous

Siderite

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

Siderite

Tim

<p>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.</p>

Tim

Siderite

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

Siderite

Tim

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.

Tim

Post a comment