Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
11 changes: 1 addition & 10 deletions Bitwarden.xcworkspace/xcshareddata/swiftpm/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion BitwardenKit/Core/Platform/Extensions/Data.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import CryptoKit
import Foundation

public extension Data {
func base64UrlEncodedString(trimPadding shouldTrim: Bool) -> String {
let encoded = base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_")
if shouldTrim {
return encoded.trimmingCharacters(in: CharacterSet(["="]))
} else {
return encoded
}
}
/// Generates a hash value for the provided data.
///
/// - Parameter using: The type of cryptographically secure hashing being performed.
Expand Down Expand Up @@ -31,7 +39,11 @@ public extension Data {
let digest = hashFunction.hash(data: self)
return Data(digest).base64EncodedString()
}


init?(base64UrlEncoded str: String) throws {
self.init(base64Encoded: try str.urlDecoded())
}

/// Transforms this Data in a hex formatted string.
/// - Returns: Hex formatted string.
func asHexString() -> String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,8 @@
"GenerateErrorReport" = "Generate error report";
"AllowAuthenticatorSyncing" = "Allow authenticator syncing";
"AuthenticatorSync" = "Authenticator sync";
"RemoteUnlockOptions" = "Remote Unlock Options";
"UnlockOtherDevicesWithThisDevice" = "Unlock other devices with this device";
"NoAccountFoundPleaseLogInAgainIfYouContinueToSeeThisError" = "No account found. Please log in again if you continue to see this error.";
"ImportError" = "Import error";
"NoLoginsWereImported" = "No logins were imported";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// SecretVerificationRequestModel.swift
// Bitwarden
//
// Created by Isaiah Inuwa on 2025-10-03.
//


import Foundation
import Networking

struct SecretVerificationRequestModel: JSONRequestBody, Equatable {
static let encoder = JSONEncoder()

// MARK: Properties

let authRequestAccessCode: String?
let masterPasswordHash: String?
let otp: String?


init(passwordHash: String) {
authRequestAccessCode = nil
masterPasswordHash = passwordHash
otp = nil
}

init(otp: String) {
masterPasswordHash = nil
self.otp = otp
authRequestAccessCode = nil
}

init(accessCode: String) {
authRequestAccessCode = accessCode
masterPasswordHash = nil
otp = nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// WebAuthnLoginSaveCredentialRequestModel.swift
// Bitwarden
//
// Created by Isaiah Inuwa on 2025-10-03.
//


import Foundation
import Networking

// MARK: - SaveCredentialRequestModel

/// The request body for an answer login request request.
///
struct WebAuthnLoginSaveCredentialRequestModel: JSONRequestBody, Equatable {
static let encoder = JSONEncoder()

// MARK: Properties
// The response received from the authenticator.
// This contains all information needed for future authentication flows.
let deviceResponse: WebAuthnLoginAttestationResponseRequest

// Nickname chosen by the user to identify this credential
let name: String

// Token required by the server to complete the creation.
// It contains encrypted information that the server needs to verify the credential.
let token: String

// True if the credential was created with PRF support.
let supportsPrf: Bool

// Used for vault encryption. See {@link RotateableKeySet.encryptedUserKey }
let encryptedUserKey: String?

// Used for vault encryption. See {@link RotateableKeySet.encryptedPublicKey }
let encryptedPublicKey: String?

// Used for vault encryption. See {@link RotateableKeySet.encryptedPrivateKey }
let encryptedPrivateKey: String?
}

struct WebAuthnLoginAttestationResponseRequest: Encodable, Equatable {
let id: String
let rawId: String
let type: String
// let extensions: [String: Any]
let response: WebAuthnLoginAttestationResponseRequestInner
}

struct WebAuthnLoginAttestationResponseRequestInner: Encodable, Equatable {
let attestationObject: String
let clientDataJson: String

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// WebAuthnLoginCredentialAssertionOptionsResponse.swift
// Bitwarden
//
// Created by Isaiah Inuwa on 2025-10-03.
//


import Foundation
import Networking

struct WebAuthnLoginCredentialAssertionOptionsResponse: JSONResponse, Equatable, Sendable {
/// Options to be provided to the webauthn authenticator.
let options: PublicKeyCredentialAssertionOptions;

/// Contains an encrypted version of the {@link options}.
/// Used by the server to validate the attestation response of newly created credentials.
let token: String;
}

struct PublicKeyCredentialAssertionOptions: Codable, Equatable, Hashable {
let allowCredentials: [BwPublicKeyCredentialDescriptor]?
let challenge: String
let extensions: AuthenticationExtensionsClientInputs?
let rpId: String
let timeout: Int?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// WebAuthnLoginCredentialCreationOptionsResponse.swift
// Bitwarden
//
// Created by Isaiah Inuwa on 2025-10-03.
//


import Foundation
import Networking

struct WebAuthnLoginCredentialCreationOptionsResponse: JSONResponse, Equatable, Sendable {
/// Options to be provided to the webauthn authenticator.
let options: PublicKeyCredentialCreationOptions;

/// Contains an encrypted version of the {@link options}.
/// Used by the server to validate the attestation response of newly created credentials.
let token: String;
}

struct PublicKeyCredentialCreationOptions: Codable, Equatable, Hashable {
// attestation?: AttestationConveyancePreference
// let authenticatorSelection: AuthenticatorSelectionCriteria?
let challenge: String
let excludeCredentials: [BwPublicKeyCredentialDescriptor]?
let extensions: AuthenticationExtensionsClientInputs?
let pubKeyCredParams: [BwPublicKeyCredentialParameters]
let rp: BwPublicKeyCredentialRpEntity
let timeout: Int?
let user: BwPublicKeyCredentialUserEntity
}


struct AuthenticationExtensionsClientInputs: Codable, Equatable, Hashable {
let prf: AuthenticationExtensionsPRFInputs?
}

struct AuthenticationExtensionsPRFInputs: Codable, Equatable, Hashable {
let eval: AuthenticationExtensionsPRFValues?
let evalByCredential: [String: AuthenticationExtensionsPRFValues]?
}

struct AuthenticationExtensionsPRFValues: Codable, Equatable, Hashable {
let first: String
let second: String?
}

struct BwPublicKeyCredentialDescriptor: Codable, Equatable, Hashable {
let type: String
let id: String
// let transports: [String]?
}

struct BwPublicKeyCredentialParameters: Codable, Equatable, Hashable {
let type: String
let alg: Int
}

struct BwPublicKeyCredentialRpEntity: Codable, Equatable, Hashable {
let id: String
let name: String
}

struct BwPublicKeyCredentialUserEntity: Codable, Equatable, Hashable {
let id: String
let name: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ protocol AuthAPIService {
///
func getIdentityToken(_ request: IdentityTokenRequestModel) async throws -> IdentityTokenResponseModel

/// Retrieves the parameters for creating a new WebAuthn credential.
/// - Parameters:
/// - request: The data needed to send the request.
func getCredentialCreationOptions(
_ request: SecretVerificationRequestModel
) async throws -> WebAuthnLoginCredentialCreationOptionsResponse

/// Retrieves the parameters for authenticating with a WebAuthn credential.
/// - Parameters:
/// - request: The data needed to send the request.
func getCredentialAssertionOptions(
_ request: SecretVerificationRequestModel
) async throws -> WebAuthnLoginCredentialAssertionOptionsResponse

/// Gets a pending login requests.
///
/// - Parameter id: The id of the request to fetch.
Expand Down Expand Up @@ -75,6 +89,9 @@ protocol AuthAPIService {
/// - Parameter model: The data needed to send the request.
///
func resendNewDeviceOtp(_ model: ResendNewDeviceOtpRequestModel) async throws

/// Registers a WebAuthn credential.
func saveCredential(_ model: WebAuthnLoginSaveCredentialRequestModel) async throws

/// Sends the trusted device keys to the server.
///
Expand Down Expand Up @@ -106,7 +123,17 @@ extension APIService: AuthAPIService {
// Filter the response to only show the non-expired, non-answered requests.
try await apiService.send(PendingLoginsRequest()).data.filter { !$0.isAnswered && !$0.isExpired }
}


func getCredentialCreationOptions(_ request: SecretVerificationRequestModel) async throws -> WebAuthnLoginCredentialCreationOptionsResponse {
try await apiService.send(WebAuthnLoginGetCredentialCreationOptionsRequest(requestModel: request))
}

func getCredentialAssertionOptions(
_ request: SecretVerificationRequestModel
) async throws -> WebAuthnLoginCredentialAssertionOptionsResponse {
try await apiService.send(WebAuthnLoginGetCredentialAssertionOptionsRequest(requestModel: request))
}

func initiateLoginWithDevice(_ requestModel: LoginWithDeviceRequestModel) async throws -> LoginRequest {
let request = LoginWithDeviceRequest(body: requestModel)
if request.requestType == AuthRequestType.adminApproval {
Expand All @@ -133,6 +160,10 @@ extension APIService: AuthAPIService {
_ = try await apiUnauthenticatedService.send(ResendNewDeviceOtpRequest(model: model))
}

func saveCredential(_ model: WebAuthnLoginSaveCredentialRequestModel) async throws {
_ = try await apiService.send(WebAuthnLoginSaveCredentialRequest(requestModel: model))
}

func updateTrustedDeviceKeys(deviceIdentifier: String, model: TrustedDeviceKeysRequestModel) async throws {
_ = try await apiService.send(TrustedDeviceKeysRequest(deviceIdentifier: deviceIdentifier, requestModel: model))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// WebAuthnLoginGetCredentialAssertionOptionsRequest.swift
// Bitwarden
//
// Created by Isaiah Inuwa on 2025-10-03.
//


import Networking

struct WebAuthnLoginGetCredentialAssertionOptionsRequest : Request {
typealias Response = WebAuthnLoginCredentialAssertionOptionsResponse

var body: SecretVerificationRequestModel? { requestModel }

var path: String { "/webauthn/assertion-options" }

var method: HTTPMethod { .post }

let requestModel: SecretVerificationRequestModel
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// WebAuthnLoginGetCredentialCreationOptionsRequest.swift
// Bitwarden
//
// Created by Isaiah Inuwa on 2025-10-03.
//


import Networking

struct WebAuthnLoginGetCredentialCreationOptionsRequest : Request {
typealias Response = WebAuthnLoginCredentialCreationOptionsResponse

var body: SecretVerificationRequestModel? { requestModel }

var path: String { "/webauthn/attestation-options" }

var method: HTTPMethod { .post }

let requestModel: SecretVerificationRequestModel
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// WebAuthnLoginSaveCredentialRequest.swift
// Bitwarden
//
// Created by Isaiah Inuwa on 2025-10-03.
//


import Networking

struct WebAuthnLoginSaveCredentialRequest : Request {
typealias Response = EmptyResponse

var body: WebAuthnLoginSaveCredentialRequestModel? { requestModel }

var path: String { "/webauthn" }

var method: HTTPMethod { .post }

let requestModel: WebAuthnLoginSaveCredentialRequestModel
}
Loading