ゴーストAPNプロファイルをバックアップの編集と復元で修復する。
デバイス

ゴーストAPNプロファイルをバックアップの編集と復元で修復する。

iPhoneのAPNプロファイルが表示されず、既にインストール済みになる症状を復旧した備忘録。バックアップから古いAPNを除外し、初期化後はAPN未設定で復元してから最後にインストールする手順書。

8k39

8k39

公開日: 2026年9月27日

はじめに

APN構成プロファイルが設定画面に表示されなくなった。新しいプロファイルをインストールしようとすると、「既にインストール済み」と表示される状態。

「プロファイルをインストールできませんでした」「モバイルデータ通信構成プロファイルがすでにインストールされています。」と表示された画面

「設定 → 一般 → VPNとデバイス管理」にも表示されず、通常の操作では削除できない。

ネットワーク設定のリセットと「すべての設定をリセット」を試しても、症状は改善しなかった。そこで、Windowsとpymobiledevice3でバックアップ内の設定を調べた。

自分の環境では、古いAPNを除外した暗号化バックアップを、初期化したiPhoneに復元することで復旧できた。

初期化 → 最低限の初期設定 → 古いAPNを除外したバックアップを復元 → 最後にAPNをインストール

初期化後はホーム画面まで最低限の設定だけ進め、APNプロファイルやアプリを追加せずに復元する。Wi-Fi接続やPCとの信頼設定は行ってよい。

復旧後は、プロファイルの表示・削除・再インストールを確認した。再起動後も表示が残り、モバイル通信も利用できた。

使用した環境

  • Windows
  • Python 3.12(掲載スクリプトは3.11以上が必要)
  • pymobiledevice3 11.19.1
  • Appleデバイスアプリ
  • iOS 27.0のiPhone
  • U-NEXT MOBILE

スクリプトはpymobiledevice3の内部処理を使うため、バージョンを11.19.1に固定している。

バックアップを調べて分かったこと

最初に確認したプロファイル一覧が空だった。

{
  "OrderedIdentifiers": [],
  "ProfileManifest": {},
  "ProfileMetadata": {},
  "Status": "Acknowledged"
}

バックアップ内のファイルには、以前使っていたLINEモバイルのAPN設定line.meが残っていた。

Domain: WirelessDomain
Path: Library/Preferences/com.apple.commcenter.cellular_profile.plist

ユーザー名等を省略した設定内容

{
  "profile": {
    "AttachAPN": {
      "apn": "line.me",
      "auth_type": "CHAP"
    },
    "APNs": [
      {
        "apn": "line.me",
        "auth_type": "CHAP"
      }
    ]
  }
}

設定画面のプロファイル一覧が空でも、通信に使うAPN設定は残っていた。

今回は、インストール時のエラーと古いAPN設定の両方から症状を確認している。プロファイル一覧が空という結果だけで、同じ症状だとは判断できない。

復元前にAPNを入れると、表示が消えた

最初は、古いAPNを除外したバックアップを、初期化せずに復元した。端末にはline.meが残り、症状は改善しなかった。

この復元で指定した--no-removeは、復元対象に含まれない項目を削除しない指定で、バックアップからファイルを除外しても、端末にある同じ設定が消えるとは限らない。

次に、iPhoneを初期化してU-NEXTのAPNをインストールすると正常に設定できた。ところが、その状態で編集済みバックアップを復元すると、通信はできてもプロファイルが再び表示されなくなった。

確認した内容 復元後の状態
通信に使うAPN unext.jp
モバイル通信 利用可能
プロファイル一覧 空
比較したプロファイル管理ファイル10件 復元元とバイト単位で一致

先に設定したU-NEXTのAPNが残る一方、プロファイルの管理情報はバックアップの内容に戻った可能性が高いと考えている。ただし、内部の処理をすべて確認できたわけではない。通信できても、プロファイルを正常に管理できる状態ではなかった。

