Protecting Signal Keys on Desktop

by Gaëtan Wattiau, Steven Yue, Adrien Guinet. Posted on Nov 14, 2024
“A lone man in a desert trying to understand the inner workings of a computer in the style of Moebius”, by DALL-E.

“A lone man in a desert trying to understand the inner workings of a computer in the style of Moebius”, by DALL-E.

At SandboxAQ, we recently delved into Signal Desktop to identify how its key management could be improved by using security hardware. Our analysis highlights the need for robust and consistent key isolation services across desktop platforms. In this blogpost,

  1. We analyse the security of Signal Desktop’s key management in light of recent attack reports, and
  2. We show how security hardware can protect Signal Desktop’s keys with a proof of concept (PoC)—a fork of Signal Desktop—and explore the tradeoffs with this approach.

Signal is an open-source messaging service that implements end-to-end encryption. Its users benefit from best-in-class privacy when communicating online.1 This level of privacy is made possible by the Signal protocol, considered the gold standard of end-to-end encrypted messaging. Its security protocol depends on cryptographic keys handled on-device. For mobile devices, Signal securely stores them in isolated and controlled locations. However, when it comes to desktop, Signal (just like everyone else) must do without OS-provided secure key management capabilities.

The problem

It all starts with a Twitter post explaining how an attacker running malware could extract all Signal Desktop’s confidential data and run a clone of the victim’s messaging application. This attack was possible because, although Signal encrypts the local database containing confidential data (including messages and keys), it stored the database decryption keys in plaintext.

Attack scenario

  1. Attacker gains access to the local Signal database encryption key. This can be accomplished through either (a) physical access to the disk at rest, bypassing disk encryption, or (b) malware operating with the victim’s privilege rights, like a malicious app.
  2. Attacker decrypts the database and extracts its data. Given that the database encryption was saved in plaintext, this process is straightforward.2
  3. Attacker impersonates the victim or compromises their messages. The compromised database contains the victim’s conversation history on this device and a set of keys used in the Signal protocol.

Here’s a visual representation of the attack vector.

Diagram describing the attack vector.

Attacker with USER privileges can access the database encryption key.

Security impact

This attack discloses all locally stored messages and all keys used by the Signal protocol which has a devastating impact to security: 

  • With access to the identity keys alone, attackers can impersonate the victim indefinitely. 
  • With access to the identity keys, the signed prekeys and the one-time prekeys, attackers can passively decrypt new messages until the keys are rotated. Active attackers can also access messages sent to the users by leveraging session resets and authenticating new prekeys themselves.3
  • With access to the sender keys, attackers have long-term access to messages sent in large group conversations.4
  • With access to the storage key, attackers have long-term access to non-messaging data, such as profile data or message receipts.

Here’s a detailed list of the compromised keys and their roles.

Compromised keyDescription
Identity keysLong term user-bound keys. Used to set up (authentication & encryption) new messaging sessions [emitter end].
Signed prekeysMedium term device-bound keys. Used to set up (authentication & encryption) new messaging sessions [receiver end]. Rotated regularly.
One-time prekeysOne-time use device-bound keys. Used to set up (authentication & encryption) new messaging sessions
Kyber (ML-KEM-1024) prekeys and last resort Kyber prekeysPost-Quantum device bound keys. Used to set up (authentication & encryption) new messaging sessions.
username/passwordDevice-bound credentials. Authenticates the signal desktop client to the Signal backend server.5
Storage keyLong term user-bound key. Used to encrypt and synchronize non-messaging data across devices.
Sender keysLong-term device-bound conversation-bound keys. Used to encrypt and authenticate messages in large group conversations.

Why is this a problem on desktop but not on smartphone?

The fix seems simple: restrict access to the Signal database encryption key from other applications. Signal has already implemented similar measures for iOS and Android, where applications are sandboxed, and secrets are protected by the iOS Keychain or the Android Keystore. This security gap isn’t new; it was identified as far back as 2021 by the Department of Defense Cyber Crime Center:

Decryption of the Signal app on different platforms ranges from straightforward on desktops, to challenging on iOS and Android, particularly when hardware security protects the keys.

The heart of the issue is that open desktop platforms lack the capability to securely store and enforce granular access control for application secrets. While services for secret storage do exist (like the .NET Data Protection API on Windows or libsecret and kwallet on Linux), they do not provide per-application access control.

Instead, Windows and Linux implement user-based access control, meaning that two applications running under the same user can access the same resources. It’s even relatively easy for malicious applications to read the private keys directly from Signal Desktop’s memory (see the bash command in footnote).6

Annotated screenshot of a memory dump of Signal Desktop.

Memory dumps of Signal Desktop contain all the keys, including the identity keys.

The official fix

An official fix was merged on July 2024: the database encryption key is no longer stored in plaintext. Instead, it is first encrypted using the Electron safeStorage API (safeStorage.encryptString(key)). The safeStorage API performs encryption using encryption services provided by the operating system.

So, all’s well now, right? Well, not quite; the devil lies in the details. The security guarantees of the safeStorage API vary from platform to platform.

On MacOS, the key is adequately protected in the keychain since access is restricted on a per-application basis.

Conversely, on Windows and Linux, attackers can still access the database encryption key. By default, these operating systems restrict access on a per-user basis, so malware running under that user can still decrypt the database encryption key. It only requires an additional step to the attack scenario above (step 2):

  1. Attacker gains access to the local Signal database encryption key.
  2. Attacker decrypts the database encryption key using their user credentials.
  3. Attacker decrypts the database and extracts its data.
  4. Attacker impersonates the victim or compromises their messages.
Diagram describing the attack vector after the official fix is applied.

Attacker can still access the plaintext database encryption key on Windows and Linux.

Can we further enhance the key protection?

The limitations of the official fix challenged us to take a step back and consider a more robust solution.

Security goals

Our main objective is preventing the unauthorized access and extraction of Signal Desktop’s key material. The attacker we’re considering can run code on the victim’s computer with user privileges. We’re also considering the risk of privilege escalation, where the attacker could gain root access to the victim’s computer. We focussed this work on protecting the Signal protocol’s most critical assets: the identity key pairs.

Our primary goal is to protect the confidentiality of Signal Desktop’s key material against such an attacker. In practice, this means that the keys are stored and used within secure hardware and never end up in userland or OS memory. Although the attacker can no longer extract the keys directly, they may still access the cryptographic functions, such as signing arbitrary data. To reduce this risk, we can require user consent before allowing access to those cryptographic functions.

As a secondary goal, we strengthen the protection of the Signal database encryption key by ensuring it is encrypted and secure while stored. The database holds other confidential data (like stored messages), which are less critical to the security of the Signal protocol. Once again, we cannot enforce that the database key is only released to the Signal Desktop application, but we can require user consent or verification.

Diagram describing the attack vector after implementation of the PoC

Malware cannot access the Signal identity key since it is kept isolated in secure hardware.

Choosing the right security measures

To accomplish our security goals, we need to select the most appropriate security measures to isolate and protect the confidentiality of the key material. Three key properties guided this decision:

  1. Security level (which determines the effectiveness of the measure against various attacker models)
  2. Availability of the security measure across platforms (primarily Windows and Linux)
  3. Compatibility of provided cryptographic algorithms with Signal (that is, a cryptographic interface that integrates with the Signal protocol).

Here, we focus on protecting the Signal identity keys. Since these keys are only used occasionally, this minimizes the impact on user experience. These keys are X25519 key pairs used in the Diffie-Hellman (ECDH) key exchange protocol.

