From 58689c8b38d55d085bdfde04b4afbbc5ef5d7e43 Mon Sep 17 00:00:00 2001 From: Dylan <103431650+svix-dylan@users.noreply.github.com> Date: Wed, 13 Jul 2022 05:37:57 -0400 Subject: [PATCH] Update libraries to accept raw webhook secret (#555) Before this change you could only pass base64 encoded secrets to the library with the optional `whsec_` prefix. This change makes it possible to pass a byte array directly (or in JS's case, a raw string) to be used as the raw secret directly, without the base64 encoding. --- csharp/Svix/Webhook.cs | 7 +++++- go/webhook.go | 6 +++++ java/lib/src/main/java/com/svix/Webhook.java | 4 ++++ javascript/src/index.ts | 23 ++++++++++++++++---- kotlin/lib/src/main/kotlin/Webhook.kt | 13 ++++++----- php/src/Webhook.php | 7 ++++++ python/svix/webhooks.py | 14 +++++++----- ruby/lib/svix/webhook.rb | 4 ++++ rust/src/webhooks.rs | 4 ++++ 9 files changed, 66 insertions(+), 16 deletions(-) diff --git a/csharp/Svix/Webhook.cs b/csharp/Svix/Webhook.cs index 60eee7eba..9c47b3582 100644 --- a/csharp/Svix/Webhook.cs +++ b/csharp/Svix/Webhook.cs @@ -31,6 +31,11 @@ public Webhook(string key) this.key = Convert.FromBase64String(key); } + public Webhook(byte[] key) + { + this.key = key; + } + public void Verify(string payload, WebHeaderCollection headers) { string msgId = headers.Get(SVIX_ID_HEADER_KEY); @@ -114,4 +119,4 @@ public string Sign(string msgId, DateTimeOffset timestamp, string payload) } } } -} \ No newline at end of file +} diff --git a/go/webhook.go b/go/webhook.go index fb20ef58c..471050f98 100644 --- a/go/webhook.go +++ b/go/webhook.go @@ -39,6 +39,12 @@ func NewWebhook(secret string) (*Webhook, error) { }, nil } +func NewWebhookRaw(secret []byte) (*Webhook, error) { + return &Webhook { + key: secret, + }, nil +} + // Verify validates the payload against the svix signature headers // using the webhooks signing secret. // diff --git a/java/lib/src/main/java/com/svix/Webhook.java b/java/lib/src/main/java/com/svix/Webhook.java index 0e96f8f1b..f6c2c2283 100644 --- a/java/lib/src/main/java/com/svix/Webhook.java +++ b/java/lib/src/main/java/com/svix/Webhook.java @@ -36,6 +36,10 @@ public Webhook(final String secret) { this.key = Base64.getDecoder().decode(sec); } + public Webhook(final byte[] secret) { + this.key = secret; + } + public void verify(final String payload, final HttpHeaders headers) throws WebhookVerificationException { Optional msgId = headers.firstValue(SVIX_MSG_ID_KEY); Optional msgSignature = headers.firstValue(SVIX_MSG_SIGNATURE_KEY); diff --git a/javascript/src/index.ts b/javascript/src/index.ts index 09035ef87..a8e2e9a51 100644 --- a/javascript/src/index.ts +++ b/javascript/src/index.ts @@ -590,18 +590,33 @@ export interface WebhookUnbrandedRequiredHeaders { "webhook-signature": string; } +export interface WebhookOptions { + format?: "raw"; +} + export class Webhook { private static prefix = "whsec_"; private readonly key: Uint8Array; - constructor(secret: string) { + constructor(secret: string | Uint8Array, options?: WebhookOptions) { if (!secret) { throw new Error("Secret can't be empty."); } - if (secret.startsWith(Webhook.prefix)) { - secret = secret.substring(Webhook.prefix.length); + if (options?.format === "raw") { + if (secret instanceof Uint8Array) { + this.key = secret; + } else { + this.key = Uint8Array.from(secret, (c) => c.charCodeAt(0)); + } + } else { + if (!(secret instanceof String)) { + throw new Error("Expected secret to be of type string"); + } + if (secret.startsWith(Webhook.prefix)) { + secret = secret.substring(Webhook.prefix.length); + } + this.key = base64.decode(secret as string); } - this.key = base64.decode(secret); } public verify( diff --git a/kotlin/lib/src/main/kotlin/Webhook.kt b/kotlin/lib/src/main/kotlin/Webhook.kt index 9a4827010..c97734a24 100644 --- a/kotlin/lib/src/main/kotlin/Webhook.kt +++ b/kotlin/lib/src/main/kotlin/Webhook.kt @@ -100,11 +100,14 @@ class Webhook(secret: String) { } } - init { - var sec = secret - if (sec.startsWith(SECRET_PREFIX)) { - sec = sec.substring(SECRET_PREFIX.length) + constructor(secret: String) { + if (secret.startsWith(SECRET_PREFIX)) { + secret = secret.substring(SECRET_PREFIX.length) } - key = Base64.getDecoder().decode(sec) + key = Base64.getDecoder().decode(secret) + } + + constructor(secret: ByteArray) { + key = secret } } diff --git a/php/src/Webhook.php b/php/src/Webhook.php index a39dfc55d..645475d92 100644 --- a/php/src/Webhook.php +++ b/php/src/Webhook.php @@ -16,6 +16,13 @@ public function __construct($secret) $this->secret = base64_decode($secret); } + public static function fromRaw($secret) + { + $obj = new self(); + $obj->secret = $secret; + return $obj; + } + public function verify($payload, $headers) { if ( diff --git a/python/svix/webhooks.py b/python/svix/webhooks.py index e4495b157..410f6cba0 100644 --- a/python/svix/webhooks.py +++ b/python/svix/webhooks.py @@ -18,16 +18,18 @@ class WebhookVerificationError(Exception): class Webhook: _SECRET_PREFIX: str = "whsec_" _whsecret: bytes - _enc_key: t.Optional[bytes] - def __init__(self, whsecret: str, *, enc_key: t.Optional[str] = None): + def __init__(self, whsecret: t.Union[str, bytes]): if not whsecret: raise RuntimeError("Secret can't be empty.") - if whsecret.startswith(self._SECRET_PREFIX): - whsecret = whsecret[len(self._SECRET_PREFIX) :] - self._whsecret = base64.b64decode(whsecret) - self._enc_key = base64.b64decode(enc_key) if enc_key is not None else None + if isinstance(whsecret, str): + if whsecret.startswith(self._SECRET_PREFIX): + whsecret = whsecret[len(self._SECRET_PREFIX) :] + self._whsecret = base64.b64decode(whsecret) + + if isinstance(whsecret, bytes): + self._whsecret = whsecret def verify(self, data: t.Union[bytes, str], headers: t.Dict[str, str]) -> t.Any: data = data if isinstance(data, str) else data.decode() diff --git a/ruby/lib/svix/webhook.rb b/ruby/lib/svix/webhook.rb index ae33c5bef..c5a858047 100644 --- a/ruby/lib/svix/webhook.rb +++ b/ruby/lib/svix/webhook.rb @@ -3,6 +3,10 @@ module Svix class Webhook + def self.new_using_raw_bytes(secret) + self.new(secret.pack("C*").force_encoding("UTF-8")) + end + def initialize(secret) if secret.start_with?(SECRET_PREFIX) secret = secret[SECRET_PREFIX.length..-1] diff --git a/rust/src/webhooks.rs b/rust/src/webhooks.rs index e47ea7632..6438494c0 100644 --- a/rust/src/webhooks.rs +++ b/rust/src/webhooks.rs @@ -53,6 +53,10 @@ impl Webhook { Ok(Webhook { key }) } + pub fn from_bytes(secret: Vec) -> Result { + Ok(Webhook { key: secret }) + } + pub fn verify(&self, payload: &[u8], headers: &HeaderMap) -> Result<(), WebhookError> { let msg_id = Self::get_header(headers, SVIX_MSG_ID_KEY, UNBRANDED_MSG_ID_KEY, "id")?; let msg_signature = Self::get_header(