use serde::{Deserialize, Serialize};
use serde_json::error::Error as JsonError;
use ssb_crypto::AsBytes;
pub use ssb_crypto::Keypair;
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Write};
use std::path::Path;
use thiserror::Error;
pub fn read<R: Read>(mut r: R) -> Result<Keypair, KeyFileError> {
let mut buf = [0u8; 1024];
r.read(&mut buf)?;
let sec_str = std::str::from_utf8(&buf)?;
read_from_str(sec_str)
}
pub fn read_from_path<P: AsRef<Path>>(path: P) -> Result<Keypair, KeyFileError> {
let f = File::open(&path)?;
read(f)
}
pub fn read_from_str(s: &str) -> Result<Keypair, KeyFileError> {
let raw_sec_str = s
.lines()
.filter(|s| !s.starts_with('#'))
.collect::<Vec<&str>>()
.concat();
let sec_str = raw_sec_str.trim_matches(char::from(0));
let sec = serde_json::from_str::<SecretFile>(&sec_str)?;
if let Some(kp) = Keypair::from_base64(&sec.private) {
Ok(kp)
} else {
Err(KeyFileError::Decode)
}
}
pub fn write(keypair: &Keypair, mut w: impl Write) -> Result<(), io::Error> {
let mut id = encode_key(&keypair.public.0);
id.insert(0, '@');
let kf = SecretFileFull {
curve: "ed25519",
public: encode_key(&keypair.public.0),
private: encode_key(keypair.as_bytes()),
id,
};
w.write(PRE_COMMENT.as_bytes())?;
let formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
let mut ser = serde_json::Serializer::with_formatter(&mut w, formatter);
kf.serialize(&mut ser).unwrap();
w.write(POST_COMMENT.as_bytes())?;
w.write(kf.id.as_bytes())?;
Ok(())
}
pub fn write_to_path<P: AsRef<Path>>(keypair: &Keypair, path: P) -> Result<(), io::Error> {
if let Some(dir) = path.as_ref().parent() {
if !dir.exists() {
std::fs::create_dir_all(&dir)?
}
}
let f = OpenOptions::new()
.create_new(true)
.write(true)
.open(&path)?;
write(keypair, f)
}
pub fn write_to_string(keypair: &Keypair) -> String {
let mut out = Vec::with_capacity(727);
write(keypair, io::Cursor::new(&mut out)).unwrap();
String::from_utf8(out).unwrap()
}
pub fn generate(w: impl Write) -> Result<Keypair, io::Error> {
let keypair = Keypair::generate();
write(&keypair, w)?;
Ok(keypair)
}
pub fn generate_at_path<P: AsRef<Path>>(path: P) -> Result<Keypair, io::Error> {
let keypair = Keypair::generate();
write_to_path(&keypair, path)?;
Ok(keypair)
}
#[derive(Debug, Deserialize)]
struct SecretFile {
private: String,
}
#[derive(Debug, Serialize)]
struct SecretFileFull {
curve: &'static str,
public: String,
private: String,
id: String,
}
fn encode_key(bytes: &[u8]) -> String {
let mut out = base64::encode_config(bytes, base64::STANDARD);
out.push_str(".ed25519");
out
}
#[derive(Error, Debug)]
pub enum KeyFileError {
#[error("Failed to read from file")]
FileRead(#[from] io::Error),
#[error("Secret file isn't valid utf8")]
Utf8(#[from] std::str::Utf8Error),
#[error("Failed to parse secret file as json")]
Json(#[from] JsonError),
#[error("Failed to decode key pair")]
Decode,
}
const PRE_COMMENT: &str = "# WARNING: Never show this to anyone.
# WARNING: Never edit it or use it on multiple devices at once.
#
# This is your SECRET, it gives you magical powers. With your secret you can
# sign your messages so that your friends can verify that the messages came
# from you. If anyone learns your secret, they can use it to impersonate you.
#
# If you use this secret on more than one device you will create a fork and
# your friends will stop replicating your content.
#
";
const POST_COMMENT: &str = "
#
# The only part of this file that's safe to share is your public name:
#
# ";
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn test_data_file(name: &str) -> PathBuf {
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("test-data");
d.push(name);
d
}
#[test]
fn read_js_file() {
let keypair = read_from_path(test_data_file("secret")).unwrap();
assert_eq!(
keypair.public.as_base64(),
"H2qXeS5sOKUqaGNFgRJ6qR48+lAeP0C9lq9IVlQMotc="
);
}
#[test]
fn read_go_file() {
let keypair = read_from_path(test_data_file("secret-go")).unwrap();
assert_eq!(
keypair.public.as_base64(),
"H2qXeS5sOKUqaGNFgRJ6qR48+lAeP0C9lq9IVlQMotc="
);
}
#[test]
fn generate_file() {
let dir = tempfile::TempDir::new().unwrap();
assert!(generate_at_path(dir.path()).is_err());
let path = dir.path().join("secret");
let kp = generate_at_path(&path).unwrap();
assert!(generate_at_path(&path).is_err());
let kp2 = read_from_path(&path).unwrap();
assert_eq!(kp.public, kp2.public);
let path = dir.path().join("foo").join("bar");
let kp = generate_at_path(&path).unwrap();
let kp2 = read_from_path(&path).unwrap();
assert_eq!(kp.public, kp2.public);
}
}