Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setup_payload: Add support for parsing setup payloads in python impl #32516

Merged
merged 6 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ jobs:
scripts/run_in_build_env.sh 'virtualenv pyenv'
source pyenv/bin/activate
pip3 install -r src/setup_payload/python/requirements.txt
python3 src/setup_payload/tests/run_python_setup_payload_gen_test.py out/chip-tool
python3 src/setup_payload/tests/run_python_setup_payload_test.py out/chip-tool

build_linux_python_lighting_device:
name: Build on Linux (python lighting-app)
Expand Down
4 changes: 2 additions & 2 deletions scripts/tools/bouffalolab/factory_qrcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@

try:
import qrcode
from generate_setup_payload import CommissioningFlow, SetupPayload
from SetupPayload import CommissioningFlow, SetupPayload
except ImportError:
SDK_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
sys.path.append(os.path.join(SDK_ROOT, "src/setup_payload/python"))
try:
import qrcode
from generate_setup_payload import CommissioningFlow, SetupPayload
from SetupPayload import CommissioningFlow, SetupPayload
except ModuleNotFoundError or ImportError:
no_onboarding_modules = True
else:
Expand Down
2 changes: 1 addition & 1 deletion scripts/tools/generate_esp32_chip_factory_bin.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
sys.path.insert(0, os.path.join(CHIP_TOPDIR, 'scripts', 'tools', 'spake2p'))
from spake2p import generate_verifier # noqa: E402 isort:skip
sys.path.insert(0, os.path.join(CHIP_TOPDIR, 'src', 'setup_payload', 'python'))
from generate_setup_payload import CommissioningFlow, SetupPayload # noqa: E402 isort:skip
from SetupPayload import CommissioningFlow, SetupPayload # noqa: E402 isort:skip

