Merge pull request #15 from firdausmntp/main

v1.0 - Initial release: ProxyPin Certificate Installer
This commit is contained in:
wanghongenpin
2026-04-19 19:22:12 +08:00
committed by GitHub
12 changed files with 1832 additions and 95 deletions

170
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,170 @@
name: Build and Release Module
on:
push:
branches: [ main, master ]
tags:
- 'v*'
pull_request:
branches: [ main, master ]
workflow_dispatch:
env:
MODULE_ID: proxypin-cert-installer
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Get version info
id: version
run: |
VERSION=$(grep 'version=' module.prop | cut -d'=' -f2)
VERSION_CODE=$(grep 'versionCode=' module.prop | cut -d'=' -f2)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version_code=$VERSION_CODE" >> $GITHUB_OUTPUT
- name: Validate module structure
run: |
echo "🔍 Validating module structure..."
# Check required files
required_files=("module.prop" "customize.sh" "post-fs-data.sh" "service.sh" "action.sh" "uninstall.sh" "META-INF/com/google/android/update-binary" "META-INF/com/google/android/updater-script")
for file in "${required_files[@]}"; do
if [ ! -f "$file" ]; then
echo "❌ Missing required file: $file"
exit 1
fi
echo "✅ Found: $file"
done
# Check certificate directory exists
if [ ! -d "system/etc/security/cacerts" ]; then
echo "❌ Missing certificate directory"
exit 1
fi
echo "✅ Certificate directory exists"
# Check certificate exists
if [ ! -f "system/etc/security/cacerts/243f0bfb.0" ]; then
echo "⚠️ Warning: ProxyPin certificate not found"
fi
echo "✅ Module structure validated!"
- name: Make scripts executable
run: |
chmod +x post-fs-data.sh
chmod +x service.sh
chmod +x customize.sh
chmod +x action.sh
chmod +x uninstall.sh
chmod +x META-INF/com/google/android/update-binary
- name: Build module ZIP
run: |
MODULE_NAME="ProxyPin-Cert-Installer-${{ steps.version.outputs.version }}"
echo "📦 Building $MODULE_NAME.zip..."
# Create zip with only required files
zip -r9 "${MODULE_NAME}.zip" \
module.prop \
customize.sh \
post-fs-data.sh \
service.sh \
action.sh \
uninstall.sh \
update.json \
META-INF/ \
system/ \
webroot/
echo "✅ Module built: ${MODULE_NAME}.zip"
echo "module_zip=${MODULE_NAME}.zip" >> $GITHUB_ENV
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: proxypin-cert-installer-${{ steps.version.outputs.version }}
path: ${{ env.module_zip }}
retention-days: 30
release:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Get version info
id: version
run: |
VERSION=$(grep 'version=' module.prop | cut -d'=' -f2)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Make scripts executable
run: |
chmod +x post-fs-data.sh
chmod +x service.sh
chmod +x customize.sh
chmod +x action.sh
chmod +x uninstall.sh
chmod +x META-INF/com/google/android/update-binary
- name: Build release ZIP
run: |
MODULE_NAME="ProxyPin-Cert-Installer-${{ steps.version.outputs.version }}"
zip -r9 "${MODULE_NAME}.zip" \
module.prop \
customize.sh \
post-fs-data.sh \
service.sh \
action.sh \
uninstall.sh \
update.json \
META-INF/ \
system/ \
webroot/
echo "module_zip=${MODULE_NAME}.zip" >> $GITHUB_ENV
- name: Generate changelog
id: changelog
run: |
echo "## ProxyPin Certificate Installer ${{ steps.version.outputs.version }}" > RELEASE_NOTES.md
echo "" >> RELEASE_NOTES.md
echo "**Author:** [firdausmntp](https://github.com/firdausmntp)" >> RELEASE_NOTES.md
echo "" >> RELEASE_NOTES.md
echo "### Features" >> RELEASE_NOTES.md
echo "- ✅ Universal root support (Magisk/KernelSU/SukiSU/APatch)" >> RELEASE_NOTES.md
echo "- ✅ Android 5.0 - 16 (API 21-36) support" >> RELEASE_NOTES.md
echo "- ✅ **APEX CA bypass for Android 14+**" >> RELEASE_NOTES.md
echo "- ✅ WebUI for status monitoring" >> RELEASE_NOTES.md
echo "- ✅ ProxyPin certificate pre-included" >> RELEASE_NOTES.md
echo "- ✅ SELinux enforcing compatible" >> RELEASE_NOTES.md
echo "" >> RELEASE_NOTES.md
echo "### Installation" >> RELEASE_NOTES.md
echo "1. Download the ZIP file below" >> RELEASE_NOTES.md
echo "2. Install via Magisk/KernelSU/SukiSU/APatch Manager" >> RELEASE_NOTES.md
echo "3. Reboot device" >> RELEASE_NOTES.md
echo "" >> RELEASE_NOTES.md
echo "### Important for Android 14+" >> RELEASE_NOTES.md
echo "This module includes APEX bypass to inject certificates into \`com.android.conscrypt\` APEX." >> RELEASE_NOTES.md
echo "If ProxyPin shows 'Certificate Not Installed', check logs at \`/data/local/tmp/ProxyPinCert.log\`" >> RELEASE_NOTES.md
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: ${{ env.module_zip }}
body_path: RELEASE_NOTES.md

52
.gitignore vendored
View File

@@ -1,17 +1,39 @@
# Miscellaneous
*.class
# Build artifacts
*.zip
*.tar.gz
build/
dist/
_build_temp/
# Build scripts (personal use only)
build.py
build.sh
build.bat
# Logs
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
Podfile.lock
# IntelliJ related
*.iml
*.ipr
*.iws
ProxyPinCert.log
ProxyPinCA.log
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
desktop.ini
ehthumbs.db
# Temp
*.tmp
*.temp
*.bak
# Python
__pycache__/
*.pyc
*.pyo

22
CHANGELOG.md Normal file
View File

@@ -0,0 +1,22 @@
# Changelog
All notable changes to this project will be documented in this file.
## [v1.0] - 2026-04-16
### Added
- Initial release
- ProxyPin CA certificate (`243f0bfb.0`) pre-included in module
- Install certificate to System CA Store automatically
- Multi-root support: Magisk, KernelSU, SukiSU, APatch
- Android 5.0 - 15+ (API 21-36) compatibility
- APEX CA bypass for Android 14+ (conscrypt module injection)
- Namespace injection into zygote processes
- Dynamic re-injection via service script after boot
- Per-process mount for all app processes
- WebUI for status monitoring (APEX status, logs, reboot)
- Action button handler for root managers
- SELinux enforcing compatible
- Comprehensive logging to `/data/local/tmp/ProxyPinCert.log`
- Uninstall cleanup script (unmount APEX, remove temp files)
- GitHub Actions CI/CD for automated builds and releases

208
README.md
View File

