argilo/secplus

Weird "Security+ 2.0®" signal

zellyn opened this issue · 16 comments

I'm getting a signal from my gate opener remote that doesn't decode properly. I'm planning on trying to figure it out myself, but thought you might be interested.

I have an 811LM remote control, which claims to be Security+ 2.0®. The gate opener is an LA412, which also claims to be Security+ 2.0®.

I can't figure out how to run secplus properly on my Mac (brew install gnuradio worked, /usr/local/Cellar/gnuradio/3.9.2.0_2/libexec/venv/bin/activate gets me most of the way, but I still get Symbol not found: __ZN13QwtPlotPicker7setAxisEii), but I took some captures with rtl_sdr, and decoded them with inspectrum.

They're weird. First, they're far longer than the codes you describe. Second, secplus thinks the bursts are in opposite order (second burst first). Third, secplus doesn't like decoding them.

I added print debugging to secplus_v2_decode and tried calling it directly:

    def process_buffer(self):
        manchester = "".join(str(b) for b in self.buffer)
        start = manchester.find("1010101010101010101010101010101001010101")
        if start == -1:
            return
        manchester = manchester[start:start+124]
        baseband = []
        for i in range(0, len(manchester), 2):
            if manchester[i:i+2] == "01":
                baseband.append(1)
            elif manchester[i:i+2] == "10":
                baseband.append(0)
            else:
                return
    
        if baseband[21] == 0:
            print('first part')
            self.pair = baseband[22:]
        elif baseband[21] == 1 and len(self.pair) == 40:
            print('second part')
            self.pair += baseband[22:]
        else:
            print('weird')

        if len(self.pair) == 80 and self.pair != self.last_pair:
            try:
                rolling, fixed = secplus.decode_v2(self.pair)
                print(secplus.pretty_v2(rolling, fixed))
                self.last_pair = self.pair
            except ValueError as e:
                print(e)
                pass


if __name__ == '__main__':
    c1 = [
        '1010101010101010101010101010101001010101100110011010011010101001010110100110100110010110100101100110100101010110100110100101010110010110100101010110100110100110100110100101',
        '1010101010101010101010101010101001010101101010011010100110101010011001011001011010101001011001100110101001010101100110010101011010011010011010101010011010010110011001011001',
    ]
    c2 = [
        '1010101010101010101010101010101001010101100110011010101001100110101001011001011001100101010101011001011001100101011001011001101001101001011001101001010101011001010101011001',
        '1010101010101010101010101010101001010101101010010110100101100110010110100110011010100110010110101001010110010101011001100101101010011010011010101010101010011001100110010110',
    ]
    c3 = [
        '1010101010101010101010101010101001010101100110011010101010101010100110010110011010100110011010010110011010100110010110011010100110100110010110100110010110010110010110010110',
        '1010101010101010101010101010101001010101101010010110100101101001100101100101101001100101100101011010100101100110101010010110011001101001101001011001011001101010100101010101',
    ]

    b = blk()
    for capture in [c1, c2, c3]:
        for burst in capture:
            b.buffer = burst
            b.process_buffer()

Here's the output:

$ python secplus_v2_decode.py 
weird
first part
second part
First two bits of packet were not zero
first part
second part
First two bits of packet were not zero
first part

If it helps, I've attached three captures. I got all eight bursts from 1 and 3, but only 6 from 2. Even and odd bursts decode identically.

captures.zip

I've been messing with this and my pin pad on my garage and have noticed the "first two bits of packet were not zero" problem too. From what I can tell from just messing with my device, if the first two bits are 01 then the payload is longer with a small change to the rolling code where it is repeated in part.

I've uploaded the file with some of my changes but I'm gonna be honest, I haven't been able to fully wrap my mind around the original script so YMMV. It does spit out the pin on my pin pad as well as show the rolling and fixed codes when the regular button is pressed so it does have that going for it. I was going to test it with some other devices before I made a pull request but since you are working on it too, here is what I got:

secplus.txt

I realized you might need the secplus_v2_decode.py file also so I uploaded both files here:

https://github.com/acoursen/secplus/blob/master/secplus_v2_decode.py
https://github.com/acoursen/secplus/blob/master/secplus.py

Your code is letting the data get past. However, it's still treating the first burst as the second, and the second burst as the first on my opener. Also, it's returning a fixed code of 2678524885645032123651, which is 71 bits… way too long. I believe the fixed code should be 40 bits or fewer.

My opener has 12 dip switches: I have no idea how they map to the fixed code, but I don't see the bit pattern (or the inverted bit pattern) in the fixed code (or in the fixed code I get by switching packet order).

The fact that you're getting the pin on your pin pad though… means you're doing something right! 🙂

Hmmm. I see, looking at your code, that you have up to 72 fixed bits. Interesting… I wonder how the dip switches map to the fixed code.

Also, I can't get the rolling code to come out sequential, using either order of bursts 😞

That's interesting ... it does look like it is sending part two first and then part one like you said. If you allow either to be sent first it actually sends valid looking codes:

