mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
parent
31788e3733
commit
67c80f3fcb
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2335,6 +2335,7 @@ dependencies = [
|
|||
"futures",
|
||||
"futures-util",
|
||||
"hashbrown",
|
||||
"hex",
|
||||
"idlset",
|
||||
"kanidm_proto",
|
||||
"lazy_static",
|
||||
|
|
|
@ -64,6 +64,7 @@ futures-util = "^0.3.21"
|
|||
gloo = "^0.8.0"
|
||||
gloo-net = "0.2.4"
|
||||
hashbrown = { version = "0.12.3", features = ["serde", "inline-more", "ahash"] }
|
||||
hex = "^0.4.3"
|
||||
http-types = "^2.12.0"
|
||||
idlset = "^0.2.4"
|
||||
# idlset = { path = "../idlset" }
|
||||
|
|
25
iam_migrations/freeipa/notes.txt
Normal file
25
iam_migrations/freeipa/notes.txt
Normal file
|
@ -0,0 +1,25 @@
|
|||
|
||||
|
||||
OTP tokens are stored in *related* entries:
|
||||
|
||||
Not sure how you would determine this in an import?
|
||||
|
||||
token key is base64 standard no pad
|
||||
|
||||
entryuuid: 94dbeb81-45f8-11ed-a50d-919b4b1a5ec0
|
||||
syncstate: Add
|
||||
dn: ipatokenuniqueid=16b6d328-cead-4b71-af5e-0a7b8622b204,cn=otp,dc=dev,dc=blackhats,dc=net,dc=au
|
||||
ipatokenOTPalgorithm: sha1
|
||||
ipatokenOTPdigits: 6
|
||||
ipatokenOTPkey: iWM6XXyQt9GZV/yiPHYW/w4qjpaIJ8IlF6DlFndHe+C/zu4
|
||||
ipatokenOwner: uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au
|
||||
ipatokenTOTPclockOffset: 0
|
||||
ipatokenTOTPtimeStep: 30
|
||||
ipatokenUniqueID: 16b6d328-cead-4b71-af5e-0a7b8622b204
|
||||
objectClass: ipatoken
|
||||
objectClass: ipatokentotp
|
||||
objectClass: top
|
||||
|
||||
|
||||
|
||||
|
|
@ -32,6 +32,7 @@ filetime.workspace = true
|
|||
futures.workspace = true
|
||||
futures-util.workspace = true
|
||||
hashbrown.workspace = true
|
||||
hex.workspace = true
|
||||
idlset.workspace = true
|
||||
kanidm_proto.workspace = true
|
||||
lazy_static.workspace = true
|
||||
|
|
|
@ -21,16 +21,23 @@ pub struct DbCidV1 {
|
|||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum DbPasswordV1 {
|
||||
PBKDF2(usize, Vec<u8>, Vec<u8>),
|
||||
PBKDF2_SHA1(usize, Vec<u8>, Vec<u8>),
|
||||
PBKDF2_SHA512(usize, Vec<u8>, Vec<u8>),
|
||||
SSHA512(Vec<u8>, Vec<u8>),
|
||||
NT_MD4(Vec<u8>),
|
||||
}
|
||||
|
||||
impl fmt::Debug for DbPasswordV1 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
DbPasswordV1::PBKDF2(_, _, _) => write!(f, "PBKDF2"),
|
||||
DbPasswordV1::PBKDF2_SHA1(_, _, _) => write!(f, "PBKDF2_SHA1"),
|
||||
DbPasswordV1::PBKDF2_SHA512(_, _, _) => write!(f, "PBKDF2_SHA512"),
|
||||
DbPasswordV1::SSHA512(_, _) => write!(f, "SSHA512"),
|
||||
DbPasswordV1::NT_MD4(_) => write!(f, "NT_MD4"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,8 @@ use std::time::{Duration, Instant};
|
|||
|
||||
use hashbrown::{HashMap as Map, HashSet};
|
||||
use kanidm_proto::v1::{BackupCodesView, CredentialDetail, CredentialDetailType, OperationError};
|
||||
use openssl::hash::MessageDigest;
|
||||
use openssl::hash::{self, MessageDigest};
|
||||
use openssl::nid::Nid;
|
||||
use openssl::pkcs5::pbkdf2_hmac;
|
||||
use openssl::sha::Sha512;
|
||||
use rand::prelude::*;
|
||||
|
@ -24,9 +25,16 @@ use crate::credential::totp::Totp;
|
|||
// NIST 800-63.b salt should be 112 bits -> 14 8u8.
|
||||
// I choose tinfoil hat though ...
|
||||
const PBKDF2_SALT_LEN: usize = 24;
|
||||
|
||||
const PBKDF2_MIN_NIST_SALT_LEN: usize = 14;
|
||||
|
||||
// Min number of rounds for a pbkdf2
|
||||
pub const PBKDF2_MIN_NIST_COST: usize = 10000;
|
||||
|
||||
// 64 * u8 -> 512 bits of out.
|
||||
const PBKDF2_KEY_LEN: usize = 64;
|
||||
const PBKDF2_IMPORT_MIN_LEN: usize = 32;
|
||||
const PBKDF2_MIN_NIST_KEY_LEN: usize = 32;
|
||||
const PBKDF2_SHA1_MIN_KEY_LEN: usize = 19;
|
||||
|
||||
const DS_SSHA512_SALT_LEN: usize = 8;
|
||||
const DS_SSHA512_HASH_LEN: usize = 64;
|
||||
|
@ -46,11 +54,20 @@ pub enum Policy {
|
|||
// I don't really feel like adding in so many restrictions, so I'll use
|
||||
// pbkdf2 in openssl because it doesn't have the same limits.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[allow(non_camel_case_types)]
|
||||
enum Kdf {
|
||||
// cost, salt, hash
|
||||
PBKDF2(usize, Vec<u8>, Vec<u8>),
|
||||
|
||||
// Imported types, will upgrade to the above.
|
||||
// cost, salt, hash
|
||||
PBKDF2_SHA1(usize, Vec<u8>, Vec<u8>),
|
||||
// cost, salt, hash
|
||||
PBKDF2_SHA512(usize, Vec<u8>, Vec<u8>),
|
||||
// salt hash
|
||||
SSHA512(Vec<u8>, Vec<u8>),
|
||||
// hash
|
||||
NT_MD4(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
@ -66,13 +83,45 @@ impl TryFrom<DbPasswordV1> for Password {
|
|||
DbPasswordV1::PBKDF2(c, s, h) => Ok(Password {
|
||||
material: Kdf::PBKDF2(c, s, h),
|
||||
}),
|
||||
DbPasswordV1::PBKDF2_SHA1(c, s, h) => Ok(Password {
|
||||
material: Kdf::PBKDF2_SHA1(c, s, h),
|
||||
}),
|
||||
DbPasswordV1::PBKDF2_SHA512(c, s, h) => Ok(Password {
|
||||
material: Kdf::PBKDF2_SHA512(c, s, h),
|
||||
}),
|
||||
DbPasswordV1::SSHA512(s, h) => Ok(Password {
|
||||
material: Kdf::SSHA512(s, h),
|
||||
}),
|
||||
DbPasswordV1::NT_MD4(h) => Ok(Password {
|
||||
material: Kdf::NT_MD4(h),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OpenLDAP based their PBKDF2 implementation on passlib from python, that uses a
|
||||
// non-standard base64 altchar set and padding that is not supported by
|
||||
// anything else in the world. To manage this, we only ever encode to base64 with
|
||||
// no pad but we have to remap ab64 to b64. This function allows b64 standard with
|
||||
// padding to pass, and remaps ab64 to b64 standard with padding.
|
||||
macro_rules! ab64_to_b64 {
|
||||
($ab64:expr) => {{
|
||||
let mut s = $ab64.replace(".", "+");
|
||||
match s.len() & 3 {
|
||||
0 => {
|
||||
// Do nothing
|
||||
}
|
||||
1 => {
|
||||
// One is invalid, do nothing, we'll error in base64
|
||||
}
|
||||
2 => s.push_str("=="),
|
||||
3 => s.push_str("="),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
s
|
||||
}};
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Password {
|
||||
type Error = ();
|
||||
|
||||
|
@ -93,7 +142,7 @@ impl TryFrom<&str> for Password {
|
|||
let c = cost.parse::<usize>().map_err(|_| ())?;
|
||||
let s: Vec<_> = salt.as_bytes().to_vec();
|
||||
let h = base64::decode(hash).map_err(|_| ())?;
|
||||
if h.len() < PBKDF2_IMPORT_MIN_LEN {
|
||||
if h.len() < PBKDF2_MIN_NIST_KEY_LEN {
|
||||
return Err(());
|
||||
}
|
||||
return Ok(Password {
|
||||
|
@ -104,6 +153,34 @@ impl TryFrom<&str> for Password {
|
|||
}
|
||||
}
|
||||
|
||||
if value.starts_with("ipaNTHash: ") {
|
||||
let nt_md4 = match value.split_once(" ") {
|
||||
Some((_, v)) => v,
|
||||
None => {
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
|
||||
let h = base64::decode_config(nt_md4, base64::STANDARD_NO_PAD).map_err(|_| ())?;
|
||||
return Ok(Password {
|
||||
material: Kdf::NT_MD4(h),
|
||||
});
|
||||
}
|
||||
|
||||
if value.starts_with("sambaNTPassword: ") {
|
||||
let nt_md4 = match value.split_once(" ") {
|
||||
Some((_, v)) => v,
|
||||
None => {
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
|
||||
let h = hex::decode(nt_md4).map_err(|_| ())?;
|
||||
return Ok(Password {
|
||||
material: Kdf::NT_MD4(h),
|
||||
});
|
||||
}
|
||||
|
||||
// Test 389ds formats
|
||||
if let Some(ds_ssha512) = value.strip_prefix("{SSHA512}") {
|
||||
let sh = base64::decode(ds_ssha512).map_err(|_| ())?;
|
||||
|
@ -116,6 +193,78 @@ impl TryFrom<&str> for Password {
|
|||
});
|
||||
}
|
||||
|
||||
// Test for OpenLDAP formats
|
||||
if value.starts_with("{PBKDF2}")
|
||||
|| value.starts_with("{PBKDF2-SHA1}")
|
||||
|| value.starts_with("{PBKDF2-SHA256}")
|
||||
|| value.starts_with("{PBKDF2-SHA512}")
|
||||
{
|
||||
let ol_pbkdf2 = match value.split_once("}") {
|
||||
Some((_, v)) => v,
|
||||
None => {
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
|
||||
let ol_pbkdf: Vec<&str> = ol_pbkdf2.split('$').collect();
|
||||
if ol_pbkdf.len() == 3 {
|
||||
let cost = ol_pbkdf[0];
|
||||
let salt = ol_pbkdf[1];
|
||||
let hash = ol_pbkdf[2];
|
||||
|
||||
let c = cost.parse::<usize>().map_err(|_| ())?;
|
||||
|
||||
let s = ab64_to_b64!(salt);
|
||||
let s =
|
||||
base64::decode_config(&s, base64::STANDARD.decode_allow_trailing_bits(true))
|
||||
.map_err(|e| {
|
||||
error!(?e, "Invalid base64 in oldap pbkdf2-sha1");
|
||||
})?;
|
||||
|
||||
let h = ab64_to_b64!(hash);
|
||||
let h =
|
||||
base64::decode_config(&h, base64::STANDARD.decode_allow_trailing_bits(true))
|
||||
.map_err(|e| {
|
||||
error!(?e, "Invalid base64 in oldap pbkdf2-sha1");
|
||||
})?;
|
||||
|
||||
// This is just sha1 in a trenchcoat.
|
||||
if value.strip_prefix("{PBKDF2}").is_some()
|
||||
|| value.strip_prefix("{PBKDF2-SHA1}").is_some()
|
||||
{
|
||||
if h.len() < PBKDF2_SHA1_MIN_KEY_LEN {
|
||||
return Err(());
|
||||
}
|
||||
return Ok(Password {
|
||||
material: Kdf::PBKDF2_SHA1(c, s, h),
|
||||
});
|
||||
}
|
||||
|
||||
if value.strip_prefix("{PBKDF2-SHA256}").is_some() {
|
||||
if h.len() < PBKDF2_MIN_NIST_KEY_LEN {
|
||||
return Err(());
|
||||
}
|
||||
return Ok(Password {
|
||||
material: Kdf::PBKDF2(c, s, h),
|
||||
});
|
||||
}
|
||||
|
||||
if value.strip_prefix("{PBKDF2-SHA512}").is_some() {
|
||||
if h.len() < PBKDF2_MIN_NIST_KEY_LEN {
|
||||
return Err(());
|
||||
}
|
||||
return Ok(Password {
|
||||
material: Kdf::PBKDF2_SHA512(c, s, h),
|
||||
});
|
||||
}
|
||||
|
||||
// Should be no way to get here!
|
||||
unreachable!();
|
||||
} else {
|
||||
warn!("oldap pbkdf2 found but invalid number of elements?");
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing matched to this point.
|
||||
Err(())
|
||||
}
|
||||
|
@ -173,7 +322,7 @@ impl Password {
|
|||
// We have to get the number of bits to derive from our stored hash
|
||||
// as some imported hash types may have variable lengths
|
||||
let key_len = key.len();
|
||||
debug_assert!(key_len >= PBKDF2_IMPORT_MIN_LEN);
|
||||
debug_assert!(key_len >= PBKDF2_MIN_NIST_KEY_LEN);
|
||||
let mut chal_key: Vec<u8> = (0..key_len).map(|_| 0).collect();
|
||||
pbkdf2_hmac(
|
||||
cleartext.as_bytes(),
|
||||
|
@ -188,6 +337,40 @@ impl Password {
|
|||
&chal_key == key
|
||||
})
|
||||
}
|
||||
Kdf::PBKDF2_SHA1(cost, salt, key) => {
|
||||
let key_len = key.len();
|
||||
debug_assert!(key_len >= PBKDF2_SHA1_MIN_KEY_LEN);
|
||||
let mut chal_key: Vec<u8> = (0..key_len).map(|_| 0).collect();
|
||||
pbkdf2_hmac(
|
||||
cleartext.as_bytes(),
|
||||
salt.as_slice(),
|
||||
*cost,
|
||||
MessageDigest::sha1(),
|
||||
chal_key.as_mut_slice(),
|
||||
)
|
||||
.map_err(|_| OperationError::CryptographyError)
|
||||
.map(|()| {
|
||||
// Actually compare the outputs.
|
||||
&chal_key == key
|
||||
})
|
||||
}
|
||||
Kdf::PBKDF2_SHA512(cost, salt, key) => {
|
||||
let key_len = key.len();
|
||||
debug_assert!(key_len >= PBKDF2_MIN_NIST_KEY_LEN);
|
||||
let mut chal_key: Vec<u8> = (0..key_len).map(|_| 0).collect();
|
||||
pbkdf2_hmac(
|
||||
cleartext.as_bytes(),
|
||||
salt.as_slice(),
|
||||
*cost,
|
||||
MessageDigest::sha512(),
|
||||
chal_key.as_mut_slice(),
|
||||
)
|
||||
.map_err(|_| OperationError::CryptographyError)
|
||||
.map(|()| {
|
||||
// Actually compare the outputs.
|
||||
&chal_key == key
|
||||
})
|
||||
}
|
||||
Kdf::SSHA512(salt, key) => {
|
||||
let mut hasher = Sha512::new();
|
||||
hasher.update(cleartext.as_bytes());
|
||||
|
@ -195,6 +378,23 @@ impl Password {
|
|||
let r = hasher.finish();
|
||||
Ok(key == &(r.to_vec()))
|
||||
}
|
||||
Kdf::NT_MD4(key) => {
|
||||
// We need to get the cleartext to utf16le for reasons.
|
||||
let clear_utf16le: Vec<u8> = cleartext
|
||||
.encode_utf16()
|
||||
.map(|c| c.to_le_bytes())
|
||||
.flat_map(|i| i.into_iter())
|
||||
.collect();
|
||||
|
||||
let dgst = MessageDigest::from_nid(Nid::MD4).ok_or_else(|| {
|
||||
error!("Unable to access MD4 - fips mode enabled?");
|
||||
OperationError::CryptographyError
|
||||
})?;
|
||||
|
||||
hash::hash(dgst, &clear_utf16le)
|
||||
.map_err(|_| OperationError::CryptographyError)
|
||||
.map(|chal_key| chal_key.as_ref() == key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,12 +403,26 @@ impl Password {
|
|||
Kdf::PBKDF2(cost, salt, hash) => {
|
||||
DbPasswordV1::PBKDF2(*cost, salt.clone(), hash.clone())
|
||||
}
|
||||
Kdf::PBKDF2_SHA1(cost, salt, hash) => {
|
||||
DbPasswordV1::PBKDF2_SHA1(*cost, salt.clone(), hash.clone())
|
||||
}
|
||||
Kdf::PBKDF2_SHA512(cost, salt, hash) => {
|
||||
DbPasswordV1::PBKDF2_SHA512(*cost, salt.clone(), hash.clone())
|
||||
}
|
||||
Kdf::SSHA512(salt, hash) => DbPasswordV1::SSHA512(salt.clone(), hash.clone()),
|
||||
Kdf::NT_MD4(hash) => DbPasswordV1::NT_MD4(hash.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn requires_upgrade(&self) -> bool {
|
||||
!matches!(&self.material, Kdf::PBKDF2(_, _, _))
|
||||
match &self.material {
|
||||
Kdf::PBKDF2_SHA512(cost, salt, hash) | Kdf::PBKDF2(cost, salt, hash) => {
|
||||
*cost < PBKDF2_MIN_NIST_COST
|
||||
|| salt.len() < PBKDF2_MIN_NIST_SALT_LEN
|
||||
|| hash.len() < PBKDF2_MIN_NIST_KEY_LEN
|
||||
}
|
||||
Kdf::PBKDF2_SHA1(_, _, _) | Kdf::SSHA512(_, _) | Kdf::NT_MD4(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -930,4 +1144,75 @@ mod tests {
|
|||
assert!(r.requires_upgrade());
|
||||
assert!(r.verify(password).unwrap_or(false));
|
||||
}
|
||||
|
||||
// Can be generated with:
|
||||
// slappasswd -s password -o module-load=/usr/lib64/openldap/pw-argon2.so -h {ARGON2}
|
||||
|
||||
#[test]
|
||||
fn test_password_from_openldap_pkbdf2() {
|
||||
let im_pw = "{PBKDF2}10000$IlfapjA351LuDSwYC0IQ8Q$saHqQTuYnjJN/tmAndT.8mJt.6w";
|
||||
let password = "password";
|
||||
let r = Password::try_from(im_pw).expect("Failed to parse");
|
||||
assert!(r.requires_upgrade());
|
||||
assert!(r.verify(password).unwrap_or(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_from_openldap_pkbdf2_sha1() {
|
||||
let im_pw = "{PBKDF2-SHA1}10000$ZBEH6B07rgQpJSikyvMU2w$TAA03a5IYkz1QlPsbJKvUsTqNV";
|
||||
let password = "password";
|
||||
let r = Password::try_from(im_pw).expect("Failed to parse");
|
||||
assert!(r.requires_upgrade());
|
||||
assert!(r.verify(password).unwrap_or(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_from_openldap_pkbdf2_sha256() {
|
||||
let im_pw = "{PBKDF2-SHA256}10000$henZGfPWw79Cs8ORDeVNrQ$1dTJy73v6n3bnTmTZFghxHXHLsAzKaAy8SksDfZBPIw";
|
||||
let password = "password";
|
||||
let r = Password::try_from(im_pw).expect("Failed to parse");
|
||||
assert!(!r.requires_upgrade());
|
||||
assert!(r.verify(password).unwrap_or(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_from_openldap_pkbdf2_sha512() {
|
||||
let im_pw = "{PBKDF2-SHA512}10000$Je1Uw19Bfv5lArzZ6V3EPw$g4T/1sqBUYWl9o93MVnyQ/8zKGSkPbKaXXsT8WmysXQJhWy8MRP2JFudSL.N9RklQYgDPxPjnfum/F2f/TrppA";
|
||||
let password = "password";
|
||||
let r = Password::try_from(im_pw).expect("Failed to parse");
|
||||
assert!(!r.requires_upgrade());
|
||||
assert!(r.verify(password).unwrap_or(false));
|
||||
}
|
||||
|
||||
/*
|
||||
// Not supported in openssl, may need an external crate.
|
||||
#[test]
|
||||
fn test_password_from_openldap_argon2() {
|
||||
let im_pw = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$IyTQMsvzB2JHDiWx8fq7Ew$VhYOA7AL0kbRXI5g2kOyyp8St1epkNj7WZyUY4pAIQQ"
|
||||
let password = "password";
|
||||
let r = Password::try_from(im_pw).expect("Failed to parse");
|
||||
assert!(r.requires_upgrade());
|
||||
assert!(r.verify(password).unwrap_or(false));
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn test_password_from_ipa_nt_hash() {
|
||||
// Base64 no pad
|
||||
let im_pw = "ipaNTHash: iEb36u6PsRetBr3YMLdYbA";
|
||||
let password = "password";
|
||||
let r = Password::try_from(im_pw).expect("Failed to parse");
|
||||
assert!(r.requires_upgrade());
|
||||
assert!(r.verify(password).unwrap_or(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_from_samba_nt_hash() {
|
||||
// Base64 no pad
|
||||
let im_pw = "sambaNTPassword: 8846F7EAEE8FB117AD06BDD830B7586C";
|
||||
let password = "password";
|
||||
let r = Password::try_from(im_pw).expect("Failed to parse");
|
||||
assert!(r.requires_upgrade());
|
||||
assert!(r.verify(password).unwrap_or(false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use super::Password;
|
||||
|
||||
const PBKDF2_MIN_NIST_COST: u64 = 10000;
|
||||
use super::{Password, PBKDF2_MIN_NIST_COST};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CryptoPolicy {
|
||||
|
@ -20,7 +18,7 @@ impl CryptoPolicy {
|
|||
pub fn time_target(t: Duration) -> Self {
|
||||
let r = match Password::bench_pbkdf2((PBKDF2_MIN_NIST_COST * 10) as usize) {
|
||||
Some(bt) => {
|
||||
let ubt = bt.as_nanos() as u64;
|
||||
let ubt = bt.as_nanos() as usize;
|
||||
|
||||
// Get the cost per thousand rounds
|
||||
let per_thou = (PBKDF2_MIN_NIST_COST * 10) / 1000;
|
||||
|
@ -28,7 +26,7 @@ impl CryptoPolicy {
|
|||
// eprintln!("{} / {}", ubt, per_thou);
|
||||
|
||||
// Now we need the attacker work in nanos
|
||||
let attack_time = t.as_nanos() as u64;
|
||||
let attack_time = t.as_nanos() as usize;
|
||||
let r = (attack_time / t_per_thou) * 1000;
|
||||
|
||||
// eprintln!("({} / {} ) * 1000", attack_time, t_per_thou);
|
||||
|
|
Loading…
Reference in a new issue