노트북 07 — 레퍼런스#

CLI 명령과 파이썬 API 표면을 한 곳에 모아둡니다.

CLI 명령#

모든 명령은 PQMSG_HOME 환경 변수를 따릅니다(기본값: ~/.pq-messenger/). 해당 디렉터리 아래에 기록되는 상태: identity.json, contacts/<alias>.json, sessions/<peer>.json, inbox/<recipient>/<uuid>.json.

명령

목적

init

새로운 Ed25519 + X25519 + ML-KEM-768 신원 생성.

show-identity

활성 신원의 공개 컴포넌트만 출력.

export-contact

외부 채널 공유용 공개 전용 컨택트 파일 작성.

import-contact

상대방 컨택트 파일을 로컬 alias로 임포트.

send

메시지를 암호화하여 수신자의 inbox에 큐잉.

recv

우리에게 도착한 가장 오래된 메시지를 디큐 후 복호화.

show-keys

디버그: 특정 피어 세션의 현재 래칫 상태 출력.

reset

~/.pq-messenger/ 삭제 (모든 신원/컨택트/상태).

pqmsg init               --name TEXT
pqmsg show-identity
pqmsg export-contact     [--output PATH]
pqmsg import-contact PATH --as ALIAS
pqmsg send  RECIPIENT BODY
pqmsg recv               [--all]
pqmsg show-keys PEER
pqmsg reset

파이썬 API#

라이브러리 전체가 한 화면에 들어올 만큼 작습니다. 패키지 루트에서의 재노출은 없으니 서브모듈에서 직접 import 하세요.

pqmsg.identity#

Identity keypairs for pq-messenger.

Each identity owns three keypairs:
  • Ed25519 (signing) — reserved; unused in this scope but provisioned

  • X25519 (DH) — classical half of hybrid X3DH

  • ML-KEM-768 — post-quantum half of hybrid X3DH

class pqmsg.identity.Contact(name: str, x25519_public: bytes, ed25519_public: bytes, ml_kem_public: bytes)#

기반 클래스: object

Public-only projection of an Identity.

ed25519_public: bytes#
ml_kem_public: bytes#
name: str#
x25519_public: bytes#
class pqmsg.identity.Identity(name: 'str', x25519_public: 'bytes', x25519_private: 'bytes', ed25519_public: 'bytes', ed25519_private: 'bytes', ml_kem_public: 'bytes', ml_kem_private: 'bytes')#

기반 클래스: object

ed25519_private: bytes#
ed25519_public: bytes#
ml_kem_private: bytes#
ml_kem_public: bytes#
name: str#
x25519_private: bytes#
x25519_public: bytes#
pqmsg.identity.export_contact(ident: Identity, path: Path) None#
pqmsg.identity.generate_identity(name: str) Identity#
pqmsg.identity.import_contact(path: Path) Contact#
pqmsg.identity.load_identity(path: Path) Identity#
pqmsg.identity.save_identity(ident: Identity, path: Path) None#

pqmsg.session#

Hybrid X3DH + Double Ratchet session.

v0.2 adds the DH half of Signal’s Double Ratchet on top of the symmetric ratchet from v0.1: every time the conversation flips direction, the next sender generates a fresh X25519 keypair, attaches the public half to the message header, and mixes DH(new_eph, peer_dh_pub) into the root key.

Out-of-order delivery within a single sending burst is supported via a bounded skipped-key cache.

class pqmsg.session.Handshake(ephemeral_pk: 'bytes', kem_ciphertext: 'bytes')#

기반 클래스: object

ephemeral_pk: bytes#
kem_ciphertext: bytes#
class pqmsg.session.Session(peer_name: 'str', root_key: 'bytes', chain_key_send: 'bytes', chain_key_recv: 'bytes', dh_send_priv: 'bytes', dh_send_pub: 'bytes', dh_recv_pub: 'bytes | None' = None, send_index: 'int' = 0, recv_index: 'int' = 0, prev_send_count: 'int' = 0, skipped_keys: 'dict' = <factory>)#

기반 클래스: object

