#[cfg(feature = "alloc")]
use super::digits::decimal;
use super::{Error, Fault};
#[cfg(feature = "alloc")]
use super::{Faults, Plmn, ProtectionScheme, RoutingIndicator, SupiType, Tolerated};
#[cfg(feature = "alloc")]
use alloc::{
format,
string::{String, ToString},
vec::Vec,
};
#[cfg(feature = "alloc")]
use core::{fmt, str::FromStr};
const LONGEST_REALM: usize = 253;
const LONGEST_LABEL: usize = 63;
pub(super) const LONGEST_USERNAME: usize = 253;
#[cfg(feature = "alloc")]
const LONGEST_OPERATOR_OUTPUT: usize = 3000 + LONGEST_USERNAME;
#[cfg(feature = "alloc")]
const LONGEST_NAI: usize = 64 + 2 * LONGEST_OPERATOR_OUTPUT + 1 + LONGEST_REALM;
#[cfg(feature = "alloc")]
pub(super) const PROFILE_A_KEY: usize = 32;
#[cfg(feature = "alloc")]
pub(super) const PROFILE_B_KEY: usize = 33;
#[cfg(feature = "alloc")]
pub(super) const MAC_TAG: usize = 8;
#[cfg(feature = "alloc")]
const A_NAI: &str = "a username, @ and a realm, as type1.rid678.schid0.useriduser17@example.com";
pub fn check_realm(realm: &str) -> Result<(), Error> {
let member = "realm";
if realm.is_empty() || realm.len() > LONGEST_REALM {
return Err(Error::length(
member,
"octets",
realm.len(),
1,
LONGEST_REALM,
));
}
let rtext = |character: char| character.is_ascii_alphanumeric() || !character.is_ascii();
let expected = "a domain name of two labels at least, of letters, digits and hyphens, \
with no hyphen at either end and 63 octets at most each, as example.com";
let form = Error::new(member, Fault::Form { expected });
let mut labels = 0;
for label in realm.split('.') {
labels += 1;
if let Some(given) = label.chars().find(|&c| !rtext(c) && c != '-') {
let expected = "letters, digits, characters above ASCII and hyphens";
return Err(Error::new(member, Fault::Character { given, expected }));
}
let ends = label.starts_with(rtext) && label.ends_with(rtext);
if label.len() > LONGEST_LABEL || !ends {
return Err(form);
}
}
if labels < 2 {
return Err(form);
}
internationalized(realm)
}
#[cfg(feature = "idna")]
fn internationalized(realm: &str) -> Result<(), Error> {
normalized("realm", realm)?;
let expected = "a domain name that IDNA takes as it is written (RFC 5891)";
let refused = Error::new("realm", Fault::Form { expected });
let ascii = idna::domain_to_ascii_strict(realm).map_err(|_| refused)?;
if realm.is_ascii() {
return Ok(());
}
let (unicode, status) = idna::domain_to_unicode(&ascii);
match status.is_ok() && unicode == realm.to_lowercase() {
true => Ok(()),
false => Err(refused),
}
}
#[cfg(not(feature = "idna"))]
fn internationalized(_: &str) -> Result<(), Error> {
Ok(())
}
#[cfg(feature = "idna")]
fn normalized(member: &'static str, text: &str) -> Result<(), Error> {
use unicode_normalization::UnicodeNormalization;
match text.nfc().eq(text.chars()) {
true => Ok(()),
false => {
let expected = "a text in Unicode normalization form C (RFC 7542 clause 2.4)";
Err(Error::new(member, Fault::Form { expected }))
}
}
}
#[cfg(not(feature = "idna"))]
fn normalized(_: &'static str, _: &str) -> Result<(), Error> {
Ok(())
}
pub fn check_username(username: &str) -> Result<(), Error> {
let member = "username";
if username.is_empty() || username.len() > LONGEST_USERNAME {
let length = username.len();
return Err(Error::length(member, "octets", length, 1, LONGEST_USERNAME));
}
for atom in username.split('.') {
if atom.is_empty() {
let expected = "atoms with a dot between two of them, none of them empty";
return Err(Error::new(member, Fault::Form { expected }));
}
if let Some(given) = atom.chars().find(|&c| !username_character(c)) {
let expected = "letters, digits, characters above ASCII that are no control \
character, and !#$%&'*+-/=?^_`{|}~";
return Err(Error::new(member, Fault::Character { given, expected }));
}
}
normalized(member, username)
}
fn username_character(character: char) -> bool {
match character.is_ascii() {
true => character.is_ascii_alphanumeric() || "!#$%&'*+-/=?^_`{|}~".contains(character),
false => !character.is_control(),
}
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn home_network_domain(plmn: Plmn) -> String {
format!(
"5gc.mnc{:0>3}.mcc{}.3gppnetwork.org",
plmn.mnc(),
plmn.mcc()
)
}
#[cfg(feature = "alloc")]
fn is_home_network_domain(realm: &str) -> bool {
let mut labels = realm.split('.');
if labels.next() != Some("5gc") {
return false;
}
let mut label = labels.next();
let nid = label.and_then(|label| label.strip_prefix("nid"));
if let Some(nid) = nid {
let valid = nid.len() == 11
&& nid.bytes().all(|digit| digit.is_ascii_hexdigit())
&& matches!(nid.as_bytes()[0], b'0'..=b'2');
if !valid {
return false;
}
label = labels.next();
}
let three_digits = |label: Option<&str>, prefix: &str| {
label
.and_then(|label| label.strip_prefix(prefix))
.is_some_and(|digits| digits.len() == 3 && digits.bytes().all(|d| d.is_ascii_digit()))
};
three_digits(label, "mnc")
&& three_digits(labels.next(), "mcc")
&& labels.next() == Some("3gppnetwork")
&& labels.next() == Some("org")
&& labels.next().is_none()
}
#[cfg(feature = "alloc")]
fn is_gli(text: &str) -> bool {
let characters = text.as_bytes();
if characters.is_empty() || characters.len() > 200 || !characters.len().is_multiple_of(4) {
return false;
}
let padding = characters.iter().rev().take_while(|&&c| c == b'=').count();
if padding > 2 {
return false;
}
let value = |character: u8| match character {
b'A'..=b'Z' => Some(character - b'A'),
b'a'..=b'z' => Some(character - b'a' + 26),
b'0'..=b'9' => Some(character - b'0' + 52),
b'+' => Some(62),
b'/' => Some(63),
_ => None,
};
let data = &characters[..characters.len() - padding];
let Some(last) = data.iter().map(|&c| value(c)).try_fold(0, |_, v| v) else {
return false;
};
match padding {
1 => last & 0x03 == 0,
2 => last & 0x0f == 0,
_ => true,
}
}
#[cfg(feature = "alloc")]
fn check_clear(kind: SupiType, clear: &str) -> Result<(), Error> {
match kind {
SupiType::Imsi => decimal("msin", clear, 1, 10),
SupiType::NetworkSpecific if clear.is_empty() => Ok(()),
SupiType::NetworkSpecific | SupiType::Gci => check_username(clear),
SupiType::Gli => match is_gli(clear) {
true => Ok(()),
false => {
let expected = "the GLI in base64 of TS 23.003 clause 28.16.4: 1 to 200 \
characters, with their padding, for 150 octets at most";
Err(Error::new("username", Fault::Form { expected }))
}
},
}
}
#[cfg(feature = "alloc")]
fn ephemeral_key_octets(scheme: ProtectionScheme) -> Option<usize> {
match scheme {
ProtectionScheme::PROFILE_A => Some(PROFILE_A_KEY),
ProtectionScheme::PROFILE_B => Some(PROFILE_B_KEY),
_ => None,
}
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NaiSuci {
supi_type: SupiType,
routing_indicator: RoutingIndicator,
scheme: ProtectionScheme,
key_id: u8,
output: Vec<u8>,
realm: String,
}
#[cfg(feature = "alloc")]
impl NaiSuci {
pub fn null(
supi_type: SupiType,
routing_indicator: RoutingIndicator,
clear: &str,
realm: &str,
) -> Result<Self, Error> {
let (scheme, output) = (ProtectionScheme::NULL, clear.as_bytes().to_vec());
Self::checked(supi_type, routing_indicator, scheme, 0, output, realm)
}
pub fn concealed(
supi_type: SupiType,
routing_indicator: RoutingIndicator,
scheme: ProtectionScheme,
key_id: u8,
output: Vec<u8>,
realm: &str,
) -> Result<Self, Error> {
if scheme.is_null() {
let expected = "a scheme that conceals: 1, 2 or 12 to 15";
let fault = Fault::Value { given: 0, expected };
return Err(Error::new("protection-scheme", fault));
}
Self::checked(supi_type, routing_indicator, scheme, key_id, output, realm)
}
fn checked(
supi_type: SupiType,
routing_indicator: RoutingIndicator,
scheme: ProtectionScheme,
key_id: u8,
output: Vec<u8>,
realm: &str,
) -> Result<Self, Error> {
let key = "home-network-public-key-identifier";
match (scheme.is_null(), key_id) {
(true, 0) | (false, 1..=254) => {}
(true, given) => {
let expected = "0 for the null scheme";
return Err(Error::new(
key,
Fault::Value {
given: given.into(),
expected,
},
));
}
(false, given) => {
let expected = "1 to 254 for a scheme that conceals";
return Err(Error::new(
key,
Fault::Value {
given: given.into(),
expected,
},
));
}
}
if matches!(supi_type, SupiType::Gci | SupiType::Gli) {
if !scheme.is_null() {
let expected = "0: a GCI and a GLI have the null scheme alone";
let given = scheme.identifier().into();
return Err(Error::new(
"protection-scheme",
Fault::Value { given, expected },
));
}
if routing_indicator.as_str() != "0" {
let expected = "0, which a GCI and a GLI have";
return Err(Error::new("routing-indicator", Fault::Form { expected }));
}
}
check_realm(realm)?;
if supi_type == SupiType::Imsi && !is_home_network_domain(realm) {
let expected = "the home network domain of an IMSI, \
5gc.mnc<MNC>.mcc<MCC>.3gppnetwork.org, with the NID of an SNPN \
after 5gc or without it";
return Err(Error::new("realm", Fault::Form { expected }));
}
let member = "scheme-output";
match ephemeral_key_octets(scheme) {
_ if scheme.is_null() => {
let expected = "a text in UTF-8";
let clear = core::str::from_utf8(&output)
.map_err(|_| Error::new(member, Fault::Form { expected }))?;
check_clear(supi_type, clear)?;
}
Some(key) => {
let (shortest, longest) = (key + 1 + MAC_TAG, key + LONGEST_USERNAME + MAC_TAG);
if !(shortest..=longest).contains(&output.len()) {
let length = output.len();
return Err(Error::length(member, "octets", length, shortest, longest));
}
}
None => {
let (length, longest) = (output.len(), LONGEST_OPERATOR_OUTPUT);
if !(1..=longest).contains(&length) {
return Err(Error::length(member, "octets", length, 1, longest));
}
}
}
Ok(Self {
supi_type,
routing_indicator,
scheme,
key_id,
output,
realm: realm.into(),
})
}
pub fn supi_type(&self) -> SupiType {
self.supi_type
}
pub fn routing_indicator(&self) -> RoutingIndicator {
self.routing_indicator
}
pub fn protection_scheme(&self) -> ProtectionScheme {
self.scheme
}
pub fn home_network_public_key_identifier(&self) -> u8 {
self.key_id
}
pub fn realm(&self) -> &str {
&self.realm
}
pub fn clear(&self) -> Option<&str> {
match self.scheme.is_null() {
true => core::str::from_utf8(&self.output).ok(),
false => None,
}
}
pub fn concealed_output(&self) -> Option<&[u8]> {
(!self.scheme.is_null()).then_some(&self.output[..])
}
pub fn ecies(&self) -> Option<(&[u8], &[u8], &[u8])> {
let key = ephemeral_key_octets(self.scheme)?;
let (public_key, rest) = self.output.split_at(key);
let (ciphertext, tag) = rest.split_at(rest.len() - MAC_TAG);
Some((public_key, ciphertext, tag))
}
pub fn parse(text: &str) -> Result<Self, Error> {
if text.len() > LONGEST_NAI {
return Err(Error::length("nai", "octets", text.len(), 1, LONGEST_NAI));
}
let (username, realm) = text
.rsplit_once('@')
.ok_or(Error::new("nai", Fault::Form { expected: A_NAI }))?;
check_realm(realm)?;
let mut fields = username.splitn(4, '.');
let mut field = |name: &'static str| fields.next().ok_or(missing(name));
let supi_type = SupiType::from_type_number(number("type", field("type")?)?)?;
let routing_indicator = field("rid")?.strip_prefix("rid").ok_or(missing("rid"))?;
let routing_indicator = RoutingIndicator::new(routing_indicator)?;
let scheme = ProtectionScheme::new(number("schid", field("schid")?)?)?;
if scheme.is_null() {
let rest = field("userid")?;
let clear = rest.strip_prefix("userid").ok_or(missing("userid"))?;
return Self::null(supi_type, routing_indicator, clear, realm);
}
let rest = field("hnkey")?;
let (key_id, output) = match ephemeral_key_octets(scheme) {
Some(key) => {
let mut parts = rest.split('.');
let key_id = number("hnkey", parts.next().unwrap_or_default())?;
let mut output = octets("ecckey", parts.next(), key, key)?;
output.extend(octets("cip", parts.next(), 1, LONGEST_USERNAME)?);
output.extend(octets("mac", parts.next(), MAC_TAG, MAC_TAG)?);
if parts.next().is_some() {
let expected = "the last field of a profile: mac and its 8 octets";
return Err(Error::new("mac", Fault::Form { expected }));
}
(key_id, output)
}
None => {
let (key_id, output) = rest.split_once('.').unwrap_or((rest, ""));
let key_id = number("hnkey", key_id)?;
let output = octets("out", Some(output), 1, LONGEST_OPERATOR_OUTPUT)?;
(key_id, output)
}
};
let (kind, routing) = (supi_type, routing_indicator);
Self::concealed(kind, routing, scheme, key_id, output, realm)
}
pub fn to_nas_octets(&self) -> Result<Vec<u8>, Error> {
if self.supi_type == SupiType::Imsi {
let expected = "a network specific identifier, a GCI or a GLI: the mobile \
identity of an IMSI has its PLMN, which its realm does not say";
return Err(Error::new("supi-type", Fault::Value { given: 0, expected }));
}
let text = self.to_string();
let mut octets = Vec::with_capacity(1 + text.len());
octets.push(self.supi_type.nas_format() << 4 | 0x01);
octets.extend_from_slice(text.as_bytes());
Ok(octets)
}
pub fn from_nas_octets(octets: &[u8]) -> Result<Self, Error> {
let (suci, faults) = Self::from_nas_octets_tolerant(octets)?;
match faults.first() {
Some(fault) => Err(*fault),
None => Ok(suci),
}
}
pub fn from_nas_octets_tolerant(octets: &[u8]) -> Result<Tolerated<Self>, Error> {
let member = "suci";
let Some((&first, text)) = octets.split_first() else {
return Err(Error::length(member, "octets", 0, 2, usize::MAX));
};
let (format, kind) = ((first >> 4) & 0x07, first & 0x07);
if kind != 1 {
let fault = Fault::Octet {
given: first,
expected: first & 0xf0 | 0x01,
};
return Err(Error::new(member, fault));
}
if !matches!(format, 1..=3) {
let expected = "1, 2 or 3: the SUPI format of a NAI";
let fault = Fault::Value {
given: format.into(),
expected,
};
return Err(Error::new("supi-format", fault));
}
let mut faults = Faults::new();
if first & 0x88 != 0 {
faults.push(Error::new(member, Fault::Spare { given: first }));
}
if text.len() > LONGEST_NAI {
return Err(Error::length("nai", "octets", text.len(), 1, LONGEST_NAI));
}
let expected = "a text in UTF-8";
let text =
core::str::from_utf8(text).map_err(|_| Error::new("nai", Fault::Form { expected }))?;
let suci = Self::parse(text)?;
if suci.supi_type.nas_format() != format {
let expected = "the SUPI type that the SUPI format of the mobile identity says";
let given = suci.supi_type.type_number().into();
return Err(Error::new("supi-type", Fault::Value { given, expected }));
}
Ok((suci, faults))
}
}
#[cfg(feature = "alloc")]
fn missing(field: &'static str) -> Error {
let expected = match field {
"type" => "type and the SUPI type in decimal, as type1",
"rid" => "rid and the routing indicator, as rid678",
"schid" => "schid and the protection scheme in decimal, as schid0",
"userid" => "userid and the identity in clear, as useriduser17",
"hnkey" => "hnkey and the key identifier in decimal, as hnkey27",
"ecckey" => "ecckey and the ephemeral public key in hexadecimal digits",
"cip" => "cip and the ciphertext in hexadecimal digits",
"mac" => "mac and the 8 octets of the MAC tag in hexadecimal digits",
_ => "out and the scheme output in hexadecimal digits",
};
Error::new(field, Fault::Form { expected })
}
#[cfg(feature = "alloc")]
fn number(field: &'static str, text: &str) -> Result<u8, Error> {
let digits = text.strip_prefix(field).ok_or(missing(field))?;
let decimal = (1..=3).contains(&digits.len())
&& digits.bytes().all(|d| d.is_ascii_digit())
&& (digits == "0" || !digits.starts_with('0'));
match decimal {
true => digits.parse().map_err(|_| missing(field)),
false => Err(missing(field)),
}
}
#[cfg(feature = "alloc")]
fn octets(
field: &'static str,
text: Option<&str>,
fewest: usize,
most: usize,
) -> Result<Vec<u8>, Error> {
let digits = text
.and_then(|text| text.strip_prefix(field))
.ok_or(missing(field))?;
if let Some(given) = digits.chars().find(|digit| !digit.is_ascii_hexdigit()) {
let expected = "hexadecimal digits";
return Err(Error::new(field, Fault::Character { given, expected }));
}
if !digits.len().is_multiple_of(2) || !(fewest..=most).contains(&(digits.len() / 2)) {
let expected = match field {
"ecckey" => "the octets of the ephemeral public key of its profile",
"cip" => "1 to 253 octets",
"mac" => "8 octets",
_ => "1 to 3253 octets",
};
return Err(Error::new(field, Fault::Form { expected }));
}
let half = |digit: u8| (digit as char).to_digit(16).unwrap_or_default() as u8;
let (pairs, _) = digits.as_bytes().as_chunks::<2>();
Ok(pairs
.iter()
.map(|[high, low]| half(*high) << 4 | half(*low))
.collect())
}
#[cfg(feature = "alloc")]
fn hexadecimal(f: &mut fmt::Formatter<'_>, octets: &[u8]) -> fmt::Result {
octets.iter().try_for_each(|octet| write!(f, "{octet:02x}"))
}
#[cfg(feature = "alloc")]
impl fmt::Display for NaiSuci {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (kind, routing, scheme) = (
self.supi_type.type_number(),
self.routing_indicator,
self.scheme.identifier(),
);
write!(f, "type{kind}.rid{routing}.schid{scheme}.")?;
let key = self.key_id;
if let Some(clear) = self.clear() {
write!(f, "userid{clear}")?;
} else if let Some((public_key, ciphertext, tag)) = self.ecies() {
write!(f, "hnkey{key}.ecckey")?;
hexadecimal(f, public_key)?;
f.write_str(".cip")?;
hexadecimal(f, ciphertext)?;
f.write_str(".mac")?;
hexadecimal(f, tag)?;
} else {
write!(f, "hnkey{key}.out")?;
hexadecimal(f, &self.output)?;
}
write!(f, "@{}", self.realm)
}
}
#[cfg(feature = "alloc")]
impl fmt::Debug for NaiSuci {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "NaiSuci({self})")
}
}
#[cfg(feature = "alloc")]
impl FromStr for NaiSuci {
type Err = Error;
fn from_str(text: &str) -> Result<Self, Error> {
Self::parse(text)
}
}