From 3d80b2b824c71fde99a2a2196e69c98bf751ddb3 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 28 Jan 2026 14:00:14 -0600 Subject: [PATCH] Add base64url Data extensions --- .../Core/Platform/Extensions/Data.swift | 22 +++++++++++++ .../Core/Platform/Extensions/DataTests.swift | 32 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/BitwardenKit/Core/Platform/Extensions/Data.swift b/BitwardenKit/Core/Platform/Extensions/Data.swift index 7934fc7e8..2344e7e5f 100644 --- a/BitwardenKit/Core/Platform/Extensions/Data.swift +++ b/BitwardenKit/Core/Platform/Extensions/Data.swift @@ -2,6 +2,28 @@ import CryptoKit import Foundation public extension Data { + // MARK: Initializers + + /// Parses bytes from a base64url-encoded string. + init?(base64UrlEncoded str: String) throws { + // .ignoreUnknownCharacters allows unpadded strings as a side-effect. + try self.init(base64Encoded: str.urlDecoded(), options: .ignoreUnknownCharacters) + } + + // MARK: Functions + + /// Encodes bytes in Data as a base64url string. + 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. diff --git a/BitwardenKit/Core/Platform/Extensions/DataTests.swift b/BitwardenKit/Core/Platform/Extensions/DataTests.swift index d4a5fadb2..d7ecdaa04 100644 --- a/BitwardenKit/Core/Platform/Extensions/DataTests.swift +++ b/BitwardenKit/Core/Platform/Extensions/DataTests.swift @@ -13,4 +13,36 @@ class DataTests: BitwardenTestCase { "0101010101010101010101010101010101010101010101010101010101010101", ) } + + func test_asBase64UrlStringPadded() { + let subject = Data(repeating: 1, count: 32) + XCTAssertEqual( + subject.base64UrlEncodedString(trimPadding: false), + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=", + ) + } + + func test_asBase64UrlStringUnpadded() { + let subject = Data(repeating: 1, count: 32) + XCTAssertEqual( + subject.base64UrlEncodedString(trimPadding: false), + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE", + ) + } + + func test_fromBase64UrlStringPadded() { + let subject = "9031WCEDOh6ZUGV_-wvUSw==" + XCTAssertEqual( + Data(base64UrlEncoded: subject)!, + Data([0xf7, 0x4d, 0xf5, 0x58, 0x21, 0x03, 0x3a, 0x1e, 0x99, 0x50, 0x65, 0x7f, 0xfb, 0x0b, 0xd4, 0x4b]), + ) + } + + func test_fromBase64UrlStringUnpadded() { + let subject = "9031WCEDOh6ZUGV_-wvUSw" + XCTAssertEqual( + Data(base64UrlEncoded: subject)!, + Data([0xf7, 0x4d, 0xf5, 0x58, 0x21, 0x03, 0x3a, 0x1e, 0x99, 0x50, 0x65, 0x7f, 0xfb, 0x0b, 0xd4, 0x4b]), + ) + } }