1use serde::{Deserialize, Serialize};
2use serde_json::error::Error as JsonError;
3use ssb_crypto::AsBytes;
4pub use ssb_crypto::Keypair;
5use std::fs::{File, OpenOptions};
6use std::io::{self, Read, Write};
7use std::path::Path;
8use thiserror::Error;
9
10pub fn read<R: Read>(mut r: R) -> Result<Keypair, KeyFileError> {
13 let mut buf = [0u8; 1024];
15 r.read(&mut buf)?;
16 let sec_str = std::str::from_utf8(&buf)?;
17 read_from_str(sec_str)
18}
19
20pub fn read_from_path<P: AsRef<Path>>(path: P) -> Result<Keypair, KeyFileError> {
22 let f = File::open(&path)?;
23 read(f)
24}
25
26pub fn read_from_str(s: &str) -> Result<Keypair, KeyFileError> {
28 let raw_sec_str = s
29 .lines()
30 .filter(|s| !s.starts_with('#'))
31 .collect::<Vec<&str>>()
32 .concat();
33 let sec_str = raw_sec_str.trim_matches(char::from(0));
34 let sec = serde_json::from_str::<SecretFile>(&sec_str)?;
35 if let Some(kp) = Keypair::from_base64(&sec.private) {
36 Ok(kp)
37 } else {
38 Err(KeyFileError::Decode)
39 }
40}
41
42pub fn write(keypair: &Keypair, mut w: impl Write) -> Result<(), io::Error> {
45 let mut id = encode_key(&keypair.public.0);
46 id.insert(0, '@');
47
48 let kf = SecretFileFull {
49 curve: "ed25519",
50 public: encode_key(&keypair.public.0),
51 private: encode_key(keypair.as_bytes()),
52 id,
53 };
54
55 w.write(PRE_COMMENT.as_bytes())?;
56 let formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
57 let mut ser = serde_json::Serializer::with_formatter(&mut w, formatter);
58 kf.serialize(&mut ser).unwrap();
59
60 w.write(POST_COMMENT.as_bytes())?;
61 w.write(kf.id.as_bytes())?;
62 Ok(())
63}
64
65pub fn write_to_path<P: AsRef<Path>>(keypair: &Keypair, path: P) -> Result<(), io::Error> {
68 if let Some(dir) = path.as_ref().parent() {
72 if !dir.exists() {
73 std::fs::create_dir_all(&dir)?
74 }
75 }
76
77 let f = OpenOptions::new()
78 .create_new(true)
79 .write(true)
80 .open(&path)?;
81 write(keypair, f)
82}
83
84pub fn write_to_string(keypair: &Keypair) -> String {
86 let mut out = Vec::with_capacity(727);
87 write(keypair, io::Cursor::new(&mut out)).unwrap();
88 String::from_utf8(out).unwrap()
89}
90
91pub fn generate(w: impl Write) -> Result<Keypair, io::Error> {
93 let keypair = Keypair::generate();
94 write(&keypair, w)?;
95 Ok(keypair)
96}
97
98pub fn generate_at_path<P: AsRef<Path>>(path: P) -> Result<Keypair, io::Error> {
102 let keypair = Keypair::generate();
103 write_to_path(&keypair, path)?;
104 Ok(keypair)
105}
106
107#[derive(Debug, Deserialize)]
110struct SecretFile {
111 private: String,
112}
113
114#[derive(Debug, Serialize)]
115struct SecretFileFull {
116 curve: &'static str,
117 public: String,
118 private: String,
119 id: String,
120}
121
122fn encode_key(bytes: &[u8]) -> String {
123 let mut out = base64::encode_config(bytes, base64::STANDARD);
124 out.push_str(".ed25519");
125 out
126}
127
128#[derive(Error, Debug)]
130pub enum KeyFileError {
131 #[error("Failed to read from file")]
132 FileRead(#[from] io::Error),
133
134 #[error("Secret file isn't valid utf8")]
135 Utf8(#[from] std::str::Utf8Error),
136
137 #[error("Failed to parse secret file as json")]
138 Json(#[from] JsonError),
139
140 #[error("Failed to decode key pair")]
141 Decode,
142}
143
144const PRE_COMMENT: &str = "# WARNING: Never show this to anyone.
145# WARNING: Never edit it or use it on multiple devices at once.
146#
147# This is your SECRET, it gives you magical powers. With your secret you can
148# sign your messages so that your friends can verify that the messages came
149# from you. If anyone learns your secret, they can use it to impersonate you.
150#
151# If you use this secret on more than one device you will create a fork and
152# your friends will stop replicating your content.
153#
154";
155
156const POST_COMMENT: &str = "
157#
158# The only part of this file that's safe to share is your public name:
159#
160# ";
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use std::path::PathBuf;
166
167 fn test_data_file(name: &str) -> PathBuf {
168 let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
169 d.push("test-data");
170 d.push(name);
171 d
172 }
173
174 #[test]
175 fn read_js_file() {
176 let keypair = read_from_path(test_data_file("secret")).unwrap();
177
178 assert_eq!(
179 keypair.public.as_base64(),
180 "H2qXeS5sOKUqaGNFgRJ6qR48+lAeP0C9lq9IVlQMotc="
181 );
182 }
183
184 #[test]
185 fn read_go_file() {
186 let keypair = read_from_path(test_data_file("secret-go")).unwrap();
187 assert_eq!(
188 keypair.public.as_base64(),
189 "H2qXeS5sOKUqaGNFgRJ6qR48+lAeP0C9lq9IVlQMotc="
190 );
191 }
192
193 #[test]
194 fn generate_file() {
195 let dir = tempfile::TempDir::new().unwrap();
196
197 assert!(generate_at_path(dir.path()).is_err());
199 let path = dir.path().join("secret");
200
201 let kp = generate_at_path(&path).unwrap();
202
203 assert!(generate_at_path(&path).is_err());
205
206 let kp2 = read_from_path(&path).unwrap();
207 assert_eq!(kp.public, kp2.public);
208
209 let path = dir.path().join("foo").join("bar");
211 let kp = generate_at_path(&path).unwrap();
212 let kp2 = read_from_path(&path).unwrap();
213 assert_eq!(kp.public, kp2.public);
214 }
215}