use std::{
fmt,
time::{Duration, SystemTime},
};
use base64::Engine;
use lexe_crypto::ed25519::{self, Signed};
use lexe_std::array;
#[cfg(any(test, feature = "test-utils"))]
use proptest_derive::Arbitrary;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use super::user::{NodePkProof, UserPk};
use crate::byte_str::ByteStr;
#[cfg(any(test, feature = "test-utils"))]
use crate::test_utils::arbitrary;
#[derive(Debug, Error)]
pub enum Error {
#[error("error verifying signed bearer auth request: {0}")]
UserVerifyError(#[source] ed25519::Error),
#[error("Decoded bearer auth token appears malformed")]
MalformedToken,
#[error("issued timestamp is too far from current auth server clock")]
ClockDrift,
#[error("auth token or auth request is expired")]
Expired,
#[error("timestamp is not a valid unix timestamp")]
InvalidTimestamp,
#[error("requested token lifetime is too long")]
InvalidLifetime,
#[error("user not signed up yet")]
NoUser,
#[error("bearer auth token is not valid base64")]
Base64Decode,
#[error("bearer auth token was not provided")]
Missing,
#[error(
"auth token's granted scope ({granted:?}) is not sufficient for \
requested scope ({requested:?})"
)]
InsufficientScope { granted: Scope, requested: Scope },
}
#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum UserSignupRequestWire {
V2(UserSignupRequestWireV2),
}
#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct UserSignupRequestWireV2 {
pub v1: UserSignupRequestWireV1,
pub partner: Option<UserPk>,
}
#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct UserSignupRequestWireV1 {
pub node_pk_proof: NodePkProof,
#[cfg_attr(
any(test, feature = "test-utils"),
proptest(strategy = "arbitrary::any_option_string()")
)]
_signup_code: Option<String>,
}
impl UserSignupRequestWireV1 {
pub fn new(node_pk_proof: NodePkProof) -> Self {
Self {
node_pk_proof,
_signup_code: None,
}
}
}
#[derive(Clone, Debug)]
pub struct BearerAuthRequest {
pub request_timestamp_secs: u64,
pub lifetime_secs: u32,
pub scope: Option<Scope>,
}
#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum BearerAuthRequestWire {
V1(BearerAuthRequestWireV1),
V2(BearerAuthRequestWireV2),
}
#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct BearerAuthRequestWireV1 {
request_timestamp_secs: u64,
lifetime_secs: u32,
}
#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct BearerAuthRequestWireV2 {
v1: BearerAuthRequestWireV1,
scope: Option<Scope>,
}
#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum Scope {
All,
NodeConnect,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BearerAuthResponse {
pub bearer_auth_token: BearerAuthToken,
}
#[derive(Clone, Serialize, Deserialize)]
#[cfg_attr(any(test, feature = "test-utils"), derive(Eq, PartialEq))]
pub struct BearerAuthToken(pub ByteStr);
impl fmt::Debug for BearerAuthToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("BearerAuthToken(..)")
}
}
#[derive(Clone)]
pub struct TokenWithExpiration {
pub token: BearerAuthToken,
pub expiration: Option<SystemTime>,
}
impl UserSignupRequestWire {
pub fn node_pk_proof(&self) -> &NodePkProof {
match self {
UserSignupRequestWire::V2(v2) => &v2.v1.node_pk_proof,
}
}
pub fn partner(&self) -> Option<&UserPk> {
match self {
UserSignupRequestWire::V2(v2) => v2.partner.as_ref(),
}
}
}
impl ed25519::Signable for UserSignupRequestWire {
const DOMAIN_SEPARATOR: [u8; 32] =
array::pad(*b"LEXE-REALM::UserSignupRequestWir");
}
impl UserSignupRequestWireV1 {
pub fn deserialize_verify(
serialized: &[u8],
) -> Result<Signed<Self>, Error> {
ed25519::verify_signed_struct(ed25519::accept_any_signer, serialized)
.map_err(Error::UserVerifyError)
}
}
impl ed25519::Signable for UserSignupRequestWireV1 {
const DOMAIN_SEPARATOR: [u8; 32] =
array::pad(*b"LEXE-REALM::UserSignupRequest");
}
impl From<UserSignupRequestWireV1> for UserSignupRequestWireV2 {
fn from(v1: UserSignupRequestWireV1) -> Self {
Self { v1, partner: None }
}
}
impl BearerAuthRequest {
pub fn new(
now: SystemTime,
token_lifetime_secs: u32,
scope: Option<Scope>,
) -> Self {
Self {
request_timestamp_secs: now
.duration_since(SystemTime::UNIX_EPOCH)
.expect("Something is very wrong with our clock")
.as_secs(),
lifetime_secs: token_lifetime_secs,
scope,
}
}
pub fn request_timestamp(&self) -> Result<SystemTime, Error> {
let t_secs = self.request_timestamp_secs;
let t_dur_secs = Duration::from_secs(t_secs);
SystemTime::UNIX_EPOCH
.checked_add(t_dur_secs)
.ok_or(Error::InvalidTimestamp)
}
}
impl From<BearerAuthRequestWire> for BearerAuthRequest {
fn from(wire: BearerAuthRequestWire) -> Self {
match wire {
BearerAuthRequestWire::V1(v1) => Self {
request_timestamp_secs: v1.request_timestamp_secs,
lifetime_secs: v1.lifetime_secs,
scope: None,
},
BearerAuthRequestWire::V2(v2) => Self {
request_timestamp_secs: v2.v1.request_timestamp_secs,
lifetime_secs: v2.v1.lifetime_secs,
scope: v2.scope,
},
}
}
}
impl From<BearerAuthRequest> for BearerAuthRequestWire {
fn from(req: BearerAuthRequest) -> Self {
Self::V2(BearerAuthRequestWireV2 {
v1: BearerAuthRequestWireV1 {
request_timestamp_secs: req.request_timestamp_secs,
lifetime_secs: req.lifetime_secs,
},
scope: req.scope,
})
}
}
impl BearerAuthRequestWire {
pub fn deserialize_verify(
serialized: &[u8],
) -> Result<Signed<Self>, Error> {
ed25519::verify_signed_struct(ed25519::accept_any_signer, serialized)
.map_err(Error::UserVerifyError)
}
}
impl ed25519::Signable for BearerAuthRequestWire {
const DOMAIN_SEPARATOR: [u8; 32] =
array::pad(*b"LEXE-REALM::BearerAuthRequest");
}
impl BearerAuthToken {
pub fn encode_from_raw_bytes(signed_token_bytes: &[u8]) -> Self {
let b64_token = base64::engine::general_purpose::URL_SAFE_NO_PAD
.encode(signed_token_bytes);
Self(ByteStr::from(b64_token))
}
pub fn decode_into_raw_bytes(&self) -> Result<Vec<u8>, Error> {
Self::decode_slice_into_raw_bytes(self.0.as_bytes())
}
pub fn decode_slice_into_raw_bytes(bytes: &[u8]) -> Result<Vec<u8>, Error> {
base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(bytes)
.map_err(|_| Error::Base64Decode)
}
}
impl fmt::Display for BearerAuthToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0.as_str())
}
}
#[cfg(any(test, feature = "test-utils"))]
mod arbitrary_impl {
use proptest::{
arbitrary::{Arbitrary, any},
strategy::{BoxedStrategy, Strategy},
};
use super::*;
impl Arbitrary for BearerAuthToken {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
any::<Vec<u8>>()
.prop_map(|bytes| {
BearerAuthToken::encode_from_raw_bytes(&bytes)
})
.boxed()
}
}
}
impl Scope {
pub fn has_permission_for(&self, requested_scope: &Self) -> bool {
let granted_scope = self;
match (granted_scope, requested_scope) {
(Scope::All, _) => true,
(Scope::NodeConnect, Scope::All) => false,
(Scope::NodeConnect, Scope::NodeConnect) => true,
}
}
}
#[cfg(test)]
mod test {
use base64::Engine;
use lexe_hex::hex;
use super::*;
use crate::test_utils::roundtrip::{
bcs_roundtrip_ok, bcs_roundtrip_proptest, signed_roundtrip_proptest,
};
#[test]
fn test_user_signup_request_wire_canonical() {
bcs_roundtrip_proptest::<UserSignupRequestWire>();
}
#[test]
fn test_user_signed_request_wire_sign_verify() {
signed_roundtrip_proptest::<UserSignupRequestWire>();
}
#[test]
fn test_bearer_auth_request_wire_canonical() {
bcs_roundtrip_proptest::<BearerAuthRequestWire>();
}
#[test]
fn test_bearer_auth_request_wire_sign_verify() {
signed_roundtrip_proptest::<BearerAuthRequestWire>();
}
#[test]
fn test_bearer_auth_request_wire_snapshot() {
let input = "00d20296490000000058020000";
let req = BearerAuthRequestWire::V1(BearerAuthRequestWireV1 {
request_timestamp_secs: 1234567890,
lifetime_secs: 10 * 60,
});
bcs_roundtrip_ok(&hex::decode(input).unwrap(), &req);
let input = "01d2029649000000005802000000";
let req = BearerAuthRequestWire::V2(BearerAuthRequestWireV2 {
v1: BearerAuthRequestWireV1 {
request_timestamp_secs: 1234567890,
lifetime_secs: 10 * 60,
},
scope: None,
});
bcs_roundtrip_ok(&hex::decode(input).unwrap(), &req);
let input = "01d202964900000000580200000101";
let req = BearerAuthRequestWire::V2(BearerAuthRequestWireV2 {
v1: BearerAuthRequestWireV1 {
request_timestamp_secs: 1234567890,
lifetime_secs: 10 * 60,
},
scope: Some(Scope::NodeConnect),
});
bcs_roundtrip_ok(&hex::decode(input).unwrap(), &req);
}
#[test]
fn test_auth_scope_canonical() {
bcs_roundtrip_proptest::<Scope>();
}
#[test]
fn test_auth_scope_snapshot() {
let input = b"\x00";
let scope = Scope::All;
bcs_roundtrip_ok(input, &scope);
let input = b"\x01";
let scope = Scope::NodeConnect;
bcs_roundtrip_ok(input, &scope);
}
#[test]
fn test_user_signup_request_wire_v1_snapshot() {
let b64 = base64::engine::general_purpose::STANDARD;
let input_with_code = "AqqWkI6A9EExJ9suasa1a4Vte7dSztOpSsGNVUHClpLb\
RjBEAiANgXon77EhDl3dq6ZASg9u/xjS3OET2um+OA6+/58UmQIgEYmJGcNNWfMy\
npScmW9joOortpvHul9bHyojSj3Im70BCUFCQ0QtMTIzNA==";
let bytes_with_code = b64.decode(input_with_code).unwrap();
let req: UserSignupRequestWireV1 =
bcs::from_bytes(&bytes_with_code).unwrap();
assert!(req.node_pk_proof.verify().is_ok());
let input_none = "AqqWkI6A9EExJ9suasa1a4Vte7dSztOpSsGNVUHClpLbRjBE\
AiANgXon77EhDl3dq6ZASg9u/xjS3OET2um+OA6+/58UmQIgEYmJGcNNWfMynpSc\
mW9joOortpvHul9bHyojSj3Im70A";
let bytes_none = b64.decode(input_none).unwrap();
let req: UserSignupRequestWireV1 =
bcs::from_bytes(&bytes_none).unwrap();
assert!(req.node_pk_proof.verify().is_ok());
}
}