Skip to content

Add key commitment to prevent invisible salamanders attack #141

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions packages/crypto/src/symmetric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import type { Base58, Cipher, Password, Payload } from './types.js'
import { base58, keyToBytes } from './util/index.js'

/**
* Symmetrically encrypts a byte array.
* Symmetrically encrypts a byte array with key commitment protection.
* This implementation prevents the "invisible salamanders" attack by
* binding the key to the ciphertext with a commitment scheme.
*/
const encryptBytes = (
/** The plaintext or object to encrypt */
Expand All @@ -16,14 +18,29 @@ const encryptBytes = (
const messageBytes = pack(payload)
const key = stretch(password)
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES)
const encrypted = sodium.crypto_secretbox_easy(messageBytes, nonce, key)

// Step 1: Create a key commitment by deriving a subkey bound to both the key and the nonce
// This ensures there's only one valid key for each ciphertext
const keyCommitment = sodium.crypto_generichash(
sodium.crypto_secretbox_KEYBYTES, // Size of secretbox key
nonce, // Bind to the nonce
key // Derive from the key
)

// Step 2: Use the committed key for encryption
// This binds the ciphertext to the specific key
const encrypted = sodium.crypto_secretbox_easy(messageBytes, nonce, keyCommitment)

// Step 3: Package everything together
const cipher: Cipher = { nonce, message: encrypted }
const cipherBytes = pack(cipher)
return cipherBytes
}

/**
* Symmetrically decrypts a message encrypted by `symmetric.encryptBytes`. Returns the original byte array.
* Symmetrically decrypts a message encrypted by `symmetric.encryptBytes`.
* Derives the same committed key to ensure the ciphertext can only be decrypted
* with the exact same key used for encryption.
*/
const decryptBytes = (
/** The encrypted data in msgpack format */
Expand All @@ -33,8 +50,23 @@ const decryptBytes = (
): Payload => {
const key = stretch(password)
const { nonce, message } = unpack(cipher) as Cipher
const decrypted = sodium.crypto_secretbox_open_easy(message, nonce, key)
return unpack(decrypted)

// Step 1: Derive the same committed key used for encryption
const keyCommitment = sodium.crypto_generichash(
sodium.crypto_secretbox_KEYBYTES,
nonce,
key
)

// Step 2: Use the committed key for decryption
// If this is not the exact same key used for encryption, decryption will fail
try {
const decrypted = sodium.crypto_secretbox_open_easy(message, nonce, keyCommitment)
return unpack(decrypted)
} catch (error) {
// When key commitment fails, sodium.crypto_secretbox_open_easy will throw
throw new Error('Decryption failed - possible invisible salamanders attack')
}
}

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/crypto/src/test/symmetric.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,18 @@ describe('crypto', () => {
const decrypted = symmetric.decryptBytes(encrypted, bytePassword)
expect(decrypted).toEqual(plaintext)
})

test('prevents invisible salamanders attack', () => {
// Encrypt a message
const encrypted = symmetric.encryptBytes(plaintext, password)

// Attempt to decrypt with wrong password should fail with specific error
const attemptToDecrypt = () => symmetric.decryptBytes(encrypted, 'wrong-password')
expect(attemptToDecrypt).toThrow('Decryption failed - possible invisible salamanders attack')

// Successful decryption with correct password
const decrypted = symmetric.decryptBytes(encrypted, password)
expect(decrypted).toEqual(plaintext)
})
})
})