hermit_toolkit_viewing_key/
lib.rs

1extern crate core;
2
3use subtle::ConstantTimeEq;
4
5use cosmwasm_std::{Env, HumanAddr, ReadonlyStorage, StdError, StdResult, Storage};
6use cosmwasm_storage::{PrefixedStorage, ReadonlyPrefixedStorage};
7
8use hermit_toolkit_crypto::{sha_256, Prng, SHA256_HASH_SIZE};
9
10pub const VIEWING_KEY_SIZE: usize = SHA256_HASH_SIZE;
11pub const VIEWING_KEY_PREFIX: &str = "api_key_";
12const SEED_KEY: &[u8] = b"::seed";
13
14/// This is the default implementation of the viewing key store, using the "viewing_keys"
15/// storage prefix.
16///
17/// You can use another storage location by implementing `ViewingKeyStore` for your own type.
18pub struct ViewingKey;
19
20impl ViewingKeyStore for ViewingKey {
21    const STORAGE_KEY: &'static [u8] = b"viewing_keys";
22}
23
24/// A trait describing the interface of a Viewing Key store/vault.
25///
26/// It includes a default implementation that only requires specifying where in the storage
27/// the keys should be held.
28pub trait ViewingKeyStore {
29    const STORAGE_KEY: &'static [u8];
30
31    /// Set the initial prng seed for the store
32    fn set_seed<S: Storage>(storage: &mut S, seed: &[u8]) {
33        let mut seed_key = Vec::new();
34        seed_key.extend_from_slice(Self::STORAGE_KEY);
35        seed_key.extend_from_slice(SEED_KEY);
36
37        storage.set(&seed_key, seed)
38    }
39
40    /// Create a new viewing key, save it to storage, and return it.
41    ///
42    /// The random entropy should be provided from some external source, such as the user.
43    fn create<S: Storage>(
44        storage: &mut S,
45        env: &Env,
46        account: &HumanAddr,
47        entropy: &[u8],
48    ) -> String {
49        let mut seed_key = Vec::with_capacity(Self::STORAGE_KEY.len() + SEED_KEY.len());
50        seed_key.extend_from_slice(Self::STORAGE_KEY);
51        seed_key.extend_from_slice(SEED_KEY);
52        let seed = storage.get(&seed_key).unwrap_or_default();
53
54        let (viewing_key, next_seed) = new_viewing_key(env, &seed, entropy);
55        let mut balance_store = PrefixedStorage::new(Self::STORAGE_KEY, storage);
56        let hashed_key = sha_256(viewing_key.as_bytes());
57        balance_store.set(account.as_str().as_bytes(), &hashed_key);
58
59        storage.set(&seed_key, &next_seed);
60
61        viewing_key
62    }
63
64    /// Set a new viewing key based on a predetermined value.
65    fn set<S: Storage>(storage: &mut S, account: &HumanAddr, viewing_key: &str) {
66        let mut balance_store = PrefixedStorage::new(Self::STORAGE_KEY, storage);
67        balance_store.set(
68            account.as_str().as_bytes(),
69            &sha_256(viewing_key.as_bytes()),
70        );
71    }
72
73    /// Check if a viewing key matches an account.
74    fn check<S: ReadonlyStorage>(
75        storage: &S,
76        account: &HumanAddr,
77        viewing_key: &str,
78    ) -> StdResult<()> {
79        let balance_store = ReadonlyPrefixedStorage::new(Self::STORAGE_KEY, storage);
80        let expected_hash = balance_store.get(account.as_str().as_bytes());
81        let expected_hash = match &expected_hash {
82            Some(hash) => hash.as_slice(),
83            None => &[0u8; VIEWING_KEY_SIZE],
84        };
85        let key_hash = sha_256(viewing_key.as_bytes());
86        if ct_slice_compare(&key_hash, expected_hash) {
87            Ok(())
88        } else {
89            Err(StdError::unauthorized())
90        }
91    }
92}
93
94fn new_viewing_key(env: &Env, seed: &[u8], entropy: &[u8]) -> (String, [u8; 32]) {
95    // 16 here represents the lengths in bytes of the block height and time.
96    let entropy_len = 16 + env.message.sender.len() + entropy.len();
97    let mut rng_entropy = Vec::with_capacity(entropy_len);
98    rng_entropy.extend_from_slice(&env.block.height.to_be_bytes());
99    rng_entropy.extend_from_slice(&env.block.time.to_be_bytes());
100    rng_entropy.extend_from_slice(env.message.sender.0.as_bytes());
101    rng_entropy.extend_from_slice(entropy);
102
103    let mut rng = Prng::new(seed, &rng_entropy);
104
105    let rand_slice = rng.rand_bytes();
106
107    let key = sha_256(&rand_slice);
108
109    let viewing_key = VIEWING_KEY_PREFIX.to_string() + &base64::encode(&key);
110    (viewing_key, rand_slice)
111}
112
113fn ct_slice_compare(s1: &[u8], s2: &[u8]) -> bool {
114    bool::from(s1.ct_eq(s2))
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    use cosmwasm_std::testing::{mock_dependencies, mock_env};
122
123    #[test]
124    fn test_viewing_keys() {
125        let account = HumanAddr("user-1".to_string());
126
127        let mut deps = mock_dependencies(20, &[]);
128        let env = mock_env(account.as_str(), &[]);
129
130        // VK not set yet:
131        let result = ViewingKey::check(&deps.storage, &account, "fake key");
132        assert_eq!(result, Err(StdError::unauthorized()));
133
134        ViewingKey::set_seed(&mut deps.storage, b"seed");
135        let viewing_key = ViewingKey::create(&mut deps.storage, &env, &account, b"entropy");
136
137        let result = ViewingKey::check(&deps.storage, &account, &viewing_key);
138        assert_eq!(result, Ok(()));
139
140        // Create a key with the same entropy. Check that it's different
141        let viewing_key_2 = ViewingKey::create(&mut deps.storage, &env, &account, b"entropy");
142        assert_ne!(viewing_key, viewing_key_2);
143
144        // VK set to another key:
145        let result = ViewingKey::check(&deps.storage, &account, "fake key");
146        assert_eq!(result, Err(StdError::unauthorized()));
147
148        let viewing_key = "custom key";
149
150        ViewingKey::set(&mut deps.storage, &account, viewing_key);
151
152        let result = ViewingKey::check(&deps.storage, &account, viewing_key);
153        assert_eq!(result, Ok(()));
154
155        // VK set to another key:
156        let result = ViewingKey::check(&deps.storage, &account, "fake key");
157        assert_eq!(result, Err(StdError::unauthorized()));
158    }
159}