Skip to content

[group key addrs 2/5]: internal/ecies: add encrypt/decrypt with ECIES #1512

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

Merged
merged 1 commit into from
Jun 25, 2025

Conversation

guggero
Copy link
Member

@guggero guggero commented May 6, 2025

Implements the ECIES encryption scheme as mentioned here: #874 (comment)

This commit creates a simple encryption and decryption function that uses ChaCha20Poly1305 for tne symmetric encryption and HKDF with SHA256 for the key derivation.
The shared key generation is not part of these functions, because we'll need to use lnd RPCs in some cases to be able to derive it.

The idea is that we'd transport the sender's ephemeral public key outside of the cipher text and use it as the additional data to authenticate the ciphertext.


This change is Reviewable

@guggero guggero requested a review from Roasbeef May 6, 2025 08:53
@coveralls
Copy link

coveralls commented May 6, 2025

Pull Request Test Coverage Report for Build 15734776865

Details

  • 74 of 99 (74.75%) changed or added relevant lines in 1 file are covered.
  • 16 unchanged lines in 4 files lost coverage.
  • Overall coverage increased (+0.07%) to 37.346%

Changes Missing Coverage Covered Lines Changed/Added Lines %
internal/ecies/ecies.go 74 99 74.75%
Files with Coverage Reduction New Missed Lines %
asset/group_key.go 2 57.52%
tapchannel/aux_leaf_signer.go 2 43.08%
tapdb/multiverse.go 6 53.03%
tapgarden/caretaker.go 6 68.68%
Totals Coverage Status
Change from base Build 15712916525: 0.07%
Covered Lines: 27357
Relevant Lines: 73253

💛 - Coveralls

@levmi levmi added the P1 label May 8, 2025
@levmi levmi added this to the v0.7 milestone May 8, 2025
@levmi levmi moved this from 🆕 New to 👀 In review in Taproot-Assets Project Board May 8, 2025
var result bytes.Buffer
result.Write(nonce)

gcm, err := cipher.NewGCMWithNonceSize(block, GCMNonceSize)
Copy link
Member

Choose a reason for hiding this comment

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

Any particular reason to use this over NewGCM? 12-byte nonces are standardized, it's also what the underlying Go packages uses with the default constructor.

Copy link
Member

Choose a reason for hiding this comment

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

See section 5.2.1 of this PDF: https://nvlpubs.nist.gov/nistpubs/legacy/sp/nistspecialpublication800-38d.pdf

For IVs, it is recommended that implementations restrict support to the length of 96 bits, to
promote interoperability, efficiency, and simplicity of design.

IV here is basically a nonce.

Copy link
Member

Choose a reason for hiding this comment

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

One alternative crypto system to consider is chacha20poly1305: https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305#New. This is what we use for encryption in LN. It also natively supports adding associative data (plaintext data included in the MAC).

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, I just used an example I found somewhere.
But I like ChaCha20Poly1305 better, changed the code to use that. We'll use the sender's ephemeral public key as the additional data, to authenticate the ciphertext. It will be part of the outer (authmailbox message) envelope, not the ciphertext itself. Does that make sense?

Copy link
Member

Choose a reason for hiding this comment

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

We'll use the sender's ephemeral public key as the additional data, to authenticate the ciphertext

Makes sense, this is exactly what I had in mind!

When it comes to the question of if we should have the key be a "part" of the cipher text (sent as a single blob), or split out into a single key, I lean towards making it part of the cipher text. That way it's a logical unit during transport.

Copy link
Contributor

@gijswijs gijswijs left a comment

Choose a reason for hiding this comment

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

Made some small comments throughout.

