Skip to content

Commit 397262e

Browse files
authored
Adds support for encrypted license keys (#335)
* implement encrypted key logic * cache public key * add SOURCEBOT_PUBLIC_KEY_PATH to docs * feedback
1 parent e5c6941 commit 397262e

File tree

9 files changed

+63
-4
lines changed

9 files changed

+63
-4
lines changed

.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ AUTH_URL="http://localhost:3000"
2424
# AUTH_EE_GOOGLE_CLIENT_SECRET=""
2525

2626
DATA_CACHE_DIR=${PWD}/.sourcebot # Path to the sourcebot cache dir (ex. ~/sourcebot/.sourcebot)
27+
SOURCEBOT_PUBLIC_KEY_PATH=${PWD}/public.pem
2728
# CONFIG_PATH=${PWD}/config.json # Path to the sourcebot config file (if one exists)
2829

2930
# Email

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111
- Added seperate page for signup. [#311](https://github.com/sourcebot-dev/sourcebot/pull/331)
12+
- Added encryption logic for license keys. [#335](https://github.com/sourcebot-dev/sourcebot/pull/335)
1213

1314
## [4.1.1] - 2025-06-03
1415

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,14 +174,15 @@ ENV REDIS_DATA_DIR=$DATA_CACHE_DIR/redis
174174
ENV DATABASE_URL="postgresql://postgres@localhost:5432/sourcebot"
175175
ENV REDIS_URL="redis://localhost:6379"
176176
ENV SRC_TENANT_ENFORCEMENT_MODE=strict
177+
ENV SOURCEBOT_PUBLIC_KEY_PATH=/app/public.pem
177178

178179
# Valid values are: debug, info, warn, error
179180
ENV SOURCEBOT_LOG_LEVEL=info
180181

181182
# Sourcebot collects anonymous usage data using [PostHog](https://posthog.com/). Uncomment this line to disable.
182183
# ENV SOURCEBOT_TELEMETRY_DISABLED=1
183184

184-
COPY package.json yarn.lock* .yarnrc.yml ./
185+
COPY package.json yarn.lock* .yarnrc.yml public.pem ./
185186
COPY .yarn ./.yarn
186187

187188
# Configure zoekt

docs/docs/configuration/environment-variables.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ The following environment variables allow you to configure your Sourcebot deploy
2626
| `SHARD_MAX_MATCH_COUNT` | `10000` | <p>The maximum shard count per query</p> |
2727
| `SMTP_CONNECTION_URL` | `-` | <p>The url to the SMTP service used for sending transactional emails. See [this doc](/docs/configuration/transactional-emails) for more info.</p> |
2828
| `SOURCEBOT_ENCRYPTION_KEY` | Automatically generated at startup if no value is provided. Generated using `openssl rand -base64 24` | <p>Used to encrypt connection secrets and generate API keys.</p> |
29+
| `SOURCEBOT_PUBLIC_KEY_PATH` | `/app/public.pem` | <p>Sourcebot's public key that's used to verify encrypted license key signatures.</p> |
2930
| `SOURCEBOT_LOG_LEVEL` | `info` | <p>The Sourcebot logging level. Valid values are `debug`, `info`, `warn`, `error`, in order of severity.</p> |
3031
| `SOURCEBOT_STRUCTURED_LOGGING_ENABLED` | `false` | <p>Enables/disable structured JSON logging. See [this doc](/docs/configuration/structured-logging) for more info.</p> |
3132
| `SOURCEBOT_STRUCTURED_LOGGING_FILE` | - | <p>Optional file to log to if structured logging is enabled</p> |

packages/crypto/src/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import crypto from 'crypto';
2+
import fs from 'fs';
23
import { SOURCEBOT_ENCRYPTION_KEY } from './environment';
34

45
const algorithm = 'aes-256-cbc';
56
const ivLength = 16; // 16 bytes for CBC
67

8+
const publicKeyCache = new Map<string, string>();
9+
710
const generateIV = (): Buffer => {
811
return crypto.randomBytes(ivLength);
912
};
@@ -63,3 +66,28 @@ export function decrypt(iv: string, encryptedText: string): string {
6366

6467
return decrypted;
6568
}
69+
70+
export function verifySignature(data: string, signature: string, publicKeyPath: string): boolean {
71+
try {
72+
let publicKey = publicKeyCache.get(publicKeyPath);
73+
74+
if (!publicKey) {
75+
if (!fs.existsSync(publicKeyPath)) {
76+
throw new Error(`Public key file not found at: ${publicKeyPath}`);
77+
}
78+
79+
publicKey = fs.readFileSync(publicKeyPath, 'utf8');
80+
publicKeyCache.set(publicKeyPath, publicKey);
81+
}
82+
83+
// Convert base64url signature to base64 if needed
84+
const base64Signature = signature.replace(/-/g, '+').replace(/_/g, '/');
85+
const paddedSignature = base64Signature + '='.repeat((4 - base64Signature.length % 4) % 4);
86+
const signatureBuffer = Buffer.from(paddedSignature, 'base64');
87+
88+
return crypto.verify(null, Buffer.from(data, 'utf8'), publicKey, signatureBuffer);
89+
} catch (error) {
90+
console.error('Error verifying signature:', error);
91+
return false;
92+
}
93+
}

packages/web/src/app/[domain]/settings/license/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,13 @@ export default async function LicensePage({ params: { domain } }: LicensePagePro
109109
<div className="grid grid-cols-2 gap-4">
110110
<div className="text-sm text-muted-foreground">Expiry Date</div>
111111
<div className={`text-sm font-mono ${isExpired ? 'text-destructive' : ''}`}>
112-
{expiryDate.toLocaleDateString("en-US", {
112+
{expiryDate.toLocaleString("en-US", {
113113
hour: "2-digit",
114114
minute: "2-digit",
115115
month: "long",
116116
day: "numeric",
117-
year: "numeric"
117+
year: "numeric",
118+
timeZoneName: "short"
118119
})} {isExpired && '(Expired)'}
119120
</div>
120121
</div>

packages/web/src/env.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export const env = createEnv({
5555

5656
DATA_CACHE_DIR: z.string(),
5757

58+
SOURCEBOT_PUBLIC_KEY_PATH: z.string(),
59+
5860
// Email
5961
SMTP_CONNECTION_URL: z.string().url().optional(),
6062
EMAIL_FROM_ADDRESS: z.string().email().optional(),

packages/web/src/features/entitlements/server.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { base64Decode } from "@/lib/utils";
44
import { z } from "zod";
55
import { SOURCEBOT_SUPPORT_EMAIL } from "@/lib/constants";
66
import { createLogger } from "@sourcebot/logger";
7+
import { verifySignature } from "@sourcebot/crypto";
78

89
const logger = createLogger('entitlements');
910

@@ -15,6 +16,7 @@ const eeLicenseKeyPayloadSchema = z.object({
1516
seats: z.number(),
1617
// ISO 8601 date string
1718
expiryDate: z.string().datetime(),
19+
sig: z.string(),
1820
});
1921

2022
type LicenseKeyPayload = z.infer<typeof eeLicenseKeyPayloadSchema>;
@@ -23,7 +25,26 @@ const decodeLicenseKeyPayload = (payload: string): LicenseKeyPayload => {
2325
try {
2426
const decodedPayload = base64Decode(payload);
2527
const payloadJson = JSON.parse(decodedPayload);
26-
return eeLicenseKeyPayloadSchema.parse(payloadJson);
28+
const licenseData = eeLicenseKeyPayloadSchema.parse(payloadJson);
29+
30+
if (env.SOURCEBOT_PUBLIC_KEY_PATH) {
31+
const dataToVerify = JSON.stringify({
32+
expiryDate: licenseData.expiryDate,
33+
id: licenseData.id,
34+
seats: licenseData.seats
35+
});
36+
37+
const isSignatureValid = verifySignature(dataToVerify, licenseData.sig, env.SOURCEBOT_PUBLIC_KEY_PATH);
38+
if (!isSignatureValid) {
39+
logger.error('License key signature verification failed');
40+
process.exit(1);
41+
}
42+
} else {
43+
logger.error('No public key path provided, unable to verify license key signature');
44+
process.exit(1);
45+
}
46+
47+
return licenseData;
2748
} catch (error) {
2849
logger.error(`Failed to decode license key payload: ${error}`);
2950
process.exit(1);

public.pem

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MCowBQYDK2VwAyEAJ8fwB3wMcuNPput/El4bK2F8vt/algcGxC6MiJqrT+c=
3+
-----END PUBLIC KEY-----

0 commit comments

Comments
 (0)