PrivacyKit

Build Status Coverage Documentation

The PrivacyKit is a framework for iOS that provides functionality to handle personal information appropriately.

A proof-of-concept Implementation of privacy services can be found at https://github.com/AppPETs/PrivacyService.

Functionality

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

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.

Passwords for Authentication

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.
}

Passwords for Later Use

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)

Cryptographic Keys

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

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 https://github.com/blochberger/Tafelsalz/issues/2, https://github.com/blochberger/Tafelsalz/issues/5).

Symmetric Encryption

Ephemeral Keys

let secretBox = SecretBox()
let plaintext = "Hello, World!".utf8Bytes
let ciphertext = secretBox.encrypt(plaintext: plaintext)
let decrypted = secretBox.decrypt(ciphertext: ciphertext)!
Persisted Keys

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)
Padding
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)!
Password Hashing
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.
}

Generic Hashing

Public Hashing
let data = "Hello, World!".utf8Bytes
let hash = GenericHash(bytes: data)
Private Hashing with Persisted Keys
// 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)

Key Derivation

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))

Key Exchange

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.

Anonymous Communication

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
}

Key-value Storage

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.

User Interactions

Device Owner Authentication

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()

Confidential QR Codes

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.

Usage

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.


  1. Apple Inc., iOS Security – iOS 11, 2018
  2. 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