Store / access private data linked to specific user

Indeed, wow! Many thanks @hunterlester for putting this together! Took me a little while to “decypher” this code :wink: Am also still not that familiar with these promises. However this helped me a lot to get a better grasp of how private MDs work!

One last, maybe dumb, question: Would this value / MD which we stored with the code above be only accessible / decryptable by the user who stored it? Because it’s a Private MD I would expect this, but am not sure. I also couldn’t verify it because I have only one account.

2 Likes

@hunterlester, It seems to me you won’t be able to decrypt what you are storing there.
I think you should generate an encryption key pair with generateEncKeyPair and use the secret encryption key instead of using the app’s public sign key, i.e. changing window.safeCrypto.getAppPubSignKey for window.safeCrypto.generateEncKeyPair and use window.safeCryptoKeyPair.getSecEncKey(encKeyPairHandle).

1 Like

I just tested it and was able to decrypt the value. I think the point was to have a static key instead of a random one, which can’t be stored permanently in a web app.

2 Likes

Ohh ok, so I guess that’s because we are then using symmetric encryption in the Private MutableData. I was wrong then somehow assuming there was an asymmetric encryption being used, cool, thanks @Mindphreaker for testing and clarifying.

2 Likes

You bring up a good question though. So far I have found that the private MD can only be decrypted by the application that created and encrypted it’s entries, since the public signing key is tied to the application. Easy to verify by changing a little information in the appInfo object and trying to decrypt an entry.

What I want to confirm, by creating another account, is if an application public signing key is also tied to the user’s authenticator account? If not, wouldn’t I simply be able to create an application with the same appInfo as you, for example, in order to obtain an identical signing key for decryption, therefore compromising privacy?

Are you on test network 18?

I want to perform a test. I’ll create an explicitly named private MD using public signing key. I’ll then pass you the exact appInfo object that i used to create application, as well as the name of the private MD for you to hash, so that you can verify if it generates an identical public signing key that you can use to decrypt my entry.

If it is the case, how likely would it be for a hacker to be able to obtain the name of the private MD in addition to the appInfo used to generate the public signing key? I don’t know how to quantify that.

Also, is this a case where we need to be asking for a secret key that is unique solely to our user authenticator account?

cc: @bochaco

CORRECTION:
Thanks to @bochaco for bringing up that we should be using getAppPubEncKey instead of getAppPubSignKey. I overlooked it because I was previously experimenting with generating key pairs, which are unique each time generated and meant for asymmetric encryption, not symmetric as we are intending.
The question still stands though and I want to see if I can replicate getAppPubEncKey with another user account.

4 Likes

The attacker would just have to know that a user was using a certain app, right?

2 Likes

Does it depend also on the fake app persuading the user to authorise it - and therefore also to run it?

This and the above points are going to be really important aren’t they! We need to put together a guide on recommended procedures and what they do /don’t guarantee.

3 Likes

Yes, just send me a PM.

This would indeed be interesting. If it’s not the case I’m really starting to think that we need something like a getUserEncKey method. Which brings me back to my previous question:

1 Like

As I understand, reading the MD RFC, the READ of and MD, even private, is not blocked so anybody can ask for this data if they know the direction and TypeTag. The protection for this data, if private, is the symmetric or asymmetric encryption. So I you share with somebody the relevant information (direction, TypeTag and decrypt Key) I see no impediment to read this data.

2 Likes

@Mindphreaker While digging around in core libraries I saw that ClientKeys struct has a symmetric encryption key. Will follow this path to see where a client’s symmetric encryption key is used. I wonder if this is a candidate, and if it would be sensible, for exposure in our API’s.
Check it out: https://github.com/maidsafe/safe_client_libs/blob/master/safe_core/src/client/account.rs#L133

3 Likes

I continued to experiment with your example from above @hunterlester and it seems, that I’m able to fetch the stored value but only until I reauthenticate the app (e.g. reload the tab and reauthenticate). After reauthentication I noticed two things:

  1. The generated nonce is different, however the returned mdHandle from newPrivate still seems to be valid because the item count matches. I don’t know if the nonce passed to newPrivate has an impact on de-/encryption.
  2. When calling encryptKey() I get a different return value compared to the time before reauthentication. This of course explains why I can’t fetch the stored value. As long as I don’t reload the page and don’t reauthenticate the encryptKey() returns the same value.

Can you confirm this or do you know why this is happening?

1 Like

I updated my code to use getAppPubEncKey instead of getAppPubSignKey.
This is working nicely.
I ran this code, then reloaded page and reauthenticated my application, retrieved the same private MD, and was able to successfully decrypt using the app’s public encryption key.

let appInfo = {
  id: 'net.maidsafe.api_playground.webclient.10',
  name: 'SAFE web API playground',
  vendor: 'MaidSafe Ltd.',
  scope: null
};

let encryptedEntries = {};

function decryptQueue(mdHandle, i) {
  let entries = Object.keys(encryptedEntries);

  if ( i === entries.length ) {
    console.log('Decrypted entry: ', encryptedEntries);
    return;
  }

  window.safeMutableData.decrypt(mdHandle, encryptedEntries[entries[i]].buf).then(decryptedValue => {
    encryptedEntries[entries[i]] = String.fromCharCode.apply(null, new Uint8Array(decryptedValue));
    decryptQueue(mdHandle, i + 1);
  })
}

window.safeApp.initialise(appInfo).then(appHandle => window.safeApp.authorise(appHandle, {
        _public: ['Read', 'Insert', 'Update', 'Delete'],
        _publicNames: ['Read', 'Insert', 'Update', 'Delete']
      }, { own_container: true })
      .then(authUri => window.safeApp.connectAuthorised(appHandle, authUri)))
      .then(appHandle => window.safeCrypto.sha3Hash(appHandle, 'explicitly_named_md')
        .then(hashedString => ({hashedString, appHandle})))
      .then(({hashedString, appHandle}) => window.safeCrypto.getAppPubEncKey(appHandle)
        .then(encKeyHandle => window.safeCryptoPubEncKey.getRaw(encKeyHandle).then(rawPubEncKey => ({rawPubEncKey, hashedString, appHandle}))))
      .then(({rawPubEncKey, hashedString, appHandle}) => window.safeCrypto.generateNonce(appHandle).then(nonce => ({rawPubEncKey, hashedString, appHandle, nonce})))
      .then(({rawPubEncKey, hashedString, appHandle, nonce}) => window.safeMutableData.newPrivate(appHandle, hashedString, 15001, rawPubEncKey.buffer, nonce.buffer)
        .then(mdHandle => window.safeMutableData.encryptKey(mdHandle, 'key1').then(encryptedKey => ({encryptedKey}))
          .then(({encryptedKey}) => window.safeMutableData.encryptValue(mdHandle, 'encrypted value').then(encryptedValue => ({encryptedKey, encryptedValue}))
            .then(({encryptedKey, encryptedValue}) => window.safeMutableData.quickSetup(mdHandle)
              .then(() => window.safeMutableData.newMutation(appHandle)
                .then(mutationHandle => window.safeMutableDataMutation.insert(mutationHandle, encryptedKey, encryptedValue)
                  .then(() => window.safeMutableData.applyEntriesMutation(mdHandle, mutationHandle).then(() => ({mdHandle, appHandle, hashedString})))
                )
              )
            )
          )
        )
      )
      .then(({mdHandle, appHandle, hashedString}) => window.safeMutableData.getEntries(mdHandle)
        .then(entriesHandle => window.safeMutableDataEntries.forEach(entriesHandle, (k, v) => {
        let key = String.fromCharCode.apply(null, k);
        let value = String.fromCharCode.apply(null, new Uint8Array(v.buf));

        console.log('Key: ', key,',', 'Value: ', value);
      }).then(() => ({appHandle, hashedString}))
    ))
    .then(({appHandle, hashedString}) => window.safeCrypto.getAppPubEncKey(appHandle)
      .then(encKeyHandle => window.safeCryptoPubEncKey.getRaw(encKeyHandle).then(rawPubEncKey => ({rawPubEncKey, hashedString, appHandle})))
      .then(({rawPubEncKey, hashedString, appHandle}) => window.safeCrypto.generateNonce(appHandle).then(nonce => ({rawPubEncKey, hashedString, appHandle, nonce})))
      .then(({rawPubEncKey, hashedString, appHandle, nonce}) => window.safeMutableData.newPrivate(appHandle, hashedString, 15001, rawPubEncKey.buffer, nonce.buffer))
      .then(mdHandle => window.safeMutableData.getEntries(mdHandle)
        .then(entriesHandle => window.safeMutableDataEntries.forEach(entriesHandle, (k, v) => {
          Object.defineProperty(encryptedEntries, k, {value: v, writable: true, enumerable: true, configurable: true});
        })).then(() => {
          console.log(mdHandle);
          decryptQueue(mdHandle,0);
        })
      )
    )

Nonce should always be unique for security purposes. I’m just learning about how this works. Thanks to @dirvine for passing along this link for education: https://www.lvh.io/posts/nonce-misuse-resistance-101.html

The critical security property of a nonce is that it’s never repeated under the same key. You can remember this by the mnemonic that a nonce is a “number used once”. If you were to repeat the nonce, the keystream would also repeat. That means that an attacker can take the two ciphertexts and XOR them to compute the XOR of the plaintexts.

5 Likes

Doesn’t SAFE use EdDSA, which uses a deterministic nonce?

Also,

  • Can someone explain why window.safeMutableData.newRandomPrivate does not have the same cryptographic parameters as window.safeMutableData.newPrivate? Or put differently: what cryptographic secret encryption key and nonce does newRandomPrivate use so I can pass them to newPrivate another time?
    I expected the difference between those functions just to be about the address being picked or not.

  • What exactly does ‘private access’ mean in the context of newRandomPrivate/newPrivate? I’m not able to find a description of this sort of terminology in the RFCs.

2 Likes

In the example getAppPubEncKey are used in symmetric encryption so, for security reasons, is better never reuse nonces. The EdDSA are for asymmetric encryption.

1 Like

The determinism of Nonce doesn’t matter - they should not be repeated for a key. If you use something deterministic then you don’t need to store it along with cipher text. If random then of you would store it along with cipher text. The nonce is stored plain-texted - so when you fetch data it will likely be combination of plain-texted Nonce and cipher text; then extract the Nonce and use it with the sec-sym-key to decrypt ciphertext.
We use this for our Nonce currently.

For MData: Private means the contents are encrypted, public means otherwise.
MDataInfo is a ptr to MData - containing sufficient info to fetch and read/update MData. It has XorName, type-tag, and enc-keys. _random() function is to be used if you want everything random (you need to provide the type-tag though).
The other function _new() is used if you want to provide your stuff. E.g. if you are designing an app where XorName is deterministic depending on user Input etc. If you want deterministic XorName but random keys, there are crypto functions in safe-app rust which you can use to obtain random keys and nonces and supply that along with your chosen XorName. It’s just a matter of mix and match.

6 Likes

I agree with that, but if a nonce is not provided to fn enc_entry_key then a random one is generated which is not stored:

https://github.com/maidsafe/safe_client_libs/blob/a490682b3cc4e0230dc79006a38bdc74c23670bf/safe_core/src/client/mdata_info.rs#L257

So, I don’t understand how the data can be decrypted in this case.

The answer is explained in one of the posts that you ‘liked’ :stuck_out_tongue: - the 2nd point here

So entry-key encryption uses deterministic but unique nonce for all entries as opposed to entry-value which uses completely random nonce (and hence needs to be stored in the network along with the cipher text). With this clever trick we can get the great benefit of indexing. So if you know the MDataInfo (which you would need to know anyway to address a MData) and if you know your entry-key (which is why MData was invented in the first place - it can be used as a key-value store) - then all you need to do is create the deterministic nonce locally (using sha3(MDataInfo::nonce + entry-key)), use that along with your secret key (MDataInfo::key) to encrypt the entry-key get the cipher-text=Vec and ask for entry-value for that entry-key.

If entry-key nonce was also absolutely random like entry-value’s nonce, then it would have to be stored along with cipher-text’ed entry-key in the network and would be impossible to index it as key-value. You would then need to fetch all entry-key,value pairs and decode every entry-key until you get the expected one and only then see the value.

6 Likes

My concern was only about an implementation detail of the function I highlighted: the third parameter is an optional nonce. If none is provided by the caller then a random one is generated locally and is not stored globally and is not returned to the caller. So I thought it was lost.

My error was that I didn’t pay attention that the nonce was serialized together with the ciphered text. Everything is ok, sorry for the disturbance.

3 Likes

Ah nw - probably worth bringing to notice is this PR, still in review. That removes the ‘optionality’ for Nonce if the Sec-Key is provided. It was optional for flexibility if in some case a user wanted an MDataInfo created with Secret-Key but no Nonce even for entry-keys (so that random one was used which would lead to inability to index) - but so far there has been no such use case and it’s just bloating logic with permutation. So we just decided to remove that (it was an option inside an option as you can see) and add it back only when required - the changes are internal and does not affect the API though.

4 Likes

This topic was automatically closed after 60 days. New replies are no longer allowed.