diff --git a/decoders/keeloq/README.md b/decoders/keeloq/README.md new file mode 100755 index 00000000..44cd449a --- /dev/null +++ b/decoders/keeloq/README.md @@ -0,0 +1,101 @@ +# KeeLoq decoder for PulseView +This decoder has been developed for [PulseView](https://sigrok.org/wiki/PulseView) using [Sigrok API](http://sigrok.org/wiki/Protocol_decoder_API) . +The following output has been obtained connecting a Logic Analizer to the PWM pin of an IC implementing the KeyLoq algorithm. + +A Code Word +![KeeLoq output](keeloq1_pv.png) +Detail +![KeeLoq output](keeloq2_pv.png) + +## KeeLoq in a nutshell +KeeLoq is proprietary block cypher widely used in industry as secure Remote Keyless Entry (RKE) systems. +It has been designed to be easily implemented at hardware level in Integrated Circuit for secure wireless transmissions. Typical applications for KeeLoq are remote controllers for gates, car immobilizers and anti-theft systems. Each remote control, known as an encoder, relies on the internal IC to transmit a command to the receiver using _Keeloq_ as a cryptographic algorithm. After information has been deciphered on the receiver side, it reacts to the command by opening a door or unlocking a system. +*** +## Code Word Structure +KeeLoq uses PWM (_Pulse Width Modulation_) to encode logical bits. In particular the documentation +states the TE as the _'Basic Pulse Element'_ and it is considered the minimum information visibile within a _'Code Word'_. + +Each transmission is organized in a logical structure named _'Code Word'_ described below : +### Preamble + Header + +----------+--------+---------------+ + | Preamble | Header | Data Portion | + +----------+--------+---------------+ + 0 TE 23 0 TE 10 0 Bits 65 + +- **Preamble** = 23 TE with 50% Duty Cycle. +- **Header** = 10 TE at Low level. + +### Data Portion + + Bits: + 0 31 32 59 60 63 64 65 + +-------------------+---------------+----+----+----+----+-------+-----+ + | Encrypted Portion | Serial Number | Button Code | Status | + +-------------------+---------------*----+----+----+----+-------+-----+ + | S3 | S0 | S1 | S2 | V-Low | RPT | + *----+----+----+----+-------+-----+ + LSB MSB +Please note that **LSB** is transmitted **first** . This means that Encrypted Portion is sent out before any other parts. + ++ **Encrypted Portion** : This data changes everytime a button is pressed according to the encryption algorithm. ++ **Serial Number** : Unique value present in each encoder. ++ **Button Code** : Button pressed. ++ **V-Low** : Indicates encoder battery voltage : High/Low. ++ **RPT** : Reports if the button is kept pressed. + + +*** + +## What this decoder does: +- It is able to recognise a sequence of _'Code Words'_ genederated by an econder that uses KeeLoq encryption algorithm. +- It decodes the PWM signals into a human readble format for each _'Code Word'_. +- it shows data in the same format as described in the official documentation. +- It shows '_Encrypted Portion_' just as transmitted by the encoder. + +## What this decoder does NOT do: +- Decrypt the '_Encrytped Portion_' +- Extract the Master encryption key. + +## Installation +Depending of your Operating System create a new directory named keeloq + +**Windows**: %LOCALAPPDATA%\libsigrokdecode\decoders\\**keeloq** + +**Linux**: ~/.local/share/libsigrokdecode/decoders/**keeloq** + + $ git clone https://github.com/rzondr/KeeLoq_decoder.git + $ cd KeeLoq_decoder + $ cp __init__.py pd.py + +- Run Pulseview. +- Add decoder selecting KeeLoq. +- Click on the decoder and select the acquisition channel. +- Run signal acquisition. +- Enjoy the reading. + +Please note that it is present also a sample file named **hcs300_sample.sr** for your convenience. + +## Tested Integrated circuits +At the writing time here the ICs succesfully tested with this decorder +- **HCS300** [Datasheet](https://ww1.microchip.com/downloads/aemDocuments/documents/MCU08/ProductDocuments/DataSheets/21137G.pdf) +- **HCS301** [Datasheet](https://ww1.microchip.com/downloads/en/devicedoc/21143b.pdf) + +Looking forward to seeing this list longer. + +### How to contribute +I aim to write a KeeLoq decoder able to recognise _'Code Words'_ generated by as much as possible encoders. +If you want to contribute to this, please send an email to : as follows: +- Subject : KeeLoq Encoder - IC name - (ex. hcs300) +- Body : Whatever you think is worth adding about the IC and the application it is used for'. +- Att'achment : A PulseView sample containing a good number of _Code Words_ in **.sr** format. +*** +# Change Log: +- Version 0.2 - [29th September 2024] + * TEs timing review + * Bug-Fix regarding LSB-MSB interpretation for the Encrypted Portion and Fixed Part + * Minor code changes + * IC: HCS301 Succesfully tested (Contribution Tobias Rothfelder) + +- Version 0.1 - [09th May 2024]: + * Recognise a sequence of _Code Words_ genederated by an econder and shows their content in human readble format. + * IC: HCS300 Succesfully tested diff --git a/decoders/keeloq/__init__.py b/decoders/keeloq/__init__.py new file mode 100755 index 00000000..8b1d188d --- /dev/null +++ b/decoders/keeloq/__init__.py @@ -0,0 +1,31 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2012 Uwe Hermann +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, see . +## + +''' +KeeLoq is a block-cypher which is widely used as an industry standards in +in secure wireless Keyless Entry (RKE) systems such as anti-theft, gates and car immobilizers. +Transmitter or encoder sends commands to the receiver using a sequence of PWM bits organized +in structured known as 'Code Word'. This decoder inteprets the bits and shows the 'Code Word' in +a human readable form. + +A typical KeeLoq implementation is for IC HCS300 : +https://ww1.microchip.com/downloads/aemDocuments/documents/MCU08/ProductDocuments/DataSheets/21137G.pdf +''' + +from .pd import Decoder diff --git a/decoders/keeloq/hcs300_sample.sr b/decoders/keeloq/hcs300_sample.sr new file mode 100755 index 00000000..263658d6 Binary files /dev/null and b/decoders/keeloq/hcs300_sample.sr differ diff --git a/decoders/keeloq/keeloq1_pv.png b/decoders/keeloq/keeloq1_pv.png new file mode 100755 index 00000000..9008ea07 Binary files /dev/null and b/decoders/keeloq/keeloq1_pv.png differ diff --git a/decoders/keeloq/keeloq2_pv.png b/decoders/keeloq/keeloq2_pv.png new file mode 100755 index 00000000..dfbc1c3a Binary files /dev/null and b/decoders/keeloq/keeloq2_pv.png differ diff --git a/decoders/keeloq/pd.py b/decoders/keeloq/pd.py new file mode 100755 index 00000000..b6b6c7e2 --- /dev/null +++ b/decoders/keeloq/pd.py @@ -0,0 +1,273 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2024 Andrea Orazi +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, see . +## + + +import sigrokdecode as srd + +class Ann: + TE, LOGICAL_BIT, CODE_WORD, ENCRYP_DATA, FIXED_DATA = range(5) + +class Decoder(srd.Decoder): + api_version = 3 + id = 'keyloq' + name = 'KeyLoq' + longname = 'KeeLoq CodeWord Decoder' + desc = 'Keeloq CodeWord Decoder for Pulseview' + license = 'gplv2+' + inputs = ['logic'] + outputs = [] + tags = ['Security/crypto'] + channels = ( + {'id': 'pwm', 'name': 'PWM', 'desc': 'Code Word Channel'}, + ) + options = ( + + ) + + annotations = ( + ('te', 'TE'), + ('logical_bit', 'Logical Bit'), + ('Code_Word', 'Code Word'), + ('encryp_data', 'Encrypted Data'), + ('fixed_data', 'Fixed Data'), + ) + annotation_rows = ( + ('bits', 'Bits', (Ann.TE,Ann.LOGICAL_BIT)), + ('code word', 'Code Word', (Ann.CODE_WORD, )), + ('data', 'Data', (Ann.ENCRYP_DATA, Ann.FIXED_DATA )), + ) + + def __init__(self): + self.reset() + + def reset(self): + self.samplerate = None + self.TEcnt = 0 #TE counter + self.Block_Init = 0 #Flag for each block of info + #TE Timing - According to documentation a TE is typically 400 usecs + #[0][1] - TE/Logical Bit 1 | [2][3] - Logical Bit 0 | [4][5] - Header Lenght + self.TE_Timing = [ 280e-6, 580e-6, 700e-6, 1000e-6, 3e-3, 6e-3 ] + self.ssBlock = 0 #Sample number of a block of intormation + self.Header_Completed = 0 #[ 0 = Not Complete 1 = Complete] + self.n = 0 # Current Sample number + self.prevN = 0 # Previous sample number + self.Bitcnt = 0 # Bit counter in Data Portion + self.trig_cond = '' #Wait - trigger condition + self.BitString = '' # A string of Logical Bit Code Word + self.KeyLoq = { "Encrypted" : "", "Serial-Number" : "", "S3" : "", "S0" : "", "S1" : "", "S2" : "", + "V-Low" : "", "RPT" : ""} #KeyLoq Code Word + + def start(self): + self.out_ann = self.register(srd.OUTPUT_ANN) + + def metadata(self, key, value): + if key == srd.SRD_CONF_SAMPLERATE: + self.samplerate = value + + #Define the beginning of each useful block of information saving the Sample Number + def Start_Block(self): + if (self.Block_Init == 0): + self.Block_Init = 1 + self.ssBlock = self.prevN + + #Shows Preable + Header + def Decode_Preable(self,t): + #According to documentation a TE is typically 400 usecs + if ((t >= self.TE_Timing[0] and t <= self.TE_Timing[1] )): + if (self.TEcnt < 23): + self.put(self.prevN, self.samplenum, self.out_ann, [Ann.TE, [str(self.TEcnt)]]) + self.Start_Block() + + #Shows Preamble + elif (self.TEcnt == 23): + self.put(self.ssBlock, self.n, self.out_ann, [Ann.CODE_WORD, ['Preamble']]) + self.put(self.prevN, self.samplenum, self.out_ann, [0, [str(self.TEcnt)]]) + + # We have reached the last Rising Edge on TEcnt == 24 + # This is not technically a TE . It is actually a period of 10 TEs known as Header + # Header has usually a lenth of 3-6 msec + elif ( (t >= self.TE_Timing[4] and t <= self.TE_Timing[5] ) and (self.TEcnt == 24 ) ): + self.put(self.prevN, self.n, self.out_ann, [Ann.CODE_WORD, ['Header']]) + self.TEcnt = 0 + self.Block_Init = 0 + self.Header_Completed = 1 #Is 1 when the decoder successfully reached the end of Preable + Header + + else: #Reset Counters because this is not a TE + self.TEcnt = 0 + self.Block_Init = 0 + + #In Data Portion bits are encoded using PWM technique + def Decode_LogicalBit(self,t): + # Logic Bit 0 = 2 TE at High Level + 1 TE at low level >> Tipically 800 usec (H) + 400 usec (L) << + # Logic Bit 1 = 1 TE at High Level + 2 TE at Low Level >> Tipically 400 usec (H) + 800 usec (L) << + # Thus, To recognise a logical bit we have to read two subsequent Edges (or HalfBits) + + LogicalBit = '' #Either '0' or '1' encoded using PWM + Valid_Bit = 0 + + #Check timing validity first + if (( t >= self.TE_Timing[0] and t <= self.TE_Timing[1] ) or (t >= self.TE_Timing[2] and t <= self.TE_Timing[3] ) ): + Valid_Bit = 1 + + #Having got the next edge pointer n = first half of the Logical bit + if (Valid_Bit): + self.Start_Block() + + #Gets the the next second half of the Logical bit to fully decode it + if (self.Bitcnt == 65): #Last bit needs special care as definitely there is no valid edge nearby + self.n = self.samplenum + 8 #Arbitrary value after last sample# to complete this Logical Bit + else: + self.wait(self.trig_cond) + self.n = self.samplenum + + #After time validity, it check wether it is '1' or '0' + if ( t >= self.TE_Timing[0] and t <= self.TE_Timing[1] ) : + LogicalBit = '1' + else: + LogicalBit = '0' + + self.put(self.prevN, self.n, self.out_ann, [Ann.LOGICAL_BIT, ['Bit ' + LogicalBit ]]) + return (LogicalBit) + + else : #In case of Invalid bit, Reset all counters and conditions + self.put(self.prevN, self.n, self.out_ann, [Ann.LOGICAL_BIT, ['>>> Invalid Bit <<< ' + LogicalBit ]]) + self.Reset_DP_Cnts() + self.Header_Completed = 0 + self.Bitcnt = -1 #Will be set to 0 in the Main Cycle + return("0") + + + #Reset Data Portion counters at the end of each decoded block + def Reset_DP_Cnts(self): + self.Block_Init = 0 + self.BitString ='' + + #Convert a Binary string into the equivalent value in Hex 0x in string format + def Bin2Hex (self): + decimal_value = int(self.BitString, 2) + # Convert integer to hexadecimal with leading zeroes + hex_value = "0x{0:0{1}X}".format(decimal_value,7) + return ( hex_value ) + + #Decode all Logical PWM Bit from 0 to 65 completing the CodeWord + def Decode_DataPortion(self, t): + # Bits 0-31 : Encrypted Portion. It comes from the algorithm. + if (self.Bitcnt <= 31 ): + # According to the documentation LSB is transmitted first. + # However, decoding a bit string to Hex, requires that LSB must be the last bit in the string sequence + # to preserve bit significance. + # That's why --self.BitString-- is appended always as LSB, whereas the last transmitted bit + # is considered as MSB. This ensures the right interpretation and conversion to Hex + # for both Encrypted and Fixed portion + self.BitString = self.Decode_LogicalBit(t) + self.BitString + + if (self.Bitcnt == 31): + self.put(self.ssBlock, self.n, self.out_ann, [Ann.CODE_WORD, ['Encrypted Portion']]) + self.KeyLoq["Encrypted"] = self.Bin2Hex () + self.put(self.ssBlock, self.n, self.out_ann, [Ann.ENCRYP_DATA, [ self.KeyLoq["Encrypted"] ]]) + self.Reset_DP_Cnts() + + # Here begins the cleartext portion known as Fixed Portion + # it is made by Serial Number + Button Code + Status (V-Low + Repeat\) + elif (self.Bitcnt >= 32 and self.Bitcnt <= 59 ): # Bits 32-59 : Serial Number + self.BitString = self.Decode_LogicalBit(t) + self.BitString + + if (self.Bitcnt == 59): + self.put(self.ssBlock, self.n, self.out_ann, [Ann.CODE_WORD, ['Serial Number']]) + self.KeyLoq["Serial-Number"] = self.Bin2Hex () + self.put(self.ssBlock, self.n, self.out_ann, [Ann.FIXED_DATA, [ self.KeyLoq["Serial-Number"] ]]) + self.Reset_DP_Cnts() + + elif (self.Bitcnt >= 60 and self.Bitcnt <= 63 ): # Bits 60-63 : Button Code + LogicalBit = self.Decode_LogicalBit(t) + + if ( self.Bitcnt == 60 ): + self.KeyLoq["S3"] = LogicalBit + self.put(self.prevN, self.n, self.out_ann, [Ann.FIXED_DATA, [ 'S3 = ' + self.KeyLoq["S3"] ]]) + elif ( self.Bitcnt == 61 ): + self.KeyLoq["S0"] = LogicalBit + self.put(self.prevN, self.n, self.out_ann, [Ann.FIXED_DATA, [ 'S0 = ' + self.KeyLoq["S0"] ]]) + elif ( self.Bitcnt == 62 ): + self.KeyLoq["S1"] = LogicalBit + self.put(self.prevN, self.n, self.out_ann, [Ann.FIXED_DATA, [ 'S1 = ' + self.KeyLoq["S1"] ]]) + elif (self.Bitcnt == 63): + self.KeyLoq["S2"] = LogicalBit + self.put(self.ssBlock, self.n, self.out_ann, [Ann.CODE_WORD, ['Button Code']]) + self.put(self.prevN, self.n, self.out_ann, [Ann.FIXED_DATA, [ 'S2 = ' + self.KeyLoq["S2"] ]]) + self.Reset_DP_Cnts() + + #Status + elif (self.Bitcnt == 64 ): # Bits 64 : V-Low + LogicalBit = self.Decode_LogicalBit(t) + + self.put(self.ssBlock, self.n, self.out_ann, [Ann.CODE_WORD, ['V-Low']]) + if (LogicalBit == '0'): + self.KeyLoq["V-Low"] = "Battery High" + else: + self.KeyLoq["V-Low"] = "Battery Low" + + self.put(self.prevN, self.n, self.out_ann, [Ann.FIXED_DATA, [ self.KeyLoq["V-Low"] ]]) + self.Reset_DP_Cnts() + + elif (self.Bitcnt == 65 ): # Bits 65 : Repeat + LogicalBit = self.Decode_LogicalBit(t) + + self.put(self.ssBlock, self.n, self.out_ann, [Ann.CODE_WORD, ['RPT']]) + if (LogicalBit == '0'): + self.KeyLoq["RPT"] = "No" + else: + self.KeyLoq["RPT"] = "Yes" + + self.put(self.prevN, self.n, self.out_ann, [Ann.FIXED_DATA, [ self.KeyLoq["RPT"] ]]) + self.Reset_DP_Cnts() + self.Header_Completed = 0 #Looks for another new CodeWord + self.Bitcnt = -1 #To start from 0 in the Main - decode() it needs to be negative + + # Main Loop + def decode(self): + if self.samplerate is None: + raise Exception('Cannot decode without samplerate.') + + t = 0 #Time between two edges + + #Each CodeWord begins with a Rising Edge. + self.trig_cond = [{0: 'r'}] # Go and look for it + self.wait(self.trig_cond) + self.prevN = self.samplenum + + self.trig_cond = [{0: 'e'}] # Go to the next Edge + + while True: + + self.wait(self.trig_cond) + self.n = self.samplenum + + #Get time (usec) between the current and the previous sample + t = (self.n - self.prevN) / self.samplerate + + #CodeWord decoding subfunctions + if (self.Header_Completed == 0): + self.TEcnt += 1 + self.Decode_Preable(t) + else: + self.Decode_DataPortion (t) + self.Bitcnt += 1 + + #Ready for the next cycle + self.prevN = self.samplenum