use qrcode::QrCode;
use qrcode::render::svg;
pub fn svg(payload: &str) -> Result<String, String> {
let code = QrCode::new(payload.as_bytes()).map_err(|e| e.to_string())?;
let doc = code
.render::<svg::Color>()
.dark_color(svg::Color("#000"))
.light_color(svg::Color("#fff"))
.quiet_zone(true)
.module_dimensions(8, 8)
.build();
let start = doc
.find("<svg")
.ok_or_else(|| "renderer produced no svg element".to_string())?;
let mut frag = doc[start..].to_string();
if !frag.starts_with("<svg ") {
return Err("renderer produced an unexpected svg tag".into());
}
frag.insert_str(5, "role=\"img\" aria-label=\"Public gage add line\" ");
let end = frag.find('>').ok_or("svg tag is not closed")?;
frag.insert_str(end + 1, "<title>Public gage add line</title>");
let lower = frag.to_ascii_lowercase();
if frag.contains("<?xml")
|| frag.contains("<!DOCTYPE")
|| lower.contains("<script")
|| lower.contains("<img")
|| lower.contains("<foreignobject")
{
return Err("svg fragment is not bare markup".into());
}
Ok(frag)
}
#[cfg(test)]
mod tests {
use super::*;
use base64::{Engine, engine::general_purpose::STANDARD as B64};
use ed25519_dalek::SigningKey;
mod qr_decode {
include!("../tests/common/qr_decode.rs");
}
fn round_trip(line: &str) -> String {
let picture = svg(line).unwrap_or_else(|e| panic!("encode {line:?}: {e}"));
let sym = qr_decode::symbol(&picture).unwrap_or_else(|e| panic!("parse: {e}"));
let code = QrCode::new(line.as_bytes()).unwrap();
let painted: Vec<bool> = code
.to_colors()
.iter()
.map(|c| c.select(true, false))
.collect();
assert_eq!(
sym.dark, painted,
"the path is not the symbol the encoder stored"
);
qr_decode::decode_modules(sym.n, &sym.dark).unwrap_or_else(|e| panic!("decode: {e}"))
}
#[test]
fn the_svg_is_well_formed() {
let line = crate::identity::add_line("icy dune", &token([1u8; 32], [2u8; 32]));
let picture = svg(&line).unwrap();
assert!(picture.starts_with("<svg "), "{picture}");
assert!(picture.ends_with("</svg>"));
assert_eq!(picture.matches("<svg").count(), 1);
assert_eq!(picture.matches("</svg>").count(), 1);
assert!(picture.contains("xmlns=\"http://www.w3.org/2000/svg\""));
assert!(picture.contains("fill=\"#000\""));
assert!(picture.contains("fill=\"#fff\""));
assert!(picture.contains("<title>Public gage add line</title>"));
assert!(!picture.contains("<?xml"));
assert!(!picture.contains("<!DOCTYPE"));
assert!(!picture.to_ascii_lowercase().contains("<script"));
let bare = picture.replace("http://www.w3.org/2000/svg", "");
assert!(
!bare.contains("http:") && !bare.contains("https:"),
"{bare}"
);
let manifest = include_str!("../Cargo.toml");
assert!(
manifest.contains(
"qrcode = { version = \"0.14.1\", default-features = false, features = [\"svg\"] }"
),
"qrcode pin drifted"
);
}
#[test]
fn the_encoded_payload_round_trips_to_the_add_line() {
assert_eq!(round_trip("01234567"), "01234567");
for handle in ["icy dune", "cheerful kingfisher", "o'brien"] {
let line = crate::identity::add_line(handle, &token([3u8; 32], [4u8; 32]));
assert_eq!(round_trip(&line), line, "{handle}");
assert!(line.starts_with("gage add "), "{line}");
}
}
#[test]
fn a_long_handle_still_encodes() {
let forty = format!("long name {}", "a".repeat(30));
assert_eq!(forty.len(), 40, "{}", forty.len());
let line = crate::identity::add_line(&forty, &token([5u8; 32], [6u8; 32]));
assert_eq!(round_trip(&line), line);
let sixty_four = "b".repeat(64);
let line = crate::identity::add_line(&sixty_four, &token([5u8; 32], [6u8; 32]));
assert!(line.len() > 152, "the long line was {}", line.len());
let picture = svg(&line).unwrap();
let sym = qr_decode::symbol(&picture).unwrap();
let version = (sym.n - 17) / 4;
assert!(
version >= 9,
"version {version}, {} modules, line is {} bytes",
sym.n,
line.len()
);
assert_eq!(qr_decode::decode_modules(sym.n, &sym.dark).unwrap(), line);
}
#[test]
fn nothing_in_the_output_contains_a_key_or_a_passphrase() {
let sign_seed = [0x11u8; 32];
let box_seed = [0x22u8; 32];
let secret_sign = B64.encode(sign_seed);
let secret_box = B64.encode(box_seed);
let passphrase = "correct horse battery staple";
let public = token(sign_seed, box_seed);
let line = crate::identity::add_line("cheerful kingfisher", &public);
assert!(
!line.contains(&secret_sign),
"add line contains the signing seed"
);
assert!(
!line.contains(&secret_box),
"add line contains the encryption seed"
);
assert!(!line.contains(passphrase));
assert!(!line.contains("passphrase"));
let picture = svg(&line).unwrap();
assert_eq!(round_trip(&line), line);
for secret in [
&secret_sign,
&secret_box,
passphrase,
"passphrase",
"sign_seed",
"box_secret",
"identity.key",
&public,
] {
assert!(!picture.contains(secret), "svg markup contains {secret}");
}
}
fn token(sign_seed: [u8; 32], box_seed: [u8; 32]) -> String {
let signing = SigningKey::from_bytes(&sign_seed);
let sealing = crypto_box::SecretKey::from_bytes(box_seed);
format!(
"{}.{}",
B64.encode(signing.verifying_key().to_bytes()),
B64.encode(sealing.public_key().as_bytes())
)
}
}