Skip to main content
Background Image

The Internet of Entrance Doors

The Open Source Smart Home - This article is part of a series.
Part 3: This Article
Or: How I never have to fumble for keys again

Compared to my previous adventures in this series - reverse-engineering a proprietary sofa protocol and decoding decades-old roof window communications - this project was refreshingly straightforward. No ladder acrobatics, no fried transistors, no proprietary bus protocols from the early 2000s… just a simple goal: Make my entrance door open automatically when I approach it.

Stories from the Open Source Smart Home - Part 3


Keys Are So Last Century
#

My Salzburg flat came with a modern keypad system for the building’s entrance door. Great in theory - no keys to lose, just remember a code. Future.

Until you try punching in a 6-digit code while balancing groceries, in the rain. And heaven forbid you get one digit wrong and have to start over while your food slowly gets soaked. Future, this was not.

But: The intercom handset system in my flat had a button to buzz people in. Every time I approached the building, loaded down with shopping bags, I’d think: “If only I could press that button from outside.” Then I’d think: “Wait, I literally automate things for fun.”

The itch was back.

intercom

The Plan: A Three-Part System
#

The system would need three parts:

  1. Door Opener: Something to physically press the intercom button
  2. Key Detector: A sensor near the entrance to detect my approach
  3. Key App: Something on my phone to authenticate that it’s actually me

Simple enough. What could possibly go wrong?

(Spoiler: I accidentally opened my door while driving past my building. We’ll get to that.)

Act I: The Intercom Surgery
#

First step: Figure out how to electronically “press” the intercom button. While I could have gone the crude route with a SwitchBot or similar mechanical button pusher, where’s the elegance in that? Plus, I have this weird compulsion to open up every electronic device I own.

intercom_manual
Image source: This Manual

The Reconnaissance
#

The intercom was a BTICINO BTI334202 SPRINT. It had four buttons that didn’t do anything, plus one big blue button for opening the door. Two screws and three plastic clips later, I was looking at a 5-wire bus where shorting the right two wires would trigger the door.

Perfect. All I needed to do was short these two contacts, and the door would open.

intercom_internals_front
intercom_internals_back

One convenient feature of this system was its screw terminals - no soldering required. I loosened them, added two wires from an ethernet cable I had lying around, and tightened them back down. Simple, clean, and (importantly) reversible.

The Clean Installation
#

For tidiness (and probably some deep-seated need to make everything look professional), I decided to run the wires through the wall. Lucky for me, the intercom already had cables feeding into the wall cavity, and there was an existing cable duct that ran directly behind where my couch would sit. Even better - that duct was still empty, so pulling the cable through it was a breeze with my pulling rod. Sometimes the automation gods smile upon you.

intercom_cable

On the other side, behind the couch, I connected the ethernet wires to an ESP32-GATEWAY board with a simple relay module. When the relay closes, it shorts the wires, “pressing” the button. The ESPHome configuration couldn’t be simpler:

esphome:
  name: intercom
esp32:
  board: esp32-gateway
  framework:
    type: arduino
logger:
api:
  password: <API_PASSWORD>
ota:
  password: <OTA_PASSWORD>
ethernet:
  type: LAN8720
  mdc_pin: GPIO23
  mdio_pin: GPIO18
  clk_mode: GPIO0_IN
  phy_addr: 0

switch:
  - platform: gpio
    pin: GPIO32
    name: "Door Opener"
    id: door_opener
    on_turn_on:
      then:
        - delay: 0.2s
        - switch.turn_off: door_opener

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO34
      inverted: true
    id: button_onboard
    on_press:
      then:
        - switch.turn_on: door_opener

The 0.2-second delay mimics a human button press - long enough to register, short enough not to annoy the door mechanism.

ESPHome exposed the relay to Home Assistant, and that forwarded it to HomeKit. Within minutes, I could open my entrance door with my phone.

“Hey Siri, open the door.” Bzzzzt. Magic.

Act II: The Authentication Dilemma
#

Now, having a button on my phone to open the door is nice, but the dream was hands-free operation. The door should just know it’s me approaching and open automatically. So, how to detect me?

The Failed Approaches
#

Attempt #1: WiFi Detection iOS Shortcuts can trigger when connecting to specific WiFi networks. It sounds perfect in theory: Trigger the door opener when my phone connects to my home WiFi. However, the problems with this became immediately apparent:

  • The WiFi sometimes reaches the surrounding area (meh, random door openings)
  • The WiFi sometimes doesn’t reach the entrance (meh, waiting in the rain)
  • The latency between connecting and the shortcut running is unpredictable in general

Attempt #2: Geolocation iOS also supports location-based automations. So: Set up a geofence around my building, trigger when entering. What could go wrong?

Well, it turns out GPS resolution is sometimes… optimistic. One time when I drove past my building on the way to my grandparents’ place, the automation triggered. Twenty minutes later, I realized my building’s door had been wide open the entire time. Well, that was a fun drive back.

Geofencing: Not precise enough for entrance doors, apparently.

The Breakthrough: BLE Authentication
#

What I needed was something with:

  • Short range (5-10 meters maximum)
  • Fast detection
  • Something that neighbors couldn’t accidentally trigger
  • Works in iOS background mode (the killer requirement)

Bluetooth Low Energy (BLE) fits those requirements perfectly. Its short range limits it to only around the entrance, and iOS had surprisingly good background support for it - originally intended for devices such as medical sensors and fitness trackers, but perfectly suited for proximity-based authentication as well.

The concept was straightforward: place a BLE beacon near the door that broadcasts a challenge, have my phone respond with the correct response, door opens. A cryptographic handshake, for doors.

Act III: The BLE Key System
#

The Protocol
#

I designed a simple challenge-response system built around SHA-256:

  1. The ESP32 beacon continuously advertises a BLE service with a “challenge” characteristic
  2. The challenge is SHA-256 of the current time (rounded to 1-second intervals) plus a salt
  3. The phone reads the challenge and computes SHA256(hex(challenge) + shared_secret)
  4. The phone writes the first 22 bytes of that hash back to a “response” characteristic
  5. The ESP32 checks the response against the current and recent previous challenges, then triggers the door if one matches

The Sensor Hardware
#

For the BLE beacon, I needed something that could run ESPHome, support Bluetooth operations, and sit inconspicuously in the building entrance without looking like a surveillance device.

The Shelly Plug Plus S turned out to be the perfect candidate for this. It’s one of those smart plugs that Shelly makes for home automation, but what makes it special for hackers is that Shelly deliberately makes their devices reflashable. They add programming headers, don’t lock down the bootloader, and even document the pinouts. It’s like they want us to hack their products (and honestly, that’s why I keep buying them). The Plus S model uses an ESP32, giving plenty of power for BLE operations while still fitting in a compact plug form factor that wouldn’t raise eyebrows in the building entrance.

shelly
Image source: Shelly Online Store

After cracking it open and finding the clearly labeled programming headers, flashing it with custom firmware was straightforward. Five minutes later, I had a fully functional smart plug running ESPHome.

The Software
#

The ESPHome configuration is fairly short because the esp32_ble_key section hides most of the complexity. That custom C++ component implements the time-based challenge-response authentication.

esphome:
  name: bouncer
  platform: ESP32
logger:
api:
  password: <API_PASSWORD>
wifi:
  ssid: <WIFI_SSID>
  password: <WIFI_PASSWORD>
  fast_connect: true

time:
  - platform: homeassistant
    id: homeassistant_time

external_components:
  - source: my_components

esp32_ble_key:
  authorizer: key_detected
  time_source: homeassistant_time
  secret: <SHARED_SECRET>

binary_sensor:
  - platform: template
    name: "Key Detected"
    id: key_detected
    device_class: presence
    filters:
      - delayed_off: 20s

Conveniently, the Shelly Plug barely reached my home WiFi from the entrance. Being permanently installed there made the connection reliable enough to consistently report back to Home Assistant.

Not shown here is the additional logic to keep the smart plug working as a power outlet, which is trivial in ESPHome and a nice bonus feature. We don’t want to lose that functionality ;)

The esp32_ble_key Custom Component
#

ESPHome has a neat feature where you can write your own components in C++ when the built-in ones don’t cut it. These live in a my_components folder and extend ESPHome’s functionality with whatever crazy protocol or hardware you need to support. Think of them as plugins that integrate seamlessly with the YAML configuration - you get all the benefits of ESPHome (OTA updates, Home Assistant integration, logging) while having the full power (and complexity) of C++.

Every second, the component generates a new challenge by hashing the current timestamp with a salt, then broadcasts it through a BLE characteristic. The phone hex-encodes that challenge, appends the shared secret, hashes the resulting string with SHA-256, and writes back the first 22 bytes. The ESP checks that response and triggers the door:

void ESP32BLEKeyComponent::process_incoming_data_(
  const std::vector<uint8_t> &data
) {
  // Calculate expected: SHA256(challenge + secret)
  std::string challenge_plus_secret = 
    format_hex(this->challenge_) + this->secret_;
  SHA256 sha;
  sha.update(challenge_plus_secret);
  auto expected_hash = sha.digest();
  
  // Compare the 22-byte prefix sent by the phone
  if (data == expected_hash_vec) {
    // Valid response - trigger door opener!
    this->authorizer_->publish_state(true);
    this->authorizer_->publish_state(false);
    return;
  }
  
  // Also check previous challenge for time drift...
}

The challenge rotates every second, so intercepted responses become useless almost immediately. To account for processing delay, the component also accepts responses derived from challenges up to 3 seconds old.

Security Considerations
#

If you are familiar with secure ranging, you might suspect this isn’t bulletproof. And you’d be right.

The obvious vulnerability here is a relay attack. Someone could intercept the BLE challenge-response authentication, relay it to an accomplice near my phone, get the valid response, and relay it back to open the door. The timeout window doesn’t help much when radio waves move at the speed of light.

For truly secure authentication, you’d need UWB (Ultra-Wideband) secure ranging that measures time-of-flight - basically using physics to prove the phone is actually there, not being relayed. Many vendors do this with their car keys, measuring the exact nanoseconds it takes for radio waves to bounce back, which can’t be faked or relayed, at least if you obey the laws of special relativity. It’s elegant, secure - and completely overkill for my use case.

Let’s be real here. This is rural Austria. My neighbors literally keep their doors unlocked. The elderly couple next door hasn’t locked their door since 1987. Someone sophisticated enough to perform a BLE relay attack would probably just… follow another resident in. The threat model isn’t sophisticated attackers - it’s “I don’t want to fish for keys while carrying groceries.”

Safe enough.

The iOS App
#

The most constrained part of the whole system was iOS. Background Bluetooth in iOS Shortcuts is… quirky. You get 30 seconds of runtime maximum, no UI, limited API access, and if you do anything Apple doesn’t like, your shortcut silently fails.

I first tried to implement it as an embedded script in one of the many JavaScript automation apps, such as Scriptable. Those apps had their own quirks, and getting BLE to work reliably was a nightmare. After several failed attempts, I built a custom iOS app for the BLE and authentication logic, then invoked it from a Shortcut that supplied the shared secret.

When triggered, the app:

  1. Scans for BLE devices with the specific service UUID
  2. Connects to the first one found
  3. Reads the challenge characteristic
  4. Computes the response
  5. Writes it back
  6. Disconnects
  7. Returns success/failure

In Swift, the core BLE logic looks like this:

class BLEClient: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, @unchecked Sendable {
    
    let targetServiceUUID = CBUUID(string: "<SERVICE-UUID>")
    let readCharacteristicUUID = CBUUID(string: "<REQUEST-UUID>")
    let writeCharacteristicUUID = CBUUID(string: "<RESPONSE-UUID>")

    var secret: String = <INJECTED-VIA-SHORTCUT>
    var targetPeripheral: CBPeripheral?

    ...

    @MainActor func scanForPeripherals() {
        if centralManager.state == .poweredOn {
            centralManager.scanForPeripherals(withServices: [targetServiceUUID], options: nil)
        ...
    }

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        Task { @MainActor in
            targetPeripheral = peripheral
            centralManager.stopScan()
            centralManager.connect(peripheral, options: nil)
        }
    }

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        peripheral.delegate = self
        peripheral.discoverServices([targetServiceUUID])
    }

    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard let services = peripheral.services else { return }
        for service in services where service.uuid == targetServiceUUID {
            peripheral.discoverCharacteristics([readCharacteristicUUID, writeCharacteristicUUID], for: service)
        }
    }

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        guard let characteristics = service.characteristics else { return }
        for characteristic in characteristics {
            if characteristic.uuid == readCharacteristicUUID {
                peripheral.readValue(for: characteristic)
            }
        }
    }

    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        if let value = characteristic.value, !value.isEmpty {
            let dataString = value.map { String(format: "%02hhx", $0) }.joined()
            let combinedString = dataString + secret
            if let hash = sha256(data: combinedString) {
                let first22Bytes = hash.prefix(22)
                guard let peripheral = targetPeripheral,
                      let service = peripheral.services?.first(where: { $0.uuid == targetServiceUUID }),
                      let characteristic = service.characteristics?.first(where: { $0.uuid == writeCharacteristicUUID }) else { return }
                peripheral.writeValue(first22Bytes, for: characteristic, type: .withResponse)
            }
            ...
        }
    }
}

To avoid battery drain, the Shortcut only invokes the app when disconnecting from CarPlay around my (coarse) home location, or when entering a (coarse) geofence around my building. To bypass the 30-second limit, I run the Shortcut multiple times in succession until it succeeds. Six attempts give me three minutes of runtime, which is more than enough time to get from my car to the door.

Putting everything together
#

Installation was simple:

  1. Install the intercom controller: Hidden behind the couch
  2. Deploy the BLE sensor: Plug the reflashed Shelly into an outlet in the building entrance
  3. Configure Home Assistant: One simple automation:
    - alias: "Open door when key detected"
      trigger:
        - platform: state
          entity_id: bouncer.key_detected
          to: 'on'
      action:
        - service: switch.turn_on
          entity_id: intercom.door_opener
    
  4. Set up the iOS Shortcut: Add the triggers and test it

I tried it out, and it worked perfectly. I could walk up to the building, and the door would unlock itself.

Living with Magic Doors
#

It’s been a year since installation, and I can report that having your door open automatically when you approach never gets old.

Visitors are consistently amazed. “Did you just open that with your phone?” Well, yes, but also no. It’s more like the door recognized me and opened itself.

Before I got used to it, sometimes even I got startled by the unexpected buzz of the door unlocking.

Future, this is.

Every time I walk up to my building, hands full of groceries, and hear that satisfying bzzt as the door unlocks itself - that’s when I remember why I love doing this.


This is Part 3 of my “Stories from the Open Source Smart Home” series. My landlord knows about it and thinks it’s black magic. The shared secret was rotated regularly (okay, I’ve been meaning to rotate it for over six months). The system was cleanly removed when I moved out, leaving no trace except for one weird cable behind where the couch used to be.

Emanuel Mairoll
Author
Emanuel Mairoll
Forward & Reverse Engineer
The Open Source Smart Home - This article is part of a series.
Part 3: This Article