mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-22 07:16:03 +00:00
Merge 9d621fcc1ca744bb7fd4a372554ce7646e04a2b7 into 1f59e814f90c3f489f48d68262cb1bf640bf6181
This commit is contained in:
commit
1b84999865
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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: "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitFoundation
|
||||
import BitLogger
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
@ -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? {
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user