├── testdata ├── emptydir │ └── .keep ├── extension │ ├── background.js │ ├── images │ │ └── image.jpeg │ └── manifest.json ├── .DS_Store ├── fake.zip ├── bobbyMol.zip ├── dodyDol.crx ├── withkey.crx ├── withkey.zip └── withkey.crx.pem ├── examples ├── extension_id │ ├── extension.crx │ └── main.go ├── pack_to_file │ ├── bobbyMol.zip │ └── main.go ├── pack_to_memory │ ├── bobbyMol.zip │ └── main.go └── write_private_key_to_file │ └── main.go ├── scripts └── protoc.bash ├── Dockerfile ├── .gitignore ├── crx3 ├── commands │ ├── version.go │ ├── id.go │ ├── command.go │ ├── unpack.go │ ├── zip.go │ ├── unzip.go │ ├── keygen.go │ ├── pack.go │ ├── base64.go │ ├── download.go │ └── pubkey.go └── main.go ├── go.mod ├── errors.go ├── base64_test.go ├── Makefile ├── .github └── workflows │ ├── release.yaml │ └── cover.yml ├── base64.go ├── docker └── protoc.dockerfile ├── copyfile.go ├── unzip_test.go ├── download.go ├── zip.go ├── keys_test.go ├── os.go ├── unzip.go ├── pb ├── crx3.proto └── crx3.pb.go ├── unpack.go ├── download_test.go ├── unpack_test.go ├── go.sum ├── keys.go ├── id.go ├── zip_test.go ├── id_test.go ├── pack_test.go ├── README.md ├── extension.go ├── pack.go ├── extension_test.go └── LICENSE /testdata/emptydir/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/extension/background.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmadfox/go-crx3/HEAD/testdata/.DS_Store -------------------------------------------------------------------------------- /testdata/fake.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmadfox/go-crx3/HEAD/testdata/fake.zip -------------------------------------------------------------------------------- /testdata/bobbyMol.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmadfox/go-crx3/HEAD/testdata/bobbyMol.zip -------------------------------------------------------------------------------- /testdata/dodyDol.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmadfox/go-crx3/HEAD/testdata/dodyDol.crx -------------------------------------------------------------------------------- /testdata/withkey.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmadfox/go-crx3/HEAD/testdata/withkey.crx -------------------------------------------------------------------------------- /testdata/withkey.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmadfox/go-crx3/HEAD/testdata/withkey.zip -------------------------------------------------------------------------------- /examples/extension_id/extension.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmadfox/go-crx3/HEAD/examples/extension_id/extension.crx -------------------------------------------------------------------------------- /examples/pack_to_file/bobbyMol.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmadfox/go-crx3/HEAD/examples/pack_to_file/bobbyMol.zip -------------------------------------------------------------------------------- /examples/pack_to_memory/bobbyMol.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmadfox/go-crx3/HEAD/examples/pack_to_memory/bobbyMol.zip -------------------------------------------------------------------------------- /testdata/extension/images/image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmadfox/go-crx3/HEAD/testdata/extension/images/image.jpeg -------------------------------------------------------------------------------- /scripts/protoc.bash: -------------------------------------------------------------------------------- 1 | PWD="$(pwd)" 2 | 3 | docker run \ 4 | --rm -v $PWD:$PWD -w $PWD github.com/mediabuyerbot/go-crx3/protoc --go_out=paths=source_relative:. ./pb/crx3.proto -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21 AS builder 2 | LABEL stage=builder 3 | WORKDIR /app 4 | COPY . . 5 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=mod -ldflags="-w -s" ./crx3/main.go 6 | 7 | FROM scratch 8 | COPY --from=builder /app/main /app/crx3 9 | ENTRYPOINT ["/app/crx3"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | tmp 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /crx3/commands/version.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func newVersionCmd(version string) *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "version", 12 | Short: "Print the version of the crx3 tools", 13 | Run: func(cmd *cobra.Command, _ []string) { 14 | fmt.Printf("crx3 tools version %s\n", version) 15 | }, 16 | } 17 | return cmd 18 | } 19 | -------------------------------------------------------------------------------- /crx3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/mediabuyerbot/go-crx3/crx3/commands" 8 | ) 9 | 10 | // TODO: add to ci VERSION=$(git describe --tags --always) ... -ldflags "-X main.Version=$VERSION" 11 | var Version = "v1.6.0" 12 | 13 | func main() { 14 | cli := commands.New(Version) 15 | if err := cli.Execute(); err != nil { 16 | fmt.Println(err) 17 | os.Exit(1) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mediabuyerbot/go-crx3 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/spf13/cobra v1.7.0 7 | github.com/stretchr/testify v1.8.0 8 | google.golang.org/protobuf v1.31.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | github.com/spf13/pflag v1.0.5 // indirect 16 | gopkg.in/yaml.v3 v3.0.1 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrUnknownFileExtension = errors.New("crx3: unknown file extension") 7 | ErrUnsupportedFileFormat = errors.New("crx3: unsupported file format") 8 | ErrExtensionNotSpecified = errors.New("crx3: extension id not specified") 9 | ErrPathNotFound = errors.New("crx3: filepath not found") 10 | ErrPrivateKeyNotFound = errors.New("crx3: private key not found") 11 | ErrInvalidReader = errors.New("crx3: invalid reader") 12 | ) 13 | -------------------------------------------------------------------------------- /examples/extension_id/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/mediabuyerbot/go-crx3" 9 | ) 10 | 11 | func main() { 12 | pwd, err := os.Getwd() 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | filename := filepath.Join(pwd, "examples", "extension_id", "extension.crx") 18 | ext := crx3.Extension(filename) 19 | 20 | id, err := ext.ID() 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | fmt.Printf("Extension ID: %s\n", id) 26 | } 27 | -------------------------------------------------------------------------------- /testdata/extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "go-crx3", 4 | "version": "999", 5 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAngWK1vsGK7o9HK7ZzSBG56+nVMg3AVqeBpTY5DaGnHyryg6Ir693a1KQ/5S3MnEBD8+bb1jnQpMOiQyndmLg6DjI7xPkVskljNt/j8I9124NseR5zjZXVsQGPW6LDDpVTHC+PUT0KkXCO+X3h8x2Inh7p7joR+1vLo/Ur9eRdjw/p/zAtxCYnWrw1Vm3CVSLCr3CqatJ0Jwyw00ANY6k5ebYHwKM9NVgsRozQX1OIPjWwGxHcj+XUQseqyfWa7XGlXgopom62ptkq7CVjgG5f7SCaoHEVyC1J8gsnN/wSJSB/m6JL8VQVFVIRQdWLMC4DLqxxiEy9aADKTM2smaAVwIDAQAB" 6 | } 7 | -------------------------------------------------------------------------------- /base64_test.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestBase64(t *testing.T) { 10 | b, err := Base64("./testdata/dodyDol.crx") 11 | assert.Nil(t, err) 12 | assert.NotEmpty(t, b) 13 | 14 | b, err = Base64("./testdata/bobbyMol.zip") 15 | assert.Error(t, err) 16 | assert.Nil(t, b) 17 | 18 | extension, err := openCrxFile("./testdata/dodyDol.crx") 19 | assert.Nil(t, err) 20 | err = extension.Close() 21 | assert.Nil(t, err) 22 | b, err = encodeExtensionToBase64Str(extension) 23 | assert.Error(t, err) 24 | assert.Nil(t, b) 25 | } 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: deps test mocks cover sync-coveralls docker-protoc proto 2 | 3 | deps: 4 | go mod download 5 | 6 | test/cover: deps 7 | go test -coverprofile=coverage.out `go list ./... | grep -v pb` 8 | go tool cover -func=coverage.out 9 | go tool cover -html=coverage.out 10 | 11 | coveralls: deps 12 | go test -coverprofile=coverage.out `go list ./... | grep -v pb` 13 | goveralls -coverprofile=coverage.out -reponame=go-webdriver -repotoken=${COVERALLS_GO_CRX3_TOKEN} -service=local 14 | 15 | docker-protoc: 16 | @docker build -t github.com/mediabuyerbot/go-crx3/protoc:latest -f \ 17 | ./docker/protoc.dockerfile . 18 | 19 | proto: docker-protoc 20 | @bash ./scripts/protoc.bash -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release Go project 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | name: GoReleaser build 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out code into the Go module directory 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set up Go 1.22 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.22 23 | id: go 24 | 25 | - name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@master 27 | with: 28 | version: latest 29 | args: release 30 | workdir: ./crx3 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/cover.yml: -------------------------------------------------------------------------------- 1 | name: Quality 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | name: Test with Coverage 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Set up Go 9 | uses: actions/setup-go@v2 10 | with: 11 | go-version: '1.21' 12 | - name: Check out code 13 | uses: actions/checkout@v2 14 | - name: Install dependencies 15 | run: | 16 | go mod download 17 | - name: Run Unit tests 18 | run: | 19 | go test -race -covermode atomic -coverprofile=covprofile ./... 20 | - name: Install goveralls 21 | run: go install github.com/mattn/goveralls@latest 22 | - name: Send coverage 23 | env: 24 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | run: goveralls -coverprofile=covprofile -service=github -------------------------------------------------------------------------------- /base64.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "io" 7 | ) 8 | 9 | // Base64 encodes an extension file to a base64 string. 10 | // It returns a bytes and an error encountered while encodes, if any. 11 | func Base64(filename string) (b []byte, err error) { 12 | extension, err := openCrxFile(filename) 13 | if err != nil { 14 | return nil, err 15 | } 16 | defer extension.Close() 17 | 18 | return encodeExtensionToBase64Str(extension) 19 | } 20 | 21 | func encodeExtensionToBase64Str(file io.Reader) ([]byte, error) { 22 | var buf bytes.Buffer 23 | encoder := base64.NewEncoder(base64.StdEncoding, &buf) 24 | if _, err := io.Copy(encoder, file); err != nil { 25 | return nil, err 26 | } 27 | if err := encoder.Close(); err != nil { 28 | return nil, err 29 | } 30 | return buf.Bytes(), nil 31 | } 32 | -------------------------------------------------------------------------------- /examples/write_private_key_to_file/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/mediabuyerbot/go-crx3" 9 | ) 10 | 11 | func main() { 12 | // 1. Generate a new private key OR load from file 13 | pk, err := crx3.NewPrivateKey() 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | pwd, err := os.Getwd() 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | pf := filepath.Join(pwd, "examples", "write_private_key_to_file", "private.pem") 24 | file, err := os.OpenFile(pf, os.O_CREATE|os.O_WRONLY, 0600) 25 | if err != nil { 26 | panic(err) 27 | } 28 | defer file.Close() 29 | 30 | // 2. Save the private key to a file 31 | if err := crx3.WritePrivateKey(file, pk); err != nil { 32 | panic(err) 33 | } 34 | 35 | fmt.Printf("Private key has been written to the file: %s\n", file.Name()) 36 | } 37 | -------------------------------------------------------------------------------- /docker/protoc.dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21 AS builder 2 | 3 | ENV PROTOC_VERSION "3.17.3" 4 | ENV PROTOC_GEN_GO_VERSION "1.5.2" 5 | 6 | RUN apt-get update -yqq && \ 7 | apt-get install -yqq curl git unzip 8 | 9 | RUN curl -sfLo protoc.zip "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip" && \ 10 | mkdir protoc && \ 11 | unzip -q -d protoc protoc.zip 12 | 13 | RUN git clone -q https://github.com/golang/protobuf && \ 14 | cd protobuf && \ 15 | git checkout -q tags/v${PROTOC_GEN_GO_VERSION} -b build && \ 16 | go build -o /go/bin/protoc-gen-go ./protoc-gen-go 17 | 18 | FROM debian:buster-slim 19 | COPY --from=builder /go/protoc/include/google /usr/local/include/google 20 | COPY --from=builder /go/protoc/bin/protoc /usr/local/bin/protoc 21 | COPY --from=builder /go/bin/protoc-gen-go /usr/local/bin/protoc-gen-go 22 | ENTRYPOINT ["/usr/local/bin/protoc"] -------------------------------------------------------------------------------- /copyfile.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // CopyFile copies the contents of the source file 'src' to the destination file 'dst'. 10 | // It returns the number of bytes copied and any encountered error. 11 | // If the source file does not exist, is not a regular file, or other errors occur during the copy, 12 | // it returns an error with a descriptive message. 13 | func CopyFile(src, dst string) (int64, error) { 14 | sourceFileStat, err := os.Stat(src) 15 | if err != nil { 16 | return 0, err 17 | } 18 | 19 | if !sourceFileStat.Mode().IsRegular() { 20 | return 0, fmt.Errorf("%s is not a regular file", src) 21 | } 22 | 23 | source, err := os.Open(src) 24 | if err != nil { 25 | return 0, err 26 | } 27 | defer source.Close() 28 | 29 | destination, err := os.Create(dst) 30 | if err != nil { 31 | return 0, err 32 | } 33 | defer destination.Close() 34 | nBytes, err := io.Copy(destination, source) 35 | 36 | return nBytes, err 37 | } 38 | -------------------------------------------------------------------------------- /examples/pack_to_file/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/mediabuyerbot/go-crx3" 9 | ) 10 | 11 | func main() { 12 | // 1. Generate a new private key OR load from file 13 | pk, err := crx3.NewPrivateKey() 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | // 2. Initialize extension 19 | pwd, err := os.Getwd() 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | // 3. Initialize extension 25 | filename := filepath.Join(pwd, "examples", "pack_to_file", "bobbyMol.zip") 26 | fmt.Println("Extension file:", filename) 27 | 28 | // 4. Pack zip file or an unpacked directory into a CRX3 file 29 | ext := crx3.Extension(filename) 30 | if err := ext.Pack(pk); err != nil { 31 | panic(err) 32 | } 33 | 34 | // 5. Write private key to a file 35 | if err := crx3.SavePrivateKey(filename+".pem", pk); err != nil { 36 | panic(err) 37 | } 38 | 39 | fmt.Printf("CRX file has been written to the file: %s\n", filename+".crx") 40 | } 41 | -------------------------------------------------------------------------------- /crx3/commands/id.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/mediabuyerbot/go-crx3" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func newIDCmd() *cobra.Command { 12 | cmd := &cobra.Command{ 13 | Use: "id [infile]", 14 | Short: "Generate id from header extension or manifest file", 15 | Long: ` 16 | The identifier is generated from the hash of the public key, which is located in the extension header or declared in the key field of the manifest. 17 | If the key is specified in the manifest, the public key is taken from there; otherwise, the search continues in the header. 18 | `, 19 | Args: func(cmd *cobra.Command, args []string) error { 20 | if len(args) < 1 { 21 | return errors.New("infile is required") 22 | } 23 | return nil 24 | }, 25 | RunE: func(cmd *cobra.Command, args []string) (err error) { 26 | infile, err := toPath(args[0]) 27 | if err != nil { 28 | return err 29 | } 30 | ext := crx3.Extension(infile) 31 | id, err := ext.ID() 32 | if err != nil { 33 | return err 34 | } 35 | fmt.Println(id) 36 | return nil 37 | }, 38 | } 39 | return cmd 40 | } 41 | -------------------------------------------------------------------------------- /crx3/commands/command.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os/user" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func New(version string) *cobra.Command { 12 | cmd := &cobra.Command{ 13 | Use: "crx3", 14 | Short: "Chrome extensions tools", 15 | } 16 | 17 | cmd.AddCommand(newPackCmd()) 18 | cmd.AddCommand(newUnpackCmd()) 19 | cmd.AddCommand(newZipCmd()) 20 | cmd.AddCommand(newUnzipCmd()) 21 | cmd.AddCommand(newBase64Cmd()) 22 | cmd.AddCommand(newKeygenCmd()) 23 | cmd.AddCommand(newDownloadCmd()) 24 | cmd.AddCommand(newIDCmd()) 25 | cmd.AddCommand(newPubkeyCmd()) 26 | cmd.AddCommand(newVersionCmd(version)) 27 | 28 | return cmd 29 | } 30 | 31 | func toPath(path string) (string, error) { 32 | if strings.HasPrefix(path, "~") { 33 | usr, err := user.Current() 34 | if err != nil { 35 | return "", err 36 | } 37 | home := usr.HomeDir 38 | if path == "~" { 39 | return home, nil 40 | } 41 | return filepath.Join(home, path[2:]), nil 42 | } 43 | return path, nil 44 | } 45 | 46 | func sanitizeKeySize(size int) int { 47 | switch size { 48 | case 2048, 3072, 4096: 49 | return size 50 | default: 51 | return 2048 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/pack_to_memory/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/mediabuyerbot/go-crx3" 11 | ) 12 | 13 | func main() { 14 | // 1. Generate a new private key OR load from file 15 | pk, err := crx3.NewPrivateKey() 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | pwd, err := os.Getwd() 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | // 2. Initialize extension 26 | filename := filepath.Join(pwd, "examples", "pack_to_memory", "bobbyMol.zip") 27 | fmt.Println("Extension file:", filename) 28 | 29 | ext := crx3.Extension(filename) 30 | buf := new(bytes.Buffer) 31 | 32 | // 3. Pack zip file or an unpacked directory into a CRX3 file 33 | if err := ext.WriteTo(buf, pk); err != nil { 34 | panic(err) 35 | } 36 | 37 | fmt.Printf("CRX file has been written to the buffer\n") 38 | fmt.Printf("CRX file size: %d bytes\n", buf.Len()) 39 | 40 | walkExtension(buf) 41 | } 42 | 43 | func walkExtension(buf *bytes.Buffer) { 44 | zipReader, _ := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) 45 | for _, file := range zipReader.File { 46 | fmt.Printf("File: %s\n", file.Name) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crx3/commands/unpack.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | crx3 "github.com/mediabuyerbot/go-crx3" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type uppackOpts struct { 12 | Outfile string 13 | } 14 | 15 | func newUnpackCmd() *cobra.Command { 16 | var opts uppackOpts 17 | cmd := &cobra.Command{ 18 | Use: "unpack [extension.crx] [flags]", 19 | Short: "Unpack chrome extension into current directory or specified directory", 20 | Args: func(cmd *cobra.Command, args []string) error { 21 | if len(args) < 1 { 22 | return errors.New("extension is required") 23 | } 24 | return nil 25 | }, 26 | RunE: func(cmd *cobra.Command, args []string) (err error) { 27 | infile, err := toPath(args[0]) 28 | if err != nil { 29 | return fmt.Errorf("invalid infile path: %w", err) 30 | } 31 | outfile, err := toPath(opts.Outfile) 32 | if err != nil { 33 | return fmt.Errorf("invalid outfile path: %w", err) 34 | } 35 | if len(outfile) > 0 { 36 | return crx3.UnpackTo(infile, outfile) 37 | } 38 | return crx3.Unpack(infile) 39 | }, 40 | } 41 | 42 | cmd.Flags().StringVarP(&opts.Outfile, "outfile", "o", "", "save to file") 43 | 44 | return cmd 45 | } 46 | -------------------------------------------------------------------------------- /crx3/commands/zip.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | crx3 "github.com/mediabuyerbot/go-crx3" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type zipOpts struct { 13 | Outfile string 14 | } 15 | 16 | func (o zipOpts) HasOutfile() bool { 17 | return len(o.Outfile) > 0 18 | } 19 | 20 | func newZipCmd() *cobra.Command { 21 | var opts zipOpts 22 | cmd := &cobra.Command{ 23 | Use: "zip [filepath]", 24 | Short: "Add unpacked extension to archive", 25 | Args: func(cmd *cobra.Command, args []string) error { 26 | if len(args) < 1 { 27 | return errors.New("filepath is required") 28 | } 29 | return nil 30 | }, 31 | RunE: func(cmd *cobra.Command, args []string) (err error) { 32 | infile, err := toPath(args[0]) 33 | if err != nil { 34 | return fmt.Errorf("invalid infile: %w", err) 35 | } 36 | if !opts.HasOutfile() { 37 | opts.Outfile = infile + ".zip" 38 | } 39 | zipFile, err := os.Create(opts.Outfile) 40 | if err != nil { 41 | return err 42 | } 43 | defer zipFile.Close() 44 | 45 | return crx3.Zip(zipFile, infile) 46 | }, 47 | } 48 | 49 | cmd.Flags().StringVarP(&opts.Outfile, "outfile", "o", "", "save to file") 50 | 51 | return cmd 52 | } 53 | -------------------------------------------------------------------------------- /crx3/commands/unzip.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | crx3 "github.com/mediabuyerbot/go-crx3" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | type unzipOpts struct { 14 | Outfile string 15 | } 16 | 17 | func (o unzipOpts) HasNotOutfile() bool { 18 | return len(o.Outfile) == 0 19 | } 20 | 21 | func newUnzipCmd() *cobra.Command { 22 | var opts unzipOpts 23 | cmd := &cobra.Command{ 24 | Use: "unzip [extension.zip]", 25 | Short: "Extract all files from the archive", 26 | Args: func(cmd *cobra.Command, args []string) error { 27 | if len(args) < 1 { 28 | return errors.New("extension is required") 29 | } 30 | return nil 31 | }, 32 | RunE: func(cmd *cobra.Command, args []string) (err error) { 33 | infile, err := toPath(args[0]) 34 | if err != nil { 35 | return fmt.Errorf("invalid infile: %w", err) 36 | } 37 | zipFile, err := os.Open(infile) 38 | if err != nil { 39 | return err 40 | } 41 | defer zipFile.Close() 42 | stat, err := zipFile.Stat() 43 | if err != nil { 44 | return err 45 | } 46 | if opts.HasNotOutfile() { 47 | opts.Outfile = strings.TrimSuffix(infile, ".zip") 48 | } 49 | return crx3.Unzip(zipFile, stat.Size(), opts.Outfile) 50 | }, 51 | } 52 | 53 | cmd.Flags().StringVarP(&opts.Outfile, "outfile", "o", "", "save to file") 54 | 55 | return cmd 56 | } 57 | -------------------------------------------------------------------------------- /crx3/commands/keygen.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | crx3 "github.com/mediabuyerbot/go-crx3" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | const pemExt = ".pem" 12 | 13 | type keygenOpts struct { 14 | PrivateKeySize int 15 | } 16 | 17 | func newKeygenCmd() *cobra.Command { 18 | var opts keygenOpts 19 | cmd := &cobra.Command{ 20 | Use: "keygen [file]", 21 | Short: "Create a new private key", 22 | Long: ` 23 | If no file is specified, the private key is printed to stdout. 24 | Otherwise, the private key is saved to the specified file. 25 | If the file does not have a .pem extension, it is added automatically. 26 | Size of the private key can be set with the --size or -s flag. Sizes of 2048, 3072, or 4096 bits are allowed. 27 | `, 28 | RunE: func(cmd *cobra.Command, args []string) (err error) { 29 | crx3.SetDefaultKeySize(sanitizeKeySize(opts.PrivateKeySize)) 30 | pk, err := crx3.NewPrivateKey() 31 | if err != nil { 32 | return err 33 | } 34 | if len(args) == 0 { 35 | key := crx3.PrivateKeyToPEM(pk) 36 | fmt.Print(string(key)) 37 | return nil 38 | } 39 | filename, err := toPath(args[0]) 40 | if err != nil { 41 | return fmt.Errorf("invalid infile path: %w", err) 42 | } 43 | if !strings.HasSuffix(filename, pemExt) { 44 | filename = filename + pemExt 45 | } 46 | return crx3.SavePrivateKey(filename, pk) 47 | }, 48 | } 49 | 50 | cmd.Flags().IntVarP(&opts.PrivateKeySize, "size", "s", 2048, "private key size") 51 | 52 | return cmd 53 | } 54 | -------------------------------------------------------------------------------- /crx3/commands/pack.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "crypto/rsa" 5 | "errors" 6 | 7 | crx3 "github.com/mediabuyerbot/go-crx3" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type packOpts struct { 12 | PrivateKey string 13 | Outfile string 14 | PrivateKeySize int 15 | } 16 | 17 | func (o packOpts) hasPem() bool { 18 | return len(o.PrivateKey) > 0 19 | } 20 | 21 | func newPackCmd() *cobra.Command { 22 | var opts packOpts 23 | cmd := &cobra.Command{ 24 | Use: "pack [extension]", 25 | Short: "Pack zip file or unzipped directory into a crx extension", 26 | Args: func(cmd *cobra.Command, args []string) error { 27 | if len(args) < 1 { 28 | return errors.New("file is required") 29 | } 30 | return nil 31 | }, 32 | RunE: func(cmd *cobra.Command, args []string) (err error) { 33 | crx3.SetDefaultKeySize(sanitizeKeySize(opts.PrivateKeySize)) 34 | unpacked, err := toPath(args[0]) 35 | if err != nil { 36 | return err 37 | } 38 | var pk *rsa.PrivateKey 39 | if opts.hasPem() { 40 | pk, err = crx3.LoadPrivateKey(opts.PrivateKey) 41 | if err != nil { 42 | return err 43 | } 44 | } 45 | out, err := toPath(opts.Outfile) 46 | if err != nil { 47 | return err 48 | } 49 | return crx3.Pack(unpacked, out, pk) 50 | }, 51 | } 52 | 53 | cmd.Flags().StringVarP(&opts.PrivateKey, "pem", "p", "", "load private key") 54 | cmd.Flags().StringVarP(&opts.Outfile, "outfile", "o", "", "save to file") 55 | cmd.Flags().IntVarP(&opts.PrivateKeySize, "size", "s", 2048, "private key size") 56 | 57 | return cmd 58 | } 59 | -------------------------------------------------------------------------------- /unzip_test.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestUnzipTo(t *testing.T) { 12 | basePath, err := os.MkdirTemp("", "unziptest") 13 | require.NoError(t, err) 14 | defer os.RemoveAll(basePath) 15 | 16 | type args struct { 17 | basepath string 18 | filename string 19 | } 20 | tests := []struct { 21 | name string 22 | args args 23 | assert func(args args) 24 | wantErr bool 25 | }{ 26 | { 27 | name: "should return error when zip file does not exists", 28 | args: args{ 29 | basepath: basePath, 30 | filename: "/some/file/does/not/exists.zip", 31 | }, 32 | wantErr: true, 33 | }, 34 | { 35 | name: "should return error when basepath does not exists", 36 | args: args{ 37 | basepath: "/some/base/path", 38 | filename: "./testdata/bobbyMol.zip", 39 | }, 40 | wantErr: true, 41 | }, 42 | { 43 | name: "should not return error when all params are valid", 44 | args: args{ 45 | basepath: basePath, 46 | filename: "./testdata/bobbyMol.zip", 47 | }, 48 | assert: func(args args) { 49 | expected := filepath.Join(args.basepath, "bobbyMol") 50 | require.True(t, isDir(expected)) 51 | }, 52 | }, 53 | } 54 | for _, tt := range tests { 55 | t.Run(tt.name, func(t *testing.T) { 56 | if err := UnzipTo(tt.args.basepath, tt.args.filename); (err != nil) != tt.wantErr { 57 | t.Errorf("UnzipTo() error = %v, wantErr %v", err, tt.wantErr) 58 | } 59 | if tt.assert != nil { 60 | tt.assert(tt.args) 61 | } 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crx3/commands/base64.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | crx3 "github.com/mediabuyerbot/go-crx3" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | type encodeOpts struct { 15 | Outfile string 16 | } 17 | 18 | func (o encodeOpts) HasOutfile() bool { 19 | return len(o.Outfile) > 0 20 | } 21 | 22 | func newBase64Cmd() *cobra.Command { 23 | var opts encodeOpts 24 | cmd := &cobra.Command{ 25 | Use: "base64 [extension.crx]", 26 | Short: "Encode an extension file to a base64 string", 27 | Args: func(cmd *cobra.Command, args []string) error { 28 | if len(args) < 1 { 29 | return errors.New("extension is required") 30 | } 31 | return nil 32 | }, 33 | RunE: func(cmd *cobra.Command, args []string) (err error) { 34 | infile, err := toPath(args[0]) 35 | if err != nil { 36 | return fmt.Errorf("invalid infile path: %w", err) 37 | } 38 | outfile, err := toPath(opts.Outfile) 39 | if err != nil { 40 | return fmt.Errorf("invalid outfile path: %w", err) 41 | } 42 | extension := crx3.Extension(infile) 43 | b, err := extension.Base64() 44 | if err != nil { 45 | return err 46 | } 47 | if len(outfile) > 0 { 48 | file, err := os.Create(outfile) 49 | if err != nil { 50 | return err 51 | } 52 | defer file.Close() 53 | if _, err := io.Copy(file, bytes.NewBuffer(b)); err != nil { 54 | return err 55 | } 56 | } else { 57 | fmt.Println(string(b)) 58 | } 59 | return nil 60 | }, 61 | } 62 | 63 | cmd.Flags().StringVarP(&opts.Outfile, "outfile", "o", "", "save to file") 64 | 65 | return cmd 66 | } 67 | -------------------------------------------------------------------------------- /download.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | var chromeExtURL = "https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&prodversion=108.0.5359.125&x=id%3D{id}%26installsource%3Dondemand%26uc" 12 | 13 | // SetWebStoreURL sets the web store url to download extensions. 14 | func SetWebStoreURL(u string) { 15 | if len(u) == 0 { 16 | return 17 | } 18 | if !strings.HasPrefix(u, "http") { 19 | u = "https://" + u 20 | } 21 | chromeExtURL = u 22 | } 23 | 24 | // DownloadFromWebStore downloads a Chrome extension from the web store. 25 | // ExtensionID can be an identifier or an url. 26 | func DownloadFromWebStore(extensionID string, filename string) error { 27 | if len(extensionID) == 0 { 28 | return ErrExtensionNotSpecified 29 | } 30 | if len(filename) == 0 { 31 | return ErrPathNotFound 32 | } 33 | 34 | filename = strings.TrimRight(filename, "/") 35 | if !strings.HasSuffix(filename, crxExt) { 36 | filename = filename + crxExt 37 | } 38 | extensionURL := makeChromeURL(extensionID) 39 | file, err := os.Create(filename) 40 | if err != nil { 41 | return err 42 | } 43 | defer file.Close() 44 | resp, err := http.Get(extensionURL) 45 | if err != nil { 46 | return err 47 | } 48 | defer resp.Body.Close() 49 | if resp.StatusCode != http.StatusOK { 50 | return fmt.Errorf("crx3: bad status %s", resp.Status) 51 | } 52 | 53 | if _, err = io.Copy(file, resp.Body); err != nil { 54 | return err 55 | } 56 | return nil 57 | } 58 | 59 | func makeChromeURL(chromeID string) string { 60 | return strings.Replace(chromeExtURL, "{id}", chromeID, 1) 61 | } 62 | -------------------------------------------------------------------------------- /zip.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | // ZipTo creates a ZIP archive with the specified 12 | // filename and adds all files from the given directory to it. 13 | func ZipTo(filename string, dirname string) error { 14 | file, err := os.Create(filename) 15 | if err != nil { 16 | return err 17 | } 18 | defer file.Close() 19 | 20 | return Zip(file, dirname) 21 | } 22 | 23 | // Zip creates a *.zip archive and adds all files 24 | // from the specified directory to it. 25 | func Zip(dst io.Writer, dirname string) error { 26 | if !isDir(dirname) { 27 | return fmt.Errorf("%w: %s", ErrPathNotFound, dirname) 28 | } 29 | wz := zip.NewWriter(dst) 30 | defer wz.Close() 31 | return filepath.Walk(dirname, 32 | func(path string, info os.FileInfo, err error) error { 33 | if err != nil { 34 | return err 35 | } 36 | if info.IsDir() { 37 | return nil 38 | } 39 | relpath, err := filepath.Rel(dirname, path) 40 | if err != nil { 41 | return err 42 | } 43 | return writeToZip(wz, path, relpath) 44 | }) 45 | } 46 | 47 | func writeToZip(w *zip.Writer, filename string, metaname string) error { 48 | fd, err := os.Open(filename) 49 | if err != nil { 50 | return err 51 | } 52 | defer fd.Close() 53 | 54 | info, err := fd.Stat() 55 | if err != nil { 56 | return err 57 | } 58 | 59 | header, err := zip.FileInfoHeader(info) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | header.Name = metaname 65 | header.Method = zip.Deflate 66 | writer, err := w.CreateHeader(header) 67 | if err != nil { 68 | return err 69 | } 70 | _, err = io.Copy(writer, fd) 71 | return err 72 | } 73 | -------------------------------------------------------------------------------- /keys_test.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSavePrivateKey(t *testing.T) { 12 | filename := filepath.Join(os.TempDir(), "key.pem") 13 | err := SavePrivateKey(filename, nil) 14 | assert.Nil(t, err) 15 | assert.FileExists(t, filename) 16 | assert.Nil(t, os.Remove(filename)) 17 | 18 | key, err := NewPrivateKey() 19 | assert.Nil(t, err) 20 | assert.Nil(t, key.Validate()) 21 | err = SavePrivateKey(filename, key) 22 | assert.Nil(t, err) 23 | assert.FileExists(t, filename) 24 | assert.Nil(t, os.Remove(filename)) 25 | } 26 | 27 | func TestSavePrivateKeyNegative(t *testing.T) { 28 | filename := "/path/to/path/key.pem" 29 | err := SavePrivateKey(filename, nil) 30 | assert.Error(t, err) 31 | } 32 | 33 | func TestLoadPrivateKey(t *testing.T) { 34 | filename := filepath.Join(os.TempDir(), "key.pem") 35 | err := SavePrivateKey(filename, nil) 36 | assert.Nil(t, err) 37 | assert.FileExists(t, filename) 38 | 39 | key, err := LoadPrivateKey(filename) 40 | assert.Nil(t, err) 41 | assert.Nil(t, key.Validate()) 42 | assert.Nil(t, os.Remove(filename)) 43 | } 44 | 45 | func TestLoadPrivateKeyNegative(t *testing.T) { 46 | key, err := LoadPrivateKey("/path/to/key.pem") 47 | assert.Error(t, err) 48 | assert.Nil(t, key) 49 | 50 | key, err = LoadPrivateKey("./testdata/pack/somefile.fg") 51 | assert.Error(t, err) 52 | assert.Nil(t, key) 53 | } 54 | 55 | func TestPrivateKeyToPEM(t *testing.T) { 56 | key, err := NewPrivateKey() 57 | assert.Nil(t, err) 58 | assert.Nil(t, key.Validate()) 59 | pem := PrivateKeyToPEM(key) 60 | assert.NotNil(t, pem) 61 | assert.NotEmpty(t, pem) 62 | } 63 | -------------------------------------------------------------------------------- /crx3/commands/download.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "os" 7 | "path" 8 | "strings" 9 | 10 | crx3 "github.com/mediabuyerbot/go-crx3" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | type downloadOpts struct { 15 | Outfile string 16 | Unpack bool 17 | } 18 | 19 | func (o downloadOpts) HasNotOutfile() bool { 20 | return len(o.Outfile) == 0 21 | } 22 | 23 | func newDownloadCmd() *cobra.Command { 24 | var opts downloadOpts 25 | cmd := &cobra.Command{ 26 | Use: "download [id or url]", 27 | Short: "Download the chrome extension from the web store", 28 | Args: func(cmd *cobra.Command, args []string) error { 29 | if len(args) < 1 { 30 | return errors.New("extension is required") 31 | } 32 | return nil 33 | }, 34 | RunE: func(cmd *cobra.Command, args []string) (err error) { 35 | extensionID := args[0] 36 | if strings.HasPrefix(extensionID, "http") { 37 | extensionID = extractExtensionID(extensionID) 38 | } 39 | if opts.HasNotOutfile() { 40 | pwd, err := os.Getwd() 41 | if err != nil { 42 | return err 43 | } 44 | opts.Outfile = path.Join(pwd, "extension.crx") 45 | } 46 | 47 | if !strings.HasSuffix(opts.Outfile, ".crx") { 48 | opts.Outfile = opts.Outfile + ".crx" 49 | } 50 | 51 | if err := crx3.DownloadFromWebStore(extensionID, opts.Outfile); err != nil { 52 | return err 53 | } 54 | 55 | if opts.Unpack { 56 | if err := crx3.Unpack(opts.Outfile); err != nil { 57 | return err 58 | } 59 | } 60 | return 61 | }, 62 | } 63 | 64 | cmd.Flags().StringVarP(&opts.Outfile, "outfile", "o", "", "save to file") 65 | cmd.Flags().BoolVarP(&opts.Unpack, "unpack", "u", true, "unpack") 66 | 67 | return cmd 68 | } 69 | 70 | func extractExtensionID(rawUrl string) string { 71 | u, err := url.Parse(rawUrl) 72 | if err != nil { 73 | return "" 74 | } 75 | urlParts := strings.Split(u.Path, "/") 76 | if len(urlParts) == 0 { 77 | return "" 78 | } 79 | return urlParts[len(urlParts)-1] 80 | } 81 | -------------------------------------------------------------------------------- /os.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | func isDir(filename string) bool { 11 | file, err := os.Open(filename) 12 | if err != nil { 13 | return false 14 | } 15 | defer file.Close() 16 | 17 | stat, err := file.Stat() 18 | if err != nil { 19 | return false 20 | } 21 | return stat.IsDir() 22 | } 23 | 24 | func isCRX(filename string) bool { 25 | size := 12 26 | file, err := os.Open(filename) 27 | if err != nil { 28 | return false 29 | } 30 | defer file.Close() 31 | buf := make([]byte, size) 32 | if _, err := file.Read(buf); err != nil { 33 | return false 34 | } 35 | if len(buf) < size { 36 | return false 37 | } 38 | if string(buf[0:4]) != "Cr24" { 39 | return false 40 | } 41 | if binary.LittleEndian.Uint32(buf[4:8]) != 3 { 42 | return false 43 | } 44 | return true 45 | } 46 | 47 | func openCrxFile(filename string) (*os.File, error) { 48 | if err := crxFileExists(filename); err != nil { 49 | return nil, err 50 | } 51 | fd, err := os.Open(filename) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return fd, nil 56 | } 57 | 58 | func crxFileExists(filename string) error { 59 | if !isCRX(filename) { 60 | return fmt.Errorf("%v got %s", ErrUnknownFileExtension, filename) 61 | } 62 | return nil 63 | } 64 | 65 | func isZip(filename string) bool { 66 | file, err := os.Open(filename) 67 | if err != nil { 68 | return false 69 | } 70 | defer file.Close() 71 | buf := make([]byte, 512) 72 | if _, err := file.Read(buf); err != nil { 73 | return false 74 | } 75 | fileType := http.DetectContentType(buf) 76 | switch fileType { 77 | case "application/x-gzip", "application/zip": 78 | return true 79 | } 80 | return false 81 | } 82 | 83 | func fileExists(filename string) bool { 84 | info, err := os.Stat(filename) 85 | if os.IsNotExist(err) || err != nil { 86 | return false 87 | } 88 | return !info.IsDir() 89 | } 90 | 91 | func dirExists(filename string) bool { 92 | info, err := os.Stat(filename) 93 | if os.IsNotExist(err) || err != nil { 94 | return false 95 | } 96 | return info.IsDir() 97 | } 98 | -------------------------------------------------------------------------------- /unzip.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | // UnzipTo extracts the contents of a ZIP archive specified by 'filename' to the 'basepath' directory. 13 | // It opens the ZIP file, creates the necessary directory structure, and extracts all files. 14 | func UnzipTo(basepath string, filename string) error { 15 | if !isDir(basepath) { 16 | return fmt.Errorf("%w: does not exists %s", 17 | ErrPathNotFound, basepath) 18 | } 19 | 20 | file, err := os.Open(filename) 21 | if err != nil { 22 | return err 23 | } 24 | defer file.Close() 25 | 26 | stat, err := file.Stat() 27 | if err != nil { 28 | return err 29 | } 30 | 31 | dirname := filepath.Join(basepath, 32 | strings.TrimSuffix(filepath.Base(filename), zipExt)) 33 | 34 | return Unzip(file, stat.Size(), dirname) 35 | } 36 | 37 | // Unzip extracts all files and directories from the provided ZIP archive. 38 | // It takes an io.ReaderAt 'r', the size 'size' of the ZIP archive, and the target directory 'unpacked' for extraction. 39 | // It iterates through the archive, creating directories and writing files as necessary. 40 | func Unzip(r io.ReaderAt, size int64, unpacked string) error { 41 | reader, err := zip.NewReader(r, size) 42 | if err != nil { 43 | return err 44 | } 45 | if _, err := os.Stat(unpacked); os.IsNotExist(err) { 46 | if err := os.Mkdir(unpacked, os.ModePerm); err != nil { 47 | return err 48 | } 49 | } 50 | for _, file := range reader.File { 51 | fpath := filepath.Join(unpacked, file.Name) 52 | if !strings.HasPrefix(fpath, filepath.Clean(unpacked)+string(os.PathSeparator)) { 53 | return fmt.Errorf("%s: illegal file path", fpath) 54 | } 55 | if file.FileInfo().IsDir() { 56 | if err := os.MkdirAll(fpath, os.ModePerm); err != nil { 57 | return err 58 | } 59 | continue 60 | } 61 | if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { 62 | return err 63 | } 64 | outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode()) 65 | if err != nil { 66 | return err 67 | } 68 | rc, err := file.Open() 69 | if err != nil { 70 | return err 71 | } 72 | if _, err = io.Copy(outFile, rc); err != nil { 73 | return err 74 | } 75 | if err = outFile.Close(); err != nil { 76 | return err 77 | } 78 | if err = rc.Close(); err != nil { 79 | return err 80 | } 81 | } 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /pb/crx3.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file 4 | 5 | syntax = "proto2"; 6 | 7 | package pb; 8 | 9 | option optimize_for = LITE_RUNTIME; 10 | option go_package = "./pb/pb"; 11 | // A CRX₃ file is a binary file of the following format: 12 | // [4 octets]: "Cr24", a magic number. 13 | // [4 octets]: The version of the *.crx file format used (currently 3). 14 | // [4 octets]: N, little-endian, the length of the header section. 15 | // [N octets]: The header (the binary encoding of a CrxFileHeader). 16 | // [M octets]: The ZIP archive. 17 | // Clients should reject CRX₃ files that contain an N that is too large for the 18 | // client to safely handle in memory. 19 | 20 | message CrxFileHeader { 21 | // PSS signature with RSA public key. The public key is formatted as a 22 | // X.509 SubjectPublicKeyInfo block, as in CRX₂. In the common case of a 23 | // developer key proof, the first 128 bits of the SHA-256 hash of the 24 | // public key must equal the crx_id. 25 | repeated AsymmetricKeyProof sha256_with_rsa = 2; 26 | 27 | // ECDSA signature, using the NIST P-256 curve. Public key appears in 28 | // named-curve format. 29 | // The pinned algorithm will be this, at least on 2017-01-01. 30 | repeated AsymmetricKeyProof sha256_with_ecdsa = 3; 31 | 32 | // The binary form of a SignedData message. We do not use a nested 33 | // SignedData message, as handlers of this message must verify the proofs 34 | // on exactly these bytes, so it is convenient to parse in two steps. 35 | // 36 | // All proofs in this CrxFile message are on the value 37 | // "CRX3 SignedData\x00" + signed_header_size + signed_header_data + 38 | // archive, where "\x00" indicates an octet with value 0, "CRX3 SignedData" 39 | // is encoded using UTF-8, signed_header_size is the size in octets of the 40 | // contents of this field and is encoded using 4 octets in little-endian 41 | // order, signed_header_data is exactly the content of this field, and 42 | // archive is the remaining contents of the file following the header. 43 | optional bytes signed_header_data = 10000; 44 | } 45 | 46 | message AsymmetricKeyProof { 47 | optional bytes public_key = 1; 48 | optional bytes signature = 2; 49 | } 50 | 51 | message SignedData { 52 | // This is simple binary, not UTF-8 encoded mpdecimal; i.e. it is exactly 53 | // 16 bytes long. 54 | optional bytes crx_id = 1; 55 | } -------------------------------------------------------------------------------- /unpack.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/mediabuyerbot/go-crx3/pb" 11 | 12 | "google.golang.org/protobuf/proto" 13 | ) 14 | 15 | // UnpackTo unpacks a CRX (Chrome Extension) file specified by 'filename' to the directory 'dirname'. 16 | // If 'dirname' does not exist, it creates the directory before unpacking. 17 | func UnpackTo(filename string, dirname string) error { 18 | if !isDir(dirname) { 19 | if err := os.Mkdir(dirname, 0755); err != nil { 20 | return err 21 | } 22 | } 23 | 24 | return unpack(filename, dirname) 25 | } 26 | 27 | // Unpack unpacks a CRX (Chrome Extension) file specified by 'filename' to its original contents. 28 | // It checks if the file is in the CRX format, reads its header and signed data, 29 | // and then extracts and decompresses the original contents. 30 | // The unpacked contents are placed in a directory with the same name as the original file (without the '.crx' extension). 31 | func Unpack(filename string) error { 32 | return unpack(filename, "") 33 | } 34 | 35 | func unpack(filename string, dirname string) error { 36 | // check if the file is in the CRX format. 37 | if len(filename) == 0 || !isCRX(filename) { 38 | return ErrUnsupportedFileFormat 39 | } 40 | 41 | // read the entire CRX file into memory. 42 | crx, err := os.ReadFile(filename) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | // extract header and signed data from the CRX file. 48 | var ( 49 | headerSize = binary.LittleEndian.Uint32(crx[8:12]) 50 | metaSize = uint32(12) 51 | v = crx[metaSize : headerSize+metaSize] 52 | header pb.CrxFileHeader 53 | signedData pb.SignedData 54 | ) 55 | 56 | // unmarshal the header data. 57 | if err = proto.Unmarshal(v, &header); err != nil { 58 | return err 59 | } 60 | 61 | // unmarshal the header data. 62 | if err = proto.Unmarshal(header.SignedHeaderData, &signedData); err != nil { 63 | return err 64 | } 65 | 66 | // check if the CRX ID has the expected length. 67 | if len(signedData.CrxId) != 16 { 68 | return ErrUnsupportedFileFormat 69 | } 70 | 71 | data := crx[len(v)+int(metaSize):] 72 | reader := bytes.NewReader(data) 73 | size := int64(len(data)) 74 | 75 | var unpacked string 76 | if len(dirname) > 0 { 77 | fn := filepath.Base(filename) 78 | unpacked = filepath.Join(dirname, strings.TrimSuffix(fn, crxExt)) 79 | } else { 80 | unpacked = strings.TrimSuffix(filename, crxExt) 81 | } 82 | 83 | return Unzip(reader, size, unpacked) 84 | } 85 | -------------------------------------------------------------------------------- /download_test.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestDownloadFromWebStore_EmptyArgs(t *testing.T) { 16 | err := DownloadFromWebStore("", "") 17 | assert.Equal(t, ErrExtensionNotSpecified, err) 18 | 19 | err = DownloadFromWebStore("ext", "") 20 | assert.Equal(t, ErrPathNotFound, err) 21 | } 22 | 23 | func TestDownloadFromWebStore_SetWebStoreURL(t *testing.T) { 24 | SetWebStoreURL("") 25 | assert.NotEmpty(t, chromeExtURL) 26 | 27 | SetWebStoreURL("custom-webstore-url") 28 | assert.Equal(t, "https://custom-webstore-url", chromeExtURL) 29 | 30 | SetWebStoreURL("http://custom-webstore-url") 31 | assert.Equal(t, "http://custom-webstore-url", chromeExtURL) 32 | } 33 | 34 | func TestDownloadFromWebStore(t *testing.T) { 35 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 36 | assert.Equal(t, http.MethodGet, r.Method) 37 | assert.Equal(t, "/extension/extensionID", r.URL.Path) 38 | 39 | w.WriteHeader(http.StatusOK) 40 | 41 | b, err := os.ReadFile("./testdata/dodyDol.crx") 42 | assert.Nil(t, err) 43 | _, err = io.Copy(w, bytes.NewReader(b)) 44 | assert.Nil(t, err) 45 | }) 46 | webStoreAPI := httptest.NewServer(handler) 47 | defer webStoreAPI.Close() 48 | 49 | webStoreURL := webStoreAPI.URL + "/extension/{id}" 50 | SetWebStoreURL(webStoreURL) 51 | filename := filepath.Join(os.TempDir(), "extension") 52 | err := DownloadFromWebStore("extensionID", filename) 53 | assert.Nil(t, err) 54 | assert.True(t, assert.FileExists(t, filename+crxExt)) 55 | assert.Nil(t, os.Remove(filename+crxExt)) 56 | } 57 | 58 | func TestDownloadFromWebStoreNegative(t *testing.T) { 59 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 60 | assert.Equal(t, http.MethodGet, r.Method) 61 | assert.Equal(t, "/extension/extensionID", r.URL.Path) 62 | w.WriteHeader(http.StatusBadRequest) 63 | }) 64 | webStoreAPI := httptest.NewServer(handler) 65 | defer webStoreAPI.Close() 66 | 67 | webStoreURL := webStoreAPI.URL + "/extension/{id}" 68 | SetWebStoreURL(webStoreURL) 69 | filename := filepath.Join("/some/path", "extension") 70 | err := DownloadFromWebStore("extensionID", filename) 71 | assert.Error(t, err) 72 | 73 | filename = filepath.Join(os.TempDir(), "extension") 74 | err = DownloadFromWebStore("extensionID", filename) 75 | assert.Error(t, err) 76 | 77 | SetWebStoreURL("{id}.{id}.{id}") 78 | err = DownloadFromWebStore("extensionID", filename) 79 | assert.Error(t, err) 80 | } 81 | -------------------------------------------------------------------------------- /unpack_test.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestUnpack(t *testing.T) { 12 | basePath, err := os.MkdirTemp("", "unpacktest") 13 | require.NoError(t, err) 14 | defer os.RemoveAll(basePath) 15 | 16 | dst := filepath.Join(basePath, "dodyDol.crx") 17 | src := "./testdata/dodyDol.crx" 18 | _, err = CopyFile(src, dst) 19 | require.NoError(t, err) 20 | 21 | type args struct { 22 | filename string 23 | } 24 | tests := []struct { 25 | name string 26 | args args 27 | assert func() 28 | wantErr bool 29 | }{ 30 | { 31 | name: "should return error when filename is empty", 32 | wantErr: true, 33 | }, 34 | { 35 | name: "should return error when file is not in crx format", 36 | args: args{ 37 | filename: "./testdata/bobbyMol.zip", 38 | }, 39 | wantErr: true, 40 | }, 41 | { 42 | name: "should not return error when all params are valid", 43 | args: args{ 44 | filename: dst, 45 | }, 46 | assert: func() { 47 | expected := filepath.Join(basePath, "dodyDol") 48 | require.True(t, isDir(expected)) 49 | }, 50 | }, 51 | } 52 | for _, tt := range tests { 53 | t.Run(tt.name, func(t *testing.T) { 54 | if err := Unpack(tt.args.filename); (err != nil) != tt.wantErr { 55 | t.Errorf("Unpack() error = %v, wantErr %v", err, tt.wantErr) 56 | } 57 | if tt.assert != nil { 58 | tt.assert() 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func TestUnpackTo(t *testing.T) { 65 | basePath, err := os.MkdirTemp("", "unpacktotest") 66 | require.NoError(t, err) 67 | defer os.RemoveAll(basePath) 68 | 69 | type args struct { 70 | filename string 71 | dirname string 72 | } 73 | tests := []struct { 74 | name string 75 | args args 76 | assert func() 77 | wantErr bool 78 | }{ 79 | { 80 | name: "should return error when dir does not exists and create failed", 81 | args: args{ 82 | filename: "./testdata/dodyDol.crx", 83 | dirname: "/not/not/not", 84 | }, 85 | wantErr: true, 86 | }, 87 | { 88 | name: "should not return error when all params are valid", 89 | args: args{ 90 | filename: "./testdata/dodyDol.crx", 91 | dirname: basePath, 92 | }, 93 | assert: func() { 94 | expected := filepath.Join(basePath, "dodyDol") 95 | require.True(t, isDir(expected)) 96 | }, 97 | }, 98 | } 99 | for _, tt := range tests { 100 | t.Run(tt.name, func(t *testing.T) { 101 | if err := UnpackTo(tt.args.filename, tt.args.dirname); (err != nil) != tt.wantErr { 102 | t.Errorf("UnpackTo() error = %v, wantErr %v", err, tt.wantErr) 103 | } 104 | if tt.assert != nil { 105 | tt.assert() 106 | } 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 6 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 7 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 8 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 9 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 13 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 14 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 15 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 16 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 19 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 20 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 21 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 22 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 23 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 24 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 25 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 26 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 29 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 31 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 32 | -------------------------------------------------------------------------------- /keys.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "os" 9 | ) 10 | 11 | var defaultKeySize = 2048 12 | 13 | // SetDefaultKeySize sets the global default key size for RSA key generation. 14 | // It accepts key sizes of 2048, 3072, or 4096 bits. If a size outside these 15 | // specified options is passed, the function panics to prevent the use of 16 | // an unsupported key size, which could lead to security vulnerabilities. 17 | // This strict enforcement helps ensure that only strong, widely accepted 18 | // key sizes are used throughout the application. 19 | // 20 | // Usage: 21 | // 22 | // SetDefaultKeySize(2048) // sets the default key size to 2048 bits 23 | // SetDefaultKeySize(4096) // sets the default key size to 4096 bits 24 | // 25 | // Valid key sizes are: 26 | // - 2048 bits 27 | // - 3072 bits 28 | // - 4096 bits 29 | // 30 | // Panics: 31 | // 32 | // This function will panic if any key size other than the above mentioned 33 | // valid sizes is attempted to be set. This is a deliberate design choice 34 | // to catch incorrect key size settings during the development phase. 35 | func SetDefaultKeySize(size int) { 36 | switch size { 37 | case 2048, 3072, 4096: 38 | defaultKeySize = size 39 | default: 40 | panic("invalid key size: only 2048, 3072, or 4096 bits are allowed") 41 | } 42 | } 43 | 44 | // NewPrivateKey returns a new RSA private key with a bit size of 4096. 45 | func NewPrivateKey() (*rsa.PrivateKey, error) { 46 | return rsa.GenerateKey(rand.Reader, defaultKeySize) 47 | } 48 | 49 | // SavePrivateKey saves the provided 'key' private key to the specified 'filename'. 50 | // If 'key' is nil, it generates a new private key and saves it to the file. 51 | func SavePrivateKey(filename string, key *rsa.PrivateKey) error { 52 | if key == nil { 53 | key, _ = NewPrivateKey() 54 | } 55 | fd, err := os.Create(filename) 56 | if err != nil { 57 | return err 58 | } 59 | defer fd.Close() 60 | bytes, err := x509.MarshalPKCS8PrivateKey(key) 61 | if err != nil { 62 | return err 63 | } 64 | block := &pem.Block{ 65 | Type: "RSA PRIVATE KEY", 66 | Bytes: bytes, 67 | } 68 | _, err = fd.Write(pem.EncodeToMemory(block)) 69 | return err 70 | } 71 | 72 | // LoadPrivateKey loads the RSA private key from the specified 'filename' into memory. 73 | // It returns the loaded private key or an error if the key cannot be loaded. 74 | func LoadPrivateKey(filename string) (*rsa.PrivateKey, error) { 75 | buf, err := os.ReadFile(filename) 76 | if err != nil { 77 | return nil, err 78 | } 79 | block, _ := pem.Decode(buf) 80 | if block == nil { 81 | return nil, ErrPrivateKeyNotFound 82 | } 83 | r, err := x509.ParsePKCS8PrivateKey(block.Bytes) 84 | if err != nil { 85 | return nil, err 86 | } 87 | return r.(*rsa.PrivateKey), nil 88 | } 89 | 90 | // PublicKeyToPEM converts the provided RSA public key to a PEM-encoded byte slice. 91 | func PrivateKeyToPEM(key *rsa.PrivateKey) []byte { 92 | bytes, _ := x509.MarshalPKCS8PrivateKey(key) 93 | block := &pem.Block{ 94 | Type: "RSA PRIVATE KEY", 95 | Bytes: bytes, 96 | } 97 | return pem.EncodeToMemory(block) 98 | } 99 | -------------------------------------------------------------------------------- /id.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "crypto/x509" 7 | "encoding/base64" 8 | "encoding/binary" 9 | "fmt" 10 | "os" 11 | 12 | "github.com/mediabuyerbot/go-crx3/pb" 13 | 14 | "google.golang.org/protobuf/proto" 15 | ) 16 | 17 | const symbols = "abcdefghijklmnopqrstuvwxyz" 18 | 19 | // ID returns the extension ID extracted from a CRX (Chrome Extension) file specified by 'filename'. 20 | // It checks if the file is in the CRX format, reads its header and signed data, 21 | // and then converts the CRX ID into a string representation 22 | func ID(filename string) (id string, err error) { 23 | if !isCRX(filename) { 24 | return id, ErrUnsupportedFileFormat 25 | } 26 | 27 | crx, err := os.ReadFile(filename) 28 | if err != nil { 29 | return id, err 30 | } 31 | 32 | var ( 33 | headerSize = binary.LittleEndian.Uint32(crx[8:12]) 34 | metaSize = uint32(12) 35 | v = crx[metaSize : headerSize+metaSize] 36 | header pb.CrxFileHeader 37 | signedData pb.SignedData 38 | ) 39 | 40 | if err := proto.Unmarshal(v, &header); err != nil { 41 | return id, err 42 | } 43 | if err := proto.Unmarshal(header.SignedHeaderData, &signedData); err != nil { 44 | return id, err 45 | } 46 | 47 | idx := strIDx() 48 | sid := fmt.Sprintf("%x", signedData.CrxId[:16]) 49 | buf := bytes.NewBuffer(nil) 50 | for _, char := range sid { 51 | index := idx[char] 52 | buf.WriteString(string(symbols[index])) 53 | } 54 | return buf.String(), nil 55 | } 56 | 57 | // IDFromPubKey generates the Chrome Extension ID from a public key. 58 | // It handles PEM formatting, base64 decoding, and SHA-256 hashing to produce the ID. 59 | // Returns the ID or an error if the key processing fails. 60 | func IDFromPubKey(pubKey []byte) (string, error) { 61 | if len(pubKey) < 64 { 62 | return "", fmt.Errorf("crx3/id: public key is empty") 63 | } 64 | 65 | if bytes.Contains(pubKey[:64], []byte("PUBLIC KEY")) { 66 | pubKey = formatPemKey(pubKey) 67 | } 68 | 69 | dbuf := make([]byte, base64.StdEncoding.DecodedLen(len(pubKey))) 70 | n, err := base64.StdEncoding.Decode(dbuf, pubKey) 71 | if err != nil { 72 | return "", fmt.Errorf("crx3/id: failed to decode public key: %w", err) 73 | } 74 | 75 | pubKeyParsed, err := x509.ParsePKIXPublicKey(dbuf[:n]) 76 | if err != nil { 77 | return "", fmt.Errorf("crx3/id: failed to parse public key: %w", err) 78 | } 79 | pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKeyParsed) 80 | if err != nil { 81 | return "", fmt.Errorf("crx3/id: failed to marshal public key: %w", err) 82 | } 83 | hash := sha256.New() 84 | hash.Write(pubKeyBytes) 85 | digest := hash.Sum(nil) 86 | 87 | idx := strIDx() 88 | sid := fmt.Sprintf("%x", digest[:16]) 89 | buf := bytes.NewBuffer(nil) 90 | for _, char := range sid { 91 | index := idx[char] 92 | buf.WriteString(string(symbols[index])) 93 | } 94 | 95 | return buf.String(), nil 96 | } 97 | 98 | func strIDx() map[rune]int { 99 | index := make(map[rune]int) 100 | src := "0123456789abcdef" 101 | for i, char := range src { 102 | index[char] = i 103 | } 104 | return index 105 | } 106 | -------------------------------------------------------------------------------- /zip_test.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "errors" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func readZip(zf *zip.File) ([]byte, error) { 16 | f, err := zf.Open() 17 | if err != nil { 18 | return nil, err 19 | } 20 | defer f.Close() 21 | return io.ReadAll(f) 22 | } 23 | 24 | func TestZip(t *testing.T) { 25 | type args struct { 26 | dirname string 27 | } 28 | tests := []struct { 29 | name string 30 | args args 31 | assert func(dst *bytes.Buffer) 32 | wantErr error 33 | }{ 34 | { 35 | name: "should return error when file path not exists", 36 | args: args{ 37 | dirname: "/some/path/to/extension", 38 | }, 39 | wantErr: ErrPathNotFound, 40 | }, 41 | { 42 | name: "should not return error when all params are valid", 43 | args: args{ 44 | dirname: "./testdata/extension", 45 | }, 46 | assert: func(buf *bytes.Buffer) { 47 | require.NotZero(t, buf.Len()) 48 | r, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) 49 | require.NoError(t, err) 50 | require.NotNil(t, r) 51 | count := 0 52 | for i := 0; i < len(r.File); i++ { 53 | file := r.File[i] 54 | data, err := readZip(file) 55 | require.NoError(t, err) 56 | require.NotZero(t, data) 57 | got := fileExists(filepath.Join("./testdata/extension", file.Name)) 58 | require.True(t, got) 59 | count++ 60 | } 61 | require.Equal(t, 3, count) 62 | }, 63 | }, 64 | } 65 | for _, tt := range tests { 66 | t.Run(tt.name, func(t *testing.T) { 67 | dst := bytes.NewBuffer(nil) 68 | if err := Zip(dst, tt.args.dirname); !errors.Is(err, tt.wantErr) { 69 | t.Errorf("Zip() error = %v, wantErr %v", err, tt.wantErr) 70 | return 71 | } 72 | if tt.assert != nil { 73 | tt.assert(dst) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | func TestZipTo(t *testing.T) { 80 | basePath, err := os.MkdirTemp("", "testzip") 81 | require.NoError(t, err) 82 | defer os.RemoveAll(basePath) 83 | 84 | type args struct { 85 | filename string 86 | dirname string 87 | } 88 | tests := []struct { 89 | name string 90 | args args 91 | wantErr bool 92 | }{ 93 | { 94 | name: "should return error when failed to create new file", 95 | args: args{ 96 | filename: "", 97 | dirname: "", 98 | }, 99 | wantErr: true, 100 | }, 101 | { 102 | name: "should return error when directory name does not exist", 103 | args: args{ 104 | filename: filepath.Join(basePath, "my.zip"), 105 | dirname: "", 106 | }, 107 | wantErr: true, 108 | }, 109 | { 110 | name: "should not return error when all params are valid", 111 | args: args{ 112 | filename: filepath.Join(basePath, "my.zip"), 113 | dirname: "./testdata/extension", 114 | }, 115 | }, 116 | } 117 | for _, tt := range tests { 118 | t.Run(tt.name, func(t *testing.T) { 119 | if err := ZipTo(tt.args.filename, tt.args.dirname); (err != nil) != tt.wantErr { 120 | t.Errorf("ZipTo() error = %v, wantErr %v", err, tt.wantErr) 121 | } 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /testdata/withkey.crx.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDCKqKG07rtsGpW 3 | YgEy+vekQxLOQNg5qkxY96yAf+Itogb6CSZ033IWyg2eUlkGUUJLHxVyV5OST8Ob 4 | Xe95SOqnexOu0gRJFxGYYZbRy8odcIBYFsRQ82vhRSu1ga0g6CE1lzNtmPpPobCR 5 | b4cmEuYGd0zC5RFl8Aa4G9/WEKid5MHMRTzqOvlaNmmjmeZ2djJfaB3JclrGBJea 6 | hjYYjNidk3udVJ2LioFQV6xM83N+YrtjRzhVP7c5PnTjkpnIZ9H+sPnhweMpARIn 7 | bh/dr/shkAbcecob2S8KKIJ2iZ74l7N1z4F4ChlpOgI3LdIgmYsbD2K96Dbndi57 8 | Z+AO8Q4xAArwqAFAaWSWj2kXxOPRk5FjCxVYRh67mlBh2HvvsaGK51BcPHDbA09l 9 | bcX2gTFccdIWgPHKyT+h545ju8oolW/VG2dx35ABSaVF97UrZPsVybNUwnBwsElT 10 | wdcofGvxkYeAJvPn7mc3VWIjJre/EJVrfmDBMYK6rkgIHfGFN+2WguternKTrzta 11 | 2morh2fOHr6G9aGX0YeV8cLoEgwSuIeQLzx4nxLuDKDEgEEOU/6CNLwhWgu0q5li 12 | wL1fnHwprdJP5fc/qU5IHRdgKkwLxxWTInypIzOazzRe5gAmVVqBTbQvTPkQDos0 13 | jK6jBARUfzvIjbHJWp4wVaO91PZA4QIDAQABAoICAEH3rkRUhzveJiK3JWUmsyBR 14 | 0X/VtCDTZSEM9MSrrjKGzAwDM9edWuu3Ni1GGQz2aqmPJAA3FOIuy2xr28K/LUo9 15 | nJBWtjIG7mlxLoaU0FR0Noa3JXfDXDGrCJCgQCvf8fh9KHHh+Zk4e/7Nf4NGBHTJ 16 | 74B/xwt1IzNF9SSLgF40rEs0ct+5raIivn1g+lXhDngvrX8VpRWF1eQgGRz6LVZM 17 | F0F60BPquMiNIPL7+49DCBtQxSjhfuSp/Zib5DecXlJD2oIDF6SEwqA24Ai3k4Dx 18 | qAMcNbiEb2DqJnkThLk9ATHTkE8yTzPbC4mnva5pvEMzVP7keFLnah9vSUaKS6Ft 19 | X4qwkTy5ImS1mSJ4Iszqpwy/lG+zFbeTWLGkrdYEtvTMxjLwzXLIq7a+1xI0gLHV 20 | NLi3BzKm/GrKh4fzgbz4FTRVtFNi4t+8gCoV3NKXbaUmT+QXb95FWiobeEQVAln3 21 | rVWD6+wZ2BvA+l6TMSkHF93SpxdW+J7xSpV7LJAtsgNJAhdj7GiJQpbU65kzdKv2 22 | EArbYG0ewdjZwiCjMHPYP9NZjihW4GaaYS372/gvYMMpcTGfxkf9yDs/T2h26MKT 23 | YNaVX3ELX5EZUCVWPOXFXwltIVbgRBmCoO3u1GNJ3TbWT8DfkMdYX+FkL22t8LrG 24 | M3EcdNuqmHCMmSC/aJeBAoIBAQDC8ccVsYCZ8SN/8wwRPjJYRojLPF9JSmuKW1Kn 25 | oDZwwrQjlhesNx6mH2gmZOUn848XzMl4ZYOXNk+Sba5c5JH6jtvNB6oHNPLfE7ND 26 | MXqBYEORqhylm1vKh/ppEw+2c7ua4KydQ5s41hYcdB97LTN+eTxMHLXeTxZCk+lD 27 | LVM+a4BRof4iHQn/mVxUUehGHYyZ/fklisYdj5i0f2yfROpI4YaAYj1VudKnW+qg 28 | gjM1Zi5uDu1BCQUGzCJFH0Miz20Kf5txfVvwmJC8YoZ7XrhPjeeliMkdS2Tw7vrs 29 | tqK3ph18rPqddYPkSwIB13zQCo8izEpOK7Wzyo8s+cJVpHx5AoIBAQD++nyaqNDe 30 | PylLIlgfHA47uswxQVqVJyexxJM7z0xpxE8Bx2DKE2KpCo+Kr9AGisjYh/sPZsEi 31 | XV6reBsWWiSZbZrJmFuqBLQ8/onIatn6TD3bihOsew+MdjtGHO4yCiiOp5gskQyx 32 | 1uCYQ6y7bv65rHcv0V6oNq22KnciTWl7GqUvUPX3st436NEdav8KZzaExm4WOzDp 33 | E0nW1X538HHN3JX7Fedvmk2PoLAOxS3Q4pIoibXeYAY308slbQsKqk4rez64PcDr 34 | 0ezmOOymR0IaUroWer0q6tWao3p3Iis4tMXTTD7JwTtZMR2X6jyD4KcLGczA0sM+ 35 | 1+qXjFOnIX2pAoIBAQCSms1DrTevjb9Ky/d5SDMIXBMn9IEcVxFE/aTNVxlZ97MN 36 | SCUJaHJuMBRdO2dygiJMnb+uAXnS0A9LaZzFU0fNDH6UVH0z6kf0J5aao60jeseV 37 | 1j6w9IM1bsmNF76rUaH2uZxWsK8dMTNztoiuU7H0HSyReM31H7j33NCBqqZ9vM7F 38 | lXPPJ9OLG0RqTSbHBBXnAS7LYu/W71TB+UoxBNzEboZ+KmNCAvs+zUtH5mKod+3W 39 | 6vbV//h9wirLnNUaaq/wQ0MdOE9aQwImClpkkTk+6tMYlCPbGgYRg0fFmRwJzK1E 40 | Q7o9jFDh8N7Tj8DXm/lFDCmdXBXL3juKcVIKoibRAoIBAQCsC96aCQDuhZXaYbEu 41 | RKMCAJgZQCzb6ZCqLabfO5Am6dQZsiuIDq6Ku1qBzQHD+E9vc8me6cm142SmtKMq 42 | YObDNCa2knx/ay8m3OWhex/b+SfgA/okbuDd+UUjmQ/MafhV0ZYntDPpp8DiXP+n 43 | dUyhglLlzBNf041BFsROPAfJjgAZvjpJycKR3SGFBRZUMbKiwrWzgHsPOfmf4Wy1 44 | h2Ny8b2tr2j7cBWXrWg+fyPcB5VxjwJNq2Nmth0kAsDpkGKwijeW3+xV8s8zxQNB 45 | a2GaG2n/ExCjbdN1xYsz6bVaTPgTDCZtwlnGZBLA18e/gI2WOvFixpQBynU7ju8/ 46 | HaUJAoIBAB1miNZhV2maCG0WKj3ZcJ6+zCDFwBouPvPmgZRIJSLXpXPgRy3PxzC7 47 | Rrcm+6DjDMuXjtjXemZ+cbhqhNZ0PEkJrtkBwVSdVmK0LYKGEUQdNJ3bDjpP4ppv 48 | WwSmB31QY7osreVdXUoCrnvWskBYiAzNAIrFRnIUl0OzPHS/mKIFJ5iHODoAsQyo 49 | ud8AnBL9amKHb35gsTA+VG7RbBHZ1X4HBDQhKnaXWatiu9C5sP5KgM9wooHJmXTV 50 | ckR8pyAgHXI5zFbErYkg0k7ksZxCoJjYDsai6VwXRg13pmnc6puc8fPvRB24SdCa 51 | n2ldSo9TRot2yNsONoF004tEq93oiQc= 52 | -----END RSA PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /id_test.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestID(t *testing.T) { 15 | filename := "./testdata/dodyDol.crx" 16 | id, err := ID(filename) 17 | assert.Nil(t, err) 18 | assert.Equal(t, "kpkcennohgffjdgaelocingbmkjnpjgc", id) 19 | } 20 | 21 | func TestIDNegative(t *testing.T) { 22 | filename := filepath.Join(os.TempDir(), "extension.id.crx") 23 | buf := new(bytes.Buffer) 24 | buf.WriteString("Cr24") 25 | // version 4 26 | _ = binary.Write(buf, binary.LittleEndian, uint32(4)) 27 | err := os.WriteFile(filename, buf.Bytes(), os.ModePerm) 28 | assert.Nil(t, err) 29 | id, err := ID(filename) 30 | assert.Error(t, err) 31 | assert.Empty(t, id) 32 | assert.Nil(t, os.Remove(filename)) 33 | } 34 | 35 | func TestIDNegative_UnmarshalHeader(t *testing.T) { 36 | filename := filepath.Join(os.TempDir(), "extension.id.crx") 37 | buf := new(bytes.Buffer) 38 | buf.WriteString("Cr24") 39 | _ = binary.Write(buf, binary.LittleEndian, uint32(3)) 40 | _ = binary.Write(buf, binary.LittleEndian, uint32(256)) 41 | tmp := make([]byte, 512) 42 | for i := 0; i < 512; i++ { 43 | tmp[i] = byte(1) 44 | } 45 | buf.Write(tmp) 46 | err := os.WriteFile(filename, buf.Bytes(), os.ModePerm) 47 | assert.Nil(t, err) 48 | id, err := ID(filename) 49 | assert.Error(t, err) 50 | assert.Empty(t, id) 51 | assert.Nil(t, os.Remove(filename)) 52 | } 53 | 54 | func TestIDNegative_UnmarshalSignedData(t *testing.T) { 55 | filename := filepath.Join(os.TempDir(), "extension.id.crx") 56 | buf := new(bytes.Buffer) 57 | buf.WriteString("Cr24") 58 | _ = binary.Write(buf, binary.LittleEndian, uint32(3)) 59 | mockdata := []byte(`some data section`) 60 | header, err := makeHeader(mockdata, mockdata, mockdata) 61 | assert.Nil(t, err) 62 | _ = binary.Write(buf, binary.LittleEndian, uint32(len(header))) 63 | buf.Write(header) 64 | err = os.WriteFile(filename, buf.Bytes(), os.ModePerm) 65 | assert.Nil(t, err) 66 | id, err := ID(filename) 67 | assert.Error(t, err) 68 | assert.Empty(t, id) 69 | assert.Nil(t, os.Remove(filename)) 70 | } 71 | 72 | func TestIDFromPubKey(t *testing.T) { 73 | var pubKey1 = []byte(`MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAj/u/XDdjlDyw7gHEtaaasZ9GdG8WOKAyJzXd8HFrDtz2Jcuy7er7MtWvHgNDA0bwpznbI5YdZeV4UfCEsA4SrA5b3MnWTHwA1bgbiDM+L9rrqvcadcKuOlTeN48Q0ijmhHlNFbTzvT9W0zw/GKv8LgXAHggxtmHQ/Z9PP2QNF5O8rUHHSL4AJ6hNcEKSBVSmbbjeVm4gSXDuED5r0nwxvRtupDxGYp8IZpP5KlExqNu1nbkPc+igCTIB6XsqijagzxewUHCdovmkb2JNtskx/PMIEv+TvWIx2BzqGp71gSh/dV7SJ3rClvWd2xj8dtxG8FfAWDTIIi0qZXWn2QhizQIDAQAB`) 74 | var pubKey2 = []byte(` 75 | -----BEGIN PUBLIC KEY----- 76 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAj/u/XDdjlDyw7gHEtaaa 77 | sZ9GdG8WOKAyJzXd8HFrDtz2Jcuy7er7MtWvHgNDA0bwpznbI5YdZeV4UfCEsA4S 78 | rA5b3MnWTHwA1bgbiDM+L9rrqvcadcKuOlTeN48Q0ijmhHlNFbTzvT9W0zw/GKv8 79 | LgXAHggxtmHQ/Z9PP2QNF5O8rUHHSL4AJ6hNcEKSBVSmbbjeVm4gSXDuED5r0nwx 80 | vRtupDxGYp8IZpP5KlExqNu1nbkPc+igCTIB6XsqijagzxewUHCdovmkb2JNtskx 81 | /PMIEv+TvWIx2BzqGp71gSh/dV7SJ3rClvWd2xj8dtxG8FfAWDTIIi0qZXWn2Qhi 82 | zQIDAQAB 83 | -----END PUBLIC KEY----- 84 | `) 85 | expectedExtensionID := "lfoeajgcchlidpicbabpmckkejpckcfb" 86 | type args struct { 87 | pubKey []byte 88 | } 89 | tests := []struct { 90 | name string 91 | args args 92 | want string 93 | wantErr bool 94 | }{ 95 | { 96 | name: "should return error when public key is empty", 97 | args: args{pubKey: []byte{}}, 98 | wantErr: true, 99 | }, 100 | { 101 | name: "should reutrn error when public key is invalid", 102 | args: args{pubKey: []byte(strings.Repeat("a", 128))}, 103 | wantErr: true, 104 | }, 105 | { 106 | name: "should return extension ID from public key", 107 | args: args{pubKey: pubKey1}, 108 | want: expectedExtensionID, 109 | wantErr: false, 110 | }, 111 | { 112 | name: "should return error when base64 decoding fails", 113 | args: args{pubKey: []byte(strings.Repeat("~", 128))}, 114 | wantErr: true, 115 | }, 116 | { 117 | name: "should return extension ID from formatted public key", 118 | args: args{pubKey: pubKey2}, 119 | want: expectedExtensionID, 120 | wantErr: false, 121 | }, 122 | } 123 | for _, tt := range tests { 124 | t.Run(tt.name, func(t *testing.T) { 125 | got, err := IDFromPubKey(tt.args.pubKey) 126 | if (err != nil) != tt.wantErr { 127 | t.Errorf("IDFromPubKey() error = %v, wantErr %v", err, tt.wantErr) 128 | return 129 | } 130 | if got != tt.want { 131 | t.Errorf("IDFromPubKey() = %v, want %v", got, tt.want) 132 | } 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /crx3/commands/pubkey.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "archive/zip" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/base64" 8 | "encoding/json" 9 | "encoding/pem" 10 | "errors" 11 | "fmt" 12 | "os" 13 | "strings" 14 | 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | type pubkeyOpts struct { 19 | PrivateKeyPath string // from private key 20 | ManifestPath string // from manifest.json 21 | ExtensionPath string // from extension/manifest.json 22 | } 23 | 24 | func (opts pubkeyOpts) Validate() bool { 25 | return len(opts.PrivateKeyPath) != 0 || 26 | len(opts.ManifestPath) != 0 || 27 | len(opts.ExtensionPath) != 0 28 | } 29 | 30 | func newPubkeyCmd() *cobra.Command { 31 | var opts pubkeyOpts 32 | cmd := &cobra.Command{ 33 | Use: "pubkey", 34 | Short: "Extract the public key from a private key, manifest.json, or CRX/ZIP file. (default: from extension)", 35 | Long: `The 'pubkey' command extracts a public key from different sources: 36 | - From a private key file (PEM format) 37 | - From the 'key' field in manifest.json of a Chrome extension 38 | - From a CRX or ZIP extension file 39 | 40 | The extracted public key is output in DER format and Base64-encoded.`, 41 | RunE: func(cmd *cobra.Command, args []string) error { 42 | if !opts.Validate() { 43 | if len(args) == 0 { 44 | return errors.New("you need to specify a source to obtain the public key") 45 | } 46 | opts.ExtensionPath = args[0] 47 | } 48 | 49 | var pubkey string 50 | var err error 51 | switch { 52 | case len(opts.PrivateKeyPath) > 0: 53 | pubkey, err = extractPublicKeyFromPrivateKey(opts.PrivateKeyPath) 54 | case len(opts.ExtensionPath) > 0: 55 | pubkey, err = extractKeyFromExtension(opts.ExtensionPath) 56 | case len(opts.ManifestPath) > 0: 57 | pubkey, err = extractKeyFromManifest(opts.ManifestPath) 58 | } 59 | if err != nil { 60 | return err 61 | } 62 | 63 | fmt.Print(pubkey) 64 | 65 | return nil 66 | }, 67 | } 68 | 69 | cmd.Flags().StringVarP(&opts.PrivateKeyPath, "private", "p", "", "extract public key from a private key file (PEM format)") 70 | cmd.Flags().StringVarP(&opts.ManifestPath, "manifest", "m", "", "extract public key from the 'key' field in manifest.json") 71 | cmd.Flags().StringVarP(&opts.ExtensionPath, "extension", "e", "", "extract public key from a CRX or ZIP extension file") 72 | 73 | return cmd 74 | } 75 | 76 | func extractPublicKeyFromPrivateKey(privKeyPath string) (string, error) { 77 | data, err := os.ReadFile(privKeyPath) 78 | if err != nil { 79 | return "", fmt.Errorf("failed to read private key file: %w", err) 80 | } 81 | block, _ := pem.Decode(data) 82 | if block == nil { 83 | return "", fmt.Errorf("failed to parse PEM") 84 | } 85 | var privKey *rsa.PrivateKey 86 | if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { 87 | privKey = key 88 | } else { 89 | parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) 90 | if err != nil { 91 | return "", fmt.Errorf("failed to parse private key (unsupported format): %w", err) 92 | } 93 | var ok bool 94 | privKey, ok = parsedKey.(*rsa.PrivateKey) 95 | if !ok { 96 | return "", fmt.Errorf("parsed PKCS#8 but it's not an RSA private key") 97 | } 98 | } 99 | pubDER, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey) 100 | if err != nil { 101 | return "", fmt.Errorf("failed to marshal public key: %w", err) 102 | } 103 | return base64.StdEncoding.EncodeToString(pubDER), nil 104 | } 105 | 106 | func extractKeyFromExtension(crxPath string) (string, error) { 107 | r, err := zip.OpenReader(crxPath) 108 | if err != nil { 109 | return "", err 110 | } 111 | defer r.Close() 112 | var manifestData map[string]interface{} 113 | for _, f := range r.File { 114 | if f.Name == "manifest.json" { 115 | rc, err := f.Open() 116 | if err != nil { 117 | return "", err 118 | } 119 | defer rc.Close() 120 | err = json.NewDecoder(rc).Decode(&manifestData) 121 | if err != nil { 122 | return "", err 123 | } 124 | key, ok := manifestData["key"].(string) 125 | if !ok { 126 | return "", fmt.Errorf("key not found in manifest.json") 127 | } 128 | return strings.TrimSpace(key), nil 129 | } 130 | } 131 | return "", fmt.Errorf("manifest.json not found in CRX") 132 | } 133 | 134 | func extractKeyFromManifest(manifestPath string) (string, error) { 135 | file, err := os.Open(manifestPath) 136 | if err != nil { 137 | return "", err 138 | } 139 | defer file.Close() 140 | var manifestData map[string]interface{} 141 | err = json.NewDecoder(file).Decode(&manifestData) 142 | if err != nil { 143 | return "", err 144 | } 145 | key, ok := manifestData["key"].(string) 146 | if !ok { 147 | return "", fmt.Errorf("key not found in manifest.json") 148 | } 149 | return strings.TrimSpace(key), nil 150 | } 151 | -------------------------------------------------------------------------------- /pack_test.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rsa" 6 | "encoding/pem" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestPack(t *testing.T) { 16 | basePath, err := os.MkdirTemp("", "packtest") 17 | require.NoError(t, err) 18 | defer os.RemoveAll(basePath) 19 | 20 | type args struct { 21 | src string 22 | dst string 23 | pk *rsa.PrivateKey 24 | } 25 | tests := []struct { 26 | name string 27 | args args 28 | assert func() 29 | wantErr bool 30 | }{ 31 | { 32 | name: "should return error when src path is empty", 33 | args: args{ 34 | src: "", 35 | dst: "/path", 36 | }, 37 | wantErr: true, 38 | }, 39 | { 40 | name: "should return error when dst path is empty", 41 | args: args{ 42 | src: "/path/to", 43 | dst: "", 44 | }, 45 | wantErr: true, 46 | }, 47 | { 48 | name: "should return error when file does not crx suffix", 49 | args: args{ 50 | src: "./testdata/extension", 51 | dst: "somefile.png", 52 | }, 53 | wantErr: true, 54 | }, 55 | { 56 | name: "should return error when src does not exists", 57 | args: args{ 58 | src: "path/not/exists", 59 | dst: "gobyMo.crx", 60 | }, 61 | wantErr: true, 62 | }, 63 | { 64 | name: "should not return when src not zipped", 65 | args: args{ 66 | src: "./testdata/extension", 67 | dst: filepath.Join(basePath, "my.crx"), 68 | }, 69 | assert: func() { 70 | expectedCrx := filepath.Join(basePath, "my.crx") 71 | expectedPem := filepath.Join(basePath, "my.crx.pem") 72 | require.True(t, fileExists(expectedCrx)) 73 | require.True(t, fileExists(expectedPem)) 74 | }, 75 | }, 76 | { 77 | name: "should not return when src zipped", 78 | args: args{ 79 | src: "./testdata/bobbyMol.zip", 80 | dst: filepath.Join(basePath, "my.crx"), 81 | }, 82 | assert: func() { 83 | expectedCrx := filepath.Join(basePath, "my.crx") 84 | expectedPem := filepath.Join(basePath, "my.crx.pem") 85 | require.True(t, fileExists(expectedCrx)) 86 | require.True(t, fileExists(expectedPem)) 87 | }, 88 | }, 89 | } 90 | for _, tt := range tests { 91 | t.Run(tt.name, func(t *testing.T) { 92 | if err := Pack(tt.args.src, tt.args.dst, tt.args.pk); (err != nil) != tt.wantErr { 93 | t.Errorf("Pack() error = %v, wantErr %v", err, tt.wantErr) 94 | } 95 | }) 96 | } 97 | } 98 | 99 | func TestReadZipFile(t *testing.T) { 100 | type args struct { 101 | filename string 102 | } 103 | tests := []struct { 104 | name string 105 | args args 106 | wantErr bool 107 | }{ 108 | { 109 | name: "should return error when filename is empty", 110 | args: args{filename: ""}, 111 | wantErr: true, 112 | }, 113 | { 114 | name: "should return error when file does not exists", 115 | args: args{filename: "/path/not/exists"}, 116 | wantErr: true, 117 | }, 118 | { 119 | name: "should return error when file is not zip", 120 | args: args{filename: "./testdata/bobbyMol.crx"}, 121 | wantErr: true, 122 | }, 123 | { 124 | name: "should not return error when file is fake zip", 125 | args: args{filename: "./testdata/fake.zip"}, 126 | wantErr: true, 127 | }, 128 | { 129 | name: "should not return error when file is zip", 130 | args: args{filename: "./testdata/bobbyMol.zip"}, 131 | wantErr: false, 132 | }, 133 | { 134 | name: "should not return error when file is directory", 135 | args: args{filename: "./testdata/extension"}, 136 | wantErr: false, 137 | }, 138 | } 139 | for _, tt := range tests { 140 | t.Run(tt.name, func(t *testing.T) { 141 | r, err := ReadZipFile(tt.args.filename) 142 | if (err != nil) != tt.wantErr { 143 | t.Errorf("ReadZipFile() error = %v, wantErr %v", err, tt.wantErr) 144 | return 145 | } 146 | if err != nil { 147 | return 148 | } 149 | data, err := io.ReadAll(r) 150 | if err != nil { 151 | panic(err) 152 | } 153 | if len(data) == 0 { 154 | t.Errorf("ReadZipFile() data = %v, want not empty", data) 155 | } 156 | }) 157 | } 158 | } 159 | 160 | func TestWritePrivateKey(t *testing.T) { 161 | pk, err := NewPrivateKey() 162 | if err != nil { 163 | panic(err) 164 | } 165 | type args struct { 166 | key *rsa.PrivateKey 167 | } 168 | tests := []struct { 169 | name string 170 | args args 171 | wantErr bool 172 | }{ 173 | { 174 | name: "sould return error when private key is nil", 175 | args: args{key: nil}, 176 | wantErr: true, 177 | }, 178 | { 179 | name: "should write private key", 180 | args: args{key: pk}, 181 | }, 182 | } 183 | for _, tt := range tests { 184 | t.Run(tt.name, func(t *testing.T) { 185 | w := &bytes.Buffer{} 186 | err := WritePrivateKey(w, tt.args.key) 187 | if (err != nil) != tt.wantErr { 188 | t.Errorf("WritePrivateKey() error = %v, wantErr %v", err, tt.wantErr) 189 | return 190 | } 191 | if err != nil { 192 | return 193 | } 194 | block := &pem.Block{ 195 | Type: "RSA PRIVATE KEY", 196 | Bytes: w.Bytes(), 197 | } 198 | got := pem.EncodeToMemory(block) 199 | require.Equal(t, string(got), string(pem.EncodeToMemory(block))) 200 | }) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-crx3 2 | [![Coverage Status](https://coveralls.io/repos/github/mmadfox/go-crx3/badge.svg?branch=master)](https://coveralls.io/github/mmadfox/go-crx3?branch=master) 3 | [![Documentation](https://godoc.org/github.com/mediabuyerbot/go-crx3?status.svg)](https://pkg.go.dev/github.com/mediabuyerbot/go-crx3) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/mediabuyerbot/go-crx3)](https://goreportcard.com/report/github.com/mediabuyerbot/go-crx3) 5 | ![Actions](https://github.com/mmadfox/go-crx3/actions/workflows/cover.yml/badge.svg) 6 | 7 | Provides a sets of tools packing, unpacking, zip, unzip, download, gen id, etc... 8 | 9 | ## Table of contents 10 | + [Code examples](/examples/) 11 | + [Installation](#installation) 12 | + [Dev commands](#commands) 13 | + [Examples](#examples) 14 | - [Encode to base64 string](#base64) 15 | - [Pack a zip file or unzipped directory into a crx extension](#pack) 16 | - [Unpack chrome extension into current directory](#unpack) 17 | - [Download a chrome extension from the web store](#download) 18 | - [Add unpacked extension to archive](#zip) 19 | - [Unzip an extension to the directory](#unzip) 20 | - [Keygen](#keygen) 21 | - [Generate extension id](#gen-id) 22 | - [IsDir, IsZip, IsCRX3 helpers](#isdir-iszip-iscrx3) 23 | - [Load or save private key](#newprivatekey-loadprivatekey-saveprivatekey) 24 | + [License](#license) 25 | 26 | 27 | ### Installation 28 | ```ssh 29 | go get -u github.com/mediabuyerbot/go-crx3/crx3 30 | go install github.com/mediabuyerbot/go-crx3/crx3@latest 31 | ``` 32 | 33 | OR download the binary from here 34 | ``` 35 | https://github.com/mmadfox/go-crx3/releases 36 | ``` 37 | 38 | ### Dev commands 39 | ```shell script 40 | $ make proto 41 | $ make test/cover 42 | ``` 43 | 44 | ### Examples 45 | #### Pack 46 | ##### Pack a zip file or unzipped directory into a crx extension 47 | ```go 48 | import crx3 "github.com/mediabuyerbot/go-crx3" 49 | 50 | if err := crx3.Extension("/path/to/file.zip").Pack(nil); err != nil { 51 | panic(err) 52 | } 53 | ``` 54 | 55 | ```go 56 | import crx3 "github.com/mediabuyerbot/go-crx3" 57 | 58 | pk, err := crx3.LoadPrivateKey("/path/to/key.pem") 59 | if err != nil { 60 | panic(err) 61 | } 62 | if err := crx3.Extension("/path/to/file.zip").Pack(pk); err != nil { 63 | panic(err) 64 | } 65 | ``` 66 | 67 | ```go 68 | import crx3 "github.com/mediabuyerbot/go-crx3" 69 | 70 | pk, err := crx3.LoadPrivateKey("/path/to/key.pem") 71 | if err != nil { 72 | panic(err) 73 | } 74 | if err := crx3.Extension("/path/to/file.zip").PackTo("/path/to/ext.crx", pk); err != nil { 75 | panic(err) 76 | } 77 | ``` 78 | ```shell script 79 | $ crx3 pack /path/to/file.zip 80 | $ crx3 pack /path/to/file.zip -p /path/to/key.pem 81 | $ crx3 pack /path/to/file.zip -p /path/to/key.pem -o /path/to/ext.crx 82 | ``` 83 | 84 | #### Unpack 85 | ##### Unpack chrome extension into current directory 86 | ```go 87 | import crx3 "github.com/mediabuyerbot/go-crx3" 88 | 89 | if err := crx3.Extension("/path/to/ext.crx").Unpack(); err != nil { 90 | panic(err) 91 | } 92 | ``` 93 | ```shell script 94 | $ crx3 unpack /path/to/ext.crx 95 | ``` 96 | 97 | #### Base64 98 | ##### Encode an extension file to a base64 string 99 | ```go 100 | import crx3 "github.com/mediabuyerbot/go-crx3" 101 | import "fmt" 102 | 103 | b, err := crx3.Extension("/path/to/ext.crx").Base64() 104 | if err != nil { 105 | panic(err) 106 | } 107 | fmt.Println(string(b)) 108 | ``` 109 | ```shell script 110 | $ crx3 base64 /path/to/ext.crx [-o /path/to/file] 111 | ``` 112 | 113 | #### Download 114 | ##### Download a chrome extension from the web store 115 | ```go 116 | import crx3 "github.com/mediabuyerbot/go-crx3" 117 | 118 | extensionID := "blipmdconlkpinefehnmjammfjpmpbjk" 119 | filepath := "/path/to/ext.crx" 120 | if err := crx3.DownloadFromWebStore(extensionID,filepath); err != nil { 121 | panic(err) 122 | } 123 | ``` 124 | ```shell script 125 | $ crx3 download blipmdconlkpinefehnmjammfjpmpbjk [-o /custom/path] 126 | $ crx3 download https://chrome.google.com/webstore/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk 127 | ``` 128 | 129 | #### Zip 130 | ##### Zip add an unpacked extension to the archive 131 | ```go 132 | import crx3 "github.com/mediabuyerbot/go-crx3" 133 | 134 | if err := crx3.Extension("/path/to/unpacked").Zip(); err != nil { 135 | panic(err) 136 | } 137 | ``` 138 | ```shell script 139 | $ crx3 zip /path/to/unpacked [-o /custom/path] 140 | ``` 141 | 142 | #### Unzip 143 | ##### Unzip an extension to the current directory 144 | ```go 145 | import crx3 "github.com/mediabuyerbot/go-crx3" 146 | 147 | if err := crx3.Extension("/path/to/ext.zip").Unzip(); err != nil { 148 | panic(err) 149 | } 150 | ``` 151 | ```shell script 152 | $ crx3 unzip /path/to/ext.zip [-o /custom/path] 153 | ``` 154 | 155 | #### Gen ID 156 | ##### Generate extension id (like dgmchnekcpklnjppdmmjlgpmpohmpmgp) 157 | ```go 158 | import crx3 "github.com/mediabuyerbot/go-crx3" 159 | 160 | id, err := crx3.Extension("/path/to/ext.crx").ID() 161 | if err != nil { 162 | panic(err) 163 | } 164 | ``` 165 | ```shell script 166 | $ crx3 id /path/to/ext.crx 167 | ``` 168 | 169 | #### IsDir, IsZip, IsCRX3 170 | ```go 171 | import crx3 "github.com/mediabuyerbot/go-crx3" 172 | 173 | crx3.Extension("/path/to/ext.zip").IsZip() 174 | crx3.Extension("/path/to/ext").IsDir() 175 | crx3.Extension("/path/to/ext.crx").IsCRX3() 176 | ``` 177 | 178 | #### NewPrivateKey, LoadPrivateKey, SavePrivateKey 179 | ```go 180 | import crx3 "github.com/mediabuyerbot/go-crx3" 181 | 182 | pk, err := crx3.NewPrivateKey() 183 | if err != nil { 184 | panic(err) 185 | } 186 | if err := crx3.SavePrivateKey("/path/to/key.pem", pk); err != nil { 187 | panic(err) 188 | } 189 | pk, err = crx3.LoadPrivateKey("/path/to/key.pem") 190 | ``` 191 | 192 | #### Keygen 193 | ```shell script 194 | $ crx3 keygen /path/to/key.pem 195 | ``` 196 | 197 | ## License 198 | go-crx3 is released under the Apache 2.0 license. See [LICENSE.txt](https://github.com/mediabuyerbot/go-crx3/blob/master/LICENSE) 199 | -------------------------------------------------------------------------------- /extension.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "archive/zip" 5 | "crypto/rsa" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | // Extension represents an extension for google chrome. 16 | type Extension string 17 | 18 | // String returns a string representation. 19 | func (e Extension) String() string { 20 | return string(e) 21 | } 22 | 23 | // ID calculates the Chrome Extension ID for the Extension instance. 24 | // It supports directories, ZIP archives, and CRX3 files. If the extension is unpacked, 25 | // contained in a ZIP archive, or is a CRX3 file with a specified key in its manifest, 26 | // the ID is generated from this key. The function returns an error if the extension is empty, 27 | // the file cannot be read, the key is not found, or the file format is unsupported. 28 | func (e Extension) ID() (string, error) { 29 | if e.IsEmpty() { 30 | return "", fmt.Errorf("%w: %s", ErrPathNotFound, e) 31 | } 32 | switch { 33 | case e.IsDir(): 34 | manifest := manifestFile(e.String()) 35 | file, err := os.ReadFile(manifest) 36 | if err != nil { 37 | return "", fmt.Errorf("crx3: failed to read file %s: %w", manifest, err) 38 | } 39 | pubkey := parseKeyFromManifest(file) 40 | if len(pubkey) == 0 { 41 | return "", fmt.Errorf("crx3: failed to parse key from manifest file %s", manifest) 42 | } 43 | return IDFromPubKey([]byte(pubkey)) 44 | case e.IsZip(): 45 | pubkey, err := parseManifestFromZip(e.String()) 46 | if err != nil { 47 | return "", err 48 | } 49 | if len(pubkey) == 0 { 50 | return "", fmt.Errorf("crx3: failed to parse key from manifest file %s", e) 51 | } 52 | return IDFromPubKey([]byte(pubkey)) 53 | case e.IsCRX3(): 54 | pubkey, err := parseManifestFromZip(e.String()) 55 | if err != nil || len(pubkey) == 0 { 56 | return ID(e.String()) 57 | } 58 | if len(pubkey) > 0 { 59 | return IDFromPubKey([]byte(pubkey)) 60 | } 61 | } 62 | return "", fmt.Errorf("%w: %s", ErrUnknownFileExtension, e) 63 | } 64 | 65 | // IsEmpty checks if the extension is empty. 66 | func (e Extension) IsEmpty() bool { 67 | return len(e.String()) == 0 68 | } 69 | 70 | // IsDir reports whether extension describes a directory. 71 | func (e Extension) IsDir() bool { 72 | return isDir(e.String()) 73 | } 74 | 75 | // IsZip reports whether extension describes a zip-archive. 76 | func (e Extension) IsZip() bool { 77 | return isZip(e.String()) 78 | } 79 | 80 | // IsCRX3 reports whether extension describes a crx file. 81 | func (e Extension) IsCRX3() bool { 82 | return isCRX(e.String()) 83 | } 84 | 85 | // Zip creates a *.zip archive and adds all the files to it. 86 | func (e Extension) Zip() error { 87 | if e.IsEmpty() { 88 | return fmt.Errorf("%w: %s", ErrPathNotFound, e) 89 | } 90 | 91 | filename := strings.TrimRight(e.String(), "/") + zipExt 92 | file, err := os.Create(filename) 93 | if err != nil { 94 | return err 95 | } 96 | defer file.Close() 97 | 98 | return Zip(file, e.String()) 99 | } 100 | 101 | // Unzip extracts all files from the archive. 102 | func (e Extension) Unzip() error { 103 | if e.IsEmpty() { 104 | return fmt.Errorf("%w: %s", ErrPathNotFound, e) 105 | } 106 | 107 | file, err := os.Open(e.String()) 108 | if err != nil { 109 | return err 110 | } 111 | defer file.Close() 112 | 113 | stat, err := file.Stat() 114 | if err != nil { 115 | return err 116 | } 117 | 118 | unpacked := strings.TrimSuffix(e.String(), zipExt) 119 | if dirExists(unpacked) { 120 | index := 1 121 | for { 122 | if index >= 100 { 123 | break 124 | } 125 | unpacked = unpacked + "(" + strconv.Itoa(index) + ")" 126 | if !dirExists(unpacked) { 127 | break 128 | } 129 | index++ 130 | } 131 | } 132 | 133 | return Unzip(file, stat.Size(), unpacked) 134 | } 135 | 136 | // Base64 encodes an extension file to a base64 string. 137 | func (e Extension) Base64() ([]byte, error) { 138 | if e.IsEmpty() { 139 | return nil, fmt.Errorf("%w: %s", ErrPathNotFound, e) 140 | } 141 | return Base64(e.String()) 142 | } 143 | 144 | // Unpack unpacks the CRX3 extension into a directory. 145 | func (e Extension) Unpack() error { 146 | if e.IsEmpty() { 147 | return fmt.Errorf("%w: %s", ErrPathNotFound, e) 148 | } 149 | return Unpack(e.String()) 150 | } 151 | 152 | // PackTo packs zip file or an unpacked directory into a CRX3 file. 153 | func (e Extension) PackTo(dst string, pk *rsa.PrivateKey) error { 154 | if e.IsEmpty() { 155 | return ErrPathNotFound 156 | } 157 | return Pack(e.String(), dst, pk) 158 | } 159 | 160 | // Pack packs zip file or an unpacked directory into a CRX3 file. 161 | func (e Extension) Pack(pk *rsa.PrivateKey) error { 162 | if e.IsEmpty() { 163 | return ErrPathNotFound 164 | } 165 | dst := strings.TrimRight(e.String(), "/") + crxExt 166 | return Pack(e.String(), dst, pk) 167 | } 168 | 169 | // WriteTo packs the contents of the Extension into a CRX file and writes it to the provided io.Writer. 170 | // This method requires a non-nil *rsa.PrivateKey to sign the CRX package. The Extension must not be empty, 171 | // and its associated zip file must be readable and correctly formatted. 172 | // 173 | // Parameters: 174 | // 175 | // w - The io.Writer where the CRX file will be written. 176 | // pk - The RSA private key used for signing the CRX file. 177 | // 178 | // Returns: 179 | // 180 | // An error if the Extension is empty, if the private key is nil, if there are issues reading the 181 | // zip file associated with the Extension, or if there is a failure during the packing process. 182 | // Errors are wrapped with context to provide more details about the failure. 183 | // 184 | // Usage example: 185 | // 186 | // ext := Extension("path/to/your/extension/folder") // OR zip file 187 | // pk, err := rsa.GenerateKey(rand.Reader, 4096) 188 | // if err != nil { 189 | // log.Fatalf("Failed to generate private key: %v", err) 190 | // } 191 | // 192 | // var buf bytes.Buffer 193 | // if err := ext.WriteTo(&buf, pk); err != nil { 194 | // log.Printf("Failed to write CRX: %v", err) 195 | // } else { 196 | // // Use buf to save CRX to a file or further processing 197 | // } 198 | func (e Extension) WriteTo(w io.Writer, pk *rsa.PrivateKey) error { 199 | if e.IsEmpty() { 200 | return fmt.Errorf("%w: %s", ErrPathNotFound, e) 201 | } 202 | if pk == nil { 203 | return fmt.Errorf("%w: for extension %s", ErrPrivateKeyNotFound, e) 204 | } 205 | reader, err := readZipFile(e.String()) 206 | if err != nil { 207 | return fmt.Errorf("crx3: failed to read zip file: %w", err) 208 | } 209 | return PackZipToCRX(reader, w, pk) 210 | } 211 | 212 | func manifestFile(path string) string { 213 | return filepath.Join(path, "manifest.json") 214 | } 215 | 216 | func parseManifestFromZip(e string) (string, error) { 217 | zipReader, err := zip.OpenReader(e) 218 | if err != nil { 219 | return "", fmt.Errorf("crx3: failed to open zip reader: %w", err) 220 | } 221 | defer zipReader.Close() 222 | for _, file := range zipReader.File { 223 | baseFilename := filepath.Base(file.Name) 224 | if baseFilename == "manifest.json" { 225 | fileReader, err := file.Open() 226 | if err != nil { 227 | return "", fmt.Errorf("crx3: failed to open file %s: %w", file.Name, err) 228 | } 229 | defer fileReader.Close() 230 | data, err := io.ReadAll(fileReader) 231 | if err != nil { 232 | return "", fmt.Errorf("crx3: failed to read file %s: %w", file.Name, err) 233 | } 234 | pubkey := parseKeyFromManifest(data) 235 | if len(pubkey) == 0 { 236 | return "", fmt.Errorf("crx3: failed to parse key from manifest file %s", e) 237 | } 238 | return pubkey, nil 239 | } 240 | } 241 | return "", fmt.Errorf("crx3: failed to find manifest file %s in zip archive", e) 242 | } 243 | 244 | func parseKeyFromManifest(data []byte) string { 245 | key := struct { 246 | Key string `json:"key"` 247 | }{} 248 | if err := json.Unmarshal(data, &key); err != nil { 249 | return "" 250 | } 251 | return key.Key 252 | } 253 | 254 | func formatPemKey(key []byte) []byte { 255 | str := string(key) 256 | str = strings.TrimSpace(str) 257 | str = strings.Replace(str, "-----BEGIN RSA PUBLIC KEY-----", "", 1) 258 | str = strings.Replace(str, "-----END RSA PUBLIC KEY-----", "", 1) 259 | str = strings.Replace(str, "-----BEGIN PUBLIC KEY-----", "", 1) 260 | str = strings.Replace(str, "-----END PUBLIC KEY-----", "", 1) 261 | str = strings.TrimSpace(str) 262 | str = strings.ReplaceAll(str, "\n", "") 263 | return []byte(str) 264 | } 265 | -------------------------------------------------------------------------------- /pack.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/sha256" 9 | "crypto/x509" 10 | "encoding/binary" 11 | "encoding/pem" 12 | "fmt" 13 | "io" 14 | "os" 15 | "path" 16 | "strings" 17 | 18 | "github.com/mediabuyerbot/go-crx3/pb" 19 | 20 | "google.golang.org/protobuf/proto" 21 | ) 22 | 23 | // PackZipToCRX reads a ZIP archive from the provided Reader, signs it using 24 | // the provided RSA private key, and writes the signed CRX file to the provided Writer. 25 | // This function is essential for producing production-ready CRX files that require 26 | // digital signatures to be installed in browsers. The function will return an error 27 | // if any issues occur during the zip reading, signing, or CRX writing processes. 28 | func PackZipToCRX(zip io.ReadSeeker, w io.Writer, pk *rsa.PrivateKey) error { 29 | if zip == nil || w == nil || pk == nil { 30 | return fmt.Errorf("crx3/pack: zip or writer or privateKey is nil") 31 | } 32 | publicKey, err := makePublicKey(pk) 33 | if err != nil { 34 | return fmt.Errorf("crx3/pack: failed to make public key: %w", err) 35 | } 36 | signedData, err := makeSignedData(publicKey) 37 | if err != nil { 38 | return fmt.Errorf("crx3/pack: failed to make signed data: %w", err) 39 | } 40 | signature, err := makeSign(zip, signedData, pk) 41 | if err != nil { 42 | return fmt.Errorf("crx3/pack: failed to make signature: %w", err) 43 | } 44 | header, err := makeHeader(publicKey, signature, signedData) 45 | if err != nil { 46 | return fmt.Errorf("crx3/pack: failed to make header: %w", err) 47 | } 48 | if _, err := zip.Seek(0, 0); err != nil { 49 | return fmt.Errorf("crx3/pack: failed to seek zip: %w", err) 50 | } 51 | if err := copyZipToCRX(w, zip, header); err != nil { 52 | return fmt.Errorf("crx3/pack: failed to copy zip to crx data: %w", err) 53 | } 54 | return nil 55 | } 56 | 57 | // WritePrivateKey writes the RSA private key to the provided io.Writer in the PEM format. 58 | // The function expects a non-nil *rsa.PrivateKey. If the key is nil, it returns an 59 | // ErrPrivateKeyNotFound error. This function handles the marshalling of the private key 60 | // into PKCS#8 format and then encodes it into PEM format before writing. 61 | // 62 | // Parameters: 63 | // 64 | // w : An io.Writer to which the PEM encoded private key will be written. 65 | // key : A non-nil *rsa.PrivateKey that will be marshalled and written. 66 | // 67 | // Returns: 68 | // 69 | // An error if the private key is nil, if there is a marshalling error, or if writing 70 | // to the io.Writer fails. The error includes a descriptive message to aid in debugging. 71 | // 72 | // Usage example: 73 | // 74 | // file, err := os.Create("private_key.pem") 75 | // if err != nil { 76 | // log.Fatal(err) 77 | // } 78 | // defer file.Close() 79 | // 80 | // privKey, err := rsa.GenerateKey(rand.Reader, 2048) 81 | // if err != nil { 82 | // log.Fatal(err) 83 | // } 84 | // 85 | // if err := WritePrivateKey(file, privKey); err != nil { 86 | // log.Printf("Failed to write private key: %v", err) 87 | // } 88 | // 89 | // Note: 90 | // 91 | // This function does not close the io.Writer; the caller is responsible for managing 92 | // the writer's lifecycle, including opening and closing it. 93 | func WritePrivateKey(w io.Writer, key *rsa.PrivateKey) error { 94 | if key == nil { 95 | return ErrPrivateKeyNotFound 96 | } 97 | bytes, err := x509.MarshalPKCS8PrivateKey(key) 98 | if err != nil { 99 | return fmt.Errorf("crx3/pack: failed to marshal private key: %w", err) 100 | } 101 | block := &pem.Block{ 102 | Type: "RSA PRIVATE KEY", 103 | Bytes: bytes, 104 | } 105 | if _, err := w.Write(pem.EncodeToMemory(block)); err != nil { 106 | return fmt.Errorf("crx3/pack: failed to write private key: %w", err) 107 | } 108 | return nil 109 | } 110 | 111 | // ReadZipFile opens and reads the contents of a zip file specified by 'filename'. 112 | // It can handle both direct paths to zip files or directories. If 'filename' is a directory, 113 | // the function zips its contents into a buffer and returns a reader for that buffer. 114 | // If 'filename' is a zip file, it reads the file into a buffer and returns a reader for it. 115 | // The function returns a *bytes.Reader to allow random access reads, which is particularly 116 | // useful for large files. It returns an error if the file cannot be opened, read, or if the 117 | // path does not correspond to a zip file or directory. 118 | func ReadZipFile(filename string) (*bytes.Reader, error) { 119 | return readZipFile(filename) 120 | } 121 | 122 | func readZipFile(filename string) (*bytes.Reader, error) { 123 | var zipData bytes.Buffer 124 | 125 | switch { 126 | case isDir(filename): 127 | if err := Zip(&zipData, filename); err != nil { 128 | return nil, err 129 | } 130 | case isZip(filename): 131 | file, err := os.Open(filename) 132 | if err != nil { 133 | return nil, err 134 | } 135 | defer file.Close() 136 | if _, err := io.Copy(&zipData, file); err != nil { 137 | return nil, err 138 | } 139 | default: 140 | return nil, ErrUnknownFileExtension 141 | } 142 | 143 | return bytes.NewReader(zipData.Bytes()), nil 144 | } 145 | 146 | const ( 147 | crxExt = ".crx" 148 | zipExt = ".zip" 149 | pemExt = ".pem" 150 | ) 151 | 152 | // Pack packs a zip file or unzipped directory into a crx extension. 153 | // It takes the source 'src' (zip file or directory), target 'dst' CRX file path, 154 | // and a private key 'pk' (optional). If 'pk' is nil, it generates a new private key. 155 | // It creates a CRX extension from the source and writes it to the destination. 156 | func Pack(src string, dst string, pk *rsa.PrivateKey) (err error) { 157 | var ( 158 | publicKey []byte 159 | signedData []byte 160 | signature []byte 161 | header []byte 162 | hasDst = len(dst) > 0 163 | isDefaultPk bool 164 | isNotCrxSuffix = path.Ext(dst) != crxExt 165 | ) 166 | 167 | if len(src) == 0 || len(dst) == 0 { 168 | return fmt.Errorf("%w, source or destination is empty", 169 | ErrPathNotFound) 170 | } 171 | 172 | if hasDst && isNotCrxSuffix { 173 | return fmt.Errorf("%w, destination file must have a .crx extension", 174 | ErrUnknownFileExtension) 175 | } 176 | 177 | zipData, err := readZipFile(src) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | // make default private key 183 | if pk == nil { 184 | pk, err = NewPrivateKey() 185 | if err != nil { 186 | return err 187 | } 188 | isDefaultPk = true 189 | } 190 | 191 | if publicKey, err = makePublicKey(pk); err != nil { 192 | return err 193 | } 194 | if signedData, err = makeSignedData(publicKey); err != nil { 195 | return err 196 | } 197 | if signature, err = makeSign(zipData, signedData, pk); err != nil { 198 | return err 199 | } 200 | if header, err = makeHeader(publicKey, signature, signedData); err != nil { 201 | return err 202 | } 203 | if _, err := zipData.Seek(0, 0); err != nil { 204 | return err 205 | } 206 | 207 | if !hasDst { 208 | crxFilename := strings.TrimSuffix(src, zipExt) 209 | crxFilename = crxFilename + crxExt 210 | dst = crxFilename 211 | } 212 | if err := writeToCRX(dst, zipData, header); err != nil { 213 | return err 214 | } 215 | if isDefaultPk { 216 | if err := saveDefaultPrivateKey(dst, pk); err != nil { 217 | return err 218 | } 219 | } 220 | return nil 221 | } 222 | 223 | func writeToCRX(filename string, zipFile io.ReadSeeker, header []byte) error { 224 | crx, err := os.Create(filename) 225 | if err != nil { 226 | return err 227 | } 228 | if _, err = crx.Write([]byte("Cr24")); err != nil { 229 | return err 230 | } 231 | if err := binary.Write(crx, binary.LittleEndian, uint32(3)); err != nil { 232 | return err 233 | } 234 | if err := binary.Write(crx, binary.LittleEndian, uint32(len(header))); err != nil { 235 | return err 236 | } 237 | if _, err := crx.Write(header); err != nil { 238 | return err 239 | } 240 | if _, err := io.Copy(crx, zipFile); err != nil { 241 | return err 242 | } 243 | return nil 244 | } 245 | 246 | func copyZipToCRX(crx io.Writer, zipFile io.ReadSeeker, header []byte) error { 247 | if _, err := crx.Write([]byte("Cr24")); err != nil { 248 | return err 249 | } 250 | if err := binary.Write(crx, binary.LittleEndian, uint32(3)); err != nil { 251 | return err 252 | } 253 | if err := binary.Write(crx, binary.LittleEndian, uint32(len(header))); err != nil { 254 | return err 255 | } 256 | if _, err := crx.Write(header); err != nil { 257 | return err 258 | } 259 | if _, err := io.Copy(crx, zipFile); err != nil { 260 | return err 261 | } 262 | return nil 263 | } 264 | 265 | func makeCRXID(publicKey []byte) []byte { 266 | hash := sha256.New() 267 | hash.Write(publicKey) 268 | return hash.Sum(nil)[0:16] 269 | } 270 | 271 | func makePublicKey(pk *rsa.PrivateKey) ([]byte, error) { 272 | return x509.MarshalPKIXPublicKey(&pk.PublicKey) 273 | } 274 | 275 | func makeSignedData(publicKey []byte) ([]byte, error) { 276 | signedData := &pb.SignedData{ 277 | CrxId: makeCRXID(publicKey), 278 | } 279 | return proto.Marshal(signedData) 280 | } 281 | 282 | func makeSign(r io.Reader, signedData []byte, pk *rsa.PrivateKey) ([]byte, error) { 283 | sign := sha256.New() 284 | sign.Write([]byte("CRX3 SignedData\x00")) 285 | if err := binary.Write(sign, binary.LittleEndian, uint32(len(signedData))); err != nil { 286 | return nil, err 287 | } 288 | sign.Write(signedData) 289 | if _, err := io.Copy(sign, r); err != nil { 290 | return nil, err 291 | } 292 | return rsa.SignPKCS1v15(rand.Reader, pk, crypto.SHA256, sign.Sum(nil)) 293 | } 294 | 295 | func makeHeader(pubKey, signature, signedData []byte) ([]byte, error) { 296 | header := &pb.CrxFileHeader{ 297 | Sha256WithRsa: []*pb.AsymmetricKeyProof{ 298 | { 299 | PublicKey: pubKey, 300 | Signature: signature, 301 | }, 302 | }, 303 | SignedHeaderData: signedData, 304 | } 305 | return proto.Marshal(header) 306 | } 307 | 308 | func saveDefaultPrivateKey(filename string, pk *rsa.PrivateKey) error { 309 | pemFilename := strings.TrimSuffix(filename, zipExt) 310 | pemFilename = pemFilename + pemExt 311 | return SavePrivateKey(pemFilename, pk) 312 | } 313 | -------------------------------------------------------------------------------- /extension_test.go: -------------------------------------------------------------------------------- 1 | package crx3 2 | 3 | import ( 4 | "crypto/rsa" 5 | "errors" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestExtension_IsEmpty(t *testing.T) { 15 | require.True(t, Extension("").IsEmpty()) 16 | require.False(t, Extension("path/to/ext").IsEmpty()) 17 | } 18 | 19 | func TestExtension_IsDir(t *testing.T) { 20 | ok := Extension("./testdata/extension").IsDir() 21 | assert.True(t, ok) 22 | ok = Extension("./testdata/dodyDol.crx").IsDir() 23 | assert.False(t, ok) 24 | } 25 | 26 | func TestExtension_IsZip(t *testing.T) { 27 | ok := Extension("./testdata/bobbyMol.zip").IsZip() 28 | assert.True(t, ok) 29 | ok = Extension("./testdata/dodyDol.crx").IsZip() 30 | assert.False(t, ok) 31 | } 32 | 33 | func TestExtension_IsCRX3(t *testing.T) { 34 | ok := Extension("./testdata/dodyDol.crx").IsCRX3() 35 | assert.True(t, ok) 36 | ok = Extension("./testdata/bobbyMol.zip").IsCRX3() 37 | assert.False(t, ok) 38 | } 39 | 40 | func TestExtension_Zip(t *testing.T) { 41 | basePath, err := os.MkdirTemp("", "ziptest") 42 | require.NoError(t, err) 43 | defer os.RemoveAll(basePath) 44 | 45 | src := "./testdata/bobbyMol.zip" 46 | dst := filepath.Join(basePath, "bobbyMol.zip") 47 | _, err = CopyFile(src, dst) 48 | require.NoError(t, err) 49 | require.NoError(t, UnzipTo(basePath, dst)) 50 | os.Remove(dst) 51 | 52 | tests := []struct { 53 | name string 54 | e Extension 55 | assert func() 56 | wantErr bool 57 | }{ 58 | { 59 | name: "should return error when extension is empty", 60 | e: Extension(""), 61 | wantErr: true, 62 | }, 63 | { 64 | name: "should return error when dir does not exists", 65 | e: Extension("path/not/exists"), 66 | wantErr: true, 67 | }, 68 | { 69 | name: "should not return error when all params are valid", 70 | e: Extension(filepath.Join(basePath, "bobbyMol")), 71 | assert: func() { 72 | expected := dst 73 | require.True(t, isZip(expected)) 74 | }, 75 | }, 76 | } 77 | for _, tt := range tests { 78 | t.Run(tt.name, func(t *testing.T) { 79 | if err := tt.e.Zip(); (err != nil) != tt.wantErr { 80 | t.Errorf("Extension.Zip() error = %v, wantErr %v", err, tt.wantErr) 81 | } 82 | if tt.assert != nil { 83 | tt.assert() 84 | } 85 | }) 86 | } 87 | } 88 | 89 | func TestExtension_Unzip(t *testing.T) { 90 | basePath, err := os.MkdirTemp("", "unziptest") 91 | require.NoError(t, err) 92 | defer os.RemoveAll(basePath) 93 | 94 | src := "./testdata/bobbyMol.zip" 95 | dst := filepath.Join(basePath, "bobbyMol.zip") 96 | _, err = CopyFile(src, dst) 97 | require.NoError(t, err) 98 | 99 | tests := []struct { 100 | name string 101 | e Extension 102 | arrange func() 103 | assert func() 104 | wantErr bool 105 | }{ 106 | { 107 | name: "should return error when extension is empty", 108 | e: Extension(""), 109 | wantErr: true, 110 | }, 111 | { 112 | name: "should return error when zip does not exists", 113 | e: Extension("file/not/found.zip"), 114 | wantErr: true, 115 | }, 116 | { 117 | name: "should not return error when dir exists", 118 | e: Extension(dst), 119 | arrange: func() { 120 | os.Mkdir(filepath.Join(basePath, "bobbyMol"), 0755) 121 | }, 122 | assert: func() { 123 | expected := filepath.Join(basePath, "bobbyMol(1)") 124 | require.True(t, isDir(expected)) 125 | }, 126 | }, 127 | { 128 | name: "should not return error when all params are valid", 129 | e: Extension(dst), 130 | arrange: func() { 131 | os.RemoveAll(filepath.Join(basePath, "bobbyMol(1)")) 132 | os.RemoveAll(filepath.Join(basePath, "bobbyMol")) 133 | }, 134 | assert: func() { 135 | expected := filepath.Join(basePath, "bobbyMol") 136 | require.True(t, isDir(expected)) 137 | }, 138 | }, 139 | } 140 | for _, tt := range tests { 141 | t.Run(tt.name, func(t *testing.T) { 142 | if tt.arrange != nil { 143 | tt.arrange() 144 | } 145 | if err := tt.e.Unzip(); (err != nil) != tt.wantErr { 146 | t.Errorf("Extension.Unzip() error = %v, wantErr %v", err, tt.wantErr) 147 | } 148 | if tt.assert != nil { 149 | tt.assert() 150 | } 151 | }) 152 | } 153 | } 154 | 155 | func TestExtension_ToBase64(t *testing.T) { 156 | _, err := Extension("").Base64() 157 | assert.True(t, errors.Is(err, ErrPathNotFound)) 158 | b, err := Extension("./testdata/dodyDol.crx").Base64() 159 | assert.Nil(t, err) 160 | assert.NotEmpty(t, b) 161 | } 162 | 163 | func TestExtension_Unpack(t *testing.T) { 164 | basePath, err := os.MkdirTemp("", "unpacktest") 165 | require.NoError(t, err) 166 | defer os.RemoveAll(basePath) 167 | 168 | src := "./testdata/dodyDol.crx" 169 | dst := filepath.Join(basePath, "dodyDol.crx") 170 | _, err = CopyFile(src, dst) 171 | require.NoError(t, err) 172 | 173 | tests := []struct { 174 | name string 175 | e Extension 176 | assert func() 177 | wantErr bool 178 | }{ 179 | { 180 | name: "should return error when extension is empty", 181 | e: Extension(""), 182 | wantErr: true, 183 | }, 184 | { 185 | name: "should not return error when all params is valid", 186 | e: Extension(dst), 187 | assert: func() { 188 | expected := filepath.Join(basePath, "dodyDol") 189 | require.True(t, isDir(expected)) 190 | }, 191 | }, 192 | } 193 | for _, tt := range tests { 194 | t.Run(tt.name, func(t *testing.T) { 195 | if err := tt.e.Unpack(); (err != nil) != tt.wantErr { 196 | t.Errorf("Extension.Unpack() error = %v, wantErr %v", err, tt.wantErr) 197 | } 198 | if tt.assert != nil { 199 | tt.assert() 200 | } 201 | }) 202 | } 203 | } 204 | 205 | func TestExtension_PackTo(t *testing.T) { 206 | basePath, err := os.MkdirTemp("", "packtotest") 207 | require.NoError(t, err) 208 | defer os.RemoveAll(basePath) 209 | 210 | src := "./testdata/bobbyMol.zip" 211 | dst := filepath.Join(basePath, "bobbyMol.zip") 212 | _, err = CopyFile(src, dst) 213 | require.NoError(t, err) 214 | require.NoError(t, UnzipTo(basePath, dst)) 215 | os.Remove(dst) 216 | 217 | type args struct { 218 | dst string 219 | pk *rsa.PrivateKey 220 | } 221 | tests := []struct { 222 | name string 223 | e Extension 224 | assert func() 225 | args args 226 | wantErr bool 227 | }{ 228 | { 229 | name: "should return error when extension is empty", 230 | e: Extension(""), 231 | wantErr: true, 232 | }, 233 | { 234 | name: "should not return when all params are valid", 235 | e: Extension(filepath.Join(basePath, "bobbyMol")), 236 | args: args{ 237 | dst: filepath.Join(basePath, "bobbyMol.crx"), 238 | }, 239 | assert: func() { 240 | require.True(t, fileExists(filepath.Join(basePath, "bobbyMol.crx"))) 241 | }, 242 | }, 243 | } 244 | for _, tt := range tests { 245 | t.Run(tt.name, func(t *testing.T) { 246 | if err := tt.e.PackTo(tt.args.dst, tt.args.pk); (err != nil) != tt.wantErr { 247 | t.Errorf("Extension.PackTo() error = %v, wantErr %v", err, tt.wantErr) 248 | } 249 | if tt.assert != nil { 250 | tt.assert() 251 | } 252 | }) 253 | } 254 | } 255 | 256 | func TestExtension_Pack(t *testing.T) { 257 | basePath, err := os.MkdirTemp("", "packtest") 258 | require.NoError(t, err) 259 | defer os.RemoveAll(basePath) 260 | 261 | src := "./testdata/bobbyMol.zip" 262 | dst := filepath.Join(basePath, "bobbyMol.zip") 263 | _, err = CopyFile(src, dst) 264 | require.NoError(t, err) 265 | require.NoError(t, UnzipTo(basePath, dst)) 266 | os.Remove(dst) 267 | 268 | tests := []struct { 269 | name string 270 | e Extension 271 | assert func() 272 | wantErr bool 273 | }{ 274 | { 275 | name: "should return error when extension is empty", 276 | e: Extension(""), 277 | wantErr: true, 278 | }, 279 | { 280 | name: "should not return when all params are valid", 281 | e: Extension(filepath.Join(basePath, "bobbyMol")), 282 | assert: func() { 283 | require.True(t, fileExists(filepath.Join(basePath, "bobbyMol.crx"))) 284 | }, 285 | }, 286 | } 287 | for _, tt := range tests { 288 | t.Run(tt.name, func(t *testing.T) { 289 | if err := tt.e.Pack(nil); (err != nil) != tt.wantErr { 290 | t.Errorf("Extension.Pack() error = %v, wantErr %v", err, tt.wantErr) 291 | } 292 | if tt.assert != nil { 293 | tt.assert() 294 | } 295 | }) 296 | } 297 | } 298 | 299 | func TestExtension_ID(t *testing.T) { 300 | tests := []struct { 301 | name string 302 | e Extension 303 | want string 304 | wantErr bool 305 | }{ 306 | { 307 | name: "should return error when extension is empty", 308 | e: Extension(""), 309 | wantErr: true, 310 | }, 311 | { 312 | name: "should return error when extension is not found", 313 | e: Extension("./testdata/withkey.crx.pem"), 314 | wantErr: true, 315 | }, 316 | { 317 | name: "should return error when key not found in manifest", 318 | e: Extension("./testdata/doodyDol.zip"), 319 | wantErr: true, 320 | }, 321 | { 322 | name: "should return extension id from unpacked extension", 323 | e: Extension("./testdata/extension"), 324 | want: "nigbihjmcbekdlkgdceknpanajdpncle", 325 | wantErr: false, 326 | }, 327 | { 328 | name: "should return extension id from zipped extension", 329 | e: Extension("./testdata/withkey.zip"), 330 | want: "nigbihjmcbekdlkgdceknpanajdpncle", 331 | wantErr: false, 332 | }, 333 | { 334 | name: "should return extension id from crx extension manifest", 335 | e: Extension("./testdata/withkey.crx"), 336 | want: "nigbihjmcbekdlkgdceknpanajdpncle", 337 | wantErr: false, 338 | }, 339 | { 340 | name: "should return extension id from crx extension", 341 | e: Extension("./testdata/dodyDol.crx"), 342 | want: "kpkcennohgffjdgaelocingbmkjnpjgc", 343 | }, 344 | } 345 | for _, tt := range tests { 346 | t.Run(tt.name, func(t *testing.T) { 347 | got, err := tt.e.ID() 348 | if (err != nil) != tt.wantErr { 349 | t.Errorf("Extension.ID() error = %v, wantErr %v", err, tt.wantErr) 350 | return 351 | } 352 | if got != tt.want { 353 | t.Errorf("Extension.ID() = %v, want %v", got, tt.want) 354 | } 355 | }) 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 MediaBuyerBot 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /pb/crx3.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file 4 | 5 | // Code generated by protoc-gen-go. DO NOT EDIT. 6 | // versions: 7 | // protoc-gen-go v1.26.0 8 | // protoc v3.17.3 9 | // source: pb/crx3.proto 10 | 11 | package pb 12 | 13 | import ( 14 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 15 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 16 | reflect "reflect" 17 | sync "sync" 18 | ) 19 | 20 | const ( 21 | // Verify that this generated code is sufficiently up-to-date. 22 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 23 | // Verify that runtime/protoimpl is sufficiently up-to-date. 24 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 25 | ) 26 | 27 | type CrxFileHeader struct { 28 | state protoimpl.MessageState 29 | sizeCache protoimpl.SizeCache 30 | unknownFields protoimpl.UnknownFields 31 | 32 | // PSS signature with RSA public key. The public key is formatted as a 33 | // X.509 SubjectPublicKeyInfo block, as in CRX₂. In the common case of a 34 | // developer key proof, the first 128 bits of the SHA-256 hash of the 35 | // public key must equal the crx_id. 36 | Sha256WithRsa []*AsymmetricKeyProof `protobuf:"bytes,2,rep,name=sha256_with_rsa,json=sha256WithRsa" json:"sha256_with_rsa,omitempty"` 37 | // ECDSA signature, using the NIST P-256 curve. Public key appears in 38 | // named-curve format. 39 | // The pinned algorithm will be this, at least on 2017-01-01. 40 | Sha256WithEcdsa []*AsymmetricKeyProof `protobuf:"bytes,3,rep,name=sha256_with_ecdsa,json=sha256WithEcdsa" json:"sha256_with_ecdsa,omitempty"` 41 | // The binary form of a SignedData message. We do not use a nested 42 | // SignedData message, as handlers of this message must verify the proofs 43 | // on exactly these bytes, so it is convenient to parse in two steps. 44 | // 45 | // All proofs in this CrxFile message are on the value 46 | // "CRX3 SignedData\x00" + signed_header_size + signed_header_data + 47 | // archive, where "\x00" indicates an octet with value 0, "CRX3 SignedData" 48 | // is encoded using UTF-8, signed_header_size is the size in octets of the 49 | // contents of this field and is encoded using 4 octets in little-endian 50 | // order, signed_header_data is exactly the content of this field, and 51 | // archive is the remaining contents of the file following the header. 52 | SignedHeaderData []byte `protobuf:"bytes,10000,opt,name=signed_header_data,json=signedHeaderData" json:"signed_header_data,omitempty"` 53 | } 54 | 55 | func (x *CrxFileHeader) Reset() { 56 | *x = CrxFileHeader{} 57 | if protoimpl.UnsafeEnabled { 58 | mi := &file_pb_crx3_proto_msgTypes[0] 59 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 60 | ms.StoreMessageInfo(mi) 61 | } 62 | } 63 | 64 | func (x *CrxFileHeader) String() string { 65 | return protoimpl.X.MessageStringOf(x) 66 | } 67 | 68 | func (*CrxFileHeader) ProtoMessage() {} 69 | 70 | func (x *CrxFileHeader) ProtoReflect() protoreflect.Message { 71 | mi := &file_pb_crx3_proto_msgTypes[0] 72 | if protoimpl.UnsafeEnabled && x != nil { 73 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 74 | if ms.LoadMessageInfo() == nil { 75 | ms.StoreMessageInfo(mi) 76 | } 77 | return ms 78 | } 79 | return mi.MessageOf(x) 80 | } 81 | 82 | // Deprecated: Use CrxFileHeader.ProtoReflect.Descriptor instead. 83 | func (*CrxFileHeader) Descriptor() ([]byte, []int) { 84 | return file_pb_crx3_proto_rawDescGZIP(), []int{0} 85 | } 86 | 87 | func (x *CrxFileHeader) GetSha256WithRsa() []*AsymmetricKeyProof { 88 | if x != nil { 89 | return x.Sha256WithRsa 90 | } 91 | return nil 92 | } 93 | 94 | func (x *CrxFileHeader) GetSha256WithEcdsa() []*AsymmetricKeyProof { 95 | if x != nil { 96 | return x.Sha256WithEcdsa 97 | } 98 | return nil 99 | } 100 | 101 | func (x *CrxFileHeader) GetSignedHeaderData() []byte { 102 | if x != nil { 103 | return x.SignedHeaderData 104 | } 105 | return nil 106 | } 107 | 108 | type AsymmetricKeyProof struct { 109 | state protoimpl.MessageState 110 | sizeCache protoimpl.SizeCache 111 | unknownFields protoimpl.UnknownFields 112 | 113 | PublicKey []byte `protobuf:"bytes,1,opt,name=public_key,json=publicKey" json:"public_key,omitempty"` 114 | Signature []byte `protobuf:"bytes,2,opt,name=signature" json:"signature,omitempty"` 115 | } 116 | 117 | func (x *AsymmetricKeyProof) Reset() { 118 | *x = AsymmetricKeyProof{} 119 | if protoimpl.UnsafeEnabled { 120 | mi := &file_pb_crx3_proto_msgTypes[1] 121 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 122 | ms.StoreMessageInfo(mi) 123 | } 124 | } 125 | 126 | func (x *AsymmetricKeyProof) String() string { 127 | return protoimpl.X.MessageStringOf(x) 128 | } 129 | 130 | func (*AsymmetricKeyProof) ProtoMessage() {} 131 | 132 | func (x *AsymmetricKeyProof) ProtoReflect() protoreflect.Message { 133 | mi := &file_pb_crx3_proto_msgTypes[1] 134 | if protoimpl.UnsafeEnabled && x != nil { 135 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 136 | if ms.LoadMessageInfo() == nil { 137 | ms.StoreMessageInfo(mi) 138 | } 139 | return ms 140 | } 141 | return mi.MessageOf(x) 142 | } 143 | 144 | // Deprecated: Use AsymmetricKeyProof.ProtoReflect.Descriptor instead. 145 | func (*AsymmetricKeyProof) Descriptor() ([]byte, []int) { 146 | return file_pb_crx3_proto_rawDescGZIP(), []int{1} 147 | } 148 | 149 | func (x *AsymmetricKeyProof) GetPublicKey() []byte { 150 | if x != nil { 151 | return x.PublicKey 152 | } 153 | return nil 154 | } 155 | 156 | func (x *AsymmetricKeyProof) GetSignature() []byte { 157 | if x != nil { 158 | return x.Signature 159 | } 160 | return nil 161 | } 162 | 163 | type SignedData struct { 164 | state protoimpl.MessageState 165 | sizeCache protoimpl.SizeCache 166 | unknownFields protoimpl.UnknownFields 167 | 168 | // This is simple binary, not UTF-8 encoded mpdecimal; i.e. it is exactly 169 | // 16 bytes long. 170 | CrxId []byte `protobuf:"bytes,1,opt,name=crx_id,json=crxId" json:"crx_id,omitempty"` 171 | } 172 | 173 | func (x *SignedData) Reset() { 174 | *x = SignedData{} 175 | if protoimpl.UnsafeEnabled { 176 | mi := &file_pb_crx3_proto_msgTypes[2] 177 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 178 | ms.StoreMessageInfo(mi) 179 | } 180 | } 181 | 182 | func (x *SignedData) String() string { 183 | return protoimpl.X.MessageStringOf(x) 184 | } 185 | 186 | func (*SignedData) ProtoMessage() {} 187 | 188 | func (x *SignedData) ProtoReflect() protoreflect.Message { 189 | mi := &file_pb_crx3_proto_msgTypes[2] 190 | if protoimpl.UnsafeEnabled && x != nil { 191 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 192 | if ms.LoadMessageInfo() == nil { 193 | ms.StoreMessageInfo(mi) 194 | } 195 | return ms 196 | } 197 | return mi.MessageOf(x) 198 | } 199 | 200 | // Deprecated: Use SignedData.ProtoReflect.Descriptor instead. 201 | func (*SignedData) Descriptor() ([]byte, []int) { 202 | return file_pb_crx3_proto_rawDescGZIP(), []int{2} 203 | } 204 | 205 | func (x *SignedData) GetCrxId() []byte { 206 | if x != nil { 207 | return x.CrxId 208 | } 209 | return nil 210 | } 211 | 212 | var File_pb_crx3_proto protoreflect.FileDescriptor 213 | 214 | var file_pb_crx3_proto_rawDesc = []byte{ 215 | 0x0a, 0x0d, 0x70, 0x62, 0x2f, 0x63, 0x72, 0x78, 0x33, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 216 | 0x02, 0x70, 0x62, 0x22, 0xc2, 0x01, 0x0a, 0x0d, 0x43, 0x72, 0x78, 0x46, 0x69, 0x6c, 0x65, 0x48, 217 | 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x0f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x5f, 218 | 0x77, 0x69, 0x74, 0x68, 0x5f, 0x72, 0x73, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 219 | 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x73, 0x79, 0x6d, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4b, 0x65, 220 | 0x79, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x0d, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x57, 0x69, 221 | 0x74, 0x68, 0x52, 0x73, 0x61, 0x12, 0x42, 0x0a, 0x11, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x5f, 222 | 0x77, 0x69, 0x74, 0x68, 0x5f, 0x65, 0x63, 0x64, 0x73, 0x61, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 223 | 0x32, 0x16, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x73, 0x79, 0x6d, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 224 | 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x0f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 225 | 0x57, 0x69, 0x74, 0x68, 0x45, 0x63, 0x64, 0x73, 0x61, 0x12, 0x2d, 0x0a, 0x12, 0x73, 0x69, 0x67, 226 | 0x6e, 0x65, 0x64, 0x5f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 227 | 0x90, 0x4e, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x48, 0x65, 228 | 0x61, 0x64, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x22, 0x51, 0x0a, 0x12, 0x41, 0x73, 0x79, 0x6d, 229 | 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x1d, 230 | 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 231 | 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 232 | 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 233 | 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x23, 0x0a, 0x0a, 0x53, 234 | 0x69, 0x67, 0x6e, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12, 0x15, 0x0a, 0x06, 0x63, 0x72, 0x78, 235 | 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x72, 0x78, 0x49, 0x64, 236 | 0x42, 0x0b, 0x48, 0x03, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 0x62, 0x2f, 0x70, 0x62, 237 | } 238 | 239 | var ( 240 | file_pb_crx3_proto_rawDescOnce sync.Once 241 | file_pb_crx3_proto_rawDescData = file_pb_crx3_proto_rawDesc 242 | ) 243 | 244 | func file_pb_crx3_proto_rawDescGZIP() []byte { 245 | file_pb_crx3_proto_rawDescOnce.Do(func() { 246 | file_pb_crx3_proto_rawDescData = protoimpl.X.CompressGZIP(file_pb_crx3_proto_rawDescData) 247 | }) 248 | return file_pb_crx3_proto_rawDescData 249 | } 250 | 251 | var file_pb_crx3_proto_msgTypes = make([]protoimpl.MessageInfo, 3) 252 | var file_pb_crx3_proto_goTypes = []interface{}{ 253 | (*CrxFileHeader)(nil), // 0: pb.CrxFileHeader 254 | (*AsymmetricKeyProof)(nil), // 1: pb.AsymmetricKeyProof 255 | (*SignedData)(nil), // 2: pb.SignedData 256 | } 257 | var file_pb_crx3_proto_depIdxs = []int32{ 258 | 1, // 0: pb.CrxFileHeader.sha256_with_rsa:type_name -> pb.AsymmetricKeyProof 259 | 1, // 1: pb.CrxFileHeader.sha256_with_ecdsa:type_name -> pb.AsymmetricKeyProof 260 | 2, // [2:2] is the sub-list for method output_type 261 | 2, // [2:2] is the sub-list for method input_type 262 | 2, // [2:2] is the sub-list for extension type_name 263 | 2, // [2:2] is the sub-list for extension extendee 264 | 0, // [0:2] is the sub-list for field type_name 265 | } 266 | 267 | func init() { file_pb_crx3_proto_init() } 268 | func file_pb_crx3_proto_init() { 269 | if File_pb_crx3_proto != nil { 270 | return 271 | } 272 | if !protoimpl.UnsafeEnabled { 273 | file_pb_crx3_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 274 | switch v := v.(*CrxFileHeader); i { 275 | case 0: 276 | return &v.state 277 | case 1: 278 | return &v.sizeCache 279 | case 2: 280 | return &v.unknownFields 281 | default: 282 | return nil 283 | } 284 | } 285 | file_pb_crx3_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 286 | switch v := v.(*AsymmetricKeyProof); i { 287 | case 0: 288 | return &v.state 289 | case 1: 290 | return &v.sizeCache 291 | case 2: 292 | return &v.unknownFields 293 | default: 294 | return nil 295 | } 296 | } 297 | file_pb_crx3_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 298 | switch v := v.(*SignedData); i { 299 | case 0: 300 | return &v.state 301 | case 1: 302 | return &v.sizeCache 303 | case 2: 304 | return &v.unknownFields 305 | default: 306 | return nil 307 | } 308 | } 309 | } 310 | type x struct{} 311 | out := protoimpl.TypeBuilder{ 312 | File: protoimpl.DescBuilder{ 313 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 314 | RawDescriptor: file_pb_crx3_proto_rawDesc, 315 | NumEnums: 0, 316 | NumMessages: 3, 317 | NumExtensions: 0, 318 | NumServices: 0, 319 | }, 320 | GoTypes: file_pb_crx3_proto_goTypes, 321 | DependencyIndexes: file_pb_crx3_proto_depIdxs, 322 | MessageInfos: file_pb_crx3_proto_msgTypes, 323 | }.Build() 324 | File_pb_crx3_proto = out.File 325 | file_pb_crx3_proto_rawDesc = nil 326 | file_pb_crx3_proto_goTypes = nil 327 | file_pb_crx3_proto_depIdxs = nil 328 | } 329 | --------------------------------------------------------------------------------