Skip to content

Commit cdfde24

Browse files
committed
Make flow encryption more flexible, reply ping
When flow endpoint is registered, the webhook should reply to the ping with an encrypted status=active response, regardless of how the subsequent flow data will be handled. This is partial support for flow endpoints for now.
1 parent c47ad33 commit cdfde24

File tree

2 files changed

+69
-23
lines changed

2 files changed

+69
-23
lines changed

src/WhatsApp/AzureFunctionsWebhook.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Text;
2+
using System.Text.Json;
23
using Azure.Data.Tables;
34
using Microsoft.AspNetCore.Http;
45
using Microsoft.AspNetCore.Mvc;
@@ -33,6 +34,28 @@ public async Task<IActionResult> Message([HttpTrigger(AuthorizationLevel.Anonymo
3334
var json = await reader.ReadToEndAsync();
3435
logger.LogDebug("Received WhatsApp message: {Message}.", json);
3536

37+
// Detect encrypted flow request setup for flows endpoints
38+
if (JsonSerializer.Deserialize<EncryptedFlowData>(json) is { } encrypted)
39+
{
40+
if (string.IsNullOrEmpty(metaOptions.Value.PrivateKey))
41+
return new StatusCodeResult(421);
42+
43+
var crypto = new FlowCryptography(metaOptions.Value.PrivateKey);
44+
if (!crypto.TryDecrypt(encrypted, out var data) || data is null)
45+
return new StatusCodeResult(421);
46+
47+
if (data.Data.TryGetProperty("action", out var action) &&
48+
action.ValueKind == JsonValueKind.String &&
49+
action.GetString() == "ping")
50+
{
51+
// This satisfies the flow publishing requirement that the endpoint is active.
52+
return new OkObjectResult(crypto.Encrypt(data.With(
53+
new { data = new { status = "active" } })));
54+
}
55+
56+
// TODO: else, how do we handle flow actions?
57+
}
58+
3659
if (await WhatsApp.Message.DeserializeAsync(json) is { } message)
3760
{
3861
// If we got a user message, we can send progress updates as configured. We ignore exceptions in the

src/WhatsApp/FlowMessage.cs

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@ public record EncryptedFlowData(
1616
[property: JsonPropertyName("initial_vector")] string IV);
1717

1818
/// <summary>Parsed flow data containing decrypted JSON and AES key/IV.</summary>
19-
public record FlowData(JsonElement Data, byte[] Key, byte[] IV);
19+
public record FlowData<TData>(TData Data, byte[] Key, byte[] IV);
20+
21+
/// <summary>Represents flow data with decrypted JSON content, AES key, and IV.</summary>
22+
public record FlowData(JsonElement Data, byte[] Key, byte[] IV) : FlowData<JsonElement>(Data, Key, IV)
23+
{
24+
/// <summary>Creates a new instance of <see cref="FlowData{TData}"/> with the specified data.</summary>
25+
public FlowData<TData> With<TData>(TData data) =>
26+
new(data, Key, IV);
27+
}
2028

2129
/// <summary>Implements the flow message encryption and decryption for the WhatsApp Business API.</summary>
2230
public class FlowCryptography : IDisposable
@@ -46,6 +54,43 @@ public FlowCryptography(string privatePem, string passphrase)
4654
rsa.ImportFromEncryptedPem(privatePem, passphrase);
4755
}
4856

57+
/// <summary>Decrypts the provided encrypted flow data into a <see cref="FlowData"/> object.</summary>
58+
public FlowData Decrypt(EncryptedFlowData data)
59+
{
60+
// Inline decode & decrypt pipeline (Base64 -> RSA -> AES-GCM -> JSON)
61+
var aesKey = rsa.Decrypt(Convert.FromBase64String(data.Key), RSAEncryptionPadding.OaepSHA256);
62+
var iv = Convert.FromBase64String(data.IV);
63+
var cipher = Convert.FromBase64String(data.Data);
64+
var plaintext = AesGcmDecrypt(aesKey, iv, cipher);
65+
var json = JsonSerializer.Deserialize<JsonElement>(Encoding.UTF8.GetString(plaintext));
66+
return new FlowData(json, aesKey, iv);
67+
}
68+
69+
/// <summary>Decrypts the provided encrypted flow data into a <see cref="FlowData"/> object, returning false on failure.</summary>
70+
public bool TryDecrypt(EncryptedFlowData data, out FlowData? result)
71+
{
72+
try
73+
{
74+
result = Decrypt(data);
75+
return true;
76+
}
77+
catch (CryptographicException)
78+
{
79+
result = null;
80+
return false;
81+
}
82+
}
83+
84+
/// <summary>Encrypts the provided flow data into a Base64-encoded string.</summary>
85+
public string Encrypt<TData>(FlowData<TData> data)
86+
{
87+
// Derive nonce via bit-flip (encapsulated) and serialize JSON directly to UTF-8 bytes.
88+
var flippedIv = FlipIvBits(data.IV);
89+
var payload = JsonSerializer.SerializeToUtf8Bytes(data.Data);
90+
var cipherWithTag = AesGcmEncrypt(data.Key, flippedIv, payload);
91+
return Convert.ToBase64String(cipherWithTag);
92+
}
93+
4994
/// <summary>Disposes the inner RSA key.</summary>
5095
public void Dispose() => rsa.Dispose();
5196

@@ -102,26 +147,4 @@ static byte[] FlipIvBits(byte[] iv)
102147
flipped[i] = (byte)~iv[i];
103148
return flipped;
104149
}
105-
106-
/// <summary>Decrypts the provided encrypted flow data into a <see cref="FlowData"/> object.</summary>
107-
public FlowData Decrypt(EncryptedFlowData data)
108-
{
109-
// Inline decode & decrypt pipeline (Base64 -> RSA -> AES-GCM -> JSON)
110-
var aesKey = rsa.Decrypt(Convert.FromBase64String(data.Key), RSAEncryptionPadding.OaepSHA256);
111-
var iv = Convert.FromBase64String(data.IV);
112-
var cipher = Convert.FromBase64String(data.Data);
113-
var plaintext = AesGcmDecrypt(aesKey, iv, cipher);
114-
var json = JsonSerializer.Deserialize<JsonElement>(Encoding.UTF8.GetString(plaintext));
115-
return new FlowData(json, aesKey, iv);
116-
}
117-
118-
/// <summary>Encrypts the provided flow data into a Base64-encoded string.</summary>
119-
public string Encrypt(FlowData data)
120-
{
121-
// Derive nonce via bit-flip (encapsulated) and serialize JSON directly to UTF-8 bytes.
122-
var flippedIv = FlipIvBits(data.IV);
123-
var payload = JsonSerializer.SerializeToUtf8Bytes(data.Data);
124-
var cipherWithTag = AesGcmEncrypt(data.Key, flippedIv, payload);
125-
return Convert.ToBase64String(cipherWithTag);
126-
}
127150
}

0 commit comments

Comments
 (0)