Skip to content

Commit

Permalink
Update libraries to accept raw webhook secret (#555)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
svix-dylan authored Jul 13, 2022
1 parent 37630ab commit 58689c8
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 16 deletions.
7 changes: 6 additions & 1 deletion csharp/Svix/Webhook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -114,4 +119,4 @@ public string Sign(string msgId, DateTimeOffset timestamp, string payload)
}
}
}
}
}
6 changes: 6 additions & 0 deletions go/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down
4 changes: 4 additions & 0 deletions java/lib/src/main/java/com/svix/Webhook.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> msgId = headers.firstValue(SVIX_MSG_ID_KEY);
Optional<String> msgSignature = headers.firstValue(SVIX_MSG_SIGNATURE_KEY);
Expand Down
23 changes: 19 additions & 4 deletions javascript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
13 changes: 8 additions & 5 deletions kotlin/lib/src/main/kotlin/Webhook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
7 changes: 7 additions & 0 deletions php/src/Webhook.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
14 changes: 8 additions & 6 deletions python/svix/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions ruby/lib/svix/webhook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 4 additions & 0 deletions rust/src/webhooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ impl Webhook {
Ok(Webhook { key })
}

pub fn from_bytes(secret: Vec<u8>) -> Result<Self, WebhookError> {
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(
Expand Down

0 comments on commit 58689c8

Please sign in to comment.