rustical_dav_push/
vapid.rs1use base64ct::{Base64UrlUnpadded, Encoding};
2use rustical_xml::XmlSerialize;
3use serde::Deserialize;
4use thiserror::Error;
5use web_push::VapidKey;
6
7#[derive(Debug, Error)]
8pub enum VapidError {
9 #[error(transparent)]
10 WebPushError(#[from] web_push::WebPushError),
11 #[error(transparent)]
12 FromUtf8Error(#[from] std::string::FromUtf8Error),
13}
14
15#[derive(Clone)]
16pub struct VapidKeypair(pub VapidKey);
17
18impl std::fmt::Debug for VapidKeypair {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 f.debug_struct("VapidPublicKeyB64").finish_non_exhaustive()
21 }
22}
23
24impl VapidKeypair {
25 #[must_use]
26 pub fn generate_p256() -> Self {
27 Self(VapidKey::generate())
28 }
29 #[must_use]
30 pub fn public(&self) -> VapidPublicKey {
31 VapidPublicKey(self.0.public_key())
32 }
33
34 pub fn from_pem(pem: &str) -> Result<Self, VapidError> {
35 Ok(Self(VapidKey::from_pem(pem)?))
36 }
37
38 pub fn to_pem(&self) -> Result<String, VapidError> {
39 Ok(self.0.to_pem()?)
40 }
41}
42
43#[derive(Clone, Deserialize, PartialEq, Eq)]
44pub struct VapidPublicKeyB64(pub String);
45
46impl std::fmt::Debug for VapidPublicKeyB64 {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 f.debug_struct("VapidPublicKeyB64").finish_non_exhaustive()
49 }
50}
51
52#[derive(Debug, Clone, XmlSerialize, PartialEq, Eq)]
53struct VapidPublicKeyProp<'b> {
54 #[xml(ty = "attr", rename = "type")]
55 pub ty: &'static str,
56 #[xml(ty = "text")]
57 pub key: &'b str,
58}
59
60impl XmlSerialize for VapidPublicKeyB64 {
61 fn serialize(
62 &self,
63 ns: Option<quick_xml::name::Namespace>,
64 tag: Option<&str>,
65 namespaces: &std::collections::HashMap<quick_xml::name::Namespace, &str>,
66 writer: &mut quick_xml::Writer<&mut Vec<u8>>,
67 ) -> std::io::Result<()> {
68 VapidPublicKeyProp {
69 ty: "p256ecdsa",
70 key: &self.0,
71 }
72 .serialize(ns, tag, namespaces, writer)
73 }
74
75 fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
76 VapidPublicKeyProp {
77 ty: "p256ecdsa",
78 key: &self.0,
79 }
80 .attributes()
81 }
82}
83
84pub struct VapidPublicKey(Vec<u8>);
85
86impl std::fmt::Debug for VapidPublicKey {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 f.debug_struct("VapidPublicKey").finish_non_exhaustive()
89 }
90}
91
92impl VapidPublicKey {
93 #[must_use]
94 pub fn encode_b64(&self) -> VapidPublicKeyB64 {
95 VapidPublicKeyB64(Base64UrlUnpadded::encode_string(&self.0))
96 }
97}
98
99#[cfg(test)]
100pub mod tests {
101 use crate::vapid::VapidKeypair;
102
103 pub const PRIVATE_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----
105MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgTB3vavSTXm+w9v6Q
1069eCwVFwRhnLfWuO3y2VwpfKhYg+hRANCAARRJ6EbENwBqqjN6v+2jxsalRvqEKUr
1073oBLcSuhKaTh5UrFE9kQUxWnmft0yL35yRmHHSpSyD3A4pqUi0satvIp
108-----END PRIVATE KEY-----
109";
110
111 pub const PRIVATE_KEY_PEM_SEC1: &str = "-----BEGIN EC PRIVATE KEY-----
112MHcCAQEEIMwug/U2ds75hkEIeou9s0kj1ziCJETswt5S9ztJ2L5SoAoGCCqGSM49
113AwEHoUQDQgAEyjUeooXqyQxljKSu17126pjAEPTyYNApO6dGQl0PexMn0T7LI3qw
114mU9ZOko2Gn7LYp5LqgA0cX6rfDftsKVvtQ==
115-----END EC PRIVATE KEY-----";
116
117 pub const PUBLIC_KEY_B64: &str =
118 "BFEnoRsQ3AGqqM3q_7aPGxqVG-oQpSvegEtxK6EppOHlSsUT2RBTFaeZ-3TIvfnJGYcdKlLIPcDimpSLSxq28ik";
119
120 #[test]
121 fn test_generate_key() {
122 let key = VapidKeypair::generate_p256();
123 let pem = key.to_pem().unwrap();
124 assert!(pem.starts_with("-----BEGIN PRIVATE KEY-----\n"));
125 assert!(pem.ends_with("-----END PRIVATE KEY-----\n"));
126 }
127
128 #[test]
129 fn test_pem_roundtrip() {
130 let key = VapidKeypair::from_pem(PRIVATE_KEY_PEM).unwrap();
131 assert_eq!(key.to_pem().unwrap(), PRIVATE_KEY_PEM);
132 }
133
134 #[test]
135 fn test_public_key() {
136 let key = VapidKeypair::from_pem(PRIVATE_KEY_PEM).unwrap();
137 assert_eq!(key.public().encode_b64().0, PUBLIC_KEY_B64);
138 }
139
140 #[test]
141 fn test_parse_pkcs8() {
142 VapidKeypair::from_pem(PRIVATE_KEY_PEM).unwrap();
143 }
144
145 #[test]
146 fn test_parse_sec1() {
147 VapidKeypair::from_pem(PRIVATE_KEY_PEM_SEC1).unwrap();
148 }
149}