Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions example/src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const authenticatorSelection = {
export default function App() {
const insets = useSafeAreaInsets();

const [result, setResult] = React.useState();
const [result, setResult] = React.useState<any>();
const [creationResponse, setCreationResponse] = React.useState<
NonNullable<Awaited<ReturnType<typeof passkey.create>>>["response"] | null
>(null);
Expand All @@ -88,7 +88,10 @@ export default function App() {
user,
authenticatorSelection,
...(Platform.OS !== "android" && {
extensions: { largeBlob: { support: "required" } },
extensions: {
largeBlob: { support: "required" },
prf: {}
},
}),
});

Expand Down Expand Up @@ -161,6 +164,26 @@ export default function App() {
setResult(json);
};

const deriveKey = async () => {
const json = await passkey.get({
rpId: rp.id,
challenge,
extensions: { prf: { eval: { first: bufferToBase64URLString(utf8StringToBuffer('my derived key'))} } },
...(credentialId && {
allowCredentials: [{ id: credentialId, type: "public-key" }],
}),
});

console.log("derive key json -", json);


setResult({
clientExtensionResults: {
prf: json?.clientExtensionResults.prf
},
});
};

return (
<View style={{ flex: 1 }}>
<ScrollView
Expand Down Expand Up @@ -188,6 +211,9 @@ export default function App() {
<Pressable style={styles.button} onPress={readBlob}>
<Text>Read Blob</Text>
</Pressable>
<Pressable style={styles.button} onPress={deriveKey}>
<Text>Derive Key (PRF)</Text>
</Pressable>
{creationResponse && (
<Pressable
style={styles.button}
Expand Down
32 changes: 30 additions & 2 deletions ios/PasskeyDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,22 @@ class PasskeyDelegate: NSObject, ASAuthorizationControllerDelegate,
)
}

var prf: AuthenticationExtensionsPRFOutputsJSON?
if #available(iOS 18.0, *) {
prf = credential.prf.flatMap { it in AuthenticationExtensionsPRFOutputsJSON(
enabled: Field.init(wrappedValue: it.isSupported),
results: Field.init(wrappedValue: it.first.map { first in AuthenticationExtensionsPRFValuesJSON(
first: Field.init(wrappedValue: first.serialize()),
second: Field.init(wrappedValue: it.second.serialize()))
}
)
)
}
}

let clientExtensionResults = AuthenticationExtensionsClientOutputsJSON(
largeBlob: Field.init(wrappedValue: largeBlob)
largeBlob: Field.init(wrappedValue: largeBlob),
prf: Field.init(wrappedValue: prf)
)

let response = AuthenticatorAttestationResponseJSON(
Expand Down Expand Up @@ -116,8 +130,22 @@ class PasskeyDelegate: NSObject, ASAuthorizationControllerDelegate,
}
}

var prf: AuthenticationExtensionsPRFOutputsJSON?
if #available(iOS 18.0, *) {
prf = credential.prf.map { AuthenticationExtensionsPRFOutputsJSON(
results: Field.init(wrappedValue: AuthenticationExtensionsPRFValuesJSON(
first: Field.init(wrappedValue: $0.first.serialize()),
second: Field.init(wrappedValue: $0.second.serialize())
)
)
)
}
}

let clientExtensionResults = AuthenticationExtensionsClientOutputsJSON(
largeBlob: Field.init(wrappedValue: largeBlob))
largeBlob: Field.init(wrappedValue: largeBlob),
prf: Field.init(wrappedValue: prf)
)

let response = AuthenticatorAssertionResponseJSON(
authenticatorData: Field.init(
Expand Down
23 changes: 22 additions & 1 deletion ios/PasskeyResponses.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ internal struct AuthenticationExtensionsClientOutputsJSON: Record {
// @available(iOS 17.0, *)
@Field
var largeBlob: AuthenticationExtensionsLargeBlobOutputsJSON?


@Field
var prf: AuthenticationExtensionsPRFOutputsJSON?
}

/**
Expand All @@ -130,3 +132,22 @@ internal struct AuthenticationExtensionsLargeBlobOutputsJSON: Record {
@Field
var written: Bool?;
};

internal struct AuthenticationExtensionsPRFValuesJSON: Record {
@Field
var first: Base64URLString

@Field
var second: Base64URLString?
}

/**
Specification reference: https://w3c.github.io/webauthn/#dictdef-authenticationextensionsprfoutputs
*/
internal struct AuthenticationExtensionsPRFOutputsJSON: Record {
@Field
var enabled: Bool?

@Field
var results: AuthenticationExtensionsPRFValuesJSON?
}
19 changes: 19 additions & 0 deletions ios/ReactNativePasskeysModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,17 @@ private func preparePlatformRegistrationRequest(challenge: Data,
}
}

if #available(iOS 18, *) {
if let prf = request.extensions?.prf {
platformKeyRegistrationRequest.prf = prf.eval.map { eval in
let first = Data(base64URLEncoded: eval.first)!
let second = eval.second.flatMap { Data(base64URLEncoded: $0) }
return .inputValues(ASAuthorizationPublicKeyCredentialPRFAssertionInput.InputValues(saltInput1: first, saltInput2: second))
} ?? .checkForSupport
}
}


if let userVerificationPref = request.authenticatorSelection?.userVerification {
platformKeyRegistrationRequest.userVerificationPreference = userVerificationPref.appleise()
}
Expand Down Expand Up @@ -272,6 +283,14 @@ private func preparePlatformAssertionRequest(challenge: Data, request: PublicKey
)
}
}

