Ins (APDU instruction codes) enum
Converts a bag of constant values (`YKPIV_INS_*`) into an enum representing APDU instruction codes (a.k.a. `ins`). Among other things, this makes the `Debug` output for `APDU` more human meaningful, since it can print a text label for the instruction rather than a code number, which is helpful in trace debugging.
This commit is contained in:
+120
-26
@@ -32,7 +32,6 @@
|
||||
|
||||
use crate::{error::Error, transaction::Transaction, Buffer};
|
||||
use log::trace;
|
||||
use std::fmt::{self, Debug};
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
/// Maximum amount of command data that can be included in an APDU
|
||||
@@ -41,13 +40,13 @@ const APDU_DATA_MAX: usize = 0xFF;
|
||||
/// Application Protocol Data Unit (APDU).
|
||||
///
|
||||
/// These messages are packets used to communicate with the YubiKey.
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct APDU {
|
||||
/// Instruction class: indicates the type of command (e.g. inter-industry or proprietary)
|
||||
cla: u8,
|
||||
|
||||
/// Instruction code: indicates the specific command (e.g. "write data")
|
||||
ins: u8,
|
||||
ins: Ins,
|
||||
|
||||
/// Instruction parameter 1 for the command (e.g. offset into file at which to write the data)
|
||||
p1: u8,
|
||||
@@ -61,10 +60,10 @@ pub(crate) struct APDU {
|
||||
|
||||
impl APDU {
|
||||
/// Create a new APDU with the given instruction code
|
||||
pub fn new(ins: u8) -> Self {
|
||||
pub fn new(ins: impl Into<Ins>) -> Self {
|
||||
Self {
|
||||
cla: 0,
|
||||
ins,
|
||||
ins: ins.into(),
|
||||
p1: 0,
|
||||
p2: 0,
|
||||
data: vec![],
|
||||
@@ -123,7 +122,7 @@ impl APDU {
|
||||
pub fn to_bytes(&self) -> Buffer {
|
||||
let mut bytes = Vec::with_capacity(5 + self.data.len());
|
||||
bytes.push(self.cla);
|
||||
bytes.push(self.ins);
|
||||
bytes.push(self.ins.code());
|
||||
bytes.push(self.p1);
|
||||
bytes.push(self.p2);
|
||||
bytes.push(self.data.len() as u8);
|
||||
@@ -132,21 +131,6 @@ impl APDU {
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for APDU {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"APDU {{ cla: {}, ins: {}, p1: {}, p2: {}, lc: {}, data: {:?} }}",
|
||||
self.cla,
|
||||
self.ins,
|
||||
self.p1,
|
||||
self.p2,
|
||||
self.data.len(),
|
||||
self.data.as_slice()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for APDU {
|
||||
fn drop(&mut self) {
|
||||
self.zeroize();
|
||||
@@ -155,16 +139,125 @@ impl Drop for APDU {
|
||||
|
||||
impl Zeroize for APDU {
|
||||
fn zeroize(&mut self) {
|
||||
self.cla.zeroize();
|
||||
self.ins.zeroize();
|
||||
self.p1.zeroize();
|
||||
self.p2.zeroize();
|
||||
// Only `data` may contain secrets
|
||||
self.data.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
/// APDU instruction codes
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Ins {
|
||||
/// Verify
|
||||
Verify,
|
||||
|
||||
/// Change reference
|
||||
ChangeReference,
|
||||
|
||||
/// Reset retry
|
||||
ResetRetry,
|
||||
|
||||
/// Generate asymmetric
|
||||
GenerateAsymmetric,
|
||||
|
||||
/// Authenticate
|
||||
Authenticate,
|
||||
|
||||
/// Get data
|
||||
GetData,
|
||||
|
||||
/// Put data
|
||||
PutData,
|
||||
|
||||
/// Select application
|
||||
SelectApplication,
|
||||
|
||||
/// Get response APDU
|
||||
GetResponseApdu,
|
||||
|
||||
// Yubico vendor specific instructions
|
||||
// <https://developers.yubico.com/PIV/Introduction/Yubico_extensions.html>
|
||||
/// Set MGM key
|
||||
SetMgmKey,
|
||||
|
||||
/// Import key
|
||||
ImportKey,
|
||||
|
||||
/// Get version
|
||||
GetVersion,
|
||||
|
||||
/// Reset device
|
||||
Reset,
|
||||
|
||||
/// Set PIN retries
|
||||
SetPinRetries,
|
||||
|
||||
/// Generate attestation certificate for asymmetric key
|
||||
Attest,
|
||||
|
||||
/// Get device serial
|
||||
GetSerial,
|
||||
|
||||
/// Other/unrecognized instruction codes
|
||||
Other(u8),
|
||||
}
|
||||
|
||||
impl Ins {
|
||||
/// Get the code that corresponds to this instruction
|
||||
pub fn code(self) -> u8 {
|
||||
match self {
|
||||
Ins::Verify => 0x20,
|
||||
Ins::ChangeReference => 0x24,
|
||||
Ins::ResetRetry => 0x2c,
|
||||
Ins::GenerateAsymmetric => 0x47,
|
||||
Ins::Authenticate => 0x87,
|
||||
Ins::GetData => 0xcb,
|
||||
Ins::PutData => 0xdb,
|
||||
Ins::SelectApplication => 0xa4,
|
||||
Ins::GetResponseApdu => 0xc0,
|
||||
Ins::SetMgmKey => 0xff,
|
||||
Ins::ImportKey => 0xfe,
|
||||
Ins::GetVersion => 0xfd,
|
||||
Ins::Reset => 0xfb,
|
||||
Ins::SetPinRetries => 0xfa,
|
||||
Ins::Attest => 0xf9,
|
||||
Ins::GetSerial => 0xf8,
|
||||
Ins::Other(code) => code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for Ins {
|
||||
fn from(code: u8) -> Self {
|
||||
match code {
|
||||
0x20 => Ins::Verify,
|
||||
0x24 => Ins::ChangeReference,
|
||||
0x2c => Ins::ResetRetry,
|
||||
0x47 => Ins::GenerateAsymmetric,
|
||||
0x87 => Ins::Authenticate,
|
||||
0xcb => Ins::GetData,
|
||||
0xdb => Ins::PutData,
|
||||
0xa4 => Ins::SelectApplication,
|
||||
0xc0 => Ins::GetResponseApdu,
|
||||
0xff => Ins::SetMgmKey,
|
||||
0xfe => Ins::ImportKey,
|
||||
0xfd => Ins::GetVersion,
|
||||
0xfb => Ins::Reset,
|
||||
0xfa => Ins::SetPinRetries,
|
||||
0xf9 => Ins::Attest,
|
||||
0xf8 => Ins::GetSerial,
|
||||
code => Ins::Other(code),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Ins> for u8 {
|
||||
fn from(ins: Ins) -> u8 {
|
||||
ins.code()
|
||||
}
|
||||
}
|
||||
|
||||
/// APDU responses
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct Response {
|
||||
/// Status words
|
||||
status_words: StatusWords,
|
||||
@@ -239,6 +332,7 @@ impl From<Vec<u8>> for Response {
|
||||
|
||||
impl Zeroize for Response {
|
||||
fn zeroize(&mut self) {
|
||||
// Only `data` may contain secrets
|
||||
self.data.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,26 +136,6 @@ pub const YKPIV_CCCID_SIZE: usize = 14;
|
||||
pub const YKPIV_CERTINFO_UNCOMPRESSED: u8 = 0;
|
||||
pub const YKPIV_CERTINFO_GZIP: u8 = 1;
|
||||
|
||||
pub const YKPIV_INS_VERIFY: u8 = 0x20;
|
||||
pub const YKPIV_INS_CHANGE_REFERENCE: u8 = 0x24;
|
||||
pub const YKPIV_INS_RESET_RETRY: u8 = 0x2c;
|
||||
pub const YKPIV_INS_GENERATE_ASYMMETRIC: u8 = 0x47;
|
||||
pub const YKPIV_INS_AUTHENTICATE: u8 = 0x87;
|
||||
pub const YKPIV_INS_GET_DATA: u8 = 0xcb;
|
||||
pub const YKPIV_INS_PUT_DATA: u8 = 0xdb;
|
||||
pub const YKPIV_INS_SELECT_APPLICATION: u8 = 0xa4;
|
||||
pub const YKPIV_INS_GET_RESPONSE_APDU: u8 = 0xc0;
|
||||
|
||||
// Yubico vendor specific instructions
|
||||
// <https://developers.yubico.com/PIV/Introduction/Yubico_extensions.html>
|
||||
pub const YKPIV_INS_SET_MGMKEY: u8 = 0xff;
|
||||
pub const YKPIV_INS_IMPORT_KEY: u8 = 0xfe;
|
||||
pub const YKPIV_INS_GET_VERSION: u8 = 0xfd;
|
||||
pub const YKPIV_INS_RESET: u8 = 0xfb;
|
||||
pub const YKPIV_INS_SET_PIN_RETRIES: u8 = 0xfa;
|
||||
pub const YKPIV_INS_ATTEST: u8 = 0xf9;
|
||||
pub const YKPIV_INS_GET_SERIAL: u8 = 0xf8;
|
||||
|
||||
pub const YKPIV_KEY_AUTHENTICATION: u8 = 0x9a;
|
||||
pub const YKPIV_KEY_CARDMGM: u8 = 0x9b;
|
||||
pub const YKPIV_KEY_SIGNATURE: u8 = 0x9c;
|
||||
|
||||
+2
-2
@@ -38,7 +38,7 @@
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
use crate::{
|
||||
apdu::StatusWords,
|
||||
apdu::{Ins, StatusWords},
|
||||
certificate::{self, Certificate},
|
||||
consts::*,
|
||||
error::Error,
|
||||
@@ -199,7 +199,7 @@ pub fn generate(
|
||||
touch_policy: u8,
|
||||
) -> Result<GeneratedKey, Error> {
|
||||
let mut in_data = [0u8; 11];
|
||||
let mut templ = [0, YKPIV_INS_GENERATE_ASYMMETRIC, 0, 0];
|
||||
let mut templ = [0, Ins::GenerateAsymmetric.code(), 0, 0];
|
||||
let setting_roca: settings::BoolValue;
|
||||
|
||||
if yubikey.device_model() == DEVTYPE_YK4
|
||||
|
||||
+20
-19
@@ -1,9 +1,14 @@
|
||||
//! YubiKey PC/SC transactions
|
||||
|
||||
use crate::{apdu::APDU, consts::*, error::Error, yubikey::*};
|
||||
use crate::{
|
||||
apdu::{Ins, APDU},
|
||||
error::Error,
|
||||
yubikey::*,
|
||||
};
|
||||
#[cfg(feature = "untested")]
|
||||
use crate::{
|
||||
apdu::{Response, StatusWords},
|
||||
consts::*,
|
||||
mgm::MgmKey,
|
||||
serialization::*,
|
||||
Buffer, ObjectId,
|
||||
@@ -60,7 +65,7 @@ impl<'tx> Transaction<'tx> {
|
||||
|
||||
/// Select application.
|
||||
pub fn select_application(&self) -> Result<(), Error> {
|
||||
let response = APDU::new(YKPIV_INS_SELECT_APPLICATION)
|
||||
let response = APDU::new(Ins::SelectApplication)
|
||||
.p1(0x04)
|
||||
.data(&AID)
|
||||
.transmit(self, 0xFF)
|
||||
@@ -83,7 +88,7 @@ impl<'tx> Transaction<'tx> {
|
||||
/// Get the version of the PIV application installed on the YubiKey
|
||||
pub fn get_version(&self) -> Result<Version, Error> {
|
||||
// get version from device
|
||||
let response = APDU::new(YKPIV_INS_GET_VERSION).transmit(self, 261)?;
|
||||
let response = APDU::new(Ins::GetVersion).transmit(self, 261)?;
|
||||
|
||||
if !response.is_success() {
|
||||
return Err(Error::GenericError);
|
||||
@@ -93,11 +98,7 @@ impl<'tx> Transaction<'tx> {
|
||||
return Err(Error::SizeError);
|
||||
}
|
||||
|
||||
Ok(Version {
|
||||
major: response.data()[0],
|
||||
minor: response.data()[1],
|
||||
patch: response.data()[2],
|
||||
})
|
||||
Ok(Version::new(response.data()[..3].try_into().unwrap()))
|
||||
}
|
||||
|
||||
/// Get YubiKey device serial number
|
||||
@@ -106,7 +107,7 @@ impl<'tx> Transaction<'tx> {
|
||||
|
||||
let response = if version.major < 5 {
|
||||
// get serial from neo/yk4 devices using the otp applet
|
||||
let sw = APDU::new(YKPIV_INS_SELECT_APPLICATION)
|
||||
let sw = APDU::new(Ins::SelectApplication)
|
||||
.p1(0x04)
|
||||
.data(&yk_applet)
|
||||
.transmit(self, 0xFF)?
|
||||
@@ -128,7 +129,7 @@ impl<'tx> Transaction<'tx> {
|
||||
}
|
||||
|
||||
// reselect the PIV applet
|
||||
let sw = APDU::new(YKPIV_INS_SELECT_APPLICATION)
|
||||
let sw = APDU::new(Ins::SelectApplication)
|
||||
.p1(0x04)
|
||||
.data(&AID)
|
||||
.transmit(self, 0xFF)?
|
||||
@@ -142,7 +143,7 @@ impl<'tx> Transaction<'tx> {
|
||||
resp
|
||||
} else {
|
||||
// get serial from yk5 and later devices using the f8 command
|
||||
let resp = APDU::new(YKPIV_INS_GET_SERIAL).transmit(self, 0xFF)?;
|
||||
let resp = APDU::new(Ins::GetSerial).transmit(self, 0xFF)?;
|
||||
|
||||
if !resp.is_success() {
|
||||
error!(
|
||||
@@ -169,7 +170,7 @@ impl<'tx> Transaction<'tx> {
|
||||
return Err(Error::SizeError);
|
||||
}
|
||||
|
||||
let response = APDU::new(YKPIV_INS_VERIFY)
|
||||
let response = APDU::new(Ins::Verify)
|
||||
.params(0x00, 0x80)
|
||||
.data(pin)
|
||||
.transmit(self, 261)?;
|
||||
@@ -185,7 +186,7 @@ impl<'tx> Transaction<'tx> {
|
||||
/// Change the PIN
|
||||
#[cfg(feature = "untested")]
|
||||
pub fn change_pin(&self, action: i32, current_pin: &[u8], new_pin: &[u8]) -> Result<(), Error> {
|
||||
let mut templ = [0, YKPIV_INS_CHANGE_REFERENCE, 0, 0x80];
|
||||
let mut templ = [0, Ins::ChangeReference.code(), 0, 0x80];
|
||||
let mut indata = Zeroizing::new([0u8; 16]);
|
||||
|
||||
if current_pin.len() > CB_PIN_MAX || new_pin.len() > CB_PIN_MAX {
|
||||
@@ -193,7 +194,7 @@ impl<'tx> Transaction<'tx> {
|
||||
}
|
||||
|
||||
if action == CHREF_ACT_UNBLOCK_PIN {
|
||||
templ[1] = YKPIV_INS_RESET_RETRY;
|
||||
templ[1] = Ins::ResetRetry.code();
|
||||
} else if action == CHREF_ACT_CHANGE_PUK {
|
||||
templ[3] = 0x81;
|
||||
}
|
||||
@@ -259,7 +260,7 @@ impl<'tx> Transaction<'tx> {
|
||||
data[2] = DES_LEN_3DES as u8;
|
||||
data[3..3 + DES_LEN_3DES].copy_from_slice(new_key.as_ref());
|
||||
|
||||
let status_words = APDU::new(YKPIV_INS_SET_MGMKEY)
|
||||
let status_words = APDU::new(Ins::SetMgmKey)
|
||||
.params(0xff, p2)
|
||||
.data(&data)
|
||||
.transmit(self, 261)?
|
||||
@@ -290,7 +291,7 @@ impl<'tx> Transaction<'tx> {
|
||||
) -> Result<(), Error> {
|
||||
let in_len = sign_in.len();
|
||||
let mut indata = [0u8; 1024];
|
||||
let templ = [0, YKPIV_INS_AUTHENTICATE, algorithm, key];
|
||||
let templ = [0, Ins::Authenticate.code(), algorithm, key];
|
||||
let mut len: usize = 0;
|
||||
|
||||
match algorithm {
|
||||
@@ -454,7 +455,7 @@ impl<'tx> Transaction<'tx> {
|
||||
sw & 0xff
|
||||
);
|
||||
|
||||
let response = APDU::new(YKPIV_INS_GET_RESPONSE_APDU).transmit(self, 261)?;
|
||||
let response = APDU::new(Ins::GetResponseApdu).transmit(self, 261)?;
|
||||
sw = response.status_words().code();
|
||||
|
||||
if sw != StatusWords::Success.code() && (sw >> 8 != 0x61) {
|
||||
@@ -481,7 +482,7 @@ impl<'tx> Transaction<'tx> {
|
||||
#[cfg(feature = "untested")]
|
||||
pub fn fetch_object(&self, object_id: ObjectId) -> Result<Buffer, Error> {
|
||||
let mut indata = [0u8; 5];
|
||||
let templ = [0, YKPIV_INS_GET_DATA, 0x3f, 0xff];
|
||||
let templ = [0, Ins::GetData.code(), 0x3f, 0xff];
|
||||
|
||||
let mut inlen = indata.len();
|
||||
let indata_remaining = set_object(object_id, &mut indata);
|
||||
@@ -524,7 +525,7 @@ impl<'tx> Transaction<'tx> {
|
||||
/// Save an object
|
||||
#[cfg(feature = "untested")]
|
||||
pub fn save_object(&self, object_id: ObjectId, indata: &[u8]) -> Result<(), Error> {
|
||||
let templ = [0, YKPIV_INS_PUT_DATA, 0x3f, 0xff];
|
||||
let templ = [0, Ins::PutData.code(), 0x3f, 0xff];
|
||||
|
||||
// TODO(tarcieri): replace with vector
|
||||
let mut data = [0u8; CB_BUF_MAX];
|
||||
|
||||
+21
-10
@@ -35,7 +35,7 @@
|
||||
|
||||
#[cfg(feature = "untested")]
|
||||
use crate::{
|
||||
apdu::{StatusWords, APDU},
|
||||
apdu::{Ins, StatusWords, APDU},
|
||||
key::SlotId,
|
||||
metadata,
|
||||
mgm::MgmKey,
|
||||
@@ -99,6 +99,17 @@ pub struct Version {
|
||||
pub patch: u8,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
/// Parse a version from bytes
|
||||
pub fn new(bytes: [u8; 3]) -> Version {
|
||||
Version {
|
||||
major: bytes[0],
|
||||
minor: bytes[1],
|
||||
patch: bytes[2],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// YubiKey Device: this is the primary API for opening a session and
|
||||
/// performing various operations.
|
||||
///
|
||||
@@ -270,7 +281,7 @@ impl YubiKey {
|
||||
let txn = self.begin_transaction()?;
|
||||
|
||||
// get a challenge from the card
|
||||
let challenge = APDU::new(YKPIV_INS_AUTHENTICATE)
|
||||
let challenge = APDU::new(Ins::Authenticate)
|
||||
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
|
||||
.data(&[0x7c, 0x02, 0x80, 0x00])
|
||||
.transmit(&txn, 261)?;
|
||||
@@ -299,7 +310,7 @@ impl YubiKey {
|
||||
let mut challenge = [0u8; 8];
|
||||
challenge.copy_from_slice(&data[14..22]);
|
||||
|
||||
let authentication = APDU::new(YKPIV_INS_AUTHENTICATE)
|
||||
let authentication = APDU::new(Ins::Authenticate)
|
||||
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
|
||||
.data(&data)
|
||||
.transmit(&txn, 261)?;
|
||||
@@ -324,7 +335,7 @@ impl YubiKey {
|
||||
pub fn deauthenticate(&mut self) -> Result<(), Error> {
|
||||
let txn = self.begin_transaction()?;
|
||||
|
||||
let status_words = APDU::new(YKPIV_INS_SELECT_APPLICATION)
|
||||
let status_words = APDU::new(Ins::SelectApplication)
|
||||
.p1(0x04)
|
||||
.data(MGMT_AID)
|
||||
.transmit(&txn, 255)?
|
||||
@@ -422,7 +433,7 @@ impl YubiKey {
|
||||
|
||||
let templ = [
|
||||
0,
|
||||
YKPIV_INS_SET_PIN_RETRIES,
|
||||
Ins::SetPinRetries.code(),
|
||||
pin_tries as u8,
|
||||
puk_tries as u8,
|
||||
];
|
||||
@@ -644,7 +655,7 @@ impl YubiKey {
|
||||
|
||||
let mut key_data = Zeroizing::new(vec![0u8; 1024]);
|
||||
let mut in_ptr: *mut u8 = key_data.as_mut_ptr();
|
||||
let templ = [0, YKPIV_INS_IMPORT_KEY, algorithm, key];
|
||||
let templ = [0, Ins::ImportKey.code(), algorithm, key];
|
||||
let mut elem_len: u32 = 0;
|
||||
let mut params: [*const u8; 5] = [ptr::null(); 5];
|
||||
let mut lens = [0usize; 5];
|
||||
@@ -795,7 +806,7 @@ impl YubiKey {
|
||||
/// <https://developers.yubico.com/PIV/Introduction/PIV_attestation.html>
|
||||
#[cfg(feature = "untested")]
|
||||
pub fn attest(&mut self, key: SlotId) -> Result<Buffer, Error> {
|
||||
let templ = [0, YKPIV_INS_ATTEST, key, 0];
|
||||
let templ = [0, Ins::Attest.code(), key, 0];
|
||||
let txn = self.begin_transaction()?;
|
||||
let response = txn.transfer_data(&templ, &[], CB_OBJ_MAX)?;
|
||||
|
||||
@@ -819,7 +830,7 @@ impl YubiKey {
|
||||
pub fn get_auth_challenge(&mut self) -> Result<[u8; 8], Error> {
|
||||
let txn = self.begin_transaction()?;
|
||||
|
||||
let response = APDU::new(YKPIV_INS_AUTHENTICATE)
|
||||
let response = APDU::new(Ins::Authenticate)
|
||||
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
|
||||
.data(&[0x7c, 0x02, 0x81, 0x00])
|
||||
.transmit(&txn, 261)?;
|
||||
@@ -844,7 +855,7 @@ impl YubiKey {
|
||||
let txn = self.begin_transaction()?;
|
||||
|
||||
// send the response to the card and a challenge of our own.
|
||||
let status_words = APDU::new(YKPIV_INS_AUTHENTICATE)
|
||||
let status_words = APDU::new(Ins::Authenticate)
|
||||
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
|
||||
.data(&data)
|
||||
.transmit(&txn, 261)?
|
||||
@@ -864,7 +875,7 @@ impl YubiKey {
|
||||
/// The reset function is only available when both pins are blocked.
|
||||
#[cfg(feature = "untested")]
|
||||
pub fn reset_device(&mut self) -> Result<(), Error> {
|
||||
let templ = [0, YKPIV_INS_RESET, 0, 0];
|
||||
let templ = [0, Ins::Reset.code(), 0, 0];
|
||||
let txn = self.begin_transaction()?;
|
||||
let status_words = txn.transfer_data(&templ, &[], 255)?.status_words();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user