Initial re-factor
This commit is contained in:
commit
29ad0d721d
14 changed files with 743 additions and 0 deletions
97
src/claims.rs
Normal file
97
src/claims.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use base64::{decode, encode_config, URL_SAFE};
|
||||
use Component;
|
||||
use error::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use serde_json::value::{from_value, to_value, Map, Value};
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct Claims {
|
||||
pub reg: Registered,
|
||||
pub private: Value
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Registered {
|
||||
pub iss: Option<String>,
|
||||
pub sub: Option<String>,
|
||||
pub aud: Option<String>,
|
||||
pub exp: Option<u64>,
|
||||
pub nbf: Option<u64>,
|
||||
pub iat: Option<u64>,
|
||||
pub jti: Option<String>,
|
||||
}
|
||||
|
||||
/// JWT Claims. Registered claims are directly accessible via the `Registered`
|
||||
/// struct embedded, while private fields are a map that contains `Json`
|
||||
/// values.
|
||||
impl Claims {
|
||||
pub fn new(reg: Registered, private: Value) -> Claims {
|
||||
Claims {
|
||||
reg: reg,
|
||||
private: private
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Claims {
|
||||
fn from_base64(raw: &str) -> Result<Claims, Error> {
|
||||
let data = try!(decode(raw));
|
||||
let reg_claims: Registered = try!(serde_json::from_slice(&data));
|
||||
|
||||
let pri_claims: Value = try!(serde_json::from_slice(&data));
|
||||
|
||||
|
||||
Ok(Claims{
|
||||
reg: reg_claims,
|
||||
private: pri_claims
|
||||
})
|
||||
}
|
||||
|
||||
fn to_base64(&self) -> Result<String, Error> {
|
||||
let mut value = try!(serde_json::to_value(&self.reg));
|
||||
let mut obj_value = &value.as_object_mut().unwrap();
|
||||
// TODO iterate private claims and add to JSON Map
|
||||
//let mut pri_value = self.private.as_object_mut().unwrap();
|
||||
|
||||
//obj_value.extend(pri_value.into_iter());
|
||||
|
||||
let s = try!(serde_json::to_string(&obj_value));
|
||||
let enc = encode_config((&*s).as_bytes(), URL_SAFE);
|
||||
Ok(enc)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::default::Default;
|
||||
use claims::{Claims, Registered};
|
||||
use Component;
|
||||
|
||||
#[test]
|
||||
fn from_base64() {
|
||||
let enc = "ew0KICAiaXNzIjogIm1pa2t5YW5nLmNvbSIsDQogICJleHAiOiAxMzAyMzE5MTAwLA0KICAibmFtZSI6ICJNaWNoYWVsIFlhbmciLA0KICAiYWRtaW4iOiB0cnVlDQp9";
|
||||
let claims = Claims::from_base64(enc).unwrap();
|
||||
|
||||
assert_eq!(claims.reg.iss.unwrap(), "mikkyang.com");
|
||||
assert_eq!(claims.reg.exp.unwrap(), 1302319100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_types() {
|
||||
let enc = "ew0KICAiaXNzIjogIm1pa2t5YW5nLmNvbSIsDQogICJleHAiOiAxMzAyMzE5MTAwLA0KICAibmFtZSI6ICJNaWNoYWVsIFlhbmciLA0KICAiYWRtaW4iOiB0cnVlDQp9";
|
||||
let claims = Registered::from_base64(enc).unwrap();
|
||||
|
||||
assert_eq!(claims.iss.unwrap(), "mikkyang.com");
|
||||
assert_eq!(claims.exp.unwrap(), 1302319100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip() {
|
||||
let mut claims: Claims = Default::default();
|
||||
claims.reg.iss = Some("mikkyang.com".into());
|
||||
claims.reg.exp = Some(1302319100);
|
||||
let enc = claims.to_base64().unwrap();
|
||||
assert_eq!(claims, Claims::from_base64(&*enc).unwrap());
|
||||
}
|
||||
}
|
47
src/crypt.rs
Normal file
47
src/crypt.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
use base64::{decode, encode_config, URL_SAFE};
|
||||
use openssl::hash::MessageDigest;
|
||||
use openssl::memcmp;
|
||||
use openssl::pkey::PKey;
|
||||
use openssl::rsa::Rsa;
|
||||
use openssl::sign::{Signer, Verifier};
|
||||
|
||||
pub fn sign(data: &str, key: &[u8], digest: MessageDigest) -> String {
|
||||
let secret_key = PKey::hmac(key).unwrap();
|
||||
|
||||
let mut signer = Signer::new(digest, &secret_key).unwrap();
|
||||
signer.update(data.as_bytes()).unwrap();
|
||||
|
||||
let mac = signer.finish().unwrap();
|
||||
encode_config(&mac, URL_SAFE)
|
||||
}
|
||||
|
||||
pub fn sign_rsa(data: &str, key: &[u8], digest: MessageDigest) -> String {
|
||||
let private_key = Rsa::private_key_from_pem(key).unwrap();
|
||||
let pkey = PKey::from_rsa(private_key).unwrap();
|
||||
|
||||
let mut signer = Signer::new(digest, &pkey).unwrap();
|
||||
signer.update(data.as_bytes()).unwrap();
|
||||
let sig = signer.finish().unwrap();
|
||||
encode_config(&sig, URL_SAFE)
|
||||
}
|
||||
|
||||
pub fn verify(target: &str, data: &str, key: &[u8], digest: MessageDigest) -> bool {
|
||||
let target_bytes: Vec<u8> = decode(target).unwrap();
|
||||
let secret_key = PKey::hmac(key).unwrap();
|
||||
|
||||
let mut signer = Signer::new(digest, &secret_key).unwrap();
|
||||
signer.update(data.as_bytes()).unwrap();
|
||||
|
||||
let mac = signer.finish().unwrap();
|
||||
|
||||
memcmp::eq(&mac, &target_bytes)
|
||||
}
|
||||
|
||||
pub fn verify_rsa(signature: &str, data: &str, key: &[u8], digest: MessageDigest) -> bool {
|
||||
let signature_bytes: Vec<u8> = decode(signature).unwrap();
|
||||
let public_key = Rsa::public_key_from_pem(key).unwrap();
|
||||
let pkey = PKey::from_rsa(public_key).unwrap();
|
||||
let mut verifier = Verifier::new(digest, &pkey).unwrap();
|
||||
verifier.update(data.as_bytes()).unwrap();
|
||||
verifier.finish(&signature_bytes).unwrap()
|
||||
}
|
23
src/error.rs
Normal file
23
src/error.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use base64::Base64Error;
|
||||
use serde_json;
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Format,
|
||||
Utf8(FromUtf8Error),
|
||||
Base64(Base64Error),
|
||||
JSON(serde_json::Error),
|
||||
}
|
||||
|
||||
macro_rules! error_wrap {
|
||||
($f: ty, $e: expr) => {
|
||||
impl From<$f> for Error {
|
||||
fn from(f: $f) -> Error { $e(f) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error_wrap!(FromUtf8Error, Error::Utf8);
|
||||
error_wrap!(Base64Error, Error::Base64);
|
||||
error_wrap!(serde_json::Error, Error::JSON);
|
74
src/header.rs
Normal file
74
src/header.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use std::default::Default;
|
||||
use Header;
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DefaultHeader {
|
||||
pub typ: Option<HeaderType>,
|
||||
pub kid: Option<String>,
|
||||
pub alg: Algorithm,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum HeaderType {
|
||||
JWT,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Algorithm {
|
||||
HS256,
|
||||
HS384,
|
||||
HS512,
|
||||
RS256,
|
||||
RS384,
|
||||
RS512
|
||||
}
|
||||
|
||||
impl Default for DefaultHeader {
|
||||
fn default() -> DefaultHeader {
|
||||
DefaultHeader {
|
||||
typ: Some(HeaderType::JWT),
|
||||
kid: None,
|
||||
alg: Algorithm::HS256,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for DefaultHeader {
|
||||
fn alg(&self) -> &Algorithm {
|
||||
&(self.alg)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use Component;
|
||||
use header::{
|
||||
Algorithm,
|
||||
DefaultHeader,
|
||||
HeaderType,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn from_base64() {
|
||||
let enc = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
|
||||
let header = DefaultHeader::from_base64(enc).unwrap();
|
||||
|
||||
assert_eq!(header.typ.unwrap(), HeaderType::JWT);
|
||||
assert_eq!(header.alg, Algorithm::HS256);
|
||||
|
||||
|
||||
let enc = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFLU0YzZyJ9";
|
||||
let header = DefaultHeader::from_base64(enc).unwrap();
|
||||
|
||||
assert_eq!(header.kid.unwrap(), "1KSF3g".to_string());
|
||||
assert_eq!(header.alg, Algorithm::RS256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip() {
|
||||
let header: DefaultHeader = Default::default();
|
||||
let enc = Component::to_base64(&header).unwrap();
|
||||
assert_eq!(header, DefaultHeader::from_base64(&*enc).unwrap());
|
||||
}
|
||||
}
|
264
src/lib.rs
Normal file
264
src/lib.rs
Normal file
|
@ -0,0 +1,264 @@
|
|||
extern crate base64;
|
||||
extern crate openssl;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
|
||||
use base64::{decode, encode_config, URL_SAFE};
|
||||
use openssl::hash::MessageDigest;
|
||||
use serde::{Serialize, Deserialize};
|
||||
pub use error::Error;
|
||||
pub use header::DefaultHeader;
|
||||
pub use header::Algorithm;
|
||||
pub use claims::Claims;
|
||||
pub use claims::Registered;
|
||||
|
||||
pub mod error;
|
||||
pub mod header;
|
||||
pub mod claims;
|
||||
mod crypt;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Token<H, C>
|
||||
where H: Component, C: Component {
|
||||
raw: Option<String>,
|
||||
pub header: H,
|
||||
pub claims: C,
|
||||
}
|
||||
|
||||
pub trait Header {
|
||||
fn alg(&self) -> &header::Algorithm;
|
||||
}
|
||||
|
||||
pub trait Component: Sized {
|
||||
fn from_base64(raw: &str) -> Result<Self, Error>;
|
||||
fn to_base64(&self) -> Result<String, Error>;
|
||||
}
|
||||
|
||||
impl<T> Component for T
|
||||
where T: Serialize + Deserialize + Sized {
|
||||
|
||||
/// Parse from a string.
|
||||
fn from_base64(raw: &str) -> Result<T, Error> {
|
||||
let data = try!(decode(raw));
|
||||
let s = try!(String::from_utf8(data));
|
||||
Ok(try!(serde_json::from_str(&*s)))
|
||||
}
|
||||
|
||||
/// Encode to a string.
|
||||
fn to_base64(&self) -> Result<String, Error> {
|
||||
let s = try!(serde_json::to_string(&self));
|
||||
let enc = encode_config((&*s).as_bytes(), URL_SAFE);
|
||||
Ok(enc)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, C> Token<H, C>
|
||||
where H: Component + Header, C: Component {
|
||||
pub fn new(header: H, claims: C) -> Token<H, C> {
|
||||
Token {
|
||||
raw: None,
|
||||
header: header,
|
||||
claims: claims,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a token from a string.
|
||||
pub fn parse(raw: &str) -> Result<Token<H, C>, Error> {
|
||||
let pieces: Vec<_> = raw.split('.').collect();
|
||||
|
||||
Ok(Token {
|
||||
raw: Some(raw.into()),
|
||||
header: try!(Component::from_base64(pieces[0])),
|
||||
claims: try!(Component::from_base64(pieces[1])),
|
||||
})
|
||||
}
|
||||
|
||||
/// Verify a from_base64 token with a key and the token's specific algorithm
|
||||
pub fn verify(&self, key: &[u8]) -> bool {
|
||||
match self.header.alg() {
|
||||
&Algorithm::HS256 => self.verify_hmac(key, MessageDigest::sha256()),
|
||||
&Algorithm::HS384 => self.verify_hmac(key, MessageDigest::sha384()),
|
||||
&Algorithm::HS512 => self.verify_hmac(key, MessageDigest::sha512()),
|
||||
&Algorithm::RS256 => self.verify_rsa(key, MessageDigest::sha256()),
|
||||
&Algorithm::RS384 => self.verify_rsa(key, MessageDigest::sha384()),
|
||||
&Algorithm::RS512 => self.verify_rsa(key, MessageDigest::sha512()),
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_hmac(&self, key: &[u8], digest: MessageDigest) -> bool {
|
||||
let raw = match self.raw {
|
||||
Some(ref s) => s,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let pieces: Vec<_> = raw.rsplitn(2, '.').collect();
|
||||
let sig = pieces[0];
|
||||
let data = pieces[1];
|
||||
|
||||
crypt::verify(sig, data, key, digest)
|
||||
}
|
||||
|
||||
fn verify_rsa(&self, key: &[u8], digest: MessageDigest) -> bool {
|
||||
let raw = match self.raw {
|
||||
Some(ref s) => s,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let pieces: Vec<_> = raw.rsplitn(2, '.').collect();
|
||||
let sig = pieces[0];
|
||||
let data = pieces[1];
|
||||
|
||||
crypt::verify_rsa(sig, data, key, digest)
|
||||
}
|
||||
|
||||
/// Generate the signed token from a key and the specific algorithm
|
||||
pub fn signed(&self, key: &[u8]) -> Result<String, Error> {
|
||||
match self.header.alg() {
|
||||
&Algorithm::HS256 => self.signed_hmac(key, MessageDigest::sha256()),
|
||||
&Algorithm::HS384 => self.signed_hmac(key, MessageDigest::sha384()),
|
||||
&Algorithm::HS512 => self.signed_hmac(key, MessageDigest::sha512()),
|
||||
&Algorithm::RS256 => self.signed_rsa(key, MessageDigest::sha256()),
|
||||
&Algorithm::RS384 => self.signed_rsa(key, MessageDigest::sha384()),
|
||||
&Algorithm::RS512 => self.signed_rsa(key, MessageDigest::sha512()),
|
||||
}
|
||||
}
|
||||
|
||||
fn signed_hmac(&self, key: &[u8], digest: MessageDigest) -> Result<String, Error> {
|
||||
let header = try!(Component::to_base64(&self.header));
|
||||
let claims = try!(self.claims.to_base64());
|
||||
let data = format!("{}.{}", header, claims);
|
||||
|
||||
let sig = crypt::sign(&*data, key, digest);
|
||||
Ok(format!("{}.{}", data, sig))
|
||||
}
|
||||
|
||||
fn signed_rsa(&self, key: &[u8], digest: MessageDigest) -> Result<String, Error> {
|
||||
let header = try!(Component::to_base64(&self.header));
|
||||
let claims = try!(self.claims.to_base64());
|
||||
let data = format!("{}.{}", header, claims);
|
||||
|
||||
let sig = crypt::sign_rsa(&*data, key, digest);
|
||||
Ok(format!("{}.{}", data, sig))
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, C> PartialEq for Token<H, C>
|
||||
where H: Component + PartialEq, C: Component + PartialEq{
|
||||
fn eq(&self, other: &Token<H, C>) -> bool {
|
||||
self.header == other.header &&
|
||||
self.claims == other.claims
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crypt::{
|
||||
sign,
|
||||
sign_rsa,
|
||||
verify,
|
||||
verify_rsa
|
||||
};
|
||||
use Claims;
|
||||
use Token;
|
||||
use header::Algorithm::{HS256,RS512};
|
||||
use header::DefaultHeader;
|
||||
use std::io::{Error, Read};
|
||||
use std::fs::File;
|
||||
use openssl::hash::MessageDigest;
|
||||
|
||||
#[test]
|
||||
pub fn sign_data() {
|
||||
let header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
|
||||
let claims = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9";
|
||||
let real_sig = "TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
|
||||
let data = format!("{}.{}", header, claims);
|
||||
|
||||
let sig = sign(&*data, "secret".as_bytes(), MessageDigest::sha256());
|
||||
|
||||
assert_eq!(sig, real_sig);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn sign_data_rsa() {
|
||||
let header = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9";
|
||||
let claims = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9";
|
||||
let real_sig = "nXdpIkFQYZXZ0VlJjHmAc5/aewHCCJpT5jP1fpexUCF/9m3NxlC7uYNXAl6NKno520oh9wVT4VV/vmPeEin7BnnoIJNPcImWcUzkYpLTrDBntiF9HCuqFaniuEVzlf8dVlRJgo8QxhmUZEjyDFjPZXZxPlPV1LD6hrtItxMKZbh1qoNY3OL7Mwo+WuSRQ0mmKj+/y3weAmx/9EaTLY639uD8+o5iZxIIf85U4e55Wdp+C9FJ4RxyHpjgoG8p87IbChfleSdWcZL3NZuxjRCHVWgS1uYG0I+LqBWpWyXnJ1zk6+w4tfxOYpZFMOIyq4tY2mxJQ78Kvcu8bTO7UdI7iA";
|
||||
let data = format!("{}.{}", header, claims);
|
||||
|
||||
let key = load_key("./examples/privateKey.pem").unwrap();
|
||||
|
||||
let sig = sign_rsa(&*data, key.as_bytes(), MessageDigest::sha256());
|
||||
|
||||
assert_eq!(sig.trim(), real_sig);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn verify_data() {
|
||||
let header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
|
||||
let claims = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9";
|
||||
let target = "TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
|
||||
let data = format!("{}.{}", header, claims);
|
||||
|
||||
assert!(verify(target, &*data, "secret".as_bytes(), MessageDigest::sha256()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn verify_data_rsa() {
|
||||
let header = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9";
|
||||
let claims = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9";
|
||||
let real_sig = "nXdpIkFQYZXZ0VlJjHmAc5/aewHCCJpT5jP1fpexUCF/9m3NxlC7uYNXAl6NKno520oh9wVT4VV/vmPeEin7BnnoIJNPcImWcUzkYpLTrDBntiF9HCuqFaniuEVzlf8dVlRJgo8QxhmUZEjyDFjPZXZxPlPV1LD6hrtItxMKZbh1qoNY3OL7Mwo+WuSRQ0mmKj+/y3weAmx/9EaTLY639uD8+o5iZxIIf85U4e55Wdp+C9FJ4RxyHpjgoG8p87IbChfleSdWcZL3NZuxjRCHVWgS1uYG0I+LqBWpWyXnJ1zk6+w4tfxOYpZFMOIyq4tY2mxJQ78Kvcu8bTO7UdI7iA";
|
||||
let data = format!("{}.{}", header, claims);
|
||||
|
||||
let key = load_key("./examples/publicKey.pub").unwrap();
|
||||
assert!(verify_rsa(&real_sig, &*data, key.as_bytes(), MessageDigest::sha256()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn raw_data() {
|
||||
let raw = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
|
||||
let token = Token::<DefaultHeader, Claims>::parse(raw).unwrap();
|
||||
|
||||
{
|
||||
assert_eq!(token.header.alg, HS256);
|
||||
}
|
||||
assert!(token.verify("secret".as_bytes()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn roundtrip() {
|
||||
let token: Token<DefaultHeader, Claims> = Default::default();
|
||||
let key = "secret".as_bytes();
|
||||
let raw = token.signed(key).unwrap();
|
||||
let same = Token::parse(&*raw).unwrap();
|
||||
|
||||
assert_eq!(token, same);
|
||||
assert!(same.verify(key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn roundtrip_rsa() {
|
||||
let token: Token<DefaultHeader, Claims> = Token {
|
||||
header: DefaultHeader {
|
||||
alg: RS512,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let private_key = load_key("./examples/privateKey.pem").unwrap();
|
||||
let raw = token.signed(private_key.as_bytes()).unwrap();
|
||||
let same = Token::parse(&*raw).unwrap();
|
||||
|
||||
assert_eq!(token, same);
|
||||
let public_key = load_key("./examples/publicKey.pub").unwrap();
|
||||
assert!(same.verify(public_key.as_bytes()));
|
||||
}
|
||||
|
||||
fn load_key(keypath: &str) -> Result<String, Error> {
|
||||
let mut key_file = try!(File::open(keypath));
|
||||
let mut key = String::new();
|
||||
try!(key_file.read_to_string(&mut key));
|
||||
Ok(key)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue