Use KeyGenerationTime in openpgp-card APIs (instead of u32 or SystemTime)
This commit is contained in:
parent
794b04725f
commit
7c8c72339b
7 changed files with 47 additions and 58 deletions
|
@ -2,18 +2,16 @@
|
||||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
use anyhow::{Error, Result};
|
use anyhow::{Error, Result};
|
||||||
use std::convert::TryInto;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::string::FromUtf8Error;
|
use std::string::FromUtf8Error;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use sequoia_openpgp::parse::Parse;
|
use sequoia_openpgp::parse::Parse;
|
||||||
use sequoia_openpgp::serialize::SerializeInto;
|
use sequoia_openpgp::serialize::SerializeInto;
|
||||||
use sequoia_openpgp::types::Timestamp;
|
|
||||||
use sequoia_openpgp::Cert;
|
use sequoia_openpgp::Cert;
|
||||||
|
|
||||||
use openpgp_card::algorithm::AlgoSimple;
|
use openpgp_card::algorithm::AlgoSimple;
|
||||||
use openpgp_card::card_do::Sex;
|
use openpgp_card::card_do::{KeyGenerationTime, Sex};
|
||||||
use openpgp_card::errors::{OcErrorStatus, OpenpgpCardError};
|
use openpgp_card::errors::{OcErrorStatus, OpenpgpCardError};
|
||||||
use openpgp_card::{CardApp, KeyType};
|
use openpgp_card::{CardApp, KeyType};
|
||||||
use openpgp_card_sequoia::{
|
use openpgp_card_sequoia::{
|
||||||
|
@ -96,7 +94,7 @@ pub fn test_sign(
|
||||||
|
|
||||||
fn check_key_upload_metadata(
|
fn check_key_upload_metadata(
|
||||||
ca: &mut CardApp,
|
ca: &mut CardApp,
|
||||||
meta: &[(String, u32)],
|
meta: &[(String, KeyGenerationTime)],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let ard = ca.get_app_data()?;
|
let ard = ca.get_app_data()?;
|
||||||
|
|
||||||
|
@ -117,21 +115,16 @@ fn check_key_upload_metadata(
|
||||||
// get_key_generation_times
|
// get_key_generation_times
|
||||||
let card_kg = ard.get_key_generation_times()?;
|
let card_kg = ard.get_key_generation_times()?;
|
||||||
|
|
||||||
let sig: u32 =
|
let sig = card_kg.signature().expect("signature creation time");
|
||||||
card_kg.signature().expect("signature creation time").into();
|
assert_eq!(sig, &meta[0].1);
|
||||||
assert_eq!(sig, meta[0].1);
|
|
||||||
|
|
||||||
let dec: u32 = card_kg
|
let dec = card_kg.decryption().expect("decryption creation time");
|
||||||
.decryption()
|
assert_eq!(dec, &meta[1].1);
|
||||||
.expect("decryption creation time")
|
|
||||||
.into();
|
|
||||||
assert_eq!(dec, meta[1].1);
|
|
||||||
|
|
||||||
let auth: u32 = card_kg
|
let auth = card_kg
|
||||||
.authentication()
|
.authentication()
|
||||||
.expect("authentication creation time")
|
.expect("authentication creation time");
|
||||||
.into();
|
assert_eq!(auth, &meta[2].1);
|
||||||
assert_eq!(auth, meta[2].1);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -220,11 +213,7 @@ pub fn test_keygen(
|
||||||
println!(" Generate subkey for Signing");
|
println!(" Generate subkey for Signing");
|
||||||
let (pkm, ts) =
|
let (pkm, ts) =
|
||||||
ca.generate_key_simple(public_to_fingerprint, KeyType::Signing, alg)?;
|
ca.generate_key_simple(public_to_fingerprint, KeyType::Signing, alg)?;
|
||||||
let key_sig = public_key_material_to_key(
|
let key_sig = public_key_material_to_key(&pkm, KeyType::Signing, ts)?;
|
||||||
&pkm,
|
|
||||||
KeyType::Signing,
|
|
||||||
Timestamp::from(ts).into(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
println!(" Generate subkey for Decryption");
|
println!(" Generate subkey for Decryption");
|
||||||
let (pkm, ts) = ca.generate_key_simple(
|
let (pkm, ts) = ca.generate_key_simple(
|
||||||
|
@ -232,11 +221,7 @@ pub fn test_keygen(
|
||||||
KeyType::Decryption,
|
KeyType::Decryption,
|
||||||
alg,
|
alg,
|
||||||
)?;
|
)?;
|
||||||
let key_dec = public_key_material_to_key(
|
let key_dec = public_key_material_to_key(&pkm, KeyType::Decryption, ts)?;
|
||||||
&pkm,
|
|
||||||
KeyType::Decryption,
|
|
||||||
Timestamp::from(ts).into(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
println!(" Generate subkey for Authentication");
|
println!(" Generate subkey for Authentication");
|
||||||
let (pkm, ts) = ca.generate_key_simple(
|
let (pkm, ts) = ca.generate_key_simple(
|
||||||
|
@ -244,11 +229,8 @@ pub fn test_keygen(
|
||||||
KeyType::Authentication,
|
KeyType::Authentication,
|
||||||
alg,
|
alg,
|
||||||
)?;
|
)?;
|
||||||
let key_aut = public_key_material_to_key(
|
let key_aut =
|
||||||
&pkm,
|
public_key_material_to_key(&pkm, KeyType::Authentication, ts)?;
|
||||||
KeyType::Authentication,
|
|
||||||
Timestamp::from(ts).into(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Generate a Cert for this set of generated keys
|
// Generate a Cert for this set of generated keys
|
||||||
|
|
||||||
|
@ -271,7 +253,7 @@ pub fn test_get_pub(
|
||||||
// --
|
// --
|
||||||
|
|
||||||
let sig = ca.get_pub_key(KeyType::Signing)?;
|
let sig = ca.get_pub_key(KeyType::Signing)?;
|
||||||
let ts = Timestamp::from(key_gen.signature().unwrap().get()).into();
|
let ts = key_gen.signature().unwrap().get().into();
|
||||||
let key = openpgp_card_sequoia::public_key_material_to_key(
|
let key = openpgp_card_sequoia::public_key_material_to_key(
|
||||||
&sig,
|
&sig,
|
||||||
KeyType::Signing,
|
KeyType::Signing,
|
||||||
|
@ -283,7 +265,7 @@ pub fn test_get_pub(
|
||||||
// --
|
// --
|
||||||
|
|
||||||
let dec = ca.get_pub_key(KeyType::Decryption)?;
|
let dec = ca.get_pub_key(KeyType::Decryption)?;
|
||||||
let ts = Timestamp::from(key_gen.decryption().unwrap().get()).into();
|
let ts = key_gen.decryption().unwrap().get().into();
|
||||||
let key = openpgp_card_sequoia::public_key_material_to_key(
|
let key = openpgp_card_sequoia::public_key_material_to_key(
|
||||||
&dec,
|
&dec,
|
||||||
KeyType::Decryption,
|
KeyType::Decryption,
|
||||||
|
@ -295,7 +277,7 @@ pub fn test_get_pub(
|
||||||
// --
|
// --
|
||||||
|
|
||||||
let auth = ca.get_pub_key(KeyType::Authentication)?;
|
let auth = ca.get_pub_key(KeyType::Authentication)?;
|
||||||
let ts = Timestamp::from(key_gen.authentication().unwrap().get()).into();
|
let ts = key_gen.authentication().unwrap().get().into();
|
||||||
let key = openpgp_card_sequoia::public_key_material_to_key(
|
let key = openpgp_card_sequoia::public_key_material_to_key(
|
||||||
&auth,
|
&auth,
|
||||||
KeyType::Authentication,
|
KeyType::Authentication,
|
||||||
|
|
|
@ -18,6 +18,7 @@ use sequoia_openpgp::serialize::stream::{
|
||||||
};
|
};
|
||||||
use sequoia_openpgp::Cert;
|
use sequoia_openpgp::Cert;
|
||||||
|
|
||||||
|
use openpgp_card::card_do::KeyGenerationTime;
|
||||||
use openpgp_card::{CardApp, KeyType};
|
use openpgp_card::{CardApp, KeyType};
|
||||||
use openpgp_card_sequoia::vka_as_uploadable_key;
|
use openpgp_card_sequoia::vka_as_uploadable_key;
|
||||||
|
|
||||||
|
@ -26,7 +27,7 @@ pub const SP: &StandardPolicy = &StandardPolicy::new();
|
||||||
pub(crate) fn upload_subkeys(
|
pub(crate) fn upload_subkeys(
|
||||||
ca: &mut CardApp,
|
ca: &mut CardApp,
|
||||||
cert: &Cert,
|
cert: &Cert,
|
||||||
) -> Result<Vec<(String, u32)>> {
|
) -> Result<Vec<(String, KeyGenerationTime)>> {
|
||||||
let mut out = vec![];
|
let mut out = vec![];
|
||||||
|
|
||||||
for kt in [
|
for kt in [
|
||||||
|
@ -45,7 +46,7 @@ pub(crate) fn upload_subkeys(
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_secs() as u32;
|
.as_secs() as u32;
|
||||||
|
|
||||||
out.push((fp, creation));
|
out.push((fp, creation.into()));
|
||||||
|
|
||||||
// upload key
|
// upload key
|
||||||
let cuk = vka_as_uploadable_key(vka, None);
|
let cuk = vka_as_uploadable_key(vka, None);
|
||||||
|
|
|
@ -34,8 +34,8 @@ use sequoia_openpgp as openpgp;
|
||||||
use openpgp_card::algorithm::{Algo, AlgoInfo, Curve};
|
use openpgp_card::algorithm::{Algo, AlgoInfo, Curve};
|
||||||
use openpgp_card::card_do::{
|
use openpgp_card::card_do::{
|
||||||
ApplicationId, ApplicationRelatedData, Cardholder, ExtendedCap,
|
ApplicationId, ApplicationRelatedData, Cardholder, ExtendedCap,
|
||||||
ExtendedLengthInfo, Features, Fingerprint, Historical, KeySet, PWStatus,
|
ExtendedLengthInfo, Features, Fingerprint, Historical, KeyGenerationTime,
|
||||||
SecuritySupportTemplate, Sex,
|
KeySet, PWStatus, SecuritySupportTemplate, Sex,
|
||||||
};
|
};
|
||||||
use openpgp_card::crypto_data::{
|
use openpgp_card::crypto_data::{
|
||||||
CardUploadableKey, Cryptogram, EccKey, EccType, Hash, PrivateKeyMaterial,
|
CardUploadableKey, Cryptogram, EccKey, EccType, Hash, PrivateKeyMaterial,
|
||||||
|
@ -92,8 +92,10 @@ pub fn vka_as_uploadable_key(
|
||||||
pub fn public_key_material_to_key(
|
pub fn public_key_material_to_key(
|
||||||
pkm: &PublicKeyMaterial,
|
pkm: &PublicKeyMaterial,
|
||||||
key_type: KeyType,
|
key_type: KeyType,
|
||||||
time: SystemTime,
|
time: KeyGenerationTime,
|
||||||
) -> Result<Key<PublicParts, UnspecifiedRole>> {
|
) -> Result<Key<PublicParts, UnspecifiedRole>> {
|
||||||
|
let time = Timestamp::from(time.get()).into();
|
||||||
|
|
||||||
match pkm {
|
match pkm {
|
||||||
PublicKeyMaterial::R(rsa) => {
|
PublicKeyMaterial::R(rsa) => {
|
||||||
let k4 = Key4::import_public_rsa(rsa.v(), rsa.n(), Some(time))?;
|
let k4 = Key4::import_public_rsa(rsa.v(), rsa.n(), Some(time))?;
|
||||||
|
@ -362,9 +364,11 @@ impl CardUploadableKey for SequoiaKey {
|
||||||
|
|
||||||
/// Number of non-leap seconds since January 1, 1970 0:00:00 UTC
|
/// Number of non-leap seconds since January 1, 1970 0:00:00 UTC
|
||||||
/// (aka "UNIX timestamp")
|
/// (aka "UNIX timestamp")
|
||||||
fn get_ts(&self) -> u32 {
|
fn get_ts(&self) -> KeyGenerationTime {
|
||||||
let ts: Timestamp = Timestamp::try_from(self.key.creation_time())
|
let ts: Timestamp = Timestamp::try_from(self.key.creation_time())
|
||||||
.expect("Creation time cannot be converted into u32 timestamp");
|
.expect("Creation time cannot be converted into u32 timestamp");
|
||||||
|
let ts: u32 = ts.into();
|
||||||
|
|
||||||
ts.into()
|
ts.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -539,11 +543,11 @@ pub fn sign(
|
||||||
/// timestamp + KeyType" (intended for use with `CardApp.generate_key()`).
|
/// timestamp + KeyType" (intended for use with `CardApp.generate_key()`).
|
||||||
pub fn public_to_fingerprint(
|
pub fn public_to_fingerprint(
|
||||||
pkm: &PublicKeyMaterial,
|
pkm: &PublicKeyMaterial,
|
||||||
ts: SystemTime,
|
time: KeyGenerationTime,
|
||||||
kt: KeyType,
|
kt: KeyType,
|
||||||
) -> Result<Fingerprint, OpenpgpCardError> {
|
) -> Result<Fingerprint, OpenpgpCardError> {
|
||||||
// Transform PublicKeyMaterial into a Sequoia Key
|
// Transform PublicKeyMaterial into a Sequoia Key
|
||||||
let key = public_key_material_to_key(pkm, kt, ts)?;
|
let key = public_key_material_to_key(pkm, kt, time)?;
|
||||||
|
|
||||||
// Get fingerprint from the Sequoia Key
|
// Get fingerprint from the Sequoia Key
|
||||||
let fp = key.fingerprint();
|
let fp = key.fingerprint();
|
||||||
|
|
|
@ -4,15 +4,14 @@
|
||||||
use std::borrow::BorrowMut;
|
use std::borrow::BorrowMut;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::time::SystemTime;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
use crate::algorithm::{Algo, AlgoInfo, AlgoSimple, RsaAttrs};
|
use crate::algorithm::{Algo, AlgoInfo, AlgoSimple, RsaAttrs};
|
||||||
use crate::apdu::{commands, response::Response};
|
use crate::apdu::{commands, response::Response};
|
||||||
use crate::card_do::{
|
use crate::card_do::{
|
||||||
ApplicationRelatedData, Cardholder, Fingerprint, PWStatus,
|
ApplicationRelatedData, Cardholder, Fingerprint, KeyGenerationTime,
|
||||||
SecuritySupportTemplate, Sex,
|
PWStatus, SecuritySupportTemplate, Sex,
|
||||||
};
|
};
|
||||||
use crate::crypto_data::{
|
use crate::crypto_data::{
|
||||||
CardUploadableKey, Cryptogram, EccType, Hash, PublicKeyMaterial,
|
CardUploadableKey, Cryptogram, EccType, Hash, PublicKeyMaterial,
|
||||||
|
@ -473,11 +472,12 @@ impl CardApp {
|
||||||
|
|
||||||
pub fn set_creation_time(
|
pub fn set_creation_time(
|
||||||
&mut self,
|
&mut self,
|
||||||
time: u32,
|
time: KeyGenerationTime,
|
||||||
key_type: KeyType,
|
key_type: KeyType,
|
||||||
) -> Result<Response, OpenpgpCardError> {
|
) -> Result<Response, OpenpgpCardError> {
|
||||||
// Timestamp update
|
// Timestamp update
|
||||||
let time_value: Vec<u8> = time
|
let time_value: Vec<u8> = time
|
||||||
|
.get()
|
||||||
.to_be_bytes()
|
.to_be_bytes()
|
||||||
.iter()
|
.iter()
|
||||||
.skip_while(|&&e| e == 0)
|
.skip_while(|&&e| e == 0)
|
||||||
|
@ -625,12 +625,12 @@ impl CardApp {
|
||||||
&mut self,
|
&mut self,
|
||||||
fp_from_pub: fn(
|
fp_from_pub: fn(
|
||||||
&PublicKeyMaterial,
|
&PublicKeyMaterial,
|
||||||
SystemTime,
|
KeyGenerationTime,
|
||||||
KeyType,
|
KeyType,
|
||||||
) -> Result<Fingerprint, OpenpgpCardError>,
|
) -> Result<Fingerprint, OpenpgpCardError>,
|
||||||
key_type: KeyType,
|
key_type: KeyType,
|
||||||
algo: Option<&Algo>,
|
algo: Option<&Algo>,
|
||||||
) -> Result<(PublicKeyMaterial, u32), OpenpgpCardError> {
|
) -> Result<(PublicKeyMaterial, KeyGenerationTime), OpenpgpCardError> {
|
||||||
keys::gen_key_with_metadata(self, fp_from_pub, key_type, algo)
|
keys::gen_key_with_metadata(self, fp_from_pub, key_type, algo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -640,12 +640,12 @@ impl CardApp {
|
||||||
&mut self,
|
&mut self,
|
||||||
fp_from_pub: fn(
|
fp_from_pub: fn(
|
||||||
&PublicKeyMaterial,
|
&PublicKeyMaterial,
|
||||||
SystemTime,
|
KeyGenerationTime,
|
||||||
KeyType,
|
KeyType,
|
||||||
) -> Result<Fingerprint, OpenpgpCardError>,
|
) -> Result<Fingerprint, OpenpgpCardError>,
|
||||||
key_type: KeyType,
|
key_type: KeyType,
|
||||||
algo: AlgoSimple,
|
algo: AlgoSimple,
|
||||||
) -> Result<(PublicKeyMaterial, u32), OpenpgpCardError> {
|
) -> Result<(PublicKeyMaterial, KeyGenerationTime), OpenpgpCardError> {
|
||||||
let algo = algo.get_algo(key_type);
|
let algo = algo.get_algo(key_type);
|
||||||
self.generate_key(fp_from_pub, key_type, Some(&algo))
|
self.generate_key(fp_from_pub, key_type, Some(&algo))
|
||||||
}
|
}
|
||||||
|
|
|
@ -182,7 +182,7 @@ impl SecuritySupportTemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An OpenPGP key generation Time
|
/// An OpenPGP key generation Time
|
||||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||||
pub struct KeyGenerationTime(u32);
|
pub struct KeyGenerationTime(u32);
|
||||||
|
|
||||||
impl KeyGenerationTime {
|
impl KeyGenerationTime {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::algorithm::Algo;
|
use crate::algorithm::Algo;
|
||||||
use crate::card_do::Fingerprint;
|
use crate::card_do::{Fingerprint, KeyGenerationTime};
|
||||||
use crate::errors::OpenpgpCardError;
|
use crate::errors::OpenpgpCardError;
|
||||||
|
|
||||||
/// A hash value that can be signed by the card.
|
/// A hash value that can be signed by the card.
|
||||||
|
@ -66,7 +66,7 @@ pub trait CardUploadableKey {
|
||||||
fn get_key(&self) -> Result<PrivateKeyMaterial>;
|
fn get_key(&self) -> Result<PrivateKeyMaterial>;
|
||||||
|
|
||||||
/// timestamp of (sub)key creation
|
/// timestamp of (sub)key creation
|
||||||
fn get_ts(&self) -> u32;
|
fn get_ts(&self) -> KeyGenerationTime;
|
||||||
|
|
||||||
/// fingerprint
|
/// fingerprint
|
||||||
fn get_fp(&self) -> Result<Fingerprint, OpenpgpCardError>;
|
fn get_fp(&self) -> Result<Fingerprint, OpenpgpCardError>;
|
||||||
|
|
|
@ -11,7 +11,7 @@ use crate::algorithm::{Algo, AlgoInfo, Curve, EccAttrs, RsaAttrs};
|
||||||
use crate::apdu::command::Command;
|
use crate::apdu::command::Command;
|
||||||
use crate::apdu::commands;
|
use crate::apdu::commands;
|
||||||
use crate::card_app::CardApp;
|
use crate::card_app::CardApp;
|
||||||
use crate::card_do::Fingerprint;
|
use crate::card_do::{Fingerprint, KeyGenerationTime};
|
||||||
use crate::crypto_data::{
|
use crate::crypto_data::{
|
||||||
CardUploadableKey, EccKey, EccPub, PrivateKeyMaterial, PublicKeyMaterial,
|
CardUploadableKey, EccKey, EccPub, PrivateKeyMaterial, PublicKeyMaterial,
|
||||||
RSAKey, RSAPub,
|
RSAKey, RSAPub,
|
||||||
|
@ -26,12 +26,12 @@ pub(crate) fn gen_key_with_metadata(
|
||||||
card_app: &mut CardApp,
|
card_app: &mut CardApp,
|
||||||
fp_from_pub: fn(
|
fp_from_pub: fn(
|
||||||
&PublicKeyMaterial,
|
&PublicKeyMaterial,
|
||||||
SystemTime,
|
KeyGenerationTime,
|
||||||
KeyType,
|
KeyType,
|
||||||
) -> Result<Fingerprint, OpenpgpCardError>,
|
) -> Result<Fingerprint, OpenpgpCardError>,
|
||||||
key_type: KeyType,
|
key_type: KeyType,
|
||||||
algo: Option<&Algo>,
|
algo: Option<&Algo>,
|
||||||
) -> Result<(PublicKeyMaterial, u32), OpenpgpCardError> {
|
) -> Result<(PublicKeyMaterial, KeyGenerationTime), OpenpgpCardError> {
|
||||||
// set algo on card if it's Some
|
// set algo on card if it's Some
|
||||||
if let Some(algo) = algo {
|
if let Some(algo) = algo {
|
||||||
card_app.set_algorithm_attributes(key_type, algo)?;
|
card_app.set_algorithm_attributes(key_type, algo)?;
|
||||||
|
@ -58,10 +58,12 @@ pub(crate) fn gen_key_with_metadata(
|
||||||
.map_err(|e| OpenpgpCardError::InternalError(anyhow!(e)))?
|
.map_err(|e| OpenpgpCardError::InternalError(anyhow!(e)))?
|
||||||
.as_secs() as u32;
|
.as_secs() as u32;
|
||||||
|
|
||||||
|
let ts = ts.into();
|
||||||
|
|
||||||
card_app.set_creation_time(ts, key_type)?;
|
card_app.set_creation_time(ts, key_type)?;
|
||||||
|
|
||||||
// calculate/store fingerprint
|
// calculate/store fingerprint
|
||||||
let fp = fp_from_pub(&pubkey, time, key_type)?;
|
let fp = fp_from_pub(&pubkey, ts, key_type)?;
|
||||||
card_app.set_fingerprint(fp, key_type)?;
|
card_app.set_fingerprint(fp, key_type)?;
|
||||||
|
|
||||||
Ok((pubkey, ts))
|
Ok((pubkey, ts))
|
||||||
|
@ -421,7 +423,7 @@ fn rsa_key_cmd(
|
||||||
fn copy_key_to_card(
|
fn copy_key_to_card(
|
||||||
card_app: &mut CardApp,
|
card_app: &mut CardApp,
|
||||||
key_type: KeyType,
|
key_type: KeyType,
|
||||||
ts: u32,
|
ts: KeyGenerationTime,
|
||||||
fp: Fingerprint,
|
fp: Fingerprint,
|
||||||
algo: &Algo,
|
algo: &Algo,
|
||||||
key_cmd: Command,
|
key_cmd: Command,
|
||||||
|
|
Loading…
Reference in a new issue