diff --git a/Cargo.toml b/Cargo.toml index 548c708..d900d80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "LICENSE" @@ -37,16 +37,16 @@ viewing-key = ["secret-toolkit-viewing-key"] notification = ["secret-toolkit-notification"] [dependencies] -secret-toolkit-crypto = { version = "0.10.2", path = "packages/crypto", optional = true } -secret-toolkit-incubator = { version = "0.10.2", path = "packages/incubator", optional = true } -secret-toolkit-permit = { version = "0.10.2", path = "packages/permit", optional = true } -secret-toolkit-serialization = { version = "0.10.2", path = "packages/serialization", optional = true } -secret-toolkit-snip20 = { version = "0.10.2", path = "packages/snip20", optional = true } -secret-toolkit-snip721 = { version = "0.10.2", path = "packages/snip721", optional = true } -secret-toolkit-storage = { version = "0.10.2", path = "packages/storage", optional = true } -secret-toolkit-utils = { version = "0.10.2", path = "packages/utils", optional = true } -secret-toolkit-viewing-key = { version = "0.10.2", path = "packages/viewing_key", optional = true } -secret-toolkit-notification = { version = "0.10.2", path = "packages/notification", optional = true } +secret-toolkit-crypto = { version = "0.10.3", path = "packages/crypto", optional = true } +secret-toolkit-incubator = { version = "0.10.3", path = "packages/incubator", optional = true } +secret-toolkit-permit = { version = "0.10.3", path = "packages/permit", optional = true } +secret-toolkit-serialization = { version = "0.10.3", path = "packages/serialization", optional = true } +secret-toolkit-snip20 = { version = "0.10.3", path = "packages/snip20", optional = true } +secret-toolkit-snip721 = { version = "0.10.3", path = "packages/snip721", optional = true } +secret-toolkit-storage = { version = "0.10.3", path = "packages/storage", optional = true } +secret-toolkit-utils = { version = "0.10.3", path = "packages/utils", optional = true } +secret-toolkit-viewing-key = { version = "0.10.3", path = "packages/viewing_key", optional = true } +secret-toolkit-notification = { version = "0.10.3", path = "packages/notification", optional = true } [workspace] members = ["packages/*"] diff --git a/packages/crypto/Cargo.toml b/packages/crypto/Cargo.toml index 6dfef7c..7c1b0ae 100644 --- a/packages/crypto/Cargo.toml +++ b/packages/crypto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-crypto" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" diff --git a/packages/incubator/Cargo.toml b/packages/incubator/Cargo.toml index 53f0ac8..f0dbb0d 100644 --- a/packages/incubator/Cargo.toml +++ b/packages/incubator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-incubator" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" @@ -16,7 +16,7 @@ all-features = true [dependencies] serde = { workspace = true, optional = true } cosmwasm-std = { workspace = true, optional = true } -secret-toolkit-serialization = { version = "0.10.2", path = "../serialization", optional = true } +secret-toolkit-serialization = { version = "0.10.3", path = "../serialization", optional = true } [features] generational-store = ["secret-toolkit-serialization", "serde", "cosmwasm-std"] diff --git a/packages/notification/Cargo.toml b/packages/notification/Cargo.toml index 861ec81..0098cd9 100644 --- a/packages/notification/Cargo.toml +++ b/packages/notification/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-notification" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["darwinzer0","blake-regalia"] license-file = "../../LICENSE" @@ -30,6 +30,6 @@ primitive-types = { version = "0.12.2", default-features = false } hex = "0.4.3" minicbor = "0.25.1" -secret-toolkit-crypto = { version = "0.10.2", path = "../crypto", features = [ +secret-toolkit-crypto = { version = "0.10.3", path = "../crypto", features = [ "hash", "hkdf" ] } diff --git a/packages/permit/Cargo.toml b/packages/permit/Cargo.toml index 34ca782..eefe594 100644 --- a/packages/permit/Cargo.toml +++ b/packages/permit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-permit" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" @@ -20,6 +20,9 @@ ripemd = { version = "0.1.3", default-features = false } schemars = { workspace = true } bech32 = "0.9.1" remain = "0.2.8" -secret-toolkit-crypto = { version = "0.10.2", path = "../crypto", features = [ +secret-toolkit-crypto = { version = "0.10.3", path = "../crypto", features = [ "hash", ] } +secret-toolkit-utils = { version = "0.10.3", path = "../utils" } +secret-toolkit-storage = { version = "0.10.3", path = "../storage" } + diff --git a/packages/permit/src/funcs.rs b/packages/permit/src/funcs.rs index 2386d00..edcd434 100644 --- a/packages/permit/src/funcs.rs +++ b/packages/permit/src/funcs.rs @@ -1,20 +1,44 @@ -use cosmwasm_std::{to_binary, Binary, CanonicalAddr, Deps, StdError, StdResult}; +use cosmwasm_std::{to_binary, Binary, CanonicalAddr, Deps, Env, StdError, StdResult, Uint64}; use ripemd::{Digest, Ripemd160}; +use secret_toolkit_utils::iso8601_utc0_to_timestamp; -use crate::{Permissions, Permit, RevokedPermits, SignedPermit}; +use crate::{ + Permissions, Permit, RevokedPermits, RevokedPermitsStore, SignedPermit, BLANKET_PERMIT_TOKEN, +}; use bech32::{ToBase32, Variant}; use secret_toolkit_crypto::sha_256; pub fn validate( deps: Deps, - storage_prefix: &str, + env: &Env, permit: &Permit, current_token_address: String, hrp: Option<&str>, ) -> StdResult { let account_hrp = hrp.unwrap_or("secret"); - if !permit.check_token(¤t_token_address) { + if permit + .params + .allowed_tokens + .contains(&BLANKET_PERMIT_TOKEN.to_string()) + { + // using blanket permit + + // assert allowed_tokens list has an exact length of 1 + if permit.params.allowed_tokens.len() != 1 { + return Err(StdError::generic_err( + "Blanket permits cannot contain other allowed tokens", + )); + } + + // assert created field is specified + if permit.params.created.is_none() { + return Err(StdError::generic_err( + "Blanket permits must have a `created` time", + )); + } + } else if !permit.check_token(¤t_token_address) { + // check that current token address is in allowed tokens return Err(StdError::generic_err(format!( "Permit doesn't apply to token {:?}, allowed tokens: {:?}", current_token_address.as_str(), @@ -27,16 +51,87 @@ pub fn validate( ))); } + // Convert the permit created field to a Timestamp + let created_timestamp = permit + .params + .created + .clone() + .map(|created| iso8601_utc0_to_timestamp(&created)) + .transpose()?; + + if let Some(created) = created_timestamp { + // Verify that the permit was not created after the current block time + if created.seconds() > env.block.time.seconds() { + return Err(StdError::generic_err( + "Permit `created` after current block time", + )); + } + } + + // Convert the permit expires field to a Timestamp + let expires_timestamp = permit + .params + .expires + .clone() + .map(|created| iso8601_utc0_to_timestamp(&created)) + .transpose()?; + + if let Some(expires) = expires_timestamp { + // Verify that the permit did not expire before the current block time + if expires.seconds() <= env.block.time.seconds() { + return Err(StdError::generic_err("Permit has expired")); + } + } + // Derive account from pubkey let pubkey = &permit.signature.pub_key.value; let base32_addr = pubkey_to_account(pubkey).0.as_slice().to_base32(); let account: String = bech32::encode(account_hrp, base32_addr, Variant::Bech32).unwrap(); + // Get the list of all revocations for this address + let revocations = RevokedPermits::list_revocations(deps.storage, &account)?; + + // Check if account has an all time permit revocation + if RevokedPermits::is_all_time_revoked(deps.storage, account.as_str())? { + return Err(StdError::generic_err(format!( + "Permits revoked by {:?}", + account.as_str() + ))); + } + + // Check if there are any revocation intervals blocking all permits + // TODO: An interval or segment tree might be preferable to make this more efficient for cases + // when the number of revocations is allowed to grow to a large amount. + for revocation in revocations { + // If the permit has a `created` field + if let Some(created) = created_timestamp { + // Revocation created before field, default 0 + let created_before = revocation + .interval + .created_before + .unwrap_or(Uint64::from(0u64)); + + // Revocation created after field, default max u64 + let created_after = revocation + .interval + .created_after + .unwrap_or(Uint64::from(u64::MAX)); + + // If the permit's `created` field falls in between created after and created before, then reject it + if created.seconds() > created_after.u64() || created.seconds() < created_before.u64() { + return Err(StdError::generic_err(format!( + "Permits created at {:?} revoked by account {:?}", + created, + account.as_str() + ))); + } + } + } + // Validate permit_name let permit_name = &permit.params.permit_name; - let is_permit_revoked = - RevokedPermits::is_permit_revoked(deps.storage, storage_prefix, &account, permit_name); + let is_permit_revoked = RevokedPermits::is_permit_revoked(deps.storage, &account, permit_name); if is_permit_revoked { return Err(StdError::generic_err(format!( "Permit {:?} was revoked by account {:?}", @@ -73,7 +168,10 @@ pub fn pubkey_to_account(pubkey: &Binary) -> CanonicalAddr { mod tests { use super::*; use crate::{PermitParams, PermitSignature, PubKey, TokenPermissions}; - use cosmwasm_std::testing::mock_dependencies; + use cosmwasm_std::{ + testing::{mock_dependencies, mock_env}, + Timestamp, + }; #[test] fn test_verify_permit() { @@ -88,36 +186,240 @@ mod tests { allowed_tokens: vec![token.clone()], permit_name: "memo_secret1rf03820fp8gngzg2w02vd30ns78qkc8rg8dxaq".to_string(), chain_id: "pulsar-2".to_string(), - permissions: vec![TokenPermissions::History] + permissions: vec![TokenPermissions::History], + created: None, + expires: None, }, signature: PermitSignature { pub_key: PubKey { r#type: "tendermint/PubKeySecp256k1".to_string(), value: Binary::from_base64("A5M49l32ZrV+SDsPnoRv8fH7ivNC4gEX9prvd4RwvRaL").unwrap(), + }, - signature: Binary::from_base64("hw/Mo3ZZYu1pEiDdymElFkuCuJzg9soDHw+4DxK7cL9rafiyykh7VynS+guotRAKXhfYMwCiyWmiznc6R+UlsQ==").unwrap() + signature: Binary::from_base64("hw/Mo3ZZYu1pEiDdymElFkuCuJzg9soDHw+4DxK7cL9rafiyykh7VynS+guotRAKXhfYMwCiyWmiznc6R+UlsQ==").unwrap() } }; - let address = validate::<_>( - deps.as_ref(), - "test", - &permit, - token.clone(), - Some("secret"), - ) - .unwrap(); + let env = mock_env(); + + let address = + validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")).unwrap(); assert_eq!( address, "secret1399pyvvk3hvwgxwt3udkslsc5jl3rqv4yshfrl".to_string() ); - let address = validate::<_>(deps.as_ref(), "test", &permit, token, Some("cosmos")).unwrap(); + let env = mock_env(); + + let address = validate::<_>(deps.as_ref(), &env, &permit, token, Some("cosmos")).unwrap(); assert_eq!( address, "cosmos1399pyvvk3hvwgxwt3udkslsc5jl3rqv4x4rq7r".to_string() ); } + + #[test] + fn test_verify_permit_created_expires() { + let deps = mock_dependencies(); + + // test both created and expired set + + //{"permit": {"params":{"chain_id":"secret-4","permit_name":"test","allowed_tokens":["secret18vd8fpwxzck93qlwghaj6arh4p7c5n8978vsyg"],"permissions":["balance"],"created":"2024-12-17T16:59:00.000Z","expires":"2024-12-20T06:59:30.333Z"},"signature":{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr"},"signature":"mFDn5w59gaDTHZ5UzEA6l+sUOtlWDx/HcSi1NpZM13YuamMehIi3mseqXcQy4loE63N0hYhyXiVZdzrPM28A+g=="}}} + + let token = "secret18vd8fpwxzck93qlwghaj6arh4p7c5n8978vsyg".to_string(); + + let permit: Permit = Permit{ + params: PermitParams { + allowed_tokens: vec![token.clone()], + permit_name: "test".to_string(), + chain_id: "secret-4".to_string(), + permissions: vec![TokenPermissions::Balance], + created: Some("2024-12-17T16:59:00.000Z".to_string()), + expires: Some("2024-12-20T06:59:30.333Z".to_string()), + }, + // {"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr"},"signature":"mFDn5w59gaDTHZ5UzEA6l+sUOtlWDx/HcSi1NpZM13YuamMehIi3mseqXcQy4loE63N0hYhyXiVZdzrPM28A+g=="} + signature: PermitSignature { + pub_key: PubKey { + r#type: "tendermint/PubKeySecp256k1".to_string(), + value: Binary::from_base64("AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr").unwrap(), + }, + signature: Binary::from_base64("mFDn5w59gaDTHZ5UzEA6l+sUOtlWDx/HcSi1NpZM13YuamMehIi3mseqXcQy4loE63N0hYhyXiVZdzrPM28A+g==").unwrap() + } + }; + + let created_seconds: u64 = 1734454740; + let expires_seconds: u64 = 1734677970; + + // validate after created, before expires + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(created_seconds + 100); + + // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 + let address = + validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")).unwrap(); + + assert_eq!( + address, + "secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9".to_string() + ); + + // validate before created + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(created_seconds - 100); + + let address = validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")); + + assert!(address.is_err(), "validated before created"); + + // validate after expires + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(expires_seconds + 100); + + let address = validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")); + + assert!(address.is_err(), "validated after expires"); + } + + #[test] + fn test_verify_blanket_permit() { + let deps = mock_dependencies(); + + // blanket permit + + //{"permit": {"params":{"chain_id":"secret-4","permit_name":"test","allowed_tokens":["ANY_TOKEN"],"permissions":["balance"],"created":"2024-12-17T16:59:00.000Z","expires":"2024-12-20T06:59:30.333Z"},"signature":{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr"},"signature":"Qte1iS54RsyRCN3rmOjA96yXQTn+eg4YaEUAU/Q5mLVGU9mOCEw6LMZjU2owLB4ogcziWrMkLOL3dtOrj3dL4Q=="}}} + + let token = BLANKET_PERMIT_TOKEN.to_string(); + + let permit: Permit = Permit{ + params: PermitParams { + allowed_tokens: vec![token.clone()], + permit_name: "test".to_string(), + chain_id: "secret-4".to_string(), + permissions: vec![TokenPermissions::Balance], + created: Some("2024-12-17T16:59:00.000Z".to_string()), + expires: Some("2024-12-20T06:59:30.333Z".to_string()), + }, + signature: PermitSignature { + pub_key: PubKey { + r#type: "tendermint/PubKeySecp256k1".to_string(), + value: Binary::from_base64("AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr").unwrap(), + }, + signature: Binary::from_base64("Qte1iS54RsyRCN3rmOjA96yXQTn+eg4YaEUAU/Q5mLVGU9mOCEw6LMZjU2owLB4ogcziWrMkLOL3dtOrj3dL4Q==").unwrap() + } + }; + + let created_seconds: u64 = 1734454740; + let expires_seconds: u64 = 1734677970; + + // validate after created, before expires + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(created_seconds + 100); + + // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 + let address = + validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")).unwrap(); + + assert_eq!( + address, + "secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9".to_string() + ); + + // validate before created + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(created_seconds - 100); + + let address = validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")); + + assert!(address.is_err(), "validated before created"); + + // validate after expires + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(expires_seconds + 100); + + let address = validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")); + + assert!(address.is_err(), "validated after expires"); + + // blanket permit invalid with another token in addition to ANY_TOKEN + + //{"permit": {"params":{"chain_id":"secret-4","permit_name":"test","allowed_tokens":["secret18vd8fpwxzck93qlwghaj6arh4p7c5n8978vsyg","ANY_TOKEN"],"permissions":["balance"],"created":"2024-12-17T16:59:00.000Z","expires":"2024-12-20T06:59:30.333Z"},"signature":{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr"},"signature":"vc36PM85beBIOmimreAD428O3ldyyUqNHxmzUYlsHaJ+560Ce8G5ibJR7KCvHJitRuds/3TvGX4dPp6l6xfrUg=="}}} + + let token = BLANKET_PERMIT_TOKEN.to_string(); + + let permit: Permit = Permit{ + params: PermitParams { + allowed_tokens: vec!["secret18vd8fpwxzck93qlwghaj6arh4p7c5n8978vsyg".to_string(), token.clone()], + permit_name: "test".to_string(), + chain_id: "secret-4".to_string(), + permissions: vec![TokenPermissions::Balance], + created: Some("2024-12-17T16:59:00.000Z".to_string()), + expires: Some("2024-12-20T06:59:30.333Z".to_string()), + }, + signature: PermitSignature { + pub_key: PubKey { + r#type: "tendermint/PubKeySecp256k1".to_string(), + value: Binary::from_base64("AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr").unwrap(), + }, + signature: Binary::from_base64("vc36PM85beBIOmimreAD428O3ldyyUqNHxmzUYlsHaJ+560Ce8G5ibJR7KCvHJitRuds/3TvGX4dPp6l6xfrUg==").unwrap() + } + }; + + let created_seconds: u64 = 1734454740; + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(created_seconds + 100); + + // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 + let address = validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")); + + assert!( + address.is_err(), + "passed with second token in addition to ANY_TOKEN" + ); + + // blanket permit invalid with no created + + //{"permit": {"params":{"chain_id":"secret-4","permit_name":"test","allowed_tokens":["ANY_TOKEN"],"permissions":["balance"],"expires":"2024-12-20T06:59:30.333Z"},"signature":{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr"},"signature":"k2tdjChWUeIfs63qcHwzUdt1C92gQ5lwvEPS4fv7GpM2geaWGpUsy6Ne+m0pda0AJEpdbiZ38KjiKNlU3CmkOw=="}}} + + let token = BLANKET_PERMIT_TOKEN.to_string(); + + let permit: Permit = Permit{ + params: PermitParams { + allowed_tokens: vec![token.clone()], + permit_name: "test".to_string(), + chain_id: "secret-4".to_string(), + permissions: vec![TokenPermissions::Balance], + created: None, + expires: Some("2024-12-20T06:59:30.333Z".to_string()), + }, + signature: PermitSignature { + pub_key: PubKey { + r#type: "tendermint/PubKeySecp256k1".to_string(), + value: Binary::from_base64("AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr").unwrap(), + }, + signature: Binary::from_base64("k2tdjChWUeIfs63qcHwzUdt1C92gQ5lwvEPS4fv7GpM2geaWGpUsy6Ne+m0pda0AJEpdbiZ38KjiKNlU3CmkOw==").unwrap() + } + }; + + let created_seconds: u64 = 1734454740; + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(created_seconds + 100); + + // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 + let address = validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")); + + assert!( + address.is_err(), + "blanket permit passed with no created field" + ); + } } diff --git a/packages/permit/src/state.rs b/packages/permit/src/state.rs index 9a47e93..7447e53 100644 --- a/packages/permit/src/state.rs +++ b/packages/permit/src/state.rs @@ -1,32 +1,219 @@ -use cosmwasm_std::Storage; +use cosmwasm_std::{StdError, StdResult, Storage, Uint64}; +use schemars::JsonSchema; +use secret_toolkit_storage::{Item, Keymap}; +use serde::{Deserialize, Serialize}; +use crate::REVOKED_ALL; + +/// This is the default implementation of the revoked permits store, using the "revoked_permits" +/// storage prefix for named permits and "all_revoked_permits" for revoked blanket permits. +/// It also sets the maximum number of all permit revocations to 10 by default. +/// +/// You can use different storage locations and parameters by implementing `RevokedPermitsStore` +/// for your own type. pub struct RevokedPermits; -impl RevokedPermits { - pub fn is_permit_revoked( - storgae: &dyn Storage, - storage_prefix: &str, - account: &str, - permit_name: &str, - ) -> bool { - let storage_key = storage_prefix.to_string() + account + permit_name; +impl<'a> RevokedPermitsStore<'a> for RevokedPermits { + const NAMED_REVOKED_PERMITS_PREFIX: &'static [u8] = b"revoked_permits"; + const ALL_REVOKED_PERMITS: Keymap<'a, u64, StoredAllRevokedInterval> = + Keymap::new(b"__all_revoked_permits__1"); + const ALL_REVOKED_NEXT_ID: Item<'a, u64> = Item::new(b"__all_revoked_permits_serial_id__1"); + const MAX_ALL_REVOKED_INTERVALS: Option = Some(10); + const ALL_TIME_REVOKED_ALL: Item<'a, bool> = Item::new(b"__all_time_revoked_all__1"); +} - storgae.get(storage_key.as_bytes()).is_some() +/// A trait describing the interface of a RevokedPermits store/vault. +/// +/// It includes a default implementation that only requires specifying where in the storage +/// the keys should be held. +pub trait RevokedPermitsStore<'a> { + const NAMED_REVOKED_PERMITS_PREFIX: &'static [u8]; + const ALL_REVOKED_PERMITS: Keymap<'a, u64, StoredAllRevokedInterval>; + const ALL_REVOKED_NEXT_ID: Item<'a, u64>; + const MAX_ALL_REVOKED_INTERVALS: Option; + const ALL_TIME_REVOKED_ALL: Item<'a, bool>; + + /// returns a bool indicating if a named permit is revoked + fn is_permit_revoked(storage: &dyn Storage, account: &str, permit_name: &str) -> bool { + let mut storage_key = Vec::new(); + storage_key.extend_from_slice(Self::NAMED_REVOKED_PERMITS_PREFIX); + storage_key.extend_from_slice(account.as_bytes()); + storage_key.extend_from_slice(permit_name.as_bytes()); + + storage.get(&storage_key).is_some() } - pub fn revoke_permit( - storage: &mut dyn Storage, - storage_prefix: &str, - account: &str, - permit_name: &str, - ) { - let storage_key = storage_prefix.to_string() + account + permit_name; + /// revokes a named permit permanently + fn revoke_permit(storage: &mut dyn Storage, account: &str, permit_name: &str) { + let mut storage_key = Vec::new(); + storage_key.extend_from_slice(Self::NAMED_REVOKED_PERMITS_PREFIX); + storage_key.extend_from_slice(account.as_bytes()); + storage_key.extend_from_slice(permit_name.as_bytes()); // Since cosmwasm V1.0 it's not possible to set an empty value, hence set some unimportant // character '_' // // Here is the line of the new panic that was added when trying to insert an empty value: // https://github.com/scrtlabs/cosmwasm/blob/f7e2b1dbf11e113e258d796288752503a5012367/packages/std/src/storage.rs#L30 - storage.set(storage_key.as_bytes(), "_".as_bytes()) + storage.set(&storage_key, "_".as_bytes()) + } + + /// revokes all permits created after and before + fn revoke_all_permits( + storage: &mut dyn Storage, + account: &str, + interval: &AllRevokedInterval, + ) -> StdResult { + // Check if *neither* `created_before` or `created_after` is supplied + // In this case, we are globally turning off all permits for this address, which + // makes it so ANY permit will be rejected. This special case does not count + // toward the maximum number of revoked intervals. + if interval.created_before.is_none() && interval.created_after.is_none() { + // get all time revocations store for this account + let all_time_revoked_store = Self::ALL_TIME_REVOKED_ALL.add_suffix(account.as_bytes()); + + // set all time revocations to true, this is idempotent + all_time_revoked_store.save(storage, &true)?; + + // return a revocation ID of "REVOKED_ALL" + return Ok(REVOKED_ALL.to_string()); + } + + // get the revocations store for this account + let all_revocations_store = Self::ALL_REVOKED_PERMITS.add_suffix(account.as_bytes()); + + // check that maximum number of revocations has not been met + if let Some(max_revocations) = Self::MAX_ALL_REVOKED_INTERVALS { + if all_revocations_store.get_len(storage)? >= max_revocations.into() { + return Err(StdError::generic_err(format!( + "Maximum number of permit revocations ({}) has been met", + max_revocations + ))); + } + } + + // get the next id store for this account + let next_id_store = Self::ALL_REVOKED_NEXT_ID.add_suffix(account.as_bytes()); + + // get the next id + let next_id = next_id_store.may_load(storage)?.unwrap_or_default(); + + // store the revocation + all_revocations_store.insert(storage, &next_id, &interval.as_stored())?; + + // increment next id + next_id_store.save(storage, &(next_id.wrapping_add(1)))?; + + Ok(format!("{}", next_id)) + } + + /// deletes the permit revocation with the given id for this account + fn delete_revocation(storage: &mut dyn Storage, account: &str, id: &str) -> StdResult<()> { + // check if this is the all time special case + if id == REVOKED_ALL { + // get all time revocations store for this account + let all_time_revoked_store = Self::ALL_TIME_REVOKED_ALL.add_suffix(account.as_bytes()); + + // set all time revocations to false + all_time_revoked_store.save(storage, &false)?; + + // return + return Ok(()); + } + + // get the revocations store for this account + let all_revocations_store = Self::ALL_REVOKED_PERMITS.add_suffix(account.as_bytes()); + + // try to convert id to a u64 + let Ok(id_str) = id.parse::() else { + return Err(StdError::generic_err("Deleted revocation id not Uint64")); + }; + + // remove the permit revocation with the given id + all_revocations_store.remove(storage, &id_str) + } + + /// lists all the revocations for the account + /// returns a vec of revocations + fn list_revocations(storage: &dyn Storage, account: &str) -> StdResult> { + // get the revocations store for this account + let all_revocations_store = Self::ALL_REVOKED_PERMITS.add_suffix(account.as_bytes()); + + // select elements and convert to AllRevocation structs + let mut result: Vec = all_revocations_store + .iter(storage)? + .filter_map(|r| match r { + Ok(r) => Some(AllRevocation { + revocation_id: format!("{}", r.0), + interval: r.1.to_humanized(), + }), + Err(_) => None, + }) + .collect(); + + // check if there is an all time revocation + if Self::is_all_time_revoked(storage, account)? { + // add that to the result + result.push(AllRevocation { + revocation_id: REVOKED_ALL.to_string(), + interval: AllRevokedInterval { + created_before: None, + created_after: None, + }, + }); + } + + Ok(result) + } + + /// returns bool if queries are all time revoked for this account + fn is_all_time_revoked(storage: &dyn Storage, account: &str) -> StdResult { + // get the all time revoked store for this account + let all_time_revoked_store = Self::ALL_TIME_REVOKED_ALL.add_suffix(account.as_bytes()); + + let result = all_time_revoked_store + .may_load(storage)? + .unwrap_or_default(); + + Ok(result) } } + +/// An interval over which all permits will be rejected +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct AllRevokedInterval { + pub created_before: Option, + pub created_after: Option, +} + +impl AllRevokedInterval { + fn as_stored(&self) -> StoredAllRevokedInterval { + StoredAllRevokedInterval { + created_before: self.created_before.map(|cb| cb.u64()), + created_after: self.created_after.map(|ca| ca.u64()), + } + } +} + +/// An interval over which all permits will be rejected +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct StoredAllRevokedInterval { + pub created_before: Option, + pub created_after: Option, +} + +impl StoredAllRevokedInterval { + fn to_humanized(&self) -> AllRevokedInterval { + AllRevokedInterval { + created_before: self.created_before.map(|cb| Uint64::from(cb)), + created_after: self.created_after.map(|ca| Uint64::from(ca)), + } + } +} + +/// Revocation id and interval data struct +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct AllRevocation { + pub revocation_id: String, + pub interval: AllRevokedInterval, +} diff --git a/packages/permit/src/structs.rs b/packages/permit/src/structs.rs index 9c03c41..6b779b9 100644 --- a/packages/permit/src/structs.rs +++ b/packages/permit/src/structs.rs @@ -6,6 +6,9 @@ use serde::{Deserialize, Serialize}; use crate::pubkey_to_account; use cosmwasm_std::{Binary, CanonicalAddr, Uint128}; +pub const BLANKET_PERMIT_TOKEN: &str = "ANY_TOKEN"; +pub const REVOKED_ALL: &str = "REVOKED_ALL"; + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct Permit { @@ -32,6 +35,8 @@ pub struct PermitParams { pub chain_id: String, #[serde(bound = "")] pub permissions: Vec, + pub created: Option, + pub expires: Option, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] @@ -162,6 +167,10 @@ impl PermitMsg { #[serde(rename_all = "snake_case")] pub struct PermitContent { pub allowed_tokens: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub created: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub expires: Option, #[serde(bound = "")] pub permissions: Vec, pub permit_name: String, @@ -171,6 +180,8 @@ impl PermitContent { pub fn from_params(params: &PermitParams) -> Self { Self { allowed_tokens: params.allowed_tokens.clone(), + created: params.created.clone(), + expires: params.expires.clone(), permit_name: params.permit_name.clone(), permissions: params.permissions.clone(), } diff --git a/packages/serialization/Cargo.toml b/packages/serialization/Cargo.toml index 2174c7f..5c6fea6 100644 --- a/packages/serialization/Cargo.toml +++ b/packages/serialization/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-serialization" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" diff --git a/packages/snip20/Cargo.toml b/packages/snip20/Cargo.toml index 3b3da51..8c164ea 100644 --- a/packages/snip20/Cargo.toml +++ b/packages/snip20/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-snip20" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" @@ -17,4 +17,4 @@ all-features = true serde = { workspace = true } schemars = { workspace = true } cosmwasm-std = { workspace = true } -secret-toolkit-utils = { version = "0.10.2", path = "../utils" } +secret-toolkit-utils = { version = "0.10.3", path = "../utils" } diff --git a/packages/snip721/Cargo.toml b/packages/snip721/Cargo.toml index 7a4f043..80157ec 100644 --- a/packages/snip721/Cargo.toml +++ b/packages/snip721/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-snip721" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" @@ -17,4 +17,4 @@ all-features = true serde = { workspace = true } schemars = { workspace = true } cosmwasm-std = { workspace = true } -secret-toolkit-utils = { version = "0.10.2", path = "../utils" } +secret-toolkit-utils = { version = "0.10.3", path = "../utils" } diff --git a/packages/storage/Cargo.toml b/packages/storage/Cargo.toml index 6924d50..3a5843d 100644 --- a/packages/storage/Cargo.toml +++ b/packages/storage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-storage" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" @@ -17,4 +17,4 @@ all-features = true serde = { workspace = true } cosmwasm-std = { workspace = true } cosmwasm-storage = { workspace = true } -secret-toolkit-serialization = { version = "0.10.2", path = "../serialization" } +secret-toolkit-serialization = { version = "0.10.3", path = "../serialization" } diff --git a/packages/utils/Cargo.toml b/packages/utils/Cargo.toml index 80783ee..9b30edb 100644 --- a/packages/utils/Cargo.toml +++ b/packages/utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-utils" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" @@ -18,3 +18,5 @@ serde = { workspace = true } schemars = { workspace = true } cosmwasm-std = { workspace = true } cosmwasm-storage = { workspace = true } + +chrono = { version = "0.4", default-features = false } \ No newline at end of file diff --git a/packages/utils/src/datetime.rs b/packages/utils/src/datetime.rs new file mode 100644 index 0000000..85b9185 --- /dev/null +++ b/packages/utils/src/datetime.rs @@ -0,0 +1,79 @@ +use chrono::{DateTime, Utc}; +use cosmwasm_std::{StdError, StdResult, Timestamp}; + +/// Converts an ISO 8601 date-time string in Zulu (UTC+0) format to a Timestamp. +/// +/// String format: {YYYY}-{MM}-{DD}T{hh}:{mm}:{ss}.{uuu}Z +/// +/// * `iso8601_str` - The ISO 8601 date-time string to convert. +/// +/// Returns StdResult +pub fn iso8601_utc0_to_timestamp(iso8601_str: &str) -> StdResult { + let Ok(datetime) = iso8601_str.parse::>() else { + return Err(StdError::generic_err("ISO 8601 string could not be parsed")); + }; + + // Verify the timezone is UTC (Zulu time) + if iso8601_str.ends_with("Z") { + Ok(Timestamp::from_seconds( + datetime.timestamp().try_into().unwrap_or_default(), + )) + } else { + Err(StdError::generic_err("ISO 8601 string not in Zulu (UTC+0)")) + } +} + +/// Converts an ISO 8601 date-time string in Zulu (UTC+0) format to seconds. +/// +/// String format: {YYYY}-{MM}-{DD}T{hh}:{mm}:{ss}.{uuu}Z +/// +/// * `iso8601_str` - The ISO 8601 date-time string to convert. +/// +/// Returns StdResult +pub fn iso8601_utc0_to_seconds(iso8601_str: &str) -> StdResult { + let Ok(datetime) = iso8601_str.parse::>() else { + return Err(StdError::generic_err("ISO 8601 string could not be parsed")); + }; + + // Verify the timezone is UTC (Zulu time) + if iso8601_str.ends_with("Z") { + let seconds = datetime.timestamp(); + if seconds < 0 { + return Err(StdError::generic_err( + "Date time before January 1, 1970 0:00:00 UTC not supported", + )); + } + Ok(seconds as u64) + } else { + Err(StdError::generic_err("ISO 8601 string not in Zulu (UTC+0)")) + } +} + +#[cfg(test)] +mod tests { + use super::iso8601_utc0_to_timestamp; + + #[test] + fn test_iso8601_utc0_to_timestamp() { + let dt_string = "2024-12-17T16:59:00.000Z"; + let timestamp = iso8601_utc0_to_timestamp(dt_string).unwrap(); + println!("{:?}", timestamp); + assert_eq!(timestamp.nanos(), 1734454740000000000); + + let dt_string = "2024-12-17T16:59:00.000"; + let timestamp = iso8601_utc0_to_timestamp(dt_string); + assert!( + timestamp.is_err(), + "datetime string without Z Ok: {:?}", + timestamp + ); + + let dt_string = "not a datetime"; + let timestamp = iso8601_utc0_to_timestamp(dt_string); + assert!( + timestamp.is_err(), + "invalid datetime string Ok: {:?}", + timestamp + ); + } +} diff --git a/packages/utils/src/lib.rs b/packages/utils/src/lib.rs index ec584aa..2610a79 100644 --- a/packages/utils/src/lib.rs +++ b/packages/utils/src/lib.rs @@ -1,9 +1,11 @@ #![doc = include_str!("../Readme.md")] pub mod calls; +pub mod datetime; pub mod feature_toggle; pub mod padding; pub mod types; pub use calls::*; +pub use datetime::*; pub use padding::*; diff --git a/packages/viewing_key/Cargo.toml b/packages/viewing_key/Cargo.toml index 7dd968f..a1f76fa 100644 --- a/packages/viewing_key/Cargo.toml +++ b/packages/viewing_key/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-viewing-key" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" @@ -20,8 +20,8 @@ base64 = "0.21.0" subtle = { version = "2.2.3", default-features = false } cosmwasm-std = { workspace = true } cosmwasm-storage = { workspace = true } -secret-toolkit-crypto = { version = "0.10.2", path = "../crypto", features = [ +secret-toolkit-crypto = { version = "0.10.3", path = "../crypto", features = [ "hash", "rand", ] } -secret-toolkit-utils = { version = "0.10.2", path = "../utils" } +secret-toolkit-utils = { version = "0.10.3", path = "../utils" }