Skip to content

Instantly share code, notes, and snippets.

@hanwencheng
Last active March 11, 2025 07:10
Show Gist options
  • Save hanwencheng/736cc28834a06d9cbd4198c03a7c6033 to your computer and use it in GitHub Desktop.
Save hanwencheng/736cc28834a06d9cbd4198c03a7c6033 to your computer and use it in GitHub Desktop.
Parity-Signer Secure Key Storage Proposal

Parity-Signer Secure Key Storage Proposal

Here I conclude the recent research and discussions with the storage of the seed/private key in parity-signer.

Current Implementation

secureStorage1 Currently, we require a user to input a pin code for encrypting the seed, and then put the encrypted seed together with other account data into a secure store. The secure store then uses the native KeysStore or KeyChain to store the encrypted account data.

Possible improvement

secureStorage2

After discussion with @kirushik and @geastwood, it has the following improvement potential to achieve a higher security level than industry standard.

1. Use a hash of the pin for encryption

Here the way we used to encrypt the pin data is from EthSign Rust library, I am not sure how it works, hope @maciejhirsz could give some hints on that. We could use Argon2 to create a hash of the pin, and use that hash to encrypt the seed. With Argon2, we can efficiently prevent brute force attack.

2. Use FaceID, TouchID for the access of secure storage

To store the encrypted account data into the keychain, we may add an extra biometric authentication layer. One thing needs to be noticed here is that apple use a so-called Local Authentication Framework to bridging the biometric data in Secure Enclave, but that does not mean that the encryption key is also kept there, it is stored in the Keychain. This is also the behavior what react-native-keychain library does.

3. Use hardware backed Keystore

iOS devices with A7 (first on iPhone6) or newer chips contain security chips but not all Android devices have the same thing. The good news is Google enforced new devices supporting Android 7 must have a hardware-backed security element.

on iOS Secure Enclave

The Secure Enclave is a hardware-based key manager that’s isolated from the main processor to provide an extra layer of security. When you store a private key in the Secure Enclave, you never actually handle the key, making it difficult for the key to becoming compromised.

AFAIK, these Secure Enclave is not implemented on any react-native keychain related libraries, I suggest we contribute to react-native-keychain.

on Android

The availability of a trusted execution environment in a system on a chip (SoC) offers an opportunity for Android devices to provide hardware-backed, strong security services to the Android OS, to platform services, and even to third-party apps

The hardware implementation on the Android maybe variant according to the device provider.(Need to use KeyInfo.isInsideSecurityHardware function to verify the storage location). So our priority is to implement it on iOS.

iOS implementation Proposal

Consider the user experience, that two authentication steps in the image2 maybe too much, And the high complexity may increase the risk at the same time,

secureStorage3

So the proposal includes two parts as highlighted with the red rectangles:

4.5 Use TouchID Authentication, and encryption key in Secure Enclave instead of Pincode

We may rely on the hardware-backed Key store instead of Argon2 hash and encryption function. They provider similar brute force prevention whereas the secure enclave could prevent software-related risks. Besides, the TouchID / FaceID supposed to be more complicated to compromise than the Pincode, if related user data are exposed, plus the Biometric data's mathematical presentation is also stored in secure enclave according to here.

6. Eliminate the authentication process for the access of Keychain

As we already have the authentication with biometric data before, here we may just rely on dependency's setItem function to help us save the account data into Keychain. Which uses kSecClassGenericPassword with the combination of kSecAttrAccount and kSecAttrService.

Remarks:

  1. Consider the still the plain-text seed will be sent to Javascript runtime. We need to force run the garbage collection each time after the seed is present. According to some discussion on Stackoverflow, it seems uncertain that how the GC works, I suggest we use the current delete account[key] implementation.
  2. Swift code will automatically release the memory if we use native code. While in Objective-C, we have to release it manually by
if (privateKey) { CFRelease(privateKey); }
if (access)     { CFRelease(access);     }
  1. In image2 we do not store the hash for comparison but rather throw an error on the decryption process, which could make the process longer to compromise the private key.

Implementation

Draft

  1. iOS Secure Enclave encryption and decryption function, use ECICE algorithms
  2. Android ECICE encryption and decryption function with tink.
  3. Generate Argon2 Hash for password comparision
  4. Create pinStore to store the pin data, and create help function to return the promise for decryption or encryption if pin is correct.

References

Rust
ios
Android
@geastwood
Copy link