One option is to isolate the key material within a software daemon running with elevated privileges, providing a cryptographic interface via inter-process communication (IPC). This solution can be ported to both Windows or Linux and provides broad coverage for algorithms and interfaces. However, the security of the daemon depends heavily on the security of the operating system, making it vulnerable to attackers with elevated privileges. Despite this, it’s a promising approach owing to its ease of deployment.

Another option is to store and use the cryptographic keys within the Trusted Platform Module (TPM), a security chip found on most PCs (thanks to the Windows 11 TPM mandate). The TPM offers robust security, even against attackers with elevated privileges.7 While the TPM specifications do include support for X25519 keys,8 this feature isn’t mandatory for PC platforms,9 and TPM manufacturers have yet to implement it.

Ultimately, we selected the YubiKey Series 5, a widely-used security key that supports a variety of cryptographic interfaces, such as FIDO, PGP, or PIV. Its secure memory defends against the strongest attack models, and as of firmware update 5.7, it supports X25519 keys via the PIV interface.

User experience

The keys are stored in the YubiKey, and user consent is required before they can be accessed. In the video below, we demonstrate opening Signal for Desktop and starting a new conversation with a contact. This process requires user consent twice: first, to decrypt the Signal database with a wrapping key, and second, to use the identity key to establish a Signal protocol session with the contact.

Demonstration of the PoC. A prompt asks the user to touch their YubiKey when needed. The first prompt is to decrypt the Signal database, the second prompt is to create the conversation.

Our solution has an impact on user experience: (a) users must carry and manipulate the YubiKey, and (b) users must interact with the YubiKey when using Signal. The number of YubiKey interactions can be reduced with different key usage policies. For example, we selected the CACHED policy for the identity key. This is because the identity key is sometimes used several times in quick succession, (e.g., when linking Signal Desktop), and the CACHED policy doesn’t require renewing consent in the 15 seconds following a touch.

Implementation

To implement our hardware key management solution, we forked the open source Signal Desktop to port the Signal identity key operations to the YubiKey instead of software.[^10]

In this section, we describe how we can use the YubiKey to protect:

  1. The database encryption key.
  2. The identity key pair.

Protecting the database encryption key

Since we have a YubiKey, we can use it to protect the Signal database encryption key too.10 This ensures that the key is protected at rest and that user consent is required to decrypt the database. Once decrypted, the key is passed to the better-sqlite3 dependency which will perform the database decryption. This approach makes integration with the existing database decryption process really straightforward. And, although it means that the database encryption key will be kept in memory, this approach does not reduce the security of the overall system since the plaintext database is kept in memory anyway.

For the sake of simplicity, we also implement wrapping the database encryption key with an ECDH operation. The YubiKey generates a random X25519 private key. When encryption or decryption is required, the YubiKey performs an ECDH operation with a static X25519 public key and use it to derive a XAES-256-GCM key (with the WebCrypto API).11 We replace calls to the encryptString/decryptString safeStorage API with PIV calls, providing backward compatibility and ability to migrate from previous encryption schemes.

Diagram describing the encryptString cryptographic process.
Diagram describing the decryptString cryptographic process.

Protecting the identity key pair

The same identity key is used in two different contexts:

  1. To perform X25519 ECDH operations, during the X3DH key agreement (really, PQXDH key agreement)12 that sets up new conversations, and
  2. To authenticate prekeys with the xEd25519 signing algorithm, when generating prekeys (namely, signed prekeys, Kyber prekeys and last resort Kyber prekeys).

Porting the ECDH operations to the YubiKey is relatively straightforward, but since XEd25519 is an uncommon signature primitive,13 protecting the identity key in that context involves more tradeoffs.

Porting X3DH to the YubiKey

The identity key is used when creating new conversations, but it is used differently when creating them as a sender or as a receiver.

In our PoC, we focus on protecting the identity key when setting up a conversation as a sender on our device. In that context, the identity key provides mutual authentication through a series of Diffie-Hellman operations.14 To port X3DH to the YubiKey, we need to insert hooks in the Signal codebase to perform ECDH in the YubiKey instead of in software.