chain_key_recv: bytes#
chain_key_send: bytes#
dh_recv_pub: bytes | None = None#
dh_send_priv: bytes#
dh_send_pub: bytes#
peer_name: str#
prev_send_count: int = 0#
recv_index: int = 0#
root_key: bytes#
send_index: int = 0#
skipped_keys: dict#
pqmsg.session.accept_session(ours: Identity, peer_name: str, peer_x25519_pub: bytes, handshake: Handshake) Session#

Bob-side: decapsulate, complete DH, derive session keys.

Bob seeds his DH-ratchet send key with his own long-term X25519 pair initially (it matches Alice’s dh_recv_pub); on his first reply he’ll rotate to a fresh keypair and ratchet the root.

pqmsg.session.decrypt(session: Session, msg: dict) bytes#

Decrypt a message, applying DH ratchet on direction flip and consulting the skipped-key cache for out-of-order delivery.

pqmsg.session.encrypt(session: Session, plaintext: bytes, *, is_first: bool, handshake: Handshake | None) dict#

Advance the send chain (rotating DH if a new burst) and encrypt.

pqmsg.session.initiate_session(ours: Identity, peer_name: str, peer_x25519_pub: bytes, peer_ml_kem_pub: bytes) tuple[Session, Handshake]#

Alice-side: generate ephemeral, encapsulate, derive session keys.

The X3DH ephemeral doubles as Alice’s first DH-ratchet keypair. Bob’s long-term X25519 public is used as Bob’s first DH-ratchet pub until Bob sends his own message with a fresh DH pub.

pqmsg.kdf#

HKDF over SHAKE256.

We use SHAKE256 (variable-length XOF) so the protocol is consistent with ML-KEM’s internal hash choice. HKDF construction (RFC 5869 style):

PRK = HMAC(salt, ikm)
OKM = HMAC(PRK, info || counter) || ...

With SHAKE256 we collapse both steps into a single XOF invocation keyed by salt || ikm, domain-separated by info. This is a standard educational construction and is sufficient for our toy messenger.

pqmsg.kdf.derive_chain_step(chain_key: bytes) tuple[bytes, bytes]#

Advance the symmetric ratchet. Returns (message_key, next_chain_key).

pqmsg.kdf.hkdf_shake256(salt: bytes, ikm: bytes, info: bytes, length: int) bytes#

Derive length bytes of keying material from (salt, ikm, info).

pqmsg.transport#

Local-filesystem transport: one file per message, oldest-first FIFO.

Each message is written atomically (<name>.json.tmp -> rename -> <name>.json) so a concurrent reader never sees a partial file. We sort by mtime for a stable FIFO ordering.

pqmsg.transport.list_inbox(*, inbox_root: Path, recipient: str) list[Path]#

List pending messages in mtime order (oldest first). Ignores .tmp files.

pqmsg.transport.pop_message(*, inbox_root: Path, recipient: str) bytes | None#

Return and remove the oldest message, or None if inbox is empty.

pqmsg.transport.send_blob(*, inbox_root: Path, recipient: str, blob: bytes) Path#

Atomically write blob into recipient’s inbox. Returns final path.

pqmsg.encoding#

Wire format v2 for messages: JSON with base64-encoded byte fields.

v2 (added in pqmsg 0.2.0): every message carries the sender’s current DH public key (dh_pub) and the length of the previous send chain (prev_chain_length) so the receiver can run the DH ratchet and skip unread keys for out-of-order delivery. v1 messages are rejected.

exception pqmsg.encoding.MessageFormatError#

디스크 와이어 포맷#

큐에 저장되는 모든 메시지는 단일 JSON 파일입니다:

{
  "version": 1,
  "sender": "alice",
  "recipient": "bob",
  "msg_index": 0,
  "kem_ciphertext": "base64...",
  "ephemeral_pk": "base64...",
  "nonce": "base64...",
  "ciphertext": "base64...",
  "sent_at": "2026-04-22T15:30:00Z"
}

세션의 첫 메시지에만 kem_ciphertextephemeral_pk가 포함됩니다; 이후 메시지에서는 생략됩니다. 근거는 노트북 01 §와이어 포맷 참조.