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