if os.getenv('IDF_PATH'):
sys.path.insert(0, os.path.join(os.getenv('IDF_PATH'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@

try:
import qrcode
from generate_setup_payload import CommissioningFlow, SetupPayload
from SetupPayload import CommissioningFlow, SetupPayload
except ImportError:
SDK_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
sys.path.append(os.path.join(SDK_ROOT, "src/setup_payload/python"))
try:
import qrcode
from generate_setup_payload import CommissioningFlow, SetupPayload
from SetupPayload import CommissioningFlow, SetupPayload
except ModuleNotFoundError or ImportError:
no_onboarding_modules = True
else:
Expand Down
23 changes: 23 additions & 0 deletions src/setup_payload/python/Base38.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
RADIX = len(CODES)
BASE38_CHARS_NEEDED_IN_CHUNK = [2, 4, 5]
MAX_BYTES_IN_CHUNK = 3
MAX_ENCODED_BYTES_IN_CHUNK = 5


def encode(bytes):
Expand All @@ -47,3 +48,25 @@ def encode(bytes):
base38_chars_needed -= 1

return qrcode


def decode(qrcode):
total_chars = len(qrcode)
decoded_bytes = bytearray()

for i in range(0, total_chars, MAX_ENCODED_BYTES_IN_CHUNK):
if (i + MAX_ENCODED_BYTES_IN_CHUNK) > total_chars:
chars_in_chunk = total_chars - i
else:
chars_in_chunk = MAX_ENCODED_BYTES_IN_CHUNK

value = 0
for j in range(i + chars_in_chunk - 1, i - 1, -1):
value = value * RADIX + CODES.index(qrcode[j])

bytes_in_chunk = BASE38_CHARS_NEEDED_IN_CHUNK.index(chars_in_chunk) + 1
for k in range(0, bytes_in_chunk):
decoded_bytes.append(value & 0xFF)
value = value >> 8

return decoded_bytes
17 changes: 10 additions & 7 deletions src/setup_payload/python/README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
## Python tool to generate Matter onboarding codes
## Python tool to generate and parse Matter onboarding codes

Generates Manual Pairing Code and QR Code
Generates and parses Manual Pairing Code and QR Code

#### example usage:

- Parse

```
./generate_setup_payload.py -h
./generate_setup_payload.py -d 3840 -p 20202021 -cf 0 -dm 2 -vid 65521 -pid 32768
./SetupPayload.py parse MT:U9VJ0OMV172PX813210
./SetupPayload.py parse 34970112332
```

- Output
- Generate

```
Manualcode : 34970112332
QRCode : MT:Y.K9042C00KA0648G00
./SetupPayload.py generate --help
./SetupPayload.py generate -d 3840 -p 20202021
./SetupPayload.py generate -d 3840 -p 20202021 --vendor-id 65521 --product-id 32768 -cf 0 -dm 2
```

For more details please refer Matter Specification
Expand Down
220 changes: 220 additions & 0 deletions src/setup_payload/python/SetupPayload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
#!/usr/bin/env python3
#
# Copyright (c) 2024 Project CHIP Authors
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import enum

import Base38
import click
from bitarray import bitarray
from bitarray.util import int2ba, zeros
from construct import BitsInteger, BitStruct, Enum
from stdnum.verhoeff import calc_check_digit

# Format for constructing manualcode
manualcode_format = BitStruct(
'version' / BitsInteger(1),
'vid_pid_present' / BitsInteger(1),
'discriminator' / BitsInteger(4),
'pincode_lsb' / BitsInteger(14),
'pincode_msb' / BitsInteger(13),
'vid' / BitsInteger(16),
'pid' / BitsInteger(16),
'padding' / BitsInteger(7), # this is intentional as BitStruct only takes 8-bit aligned data
)

# Format for constructing qrcode
# qrcode bytes are packed as lsb....msb, hence the order is reversed
qrcode_format = BitStruct(
'padding' / BitsInteger(4),
'pincode' / BitsInteger(27),
'discriminator' / BitsInteger(12),
'discovery' / BitsInteger(8),
'flow' / Enum(BitsInteger(2),
Standard=0, UserIntent=1, Custom=2),
'pid' / BitsInteger(16),
'vid' / BitsInteger(16),
'version' / BitsInteger(3),
)


class CommissioningFlow(enum.IntEnum):
Standard = 0,
UserIntent = 1,
Custom = 2


class SetupPayload:
def __init__(self, discriminator, pincode, rendezvous=4, flow=CommissioningFlow.Standard, vid=0, pid=0):
self.long_discriminator = discriminator
self.short_discriminator = discriminator >> 8
self.pincode = pincode
self.discovery = rendezvous
self.flow = flow
self.vid = vid
self.pid = pid

def p_print(self):
print('{:<{}} :{}'.format('Flow', 24, self.flow))
print('{:<{}} :{}'.format('Pincode', 24, self.pincode))
print('{:<{}} :{}'.format('Short Discriminator', 24, self.short_discriminator))
if self.long_discriminator:
print('{:<{}} :{}'.format('Long Discriminator', 24, self.long_discriminator))
if self.discovery:
print('{:<{}} :{}'.format('Discovery Capabilities', 24, self.discovery))
if self.vid is not None and self.pid is not None:
print('{:<{}} :{:<{}} (0x{:04x})'.format('Vendor Id', 24, self.vid, 6, self.vid))
print('{:<{}} :{:<{}} (0x{:04x})'.format('Product Id', 24, self.pid, 6, self.pid))

def qrcode_dict(self):
return {
'version': 0,
'vid': self.vid,
'pid': self.pid,
'flow': int(self.flow),
'discovery': self.discovery,
'discriminator': self.long_discriminator,
'pincode': self.pincode,
'padding': 0,
}

def manualcode_dict(self):
return {
'version': 0,
'vid_pid_present': 0 if self.flow == CommissioningFlow.Standard else 1,
'discriminator': self.short_discriminator,
'pincode_lsb': self.pincode & 0x3FFF, # 14 ls-bits
'pincode_msb': self.pincode >> 14, # 13 ms-bits
tcarmelveilleux marked this conversation as resolved.
Show resolved Hide resolved
'vid': 0 if self.flow == CommissioningFlow.Standard else self.vid,
'pid': 0 if self.flow == CommissioningFlow.Standard else self.pid,
'padding': 0,
}

def generate_qrcode(self):
data = qrcode_format.build(self.qrcode_dict())
b38_encoded = Base38.encode(data[::-1]) # reversing
return 'MT:{}'.format(b38_encoded)

def generate_manualcode(self):
CHUNK1_START = 0
CHUNK1_LEN = 4
CHUNK2_START = CHUNK1_START + CHUNK1_LEN
CHUNK2_LEN = 16
CHUNK3_START = CHUNK2_START + CHUNK2_LEN
CHUNK3_LEN = 13

bytes = manualcode_format.build(self.manualcode_dict())
bits = bitarray()
bits.frombytes(bytes)

chunk1 = str(int(bits[CHUNK1_START:CHUNK1_START + CHUNK1_LEN].to01(), 2)).zfill(1)
chunk2 = str(int(bits[CHUNK2_START:CHUNK2_START + CHUNK2_LEN].to01(), 2)).zfill(5)
chunk3 = str(int(bits[CHUNK3_START:CHUNK3_START + CHUNK3_LEN].to01(), 2)).zfill(4)
chunk4 = str(self.vid).zfill(5) if self.flow != CommissioningFlow.Standard else ''
chunk5 = str(self.pid).zfill(5) if self.flow != CommissioningFlow.Standard else ''
tcarmelveilleux marked this conversation as resolved.
Show resolved Hide resolved
payload = '{}{}{}{}{}'.format(chunk1, chunk2, chunk3, chunk4, chunk5)
return '{}{}'.format(payload, calc_check_digit(payload))

@staticmethod
def from_container(container, is_qrcode):
payload = None
if is_qrcode:
payload = SetupPayload(container['discriminator'], container['pincode'],
container['discovery'], CommissioningFlow(container['flow'].__int__()),
container['vid'], container['pid'])
else:
payload = SetupPayload(discriminator=container['discriminator'],
pincode=(container['pincode_msb'] << 14) | container['pincode_lsb'],
vid=container['vid'] if container['vid_pid_present'] else None,
pid=container['pid'] if container['vid_pid_present'] else None)
payload.short_discriminator = container['discriminator']
payload.long_discriminator = None
payload.discovery = None
payload.flow = 2 if container['vid_pid_present'] else 0

return payload

@staticmethod
def parse_qrcode(payload):
payload = payload[3:] # remove 'MT:'
b38_decoded = Base38.decode(payload)[::-1]
container = qrcode_format.parse(b38_decoded)
return SetupPayload.from_container(container, is_qrcode=True)

@staticmethod
def parse_manualcode(payload):
payload_len = len(payload)
if payload_len != 11 and payload_len != 21:
print('Invalid length')
return None

# if first digit is greater than 7 the its not v1
if int(str(payload)[0]) > 7:
print('incorrect first digit')
return None

if calc_check_digit(payload[:-1]) != str(payload)[-1]:
print('check digit mismatch')
return None

# vid_pid_present bit position
is_long = int(str(payload)[0]) & (1 << 2)

bits = int2ba(int(payload[0]), length=4)
bits += int2ba(int(payload[1:6]), length=16)
bits += int2ba(int(payload[6:10]), length=13)
bits += int2ba(int(payload[10:15]), length=16) if is_long else zeros(16)
bits += int2ba(int(payload[15:20]), length=16) if is_long else zeros(16)
bits += zeros(7) # padding

container = manualcode_format.parse(bits.tobytes())
return SetupPayload.from_container(container, is_qrcode=False)

@staticmethod
def parse(payload):
if payload.startswith('MT:'):
return SetupPayload.parse_qrcode(payload)
else:
return SetupPayload.parse_manualcode(payload)


@click.group()
def cli():
pass


@cli.command()
@click.argument('payload')
def parse(payload):
click.echo(f'Parsing payload: {payload}')
SetupPayload.parse(payload).p_print()


@cli.command()
@click.option('--discriminator', '-d', required=True, type=click.IntRange(0, 0xFFF), help='Discriminator')
@click.option('--passcode', '-p', required=True, type=click.IntRange(1, 0x5F5E0FE), help='setup pincode')
@click.option('--vendor-id', '-vid', type=click.IntRange(0, 0xFFFF), default=0, help='Vendor ID')
@click.option('--product-id', '-pid', type=click.IntRange(0, 0xFFFF), default=0, help='Product ID')
@click.option('--discovery-cap-bitmask', '-dm', type=click.IntRange(0, 7), default=4, help='Commissionable device discovery capability bitmask. 0:SoftAP, 1:BLE, 2:OnNetwork. Default: OnNetwork')
@click.option('--commissioning-flow', '-cf', type=click.IntRange(0, 2), default=0, help='Commissioning flow, 0:Standard, 1:User-Intent, 2:Custom')
def generate(passcode, discriminator, vendor_id, product_id, discovery_cap_bitmask, commissioning_flow):
payload = SetupPayload(discriminator, passcode, discovery_cap_bitmask, commissioning_flow, vendor_id, product_id)
print("Manualcode : {}".format(payload.generate_manualcode()))
print("QRCode : {}".format(payload.generate_qrcode()))


if __name__ == '__main__':
cli()
Loading
Loading