if #available(iOS 18, *) {
platformKeyAssertionRequest.prf = request.extensions?.prf?.eval.map { eval in
let first = Data(base64URLEncoded: eval.first)!
let second = eval.second.flatMap { Data(base64URLEncoded: $0) }
return .inputValues(ASAuthorizationPublicKeyCredentialPRFAssertionInput.InputValues(saltInput1: first, saltInput2: second))
}
}

// TODO: integrate this
// platformKeyAssertionRequest.shouldShowHybridTransport
Expand Down
29 changes: 29 additions & 0 deletions ios/Shared.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ExpoModulesCore
import AuthenticationServices
import CryptoKit

// - Enums

Expand Down Expand Up @@ -248,6 +249,17 @@ internal struct AuthenticationExtensionsLargeBlobInputs: Record {
var write: Base64URLString?
}

internal struct AuthenticationExtensionsPrfEvalInputs: Record {
@Field
var first: Base64URLString
@Field
var second: Base64URLString?
}

internal struct AuthenticationExtensionsPrfInputs: Record {
@Field
var eval: AuthenticationExtensionsPrfEvalInputs?
}

/**
Specification reference: https://w3c.github.io/webauthn/#dictdef-authenticationextensionsclientinputs
Expand All @@ -256,6 +268,9 @@ internal struct AuthenticationExtensionsClientInputs: Record {

@Field
var largeBlob: AuthenticationExtensionsLargeBlobInputs?

@Field
var prf: AuthenticationExtensionsPrfInputs?
}

// ! There is only one webauthn extension currently supported on iOS as of iOS 17.0:
Expand Down Expand Up @@ -326,3 +341,17 @@ public extension Data {
return result
}
}

extension SymmetricKey {
func serialize() -> String {
return self.withUnsafeBytes { body in
Data(body).base64EncodedString()
}
}
}

extension SymmetricKey? {
func serialize() -> String? {
return self.map { $0.serialize() }
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@
"react": "*",
"react-native": ">=0.71.0"
}
}
}
39 changes: 37 additions & 2 deletions src/ReactNativePasskeys.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ export interface AuthenticatorAssertionResponseJSON {
userHandle?: string;
}

/**
* - Specification reference: https://w3c.github.io/webauthn/#dictdef-authenticationextensionsprfvalues
*/
export interface AuthenticationExtensionsPrfInputs {
eval?: { first: Base64URLString; second?: Base64URLString }
}

/**
* TypeScript's types are behind the latest extensions spec, so we define them here.
* Should eventually be replaced by TypeScript's when TypeScript gets updated to
Expand All @@ -125,6 +132,7 @@ export interface AuthenticatorAssertionResponseJSON {
export interface AuthenticationExtensionsClientInputs
extends TypeScriptAuthenticationExtensionsClientInputs {
largeBlob?: AuthenticationExtensionsLargeBlobInputs;
prf?: AuthenticationExtensionsPrfInputs;
}

export type LargeBlobSupport = "preferred" | "required";
Expand All @@ -145,15 +153,23 @@ export interface AuthenticationExtensionsLargeBlobInputs {
}

// - largeBlob extension: https://w3c.github.io/webauthn/#sctn-large-blob-extension
// - prf extension: https://w3c.github.io/webauthn/#prf-extension
export interface AuthenticationExtensionsClientOutputs {
largeBlob?: Omit<AuthenticationExtensionsLargeBlobOutputs, "blob"> & {
blob?: ArrayBuffer;
};
prf?: Omit<AuthenticationExtensionsPRFOutputsJSON, "results"> & {
results: {
first: ArrayBuffer;
second?: ArrayBuffer;
}
};
}

// - largeBlob extension: https://w3c.github.io/webauthn/#sctn-large-blob-extension
export interface AuthenticationExtensionsClientOutputsJSON {
largeBlob?: AuthenticationExtensionsLargeBlobOutputs;
prf?: AuthenticationExtensionsPRFOutputsJSON
}

/**
Expand All @@ -170,16 +186,35 @@ export interface AuthenticationExtensionsLargeBlobOutputs {
written?: boolean;
}


/**
* - Specification reference: https://w3c.github.io/webauthn/#dictdef-authenticationextensionsprfvalues
*/
export interface AuthenticationExtensionsPRFValuesJSON {
first: Base64URLString;
second?: Base64URLString
}

/**
* - Specification reference: https://w3c.github.io/webauthn/#dictdef-authenticationextensionsprfoutputs
*/
export interface AuthenticationExtensionsPRFOutputsJSON {
// - true if, and only if, the PRF is available for use with the created credential. This is only reported during registration and is not present in the case of authentication.
enabled?: boolean;

results?: AuthenticationExtensionsPRFValuesJSON
}

/**
* A library specific type that combines the JSON results of a registration operation with a method
* to get the public key of the new credential since these are not available directly from the native side
*/
export interface CreationReponse extends Omit<RegistrationResponseJSON, "response"> {
export interface CreationResponse extends Omit<RegistrationResponseJSON, "response"> {
response: RegistrationResponseJSON["response"] & {
/**
* This operation returns an ArrayBuffer containing the DER SubjectPublicKeyInfo of the new credential, or null if this is not available.
* https://w3c.github.io/webauthn/#dom-authenticatorattestationresponse-getpublickey
*/
getPublicKey(): Uint8Array | null;
getPublicKey(): ArrayBuffer | null;
};
}
Loading