use std::collections::BTreeMap;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64;
use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use crate::credential::hmac256;
use crate::error::Result;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PostPolicy {
pub expiration: String,
pub conditions: Vec<serde_json::Value>,
}
impl PostPolicy {
pub fn new() -> Self {
Self::default()
}
pub fn expiration_rfc3339(mut self, rfc3339: impl Into<String>) -> Self {
self.expiration = rfc3339.into();
self
}
pub fn expire_in(mut self, secs: i64) -> Self {
let ts = Timestamp::now()
.checked_add(jiff::Span::new().seconds(secs))
.unwrap_or_else(|_| Timestamp::now());
self.expiration = ts.to_string();
self
}
pub fn condition(mut self, cond: serde_json::Value) -> Self {
self.conditions.push(cond);
self
}
pub fn to_base64(&self) -> Result<String> {
let json = serde_json::to_string(self)?;
Ok(BASE64.encode(json.as_bytes()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostObjectFormFields {
pub policy: String,
pub signature_version: String,
pub credential: String,
pub date: String,
pub signature: String,
pub security_token: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PostObjectSignV4Input<'a> {
pub access_key_id: &'a str,
pub access_key_secret: &'a str,
pub security_token: Option<&'a str>,
pub region: &'a str,
pub product: &'a str,
pub policy: &'a PostPolicy,
pub timestamp: Timestamp,
}
pub fn sign_post_object_v4(input: PostObjectSignV4Input<'_>) -> Result<PostObjectFormFields> {
let date_short = input.timestamp.strftime("%Y%m%d").to_string();
let date_long = input.timestamp.strftime("%Y%m%dT%H%M%SZ").to_string();
let credential = format!(
"{}/{date_short}/{}/{}/aliyun_v4_request",
input.access_key_id, input.region, input.product
);
let mut policy = input.policy.clone();
policy
.conditions
.push(serde_json::json!({"x-oss-signature-version": "OSS4-HMAC-SHA256"}));
policy
.conditions
.push(serde_json::json!({"x-oss-credential": credential}));
policy
.conditions
.push(serde_json::json!({"x-oss-date": date_long}));
if let Some(token) = input.security_token {
policy
.conditions
.push(serde_json::json!({"x-oss-security-token": token}));
}
let policy_b64 = policy.to_base64()?;
let secret = format!("aliyun_v4{}", input.access_key_secret);
let k_date = hmac256(secret.as_bytes(), &date_short)?;
let k_region = hmac256(&k_date, input.region)?;
let k_product = hmac256(&k_region, input.product)?;
let signing_key = hmac256(&k_product, "aliyun_v4_request")?;
let sig_bytes = hmac256(&signing_key, &policy_b64)?;
let signature = hex::encode(sig_bytes);
Ok(PostObjectFormFields {
policy: policy_b64,
signature_version: "OSS4-HMAC-SHA256".into(),
credential,
date: date_long,
signature,
security_token: input.security_token.map(str::to_string),
})
}
#[derive(Debug, Clone)]
pub struct PostObjectSignV1Input<'a> {
pub access_key_id: &'a str,
pub access_key_secret: &'a str,
pub security_token: Option<&'a str>,
pub policy: &'a PostPolicy,
}
pub fn sign_post_object_v1(input: PostObjectSignV1Input<'_>) -> Result<PostObjectFormFields> {
use hmac::{Hmac, KeyInit, Mac};
use sha1::Sha1;
let policy_b64 = input.policy.to_base64()?;
let mut mac = Hmac::<Sha1>::new_from_slice(input.access_key_secret.as_bytes())
.map_err(|e| anyhow::anyhow!("invalid HMAC key length: {e}"))?;
mac.update(policy_b64.as_bytes());
let sig = mac.finalize().into_bytes();
let signature = BASE64.encode(sig);
Ok(PostObjectFormFields {
policy: policy_b64,
signature_version: String::new(),
credential: input.access_key_id.to_string(),
date: String::new(),
signature,
security_token: input.security_token.map(str::to_string),
})
}
pub fn build_form_fields(
signed: &PostObjectFormFields,
key: impl Into<String>,
extra: BTreeMap<String, String>,
) -> BTreeMap<String, String> {
let mut fields = BTreeMap::new();
fields.insert("key".to_string(), key.into());
fields.insert("policy".to_string(), signed.policy.clone());
if !signed.signature_version.is_empty() {
fields.insert("x-oss-signature-version".to_string(), signed.signature_version.clone());
fields.insert("x-oss-credential".to_string(), signed.credential.clone());
fields.insert("x-oss-date".to_string(), signed.date.clone());
fields.insert("x-oss-signature".to_string(), signed.signature.clone());
} else {
fields.insert("OSSAccessKeyId".to_string(), signed.credential.clone());
fields.insert("Signature".to_string(), signed.signature.clone());
}
if let Some(token) = &signed.security_token {
fields.insert("x-oss-security-token".to_string(), token.clone());
}
fields.extend(extra);
fields
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_policy_to_base64() {
let policy = PostPolicy::new()
.expiration_rfc3339("2030-01-01T00:00:00Z")
.condition(serde_json::json!({"bucket": "examplebucket"}));
let b64 = policy.to_base64().unwrap();
let json = String::from_utf8(BASE64.decode(&b64).unwrap()).unwrap();
assert!(json.contains("\"expiration\":\"2030-01-01T00:00:00Z\""));
assert!(json.contains("\"bucket\":\"examplebucket\""));
}
#[test]
fn test_sign_v4_deterministic() {
let policy = PostPolicy::new()
.expiration_rfc3339("2030-01-01T00:00:00Z")
.condition(serde_json::json!({"bucket": "examplebucket"}));
let ts: Timestamp = "2024-01-01T00:00:00Z".parse().unwrap();
let signed = sign_post_object_v4(PostObjectSignV4Input {
access_key_id: "AKID",
access_key_secret: "SECRET",
security_token: None,
region: "cn-hangzhou",
product: "oss",
policy: &policy,
timestamp: ts,
})
.unwrap();
assert_eq!(signed.signature_version, "OSS4-HMAC-SHA256");
assert_eq!(signed.date, "20240101T000000Z");
assert_eq!(signed.credential, "AKID/20240101/cn-hangzhou/oss/aliyun_v4_request");
let again = sign_post_object_v4(PostObjectSignV4Input {
access_key_id: "AKID",
access_key_secret: "SECRET",
security_token: None,
region: "cn-hangzhou",
product: "oss",
policy: &policy,
timestamp: ts,
})
.unwrap();
assert_eq!(signed.signature, again.signature);
assert!(signed.signature.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_sign_v1() {
let policy = PostPolicy::new().expiration_rfc3339("2030-01-01T00:00:00Z");
let signed = sign_post_object_v1(PostObjectSignV1Input {
access_key_id: "AKID",
access_key_secret: "SECRET",
security_token: None,
policy: &policy,
})
.unwrap();
assert_eq!(signed.credential, "AKID");
assert!(!signed.signature.is_empty());
assert_eq!(signed.signature_version, ""); }
#[test]
fn test_build_form_fields_v4() {
let signed = PostObjectFormFields {
policy: "POLICY".into(),
signature_version: "OSS4-HMAC-SHA256".into(),
credential: "AKID/20240101/cn-hangzhou/oss/aliyun_v4_request".into(),
date: "20240101T000000Z".into(),
signature: "abcd".into(),
security_token: None,
};
let fields = build_form_fields(&signed, "user/eric/file.jpg", BTreeMap::new());
assert_eq!(fields.get("key").unwrap(), "user/eric/file.jpg");
assert_eq!(fields.get("policy").unwrap(), "POLICY");
assert_eq!(fields.get("x-oss-signature").unwrap(), "abcd");
assert!(!fields.contains_key("OSSAccessKeyId"));
}
#[test]
fn test_build_form_fields_v1() {
let signed = PostObjectFormFields {
policy: "POLICY".into(),
signature_version: String::new(),
credential: "AKID".into(),
date: String::new(),
signature: "base64sig==".into(),
security_token: None,
};
let fields = build_form_fields(&signed, "file.jpg", BTreeMap::new());
assert_eq!(fields.get("OSSAccessKeyId").unwrap(), "AKID");
assert_eq!(fields.get("Signature").unwrap(), "base64sig==");
assert!(!fields.contains_key("x-oss-signature"));
}
}