Diagrams: five hand-crafted SVGs explaining the protocol + architecture
The auto-extracted manual SVGs were unusable PDF text-glyph soup. These are fresh, theme-aware (currentColor everywhere, accent via the --sl-color-accent CSS var), and built to teach. src/assets/diagrams/handshake-sequence.svg Sequence diagram with CLIENT and CONTROLLER swim lanes, five steps: ClientRequestNewSession -> ControllerAckNewSession (carries SessionID) -> derive SessionKey (inline note) -> ClientRequestSecureSession (encrypted, accent-coloured) -> ControllerAckSecureSession (encrypted) -> first OmniLink2Message. Plaintext arrows in currentColor, encrypted arrows in accent. src/assets/diagrams/packet-structure.svg Bytes-on-the-wire box diagram: outer Packet header (seq u16 + type + reserved + encrypted payload) decomposed below into the inner Message (start byte 0x21, length, opcode, data, CRC u16 LE). Plain vs encrypted fields colour-coded with a legend. src/assets/diagrams/session-key-derivation.svg Quirk #1 visual. Three rows of byte cells: ControllerKey (16 bytes, with bytes 0..10 in plain colour and 11..15 highlighted), SessionID (5 bytes), and the resulting SessionKey with the XOR boundary visible. XOR operator in the accent colour to draw the eye. src/assets/diagrams/per-block-whitening.svg Quirk #2 visual. seq pill at the top, three blocks below (block 1, block 2, block N) each showing 16 byte cells with the first two highlighted in accent and labelled with the seq XOR mask. Drives home that it's the SAME mask on EVERY block. src/assets/diagrams/architecture.svg Three groups (LIBRARY, HA INTEGRATION, TEST SURFACE) with boxes inside. Library shows the four protocol-layer modules + connection + client + models + events. HA shows coordinator + 8 platforms. Test surface shows MockPanel (accent-coloured), HA test harness, e2e tests, unit tests. One accent-coloured arrow runs from OmniConnection across to MockPanel labelled 'TCP/4369 (encrypted)'. src/assets/diagrams/pca-file-format.svg Key chain: hardcoded keyPC01 -> decrypts PCA01.CFG (boxes for the CFG fields including the highlighted pca_key) -> arrow showing the extracted pca_key -> decrypts the .pca file (boxes for PCA03 magic, account info, model byte, body, and the highlighted ControllerKey) -> caption 'feeds session-key derivation (quirk #1)'. Wired in via inline-SVG-via-?raw-import + set:html (so currentColor adapts to the theme). Required converting four pages to .mdx: reference/protocol.mdx + handshake + packet diagrams reference/file-format.mdx + pca-file-format diagram explanation/quirks.mdx + session-key + whitening diagrams explanation/architecture.mdx + architecture diagram Two MDX paper cuts during conversion: bare '<100ms' and '<50ms' in architecture.mdx confused the JSX parser; backticked them as . Build: 23 pages clean. Verified inline SVG ships in the rendered HTML (grep for SVG title IDs returns 2/2 hits per relevant page). Container rebuilt + redeployed. Protocol page is now 92750 bytes (was ~63000), quirks page 84156 (was ~63000).
This commit is contained in:
parent
d7ee0a3e98
commit
d5d2ea3d32
120
src/assets/diagrams/architecture.svg
Normal file
120
src/assets/diagrams/architecture.svg
Normal file
@ -0,0 +1,120 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 460" role="img" aria-labelledby="arch-title arch-desc" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
|
||||
<title id="arch-title">omni-pca architecture overview</title>
|
||||
<desc id="arch-desc">Diagram of how the omni_pca library, the Home Assistant integration, the mock panel, and the test harness fit together. The library wraps a four-layer protocol stack (crypto, packet, message, opcodes) with a higher-level OmniClient. The HA integration's coordinator uses the client. The mock panel is a controller-side implementation of the same protocol used both for development and for the HA integration tests.</desc>
|
||||
|
||||
<style>
|
||||
.box { fill: currentColor; fill-opacity: 0.06; stroke: currentColor; stroke-opacity: 0.55; stroke-width: 1.2; }
|
||||
.box-mock { fill: var(--sl-color-accent, #d97706); fill-opacity: 0.10; stroke: var(--sl-color-accent, #d97706); stroke-opacity: 0.7; stroke-width: 1.2; }
|
||||
.label { font-size: 13px; font-weight: 700; fill: currentColor; }
|
||||
.label-mock { font-size: 13px; font-weight: 700; fill: var(--sl-color-accent, #d97706); }
|
||||
.sub { font-size: 10px; opacity: 0.75; fill: currentColor; }
|
||||
.arrow { stroke: currentColor; stroke-width: 1.5; fill: none; opacity: 0.55; }
|
||||
.arrow-net { stroke: var(--sl-color-accent, #d97706); stroke-width: 1.8; fill: none; opacity: 0.85; }
|
||||
.net-tag { font-size: 9px; font-style: italic; fill: var(--sl-color-accent, #d97706); font-weight: 600; }
|
||||
.group-bg { fill: currentColor; fill-opacity: 0.025; stroke: currentColor; stroke-opacity: 0.2; stroke-width: 1; stroke-dasharray: 3 3; }
|
||||
.group-label { font-size: 10px; font-weight: 700; fill: currentColor; opacity: 0.55; letter-spacing: 0.06em; }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="currentColor" opacity="0.65"/>
|
||||
</marker>
|
||||
<marker id="ahNet" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="var(--sl-color-accent, #d97706)" opacity="0.9"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Group: Library -->
|
||||
<rect x="20" y="20" width="280" height="370" class="group-bg" rx="6"/>
|
||||
<text x="34" y="40" class="group-label">LIBRARY · omni_pca</text>
|
||||
|
||||
<!-- Protocol-layer boxes -->
|
||||
<rect x="40" y="60" width="110" height="44" rx="4" class="box"/>
|
||||
<text x="95" y="80" text-anchor="middle" class="label">crypto</text>
|
||||
<text x="95" y="96" text-anchor="middle" class="sub">AES + whitening</text>
|
||||
|
||||
<rect x="170" y="60" width="110" height="44" rx="4" class="box"/>
|
||||
<text x="225" y="80" text-anchor="middle" class="label">packet</text>
|
||||
<text x="225" y="96" text-anchor="middle" class="sub">outer framing</text>
|
||||
|
||||
<rect x="40" y="120" width="110" height="44" rx="4" class="box"/>
|
||||
<text x="95" y="140" text-anchor="middle" class="label">message</text>
|
||||
<text x="95" y="156" text-anchor="middle" class="sub">CRC-16, opcodes</text>
|
||||
|
||||
<rect x="170" y="120" width="110" height="44" rx="4" class="box"/>
|
||||
<text x="225" y="140" text-anchor="middle" class="label">opcodes</text>
|
||||
<text x="225" y="156" text-anchor="middle" class="sub">v1 / v2 IntEnums</text>
|
||||
|
||||
<!-- Connection -->
|
||||
<rect x="40" y="190" width="240" height="44" rx="4" class="box"/>
|
||||
<text x="160" y="210" text-anchor="middle" class="label">OmniConnection</text>
|
||||
<text x="160" y="226" text-anchor="middle" class="sub">handshake, sequence, reader task</text>
|
||||
|
||||
<!-- Client -->
|
||||
<rect x="40" y="250" width="240" height="44" rx="4" class="box"/>
|
||||
<text x="160" y="270" text-anchor="middle" class="label">OmniClient</text>
|
||||
<text x="160" y="286" text-anchor="middle" class="sub">typed methods, events()</text>
|
||||
|
||||
<!-- Models / events -->
|
||||
<rect x="40" y="310" width="115" height="44" rx="4" class="box"/>
|
||||
<text x="97" y="330" text-anchor="middle" class="label">models</text>
|
||||
<text x="97" y="346" text-anchor="middle" class="sub">21 dataclasses</text>
|
||||
|
||||
<rect x="165" y="310" width="115" height="44" rx="4" class="box"/>
|
||||
<text x="222" y="330" text-anchor="middle" class="label">events</text>
|
||||
<text x="222" y="346" text-anchor="middle" class="sub">26 SystemEvent types</text>
|
||||
|
||||
<!-- Group: HA integration -->
|
||||
<rect x="320" y="20" width="200" height="180" class="group-bg" rx="6"/>
|
||||
<text x="334" y="40" class="group-label">HA INTEGRATION · custom_components/omni_pca</text>
|
||||
|
||||
<rect x="340" y="60" width="160" height="44" rx="4" class="box"/>
|
||||
<text x="420" y="80" text-anchor="middle" class="label">Coordinator</text>
|
||||
<text x="420" y="96" text-anchor="middle" class="sub">discover + poll + push</text>
|
||||
|
||||
<rect x="340" y="120" width="160" height="68" rx="4" class="box"/>
|
||||
<text x="420" y="142" text-anchor="middle" class="label">8 entity platforms</text>
|
||||
<text x="420" y="158" text-anchor="middle" class="sub">alarm · binary_sensor · button</text>
|
||||
<text x="420" y="172" text-anchor="middle" class="sub">climate · event · light · sensor · switch</text>
|
||||
|
||||
<!-- Group: Mock + tests -->
|
||||
<rect x="540" y="20" width="160" height="370" class="group-bg" rx="6"/>
|
||||
<text x="554" y="40" class="group-label">TEST SURFACE</text>
|
||||
|
||||
<rect x="560" y="60" width="120" height="68" rx="4" class="box-mock"/>
|
||||
<text x="620" y="82" text-anchor="middle" class="label-mock">MockPanel</text>
|
||||
<text x="620" y="98" text-anchor="middle" class="sub">controller-side</text>
|
||||
<text x="620" y="112" text-anchor="middle" class="sub">async TCP server</text>
|
||||
|
||||
<rect x="560" y="150" width="120" height="68" rx="4" class="box"/>
|
||||
<text x="620" y="172" text-anchor="middle" class="label">HA test harness</text>
|
||||
<text x="620" y="188" text-anchor="middle" class="sub">in-process HA</text>
|
||||
<text x="620" y="202" text-anchor="middle" class="sub">12 tests</text>
|
||||
|
||||
<rect x="560" y="240" width="120" height="68" rx="4" class="box"/>
|
||||
<text x="620" y="262" text-anchor="middle" class="label">e2e tests</text>
|
||||
<text x="620" y="278" text-anchor="middle" class="sub">17 client ↔ mock</text>
|
||||
<text x="620" y="292" text-anchor="middle" class="sub">round-trips</text>
|
||||
|
||||
<rect x="560" y="320" width="120" height="60" rx="4" class="box"/>
|
||||
<text x="620" y="342" text-anchor="middle" class="label">unit tests</text>
|
||||
<text x="620" y="358" text-anchor="middle" class="sub">300+ on primitives</text>
|
||||
<text x="620" y="372" text-anchor="middle" class="sub">+ helpers</text>
|
||||
|
||||
<!-- Arrows -->
|
||||
<!-- Coordinator uses OmniClient -->
|
||||
<line x1="340" y1="84" x2="290" y2="270" class="arrow" marker-end="url(#ah)"/>
|
||||
<text x="305" y="190" class="sub" transform="rotate(-72 305 190)">uses</text>
|
||||
|
||||
<!-- Library connection over TCP to a real panel OR the mock -->
|
||||
<path d="M 280 212 C 480 212, 480 90, 560 90" class="arrow-net" marker-end="url(#ahNet)"/>
|
||||
<text x="510" y="160" class="net-tag">TCP/4369 (encrypted)</text>
|
||||
|
||||
<!-- HA test harness drives HA integration -->
|
||||
<line x1="560" y1="178" x2="500" y2="178" class="arrow" marker-end="url(#ah)"/>
|
||||
<text x="515" y="170" class="sub">drives</text>
|
||||
|
||||
<!-- Bottom legend / caption -->
|
||||
<text x="20" y="425" class="sub">Solid arrows = function calls in-process · accent arrows = real TCP traffic.</text>
|
||||
<text x="20" y="442" class="sub">The mock panel is the same thing the real panel is, on the wire — it lets every test run without hardware.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
75
src/assets/diagrams/handshake-sequence.svg
Normal file
75
src/assets/diagrams/handshake-sequence.svg
Normal file
@ -0,0 +1,75 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 460" role="img" aria-labelledby="handshake-title handshake-desc" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">
|
||||
<title id="handshake-title">Omni-Link II secure-session handshake</title>
|
||||
<desc id="handshake-desc">Sequence diagram showing four packets exchanged between client and controller to establish an encrypted session: ClientRequestNewSession, ControllerAckNewSession (carrying the SessionID), ClientRequestSecureSession (encrypted), and ControllerAckSecureSession (encrypted). After the handshake the first encrypted v2 message can be sent.</desc>
|
||||
|
||||
<style>
|
||||
.lane-label { font-size: 14px; font-weight: 600; }
|
||||
.step-num { font-size: 11px; font-weight: 700; opacity: 0.7; }
|
||||
.pkt-label { font-size: 12px; font-weight: 600; }
|
||||
.pkt-meta { font-size: 10px; opacity: 0.75; font-style: italic; }
|
||||
.lane-line { stroke: currentColor; stroke-width: 1.5; opacity: 0.35; stroke-dasharray: 4 3; }
|
||||
.arrow { stroke: currentColor; stroke-width: 1.8; fill: none; }
|
||||
.arrow-enc { stroke: var(--sl-color-accent, #d97706); stroke-width: 1.8; fill: none; }
|
||||
.pkt-enc { fill: var(--sl-color-accent, #d97706); }
|
||||
.head { fill: currentColor; }
|
||||
.head-enc { fill: var(--sl-color-accent, #d97706); }
|
||||
.lane-head { fill: currentColor; opacity: 0.06; stroke: currentColor; stroke-opacity: 0.4; }
|
||||
.lane-text { fill: currentColor; }
|
||||
.step-pad { fill: currentColor; opacity: 0.05; }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 z" class="head"/>
|
||||
</marker>
|
||||
<marker id="arrEnc" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 z" class="head-enc"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Lane headers -->
|
||||
<rect x="100" y="20" width="180" height="34" rx="4" class="lane-head"/>
|
||||
<text x="190" y="42" text-anchor="middle" class="lane-label lane-text">CLIENT</text>
|
||||
|
||||
<rect x="440" y="20" width="180" height="34" rx="4" class="lane-head"/>
|
||||
<text x="530" y="42" text-anchor="middle" class="lane-label lane-text">CONTROLLER</text>
|
||||
|
||||
<!-- Lane lines -->
|
||||
<line x1="190" y1="60" x2="190" y2="430" class="lane-line"/>
|
||||
<line x1="530" y1="60" x2="530" y2="430" class="lane-line"/>
|
||||
|
||||
<!-- Step 1: ClientRequestNewSession -->
|
||||
<text x="50" y="92" class="step-num lane-text">1</text>
|
||||
<line x1="190" y1="90" x2="525" y2="90" class="arrow" marker-end="url(#arr)"/>
|
||||
<text x="357" y="82" text-anchor="middle" class="pkt-label lane-text">ClientRequestNewSession (0x01)</text>
|
||||
<text x="357" y="106" text-anchor="middle" class="pkt-meta lane-text">no payload · plaintext · seq=1</text>
|
||||
|
||||
<!-- Step 2: ControllerAckNewSession -->
|
||||
<text x="50" y="162" class="step-num lane-text">2</text>
|
||||
<line x1="525" y1="160" x2="195" y2="160" class="arrow" marker-end="url(#arr)"/>
|
||||
<text x="357" y="152" text-anchor="middle" class="pkt-label lane-text">ControllerAckNewSession (0x02)</text>
|
||||
<text x="357" y="176" text-anchor="middle" class="pkt-meta lane-text">7 bytes: 00 01 + 5-byte SessionID · plaintext</text>
|
||||
|
||||
<!-- Inline derivation note between steps 2 and 3 -->
|
||||
<rect x="50" y="195" width="280" height="40" rx="4" class="step-pad"/>
|
||||
<text x="190" y="212" text-anchor="middle" class="pkt-meta lane-text">SessionKey = ControllerKey[0:11]</text>
|
||||
<text x="190" y="226" text-anchor="middle" class="pkt-meta lane-text"> || (ControllerKey[11:16] XOR SessionID)</text>
|
||||
|
||||
<!-- Step 3: ClientRequestSecureSession -->
|
||||
<text x="50" y="272" class="step-num lane-text">3</text>
|
||||
<line x1="190" y1="270" x2="525" y2="270" class="arrow-enc" marker-end="url(#arrEnc)"/>
|
||||
<text x="357" y="262" text-anchor="middle" class="pkt-label pkt-enc">ClientRequestSecureSession (0x03)</text>
|
||||
<text x="357" y="286" text-anchor="middle" class="pkt-meta lane-text">SessionID echoed · AES-128-ECB · per-block whitening</text>
|
||||
|
||||
<!-- Step 4: ControllerAckSecureSession -->
|
||||
<text x="50" y="342" class="step-num lane-text">4</text>
|
||||
<line x1="525" y1="340" x2="195" y2="340" class="arrow-enc" marker-end="url(#arrEnc)"/>
|
||||
<text x="357" y="332" text-anchor="middle" class="pkt-label pkt-enc">ControllerAckSecureSession (0x04)</text>
|
||||
<text x="357" y="356" text-anchor="middle" class="pkt-meta lane-text">SessionID echoed back · proves controller has the key</text>
|
||||
|
||||
<!-- Step 5: First encrypted command -->
|
||||
<text x="50" y="412" class="step-num lane-text">5</text>
|
||||
<line x1="190" y1="410" x2="525" y2="410" class="arrow-enc" marker-end="url(#arrEnc)"/>
|
||||
<text x="357" y="402" text-anchor="middle" class="pkt-label pkt-enc">OmniLink2Message (0x20)</text>
|
||||
<text x="357" y="426" text-anchor="middle" class="pkt-meta lane-text">e.g. RequestSystemInformation · session is now ONLINE</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
69
src/assets/diagrams/packet-structure.svg
Normal file
69
src/assets/diagrams/packet-structure.svg
Normal file
@ -0,0 +1,69 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 360" role="img" aria-labelledby="pkt-title pkt-desc" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">
|
||||
<title id="pkt-title">Omni-Link II packet and message structure</title>
|
||||
<desc id="pkt-desc">The wire packet has a 4-byte plaintext header (sequence number, packet type, reserved) followed by an optional payload. For OmniLink2Message packets the payload is AES-encrypted bytes that decrypt to an inner Message: start char (0x21), length byte, opcode byte, data bytes, and a two-byte CRC-16/MODBUS.</desc>
|
||||
|
||||
<style>
|
||||
.label { font-size: 11px; font-weight: 600; }
|
||||
.meta { font-size: 10px; opacity: 0.7; font-style: italic; }
|
||||
.legend { font-size: 10px; opacity: 0.85; }
|
||||
.field { fill: currentColor; opacity: 0.06; stroke: currentColor; stroke-opacity: 0.5; stroke-width: 1; }
|
||||
.field-enc { fill: var(--sl-color-accent, #d97706); fill-opacity: 0.12; stroke: var(--sl-color-accent, #d97706); stroke-opacity: 0.6; stroke-width: 1; }
|
||||
.text { fill: currentColor; }
|
||||
.text-enc { fill: var(--sl-color-accent, #d97706); font-weight: 700; }
|
||||
.brace { stroke: currentColor; stroke-width: 1.2; fill: none; opacity: 0.45; }
|
||||
</style>
|
||||
|
||||
<!-- Outer packet header -->
|
||||
<text x="20" y="32" class="label text">Wire packet</text>
|
||||
<text x="20" y="48" class="meta text">4-byte plaintext header + N-byte payload</text>
|
||||
|
||||
<rect x="20" y="62" width="120" height="44" class="field"/>
|
||||
<text x="80" y="80" text-anchor="middle" class="label text">seq (BE u16)</text>
|
||||
<text x="80" y="96" text-anchor="middle" class="meta text">monotonic, skips 0</text>
|
||||
|
||||
<rect x="140" y="62" width="80" height="44" class="field"/>
|
||||
<text x="180" y="80" text-anchor="middle" class="label text">type</text>
|
||||
<text x="180" y="96" text-anchor="middle" class="meta text">0x20 = v2 msg</text>
|
||||
|
||||
<rect x="220" y="62" width="60" height="44" class="field"/>
|
||||
<text x="250" y="80" text-anchor="middle" class="label text">reserved</text>
|
||||
<text x="250" y="96" text-anchor="middle" class="meta text">0x00</text>
|
||||
|
||||
<rect x="280" y="62" width="380" height="44" class="field-enc"/>
|
||||
<text x="470" y="80" text-anchor="middle" class="label text-enc">AES-encrypted payload</text>
|
||||
<text x="470" y="96" text-anchor="middle" class="meta text">N × 16 bytes (zero-padded, per-block whitened)</text>
|
||||
|
||||
<!-- Brace down to the inner message -->
|
||||
<path d="M 280 110 Q 280 130 290 140 L 460 140 Q 470 140 470 152" class="brace"/>
|
||||
<text x="470" y="160" text-anchor="middle" class="meta text">decrypts to →</text>
|
||||
|
||||
<!-- Inner message -->
|
||||
<text x="20" y="200" class="label text">Inner message (after decrypt + unwhiten)</text>
|
||||
<text x="20" y="216" class="meta text">v2 framing: start, length, opcode, data, CRC</text>
|
||||
|
||||
<rect x="20" y="230" width="60" height="44" class="field"/>
|
||||
<text x="50" y="248" text-anchor="middle" class="label text">start</text>
|
||||
<text x="50" y="264" text-anchor="middle" class="meta text">0x21</text>
|
||||
|
||||
<rect x="80" y="230" width="60" height="44" class="field"/>
|
||||
<text x="110" y="248" text-anchor="middle" class="label text">length</text>
|
||||
<text x="110" y="264" text-anchor="middle" class="meta text">u8 of data</text>
|
||||
|
||||
<rect x="140" y="230" width="80" height="44" class="field"/>
|
||||
<text x="180" y="248" text-anchor="middle" class="label text">opcode</text>
|
||||
<text x="180" y="264" text-anchor="middle" class="meta text">e.g. 0x16 = SysInfo</text>
|
||||
|
||||
<rect x="220" y="230" width="320" height="44" class="field"/>
|
||||
<text x="380" y="248" text-anchor="middle" class="label text">data</text>
|
||||
<text x="380" y="264" text-anchor="middle" class="meta text">payload bytes for this opcode</text>
|
||||
|
||||
<rect x="540" y="230" width="120" height="44" class="field"/>
|
||||
<text x="600" y="248" text-anchor="middle" class="label text">CRC (LE u16)</text>
|
||||
<text x="600" y="264" text-anchor="middle" class="meta text">CRC-16/MODBUS</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="20" y="305" width="14" height="14" class="field"/>
|
||||
<text x="40" y="316" class="legend text">Plaintext on the wire</text>
|
||||
<rect x="200" y="305" width="14" height="14" class="field-enc"/>
|
||||
<text x="220" y="316" class="legend text">AES-encrypted (with per-block whitening)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
73
src/assets/diagrams/pca-file-format.svg
Normal file
73
src/assets/diagrams/pca-file-format.svg
Normal file
@ -0,0 +1,73 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 360" role="img" aria-labelledby="pca-title pca-desc" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">
|
||||
<title id="pca-title">.pca file format and key chain</title>
|
||||
<desc id="pca-desc">PCA01.CFG is encrypted with the hardcoded keyPC01 and contains a per-installation pca_key field. That pca_key decrypts the .pca account file, whose plaintext starts with the PCA03 magic and contains the panel's network address, port, and ControllerKey for live AES sessions.</desc>
|
||||
|
||||
<style>
|
||||
.label { font-size: 12px; font-weight: 700; fill: currentColor; }
|
||||
.meta { font-size: 10px; opacity: 0.75; font-style: italic; fill: currentColor; }
|
||||
.field { font-size: 11px; font-weight: 600; fill: currentColor; }
|
||||
.box { fill: currentColor; fill-opacity: 0.05; stroke: currentColor; stroke-opacity: 0.55; stroke-width: 1.2; }
|
||||
.box-key { fill: var(--sl-color-accent, #d97706); fill-opacity: 0.15; stroke: var(--sl-color-accent, #d97706); stroke-opacity: 0.85; stroke-width: 1.5; }
|
||||
.accent { fill: var(--sl-color-accent, #d97706); font-weight: 700; }
|
||||
.arrow { stroke: currentColor; stroke-width: 1.5; fill: none; opacity: 0.55; }
|
||||
.arrow-key { stroke: var(--sl-color-accent, #d97706); stroke-width: 2; fill: none; opacity: 0.9; stroke-dasharray: 4 3; }
|
||||
.key-pill { fill: var(--sl-color-accent, #d97706); fill-opacity: 0.15; stroke: var(--sl-color-accent, #d97706); stroke-opacity: 0.85; stroke-width: 1; }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<marker id="apca" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="currentColor" opacity="0.65"/>
|
||||
</marker>
|
||||
<marker id="apcaKey" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="var(--sl-color-accent, #d97706)" opacity="0.9"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Hardcoded key 1 -->
|
||||
<rect x="20" y="20" width="200" height="38" rx="6" class="key-pill"/>
|
||||
<text x="120" y="38" text-anchor="middle" class="label accent">keyPC01 (hardcoded)</text>
|
||||
<text x="120" y="52" text-anchor="middle" class="meta">0x14326573 — 32 bits, in source</text>
|
||||
|
||||
<!-- PCA01.CFG -->
|
||||
<rect x="20" y="90" width="280" height="170" rx="6" class="box"/>
|
||||
<text x="34" y="110" class="label">PCA01.CFG · encrypted</text>
|
||||
<text x="34" y="125" class="meta">Borland-Pascal LCG ⊕ stream</text>
|
||||
|
||||
<text x="34" y="156" class="field">CFG version tag (e.g. CFG05)</text>
|
||||
<text x="34" y="172" class="field">modem AT init / dial / hangup commands</text>
|
||||
<text x="34" y="188" class="field">port + IRQ + baud</text>
|
||||
|
||||
<rect x="30" y="200" width="260" height="22" rx="3" class="box-key"/>
|
||||
<text x="160" y="216" text-anchor="middle" class="field accent">pca_key (uint32)</text>
|
||||
|
||||
<text x="34" y="240" class="field">password, printer port, serial port…</text>
|
||||
|
||||
<!-- Arrow: keyPC01 decrypts PCA01.CFG -->
|
||||
<line x1="120" y1="58" x2="120" y2="88" class="arrow-key" marker-end="url(#apcaKey)"/>
|
||||
<text x="130" y="78" class="meta accent">decrypts</text>
|
||||
|
||||
<!-- Arrow: extracted pca_key decrypts .pca -->
|
||||
<path d="M 290 211 C 350 211, 360 211, 380 211" class="arrow-key" marker-end="url(#apcaKey)"/>
|
||||
<text x="335" y="204" text-anchor="middle" class="meta accent">extracted →</text>
|
||||
|
||||
<!-- .pca file -->
|
||||
<rect x="380" y="90" width="320" height="245" rx="6" class="box"/>
|
||||
<text x="394" y="110" class="label">My_Account.pca · encrypted</text>
|
||||
<text x="394" y="125" class="meta">same XOR cipher, per-install key</text>
|
||||
|
||||
<text x="394" y="156" class="field">PCA03 magic (5 bytes)</text>
|
||||
<text x="394" y="172" class="field">AccountName · AccountAddress · …</text>
|
||||
<text x="394" y="188" class="field">model byte (e.g. 16 = OMNI_PRO_II)</text>
|
||||
<text x="394" y="204" class="field">firmware major/minor/revision</text>
|
||||
|
||||
<text x="394" y="232" class="meta">…body: zones, units, areas, programs, …</text>
|
||||
|
||||
<text x="394" y="262" class="field">Connection.NetworkAddress (string)</text>
|
||||
<text x="394" y="278" class="field">Connection.NetworkPort (string)</text>
|
||||
|
||||
<rect x="390" y="290" width="300" height="22" rx="3" class="box-key"/>
|
||||
<text x="540" y="306" text-anchor="middle" class="field accent">Connection.ControllerKey (16 bytes)</text>
|
||||
|
||||
<!-- Arrow: ControllerKey is the AES base for live sessions -->
|
||||
<text x="540" y="335" text-anchor="middle" class="meta accent">→ feeds session-key derivation (quirk #1)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
107
src/assets/diagrams/per-block-whitening.svg
Normal file
107
src/assets/diagrams/per-block-whitening.svg
Normal file
@ -0,0 +1,107 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 320" role="img" aria-labelledby="bw-title bw-desc" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">
|
||||
<title id="bw-title">Per-block XOR pre-whitening — quirk #2</title>
|
||||
<desc id="bw-desc">Before AES-encrypting each 16-byte block of a packet's payload, the first two bytes of every block are XORed with the packet's 16-bit sequence number, high byte first then low byte. The same XOR mask is applied to every block in the same packet. Decryption reverses the operation after AES-decrypt.</desc>
|
||||
|
||||
<style>
|
||||
.label { font-size: 12px; font-weight: 700; }
|
||||
.meta { font-size: 10px; opacity: 0.75; font-style: italic; }
|
||||
.byte { font-size: 10px; font-weight: 600; }
|
||||
.cell { stroke: currentColor; stroke-width: 1; fill: currentColor; fill-opacity: 0.06; }
|
||||
.cell-xor { fill: var(--sl-color-accent, #d97706); fill-opacity: 0.18; stroke: var(--sl-color-accent, #d97706); stroke-width: 1; }
|
||||
.text { fill: currentColor; }
|
||||
.text-enc { fill: var(--sl-color-accent, #d97706); font-weight: 700; }
|
||||
.seq-pill { fill: var(--sl-color-accent, #d97706); fill-opacity: 0.18; stroke: var(--sl-color-accent, #d97706); stroke-width: 1; }
|
||||
.arrow { stroke: currentColor; stroke-width: 1.4; fill: none; opacity: 0.55; }
|
||||
.xor-op { font-size: 16px; font-weight: 700; fill: var(--sl-color-accent, #d97706); }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<marker id="dnw" viewBox="0 0 10 10" refX="5" refY="9" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M0,0 L10,0 L5,10 z" fill="currentColor" opacity="0.6"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Top: packet header reminder -->
|
||||
<rect x="20" y="14" width="120" height="28" class="seq-pill" rx="4"/>
|
||||
<text x="80" y="33" text-anchor="middle" class="label text-enc">seq = 0x0042</text>
|
||||
|
||||
<text x="160" y="33" class="meta text">…the packet's 16-bit sequence number is the XOR mask source.</text>
|
||||
|
||||
<!-- Block 1 -->
|
||||
<text x="20" y="84" class="label text">Block 1</text>
|
||||
<text x="20" y="100" class="meta text">first 16 bytes of payload</text>
|
||||
|
||||
<g transform="translate(140, 70)">
|
||||
<rect x="0" y="0" width="32" height="32" class="cell cell-xor"/>
|
||||
<rect x="32" y="0" width="32" height="32" class="cell cell-xor"/>
|
||||
<rect x="64" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="96" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="128" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="160" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="192" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="224" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="256" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="288" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="320" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="352" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="384" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="416" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="448" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="480" y="0" width="32" height="32" class="cell"/>
|
||||
<text x="16" y="20" text-anchor="middle" class="byte text-enc">⊕00</text>
|
||||
<text x="48" y="20" text-anchor="middle" class="byte text-enc">⊕42</text>
|
||||
</g>
|
||||
|
||||
<!-- Block 2 -->
|
||||
<text x="20" y="172" class="label text">Block 2</text>
|
||||
<text x="20" y="188" class="meta text">next 16 bytes — same mask</text>
|
||||
<g transform="translate(140, 158)">
|
||||
<rect x="0" y="0" width="32" height="32" class="cell cell-xor"/>
|
||||
<rect x="32" y="0" width="32" height="32" class="cell cell-xor"/>
|
||||
<rect x="64" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="96" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="128" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="160" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="192" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="224" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="256" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="288" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="320" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="352" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="384" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="416" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="448" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="480" y="0" width="32" height="32" class="cell"/>
|
||||
<text x="16" y="20" text-anchor="middle" class="byte text-enc">⊕00</text>
|
||||
<text x="48" y="20" text-anchor="middle" class="byte text-enc">⊕42</text>
|
||||
</g>
|
||||
|
||||
<!-- Block 3 (etc) -->
|
||||
<text x="20" y="260" class="label text">Block N</text>
|
||||
<text x="20" y="276" class="meta text">…same mask, every block</text>
|
||||
<g transform="translate(140, 246)">
|
||||
<rect x="0" y="0" width="32" height="32" class="cell cell-xor"/>
|
||||
<rect x="32" y="0" width="32" height="32" class="cell cell-xor"/>
|
||||
<rect x="64" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="96" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="128" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="160" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="192" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="224" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="256" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="288" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="320" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="352" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="384" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="416" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="448" y="0" width="32" height="32" class="cell"/>
|
||||
<rect x="480" y="0" width="32" height="32" class="cell"/>
|
||||
<text x="16" y="20" text-anchor="middle" class="byte text-enc">⊕00</text>
|
||||
<text x="48" y="20" text-anchor="middle" class="byte text-enc">⊕42</text>
|
||||
</g>
|
||||
|
||||
<!-- Right side annotations -->
|
||||
<text x="660" y="86" text-anchor="middle" class="meta text-enc">⊕ seq_hi</text>
|
||||
<text x="660" y="100" text-anchor="middle" class="meta text-enc">⊕ seq_lo</text>
|
||||
<text x="660" y="116" text-anchor="middle" class="meta text">then AES</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
103
src/assets/diagrams/session-key-derivation.svg
Normal file
103
src/assets/diagrams/session-key-derivation.svg
Normal file
@ -0,0 +1,103 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 280" role="img" aria-labelledby="sk-title sk-desc" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">
|
||||
<title id="sk-title">Session key derivation — quirk #1</title>
|
||||
<desc id="sk-desc">The 16-byte session AES key is built from the 16-byte ControllerKey and the 5-byte SessionID. Bytes 0 through 10 of the ControllerKey are kept verbatim. Bytes 11 through 15 of the ControllerKey are XORed with bytes 0 through 4 of the SessionID. The result is the per-session AES-128 key.</desc>
|
||||
|
||||
<style>
|
||||
.label { font-size: 12px; font-weight: 700; }
|
||||
.meta { font-size: 10px; opacity: 0.75; font-style: italic; }
|
||||
.byte { font-size: 10px; font-weight: 600; }
|
||||
.cell { stroke: currentColor; stroke-width: 1; fill: none; }
|
||||
.cell-keep { fill: currentColor; fill-opacity: 0.06; }
|
||||
.cell-xor { fill: var(--sl-color-accent, #d97706); fill-opacity: 0.12; stroke: var(--sl-color-accent, #d97706); }
|
||||
.cell-id { fill: currentColor; fill-opacity: 0.18; }
|
||||
.cell-out { fill: currentColor; fill-opacity: 0.10; }
|
||||
.text { fill: currentColor; }
|
||||
.arrow { stroke: currentColor; stroke-width: 1.4; fill: none; opacity: 0.55; }
|
||||
.xor-op { font-size: 22px; font-weight: 700; fill: var(--sl-color-accent, #d97706); }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<marker id="dn" viewBox="0 0 10 10" refX="5" refY="9" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M0,0 L10,0 L5,10 z" fill="currentColor" opacity="0.6"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Row 1: ControllerKey (16 bytes) -->
|
||||
<text x="20" y="40" class="label text">ControllerKey (16 bytes)</text>
|
||||
|
||||
<g transform="translate(180, 22)">
|
||||
<!-- 11 keep bytes -->
|
||||
<g>
|
||||
<rect x="0" y="0" width="30" height="30" class="cell cell-keep"/>
|
||||
<rect x="30" y="0" width="30" height="30" class="cell cell-keep"/>
|
||||
<rect x="60" y="0" width="30" height="30" class="cell cell-keep"/>
|
||||
<rect x="90" y="0" width="30" height="30" class="cell cell-keep"/>
|
||||
<rect x="120" y="0" width="30" height="30" class="cell cell-keep"/>
|
||||
<rect x="150" y="0" width="30" height="30" class="cell cell-keep"/>
|
||||
<rect x="180" y="0" width="30" height="30" class="cell cell-keep"/>
|
||||
<rect x="210" y="0" width="30" height="30" class="cell cell-keep"/>
|
||||
<rect x="240" y="0" width="30" height="30" class="cell cell-keep"/>
|
||||
<rect x="270" y="0" width="30" height="30" class="cell cell-keep"/>
|
||||
<rect x="300" y="0" width="30" height="30" class="cell cell-keep"/>
|
||||
<text x="0" y="20" dx="15" text-anchor="middle" class="byte text">0</text>
|
||||
<text x="30" y="20" dx="15" text-anchor="middle" class="byte text">1</text>
|
||||
<text x="60" y="20" dx="15" text-anchor="middle" class="byte text">2</text>
|
||||
<text x="90" y="20" dx="15" text-anchor="middle" class="byte text">3</text>
|
||||
<text x="120" y="20" dx="15" text-anchor="middle" class="byte text">4</text>
|
||||
<text x="150" y="20" dx="15" text-anchor="middle" class="byte text">5</text>
|
||||
<text x="180" y="20" dx="15" text-anchor="middle" class="byte text">6</text>
|
||||
<text x="210" y="20" dx="15" text-anchor="middle" class="byte text">7</text>
|
||||
<text x="240" y="20" dx="15" text-anchor="middle" class="byte text">8</text>
|
||||
<text x="270" y="20" dx="15" text-anchor="middle" class="byte text">9</text>
|
||||
<text x="300" y="20" dx="15" text-anchor="middle" class="byte text">10</text>
|
||||
</g>
|
||||
<!-- 5 xor bytes -->
|
||||
<g>
|
||||
<rect x="330" y="0" width="30" height="30" class="cell cell-xor"/>
|
||||
<rect x="360" y="0" width="30" height="30" class="cell cell-xor"/>
|
||||
<rect x="390" y="0" width="30" height="30" class="cell cell-xor"/>
|
||||
<rect x="420" y="0" width="30" height="30" class="cell cell-xor"/>
|
||||
<rect x="450" y="0" width="30" height="30" class="cell cell-xor"/>
|
||||
<text x="330" y="20" dx="15" text-anchor="middle" class="byte text">11</text>
|
||||
<text x="360" y="20" dx="15" text-anchor="middle" class="byte text">12</text>
|
||||
<text x="390" y="20" dx="15" text-anchor="middle" class="byte text">13</text>
|
||||
<text x="420" y="20" dx="15" text-anchor="middle" class="byte text">14</text>
|
||||
<text x="450" y="20" dx="15" text-anchor="middle" class="byte text">15</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Annotations -->
|
||||
<text x="345" y="78" text-anchor="middle" class="meta text">11 bytes kept verbatim →</text>
|
||||
<text x="585" y="78" text-anchor="middle" class="meta text">5 bytes XORed ↓</text>
|
||||
|
||||
<!-- XOR operator -->
|
||||
<text x="585" y="125" text-anchor="middle" class="xor-op">⊕</text>
|
||||
|
||||
<!-- Row 2: SessionID -->
|
||||
<text x="20" y="160" class="label text">SessionID (5 bytes)</text>
|
||||
<g transform="translate(510, 142)">
|
||||
<rect x="0" y="0" width="30" height="30" class="cell cell-id"/>
|
||||
<rect x="30" y="0" width="30" height="30" class="cell cell-id"/>
|
||||
<rect x="60" y="0" width="30" height="30" class="cell cell-id"/>
|
||||
<rect x="90" y="0" width="30" height="30" class="cell cell-id"/>
|
||||
<rect x="120" y="0" width="30" height="30" class="cell cell-id"/>
|
||||
<text x="0" y="20" dx="15" text-anchor="middle" class="byte text">0</text>
|
||||
<text x="30" y="20" dx="15" text-anchor="middle" class="byte text">1</text>
|
||||
<text x="60" y="20" dx="15" text-anchor="middle" class="byte text">2</text>
|
||||
<text x="90" y="20" dx="15" text-anchor="middle" class="byte text">3</text>
|
||||
<text x="120" y="20" dx="15" text-anchor="middle" class="byte text">4</text>
|
||||
</g>
|
||||
|
||||
<!-- Down arrows from rows 1 & 2 to row 3 -->
|
||||
<line x1="345" y1="180" x2="345" y2="220" class="arrow" marker-end="url(#dn)"/>
|
||||
<line x1="585" y1="180" x2="585" y2="220" class="arrow" marker-end="url(#dn)"/>
|
||||
|
||||
<!-- Row 3: SessionKey -->
|
||||
<text x="20" y="245" class="label text">SessionKey (16 bytes)</text>
|
||||
<g transform="translate(180, 228)">
|
||||
<rect x="0" y="0" width="330" height="30" class="cell cell-out"/>
|
||||
<text x="165" y="20" text-anchor="middle" class="meta text">CK[0..11) — verbatim</text>
|
||||
<rect x="330" y="0" width="150" height="30" class="cell cell-out" stroke="var(--sl-color-accent, #d97706)" stroke-width="1.5"/>
|
||||
<text x="405" y="20" text-anchor="middle" class="meta text-enc">CK[11..16) ⊕ SessionID</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
@ -3,6 +3,10 @@ title: Architecture overview
|
||||
description: How the library, the Home Assistant integration, the mock panel, and the test stack fit together.
|
||||
---
|
||||
|
||||
import Architecture from '../../../assets/diagrams/architecture.svg?raw';
|
||||
|
||||
<div style="margin: 1.5rem 0;" set:html={Architecture} />
|
||||
|
||||
The project has four moving parts:
|
||||
|
||||
1. The Python library (`omni_pca`) — protocol, client, models.
|
||||
@ -85,7 +89,7 @@ method calls.
|
||||
`helpers.py` is a strict no-HA-imports zone. Every translation between
|
||||
Omni's wire encoding and HA's UI encoding (zone-type → device-class,
|
||||
brightness conversion, HVAC mode round-trip, alarm state) lives there as
|
||||
a pure function. 61 unit tests cover it; they run in <100ms because they
|
||||
a pure function. 61 unit tests cover it; they run in `<100ms` because they
|
||||
don't have to boot HA.
|
||||
|
||||
## The mock panel
|
||||
@ -184,6 +188,6 @@ What happens when a user toggles a light in the HA UI:
|
||||
23. HA UI re-renders the light card
|
||||
```
|
||||
|
||||
Steps 4-13 happen in <50ms over a real TCP socket. Steps 15-22 happen in a
|
||||
Steps 4-13 happen in `<50ms` over a real TCP socket. Steps 15-22 happen in a
|
||||
similar window. The user sees the light card update on the UI essentially
|
||||
immediately.
|
||||
@ -3,6 +3,9 @@ title: The two non-public quirks
|
||||
description: Why public Omni-Link clients silently fail on the first encrypted message — session key XOR mix and per-block pre-whitening before AES.
|
||||
---
|
||||
|
||||
import SessionKey from '../../../assets/diagrams/session-key-derivation.svg?raw';
|
||||
import Whitening from '../../../assets/diagrams/per-block-whitening.svg?raw';
|
||||
|
||||
The Omni-Link II protocol, as documented in the publicly-available spec, looks
|
||||
like a textbook AES-128-ECB session over TCP: handshake, derive a key, encrypt
|
||||
everything from then on. As implemented by HAI's PC Access 3.17, it isn't.
|
||||
@ -38,6 +41,8 @@ exactly to talk to the panel.
|
||||
|
||||
## Quirk #1 — session key XOR mix
|
||||
|
||||
<div style="margin: 1rem 0 1.5rem;" set:html={SessionKey} />
|
||||
|
||||
The `ControllerKey` is the 16-byte AES-128 key that lives in the panel's
|
||||
NVRAM and inside the encrypted `.pca` config file. The naive expectation is
|
||||
that this key is what AES uses for the session. It isn't.
|
||||
@ -85,6 +90,8 @@ in practice is always because you didn't apply the XOR mix."
|
||||
|
||||
## Quirk #2 — per-block XOR pre-whitening before AES
|
||||
|
||||
<div style="margin: 1rem 0 1.5rem;" set:html={Whitening} />
|
||||
|
||||
This is the headline.
|
||||
|
||||
Before AES-encrypting any payload block, the *first two bytes of every
|
||||
@ -3,6 +3,10 @@ title: .pca and PCA01.CFG file format
|
||||
description: Borland-Pascal LCG XOR cipher, three keys, and the on-disk layout for HAI's PC Access account export and app-settings files.
|
||||
---
|
||||
|
||||
import PcaFormat from '../../../assets/diagrams/pca-file-format.svg?raw';
|
||||
|
||||
<div style="margin: 1.5rem 0;" set:html={PcaFormat} />
|
||||
|
||||
The `.pca` and `PCA01.CFG` files written by HAI's PC Access are *not* AES.
|
||||
Despite the existence of `clsAES` in the same binary, both file formats use a
|
||||
Borland-Pascal-style linear-congruential generator (LCG) keystream XORed
|
||||
@ -3,6 +3,11 @@ title: Omni-Link II protocol
|
||||
description: Byte-level handshake, packet layouts, key derivation, and steady-state encryption rules for Omni-Link II as implemented by HAI's PC Access 3.17.
|
||||
---
|
||||
|
||||
import HandshakeSequence from '../../../assets/diagrams/handshake-sequence.svg?raw';
|
||||
import PacketStructure from '../../../assets/diagrams/packet-structure.svg?raw';
|
||||
|
||||
<div style="margin: 1.5rem 0;" set:html={HandshakeSequence} />
|
||||
|
||||
TCP/v2 PC Access opens a secure session by exchanging two unencrypted control
|
||||
packets to derive a per-session AES-128-ECB key from the panel's 16-byte
|
||||
`ControllerKey` XOR-mixed with a 5-byte controller-supplied `SessionID`, then
|
||||
@ -71,6 +76,8 @@ UDP / 1808 TCP).
|
||||
|
||||
## Packet payload byte layouts
|
||||
|
||||
<div style="margin: 1rem 0;" set:html={PacketStructure} />
|
||||
|
||||
All offsets are **into the packet payload**, i.e., after the 4-byte outer
|
||||
header (`[seq_hi][seq_lo][type][reserved=0]`).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user