1use {
2 crate::remote_wallet::{
3 RemoteWallet, RemoteWalletError, RemoteWalletInfo, RemoteWalletManager,
4 },
5 console::Emoji,
6 dialoguer::{theme::ColorfulTheme, Select},
7 semver::Version as FirmwareVersion,
8 solana_derivation_path::DerivationPath,
9 std::{fmt, rc::Rc},
10};
11#[cfg(feature = "hidapi")]
12use {
13 crate::{ledger_error::LedgerError, locator::Manufacturer},
14 log::*,
15 num_traits::FromPrimitive,
16 solana_sdk::{pubkey::Pubkey, signature::Signature},
17 std::{cmp::min, convert::TryFrom},
18};
19
20static CHECK_MARK: Emoji = Emoji("✅ ", "");
21
22const DEPRECATE_VERSION_BEFORE: FirmwareVersion = FirmwareVersion::new(0, 2, 0);
23
24const APDU_TAG: u8 = 0x05;
25const APDU_CLA: u8 = 0xe0;
26const APDU_PAYLOAD_HEADER_LEN: usize = 7;
27const DEPRECATED_APDU_PAYLOAD_HEADER_LEN: usize = 8;
28const P1_NON_CONFIRM: u8 = 0x00;
29const P1_CONFIRM: u8 = 0x01;
30const P2_EXTEND: u8 = 0x01;
31const P2_MORE: u8 = 0x02;
32const MAX_CHUNK_SIZE: usize = 255;
33
34const APDU_SUCCESS_CODE: usize = 0x9000;
35
36const LEDGER_VID: u16 = 0x2c97;
38const LEDGER_NANO_S_PIDS: [u16; 33] = [
40 0x0001, 0x1000, 0x1001, 0x1002, 0x1003, 0x1004, 0x1005, 0x1006, 0x1007, 0x1008, 0x1009, 0x100a,
41 0x100b, 0x100c, 0x100d, 0x100e, 0x100f, 0x1010, 0x1011, 0x1012, 0x1013, 0x1014, 0x1015, 0x1016,
42 0x1017, 0x1018, 0x1019, 0x101a, 0x101b, 0x101c, 0x101d, 0x101e, 0x101f,
43];
44const LEDGER_NANO_X_PIDS: [u16; 33] = [
45 0x0004, 0x4000, 0x4001, 0x4002, 0x4003, 0x4004, 0x4005, 0x4006, 0x4007, 0x4008, 0x4009, 0x400a,
46 0x400b, 0x400c, 0x400d, 0x400e, 0x400f, 0x4010, 0x4011, 0x4012, 0x4013, 0x4014, 0x4015, 0x4016,
47 0x4017, 0x4018, 0x4019, 0x401a, 0x401b, 0x401c, 0x401d, 0x401e, 0x401f,
48];
49const LEDGER_NANO_S_PLUS_PIDS: [u16; 33] = [
50 0x0005, 0x5000, 0x5001, 0x5002, 0x5003, 0x5004, 0x5005, 0x5006, 0x5007, 0x5008, 0x5009, 0x500a,
51 0x500b, 0x500c, 0x500d, 0x500e, 0x500f, 0x5010, 0x5011, 0x5012, 0x5013, 0x5014, 0x5015, 0x5016,
52 0x5017, 0x5018, 0x5019, 0x501a, 0x501b, 0x501c, 0x501d, 0x501e, 0x501f,
53];
54const LEDGER_TRANSPORT_HEADER_LEN: usize = 5;
55
56const HID_PACKET_SIZE: usize = 64 + HID_PREFIX_ZERO;
57
58#[cfg(windows)]
59const HID_PREFIX_ZERO: usize = 1;
60#[cfg(not(windows))]
61const HID_PREFIX_ZERO: usize = 0;
62
63mod commands {
64 pub const DEPRECATED_GET_APP_CONFIGURATION: u8 = 0x01;
65 pub const DEPRECATED_GET_PUBKEY: u8 = 0x02;
66 pub const DEPRECATED_SIGN_MESSAGE: u8 = 0x03;
67 pub const GET_APP_CONFIGURATION: u8 = 0x04;
68 pub const GET_PUBKEY: u8 = 0x05;
69 pub const SIGN_MESSAGE: u8 = 0x06;
70 pub const SIGN_OFFCHAIN_MESSAGE: u8 = 0x07;
71}
72
73enum ConfigurationVersion {
74 Deprecated(Vec<u8>),
75 Current(Vec<u8>),
76}
77
78#[derive(Debug)]
79pub enum PubkeyDisplayMode {
80 Short,
81 Long,
82}
83
84#[derive(Debug)]
85pub struct LedgerSettings {
86 pub enable_blind_signing: bool,
87 pub pubkey_display: PubkeyDisplayMode,
88}
89
90pub struct LedgerWallet {
92 #[cfg(feature = "hidapi")]
93 pub device: hidapi::HidDevice,
94 pub pretty_path: String,
95 pub version: FirmwareVersion,
96}
97
98impl fmt::Debug for LedgerWallet {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 write!(f, "HidDevice")
101 }
102}
103
104#[cfg(feature = "hidapi")]
105impl LedgerWallet {
106 pub fn new(device: hidapi::HidDevice) -> Self {
107 Self {
108 device,
109 pretty_path: String::default(),
110 version: FirmwareVersion::new(0, 0, 0),
111 }
112 }
113
114 fn write(
130 &self,
131 command: u8,
132 p1: u8,
133 p2: u8,
134 data: &[u8],
135 outdated_app: bool,
136 ) -> Result<(), RemoteWalletError> {
137 let data_len = data.len();
138 let mut offset = 0;
139 let mut sequence_number = 0;
140 let mut hid_chunk = [0_u8; HID_PACKET_SIZE];
141
142 while sequence_number == 0 || offset < data_len {
143 let header = if sequence_number == 0 {
144 if outdated_app {
145 LEDGER_TRANSPORT_HEADER_LEN + DEPRECATED_APDU_PAYLOAD_HEADER_LEN
146 } else {
147 LEDGER_TRANSPORT_HEADER_LEN + APDU_PAYLOAD_HEADER_LEN
148 }
149 } else {
150 LEDGER_TRANSPORT_HEADER_LEN
151 };
152 let size = min(64 - header, data_len - offset);
153 {
154 let chunk = &mut hid_chunk[HID_PREFIX_ZERO..];
155 chunk[0..5].copy_from_slice(&[
156 0x01,
157 0x01,
158 APDU_TAG,
159 (sequence_number >> 8) as u8,
160 (sequence_number & 0xff) as u8,
161 ]);
162
163 if sequence_number == 0 {
164 if outdated_app {
165 let data_len = data.len() + 6;
166 chunk[5..13].copy_from_slice(&[
167 (data_len >> 8) as u8,
168 (data_len & 0xff) as u8,
169 APDU_CLA,
170 command,
171 p1,
172 p2,
173 (data.len() >> 8) as u8,
174 data.len() as u8,
175 ]);
176 } else {
177 let data_len = data.len() + 5;
178 chunk[5..12].copy_from_slice(&[
179 (data_len >> 8) as u8,
180 (data_len & 0xff) as u8,
181 APDU_CLA,
182 command,
183 p1,
184 p2,
185 data.len() as u8,
186 ]);
187 }
188 }
189
190 chunk[header..header + size].copy_from_slice(&data[offset..offset + size]);
191 }
192 trace!("Ledger write {:?}", &hid_chunk[..]);
193 let n = self.device.write(&hid_chunk[..])?;
194 if n < size + header {
195 return Err(RemoteWalletError::Protocol("Write data size mismatch"));
196 }
197 offset += size;
198 sequence_number += 1;
199 if sequence_number >= 0xffff {
200 return Err(RemoteWalletError::Protocol(
201 "Maximum sequence number reached",
202 ));
203 }
204 }
205 Ok(())
206 }
207
208 fn read(&self) -> Result<Vec<u8>, RemoteWalletError> {
219 let mut message_size = 0;
220 let mut message = Vec::new();
221
222 for chunk_index in 0..=0xffff {
224 let mut chunk: [u8; HID_PACKET_SIZE] = [0; HID_PACKET_SIZE];
225 let chunk_size = self.device.read(&mut chunk)?;
226 trace!("Ledger read {:?}", &chunk[..]);
227 if chunk_size < LEDGER_TRANSPORT_HEADER_LEN
228 || chunk[0] != 0x01
229 || chunk[1] != 0x01
230 || chunk[2] != APDU_TAG
231 {
232 return Err(RemoteWalletError::Protocol("Unexpected chunk header"));
233 }
234 let seq = (chunk[3] as usize) << 8 | (chunk[4] as usize);
235 if seq != chunk_index {
236 return Err(RemoteWalletError::Protocol("Unexpected chunk header"));
237 }
238
239 let mut offset = 5;
240 if seq == 0 {
241 if chunk_size < 7 {
243 return Err(RemoteWalletError::Protocol("Unexpected chunk header"));
244 }
245 message_size = (chunk[5] as usize) << 8 | (chunk[6] as usize);
246 offset += 2;
247 }
248 message.extend_from_slice(&chunk[offset..chunk_size]);
249 message.truncate(message_size);
250 if message.len() == message_size {
251 break;
252 }
253 }
254 if message.len() < 2 {
255 return Err(RemoteWalletError::Protocol("No status word"));
256 }
257 let status =
258 (message[message.len() - 2] as usize) << 8 | (message[message.len() - 1] as usize);
259 trace!("Read status {:x}", status);
260 Self::parse_status(status)?;
261 let new_len = message.len() - 2;
262 message.truncate(new_len);
263 Ok(message)
264 }
265
266 fn _send_apdu(
267 &self,
268 command: u8,
269 p1: u8,
270 p2: u8,
271 data: &[u8],
272 outdated_app: bool,
273 ) -> Result<Vec<u8>, RemoteWalletError> {
274 self.write(command, p1, p2, data, outdated_app)?;
275 if p1 == P1_CONFIRM && is_last_part(p2) {
276 println!(
277 "Waiting for your approval on {} {}",
278 self.name(),
279 self.pretty_path
280 );
281 let result = self.read()?;
282 println!("{CHECK_MARK}Approved");
283 Ok(result)
284 } else {
285 self.read()
286 }
287 }
288
289 fn send_apdu(
290 &self,
291 command: u8,
292 p1: u8,
293 p2: u8,
294 data: &[u8],
295 ) -> Result<Vec<u8>, RemoteWalletError> {
296 self._send_apdu(command, p1, p2, data, self.outdated_app())
297 }
298
299 fn get_firmware_version(&self) -> Result<FirmwareVersion, RemoteWalletError> {
300 self.get_configuration_vector().map(|config| match config {
301 ConfigurationVersion::Current(config) => {
302 FirmwareVersion::new(config[2].into(), config[3].into(), config[4].into())
303 }
304 ConfigurationVersion::Deprecated(config) => {
305 FirmwareVersion::new(config[1].into(), config[2].into(), config[3].into())
306 }
307 })
308 }
309
310 pub fn get_settings(&self) -> Result<LedgerSettings, RemoteWalletError> {
311 self.get_configuration_vector().map(|config| match config {
312 ConfigurationVersion::Current(config) => {
313 let enable_blind_signing = config[0] != 0;
314 let pubkey_display = if config[1] == 0 {
315 PubkeyDisplayMode::Long
316 } else {
317 PubkeyDisplayMode::Short
318 };
319 LedgerSettings {
320 enable_blind_signing,
321 pubkey_display,
322 }
323 }
324 ConfigurationVersion::Deprecated(_) => LedgerSettings {
325 enable_blind_signing: false,
326 pubkey_display: PubkeyDisplayMode::Short,
327 },
328 })
329 }
330
331 fn get_configuration_vector(&self) -> Result<ConfigurationVersion, RemoteWalletError> {
332 if let Ok(config) = self._send_apdu(commands::GET_APP_CONFIGURATION, 0, 0, &[], false) {
333 if config.len() != 5 {
334 return Err(RemoteWalletError::Protocol("Version packet size mismatch"));
335 }
336 Ok(ConfigurationVersion::Current(config))
337 } else {
338 let config =
339 self._send_apdu(commands::DEPRECATED_GET_APP_CONFIGURATION, 0, 0, &[], true)?;
340 if config.len() != 4 {
341 return Err(RemoteWalletError::Protocol("Version packet size mismatch"));
342 }
343 Ok(ConfigurationVersion::Deprecated(config))
344 }
345 }
346
347 fn outdated_app(&self) -> bool {
348 self.version < DEPRECATE_VERSION_BEFORE
349 }
350
351 fn parse_status(status: usize) -> Result<(), RemoteWalletError> {
352 if status == APDU_SUCCESS_CODE {
353 Ok(())
354 } else if let Some(err) = LedgerError::from_usize(status) {
355 Err(err.into())
356 } else {
357 Err(RemoteWalletError::Protocol("Unknown error"))
358 }
359 }
360}
361
362#[cfg(not(feature = "hidapi"))]
363impl RemoteWallet<Self> for LedgerWallet {}
364#[cfg(feature = "hidapi")]
365impl RemoteWallet<hidapi::DeviceInfo> for LedgerWallet {
366 fn name(&self) -> &str {
367 "Ledger hardware wallet"
368 }
369
370 fn read_device(
371 &mut self,
372 dev_info: &hidapi::DeviceInfo,
373 ) -> Result<RemoteWalletInfo, RemoteWalletError> {
374 let manufacturer = dev_info
375 .manufacturer_string()
376 .and_then(|s| Manufacturer::try_from(s).ok())
377 .unwrap_or_default();
378 let model = dev_info
379 .product_string()
380 .unwrap_or("Unknown")
381 .to_lowercase()
382 .replace(' ', "-");
383 let serial = dev_info.serial_number().unwrap_or("Unknown").to_string();
384 let host_device_path = dev_info.path().to_string_lossy().to_string();
385 let version = self.get_firmware_version()?;
386 self.version = version;
387 let pubkey_result = self.get_pubkey(&DerivationPath::default(), false);
388 let (pubkey, error) = match pubkey_result {
389 Ok(pubkey) => (pubkey, None),
390 Err(err) => (Pubkey::default(), Some(err)),
391 };
392 Ok(RemoteWalletInfo {
393 model,
394 manufacturer,
395 serial,
396 host_device_path,
397 pubkey,
398 error,
399 })
400 }
401
402 fn get_pubkey(
403 &self,
404 derivation_path: &DerivationPath,
405 confirm_key: bool,
406 ) -> Result<Pubkey, RemoteWalletError> {
407 let derivation_path = extend_and_serialize(derivation_path);
408
409 let key = self.send_apdu(
410 if self.outdated_app() {
411 commands::DEPRECATED_GET_PUBKEY
412 } else {
413 commands::GET_PUBKEY
414 },
415 if confirm_key {
416 P1_CONFIRM
417 } else {
418 P1_NON_CONFIRM
419 },
420 0,
421 &derivation_path,
422 )?;
423 Pubkey::try_from(key).map_err(|_| RemoteWalletError::Protocol("Key packet size mismatch"))
424 }
425
426 fn sign_message(
427 &self,
428 derivation_path: &DerivationPath,
429 data: &[u8],
430 ) -> Result<Signature, RemoteWalletError> {
431 if !data.is_empty() && data[0] == 0xff {
436 return self.sign_offchain_message(derivation_path, data);
437 }
438 let mut payload = if self.outdated_app() {
439 extend_and_serialize(derivation_path)
440 } else {
441 extend_and_serialize_multiple(&[derivation_path])
442 };
443 if data.len() > u16::MAX as usize {
444 return Err(RemoteWalletError::InvalidInput(
445 "Message to sign is too long".to_string(),
446 ));
447 }
448
449 let max_size = MAX_CHUNK_SIZE - payload.len();
452 let empty = vec![];
453 let (data, remaining_data) = if data.len() > max_size {
454 data.split_at(max_size)
455 } else {
456 (data, empty.as_ref())
457 };
458
459 if self.outdated_app() {
461 for byte in (data.len() as u16).to_be_bytes().iter() {
462 payload.push(*byte);
463 }
464 }
465 payload.extend_from_slice(data);
466 trace!("Serialized payload length {:?}", payload.len());
467
468 let p2 = if remaining_data.is_empty() {
469 0
470 } else {
471 P2_MORE
472 };
473
474 let p1 = P1_CONFIRM;
475 let mut result = self.send_apdu(
476 if self.outdated_app() {
477 commands::DEPRECATED_SIGN_MESSAGE
478 } else {
479 commands::SIGN_MESSAGE
480 },
481 p1,
482 p2,
483 &payload,
484 )?;
485
486 if !remaining_data.is_empty() {
488 let mut chunks: Vec<_> = remaining_data
489 .chunks(MAX_CHUNK_SIZE)
490 .map(|data| {
491 let mut payload = if self.outdated_app() {
492 (data.len() as u16).to_be_bytes().to_vec()
493 } else {
494 vec![]
495 };
496 payload.extend_from_slice(data);
497 let p2 = P2_EXTEND | P2_MORE;
498 (p2, payload)
499 })
500 .collect();
501
502 chunks.last_mut().unwrap().0 &= !P2_MORE;
504
505 for (p2, payload) in chunks {
506 result = self.send_apdu(
507 if self.outdated_app() {
508 commands::DEPRECATED_SIGN_MESSAGE
509 } else {
510 commands::SIGN_MESSAGE
511 },
512 p1,
513 p2,
514 &payload,
515 )?;
516 }
517 }
518
519 Signature::try_from(result)
520 .map_err(|_| RemoteWalletError::Protocol("Signature packet size mismatch"))
521 }
522
523 fn sign_offchain_message(
524 &self,
525 derivation_path: &DerivationPath,
526 message: &[u8],
527 ) -> Result<Signature, RemoteWalletError> {
528 if message.len()
529 > solana_sdk::offchain_message::v0::OffchainMessage::MAX_LEN_LEDGER
530 + solana_sdk::offchain_message::v0::OffchainMessage::HEADER_LEN
531 {
532 return Err(RemoteWalletError::InvalidInput(
533 "Off-chain message to sign is too long".to_string(),
534 ));
535 }
536
537 let mut data = extend_and_serialize_multiple(&[derivation_path]);
538 data.extend_from_slice(message);
539
540 let p1 = P1_CONFIRM;
541 let mut p2 = 0;
542 let mut payload = data.as_slice();
543 while payload.len() > MAX_CHUNK_SIZE {
544 let chunk = &payload[..MAX_CHUNK_SIZE];
545 self.send_apdu(commands::SIGN_OFFCHAIN_MESSAGE, p1, p2 | P2_MORE, chunk)?;
546 payload = &payload[MAX_CHUNK_SIZE..];
547 p2 |= P2_EXTEND;
548 }
549
550 let result = self.send_apdu(commands::SIGN_OFFCHAIN_MESSAGE, p1, p2, payload)?;
551 Signature::try_from(result)
552 .map_err(|_| RemoteWalletError::Protocol("Signature packet size mismatch"))
553 }
554}
555
556pub fn is_valid_ledger(vendor_id: u16, product_id: u16) -> bool {
558 let product_ids = [
559 LEDGER_NANO_S_PIDS,
560 LEDGER_NANO_X_PIDS,
561 LEDGER_NANO_S_PLUS_PIDS,
562 ];
563 vendor_id == LEDGER_VID && product_ids.iter().any(|pids| pids.contains(&product_id))
564}
565
566fn extend_and_serialize(derivation_path: &DerivationPath) -> Vec<u8> {
568 let byte = if derivation_path.change().is_some() {
569 4
570 } else if derivation_path.account().is_some() {
571 3
572 } else {
573 2
574 };
575 let mut concat_derivation = vec![byte];
576 for index in derivation_path.path() {
577 concat_derivation.extend_from_slice(&index.to_bits().to_be_bytes());
578 }
579 concat_derivation
580}
581
582fn extend_and_serialize_multiple(derivation_paths: &[&DerivationPath]) -> Vec<u8> {
583 let mut concat_derivation = vec![derivation_paths.len() as u8];
584 for derivation_path in derivation_paths {
585 concat_derivation.append(&mut extend_and_serialize(derivation_path));
586 }
587 concat_derivation
588}
589
590pub fn get_ledger_from_info(
592 info: RemoteWalletInfo,
593 keypair_name: &str,
594 wallet_manager: &RemoteWalletManager,
595) -> Result<Rc<LedgerWallet>, RemoteWalletError> {
596 let devices = wallet_manager.list_devices();
597 let mut matches = devices
598 .iter()
599 .filter(|&device_info| device_info.matches(&info));
600 if matches
601 .clone()
602 .all(|device_info| device_info.error.is_some())
603 {
604 let first_device = matches.next();
605 if let Some(device) = first_device {
606 return Err(device.error.clone().unwrap());
607 }
608 }
609 let mut matches: Vec<(String, String)> = matches
610 .filter(|&device_info| device_info.error.is_none())
611 .map(|device_info| {
612 let query_item = format!("{} ({})", device_info.get_pretty_path(), device_info.model,);
613 (device_info.host_device_path.clone(), query_item)
614 })
615 .collect();
616 if matches.is_empty() {
617 return Err(RemoteWalletError::NoDeviceFound);
618 }
619 matches.sort_by(|a, b| a.1.cmp(&b.1));
620 let (host_device_paths, items): (Vec<String>, Vec<String>) = matches.into_iter().unzip();
621
622 let wallet_host_device_path = if host_device_paths.len() > 1 {
623 let selection = Select::with_theme(&ColorfulTheme::default())
624 .with_prompt(format!(
625 "Multiple hardware wallets found. Please select a device for {keypair_name:?}"
626 ))
627 .default(0)
628 .items(&items[..])
629 .interact()
630 .unwrap();
631 &host_device_paths[selection]
632 } else {
633 &host_device_paths[0]
634 };
635 wallet_manager.get_ledger(wallet_host_device_path)
636}
637
638fn is_last_part(p2: u8) -> bool {
640 p2 & P2_MORE == 0
641}
642
643#[cfg(test)]
644mod tests {
645 use super::*;
646
647 #[test]
648 fn test_is_last_part() {
649 assert!(is_last_part(0b00));
651 assert!(is_last_part(0b01));
652 assert!(is_last_part(0b101));
653 assert!(is_last_part(0b1001));
654 assert!(is_last_part(0b1101));
655
656 assert!(!is_last_part(0b10));
658 assert!(!is_last_part(0b11));
659 assert!(!is_last_part(0b110));
660 assert!(!is_last_part(0b111));
661 assert!(!is_last_part(0b1010));
662
663 let p2 = 0;
665 assert!(is_last_part(p2));
666 let p2 = P2_EXTEND | P2_MORE;
667 assert!(!is_last_part(p2));
668 assert!(is_last_part(p2 & !P2_MORE));
669 }
670
671 #[test]
672 fn test_parse_status() {
673 LedgerWallet::parse_status(APDU_SUCCESS_CODE).expect("unexpected result");
674 if let RemoteWalletError::LedgerError(err) = LedgerWallet::parse_status(0x6985).unwrap_err()
675 {
676 assert_eq!(err, LedgerError::UserCancel);
677 }
678 if let RemoteWalletError::Protocol(err) = LedgerWallet::parse_status(0x6fff).unwrap_err() {
679 assert_eq!(err, "Unknown error");
680 }
681 }
682}