// SHA256.
func HkdfSha256(secret []byte) ([32]byte, error) {
var key [32]byte
kdf := hkdf.New(sha256.New, secret, nil, nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe add a comment here explaining why it's ok to not use a salt here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, adding some salt here wouldn't be too bad of an idea. Added the protocol name to make the derived key unique to this application.

func EncryptSha256ChaCha20Poly1305(sharedSecret [32]byte, msg []byte,
additionalData []byte) ([]byte, error) {

// We begin by stretching the shared secret using HKDF with SHA256.
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a lot of stretching involved if it goes from 32 bytes to 32 bytes. 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, you're right. Re-formulated to "hardening against brute forcing".

Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

Looking pretty good!

}

// Select a random nonce, and leave capacity for the ciphertext.
nonce := make(
Copy link
Member

Choose a reason for hiding this comment

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

As we're using a random nonce here, we should use NewX, which is meant for cases where a counter-like nonce isn't used. Basically some extra security margin for when nonces are generated randomly.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, yes, makes sense. Changed to X.

Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

Reviewed 3 of 3 files at r1, all commit messages.
Reviewable status: all files reviewed, 5 unresolved discussions (waiting on @guggero)

Copy link
Contributor

@gijswijs gijswijs left a comment

Choose a reason for hiding this comment

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

LGTM!

Should we be worried about the TestLightningTerminal/custom_channels_multi_rfq (119.50s) litd itest failing?

@guggero
Copy link
Member Author

guggero commented May 28, 2025

Should we be worried about the TestLightningTerminal/custom_channels_multi_rfq (119.50s) litd itest failing?

No, that looks like a flake. Re-triggered it.

This commit creates a simple encryption and decryption function that
uses ChaCha20Poly1305 for tne symmetric encryption and HKDF with SHA256 for
the key derivation.
The shared key generation is not part of these functions, because we'll
need to use lnd RPCs in some cases to be able to derive it.
@lightninglabs-deploy
Copy link

@Roasbeef: review reminder

@guggero guggero changed the title [group key addrs 2/?]: internal/ecies: add encrypt/decrypt with ECIES [group key addrs 2/4]: internal/ecies: add encrypt/decrypt with ECIES Jun 19, 2025
@guggero
Copy link
Member Author

guggero commented Jun 23, 2025

@Roasbeef friendly review ping.

@guggero guggero changed the title [group key addrs 2/4]: internal/ecies: add encrypt/decrypt with ECIES [group key addrs 2/5]: internal/ecies: add encrypt/decrypt with ECIES Jun 23, 2025
@levmi levmi added P0 and removed P1 labels Jun 24, 2025
Comment on lines +44 to +49
// We begin by hardening the shared secret against brute forcing by
// using HKDF with SHA256.
stretchedKey, err := HkdfSha256(sharedSecret[:], []byte(protocolName))
if err != nil {
return nil, fmt.Errorf("cannot derive hkdf key: %w", err)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this actually add "hardening"? I think that HKDF with a constant salt doesn’t add brute-force hardening.

I wander if we can't just use the nonce below as the salt here. And then protocolName can be the info arg in HkdfSha256 's hkdf.New call.

If we use the nonce as the salt then Encrypt... and Decrypt... will then have access to the same (random) salt and the serialization format doesn't change.

Copy link
Member

Choose a reason for hiding this comment

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

At a high level, you can view it as just binding the shared secret we create to our particular context (eg: if we change the protocol name, for the same shared secret we get a diff stretched key). The Noise Protocol does something similar to create an initial hash accumulator value which gets mixed into the initial shared secrets.

Copy link
Contributor

@ffranr ffranr Jun 25, 2025

Choose a reason for hiding this comment

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

Yes, I see this as just a form of binding as well. I think we should update the comments in ecies.go to clarify that this isn't providing hardening, but rather serving as a binding mechanism.

Thanks for the link!

Copy link
Member Author

Choose a reason for hiding this comment

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

IMO it does harden against brute force because you need to use more CPU cycles per shared secret you want to try. But I guess in this context that's not really relevant as you'd attack the encryption in different manners than doing brute force.

Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

LGTM 🎊

@Roasbeef Roasbeef merged commit 11b5403 into main Jun 25, 2025
18 checks passed
@github-project-automation github-project-automation bot moved this from 👀 In review to ✅ Done in Taproot-Assets Project Board Jun 25, 2025
@guggero guggero deleted the ecies branch June 25, 2025 07:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: ✅ Done
Development

Successfully merging this pull request may close these issues.

7 participants