Merge 9d621fcc1ca744bb7fd4a372554ce7646e04a2b7 into 1f59e814f90c3f489f48d68262cb1bf640bf6181

This commit is contained in:
Vidit Kulshrestha 2026-08-02 12:00:47 +02:00 committed by GitHub
commit 1b84999865
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 109 additions and 51 deletions

View File

@ -1,22 +0,0 @@
import Foundation
enum Base64URLCoding {
static func encode(_ data: Data) -> String {
data.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
static func decode(_ string: String) -> Data? {
var base64 = string
let padding = (4 - (base64.count % 4)) % 4
if padding > 0 {
base64 += String(repeating: "=", count: padding)
}
base64 = base64
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
return Data(base64Encoded: base64)
}
}

View File

@ -28,7 +28,7 @@ struct NostrEmbeddedBitChat {
)
guard let data = packet.toBinaryData() else { return nil }
return "bitchat1:" + base64URLEncode(data)
return "bitchat1:" + Base64URLCoding.encode(data)
}
/// Build a `bitchat1:` base64url-encoded BitChat packet carrying a delivery/read ack for Nostr DMs.
@ -51,7 +51,7 @@ struct NostrEmbeddedBitChat {
)
guard let data = packet.toBinaryData() else { return nil }
return "bitchat1:" + base64URLEncode(data)
return "bitchat1:" + Base64URLCoding.encode(data)
}
/// Build a `bitchat1:` ACK (delivered/read) without an embedded recipient peer ID (geohash DMs).
@ -72,7 +72,7 @@ struct NostrEmbeddedBitChat {
)
guard let data = packet.toBinaryData() else { return nil }
return "bitchat1:" + base64URLEncode(data)
return "bitchat1:" + Base64URLCoding.encode(data)
}
/// Build a `bitchat1:` payload without an embedded recipient peer ID (used for geohash DMs).
@ -94,7 +94,7 @@ struct NostrEmbeddedBitChat {
)
guard let data = packet.toBinaryData() else { return nil }
return "bitchat1:" + base64URLEncode(data)
return "bitchat1:" + Base64URLCoding.encode(data)
}
private static func normalizeRecipientPeerID(_ recipientPeerID: PeerID) -> PeerID {
@ -110,13 +110,4 @@ struct NostrEmbeddedBitChat {
// Fallback: return as-is (expecting 16 hex chars) caller should pass a valid peer ID
return recipientPeerID
}
/// Base64url encode without padding
private static func base64URLEncode(_ data: Data) -> String {
let b64 = data.base64EncodedString()
return b64
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}

View File

@ -1,3 +1,4 @@
import BitFoundation
import BitLogger
import Foundation
import CryptoKit

View File

@ -14,6 +14,7 @@
// For more information, see <https://unlicense.org>
//
import BitFoundation
import Foundation
enum CashuTokenDecoder {
@ -81,7 +82,7 @@ enum CashuTokenDecoder {
static func decode(_ raw: String, strict: Bool = false) -> TokenInfo? {
guard let token = bareToken(from: raw) else { return nil }
let version = String(token[token.index(token.startIndex, offsetBy: 5)])
guard let payload = base64URLDecode(String(token.dropFirst(6))), !payload.isEmpty else {
guard let payload = Base64URLCoding.decode(String(token.dropFirst(6))), !payload.isEmpty else {
return nil
}
let info: TokenInfo?
@ -108,20 +109,6 @@ enum CashuTokenDecoder {
return info
}
// MARK: - Base64url
private static func base64URLDecode(_ input: String) -> Data? {
var s = input
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
// Normalize padding (wallets emit both padded and unpadded forms)
s = s.replacingOccurrences(of: "=", with: "")
let remainder = s.count % 4
if remainder == 1 { return nil }
if remainder > 0 { s += String(repeating: "=", count: 4 - remainder) }
return Data(base64Encoded: s)
}
// MARK: - V3 (JSON)
private static func decodeV3(_ payload: Data) -> TokenInfo? {

View File

@ -1,3 +1,4 @@
import BitFoundation
import Foundation
/// QR verification scaffolding: schema, signing, and basic challenge/response helpers.
@ -85,7 +86,7 @@ final class VerificationService {
let ts = Int64(Date().timeIntervalSince1970)
var nonce = Data(count: 16)
_ = nonce.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, 16, $0.baseAddress!) }
let nonceB64 = nonce.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
let nonceB64 = Base64URLCoding.encode(nonce)
let payload = VerificationQR(v: 1, noiseKeyHex: noiseKey, signKeyHex: signKey, npub: npub, nickname: nickname, ts: ts, nonceB64: nonceB64, sigHex: "")
let msg = payload.canonicalBytes()
guard let sig = transport.noiseSignData(msg) else { return nil }

View File

@ -0,0 +1,38 @@
//
// Base64URLCoding.swift
// BitFoundation
//
// Single implementation of base64url (RFC 4648 §5) used across the app
// Nostr embeddings, Cashu tokens, and verification QR payloads all share
// these helpers so padding and alphabet handling cannot drift per call site.
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
public enum Base64URLCoding {
/// Encode data as base64url without padding.
public static func encode(_ data: Data) -> String {
data.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
/// Decode a base64url string, accepting both padded and unpadded forms
/// (external producers, e.g. Cashu wallets, emit both).
public static func decode(_ string: String) -> Data? {
var base64 = string
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
// Normalize padding: strip any '=' then re-pad to a multiple of 4.
base64 = base64.replacingOccurrences(of: "=", with: "")
let remainder = base64.count % 4
// No valid base64 payload has length 1 (mod 4).
if remainder == 1 { return nil }
if remainder > 0 { base64 += String(repeating: "=", count: 4 - remainder) }
return Data(base64Encoded: base64)
}
}

View File

@ -0,0 +1,62 @@
//
// Base64URLCodingTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Testing
import Foundation
@testable import BitFoundation
struct Base64URLCodingTests {
@Test
func encodeUsesURLAlphabetWithoutPadding() {
// 0xFF 0xEF is "/+8=" in standard base64: exercises both
// substituted characters and padding removal.
#expect(Base64URLCoding.encode(Data([0xFF, 0xEF])) == "_-8")
#expect(Base64URLCoding.encode(Data("Man".utf8)) == "TWFu")
#expect(Base64URLCoding.encode(Data("Ma".utf8)) == "TWE")
#expect(Base64URLCoding.encode(Data("M".utf8)) == "TQ")
#expect(Base64URLCoding.encode(Data()) == "")
}
@Test
func decodeAcceptsUnpaddedInput() {
#expect(Base64URLCoding.decode("_-8") == Data([0xFF, 0xEF]))
#expect(Base64URLCoding.decode("TWFu") == Data("Man".utf8))
#expect(Base64URLCoding.decode("TWE") == Data("Ma".utf8))
#expect(Base64URLCoding.decode("TQ") == Data("M".utf8))
#expect(Base64URLCoding.decode("") == Data())
}
@Test
func decodeAcceptsPaddedInput() {
// External producers (e.g. Cashu wallets) emit padded forms too.
#expect(Base64URLCoding.decode("_-8=") == Data([0xFF, 0xEF]))
#expect(Base64URLCoding.decode("TWE=") == Data("Ma".utf8))
#expect(Base64URLCoding.decode("TQ==") == Data("M".utf8))
}
@Test
func decodeRejectsInvalidInput() {
// Length 1 (mod 4) can never be valid base64.
#expect(Base64URLCoding.decode("TQQQQ") == nil)
// Characters outside the base64url alphabet.
#expect(Base64URLCoding.decode("!!!") == nil)
#expect(Base64URLCoding.decode("TW Fu") == nil)
}
@Test
func roundTripsAllPaddingLengths() {
for length in 0..<16 {
let data = Data((0..<length).map { UInt8($0 &* 37 &+ 11) })
let encoded = Base64URLCoding.encode(data)
#expect(!encoded.contains("+"))
#expect(!encoded.contains("/"))
#expect(!encoded.contains("="))
#expect(Base64URLCoding.decode(encoded) == data)
}
}
}