そこで、同じ編集済みバックアップを使い、初期化後にAPNを入れずに復元した。復元が終わってからAPNをインストールすると、正常に管理できる状態に戻った。

作業手順

1. 暗号化バックアップを作成する

初期化する前に、Appleデバイスアプリで対象のiPhoneを選び、「ローカルのバックアップを暗号化」を有効にしてバックアップを作成する。

作成日時と暗号化されていることを確認し、設定したパスワードを控える。このパスワードは、Apple Accountのパスワードとは別だ。

暗号化バックアップには、保存済みパスワード、ヘルスケア、通話履歴なども含まれる。これらは非暗号化のバックアップでは対象にならない。Face IDや端末のパスコードは引き継がれない。

Apple公式:バックアップの暗号化について

C:\Users\ユーザー名\Apple\MobileSync\Backup\端末のUDID

保存先が異なる場合は、手順3のprepareコマンドで--sourceに実際のフォルダーを指定する。

作業途中で、初期化直後のiPhoneをこの保存先にバックアップしてはいけない。元のデータを上書きするおそれがある。

2. pymobiledevice3をインストールする

Python 3.12を導入し、PowerShellで次のコマンドを実行する。

Python公式ダウンロード

py -m pip install "pymobiledevice3==11.19.1"

iPhoneをUSB接続してロックを解除し、「このコンピュータを信頼」を許可する。次のコマンドで接続した端末を確認できる。

py -m pymobiledevice3 usbmux list

表示されたIdentifierを確認し、次の値を自分のiPhoneのものに置き換える。

$env:APN_TARGET_UDID = "ここを自分のiPhoneのIdentifierに置き換える"

この環境変数は、同じPowerShellから実行するスクリプトが使う。PowerShellを閉じて開き直した場合は、再度設定が必要。

対象が意図したiPhoneであることを確認してから、ペアリングとプロファイル一覧の取得を行う。

py -m pymobiledevice3 lockdown pair --udid "$env:APN_TARGET_UDID"
py -m pymobiledevice3 profile list --udid "$env:APN_TARGET_UDID"

削除可能な古いAPNプロファイルが一覧にある場合は、まず設定アプリからの通常の削除を検討する。

pymobiledevice3のGitHub

3. 古いAPNを除外したバックアップのコピーを作る

スクリプトでバックアップ全体を別フォルダーにコピーし、コピー側からAPNファイル1件と、その管理レコードを除外する。

暗号化バックアップのManifest.dbは、ファイル一覧を管理する暗号化されたデータベースで、スクリプトはこのファイルを復号して編集し、再暗号化する。

処理後は、除外したレコードが1件だけであることや、再暗号化したデータを読み出せることを確認する。

以下のコードをapn_encrypted_repair.pyという名前で「ダウンロード」フォルダーに保存する。掲載コードは、作業時に固定していた端末IDを、環境変数で指定する形に調整したもの。

バックアップ編集・復元スクリプトを表示
#!/usr/bin/env python3
"""Prepare an encrypted backup copy without one APN plist; restore separately.
 
For pymobiledevice3 11.19.1. No device access during prepare.
Experimental APN repair: preparation checks do NOT prove an iOS restore will
succeed or resolve the ghost APN. Never use APN-Diagnostic as the source.
"""
import argparse
from contextlib import closing
import getpass
import hashlib
from importlib.metadata import version
import json
import os
from pathlib import Path
import plistlib
import re
import runpy
import shutil
import sqlite3
import sys
import tempfile
 
UDID = os.environ.get('APN_TARGET_UDID', '').strip()
DOMAIN = 'WirelessDomain'
RELATIVE = 'Library/Preferences/com.apple.commcenter.cellular_profile.plist'
RECEIPT = 'APN_REPAIR_READY.json'
 
 
def require(condition, message):
    if not condition:
        raise RuntimeError(message)
 
 