Security+ 2.0: rolling=240129675 fixed=4616223061045564932096 (id1=1000 id2=3508 id3=580 id4=0 pin=1019)
Security+ 2.0: rolling=240129676 fixed=4616223061045564932096 (id1=1000 id2=3508 id3=580 id4=0 pin=1019)
Security+ 2.0: rolling=240129677 fixed=4616223061045564932096 (id1=1000 id2=3508 id3=580 id4=0 pin=1019)

temp.txt

Now that is fascinating! I wonder how the opener knows the difference between an out-of-order pair of bursts and a situation where it caught the end of one and the start of another? Perhaps it just always decodes the halves independently, and opens the gate any time it reaches a valid fixed and rolling code? Or perhaps there's some kind of checksum.

The other interesting thing is that I cannot find the binary pattern of the DIP switches in the binary representation of the fixed code.

DIPs: 001111111011 (or 110000000100)
Out-of-order fixed code: 111110100011111011110110110100000011100100010011010000000000000000000000

Perhaps @argilo knows how they usually map? Perhaps when Chamberlain went from 9 to 12 DIP switches they shuffled things around?

It looks like the dip switches are just the binary representation of the pin (1019). If you look at my _fixed_pretty_v2 function i took a stab at decoding the fixed part but the best I came up with was four possibly static components seperated by three parts that change with the pin. The pin looks like it is split and reversed across d1 and d2 and d3 is a sort of checksum. I will say I don't think the "static" parts are truly static though because i have seen them change when I just press the enter button with no numbers.

I would be curious too how the other security+ pretty print works with all the base 3 stuff (and through out the rest of the protocol for that matter). I've only ever worked with base 2 stuff and very lightly at that.

Ah, lol. I didn't even think to look at the PIN 😂. Nice catch. I've been trying to read the patent, but it's pretty impenetrable…

Have you looked at the v1 pretty printer? It does some fairly complex decoding of the PIN, and even of the suffix (# or *). Might be similar?

@argilo and @acoursen I also find it pretty suspicious that your examples in the test file have a rolling code of 240124710-ish and mine have a rolling code of 240129675-ish.

Subtracting 240123904 (0b11100101<<20) gives 806 for yours, and 5771 for mine, which seem far more reasonable. (We've been in our house for about three years, and probably open/close the gate 2 or 3 times a day: 3 * 365 * 4 = 4380.)

Hahaha. I think my gate cares only about the PIN! 😂 It opens on repeat signals!

Interesting! When I added support for Security+ 2.0, I only had one transmitter to test with, so I didn't encounter the longer packets.

@acoursen If you're interested in contributing your changes, you're welcome to open a pull request. You'll need to update the test suite though, since several of the existing tests now fail:

$ ./test_secplus.py 
......EF........F.....
======================================================================
ERROR: test_decode_v2_robustness (__main__.TestSecplus)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "./test_secplus.py", line 274, in test_decode_v2_robustness
    rolling, fixed = secplus.decode_v2(random_code)
  File "/home/argilo/git/secplus/secplus.py", line 130, in decode_v2
    rolling2, fixed2 = _decode_v2_half(code[64:64+64])
  File "/home/argilo/git/secplus/secplus.py", line 106, in _decode_v2_half
    rolling.append((parts[2][i] << 1) | parts[2][i+1])
IndexError: list index out of range

======================================================================
FAIL: test_decode_v2_zero_bits (__main__.TestSecplus)
----------------------------------------------------------------------
ValueError: First two bits of packet were not 00 or 01: [1, 0, 0, 0, 0, 0]

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "./test_secplus.py", line 236, in test_decode_v2_zero_bits
    secplus.decode_v2(broken_code)
AssertionError: "First two bits of packet were not zero" does not match "First two bits of packet were not 00 or 01: [1, 0, 0, 0, 0, 0]"

======================================================================
FAIL: test_encode_v2_fixed_limit (__main__.TestSecplus)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "./test_secplus.py", line 200, in test_encode_v2_fixed_limit
    secplus.encode_v2(rolling, fixed)
AssertionError: ValueError not raised

----------------------------------------------------------------------
Ran 22 tests in 1.730s

FAILED (failures=2, errors=1)

You can run the test suite locally by executing ./test_secplus.py. I've also added a GitHub Actions workflow that runs the tests on pull requests and commits.

I now have a Security+ 2.0 PIN pad, which uses payload type 01 (which is a "supplemental data payload", according to https://patents.google.com/patent/US8422667B2/en). Building on @acoursen's work, I've added proper support for encoding & decoding packets with supplemental data. I don't yet know what all the bits in the supplemental data packet mean, but I've added elementary pretty-printing for PIN values.

The wireline protocol (see #5) also includes a supplemental data payload.

I've improved pretty-printing of PIN values, and also added an extra parameter (--data) to allow supplemental data payloads to be transmitted. With that, I now consider this issue resolved so I'll close it off.