@@ -0,0 +1,34 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use base64::engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig};
|
||||
use base64::{Engine as _, alphabet};
|
||||
|
||||
/// Unpadded base64url (RFC 4648 §5), 43 characters for 32 bytes: §8's value
|
||||
/// encoding at any depth.
|
||||
///
|
||||
/// The reader tolerates non-zero trailing bits; the writer never emits them.
|
||||
/// The spec's own worked example (`examples.md` §6.2) contains five such
|
||||
/// values, and a reader cannot tell a mis-encoded named field from a correctly
|
||||
/// encoded one, so the boundary is the writer's alone.
|
||||
const BASE64URL: GeneralPurpose = GeneralPurpose::new(
|
||||
&alphabet::URL_SAFE,
|
||||
GeneralPurposeConfig::new()
|
||||
.with_encode_padding(false)
|
||||
.with_decode_padding_mode(DecodePaddingMode::RequireNone)
|
||||
.with_decode_allow_trailing_bits(true),
|
||||
);
|
||||
|
||||
pub(crate) fn encode(bytes: &[u8]) -> String {
|
||||
BASE64URL.encode(bytes)
|
||||
}
|
||||
|
||||
/// Decodes one 32-byte value, the width every §8 field has.
|
||||
pub(crate) fn decode_32(value: &str) -> Result<[u8; 32]> {
|
||||
let bytes = BASE64URL
|
||||
.decode(value.trim())
|
||||
.map_err(|error| anyhow!("invalid base64url: {error}"))?;
|
||||
|
||||
bytes
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| anyhow!("expected 32 bytes, got {}", bytes.len()))
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
pub mod base64url;
|
||||
pub mod derive;
|
||||
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use data_encoding::HEXLOWER;
|
||||
use rand::TryRng as _;
|
||||
use rand::rngs::SysRng;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Extra;
|
||||
|
||||
/// Decode a 64-character lowercase-hex string into 32 bytes.
|
||||
///
|
||||
/// Uppercase and other non-canonical spellings are rejected.
|
||||
pub(crate) fn decode_hex_32(value: &str) -> Result<[u8; 32]> {
|
||||
pub fn decode_hex_32(value: &str) -> Result<[u8; 32]> {
|
||||
decode_hex_lower::<32>(value)
|
||||
}
|
||||
|
||||
@@ -38,3 +44,34 @@ pub(crate) fn random_32() -> Result<[u8; 32]> {
|
||||
fill_random(&mut bytes)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Hex to unpadded base64url for one 32-byte §8 value.
|
||||
pub(crate) fn hex32_to_base64(value: &str) -> Result<String> {
|
||||
Ok(base64url::encode(&decode_hex_32(value)?))
|
||||
}
|
||||
|
||||
/// Unpadded base64url to lowercase hex for one 32-byte §8 value.
|
||||
pub(crate) fn base64_to_hex32(value: &str) -> Result<String> {
|
||||
Ok(HEXLOWER.encode(&base64url::decode_32(value)?))
|
||||
}
|
||||
|
||||
/// Canonical JSON bytes: the total-order tie-break every content merge uses.
|
||||
pub(crate) fn canonical<T: Serialize>(value: &T) -> String {
|
||||
serde_json::to_string(value).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Unions an unknown-field map. Where both sides carry a key, the
|
||||
/// lexicographically lowest canonical bytes win, so two devices converge
|
||||
/// instead of flapping.
|
||||
pub(crate) fn union(into: &mut Extra, other: Extra) {
|
||||
for (key, value) in other {
|
||||
let replace = match into.get(&key) {
|
||||
Some(existing) => canonical(&value) < canonical(existing),
|
||||
None => true,
|
||||
};
|
||||
|
||||
if replace {
|
||||
into.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user