# Webhook Security

Your webhook endpoint will be accessible to the public internet, so it's important that you verify incoming webhooks to ensure they have actually come from LHV. Every incoming webhook will have an `X-LHV-HMAC` header, containing a hex-encoded SHA256 HMAC formed from the webhook body and your shared secret.

## Managing Your Shared Secret

Every webhook configuration has a secret, shared between you and LHV, which is used for verifying webhook payloads. You can use a same one for different configurations, or have each one be different.

You can either set the secret yourself, or leave it absent and we will generate one automatically. To increase security, we will only show you the secret once - in the response from a create/edit request.

When you edit an existing webhook, you can overwrite the secret with a new one you choose, or leave it absent to generate a new secret (used with immediate effect). If you are editing other fields but want to keep the same secret, then you must provide it in the edit request body.

The secrets themselves are represented as strings, but HMACs work on raw bytes, so you will need to convert it to a byte array of UTF-8 characters (example code is shown later). You can use any valid character in your secret, but it's often wise to avoid special characters anyway.

Secrets should have high entropy to ensure security. For our automaticly-generated secrets, we use a 64-character alphabet (`[a-zA-Z0-9_\-]`) and generate a string of length 64. This works out to 384 bits of entropy (`LENGTH * log2(ALPHABET_SIZE)`).

{% hint style="info" %}
If you're generating your own secret, remember to use a cryptographically secure PRNG. For Kotlin/Java/JVM, try `SecureRandom.getInstanceStrong()`
{% endhint %}

## Verifying HMACs

To verify a webhook, you should generate the expected HMAC in the same way, and then confirm that it matches the value from the header. If the result is not an exact match (or header is missing), you should discard the webhook as illegitimate, and log this as an error.&#x20;

For security, you should use a constant-time equals function (as shown in the code examples). If you compare hex strings rather than bytes, then do so case *in*sensitively (e.g. "af12" == "AF12").

Here is rough pseudocode for how you should use the result of HMAC validation. Actual implementations of `verifyHmac` are given in the next section

```kotlin
@POSTHandler("/my-webhook-endpoint")
fun webhookHandler(incomingWebhook: HttpMessage) {
    val rawPayload: ByteArray = incomingWebhook.requestBodyAsBytes
    val hmacString = incomingWebhook.headers["X-LHV-HMAC"]
    
    if (hmacString != null && verifyHmac(rawPayload, MY_SECRET, hmacString)) {
        process(incomingWebhook)
    } else {
        log.error("Webhook with invalid HMAC received!")
        fail()
    }
}
```

Note that `rawPayload` must be the exact bytes of the HTTP request body. Whitespace trimming or reformatting will result in a failed HMAC comparison. If you can only extract the body as a string then you can still convert it to a UTF-8 byte array - the example code does this to turn `secret` into `secretBytes`, so you can copy that for the request body.

{% hint style="info" %}
Make sure to always use the `SHA256`algorithm when processing HMACs.
{% endhint %}

Once you have verified the HMAC of the message, it's also worth making sure that the `clientCode` and `clientCountry` fields are correct, and that the `eventTimestamp` is within expected bounds.

## Example verifyHmac Code

{% tabs %}
{% tab title="Kotlin/Java" %}

### Kotlin

```kotlin
import java.security.MessageDigest;
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

// https://mvnrepository.com/artifact/commons-codec/commons-codec
import org.apache.commons.codec.binary.Hex

const val HMAC_ALGORITHM = "HmacSHA256"

private fun generateHmac(payload: ByteArray, secret: String): ByteArray {
    val secretBytes = secret.encodeToByteArray()
    val keySpec = SecretKeySpec(secretBytes, HMAC_ALGORITHM)
    val mac = Mac.getInstance(HMAC_ALGORITHM)
    mac.init(keySpec)
    return mac.doFinal(payload)
}

fun verifyHmac(payload: ByteArray, secret: String, receivedHmacHeader: String): Boolean {
    val receivedHmac = Hex.decodeHex(receivedHmacHeader)
    val expectedHmac = generateHmac(payload, secret)
    return MessageDigest.isEqual(receivedHmac, expectedHmac)
}
```

### Java

```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest;
import java.util.Arrays;

// https://mvnrepository.com/artifact/commons-codec/commons-codec
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;

// Class definition omitted

private static final String HMAC_ALGORITHM = "HmacSHA256";

private static byte[] generateHmac(byte[] payload, String secret)
        throws NoSuchAlgorithmException, InvalidKeyException {
    byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8)
    SecretKeySpec keySpec = new SecretKeySpec(secretBytes, HMAC_ALGORITHM);
    Mac mac = Mac.getInstance(HMAC_ALGORITHM);
    mac.init(keySpec);
    return mac.doFinal(payload);
}

public static boolean verifyHmac(byte[] payload, String secret, String receivedHmacHeader)
        throws NoSuchAlgorithmException, InvalidKeyException, DecoderException {
    byte[] receivedHmac = Hex.decodeHex(receivedHmacHeader);
    byte[] expectedHmac = generateHmac(payload, secret);
    return MessageDigest.isEqual(receivedHmac, expectedHmac);
}
```

{% endtab %}

{% tab title="NodeJS" %}

### Typescript

```typescript
import { createHmac, timingSafeEqual } from 'crypto';

function generateHmac(payload: Buffer, secret: string): Buffer {
    const secretBytes = Buffer.from(secret, 'utf8');
    const hmac = createHmac('sha256', secretBytes);
    hmac.update(payload);
    return hmac.digest();
}

function verifyHmac(payload: Buffer, secret: string, receivedHmacHeader: string): boolean {
    const receivedHmac = Buffer.from(receivedHmacHeader, 'hex');
    const expectedHmac = generateHmac(payload, secret);
    return timingSafeEqual(receivedHmac, expectedHmac);
}
```

### Javascript

```javascript
const crypto = require('crypto');

function generateHmac(payload, secret) {
    const secretBytes = Buffer.from(secret, 'utf8');
    const hmac = crypto.createHmac('sha256', secretBytes);
    hmac.update(payload);
    return hmac.digest();
}

function verifyHmac(payload, secret, receivedHmacHeader) {
    const receivedHmac = Buffer.from(receivedHmacHeader, 'hex');
    const expectedHmac = generateHmac(payload, secret);
    return crypto.timingSafeEqual(receivedHmac, expectedHmac);
}
```

{% endtab %}

{% tab title="Python" %}

```python
import hmac
import hashlib

def generate_hmac(payload: bytes, secret: str) -> bytes:
    secret_bytes = secret.encode('utf-8')
    return hmac.new(secret_bytes, payload, hashlib.sha256).digest()

def verify_hmac(payload: bytes, secret: str, received_hmac_header: str) -> bool:
    received_hmac = bytes.fromhex(received_hmac_header)
    expected_hmac = generate_hmac(payload, secret)
    return hmac.compare_digest(received_hmac, expected_hmac)
```

{% endtab %}

{% tab title="Go" %}

```go
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

func generateHmac(payload []byte, secret string) []byte {
    secretBytes := []byte(secret)
    h := hmac.New(sha256.New, secretBytes)
    h.Write(payload)
    return h.Sum(nil)
}

func verifyHmac(payload []byte, secret, receivedHmacHeader string) bool {
    receivedHmac, err := hex.DecodeString(receivedHmacHeader)
    if err != nil {
        return false
    }
    expectedHmac := generateHmac(payload, secret)
    return hmac.Equal(receivedHmac, expectedHmac)
}
```

{% endtab %}

{% tab title="Ruby" %}

```ruby
require 'openssl'
require 'rack/utils' # https://rubygems.org/gems/rack

# For this implementation the payload and secret actually *should* just be strings
def generate_hmac(payload, secret)
  OpenSSL::HMAC.digest('sha256', secret, payload)
end

def verify_hmac(payload, secret, received_hmac_header)
  received_hmac = [received_hmac_header].pack('H*')
  expected_hmac = generate_hmac(payload, secret)
  Rack::Utils.secure_compare(received_hmac, expected_hmac)
end
```

{% endtab %}

{% tab title="C#" %}

```csharp
using System;
using System.Text;
using System.Security.Cryptography;

// Class definition omitted

private const string HMAC_ALGORITHM = "HmacSHA256";

private static byte[] GenerateHmac(byte[] payload, string secret)
{
    byte[] secretBytes = System.Text.Encoding.UTF8.GetBytes(secret)
    using (var hmac = new HMACSHA256(secretBytes))
    {
        return hmac.ComputeHash(payload);
    }
}

public static bool VerifyHmac(byte[] payload, string secret, string receivedHmacHeader)
{
    byte[] receivedHmac = Convert.FromHexString(receivedHmacHeader);
    byte[] expectedHmac = GenerateHmac(payload, secret);
    return CryptographicOperations.FixedTimeEquals(receivedHmac, expectedHmac);
}
```

{% endtab %}
{% endtabs %}

## Example Payload and HMAC

You can use these values to test that you're generating HMACs correctly:

* Secret: `example_secret_for_docs`
* Payload: `{"eventId":"bd960667-37cf-4698-b63a-919aa282ef3c","eventTimestamp":"2024-07-10T10:39:33.295567","subscriptionReference":"2345","messageResponseId":"0566defb-2c1b-4649-9c1d-cda27d2f5a10","messageRequestId":"dd14b64d-0553-4bea-acb5-71f45697e126","messageType":"VIBAN_OPEN","messageCreatedTime":"2024-07-10T10:39:33.263134","clientCode":"123","clientCountry":"GB","bankCode":"LHVUK"}`
* `X-LHV-HMAC`: `79ece3b561a9a95a56edf5d8c63224b1fa43f0198442537abe22a7e3ba99e774`

Remember that you must copy these *exactly*. Even a stray space at the end will invalidate it.

{% hint style="info" %}
Having trouble with the above example? Here are the same secret and payload encoded in base64. Try decoding and using them in your test instead.

If that solves the problem then it was probably just down to browser encoding or copy and paste issues, *not* your implementation.

* Secret: `ZXhhbXBsZV9zZWNyZXRfZm9yX2RvY3M=`
* Payload: `eyJldmVudElkIjoiYmQ5NjA2NjctMzdjZi00Njk4LWI2M2EtOTE5YWEyODJlZjNjIiwiZXZlbnRUaW1lc3RhbXAiOiIyMDI0LTA3LTEwVDEwOjM5OjMzLjI5NTU2NyIsInN1YnNjcmlwdGlvblJlZmVyZW5jZSI6IjIzNDUiLCJtZXNzYWdlUmVzcG9uc2VJZCI6IjA1NjZkZWZiLTJjMWItNDY0OS05YzFkLWNkYTI3ZDJmNWExMCIsIm1lc3NhZ2VSZXF1ZXN0SWQiOiJkZDE0YjY0ZC0wNTUzLTRiZWEtYWNiNS03MWY0NTY5N2UxMjYiLCJtZXNzYWdlVHlwZSI6IlZJQkFOX09QRU4iLCJtZXNzYWdlQ3JlYXRlZFRpbWUiOiIyMDI0LTA3LTEwVDEwOjM5OjMzLjI2MzEzNCIsImNsaWVudENvZGUiOiIxMjMiLCJjbGllbnRDb3VudHJ5IjoiR0IiLCJiYW5rQ29kZSI6IkxIVlVLIn0=`
  {% endhint %}
