mod error;
pub use crate::error::{Error, Result};
use rsa::{Hash, PaddingScheme::PKCS1v15Sign, PublicKey, RSAPrivateKey, RSAPublicKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug)]
pub struct BizContent<'a> {
out_trade_no: &'a str,
qr_code_timeout_express: &'a str,
subject: &'a str,
total_amount: f32,
}
impl<'a> BizContent<'a> {
pub fn new(
out_trade_no: &'a str,
qr_code_timeout_express: &'a str,
subject: &'a str,
total_amount: f32,
) -> Self {
Self {
out_trade_no,
qr_code_timeout_express,
subject,
total_amount,
}
}
}
impl<'a> std::fmt::Display for BizContent<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(
f,
r#"{{"out_trade_no":"{}","qr_code_timeout_express":"{}m","subject":"{}","total_amount":{}}}"#,
self.out_trade_no, self.qr_code_timeout_express, self.subject, self.total_amount
)
}
}
#[inline]
fn varify(public_key: RSAPublicKey, content: &str, sig: &str) -> Result<()> {
let mut sh = Sha256::new();
sh.update(content);
let hashed: &[u8] = &sh.finalize();
let sig = base64::decode(sig)?;
Ok(public_key.verify(
PKCS1v15Sign {
hash: Some(Hash::SHA2_256),
},
hashed,
&sig,
)?)
}
pub trait Signable {
fn presigned_content(&self) -> String;
}
#[derive(Debug)]
pub struct PreCreateRequest<'a> {
app_id: &'a str,
biz_content: BizContent<'a>,
charset: &'a str,
method: &'a str,
notify_url: &'a str,
sign_type: &'a str,
sign: Option<String>,
timestamp: &'a str,
version: &'a str,
}
impl<'a> PreCreateRequest<'a> {
pub fn new(
app_id: &'a str,
timestamp: &'a str,
notify_url: &'a str,
biz_content: BizContent<'a>,
) -> Self {
Self {
app_id,
method: "alipay.trade.precreate",
charset: "utf-8",
sign_type: "RSA2",
sign: None,
timestamp,
version: "1.0",
notify_url,
biz_content,
}
}
pub fn sign(&mut self, pk: RSAPrivateKey) -> Result<()> {
let mut sh = Sha256::new();
sh.update(self.presigned_content());
let hashed: &[u8] = &sh.finalize();
let res = pk.sign(
PKCS1v15Sign {
hash: Some(Hash::SHA2_256),
},
hashed,
)?;
self.sign = Some(base64::encode(res));
Ok(())
}
}
impl<'a> Signable for PreCreateRequest<'a> {
fn presigned_content(&self) -> String {
format!(
r#"app_id={}&biz_content={}&charset={}&method={}¬ify_url={}&sign_type={}×tamp={}&version={}"#,
self.app_id,
self.biz_content,
self.charset,
self.method,
self.notify_url,
self.sign_type,
self.timestamp,
self.version
)
}
}
impl<'a> std::fmt::Display for PreCreateRequest<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
if let Some(s) = &self.sign {
let params = &[
("app_id", self.app_id),
("biz_content", &self.biz_content.to_string()),
("charset", self.charset),
("method", self.method),
("notify_url", self.notify_url),
("sign_type", self.sign_type),
("sign", s),
("timestamp", self.timestamp),
("version", self.version),
];
if let Ok(query) = serde_urlencoded::to_string(params) {
return write!(f, "{}", query);
}
}
Err(std::fmt::Error)
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PrecreateResponseWrap {
pub alipay_trade_precreate_response: AlipayTradePrecreateResponse,
pub sign: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AlipayTradePrecreateResponse {
code: String,
msg: String,
out_trade_no: String,
qr_code: String,
#[serde(skip_serializing_if = "Option::is_none")]
sub_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
sub_msg: Option<String>,
}
impl PrecreateResponseWrap {
pub fn varify(&self, public_key: RSAPublicKey) -> Result<()> {
varify(
public_key,
&self.alipay_trade_precreate_response.presigned_content(),
&self.sign,
)
}
}
impl Signable for AlipayTradePrecreateResponse {
fn presigned_content(&self) -> String {
let mut c = format!(
r#"{{"code":"{}","msg":"{}","out_trade_no":"{}","qr_code":"{}""#,
self.code,
self.msg,
self.out_trade_no,
self.qr_code.replace("/", "\\/")
);
if let Some(sub_code) = &self.sub_code {
c.push_str(&format!(r#","sub_code":"{}""#, sub_code));
}
if let Some(sub_msg) = &self.sub_msg {
c.push_str(&format!(r#","sub_msg":"{}""#, sub_msg));
}
c.push_str("}");
c
}
}
#[derive(Debug)]
pub struct NotifyQuery {
pub query_map: std::collections::BTreeMap<String, String>,
}
impl NotifyQuery {
pub fn varify(&self, public_key: RSAPublicKey) -> Result<()> {
match (self.query_map.get("sign"), self.query_map.get("sign_type")) {
(Some(sig), Some(t)) if t == "RSA2" => {
varify(public_key, &self.presigned_content(), sig)
}
_ => Err(Error::Rsa(rsa::errors::Error::Verification)),
}
}
}
impl From<&str> for NotifyQuery {
fn from(query: &str) -> Self {
let ql = query.split('&');
let mut query_map = std::collections::BTreeMap::new();
for i in ql {
let mut kv = i.splitn(2, '=');
match (kv.next(), kv.next()) {
(Some(k), Some(v)) => {
query_map.insert(k.to_owned(), v.to_owned());
}
_ => {}
}
}
NotifyQuery { query_map }
}
}
impl Signable for NotifyQuery {
fn presigned_content(&self) -> String {
let ql = self
.query_map
.iter()
.filter(|(k, _)| **k != "sign" && **k != "sign_type")
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>();
ql.join("&")
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{FixedOffset, Utc};
use std::io::Read;
fn current() -> String {
Utc::now()
.with_timezone(&FixedOffset::east(8 * 3600))
.format("%Y-%m-%d %H:%M:%S")
.to_string()
}
fn private_key(path: &str) -> RSAPrivateKey {
let mut f = std::fs::File::open(path).unwrap();
let mut content = String::default();
f.read_to_string(&mut content).unwrap();
let der_encoded = content.lines().filter(|line| !line.starts_with("-")).fold(
String::new(),
|mut data, line| {
data.push_str(&line);
data
},
);
let der_bytes = base64::decode(&der_encoded).expect("failed to decode base64 content");
RSAPrivateKey::from_pkcs8(&der_bytes).unwrap()
}
fn public_key(path: &str) -> RSAPublicKey {
let mut f = std::fs::File::open(path).unwrap();
let mut content = String::default();
f.read_to_string(&mut content).unwrap();
let der_encoded = content.lines().filter(|line| !line.starts_with("-")).fold(
String::new(),
|mut data, line| {
data.push_str(&line);
data
},
);
let der_bytes = base64::decode(&der_encoded).expect("failed to decode base64 content");
RSAPublicKey::from_pkcs8(&der_bytes).expect("failed to parse key")
}
fn create_request<'a>(timestamp: &'a str) -> PreCreateRequest<'a> {
let biz_content = BizContent::new("16089520029516", "5", "测试商品", 0.2f32);
PreCreateRequest::new(
"2021002116638987",
timestamp,
"http://api.test.alipay.net/atinterface/receive_notify.htm",
biz_content,
)
}
#[test]
fn precreate_test() {
let timestamp = current();
let mut req = create_request(×tamp);
let pk = private_key("/Users/tianen/dev/secret/app_private_key_pkcs8.pem");
assert!(req.sign(pk).is_ok());
let url = format!("https://openapi.alipay.com/gateway.do?{}", req);
let resp = ureq::post(&url).call();
let resp = resp
.into_json_deserialize::<PrecreateResponseWrap>()
.unwrap();
assert_eq!(resp.alipay_trade_precreate_response.msg, "Success");
let pk = public_key("/Users/tianen/dev/secret/alipay_public_key.pem");
assert!(resp.varify(pk).is_ok());
}
#[test]
fn async_notify_check_test() {
let req_query = "total_amount=2.00&buyer_id=2088102116773037&body=大乐透2.1&trade_no=2016071921001003030200089909&refund_fee=0.00¬ify_time=2016-07-19 14:10:49&subject=大乐透2.1&sign_type=RSA2&charset=utf-8¬ify_type=trade_status_sync&out_trade_no=0719141034-6418&gmt_close=2016-07-19 14:10:46&gmt_payment=2016-07-19 14:10:47&trade_status=TRADE_SUCCESS&version=1.0&sign=kPbQIjX+xQc8F0/A6/AocELIjhhZnGbcBN6G4MM/HmfWL4ZiHM6fWl5NQhzXJusaklZ1LFuMo+lHQUELAYeugH8LYFvxnNajOvZhuxNFbN2LhF0l/KL8ANtj8oyPM4NN7Qft2kWJTDJUpQOzCzNnV9hDxh5AaT9FPqRS6ZKxnzM=&gmt_create=2016-07-19 14:10:44&app_id=2015102700040153&seller_id=2088102119685838¬ify_id=4a91b7a78a503640467525113fb7d8bg8e";
let nq = NotifyQuery::from(req_query);
let pk = public_key("./alipay_public_key.pem");
assert!(nq.varify(pk).is_ok());
}
}