Skip to content
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

EncryptorDecryptor Trait for Logins Component #6469

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

jo
Copy link

@jo jo commented Nov 12, 2024

The encryption and decryption of credentials is outsourced to a Foreign Trait so that Android, desktop and iOS can each bring their own implementations of encryption, including key management. This makes the Logins API way simpler and cleaner. It is the first step in making AS-Logins desktop-ready.

The new EncryptorDecryptor trait replaces the current EncryptorDecryptor struct. Instead of using the decrypt_struct method, which involves serializing and deserializing the string to be encrypted, this trait uses only byte-based operations and serialization is up to the consumer.

We do not Uniffi-expose the crypto primitives encrypt, decrypt, encrypt_struct and decrypt_struct anymore. Also EncryptedLogin will not be exposed anymore.

A ManagedEncryptorDecryptor will provide an EncryptorDecryptor implementation which uses the currently used crypto methods, given a KeyManager implementation to ease adaption for mobile.

BREAKING CHANGE

This commit introduces breaking changes to the Logins component:

During initialization, it receives an additional argument, a
EncryptorDecryptorTrait implementation. In addition, several LoginsStore
API methods have been changed to not require an encryption key argument
anymore, and return Logins objects instead of EncryptedLogins.

Additionally, a new API method has been added to the LoginsStore,
has_logins_by_base_domain(&self, base_domain: &str), which can be used
to check for the existence of a login for a given base domain.

EncryptorDecryptor

With the introduction of the EncryptorDecryptor trait, encryption
becomes transparent. That means, the LoginStore API receives some
breaking changes as outlined above. A ManagedEncryptorDecryptor will
provide an EncryptorDecryptor implementation which uses the currently
used crypto methods, given a KeyManager implementation. This eases
adaption for mobile. Furthermore, we provide a StaticKeyManager
implementation, which can be used in tests and in cases where the key is

  • you name it - static. Constructors Now an implementation of the above
    property must be passed to the constructors. To do this, the signatures
    are extended as follows:
pub fn new(path: impl AsRef<Path>, encdec: Arc<dyn EncryptorDecryptor>) -> ApiResult<Self>
pub fn new_from_db(db: LoginDb, encdec: Arc<dyn EncryptorDecryptor>) -> Self
pub fn new_in_memory(encdec: Arc<dyn EncryptorDecryptor>) -> ApiResult<Self>

LoginStore API Methods

This allows the LoginStore API to be simplified as follows, making
encryption transparent by eliminating the need to pass the key and
allowing the methods to return decrypted login objects.

pub fn list(&self) -> ApiResult<Vec<Login>>
pub fn get(&self, id: &str) -> ApiResult<Option<Login>>
pub fn get_by_base_domain(&self, base_domain: &str) -> ApiResult<Vec<Login>>
pub fn find_login_to_update(&self, entry: LoginEntry) -> ApiResult<Option<Login>>
pub fn update(&self, id: &str, entry: LoginEntry) -> ApiResult<Login>
pub fn add(&self, entry: LoginEntry) -> ApiResult<Login>
pub fn add_or_update(&self, entry: LoginEntry) -> ApiResult<Login>

We will stop Uniffi-exposing the crypto primitives encrypt, decrypt,
encrypt_struct and decrypt_struct. Also EncryptedLogin will not be
exposed anymore. Checking for the Existence of Logins for a given Base
Domain In order to check for the existence of stored logins for a given
base domain, we provide an additional store method,
has_logins_by_base_domain(&self, base_domain: &str), which does not
utilize the EncryptorDecryptor.

@jo jo changed the title Encryptor decryptor trait WIP: Encryptor decryptor trait Nov 12, 2024
@jo jo force-pushed the encryptor-decryptor-trait branch 11 times, most recently from 03f06ad to a3c0e96 Compare November 19, 2024 10:48
@jo jo force-pushed the encryptor-decryptor-trait branch from be673a2 to af91a5e Compare November 25, 2024 11:40
@jo jo changed the title WIP: Encryptor decryptor trait WIP: EncryptorDecryptor Trait Nov 25, 2024
@linabutler linabutler self-requested a review November 25, 2024 16:37
Copy link
Member

@mhammond mhammond left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fantastic and a massive improvement, thanks! Dealing with Android and iOS will be a challenge though.

components/logins/src/db.rs Outdated Show resolved Hide resolved
@jo jo mentioned this pull request Nov 26, 2024
5 tasks
bendk
bendk previously approved these changes Nov 26, 2024
Copy link
Contributor

@bendk bendk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really great to me, I like the overall approach. I commented on some of the details -- treat them as suggestions only. I don't have strong feelings either way.

Hopefully the consumer app updates aren't too bad.

components/logins/src/logins.udl Show resolved Hide resolved
components/logins/src/encryption.rs Show resolved Hide resolved
components/logins/src/logins.udl Show resolved Hide resolved
@mergify mergify bot dismissed bendk’s stale review December 2, 2024 09:18

The pull request has been modified, dismissing previous reviews.

@jo jo force-pushed the encryptor-decryptor-trait branch 4 times, most recently from 2f13b8a to 497ca51 Compare December 3, 2024 10:15
@jo jo changed the title WIP: EncryptorDecryptor Trait EncryptorDecryptor Trait Dec 3, 2024
@jo jo changed the title EncryptorDecryptor Trait EncryptorDecryptor Trait for Logins Component Dec 3, 2024
@jo jo force-pushed the encryptor-decryptor-trait branch 4 times, most recently from 8a434a3 to 4cbc6da Compare December 4, 2024 15:45
@jo jo force-pushed the encryptor-decryptor-trait branch from 32992bf to c941153 Compare December 4, 2024 16:01
@jo jo changed the title EncryptorDecryptor Trait for Logins Component WIP: EncryptorDecryptor Trait for Logins Component Dec 4, 2024
@jo jo force-pushed the encryptor-decryptor-trait branch 5 times, most recently from 48bfe9f to 772e1c9 Compare December 4, 2024 17:05
@jo jo changed the title WIP: EncryptorDecryptor Trait for Logins Component EncryptorDecryptor Trait for Logins Component Dec 4, 2024
@jo jo force-pushed the encryptor-decryptor-trait branch from 772e1c9 to a290d50 Compare December 9, 2024 09:49
@jo jo requested review from bendk and mhammond December 9, 2024 12:38
bendk
bendk previously requested changes Dec 9, 2024
Copy link
Contributor

@bendk bendk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me. A lot of code is changing, but in general it seems like the same system as before only better. I mostly just had suggestions around the error handling.

components/logins/src/encryption.rs Outdated Show resolved Hide resolved
components/logins/src/error.rs Show resolved Hide resolved
components/logins/src/error.rs Outdated Show resolved Hide resolved
components/logins/src/error.rs Outdated Show resolved Hide resolved
components/logins/src/store.rs Show resolved Hide resolved
components/logins/src/sync/engine.rs Show resolved Hide resolved
@mergify mergify bot dismissed bendk’s stale review December 10, 2024 11:56

The pull request has been modified, dismissing previous reviews.

@jo jo force-pushed the encryptor-decryptor-trait branch from 9a5f309 to ac36ae4 Compare December 10, 2024 11:56
@jo
Copy link
Author

jo commented Dec 10, 2024

This looks great to me. A lot of code is changing, but in general it seems like the same system as before only better. I mostly just had suggestions around the error handling.

Thanks for the good points!

@jo jo requested a review from bendk December 10, 2024 11:59
@jo jo force-pushed the encryptor-decryptor-trait branch from ac36ae4 to c2e2382 Compare December 10, 2024 13:35
bendk
bendk previously approved these changes Dec 10, 2024
Copy link
Contributor

@bendk bendk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes look great to me, I'm all for merging this. I think we should get a second review if possible though.

@jo jo force-pushed the encryptor-decryptor-trait branch from c2e2382 to 4641d2f Compare December 13, 2024 09:39
@mergify mergify bot dismissed bendk’s stale review December 13, 2024 09:40

The pull request has been modified, dismissing previous reviews.

mhammond
mhammond previously approved these changes Dec 13, 2024
Copy link
Member

@mhammond mhammond left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fantastic!

@@ -72,15 +68,6 @@ dictionary Login {
SecureLoginFields sec_fields;
};

/// An encrypted version of [Login]. This is what we return for all the "read"
/// APIs - we never return the cleartext of encrypted fields.
dictionary EncryptedLogin {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really happy to see this die. Back in the day, we were torn about whether the logins store should be able to function while in a "locked" state - ie, we were picturing a world where the app knows a login exists for a site, but the app must force the user to unlock via biometrics etc before the actual details were available, which then allows the app to "re-lock" the store at its convenience.

It looks like you've decided this is a non-goal, and your team is the correct team to make that decision, and the extra simplicity that comes with that is a nice bonus!

You might be able to take this further though - it seems that without EncryptedLogin you might also be able to flatten out the Login record so avoid the nested records (ie, can RecordFields, and SecureLoginFields now die?) While that might be able to be done as a followup, the cost of re-breaking the mobile platforms might mean the opportunity to do so might not arise. IMO, the current splitting of these records makes the use of this component in the mobile code quite complicated to understand so would likely benefit from a simplification here.

(alternatively, a different split might make sense - eg, split out "editable" fields from "computed" fields, but even that might not have enough value)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think the advantages of a simplified way of working for LoginsStore outweigh the disadvantages. The idea of a locked state further complicates things and has a knock-on effect at lower levels. We can still perform certain operations without the store unlocked – for example, I added (thanks @bendk!) the method has_logins_by_base_domain(&self, base_domain: &str) so that the application can still check whether logins exist for a specific domain.

I like that you suggest further simplifying the login structure. I hadn't even dared to tackle that :) I'll discuss it with the mobile teams. On Android, the code is already abstracted, so the changes would be very simple here. On iOS, I'm not sure. At the latest now, a decoupling on the iOS side seems sensible to me in order to be able to make adjustments more agilely.

Furthermore, we would actually like to encrypt more fields, especially the domain. This information could already be interesting for certain attackers and some users could therefore support such changes. Although this would complicate or even prevent the lookup by domain in the locked state.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mhammond something like this: jo#2?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah! I'm a little torn by RecordFields though - roughly speaking, they are fields which the consumer should never provide - eg, when creating a login you never specify the id or the number of times it has been used. This is a tension we often face - use different structs for "get" vs "set" operations, which makes consumers a little more painful in some ways, but makes things clearer in others.

But certainly the flattening of LoginFields looks like a big improvement.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I know, this is an old problem. And then, of course, there is something appealing about grouping all these meta fields, which only play a role when reading, into a separate record struct. And on the other hand, there is this simplicity and clarity ¯_(ツ)_/¯

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exactly :) It sounds like we agree there's no objectively correct answer here, so I can get behind whatever you decide!

components/logins/src/encryption.rs Show resolved Hide resolved
components/logins/src/encryption.rs Outdated Show resolved Hide resolved
fn get_key(&self) -> ApiResult<Vec<u8>> {
Ok(self.key.as_bytes().into())
}
}

#[handle_error(Error)]
pub fn create_canary(text: &str, key: &str) -> ApiResult<String> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The canary story can probably be better too - the intent there is to better work with keys which are our of our control. IIUC there's no equivalent thing on desktop. Maybe that concept could be optionally added to the traits? This might be good fodder for a followup.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, very good idea!

components/logins/src/error.rs Outdated Show resolved Hide resolved
@jo jo force-pushed the encryptor-decryptor-trait branch 2 times, most recently from ae5bc5e to 78ec15d Compare December 16, 2024 15:02
@mergify mergify bot dismissed mhammond’s stale review December 16, 2024 15:02

The pull request has been modified, dismissing previous reviews.

@jo jo force-pushed the encryptor-decryptor-trait branch 3 times, most recently from 5d748a1 to 949c187 Compare December 16, 2024 17:00
This prepares the Logins component for the desktop and simplifies its
API.

BREAKING CHANGE:
This commit introduces breaking changes to the Logins component:

During initialization, it receives an additional argument, a
EncryptorDecryptorTrait implementation. In addition, several LoginsStore
API methods have been changed to not require an encryption key argument
anymore, and return Logins objects instead of EncryptedLogins.

Additionally, a new API method has been added to the LoginsStore,
`has_logins_by_base_domain(&self, base_domain: &str)`, which can be used
to check for the existence of a login for a given base domain.

**EncryptorDecryptor**

With the introduction of the EncryptorDecryptor trait, encryption
becomes transparent. That means, the LoginStore API receives some
breaking changes as outlined above.  A ManagedEncryptorDecryptor will
provide an EncryptorDecryptor implementation which uses the currently
used crypto methods, given a KeyManager implementation. This eases
adaption for mobile.  Furthermore, we provide a StaticKeyManager
implementation, which can be used in tests and in cases where the key is
- you name it - static.  Constructors Now an implementation of the above
property must be passed to the constructors. To do this, the signatures
are extended as follows:

```
pub fn new(path: impl AsRef<Path>, encdec: Arc<dyn EncryptorDecryptor>) -> ApiResult<Self>
pub fn new_from_db(db: LoginDb, encdec: Arc<dyn EncryptorDecryptor>) -> Self
pub fn new_in_memory(encdec: Arc<dyn EncryptorDecryptor>) -> ApiResult<Self>
```

**LoginStore API Methods**
This allows the LoginStore API to be simplified as follows, making
encryption transparent by eliminating the need to pass the key and
allowing the methods to return decrypted login objects.

```
pub fn list(&self) -> ApiResult<Vec<Login>>
pub fn get(&self, id: &str) -> ApiResult<Option<Login>>
pub fn get_by_base_domain(&self, base_domain: &str) -> ApiResult<Vec<Login>>
pub fn find_login_to_update(&self, entry: LoginEntry) -> ApiResult<Option<Login>>
pub fn update(&self, id: &str, entry: LoginEntry) -> ApiResult<Login>
pub fn add(&self, entry: LoginEntry) -> ApiResult<Login>
pub fn add_or_update(&self, entry: LoginEntry) -> ApiResult<Login>
```

We will stop Uniffi-exposing the crypto primitives encrypt, decrypt,
encrypt_struct and decrypt_struct. Also EncryptedLogin will not be
exposed anymore.  Checking for the Existence of Logins for a given Base
Domain In order to check for the existence of stored logins for a given
base domain, we provide an additional store method,
has_logins_by_base_domain(&self, base_domain: &str), which does not
utilize the EncryptorDecryptor.

Another by-change is in the `check_canary` function: here we do not
throw anymore if a wrong key is used but return false.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants