Skip to content

lightning: encrypt stored seed#4146

Open
Beerosagos wants to merge 1 commit into
BitBoxSwiss:staging-sparkfrom
Beerosagos:seed-encryption
Open

lightning: encrypt stored seed#4146
Beerosagos wants to merge 1 commit into
BitBoxSwiss:staging-sparkfrom
Beerosagos:seed-encryption

Conversation

@Beerosagos
Copy link
Copy Markdown
Collaborator

Add platform hooks for storing a Lightning seed encryption key outside the backend config and use them on Android via the keystore.

Activation now derives the mnemonic as before, generates a random 32-byte key in the backend, encrypts the mnemonic with AES-GCM, stores only the ciphertext in the Lightning config, and hands the base64-encoded key to the platform environment.

On Android, the environment wraps that key with an account-specific AES key from AndroidKeyStore and stores the wrapped blob in SharedPreferences. Unlocking loads and unwraps the stored key, decrypts the mnemonic in the backend, and then connects Breez. Deactivation removes both the wrapped key and the encrypted mnemonic.

Android intentionally treats missing wrapped keys as requiring Lightning reactivation. This can affect existing dev wallets created before this change, but the feature has no production users yet and the migration cost is acceptable at this stage.

Other platforms keep returning false or stub implementations for now. This is intentional preparatory work so iOS, Qt, and web can adopt secure storage later without another backend API change.

Before asking for reviews, here is a check list of the most common things you might need to consider:

  • updating the Changelog
  • writing unit tests
  • checking if your changes affect other coins or tokens in unintended ways
  • testing on multiple environments (Qt, Android, ...)
  • having an AI review your changes

