wnfs-wg / rs-wnfs

Rust implementation of the WebNative FileSystem (WNFS) specification
https://github.com/wnfs-wg
Apache License 2.0
147 stars 24 forks source link

exposing a constructor for PrivateRef #86

Closed ehsan6sha closed 2 years ago

ehsan6sha commented 2 years ago

NB: Feature requests will only be considered if they solve a pain

Summary: Exposing a constructor for PrivateRef

Problem

The only way to obtain the root private key of WNFS-RS and use it on antoher device(login), right now, is to call .get_private_ref() on the directory on the first device and then serialize it and move it over to the other device. This doesn't allow deriving it deterministically. In both the current version of fs used in webnative and WNFS go there is a constructor interface available to the programmer that can feed the private key that WNFS uses for the root. However, it is not exposed in WNFS rust version. exposing it allows application developer to feed the private key from their own login process (whatever it is) and it is deterministic, and based on users' login and not random.

Impact

We can then decrypt WNFS tree on any device that the user has without needing to transfer the keys as long as users logs in on different devices and the program can create the same keypair. It gives more flexibility to the application developer on what approach they want and also gives the user more control over which private key to use. If developer wants to feed a deterministic key, they can, if they want a random key, they still can do it.

Solution

Exposing the constructor for PrivateRef

Detail

Is your feature request related to a problem? Please describe. It's frustrating where the login cannot be linked to WNFS encryption and we still need to transfer the private key between multiple devices even when the user completes a login process.

Describe the solution you'd like Like webnative js version and go version, I want to be able to construct the Private_ref myself from a keypair or seed hash (both work).

Describe alternatives you've considered The only way to obtain the root private key of WNFS-RS and use it on antoher device(login), right now, is to call .get_private_ref() on the directory on the first device and then serialize it and move it over to the other device. This doesn't allow deriving it deterministically.

Additional context It is also discussed on IPFS Discord on #wnfs with Matheus and Boris

matheus23 commented 2 years ago

Thanks for filing this @ehsan6sha :pray: The proposed solution sounds great. If you want this to happen sooner, please feel free to open a PR that makes this change, perhaps additionally adds a doc comment for the constructor (if not present already), and I'd be happy to have that merged soon! :)

matheus23 commented 2 years ago

Actually there's already PrivateRef::from_revision_key. It's public, but not documented, and I think it may just not have been released yet.

I think what you want to do is deterministically derive the root PrivateRef from a shared secret on two devices.

You'd both have to deterministically generate the inumber and Ratchet for the root PrivateDirectory.

It'd work like this:

fn derive_private_ref(shared_secret: [u8; 32]) -> PrivateRef {
    // blake3 or some other keyed hashing function
    let inumber = blake3::keyed_hash(&shared_secret, b"derived inumber");
    let ratchet_seed = blake3::keyed_hash(&shared_secret, b"derived ratchet seed");
    let ratchet = Ratchet::zero(ratchet_seed);
    let revision_key = RevisionKey::from(&ratchet);
    let mut namefilter = Namefilter::default();
    namefilter.add(&inumber);
    namefilter.add(&revision_key.0.as_bytes());
    namefilter.saturate();
    let saturated_name_hash = Sha3_256::hash(&namefilter);
    PrivateRef::from_revision_key(saturated_name_hash, revision_key)
}

The remaining problem is that this requires the original PrivateDirectory to have been generated with the same inumber and revision_key. That's not possible today unless we expose some operators that allow this. We could work on that!

ehsan6sha commented 2 years ago

Actually there's already PrivateRef::from_revision_key. It's public, but not documented, and I think it may just not have been released yet.

I think what you want to do is deterministically derive the root PrivateRef from a shared secret on two devices.

You'd both have to deterministically generate the inumber and Ratchet for the root PrivateDirectory.

It'd work like this:

fn derive_private_ref(shared_secret: [u8; 32]) -> PrivateRef {
    // blake3 or some other keyed hashing function
    let inumber = blake3::keyed_hash(&shared_secret, b"derived inumber");
    let ratchet_seed = blake3::keyed_hash(&shared_secret, b"derived ratchet seed");
    let ratchet = Ratchet::zero(ratchet_seed);
    let revision_key = RevisionKey::from(&ratchet);
    let mut namefilter = Namefilter::default();
    namefilter.add(&inumber);
    namefilter.add(&revision_key.0.as_bytes());
    namefilter.saturate();
    let saturated_name_hash = Sha3_256::hash(&namefilter);
    PrivateRef::from_revision_key(saturated_name_hash, revision_key)
}

The remaining problem is that this requires the original PrivateDirectory to have been generated with the same inumber and revision_key. That's not possible today unless we expose some operators that allow this. We could work on that!

So basically we have a public function available to create the PrivateRef from an external key here and now we need to do modifications to feed this PrivateRef to the PrivateDirectory? I think needed changes would be in the methods below, right? https://github.com/wnfs-wg/rs-wnfs/blob/80dbe82dd41e9eebda77960b930458d4d1feeb69/wnfs/src/private/node.rs#L500

https://github.com/wnfs-wg/rs-wnfs/blob/80dbe82dd41e9eebda77960b930458d4d1feeb69/wnfs/src/private/directory.rs#L94

matheus23 commented 2 years ago

So basically we have a public function available to create the PrivateRef from an external key here and now we need to do modifications to feed this PrivateRef to the PrivateDirectory? I think needed changes would be in the methods below, right?

Not quite.

  1. We will probably only add some methods to support your use case and
  2. It's not possible to create a PrivateDirectory that fits a given PrivateRef. You need to create both from a common seed.
appcypher commented 2 years ago

@matheus23 If the private key can be hashed, we can expose the seed part of the ratchet generation to make it available for deterministic key gen cases. I believe we wanted to do this at one point but it didn't make it in after some refactor.

impl PrivateNodeHeader {
    pub(crate) fn with_seed<R: RngCore>(parent_bare_name: Namefilter, ratchet_seed: HashOutput, rng: &mut R) -> Self {
        let inumber = utils::get_random_bytes::<HASH_BYTE_SIZE>(rng);
        Self {
            bare_name: {
                let mut namefilter = parent_bare_name;
                namefilter.add(&inumber);
                namefilter
            },
            ratchet: Ratchet::zero(ratchet_seed),
            inumber,
        }
    }

    // ...
}
impl PrivateDirectory {
    pub fn with_seed<R: RngCore>(parent_bare_name: Namefilter, time: DateTime<Utc>, ratchet_seed: HashOutput, rng: &mut R) -> Self {
        Self {
            version: Version::new(0, 2, 0),
            header: PrivateNodeHeader::with_seed(parent_bare_name, ratchet_seed, rng),
            metadata: Metadata::new(time),
            entries: BTreeMap::new(),
        }
    }

    // ...
}
matheus23 commented 2 years ago

Yeah this is practically almost what we want I think. The only thing missing is that the inumber shouldn't be random. Technically you could achieve that by just passing in the same deterministic impl RngCore to both methods, but at this point it may be easier to just pass in the inumber directly.

appcypher commented 2 years ago

Yeah passing in a deterministic Rng would work too but it is more work than necessary and can easily break with future changes depending on how we use rng. I also considered exposing the inumber but a user of the interface might not pay too much attention to its randomness. Maybe a warning in the doc that screams the importance of it being crypto secure to avoid collision.