Diagram describing the Diffie-Hellman operations in Signal X3DH.

The initial session key depends on both users’ sets of keys of varied lifetimes. This provides mutual authentication and a level of post-compromise security (the ability to secure messages after the compromise of secret keys).

Where to insert the hooks?

The core cryptographic operations of the signal protocol are implemented in libsignal, a cross-platform Rust library. On Desktop, libsignal is compiled to WebAssembly (WASM) and called from the Signal Desktop electron renderer process when sending a message. Specifically, identity keys are used in the initialize_alice_session function in the ratchet.rs file.

A sequence diagram describing the upstream use of the Signal identity key.

The Signal identity key is used within the libsignal WASM blob called by the renderer process.

To replace software uses of the identity key with calls to the YubiKey API, we only need to place a single hook within the ratchet.rs file. However, to interact with the YubiKey from the WASM library we need to jump through a few more hoops, as system APIs cannot be accessed from the renderer process within which libsignal runs.

The sequence diagram below describes the required steps for the libsignal (WASM) library to communicate with the YubiKey.

A sequence diagram describing the POC's use of the Signal identity key.

In our PoC, the Signal identity key is kept in the YubiKey. We insert hooks in libsignal to perform the ECDH operation using the YubiKey instead of WASM software.

Our PoC slightly deviates from the diagram above: we use a separate daemon to perform the YubiKey API calls. The main Signal process communicates with this daemon via a UNIX socket. This allows us to quickly swap hardware backends during our experimentation. The daemon exposes a single API endpoint that performs ECDH, given a private key handle and a public key.

Using the YubiKey Personal Identity Verification (PIV) API

The PIV API offers the features we need to securely import and use the Signal identity key in the X3DH protocol:

  1. A Diffie-Hellman key agreement
  2. The X25519 curve, and
  3. A key usage policy requiring user interaction (touch or PIN)

To interact with the PIV API, we use the Rust library yubikey.rs. Our local fork adds support for the X25519 curve. In the end, this works perfectly well, but, as always when working with cryptography, we spent some time debugging errors related to key formats.

The challenges with supporting XEd25519

XEd25519 is a Schnorr digital signature primitive developed for Signal in 2016. It generates compliant EdDSA signatures with an X25519 key pair. XEd25519 allows the same key pair to perform both ECDH and DSA operations.

In Signal, the same identity key pair both (a) authenticates the signed prekeys with XEd25519, and (b) derives shared secrets with X25519.

Unfortunately, although the YubiKey recently added support for the Ed25519 signature algorithm, it doesn’t support the less common XEd25519 algorithm. This means that the authentication of prekeys cannot be offloaded to a YubiKey. Here are a few ways that Signal could be modified to still benefit from security hardware on desktop.

  1. Migrate to standardized cryptographic primitives.

A first solution involves updating the X3DH protocol to use now widespread cryptographic primitives supported by security hardware. XEd25519/X25519 is not the only way to combine authentication and key exchange with a single key pair.

This solution is conceptually easy, but involves practical obstacles. While it would provide straightforward hardware integration, updating core protocols is challenging. In effect, it requires updating all accounts’ identity keys while maintaining some amount of backward compatibility.

  1. Make the phone application generate signed prekeys for the desktop application.

The same identity keys are present on both phone and desktop applications (when linking, the primary device sends its key material to the Desktop via an encrypted channel). Therefore, the phone can easily generate signed prekeys on behalf of the linked devices. This way, Signal Desktop wouldn’t have to perform xEd25519 locally, and the key remains only in the YubiKey or the phone. The encrypted channel between the phone and devices is easy to set up as they already share secrets (like the identity keys or the storage key).

Signing with the phone comes with a tradeoff between user experience and security. Indeed, users would have to connect their phone regularly to upload fresh prekeys for the benefit of linked devices. To make it more convenient, we could slightly extend the lifetime of signed prekeys (today, keys are rotated every 1.5 days), but this would also slightly reduce post-compromise security.

