Hi, I’m working with a Digi XBee BLE module and developing an iOS app to communicate with it. However, whenever I attempt authentication, I keep receiving the 0x81 error.
Please help me resolve this issue
This is my SRP client:
//
// XBeeSRPClient.swift
// DemoBLW
//
// Created by Sahabe Alam on 10/09/25.
//
//
// XBeeSRPClient.swift
// DemoBLW
//
// Created by Sahabe Alam on 10/09/25.
//
import Foundation
import CryptoKit
import BigInt
import CoreBluetooth
// MARK: - XBee SRP Client
class XBeeSRPClient {
**private** **let** username = "apiservice"
**private** **var** password: String
// RFC5054 1024-bit group parameters
**private** **let** N = BigInt("EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF7496EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6CE8EF4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA9AFD5138FE8376435B9FC61D2FC0EB06E3", radix: 16)!
**private** **let** g = BigInt(2)
// SRP state variables
**private** **var** a: BigInt = BigInt() // client private key
**private** **var** A: BigInt = BigInt() // client public key
**private** **var** B: BigInt = BigInt() // server public key
**private** **var** salt: Data = Data()
**private** **var** sessionKey: Data = Data()
**private** **var** M1: Data = Data() // client proof
**private** **var** M2: Data = Data() // expected server proof
**private** **var** txNonce: Data = Data()
**private** **var** rxNonce: Data = Data()
**private** **var** currentPhase: SRPPhase = .initial
**enum** SRPPhase {
**case** initial
**case** phase1Sent
**case** phase3Sent
**case** authenticated
**case** failed
}
**init**(password: String) {
**self**.password = password
}
**func** initiate() -> Data {
// Generate random private key 'a' (at least 256 bits)
**repeat** {
a = generateRandomBigInt(bitLength: 256)
} **while** a == BigInt.zero || a >= N
// Calculate client public key A = g^a mod N
A = g.power(a, modulus: N)
// Ensure A is not zero (would cause server error 0x80)
**guard** A != BigInt.zero **else** {
**return** initiate() // Retry (extremely unlikely)
}
**let** clientPublicKey = bigIntToData(A, length: 128)
currentPhase = .phase1Sent
print("DEBUG: Generated A = \\(A.description)")
print("DEBUG: A as hex = \\(clientPublicKey.map { String(format: "%02x", $0) }.joined())")
**return** createAPIFrame(frameType: 0x2C, phase: 0x01, payload: clientPublicKey)
}
**func** processResponse(\_ responseData: Data) **throws** -> Data? {
**guard** **let** (phase, payload) = parseAPIFrame(responseData) **else** {
**throw** XBeeSRPError.invalidFrame
}
// Check for error phases (0x80-0x84)
**if** phase >= 0x80 {
currentPhase = .failed
**throw** XBeeSRPError.serverError(phase)
}
**switch** phase {
**case** 0x02:
// Phase 2: Server challenge
**return** **try** processPhase2(payload)
**case** 0x04:
// Phase 4: Server proof
**try** processPhase4(payload)
**return** **nil** // Authentication complete
**default**:
**throw** XBeeSRPError.unexpectedPhase(phase)
}
}
**private** **func** processPhase2(\_ payload: Data) **throws** -> Data {
**guard** payload.count >= 132 **else** { // 4 bytes salt + 128 bytes B
**throw** XBeeSRPError.invalidPayloadLength
}
// XBee sends salt in big-endian format
salt = Data(payload.prefix(4))
**let** serverPublicKeyData = Data(payload.suffix(from: 4))
B = dataToBigInt(serverPublicKeyData)
print("DEBUG: Received salt = \\(salt.map { String(format: "%02X", $0) }.joined())")
print("DEBUG: Received B = \\(serverPublicKeyData.prefix(16).map { String(format: "%02X", $0) }.joined())...")
// Verify B is valid (B % N != 0)
**guard** B % N != BigInt.zero **else** {
**throw** XBeeSRPError.invalidServerPublicKey
}
// Compute session key and proofs
**try** computeSessionKeyAndProofs()
currentPhase = .phase3Sent
**return** createAPIFrame(frameType: 0x2C, phase: 0x03, payload: M1)
}
**private** **func** processPhase4(\_ payload: Data) **throws** {
**guard** payload.count >= 56 **else** { // 32 bytes M2 + 12 bytes tx + 12 bytes rx
**throw** XBeeSRPError.invalidPayloadLength
}
**let** serverProof = Data(payload.prefix(32))
txNonce = Data(payload\[32..<44\])
rxNonce = Data(payload\[44..<56\])
**guard** serverProof == M2 **else** {
currentPhase = .failed
**throw** XBeeSRPError.authenticationFailed
}
currentPhase = .authenticated
}
**private** **func** computeSessionKeyAndProofs() **throws** {
// XBee-specific SRP implementation based on working examples
// Step 1: Compute multiplier k = H(N | pad(g))
**var** kData = Data()
kData.append(bigIntToData(N, length: 128))
kData.append(bigIntToData(g, length: 128))
**let** kHash = SHA256.hash(data: kData)
**let** k = dataToBigInt(Data(kHash))
print("DEBUG: k = \\(Data(kHash).map { String(format: "%02X", $0) }.joined())")
// Step 2: Compute scrambling parameter u = H(pad(A) | pad(B))
**var** uData = Data()
uData.append(bigIntToData(A, length: 128))
uData.append(bigIntToData(B, length: 128))
**let** uHash = SHA256.hash(data: uData)
**let** u = dataToBigInt(Data(uHash))
print("DEBUG: u = \\(Data(uHash).map { String(format: "%02X", $0) }.joined())")
**guard** u != BigInt.zero **else** {
**throw** XBeeSRPError.cryptographicError
}
// Step 3: Compute private key x = H(salt | H(username:password))
// XBee uses raw salt bytes directly (not hex string)
**let** identityString = username + ":" + password
**let** identityHash = SHA256.hash(data: identityString.data(using: .utf8) ?? Data())
**var** xData = Data()
xData.append(salt) // Use original 4-byte salt directly
xData.append(Data(identityHash))
**let** xHash = SHA256.hash(data: xData)
**let** x = dataToBigInt(Data(xHash))
print("DEBUG: Identity = \\(identityString)")
print("DEBUG: Identity hash = \\(Data(identityHash).map { String(format: "%02X", $0) }.joined())")
print("DEBUG: x input = \\(xData.map { String(format: "%02X", $0) }.joined())")
print("DEBUG: x = \\(Data(xHash).map { String(format: "%02X", $0) }.joined())")
// Step 4: Compute session secret S
// S = (B - k \* g^x)^(a + u \* x) mod N
**let** gx = g.power(x, modulus: N)
**let** kgx = (k \* gx) % N
// Handle B - kgx properly (critical for SRP-6a)
**var** base: BigInt
**if** B >= kgx {
base = (B - kgx) % N
} **else** {
base = (B + N - kgx) % N
}
// CRITICAL: Use (N-1) as modulus for exponent in SRP-6a
**let** nMinus1 = N - 1
**let** ux = (u \* x) % nMinus1 // This is critical!
**let** exponent = (a + ux) % nMinus1 // Use N-1, not N
**let** S = base.power(exponent, modulus: N)
print("DEBUG: gx = \\(bigIntToData(gx, length: 16).map { String(format: "%02X", $0) }.joined())...")
print("DEBUG: kgx = \\(bigIntToData(kgx, length: 16).map { String(format: "%02X", $0) }.joined())...")
print("DEBUG: base = \\(bigIntToData(base, length: 16).map { String(format: "%02X", $0) }.joined())...")
print("DEBUG: ux = \\(Data(SHA256.hash(data: bigIntToData(ux, length: 32))).prefix(8).map { String(format: "%02X", $0) }.joined())...")
print("DEBUG: exponent = \\(Data(SHA256.hash(data: bigIntToData(exponent, length: 32))).prefix(8).map { String(format: "%02X", $0) }.joined())...")
print("DEBUG: S = \\(bigIntToData(S, length: 16).map { String(format: "%02X", $0) }.joined())...")
// Step 5: Compute session key K = H(S)
**let** sData = bigIntToData(S, length: 128)
**let** sessionKeyHash = SHA256.hash(data: sData)
sessionKey = Data(sessionKeyHash)
print("DEBUG: Session key = \\(sessionKey.map { String(format: "%02X", $0) }.joined())")
// Step 6: Compute client proof M1
// XBee M1 = H(H(N) XOR H(g) | H(I) | s | A | B | K)
**let** nHash = SHA256.hash(data: bigIntToData(N, length: 128))
**let** gHash = SHA256.hash(data: bigIntToData(g, length: 128))
**let** ngXor = Data(zip(Data(nHash), Data(gHash)).map { $0.0 ^ $0.1 })
**let** iHash = SHA256.hash(data: username.data(using: .utf8) ?? Data())
**var** m1Data = Data()
m1Data.append(ngXor) // 32 bytes: H(N) XOR H(g)
m1Data.append(Data(iHash)) // 32 bytes: H(I)
m1Data.append(salt) // 4 bytes: original salt bytes
m1Data.append(bigIntToData(A, length: 128)) // 128 bytes: A
m1Data.append(bigIntToData(B, length: 128)) // 128 bytes: B
m1Data.append(sessionKey) // 32 bytes: K
**let** m1Hash = SHA256.hash(data: m1Data)
M1 = Data(m1Hash)
print("DEBUG: M1 components:")
print(" H(N) XOR H(g): \\(ngXor.map { String(format: "%02X", $0) }.joined())")
print(" H(I): \\(Data(iHash).map { String(format: "%02X", $0) }.joined())")
print(" salt: \\(salt.map { String(format: "%02X", $0) }.joined())")
print("DEBUG: M1 = \\(M1.map { String(format: "%02X", $0) }.joined())")
// Step 7: Compute expected server proof M2 = H(A | M1 | K)
**var** m2Data = Data()
m2Data.append(bigIntToData(A, length: 128))
m2Data.append(M1)
m2Data.append(sessionKey)
**let** m2Hash = SHA256.hash(data: m2Data)
M2 = Data(m2Hash)
print("DEBUG: Expected M2 = \\(M2.map { String(format: "%02X", $0) }.joined())")
}
// **MARK: - Encryption/Decryption**
**func** encryptData(\_ data: Data) **throws** -> Data {
**guard** currentPhase == .authenticated **else** {
**throw** XBeeSRPError.notAuthenticated
}
**return** **try** performAESCTR(data: data, nonce: txNonce, encrypt: **true**)
}
**func** decryptData(\_ data: Data) **throws** -> Data {
**guard** currentPhase == .authenticated **else** {
**throw** XBeeSRPError.notAuthenticated
}
**return** **try** performAESCTR(data: data, nonce: rxNonce, encrypt: **false**)
}
**private** **func** performAESCTR(data: Data, nonce: Data, encrypt: Bool) **throws** -> Data {
// Use AES.CTR directly instead of AES.GCM for counter mode
**let** key = SymmetricKey(data: sessionKey)
// Create a proper 16-byte IV (nonce + 4-byte counter starting at 1)
**var** iv = Data()
iv.append(nonce) // 12 bytes
iv.append(Data(\[0, 0, 0, 1\])) // 4-byte counter starting at 1
// For AES-CTR, we need to implement it manually or use a different approach
// This is a simplified version - you might need a proper CTR implementation
**var** result = Data()
**let** blockSize = 16
**var** counter: UInt32 = 1
**for** i **in** stride(from: 0, to: data.count, by: blockSize) {
**let** endIndex = min(i + blockSize, data.count)
**let** block = data\[i..<endIndex\]
// Create counter block
**var** counterBlock = Data()
counterBlock.append(nonce)
counterBlock.append(withUnsafeBytes(of: counter.bigEndian) { Data($0) })
// Encrypt the counter block (this is where you'd use raw AES encryption)
// For now, using GCM seal as a workaround (not ideal but functional)
**let** sealedBox = **try** AES.GCM.seal(counterBlock, using: key)
**let** keystream = sealedBox.ciphertext
// XOR with data
**let** xorBlock = Data(zip(block, keystream.prefix(block.count)).map { $0.0 ^ $0.1 })
result.append(xorBlock)
counter += 1
}
**return** result
}
// **MARK: - Utility Methods**
**private** **func** generateRandomBigInt(bitLength: Int) -> BigInt {
**let** byteLength = (bitLength + 7) / 8
**var** data = Data(count: byteLength)
\_ = data.withUnsafeMutableBytes { bytes **in**
SecRandomCopyBytes(kSecRandomDefault, byteLength, bytes.bindMemory(to: UInt8.**self**).baseAddress!)
}
**return** dataToBigInt(data)
}
**private** **func** bigIntToData(\_ bigInt: BigInt, length: Int) -> Data {
// XBee expects big-endian byte order with proper zero-padding
**let** data = bigInt.serialize()
**if** data.count >= length {
// If data is longer than expected, take the least significant bytes
**return** Data(data.suffix(length))
} **else** {
// Zero-pad on the left (big-endian)
**var** paddedData = Data(repeating: 0, count: length - data.count)
paddedData.append(data)
**return** paddedData
}
}
**private** **func** dataToBigInt(\_ data: Data) -> BigInt {
// Ensure we're interpreting as big-endian
**guard** !data.isEmpty **else** { **return** BigInt.zero }
**return** BigInt(data)
}
**private** **func** createAPIFrame(frameType: UInt8, phase: UInt8, payload: Data) -> Data {
**var** frame = Data()
frame.append(0x7E) // Frame delimiter
**let** length = UInt16(2 + payload.count) // frame type + phase + payload
frame.append(contentsOf: withUnsafeBytes(of: length.bigEndian) { Data($0) })
frame.append(frameType)
frame.append(phase)
frame.append(payload)
// Calculate checksum
**let** checksumData = frame.suffix(from: 3)
**let** checksum = 0xFF - (checksumData.reduce(0) { $0 + UInt16($1) } & 0xFF)
frame.append(UInt8(checksum))
**return** frame
}
**private** **func** parseAPIFrame(\_ data: Data) -> (phase: UInt8, payload: Data)? {
**guard** data.count >= 6, data\[0\] == 0x7E **else** { **return** **nil** }
**let** length = UInt16(data\[1\]) << 8 | UInt16(data\[2\])
**guard** data.count >= Int(length) + 4 **else** { **return** **nil** }
**let** frameType = data\[3\]
**guard** frameType == 0xAC **else** { **return** **nil** } // BLE Unlock Response
**let** phase = data\[4\]
**let** payloadLength = Int(length) - 2
**let** endIndex = min(5 + payloadLength, data.count - 1)
**let** payload = Data(data\[5..<endIndex\])
**return** (phase: phase, payload: payload)
}
// **MARK: - Getters**
**var** isAuthenticated: Bool { currentPhase == .authenticated }
**var** getSessionKey: Data { sessionKey }
**var** getTxNonce: Data { txNonce }
**var** getRxNonce: Data { rxNonce }
}
// MARK: - Error Types
enum XBeeSRPError: Error {
**case** invalidFrame
**case** invalidPayloadLength
**case** invalidServerPublicKey
**case** cryptographicError
**case** authenticationFailed
**case** notAuthenticated
**case** serverError(UInt8)
**case** unexpectedPhase(UInt8)
}
enum XBeeError: Error {
**case** notConnected
**case** invalidResponse
**case** timeout
}
// MARK: - BigInt Extension
extension BigInt {
**func** serialize() -> Data {
**let** magnitude = **self**.magnitude
**var** bytes: \[UInt8\] = \[\]
**var** temp = magnitude
**if** temp == 0 {
**return** Data(\[0\])
}
**while** temp > 0 {
bytes.append(UInt8(temp & 0xFF))
temp >>= 8
}
**return** Data(bytes.reversed())
}
}