Skip to main content

netget/server/spice/
wire.rs

1//! The SPICE wire format, shared by NetGet's server and client.
2//!
3//! Everything here is little-endian and packed, as `spice/protocol.h` and `spice.proto`
4//! (spice-protocol / spice-common) define it:
5//!
6//! - **Link stage**, once per channel connection: `SpiceLinkHeader` (`REDQ`, 2.2, size), the
7//!   client's `SpiceLinkMess` (connection id, channel type and id, capabilities), the server's
8//!   `SpiceLinkReply` (error, a 1024-bit RSA public key as an X.509 SubjectPublicKeyInfo,
9//!   capabilities), the client's `SpiceLinkAuthMechanism` when both sides offer
10//!   `AUTH_SELECTION`, the 128-byte RSA-OAEP ticket, and the server's 4-byte link result.
11//! - **Data stage**: every message is a header and a body. With `MINI_HEADER` negotiated by
12//!   both sides the header is `type: u16, size: u32`; without it, it is
13//!   `serial: u64, type: u16, size: u32, sub_list: u32`.
14//!
15//! The ticket is the password, NUL-terminated, encrypted with RSA PKCS#1 OAEP (SHA-1 and
16//! MGF1-SHA-1, OpenSSL's `RSA_PKCS1_OAEP_PADDING`) under the server's key. That is all SPICE's
17//! own authentication is: it hides the password from a passive observer of the link, and it
18//! authenticates nothing about the server.
19use anyhow::{anyhow, bail, ensure, Context, Result};
20use tokio::io::{AsyncRead, AsyncReadExt};
21
22/// `SPICE_MAGIC`: the four bytes `REDQ`.
23pub const MAGIC: [u8; 4] = *b"REDQ";
24pub const VERSION_MAJOR: u32 = 2;
25pub const VERSION_MINOR: u32 = 2;
26/// `SpiceLinkHeader`: magic, major, minor, size.
27pub const LINK_HEADER_BYTES: usize = 16;
28/// The fixed part of `SpiceLinkMess`, before its capability words.
29pub const LINK_MESS_BYTES: usize = 18;
30/// The fixed part of `SpiceLinkReply`, before its capability words.
31pub const LINK_REPLY_BYTES: usize = 4 + TICKET_PUBKEY_BYTES + 12;
32/// `SPICE_TICKET_PUBKEY_BYTES`: a 1024-bit RSA key as DER SubjectPublicKeyInfo.
33pub const TICKET_PUBKEY_BYTES: usize = 162;
34/// An RSA-1024 ciphertext.
35pub const TICKET_BYTES: usize = 128;
36/// `SPICE_MAX_PASSWORD_LENGTH`.
37pub const MAX_PASSWORD_LENGTH: usize = 60;
38/// The largest link message (header `size`) either side reads. A real one is 26 to ~200
39/// bytes; the size is a peer-chosen `u32`, refused against this before anything is allocated.
40pub const MAX_LINK_BYTES: u32 = 4096;
41/// The most capability words either side accepts in one list. Every capability defined today
42/// fits in one word; the count is a peer-chosen `u32`.
43pub const MAX_CAP_WORDS: u32 = 64;
44pub const MINI_HEADER_BYTES: usize = 6;
45pub const FULL_HEADER_BYTES: usize = 18;
46
47/// Common capabilities (`SPICE_COMMON_CAP_*`), as bit numbers.
48pub mod common_cap {
49    pub const AUTH_SELECTION: u32 = 0;
50    pub const AUTH_SPICE: u32 = 1;
51    pub const AUTH_SASL: u32 = 2;
52    pub const MINI_HEADER: u32 = 3;
53}
54
55/// `SpiceLinkAuthMechanism` values.
56pub const AUTH_MECHANISM_SPICE: u32 = 1;
57
58/// Main channel capabilities (`SPICE_MAIN_CAP_*`).
59pub mod main_cap {
60    pub const NAME_AND_UUID: u32 = 1;
61}
62
63/// Display channel capabilities (`SPICE_DISPLAY_CAP_*`).
64pub mod display_cap {
65    pub const PREF_COMPRESSION: u32 = 6;
66}
67
68/// Channel types (`SPICE_CHANNEL_*`).
69pub mod channel {
70    pub const MAIN: u8 = 1;
71    pub const DISPLAY: u8 = 2;
72    pub const INPUTS: u8 = 3;
73    pub const CURSOR: u8 = 4;
74    pub const PLAYBACK: u8 = 5;
75    pub const RECORD: u8 = 6;
76
77    pub fn name(t: u8) -> &'static str {
78        match t {
79            MAIN => "main",
80            DISPLAY => "display",
81            INPUTS => "inputs",
82            CURSOR => "cursor",
83            PLAYBACK => "playback",
84            RECORD => "record",
85            8 => "smartcard",
86            9 => "usbredir",
87            10 => "port",
88            11 => "webdav",
89            _ => "unknown",
90        }
91    }
92}
93
94/// Link errors (`SPICE_LINK_ERR_*`), carried in `SpiceLinkReply.error` and the link result.
95pub mod link_err {
96    pub const OK: u32 = 0;
97    pub const ERROR: u32 = 1;
98    pub const INVALID_MAGIC: u32 = 2;
99    pub const INVALID_DATA: u32 = 3;
100    pub const VERSION_MISMATCH: u32 = 4;
101    pub const NEED_SECURED: u32 = 5;
102    pub const NEED_UNSECURED: u32 = 6;
103    pub const PERMISSION_DENIED: u32 = 7;
104    pub const BAD_CONNECTION_ID: u32 = 8;
105    pub const CHANNEL_NOT_AVAILABLE: u32 = 9;
106
107    pub fn name(code: u32) -> &'static str {
108        match code {
109            OK => "ok",
110            ERROR => "error",
111            INVALID_MAGIC => "invalid magic",
112            INVALID_DATA => "invalid data",
113            VERSION_MISMATCH => "version mismatch",
114            NEED_SECURED => "the server requires TLS",
115            NEED_UNSECURED => "the server requires a plain connection",
116            PERMISSION_DENIED => "permission denied",
117            BAD_CONNECTION_ID => "bad connection id",
118            CHANNEL_NOT_AVAILABLE => "channel not available",
119            _ => "unknown link error",
120        }
121    }
122}
123
124/// Server-to-client message types (`SPICE_MSG_*`).
125pub mod msg {
126    pub const SET_ACK: u16 = 3;
127    pub const PING: u16 = 4;
128    pub const WAIT_FOR_CHANNELS: u16 = 5;
129    pub const DISCONNECTING: u16 = 6;
130    pub const NOTIFY: u16 = 7;
131
132    pub const MAIN_INIT: u16 = 103;
133    pub const MAIN_CHANNELS_LIST: u16 = 104;
134    pub const MAIN_MOUSE_MODE: u16 = 105;
135    pub const MAIN_MULTI_MEDIA_TIME: u16 = 106;
136    pub const MAIN_AGENT_CONNECTED: u16 = 107;
137    pub const MAIN_AGENT_DISCONNECTED: u16 = 108;
138    pub const MAIN_NAME: u16 = 113;
139    pub const MAIN_UUID: u16 = 114;
140
141    pub const DISPLAY_MODE: u16 = 101;
142    pub const DISPLAY_MARK: u16 = 102;
143    pub const DISPLAY_RESET: u16 = 103;
144    pub const DISPLAY_COPY_BITS: u16 = 104;
145    pub const DISPLAY_INVAL_LIST: u16 = 105;
146    pub const DISPLAY_INVAL_ALL_PIXMAPS: u16 = 106;
147    pub const DISPLAY_INVAL_PALETTE: u16 = 107;
148    pub const DISPLAY_INVAL_ALL_PALETTES: u16 = 108;
149    pub const DISPLAY_STREAM_CREATE: u16 = 122;
150    pub const DISPLAY_STREAM_DATA: u16 = 123;
151    pub const DISPLAY_STREAM_CLIP: u16 = 124;
152    pub const DISPLAY_STREAM_DESTROY: u16 = 125;
153    pub const DISPLAY_STREAM_DESTROY_ALL: u16 = 126;
154    pub const DISPLAY_DRAW_FILL: u16 = 302;
155    pub const DISPLAY_DRAW_OPAQUE: u16 = 303;
156    pub const DISPLAY_DRAW_COPY: u16 = 304;
157    pub const DISPLAY_DRAW_BLEND: u16 = 305;
158    pub const DISPLAY_DRAW_BLACKNESS: u16 = 306;
159    pub const DISPLAY_DRAW_WHITENESS: u16 = 307;
160    pub const DISPLAY_DRAW_INVERS: u16 = 308;
161    pub const DISPLAY_DRAW_ROP3: u16 = 309;
162    pub const DISPLAY_DRAW_STROKE: u16 = 310;
163    pub const DISPLAY_DRAW_TEXT: u16 = 311;
164    pub const DISPLAY_DRAW_TRANSPARENT: u16 = 312;
165    pub const DISPLAY_DRAW_ALPHA_BLEND: u16 = 313;
166    pub const DISPLAY_SURFACE_CREATE: u16 = 314;
167    pub const DISPLAY_SURFACE_DESTROY: u16 = 315;
168    pub const DISPLAY_STREAM_DATA_SIZED: u16 = 316;
169    pub const DISPLAY_MONITORS_CONFIG: u16 = 317;
170    pub const DISPLAY_DRAW_COMPOSITE: u16 = 318;
171
172    pub const INPUTS_INIT: u16 = 101;
173    pub const INPUTS_KEY_MODIFIERS: u16 = 102;
174    pub const INPUTS_MOUSE_MOTION_ACK: u16 = 111;
175}
176
177/// Client-to-server message types (`SPICE_MSGC_*`).
178pub mod msgc {
179    pub const ACK_SYNC: u16 = 1;
180    pub const ACK: u16 = 2;
181    pub const PONG: u16 = 3;
182    pub const DISCONNECTING: u16 = 6;
183
184    pub const MAIN_CLIENT_INFO: u16 = 101;
185    pub const MAIN_ATTACH_CHANNELS: u16 = 104;
186    pub const MAIN_MOUSE_MODE_REQUEST: u16 = 105;
187    pub const MAIN_AGENT_START: u16 = 106;
188    pub const MAIN_AGENT_DATA: u16 = 107;
189    pub const MAIN_AGENT_TOKEN: u16 = 108;
190
191    pub const DISPLAY_INIT: u16 = 101;
192    pub const DISPLAY_STREAM_REPORT: u16 = 102;
193    pub const DISPLAY_PREFERRED_COMPRESSION: u16 = 103;
194
195    pub const INPUTS_KEY_DOWN: u16 = 101;
196    pub const INPUTS_KEY_UP: u16 = 102;
197    pub const INPUTS_KEY_MODIFIERS: u16 = 103;
198    pub const INPUTS_KEY_SCANCODE: u16 = 104;
199    pub const INPUTS_MOUSE_MOTION: u16 = 111;
200    pub const INPUTS_MOUSE_POSITION: u16 = 112;
201    pub const INPUTS_MOUSE_PRESS: u16 = 113;
202    pub const INPUTS_MOUSE_RELEASE: u16 = 114;
203}
204
205/// Mouse modes (`SPICE_MOUSE_MODE_*`): bits in `supported_mouse_modes`.
206pub const MOUSE_MODE_SERVER: u32 = 1;
207pub const MOUSE_MODE_CLIENT: u32 = 2;
208
209/// Mouse buttons (`SPICE_MOUSE_BUTTON_*`) and the matching `buttons_state` bits.
210pub mod button {
211    pub const LEFT: u8 = 1;
212    pub const MIDDLE: u8 = 2;
213    pub const RIGHT: u8 = 3;
214    pub const UP: u8 = 4;
215    pub const DOWN: u8 = 5;
216
217    pub fn name(b: u8) -> &'static str {
218        match b {
219            LEFT => "left",
220            MIDDLE => "middle",
221            RIGHT => "right",
222            UP => "scroll_up",
223            DOWN => "scroll_down",
224            _ => "other",
225        }
226    }
227
228    /// The `buttons_state` bit for a button, 0 for the wheel and unknown buttons.
229    pub fn mask(b: u8) -> u16 {
230        match b {
231            LEFT => 1,
232            MIDDLE => 2,
233            RIGHT => 4,
234            _ => 0,
235        }
236    }
237
238    pub fn names_in(state: u16) -> Vec<&'static str> {
239        [(1u16, "left"), (2, "middle"), (4, "right")]
240            .iter()
241            .filter(|(bit, _)| state & bit != 0)
242            .map(|(_, n)| *n)
243            .collect()
244    }
245}
246
247/// Surface formats (`SPICE_SURFACE_FMT_*`).
248pub const SURFACE_FMT_32_XRGB: u32 = 32;
249pub const SURFACE_FLAGS_PRIMARY: u32 = 1;
250
251/// Image types (`SPICE_IMAGE_TYPE_*`).
252pub mod image_type {
253    pub const BITMAP: u8 = 0;
254    pub const QUIC: u8 = 1;
255    pub const LZ_PLT: u8 = 100;
256    pub const LZ_RGB: u8 = 101;
257    pub const GLZ_RGB: u8 = 102;
258    pub const FROM_CACHE: u8 = 103;
259    pub const SURFACE: u8 = 104;
260    pub const JPEG: u8 = 105;
261    pub const FROM_CACHE_LOSSLESS: u8 = 106;
262    pub const ZLIB_GLZ_RGB: u8 = 107;
263    pub const JPEG_ALPHA: u8 = 108;
264    pub const LZ4: u8 = 109;
265
266    pub fn name(t: u8) -> &'static str {
267        match t {
268            BITMAP => "bitmap",
269            QUIC => "quic",
270            LZ_PLT => "lz_plt",
271            LZ_RGB => "lz_rgb",
272            GLZ_RGB => "glz_rgb",
273            FROM_CACHE => "from_cache",
274            SURFACE => "surface",
275            JPEG => "jpeg",
276            FROM_CACHE_LOSSLESS => "from_cache_lossless",
277            ZLIB_GLZ_RGB => "zlib_glz_rgb",
278            JPEG_ALPHA => "jpeg_alpha",
279            LZ4 => "lz4",
280            _ => "unknown",
281        }
282    }
283}
284
285/// `SPICE_IMAGE_FLAGS_CACHE_ME`.
286pub const IMAGE_FLAGS_CACHE_ME: u8 = 1;
287/// Bitmap formats (`SPICE_BITMAP_FMT_*`) this code reads or writes.
288pub const BITMAP_FMT_24BIT: u8 = 7;
289pub const BITMAP_FMT_32BIT: u8 = 8;
290pub const BITMAP_FMT_RGBA: u8 = 9;
291/// `SPICE_BITMAP_FLAGS_PAL_FROM_CACHE`, `SPICE_BITMAP_FLAGS_TOP_DOWN`.
292pub const BITMAP_FLAGS_PAL_FROM_CACHE: u8 = 2;
293pub const BITMAP_FLAGS_TOP_DOWN: u8 = 4;
294/// `SPICE_ROPD_OP_PUT`.
295pub const ROPD_OP_PUT: u16 = 1 << 3;
296/// `SPICE_IMAGE_SCALE_MODE_NEAREST`.
297pub const SCALE_MODE_NEAREST: u8 = 1;
298/// `SPICE_IMAGE_COMPRESSION_OFF`, for `SPICE_MSGC_DISPLAY_PREFERRED_COMPRESSION`.
299pub const IMAGE_COMPRESSION_OFF: u8 = 1;
300
301/// Notify severities (`SPICE_NOTIFY_SEVERITY_*`) and visibility.
302pub const NOTIFY_SEVERITY_INFO: u32 = 0;
303pub const NOTIFY_SEVERITY_WARN: u32 = 1;
304pub const NOTIFY_SEVERITY_ERROR: u32 = 2;
305pub const NOTIFY_VISIBILITY_HIGH: u32 = 2;
306
307pub fn severity_name(s: u32) -> &'static str {
308    match s {
309        NOTIFY_SEVERITY_INFO => "info",
310        NOTIFY_SEVERITY_WARN => "warn",
311        NOTIFY_SEVERITY_ERROR => "error",
312        _ => "unknown",
313    }
314}
315
316// ---------------------------------------------------------------------------------------------
317// Little-endian cursor
318// ---------------------------------------------------------------------------------------------
319
320/// A bounds-checked little-endian reader over a message body. Every read either returns the
321/// value or an error naming the field: nothing here indexes past the end.
322pub struct Cursor<'a> {
323    data: &'a [u8],
324    pos: usize,
325}
326
327impl<'a> Cursor<'a> {
328    pub fn new(data: &'a [u8]) -> Self {
329        Self { data, pos: 0 }
330    }
331    pub fn at(data: &'a [u8], pos: usize) -> Self {
332        Self { data, pos }
333    }
334    pub fn pos(&self) -> usize {
335        self.pos
336    }
337    pub fn remaining(&self) -> usize {
338        self.data.len().saturating_sub(self.pos)
339    }
340    pub fn bytes(&mut self, n: usize, what: &str) -> Result<&'a [u8]> {
341        let end = self
342            .pos
343            .checked_add(n)
344            .filter(|e| *e <= self.data.len())
345            .ok_or_else(|| anyhow!("{what}: {n} bytes needed, {} left", self.remaining()))?;
346        let out = &self.data[self.pos..end];
347        self.pos = end;
348        Ok(out)
349    }
350    pub fn u8(&mut self, what: &str) -> Result<u8> {
351        Ok(self.bytes(1, what)?[0])
352    }
353    pub fn u16(&mut self, what: &str) -> Result<u16> {
354        let b = self.bytes(2, what)?;
355        Ok(u16::from_le_bytes([b[0], b[1]]))
356    }
357    pub fn u32(&mut self, what: &str) -> Result<u32> {
358        let b = self.bytes(4, what)?;
359        Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
360    }
361    pub fn i32(&mut self, what: &str) -> Result<i32> {
362        Ok(self.u32(what)? as i32)
363    }
364    pub fn u64(&mut self, what: &str) -> Result<u64> {
365        let b = self.bytes(8, what)?;
366        let mut a = [0u8; 8];
367        a.copy_from_slice(b);
368        Ok(u64::from_le_bytes(a))
369    }
370}
371
372/// Whether capability `bit` is set in a list of capability words.
373pub fn has_cap(caps: &[u32], bit: u32) -> bool {
374    caps.get((bit / 32) as usize)
375        .is_some_and(|w| w & (1 << (bit % 32)) != 0)
376}
377
378/// One capability word with the given bits set.
379pub fn caps_word(bits: &[u32]) -> u32 {
380    bits.iter()
381        .filter(|b| **b < 32)
382        .fold(0, |w, b| w | (1 << b))
383}
384
385// ---------------------------------------------------------------------------------------------
386// Link stage
387// ---------------------------------------------------------------------------------------------
388
389/// A `SpiceLinkHeader` around a link body.
390pub fn link_header(size: u32) -> Vec<u8> {
391    let mut v = Vec::with_capacity(LINK_HEADER_BYTES);
392    v.extend_from_slice(&MAGIC);
393    v.extend_from_slice(&VERSION_MAJOR.to_le_bytes());
394    v.extend_from_slice(&VERSION_MINOR.to_le_bytes());
395    v.extend_from_slice(&size.to_le_bytes());
396    v
397}
398
399/// Why a link header was refused, as the `SPICE_LINK_ERR_*` to answer with.
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub struct LinkHeaderError(pub u32);
402
403/// Check a link header and return the size of the body that follows. The size is refused
404/// against [`MAX_LINK_BYTES`] here, before anything is read or allocated for it.
405pub fn parse_link_header(h: &[u8; LINK_HEADER_BYTES], min: usize) -> Result<u32, LinkHeaderError> {
406    if h[0..4] != MAGIC {
407        return Err(LinkHeaderError(link_err::INVALID_MAGIC));
408    }
409    let major = u32::from_le_bytes([h[4], h[5], h[6], h[7]]);
410    if major != VERSION_MAJOR {
411        return Err(LinkHeaderError(link_err::VERSION_MISMATCH));
412    }
413    let size = u32::from_le_bytes([h[12], h[13], h[14], h[15]]);
414    if (size as usize) < min || size > MAX_LINK_BYTES {
415        return Err(LinkHeaderError(link_err::INVALID_DATA));
416    }
417    Ok(size)
418}
419
420/// Read a capability list of `common` then `channel` words at `offset` within a link body.
421fn read_caps(body: &[u8], common: u32, channel: u32, offset: u32) -> Result<(Vec<u32>, Vec<u32>)> {
422    ensure!(
423        common <= MAX_CAP_WORDS && channel <= MAX_CAP_WORDS,
424        "{common} common and {channel} channel capability words; at most {MAX_CAP_WORDS} each"
425    );
426    let mut c = Cursor::at(body, offset as usize);
427    let mut read = |n: u32| -> Result<Vec<u32>> { (0..n).map(|_| c.u32("capability")).collect() };
428    let common = read(common)?;
429    let channel = read(channel)?;
430    Ok((common, channel))
431}
432
433/// The client's `SpiceLinkMess`.
434#[derive(Debug, Clone, PartialEq, Eq)]
435pub struct LinkMess {
436    pub connection_id: u32,
437    pub channel_type: u8,
438    pub channel_id: u8,
439    pub common_caps: Vec<u32>,
440    pub channel_caps: Vec<u32>,
441}
442
443impl LinkMess {
444    /// Header and body, ready to write.
445    pub fn encode(&self) -> Vec<u8> {
446        let caps = (self.common_caps.len() + self.channel_caps.len()) * 4;
447        let mut v = link_header((LINK_MESS_BYTES + caps) as u32);
448        v.extend_from_slice(&self.connection_id.to_le_bytes());
449        v.push(self.channel_type);
450        v.push(self.channel_id);
451        v.extend_from_slice(&(self.common_caps.len() as u32).to_le_bytes());
452        v.extend_from_slice(&(self.channel_caps.len() as u32).to_le_bytes());
453        v.extend_from_slice(&(LINK_MESS_BYTES as u32).to_le_bytes());
454        for w in self.common_caps.iter().chain(&self.channel_caps) {
455            v.extend_from_slice(&w.to_le_bytes());
456        }
457        v
458    }
459
460    pub fn decode(body: &[u8]) -> Result<Self> {
461        let mut c = Cursor::new(body);
462        let connection_id = c.u32("connection id")?;
463        let channel_type = c.u8("channel type")?;
464        let channel_id = c.u8("channel id")?;
465        let common = c.u32("common caps count")?;
466        let channel = c.u32("channel caps count")?;
467        let offset = c.u32("caps offset")?;
468        ensure!(
469            offset as usize >= LINK_MESS_BYTES,
470            "capabilities at offset {offset} overlap the link message"
471        );
472        let (common_caps, channel_caps) = read_caps(body, common, channel, offset)?;
473        Ok(Self {
474            connection_id,
475            channel_type,
476            channel_id,
477            common_caps,
478            channel_caps,
479        })
480    }
481}
482
483/// The server's `SpiceLinkReply`.
484#[derive(Debug, Clone, PartialEq, Eq)]
485pub struct LinkReply {
486    pub error: u32,
487    pub pubkey: Vec<u8>,
488    pub common_caps: Vec<u32>,
489    pub channel_caps: Vec<u32>,
490}
491
492impl LinkReply {
493    pub fn encode(&self) -> Vec<u8> {
494        let caps = (self.common_caps.len() + self.channel_caps.len()) * 4;
495        let mut v = link_header((LINK_REPLY_BYTES + caps) as u32);
496        v.extend_from_slice(&self.error.to_le_bytes());
497        let mut key = [0u8; TICKET_PUBKEY_BYTES];
498        let n = self.pubkey.len().min(TICKET_PUBKEY_BYTES);
499        key[..n].copy_from_slice(&self.pubkey[..n]);
500        v.extend_from_slice(&key);
501        v.extend_from_slice(&(self.common_caps.len() as u32).to_le_bytes());
502        v.extend_from_slice(&(self.channel_caps.len() as u32).to_le_bytes());
503        v.extend_from_slice(&(LINK_REPLY_BYTES as u32).to_le_bytes());
504        for w in self.common_caps.iter().chain(&self.channel_caps) {
505            v.extend_from_slice(&w.to_le_bytes());
506        }
507        v
508    }
509
510    /// An error-only reply, as spice-server sends one: no key, no capabilities.
511    pub fn error(code: u32) -> Vec<u8> {
512        Self {
513            error: code,
514            pubkey: Vec::new(),
515            common_caps: Vec::new(),
516            channel_caps: Vec::new(),
517        }
518        .encode()
519    }
520
521    pub fn decode(body: &[u8]) -> Result<Self> {
522        let mut c = Cursor::new(body);
523        let error = c.u32("link error")?;
524        if error != link_err::OK {
525            return Ok(Self {
526                error,
527                pubkey: Vec::new(),
528                common_caps: Vec::new(),
529                channel_caps: Vec::new(),
530            });
531        }
532        let pubkey = c.bytes(TICKET_PUBKEY_BYTES, "public key")?.to_vec();
533        let common = c.u32("common caps count")?;
534        let channel = c.u32("channel caps count")?;
535        let offset = c.u32("caps offset")?;
536        ensure!(
537            offset as usize >= LINK_REPLY_BYTES,
538            "capabilities at offset {offset} overlap the link reply"
539        );
540        let (common_caps, channel_caps) = read_caps(body, common, channel, offset)?;
541        Ok(Self {
542            error,
543            pubkey,
544            common_caps,
545            channel_caps,
546        })
547    }
548}
549
550// ---------------------------------------------------------------------------------------------
551// The ticket
552// ---------------------------------------------------------------------------------------------
553
554/// A fresh RSA-1024 key, as spice-server makes one for the link (`SPICE_TICKET_KEY_PAIR_LENGTH`).
555pub fn generate_ticket_key() -> Result<rsa::RsaPrivateKey> {
556    rsa::RsaPrivateKey::new(&mut rand::rngs::OsRng, 1024).context("generating the ticket key")
557}
558
559/// The public half as the 162-byte X.509 SubjectPublicKeyInfo the link reply carries.
560pub fn ticket_public_key(key: &rsa::RsaPrivateKey) -> Result<Vec<u8>> {
561    use rsa::pkcs8::EncodePublicKey;
562    let der = key
563        .to_public_key()
564        .to_public_key_der()
565        .context("encoding the ticket key")?;
566    let bytes = der.as_bytes().to_vec();
567    ensure!(
568        bytes.len() == TICKET_PUBKEY_BYTES,
569        "the ticket key encodes to {} bytes, not {TICKET_PUBKEY_BYTES}",
570        bytes.len()
571    );
572    Ok(bytes)
573}
574
575/// Encrypt `password` (NUL-terminated, as spice-gtk sends it) under the server's key.
576pub fn encrypt_ticket(pubkey_der: &[u8], password: &str) -> Result<Vec<u8>> {
577    use rsa::pkcs8::DecodePublicKey;
578    ensure!(
579        password.len() <= MAX_PASSWORD_LENGTH,
580        "a SPICE password is at most {MAX_PASSWORD_LENGTH} bytes"
581    );
582    let key = rsa::RsaPublicKey::from_public_key_der(pubkey_der)
583        .context("the server's ticket key is not an RSA SubjectPublicKeyInfo")?;
584    let mut plain = password.as_bytes().to_vec();
585    plain.push(0);
586    key.encrypt(
587        &mut rand::rngs::OsRng,
588        rsa::Oaep::new::<sha1::Sha1>(),
589        &plain,
590    )
591    .context("encrypting the ticket")
592}
593
594/// Decrypt a ticket and return the password bytes before the first NUL, or `None` when the
595/// ciphertext does not decrypt under this key.
596pub fn decrypt_ticket(key: &rsa::RsaPrivateKey, ticket: &[u8]) -> Option<Vec<u8>> {
597    let plain = key.decrypt(rsa::Oaep::new::<sha1::Sha1>(), ticket).ok()?;
598    let end = plain.iter().position(|b| *b == 0).unwrap_or(plain.len());
599    Some(plain[..end].to_vec())
600}
601
602/// Compare two secrets without stopping at the first difference.
603pub fn secrets_equal(a: &[u8], b: &[u8]) -> bool {
604    let mut diff = u8::from(a.len() != b.len());
605    for i in 0..a.len().max(b.len()) {
606        diff |= a.get(i).copied().unwrap_or(0) ^ b.get(i).copied().unwrap_or(0);
607    }
608    diff == 0
609}
610
611// ---------------------------------------------------------------------------------------------
612// Data stage
613// ---------------------------------------------------------------------------------------------
614
615/// A data header: the mini form (`type, size`) or the full one (`serial, type, size, sub_list`).
616pub fn data_header(mini: bool, serial: u64, msg_type: u16, size: u32) -> Vec<u8> {
617    let mut v = Vec::with_capacity(FULL_HEADER_BYTES);
618    if !mini {
619        v.extend_from_slice(&serial.to_le_bytes());
620    }
621    v.extend_from_slice(&msg_type.to_le_bytes());
622    v.extend_from_slice(&size.to_le_bytes());
623    if !mini {
624        v.extend_from_slice(&0u32.to_le_bytes());
625    }
626    v
627}
628
629/// Why a data message could not be read.
630#[derive(Debug)]
631pub enum ReadError {
632    /// The header declared a body larger than the reader accepts; nothing was read for it.
633    TooLarge {
634        msg_type: u16,
635        size: u32,
636        max: u32,
637    },
638    /// The full header carried a sub-message list, which nothing sends any more.
639    SubList,
640    Io(std::io::Error),
641}
642
643impl std::fmt::Display for ReadError {
644    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
645        match self {
646            ReadError::TooLarge {
647                msg_type,
648                size,
649                max,
650            } => write!(
651                f,
652                "message type {msg_type} declares {size} bytes, above the {max} byte limit"
653            ),
654            ReadError::SubList => write!(f, "a message carried a sub-message list"),
655            ReadError::Io(e) => write!(f, "{e}"),
656        }
657    }
658}
659
660impl std::error::Error for ReadError {}
661
662impl From<std::io::Error> for ReadError {
663    fn from(e: std::io::Error) -> Self {
664        ReadError::Io(e)
665    }
666}
667
668/// Read one data message. The declared size is refused against `max` before the body buffer
669/// is allocated.
670pub async fn read_message<R: AsyncRead + Unpin>(
671    r: &mut R,
672    mini: bool,
673    max: u32,
674) -> std::result::Result<(u16, Vec<u8>), ReadError> {
675    let mut h = [0u8; FULL_HEADER_BYTES];
676    let n = if mini {
677        MINI_HEADER_BYTES
678    } else {
679        FULL_HEADER_BYTES
680    };
681    r.read_exact(&mut h[..n]).await?;
682    let at = if mini { 0 } else { 8 };
683    let msg_type = u16::from_le_bytes([h[at], h[at + 1]]);
684    let size = u32::from_le_bytes([h[at + 2], h[at + 3], h[at + 4], h[at + 5]]);
685    if !mini && u32::from_le_bytes([h[14], h[15], h[16], h[17]]) != 0 {
686        return Err(ReadError::SubList);
687    }
688    if size > max {
689        return Err(ReadError::TooLarge {
690            msg_type,
691            size,
692            max,
693        });
694    }
695    let mut body = vec![0u8; size as usize];
696    r.read_exact(&mut body).await?;
697    Ok((msg_type, body))
698}
699
700// ---------------------------------------------------------------------------------------------
701// Message bodies
702// ---------------------------------------------------------------------------------------------
703
704/// `SpiceMsgMainInit`.
705#[allow(clippy::too_many_arguments)]
706pub fn main_init(
707    session_id: u32,
708    display_channels_hint: u32,
709    supported_mouse_modes: u32,
710    current_mouse_mode: u32,
711    agent_connected: u32,
712    agent_tokens: u32,
713    multi_media_time: u32,
714    ram_hint: u32,
715) -> Vec<u8> {
716    [
717        session_id,
718        display_channels_hint,
719        supported_mouse_modes,
720        current_mouse_mode,
721        agent_connected,
722        agent_tokens,
723        multi_media_time,
724        ram_hint,
725    ]
726    .iter()
727    .flat_map(|w| w.to_le_bytes())
728    .collect()
729}
730
731/// `SpiceMsgChannels`: a count, then `(type, id)` pairs.
732pub fn channels_list(channels: &[(u8, u8)]) -> Vec<u8> {
733    let mut v = (channels.len() as u32).to_le_bytes().to_vec();
734    for (t, id) in channels {
735        v.push(*t);
736        v.push(*id);
737    }
738    v
739}
740
741/// `SpiceMsgMainMouseMode`: supported and current, `u16` each.
742pub fn mouse_mode(supported: u16, current: u16) -> Vec<u8> {
743    let mut v = supported.to_le_bytes().to_vec();
744    v.extend_from_slice(&current.to_le_bytes());
745    v
746}
747
748/// `SpiceMsgMainName`: the length counts the terminating NUL, which is sent.
749pub fn main_name(name: &str) -> Vec<u8> {
750    let mut v = ((name.len() + 1) as u32).to_le_bytes().to_vec();
751    v.extend_from_slice(name.as_bytes());
752    v.push(0);
753    v
754}
755
756/// `SpiceMsgNotify`. Like spice-server, `message_len` excludes the NUL that follows it.
757pub fn notify(time_stamp: u64, severity: u32, what: u32, message: &str) -> Vec<u8> {
758    let mut v = time_stamp.to_le_bytes().to_vec();
759    v.extend_from_slice(&severity.to_le_bytes());
760    v.extend_from_slice(&NOTIFY_VISIBILITY_HIGH.to_le_bytes());
761    v.extend_from_slice(&what.to_le_bytes());
762    v.extend_from_slice(&(message.len() as u32).to_le_bytes());
763    v.extend_from_slice(message.as_bytes());
764    v.push(0);
765    v
766}
767
768/// `SpiceMsgSurfaceCreate`.
769pub fn surface_create(
770    surface_id: u32,
771    width: u32,
772    height: u32,
773    format: u32,
774    flags: u32,
775) -> Vec<u8> {
776    [surface_id, width, height, format, flags]
777        .iter()
778        .flat_map(|w| w.to_le_bytes())
779        .collect()
780}
781
782/// A `SpiceRect`, which is `top, left, bottom, right` on the wire.
783fn push_rect(v: &mut Vec<u8>, top: i32, left: i32, bottom: i32, right: i32) {
784    for x in [top, left, bottom, right] {
785        v.extend_from_slice(&x.to_le_bytes());
786    }
787}
788
789/// The byte offset of the image in a DRAW_COPY whose clip is NONE: DisplayBase (21), the
790/// image pointer (4), the source rect (16), rop (2), scale mode (1) and the mask (13).
791pub const DRAW_COPY_IMAGE_OFFSET: u32 = 57;
792
793/// `SpiceMsgDisplayDrawCopy` placing an uncompressed 32-bit top-down bitmap of
794/// `width` × `height` pixels at (`left`, `top`) on `surface_id`. `bgrx` is `width * 4` bytes
795/// a row.
796pub fn draw_copy_bitmap(
797    surface_id: u32,
798    left: u32,
799    top: u32,
800    width: u32,
801    height: u32,
802    image_id: u64,
803    bgrx: &[u8],
804) -> Vec<u8> {
805    let stride = width * 4;
806    let mut v = Vec::with_capacity(DRAW_COPY_IMAGE_OFFSET as usize + 32 + bgrx.len());
807    // DisplayBase: surface, box, clip NONE.
808    v.extend_from_slice(&surface_id.to_le_bytes());
809    let (t, l) = (top as i32, left as i32);
810    push_rect(&mut v, t, l, t + height as i32, l + width as i32);
811    v.push(0);
812    // Copy: the image, the whole of it as the source, PUT, nearest, no mask.
813    v.extend_from_slice(&DRAW_COPY_IMAGE_OFFSET.to_le_bytes());
814    push_rect(&mut v, 0, 0, height as i32, width as i32);
815    v.extend_from_slice(&ROPD_OP_PUT.to_le_bytes());
816    v.push(SCALE_MODE_NEAREST);
817    v.push(0); // mask flags
818    v.extend_from_slice(&0i32.to_le_bytes()); // mask pos x
819    v.extend_from_slice(&0i32.to_le_bytes()); // mask pos y
820    v.extend_from_slice(&0u32.to_le_bytes()); // mask bitmap: none
821    debug_assert_eq!(v.len(), DRAW_COPY_IMAGE_OFFSET as usize);
822    // SpiceImageDescriptor.
823    v.extend_from_slice(&image_id.to_le_bytes());
824    v.push(image_type::BITMAP);
825    v.push(0);
826    v.extend_from_slice(&width.to_le_bytes());
827    v.extend_from_slice(&height.to_le_bytes());
828    // SpiceBitmap: 32-bit, top-down, no palette.
829    v.push(BITMAP_FMT_32BIT);
830    v.push(BITMAP_FLAGS_TOP_DOWN);
831    v.extend_from_slice(&width.to_le_bytes());
832    v.extend_from_slice(&height.to_le_bytes());
833    v.extend_from_slice(&stride.to_le_bytes());
834    v.extend_from_slice(&0u32.to_le_bytes());
835    v.extend_from_slice(bgrx);
836    v
837}
838
839/// A message body whose first field is one `u32`.
840pub fn u32_body(x: u32) -> Vec<u8> {
841    x.to_le_bytes().to_vec()
842}
843
844/// `SpiceMsgcDisplayInit`: pixmap cache id and size (pixels), GLZ dictionary id and window.
845pub fn display_init(cache_id: u8, cache_size: i64, glz_id: u8, glz_window: i32) -> Vec<u8> {
846    let mut v = vec![cache_id];
847    v.extend_from_slice(&cache_size.to_le_bytes());
848    v.push(glz_id);
849    v.extend_from_slice(&glz_window.to_le_bytes());
850    v
851}
852
853/// `SpiceMsgPing` answered as `SpiceMsgcPong`: the id and timestamp, without the data.
854pub fn pong_for(ping: &[u8]) -> Result<Vec<u8>> {
855    if ping.len() < 12 {
856        bail!("a PING of {} bytes has no id and timestamp", ping.len());
857    }
858    Ok(ping[..12].to_vec())
859}
860
861/// `SpiceMsgcMouseMotion`.
862pub fn mouse_motion(dx: i32, dy: i32, buttons: u16) -> Vec<u8> {
863    let mut v = dx.to_le_bytes().to_vec();
864    v.extend_from_slice(&dy.to_le_bytes());
865    v.extend_from_slice(&buttons.to_le_bytes());
866    v
867}
868
869/// `SpiceMsgcMousePosition`.
870pub fn mouse_position(x: u32, y: u32, buttons: u16, display_id: u8) -> Vec<u8> {
871    let mut v = x.to_le_bytes().to_vec();
872    v.extend_from_slice(&y.to_le_bytes());
873    v.extend_from_slice(&buttons.to_le_bytes());
874    v.push(display_id);
875    v
876}
877
878/// `SpiceMsgcMousePress` / `SpiceMsgcMouseRelease`.
879pub fn mouse_button(button: u8, buttons: u16) -> Vec<u8> {
880    let mut v = vec![button];
881    v.extend_from_slice(&buttons.to_le_bytes());
882    v
883}