Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crx file support added (only for reading) #810

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
}
Loading