Skip to content

Commit

Permalink
Crx file support added (only for reading)
Browse files Browse the repository at this point in the history
  • Loading branch information
NilsHoyer committed Feb 17, 2024
1 parent ab5535e commit 130dfc3
Show file tree
Hide file tree
Showing 3 changed files with 340 additions and 0 deletions.
126 changes: 126 additions & 0 deletions reference/CRX Package Format.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<h1>CRX Package Format</h1>
<p>
CRX files are ZIP files with a special header and the <code>.crx</code> file
extension.
</p>
<h2 id="package_header">Package header</h2>
<p>
The header contains the author's public key and the extension's signature.
The signature is generated from the ZIP file using SHA-1 with the
author's private key. The header requires a little-endian byte ordering with
4-byte alignment. The following table describes the fields of
the <code>.crx</code> header in order:
</p>
<table class="simple">
<tr>
<th>Field</th><th>Type</th><th>Length</th><th>Value</th><th>Description</th>
</tr>
<tr>
<td><em>magic number</em></td><td>char[]</td><td>32 bits</td><td>Cr24</td>
<td>
Chrome requires this constant at the beginning of every <code>.crx</code>
package.
</td>
</tr>
<tr>
<td><em>version</em></td><td>unsigned&nbsp;int</td><td>32 bits</td><td>2</td>
<td>The version of the <code>*.crx</code> file format used (currently 2).</td>
</tr>
<tr>
<td><em>public key length</em></td><td>unsigned&nbsp;int</td><td>32 bits</td>
<td><i>pubkey.length</i></td>
<td>
The length of the RSA public key in <em>bytes</em>.
</td>
</tr>
<tr>
<td><em>signature length</em></td><td>unsigned&nbsp;int</td><td>32 bits</td>
<td><i>sig.length</i></td>
<td>
The length of the signature in <em>bytes</em>.
</td>
</tr>
<tr>
<td><em>public key</em></td><td>byte[]</td><td><i>pubkey.length</i></i></td>
<td><i>pubkey.contents</i></td>
<td>
The contents of the author's RSA public key, formatted as an X509
SubjectPublicKeyInfo block.
</td>
</tr>
<tr>
<td><em>signature</em></td><td>byte[]</td><td><i>sig.length</i></td>
<td><i>sig.contents</i></td>
<td>
The signature of the ZIP content using the author's private key. The
signature is created using the RSA algorithm with the SHA-1 hash function.
</td>
</tr>
</table>
<h2 id="extensions_contents">Extension contents</h2>
<p>
The extension's ZIP file is appended to the <code>*.crx</code> package after the
header. This should be the same ZIP file that the signature in the header
was generated from.
</p>
<h2 id="example">Example</h2>
<p>
The following is an example hex dump from the beginning of a <code>.crx</code>
file.
</p>
<pre>
43 72 32 34 # "Cr24" -- the magic number
02 00 00 00 # 2 -- the crx format version number
A2 00 00 00 # 162 -- length of public key in bytes
80 00 00 00 # 128 -- length of signature in bytes
........... # the contents of the public key
........... # the contents of the signature
........... # the contents of the zip file
</pre>
<h2 id="scripts">Packaging scripts</h2>
<p>
Members of the community have written the following scripts to package
<code>.crx</code> files.
</p>
<h3 id="ruby">Ruby</h3>
<blockquote>
<a href="http://github.com/Constellation/crxmake">github: crxmake</a>
</blockquote>
<h3 id="bash">Bash</h3>
<pre>
#!/bin/bash -e
#
# Purpose: Pack a Chromium extension directory into crx format
if test $# -ne 2; then
echo "Usage: crxmake.sh &lt;extension dir&gt; &lt;pem path&gt;"
exit 1
fi
dir=$1
key=$2
name=$(basename "$dir")
crx="$name.crx"
pub="$name.pub"
sig="$name.sig"
zip="$name.zip"
trap 'rm -f "$pub" "$sig" "$zip"' EXIT
# zip up the crx dir
cwd=$(pwd -P)
(cd "$dir" && zip -qr -9 -X "$cwd/$zip" .)
# signature
openssl sha1 -sha1 -binary -sign "$key" < "$zip" > "$sig"
# public key
openssl rsa -pubout -outform DER < "$key" > "$pub" 2>/dev/null
byte_swap () {
# Take "abcdefgh" and return it as "ghefcdab"
echo "${1:6:2}${1:4:2}${1:2:2}${1:0:2}"
}
crmagic_hex="4372 3234" # Cr24
version_hex="0200 0000" # 2
pub_len_hex=$(byte_swap $(printf '%08x\n' $(ls -l "$pub" | awk '{print $5}')))
sig_len_hex=$(byte_swap $(printf '%08x\n' $(ls -l "$sig" | awk '{print $5}')))
(
echo "$crmagic_hex $version_hex $pub_len_hex $sig_len_hex" | xxd -r -p
cat "$pub" "$sig" "$zip"
) > "$crx"
echo "Wrote $crx"
</pre>
52 changes: 52 additions & 0 deletions reference/Crx3.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
syntax = "proto2";
option optimize_for = LITE_RUNTIME;
package crx_file;
// A CRX₃ file is a binary file of the following format:
// [4 octets]: "Cr24", a magic number.
// [4 octets]: The version of the *.crx file format used (currently 3).
// [4 octets]: N, little-endian, the length of the header section.
// [N octets]: The header (the binary encoding of a CrxFileHeader).
// [M octets]: The ZIP archive.
// Clients should reject CRX₃ files that contain an N that is too large for the
// client to safely handle in memory.
message CrxFileHeader {
// PSS signature with RSA public key. The public key is formatted as a
// X.509 SubjectPublicKeyInfo block, as in CRX₂. In the common case of a
// developer key proof, the first 128 bits of the SHA-256 hash of the
// public key must equal the crx_id.
repeated AsymmetricKeyProof sha256_with_rsa = 2;
// ECDSA signature, using the NIST P-256 curve. Public key appears in
// named-curve format.
// The pinned algorithm will be this, at least on 2017-01-01.
repeated AsymmetricKeyProof sha256_with_ecdsa = 3;
// A verified contents file containing signatures over the archive contents.
// The verified contents are encoded in UTF-8 and then GZIP-compressed.
// Consult
// https://source.chromium.org/chromium/chromium/src/+/main:extensions/browser/verified_contents.h
// for information about the verified contents format.
optional bytes verified_contents = 4;
// The binary form of a SignedData message. We do not use a nested
// SignedData message, as handlers of this message must verify the proofs
// on exactly these bytes, so it is convenient to parse in two steps.
//
// All proofs in this CrxFile message are on the value
// "CRX3 SignedData\x00" + signed_header_size + signed_header_data +
// archive, where "\x00" indicates an octet with value 0, "CRX3 SignedData"
// is encoded using UTF-8, signed_header_size is the size in octets of the
// contents of this field and is encoded using 4 octets in little-endian
// order, signed_header_data is exactly the content of this field, and
// archive is the remaining contents of the file following the header.
optional bytes signed_header_data = 10000;
}
message AsymmetricKeyProof {
optional bytes public_key = 1;
optional bytes signature = 2;
}
message SignedData {
// This is simple binary, not UTF-8 encoded mpdecimal; i.e. it is exactly
// 16 bytes long.
optional bytes crx_id = 1;
}
162 changes: 162 additions & 0 deletions src/SharpCompress/Archives/Crx/CrxArchive.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SharpCompress.Common;
using SharpCompress.Common.Zip;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.Compressors.Deflate;
using SharpCompress.IO;
using SharpCompress.Readers;
using SharpCompress.Readers.Zip;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;
using SharpCompress.Archives.Zip;
using System.Text;
using System.Reflection;

namespace SharpCompress.Archives.Crx;

public class CrxArchive : ZipArchive
{
private const int MINIMUM_CRX_HEADER_LENGTH = 16;

private string _tempFilename;
private FileStream _fileStream;

/// <summary>
/// Constructor with a SourceStream able to handle SourceStreams.
/// </summary>
/// <param name="srcStream"></param>
internal CrxArchive(SourceStream srcStream, FileStream fileStream, string tempFilename)
: base(srcStream)
{
_fileStream = fileStream;
_tempFilename = tempFilename;
}

public override void Dispose()
{
_fileStream.Dispose();

File.Delete(_tempFilename);

base.Dispose();
}

/// <summary>
/// Constructor expects a filepath to an existing file.
/// </summary>
/// <param name="filePath"></param>
/// <param name="readerOptions"></param>
public new static ZipArchive Open(string filePath, ReaderOptions? readerOptions = null)
{
filePath.CheckNotNullOrEmpty(nameof(filePath));

var stream = File.Open(filePath, FileMode.Open);

return Open(stream, readerOptions);
}

/// <summary>
/// Constructor with a FileInfo object to an existing file.
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="readerOptions"></param>
public new static ZipArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.CheckNotNull(nameof(fileInfo));
return Open(fileInfo.FullName, readerOptions);
}

/// <summary>
/// Takes a seekable Stream as a source
/// </summary>
/// <param name="stream"></param>
/// <param name="readerOptions"></param>
public new static CrxArchive Open(Stream stream, ReaderOptions? readerOptions = null)
{
stream.CheckNotNull(nameof(stream));


if (stream.Length < MINIMUM_CRX_HEADER_LENGTH)
{
throw new ArchiveException(
"Could not find Crx file header at the begin of the file. File may be corrupted."
);
}

var buffer = new byte[4];
stream.Read(buffer, 0, buffer.Length);
if (Encoding.ASCII.GetString(buffer) != "Cr24")
throw new ArchiveException("Invalid Crx file header");

stream.Read(buffer, 0, buffer.Length);
var version = BitConverter.ToUInt32(buffer, 0);
if (version != 3)
throw new ArchiveException(string.Format("Invalid Crx version ({0}). Only Crx version 3 is supported.", version));

stream.Read(buffer, 0, buffer.Length);
var headerLength = BitConverter.ToUInt32(buffer, 0);
if (stream.Length < stream.Position + headerLength)
throw new ArchiveException(string.Format("Invalid Crx header length ({0}).", headerLength));

stream.Seek(headerLength, SeekOrigin.Current);


var tempFilename = Path.GetTempFileName();
File.Delete(tempFilename);

var fileStream = File.Open(tempFilename, FileMode.Create);
stream.CopyTo(fileStream);
fileStream.Seek(0, SeekOrigin.Begin);


return new CrxArchive(
new SourceStream(fileStream, i => null, readerOptions ?? new ReaderOptions()),
fileStream,
tempFilename
);
}

public static bool IsCrxFile(string filePath, string? password = null) =>
IsCrxFile(new FileInfo(filePath), password);

public static bool IsCrxFile(FileInfo fileInfo, string? password = null)
{
if (!fileInfo.Exists)
{
return false;
}
using Stream stream = fileInfo.OpenRead();
return IsCrxFile(stream, password);
}

public static bool IsCrxFile(Stream stream, string? password = null)
{
var headerFactory = new StreamingZipHeaderFactory(password, new ArchiveEncoding(), null);
try
{
var header = headerFactory
.ReadStreamHeader(stream)
.FirstOrDefault(x => x.ZipHeaderType != ZipHeaderType.Split);
if (header is null)
{
return false;
}
return Enum.IsDefined(typeof(ZipHeaderType), header.ZipHeaderType);
}
catch (CryptographicException)
{
return true;
}
catch
{
return false;
}
}
protected override IEnumerable<ZipVolume> LoadVolumes(SourceStream srcStream)
{
return new ZipVolume(SrcStream, ReaderOptions, 0).AsEnumerable();
}
}

0 comments on commit 130dfc3

Please sign in to comment.