1use crate::{PublicKey, SecretKey};
2use near_account_id::AccountId;
3use std::fs::File;
4use std::io;
5use std::io::{Read, Write};
6use std::path::Path;
7
8#[derive(serde::Serialize, serde::Deserialize)]
9pub struct KeyFile {
10 pub account_id: AccountId,
11 pub public_key: PublicKey,
12 #[serde(alias = "private_key")]
16 pub secret_key: SecretKey,
17}
18
19impl KeyFile {
20 pub fn write_to_file(&self, path: &Path) -> io::Result<()> {
21 let data = serde_json::to_string_pretty(self)?;
22 let mut file = Self::create(path)?;
23 file.write_all(data.as_bytes())
24 }
25
26 #[cfg(unix)]
27 fn create(path: &Path) -> io::Result<File> {
28 use std::os::unix::fs::OpenOptionsExt;
29 std::fs::File::options().mode(0o600).write(true).create(true).truncate(true).open(path)
30 }
31
32 #[cfg(not(unix))]
33 fn create(path: &Path) -> io::Result<File> {
34 std::fs::File::create(path)
35 }
36
37 pub fn from_file(path: &Path) -> io::Result<Self> {
38 let mut file = File::open(path)?;
39 let mut json_config_str = String::new();
40 file.read_to_string(&mut json_config_str)?;
41 let json_str_without_comments: String =
42 near_config_utils::strip_comments_from_json_str(&json_config_str)?;
43
44 let key_file: Self = serde_json::from_str(&json_str_without_comments)?;
45 if key_file.secret_key.public_key() != key_file.public_key {
46 return Err(io::Error::new(
47 io::ErrorKind::InvalidData,
48 "public key does not correspond to secret key in key file",
49 ));
50 }
51 Ok(key_file)
52 }
53}
54
55#[cfg(test)]
56mod test {
57 use super::*;
58
59 const ACCOUNT_ID: &str = "example";
60 const SECRET_KEY: &str = "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr";
61 const KEY_FILE_CONTENTS: &str = r#"{
62 "account_id": "example",
63 "public_key": "ed25519:6DSjZ8mvsRZDvFqFxo8tCKePG96omXW7eVYVSySmDk8e",
64 "secret_key": "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr"
65}"#;
66
67 #[test]
68 fn test_to_file() {
69 let tmp = tempfile::TempDir::new().unwrap();
70 let path = tmp.path().join("key-file");
71
72 let account_id = ACCOUNT_ID.parse().unwrap();
73 let secret_key: SecretKey = SECRET_KEY.parse().unwrap();
74 let public_key = secret_key.public_key();
75 let key = KeyFile { account_id, public_key, secret_key };
76 key.write_to_file(&path).unwrap();
77
78 assert_eq!(KEY_FILE_CONTENTS, std::fs::read_to_string(&path).unwrap());
79
80 #[cfg(unix)]
81 {
82 use std::os::unix::fs::PermissionsExt;
83 let got = std::fs::metadata(&path).unwrap().permissions().mode();
84 assert_eq!(0o600, got & 0o777);
85 }
86 }
87
88 #[test]
89 fn test_from_file() {
90 fn load(contents: &[u8]) -> io::Result<()> {
91 let tmp = tempfile::NamedTempFile::new().unwrap();
92 tmp.as_file().write_all(contents).unwrap();
93 let result = KeyFile::from_file(tmp.path());
94 tmp.close().unwrap();
95
96 result.map(|key| {
97 assert_eq!(ACCOUNT_ID, key.account_id.to_string());
98 let secret_key: SecretKey = SECRET_KEY.parse().unwrap();
99 assert_eq!(secret_key, key.secret_key);
100 assert_eq!(secret_key.public_key(), key.public_key);
101 })
102 }
103
104 load(KEY_FILE_CONTENTS.as_bytes()).unwrap();
105
106 let contents = KEY_FILE_CONTENTS.replace("secret_key", "private_key");
108 load(contents.as_bytes()).unwrap();
109
110 let err = load(br#"{
112 "account_id": "example",
113 "public_key": "ed25519:6DSjZ8mvsRZDvFqFxo8tCKePG96omXW7eVYVSySmDk8e",
114 "secret_key": "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr",
115 "private_key": "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr"
116 }"#).unwrap_err();
117 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
118 let inner_msg = err.into_inner().unwrap().to_string();
119 assert!(inner_msg.contains("duplicate field"));
120 }
121
122 #[test]
123 fn test_from_file_mismatched_keys() {
124 let tmp = tempfile::TempDir::new().unwrap();
125 let path = tmp.path().join("key-file");
126
127 let account_id: AccountId = ACCOUNT_ID.parse().unwrap();
128 let secret_key: SecretKey = SECRET_KEY.parse().unwrap();
129 let wrong_public_key =
130 "ed25519:3ThNatHmFR6gtMEFMEBPw6M1PGH6FVbGMzwBd4JqHTFT".parse().unwrap();
131 let key_file = KeyFile { account_id, public_key: wrong_public_key, secret_key };
132 key_file.write_to_file(&path).unwrap();
133
134 let err = match KeyFile::from_file(&path) {
135 Err(e) => e,
136 Ok(_) => panic!("expected error for mismatched keys"),
137 };
138 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
139 assert!(err.to_string().contains("public key does not correspond to secret key"));
140 }
141}