I had reason to investigate this properly today and ended up running a comprehensive set of tests here in my office. I figured that, once I’ve done all this work, I might as well share the results with everyone.
....
Accessible | Backed Up By | Restore On | Result |
Normal | iTunes | same device | restored |
| | different device | gone |
| iTunes, encrypted | same device | restored |
| | different device | restored |
| iCloud | same device | restored |
| | different device | gone |
ThisDeviceOnly | iTunes | same device | restored |
| | different device | gone |
| iTunes, encrypted | same device | restored |
| | different device | gone |
| iCloud | same device | restored |
| | different device | gone |
Some testing parameters:
- Xcode 10.1
- iOS 12 (12.0.1 on one device, 12.1.1 on the other)
- iCloud account has neither two-factor nor two-step authentication enabled
However, I don’t think any of these parameters make a difference. As far as I know things have always behaved this way.
Finally, my test code it pasted in below, just in case you want to try this yourself (in which case you’ll get very familiar with the iOS setup assistant :-).
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
class MainViewController : UITableViewController {
@IBOutlet var normalLabel: UILabel!
@IBOutlet var thisDeviceOnlyLabel: UILabel!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.load()
}
private func load(account: String, into label: UILabel) {
var copyResult: CFTypeRef? = nil
let err = SecItemCopyMatching([
kSecClass: kSecClassGenericPassword,
kSecAttrService: "KeychainBackupTest",
kSecAttrAccount: account,
kSecReturnData: true,
] as NSDictionary, ©Result)
switch err {
case errSecItemNotFound:
label.text = "-"
case errSecSuccess:
let resultData = copyResult! as! NSData as Data
let result = String(bytes: resultData, encoding: .utf8)!
label.text = result
default:
label.text = "\(err)"
}
}
private func load() {
self.load(account: "normal", into: self.normalLabel)
self.load(account: "thisDeviceOnly", into: self.thisDeviceOnlyLabel)
}
private func save(account: String, access: CFString, value: String) {
_ = SecItemAdd([
kSecClass: kSecClassGenericPassword,
kSecAttrService: "KeychainBackupTest",
kSecAttrAccount: account,
kSecAttrAccessible: access,
kSecValueData: value.data(using: .utf8)!
] as NSDictionary, nil)
}
private func save() {
let value = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
// We use a time as the value so it's easy to see that the restored value makes sense.
self.save(account: "normal", access: kSecAttrAccessibleWhenUnlocked, value: value)
self.save(account: "thisDeviceOnly", access: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, value: value)
self.load()
}
private func reset() {
_ = SecItemDelete([
kSecClass: kSecClassGenericPassword,
] as NSDictionary)
self.load()
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch (indexPath.section, indexPath.row) {
case (0, 0): break
case (0, 1): break
case (0, 2): self.save()
case (0, 3): self.reset()
default: fatalError()
}
self.tableView.deselectRow(at: indexPath, animated: true)
}
}