-
Notifications
You must be signed in to change notification settings - Fork 3
Add encryption key derivation from PIN #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
73ba8cc to
59980c5
Compare
|
When creating a SoftwareAuth instance, the runner can give it some key material that would come from the hardware itself. This makes it slightly harder to extract data required for any brute-forcing. Here is a pseudocode overview of the key derivation process. // length depends on hardware
let device_ikm: [u8]= values_from_hardware();
// generated on first power-up, stays constant for the lifetime of the device
let device_salt: [u8;32] = csprng();
// The salt is useful for the security proof of HKDF
let device_prk = hkdf_extract(salt: device_salt, ikm: device_ikm);
// There is domain separation between each app
for app_id in apps {
let app_encryption_key: [u8; 32] = hkdf_expand(device_prk, info: app_id, 32);
// Deriving keys from a PIN or Password provided by the user
for pwd in passwords {
let salt = csprng();
// Per-pin key is fully random, then wrapped using the Pin
let wrapped_key = csprng();
// This is fine because app_encryption_key is uniform, therefore pin_key is too
//
// We can't use PBKDF or argon2 here because of limited hardware.
// Ideally such a step would be done on the host
//
// `pin_key` is never stored, and derived on each call to `CheckPin` and `GetPinKey`
let pin_key = HMAC(key: app_encryption_key, pin_id || len(pwd) || pwd || salt);
// On pin creation or change, the key is wrapped and stored on a persistent filesystem
// The constant nonce is acceptable and won't lead to nonce reuse because the `pin_key` is only used to encrypt data once
// Any change of pin changes the `pin_key` also changes the salt
// which means that it is not possible to use the same key twice
store(aead_encrypt(key: pin_key, data: wrapped_key, nonce: [0;12]))
// On GetPinKey requests, this gets us the key
aead_decrypt(key: pin_key, data: load(), nonce: [0;12]))
}
} |
|
Please add a patch for littlefs2 to fix the CI. https://github.com/Nitrokey/littlefs2/releases/tag/v0.3.2-nitrokey-2 should work. |
|
This patch prevents trussed from compiling because of the |
|
It appears to work by using your littlefs 0.4 branch. |
Cargo.toml
Outdated
| trussed = { version = "0.1.0", features = ["serde-extensions", "virt"] } | ||
|
|
||
| [patch.crates-io] | ||
| trussed = { git = "https://github.com/robin-nitrokey/trussed.git", rev = "f943d88aa43c72bc76c5c8bf8c8b5bb3638a4b85" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use https://github.com/Nitrokey/trussed/releases/tag/v0.1.0-nitrokey-5 instead for easier readability and for reproducibility?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually the branch we were currently using was missing trussed-dev/trussed#94 so I fixed it. I thought it was the other way around
12de3ce to
709150d
Compare
|
After a bit of discussion with @szszszsz here is a clearer version of the pseudocode: // length depends on hardware
let device_ikm: [u8]= values_from_hardware();
// generated on first power-up, stays constant for the lifetime of the device
let device_salt: [u8;32] = csprng();
// The salt is useful for the security proof of HKDF
let device_prk = hkdf_extract(salt: device_salt, ikm: device_ikm);
fn get_app_key(app_id) {
// Domain separation between the apps
// This means the `app_key` can also be used for purposes other than Pin key derivation.
return hkdf_expand(prk: device_prk, info: app_id, output_len: 32);
}
fn register_pin(app_id, pin_id, pin) {
let app_key = get_app_key(app_id);
let salt = csprng();
let key_to_be_wrapped = csprng();
// Get a pseudo-random key from the pin and the salt
//
// This is fine because app_key is uniform, therefore pin_key is too
// because HMAC is a PRF
//
// We can't use PBKDF or argon2 here because of limited hardware.
// Ideally such a step would be done on the host
//
// `pin_kek` is never stored
let pin_kek = HMAC(key: app_key, pin_id || len(pin) || pin || salt);
// On pin creation or change, the key is wrapped and stored on a persistent filesystem
// The constant nonce is acceptable and won't lead to nonce reuse because the `pin_kek` is only used to encrypt this data once
//
// Any change of pin changes also changes the salt
// which means that it is not possible to get the `pin_kek` twice
let wrapped_key = aead_encrypt(key: pin_kek, data: key_to_be_wrapped, nonce: [0;12]);
to_presistent_storage(salt, wrapped_key);
}
fn get_pin_key(app_id, pin_id, pin) {
let app_key = get_app_key(app_id);
let (salt, wrapped_key) = from_persistent_storage();
// re-derive the pin kek
let pin_kek = HMAC(key: app_key, pin_id || len(pin) || pin || salt);
// Unwrap the key
let unwrapped_key = aead_decrypt(key: pin_kek, data: wrapped_key , nonce: [0;12])
return unwrapped_key;
}
fn change_pin(app_id, pin_id, old_pin, new_pin) {
let app_key = get_app_key(app_id);
let key_to_be_wrapped = get_pin_key(app_id, pin_id, pin);
// The procedure is the same as for `register_pin` but it reuses the `key_to_be_wrapped` instead of generating it
// Generate a new salt for the new pin
let salt = csprng();
let pin_kek = HMAC(key: app_key, pin_id || len(new_pin) || new_pin || salt);
let wrapped_key = aead_encrypt(key: pin_kek, data: key_to_be_wrapped, nonce: [0;12]);
to_presistent_storage(salt, wrapped_key);
}The algorithm used are SHA-256 (for HKDF and HMAC) and ChaCha8Poly1305 (for AEAD). The CSPRNG comes from Trussed, so it's a References: |
cef1592 to
5962c75
Compare
|
The reference to ChaCha8Poly1305 seems to be wrong. Can you check that? |
|
There isn't really a standard to point to for ChaCha8 so it points instead to a publication justifying reduced rounds. I also added the ChaCha20 RFC for completeness |
|
In other words, response to my request is at:
|
These are irrelevant as we don't use Poly1305 directly but instead use it as part of the ChaCha20Poly1305 AEAD construction, so the Poly1305 key is generated using ChaCha: https://datatracker.ietf.org/doc/html/rfc8439#section-2.6 using the key and the nonce, making it unpredictable. The relevant part applies to the nonce: https://datatracker.ietf.org/doc/html/rfc8439#section-2.8
In our case it is ok because the |
Here is the proper Chacha8 description: |
f9dcd0c to
7b32e40
Compare
7b32e40 to
29c3f37
Compare
robin-nitrokey
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, though I did not double-check the implementation details yet.
The only issue I see is that it is easy to make a mistake when choosing between the SetPin and ChangePin requests. Should we drop the SetPin request to avoid accidentally dropping the derived key? See also: Nitrokey/trussed-secrets-app#34 (comment)
|
I think we also talked about having a way to access the application key (or deriving a key from it) when no PIN is set. That’s not implemented yet, right? |
We could instead make it fail when the Pin type is not a simple hash to ensure such bugs are obvious in testing. I still think we want to have it available.
No that is not implemented. I think it can be a separate PR. Do we need it now? |
Sounds good. |
|
Another solution I see would be to separate |
29c3f37 to
d2b08be
Compare
Would there be a difference between the two requests? Or just two names for the same request? |
|
|
Makes sense. Do you want to add it to this PR or should we do it separately? |
|
We can do it in another PR. |

The test currently fail because of a bug incbor-smolfixed in https://github.com/nickray/cbor-smol/pullsThe tests now fails because of the littlefs2 lookahead bug fixed in version 0.4