use crate::core::StoreError;
const EM_DASH: char = '\u{2014}';
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NoteSignature {
pub name: String,
pub key_id: [u8; 4],
pub signature: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedNote {
pub text: String,
pub signatures: Vec<NoteSignature>,
}
#[must_use]
pub fn key_id(name: &str, signature_type: u8, public_key: &[u8]) -> [u8; 4] {
use sha2::{Digest as _, Sha256};
let mut h = Sha256::new();
h.update(name.as_bytes());
h.update([0x0A]);
h.update([signature_type]);
h.update(public_key);
let full = h.finalize();
[full[0], full[1], full[2], full[3]]
}
impl SignedNote {
pub fn new(text: impl Into<String>) -> Result<Self, StoreError> {
let text = text.into();
Self::validate_text(&text)?;
Ok(Self {
text,
signatures: Vec::new(),
})
}
pub fn validate_text(text: &str) -> Result<(), StoreError> {
let bad = |what: &str| StoreError::Backend(format!("signed note: {what}"));
if text.is_empty() {
return Err(bad("the body is empty"));
}
if !text.ends_with('\n') {
return Err(bad(
"the body does not end in a newline — the trailing newline is part of \
what gets signed, so a body without one signs different bytes than the \
verifier will check",
));
}
if text.trim_end_matches('\n').contains("\n\n") {
return Err(bad(
"the body contains a blank line, which is the separator between a note \
and its signatures — such a note cannot be read back as the same body, \
so its signatures would cover bytes no verifier will hash",
));
}
if let Some(c) = text.chars().find(|c| c.is_control() && *c != '\n') {
return Err(bad(&format!(
"the body contains the control character {:#04x}, which the note format \
forbids",
c as u32
)));
}
Ok(())
}
pub fn with_signature(mut self, signature: NoteSignature) -> Result<Self, StoreError> {
Self::validate_name(&signature.name)?;
self.signatures.push(signature);
Ok(self)
}
pub fn validate_name(name: &str) -> Result<(), StoreError> {
let bad = |what: &str| StoreError::Backend(format!("signed note: a key name {what}"));
if name.is_empty() {
return Err(bad("is empty, so the signature line names nobody"));
}
if let Some(c) = name
.chars()
.find(|c| c.is_whitespace() || c.is_control() || *c == EM_DASH)
{
return Err(bad(&format!(
"contains {c:?}, which the signature line uses as structure — the note \
would serialise and read back as a different name, a truncated payload, \
or an extra signature line nobody wrote"
)));
}
Ok(())
}
#[must_use]
pub fn to_wire(&self) -> String {
let mut out = self.text.clone();
out.push('\n');
for s in &self.signatures {
let mut payload = Vec::with_capacity(4 + s.signature.len());
payload.extend_from_slice(&s.key_id);
payload.extend_from_slice(&s.signature);
out.push(EM_DASH);
out.push(' ');
out.push_str(&s.name);
out.push(' ');
out.push_str(&b64(&payload));
out.push('\n');
}
out
}
pub fn parse(wire: &str) -> Result<Self, StoreError> {
let bad = |what: &str| StoreError::Backend(format!("signed note: {what}"));
let (text, rest) = wire
.split_once("\n\n")
.ok_or_else(|| bad("no blank line separating the body from its signatures"))?;
let text = format!("{text}\n");
Self::validate_text(&text)?;
let mut signatures = Vec::new();
for line in rest.lines().filter(|l| !l.is_empty()) {
let body = line.strip_prefix(EM_DASH).ok_or_else(|| {
bad(
"a signature line does not begin with an em dash (U+2014). A hyphen \
looks identical in most terminals and is not the same byte",
)
})?;
let body = body
.strip_prefix(' ')
.ok_or_else(|| bad("no space after the em dash"))?;
let (name, payload) = body
.split_once(' ')
.ok_or_else(|| bad("a signature line has no base64 payload"))?;
let raw = unb64(payload).ok_or_else(|| bad("the payload is not valid base64"))?;
if raw.len() < 5 {
return Err(bad(
"the payload is shorter than a key id plus a signature, so it cannot \
be either",
));
}
Self::validate_name(name)?;
signatures.push(NoteSignature {
name: name.to_owned(),
key_id: [raw[0], raw[1], raw[2], raw[3]],
signature: raw[4..].to_vec(),
});
}
Ok(Self { text, signatures })
}
}
pub(crate) fn b64(bytes: &[u8]) -> String {
const A: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::new();
for chunk in bytes.chunks(3) {
let b = [
chunk[0],
*chunk.get(1).unwrap_or(&0),
*chunk.get(2).unwrap_or(&0),
];
let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
out.push(A[(n >> 18) as usize & 63] as char);
out.push(A[(n >> 12) as usize & 63] as char);
out.push(if chunk.len() > 1 {
A[(n >> 6) as usize & 63] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
A[n as usize & 63] as char
} else {
'='
});
}
out
}
pub(crate) fn unb64(s: &str) -> Option<Vec<u8>> {
let val = |c: u8| -> Option<u32> {
Some(match c {
b'A'..=b'Z' => u32::from(c - b'A'),
b'a'..=b'z' => u32::from(c - b'a') + 26,
b'0'..=b'9' => u32::from(c - b'0') + 52,
b'+' => 62,
b'/' => 63,
_ => return None,
})
};
let raw = s.as_bytes();
if raw.is_empty() || !raw.len().is_multiple_of(4) {
return None;
}
let pad = raw.iter().rev().take_while(|c| **c == b'=').count();
if pad > 2 || raw[..raw.len() - pad].contains(&b'=') {
return None;
}
let mut out = Vec::with_capacity(raw.len() / 4 * 3 - pad);
for chunk in raw.chunks(4) {
let live = chunk.iter().take_while(|c| **c != b'=').count();
let mut n = 0u32;
for (i, c) in chunk[..live].iter().enumerate() {
n |= val(*c)? << (18 - 6 * i);
}
let take = live * 6 / 8;
for i in 0..take {
out.push(((n >> (16 - 8 * i)) & 0xff) as u8);
}
if n & ((1 << (24 - take * 8)) - 1) != 0 {
return None;
}
}
Some(out)
}