Skip to content

Commit

Permalink
Update iOS for Swift package (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
lambdapioneer authored Nov 20, 2023
1 parent ea89a66 commit 4947cce
Show file tree
Hide file tree
Showing 40 changed files with 838 additions and 800 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/android.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
on:
push:
paths:
- "android/**"
- ".github/workflows/android.yaml"
branches: [ "main" ]
pull_request:
paths:
- "android/**"
- ".github/workflows/android.yaml"
branches: [ "main" ]

name: Android
Expand Down
43 changes: 43 additions & 0 deletions .github/workflows/ios.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
on:
push:
paths:
- "ios/**"
- ".github/workflows/ios.yaml"
branches: [ "main" ]

name: iOS

jobs:
build:
name: Synchronize libraries
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3

- name: Add secret key to runner
env:
SLOTH_IOS_KEY: ${{ secrets.SLOTH_IOS_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SLOTH_IOS_KEY" > ~/.ssh/id_ed25519
chmod 400 ~/.ssh/id_ed25519
wc ~/.ssh/id_ed25519
- name: Checkout remote
run: |
git config --global user.email "[email protected]"
git config --global user.name "GitHub Action Sync Bot"
git clone [email protected]:lambdapioneer/sloth-ios.git target
- name: Replace contents
run: |
cd target
cp -rv ../ios/RainbowSloth/* .
- name: Pushy to remote
run: |
cd target
git add .
git diff-index --quiet HEAD || git commit -m "Automatic publish from github.com/lambdapioneer/sloth"
git push origin main
5 changes: 0 additions & 5 deletions ios/.gitignore

This file was deleted.

2 changes: 0 additions & 2 deletions ios/Artifacts/.gitignore

This file was deleted.

32 changes: 11 additions & 21 deletions ios/README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,29 @@
# Sloth: iOS

We have implemented **RainbowSloth** for iOS as a demo project included in this folder
This code is developed as an academic prototype and not intended for production use yet.
We have implemented **RainbowSloth** for iOS in this folder
It consists of the `RainbowSloth` package and the `Sloth` demo app.

## Getting started

For install the Argon2 dependency using CocoaPods.
If you do not have CocoaPods installed, follow the instructions on their website: https://cocoapods.org/
## Using the RainbowSloth package

Then enter the iOS folder and execute the following:
The `RainbowSloth` package is synced to another repository to allow it to be included as a Swift package dependency.
Refer to the `README.md` over there: https://github.com/lambdapioneer/sloth-ios

```bash
pod install
```

Then open the `Sloth.xcworkspace` file in Xcode.
It is important to open the workspace file and not the project file.


### Adding Sloth to your iOS project
## Tests

> [!IMPORTANT]
> We have not _yet_ published library artifacts. For now, you need to build the project locally.
Most low-level tests live inside the `RainbowSloth` package.
However, those that require the Secure Enclave live in the `Sloth` app test target as they require entitlements.
When testing, make sure to execute both sets of tests.


## The benchmark app

The project comes with a benchmark app that is also used to generate the performance numbers that are reported in the paper.
You can install it on a real device or a simulator using Xcode.

![Screenshot of the app](docs/screenshot-framed.png)

For our evaluation we created .IPA files that we then run online using AWS DeviceFarm.
During these runs we manually interacted with the app through the web interface of DeviceFarm.
The app reports the results to a custom server that we run on one of our servers.
Expand All @@ -38,8 +33,3 @@ To replicate these steps first start the server on a server and update the IP ad
Build the artifacts by running `build_artifact.sh`.
Start the simulator (or use real devices) and install the IPA file.
Execute the scenarios under test using the UI and observe that results are collected on the server as log files.


## Tests

The project bundles tests for the individual components and you can execute them as usual through the IDE on either the simulators or a real device.
8 changes: 8 additions & 0 deletions ios/RainbowSloth/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
27 changes: 27 additions & 0 deletions ios/RainbowSloth/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// swift-tools-version: 5.9

import PackageDescription

let package = Package(
name: "RainbowSloth",
platforms: [.iOS(.v14)],
products: [
.library(
name: "RainbowSloth",
targets: ["RainbowSloth"]),
],
dependencies: [
.package(url: "https://github.com/jedisct1/swift-sodium.git", from: "0.9.1"),
],
targets: [
.target(
name: "RainbowSloth",
dependencies: [
.product(name: "Sodium", package: "swift-sodium")
]
),
.testTarget(
name: "RainbowSlothTests",
dependencies: ["RainbowSloth"]),
]
)
49 changes: 49 additions & 0 deletions ios/RainbowSloth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Sloth: iOS

We have implemented the SE-backed key stretching scheme RainbowSloth for iOS 15+.
This folder gets synced into a designated repository to allow inclusion as a Swift package dependency.

For changes refer to the main repository here: https://github.com/lambdapioneer/sloth


## Setting up

Add this repository as a dependency to your `Package.swift` file like so:

```swift
dependencies: [
.package(url: "https://github.com/lambdapioneer/sloth-ios.git", from: "0.0.1"),
],

// ...

dependencies: [
.product(name: "RainbowSloth", package: "sloth-ios")
]
```


## Using RainbowSloth

After adding the dependency you can import the library in the respective `.swift` files and use it:

```swift
import RainbowSloth

// create a new Sloth instance
let sloth = RainbowSloth(withN: 100) // see paper on how to choose `n`

// create a new key
let (storageState, key) = try sloth.keygen(
pw: "user-passphrase",
handle: "your-identifier",
outputLength: 32
)

// re-derive the same key later
let key = try sloth.derive(
storageState: storageState,
pw: "user-passphrase",
outputLength: 32
)
```
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import CryptoKit
import Foundation

/// A simple wrapper around `HDKF` with `SHA256` using `Data` types.
public struct HkdfSha256 {

public static func derive(salt: Data, ikm: Data, info: Data, l: Int) -> Data {
/// Derives a key from the initial key material `ikm` using the given `salt` and `info`. The output will be `outputLength` bytes long.
public static func derive(salt: Data, ikm: Data, info: Data, outputLength: Int) -> Data {
let output = HKDF<SHA256>.deriveKey(
inputKeyMaterial: SymmetricKey(data: ikm),
salt: salt,
info: info,
outputByteCount: l
outputByteCount: outputLength
)
return output.withUnsafeBytes { body in
Data(body)
Expand Down
29 changes: 29 additions & 0 deletions ios/RainbowSloth/Sources/RainbowSloth/PwHash.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Sodium
import Foundation

/// Wrapper around the Argon2id password hashing algorithm as provided by `Sodium`.
public struct PwHash {

/// Derives a key from the given `salt` and password `pw`. The output will be `outputLength` bytes long.
public static func derive(salt: Data, pw: Data, outputLength: Int) -> Data {
// OWASP: "Use Argon2id with a minimum configuration of 19 MiB of memory, an iteration count of 2, and 1 degree of parallelism."
let sodium = Sodium.init()
let sodiumPwHash = sodium.pwHash
let res = sodiumPwHash.hash(
outputLength: outputLength,
passwd: Array(pw),
salt: Array(salt),
opsLimit: 2,
memLimit: 19*1024*1024 // 19 MiB
)
return Data(res!)
}

/// Creates a new random `salt` byte array that can be used with the `derive` function.
public static func randomSalt(outputLength: Int = 16) -> Data {
var bytes = [UInt8](repeating: 0, count: outputLength)
let result = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
assert(result == errSecSuccess, "Failed to generate random bytes")
return Data(bytes)
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import Foundation


let DEFAULT_HANDLE = Data()
let P256_PUBLIC_KEY_SIZE = 32
private let P256_PUBLIC_KEY_SIZE = 32

/// Implementation of the RainbowSloth algorithm for tuneable password-based key stretching on iOS using the SecureEnclave (SE).
/// The parameter `n` determines the number of required SE operations and can be increased for higher security levels.
/// Refer to the paper and documentation for more details.
public struct RainbowSloth {
var n: Int

init(withN n: Int) {
public init(withN n: Int) {
self.n = n;
}

func keygen(pw: String, handle: String, outputLength: Int) throws -> (RainbowSlothStorageState, Data) {
/// Generates a new key under the handle `handle` using the password `pw`. The output will be `outputLength` bytes long.
/// Every call to this function will result in a new key and any existing key under the same handle will be overwritten.
/// Calls to `derive` using the returned storage state will result in the same key.
public func keygen(pw: String, handle: String, outputLength: Int) throws -> (RainbowSlothStorageState, Data) {
let storageState = RainbowSlothStorageState(
handle: handle,
salt: randomSalt()
salt: PwHash.randomSalt()
)

try SecureEnclave.resetSecretSeKey(handle: storageState.handle)
Expand All @@ -24,13 +29,16 @@ public struct RainbowSloth {
return (storageState, k)
}

func derive(storageState: RainbowSlothStorageState, pw: String, outputLength: Int) throws -> Data {
/// Re-derives a key from the given storage state. The result will be `outputLength` bytes long.
public func derive(storageState: RainbowSlothStorageState, pw: String, outputLength: Int) throws -> Data {
let pres = try preambleDerive(storageState: storageState, pw: pw)
let k = try innerDerive(storageState: storageState, pres: pres, outputLength: outputLength)
return k
}

func eval(storageState: RainbowSlothStorageState, pw: String, outputLength: Int, iterations: Int) throws -> [Double] {
/// An evaluation method of the internal derive method to measure the effective time guarantees. The returned array contains
/// `iterations` many measurements of this operation in seconds.
public func eval(storageState: RainbowSlothStorageState, pw: String, outputLength: Int, iterations: Int) throws -> [Double] {
let pres = try preambleDerive(storageState: storageState, pw: pw)

var durations = [Double]()
Expand All @@ -47,7 +55,8 @@ public struct RainbowSloth {

private func preambleDerive(storageState: RainbowSlothStorageState, pw: String) throws -> [Data] {
let l = n * P256_PUBLIC_KEY_SIZE
let pres_combined = PwHash.derive(salt: storageState.salt, pw: pw, l: l)
let pwBytes = pw.data(using: .utf8)!
let pres_combined = PwHash.derive(salt: storageState.salt, pw: pwBytes, outputLength: l)
return pres_combined.chunkify(length: P256_PUBLIC_KEY_SIZE)
}

Expand All @@ -66,13 +75,14 @@ public struct RainbowSloth {
salt: storageState.salt,
ikm: posts_combined,
info: Data(),
l: outputLength
outputLength: outputLength
)

return k
}
}

/// The required data to be persisted for later re-deriving the same key using `RainbowSloth`.
public struct RainbowSlothStorageState {
var handle: String
var salt: Data
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import CryptoKit
import Foundation

/// Helper functions for creating and manipulating P256 keys.
public struct ReHashToEc {

/// Deterministically maps an input `seed` value to a P256 `SecKey`.
public static func rehashToP256(seed: Data) -> SecKey {
let salt = Data()
let info = Data()
var counter = 0
while (true) {
let seedString = "\(seed.toHex)\(counter)"
let currentSeed = seedString.data(using: .ascii)!
var arr = HkdfSha256.derive(salt: salt, ikm: currentSeed, info: info, l: 33)
var arr = HkdfSha256.derive(salt: salt, ikm: currentSeed, info: info, outputLength: 33)
arr[0] = 0x02 | (arr[0] & 0x01)

// try to convert and for any failure restart the loop with an incremented counter
Expand All @@ -27,7 +29,8 @@ public struct ReHashToEc {
}
}

public static func convertCompressedToX963(data: Data) throws -> Data {
/// Convert a P256 key from its compressed representation to the X963 represeantation that is expected for importing it the iOS APIs.
internal static func convertCompressedToX963(data: Data) throws -> Data {
// Unfortunately, iOS only added support for the SEC1-style compressed representation
// in iOS 16. That works as expected on an array with 33 bytes with the first one being 0x02/0x03
// to indicate which of the possible Y values to choose.
Expand All @@ -47,14 +50,14 @@ public struct ReHashToEc {
}
}

static func convertCompressedToX963_below16(data: Data) throws -> Data {
internal static func convertCompressedToX963_below16(data: Data) throws -> Data {
// drop the first byte from the compressed SEC-1 representation for Apple's special compact representation
let trimmedData = data.subdata(in: 1..<data.count)
return try P256.Signing.PublicKey.init(compactRepresentation: trimmedData).x963Representation
}

@available(iOS 16.0, *)
static func convertCompressedToX963_16andAbove(data: Data) throws -> Data {
internal static func convertCompressedToX963_16andAbove(data: Data) throws -> Data {
return try P256.Signing.PublicKey.init(compressedRepresentation: data).x963Representation
}
}
Loading

0 comments on commit 4947cce

Please sign in to comment.