pub mod derivation_path;
pub use derivation_path::DerivationPath;
use crate::constants::*;
macro_rules! impl_new_with_range {
($thing:ident, $range:expr_2021) => {
impl_new_with_range!($thing, $range, 0_u8..);
};
($thing:ident, $range:expr_2021, $valid_char_range:expr_2021) => {
impl $thing {
pub fn new(value: impl AsRef<[u8]>) -> Result<Self, EncodeError> {
let value = value.as_ref();
for c in value {
if !$valid_char_range.contains(c) {
return Err(EncodeError::InvalidCharValue);
}
}
let type_name = std::any::type_name::<$thing>();
#[allow(unused_comparisons)]
if value.len() < $range.start || value.len() > $range.end {
return Err(EncodeError::LengthMismatch(type_name, value.len()));
}
Ok(Self(value.to_owned()))
}
}
};
}
pub struct DescriptorName(pub(crate) Vec<u8>);
pub struct Upload(Vec<u8>);
pub struct Message(Vec<u8>);
pub struct Username(Vec<u8>);
pub struct AuthToken(Vec<u8>);
#[derive(Default)]
pub struct Secret(Vec<u8>);
pub struct RedeemScript(Vec<u8>);
pub struct Passphrase(Vec<u8>);
pub struct HsmStartParams {
pub length: u32,
pub file_sha: [u8; 32],
}
pub struct XfpPath {
pub fingerprint: u32,
pub path: DerivationPath,
}
#[repr(u32)]
#[derive(Copy, Clone, Default)]
pub enum FileNo {
Zero = 0,
#[default]
One = 1,
}
impl_new_with_range!(DescriptorName, 2..40);
impl_new_with_range!(Upload, 1..MAX_BLK_LEN);
impl_new_with_range!(Message, 1..240);
impl_new_with_range!(Username, 1..16);
impl_new_with_range!(AuthToken, 6..32);
impl_new_with_range!(RedeemScript, 30..520);
impl_new_with_range!(Passphrase, 0..100, 32..=126);
impl Secret {
pub fn new(value: impl AsRef<[u8]>) -> Result<Self, EncodeError> {
let value = value.as_ref();
match value.len() {
0 | 10 | 20 | 32 => Ok(Self(value.to_owned())),
_ => Err(EncodeError::LengthNotOneOf("Secret", vec![0, 10, 20, 32])),
}
}
}
#[allow(non_camel_case_types)]
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AddressFormat {
P2PKH = AFC_PUBKEY,
P2SH = AFC_SCRIPT,
P2WPKH = AFC_PUBKEY | AFC_SEGWIT | AFC_BECH32,
P2WSH = AFC_SCRIPT | AFC_SEGWIT | AFC_BECH32,
P2WPKH_P2SH = AFC_WRAPPED | AFC_PUBKEY | AFC_SEGWIT,
P2WSH_P2SH = AFC_WRAPPED | AFC_SCRIPT | AFC_SEGWIT,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy)]
pub enum AuthMode {
TOTP = 0x01,
HOTP = 0x02,
HMAC = 0x03,
}
pub(crate) enum Request {
Logout,
Reboot,
Version,
Ping(Vec<u8>),
Bip39Passphrase(Passphrase),
GetPassphraseDone,
CheckMitm,
StartBackup,
RestoreBackup {
length: u32,
file_sha: [u8; 32],
custom_pwd: bool,
plaintext: bool,
tmp: bool,
},
EncryptStart {
device_pubkey: [u8; 64],
version: Option<u32>,
},
Upload {
offset: u32,
total_size: u32,
data: Upload,
},
Download {
offset: u32,
length: u32,
file_number: FileNo,
},
Sha256,
SignTransaction {
length: u32,
file_sha: [u8; 32],
flags: Option<u32>,
descriptor_name: Option<DescriptorName>,
},
SignMessage {
raw_msg: Message,
subpath: Option<DerivationPath>,
addr_fmt: AddressFormat,
},
GetSignedMessage,
GetBackupFile,
GetSignedTransaction,
MiniscriptPolicy {
descriptor_name: DescriptorName,
},
MiniscriptAddress {
descriptor_name: DescriptorName,
change: bool,
index: u32,
},
MiniscriptEnroll {
length: u32,
file_sha: [u8; 32],
},
MiniscriptList,
MiniscriptDelete {
descriptor_name: DescriptorName,
},
MiniscriptGetDescriptor {
descriptor_name: DescriptorName,
},
MultisigEnroll {
length: u32,
file_sha: [u8; 32],
},
MultiSigCheck {
m: u32,
n: u32,
xfp_xor: u32,
},
GetXPub(Option<DerivationPath>),
ShowAddress {
subpath: DerivationPath,
addr_fmt: AddressFormat,
},
ShowP2SHAddress {
min_signers: u8,
xfp_paths: Vec<XfpPath>,
redeem_script: RedeemScript,
address_format: AddressFormat,
},
Blockchain,
BagNumber(Option<String>),
HsmStart(Option<HsmStartParams>),
HsmStatus,
CreateUser {
username: Username,
auth_mode: AuthMode,
secret: Option<Secret>,
show_qr: bool,
},
DeleteUser(Username),
UserAuth {
username: Username,
token: AuthToken,
totp_time: u32,
},
GetStorageLocker,
}
impl Request {
pub fn encode(self) -> Vec<u8> {
match self {
Request::Logout => cmd("logo"),
Request::Reboot => cmd("rebo"),
Request::Version => cmd("vers"),
Request::Ping(msg) => {
let mut buf = cmd("ping");
buf.extend(msg);
buf
}
Request::Bip39Passphrase(pw) => {
let mut buf = cmd("pass");
buf.extend(pw.0);
buf
}
Request::GetPassphraseDone => cmd("pwok"),
Request::CheckMitm => cmd("mitm"),
Request::StartBackup => cmd("back"),
Request::EncryptStart {
device_pubkey,
version,
} => {
let mut buf = cmd("ncry");
buf.extend(version.unwrap_or(1).to_le_bytes());
buf.extend(device_pubkey);
buf
}
Request::Upload {
offset,
total_size,
data,
} => {
let mut buf = cmd("upld");
buf.extend(offset.to_le_bytes());
buf.extend(total_size.to_le_bytes());
buf.extend(data.0);
buf
}
Request::Download {
offset,
length,
file_number,
} => {
let mut buf = cmd("dwld");
buf.extend(offset.to_le_bytes());
buf.extend(length.to_le_bytes());
buf.extend((file_number as u32).to_le_bytes());
buf
}
Request::Sha256 => cmd("sha2"),
Request::SignTransaction {
length,
file_sha,
flags,
descriptor_name: miniscript_name,
} => {
let mut buf = cmd("stxn");
let flags = flags.unwrap_or_default();
buf.extend(length.to_le_bytes());
buf.extend(flags.to_le_bytes());
buf.extend(file_sha);
if let Some(name) = miniscript_name {
buf.extend((name.0.len() as u8).to_le_bytes());
buf.extend(name.0);
}
buf
}
Request::SignMessage {
raw_msg,
subpath,
addr_fmt,
} => {
let subpath = subpath.unwrap_or_default().to_string();
let mut buf = cmd("smsg");
buf.extend((addr_fmt as u32).to_le_bytes());
buf.extend((subpath.len() as u32).to_le_bytes());
buf.extend((raw_msg.0.len() as u32).to_le_bytes());
buf.extend(subpath.as_bytes());
buf.extend(raw_msg.0);
buf
}
Request::GetSignedMessage => cmd("smok"),
Request::GetBackupFile => cmd("bkok"),
Request::GetSignedTransaction => cmd("stok"),
Request::MiniscriptAddress {
descriptor_name,
change,
index,
} => {
let mut buf = cmd("msas");
buf.extend((change as u32).to_le_bytes());
buf.extend(index.to_le_bytes());
buf.extend(descriptor_name.0);
buf
}
Request::MiniscriptEnroll { length, file_sha } => {
let mut buf = cmd("mins");
buf.extend(length.to_le_bytes());
buf.extend(file_sha);
buf
}
Request::MiniscriptGetDescriptor { descriptor_name } => {
let mut buf = cmd("msgt");
buf.extend(descriptor_name.0);
buf
}
Request::MultisigEnroll { length, file_sha } => {
let mut buf = cmd("enrl");
buf.extend(length.to_le_bytes());
buf.extend(file_sha);
buf
}
Request::MultiSigCheck { m, n, xfp_xor } => {
let mut buf = cmd("msck");
buf.extend(m.to_le_bytes());
buf.extend(n.to_le_bytes());
buf.extend(xfp_xor.to_le_bytes());
buf
}
Request::GetXPub(subpath) => {
let mut buf = cmd("xpub");
buf.extend(subpath.unwrap_or_default().to_string().as_bytes());
buf
}
Request::ShowAddress { subpath, addr_fmt } => {
let mut buf = cmd("show");
buf.extend((addr_fmt as u32).to_le_bytes());
buf.extend(subpath.to_string().as_bytes());
buf
}
Request::ShowP2SHAddress {
min_signers,
xfp_paths,
redeem_script,
address_format,
} => {
let mut buf = cmd("p2sh");
buf.extend((address_format as u32).to_le_bytes());
buf.extend(min_signers.to_le_bytes());
buf.extend((xfp_paths.len() as u8).to_le_bytes());
buf.extend((redeem_script.0.len() as u16).to_le_bytes());
buf.extend(redeem_script.0);
for XfpPath { fingerprint, path } in xfp_paths {
buf.push((1 + path.children().len()) as u8);
buf.extend(fingerprint.to_le_bytes());
for child in path.children().iter() {
buf.extend(child.value().to_le_bytes());
}
}
buf
}
Request::Blockchain => cmd("blkc"),
Request::BagNumber(number) => {
let mut buf = cmd("bagi");
if let Some(number) = number {
buf.extend(number.as_bytes());
}
buf
}
Request::HsmStart(params) => {
let mut buf = cmd("hsms");
if let Some(HsmStartParams { length, file_sha }) = params {
buf.extend(length.to_le_bytes());
buf.extend(file_sha);
}
buf
}
Request::HsmStatus => cmd("hsts"),
Request::CreateUser {
username,
auth_mode,
secret,
show_qr,
} => {
let secret = secret.unwrap_or_default();
let mut buf = cmd("nwur");
buf.push((auth_mode as u8) | if show_qr { USER_AUTH_SHOW_QR } else { 0x00 });
buf.push(username.0.len() as u8);
buf.push(secret.0.len() as u8);
buf.extend(username.0);
buf.extend(secret.0);
buf
}
Request::DeleteUser(username) => {
let mut buf = cmd("rmur");
buf.push(username.0.len() as u8);
buf.extend(username.0);
buf
}
Request::UserAuth {
username,
token,
totp_time,
} => {
let mut buf = cmd("user");
buf.extend(totp_time.to_le_bytes());
buf.push(username.0.len() as u8);
buf.push(token.0.len() as u8);
buf.extend(username.0);
buf.extend(token.0);
buf
}
Request::GetStorageLocker => cmd("gslr"),
Request::MiniscriptPolicy {
descriptor_name: name,
} => {
let mut buf = cmd("mspl");
buf.extend(name.0);
buf
}
Request::RestoreBackup {
length,
file_sha,
custom_pwd,
plaintext,
tmp,
} => {
let mut flags = 0u8;
if custom_pwd {
flags |= 1;
}
if plaintext {
flags |= 2;
}
if tmp {
flags |= 4;
}
let mut buf = cmd("rest");
buf.extend(length.to_le_bytes());
buf.extend(file_sha);
buf.push(flags);
buf
}
Request::MiniscriptDelete { descriptor_name } => {
let mut buf = cmd("msdl");
buf.extend(descriptor_name.0);
buf
}
Request::MiniscriptList => cmd("msls"),
}
}
}
#[derive(Debug)]
pub enum EncodeError {
LengthMismatch(&'static str, usize),
LengthNotOneOf(&'static str, Vec<usize>),
InvalidCharValue,
}
fn cmd(name: &str) -> Vec<u8> {
name.as_bytes().to_owned()
}
#[derive(Debug)]
pub enum Response {
Ok,
Refused,
Busy,
Binary(Vec<u8>),
Int1(u32),
Int2(u64),
Int3(u128),
MyPub {
dev_pubkey: [u8; 64],
xpub_fingerprint: [u8; 4],
xpub: Option<String>,
},
Ascii(String),
TxSigned {
length: u32,
sha256: [u8; 32],
},
MessageSigned {
address: String,
signature: [u8; 65],
},
}
impl Response {
pub fn decode(payload: &[u8]) -> Result<Self, DecodeError> {
let (command, data) = split(payload, 4)?;
match command {
b"okay" => Ok(Response::Ok),
b"refu" => Ok(Response::Refused),
b"busy" => Ok(Response::Busy),
b"biny" => Ok(Response::Binary(data.to_owned())),
b"int1" => decode_u32(data.get(0..4)).map(Response::Int1),
b"int2" => Ok(Response::Int2(decode_u64(data.get(0..8))?)),
b"int3" => Ok(Response::Int3(decode_u128(data.get(0..12))?)),
b"mypb" => {
let (dev_pubkey, data) = split(data, 64)?;
let dev_pubkey = dev_pubkey
.try_into()
.map_err(|_| DecodeError::Decode("pubkey"))?;
let xpub_fingerprint = data
.get(0..4)
.ok_or(DecodeError::Decode("xfp wants 4 bytes"))?
.try_into()
.expect("infallible");
let xpub_len = decode_u32(data.get(4..8))? as usize;
let xpub = if xpub_len > 0 {
data.get(8..8 + xpub_len)
.map(|d| String::from_utf8(d.to_owned()))
.transpose()
.map_err(DecodeError::Utf8)?
} else {
None
};
Ok(Response::MyPub {
dev_pubkey,
xpub_fingerprint,
xpub,
})
}
b"asci" => Ok(Response::Ascii(bytes_as_string(data).map(str::to_owned)?)),
b"strx" => {
let (length, sha256) = split(data, 4)?;
let length = decode_u32(Some(length))?;
let sha256 = sha256
.try_into()
.map_err(|_| DecodeError::Decode("checksum"))?;
Ok(Response::TxSigned { length, sha256 })
}
b"smrx" => {
let (addr_len, address_and_sig) = split(data, 4)?;
let addr_len = decode_u32(Some(addr_len))?;
let (address, sig) = split(address_and_sig, addr_len as usize)?;
let address = bytes_as_string(address)?.to_owned();
let signature: [u8; 65] = sig.try_into().map_err(|_| DecodeError::Decode("sig"))?;
Ok(Response::MessageSigned { address, signature })
}
b"err_" => Err(DecodeError::Protocol(
bytes_as_string(data).map(str::to_owned)?,
)),
b"fram" => Err(DecodeError::Framing(
bytes_as_string(data).map(str::to_owned)?,
)),
_ => Err(DecodeError::UnknownFrame(command.to_owned())),
}
}
pub fn is_ok(&self) -> bool {
matches!(self, Self::Ok)
}
pub fn into_int1(self) -> Result<u32, Self> {
if let Self::Int1(i) = self {
Ok(i)
} else {
Err(self)
}
}
pub fn into_ascii(self) -> Result<String, Self> {
if let Self::Ascii(s) = self {
Ok(s)
} else {
Err(self)
}
}
pub fn into_binary(self) -> Result<Vec<u8>, Self> {
if let Self::Binary(v) = self {
Ok(v)
} else {
Err(self)
}
}
#[allow(clippy::type_complexity)]
pub fn into_my_pub(self) -> Result<([u8; 64], [u8; 4], Option<String>), Self> {
if let Self::MyPub {
dev_pubkey,
xpub_fingerprint,
xpub,
} = self
{
Ok((dev_pubkey, xpub_fingerprint, xpub))
} else {
Err(self)
}
}
pub fn into_ok(self) -> Result<(), Response> {
if self.is_ok() { Ok(()) } else { Err(self) }
}
}
fn decode_u32(bytes: Option<&[u8]>) -> Result<u32, DecodeError> {
match bytes {
Some(bytes) if bytes.len() == 4 => Ok(u32::from_le_bytes(
bytes.try_into().map_err(|_| DecodeError::Decode("u32"))?,
)),
_ => Err(DecodeError::Decode("u32")),
}
}
fn decode_u64(bytes: Option<&[u8]>) -> Result<u64, DecodeError> {
match bytes {
Some(bytes) if bytes.len() == 8 => Ok(u64::from_le_bytes(
bytes.try_into().map_err(|_| DecodeError::Decode("u64"))?,
)),
_ => Err(DecodeError::Decode("u64")),
}
}
fn decode_u128(bytes: Option<&[u8]>) -> Result<u128, DecodeError> {
match bytes {
Some(bytes) if bytes.len() == 12 => Ok(u128::from_le_bytes(
bytes
.iter()
.chain(&[0, 0, 0, 0])
.cloned()
.collect::<Vec<_>>()
.try_into()
.map_err(|_| DecodeError::Decode("u128"))?,
)),
_ => Err(DecodeError::Decode("u128")),
}
}
fn split(bytes: &[u8], mid: usize) -> Result<(&[u8], &[u8]), DecodeError> {
match bytes.len().cmp(&mid) {
std::cmp::Ordering::Less => Err(DecodeError::Decode("unexpected slice length")),
_ => Ok(bytes.split_at(mid)),
}
}
fn bytes_as_string(bytes: &[u8]) -> Result<&str, DecodeError> {
use std::str;
str::from_utf8(bytes).map_err(|_| DecodeError::Decode("utf8"))
}
#[derive(Debug)]
pub enum DecodeError {
UnknownFrame(Vec<u8>),
Framing(String),
Decode(&'static str),
Protocol(String),
Utf8(std::string::FromUtf8Error),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_test() {
encode_eq(b"logo", Request::Logout);
encode_eq(b"rebo", Request::Reboot);
encode_eq(b"vers", Request::Version);
encode_eq(b"pingHello", Request::Ping("Hello".as_bytes().to_owned()));
encode_eq(
b"pass123",
Request::Bip39Passphrase(Passphrase::new("123").unwrap()),
);
encode_eq(
b"pass",
Request::Bip39Passphrase(Passphrase::new("").unwrap()),
);
encode_eq(b"pwok", Request::GetPassphraseDone);
encode_eq(b"mitm", Request::CheckMitm);
encode_eq(b"back", Request::StartBackup);
encode_eq(
&[
110, 99, 114, 121, 1, 0, 0, 0, 82, 246, 129, 254, 167, 146, 135, 174, 60, 60, 152,
151, 192, 167, 53, 120, 248, 31, 108, 213, 131, 160, 94, 44, 58, 189, 111, 107,
237, 24, 89, 172, 82, 246, 129, 254, 167, 146, 135, 174, 60, 60, 152, 151, 192,
167, 53, 120, 248, 31, 108, 213, 131, 160, 94, 44, 58, 189, 111, 107, 237, 24, 89,
172,
],
Request::EncryptStart {
device_pubkey: BYTES_64.to_owned(),
version: None,
},
);
encode_eq(
b"upld\x05\x00\x00\x00\x07\x00\x00\x00data 123",
Request::Upload {
offset: 5,
total_size: 7,
data: Upload::new("data 123".as_bytes()).unwrap(),
},
);
encode_eq(
b"dwld\x05\x00\x00\x00\x07\x00\x00\x00\x01\x00\x00\x00",
Request::Download {
offset: 5,
length: 7,
file_number: FileNo::One,
},
);
encode_eq(b"sha2", Request::Sha256);
encode_eq(
&[
115, 116, 120, 110, 89, 1, 0, 0, 7, 0, 0, 0, 82, 246, 129, 254, 167, 146, 135, 174,
60, 60, 152, 151, 192, 167, 53, 120, 248, 31, 108, 213, 131, 160, 94, 44, 58, 189,
111, 107, 237, 24, 89, 172,
],
Request::SignTransaction {
length: 345,
file_sha: BYTES_32.to_owned(),
flags: Some(STXN_FINALIZE | STXN_SIGNED | STXN_VISUALIZE),
descriptor_name: None,
},
);
encode_eq(
&[
115, 116, 120, 110, 89, 1, 0, 0, 7, 0, 0, 0, 82, 246, 129, 254, 167, 146, 135, 174,
60, 60, 152, 151, 192, 167, 53, 120, 248, 31, 108, 213, 131, 160, 94, 44, 58, 189,
111, 107, 237, 24, 89, 172, 4, 116, 101, 115, 116,
],
Request::SignTransaction {
length: 345,
file_sha: BYTES_32.to_owned(),
flags: Some(STXN_FINALIZE | STXN_SIGNED | STXN_VISUALIZE),
descriptor_name: Some(DescriptorName::new("test").unwrap()),
},
);
encode_eq(
&[
115, 109, 115, 103, 19, 0, 0, 0, 1, 0, 0, 0, 11, 0, 0, 0, 109, 72, 101, 108, 108,
111, 32, 87, 111, 114, 108, 100,
],
Request::SignMessage {
raw_msg: Message::new("Hello World").unwrap(),
subpath: None,
addr_fmt: AddressFormat::P2WPKH_P2SH,
},
);
encode_eq(b"smok", Request::GetSignedMessage);
encode_eq(b"bkok", Request::GetBackupFile);
encode_eq(b"stok", Request::GetSignedTransaction);
encode_eq(
&[
101, 110, 114, 108, 99, 0, 0, 0, 82, 246, 129, 254, 167, 146, 135, 174, 60, 60,
152, 151, 192, 167, 53, 120, 248, 31, 108, 213, 131, 160, 94, 44, 58, 189, 111,
107, 237, 24, 89, 172,
],
Request::MultisigEnroll {
length: 99,
file_sha: BYTES_32.to_owned(),
},
);
encode_eq(
b"msckd\x00\x00\x00\xc8\x00\x00\x00,\x01\x00\x00",
Request::MultiSigCheck {
m: 100,
n: 200,
xfp_xor: 300,
},
);
encode_eq(b"xpubm", Request::GetXPub(None));
encode_eq(
b"xpubm/44'/0'/0'/0/0",
Request::GetXPub(DerivationPath::new("m/44'/0'/0'/0/0").ok()),
);
encode_eq(
b"show\x07\x00\x00\x00m/44'/0'/0'/0/0",
Request::ShowAddress {
subpath: DerivationPath::new("m/44'/0'/0'/0/0").unwrap(),
addr_fmt: AddressFormat::P2WPKH,
},
);
encode_eq(
&[
112, 50, 115, 104, 8, 0, 0, 0, 5, 2, 32, 0, 82, 246, 129, 254, 167, 146, 135, 174,
60, 60, 152, 151, 192, 167, 53, 120, 248, 31, 108, 213, 131, 160, 94, 44, 58, 189,
111, 107, 237, 24, 89, 172, 4, 67, 105, 5, 15, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
4, 81, 37, 21, 158, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0,
],
Request::ShowP2SHAddress {
min_signers: 5,
redeem_script: RedeemScript::new(BYTES_32).unwrap(),
xfp_paths: vec![
XfpPath {
fingerprint: 252012867,
path: DerivationPath::new("m/1/0/0").unwrap(),
},
XfpPath {
fingerprint: 2652185937,
path: DerivationPath::new("m/4/4/0").unwrap(),
},
],
address_format: AddressFormat::P2SH,
},
);
encode_eq(b"blkc", Request::Blockchain);
encode_eq(b"bagi", Request::BagNumber(None));
encode_eq(
b"bagi123abc",
Request::BagNumber(Some("123abc".to_string())),
);
encode_eq(b"hsms", Request::HsmStart(None));
encode_eq(
&[
104, 115, 109, 115, 89, 1, 0, 0, 82, 246, 129, 254, 167, 146, 135, 174, 60, 60,
152, 151, 192, 167, 53, 120, 248, 31, 108, 213, 131, 160, 94, 44, 58, 189, 111,
107, 237, 24, 89, 172,
],
Request::HsmStart(Some(HsmStartParams {
length: 345,
file_sha: BYTES_32.to_owned(),
})),
);
encode_eq(b"hsts", Request::HsmStatus);
encode_eq(
b"nwur\x02\x05\x00user1",
Request::CreateUser {
auth_mode: AuthMode::HOTP,
username: Username::new("user1").unwrap(),
secret: None,
show_qr: false,
},
);
encode_eq(
b"nwur\x82\x05\nuser1secret1234",
Request::CreateUser {
auth_mode: AuthMode::HOTP,
username: Username::new("user1").unwrap(),
secret: Secret::new("secret1234").ok(),
show_qr: true,
},
);
encode_eq(
b"rmur\x05user1",
Request::DeleteUser(Username::new("user1").unwrap()),
);
encode_eq(
b"user\x0c\x00\x00\x00\x05\tuser1token1234",
Request::UserAuth {
username: Username::new("user1").unwrap(),
token: AuthToken::new("token1234").unwrap(),
totp_time: 12,
},
);
encode_eq(b"gslr", Request::GetStorageLocker);
}
#[test]
fn decode_test() {
assert!(matches!(
Response::decode(b"abcd"),
Err(DecodeError::UnknownFrame(name)) if name == [97, 98, 99, 100]));
assert!(matches!(
Response::decode(b"fram1234"),
Err(DecodeError::Framing(text)) if text == "1234",
));
assert!(matches!(
Response::decode(b"err_1234"),
Err(DecodeError::Protocol(text)) if text == "1234",
));
assert!(matches!(Response::decode(b"refu"), Ok(Response::Refused)));
assert!(matches!(Response::decode(b"busy"), Ok(Response::Busy)));
assert!(
matches!(Response::decode(b"biny1234"), Ok(Response::Binary(data)) if data == [49, 50, 51, 52])
);
assert!(
matches!(Response::decode(b"int1\xFE\xFF\xFF\xFF"), Ok(Response::Int1(i)) if i == u32::MAX - 1)
);
assert!(
matches!(Response::decode(b"int2\xFE\xFF\xFF\xFF\xFF\xFF\xFF\xFF"),
Ok(Response::Int2(i)) if i == u64::MAX - 1)
);
assert!(
matches!(Response::decode(b"int3\xFE\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"),
Ok(Response::Int3(i)) if i == 79228162514264337593543950334_u128)
);
assert!(matches!(
Response::decode(&[
109, 121, 112, 98, 82, 246, 129, 254, 167, 146, 135, 174, 60, 60, 152, 151, 192, 167, 53,
120, 248, 31, 108, 213, 131, 160, 94, 44, 58, 189, 111, 107, 237, 24, 89, 172, 82, 246,
129, 254, 167, 146, 135, 174, 60, 60, 152, 151, 192, 167, 53, 120, 248, 31, 108, 213, 131,
160, 94, 44, 58, 189, 111, 107, 237, 24, 89, 172, 64, 226, 1, 0, 111, 0, 0, 0, 116, 112,
117, 98, 68, 65, 101, 110, 102, 119, 78, 117, 53, 71, 121, 67, 74, 87, 118, 56, 111, 113,
82, 65, 99, 107, 100, 75, 77, 83, 85, 111, 90, 106, 103, 86, 70, 53, 112, 56, 87, 118, 81,
119, 72, 81, 101, 88, 106, 68, 104, 65, 72, 109, 71, 114, 80, 97, 52, 97, 52, 121, 50, 70,
110, 55, 72, 70, 50, 110, 102, 67, 76, 101, 102, 74, 97, 110, 72, 86, 51, 110, 121, 49, 85,
89, 50, 53, 77, 82, 86, 111, 103, 105, 122, 66, 50, 122, 82, 85, 100, 65, 111, 55, 84, 114,
57, 88, 65, 106, 109,
]),
Ok(Response::MyPub {
dev_pubkey,
xpub_fingerprint,
xpub: Some(xpub)
}) if &dev_pubkey == BYTES_64 && xpub_fingerprint == [64, 226, 1, 0] &&
xpub == XPUB
));
assert!(matches!(Response::decode(b"ascihello"),
Ok(Response::Ascii(s)) if &s == "hello"));
assert!(matches!(
Response::decode(&[
115, 109, 114, 120, 34, 0, 0, 0, 49, 81, 50, 84, 87, 72, 69, 51, 71, 77, 100, 66,
54, 66, 90, 75, 97, 102, 113, 119, 120, 88, 116, 87, 65, 87, 103, 70, 116, 53, 74,
118, 109, 51, 82, 246, 129, 254, 167, 146, 135, 174, 60, 60, 152, 151, 192, 167,
53, 120, 248, 31, 108, 213, 131, 160, 94, 44, 58, 189, 111, 107, 237, 24, 89, 172,
82, 246, 129, 254, 167, 146, 135, 174, 60, 60, 152, 151, 192, 167, 53, 120, 248,
31, 108, 213, 131, 160, 94, 44, 58, 189, 111, 107, 237, 24, 89, 172, 55
]),
Ok(Response::MessageSigned { address, signature })
if &address.to_string() == "1Q2TWHE3GMdB6BZKafqwxXtWAWgFt5Jvm3"
&& &signature[0..64] == BYTES_64 && signature[64] == 55
));
assert!(matches!(
Response::decode(&[
115, 116, 114, 120, 89, 1, 0, 0, 82, 246, 129, 254, 167, 146, 135, 174, 60, 60,
152, 151, 192, 167, 53, 120, 248, 31, 108, 213, 131, 160, 94, 44, 58, 189, 111,
107, 237, 24, 89, 172
]),
Ok(Response::TxSigned { length, sha256 }) if length == 345 && &sha256 == BYTES_32));
}
fn encode_eq(a: &'static [u8], b: Request) {
assert_eq!(a, b.encode().as_slice());
}
const BYTES_32: &[u8; 32] = &[
82, 246, 129, 254, 167, 146, 135, 174, 60, 60, 152, 151, 192, 167, 53, 120, 248, 31, 108,
213, 131, 160, 94, 44, 58, 189, 111, 107, 237, 24, 89, 172,
];
const BYTES_64: &[u8; 64] = &[
82, 246, 129, 254, 167, 146, 135, 174, 60, 60, 152, 151, 192, 167, 53, 120, 248, 31, 108,
213, 131, 160, 94, 44, 58, 189, 111, 107, 237, 24, 89, 172, 82, 246, 129, 254, 167, 146,
135, 174, 60, 60, 152, 151, 192, 167, 53, 120, 248, 31, 108, 213, 131, 160, 94, 44, 58,
189, 111, 107, 237, 24, 89, 172,
];
const XPUB: &str = "tpubDAenfwNu5GyCJWv8oqRAckdKMSUoZjgVF5p8WvQwHQeXjDhAHmGrPa4a4y2Fn7HF2nfCLefJanHV3ny1UY25MRVogizB2zRUdAo7Tr9XAjm";
}