@Beerosagos
Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request adds Lightning mnemonic encryption support with platform-specific key storage. The Environment interface gains four methods for checking encryption availability and managing per-account encryption keys. The Lightning service now encrypts mnemonics using AES-GCM before storing them and decrypts them on Breez connection, with key lifecycle tied to account activation/deactivation. The backend bridge wires the environment through to mobile clients. Android implements real encryption using Android KeyStore with AES-256-GCM wrapping keys persisted in SharedPreferences; iOS, Qt, and web provide stub implementations. Test infrastructure is updated throughout to support the new environment dependency.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/bridgecommon/bridgecommon.go`:
- Around line 280-294: The current BackendEnvironment methods
StoreLightningEncryptionKey and LoadLightningEncryptionKey silently
succeed/return empty when their delegates are nil, which can hide
misconfiguration; change them to return a clear error when
StoreLightningEncryptionKeyFunc or LoadLightningEncryptionKeyFunc is nil so
callers (e.g., Lightning.sealMnemonic/Lightning.unsealMnemonic) fail fast.
Specifically, in BackendEnvironment.StoreLightningEncryptionKey return a non-nil
error if StoreLightningEncryptionKeyFunc == nil (rather than nil), and in
BackendEnvironment.LoadLightningEncryptionKey return ("", error) if
LoadLightningEncryptionKeyFunc == nil; reference the methods
StoreLightningEncryptionKey, LoadLightningEncryptionKey and the
CanEncryptLightningMnemonicFunc contract to ensure activation-time
misconfiguration surfaces immediately.

In `@backend/lightning/lightning.go`:
- Around line 297-343: sealMnemonic and unsealMnemonic currently discard
underlying errors and return generic messages; update all failure paths in
sealMnemonic and unsealMnemonic to log the original error using the logger (e.g.
lightning.log.WithError(err).WithField("account", accountCode or
account.Code).Error("...")) before mapping to the user-facing errp.New(...) so
callers get the same generic message while the real cause is recorded; follow
the project's structured logging pattern (logging.Get().WithGroup()/WithField())
and include contextual fields (account code, operation) for each error source
(io.ReadFull, encryptMnemonic/decryptMnemonic, StoreLightningEncryptionKey,
LoadLightningEncryptionKey, base64 decode) prior to returning the mapped error.

In
`@frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/LightningEncryptionHelper.java`:
- Around line 81-84: Introduce a named constant for the GCM IV length and
replace hard-coded 12 values: add a private static final int GCM_IV_LENGTH_BYTES
= 12 to LightningEncryptionHelper and use GCM_IV_LENGTH_BYTES in both the method
that prepends the IV (storeKey or equivalent) and in loadKey where the IV is
sliced and the ciphertext length computed (currently using new byte[12] and
iv.length assumptions). Update all references (IV allocation, System.arraycopy
offsets/lengths) to use GCM_IV_LENGTH_BYTES so the format coupling is explicit
and maintainable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f102aa11-650c-48fe-ab38-3793974cf7fe

📥 Commits

Reviewing files that changed from the base of the PR and between a61476a and 71f017f.

📒 Files selected for processing (13)
  • backend/backend.go
  • backend/backend_test.go
  • backend/bridgecommon/bridgecommon.go
  • backend/bridgecommon/bridgecommon_test.go
  • backend/handlers/handlers_test.go
  • backend/lightning/lightning.go
  • backend/lightning/lightning_test.go
  • backend/mobileserver/mobileserver.go
  • cmd/servewallet/main.go
  • frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/GoViewModel.java
  • frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/LightningEncryptionHelper.java
  • frontends/ios/BitBoxApp/BitBoxApp/BitBoxAppApp.swift
  • frontends/qt/server/server.go

Comment on lines +280 to +294
// StoreLightningEncryptionKey implements backend.Environment.
func (env *BackendEnvironment) StoreLightningEncryptionKey(accountCode string, encryptionKey string) error {
if env.StoreLightningEncryptionKeyFunc != nil {
return env.StoreLightningEncryptionKeyFunc(accountCode, encryptionKey)
}
return nil
}

// LoadLightningEncryptionKey implements backend.Environment.
func (env *BackendEnvironment) LoadLightningEncryptionKey(accountCode string) (string, error) {
if env.LoadLightningEncryptionKeyFunc != nil {
return env.LoadLightningEncryptionKeyFunc(accountCode)
}
return "", nil
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 14, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Silent defaults on data-critical store/load could mask misconfiguration.

If a future caller sets CanEncryptLightningMnemonicFunc to return true but forgets to wire StoreLightningEncryptionKeyFunc or LoadLightningEncryptionKeyFunc, Lightning.sealMnemonic will think storage succeeded and Lightning.unsealMnemonic will receive an empty key (then fail base64-decode or AES-GCM open with the generic "reactivation required" message). This is consistent with how the other delegates degrade, but those are UX-only; here the consequence is users locked out of their Lightning seed. Consider returning an error when the func is missing so the misconfiguration surfaces immediately during activation rather than later at unlock time.

🛡️ Proposed defensive fix
 // StoreLightningEncryptionKey implements backend.Environment.
 func (env *BackendEnvironment) StoreLightningEncryptionKey(accountCode string, encryptionKey string) error {
 	if env.StoreLightningEncryptionKeyFunc != nil {
 		return env.StoreLightningEncryptionKeyFunc(accountCode, encryptionKey)
 	}
-	return nil
+	return errp.New("StoreLightningEncryptionKey not implemented in this environment")
 }
 
 // LoadLightningEncryptionKey implements backend.Environment.
 func (env *BackendEnvironment) LoadLightningEncryptionKey(accountCode string) (string, error) {
 	if env.LoadLightningEncryptionKeyFunc != nil {
 		return env.LoadLightningEncryptionKeyFunc(accountCode)
 	}
-	return "", nil
+	return "", errp.New("LoadLightningEncryptionKey not implemented in this environment")
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/bridgecommon/bridgecommon.go` around lines 280 - 294, The current
BackendEnvironment methods StoreLightningEncryptionKey and
LoadLightningEncryptionKey silently succeed/return empty when their delegates
are nil, which can hide misconfiguration; change them to return a clear error
when StoreLightningEncryptionKeyFunc or LoadLightningEncryptionKeyFunc is nil so
callers (e.g., Lightning.sealMnemonic/Lightning.unsealMnemonic) fail fast.
Specifically, in BackendEnvironment.StoreLightningEncryptionKey return a non-nil
error if StoreLightningEncryptionKeyFunc == nil (rather than nil), and in
BackendEnvironment.LoadLightningEncryptionKey return ("", error) if
LoadLightningEncryptionKeyFunc == nil; reference the methods
StoreLightningEncryptionKey, LoadLightningEncryptionKey and the
CanEncryptLightningMnemonicFunc contract to ensure activation-time
misconfiguration surfaces immediately.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We need this to allow using the seed without encryption in webdev mode. Lightning activation will be hidden in the UI for all the other platforms that don't support encryption in a future PR. I'll keep it as is for now.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Beerosagos, understood — since CanEncryptLightningMnemonic() returns false for webdev (and other non-supporting platforms), the Store/Load paths won't be reached during normal flow, and the future UI guard will provide an additional layer of protection. That makes the silent defaults a reasonable design choice here. Thanks for the context!


