feat: initial commit
Signed-off-by: Louis Vallat <contact@louis-vallat.dev>
This commit is contained in:
commit
4dd87bf40b
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/target
|
||||||
|
.idea
|
||||||
|
*.key
|
||||||
|
*.passbolt
|
||||||
|
.DS_Store
|
2693
Cargo.lock
generated
Normal file
2693
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "passbolt_rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
base64 = "0.22.1"
|
||||||
|
pgp = "0.13.1"
|
||||||
|
rand = "0.8.5"
|
||||||
|
reqwest = { version = "0.12.5", features = ["blocking", "json", "cookies"] }
|
||||||
|
serde = { version = "1.0.207", features = ["derive"] }
|
||||||
|
serde_json = "1.0.124"
|
||||||
|
uuid = { version = "1.10.0", features = ["v4"] }
|
311
src/main.rs
Normal file
311
src/main.rs
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
use base64::prelude::BASE64_STANDARD;
|
||||||
|
use base64::Engine;
|
||||||
|
use core::str;
|
||||||
|
use pgp::cleartext::CleartextSignedMessage;
|
||||||
|
use pgp::crypto::hash::HashAlgorithm;
|
||||||
|
use pgp::{
|
||||||
|
crypto::sym::SymmetricKeyAlgorithm,
|
||||||
|
types::{KeyTrait, SecretKeyTrait},
|
||||||
|
ArmorOptions, Deserializable, Message, SignedPublicKey, SignedSecretKey,
|
||||||
|
};
|
||||||
|
use rand::prelude::ThreadRng;
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
use std::{env, fs};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AccountKit {
|
||||||
|
domain: String,
|
||||||
|
user_id: String,
|
||||||
|
username: String,
|
||||||
|
first_name: String,
|
||||||
|
last_name: String,
|
||||||
|
user_private_armored_key: String,
|
||||||
|
user_public_armored_key: String,
|
||||||
|
server_public_armored_key: String,
|
||||||
|
security_token: SecurityToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct SecurityToken {
|
||||||
|
code: String,
|
||||||
|
color: String,
|
||||||
|
textcolor: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct KeyBody {
|
||||||
|
body: Key,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ChallengeBody {
|
||||||
|
body: Challenge,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Key {
|
||||||
|
keydata: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Challenge {
|
||||||
|
challenge: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ChallengeResponse {
|
||||||
|
version: String,
|
||||||
|
domain: String,
|
||||||
|
verify_token: String,
|
||||||
|
access_token: String,
|
||||||
|
refresh_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_account_kit(path: &str) -> CleartextSignedMessage {
|
||||||
|
let account_kit_bytes = fs::read(path).expect("Could not read account kit file");
|
||||||
|
let account_kit_content = BASE64_STANDARD
|
||||||
|
.decode(account_kit_bytes)
|
||||||
|
.expect("Could not base64 decode account kit file");
|
||||||
|
let (msg, _headers_msg) = CleartextSignedMessage::from_string(
|
||||||
|
&String::from_utf8(account_kit_content).expect("Account kit is not UTF8 decodable."),
|
||||||
|
)
|
||||||
|
.expect("Could not parse account kit file as signed PGP message");
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
|
||||||
|
fn panic_on_keypair_issue(private_key: &SignedSecretKey, public_key: &SignedPublicKey) {
|
||||||
|
if private_key.verify().is_err() {
|
||||||
|
panic!("Could not verify user's private key.");
|
||||||
|
}
|
||||||
|
if !private_key.is_signing_key() {
|
||||||
|
panic!("User's private key is not a signing key.");
|
||||||
|
}
|
||||||
|
if !public_key.is_encryption_key() {
|
||||||
|
panic!("User's public key is not encryption key.");
|
||||||
|
}
|
||||||
|
if private_key.public_key().fingerprint() != public_key.fingerprint() {
|
||||||
|
panic!("Generated public key and given public keys don't have matching fingerprint.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login(
|
||||||
|
mut rng: &mut ThreadRng,
|
||||||
|
account_kit: &AccountKit,
|
||||||
|
client: &Client,
|
||||||
|
user_private_key: &SignedSecretKey,
|
||||||
|
user_key_passphrase: &String,
|
||||||
|
server_public_key: &SignedPublicKey,
|
||||||
|
) -> ChallengeResponse {
|
||||||
|
let challenge_token = Uuid::new_v4().to_string();
|
||||||
|
let challenge_expiration_date = SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.expect("Couldn't get time since epoch.")
|
||||||
|
+ Duration::from_secs(120);
|
||||||
|
let challenge = json!({
|
||||||
|
"version": "1.0.0",
|
||||||
|
"domain": account_kit.domain,
|
||||||
|
"verify_token": challenge_token,
|
||||||
|
"verify_token_expiry": challenge_expiration_date.as_secs(),
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
let challenge_message = Message::new_literal("", &challenge)
|
||||||
|
.sign(
|
||||||
|
&user_private_key,
|
||||||
|
|| user_key_passphrase.clone(),
|
||||||
|
HashAlgorithm::SHA3_512,
|
||||||
|
)
|
||||||
|
.expect("Could not sign challenge message.")
|
||||||
|
.encrypt_to_keys(
|
||||||
|
&mut rng,
|
||||||
|
SymmetricKeyAlgorithm::AES128,
|
||||||
|
&[&server_public_key],
|
||||||
|
)
|
||||||
|
.expect("Could not encrypt challenge message.");
|
||||||
|
let armored_challenge = challenge_message
|
||||||
|
.to_armored_string(ArmorOptions::default())
|
||||||
|
.expect("Could not armor encrypted challenge message.");
|
||||||
|
let login_req = client
|
||||||
|
.post(format!("{}/auth/jwt/login.json", account_kit.domain))
|
||||||
|
.json(&json!({
|
||||||
|
"user_id": account_kit.user_id,
|
||||||
|
"challenge": armored_challenge,
|
||||||
|
}));
|
||||||
|
let login_res = login_req.send().expect("Could not send login request.");
|
||||||
|
let challenge_response: ChallengeBody = login_res
|
||||||
|
.json()
|
||||||
|
.expect("Could not de-serialize server response.");
|
||||||
|
|
||||||
|
let (armored_challenge_response, _headers) =
|
||||||
|
Message::from_string(&challenge_response.body.challenge)
|
||||||
|
.expect("Couldn't load armored challenge response.");
|
||||||
|
let (decrypted_challenge_response, _key_ids) = armored_challenge_response
|
||||||
|
.decrypt(|| user_key_passphrase.clone(), &[&user_private_key])
|
||||||
|
.expect("Could not decrypt challenge response.");
|
||||||
|
let literal_data_bytes = decrypted_challenge_response
|
||||||
|
.get_literal()
|
||||||
|
.expect("Could not get literal data from decrypted challenge.")
|
||||||
|
.data();
|
||||||
|
let literal_data = str::from_utf8(literal_data_bytes)
|
||||||
|
.expect("Could not turn challenge response bytes to str.");
|
||||||
|
let challenge_response: ChallengeResponse = serde_json::from_str(literal_data)
|
||||||
|
.expect("Could not de-serialize challenge response to struct.");
|
||||||
|
if challenge_response.version != "1.0.0" {
|
||||||
|
panic!("Challenge version mismatch.");
|
||||||
|
}
|
||||||
|
if challenge_response.domain != format!("{}/", account_kit.domain) {
|
||||||
|
panic!("Challenge response domain doesn't match the hostname we have.");
|
||||||
|
}
|
||||||
|
if challenge_response.verify_token != challenge_token {
|
||||||
|
panic!("Challenge response token doesn't match the one we sent.");
|
||||||
|
}
|
||||||
|
challenge_response
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_access_token(
|
||||||
|
account_kit: &AccountKit,
|
||||||
|
client: &Client,
|
||||||
|
mut challenge_response: ChallengeResponse,
|
||||||
|
) -> ChallengeResponse {
|
||||||
|
let refresh_req = client
|
||||||
|
.post(format!("{}/auth/jwt/refresh.json", account_kit.domain))
|
||||||
|
.json(&json!({
|
||||||
|
"user_id": account_kit.user_id,
|
||||||
|
"refresh_token": challenge_response.refresh_token,
|
||||||
|
}));
|
||||||
|
let refresh_res = refresh_req.send().expect("Could not send refresh request.");
|
||||||
|
{
|
||||||
|
let refresh_cookie = refresh_res
|
||||||
|
.cookies()
|
||||||
|
.find(|cookie| cookie.name() == "refresh_token")
|
||||||
|
.expect("Couldn't find new refresh token in cookies.");
|
||||||
|
challenge_response.refresh_token = refresh_cookie.value().to_string();
|
||||||
|
}
|
||||||
|
let refresh_body = refresh_res
|
||||||
|
.json::<Value>()
|
||||||
|
.expect("Couldn't de-serialize refresh response.");
|
||||||
|
let refresh_body_value = refresh_body
|
||||||
|
.get("body")
|
||||||
|
.expect("Couldn't get body for response.");
|
||||||
|
let new_access_token = refresh_body_value
|
||||||
|
.get("access_token")
|
||||||
|
.expect("Couldn't get access_token from refresh response body.")
|
||||||
|
.as_str()
|
||||||
|
.expect("Could not convert access_token to str.");
|
||||||
|
if new_access_token == challenge_response.access_token {
|
||||||
|
println!("Access token didn't change during refresh.");
|
||||||
|
} else {
|
||||||
|
challenge_response.access_token = new_access_token.to_string();
|
||||||
|
}
|
||||||
|
challenge_response
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Init
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() < 2 {
|
||||||
|
panic!("Please provide private key passphrase as argument.")
|
||||||
|
}
|
||||||
|
let key_passphrase = &args[1];
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let client = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(true)
|
||||||
|
.build()
|
||||||
|
.expect("Could not build HTTP client.");
|
||||||
|
|
||||||
|
// Read account kit
|
||||||
|
let account_kit_message = read_account_kit("./account-kit.passbolt");
|
||||||
|
let account_kit: AccountKit = serde_json::from_str(account_kit_message.text())
|
||||||
|
.expect("Could not deserialize account kit file.");
|
||||||
|
println!(
|
||||||
|
"Found account kit for {} {} <{}>.",
|
||||||
|
account_kit.first_name, account_kit.last_name, account_kit.username
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"Security token for said account is {} with font color {} and background color {}.",
|
||||||
|
account_kit.security_token.code,
|
||||||
|
account_kit.security_token.textcolor,
|
||||||
|
account_kit.security_token.color
|
||||||
|
);
|
||||||
|
let (account_kit_server_public_key, _headers) =
|
||||||
|
SignedPublicKey::from_string(&account_kit.server_public_armored_key)
|
||||||
|
.expect("Could not parse account kit server public key.");
|
||||||
|
let (user_public_key, _headers) =
|
||||||
|
SignedPublicKey::from_string(&account_kit.user_public_armored_key)
|
||||||
|
.expect("Could not parse account kit server public key.");
|
||||||
|
let (user_private_key, _headers) =
|
||||||
|
SignedSecretKey::from_string(&account_kit.user_private_armored_key)
|
||||||
|
.expect("Could not get user's signed key from account kit.");
|
||||||
|
if account_kit_message.verify(&user_public_key).is_err() {
|
||||||
|
panic!("Could not verify account kit message against public key in said kit.");
|
||||||
|
}
|
||||||
|
panic_on_keypair_issue(&user_private_key, &user_public_key);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Get server's public key
|
||||||
|
let verify_res = client
|
||||||
|
.get(format!("{}/auth/verify.json", account_kit.domain))
|
||||||
|
.send()
|
||||||
|
.expect("Could not send verify request.");
|
||||||
|
let body: KeyBody = verify_res
|
||||||
|
.json()
|
||||||
|
.expect("Could not de-serialize verify request body.");
|
||||||
|
let (server_public_key, _headers) =
|
||||||
|
SignedPublicKey::from_string(&body.body.keydata).expect("Couldn't parse public key.");
|
||||||
|
if server_public_key.fingerprint() != account_kit_server_public_key.fingerprint() {
|
||||||
|
panic!("Server public key fingerprint doesn't match its fingerprint in account kit.");
|
||||||
|
}
|
||||||
|
if !server_public_key.is_encryption_key() {
|
||||||
|
panic!("Server public key is not encryption key.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log in
|
||||||
|
let challenge_response = login(
|
||||||
|
&mut rng,
|
||||||
|
&account_kit,
|
||||||
|
&client,
|
||||||
|
&user_private_key,
|
||||||
|
key_passphrase,
|
||||||
|
&server_public_key,
|
||||||
|
);
|
||||||
|
println!("Access token: {:?}", challenge_response.access_token);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Refresh access tokens
|
||||||
|
let challenge_response = refresh_access_token(&account_kit, &client, challenge_response);
|
||||||
|
println!(
|
||||||
|
"Refreshed access token: {:?}",
|
||||||
|
challenge_response.access_token
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Get information about us
|
||||||
|
let me_res = client
|
||||||
|
.get(format!("{}/users/me.json", account_kit.domain))
|
||||||
|
.bearer_auth(&challenge_response.access_token)
|
||||||
|
.send()
|
||||||
|
.expect("Could not send users request.");
|
||||||
|
println!(
|
||||||
|
"Me: {:#?}",
|
||||||
|
me_res
|
||||||
|
.json::<Value>()
|
||||||
|
.expect("Could not de-serialize user data.")
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Log out
|
||||||
|
let logout_req = client
|
||||||
|
.post(format!("{}/auth/jwt/logout.json", account_kit.domain))
|
||||||
|
.bearer_auth(&challenge_response.access_token)
|
||||||
|
.json(&json!({
|
||||||
|
"refresh_token": challenge_response.refresh_token,
|
||||||
|
}));
|
||||||
|
let logout_res = logout_req.send().expect("Could not send logout request.");
|
||||||
|
if logout_res.status() != StatusCode::OK {
|
||||||
|
panic!("Could not log out.")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user