Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
abe07d5
chore(tools): clean old installs before SP1 prep
xuandung38 Oct 30, 2026
1c71950
docs(tools): update pairing instructions from Wi-Fi to Bluetooth
xuandung38 Oct 30, 2026
ff9066e
chore(tools): add verify-android-ui.sh
xuandung38 Oct 30, 2026
4bda0ec
fix(tools): avoid unbound TEST_SUMMARY when gradlew is missing
xuandung38 Oct 30, 2026
17fb180
chore(tools): add release-push.sh and release-tag.sh
xuandung38 Oct 30, 2026
2d590a0
docs(spec): add Phase 7 BLE/Wi-Fi Direct offline connectivity design
xuandung38 Oct 30, 2026
7b622e1
docs(plan): mark Phase 7 started, scope cut to both-offline
xuandung38 Oct 30, 2026
8270089
docs(reports): add Phase 7 G7 spike numbers and SP1 measurement runbook
xuandung38 Oct 30, 2026
603dd70
test(tools): add verify-phase7.sh for Android+Apple Phase 7 checks
xuandung38 Oct 30, 2026
8d6b87d
test(tools): add SP1 real-device measurement scripts
xuandung38 Oct 30, 2026
3a38d1d
fix(tools): sync main by tree equality in release-tag.sh
xuandung38 Oct 30, 2026
facf1c3
fix(tools): check all repos before tagging in release-tag.sh
xuandung38 Oct 30, 2026
1ddc278
docs(changelog): correct Phase 7 0.2.0 entry to match shipped code
xuandung38 Oct 30, 2026
9a31fca
docs(roadmap): mark Phase 7 started, point workspace tips at v0.2.0
xuandung38 Oct 30, 2026
448f0d6
docs(claude): point Workspace tips at the 0.2.0 changelog commits
xuandung38 Oct 30, 2026
73962d2
docs(security): add v0.2.0 release scan report
xuandung38 Oct 30, 2026
218b185
fix(tools): make release-push.sh rerunnable and stop changing upstream
xuandung38 Oct 30, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ google-services.json
# Xuất tài liệu (tools/docs/build_design_html.py)
/build/

# Log cua tools/verify-phase7.sh
/.build-logs/

# Kho ".github" của org (hồ sơ, file cộng đồng mặc định) — clone vào đây bằng tools/workspace.sh clone
/.github-org/

Expand Down
90 changes: 90 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,96 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

## [0.2.0] — 2026-10-30

### Added

- **Offline connectivity (Phase 7):** when both devices are near each other without a network, they connect over Bluetooth
L2CAP. The control channel (clipboard text, SMS and call notifications) carries message frames over a link-security layer
(Noise-style handshake keyed by the shared pairing secret). No relay deployment in this release; scenario Y (phone on
mobile data, Mac on another Wi-Fi) also uses Bluetooth when the two are near. First pairing over Bluetooth: QR scan brings
up the credential, then the phone advertises a proof and waits for the Mac to establish the link, then the user can
confirm the pairing value or enter a PIN over Bluetooth through CPace (constant-time password-authenticated key exchange,
X25519, SHA-512).

- Bluetooth advertisement with a 128-bit service UUID and rotating hint (0.4.5 `BT_SERVICE_UUID`, `BT_HINT_PERIOD`,
service data format frozen by test vectors in `shared/test-vectors/`); link-security handshake (Noise prologue,
`challenge`, `noise1`, `noise2`) with frame codec (channel IDs, `more` flag, short and long control lanes, record
length bounds); Bluetooth admission policy (link cap, pre-auth connection cap, authentication failure block) on both
platforms (A-SVC advertiser on Android, CoreBluetooth central and peripheral on macOS).

- CPace password-authenticated key exchange for offline PIN pairing over Bluetooth (SP2, `CPACE-X25519-SHA512`): the shared
secret is derived from the PIN at both ends without a third party; online guessing is limited to one attempt per channel,
with a global per-PIN counter (three failures per PIN, then lockout); the spec and test vectors (cpace.json, Android
Kotlin and macOS Swift implementations with constant-time arithmetic in `core/crypto` or `HLCrypto`).

- **State machine update (0.11):** the Mac's connection manager now starts connecting when Bluetooth is authorized and on,
even without a network (new condition in the state machine); network loss ends LAN or relay sessions only, not Bluetooth
(so scenario X stays connected). Routes are prioritized: LAN > Bluetooth (on the Mac), and all pre-auth errors (PAIR_UNKNOWN,
PAIR_REVOKED, UNSUPPORTED_VERSION, AUTH_FAILED) are checked for TLS certification (`phoneAuthenticatedByTLS`), so only
the pinned-TLS LAN route can unpair, remove pairs, or stop reconnecting (AC8).

- **Pre-auth admission and Bluetooth bans (0.4.5, 0.5):** every pre-auth frame on Bluetooth is checked against a device-global
nonce challenge (issued once per connection attempt) and a link-key confirmation after the handshake. Failed confirmations
increment a counter; after `BT_AUTH_FAIL_BLOCK` consecutive failures, the pre-auth channel is blocked for `BT_AUTH_FAIL_TIMEOUT`,
preventing slot hogging and replay attacks.

- **Bluetooth prompts (AC7):** Android 12+ — decision logic for the one-time "Nearby devices" prompt (SET-01) and a
re-ask interval for enabling Bluetooth when there is no LAN; macOS — the system Bluetooth permission dialog
(`NSBluetoothAlwaysUsageDescription`) before scanning starts (SET-03). iOS prompts inherited from the existing Mac
integration.

- **Hub and shared assets for Phase 7:** detailed design spec updates (00-common-specs.md, 01-setup-settings.md, 02-pairing.md,
03-connectivity.md, 04-clipboard.md, all en and vi); test vectors for link-security, frame codec, advertisement and CPace;
new UI strings for the "Connected via Bluetooth" label and the Mac Bluetooth permission prompt text.

### Changed

- Device label in the connection status: "Connected via Bluetooth" appears on both Android and Mac when the active route is
Bluetooth (`core/design`'s `HLConnectionStatus` + `ui/devices` for Android, `HLMacUI`/`HLiOSUI` for Apple).

- Android foreground service `connectedDevice` (A-SVC): now advertises the Bluetooth service UUID and rotates the hint with the
shared secret, independent of the LAN server; the existing `ControlServer.serveRelayPeer` pattern is extended to `serveBluetoothPeer`
for `SessionTransport.BLUETOOTH`.

- macOS and iOS connection manager: `ConnectionManager` now routes to `.bluetooth` alongside `.relay`, with upgrades from Bluetooth
to LAN when a network appears; `ControlSession` enforces the pre-auth rule `phoneAuthenticatedByTLS` (originally only checking
`route == .relay`).

### Fixed

- ~~Relay routes kept in the code but not deployed; the `relay` constant and existing relay code paths are left untouched~~ (no
changes here; relay is intact for future gates G4, G5, G6 or if scenario Y demands it in a later phase).

### Security

- Offline pairing (PAIR-01 flow B and C): no pre-shared public key (QR-encoded `pr`) is required; the proof is an HMAC of the
pairing secret, and every pairing frame on a secured Bluetooth link is MAC'd with the link key before a slot is granted (D7.1).
PIN guessing is limited by the per-PIN counter (3 attempts, then lockout), and the CPace exchange runs at each attempt with the
PIN as the shared secret, so no stable state is exposed to a passive sniffer (AC9, `K_bt_pin` never leaves the channel).

- Bluetooth link-security (D7.1 0.4.5): an unbonded L2CAP CoC link is encrypted with a Noise-style handshake keyed by the shared
pairing secret (`K_bt_disc` and `K_bt_link`, derived once per link and rotated at an interval); link-layer encryption is
assumed to be absent (no bond), so frame-level encryption is mandatory. The prologue is bound to the peer's rotating hint and
the sender's discovery key, so a replayed or cloned advertiser cannot form a valid handshake (AC9).

- Pre-auth errors (CONN-01 E4, CONN-03 E9): forged PAIR_UNKNOWN, PAIR_REVOKED, UNSUPPORTED_VERSION or AUTH_FAILED on Bluetooth
cannot unpair, remove a pair, or stop reconnecting — only the pinned-TLS LAN route is exempt from this rule (AC8).

### Specs

Designed in the Phase 7 detailed-design update (00-common-specs.md 0.4.6/0.4.7, 01-setup-settings.md) but not implemented
on either platform yet — tracked for a later release:

- **Wi-Fi Direct** (A7.5 / M7.5): randomly-generated SSID and passphrase per group, the Mac socket bound to the P2P
interface only, and the Android 10–12 Location prompt it needs. No Android or Apple code exists for this today.
- **Per-connection bulk stream keys** (`K_bulk` and nonces, A7.4 / M7.4): a bulk transfer (images via CLIP-03) with its own
handshake and encryption keys per stream. Android's `core/transport` reserves channel `0x02` and the chunk/priority
scheduler for this lane, but nothing encrypts or sends bulk content through it yet; there is no Apple code at all.
- **Bluetooth status UI beyond the connected label:** the Mac "Bluetooth off" / "Bluetooth denied" status lines, their
settings button, the Bluetooth permission-primer screens and the Settings feature-card label all exist as strings in
`handlive-shared/strings/ui-strings.json`, but no Android or Apple view calls them yet.

## [0.1.1] — 2026-10-22

### Added
Expand Down
52 changes: 52 additions & 0 deletions CHANGELOG.vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,58 @@ Các thay đổi đáng chú ý của kho hub HandLive (tài liệu và kế ho

## [Unreleased]

## [0.2.0] — 30/10/2026

### Thêm mới

- **Kết nối khi không mạng (Phase 7):** khi cả hai thiết bị ở gần nhau mà không có mạng, chúng tự kết nối qua BLE L2CAP. Kênh điều khiển (sao chép bảng nhớ tạm, SMS và thông báo cuộc gọi) chở frame thông điệp qua một lớp bảo mật link (bắt tay Noise-style dùng secret chia sẻ từ ghép đôi). Phiên bản này không deploy relay; kịch bản Y (điện thoại dùng dữ liệu di động, Mac ở Wi-Fi khác) cũng dùng Bluetooth khi hai máy gần nhau. Ghép đôi lần đầu qua Bluetooth: quét QR mang thông tin xác thực, rồi điện thoại quảng bá bằng chứng và chờ Mac nối liên kết, rồi người dùng có thể xác nhận giá trị ghép đôi hoặc nhập PIN qua Bluetooth bằng CPace (trao đổi khóa xác thực mật khẩu hằng số, X25519, SHA-512).

- Quảng bá Bluetooth với UUID dịch vụ 128-bit và hint xoay vòng (0.4.5 `BT_SERVICE_UUID`, `BT_HINT_PERIOD`, định dạng dữ liệu dịch vụ chốt bằng vector kiểm tra trong `shared/test-vectors/`); bắt tay bảo mật link (Noise prologue, `challenge`, `noise1`, `noise2`) với codec frame (ID kênh, cờ `more`, kênh điều khiển ngắn và dài, giới hạn độ dài bản ghi); chính sách admission Bluetooth (giới hạn link, giới hạn kết nối pre-auth, khóa lỗi xác thực) trên cả hai nền tảng (advertiser A-SVC trên Android, central và peripheral CoreBluetooth trên macOS).

- CPace trao đổi khóa xác thực mật khẩu cho ghép đôi PIN offline qua Bluetooth (SP2, `CPACE-X25519-SHA512`): secret chia sẻ suy dẫn từ PIN ở cả hai đầu mà không cần bên thứ ba; phỏng đoán online chỉ được một lần mỗi kênh, với bộ đếm toàn cầu mỗi PIN (ba lần thất bại mỗi PIN, rồi khóa); spec và vector kiểm tra (cpace.json, cài đặt Kotlin Android và Swift macOS với số học hằng số trong `core/crypto` hoặc `HLCrypto`).

- **Cập nhật máy trạng thái (0.11):** trình quản lý kết nối của Mac giờ bắt đầu kết nối khi Bluetooth được phép và bật, thậm chí khi không có mạng (điều kiện mới trong máy trạng thái); mất mạng chỉ kết thúc phiên LAN hoặc relay, không phải Bluetooth (nên kịch bản X vẫn kết nối). Route được ưu tiên: LAN > Bluetooth (trên Mac), và mọi lỗi pre-auth (PAIR_UNKNOWN, PAIR_REVOKED, UNSUPPORTED_VERSION, AUTH_FAILED) được kiểm tra chứng chỉ TLS (`phoneAuthenticatedByTLS`), nên chỉ route LAN có TLS ghim mới có thể hủy ghép đôi, gỡ cặp, hay dừng nối lại (AC8).

- **Admission pre-auth và cấm Bluetooth (0.4.5, 0.5):** mọi frame pre-auth trên Bluetooth được kiểm tra với nonce challenge toàn cầu thiết bị (cấp một lần mỗi lần thử kết nối) và xác nhận khóa link sau bắt tay. Xác nhận thất bại tăng bộ đếm; sau `BT_AUTH_FAIL_BLOCK` lần thất bại liên tiếp, kênh pre-auth bị khóa trong `BT_AUTH_FAIL_TIMEOUT`, ngăn slot hogging và tấn công replay.

- **Prompt Bluetooth (AC7):** Android 12+ — logic quyết định cho prompt "Thiết bị gần đó" một lần (SET-01) và chu kỳ hỏi lại bật Bluetooth khi không có LAN; macOS — hộp thoại quyền Bluetooth của hệ thống (`NSBluetoothAlwaysUsageDescription`) trước khi quét (SET-03). Prompt iOS thừa kế từ tích hợp Mac hiện tại.

- **Tài sản hub và shared cho Phase 7:** cập nhật spec thiết kế chi tiết (00-common-specs.md, 01-setup-settings.md, 02-pairing.md, 03-connectivity.md, 04-clipboard.md, cả en và vi); vector kiểm tra cho bảo mật link, codec frame, quảng bá và CPace; chuỗi UI mới cho nhãn "Đã kết nối qua Bluetooth" và chữ prompt quyền Bluetooth trên Mac.

### Thay đổi

- Nhãn thiết bị trong trạng thái kết nối: "Đã kết nối qua Bluetooth" hiện trên cả Android và Mac khi route hoạt động là Bluetooth (`HLConnectionStatus` trong `core/design` + `ui/devices` cho Android, `HLMacUI`/`HLiOSUI` cho Apple).

- Dịch vụ foreground Android `connectedDevice` (A-SVC): giờ quảng bá UUID dịch vụ Bluetooth và xoay vòng hint với secret chia sẻ, độc lập với server LAN; mẫu `ControlServer.serveRelayPeer` hiện có được mở rộng sang `serveBluetoothPeer` cho `SessionTransport.BLUETOOTH`.

- Trình quản lý kết nối macOS và iOS: `ConnectionManager` giờ định tuyến sang `.bluetooth` bên cạnh `.relay`, với nâng cấp từ Bluetooth lên LAN khi mạng xuất hiện; `ControlSession` thực thi quy tắc pre-auth `phoneAuthenticatedByTLS` (ban đầu chỉ kiểm tra `route == .relay`).

### Fixed

- ~~Route relay giữ trong code nhưng không deploy; constant `relay` và route code relay hiện tại giữ nguyên~~ (không có thay đổi ở đây; relay toàn vẹn cho gate G4, G5, G6 sau hoặc nếu kịch bản Y đòi hỏi trong phase sau).

### Security

- Ghép đôi offline (PAIR-01 luồng B và C): không cần khóa công khai được chia sẻ trước (mã hóa `pr` trong QR); bằng chứng là HMAC của secret ghép đôi, và mọi frame ghép đôi trên liên kết Bluetooth bảo mật được MAC với khóa link trước khi cấp slot (D7.1). Phỏng đoán PIN bị giới hạn bằng bộ đếm mỗi PIN (3 lần, rồi khóa), và trao đổi CPace chạy ở mỗi lần thử với PIN là secret chia sẻ, nên không có state ổn định nào lộ ra cho sniffer bị động (AC9, `K_bt_pin` không bao giờ rời kênh).

- Bảo mật link Bluetooth (D7.1 0.4.5): liên kết L2CAP CoC chưa bond được mã hóa bằng bắt tay Noise-style khóa từ secret ghép đôi chia sẻ (`K_bt_disc` và `K_bt_link`, suy dẫn một lần mỗi link và xoay vòng ở khoảng thời gian); mã hóa tầng link giả định vắng mặt (không bond), nên mã hóa tầng frame là bắt buộc. Prologue được ràng buộc với hint xoay vòng của peer và khóa khám phá của người gửi, nên advertiser bị replay hoặc nhái không thể hình thành bắt tay hợp lệ (AC9).

- Lỗi pre-auth (CONN-01 E4, CONN-03 E9): PAIR_UNKNOWN, PAIR_REVOKED, UNSUPPORTED_VERSION hoặc AUTH_FAILED giả trên Bluetooth không thể hủy ghép đôi, gỡ cặp, hay dừng nối lại — chỉ route LAN có TLS ghim được miễn trừ quy tắc này (AC8).

### Specs

Đã thiết kế trong bản cập nhật spec Phase 7 (00-common-specs.md 0.4.6/0.4.7, 01-setup-settings.md) nhưng chưa code trên
nền tảng nào — để lại cho bản phát hành sau:

- **Wi-Fi Direct** (A7.5 / M7.5): SSID và passphrase sinh ngẫu nhiên mỗi nhóm, socket Mac ràng buộc chỉ với interface
P2P, và prompt Location Android 10–12 mà nó cần. Chưa có code Android hay Apple nào cho phần này.
- **Khóa stream bulk trên mỗi kết nối** (`K_bulk` và nonce, A7.4 / M7.4): truyền bulk (ảnh qua CLIP-03) với bắt tay và
khóa mã hóa riêng cho mỗi stream. `core/transport` của Android đã dành sẵn kênh `0x02` và bộ lập lịch chunk/ưu tiên
cho làn này, nhưng chưa có gì mã hóa hay gửi nội dung bulk qua đó; bên Apple chưa có code nào.
- **UI Bluetooth ngoài nhãn đã kết nối:** dòng trạng thái "Bluetooth tắt" / "Bluetooth bị từ chối" trên Mac, nút cài
đặt của nó, màn hình prompt quyền Bluetooth và nhãn feature-card trong Settings đều đã có chuỗi trong
`handlive-shared/strings/ui-strings.json`, nhưng chưa màn hình Android hay Apple nào gọi tới.

## [0.1.1] — 22/10/2026

### Thêm mới
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this
repository.

## Project status: Phase 0–3 on `main`; Phase 5–6 spikes on `main`; latest release `v0.1.1` (2026-10-22); G1/G2/G5/G6 open; G4 closed (Phase 4 stopped, owner decision 2026-10-12)
## Project status: Phase 0–3 on `main`; Phase 5–6 spikes on `main`; Phase 7 (Bluetooth offline connectivity) started 2026-10-28, BLE L2CAP pairing/control channel on `main`, release `v0.2.0` pending the owner's tag (Wi-Fi Direct and relay bulk dropped from this phase's scope); latest tagged release `v0.1.1` (2026-10-22); G1/G2/G5/G6 open; G4 closed (Phase 4 stopped, owner decision 2026-10-12)

Phase 0–3 product code is on `main` (clipboard, SMS, iOS shell, Rust relay, call metadata/control). Phase 4–6
**spike probes** are also on `main` (merged 2026-09-30): `HFPSpike`, `CameraSpike`, Android `tools/web-spike`,
Expand Down Expand Up @@ -61,7 +61,7 @@ project owner in Vietnamese with diacritics.

**Commit identity:** always `Hồ Xuân Dũng <me@hxd.vn>` (GitHub `xuandung38`). Never `dunghx1@viettel.com.vn` or any Viettel email in author/committer/`Signed-off-by`. Use `env -u CURSOR_AGENT git commit` so Cursor does not inject Co-authored-by trailers.

**Workspace tips to treat as current `main`** (tag `v0.1.1`, 2026-10-22): shared `56b4c93`, relay `17f367b`, android `8e4022a`, apple `098996c`, hub at the `v0.1.1` tag. Confirm with `tools/workspace.sh status` before coding. **Local JDK:** Gradle needs `JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home` in a non-login shell, else `gradlew` prints "Unable to locate a Java Runtime" and a piped command still exits 0.
**Workspace tips to treat as current `main`** (ahead of tag `v0.1.1`; release `v0.2.0` pending the owner's tag, Phase 7 Bluetooth offline connectivity): shared `5cbe4aa`, relay `17f367b` (unchanged since `v0.1.1`), android `ed8e41f`, apple `7d3ce0b`, hub `main` tip. Confirm with `tools/workspace.sh status` before coding. **Local JDK:** Gradle needs `JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home` in a non-login shell, else `gradlew` prints "Unable to locate a Java Runtime" and a piped command still exits 0.

### In progress / blocked (owner or gate)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Phases ship in order, each as a usable slice. One exception (owner, 2026-09-28):
| **4** Call audio | Bluetooth HFP/SCO and Opus fallback | **Stopped** (owner decision, 2026-10-12) · G4 closed: only macOS 27 is available, so calls stay notification + answer/decline/end, no audio |
| **5** Virtual camera / mic | CMIOExtension and AudioServerPlugin | **Spike on `main`** (`CameraSpike`) · G5 needs a paid Apple Developer team |
| **6** Continue Browsing | Foreground URL handover across devices | **Spike on `main`** · Chrome and Samsung Internet measured; browser go/no-go pending |
| **7** Connect anywhere | Auto link without a shared Wi-Fi network | **Proposed** (2026-10-01) · starts after G5 and G6 (G4 closed 2026-10-12) with spike G7 |
| **7** Connect anywhere | Auto link without a shared Wi-Fi network | **Started** (owner decision, 2026-10-28), no wait on G4/G5/G6 · spike G7 done; Bluetooth L2CAP pairing and control channel on `main`, Wi-Fi Direct and relay bulk dropped from this phase's scope |
| **G0** Vectors | Shared cross-platform crypto test vectors | **Done** |
| **G1** Real devices | Real-device Phase 1 matrix validation | **Open** (before 1.0) · App Store Connect screenshots recaptured at native 1320×2868 (2026-10-12) |
| **G2** Play Console | SMS and call log permissions declaration | **Open** (before 1.0; Plan B: F-Droid/APK) · Play Console screenshots recaptured at 1080×1920 (2026-10-12) |
Expand Down
Loading
Loading