def plist(path):
    return plistlib.loads(path.read_bytes())
 
 
def digest(path):
    with path.open('rb') as f:
        return hashlib.file_digest(f, 'sha256').hexdigest()
 
 
def readonly(path):
    return sqlite3.connect(path.resolve().as_uri() + '?mode=ro', uri=True)
 
 
def check_db(db):
    require(db.execute('PRAGMA quick_check').fetchall() == [('ok',)],
            'Manifest database integrity check failed.')
 
 
def payload_path(folder, file_id):
    require(isinstance(file_id, str) and re.fullmatch('[0-9a-fA-F]{40}', file_id),
            'Unexpected backup file ID.')
    nested = folder / file_id[:2] / file_id
    return nested if nested.is_file() else folder / file_id
 
 
def backend():
    require(version('pymobiledevice3') == '11.19.1',
            'This script was checked against pymobiledevice3 11.19.1. Stop on a different version.')
    from pymobiledevice3.services.mobilebackup2 import Mobilebackup2Service
    return Mobilebackup2Service
 
 
def edit_manifest(original, edited):
    """Copy the decrypted database, remove exactly one row, compare all rows."""
    with closing(readonly(original)) as db:
        check_db(db)
        matches = db.execute(
            'SELECT fileID, flags FROM Files WHERE domain=? AND relativePath=?',
            (DOMAIN, RELATIVE)).fetchall()
        require(len(matches) == 1 and matches[0][1] == 1,
                'Expected exactly one regular APN file entry.')
        file_id = matches[0][0]
        count = db.execute('SELECT COUNT(*) FROM Files').fetchone()[0]
        # Reject incomplete/filtered snapshots before creating a repair copy.
        regular_ids = [r[0] for r in db.execute('SELECT fileID FROM Files WHERE flags=1')]
        with closing(sqlite3.connect(edited)) as out:
            db.backup(out)
    with closing(sqlite3.connect(edited)) as db:
        with db:
            n = db.execute('DELETE FROM Files WHERE fileID=? AND domain=? AND relativePath=?',
                           (file_id, DOMAIN, RELATIVE)).rowcount
            require(n == 1, 'Deletion count is not one.')
        check_db(db)
        require(db.execute('SELECT COUNT(*) FROM Files').fetchone()[0] == count - 1,
                'Unexpected remaining row count.')
        db.execute('ATTACH DATABASE ? AS original', (str(original),))
        removed = db.execute('SELECT * FROM original.Files EXCEPT SELECT * FROM main.Files').fetchall()
        added = db.execute('SELECT * FROM main.Files EXCEPT SELECT * FROM original.Files').fetchall()
        require(len(removed) == 1 and not added,
                'Other file metadata changed unexpectedly.')
        db.execute('DETACH DATABASE original')
    return file_id, count, regular_ids
 
 
