From 0223592b7eb1a80ff2b7c2ec95e2ad641da07ad8 Mon Sep 17 00:00:00 2001 From: Katharina Fey Date: Mon, 6 Aug 2018 16:13:33 +0200 Subject: More refactoring and trying to get keystores to work properly --- Cargo.lock | 10 ++--- lockchain-core/examples/user_registry.rs | 33 --------------- lockchain-core/src/crypto/encoding.rs | 24 +++++++++++ lockchain-core/src/crypto/hashing.rs | 25 +++++++++++ lockchain-core/src/crypto/keystore.rs | 65 ----------------------------- lockchain-core/src/crypto/mod.rs | 9 ++-- lockchain-core/src/crypto/random.rs | 46 ++++++++++++++++++++ lockchain-core/src/crypto/secrets.rs | 25 ----------- lockchain-core/src/crypto/store.rs | 15 ------- lockchain-core/src/crypto/utils/encoding.rs | 24 ----------- lockchain-core/src/crypto/utils/hashing.rs | 25 ----------- lockchain-core/src/crypto/utils/mod.rs | 5 --- lockchain-core/src/crypto/utils/random.rs | 46 -------------------- lockchain-core/src/users/keystore.rs | 65 +++++++++++++++++++++++++++++ lockchain-core/src/users/mod.rs | 32 +++++--------- lockchain-core/src/users/rights.rs | 22 ++++++++++ lockchain-core/src/users/secrets.rs | 32 ++++++++++++++ lockchain-core/src/users/store.rs | 39 +++++++++++++++++ lockchain-crypto/Cargo.toml | 2 +- lockchain-files/Cargo.toml | 2 +- lockchain-http/Cargo.toml | 2 +- 21 files changed, 275 insertions(+), 273 deletions(-) delete mode 100644 lockchain-core/examples/user_registry.rs create mode 100644 lockchain-core/src/crypto/encoding.rs create mode 100644 lockchain-core/src/crypto/hashing.rs delete mode 100644 lockchain-core/src/crypto/keystore.rs create mode 100644 lockchain-core/src/crypto/random.rs delete mode 100644 lockchain-core/src/crypto/secrets.rs delete mode 100644 lockchain-core/src/crypto/store.rs delete mode 100644 lockchain-core/src/crypto/utils/encoding.rs delete mode 100644 lockchain-core/src/crypto/utils/hashing.rs delete mode 100644 lockchain-core/src/crypto/utils/mod.rs delete mode 100644 lockchain-core/src/crypto/utils/random.rs create mode 100644 lockchain-core/src/users/keystore.rs create mode 100644 lockchain-core/src/users/rights.rs create mode 100644 lockchain-core/src/users/secrets.rs create mode 100644 lockchain-core/src/users/store.rs diff --git a/Cargo.lock b/Cargo.lock index c69f091..1d3b2ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -901,7 +901,7 @@ dependencies = [ name = "lockchain-crypto" version = "0.8.1-alpha.0" dependencies = [ - "lockchain-core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lockchain-core 0.9.1-alpha.0", "miscreant 0.4.0-beta2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)", @@ -911,18 +911,18 @@ dependencies = [ name = "lockchain-files" version = "0.9.1-alpha.0" dependencies = [ - "lockchain-core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lockchain-core 0.9.1-alpha.0", ] [[package]] name = "lockchain-http" -version = "0.4.0" +version = "0.4.1-alpha.0" dependencies = [ "actix 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)", "actix-web 0.6.15 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", - "lockchain-core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lockchain-core 0.9.1-alpha.0", "serde 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -935,7 +935,7 @@ dependencies = [ "insult 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "lockchain-core 0.9.1-alpha.0", "lockchain-files 0.9.1-alpha.0", - "lockchain-http 0.4.0", + "lockchain-http 0.4.1-alpha.0", ] [[package]] diff --git a/lockchain-core/examples/user_registry.rs b/lockchain-core/examples/user_registry.rs deleted file mode 100644 index 8bb5c69..0000000 --- a/lockchain-core/examples/user_registry.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! User registry in a vault is done via the metadata store -//! -//! In addition to that the `lockchain-core ` provides some simple -//! utilities to manage Users and UserStore objects, mapping them -//! onto metadata stores. -//! -//! In this example we will define a function that takes a generic -//! Vault implementation backend (because lockchain-core doesn't -//! provide a concrete way of doing this) and registering a user -//! into it. -//! -//! Please note you can't actually _run_ this code example, because -//! no concrete type can be known. The exact same example (with a type) -//! can however be found in `lockchain-files` - - -extern crate lockchain_core as lockchain; -use lockchain::users::{User, UserStore, Access, Role}; -use lockchain::traits::Vault; -use lockchain::EncryptedBody; - -fn main() { - // register(your_vault_here, "spacekookie", "password"); -} - -/// This function takes a generic Vault which MUST implement -/// the EncryptedBody backend. This would normally be the case -/// for the `DataVault` provided by `lockchain-files` -fn register>(vault: &mut V, username: &str, password: &str) { - let me = User::register(username, password); -} - - diff --git a/lockchain-core/src/crypto/encoding.rs b/lockchain-core/src/crypto/encoding.rs new file mode 100644 index 0000000..0c49490 --- /dev/null +++ b/lockchain-core/src/crypto/encoding.rs @@ -0,0 +1,24 @@ +//! Easy to use encoding utility functions + +use base64; +use std::fmt::Write; + +/// Encode a piece of arbitary data into a bse64 string +pub fn base64_encode(data: &Vec) -> String { + return base64::encode(data); +} + +/// Decode a base64 string into arbitrary data +pub fn base64_decode(data: &String) -> Vec { + return base64::decode(data).unwrap(); +} + +/// Simply encode a byte-string as hexadecimal symbols +pub fn encode_hex(data: &str) -> String { + let mut s = String::new(); + for &byte in data.as_bytes() { + write!(&mut s, "{:X}", byte).expect("Unable to HEX encode!"); + } + + return s; +} \ No newline at end of file diff --git a/lockchain-core/src/crypto/hashing.rs b/lockchain-core/src/crypto/hashing.rs new file mode 100644 index 0000000..4a24a17 --- /dev/null +++ b/lockchain-core/src/crypto/hashing.rs @@ -0,0 +1,25 @@ +//! Hashing utility functions for various applications + +use blake2::digest::{Input, VariableOutput}; +use blake2::Blake2s; + +const BLAKE_16_LENGTH: usize = 16; + +/// Hash a value with blake2 +pub fn blake2(data: &str, salt: &str) -> [u8; BLAKE_16_LENGTH] { + let mut hasher = match Blake2s::new(BLAKE_16_LENGTH) { + Ok(res) => res, + Err(some) => panic!(some), + }; + + let to_hash = format!("{}{}", data, salt); + hasher.process(to_hash.as_bytes()); + + let mut buffer = [0u8; BLAKE_16_LENGTH]; + match hasher.variable_result(&mut buffer) { + Ok(res) => res, + Err(e) => panic!(e), + }; + + return buffer; +} diff --git a/lockchain-core/src/crypto/keystore.rs b/lockchain-core/src/crypto/keystore.rs deleted file mode 100644 index 71d7be2..0000000 --- a/lockchain-core/src/crypto/keystore.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! A keystore is a specific implementation of a metadata store -//! -//! At it's core it provides three simple functions -//! -//! - Adding keys for a user -//! - Removing keys -//! - Retrieving keys - -use traits::{AutoEncoder, Base64AutoEncoder}; -use {crypto::Key, meta::MetaDomain}; - -use std::collections::HashMap; - -#[derive(Serialize, Deserialize)] -pub struct KeyStore { - keys: HashMap, -} - -impl KeyStore { - pub fn add_key(&mut self, user: &str, key: Key) { - self.keys.insert(user.into(), key); - } - - pub fn revoke_key(&mut self, user: &str) { - self.keys.remove(user); - } -} - -impl AutoEncoder for KeyStore {} - -impl From for KeyStore { - fn from(d: MetaDomain) -> Self { - Self { - keys: d - .all() - .iter() - .map(|(k, v)| { - ( - k.clone(), - match v { - ::Payload::Text(s) => Key::decode(&String::from_base64(s)).unwrap(), - _ => unreachable!(), - }, - ) - }) - .collect(), - } - } -} - -impl From for MetaDomain { - fn from(ks: KeyStore) -> Self { - MetaDomain::new("keystore").fill( - ks.keys - .iter() - .map(|(name, key)| { - ( - name.clone(), - ::Payload::Text(key.encode().unwrap().to_base64()), - ) - }) - .collect(), - ) - } -} diff --git a/lockchain-core/src/crypto/mod.rs b/lockchain-core/src/crypto/mod.rs index ad9c0e1..ef52912 100644 --- a/lockchain-core/src/crypto/mod.rs +++ b/lockchain-core/src/crypto/mod.rs @@ -13,11 +13,10 @@ mod keys { } mod data; -mod utils; -pub use utils::*; -pub mod keystore; -pub mod store; +pub mod encoding; +pub mod random; +pub mod hashing; pub use self::data::PackedData; -pub use self::keystore::KeyStore; pub use self::keys::{Key, KeyType}; +pub use users::KeyStore; \ No newline at end of file diff --git a/lockchain-core/src/crypto/random.rs b/lockchain-core/src/crypto/random.rs new file mode 100644 index 0000000..7d31992 --- /dev/null +++ b/lockchain-core/src/crypto/random.rs @@ -0,0 +1,46 @@ +//! A small convenience wrapper around `rand` + +use rand::{thread_rng, Rng}; + +/// Generate a random number with an upper bound +pub fn number(bound: u64) -> u64 { + return thread_rng().next_u64() % bound; +} + +/// Generate a sequence of random bytes that are returned +/// as a vector. +/// +/// Can at most allocate 2048 bytes at a time +/// FIXME: That shouldn't have a limit! +pub fn bytes(length: usize) -> Vec { + let mut vec: Vec = Vec::new(); + + if length > 2048 { + return vec; + } + + let mut random_data = [0u8; 2048]; + thread_rng().fill_bytes(&mut random_data); + + for i in 0..length { + vec.push(random_data[i]); + } + + return vec; +} + +/// A small utility wraper around bcrypt to allow +/// easy password checking. +pub mod passwd { + use bcrypt::{self, DEFAULT_COST}; + + /// Create a new password, returning a hash + pub fn create(pw: &str) -> Option { + Some(bcrypt::hash(pw, DEFAULT_COST).ok()?) + } + + /// Verify a password against it's stored hash + pub fn verify(pw: &str, hash: &str) -> Option { + bcrypt::verify(pw, hash).ok() + } +} diff --git a/lockchain-core/src/crypto/secrets.rs b/lockchain-core/src/crypto/secrets.rs deleted file mode 100644 index 87e2c13..0000000 --- a/lockchain-core/src/crypto/secrets.rs +++ /dev/null @@ -1,25 +0,0 @@ - -/// Specifies the type of secret that's used to derive a vault user secret -pub enum SecretType { - /// A simple password - Plain, - /// A keyfile that allows asymetric trust operations - Keyfile, - /// Signing a user password with the id of a yubikey - Combine, -} - -/// The backing secret for user authentication -/// -/// This is _always_ in a non-recoverable form, i.e. a hash -/// and salted password. **However** it does reveal something -/// about the user setup, i.e. the type of secret used. -/// -/// Depending on what secret is used, there are other operations that -/// might be supported to verify operations. For example, a `Keyfile` -/// secret can deposit the entire public key in the `content` field, -/// then use asymmetric operations to verify operations more thoroughly. -pub struct UserSecret { - type: SecretType, - content: String, -} diff --git a/lockchain-core/src/crypto/store.rs b/lockchain-core/src/crypto/store.rs deleted file mode 100644 index f57359d..0000000 --- a/lockchain-core/src/crypto/store.rs +++ /dev/null @@ -1,15 +0,0 @@ - -use std::collections::HashMap; - -/// A thin user keystore -/// -/// It's implementation can manage multiple keys per user, of various -/// types and constrained for limited access rights. -pub struct KeyStore { - -} - -struct StoreUser { - name: String, - HashMap< -} \ No newline at end of file diff --git a/lockchain-core/src/crypto/utils/encoding.rs b/lockchain-core/src/crypto/utils/encoding.rs deleted file mode 100644 index 0c49490..0000000 --- a/lockchain-core/src/crypto/utils/encoding.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Easy to use encoding utility functions - -use base64; -use std::fmt::Write; - -/// Encode a piece of arbitary data into a bse64 string -pub fn base64_encode(data: &Vec) -> String { - return base64::encode(data); -} - -/// Decode a base64 string into arbitrary data -pub fn base64_decode(data: &String) -> Vec { - return base64::decode(data).unwrap(); -} - -/// Simply encode a byte-string as hexadecimal symbols -pub fn encode_hex(data: &str) -> String { - let mut s = String::new(); - for &byte in data.as_bytes() { - write!(&mut s, "{:X}", byte).expect("Unable to HEX encode!"); - } - - return s; -} \ No newline at end of file diff --git a/lockchain-core/src/crypto/utils/hashing.rs b/lockchain-core/src/crypto/utils/hashing.rs deleted file mode 100644 index 4a24a17..0000000 --- a/lockchain-core/src/crypto/utils/hashing.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Hashing utility functions for various applications - -use blake2::digest::{Input, VariableOutput}; -use blake2::Blake2s; - -const BLAKE_16_LENGTH: usize = 16; - -/// Hash a value with blake2 -pub fn blake2(data: &str, salt: &str) -> [u8; BLAKE_16_LENGTH] { - let mut hasher = match Blake2s::new(BLAKE_16_LENGTH) { - Ok(res) => res, - Err(some) => panic!(some), - }; - - let to_hash = format!("{}{}", data, salt); - hasher.process(to_hash.as_bytes()); - - let mut buffer = [0u8; BLAKE_16_LENGTH]; - match hasher.variable_result(&mut buffer) { - Ok(res) => res, - Err(e) => panic!(e), - }; - - return buffer; -} diff --git a/lockchain-core/src/crypto/utils/mod.rs b/lockchain-core/src/crypto/utils/mod.rs deleted file mode 100644 index d57689c..0000000 --- a/lockchain-core/src/crypto/utils/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! A collection of common cryptography utilities. - -pub mod encoding; -pub mod random; -pub mod hashing; \ No newline at end of file diff --git a/lockchain-core/src/crypto/utils/random.rs b/lockchain-core/src/crypto/utils/random.rs deleted file mode 100644 index 7d31992..0000000 --- a/lockchain-core/src/crypto/utils/random.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! A small convenience wrapper around `rand` - -use rand::{thread_rng, Rng}; - -/// Generate a random number with an upper bound -pub fn number(bound: u64) -> u64 { - return thread_rng().next_u64() % bound; -} - -/// Generate a sequence of random bytes that are returned -/// as a vector. -/// -/// Can at most allocate 2048 bytes at a time -/// FIXME: That shouldn't have a limit! -pub fn bytes(length: usize) -> Vec { - let mut vec: Vec = Vec::new(); - - if length > 2048 { - return vec; - } - - let mut random_data = [0u8; 2048]; - thread_rng().fill_bytes(&mut random_data); - - for i in 0..length { - vec.push(random_data[i]); - } - - return vec; -} - -/// A small utility wraper around bcrypt to allow -/// easy password checking. -pub mod passwd { - use bcrypt::{self, DEFAULT_COST}; - - /// Create a new password, returning a hash - pub fn create(pw: &str) -> Option { - Some(bcrypt::hash(pw, DEFAULT_COST).ok()?) - } - - /// Verify a password against it's stored hash - pub fn verify(pw: &str, hash: &str) -> Option { - bcrypt::verify(pw, hash).ok() - } -} diff --git a/lockchain-core/src/users/keystore.rs b/lockchain-core/src/users/keystore.rs new file mode 100644 index 0000000..71d7be2 --- /dev/null +++ b/lockchain-core/src/users/keystore.rs @@ -0,0 +1,65 @@ +//! A keystore is a specific implementation of a metadata store +//! +//! At it's core it provides three simple functions +//! +//! - Adding keys for a user +//! - Removing keys +//! - Retrieving keys + +use traits::{AutoEncoder, Base64AutoEncoder}; +use {crypto::Key, meta::MetaDomain}; + +use std::collections::HashMap; + +#[derive(Serialize, Deserialize)] +pub struct KeyStore { + keys: HashMap, +} + +impl KeyStore { + pub fn add_key(&mut self, user: &str, key: Key) { + self.keys.insert(user.into(), key); + } + + pub fn revoke_key(&mut self, user: &str) { + self.keys.remove(user); + } +} + +impl AutoEncoder for KeyStore {} + +impl From for KeyStore { + fn from(d: MetaDomain) -> Self { + Self { + keys: d + .all() + .iter() + .map(|(k, v)| { + ( + k.clone(), + match v { + ::Payload::Text(s) => Key::decode(&String::from_base64(s)).unwrap(), + _ => unreachable!(), + }, + ) + }) + .collect(), + } + } +} + +impl From for MetaDomain { + fn from(ks: KeyStore) -> Self { + MetaDomain::new("keystore").fill( + ks.keys + .iter() + .map(|(name, key)| { + ( + name.clone(), + ::Payload::Text(key.encode().unwrap().to_base64()), + ) + }) + .collect(), + ) + } +} diff --git a/lockchain-core/src/users/mod.rs b/lockchain-core/src/users/mod.rs index 338cc5e..e9205d1 100644 --- a/lockchain-core/src/users/mod.rs +++ b/lockchain-core/src/users/mod.rs @@ -14,10 +14,19 @@ //! data to load and store them into a metadata store. mod auth; +mod rights; mod tokens; +mod keystore; + +mod store; +mod secrets; + pub use self::auth::pam_authenticate; +pub use self::keystore::KeyStore; pub use self::tokens::Token; + pub use errors::AuthError; +pub use self::rights::{Access, Role}; use crypto::{encoding, hashing, random}; use std::collections::HashMap; @@ -26,27 +35,6 @@ use { traits::{AutoEncoder, Base64AutoEncoder}, }; -/// Specifies access to a resource -#[derive(Hash, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub enum Access { - /// Allows access to vault metadata & index files - Vault(Role), - /// Allows access to a record resource inside a vault - Record(Role, String), -} - -impl AutoEncoder for Access {} - -/// Specifies the capabilities of a user -#[derive(Hash, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub enum Role { - Reader, - Editor, - Admin, -} - -impl AutoEncoder for Role {} - /// A generic user representation /// /// A user has an identify check built in that can verify a passphrase @@ -83,7 +71,7 @@ impl User { self.pw_hash == encoding::base64_encode(&hashing::blake2(pw, &self.name).to_vec()) } /// Provides a hook to use second-factor authentication to authorise - /// + /// /// This is meant to be used with an external Yubikey pub fn second_auth_verify(&mut self) -> bool { unimplemented!() diff --git a/lockchain-core/src/users/rights.rs b/lockchain-core/src/users/rights.rs new file mode 100644 index 0000000..4404253 --- /dev/null +++ b/lockchain-core/src/users/rights.rs @@ -0,0 +1,22 @@ +use traits::AutoEncoder; + +/// Specifies access to a resource +#[derive(Hash, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub enum Access { + /// Allows access to vault metadata & index files + Vault(Role), + /// Allows access to a record resource inside a vault + Record(Role, String), +} + +impl AutoEncoder for Access {} + +/// Specifies the capabilities of a user +#[derive(Hash, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub enum Role { + Reader, + Editor, + Admin, +} + +impl AutoEncoder for Role {} diff --git a/lockchain-core/src/users/secrets.rs b/lockchain-core/src/users/secrets.rs new file mode 100644 index 0000000..8210a09 --- /dev/null +++ b/lockchain-core/src/users/secrets.rs @@ -0,0 +1,32 @@ +use traits::AutoEncoder; + +/// Specifies the type of secret that's used to derive a vault user secret +#[derive(Serialize, Deserialize)] +pub enum SecretType { + /// A simple password + Plain, + /// A keyfile that allows asymetric trust operations + Keyfile, + /// Signing a user password with the id of a yubikey + Combine, +} + +impl AutoEncoder for SecretType {} + +/// The backing secret for user authentication +/// +/// This is _always_ in a non-recoverable form, i.e. a hash +/// and salted password. **However** it does reveal something +/// about the user setup, i.e. the type of secret used. +/// +/// Depending on what secret is used, there are other operations that +/// might be supported to verify operations. For example, a `Keyfile` +/// secret can deposit the entire public key in the `content` field, +/// then use asymmetric operations to verify operations more thoroughly. +#[derive(Serialize, Deserialize)] +pub struct UserSecret { + tt: SecretType, + content: String, +} + +impl AutoEncoder for UserSecret {} diff --git a/lockchain-core/src/users/store.rs b/lockchain-core/src/users/store.rs new file mode 100644 index 0000000..d400668 --- /dev/null +++ b/lockchain-core/src/users/store.rs @@ -0,0 +1,39 @@ +use super::rights::Access; +use super::secrets::SecretType; +use crypto::Key; +use std::collections::HashMap; + +/// A thin user keystore +/// +/// It's implementation can manage multiple keys per user, of various +/// types and constrained for limited access rights. +pub struct KeyStore { + store: HashMap, +} + +struct StoreUser { + name: String, + keys: HashMap, +} + +impl KeyStore { + /// Create a new, empty keystore + /// + /// This is most likely *not* what you want. Instead, transform + /// a `MetaData` object into a keystore. + pub fn new() -> Self { + Self { + store: HashMap::new(), + } + } + + pub fn add_user(&mut self) {} + + pub fn rm_user(&mut self) {} + + pub fn add_key(&mut self, user: String, k: Key, access: Access) {} + + pub fn get_key(&self, user: String, access: Access) -> &Key { + unimplemented!() + } +} diff --git a/lockchain-crypto/Cargo.toml b/lockchain-crypto/Cargo.toml index 656f60f..10a69ae 100644 --- a/lockchain-crypto/Cargo.toml +++ b/lockchain-crypto/Cargo.toml @@ -9,7 +9,7 @@ version = "0.8.1-alpha.0" authors = ["Katharina Fey "] [dependencies] -lockchain-core = { version = "0.9.0", path = "../lockchain-core" } +lockchain-core = { version = "0.9.1-alpha.0", path = "../lockchain-core" } serde_derive = "1.0" serde = "1.0" diff --git a/lockchain-files/Cargo.toml b/lockchain-files/Cargo.toml index 8656fbf..e63455a 100644 --- a/lockchain-files/Cargo.toml +++ b/lockchain-files/Cargo.toml @@ -10,4 +10,4 @@ readme = "README.md" license = "MIT/X11 OR Apache-2.0" [dependencies] -lockchain-core = { version = "0.9.0" } # lockchain-core = { version = "0.9.1-alpha.0", path = "../lockchain-core" } +lockchain-core = { version = "0.9.1-alpha.0", path = "../lockchain-core" } diff --git a/lockchain-http/Cargo.toml b/lockchain-http/Cargo.toml index 128c9c9..9150033 100644 --- a/lockchain-http/Cargo.toml +++ b/lockchain-http/Cargo.toml @@ -10,7 +10,7 @@ readme = "README.md" license = "MIT/X11 OR Apache-2.0" [dependencies] -lockchain-core = { version = "0.9.0" }# lockchain-core = { version = "0.9.1-alpha.0", path = "../lockchain-core" } +lockchain-core = { version = "0.9.1-alpha.0", path = "../lockchain-core" } env_logger = "0.5" # Serialisation stack -- cgit v1.2.3