As it happens, this solutions seems applicable. This is a testament to the design of the X3DH protocol and its asynchronous requirements, enabling flexibility in key management without having to compromise on the security guarantees provided by signed prekeys.15

Final thoughts

According to some, Signal Desktop requires significant improvements to match the security of its mobile applications. This is largely due to the access control model adopted by most PC systems. Indeed, user-based access control cannot protect against rogue applications. This weakness is exacerbated with secure messaging applications as their entire value proposition relies on the security of their cryptographic keys.

We have demonstrated how to use a hardware security module to mitigate this threat. We showed how to modify the Signal Desktop application to secure its most critical cryptographic keys using a YubiKey, ensuring that even advanced attackers cannot maintain indefinite control over compromised Signal Desktop users.

Across the industry, there’s growing concern about securing cryptographic material at the edge—specifically, how to protect cryptographic keys and secrets used on endpoints and connected systems. For instance, as we were working on this project, the Chrome security team released an update that encrypts user secrets with a colocated daemon running with SYSTEM privileges. This approach mitigates similar attacks to what we describe in this blogpost. We believe that strong key protection using hardware and enforceable key usage policies is essential to defend against both current and emerging risks, and we’re actively developing robust solutions to address these challenges.

[^10] Our fork of Signal-Desktop can be found here. Our fork of libsignal can be found here.


  1. So much so that messaging apps (like WhatsApp) are adopting the Signal protocol. ↩︎

  2. If you’re running the vulnerable version of Signal, you can easily decrypt the local database and dump confidential data using this script↩︎

  3. Thanks to Signal’s Double Ratchet algorithm, it does require an active attacker: MITM or actively resetting sessions with the Signal server. ↩︎

  4. Large groups do not benefit from post-compromise security, as they do not use the Double Ratchet algorithm. ↩︎

  5. In practice, attackers must also compromise the device-bound password, which authenticates the linked device to the Signal server, although this is irrelevant to the security of the Signal protocol. ↩︎

  6. (1) Find the Signal renderer process pid: ps aux | grep Signal | grep renderer

    (2) Dump the process memory:

      grep rw-p /proc/<pid>/maps \
      | awk '{print $1}' \
      | awk -F'-' {'print "z-ex \"dump memory "$1"-"$2".dump 0x"$1" 0x"$2"\""'} \
      | tr '\n' ' ' \
      | xargs gdb --batch --pid <pid> \
      && cat *.dump > signal.dump
    

    (3) Finally, scanning for the keys is relatively easy as they are structured: <32_bytes_private_scalar> || 0x05 || <32_bytes_25519_public_point>. Any match can be confirmed by checking that private_scalar * base_point == public_point↩︎

  7. The strength depends on which TPM implementation is used (firmware TPM, hardware TPM, etc.). ↩︎

  8. See the TPM Library specification – Part 2, section 6.4. ↩︎

  9. See section 4.3 of the TCG PC Client Platform TPM Profile specification↩︎

  10. See the commit that introduced this change. ↩︎

  11. We chose XAES-256-GCM to provide authentication (thanks to GCM), safe implementation (thanks to WebCrypto), and the ability to generate random IVs without collision (which is a risk with the 12-bytes IV vanilla AES-GCM). ↩︎

  12. The PQXDH augments the X3DH protocol with post-quantum cryptography. For the purpose of this blogpost, mentions of X3DH also refer to PQXDH, since we explore points that the X3DH specification discuss more thoroughly. ↩︎

  13. Unused outside of the Signal Protocol to the best of our knowledge. ↩︎

  14. See section 3.3. in the X3DH whitepaper. ↩︎

  15. For more details on the security guarantees provided by signed prekeys, see ‘paragraph 4.5. (Signatures)’ in the X3DH specification (retrieved 01/10/2024). ↩︎