-
Notifications
You must be signed in to change notification settings - Fork 485
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Crx file support added (only for reading)
- Loading branch information
NilsHoyer
committed
Feb 17, 2024
1 parent
ab5535e
commit 130dfc3
Showing
3 changed files
with
340 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 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 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 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 <extension dir> <pem path>" | ||
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |