Skip to content

bug: inbound E2EE media HMAC is not verified before decrypting #191

@Adri11334

Description

@Adri11334

Bug Report: inbound E2EE media HMAC is not verified before decrypting

Describe the bug

Inbound E2EE media decryption strips the final 32-byte HMAC tag but does not verify it before decrypting the media bytes.

The decrypt helper derives keys from keyMaterial, but the derived MAC key is not used:

func (lc *LineClient) decryptImageData(encryptedData []byte, keyMaterialB64 string) ([]byte, error) {

// macKey := derived[32:64] // for HMAC verification

It then removes the last 32 bytes from the input and decrypts the remaining AES-CTR ciphertext directly:

encryptedData = encryptedData[:len(encryptedData)-32]

Outbound media encryption does append HMACs, so the integrity tag exists on the wire:

h := hmac.New(sha256.New, macKey)

// For videos: compute HMAC on chunk hashes

h := hmac.New(sha256.New, macKey)

The receive path wires all media handlers through this decrypt helper:

DecryptMedia: lc.decryptImageData,

Example inbound media calls:

decryptedImg, err := h.DecryptMedia(imgData, decryptInfo.KeyMaterial)

decryptedFile, err := h.DecryptMedia(fileData, decryptInfo.KeyMaterial)

decryptedAudio, err := h.DecryptMedia(audioData, decryptInfo.KeyMaterial)

decryptedVideo, err := h.DecryptMedia(videoData, decryptInfo.KeyMaterial)

To Reproduce

This can be reproduced with a small unit test using the existing helpers:

  1. Encrypt a byte slice with encryptFileData.
  2. Flip one byte in the ciphertext.
  3. Call decryptImageData.
  4. Observe that decryption succeeds and returns altered plaintext instead of rejecting the media.

The same also happens if only the final HMAC tag byte is modified: decryptImageData still returns plaintext with no error.

Expected behavior

Inbound E2EE media should verify the appended HMAC before decrypting or uploading bytes to Matrix.

If the HMAC does not match, the bridge should fail closed, for example by returning an error or sending an unavailable-media placeholder.

Screenshots or logs

No UI screenshot needed. This is in the E2EE media receive path.

Additional context

AES-CTR does not provide integrity by itself. Without the HMAC check, any actor able to modify encrypted OBS media bytes after upload can alter the plaintext that the bridge uploads to Matrix, while the media still appears to come from the original LINE sender.

This is mainly relevant to the Letter Sealing / E2EE media trust boundary: LINE OBS/CDN/storage should not be able to silently modify E2EE media accepted by the bridge.

Possible implementation direction:

  • Split incoming media into ciphertext and receivedMAC.
  • Derive and use macKey.
  • Verify with hmac.Equal before AES-CTR decryption.
  • For image/file/audio/thumbnail media, verify HMAC-SHA256(macKey, ciphertext), matching the file and thumbnail send paths.
  • For video media, match the existing send-side convention in encryptVideoData, which computes the HMAC over generateChunkHashes(ciphertext).

One implementation detail: the current receive-side API is generic:

DecryptMedia func(data []byte, keyMaterial string) ([]byte, error)

but video uses a different HMAC input than image/file/audio. The fix may need either a media-type-aware decrypt helper or separate helpers for file-like media and video media.

Suggested test coverage:

  • tampered ciphertext is rejected
  • tampered HMAC tag is rejected
  • valid encrypted image/file/audio media still decrypts
  • valid encrypted video media still decrypts using the chunk-hash HMAC mode

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions