Skip to main content

near_crypto/
key_file.rs

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    // Credential files generated which near cli works with have private_key
13    // rather than secret_key field.  To make it possible to read those from
14    // neard add private_key as an alias to this field so either will work.
15    #[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        // Test private_key alias for secret_key works.
107        let contents = KEY_FILE_CONTENTS.replace("secret_key", "private_key");
108        load(contents.as_bytes()).unwrap();
109
110        // Test private_key is mutually exclusive with secret_key.
111        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}