use crate::error::SecurityError;
use aes::Aes128;
use aes::cipher::{BlockEncrypt, KeyInit};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;
const PROFILE_A_ENC_KEY_LEN: usize = 16;
const PROFILE_A_MAC_KEY_LEN: usize = 32;
const PROFILE_A_ICB_LEN: usize = 16;
const PROFILE_A_MAC_LEN: usize = 8;
const PROFILE_A_PUB_KEY_LEN: usize = 32;
const PROFILE_B_ENC_KEY_LEN: usize = 16;
const PROFILE_B_MAC_KEY_LEN: usize = 32;
const PROFILE_B_ICB_LEN: usize = 16;
const PROFILE_B_MAC_LEN: usize = 8;
const PROFILE_B_PUB_KEY_LEN: usize = 33;
fn ansi_x963_kdf(
shared_key: &[u8],
public_key: &[u8],
enc_key_len: usize,
mac_key_len: usize,
) -> Vec<u8> {
use sha2::Digest;
let total = enc_key_len + mac_key_len;
let hash_len = 32usize;
let rounds = total.div_ceil(hash_len);
let mut kdf_key = Vec::with_capacity(rounds * hash_len);
for i in 0u32..rounds as u32 {
let counter = (i + 1).to_be_bytes();
let mut h = Sha256::new();
h.update(shared_key);
h.update(counter);
h.update(public_key);
kdf_key.extend_from_slice(&h.finalize());
}
kdf_key
}
fn aes128_ctr(key: &[u8; 16], icb: &[u8; 16], data: &[u8]) -> Vec<u8> {
let aes = Aes128::new(key.into());
let mut out = Vec::with_capacity(data.len());
let mut counter = u128::from_be_bytes(*icb);
for chunk in data.chunks(16) {
let mut block = aes::Block::clone_from_slice(&counter.to_be_bytes());
aes.encrypt_block(&mut block);
for (d, k) in chunk.iter().zip(block.iter()) {
out.push(d ^ k);
}
counter = counter.wrapping_add(1);
}
out
}
fn hmac_sha256_8(key: &[u8], data: &[u8]) -> [u8; 8] {
let mut mac = <Hmac<Sha256> as KeyInit>::new_from_slice(key)
.expect("HMAC-SHA-256 accepts any key size per RFC 2104");
mac.update(data);
let full = mac.finalize().into_bytes();
full[..8]
.try_into()
.expect("SHA-256 output is 32 bytes, 8-byte prefix always valid")
}
pub fn msin_to_bcd(msin: &str) -> Vec<u8> {
debug_assert!(
msin.as_bytes().iter().all(|b| b.is_ascii_digit()),
"MSIN must be ASCII digits"
);
let bytes = msin.as_bytes();
let len = bytes.len().div_ceil(2);
let mut out = Vec::with_capacity(len);
let mut i = 0;
while i < bytes.len() {
let lo = bytes[i] - b'0';
let hi = if i + 1 < bytes.len() {
bytes[i + 1] - b'0'
} else {
0xF
};
out.push(lo | (hi << 4));
i += 2;
}
out
}
pub fn suci_scheme_output_a(
msin_bcd: &[u8],
hn_pub_key: &[u8; 32],
) -> Result<Vec<u8>, SecurityError> {
use rand_core::OsRng;
use x25519_dalek::{EphemeralSecret, PublicKey};
let hn_pub = PublicKey::from(*hn_pub_key);
let eph_secret = EphemeralSecret::random_from_rng(OsRng);
let eph_pub = PublicKey::from(&eph_secret);
let shared = eph_secret.diffie_hellman(&hn_pub);
let eph_pub_bytes = eph_pub.as_bytes();
let kdf = ansi_x963_kdf(
shared.as_bytes(),
eph_pub_bytes,
PROFILE_A_ENC_KEY_LEN + PROFILE_A_ICB_LEN,
PROFILE_A_MAC_KEY_LEN,
);
let enc_key: &[u8; PROFILE_A_ENC_KEY_LEN] = kdf[0..PROFILE_A_ENC_KEY_LEN]
.try_into()
.expect("KDF output is at least ENC_KEY_LEN bytes");
let icb: &[u8; PROFILE_A_ICB_LEN] = kdf
[PROFILE_A_ENC_KEY_LEN..PROFILE_A_ENC_KEY_LEN + PROFILE_A_ICB_LEN]
.try_into()
.expect("KDF output is at least ENC_KEY_LEN + ICB_LEN bytes");
let mac_key = &kdf[kdf.len() - PROFILE_A_MAC_KEY_LEN..];
let ciphertext = aes128_ctr(enc_key, icb, msin_bcd);
let mac = hmac_sha256_8(mac_key, &ciphertext);
let mut out = Vec::with_capacity(PROFILE_A_PUB_KEY_LEN + msin_bcd.len() + PROFILE_A_MAC_LEN);
out.extend_from_slice(eph_pub_bytes);
out.extend_from_slice(&ciphertext);
out.extend_from_slice(&mac);
Ok(out)
}
pub fn suci_scheme_output_b(msin_bcd: &[u8], hn_pub_key: &[u8]) -> Result<Vec<u8>, SecurityError> {
use p256::PublicKey;
use p256::ecdh::EphemeralSecret;
use p256::elliptic_curve::sec1::ToEncodedPoint;
use rand_core::OsRng;
let hn_pub = PublicKey::from_sec1_bytes(hn_pub_key)
.map_err(|e| SecurityError::Ecies(format!("invalid P-256 public key: {}", e)))?;
let eph_secret = EphemeralSecret::random(&mut OsRng);
let eph_pub = eph_secret.public_key();
let eph_pub_enc = eph_pub.to_encoded_point(true);
let eph_pub_bytes = eph_pub_enc.as_bytes();
let shared = eph_secret.diffie_hellman(&hn_pub);
let shared_bytes = shared.raw_secret_bytes();
let kdf = ansi_x963_kdf(
shared_bytes.as_slice(),
eph_pub_bytes,
PROFILE_B_ENC_KEY_LEN + PROFILE_B_ICB_LEN,
PROFILE_B_MAC_KEY_LEN,
);
let enc_key: &[u8; PROFILE_B_ENC_KEY_LEN] = kdf[0..PROFILE_B_ENC_KEY_LEN]
.try_into()
.expect("KDF output is at least ENC_KEY_LEN bytes");
let icb: &[u8; PROFILE_B_ICB_LEN] = kdf
[PROFILE_B_ENC_KEY_LEN..PROFILE_B_ENC_KEY_LEN + PROFILE_B_ICB_LEN]
.try_into()
.expect("KDF output is at least ENC_KEY_LEN + ICB_LEN bytes");
let mac_key = &kdf[kdf.len() - PROFILE_B_MAC_KEY_LEN..];
let ciphertext = aes128_ctr(enc_key, icb, msin_bcd);
let mac = hmac_sha256_8(mac_key, &ciphertext);
let mut out = Vec::with_capacity(PROFILE_B_PUB_KEY_LEN + msin_bcd.len() + PROFILE_B_MAC_LEN);
out.extend_from_slice(eph_pub_bytes);
out.extend_from_slice(&ciphertext);
out.extend_from_slice(&mac);
Ok(out)
}
pub fn suci_conceal(
msin_bcd: &[u8],
scheme_id: u8,
hn_pub_key: &[u8],
) -> Result<Vec<u8>, SecurityError> {
match scheme_id {
0x00 => Ok(msin_bcd.to_vec()),
0x01 => {
let key: &[u8; 32] =
hn_pub_key
.try_into()
.map_err(|_| SecurityError::InvalidKeyLength {
expected: 32,
got: hn_pub_key.len(),
})?;
suci_scheme_output_a(msin_bcd, key)
}
0x02 => suci_scheme_output_b(msin_bcd, hn_pub_key),
_ => Err(SecurityError::Ecies(format!(
"unsupported protection scheme: {}",
scheme_id
))),
}
}
pub fn suci_decrypt_a(
scheme_output: &[u8],
hn_priv_key: &[u8; 32],
) -> Result<Vec<u8>, SecurityError> {
use x25519_dalek::{PublicKey, StaticSecret};
if scheme_output.len() < PROFILE_A_PUB_KEY_LEN + PROFILE_A_MAC_LEN + 1 {
return Err(SecurityError::Ecies(
"Profile A scheme output too short".into(),
));
}
let priv_key = StaticSecret::from(*hn_priv_key);
let eph_pub = PublicKey::from(
<[u8; 32]>::try_from(&scheme_output[0..32])
.map_err(|_| SecurityError::Ecies("invalid ephemeral public key".into()))?,
);
let shared = priv_key.diffie_hellman(&eph_pub);
let kdf = ansi_x963_kdf(
shared.as_bytes(),
&scheme_output[0..32],
PROFILE_A_ENC_KEY_LEN + PROFILE_A_ICB_LEN,
PROFILE_A_MAC_KEY_LEN,
);
let enc_key: &[u8; 16] = kdf[0..16].try_into().expect("KDF output >= 16");
let icb: &[u8; 16] = kdf[16..32].try_into().expect("KDF output >= 32");
let mac_key = &kdf[32..];
let ciphertext = &scheme_output[32..scheme_output.len() - 8];
let mac_received = &scheme_output[scheme_output.len() - 8..];
let mac_computed = hmac_sha256_8(mac_key, ciphertext);
if mac_received.ct_eq(&mac_computed).unwrap_u8() != 1 {
return Err(SecurityError::MacMismatch);
}
Ok(aes128_ctr(enc_key, icb, ciphertext))
}
pub fn suci_decrypt_b(scheme_output: &[u8], hn_priv_key: &[u8]) -> Result<Vec<u8>, SecurityError> {
use p256::elliptic_curve::sec1::ToEncodedPoint;
use p256::{PublicKey, SecretKey};
#[allow(clippy::unnecessary_fallible_conversions)] let priv_key = SecretKey::from_bytes(
hn_priv_key
.try_into()
.map_err(|_| SecurityError::Ecies("P-256 private key must be 32 bytes".into()))?,
)
.map_err(|e| SecurityError::Ecies(format!("invalid P-256 private key: {}", e)))?;
let eph_pub_len = if scheme_output.first() == Some(&0x04) {
65
} else {
33
};
if scheme_output.len() < eph_pub_len + PROFILE_B_MAC_LEN + 1 {
return Err(SecurityError::Ecies(
"Profile B scheme output too short".into(),
));
}
let eph_pub_bytes = &scheme_output[0..eph_pub_len];
let eph_pub = PublicKey::from_sec1_bytes(eph_pub_bytes)
.map_err(|e| SecurityError::Ecies(format!("invalid ephemeral P-256 key: {}", e)))?;
let compressed = eph_pub.to_encoded_point(true);
let kdf_pub_bytes = compressed.as_bytes();
let shared = p256::elliptic_curve::ecdh::diffie_hellman(
priv_key.to_nonzero_scalar(),
eph_pub.as_affine(),
);
let shared_bytes = shared.raw_secret_bytes();
let kdf = ansi_x963_kdf(
shared_bytes.as_slice(),
kdf_pub_bytes,
PROFILE_B_ENC_KEY_LEN + PROFILE_B_ICB_LEN,
PROFILE_B_MAC_KEY_LEN,
);
let enc_key: &[u8; 16] = kdf[0..16].try_into().expect("KDF output >= 16");
let icb: &[u8; 16] = kdf[16..32].try_into().expect("KDF output >= 32");
let mac_key = &kdf[32..];
let ciphertext = &scheme_output[eph_pub_len..scheme_output.len() - 8];
let mac_received = &scheme_output[scheme_output.len() - 8..];
let mac_computed = hmac_sha256_8(mac_key, ciphertext);
if mac_received.ct_eq(&mac_computed).unwrap_u8() != 1 {
return Err(SecurityError::MacMismatch);
}
Ok(aes128_ctr(enc_key, icb, ciphertext))
}
pub fn suci_to_supi(suci: &[u8], hn_priv_key: Option<&[u8]>) -> Option<String> {
if suci.len() < 9 || suci[0] & 0x07 != 0x01 {
return None;
}
let supi_format = (suci[0] >> 4) & 0x0F;
if supi_format != 0 {
return None; }
let protection_scheme = suci[6] & 0x0F;
let (mcc, mnc) = crate::plmn::plmn_from_bytes(&suci[1..4])?;
if mcc.len() != 3 || !mcc.chars().all(|c| c.is_ascii_digit()) {
return None;
}
if !(2..=3).contains(&mnc.len()) || !mnc.chars().all(|c| c.is_ascii_digit()) {
return None;
}
let msin = match protection_scheme {
0x00 => crate::plmn::tbcd_decode(&suci[8..]),
0x01 => {
let priv_key = hn_priv_key?;
let priv_key: &[u8; 32] = priv_key.try_into().ok()?;
let msin_bcd = suci_decrypt_a(&suci[8..], priv_key).ok()?;
crate::plmn::tbcd_decode(&msin_bcd)
}
0x02 => {
let priv_key = hn_priv_key?;
let msin_bcd = suci_decrypt_b(&suci[8..], priv_key).ok()?;
crate::plmn::tbcd_decode(&msin_bcd)
}
_ => return None,
};
Some(format!("imsi-{}{}{}", mcc, mnc, msin))
}
pub fn suci_to_string(suci: &[u8]) -> Option<String> {
if suci.len() < 9 || suci[0] & 0x07 != 0x01 {
return None;
}
let (mcc, mnc) = crate::plmn::plmn_from_bytes(&suci[1..4])?;
let ri = crate::plmn::tbcd_decode(&suci[4..6]);
let scheme_id = suci[6] & 0x0F;
let key_id = suci[7];
let scheme_output = if scheme_id == 0 {
crate::plmn::tbcd_decode(&suci[8..])
} else {
hex::encode(&suci[8..])
};
Some(format!(
"suci-0-{}-{}-{}-{}-{}-{}",
mcc, mnc, ri, scheme_id, key_id, scheme_output
))
}
#[cfg(test)]
mod tests {
use super::*;
const PROFILE_A_PRIV: &str = "c53c22208b61860b06c62e5406a7b330c2b577aa5558981510d128247d38bd1d";
const PROFILE_A_PUB: &str = "5a8d38864820197c3394b92613b20b91633cbd897119273bf8e4a6f4eec0a650";
const PROFILE_B_PRIV: &str = "F1AB1074477EBCC7F554EA1C5FC368B1616730155E0041AC447D6301975FECDA";
const PROFILE_B_PUB: &str = "0472DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD15A7DED52FCBB097A4ED250E036C7B9C8C7004C4EEDC4F068CD7BF8D3F900E3B4";
#[test]
fn test_msin_to_bcd_even() {
assert_eq!(msin_to_bcd("00007487"), vec![0x00, 0x00, 0x47, 0x78]);
}
#[test]
fn test_msin_to_bcd_odd() {
assert_eq!(msin_to_bcd("001002086"), vec![0x00, 0x01, 0x20, 0x80, 0xF6]);
}
#[test]
fn test_msin_to_bcd_ten_digits() {
assert_eq!(
msin_to_bcd("0123456789"),
vec![0x10, 0x32, 0x54, 0x76, 0x98]
);
}
#[test]
fn test_msin_to_bcd_all_zeros() {
assert_eq!(
msin_to_bcd("0000000001"),
vec![0x00, 0x00, 0x00, 0x00, 0x10]
);
}
#[test]
fn test_profile_a_round_trip() {
let pub_bytes: [u8; 32] = hex::decode(PROFILE_A_PUB).unwrap().try_into().unwrap();
let priv_bytes: [u8; 32] = hex::decode(PROFILE_A_PRIV).unwrap().try_into().unwrap();
let msin_bcd = msin_to_bcd("0000000001");
let scheme_output = suci_scheme_output_a(&msin_bcd, &pub_bytes).unwrap();
assert_eq!(
suci_decrypt_a(&scheme_output, &priv_bytes).unwrap(),
msin_bcd
);
}
#[test]
fn test_profile_a_known_vector() {
let priv_bytes: [u8; 32] = hex::decode(PROFILE_A_PRIV).unwrap().try_into().unwrap();
let scheme_output = hex::decode(
"b2e92f836055a255837debf850b528997ce0201cb82adfe4be1f587d07d8457d\
cb02352410\
cddd9e730ef3fa87",
)
.unwrap();
let plaintext = suci_decrypt_a(&scheme_output, &priv_bytes).unwrap();
assert_eq!(plaintext, msin_to_bcd("001002086"));
}
#[test]
fn test_profile_b_round_trip() {
let pub_bytes = hex::decode(PROFILE_B_PUB).unwrap();
let priv_bytes = hex::decode(PROFILE_B_PRIV).unwrap();
let msin_bcd = msin_to_bcd("0000000001");
let scheme_output = suci_scheme_output_b(&msin_bcd, &pub_bytes).unwrap();
assert_eq!(
suci_decrypt_b(&scheme_output, &priv_bytes).unwrap(),
msin_bcd
);
}
#[test]
fn test_profile_b_known_vector_compressed_msin9() {
let priv_bytes = hex::decode(PROFILE_B_PRIV).unwrap();
let scheme_output = hex::decode(
"039aab8376597021e855679a9778ea0b67396e68c66df32c0f41e9acca2da9b9d1\
46a33fc271\
6ac7dae96aa30a4d",
)
.unwrap();
let plaintext = suci_decrypt_b(&scheme_output, &priv_bytes).unwrap();
assert_eq!(plaintext, msin_to_bcd("001002086"));
}
#[test]
fn test_profile_b_known_vector_compressed_msin10() {
let priv_bytes = hex::decode(PROFILE_B_PRIV).unwrap();
let scheme_output = hex::decode(
"03a7b1db2a9db9d44112b59d03d8243dc6089fd91d2ecb78f5d16298634682e94\
373888b22bdc9293d1681922e17",
)
.unwrap();
let plaintext = suci_decrypt_b(&scheme_output, &priv_bytes).unwrap();
assert_eq!(plaintext, msin_to_bcd("0123456789"));
}
#[test]
fn test_profile_b_known_vector_uncompressed_eph() {
let priv_bytes = hex::decode(PROFILE_B_PRIV).unwrap();
let scheme_output = hex::decode(
"049AAB8376597021E855679A9778EA0B67396E68C66DF32C0F41E9ACCA2DA9B9D1\
D1F44EA1C87AA7478B954537BDE79951E748A43294A4F4CF86EAFF1789C9C81F\
46A33FC2716AC7DAE96AA30A4D",
)
.unwrap();
let plaintext = suci_decrypt_b(&scheme_output, &priv_bytes).unwrap();
assert_eq!(plaintext, msin_to_bcd("001002086"));
}
#[test]
fn test_profile_b_round_trip_various_msins() {
let pub_bytes = hex::decode(PROFILE_B_PUB).unwrap();
let priv_bytes = hex::decode(PROFILE_B_PRIV).unwrap();
for msin in &[
"0010020862",
"00007487",
"001002086",
"0123456789",
"0000000001",
] {
let msin_bcd = msin_to_bcd(msin);
let scheme_output = suci_scheme_output_b(&msin_bcd, &pub_bytes).unwrap();
assert_eq!(
suci_decrypt_b(&scheme_output, &priv_bytes).unwrap(),
msin_bcd,
"msin={}",
msin
);
}
}
#[test]
fn test_profile_a_round_trip_various_msins() {
let pub_bytes: [u8; 32] = hex::decode(PROFILE_A_PUB).unwrap().try_into().unwrap();
let priv_bytes: [u8; 32] = hex::decode(PROFILE_A_PRIV).unwrap().try_into().unwrap();
for msin in &[
"0010020862",
"00007487",
"001002086",
"0123456789",
"0000000001",
] {
let msin_bcd = msin_to_bcd(msin);
let scheme_output = suci_scheme_output_a(&msin_bcd, &pub_bytes).unwrap();
assert_eq!(
suci_decrypt_a(&scheme_output, &priv_bytes).unwrap(),
msin_bcd,
"msin={}",
msin
);
}
}
#[test]
fn test_suci_to_supi_profile_a_known_vector() {
let priv_bytes = hex::decode(PROFILE_A_PRIV).unwrap();
let pub_bytes: [u8; 32] = hex::decode(PROFILE_A_PUB).unwrap().try_into().unwrap();
let msin_bcd = msin_to_bcd("001002086");
let scheme_output = suci_scheme_output_a(&msin_bcd, &pub_bytes).unwrap();
let mut suci = vec![0x01]; suci.extend_from_slice(&crate::plmn::plmn_to_bytes("208", "93")); suci.extend_from_slice(&[0x00, 0x00]); suci.push(0x01); suci.push(0x01); suci.extend_from_slice(&scheme_output);
let result = suci_to_supi(&suci, Some(&priv_bytes));
assert_eq!(result, Some("imsi-20893001002086".to_string()));
}
#[test]
fn test_suci_to_supi_profile_b_known_vector() {
let priv_bytes = hex::decode(PROFILE_B_PRIV).unwrap();
let pub_bytes = hex::decode(PROFILE_B_PUB).unwrap();
let msin_bcd = msin_to_bcd("001002086");
let scheme_output = suci_scheme_output_b(&msin_bcd, &pub_bytes).unwrap();
let mut suci = vec![0x01];
suci.extend_from_slice(&crate::plmn::plmn_to_bytes("208", "93"));
suci.extend_from_slice(&[0x00, 0x00]);
suci.push(0x02); suci.push(0x02); suci.extend_from_slice(&scheme_output);
let result = suci_to_supi(&suci, Some(&priv_bytes));
assert_eq!(result, Some("imsi-20893001002086".to_string()));
}
#[test]
fn test_suci_to_supi_null_scheme() {
let mut suci = vec![0x01];
suci.extend_from_slice(&crate::plmn::plmn_to_bytes("208", "93"));
suci.extend_from_slice(&[0x00, 0x00]);
suci.push(0x00); suci.push(0x00); suci.extend_from_slice(&msin_to_bcd("0000000001"));
let result = suci_to_supi(&suci, None);
assert_eq!(result, Some("imsi-208930000000001".to_string()));
}
#[test]
fn test_suci_to_supi_no_key_for_profile_a() {
let pub_bytes: [u8; 32] = hex::decode(PROFILE_A_PUB).unwrap().try_into().unwrap();
let msin_bcd = msin_to_bcd("001002086");
let scheme_output = suci_scheme_output_a(&msin_bcd, &pub_bytes).unwrap();
let mut suci = vec![0x01];
suci.extend_from_slice(&crate::plmn::plmn_to_bytes("208", "93"));
suci.extend_from_slice(&[0x00, 0x00]);
suci.push(0x01);
suci.push(0x01);
suci.extend_from_slice(&scheme_output);
assert_eq!(suci_to_supi(&suci, None), None);
}
}