✏️ Learnings added
Learnt from: Beerosagos
Repo: BitBoxSwiss/bitbox-wallet-app PR: 4146
File: backend/bridgecommon/bridgecommon.go:280-294
Timestamp: 2026-05-14T09:40:51.331Z
Learning: In `backend/bridgecommon/bridgecommon.go`, the `BackendEnvironment` methods `StoreLightningEncryptionKey` and `LoadLightningEncryptionKey` intentionally return `nil`/`("", nil)` when their delegate funcs are not set. This is by design to support webdev mode (and other non-encrypting platforms) where `CanEncryptLightningMnemonic()` returns `false`, gating all encryption code paths. A future PR will hide the Lightning activation UI on platforms that do not support mnemonic encryption, adding another layer of protection.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread backend/lightning/lightning.go
@Beerosagos Beerosagos requested a review from benma May 14, 2026 10:17
@Beerosagos Beerosagos marked this pull request as ready for review May 14, 2026 10:17
@Beerosagos Beerosagos requested a review from a team as a code owner May 14, 2026 10:17

if lightning.environment.CanEncryptLightningMnemonic() {
if err := lightning.environment.DeleteLightningEncryptionKey(string(account.Code)); err != nil {
lightning.log.WithError(err).Warn("Error deleting lightning encryption key")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not return the error? Seems like a serious enough error.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Well, even if the encryption key cannot be deleted for any reason, the deactivation basically completes anyway. And I don't think it should bring any issue on a following new activation, unless the Android keystore is completely broken. So I think that returning an error may add more confusion than value

return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
}

private static SecretKey getOrCreateWrappingKey(String accountCode) throws Exception {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Codex:

Restore path can strand Lightning accounts.
frontends/android/BitBoxApp/app/src/main/AndroidManifest.xml:32 enables backup, and Android backup includes app data/shared prefs by default. If lightning.json and lightning-encryption.xml restore without
the AndroidKeyStore entry, loadKey() still calls getOrCreateWrappingKey() at frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/LightningEncryptionHelper.java:82, creating a fresh wrong
key instead of reporting “missing key”. Decryption then fails generically. I’d split create/load paths: load should error if the alias is absent, and backup rules should likely exclude the Lightning prefs
and/or lightning.json.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Great catch! Should be fixed now 🙏

Add platform hooks for storing a Lightning seed encryption key
outside the backend config and use them on Android via the
keystore.

Activation now derives the mnemonic as before, generates a random
32-byte key in the backend, encrypts the mnemonic with AES-GCM,
stores only the ciphertext in the Lightning config, and hands the
base64-encoded key to the platform environment.

On Android, the environment wraps that key with an
account-specific AES key from AndroidKeyStore and stores the
wrapped blob in SharedPreferences. Unlocking loads and unwraps
the stored key, decrypts the mnemonic in the backend, and then
connects Breez. Deactivation removes both the wrapped key and the
encrypted mnemonic.

Android intentionally treats missing wrapped keys as requiring
Lightning reactivation. This can affect existing dev wallets
created before this change, but the feature has no production
users yet and the migration cost is acceptable at this stage.

Other platforms keep returning false or stub implementations for
now. This is intentional preparatory work so iOS, Qt, and web can
adopt secure storage later without another backend API change.
@Beerosagos Beerosagos requested a review from benma May 20, 2026 09:38
@Beerosagos
Copy link
Copy Markdown
Collaborator Author

@benma PTAL 🙏

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.

2 participants