Skip to content

A .NET Standard 2.1 library for generating and validating Branca tokens.

License

LGPL-3.0, GPL-3.0 licenses found

Licenses found

LGPL-3.0
COPYING.LESSER
GPL-3.0
COPYING
Notifications You must be signed in to change notification settings

AmanAgnihotri/Branca

Repository files navigation

Branca

A .NET Standard 2.1 library for generating and validating Branca tokens.

Status Nuget Downloads License

Overview

Branca tokens are authenticated and encrypted API tokens using modern crypto.

From Branca.io:

Branca is a secure, easy to use token format which makes it hard to shoot yourself in the foot. It uses IETF XChaCha20-Poly1305 AEAD symmetric encryption to create encrypted and tamperproof tokens. The encrypted token is base62 encoded which makes it URL safe. The payload itself is an arbitrary sequence of bytes. You could use a JSON object, plain text string or even binary data serialized by MessagePack or Protocol Buffers.

Although not a goal, it is possible to use Branca as an Alternative to JWT.

You can read the Branca Token Specification for design-specific details.

Usage

using Branca;

Issuing Secret Key

The secret key is to be 32 bytes in size. You can generate a random one using the following:

byte[] key = new byte[32];
RandomNumberGenerator.Fill(key);

Alternatively, you can setup a hexadecimal string consisting of exactly 64 characters:

HexKey key = "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974";

Configuring Branca

Once you have a secret key, be it a byte[] type or a HexKey type, you can configure Branca as follows:

BrancaService branca = new(key);

Branca can optionally take in BrancaSettings, which has a few parameters for configuration. They come with sensible defaults so you may not need to change it at all.

BrancaService branca = new(key, new BrancaSettings
{
  MaxStackLimit = 1024,
  TokenLifetimeInSeconds = 3600
});

For the sake of performance, Branca allocates various resources during encoding and decoding on the stack. To prevent abuse of the stack, a stack limit is enforced. If the limit is reached due to a large payload, it defaults to allocating it on the heap for the particular encoding/decoding. This limit is by default set to 1024 bytes.

Branca also considers all tokens it generates to be valid for an hour from the time of their generation. You can configure this value based on your use-case. Alternatively, set it up as null to make Branca tokens valid forever.

There exists a Timer configuration for BrancaSettings too. It comes with an internal implementation that uses current time accordingly. You can pass on your own implementation of this interface if you want to control the way time flows for Branca. This will not be needed in all general cases.

Encoding/Encrypting Payload

Now that Branca is configured, you can encode your payload. Branca can accept string or ReadOnlySpan<byte> as payload. So, you can send a byte[] as payload if you are using some binary serialiser to save space.

If your payload is "Hello, World", you can generate a Branca token as follows:

string token = branca.Encode("Hello, World!");

The above will generate a token like:

XZAXYkd0ZbbCjxgr5xOelGcPXWhNFUy1oNfbQ3vfbi5LL4TIeyGq5rBKESaXXpyfjNBSOjbaOTlhWG

Likewise, you can make use of MessagePack or Protocol Buffer or even System.Text.Json to serialize your payload and pass in binary data to branca for generating a specific token.

Decoding/Decrypting Payload

Decoding a Branca token for validation is just as simple:

if (branca.TryDecode(token, out byte[] payload))
{
  // Hello, World!
  string message = Encoding.UTF8.GetString(payload);
}

You get to decide how to interpret the payload which comes out as an array of bytes. If you used MessagePack, deserialize the payload using it to the object it is supposed to be.

Decoding validates the token and even confirms the expiry (if you have setup some lifetime for the token, default being one hour). In case of failure, the TryDecode returns false and you can reject the token accordingly.

For whatever reason, if you also want to get the time the Branca token was created, you can do the following:

if (branca.TryDecode(token, out byte[] payload, out uint createTime))
{
  // successful decoding
}
else
{
  // unauthorized attempt
}

More Examples

Using System.Text.Json

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

Suppose you have a payload structure as follows:

public sealed record Payload
{
  [JsonPropertyName("s")]
  public ulong Subject { get; init; }

  [JsonPropertyName("n")]
  public string Name { get; init; }

  [JsonPropertyName("e")]
  public string Email { get; init; }
}

I explicitly setup the JsonPropertyName values for the properties to conserve token space.

Let's initialize the payload with some data:

Payload payload = new()
{
  Subject = 123456789, Name = "Some Name", Email = "some@example.com"
};

You can additionally setup the JsonSerializerOptions:

JsonSerializerOptions options = new()
{
  PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

And we finally can print a serialized version of this payload:

Console.WriteLine(JsonSerializer.Serialize(payload, options));
// {"s":123456789,"n":"Some Name","e":"some@example.com"}

We can tell JsonSerializer to serialize this payload directly to a byte array:

byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(payload, options);

And finally, with a BrancaService instance, we can encode and decode it as follows:

string token = branca.Encode(bytes);
// m6ehc87WdoSdE4WBJ1Phgt1a0OPXc3zOKn9NE3Rz7RWx2MJwaBZuI4s50wzRWahrGqJvGO2kXwvwbINpTdyP2qtJmuFuq9ADxFz05JEFAB2icTTF7GNr7TOS6reUU2nir8eU7
if (branca.TryDecode(token, out byte[] data, out uint createTime))
{
  var decodedPayload = JsonSerializer.Deserialize<Payload>(data, options);

  if (decodedPayload is not null)
  {
    Console.WriteLine(decodedPayload.Subject); // 123456789
    Console.WriteLine(decodedPayload.Name);    // Some Name
    Console.WriteLine(decodedPayload.Email);   // some@example.com
  }

  Console.WriteLine(DateTimeOffset.FromUnixTimeSeconds(createTime));
}

Using MessagePack

using MessagePack;
using System;

Suppose you have a payload structure as follows:

[MessagePackObject]
public sealed record Payload
{
  [Key(0)]
  public ulong Subject { get; init; }

  [Key(1)]
  public string Name { get; init; }

  [Key(2)]
  public string Email { get; init; }
}

Let's initialize the payload with some data:

Payload payload = new()
{
  Subject = 123456789, Name = "Some Name", Email = "some@example.com"
};

We can tell MessagePackSerializer to serialize this payload to a byte array:

byte[] bytes = MessagePackSerializer.Serialize(payload);

And finally, with a BrancaService instance, we can encode and decode it as follows:

string token = branca.Encode(bytes);
// Jm79HC2CHnD1glDNBPucf02yCnyF70Kv2M7ELsNNqI1kJlX8ftrlV0IIYvuyHURpgrzXmvu4uuhmbmOkX3d0gBpxguUDXomxMgKeuCtpj
if (branca.TryDecode(token, out byte[] data, out uint createTime))
{
  var decodedPayload = MessagePackSerializer.Deserialize<Payload>(data);

  Console.WriteLine(decodedPayload.Subject); // 123456789
  Console.WriteLine(decodedPayload.Name);    // Some Name
  Console.WriteLine(decodedPayload.Email);   // some@example.com

  Console.WriteLine(DateTimeOffset.FromUnixTimeSeconds(createTime));
}

License

Branca is a .NET Standard 2.1 library for generating and validating Branca tokens.
Copyright © 2022 Aman Agnihotri (amanagnihotri@pm.me)

Branca is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Branca is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with Branca. If not, see GNU Licenses.