diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..259b00c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +name: Go Release + +on: + push: + # 创建 tag 时 + tags: + - v* + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + # 检出代码 + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install upx + run: sudo apt-get install -y upx + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 'stable' + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + # either 'goreleaser' (default) or 'goreleaser-pro' + distribution: goreleaser + # 'latest', 'nightly', or a semver + version: 'latest' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ed0965 --- /dev/null +++ b/.gitignore @@ -0,0 +1,203 @@ +### GoLand+all template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Go template +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +out/* + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### GoLand template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# AWS User-specific + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# SonarLint plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### GoLand+iml template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# AWS User-specific + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# SonarLint plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + + +dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..4585796 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,38 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com + +# The lines below are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/need to use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 2 + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + +upx: + - enabled: true + compress: best + lzma: true + +builds: + - env: + - CGO_ENABLED=0 + goos: + - windows + +archives: + - format: binary + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +checksum: + algorithm: md5 \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/JavaSceneConfigState.xml b/.idea/JavaSceneConfigState.xml new file mode 100644 index 0000000..68a9d6e --- /dev/null +++ b/.idea/JavaSceneConfigState.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/KillWxapkg.iml b/.idea/KillWxapkg.iml new file mode 100644 index 0000000..16dc49f --- /dev/null +++ b/.idea/KillWxapkg.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..8d66637 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..ec50427 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,41 @@ + + + + + + + + + DOM 问题JavaScript 和 TypeScript + + + Go + + + HTTP 客户端 + + + JavaScript 和 TypeScript + + + SQL + + + 代码样式问题Go + + + 可能的 bugGo + + + 常规JavaScript 和 TypeScript + + + + + 用户定义 + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5592cc7 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml new file mode 100644 index 0000000..cb1a783 --- /dev/null +++ b/.idea/watcherTasks.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d22455 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# KillWxapkg + +> 存Golang实现,一个用于自动化反编译微信小程序的工具,小程序安全利器,自动解密,解包,可最大程度还原工程目录 + +[![stable](https://img.shields.io/badge/stable-stable-green.svg)](https://github.com/Ackites/KillWxapkg) +[![license](https://img.shields.io/github/license/Ackites/KillWxapkg)]() +[![download](https://img.shields.io/github/downloads/Ackites/KillWxapkg/total)]() +[![release](https://img.shields.io/github/v/release/Ackites/KillWxapkg)]() +[![commit](https://img.shields.io/github/last-commit/Ackites/KillWxapkg)]() +[![issues](https://img.shields.io/github/issues/Ackites/KillWxapkg)]() +[![pull](https://img.shields.io/github/issues-pr/Ackites/KillWxapkg)]() +[![fork](https://img.shields.io/github/forks/Ackites/KillWxapkg)]() +[![star](https://img.shields.io/github/stars/Ackites/KillWxapkg)]() +[![go](https://img.shields.io/github/go-mod/go-version/Ackites/KillWxapkg)]() +[![size](https://img.shields.io/github/repo-size/Ackites/KillWxapkg)]() +[![contributors](https://img.shields.io/github/contributors/Ackites/KillWxapkg)]() +------------------ +## 介绍 + +- [x] 小程序自动解密 +- [x] 小程序自动解包,支持代码美化输出 + - [x] Json美化 + - [x] JavaScript美化 + - [x] Html美化 +- [x] 支持还原源代码工程目录结构 + - [x] Json配置文件还原 + - [x] JavaScript代码还原 + - [ ] Wxml代码还原 + - [ ] Wxss代码还原 +- [ ] 转换 Uni-app 项目 +- [ ] 敏感数据导出 + +### 工程结构还原 + +#### 未还原 + + +#### 还原后 + + +## 安装 + +- 下载最新版本的[release](https://github.com/Ackites/KillWxapkg/releases)包 +- 自行编译 + ```shell + # 克隆项目 + git clone https://github.com/Ackites/KillWxapkg.git + + # 进入项目目录 + cd ./KillWxapkg + + # 下载依赖 + go mod tidy + + # 编译 + go build + ``` + +## 用法 + +> 使用方法: program -id= -in=<输入文件1,输入文件2> 或 -in=<输入目录> -out=<输出目录> +> [-restore] [-pretty] [-ext=<文件后缀>] + +### 参数说明 +- `-id string` + - 微信小程序的AppID + - 包已解密,可不指定 + - 例:-id=wx7627e1630485288d +- `-in string` + - 输入文件路径(多个文件用逗号分隔)或输入目录路径 + - 自动检测,已解密的包,自动解包,未解密的包,自动解密后解包 + - 解密后的包会保存到输入目录下以AppID命名的文件夹 + - 例:-in=app.wxpkg,app1.wxapkg + - 例:-in=C:\Users\mi\Desktop\Applet\64 +- `-out string` + - 输出目录路径(如果未指定,则默认保存到输入目录下以AppID命名的文件夹) +- `-restore` + - 是否还原源代码工程目录结构,默认不还原 +- `-pretty` + - 是否美化输出,默认不美化,美化需较长时间 +- `-ext string` + - 处理的文件后缀 (default ".wxapkg") + - 例:-ext=.wxapkg +- `-help` + - 显示帮助信息 + +### 获取微信小程序AppID + + + +#### 文件夹名即为AppID + + + +进入文件夹下,即可找到.wxapkg文件 + + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=Ackites/KillWxapkg&type=Date)](https://star-history.com/#Ackites/KillWxapkg&Date) \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..b4b7954 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,59 @@ +package cmd + +import ( + "flag" + "fmt" + "log" + "sync" + + . "github.com/Ackites/KillWxapkg/internal/cmd" + . "github.com/Ackites/KillWxapkg/internal/config" + "github.com/Ackites/KillWxapkg/internal/restore" +) + +func Execute(appID, input, outputDir, fileExt string, restoreDir bool, pretty bool) { + if appID == "" || input == "" { + fmt.Println("使用方法: program -id= -in=<输入文件1,输入文件2> 或 -in=<输入目录> -out=<输出目录> [-ext=<文件后缀>] [-restore]") + flag.PrintDefaults() + return + } + + // 存储配置 + configManager := NewSharedConfigManager() + configManager.Set("appID", appID) + configManager.Set("input", input) + configManager.Set("outputDir", outputDir) + configManager.Set("fileExt", fileExt) + configManager.Set("restoreDir", restoreDir) + configManager.Set("pretty", pretty) + + inputFiles := ParseInput(input, fileExt) + + if len(inputFiles) == 0 { + log.Println("未找到任何文件") + return + } + + // 确定输出目录 + if outputDir == "" { + outputDir = DetermineOutputDir(input, appID) + } + + var wg sync.WaitGroup + for _, inputFile := range inputFiles { + wg.Add(1) + go func(file string) { + defer wg.Done() + err := ProcessFile(file, outputDir, appID) + if err != nil { + log.Printf("处理文件 %s 时出错: %v\n", file, err) + } else { + log.Printf("成功处理文件: %s\n", file) + } + }(inputFile) + } + wg.Wait() + + // 还原工程目录结构 + restore.ProjectStructure(outputDir, restoreDir) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b7e2479 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/Ackites/KillWxapkg + +go 1.22 + +require ( + github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c + github.com/dop251/goja v0.0.0-20240707163329-b1681fb2a2f5 + github.com/gorilla/css v1.0.1 + github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 + golang.org/x/crypto v0.25.0 + golang.org/x/net v0.21.0 +) + +require ( + github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect + golang.org/x/text v0.16.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e649aef --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c h1:+Zo5Ca9GH0RoeVZQKzFJcTLoAixx5s5Gq3pTIS+n354= +github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c/go.mod h1:HJGU9ULdREjOcVGZVPB5s6zYmHi1RxzT71l2wQyLmnE= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20240707163329-b1681fb2a2f5 h1:ZRqTaoW9WZ2DqeOQGhK9q73eCb47SEs30GV2IRHT9bo= +github.com/dop251/goja v0.0.0-20240707163329-b1681fb2a2f5/go.mod h1:o31y53rb/qiIAONF7w3FHJZRqqP3fzHUr1HqanthByw= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts= +github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/images/img.png b/images/img.png new file mode 100644 index 0000000..b306530 Binary files /dev/null and b/images/img.png differ diff --git a/images/img1.png b/images/img1.png new file mode 100644 index 0000000..907a323 Binary files /dev/null and b/images/img1.png differ diff --git a/images/img2.png b/images/img2.png new file mode 100644 index 0000000..4150c19 Binary files /dev/null and b/images/img2.png differ diff --git a/images/img3.png b/images/img3.png new file mode 100644 index 0000000..2386258 Binary files /dev/null and b/images/img3.png differ diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go new file mode 100644 index 0000000..8ae6671 --- /dev/null +++ b/internal/cmd/cmd.go @@ -0,0 +1,146 @@ +package cmd + +import ( + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" + + "github.com/Ackites/KillWxapkg/internal/decrypt" + "github.com/Ackites/KillWxapkg/internal/unpack" +) + +// ParseInput 解析输入文件 +func ParseInput(input, fileExt string) []string { + var inputFiles []string + if fileInfo, err := os.Stat(input); err == nil && fileInfo.IsDir() { + files, err := os.ReadDir(input) + if err != nil { + log.Fatalf("读取输入目录失败: %v", err) + } + for _, file := range files { + if !file.IsDir() && strings.HasSuffix(file.Name(), fileExt) { + inputFiles = append(inputFiles, filepath.Join(input, file.Name())) + } + } + } else { + inputFiles = strings.Split(input, ",") + } + + // 过滤掉不存在的文件 + var validFiles []string + for _, file := range inputFiles { + if _, err := os.Stat(file); err == nil { + validFiles = append(validFiles, file) + } + } + return validFiles +} + +// DetermineOutputDir 确定输出目录 +func DetermineOutputDir(input, appID string) string { + var baseDir string + if fileInfo, err := os.Stat(input); err == nil && fileInfo.IsDir() { + baseDir = input + } else { + baseDir = filepath.Dir(input) + } + return filepath.Join(baseDir, appID) +} + +// ProcessFile 合并目录 +func ProcessFile(inputFile, outputDir, appID string) error { + log.Printf("开始处理文件: %s\n", inputFile) + + // 确定解密后的文件路径 + decryptedFilePath := filepath.Join(outputDir, filepath.Base(inputFile)) + + // 解密 + decryptedData, err := decrypt.DecryptWxapkg(inputFile, appID) + if err != nil { + return fmt.Errorf("解密失败: %v", err) + } + + // 保存解密后的文件 + err = os.MkdirAll(outputDir, 0755) + if err != nil { + return fmt.Errorf("创建输出目录失败: %v", err) + } + + err = os.WriteFile(decryptedFilePath, decryptedData, 0755) + if err != nil { + return fmt.Errorf("保存解密文件失败: %v", err) + } + + log.Printf("文件解密并保存到: %s\n", decryptedFilePath) + + // 解包到临时目录 + tempDir, err := os.MkdirTemp("", "wxapkg") + if err != nil { + return fmt.Errorf("创建临时目录失败: %v", err) + } + defer func(path string) { + err := os.RemoveAll(path) + if err != nil { + log.Printf("删除临时目录 %s 失败: %v\n", path, err) + } + }(tempDir) + + err = unpack.UnpackWxapkg(decryptedData, tempDir) + if err != nil { + return fmt.Errorf("解包失败: %v", err) + } + + // 合并解包后的内容到输出目录 + err = mergeDirs(tempDir, outputDir) + if err != nil { + return fmt.Errorf("合并目录失败: %v", err) + } + + return nil +} + +// mergeDirs 合并目录 +func mergeDirs(srcDir, dstDir string) error { + return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + relPath, err := filepath.Rel(srcDir, path) + if err != nil { + return err + } + targetPath := filepath.Join(dstDir, relPath) + + if info.IsDir() { + return os.MkdirAll(targetPath, 0755) + } + + srcFile, err := os.Open(path) + if err != nil { + return err + } + defer func(srcFile *os.File) { + err := srcFile.Close() + if err != nil { + log.Printf("关闭文件 %s 失败: %v\n", srcFile.Name(), err) + } + }(srcFile) + + dstFile, err := os.Create(targetPath) + if err != nil { + return err + } + defer func(dstFile *os.File) { + err := dstFile.Close() + if err != nil { + log.Printf("关闭文件 %s 失败: %v\n", dstFile.Name(), err) + } + }(dstFile) + + _, err = io.Copy(dstFile, srcFile) + return err + }) +} diff --git a/internal/config/delete.go b/internal/config/delete.go new file mode 100644 index 0000000..0fe0e7f --- /dev/null +++ b/internal/config/delete.go @@ -0,0 +1,73 @@ +package config + +import ( + "context" + "log" + "os" + "sync" +) + +// FileDeletionManager 用于管理需要删除的文件列表 +type FileDeletionManager struct { + mu sync.Mutex + files map[string]bool + cancelFn context.CancelFunc + ctx context.Context +} + +var deleteInstance *FileDeletionManager +var deleteOnce sync.Once + +// NewFileDeletionManager 创建或获取一个单例的FileDeletionManager +func NewFileDeletionManager() *FileDeletionManager { + deleteOnce.Do(func() { + c := context.Background() + ctx, cancel := context.WithCancel(c) + deleteInstance = &FileDeletionManager{ + files: make(map[string]bool), + cancelFn: cancel, + ctx: ctx, + } + }) + return deleteInstance +} + +// AddFile 添加文件路径到删除列表 +func (f *FileDeletionManager) AddFile(filePath string) { + f.mu.Lock() + defer f.mu.Unlock() + f.files[filePath] = true +} + +// DeleteFiles 删除所有在列表中的文件 +func (f *FileDeletionManager) DeleteFiles() { + f.mu.Lock() + files := make([]string, 0, len(f.files)) + for file := range f.files { + files = append(files, file) + } + f.mu.Unlock() + + for _, file := range files { + select { + case <-f.ctx.Done(): + log.Println("文件删除操作已取消") + return + default: + err := os.Remove(file) + if err != nil { + log.Printf("删除文件 %s 失败: %v\n", file, err) + } else { + f.mu.Lock() + delete(f.files, file) // 删除成功后从列表中移除文件 + f.mu.Unlock() + log.Printf("文件 %s 已成功删除", file) + } + } + } +} + +// Cancel 取消删除操作 +func (f *FileDeletionManager) Cancel() { + f.cancelFn() +} diff --git a/internal/config/share.go b/internal/config/share.go new file mode 100644 index 0000000..e654c10 --- /dev/null +++ b/internal/config/share.go @@ -0,0 +1,79 @@ +package config + +import ( + "sync" +) + +// SharedConfigManager 用于管理共享配置 +type SharedConfigManager struct { + mu sync.RWMutex + settings map[string]interface{} +} + +var shareInstance *SharedConfigManager +var shareOnce sync.Once + +// NewSharedConfigManager 创建一个新的SharedConfigManager +func NewSharedConfigManager() *SharedConfigManager { + shareOnce.Do(func() { + shareInstance = &SharedConfigManager{ + settings: make(map[string]interface{}), + } + }) + return shareInstance +} + +// Set 设置一个配置项的值 +func (scm *SharedConfigManager) Set(key string, value interface{}) { + scm.mu.Lock() + defer scm.mu.Unlock() + scm.settings[key] = value +} + +// SetBulk 批量设置配置项的值 +func (scm *SharedConfigManager) SetBulk(configs map[string]interface{}) { + scm.mu.Lock() + defer scm.mu.Unlock() + for key, value := range configs { + scm.settings[key] = value + } +} + +// Get 获取一个配置项的值 +func (scm *SharedConfigManager) Get(key string) (interface{}, bool) { + scm.mu.RLock() + defer scm.mu.RUnlock() + value, exists := scm.settings[key] + return value, exists +} + +// GetBulk 批量获取配置项的值 +func (scm *SharedConfigManager) GetBulk(keys []string) map[string]interface{} { + scm.mu.RLock() + defer scm.mu.RUnlock() + results := make(map[string]interface{}) + for _, key := range keys { + if value, exists := scm.settings[key]; exists { + results[key] = value + } + } + return results +} + +// Delete 删除一个配置项 +func (scm *SharedConfigManager) Delete(key string) { + scm.mu.Lock() + defer scm.mu.Unlock() + delete(scm.settings, key) +} + +// GetAll 返回所有配置项的副本 +func (scm *SharedConfigManager) GetAll() map[string]interface{} { + scm.mu.RLock() + defer scm.mu.RUnlock() + c := make(map[string]interface{}) + for key, value := range scm.settings { + c[key] = value + } + return c +} diff --git a/internal/decrypt/decrypt.go b/internal/decrypt/decrypt.go new file mode 100644 index 0000000..c476271 --- /dev/null +++ b/internal/decrypt/decrypt.go @@ -0,0 +1,73 @@ +package decrypt + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/sha1" + "encoding/binary" + "fmt" + "os" + + "golang.org/x/crypto/pbkdf2" +) + +const ( + saltStr = "saltiest" + ivStr = "the iv: 16 bytes" + fileHeader = "V1MMWX" + defaultXorKey = 0x66 +) + +func DecryptWxapkg(inputFile, appID string) ([]byte, error) { + ciphertext, err := os.ReadFile(inputFile) + if err != nil { + return nil, fmt.Errorf("读取文件失败: %v", err) + } + + // 先检查是否已解密 + reader := bytes.NewReader(ciphertext) + var firstMark byte + binary.Read(reader, binary.BigEndian, &firstMark) + var info1, indexInfoLength, bodyInfoLength uint32 + binary.Read(reader, binary.BigEndian, &info1) + binary.Read(reader, binary.BigEndian, &indexInfoLength) + binary.Read(reader, binary.BigEndian, &bodyInfoLength) + var lastMark byte + binary.Read(reader, binary.BigEndian, &lastMark) + if firstMark == 0xBE && lastMark == 0xED { + // 已解密直接返回 + return ciphertext, nil + } + + if string(ciphertext[:len(fileHeader)]) != fileHeader { + return nil, fmt.Errorf("无效的文件格式") + } + + key := pbkdf2.Key([]byte(appID), []byte(saltStr), 1000, 32, sha1.New) + + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("创建AES密码块失败: %v", err) + } + + iv := []byte(ivStr) + mode := cipher.NewCBCDecrypter(block, iv) + originData := make([]byte, 1024) + mode.CryptBlocks(originData, ciphertext[6:1024+6]) + + afData := make([]byte, len(ciphertext)-1024-6) + var xorKey byte + if len(appID) >= 2 { + xorKey = appID[len(appID)-2] + } else { + xorKey = defaultXorKey + } + for i, b := range ciphertext[1024+6:] { + afData[i] = b ^ xorKey + } + + originData = append(originData[:1023], afData...) + + return originData, nil +} diff --git a/internal/restore/javascript.go b/internal/restore/javascript.go new file mode 100644 index 0000000..5aa286c --- /dev/null +++ b/internal/restore/javascript.go @@ -0,0 +1,32 @@ +package restore + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/Ackites/KillWxapkg/internal/unpack" +) + +// ProcessJavaScriptFiles 分割JavaScript文件 +func ProcessJavaScriptFiles(configFile, outputDir string) error { + content, err := os.ReadFile(configFile) + if err != nil { + return fmt.Errorf("读取配置文件失败: %v", err) + } + + var config struct { + SubPackages []unpack.SubPackage `json:"subPackages"` + } + err = json.Unmarshal(content, &config) + if err != nil { + return fmt.Errorf("解析配置文件失败: %v", err) + } + + err = unpack.ProcessJavaScriptFiles(outputDir, config) + if err != nil { + return fmt.Errorf("处理JavaScript文件失败: %v", err) + } + + return nil +} diff --git a/internal/restore/restore.go b/internal/restore/restore.go new file mode 100644 index 0000000..613cab3 --- /dev/null +++ b/internal/restore/restore.go @@ -0,0 +1,45 @@ +package restore + +import ( + "log" + "path/filepath" + + "github.com/Ackites/KillWxapkg/internal/config" + "github.com/Ackites/KillWxapkg/internal/unpack" +) + +// ProjectStructure 是否还原工程目录结构 +func ProjectStructure(outputDir string, restoreDir bool) { + if !restoreDir { + return + } + + // 创建文件删除管理器 + manager := config.NewFileDeletionManager() + + // 配置文件还原 + configFile := filepath.Join(outputDir, "app-config.json") + err := unpack.ProcessConfigFiles(configFile) + if err != nil { + log.Printf("还原工程目录结构失败: %v\n", err) + } else { + manager.AddFile(configFile) + } + + // JavaScript文件还原 + err = ProcessJavaScriptFiles(configFile, outputDir) + if err != nil { + log.Printf("处理JavaScript文件失败: %v\n", err) + } + + // WXSS文件还原 + //var config unpack.AppConfig + //content, err := os.ReadFile(configFile) + //if err == nil { + // _ = json.Unmarshal(content, &config) + //} + //ProcessWxssFiles(outputDir, config) + + // 执行删除文件操作 + manager.DeleteFiles() +} diff --git a/internal/restore/wxss.go b/internal/restore/wxss.go new file mode 100644 index 0000000..d267b74 --- /dev/null +++ b/internal/restore/wxss.go @@ -0,0 +1,19 @@ +package restore + +import ( + "path/filepath" + + "github.com/Ackites/KillWxapkg/internal/unpack" +) + +// ProcessWxssFiles 分割 WXSS 文件 +func ProcessWxssFiles(outputDir string, config unpack.AppConfig) { + // 处理主包的 WXSS 文件 + unpack.ProcessXssFiles(outputDir, "") + + // 处理子包的 WXSS 文件 + for _, subPackage := range config.SubPackages { + subPackageDir := filepath.Join(outputDir, subPackage.Root) + unpack.ProcessXssFiles(subPackageDir, outputDir) + } +} diff --git a/internal/unpack/fileMagicNumbers.go b/internal/unpack/fileMagicNumbers.go new file mode 100644 index 0000000..5b77ccc --- /dev/null +++ b/internal/unpack/fileMagicNumbers.go @@ -0,0 +1,94 @@ +package unpack + +// FileMagicNumbers 文件类型及其对应的魔数(文件头标识) +var FileMagicNumbers = map[string][]byte{ + // 图像文件 + ".png": {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'}, + ".jpg": {0xFF, 0xD8, 0xFF}, + ".jpeg": {0xFF, 0xD8, 0xFF}, + ".gif": {'G', 'I', 'F', '8'}, + ".bmp": {'B', 'M'}, + ".webp": {'R', 'I', 'F', 'F'}, + ".tiff": {0x49, 0x49, 0x2A, 0x00}, + ".cr2": {0x49, 0x49, 0x2A, 0x00, 0x10, 0x00, 0x00, 0x00, 0x43, 0x52}, + ".ico": {0x00, 0x00, 0x01, 0x00}, + ".heic": {'f', 't', 'y', 'p', 'h', 'e', 'i', 'c'}, + + // 文档文件 + ".pdf": {'%', 'P', 'D', 'F', '-'}, + ".doc": {0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1}, + ".docx": {'P', 'K', 0x03, 0x04}, + ".xls": {0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1}, + ".xlsx": {'P', 'K', 0x03, 0x04}, + ".ppt": {0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1}, + ".pptx": {'P', 'K', 0x03, 0x04}, + ".odt": {'P', 'K', 0x03, 0x04}, + ".ods": {'P', 'K', 0x03, 0x04}, + ".odp": {'P', 'K', 0x03, 0x04}, + ".rtf": {'{', '\\', 'r', 't', 'f', '1'}, + ".epub": {'P', 'K', 0x03, 0x04}, + ".mobi": {'M', 'O', 'B', 'I'}, + + // 压缩文件 + ".zip": {'P', 'K', 0x03, 0x04}, + ".rar": {'R', 'a', 'r', '!', 0x1A, 0x07, 0x00}, + ".7z": {'7', 'z', 0xBC, 0xAF, 0x27, 0x1C}, + ".gz": {0x1F, 0x8B, 0x08}, + ".tar": {'u', 's', 't', 'a', 'r'}, + ".bz2": {'B', 'Z', 'h'}, + ".xz": {0xFD, '7', 'z', 'X', 'Z', 0x00}, + + // 音频文件 + ".mp3": {0xFF, 0xFB}, + ".wav": {'R', 'I', 'F', 'F'}, + ".ogg": {'O', 'g', 'g', 'S'}, + ".flac": {'f', 'L', 'a', 'C'}, + ".aac": {0xFF, 0xF1}, + ".m4a": {'f', 't', 'y', 'p', 'M', '4', 'A', ' '}, + ".mid": {'M', 'T', 'h', 'd'}, + ".aiff": {'F', 'O', 'R', 'M'}, + + // 视频文件 + ".mp4": {0x00, 0x00, 0x00, 0x18, 'f', 't', 'y', 'p'}, + ".avi": {'R', 'I', 'F', 'F'}, + ".mkv": {0x1A, 0x45, 0xDF, 0xA3}, + ".flv": {'F', 'L', 'V'}, + ".mov": {'f', 't', 'y', 'p', 'q', 't', ' ', ' '}, + ".webm": {0x1A, 0x45, 0xDF, 0xA3}, + ".mpg": {0x00, 0x00, 0x01, 0xBA}, + ".wmv": {0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11}, + + // 可执行文件和库文件 + ".exe": {'M', 'Z'}, + ".elf": {0x7F, 'E', 'L', 'F'}, + ".so": {0x7F, 'E', 'L', 'F'}, + ".dll": {'M', 'Z'}, + ".app": {0xCA, 0xFE, 0xBA, 0xBE}, // Mach-O 文件格式 (macOS) + + // 脚本和标记语言文件 + ".json": {'{'}, // 假设 JSON 文件以 { 开头 + ".xml": {'<', '?', 'x', 'm', 'l'}, + ".html": {'<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E'}, + + // 字体文件 + ".ttf": {0x00, 0x01, 0x00, 0x00, 0x00}, + ".otf": {'O', 'T', 'T', 'O'}, + ".woff": {'w', 'O', 'F', 'F'}, + ".woff2": {'w', 'O', 'F', '2'}, + + // 数据库文件 + ".sqlite": {'S', 'Q', 'L', 'i', 't', 'e', ' ', 'f', 'o', 'r', 'm', 'a', 't', ' ', '3', 0x00}, + ".db": {0x00, 0x06, 0x15, 0x61, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00}, + + // 其他 + ".swf": {'F', 'W', 'S'}, + ".class": {0xCA, 0xFE, 0xBA, 0xBE}, + ".psd": {'8', 'B', 'P', 'S'}, + ".torrent": {'d', '8', ':', 'a', 'n', 'n', 'o', 'u', 'n', 'c', 'e'}, + ".blend": {'B', 'L', 'E', 'N', 'D', 'E', 'R'}, + ".pcap": {0xD4, 0xC3, 0xB2, 0xA1}, + ".dwg": {0x41, 0x43, 0x31, 0x30}, + ".iso": {0x43, 0x44, 0x30, 0x30, 0x31}, + ".vsd": {0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1}, + ".mdb": {0x00, 0x01, 0x00, 0x00, 0x53, 0x74, 0x61, 0x6E, 0x64, 0x61, 0x72, 0x64, 0x20, 0x4A, 0x65, 0x74}, +} diff --git a/internal/unpack/formatter/formatter.go b/internal/unpack/formatter/formatter.go new file mode 100644 index 0000000..5a623a4 --- /dev/null +++ b/internal/unpack/formatter/formatter.go @@ -0,0 +1,38 @@ +package formatter + +import ( + "fmt" + "strings" + + . "github.com/Ackites/KillWxapkg/internal/config" +) + +// Formatter 是一个文件格式化器接口 +type Formatter interface { + Format([]byte) ([]byte, error) +} + +// 注册所有格式化器 +var formatters = map[string]Formatter{} + +// RegisterFormatter 注册文件扩展名对应的格式化器 +func RegisterFormatter(ext string, formatter Formatter) { + formatters[strings.ToLower(ext)] = formatter +} + +// GetFormatter 返回文件扩展名对应的格式化器 +func GetFormatter(ext string) (Formatter, error) { + formatter, exists := formatters[strings.ToLower(ext)] + if !exists { + return nil, fmt.Errorf("不支持的文件类型: %s", ext) + } + configManager := NewSharedConfigManager() + if pretty, ok := configManager.Get("pretty"); ok { + if p, o := pretty.(bool); o { + if !p && ext == ".js" { + return nil, fmt.Errorf("不进行美化输出") + } + } + } + return formatter, nil +} diff --git a/internal/unpack/formatter/htmlformatter.go b/internal/unpack/formatter/htmlformatter.go new file mode 100644 index 0000000..3aee7f8 --- /dev/null +++ b/internal/unpack/formatter/htmlformatter.go @@ -0,0 +1,69 @@ +package formatter + +import ( + "bytes" + "regexp" + "strings" + + "github.com/yosssi/gohtml" +) + +// HTMLFormatter 结构体,用于格式化 HTML 代码 +type HTMLFormatter struct { + jsFormatter *JSFormatter +} + +// 正则表达式用于匹配 HTML 中的 `) + +// NewHTMLFormatter 创建一个新的 HTMLFormatter 实例 +func NewHTMLFormatter() *HTMLFormatter { + return &HTMLFormatter{ + jsFormatter: NewJSFormatter(), + } +} + +// Format 方法用于格式化 HTML 代码 +// input: 原始的 HTML 代码字节切片 +// 返回值: 格式化后的 HTML 代码字节切片和错误信息(如果有) +func (f *HTMLFormatter) Format(input []byte) ([]byte, error) { + // 使用 gohtml 库格式化 HTML 代码 + data := gohtml.FormatBytes(bytes.TrimSpace(input)) + + // 替换