Hash shouldn't be used to encrypt anything. It should only be used for verifying the validity of the password. Theoretically, a hash can just be stored anywhere. However, with a higher security setup, it can be stored in the keychain (ios) which could potentially be a bad UX, since to access keychain user need to pass authentication.

@geastwood
Copy link

Don't understand whey GC plays a role here. JavaScript in ReactNative run under JavaScriptCore (for iOS), it's run under a VM and thread safe, values can not simply be shared.

Seed in app UI will have to exist in plain text, and it's totally fine. Only if we send it over the bridge in plain text, which may raise concerns. Which is something we shouldn't do. When calling setItem encrypted version should always be used.

@geastwood
Copy link

AFAIK, in browser world there is no way to interact with GC, I can imagine on iOS and Android, it's even less possible.

@maciejhirsz
Copy link

maciejhirsz commented Aug 14, 2019

Couple things. First, the extra hashing on the pin doesn't add much in terms of security, we can always increase the number of AES iterations to increase the difficulty and combat brute force that way.

As for the discussion around GC, forcing collection doesn't really help us as the GC almost certainly doesn't erase the memory it's freeing, so it's still technically possible to obtain access to unallocated memory from outside of the app and read the seed this way (although that requires having a malicious app installed on the device). A much better way of handling things would be to keep the seed in Rust-managed memory, where we can guarantee it being zeroed after use. Note that this would work for the current implementation of the signing, but if we move decryption away from Rust to whatever secure enclave shenanigans, we will still have to move secrets around in-memory in JS.

@geastwood
Copy link

JavaScript is run under a VM, which abstract the memory already and guarantee memory safety. It won't have the issue as you described.

@maciejhirsz
Copy link

maciejhirsz commented Aug 14, 2019

It is memory safe in that it cannot share active allocations. Once the VM frees the memory, or you shut down the VM, nothing prevents another process from allocating memory previously allocated to the VM and then reading from it without initializing (zeroing) it. This is also the main reason why web browsers prevent you from creating uninitialized Unit8Arrays, while something like node exposes allocUnsafe on Buffer, which explicitly states that it "may contain sensitive data". The only way to prevent this from happening is to zero the memory before you de-allocate it, but you can't do that with JS primitives (you can do it with array buffers though).

@hanwencheng
Copy link
Author

Indeed with high iteration AES could generate similar randomness as KDF, the main advantage of Argon2 over AES is that 1. it provides a better resistance against GPU/ASIC attacks (due to being a memory-hard function), 2.Argon2i could prevent us from side-channel attacks like timing attack and power consumption attack.
I think with Rust what we do currently with bridge is to send the password to, and receive seed from the bridge. Correct me if I am wrong. What we do in the proposal will at least be same, specifically

  • In second workflow image, as discussed with Kirill is that in Javascript the Argon2 hash will be quite slow (up to 10 seconds), so if we do, that implementation would be better in Rust. Then what we would do with bridge is still the same: send the password to it, and receive seed from it.
  • In the last workflow image with Enclave on iOS. With TouchID we do not need KDF function and the another key will generated in native code, as well as encryption and decryption process. We will only receive the seed from the bridge,

@geastwood
Copy link

@maciejhirsz, I think we are discussing in the sandboxing context for both iOS and Android which is enforced on the kernel level. I do understand for allocUnsafe with unfilled buffer, the underlying memory could contain sensitive information (from other processes), but I see this is not fitting into the scenario at hand. The plain text for either password or seed has to be managed by JavaScript somehow. For example, user input a password, the password has to be a clear text and assigned to a primitive string variable.

IMO, password should never be sent over via Bridge at all. It should be ephemeral. The only place to get the password is literally from a text input box in UI, which in detail means the following:

  1. when the password is created together with the seed, symmetrically encrypt the seed with password to generate passwordEncryptedSeed
  2. use bcrypt to and password generate a passwordHash
  3. send passwordEncryptedSeed and passwordHash over the bridge to store on the native side with appropriate ACL
  4. forget the password all together, don't save the password anywhere.

And in any action which requires:

  • to use the password => ask the user to input the password and use passwordHash to verify it
  • to use the seed => verify password first and use the password to decrypt (verify) the passwordEncryptedSeed

Places to apply security:

  • when using bcrypt
  • the place where to store both passwordHash and passwordEncryptedSeed, although they are both hashed data, we can still put them behind FaceId/TouchId as an additional layer
  • if Enclave has to be used for seed, the key generated by Enclave can encrypt passwordEncryptedSeed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment