use std::str::FromStr;
use anyhow::{anyhow, bail, Ok};
use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use solana_sdk::{pubkey::Pubkey, signature::Signature};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecretKeyV1 {
pub version: u8, pub wallet: Wallet, pub signer: String, pub signature: String, pub metadata: MetadataV1, }
impl SecretKeyV1 {
pub fn into_string(self, scope: &str) -> anyhow::Result<String> {
let raw = SecretKeyRawV1::try_from(self)?;
let base58_encoded = bs58::encode(raw.into_bytes())
.into_string();
Ok(format!("aimo-sk-{scope}-{base58_encoded}"))
}
fn split_sk_string(sk: &str) -> Option<(&str, &str)> {
let mut parts = sk.splitn(4, '-');
let aimo = parts.next()?;
if aimo != "aimo" {
return None;
}
let prefix = parts.next()?;
if prefix != "sk" {
return None;
}
let scope = parts.next()?;
let base58_value = parts.next()?;
Some((scope, base58_value))
}
pub fn decode(sk: &str) -> anyhow::Result<(String, Self)> {
let (scope, key) = Self::split_sk_string(sk).ok_or(anyhow!(
"Invalid secret key: Failed to split secret key into valid parts"
))?;
let decoded_bytes = bs58::decode(key).into_vec()?;
let raw = SecretKeyRawV1::from_bytes(&decoded_bytes[..])?;
Ok((scope.to_string(), raw.try_into()?))
}
pub fn verify_signature(&self) -> anyhow::Result<()> {
let canonical_metadata = self.metadata.to_canonical_json()?;
let public_key = Pubkey::from_str(&self.signer)?;
let signature = Signature::from_str(&self.signature)?;
let is_valid = signature.verify(public_key.as_ref(), canonical_metadata.as_bytes());
if !is_valid {
bail!("Wrong signature");
}
if let Some(dt) = DateTime::<Utc>::from_timestamp_millis(
self.metadata.created_at + self.metadata.valid_for,
) {
if dt < Utc::now() {
bail!("Expired");
}
} else {
bail!("Invalid timestamp");
}
Ok(())
}
pub fn into_hash(self) -> anyhow::Result<[u8; 32]> {
let bytes = SecretKeyRawV1::try_from(self)?.into_bytes();
let mut hasher = Sha256::new();
hasher.update(&bytes[..]);
Ok(hasher.finalize().into())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct MetadataV1 {
pub created_at: i64,
pub valid_for: i64,
pub usage_limit: u64,
pub scopes: Vec<Scope>,
}
impl MetadataV1 {
pub fn to_canonical_json(&self) -> anyhow::Result<String> {
let metadata_json = serde_json::to_value(self)?;
canonical_json::to_string(&metadata_json).map_err(Into::into)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub enum Wallet {
#[serde(rename = "solana")]
Solana,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub enum Scope {
#[serde(rename = "completion_model")]
CompletionModel,
}
impl FromStr for Scope {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"completion_model" => Ok(Self::CompletionModel),
_ => Err(anyhow!("Scope {s} not supported")),
}
}
}
type WalletEnum = u8;
type ScopeBitMap = u64;
#[derive(Debug, Clone)]
pub struct SecretKeyRawV1 {
pub version: u8, pub wallet: WalletEnum, pub signer: [u8; 32], pub signature: [u8; 64], pub metadata: MetadataRawV1, }
impl SecretKeyRawV1 {
pub const BYTES: usize = 130;
pub fn into_bytes(self) -> Vec<u8> {
[
vec![self.version, self.wallet],
self.signer.to_vec(),
self.signature.to_vec(),
self.metadata.into_bytes(),
]
.concat()
}
pub fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
if bytes.len() != Self::BYTES {
bail!(
"Bytes length doesn't match: expect {}, got {}",
Self::BYTES,
bytes.len()
);
}
let version = bytes[0];
let wallet = bytes[1];
let signer: [u8; 32] = bytes[2..34].try_into()?;
let signature: [u8; 64] = bytes[34..98].try_into()?;
let metadata = MetadataRawV1::from_bytes(&bytes[98..130])?;
Ok(Self {
version,
wallet,
signer,
signature,
metadata,
})
}
}
impl TryFrom<SecretKeyV1> for SecretKeyRawV1 {
type Error = anyhow::Error;
fn try_from(value: SecretKeyV1) -> Result<Self, Self::Error> {
if value.wallet != Wallet::Solana {
bail!("Wallet not supported");
}
let signer: [u8; 32] = bs58::decode(&value.signer).into_vec()?[..].try_into()?;
let signature: [u8; 64] = bs58::decode(&value.signature).into_vec()?[..].try_into()?;
let wallet: WalletEnum = wallets::SOLANA;
Ok(Self {
version: value.version,
wallet,
signer,
signature,
metadata: value.metadata.try_into()?,
})
}
}
impl TryFrom<SecretKeyRawV1> for SecretKeyV1 {
type Error = anyhow::Error;
fn try_from(value: SecretKeyRawV1) -> Result<Self, Self::Error> {
if value.wallet >= wallets::TOTAL_WALLETS_SUPPORTED {
bail!("Unsupported wallet type in secret key: {}", value.wallet);
}
let wallet = match value.wallet {
wallets::SOLANA => Wallet::Solana,
_ => Wallet::Solana,
};
let signer = bs58::encode(&value.signer).into_string();
let signature = bs58::encode(&value.signature).into_string();
Ok(Self {
version: value.version,
wallet,
signer,
signature,
metadata: value.metadata.try_into()?,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct MetadataRawV1 {
pub created_at: i64, pub valid_for: i64, pub usage_limit: u64, pub scopes: ScopeBitMap, }
impl MetadataRawV1 {
pub const BYTES: usize = 32;
pub fn into_bytes(self) -> Vec<u8> {
[
self.created_at.to_be_bytes().to_vec(),
self.valid_for.to_be_bytes().to_vec(),
self.usage_limit.to_be_bytes().to_vec(),
self.scopes.to_be_bytes().to_vec(),
]
.concat()
}
pub fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
if bytes.len() != Self::BYTES {
bail!(
"Bytes length doesn't match: expect {}, got {}",
Self::BYTES,
bytes.len()
);
}
let created_at = i64::from_be_bytes(bytes[0..8].try_into()?);
let valid_for = i64::from_be_bytes(bytes[8..16].try_into()?);
let usage_limit = u64::from_be_bytes(bytes[16..24].try_into()?);
let scopes = u64::from_be_bytes(bytes[24..32].try_into()?);
Ok(Self {
created_at,
valid_for,
usage_limit,
scopes,
})
}
}
impl TryFrom<MetadataV1> for MetadataRawV1 {
type Error = anyhow::Error;
fn try_from(value: MetadataV1) -> Result<Self, Self::Error> {
let bitmap: ScopeBitMap = value.scopes.iter().fold(0, |bm, scope| match scope {
Scope::CompletionModel => bm | 1 << scopes::COMPLETION_MODEL,
});
Ok(Self {
created_at: value.created_at,
valid_for: value.valid_for,
usage_limit: value.usage_limit,
scopes: bitmap,
})
}
}
impl TryFrom<MetadataRawV1> for MetadataV1 {
type Error = anyhow::Error;
fn try_from(value: MetadataRawV1) -> Result<Self, Self::Error> {
if (!scopes::SCOPES_SUPPORTED & value.scopes) > 0 {
bail!("Secret key contains currently unsupported scope type");
}
let scopes = if value.scopes | scopes::COMPLETION_MODEL > 0 {
vec![Scope::CompletionModel]
} else {
vec![]
};
Ok(Self {
created_at: value.created_at,
valid_for: value.valid_for,
usage_limit: value.usage_limit,
scopes,
})
}
}
pub mod wallets {
use super::WalletEnum;
pub const TOTAL_WALLETS_SUPPORTED: WalletEnum = 1;
pub const SOLANA: WalletEnum = 0x00;
}
pub mod scopes {
use super::ScopeBitMap;
pub const SCOPES_SUPPORTED: ScopeBitMap = 0x01;
pub const COMPLETION_MODEL: ScopeBitMap = 0;
}