Proximity proof
Overview
The following instructions only help satisfy Kisi's reader restriction, and implicitly also Kisi's geofence restriction. Tap to unlock will not work unless you fully integrate Kisi's mobile SDKs.
To build a secure, proximity-based unlock flow on top of Kisi, any mobile app needs to:
- Detect a nearby Kisi iBeacon.
- Parse the beacon's advertisement (UUID, Major & Minor).
- Extract the lock's unique identifier (lock ID).
- Generate a time-based one-time password (TOTP) for that lock.
- Send a proximity proof (the TOTP) to Kisi's unlock API.
Kisi readers rotate among five UUIDs to improve detection and pack both the lock ID and TOTP into the 32-bit Major+Minor fields.
iBeacon format
Each Kisi reader advertises as an iBeacon with:
- UUID (128 bits)
- Major (16 bits)
- Minor (16 bits)
The app needs to subscribe to Kisi's UUIDs using the mobile operation system's APIs, then read Major+Minor when in range.
UUID
Each reader cycles through five fixed UUIDs. Your scanner should listen for all of them:
UUID |
---|
6dfe2064-5186-11ea-8dfe-4b73218bf946 |
6f9e62a8-5186-11ea-8ed0-4314341d01dd |
71fb9eb2-5186-11ea-b1f4-bb928fefb7fd |
73f789ec-5186-11ea-9973-87896ce82dd8 |
74689eac-5186-11ea-be2d-27ec120c3b0f |
Major + Minor
To transmit lock identification and a one-time password (OTP) via a standard iBeacon payload, we encode both values into the major and minor fields, which are each 16 bits (total: 32 bits).
Since this space is limited, we truncate both the lock ID and OTP as follows:
- Lock ID (22 bits)
- The Lock ID is encoded into the upper 22 bits of the combined 32-bit value.
- This limits the addressable lock ID space to: 2²² = 4,194,304 unique IDs
- This is sufficient for scalability up to ~4 million locks.
- OTP (10 bits)
- The OTP is encoded into the lower 10 bits.
- This results in a maximum range of: 2¹⁰ = 1024 → OTPs from 000 to 1023
Unlike more widely adopted 6-digit TOTPs, this yields a 3-digit OTP, which trades off entropy for compatibility with the iBeacon packet format.
The order in the 32 bit value is as follows:
- Lock ID (bits 0…21): unsigned, MSB
- TOTP (bits 22…31): unsigned, MSB
Bit diagram:
Major (16 bits) Minor (16 bits)
┌────────────────────┬──────────────────────────┐
| xxxx xxxx xxxx xxxx yyyy yyyy yy|yy yyyy yyyy |
└───── ───────────────┴──────────────────────────┘
↑ ↑ ↑ ↑
└────────┬─────────────────────┘ └───┬──────┘
│ │
Lock ID ─┘ TOTP ─┘
Extracting the TOTP and lock ID
After detecting a beacon:
- Android (Kotlin)
- iOS (Swift)
fun handleBeacon(major: Int, minor: Int) {
val combined = (major.toLong() shl 16) or (minor.toLong() and 0xFFFF)
val lockID = (combined shr 10).toInt() // bits 10…31
val otp = (combined and 0x3FF).toInt() // bits 0…9
println("🔒 Lock ID: $lockID")
println("🔑 TOTP: $otp")
}
func handleBeacon(major: UInt16, minor: UInt16) {
let combined = UInt32(major) << 16 | UInt32(minor)
let lockID = combined >> 10
let otp = UInt16(combined & 0x3FF)
print("🔒 Lock ID:", lockID)
print("🔑 TOTP:", otp)
}
Sending the proximity proof
After extracting lockID
and otp
from the beacon, send them to the unlock endpoint. For example, using dummy iBeacon values:
- Android (Kotlin)
- iOS (Swift)
// Dummy iBeacon values (scanned from Android BLE API)
val beaconMajor = 0x1234
val beaconMinor = 0x03E8
// Combine and extract lockID and TOTP
val combined = (beaconMajor.toLong() shl 16) or (beaconMinor.toLong() and 0xFFFF)
val lockID = (combined shr 10).toInt()
val otp = (combined and 0x3FF).toInt()
val otpString = String.format("%03d", otp) // zero-pad to 3 digits
val json = JSONObject().apply {
put("lock", JSONObject().apply {
put("proximity_proof", otpString)
})
}
val body = json.toString().toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("https://api.kisi.io/locks/$lockID/unlock")
.addHeader("Authorization", "Bearer $accessToken")
.post(body)
.build()
OkHttpClient().newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) { /*…*/ }
override fun onFailure(call: Call, e: IOException) { /* …*/ }
})
// Dummy iBeacon values (scanned from CoreBluetooth)
let beaconMajor: UInt16 = 0x1234
let beaconMinor: UInt16 = 0x03E8
// Combine and extract lockID and TOTP
let combined = UInt32(beaconMajor) << 16 | UInt32(beaconMinor)
let lockID = combined >> 10
let otp = UInt16(combined & 0x3FF)
let otpString = String(format: "%03d", otp) // zero-pad to 3 digits
let parameters: [String: Any] = [
"lock": ["proximity_proof": otpString]
]
AF.request(
"https://api.kisi.io/locks/\(lockID)/unlock",
method: .post,
parameters: parameters,
encoding: JSONEncoding.default,
headers: ["Authorization": "Bearer \(yourAccessToken)"]
).response { resp in
// handle success / error
}