@@ -1,8 +1,204 @@
# ProxyPin Certificate
这是一个 Magisk 模块 用于安装ProxyPin系统证书, 安装完后需要重启手机.
# 📱 ProxyPin Certificate Installer
抓包下载地址
https://github.com/wanghongenpin/network_proxy_flutter
<p align="center">
<img src="https://img.shields.io/badge/Version-v1.0-blue?style=for-the-badge" alt="Version"/>
<img src="https://img.shields.io/badge/Android-5.0--15-green?style=for-the-badge&logo=android" alt="Android"/>
<img src="https://img.shields.io/badge/License-GPL--3.0-yellow?style=for-the-badge" alt="License"/>
</p>
安装证书不成功可尝试其他模块
如: https://github.com/ys1231/MoveCertificate
<p align="center">
<img src="https://img.shields.io/badge/Magisk-20.4%2B-00AF9C?style=flat-square&logo=magisk" alt="Magisk"/>
<img src="https://img.shields.io/badge/KernelSU-Supported-orange?style=flat-square" alt="KernelSU"/>
<img src="https://img.shields.io/badge/SukiSU-✓%20Tested-9333ea?style=flat-square" alt="SukiSU"/>
<img src="https://img.shields.io/badge/APatch-Supported-blue?style=flat-square" alt="APatch"/>
</p>
<p align="center">
<b>📱 Install ProxyPin CA Certificate to System CA Store</b><br>
<sub>Tested on Android 15 with SukiSU v40201</sub>
</p>
---
## 📋 Description
This Magisk/KernelSU/APatch module installs the **ProxyPin CA Certificate** (`243f0bfb.0`) into the Android System CA Store, enabling HTTPS traffic interception with the [ProxyPin](https://github.com/wanghongenpin/network_proxy_flutter) app.
> ✅ **Certificate is pre-included** in this module. Just install, reboot, and you're done!
---
## ✨ Key Features
| Feature | Description |
|---------|-------------|
| 🔧 **Multi-Root Support** | Works with Magisk, KernelSU, SukiSU, APatch |
| 📱 **Wide Android Support** | Android 5.0 - 15 (API 21-35) |
| 🔓 **APEX Bypass** | Proper injection for Android 14+ conscrypt APEX |
| 🖥️ **WebUI Interface** | Monitor status and manage module visually |
| 🛡️ **SELinux Compatible** | Works with SELinux enforcing |
| 💾 **Systemless** | Does not modify /system partition |
| 📦 **Pre-included Cert** | Certificate `243f0bfb.0` included, no manual steps |
---
## 📱 Tested Compatibility
### ✅ Verified Working
| Device | Android | Root | Status |
|--------|---------|------|--------|
| Redmi Note 8 Pro | 15 (API 35) | SukiSU v40201 | ✅ **Tested** |
### Root Solutions Support
| Solution | Status | Notes |
|----------|--------|-------|
| **SukiSU** | ✅ Tested | Fully working with WebUI |
| **KernelSU** | ✅ Supported | With WebUI support |
| **Magisk** | ✅ Supported | v20.4+ required |
| **APatch** | ✅ Supported | v10300+ required |
### Android Versions
| Version | API | Status |
|---------|-----|--------|
| Android 5.0 - 13 | 21-33 | ✅ Standard Magic Mount |
| Android 14 | 34 | ✅ APEX Bypass |
| Android 15 | 35 | ✅ APEX Bypass (Tested) |
---
## 🚀 Quick Start
### Step 1: Install Module
1. Download `ProxyPin-Cert-Installer-v1.0.zip`
2. Install via **Magisk/KernelSU/SukiSU/APatch** Manager
3. **Reboot** device
### Step 2: Verify
1. Go to **Settings****Security****Trusted credentials****System**
2. Look for **ProxyPin CA** certificate
3. Open ProxyPin and start capturing
> 💡 The certificate (`243f0bfb.0`) is already pre-included in the module. No export or manual copy needed.
---
## 🖥️ WebUI Features
Access WebUI through your root manager's module settings:
| Feature | Description |
|---------|-------------|
| 📊 **Status Monitor** | View module and APEX injection status |
| 💉 **Re-inject** | Manually trigger certificate injection |
| 📋 **View Logs** | Check module operation logs |
| 🔄 **Reboot** | Quick reboot to apply changes |
---
## 🔧 Android 14+ APEX Bypass
Starting Android 14, CA certificates moved to APEX module (`com.android.conscrypt`).
This module implements:
- **Namespace Injection** - Mounts into zygote namespaces
- **Dynamic Re-injection** - Service script reinjects after boot
- **Per-process Mount** - All app processes see the certificate
### Verify Injection
```bash
# Check if certificate is in APEX
ls /apex/com.android.conscrypt/cacerts/*.0 | head -5
# View module logs
cat /data/local/tmp/ProxyPinCert.log
```
---
## ⚠️ Troubleshooting
### "Certificate Not Installed" in ProxyPin
```bash
# 1. Check logs
cat /data/local/tmp/ProxyPinCert.log
# 2. Manual re-inject
su -c "sh /data/adb/modules/proxypin-cert-installer/post-fs-data.sh"
# 3. Force stop and reopen ProxyPin
```
### "Unknown Publisher" Error
- **SukiSU/KernelSU**: Settings → Enable "Allow untrusted modules"
- **APatch**: Settings → Security → Enable "Allow unknown sources"
### WebUI Not Loading
1. Ensure `webroot/index.html` exists in module
2. Check if root manager supports WebUI
3. Try reinstalling module
---
## 📁 Module Structure
```
proxypin-cert-installer/
├── META-INF/com/google/android/
│ ├── update-binary
│ └── updater-script
├── system/etc/security/cacerts/
│ └── 243f0bfb.0 # ProxyPin CA cert (pre-included)
├── webroot/
│ └── index.html # WebUI
├── module.prop
├── customize.sh # Installation script
├── post-fs-data.sh # APEX injection
├── service.sh # Post-boot injection
├── action.sh # Action button handler
└── uninstall.sh # Cleanup script
```
---
## 📝 Changelog
### v1.0 (Current)
- ✅ Initial release
- ✅ Certificate pre-included (`243f0bfb.0`)
- ✅ Multi-root support (Magisk/KernelSU/SukiSU/APatch)
- ✅ Android 5.0-15+ support
- ✅ APEX CA bypass for Android 14+
- ✅ WebUI for status monitoring
- ✅ SELinux enforcing compatible
- ✅ GitHub Actions CI/CD
## 📄 License
GPL-3.0 License - See [LICENSE](LICENSE) for details.
---
## 🙏 Credits
- **[firdausmntp](https://github.com/firdausmntp)** - Author & Maintainer
- [wanghongenpin](https://github.com/wanghongenpin/network_proxy_flutter) - ProxyPin App
- [topjohnwu](https://github.com/topjohnwu) - Magisk Framework
- [tiann](https://github.com/tiann) - KernelSU Framework
- [pomelohan](https://github.com/pomelohan/SukiSU-Ultra) - SukiSU Framework
- [bmax121](https://github.com/bmax121) - APatch Framework
---
## 🔗 Links
[![GitHub](https://img.shields.io/badge/GitHub-Repository-181717?style=for-the-badge&logo=github)](https://github.com/firdausmntp/ProxyPin-cert-installer)
[![Issues](https://img.shields.io/badge/Report-Issues-red?style=for-the-badge&logo=github)](https://github.com/firdausmntp/ProxyPin-cert-installer/issues)
[![ProxyPin](https://img.shields.io/badge/ProxyPin-Website-blue?style=for-the-badge)](https://github.com/wanghongenpin/network_proxy_flutter)

138
action.sh Normal file
View File

@@ -0,0 +1,138 @@
#!/system/bin/sh
# This script runs when user presses Action button in root manager
MODDIR=${0%/*}
LOG_FILE="/data/local/tmp/ProxyPinCert.log"
CERT_DIR="$MODDIR/system/etc/security/cacerts"
echo "╔════════════════════════════════════════╗"
echo "║ ProxyPin Certificate Installer v1.0 ║"
echo "║ by firdausmntp ║"
echo "╚════════════════════════════════════════╝"
echo ""
# Get API level
API=$(getprop ro.build.version.sdk)
ANDROID_VERSION=$(getprop ro.build.version.release)
echo "- Android: $ANDROID_VERSION (API $API)"
# Check certificate using find (more reliable)
CERT_COUNT=$(find "$CERT_DIR" -maxdepth 1 -type f -name "*.0" 2>/dev/null | wc -l)
CERT_COUNT=$(echo "$CERT_COUNT" | tr -d ' ')
echo "- Certificates in module: $CERT_COUNT"
if [ "$CERT_COUNT" -eq 0 ] || [ -z "$CERT_COUNT" ]; then
echo ""
echo "⚠️ CERTIFICATE NOT FOUND!"
echo ""
echo "Certificate 243f0bfb.0 is missing."
echo "Try reinstalling the module."
echo ""
exit 1
fi
# Show certificate info
echo ""
echo "Certificates:"
find "$CERT_DIR" -maxdepth 1 -type f -name "*.0" 2>/dev/null | while read cert; do
name=$(basename "$cert")
size=$(ls -la "$cert" | awk '{print $5}')
# Try to get certificate subject
subject=$(openssl x509 -in "$cert" -noout -subject 2>/dev/null | sed 's/subject=//g' | head -1)
echo " - $name ($size bytes)"
if [ -n "$subject" ]; then
echo " Subject: $(echo "$subject" | cut -c1-60)..."
fi
done
# Check system cacerts
echo ""
echo "System CA Store Status:"
SYSTEM_CERT=$(find "$CERT_DIR" -maxdepth 1 -type f -name "*.0" 2>/dev/null | head -1 | xargs basename 2>/dev/null)
if [ -n "$SYSTEM_CERT" ]; then
if [ -f "/system/etc/security/cacerts/$SYSTEM_CERT" ]; then
echo "$SYSTEM_CERT present in /system/etc/security/cacerts"
else
echo "$SYSTEM_CERT NOT in /system/etc/security/cacerts (Magic Mount may not be active yet)"
fi
fi
# Check APEX status on Android 14+
if [ "$API" -ge 34 ]; then
echo ""
echo "Android 14+ APEX Status:"
APEX_DIR="/apex/com.android.conscrypt/cacerts"
if [ -d "$APEX_DIR" ]; then
find "$CERT_DIR" -maxdepth 1 -type f -name "*.0" 2>/dev/null | while read cert; do
name=$(basename "$cert")
if [ -f "$APEX_DIR/$name" ]; then
echo "$name present in APEX"
else
echo "$name NOT in APEX (needs re-injection or reboot)"
fi
done
echo ""
APEX_COUNT=$(find "$APEX_DIR" -maxdepth 1 -name "*.0" -type f 2>/dev/null | wc -l)
echo " Total certs in APEX: $APEX_COUNT"
else
echo " APEX CA directory not found"
fi
else
echo ""
echo "Note: Standard Magic Mount is used for Android < 14"
fi
# Check Trusted Credentials hint
echo ""
echo "📱 How to verify:"
echo " Settings → Security → Encryption & credentials"
echo " → Trusted credentials → System"
echo " Look for: ProxyPin CA"
# Option to re-inject
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Options:"
echo " 1. Re-inject certificates (for Android 14+ APEX)"
echo " 2. View logs"
echo " 3. Force reboot"
echo " 4. Exit"
echo ""
read -p "Select option [1-4]: " choice
case "$choice" in
1)
echo ""
echo "Running certificate re-injection..."
sh "$MODDIR/service.sh"
echo ""
echo "Done! Please check:"
echo " - Settings → Security → Trusted credentials → System"
echo " - ProxyPin app should detect the certificate"
;;
2)
echo ""
echo "=== Recent Logs ==="
tail -50 "$LOG_FILE" 2>/dev/null || echo "No logs found"
;;
3)
echo ""
echo "Rebooting device..."
reboot
;;
4)
echo "Goodbye!"
;;
*)
echo "Invalid option"
;;
esac
if [ "$KSU" = "true" ] || [ "$APATCH" = "true" ]; then
echo ""
echo "Dialog will close in 10 seconds..."
sleep 10
fi

View File

@@ -1,17 +1,215 @@
#!/system/bin/sh
# Android 5.0 - 16 (API 21-36) compatible
SKIPUNZIP=0
ASH_STANDALONE=0
#################
# Helper Functions
#################
ui_print "开始安装模块"
print_banner() {
ui_print "╔════════════════════════════════════════╗"
ui_print "║ ProxyPin Certificate Installer v1.0 ║"
ui_print "║ by firdausmntp ║"
ui_print "╚════════════════════════════════════════╝"
ui_print ""
}
ui_print "提取模块证书"
detect_root_solution() {
if [ "$KSU" = "true" ]; then
# Check for SukiSU specifically
if [ -f /data/adb/ksu/bin/ksud ]; then
if strings /data/adb/ksu/bin/ksud 2>/dev/null | grep -qi "sukisu"; then
ROOT_IMPL="SukiSU"
else
ROOT_IMPL="KernelSU"
fi
else
ROOT_IMPL="KernelSU"
fi
ROOT_VER="$KSU_VER"
ROOT_VER_CODE="$KSU_VER_CODE"
elif [ "$APATCH" = "true" ]; then
ROOT_IMPL="APatch"
ROOT_VER="$APATCH_VER"
ROOT_VER_CODE="$APATCH_VER_CODE"
else
ROOT_IMPL="Magisk"
ROOT_VER="$MAGISK_VER"
ROOT_VER_CODE="$MAGISK_VER_CODE"
fi
}
unzip -o "$ZIPFILE" 'system/*' -d $MODPATH >&2
#################
# Compatibility Check
#################
ui_print "安装成功,重启手机后去系统证书查看ProxyPinCA是否生效."
check_compatibility() {
# Check API level
API=$(getprop ro.build.version.sdk)
[ -z "$API" ] && API=21
ui_print " "
if [ "$API" -lt 21 ]; then
abort "! ERROR: Minimum Android 5.0 (API 21) required!"
fi
set_perm_recursive $MODPATH 0 0 0755 0644
if [ "$API" -gt 36 ]; then
ui_print "! WARNING: Untested Android version (API $API)"
ui_print " Proceeding anyway..."
fi
# Root solution version checks
case "$ROOT_IMPL" in
"Magisk")
[ "$ROOT_VER_CODE" -lt 20400 ] && abort "! ERROR: Magisk v20.4+ required!"
;;
"KernelSU"|"SukiSU")
if [ "$ROOT_VER_CODE" -lt 10000 ]; then
ui_print "! WARNING: Old $ROOT_IMPL version"
fi
;;
"APatch")
if [ "$ROOT_VER_CODE" -lt 10300 ]; then
ui_print "! WARNING: Old APatch version"
fi
;;
esac
}
#################
#################
setup_permissions() {
ui_print "- Setting permissions..."
# System certificate directory
if [ -d "$MODPATH/system/etc/security/cacerts" ]; then
set_perm_recursive "$MODPATH/system/etc/security/cacerts" 0 0 0755 0644
fi
# Scripts
for script in post-fs-data.sh service.sh uninstall.sh action.sh; do
[ -f "$MODPATH/$script" ] && set_perm "$MODPATH/$script" 0 0 0755
done
# WebUI
if [ -d "$MODPATH/webroot" ]; then
set_perm_recursive "$MODPATH/webroot" 0 0 0755 0644
fi
}
cleanup_certificates() {
ui_print "- Cleaning up non-certificate files..."
local CERT_DIR="$MODPATH/system/etc/security/cacerts"
# Remove README.md if exists
if [ -f "$CERT_DIR/README.md" ]; then
rm -f "$CERT_DIR/README.md"
ui_print " Removed README.md"
fi
# Remove .gitkeep if exists
if [ -f "$CERT_DIR/.gitkeep" ]; then
rm -f "$CERT_DIR/.gitkeep"
ui_print " Removed .gitkeep"
fi
# Remove any other non-.0 files that might cause issues
for file in "$CERT_DIR"/*; do
if [ -f "$file" ]; then
case "$(basename "$file")" in
*.0)
# Valid certificate, keep it
;;
*)
# Non-certificate file, remove it
rm -f "$file"
ui_print " Removed $(basename "$file")"
;;
esac
fi
done
}
check_certificate() {
local cert_dir="$MODPATH/system/etc/security/cacerts"
local cert_count=$(find "$cert_dir" -maxdepth 1 -type f -name "*.0" 2>/dev/null | wc -l)
cert_count=$(echo "$cert_count" | tr -d ' ') # Remove whitespace
if [ "$cert_count" -eq 0 ] || [ -z "$cert_count" ]; then
ui_print ""
ui_print "╔════════════════════════════════════════╗"
ui_print "║ ⚠️ CERTIFICATE NOT FOUND! ║"
ui_print "╚════════════════════════════════════════╝"
ui_print ""
ui_print "! Certificate 243f0bfb.0 was not extracted."
ui_print "! Try reinstalling the module."
ui_print ""
else
ui_print "✓ Found $cert_count certificate(s)"
# List certificates
find "$cert_dir" -maxdepth 1 -type f -name "*.0" 2>/dev/null | while read cert; do
ui_print " - $(basename "$cert")"
done
fi
}
setup_android14_plus() {
API=$(getprop ro.build.version.sdk)
if [ "$API" -ge 34 ]; then
ui_print "- Android 14+ detected (API $API)"
ui_print "- APEX CA bypass will be configured"
# Ensure scripts are properly configured for APEX
chmod 0755 "$MODPATH/post-fs-data.sh" 2>/dev/null
chmod 0755 "$MODPATH/service.sh" 2>/dev/null
fi
}
print_summary() {
ui_print ""
ui_print "╔════════════════════════════════════════╗"
ui_print "║ ✓ Installation Complete! ║"
ui_print "╚════════════════════════════════════════╝"
ui_print ""
ui_print " Root Solution: $ROOT_IMPL ($ROOT_VER)"
ui_print " Android API: $API"
ui_print ""
ui_print " ⚡ Actions:"
ui_print " • Reboot your device"
ui_print " • Check: Settings → Security → Trusted credentials"
ui_print ""
if [ "$API" -ge 34 ]; then
ui_print " 📱 Android 14+ Notes:"
ui_print " • APEX bypass runs automatically"
ui_print " • Check ProxyPin after reboot"
ui_print ""
fi
ui_print " 📋 Logs: /data/local/tmp/ProxyPinCert.log"
ui_print ""
# Installation
ui_print " GitHub: https://github.com/firdausmntp/ProxyPin-cert-installer"
ui_print ""
}
#################
# Main
#################
print_banner
detect_root_solution
ui_print "- Detected: $ROOT_IMPL"
ui_print "- Version: $ROOT_VER (code: $ROOT_VER_CODE)"
ui_print ""
check_compatibility
setup_permissions
cleanup_certificates
check_certificate
setup_android14_plus
print_summary

View File

@@ -1,6 +1,11 @@
id=ProxyPinCA
name=ProxyPinCA
version=1.2.0
versionCode=3
author=ProxyPin
description=ProxyPin certificate.
id=proxypin-cert-installer
name=ProxyPin Certificate Installer
version=v1.0
versionCode=1
author=firdausmntp
description=Install ProxyPin CA certificate to System CA Store. Supports Magisk/KernelSU/SukiSU/APatch. Android 5.0-15 compatible with APEX bypass for Android 14+.
updateJson=https://raw.githubusercontent.com/firdausmntp/ProxyPin-cert-installer/main/update.json
support=https://github.com/firdausmntp/ProxyPin-cert-installer
donate=https://github.com/sponsors/firdausmntp
minApi=21
maxApi=36

View File

@@ -1,70 +1,137 @@
#!/system/bin/sh
exec > /data/local/tmp/ProxyPinCA.log
exec 2>&1
#set -x
# ProxyPin Certificate Installer - Post-fs-data Script
# Author: firdausmntp
# GitHub: https://github.com/firdausmntp/ProxyPin-cert-installer
#
# Android 14+ APEX Bypass - Aggressive Implementation
MODDIR=${0%/*}
LOG_FILE="/data/local/tmp/ProxyPinCert.log"
CERT_DIR="${MODDIR}/system/etc/security/cacerts"
TEMP_DIR="/data/local/tmp/proxypin-apex-ca"
set_context() {
[ "$(getenforce)" = "Enforcing" ] || return 0
# Initialize logging
mkdir -p /data/local/tmp 2>/dev/null
echo "" > "$LOG_FILE"
default_selinux_context=u:object_r:system_file:s0
selinux_context=$(ls -Zd $1 | awk '{print $1}')
if [ -n "$selinux_context" ] && [ "$selinux_context" != "?" ]; then
chcon -R $selinux_context $2
else
chcon -R $default_selinux_context $2
fi
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
#LOG_PATH="/data/local/tmp/ProxyPinCA.log"
echo "[$(date +%F) $(date +%T)] - ProxyPinCA post-fs-data.sh start."
chown -R 0:0 ${MODDIR}/system/etc/security/cacerts
if [ -d /apex/com.android.conscrypt/cacerts ]; then
# 检测到 android 14 以上,存在该证书目录
CERT_HASH=243f0bfb
log "╔══════════════════════════════════════════════════════╗"
log "║ ProxyPin Certificate Installer v1.0 ║"
log "╚══════════════════════════════════════════════════════╝"
log ""
log "Post-fs-data started"
log "Module: $MODDIR"
CERT_FILE=${MODDIR}/system/etc/security/cacerts/${CERT_HASH}.0
echo "[$(date +%F) $(date +%T)] - CERT_FILE: ${CERT_FILE}"
if ! [ -e "${CERT_FILE}" ]; then
echo "[$(date +%F) $(date +%T)] - ProxyPinCA certificate not found."
exit 0
fi
API=$(getprop ro.build.version.sdk)
ANDROID_VERSION=$(getprop ro.build.version.release)
log "Android: $ANDROID_VERSION (API $API)"
TEMP_DIR=/data/local/tmp/cacerts-copy
rm -rf "$TEMP_DIR"
mkdir -p -m 700 "$TEMP_DIR"
mount -t tmpfs tmpfs "$TEMP_DIR"
# 复制证书到临时目录
cp -f /apex/com.android.conscrypt/cacerts/* "$TEMP_DIR"
cp -f $CERT_FILE "$TEMP_DIR"
chown -R 0:0 "$TEMP_DIR"
set_context /apex/com.android.conscrypt/cacerts "$TEMP_DIR"
# 检查新证书是否成功添加
CERTS_NUM="$(ls -1 "$TEMP_DIR" | wc -l)"
if [ "$CERTS_NUM" -gt 10 ]; then
mount -o bind "$TEMP_DIR" /apex/com.android.conscrypt/cacerts
for pid in 1 $(pgrep zygote) $(pgrep zygote64); do
nsenter --mount=/proc/${pid}/ns/mnt -- \
mount --bind "$TEMP_DIR" /apex/com.android.conscrypt/cacerts
done
echo "[$(date +%F) $(date +%T)] - Mount success!"
else
echo "[$(date +%F) $(date +%T)] - Mount failed!"
fi
# 卸载临时目录
umount "$TEMP_DIR"
rmdir "$TEMP_DIR"
# Detect root
if [ "$KSU" = "true" ]; then
log "Root: KernelSU/SukiSU (v$KSU_VER_CODE)"
elif [ "$APATCH" = "true" ]; then
log "Root: APatch (v$APATCH_VER_CODE)"
else
echo "[$(date +%F) $(date +%T)] - Android version lower than 14 detected"
set_context /system/etc/security/cacerts ${MODDIR}/system/etc/security/cacerts
echo "[$(date +%F) $(date +%T)] - Mount success!"
fi
log "Root: Magisk/Other"
fi
# Find our certificate
CERT_FILE=$(find "$CERT_DIR" -maxdepth 1 -type f -name "*.0" 2>/dev/null | head -1)
CERT_NAME=$(basename "$CERT_FILE" 2>/dev/null)
if [ -z "$CERT_FILE" ] || [ ! -f "$CERT_FILE" ]; then
log "ERROR: No certificate file found in $CERT_DIR"
log "Certificate 243f0bfb.0 is missing. Try reinstalling the module."
exit 0
fi
log "Certificate: $CERT_NAME"
# For Android < 14, Magic Mount handles it
if [ "$API" -lt 34 ]; then
log "Android < 14: Using Magic Mount"
log "Done!"
exit 0
fi
log ""
log "=== Android 14+ APEX Injection ==="
APEX_CACERTS="/apex/com.android.conscrypt/cacerts"
if [ ! -d "$APEX_CACERTS" ]; then
log "ERROR: APEX cacerts not found"
exit 1
fi
# Prepare temp directory with tmpfs
log "Preparing tmpfs..."
umount "$TEMP_DIR" 2>/dev/null
rm -rf "$TEMP_DIR" 2>/dev/null
mkdir -p "$TEMP_DIR"
if ! mount -t tmpfs tmpfs "$TEMP_DIR"; then
log "ERROR: Failed to mount tmpfs"
exit 1
fi
# Copy existing certs
log "Copying system certificates..."
cp -a "$APEX_CACERTS"/* "$TEMP_DIR/" 2>/dev/null
ORIG_COUNT=$(ls -1 "$TEMP_DIR"/*.0 2>/dev/null | wc -l)
log "System certs: $ORIG_COUNT"
# Add our certificate
log "Adding: $CERT_NAME"
cp -f "$CERT_FILE" "$TEMP_DIR/$CERT_NAME"
# Set permissions
chown -R 0:0 "$TEMP_DIR"
chmod 755 "$TEMP_DIR"
chmod 644 "$TEMP_DIR"/*
# Set SELinux context
APEX_CONTEXT=$(ls -Zd "$APEX_CACERTS" 2>/dev/null | awk '{print $1}')
if [ -n "$APEX_CONTEXT" ] && [ "$APEX_CONTEXT" != "?" ]; then
chcon -R "$APEX_CONTEXT" "$TEMP_DIR" 2>/dev/null
log "SELinux context: $APEX_CONTEXT"
fi
TOTAL_COUNT=$(ls -1 "$TEMP_DIR"/*.0 2>/dev/null | wc -l)
log "Total certs: $TOTAL_COUNT"
# Mount to APEX - try multiple approaches
log ""
log "Mounting to APEX..."
# 1. Global mount
mount --bind "$TEMP_DIR" "$APEX_CACERTS" && log "✓ Global mount"
# 2. Init namespace (PID 1)
nsenter --mount=/proc/1/ns/mnt -- mount --bind "$TEMP_DIR" "$APEX_CACERTS" 2>/dev/null && log "✓ Init (PID 1)"
# 3. Zygote namespaces - critical for apps
for zygote in zygote zygote64; do
PID=$(pidof "$zygote" 2>/dev/null)
if [ -n "$PID" ]; then
nsenter --mount=/proc/$PID/ns/mnt -- mount --bind "$TEMP_DIR" "$APEX_CACERTS" 2>/dev/null && log "$zygote (PID $PID)"
fi
done
# 4. Keep post-fs-data lightweight; service.sh handles targeted post-boot app namespaces.
log "Deferred per-app namespace injection to service.sh"
# Verify
log ""
if [ -f "$APEX_CACERTS/$CERT_NAME" ]; then
log "✓ SUCCESS: $CERT_NAME is in APEX!"
else
log "✗ Certificate not visible in APEX (namespace isolation)"
fi
log ""
log "Post-fs-data completed"
log "══════════════════════════════════════════════════════"

116
service.sh Normal file
View File

@@ -0,0 +1,116 @@
#!/system/bin/sh
# ProxyPin Certificate Installer - Service Script
# Runs after boot to ensure APEX injection persists for all apps
MODDIR=${0%/*}
LOG_FILE="/data/local/tmp/ProxyPinCert.log"
CERT_DIR="${MODDIR}/system/etc/security/cacerts"
TEMP_DIR="/data/local/tmp/proxypin-apex-ca"
APEX_CACERTS="/apex/com.android.conscrypt/cacerts"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [service] $1" >> "$LOG_FILE"
}
log ""
log "══════════════════════════════════════════════════════"
log "Service script started"
API=$(getprop ro.build.version.sdk)
log "Android API: $API"
# Wait for boot
count=0
while [ "$(getprop sys.boot_completed)" != "1" ] && [ $count -lt 60 ]; do
sleep 1
count=$((count + 1))
done
log "Boot completed (${count}s)"
# Skip for Android < 14
if [ "$API" -lt 34 ]; then
log "Android < 14, skipping"
exit 0
fi
# Find our cert
CERT_FILE=$(find "$CERT_DIR" -maxdepth 1 -type f -name "*.0" 2>/dev/null | head -1)
CERT_NAME=$(basename "$CERT_FILE" 2>/dev/null)
if [ -z "$CERT_NAME" ]; then
log "ERROR: No certificate found"
exit 1
fi
log "Certificate: $CERT_NAME"
# Check if already mounted correctly
if [ -f "$APEX_CACERTS/$CERT_NAME" ]; then
log "✓ Certificate already in APEX"
else
log "Certificate not in APEX, re-injecting..."
# Ensure tmpfs is mounted
if ! mountpoint -q "$TEMP_DIR" 2>/dev/null; then
mkdir -p "$TEMP_DIR"
mount -t tmpfs tmpfs "$TEMP_DIR"
cp -a "$APEX_CACERTS"/* "$TEMP_DIR/" 2>/dev/null
cp -f "$CERT_FILE" "$TEMP_DIR/"
chown -R 0:0 "$TEMP_DIR"
chmod 755 "$TEMP_DIR"
chmod 644 "$TEMP_DIR"/*
APEX_CONTEXT=$(ls -Zd "$APEX_CACERTS" 2>/dev/null | awk '{print $1}')
[ -n "$APEX_CONTEXT" ] && chcon -R "$APEX_CONTEXT" "$TEMP_DIR" 2>/dev/null
fi
# Mount globally
mount --bind "$TEMP_DIR" "$APEX_CACERTS" 2>/dev/null
nsenter --mount=/proc/1/ns/mnt -- mount --bind "$TEMP_DIR" "$APEX_CACERTS" 2>/dev/null
fi
# CRITICAL: Mount in all zygote namespaces
# This ensures ALL apps can see the certificate
log "Injecting to zygote namespaces..."
for zygote in zygote zygote64; do
PID=$(pidof "$zygote" 2>/dev/null)
if [ -n "$PID" ]; then
nsenter --mount=/proc/$PID/ns/mnt -- mount --bind "$TEMP_DIR" "$APEX_CACERTS" 2>/dev/null
log " $zygote (PID $PID)"
fi
done
# Also inject to Settings app (for Trusted Credentials visibility)
SETTINGS_PID=$(pidof com.android.settings 2>/dev/null)
if [ -n "$SETTINGS_PID" ]; then
nsenter --mount=/proc/$SETTINGS_PID/ns/mnt -- mount --bind "$TEMP_DIR" "$APEX_CACERTS" 2>/dev/null
log " Settings app (PID $SETTINGS_PID)"
fi
# Inject to ProxyPin if running
PROXYPIN_PID=$(pidof com.proxy.pin 2>/dev/null)
if [ -z "$PROXYPIN_PID" ]; then
# Try alternative package names
PROXYPIN_PID=$(pidof com.network.proxy 2>/dev/null)
fi
if [ -n "$PROXYPIN_PID" ]; then
nsenter --mount=/proc/$PROXYPIN_PID/ns/mnt -- mount --bind "$TEMP_DIR" "$APEX_CACERTS" 2>/dev/null
log " ProxyPin app (PID $PROXYPIN_PID)"
fi
# Final verification
sleep 2
if [ -f "$APEX_CACERTS/$CERT_NAME" ]; then
log "✓ SUCCESS: Certificate in APEX"
else
log "✗ Certificate may not be visible to all apps"
fi
# Count
APEX_COUNT=$(ls -1 "$APEX_CACERTS"/*.0 2>/dev/null | wc -l)
log "APEX certificates: $APEX_COUNT"
log ""
log "Service completed"
log "══════════════════════════════════════════════════════"

56
uninstall.sh Normal file
View File

@@ -0,0 +1,56 @@
#!/system/bin/sh
# ProxyPin Certificate Installer - Uninstall Script
# Author: firdausmntp
# GitHub: https://github.com/firdausmntp/ProxyPin-cert-installer
MODDIR=${0%/*}
LOG_FILE="/data/local/tmp/ProxyPinCert.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [uninstall] $1" >> "$LOG_FILE"
}
log "Uninstall script started"
# Unmount APEX certificates if mounted
unmount_apex() {
local apex_dir="/apex/com.android.conscrypt/cacerts"
local temp_dir="/data/local/tmp/proxypin-apex-ca"
# Unmount APEX directory
if mountpoint -q "$apex_dir" 2>/dev/null; then
umount "$apex_dir" 2>/dev/null
log "Unmounted APEX CA directory"
fi
# Try to unmount in all namespaces
for pid in 1 $(pidof zygote 2>/dev/null) $(pidof zygote64 2>/dev/null); do
if [ -d "/proc/$pid/ns/mnt" ]; then
nsenter --mount=/proc/$pid/ns/mnt -- \
umount "$apex_dir" 2>/dev/null
fi
done
# Unmount and remove temp directory
if mountpoint -q "$temp_dir" 2>/dev/null; then
umount "$temp_dir" 2>/dev/null
log "Unmounted temp directory"
fi
rm -rf "$temp_dir" 2>/dev/null
}
# Cleanup temporary files
cleanup_temp() {
rm -rf /data/local/tmp/apex-ca-copy 2>/dev/null
rm -rf /data/local/tmp/apex-ca-reinject 2>/dev/null
rm -rf /data/local/tmp/proxypin-apex-ca 2>/dev/null
rm -f "$MODDIR/.apex_bypass_needed" 2>/dev/null
log "Cleaned up temporary files"
}
# Main
unmount_apex
cleanup_temp
log "Uninstall completed"
log "NOTE: Reboot recommended to fully remove certificate from system"

6
update.json Normal file
View File

@@ -0,0 +1,6 @@
{
"version": "v1.0",
"versionCode": 1,
"zipUrl": "https://github.com/firdausmntp/ProxyPin-cert-installer/releases/download/v1.0/ProxyPin-Cert-Installer-v1.0.zip",
"changelog": "v1.0 (2026-04-16)\n- Initial release\n- ProxyPin CA certificate pre-included (243f0bfb.0)\n- Multi-root support (Magisk/KernelSU/SukiSU/APatch)\n- Android 5.0-15+ support\n- APEX CA bypass for Android 14+\n- WebUI for status monitoring\n- SELinux enforcing compatible"
}

741
webroot/index.html Normal file
View File

@@ -0,0 +1,741 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>ProxyPin Certificate Manager</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
:root {
--bg: #0f1118;
--surface: #181a24;
--surface-2: #1e2130;
--surface-3: #252838;
--border: #2a2d3a;
--text: #e2e4ea;
--text-2: #9196a8;
--text-3: #5e6374;
--primary: #5b7bf7;
--primary-dim: rgba(91, 123, 247, 0.12);
--green: #3ddc84;
--green-dim: rgba(61, 220, 132, 0.12);
--red: #f25555;
--red-dim: rgba(242, 85, 85, 0.12);
--yellow: #f5c542;
--yellow-dim: rgba(245, 197, 66, 0.12);
}
body {
font-family: -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.5;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
.app {
max-width: 460px;
margin: 0 auto;
padding: 0 16px 32px;
}
/* Toolbar */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
position: sticky;
top: 0;
z-index: 10;
background: var(--bg);
}
.toolbar-title {
font-size: 17px;
font-weight: 600;
}
.toolbar-version {
font-size: 12px;
color: var(--text-3);
background: var(--surface);
padding: 4px 10px;
border-radius: 12px;
}
/* Section */
.section {
margin-bottom: 20px;
}
.section-label {
font-size: 12px;
font-weight: 600;
color: var(--text-3);
text-transform: uppercase;
letter-spacing: 0.6px;
padding: 0 4px;
margin-bottom: 8px;
}
/* Card base */
.card {
background: var(--surface);
border-radius: 14px;
overflow: hidden;
}
/* Status rows */
.status-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
}
.status-row:last-child {
border-bottom: none;
}
.status-row .label {
font-size: 14px;
color: var(--text-2);
}
.status-row .value {
font-size: 13px;
font-weight: 600;
}
.chip {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
}
.chip.ok { background: var(--green-dim); color: var(--green); }
.chip.warn { background: var(--yellow-dim); color: var(--yellow); }
.chip.err { background: var(--red-dim); color: var(--red); }
.chip.info { background: var(--primary-dim); color: var(--primary); }
.chip .dot {
width: 6px; height: 6px;
border-radius: 50%;
background: currentColor;
}
/* Upload area */
.upload-card {
background: var(--surface);
border-radius: 14px;
padding: 20px 16px;
}
.upload-area {
border: 1.5px dashed var(--border);
border-radius: 10px;
padding: 28px 16px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
position: relative;
}
.upload-area:active,
.upload-area.over {
border-color: var(--primary);
background: var(--primary-dim);
}
.upload-area input {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
}
.upload-area .ic {
font-size: 28px;
opacity: 0.8;
margin-bottom: 8px;
}
.upload-area .t1 {
font-size: 14px;
font-weight: 500;
color: var(--text-2);
}
.upload-area .t2 {
font-size: 12px;
color: var(--text-3);
margin-top: 4px;
}
/* Selected file */
.file-row {
display: none;
align-items: center;
gap: 12px;
margin-top: 14px;
padding: 12px;
background: var(--surface-2);
border-radius: 10px;
}
.file-row.show { display: flex; }
.file-row .name {
flex: 1;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-row .size {
font-size: 11px;
color: var(--text-3);
}
.file-row .rm {
width: 28px; height: 28px;
border: none;
background: var(--red-dim);
color: var(--red);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
display: flex;
align-items: center;
justify-content: center;
}
/* Buttons */
.btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 13px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
border: none;
cursor: pointer;
transition: opacity 0.2s, transform 0.1s;
-webkit-user-select: none;
}
.btn:active { transform: scale(0.98); }
.btn:disabled { opacity: 0.4; pointer-events: none; }
.btn-primary {
background: var(--primary);
color: #fff;
margin-top: 14px;
}
.btn-outline {
background: transparent;
color: var(--text-2);
border: 1px solid var(--border);
}
.btn-outline:active {
background: var(--surface-2);
}
.btn-danger {
background: var(--red);
color: #fff;
}
/* Action list */
.action-item {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
cursor: pointer;
transition: background 0.15s;
-webkit-user-select: none;
}
.action-item:last-child { border-bottom: none; }
.action-item:active { background: var(--surface-2); }
.action-item .a-icon {
width: 36px; height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
flex-shrink: 0;
}
.action-item .a-icon.blue { background: var(--primary-dim); }
.action-item .a-icon.amber { background: var(--yellow-dim); }
.action-item .a-icon.red { background: var(--red-dim); }
.action-item .a-icon.green { background: var(--green-dim); }
.action-item .a-text .a-title {
font-size: 14px;
font-weight: 500;
}
.action-item .a-text .a-desc {
font-size: 12px;
color: var(--text-3);
margin-top: 1px;
}
.action-item .arrow {
margin-left: auto;
color: var(--text-3);
font-size: 14px;
}
/* Logs */
.log-box {
display: none;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
margin: 12px 16px 16px;
padding: 12px;
max-height: 260px;
overflow-y: auto;
font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
font-size: 11px;
line-height: 1.7;
color: var(--text-3);
}
.log-box.show { display: block; }
.log-box::-webkit-scrollbar { width: 3px; }
.log-box::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.log-box .l { padding: 1px 0; word-break: break-all; }
.log-box .l.g { color: var(--green); }
.log-box .l.r { color: var(--red); }
.log-box .l.y { color: var(--yellow); }
.log-box .l.b { color: var(--primary); }
/* Progress */
.progress {
height: 3px;
background: var(--surface-3);
border-radius: 3px;
margin-top: 12px;
overflow: hidden;
display: none;
}
.progress.show { display: block; }
.progress .bar {
height: 100%;
background: var(--primary);
width: 0%;
border-radius: 3px;
transition: width 0.4s ease;
}
/* Toast */
.toast-wrap {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
width: calc(100% - 32px);
max-width: 428px;
pointer-events: none;
}
.toast {
padding: 13px 16px;
border-radius: 12px;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
gap: 10px;
pointer-events: auto;
animation: tIn 0.3s ease;
margin-top: 8px;
}
.toast.ok { background: #1a2e22; border: 1px solid #264d35; color: var(--green); }
.toast.err { background: #2e1a1a; border: 1px solid #4d2626; color: var(--red); }
.toast.inf { background: #1a1e2e; border: 1px solid #26354d; color: var(--primary); }
.toast.out { animation: tOut 0.25s ease forwards; }
@keyframes tIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
@keyframes tOut { to { opacity: 0; transform: translateY(8px); } }
/* Modal */
.modal-bg {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.6);
z-index: 50;
align-items: flex-end;
justify-content: center;
padding: 16px;
}
.modal-bg.show { display: flex; }
.modal-box {
background: var(--surface);
border-radius: 18px;
width: 100%;
max-width: 400px;
padding: 24px 20px;
animation: mIn 0.25s ease;
}
@keyframes mIn { from { opacity: 0; transform: translateY(40px); } to { opacity: 1; transform: translateY(0); } }
.modal-box h3 { font-size: 16px; font-weight: 600; margin-bottom: 6px; }
.modal-box p { font-size: 13px; color: var(--text-2); margin-bottom: 18px; }
.modal-box .btns { display: flex; gap: 10px; }
.modal-box .btns .btn { flex: 1; }
/* Spinner */
.spin {
width: 15px; height: 15px;
border: 2px solid rgba(255,255,255,0.25);
border-top-color: #fff;
border-radius: 50%;
animation: sp 0.6s linear infinite;
display: inline-block;
}
@keyframes sp { to { transform: rotate(360deg); } }
/* Footer */
.foot {
text-align: center;
padding: 16px 0 8px;
font-size: 11px;
color: var(--text-3);
}
.foot a { color: var(--text-2); text-decoration: none; }
</style>
</head>
<body>
<div class="toast-wrap" id="toasts"></div>
<div class="modal-bg" id="rebootModal">
<div class="modal-box">
<h3>Reboot device?</h3>
<p>Device will restart to apply certificate changes. Make sure all work is saved.</p>
<div class="btns">
<button class="btn btn-outline" onclick="closeModal()">Cancel</button>
<button class="btn btn-danger" onclick="doReboot()">Reboot</button>
</div>
</div>
</div>
<div class="app">
<div class="toolbar">
<span class="toolbar-title">ProxyPin Cert Manager</span>
<span class="toolbar-version">v1.0</span>
</div>
<!-- Status -->
<div class="section">
<div class="section-label">Status</div>
<div class="card">
<div class="status-row">
<span class="label">Module</span>
<span id="sModule"><span class="chip info"><span class="dot"></span> checking</span></span>
</div>
<div class="status-row">
<span class="label">Certificate</span>
<span id="sCert"><span class="chip info"><span class="dot"></span> checking</span></span>
</div>
<div class="status-row">
<span class="label">APEX Injection</span>
<span id="sApex"><span class="chip info"><span class="dot"></span> checking</span></span>
</div>
<div class="status-row">
<span class="label">Android</span>
<span id="sAndroid" class="value" style="color:var(--text-2)"></span>
</div>
</div>
</div>
<!-- Upload -->
<div class="section">
<div class="section-label">Certificate</div>
<div class="upload-card">
<div class="upload-area" id="dropZone">
<div class="ic">📄</div>
<div class="t1">Select certificate file</div>
<div class="t2">Use hash filename format (.0), example: 243f0bfb.0</div>
<input type="file" id="fileIn" accept=".0">
</div>
<div class="file-row" id="fileRow">
<span class="name" id="fName"></span>
<span class="size" id="fSize"></span>
<button class="rm" onclick="clearFile()"></button>
</div>
<div class="progress" id="prog"><div class="bar" id="progBar"></div></div>
<button class="btn btn-primary" id="installBtn" disabled onclick="install()">Install Certificate</button>
</div>
</div>
<!-- Actions -->
<div class="section">
<div class="section-label">Actions</div>
<div class="card">
<div class="action-item" onclick="reinject()">
<div class="a-icon blue">💉</div>
<div class="a-text">
<div class="a-title">Re-inject Certificate</div>
<div class="a-desc">Force APEX namespace injection</div>
</div>
<span class="arrow"></span>
</div>
<div class="action-item" onclick="toggleLog()">
<div class="a-icon amber">📋</div>
<div class="a-text">
<div class="a-title">View Logs</div>
<div class="a-desc">Module operation logs</div>
</div>
<span class="arrow"></span>
</div>
<div class="action-item" onclick="checkAll()">
<div class="a-icon green">🔄</div>
<div class="a-text">
<div class="a-title">Refresh Status</div>
<div class="a-desc">Recheck module and certificate</div>
</div>
<span class="arrow"></span>
</div>
<div class="action-item" onclick="showReboot()">
<div class="a-icon red"></div>
<div class="a-text">
<div class="a-title">Reboot Device</div>
<div class="a-desc">Apply pending changes</div>
</div>
<span class="arrow"></span>
</div>
</div>
<div class="log-box" id="logBox"></div>
</div>
<div class="foot">
<a href="https://github.com/firdausmntp/ProxyPin-cert-installer">GitHub</a>
&nbsp;·&nbsp;
<a href="https://github.com/wanghongenpin/network_proxy_flutter">ProxyPin</a>
</div>
</div>
<script>
const MOD = '/data/adb/modules/proxypin-cert-installer';
const CERTS = MOD + '/system/etc/security/cacerts';
const LOG = '/data/local/tmp/ProxyPinCert.log';
function exec(cmd) {
return new Promise((resolve, reject) => {
const cb = '_c' + Date.now() + Math.random().toString(36).slice(2,6);
window[cb] = (e, out, err) => { delete window[cb]; e === 0 ? resolve(out||'') : reject(new Error(err||'err')); };
try { ksu.exec(cmd, '{}', cb); } catch(x) { delete window[cb]; reject(x); }
});
}
async function run(cmd) { try { return await exec(cmd); } catch(e) { return ''; } }
// Toast
function toast(msg, type='inf') {
const el = document.createElement('div');
el.className = 'toast ' + type;
el.textContent = msg;
document.getElementById('toasts').appendChild(el);
setTimeout(() => { el.classList.add('out'); setTimeout(() => el.remove(), 250); }, 3000);
}
// Status
async function checkAll() {
const sM = document.getElementById('sModule');
const sC = document.getElementById('sCert');
const sA = document.getElementById('sApex');
const sV = document.getElementById('sAndroid');
// Module
try {
const exists = (await run(`[ -d "${MOD}" ] && echo y`)).trim();
const disabled = (await run(`[ -f "${MOD}/disable" ] && echo y`)).trim();
if (exists === 'y' && disabled !== 'y') sM.innerHTML = '<span class="chip ok"><span class="dot"></span> Active</span>';
else if (disabled === 'y') sM.innerHTML = '<span class="chip warn"><span class="dot"></span> Disabled</span>';
else sM.innerHTML = '<span class="chip err"><span class="dot"></span> Not found</span>';
} catch(e) { sM.innerHTML = '<span class="chip warn"><span class="dot"></span> Unknown</span>'; }
// Cert
try {
const n = parseInt((await run(`find "${CERTS}" -maxdepth 1 -type f -name "*.0" 2>/dev/null | wc -l`)).trim()) || 0;
if (n > 0) sC.innerHTML = '<span class="chip ok"><span class="dot"></span> Installed (' + n + ')</span>';
else sC.innerHTML = '<span class="chip err"><span class="dot"></span> Missing</span>';
} catch(e) { sC.innerHTML = '<span class="chip warn"><span class="dot"></span> Error</span>'; }
// APEX
try {
const api = parseInt((await run('getprop ro.build.version.sdk')).trim()) || 0;
if (api >= 34) {
const cn = (await run(`find "${CERTS}" -maxdepth 1 -type f -name "*.0" 2>/dev/null | head -1 | xargs basename 2>/dev/null`)).trim();
if (cn) {
const ok = (await run(`[ -f "/apex/com.android.conscrypt/cacerts/${cn}" ] && echo y`)).trim();
sA.innerHTML = ok === 'y'
? '<span class="chip ok"><span class="dot"></span> Injected</span>'
: '<span class="chip warn"><span class="dot"></span> Pending</span>';
} else {
sA.innerHTML = '<span class="chip err"><span class="dot"></span> No cert</span>';
}
} else {
sA.innerHTML = '<span class="chip info"><span class="dot"></span> N/A</span>';
}
} catch(e) { sA.innerHTML = '<span class="chip info"><span class="dot"></span> N/A</span>'; }
// Android
try {
const ver = (await run('getprop ro.build.version.release')).trim();
const api = (await run('getprop ro.build.version.sdk')).trim();
sV.textContent = ver + ' (API ' + api + ')';
} catch(e) { sV.textContent = '—'; }
}
// File
const fileIn = document.getElementById('fileIn');
const dropZone = document.getElementById('dropZone');
fileIn.addEventListener('change', onFile);
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('over'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('over'));
dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('over'); fileIn.files = e.dataTransfer.files; onFile(); });
function onFile() {
const f = fileIn.files[0];
if (!f) return;
const certName = f.name.trim().toLowerCase();
const ok = /^[0-9a-f]{8}\.0$/.test(certName);
if (!ok) {
toast('Filename must be hash format: 8 hex chars + .0', 'err');
fileIn.value = '';
return;
}
document.getElementById('fName').textContent = f.name;
document.getElementById('fSize').textContent = fmt(f.size);
document.getElementById('fileRow').classList.add('show');
document.getElementById('installBtn').disabled = false;
}
function clearFile() {
fileIn.value = '';
document.getElementById('fileRow').classList.remove('show');
document.getElementById('installBtn').disabled = true;
}
function fmt(b) {
if (b < 1024) return b + ' B';
return (b/1024).toFixed(1) + ' KB';
}
// Install
async function install() {
const f = fileIn.files[0];
if (!f) return;
const certName = f.name.trim().toLowerCase();
if (!/^[0-9a-f]{8}\.0$/.test(certName)) {
toast('Invalid certificate filename', 'err');
return;
}
const btn = document.getElementById('installBtn');
const prog = document.getElementById('prog');
const bar = document.getElementById('progBar');
const tmpPath = '/data/local/tmp/proxypin-upload.cert';
btn.disabled = true;
btn.innerHTML = '<span class="spin"></span> Installing...';
prog.classList.add('show');
bar.style.width = '30%';
try {
const b64 = await new Promise((ok, no) => {
const r = new FileReader();
r.onload = () => ok(r.result.split(',')[1]);
r.onerror = no;
r.readAsDataURL(f);
});
bar.style.width = '60%';
await run(`mkdir -p "${CERTS}"; rm -f "${tmpPath}"`);
await run(`echo '${b64}' | base64 -d > "${tmpPath}"`);
await run(`install -m 0644 -o 0 -g 0 "${tmpPath}" "${CERTS}/${certName}"`);
await run(`rm -f "${tmpPath}"`);
bar.style.width = '90%';
const v = (await run(`[ -f "${CERTS}/${certName}" ] && echo y`)).trim();
if (v === 'y') {
bar.style.width = '100%';
toast('Certificate installed. Reboot to apply.', 'ok');
setTimeout(() => { clearFile(); prog.classList.remove('show'); bar.style.width='0'; checkAll(); }, 1200);
} else throw new Error('Write failed');
} catch(e) {
toast('Install failed: ' + e.message, 'err');
prog.classList.remove('show');
bar.style.width = '0';
}
btn.disabled = false;
btn.textContent = 'Install Certificate';
}
// Actions
async function reinject() {
toast('Running re-injection...', 'inf');
await run(`sh ${MOD}/service.sh`);
toast('Re-injection done', 'ok');
setTimeout(checkAll, 800);
}
let logOpen = false;
async function toggleLog() {
const box = document.getElementById('logBox');
if (logOpen) { box.classList.remove('show'); logOpen = false; return; }
const raw = await run(`tail -60 "${LOG}" 2>/dev/null || echo "No logs available"`);
const lines = raw.split('\n').filter(l => l.trim());
box.innerHTML = lines.map(l => {
let c = '';
if (/✓|SUCCESS/.test(l)) c = 'g';
else if (/ERROR|✗|FAIL/.test(l)) c = 'r';
else if (/WARNING|⚠/.test(l)) c = 'y';
else if (/═|╔|╚|===/.test(l)) c = 'b';
return '<div class="l ' + c + '">' + esc(l) + '</div>';
}).join('');
box.classList.add('show');
box.scrollTop = box.scrollHeight;
logOpen = true;
}
function esc(s) { const d = document.createElement('span'); d.textContent = s; return d.innerHTML; }
function showReboot() { document.getElementById('rebootModal').classList.add('show'); }
function closeModal() { document.getElementById('rebootModal').classList.remove('show'); }
async function doReboot() { closeModal(); toast('Rebooting...', 'inf'); await run('reboot'); }
document.addEventListener('DOMContentLoaded', checkAll);
</script>
</body>
</html>