Handle openssl in rust. Working getting payments

This commit is contained in:
Yuri Iozzelli 2020-10-11 22:21:18 +02:00
parent 76bbb96490
commit dfa69c9204
3 changed files with 409 additions and 12 deletions

118
Cargo.lock generated
View File

@ -6,23 +6,54 @@ version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b"
[[package]]
name = "arrayref"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
[[package]]
name = "arrayvec"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "base64"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "blake2b_simd"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a"
dependencies = [
"arrayref",
"arrayvec",
"constant_time_eq",
]
[[package]]
name = "bunqledger"
version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"confy",
"dotenv",
"isahc",
"openssl",
@ -48,6 +79,23 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "confy"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2913470204e9e8498a0f31f17f90a0de801ae92c8c5ac18c49af4819e6786697"
dependencies = [
"directories",
"serde",
"toml",
]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "crossbeam-channel"
version = "0.4.4"
@ -100,6 +148,27 @@ dependencies = [
"winapi",
]
[[package]]
name = "directories"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c"
dependencies = [
"cfg-if",
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "dotenv"
version = "0.15.0"
@ -181,6 +250,17 @@ dependencies = [
"slab",
]
[[package]]
name = "getrandom"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "http"
version = "0.2.1"
@ -381,6 +461,29 @@ version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "redox_users"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
dependencies = [
"getrandom",
"redox_syscall",
"rust-argon2",
]
[[package]]
name = "rust-argon2"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19"
dependencies = [
"base64",
"blake2b_simd",
"constant_time_eq",
"crossbeam-utils",
]
[[package]]
name = "ryu"
version = "1.0.5"
@ -469,6 +572,15 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "toml"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a"
dependencies = [
"serde",
]
[[package]]
name = "tracing"
version = "0.1.19"
@ -523,6 +635,12 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "winapi"
version = "0.3.9"

View File

@ -13,3 +13,5 @@ dotenv = "0.15.0"
openssl = "0.10.30"
serde_json = "1.0.57"
serde = { version = "1.0.116", features = ["derive"] }
base64 = "0.12.3"
confy = "0.4.0"

View File

@ -1,27 +1,304 @@
use anyhow::Result;
use isahc::prelude::*;
use serde::Serialize;
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use openssl::sign::{Signer, Verifier};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
const BASE: &str = "https://public-api.sandbox.bunq.com/v1/";
const BASE: &str = "https://api.bunq.com";
#[derive(Serialize)]
struct Installation<'a> {
client_public_key: &'a str,
}
#[derive(Deserialize)]
struct Token {
token: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct InstallationResponse {
token: Token,
}
#[derive(Serialize)]
struct DeviceServer<'a> {
description: &'a str,
secret: &'a str,
permitted_ips: &'a [&'a str],
}
#[derive(Serialize)]
struct SessionServer<'a> {
secret: &'a str,
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct SessionServerResponse {
token: Token,
user_person: UserPerson,
}
#[derive(Deserialize)]
struct UserPerson {
id: i64,
}
fn sign<K: openssl::pkey::HasPrivate>(body: &str, key: &PKey<K>) -> Result<String> {
let mut signer = Signer::new(MessageDigest::sha256(), key)?;
let sig = signer.sign_oneshot_to_vec(body.as_bytes())?;
Ok(base64::encode(&sig))
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct RawResponse {
response: Vec<serde_json::Value>,
pagination: Option<Pagination>,
}
struct Response<T> {
response: T,
pagination: Option<Pagination>,
}
impl RawResponse {
fn decode_retarded<T: DeserializeOwned>(self) -> Result<Response<T>> {
let mut map = serde_json::Map::new();
for e in self.response {
if let serde_json::Value::Object(e) = e {
let (k, v) = e
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("malformed response"))?;
map.insert(k, v);
}
}
Ok(Response {
response: serde_json::from_value(map.into())?,
pagination: self.pagination,
})
}
}
fn deserialize_retarded_response<T: DeserializeOwned>(r: &str) -> Result<Response<T>> {
let r: RawResponse = serde_json::from_str(r)?;
r.decode_retarded()
}
fn deserialize_normal_response<T: DeserializeOwned>(r: &str) -> Result<Response<T>> {
let r: RawResponse = serde_json::from_str(r)?;
Ok(Response {
response: serde_json::from_value(r.response.into())?,
pagination: r.pagination,
})
}
#[derive(Serialize, Deserialize, Default)]
struct AppState {
token: String,
pem_private: String,
}
#[derive(Serialize, Deserialize, Default)]
struct AppConfig {
api_key: String,
state: Option<AppState>,
}
struct AppConfigReady {
api_key: String,
token: String,
keypair: PKey<openssl::pkey::Private>,
user_id: i64,
}
impl AppConfig {
pub fn load() -> Result<AppConfig> {
Ok(confy::load("bunqledger")?)
}
pub fn save(&self) -> Result<()> {
confy::store("bunqledger", self)?;
Ok(())
}
pub fn install(mut self) -> Result<AppConfigReady> {
let api_key = &self.api_key;
let keypair = if let Some(state) = &self.state {
PKey::private_key_from_pem(state.pem_private.as_bytes())?
} else {
let rsa = Rsa::generate(2048)?;
let pem_private = rsa.private_key_to_pem()?;
let pem_private = String::from_utf8(pem_private)?;
let keypair = PKey::from_rsa(rsa)?;
let pem_public = String::from_utf8(keypair.public_key_to_pem()?)?;
let body = Installation {
client_public_key: &pem_public,
};
let response = isahc::post(
format!("{}/v1/installation", BASE),
serde_json::to_string(&body)?,
)?
.text()?;
let response: InstallationResponse = deserialize_retarded_response(&response)?.response;
let token = response.token.token;
let body = DeviceServer {
description: "awesome",
secret: &api_key,
permitted_ips: &["31.21.118.143", "*"],
};
let body = serde_json::to_string(&body)?;
let mut response = isahc::http::Request::post(format!("{}/v1/device-server", BASE))
.header("X-Bunq-Client-Authentication", &token)
.body(body)?
.send()?;
println!("{}", response.text()?);
self.state = Some(AppState { pem_private, token });
self.save()?;
keypair
};
let token = self.state.unwrap().token;
let body = SessionServer { secret: &api_key };
let body = serde_json::to_string(&body)?;
let sig = sign(&body, &keypair)?;
let response = isahc::http::Request::post(format!("{}/v1/session-server", BASE))
.header("X-Bunq-Client-Authentication", &token)
.header("X-Bunq-Client-Signature", &sig)
.body(body)?
.send()?
.text()?;
let r: SessionServerResponse = deserialize_retarded_response(&response)?.response;
Ok(AppConfigReady {
api_key: self.api_key,
keypair,
token: r.token.token,
user_id: r.user_person.id,
})
}
}
impl AppConfigReady {
pub fn monetary_accounts(&self) -> Result<Vec<MonetaryAccountBank>> {
let response = isahc::http::Request::get(format!(
"{}/v1/user/{}/monetary-account",
BASE, self.user_id
))
.header("X-Bunq-Client-Authentication", &self.token)
.body(())?
.send()?
.text()?;
Ok(
deserialize_normal_response::<Vec<MonetaryAccount>>(&response)?
.response
.into_iter()
.map(|m| m.monetary_account_bank)
.collect(),
)
}
pub fn payments(&self, acc: &MonetaryAccountBank) -> Result<Vec<Payment>> {
let next_page = |url: &str| -> Result<(_, _)> {
let response = isahc::http::Request::get(url)
.header("X-Bunq-Client-Authentication", &self.token)
.body(())?
.send()?
.text()?;
let Response {
response,
pagination,
} = deserialize_normal_response::<Vec<PaymentPayment>>(&response)?;
Ok((
response.into_iter().map(|p| p.payment).collect(),
pagination,
))
};
let mut url = format!(
"/v1/user/{}/monetary-account/{}/payment",
self.user_id, acc.id
);
let mut all = Vec::new();
loop {
let (mut payments, pag) = next_page(&format!("{}{}", BASE, url))?;
all.append(&mut payments);
if let Some(Pagination{older_url: Some(older_url),..}) = pag {
url = older_url;
} else {
break;
}
}
Ok(all)
}
}
#[derive(Deserialize, Debug)]
struct LabelMonetaryAccount {
iban: Option<String>,
display_name: String,
merchant_category_code: Option<String>,
}
#[derive(Deserialize, Debug)]
struct Amount {
value: String,
currency: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct PaymentPayment {
payment: Payment,
}
#[derive(Deserialize, Debug)]
struct Pagination {
future_url: Option<String>,
newer_url: Option<String>,
older_url: Option<String>,
}
#[derive(Deserialize, Debug)]
struct Payment {
alias: LabelMonetaryAccount,
counterparty_alias: LabelMonetaryAccount,
amount: Amount,
balance_after_mutation: Amount,
created: String,
updated: String,
description: String,
id: i64,
monetary_account_id: i64,
#[serde(rename = "type")]
type_: String,
sub_type: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct MonetaryAccount {
monetary_account_bank: MonetaryAccountBank,
}
#[derive(Deserialize, Debug)]
struct MonetaryAccountBank {
id: i64,
description: String,
}
fn main() -> Result<()> {
dotenv::dotenv()?;
let cfg = AppConfig::load()?;
let api_key = std::env::var("API_KEY")?;
let cfg = cfg.install()?;
let accs = cfg.monetary_accounts()?;
println!("{:#?}", accs);
let acc = &accs[0];
let ps = cfg.payments(acc)?;
println!("{:#?}", ps);
let pem_private = include_bytes!("../private.pem");
let pem_public = include_bytes!("../public.pem");
let keypair = PKey::private_key_from_pem(pem_private)?;
let client_public_key = std::str::from_utf8(pem_public)?;
let body = Installation { client_public_key: &client_public_key};
let mut response = isahc::post(format!("{}installation",BASE), serde_json::to_string(&body)?)?;
println!("{}", response.text()?);
Ok(())
}