def prepare(source, output, password, service):
    source, output = source.resolve(), output.resolve()
    require(source.is_dir(), 'Source backup folder does not exist.')
    require(not output.exists(), 'Output folder already exists. Nothing was overwritten.')
    require(not source.is_relative_to(output) and not output.is_relative_to(source),
            'Source and output folders must be separate.')
    require(not any(name in str(source) for name in ('APN-Diagnostic', 'APN-Current')),
            'A diagnostic backup must never be restored.')
    require(plist(source / 'Manifest.plist').get('IsEncrypted') is True,
            'The source is not encrypted. Do not use the earlier APN-Repair copy.')
    require(plist(source / 'Status.plist').get('SnapshotState') == 'finished',
            'The source backup is not marked finished.')
    info = plist(source / 'Info.plist')
    identity = info.get('Target Identifier') or info.get('Unique Identifier')
    require(isinstance(identity, str) and identity.lower() == UDID.lower(),
            'The source backup is not from the expected new iPhone.')
    for suffix in ('-wal', '-journal'):
        p = source / ('Manifest.db' + suffix)
        require(not p.exists() or p.stat().st_size == 0, 'Active database journal found. Close Apple Devices.')
    original_hash = digest(source / 'Manifest.db')
    metadata_hash = digest(source / 'Manifest.plist')
    print('Source:', source, flush=True)
    print('Backup date (as stored):', info.get('Last Backup Date', 'unknown'), flush=True)
    print('Decrypting and checking the manifest...', flush=True)
    # Closed files in a TemporaryDirectory avoid Windows NamedTemporaryFile locks.
    with tempfile.TemporaryDirectory(prefix='apn-check-') as tmp:
        tmp = Path(tmp)
        original, edited = tmp / 'original.db', tmp / 'edited.db'
        key = service._decrypt_backup_manifest_db(source, password, original)
        file_id, count, regular_ids = edit_manifest(original, edited)
        print('Checking that all backed-up file payloads exist...', flush=True)
        for fid in regular_ids:
            require(payload_path(source, fid).is_file(),
                    'Source contains missing payloads; it may be incomplete or filtered. Stopped.')
        old_blob = payload_path(source, file_id)
        old_blob_hash = digest(old_blob)
        print('Calculating required disk space...', flush=True)
        total = sum(p.stat().st_size for p in source.rglob('*') if p.is_file())
        require(output.parent.is_dir(), 'Output parent folder does not exist.')
        free = shutil.disk_usage(output.parent).free
        print(f'Backup: {total / 2**30:.2f} GiB; free: {free / 2**30:.2f} GiB', flush=True)
        require(free >= total + 2 * 1024**3, 'Not enough space for a physical copy plus 2 GiB.')
        output.mkdir()
        (output / 'INCOMPLETE_DO_NOT_RESTORE.txt').write_text(
            'Preparation is incomplete. Do not restore this folder.\n', encoding='utf-8')
        dest = output / UDID
        print('Copying the COMPLETE encrypted backup. This can take several minutes...', flush=True)
        shutil.copytree(source, dest)
        require(digest(dest / 'Manifest.db') == original_hash, 'Manifest changed during copying.')
        require(digest(dest / 'Manifest.plist') == metadata_hash, 'Encryption metadata changed during copying.')
        for fid in regular_ids:
            a, b = payload_path(source, fid), payload_path(dest, fid)
            require(b.is_file() and b.stat().st_size == a.stat().st_size,
                    'A copied payload is missing or has the wrong size.')
        print('Re-encrypting the edited manifest...', flush=True)
        staged = tmp / 'Manifest.encrypted'
        service._encrypt_backup_manifest_db(edited, staged, key)
        # The copy alone is modified. Every other encrypted payload stays intact.
        shutil.copyfile(staged, dest / 'Manifest.db')
        verify = tmp / 'verify.db'
        service._decrypt_backup_manifest_db(dest, password, verify)
        require(digest(verify) == digest(edited), 'Encrypted manifest round-trip check failed.')
        with closing(readonly(verify)) as db:
            check_db(db)
            require(db.execute('SELECT COUNT(*) FROM Files WHERE domain=? AND relativePath=?',
                               (DOMAIN, RELATIVE)).fetchone()[0] == 0, 'APN entry is still present.')
        payload_path(dest, file_id).unlink()
        require(digest(source / 'Manifest.db') == original_hash and
                digest(source / 'Manifest.plist') == metadata_hash and
                digest(old_blob) == old_blob_hash, 'Source changed during preparation. Do not restore.')
        receipt = {
            'format': 1, 'udid': UDID, 'source': str(source), 'removed_file_id': file_id,
            'removed_domain': DOMAIN, 'removed_path': RELATIVE,
            'source_entries': count, 'remaining_entries': count - 1,
            'manifest_sha256': digest(dest / 'Manifest.db'),
            'manifest_plist_sha256': digest(dest / 'Manifest.plist')
        }
        (output / RECEIPT).write_text(json.dumps(receipt, indent=2), encoding='utf-8')
        (output / 'INCOMPLETE_DO_NOT_RESTORE.txt').unlink()
    print('\nPREPARED: encrypted APN-excluded FULL backup copy')
    print('Output:', output)
    print('Removed entries: 1; all other Files rows unchanged')
    print('Encryption round-trip: OK; source metadata and APN payload unchanged')
    print('No device restore was performed.')
 
 
