Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd_device_debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func runImageCommand(ctx commandContext) {
err := imagemounter.MountImage(ctx.Device, imagePath)
if err != nil {
slog.Error("error mounting image", "image", imagePath, "udid", ctx.Device.Properties.SerialNumber, "err", err)
return
os.Exit(1)
}
slog.Info("success mounting image", "image", imagePath, "udid", ctx.Device.Properties.SerialNumber)
}
Expand Down
58 changes: 58 additions & 0 deletions cmd_device_management.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bufio"
"encoding/base64"
"fmt"
"log/slog"
"os"
Expand Down Expand Up @@ -270,3 +271,60 @@ func runDevModeCommand(ctx commandContext) {
slog.Info("Developer Mode menu has been revealed on the device. Go to Settings → Privacy & Security → Developer Mode to enable it.")
}
}

func runMdmCommand(ctx commandContext) {
p12file, _ := ctx.Args.String("--p12file")
p12password, _ := ctx.Args.String("--password")
if p12password == "" {
p12password = os.Getenv("P12_PASSWORD")
}

p12bytes, err := os.ReadFile(p12file)
exitIfError("could not read p12file", err)

conn, err := mcinstall.New(ctx.Device)
exitIfError("failed to connect to MCInstall service", err)

err = conn.Escalate(p12bytes, p12password)
exitIfError("failed to escalate MCInstall session", err)

if fetchToken, _ := ctx.Args.Bool("fetch-unlock-token"); fetchToken {
output, _ := ctx.Args.String("--output")
token, err := conn.FetchUnlockToken()
exitIfError("failed to fetch unlock token", err)
if output == "-" {
fmt.Println(base64.StdEncoding.EncodeToString(token))
} else {
err = os.WriteFile(output, token, 0o600)
exitIfError("failed to write token file", err)
fmt.Println(convertToJSONString(map[string]any{"path": output, "bytes": len(token)}))
}
return
}

if clearPasscode, _ := ctx.Args.Bool("clear-passcode"); clearPasscode {
tokenFile, _ := ctx.Args.String("--token")
var tokenBytes []byte
if tokenFile == "-" {
var encoded string
_, err = fmt.Scan(&encoded)
exitIfError("could not read token from stdin", err)
tokenBytes, err = base64.StdEncoding.DecodeString(encoded)
exitIfError("could not base64-decode token", err)
} else {
tokenBytes, err = os.ReadFile(tokenFile)
exitIfError("could not read token file", err)
}
err = conn.ClearPasscode(tokenBytes)
exitIfError("failed to clear passcode", err)
fmt.Println(convertToJSONString(map[string]any{"status": "ok"}))
return
}

if clearScreenTime, _ := ctx.Args.Bool("clear-screen-time-password"); clearScreenTime {
err = conn.ClearScreenTimePassword()
exitIfError("failed to clear Screen Time password", err)
fmt.Println(convertToJSONString(map[string]any{"status": "ok"}))
return
}
}
1 change: 1 addition & 0 deletions cmd_device_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ var deviceCommands = []command{
commandByBool("setlocationgpx", runSetLocationGPXCommand),
commandByBool("timeformat", runTimeFormatCommand),
commandByBool("httpproxy", runHTTPProxyCommand),
commandByBool("mdm", runMdmCommand),
commandByBool("profile", runProfileCommand),
commandByBool("forward", runForwardCommand),
commandByBool("launch", runLaunchCommand),
Expand Down
47 changes: 47 additions & 0 deletions ios/mcinstall/mcinstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,50 @@ func (mcInstallConn *Connection) SetWallpaperSupervised(image []byte, screen Wal
func (mcInstallConn *Connection) GetCloudConfiguration() (map[string]interface{}, error) {
return mcInstallConn.sendAndReceive(request("GetCloudConfiguration"))
}

// FetchUnlockToken retrieves the passcode unlock token from an already-escalated
// MCInstall connection. The device must have no passcode set; it returns an error
// with DMCKeybagErrorDomain code 37002 if a passcode is active.
func (mcInstallConn *Connection) FetchUnlockToken() ([]byte, error) {
response, err := mcInstallConn.sendAndReceive(request("RequestUnlockToken"))
if err != nil {
return nil, err
}
if !checkStatus(response) {
return nil, fmt.Errorf("RequestUnlockToken failed: %+v", response)
}
token, ok := response["UnlockToken"].([]byte)
if !ok {
return nil, fmt.Errorf("UnlockToken missing or wrong type in response")
}
return token, nil
}

// ClearPasscode removes the device lock passcode using a previously saved unlock
// token on an already-escalated MCInstall connection.
func (mcInstallConn *Connection) ClearPasscode(unlockToken []byte) error {
response, err := mcInstallConn.sendAndReceive(map[string]interface{}{
"RequestType": "ClearPasscode",
"UnlockToken": unlockToken,
})
if err != nil {
return err
}
if !checkStatus(response) {
return fmt.Errorf("ClearPasscode failed: %+v", response)
}
return nil
}

// ClearScreenTimePassword clears the Screen Time restrictions passcode on an
// already-escalated MCInstall connection. No unlock token is required.
func (mcInstallConn *Connection) ClearScreenTimePassword() error {
response, err := mcInstallConn.sendAndReceive(request("ClearRestrictionsPassword"))
if err != nil {
return err
}
if !checkStatus(response) {
return fmt.Errorf("ClearRestrictionsPassword failed: %+v", response)
}
return nil
}
11 changes: 10 additions & 1 deletion ios/pair.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,16 @@ func PairSupervised(device DeviceEntry, p12bytes []byte, p12Password string) err
if err != nil {
return err
}
escrow := respMap["EscrowBag"].([]byte)
if errMsg, ok := respMap["Error"].(string); ok {
return fmt.Errorf("PairSupervised failed: %s", errMsg)
}
escrow, ok := respMap["EscrowBag"].([]byte)
if !ok {
// Device is locked — it accepted the supervisor identity but cannot update its
// trust store or generate a new escrow bag while the passcode screen is active.
// The existing pair record in usbmuxd is still valid; skip saving a new one.
return nil
}

usbmuxConn, err = NewUsbMuxConnectionSimple()
defer usbmuxConn.Close()
Expand Down
25 changes: 25 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ Usage:
ios listen [options]
ios lockdown get [<key>] [--domain=<domain>] [options]
ios memlimitoff (--process=<processName>) [options]
ios mdm fetch-unlock-token --p12file=<p12file> --output=<output> [--password=<p12password>] [options]
ios mdm clear-passcode --p12file=<p12file> --token=<tokenFile> [--password=<p12password>] [options]
ios mdm clear-screen-time-password --p12file=<p12file> [--password=<p12password>] [options]
ios mobilegestalt <key>... [--plist] [options]
ios pair [--p12file=<orgid>] [--password=<p12password>] [options]
ios pcap [options] [--pid=<processID>] [--process=<processName>]
Expand Down Expand Up @@ -354,6 +357,28 @@ The commands work as following:

ios memlimitoff (--process=<processName>) [options] Waives memory limit set by iOS (For instance a Broadcast Extension limit is 50 MB).

ios mdm fetch-unlock-token --p12file=<p12file> --output=<output> [--password=<p12password>]
Save the device passcode unlock token. The device must have no passcode
set; run once during provisioning. The token can be passed to
"mdm clear-passcode" at any time later.
Use --output=<file> to write raw bytes to a file, or --output=- to
print base64-encoded token to stdout (for piping into a secrets manager).
Requires supervision: pass --p12file and --password (or P12_PASSWORD env var).

ios mdm clear-passcode --p12file=<p12file> --token=<tokenFile> [--password=<p12password>]
Remove the device lock passcode using a previously saved unlock token.
The token must have been saved before the passcode was set.
Works regardless of current lock state; does not require knowing the passcode.
Use --token=<file> for a raw token file, or --token=- to read a
base64-encoded token from stdin.
Requires supervision: pass --p12file and --password (or P12_PASSWORD env var).

ios mdm clear-screen-time-password --p12file=<p12file> [--password=<p12password>]
Clear the Screen Time restrictions passcode (4-digit PIN protecting Screen Time settings).
No unlock token required; supervisor identity alone suffices.
Does not affect profile-based restrictions or the device lock passcode.
Requires supervision: pass --p12file and --password (or P12_PASSWORD env var).

ios mobilegestalt <key>... [--plist] [options] Lets you query mobilegestalt keys.
Standard output is json but if desired you can get it in plist format by adding the --plist param.
Ex.: "ios mobilegestalt MainScreenCanvasSizes ArtworkTraits --plist"
Expand Down
Loading