Unable to authenticate always getting 0x81 bad proof key

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())

}

}

I would suggest you look at the XBee Mobile application SDK to see how we implemented this function.