Skip to content

Commit 76767bd

Browse files
Migrate iOS library & showcase app
1 parent 1deb1f8 commit 76767bd

File tree

5 files changed

+95
-75
lines changed

5 files changed

+95
-75
lines changed

ios/MobileSdk/Sources/MobileSdk/KeyManager.swift

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ public class KeyManager: NSObject, SpruceIDMobileSdkRs.KeyStore, ObservableObjec
5353

5454
public func getSigningKey(alias: SpruceIDMobileSdkRs.KeyAlias) throws -> any SpruceIDMobileSdkRs
5555
.SigningKey {
56-
guard let jwkString = Self.getJwk(id: alias) else {
56+
guard let jwk = Self.getJwk(id: alias) else {
5757
throw KeyManError.missing
5858
}
59-
return P256SigningKey(alias: alias, jwkString: jwkString)
59+
return P256SigningKey(alias: alias, jwkString: jwk.description)
6060
}
6161

6262
/**
@@ -156,9 +156,9 @@ public class KeyManager: NSObject, SpruceIDMobileSdkRs.KeyStore, ObservableObjec
156156
}
157157

158158
/**
159-
* Returns a JWK for a particular secret key by key id.
159+
* Returns a JWK for a particular sec
160160
*/
161-
public static func getJwk(id: String, accessGroup: String? = nil) -> String? {
161+
public static func getJwk(id: String, accessGroup: String? = nil) -> Jwk? {
162162
guard let key = getSecretKey(id: id, accessGroup: accessGroup) else { return nil }
163163

164164
guard let publicKey = SecKeyCopyPublicKey(key) else {
@@ -174,22 +174,20 @@ public class KeyManager: NSObject, SpruceIDMobileSdkRs.KeyStore, ObservableObjec
174174
let xDataRaw: Data = fullData.subdata(in: 0..<32)
175175
let yDataRaw: Data = fullData.subdata(in: 32..<64)
176176

177-
let xCoordinate = xDataRaw.base64EncodedUrlSafe
178-
let yCoordinate = yDataRaw.base64EncodedUrlSafe
179-
180-
let jsonObject: [String: Any] = [
181-
"kty": "EC",
182-
"crv": "P-256",
183-
"alg": "ES256",
184-
"x": xCoordinate,
185-
"y": yCoordinate
186-
]
187-
188-
guard let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject, options: [])
189-
else { return nil }
190-
let jsonString = String(data: jsonData, encoding: String.Encoding.ascii)!
177+
return jwkFromPublicP256(x: xDataRaw, y: yDataRaw)
178+
}
179+
180+
/**
181+
* Returns the public key of the given key pair as a JWK.
182+
*
183+
* Creates the key if it doesn't exist.
184+
*/
185+
public static func getOrInsertJwk(id: String) -> Jwk {
186+
if !KeyManager.keyExists(id: id) {
187+
_ = KeyManager.generateSigningKey(id: id)
188+
}
191189

192-
return jsonString
190+
return KeyManager.getJwk(id: id)!
193191
}
194192

195193
/**

ios/Showcase/Targets/App/Sources/App.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ struct AppApp: App {
99
}
1010
}
1111
}
12+

ios/Showcase/Targets/AppUIKit/Sources/dataStores/HacApplicationObservable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class HacApplicationObservable: ObservableObject {
5555
if !KeyManager.keyExists(id: DEFAULT_SIGNING_KEY_ID) {
5656
_ = KeyManager.generateSigningKey(id: DEFAULT_SIGNING_KEY_ID)
5757
}
58-
return KeyManager.getJwk(id: DEFAULT_SIGNING_KEY_ID)
58+
return KeyManager.getJwk(id: DEFAULT_SIGNING_KEY_ID)?.description
5959
}
6060

6161
@MainActor func getNonce() async -> String? {

ios/Showcase/Targets/AppUIKit/Sources/wallet/HandleOID4VCIView.swift

Lines changed: 75 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -26,64 +26,64 @@ struct HandleOID4VCIView: View {
2626

2727
func getCredential(credentialOffer: String) {
2828
loading = true
29-
let client = Oid4vciAsyncHttpClient()
30-
let oid4vciSession = Oid4vci.newWithAsyncClient(client: client)
29+
30+
// Setup HTTP client.
31+
let httpClient = Oid4vciAsyncHttpClient()
32+
33+
// Setup signer.
34+
let jwk = KeyManager.getOrInsertJwk(id: DEFAULT_SIGNING_KEY_ID)
35+
let didUrl = generateDidJwkUrl(jwk: jwk)
36+
jwk.setKid(kid: didUrl.description)
37+
let signer = KeyManagerJwkSigner(id: DEFAULT_SIGNING_KEY_ID, jwk: jwk)
38+
39+
let clientId = didUrl.did().description
40+
let oid4vciClient = Oid4vciClient(clientId: clientId)
41+
3142
Task {
3243
do {
33-
try await oid4vciSession.initiateWithOffer(
34-
credentialOffer: credentialOffer,
35-
clientId: "skit-demo-wallet",
36-
redirectUrl: "https://spruceid.com"
37-
)
38-
39-
let nonce = try await oid4vciSession.exchangeToken()
40-
41-
let metadata = try oid4vciSession.getMetadata()
42-
43-
if !KeyManager.keyExists(id: DEFAULT_SIGNING_KEY_ID) {
44-
_ = KeyManager.generateSigningKey(
45-
id: DEFAULT_SIGNING_KEY_ID
46-
)
44+
let offerUrl = if url.starts(with: "openid-credential-offer://") {
45+
url
46+
} else {
47+
"openid-credential-offer://\(url)"
4748
}
48-
49-
let jwk = KeyManager.getJwk(id: DEFAULT_SIGNING_KEY_ID)
50-
51-
let signingInput =
52-
try await SpruceIDMobileSdkRs.generatePopPrepare(
53-
audience: metadata.issuer(),
54-
nonce: nonce,
55-
didMethod: .jwk,
56-
publicJwk: jwk!,
57-
durationInSecs: nil
58-
)
59-
60-
let signature = KeyManager.signPayload(
61-
id: DEFAULT_SIGNING_KEY_ID,
62-
payload: [UInt8](signingInput)
63-
)
64-
65-
let pop = try SpruceIDMobileSdkRs.generatePopComplete(
66-
signingInput: signingInput,
67-
signatureDer: Data(signature!)
68-
)
69-
70-
try oid4vciSession.setContextMap(
71-
values: getVCPlaygroundOID4VCIContext()
72-
)
73-
74-
self.credentialPack = CredentialPack()
75-
let credentials = try await oid4vciSession.exchangeCredential(
76-
proofsOfPossession: [pop],
77-
options: Oid4vciExchangeOptions(verifyAfterExchange: false)
78-
)
79-
80-
credentials.forEach {
81-
let cred = String(decoding: Data($0.payload), as: UTF8.self)
82-
self.credential = cred
49+
50+
let credentialOffer = try await oid4vciClient.resolveOfferUrl(httpClient: httpClient, credentialOfferUrl: offerUrl)
51+
let credentialIssuer = credentialOffer.credentialIssuer()
52+
53+
let state = try await oid4vciClient.acceptOffer(httpClient: httpClient, credentialOffer: credentialOffer)
54+
55+
switch state {
56+
case .requiresAuthorizationCode(_):
57+
err = "Authorization Code Grant not supported"
58+
case .requiresTxCode(_):
59+
err = "Transaction Code not supported"
60+
case .ready(let credentialToken):
61+
let credentialId = try credentialToken.defaultCredentialId()
62+
63+
// Generate Proof of Possession.
64+
let nonce = try await credentialToken.getNonce(httpClient: httpClient)
65+
let jwt = try await createJwtProof(issuer: clientId, audience: credentialIssuer, expireInSecs: nil, nonce: nonce, signer: signer)
66+
let proofs = Proofs.jwt([jwt])
67+
68+
// Exchange token against credential.
69+
let response = try await oid4vciClient.exchangeCredential(httpClient: httpClient, token: credentialToken, credential: credentialId, proofs: proofs)
70+
71+
switch response {
72+
case .deferred(_):
73+
err = "Deferred credentials not supported"
74+
case .immediate(let response):
75+
guard let rawCredential = response.credentials.first else {
76+
throw NSError(domain: "OID4VCI", code: 0, userInfo: [
77+
"CredentialOfferUrl": offerUrl,
78+
"CredentialIssuer": credentialIssuer
79+
])
80+
}
81+
82+
credential = String(decoding: Data(rawCredential.payload), as: UTF8.self)
83+
84+
onSuccess?()
85+
}
8386
}
84-
85-
onSuccess?()
86-
8787
} catch {
8888
err = error.localizedDescription
8989
print(error)
@@ -119,6 +119,27 @@ struct HandleOID4VCIView: View {
119119
}
120120
}
121121

122+
class KeyManagerJwkSigner: JwsSigner, @unchecked Sendable {
123+
let id: String
124+
let jwk: Jwk
125+
126+
init(id: String, jwk: Jwk) {
127+
self.id = id
128+
self.jwk = jwk
129+
}
130+
131+
func fetchInfo() async throws -> JwsSignerInfo {
132+
return try await jwk.fetchInfo()
133+
}
134+
135+
func signBytes(signingBytes: Data) async throws -> Data {
136+
return try decodeDerSignature(signatureDer: Data(KeyManager.signPayload(
137+
id: DEFAULT_SIGNING_KEY_ID,
138+
payload: [UInt8](signingBytes)
139+
)!))
140+
}
141+
}
142+
122143
func getVCPlaygroundOID4VCIContext() throws -> [String: String] {
123144
var context: [String: String] = [:]
124145

ios/Showcase/Targets/AppUIKit/Sources/wallet/HandleOID4VPView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class Signer: PresentationSigner {
2828
throw Oid4vpSignerError.illegalArgumentException(
2929
reason: "Invalid kid")
3030
} else {
31-
self._jwk = jwk!
31+
self._jwk = jwk!.description
3232
}
3333
}
3434

0 commit comments

Comments
 (0)