def restore(output, service):
    output = output.resolve()
    require(not (output / 'INCOMPLETE_DO_NOT_RESTORE.txt').exists(), 'Preparation is incomplete.')
    receipt = json.loads((output / RECEIPT).read_text(encoding='utf-8'))
    require(receipt.get('format') == 1 and receipt.get('udid') == UDID and
            receipt.get('removed_domain') == DOMAIN and receipt.get('removed_path') == RELATIVE,
            'Unexpected preparation receipt.')
    dest = output / UDID
    require(digest(dest / 'Manifest.db') == receipt['manifest_sha256'] and
            digest(dest / 'Manifest.plist') == receipt['manifest_plist_sha256'],
            'The prepared backup has changed. Stopped.')
    require(plist(dest / 'Manifest.plist').get('IsEncrypted') is True, 'Backup must remain encrypted.')
    require(not payload_path(dest, receipt['removed_file_id']).exists(), 'Excluded APN payload is present.')
    print('This command RESTORES DATA AND SETTINGS to the connected new iPhone.')
    print('Use after recovery/erase and minimal setup. APN repair is not guaranteed.')
    print('Backup:', output)
    password = getpass.getpass('Encrypted backup password (hidden): ')
    require(bool(password), 'Empty password. Stopped.')
    with tempfile.TemporaryDirectory(prefix='apn-restore-check-') as tmp:
        db_path = Path(tmp) / 'verify.db'
        service._decrypt_backup_manifest_db(dest, password, db_path)
        with closing(readonly(db_path)) as db:
            check_db(db)
            require(db.execute('SELECT COUNT(*) FROM Files').fetchone()[0] == receipt['remaining_entries'],
                    'Unexpected manifest row count.')
            require(db.execute('SELECT COUNT(*) FROM Files WHERE domain=? AND relativePath=?',
                               (DOMAIN, RELATIVE)).fetchone()[0] == 0, 'APN entry is still present.')
    # Password is passed only in this Python process, not the OS command line.
    sys.argv = ['pymobiledevice3', 'backup2', 'restore', '--udid', UDID,
                '--system', '--no-remove', '--reboot', '--password', password, str(output)]
    try:
        runpy.run_module('pymobiledevice3', run_name='__main__')
    finally:
        sys.argv = [sys.argv[0]]
 
 
def main():
    user = Path(os.environ.get('USERPROFILE', str(Path.home())))
    parser = argparse.ArgumentParser(description=__doc__)
    sub = parser.add_subparsers(dest='command', required=True)
    prep = sub.add_parser('prepare', help='Copy and edit the backup on this PC; no device access')
    prep.add_argument('--source', type=Path, default=user / 'Apple/MobileSync/Backup' / UDID)
    prep.add_argument('--output', type=Path, default=user / 'APN-Repair-Encrypted')
    rest = sub.add_parser('restore', help='Restore the prepared copy to the specified iPhone')
    rest.add_argument('--output', type=Path, default=user / 'APN-Repair-Encrypted')
    args = parser.parse_args()
    require(re.fullmatch(r'(?:[0-9A-Fa-f]{8}-[0-9A-Fa-f]{16}|[0-9A-Fa-f]{40})', UDID) is not None,
            'Set APN_TARGET_UDID to the target iPhone UDID before running this script.')
    service = backend()
    if args.command == 'prepare':
        require((args.source / 'Manifest.plist').is_file(), 'Source backup not found.')
        require(plist(args.source / 'Manifest.plist').get('IsEncrypted') is True,
                'Source is not encrypted. Do not recreate or overwrite your preserved backup.')
        password = getpass.getpass('Encrypted backup password (hidden): ')
        require(bool(password), 'Empty password. Stopped.')
        prepare(args.source, args.output, password, service)
    else:
        restore(args.output, service)
 
 
