Created
March 3, 2025 00:18
-
-
Save forcequitOS/c4526c303cfb1e990eb33e2b3cfbe997 to your computer and use it in GitHub Desktop.
Extract an Apple Watch backup from an unencrypted iPhone backup. This script was written 100% by AI, good luck.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Set your iPhone's backup path before complaining | |
# Also, please make sure your iPhone actually has/had a paired Apple Watch. Otherwise, it's going to be very embarrasing if the script doesn't work. | |
# This script was genuinely written 100% by Claude. I am almost 100% sure it's missing a LOT of stuff. But, hey, it's something, and it's the only open-source way to extract user-files from watchOS /shrug. | |
# This script is intended for usage on macOS. It might work on Linux too, I don't know, and I haven't tested it there. Good luck. | |
BACKUP_PATH="$HOME/Library/Application Support/MobileSync" | |
OUTPUT_DIR="$HOME/NanoBackupExtract" | |
# Create output directory | |
mkdir -p "$OUTPUT_DIR" | |
echo "Extracting files from $BACKUP_PATH to $OUTPUT_DIR..." | |
# Clear previous output to start fresh | |
rm -rf "$OUTPUT_DIR" | |
mkdir -p "$OUTPUT_DIR" | |
# Function to find a file in the backup | |
find_backup_file() { | |
local file_id="$1" | |
local domain="$2" | |
local rel_path="$3" | |
# Try to find the file in various possible locations | |
# 1. Direct file ID | |
if [ -f "$BACKUP_PATH/$file_id" ]; then | |
echo "$BACKUP_PATH/$file_id" | |
return 0 | |
fi | |
# 2. In Backup directory | |
if [ -f "$BACKUP_PATH/Backup/$file_id" ]; then | |
echo "$BACKUP_PATH/Backup/$file_id" | |
return 0 | |
fi | |
# 3. In two-char subdirectory structure (common in iOS backups) | |
local first_two="${file_id:0:2}" | |
if [ -f "$BACKUP_PATH/$first_two/$file_id" ]; then | |
echo "$BACKUP_PATH/$first_two/$file_id" | |
return 0 | |
fi | |
# 4. Try with domain-path hash method | |
local hash_input="${domain}-${rel_path}" | |
local hash=$(echo -n "$hash_input" | openssl sha1 | awk '{print $2}') | |
# Try direct hash | |
if [ -f "$BACKUP_PATH/$hash" ]; then | |
echo "$BACKUP_PATH/$hash" | |
return 0 | |
fi | |
# Try in two-char subdirectory | |
local hash_first_two="${hash:0:2}" | |
if [ -f "$BACKUP_PATH/$hash_first_two/$hash" ]; then | |
echo "$BACKUP_PATH/$hash_first_two/$hash" | |
return 0 | |
fi | |
echo "" | |
return 1 | |
} | |
# Query and process the files | |
sqlite3 "$BACKUP_PATH/Manifest.db" \ | |
"SELECT fileID, domain, relativePath FROM Files WHERE relativePath LIKE 'Library/NanoBackup/%';" | \ | |
while IFS='|' read -r file_id domain rel_path; do | |
echo "Processing: $domain - $rel_path" | |
# Determine if this is likely a file or directory | |
# We'll assume it's a directory if the path ends with / or doesn't contain a period in the last component | |
is_dir=0 | |
if [[ "$rel_path" == */ ]]; then | |
is_dir=1 | |
else | |
# Get the basename | |
basename=$(basename "$rel_path") | |
# If no dot, or dot is first character (hidden file but no extension), treat as directory | |
if [[ "$basename" != *.* || "$basename" == .* && "$basename" != *.*.* ]]; then | |
is_dir=1 | |
fi | |
fi | |
# Create destination path | |
dest_path="$OUTPUT_DIR/$domain/$rel_path" | |
if [ $is_dir -eq 1 ]; then | |
# It's a directory, just create it | |
echo " → Creating directory: $dest_path" | |
mkdir -p "$dest_path" | |
else | |
# It's a file | |
# Make sure parent directory exists | |
parent_dir=$(dirname "$dest_path") | |
mkdir -p "$parent_dir" | |
# Find the source file | |
source_path=$(find_backup_file "$file_id" "$domain" "$rel_path") | |
if [ -n "$source_path" ]; then | |
echo " → Copying file: $source_path → $dest_path" | |
cp "$source_path" "$dest_path" | |
if [ $? -eq 0 ]; then | |
echo " ✓ Success" | |
else | |
echo " ✗ Failed to copy" | |
fi | |
else | |
echo " → File not found, trying database BLOB extract" | |
# Try extracting directly from the database as a last resort | |
sqlite3 "$BACKUP_PATH/Manifest.db" \ | |
"SELECT writefile('$dest_path', file) FROM Files WHERE fileID='$file_id';" | |
if [ -f "$dest_path" ]; then | |
echo " ✓ Extracted from database BLOB" | |
else | |
echo " ✗ Failed to extract file" | |
fi | |
fi | |
fi | |
done | |
echo "Extraction complete! Files are in: $OUTPUT_DIR" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment