The PrivacyKit
is a framework for iOS that provides functionality to handle personal information appropriately.
- Repository: https://github.com/AppPETs/PrivacyKit
- Documentation: https://apppets.github.io/PrivacyKit
- Issues: https://github.com/AppPETs/PrivacyKit/issues
A proof-of-concept Implementation of privacy services can be found at https://github.com/AppPETs/PrivacyService.
Several technologies can be used to enhance privacy, also known as privacy-enhancing technologies (PETs). Many PETs are known in research, but are not easily available to developers. The goal of this project is to make PETs accessible to app developers. The following functionality has been implemented in a way that it can be easily used by application developers.
Storing credentials is not as easy as it sounds. Many applications do this wrong and store a password for authenticating a user in plaintext or cryptographic key alongside the encrypted data. It is better to use the credential storage offered by iOS, the Keychain services. Credentials stored there are encrypted by the Secure Enclave¹. Unfortunately the Keychain services are only accessible by a low-level API, with insufficient documentation, convenience APIs for different tasks have been added.
If the goal ist to simply authenticate the user, by validating if he knows a previously set password, then the Password
class of the Tafelsalz project should be used. The password must not be stored. A hash generated by a password hashing function should be stored instead. The stored hash keeps the actual password secret and protects against leaks. For authenticating the user via Touch-ID, Face-ID or similar, see the Authenticate Device Owner section.
let password = Password("Correct Horse Battery Staple")!
let hashedPassword = password.hash()!
// Store `hashedPassword.string` to database.
// If a user wants to authenticate, just read it from the database and
// verify it against the password given by the user.
if hashedPassword.isVerified(by: password) {
// The user is authenticated successfully.
}
If the goal is to store a password, which later is used, e.g., to authenticate a user to a third-party web service, the password can be stored inside the system's Keychain, by using the Keychain
project.
import Keychain
let account = "user" // A user account, for which the password is used
let service = "service" // A service, e.g., your app name
let label = "\(account)@\(service)" // Descriptive name
let item = GenericPasswordItem(for: service, using: account, with: label)
// Store password
try Keychain.store(password: "foo", in: item)
// Retrieve password
let password = try Keychain.retrievePassword(for: item)
// Update password
try Keychain.update(password: "bar", for: item)
// Delete item
try Keychain.delete(item: item)
If your goal is to store cryptographic keys, you can either use the Keychain
project directly, or use the Persona
class of the Tafelsalz project.
// Create a persona
let alice = Persona(uniqueName: "Alice")
// Once a secret of that persona is used, it will be persisted in the
// system's Keychain.
let secretBox = SecretBox(persona: alice)!
// Use your SecretBox as usual
let plaintext = "Hello, World!".utf8Bytes
let ciphertext = secretBox.encrypt(plaintext: plaintext)
let decrypted = secretBox.decrypt(ciphertext: ciphertext)!
// Forget the persona and remove all related Keychain entries
try! Persona.forget(alice)
Encryption and decryption can be done by using the Tafelsalz project.
Note that asymmetric encryption as well as stream encryption are not supported, yet (see blochberger/Tafelsalz#2, blochberger/Tafelsalz#5).
let secretBox = SecretBox()
let plaintext = "Hello, World!".utf8Bytes
let ciphertext = secretBox.encrypt(plaintext: plaintext)
let decrypted = secretBox.decrypt(ciphertext: ciphertext)!
The cryptographic keys in this example are stored within the system's Keychain. See Cryptographic Keys for details.
// Create a persona
let alice = Persona(uniqueName: "Alice")
// Once a secret of that persona is used, it will be persisted in the
// system's Keychain.
let secretBox = SecretBox(persona: alice)!
// Use your SecretBox as usual
let plaintext = "Hello, World!".utf8Bytes
let ciphertext = secretBox.encrypt(plaintext: plaintext)
let decrypted = secretBox.decrypt(ciphertext: ciphertext)!
// Forget the persona and remove all related Keychain entries
try! Persona.forget(alice)
let secretBox = SecretBox()
let plaintext = "Hello, World!".utf8Bytes
let padding: Padding = .padded(blockSize: 16)
let ciphertext = secretBox.encrypt(plaintext: plaintext, padding: padding)
let decrypted = secretBox.decrypt(ciphertext: ciphertext, padding: padding)!
let password = Password("Correct Horse Battery Staple")!
let hashedPassword = password.hash()!
// Store `hashedPassword.string` to database.
// If a user wants to authenticate, just read it from the database and
// verify it against the password given by the user.
if hashedPassword.isVerified(by: password) {
// The user is authenticated successfully.
}
let data = "Hello, World!".utf8Bytes
let hash = GenericHash(bytes: data)
// Create a persona
let alice = Persona(uniqueName: "Alice")
// Generate a personalized hash for that persona
let data = "Hello, World!".utf8Bytes
let hash = GenericHash(bytes: data, for: alice)
// Forget the persona and remove all related Keychain entries
try! Persona.forget(alice)
let context = MasterKey.Context("Examples")!
let masterKey = MasterKey()
let subKey1 = masterKey.derive(sizeInBytes: MasterKey.DerivedKey.MinimumSizeInBytes, with: 0, and: context)!
let subKey2 = masterKey.derive(sizeInBytes: MasterKey.DerivedKey.MinimumSizeInBytes, with: 1, and: context)!
// You can also derive a key in order to use it with secret boxes
let secretBox = SecretBox(secretKey: masterKey.derive(with: 0, and: context))
let alice = KeyExchange(side: .client)
let bob = KeyExchange(side: .server)
let alicesSessionKey = alice.sessionKey(for: bob.publicKey)
let bobsSessionKey = bob.sessionKey(for: alice.publicKey)
// alicesSessionKey == bobsSessionKey
There is a demo application available for iOS, which shows how to exchange secrets between two devices, using the key exchange mechanism with QR codes, see SecretSharing-iOS.
In order to protect the identity of users from network attackers or curious server operators an anonymization mechanism is offered as well. It is based on Shalon². Note that there are other services, which provide a higher degree of anonymity, such as Tor.
In order to use a proxy server as a Shalon proxy, the proxy server itself needs to support TLS, see Shalon Server Configuration. A demo service is provided at shalon1.jondonym.net
.
The implementation works seamlessly with the default URL loading system of iOS and macOS by using a custom URL protocol (ShalonURLProtocol
). One first needs to register the URL protocol. After that, URLs of the format httpss://proxy:port/target:port/index.html
can be used to connect through proxy
on port
to https://target:port/index.html
. To use more than one proxy (up to three), e.g., use httpssss://proxy1/proxy2/proxy3/target/index.html
for connecting via three proxies.
// Register the URL protocol
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses?.append(ShalonURLProtocol.self)
// Use Shalon URLs like you would use any other
let session = URLSession(configuration: configuration)
let url = URL(string: "httpss://shalon1.jondonym.net/example.com/")!
let task = session.dataTask(with: url) {
optionalUrl, optionalResponse, optionalError in
// Handle response
}
One can also use the Shalon
class directly, which gives slightly more control over the HTTP data sent to the target server, e.g., the User-Agent
header can not be dropped when using URL sessions.
let proxy1 = Target(withHostname: "shalon1.jondonym.net", andPort: 443)!
let target = Target(withHostname: "www.example.com", andPort: 443)!
let shalon = Shalon(withTarget: target)
shalon.addLayer(proxy1)
shalon.issue(request: Request(withMethod: .head, andUrl: url)!) {
optionalResponse, optionalError in
// Handle response
}
A simple key-value storage is offered, where keys and values are protected in a way, that different users, can store key-value pairs in a shared memory without any access control. A demo service is implemented by the PrivacyService project.
let url = URL(string: "httpss://shalon1.jondonym.net:443/services.app-pets.org")!
let alice = Persona(uniqueName: "alice")
let context = SecureKeyValueStorage.Context("TODOLIST")!
let privacyService = PrivacyService(baseUrl: url)
let storage = SecureKeyValueStorage(with: privacyService, for: persona, context: context)!
// Store something
storage.store(key: "My PIN", value: Data("1234".utf8)) {
optionalError in
if let error = optionalError {
// TODO Handle error
}
}
// Retrieve something
storage.retrieve(for: "My PIN") {
optionalValue, optionalError in
precondition((optionalValue != nil) == (optionalError != nil))
guard let value = optionalValue else {
let error = optionalError!
// TODO Handle error
return
}
// Success, do something with `value`
}
// Remove something
storage.remove(key: "My PIN") {
optionalError in
if let error = optionalError {
// TODO Handle error
}
}
A demo application is available for iOS, which utilizes the key-value storage, see Todo-iOS.
There is a convenience API to authenticate the device's owner. It will use Face-ID or Touch-ID if available and activated and will fall back to authenticate with the owner's passcode. In order to use Face-ID, add NSFaceIDUsageDescription
to your Info.plist
.
var context = authenticateDeviceOwner(reason: "Unlock something") {
authenticationError in
guard authenticationError == nil else {
// Failed to authenticate (the user just might have cancelled)
// TODO: Handle error
return
}
// Successfully authenticated
unlockSomething()
}
// Invalidate context
context.invalidate()
If the goal is to show a QR code on screen, but only if the device owner has authenticated himself, the ConfidentialQrCodeView
class can be used. This might be useful for protecting information from being displayed unaware of the device's owner, such as Wi-Fi credentials.
In order to use it, set it as a class for an image view in Interface Builder. The default image, e.g., as set in Interface Builder, of the image view, will act as cover image, which can be displayed, if the owner is not authenticated. The user can tap on the cover image, will then be asked to authenticate himself via Face-ID or Touch-ID if available and activated, and will fall back to the owner's passcode. If authentication succeeds the QR code, previously set, will be displayed until the user taps again.
Assuming you have a Git repository for your project, than you can use the
PrivacyKit
framework by adding it as a submodule:
git submodule add https://github.com/AppPETs/PrivacyKit/issues
git submodule update --init --recursive # This will also fetch dependencies
Then open your applications Xcode project and drag and drop the
PrivacyKit.xcodeproj
into it. In the project and under Embedded Frameworks add
the PrivacyKit.framework
.
- Apple Inc., iOS Security – iOS 11, 2018
- A. Panchenko, B. Westermann, L. Pimenidis, and C. Andersson, SHALON: Lightweight Anonymization Based on Open Standards in Proceedings of the 18th International Conference on Computer Communications and Networks, IEEE ICCCN 2009, San Francisco, California, August 3-6, 2009, pp. 1–7