if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print('\nInterrupted. Do not restore a copy without PREPARED status.', file=sys.stderr)
        sys.exit(130)
    except Exception as exc:
        print(f'ERROR: {type(exc).__name__}: {exc}', file=sys.stderr)
        print('Stopped. Check the error above. A restore error does not by itself mean the prepared copy is incomplete.', file=sys.stderr)
        sys.exit(1)

バックアップの作成が終わっていることを確認し、AppleデバイスとiTunesを閉じる。その後、PowerShellで次を実行する。

py "$env:USERPROFILE\Downloads\apn_encrypted_repair.py" prepare

暗号化バックアップのパスワードを入力する。入力した文字は画面に表示されない。

py "$env:USERPROFILE\Downloads\apn_encrypted_repair.py" prepare --source "D:\iPhoneBackup\自分の端末のUDID"

成功すると、次のように表示される。

PREPARED: encrypted APN-excluded FULL backup copy
Removed entries: 1; all other Files rows unchanged
Encryption round-trip: OK; source metadata and APN payload unchanged
No device restore was performed.

編集済みコピーの標準の保存先

C:\Users\ユーザー名\APN-Repair-Encrypted\端末のUDID

prepareはPC内で処理するため、iPhoneを接続していなくても実行できる。バックアップ全体をコピーするので、加えて作業用の空き容量が必要です。

PREPAREDが出る前にエラーで停止したコピーは、復元に使わないこと。出力先がすでに存在する場合も、上書きせずに停止する。

4. iPhoneを初期化する

元の暗号化バックアップが残っていることと、編集済みコピーにPREPAREDが表示されたことを確認してから、iPhoneを初期化する。

端末から消去する場合は、「設定 → 一般 → 転送またはiPhoneをリセット → すべてのコンテンツと設定を消去」へ進む。eSIMを残す場合は、eSIMを保持してデータを消去する選択肢を選ぶ。

Apple公式:コンピュータを使ってiPhoneを初期化する

リカバリモードを使う場合は、次の手順を参照する。

Apple公式:iPhoneをアップデートまたは復元できない場合

5. APNを入れずに、最低限の初期設定だけ進める

APNはまだインストールせず、「こんにちは」画面から次のように設定する。

設定項目 選ぶ内容
クイックスタート もう一方のデバイスなしで設定
ネットワーク Wi-Fiに接続
アプリとデータ アプリとデータを転送しない
Apple Account あとで設定
Face ID、Apple Payなど 後で設定してよい
APNプロファイル まだインストールしない

ホーム画面まで進み、USB接続時の「このコンピュータを信頼」を許可する。端末のパスコードは設定してよい。

Wi-Fi接続やPCとの信頼設定は、復元に必要な準備だ。APNプロファイルやアプリは追加せずに、次の復元へ進む。

Apple公式:iPhoneやiPadを初期設定する

6. 編集済みバックアップを復元する

iPhoneをUSB接続してロックを解除し、PCのAppleデバイスとiTunesを閉じる。

Apple Accountにすでにサインインしている場合は、「設定 → 自分の名前 → 探す → iPhoneを探す」をオフにする。有効なままだと、エラー211で復元が拒否される。

手順2でAPN_TARGET_UDIDを設定したPowerShellから、次を実行する。

py "$env:USERPROFILE\Downloads\apn_encrypted_repair.py" restore

暗号化バックアップのパスワードを入力すると、データと設定の復元が始まる。完了して再起動するまで、USB接続を維持する。

スクリプトでは--system --no-remove --rebootを指定して復元する。--no-removeを付けても、バックアップに含まれる管理情報は復元される。端末の設定をすべて保護する指定ではないため、復元前にAPNを入れないこと。

pymobiledevice3のbackup2コマンド

7. セットアップ完了後にAPNをインストールする

復元後のセットアップを終え、ホーム画面まで進んでから、契約している通信事業者のAPNをインストールする。

U-NEXT MOBILEの場合は、Safariで公式の案内ページを開き、プロファイルをダウンロードしてインストールする。複数回線を使っている場合は、公式手順に従い、U-NEXT MOBILEだけを有効にして設定する。

U-NEXT公式:iPhone/iPadのAPN設定方法

設定が終わったら、次を確認する。

  • 「設定 → 一般 → VPNとデバイス管理」にU-NEXTのプロファイルが表示される。
  • プロファイルの詳細を開ける。確認のために削除する必要はない。
  • Wi-Fiをオフにして、対象回線でWebサイトを表示できる。
  • 写真、メッセージ、ヘルスケアなど、移行したかったデータが戻っている。

iCloud写真など、もともとiCloudと同期していたデータは、Apple Accountへのサインイン後に同期状況も確認する。アプリによっては、個別のログインや引き継ぎ操作が必要です。

復旧後に確認したこと

最終的に、自分の環境では次の動作を確認した。

確認項目 結果
復元完了後のAPNインストール 正常にインストールできた
インストールしたプロファイルの削除 正常に削除できた
削除後の再インストール 正常にインストールできた
iPhone再起動後の表示 構成プロファイル欄に残っていた
モバイル通信 利用できた

通信に加え、プロファイルの表示・削除・再インストールまで確認できたため、今回の症状は復旧したと判断。

復元前にAPNを入れたときはバックアップから復元時に表示が消え、復元後に入れたときは再起動しても表示が残った。前者では、比較したプロファイル管理ファイル10件が復元元と完全に一致している。

これらの結果から、今回の再発には、バックアップから戻る管理情報と、復元前に入れたAPN設定の組み合わせが関係した可能性が高いと考えている。

ただし、最初の移行時にゴーストAPNが発生した原因までは特定できていない。

作業中に出たエラー

「Find My iPhone must be disabled」と表示される

エラー211は、「iPhoneを探す」が有効な状態でバックアップを復元しようとしたときに出た。

「iPhoneを探す」をオフにして、同じrestoreコマンドを実行する。prepareで作ったコピーを作り直す必要はない。

Appleデバイスでエラー9、1109が出る

これらのエラーは、AppleデバイスでiOSを復元している途中に出た。pymobiledevice3でのバックアップ編集とは、発生した工程が異なる。

エラー9が出た場合は、USB接続やケーブルを確認し、別のPCでの復元も試す。エラー1109が出た場合は、PCからAppleのアップデートサーバーへの通信を確認し、別の回線でも試せる。エラー番号だけで本体の故障とは判断できない。

復元後、Apple Accountのサインインが進まない

今回の作業では、サインイン画面が繰り返し表示されたり、「サインイン中」のまま進まなくなったりした。自分の環境では、再起動後にサインインできている。

同じ症状が出た場合は、サインインを後回しにしてセットアップを終え、再起動後に「設定」から試す方法もある。これは今回の対処例であり、すべてのサインイン不良が再起動で直るとは限らない。

Apple公式:Apple Accountにサインインする

まとめ

古いAPNファイルをバックアップから除外しても、端末に残った設定が復元で消えるとは限らない。また、初期化した端末でも、APNを先に入れると、バックアップから戻る管理情報と食い違う可能性がある。

自分の環境では、初期化後はAPNを設定せず、データを戻してから最後にAPNを入れる順番で復旧できた。未編集のバックアップを戻すだけでは、古いAPNも復元される可能性がある。

元の暗号化バックアップは、通信と必要なデータの復元を確認できるまで残しておく。