2
\$\begingroup\$

This is a very stripped-down version of a crypto library I am writing. There are cryptographic algorithms like RSA, DSA, ECDSA, EdDSA, ... which all have a similar pattern of having a private key, a public key, a signature and operations like sign data with a private key that gives a signature result, verify signature with a public key that gives a bool result.

There are 3 conflicting requirements:

  • The library integrators should be able to implement generic algorithms on top of the library types which are type safe and use only types from 1 crypto
  • Each cryptography should have its own type family. When the user tries to verify an RSA signature with an EdDSA public key, they should get an error explaining they mixed up different cryptos (so I need type safety)
  • The library should be able to deserialize input and for example create a strongly typed EdDSA public key without knowing in advance the input will belong to the EdDSA type family
trait Family {
    type T1: T1<Self>;
    type T2: T2<Self>;
    fn x() -> Self::T1;
}

trait T1<F: Family + ?Sized> {
    fn y(&self) -> F::T2;
}

trait T2<F: Family + ?Sized> {
    fn z(&self) -> F::T1;
}

//----------------------------------------------------------------------

struct FamilyImplA {}

impl Family for FamilyImplA {
    type T1 = T1ImplA;
    type T2 = T2ImplA;
    fn x() -> T1ImplA {
        println!("FamilyImplA::x was called");
        T1ImplA {}
    }
}

struct T1ImplA {}

impl T1<FamilyImplA> for T1ImplA {
    fn y(&self) -> T2ImplA {
        println!("T1ImplA::y was called");
        T2ImplA {}
    }
}

struct T2ImplA {}

impl T2<FamilyImplA> for T2ImplA {
    fn z(&self) -> T1ImplA {
        println!("T2ImplA::z was called");
        T1ImplA {}
    }
}

//----------------------------------------------------------------------

struct FamilyImplB {}

impl Family for FamilyImplB {
    type T1 = T1ImplB;
    type T2 = T2ImplB;
    fn x() -> T1ImplB {
        println!("FamilyImplB::x was called");
        T1ImplB {}
    }
}

struct T1ImplB {}

impl T1<FamilyImplB> for T1ImplB {
    fn y(&self) -> T2ImplB {
        println!("T1ImplB::y was called");
        T2ImplB {}
    }
}

struct T2ImplB {}

impl T2<FamilyImplB> for T2ImplB {
    fn z(&self) -> T1ImplB {
        println!("T2ImplB::z was called");
        T1ImplB {}
    }
}

//----------------------------------------------------------------------

use std::any::Any;

enum Discriminator {
    A,
    B,
}

struct ErasedFamily {}

impl ErasedFamily {
    fn x_a() -> ErasedT1 {
        ErasedT1 { d:Discriminator::A, t1:Box::new( FamilyImplA::x() ) }
    }
    fn x_b() -> ErasedT1 {
        ErasedT1 { d:Discriminator::B, t1:Box::new( FamilyImplB::x() ) }
    }
}

impl Family for ErasedFamily {
    type T1 = ErasedT1;
    type T2 = ErasedT2;
    fn x() -> ErasedT1 {
        // Cannot decide on what to create without a discriminator
        unimplemented!()
    }
}

struct ErasedT1 {
    d: Discriminator,
    t1: Box<Any>,
}

impl T1<ErasedFamily> for ErasedT1 {
    fn y(&self) -> ErasedT2 {
        match self.d {
            Discriminator::A => {
                let t1 = self.t1.downcast_ref::<T1ImplA>().unwrap();
                let r = t1.y();
                ErasedT2 { d:Discriminator::A, t2:Box::new( r ) }
            },
            Discriminator::B => {
                let t1 = self.t1.downcast_ref::<T1ImplB>().unwrap();
                let r = t1.y();
                ErasedT2 { d:Discriminator::B, t2:Box::new( r ) }
            }
        }
    }
}

struct ErasedT2 {
    d: Discriminator,
    t2: Box<Any>,
}

impl T2<ErasedFamily> for ErasedT2 {
    fn z(&self) -> ErasedT1 {
        match self.d {
            Discriminator::A => {
                let t2 = self.t2.downcast_ref::<T2ImplA>().unwrap();
                let r = t2.z();
                ErasedT1 { d:Discriminator::A, t1:Box::new( r ) }
            },
            Discriminator::B => {
                let t2 = self.t2.downcast_ref::<T2ImplB>().unwrap();
                let r = t2.z();
                ErasedT1 { d:Discriminator::B, t1:Box::new( r ) }
            }
        }
    }
}

//----------------------------------------------------------------------

fn generic<F: Family>(t1: F::T1) -> F::T1 {
    t1.y().z()
}

fn main() {
    let _t1a = generic::<FamilyImplA>(FamilyImplA::x());
    let _t1b = generic::<FamilyImplB>(FamilyImplB::x());

    let _t1ea = generic::<ErasedFamily>(ErasedFamily::x_a());
    let _t1eb = generic::<ErasedFamily>(ErasedFamily::x_b());
}

Playground

In production, I have declarative macros for type erasure and reification at the moment to avoid duplication. Is there another way to do this in Rust 1.32.0?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.