mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
Adds support for encrypted license keys (#335)
* implement encrypted key logic * cache public key * add SOURCEBOT_PUBLIC_KEY_PATH to docs * feedback
This commit is contained in:
parent
e5c6941d69
commit
397262ecf7
9 changed files with 63 additions and 4 deletions
|
|
@ -24,6 +24,7 @@ AUTH_URL="http://localhost:3000"
|
||||||
# AUTH_EE_GOOGLE_CLIENT_SECRET=""
|
# AUTH_EE_GOOGLE_CLIENT_SECRET=""
|
||||||
|
|
||||||
DATA_CACHE_DIR=${PWD}/.sourcebot # Path to the sourcebot cache dir (ex. ~/sourcebot/.sourcebot)
|
DATA_CACHE_DIR=${PWD}/.sourcebot # Path to the sourcebot cache dir (ex. ~/sourcebot/.sourcebot)
|
||||||
|
SOURCEBOT_PUBLIC_KEY_PATH=${PWD}/public.pem
|
||||||
# CONFIG_PATH=${PWD}/config.json # Path to the sourcebot config file (if one exists)
|
# CONFIG_PATH=${PWD}/config.json # Path to the sourcebot config file (if one exists)
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Added seperate page for signup. [#311](https://github.com/sourcebot-dev/sourcebot/pull/331)
|
- Added seperate page for signup. [#311](https://github.com/sourcebot-dev/sourcebot/pull/331)
|
||||||
|
- Added encryption logic for license keys. [#335](https://github.com/sourcebot-dev/sourcebot/pull/335)
|
||||||
|
|
||||||
## [4.1.1] - 2025-06-03
|
## [4.1.1] - 2025-06-03
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,7 @@ ENV REDIS_DATA_DIR=$DATA_CACHE_DIR/redis
|
||||||
ENV DATABASE_URL="postgresql://postgres@localhost:5432/sourcebot"
|
ENV DATABASE_URL="postgresql://postgres@localhost:5432/sourcebot"
|
||||||
ENV REDIS_URL="redis://localhost:6379"
|
ENV REDIS_URL="redis://localhost:6379"
|
||||||
ENV SRC_TENANT_ENFORCEMENT_MODE=strict
|
ENV SRC_TENANT_ENFORCEMENT_MODE=strict
|
||||||
|
ENV SOURCEBOT_PUBLIC_KEY_PATH=/app/public.pem
|
||||||
|
|
||||||
# Valid values are: debug, info, warn, error
|
# Valid values are: debug, info, warn, error
|
||||||
ENV SOURCEBOT_LOG_LEVEL=info
|
ENV SOURCEBOT_LOG_LEVEL=info
|
||||||
|
|
@ -181,7 +182,7 @@ ENV SOURCEBOT_LOG_LEVEL=info
|
||||||
# Sourcebot collects anonymous usage data using [PostHog](https://posthog.com/). Uncomment this line to disable.
|
# Sourcebot collects anonymous usage data using [PostHog](https://posthog.com/). Uncomment this line to disable.
|
||||||
# ENV SOURCEBOT_TELEMETRY_DISABLED=1
|
# ENV SOURCEBOT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
COPY package.json yarn.lock* .yarnrc.yml ./
|
COPY package.json yarn.lock* .yarnrc.yml public.pem ./
|
||||||
COPY .yarn ./.yarn
|
COPY .yarn ./.yarn
|
||||||
|
|
||||||
# Configure zoekt
|
# Configure zoekt
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ The following environment variables allow you to configure your Sourcebot deploy
|
||||||
| `SHARD_MAX_MATCH_COUNT` | `10000` | <p>The maximum shard count per query</p> |
|
| `SHARD_MAX_MATCH_COUNT` | `10000` | <p>The maximum shard count per query</p> |
|
||||||
| `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> |
|
| `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> |
|
||||||
| `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> |
|
| `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> |
|
||||||
|
| `SOURCEBOT_PUBLIC_KEY_PATH` | `/app/public.pem` | <p>Sourcebot's public key that's used to verify encrypted license key signatures.</p> |
|
||||||
| `SOURCEBOT_LOG_LEVEL` | `info` | <p>The Sourcebot logging level. Valid values are `debug`, `info`, `warn`, `error`, in order of severity.</p> |
|
| `SOURCEBOT_LOG_LEVEL` | `info` | <p>The Sourcebot logging level. Valid values are `debug`, `info`, `warn`, `error`, in order of severity.</p> |
|
||||||
| `SOURCEBOT_STRUCTURED_LOGGING_ENABLED` | `false` | <p>Enables/disable structured JSON logging. See [this doc](/docs/configuration/structured-logging) for more info.</p> |
|
| `SOURCEBOT_STRUCTURED_LOGGING_ENABLED` | `false` | <p>Enables/disable structured JSON logging. See [this doc](/docs/configuration/structured-logging) for more info.</p> |
|
||||||
| `SOURCEBOT_STRUCTURED_LOGGING_FILE` | - | <p>Optional file to log to if structured logging is enabled</p> |
|
| `SOURCEBOT_STRUCTURED_LOGGING_FILE` | - | <p>Optional file to log to if structured logging is enabled</p> |
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import fs from 'fs';
|
||||||
import { SOURCEBOT_ENCRYPTION_KEY } from './environment';
|
import { SOURCEBOT_ENCRYPTION_KEY } from './environment';
|
||||||
|
|
||||||
const algorithm = 'aes-256-cbc';
|
const algorithm = 'aes-256-cbc';
|
||||||
const ivLength = 16; // 16 bytes for CBC
|
const ivLength = 16; // 16 bytes for CBC
|
||||||
|
|
||||||
|
const publicKeyCache = new Map<string, string>();
|
||||||
|
|
||||||
const generateIV = (): Buffer => {
|
const generateIV = (): Buffer => {
|
||||||
return crypto.randomBytes(ivLength);
|
return crypto.randomBytes(ivLength);
|
||||||
};
|
};
|
||||||
|
|
@ -63,3 +66,28 @@ export function decrypt(iv: string, encryptedText: string): string {
|
||||||
|
|
||||||
return decrypted;
|
return decrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function verifySignature(data: string, signature: string, publicKeyPath: string): boolean {
|
||||||
|
try {
|
||||||
|
let publicKey = publicKeyCache.get(publicKeyPath);
|
||||||
|
|
||||||
|
if (!publicKey) {
|
||||||
|
if (!fs.existsSync(publicKeyPath)) {
|
||||||
|
throw new Error(`Public key file not found at: ${publicKeyPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey = fs.readFileSync(publicKeyPath, 'utf8');
|
||||||
|
publicKeyCache.set(publicKeyPath, publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert base64url signature to base64 if needed
|
||||||
|
const base64Signature = signature.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const paddedSignature = base64Signature + '='.repeat((4 - base64Signature.length % 4) % 4);
|
||||||
|
const signatureBuffer = Buffer.from(paddedSignature, 'base64');
|
||||||
|
|
||||||
|
return crypto.verify(null, Buffer.from(data, 'utf8'), publicKey, signatureBuffer);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error verifying signature:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,12 +109,13 @@ export default async function LicensePage({ params: { domain } }: LicensePagePro
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="text-sm text-muted-foreground">Expiry Date</div>
|
<div className="text-sm text-muted-foreground">Expiry Date</div>
|
||||||
<div className={`text-sm font-mono ${isExpired ? 'text-destructive' : ''}`}>
|
<div className={`text-sm font-mono ${isExpired ? 'text-destructive' : ''}`}>
|
||||||
{expiryDate.toLocaleDateString("en-US", {
|
{expiryDate.toLocaleString("en-US", {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
year: "numeric"
|
year: "numeric",
|
||||||
|
timeZoneName: "short"
|
||||||
})} {isExpired && '(Expired)'}
|
})} {isExpired && '(Expired)'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@ export const env = createEnv({
|
||||||
|
|
||||||
DATA_CACHE_DIR: z.string(),
|
DATA_CACHE_DIR: z.string(),
|
||||||
|
|
||||||
|
SOURCEBOT_PUBLIC_KEY_PATH: z.string(),
|
||||||
|
|
||||||
// Email
|
// Email
|
||||||
SMTP_CONNECTION_URL: z.string().url().optional(),
|
SMTP_CONNECTION_URL: z.string().url().optional(),
|
||||||
EMAIL_FROM_ADDRESS: z.string().email().optional(),
|
EMAIL_FROM_ADDRESS: z.string().email().optional(),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { base64Decode } from "@/lib/utils";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { SOURCEBOT_SUPPORT_EMAIL } from "@/lib/constants";
|
import { SOURCEBOT_SUPPORT_EMAIL } from "@/lib/constants";
|
||||||
import { createLogger } from "@sourcebot/logger";
|
import { createLogger } from "@sourcebot/logger";
|
||||||
|
import { verifySignature } from "@sourcebot/crypto";
|
||||||
|
|
||||||
const logger = createLogger('entitlements');
|
const logger = createLogger('entitlements');
|
||||||
|
|
||||||
|
|
@ -15,6 +16,7 @@ const eeLicenseKeyPayloadSchema = z.object({
|
||||||
seats: z.number(),
|
seats: z.number(),
|
||||||
// ISO 8601 date string
|
// ISO 8601 date string
|
||||||
expiryDate: z.string().datetime(),
|
expiryDate: z.string().datetime(),
|
||||||
|
sig: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type LicenseKeyPayload = z.infer<typeof eeLicenseKeyPayloadSchema>;
|
type LicenseKeyPayload = z.infer<typeof eeLicenseKeyPayloadSchema>;
|
||||||
|
|
@ -23,7 +25,26 @@ const decodeLicenseKeyPayload = (payload: string): LicenseKeyPayload => {
|
||||||
try {
|
try {
|
||||||
const decodedPayload = base64Decode(payload);
|
const decodedPayload = base64Decode(payload);
|
||||||
const payloadJson = JSON.parse(decodedPayload);
|
const payloadJson = JSON.parse(decodedPayload);
|
||||||
return eeLicenseKeyPayloadSchema.parse(payloadJson);
|
const licenseData = eeLicenseKeyPayloadSchema.parse(payloadJson);
|
||||||
|
|
||||||
|
if (env.SOURCEBOT_PUBLIC_KEY_PATH) {
|
||||||
|
const dataToVerify = JSON.stringify({
|
||||||
|
expiryDate: licenseData.expiryDate,
|
||||||
|
id: licenseData.id,
|
||||||
|
seats: licenseData.seats
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSignatureValid = verifySignature(dataToVerify, licenseData.sig, env.SOURCEBOT_PUBLIC_KEY_PATH);
|
||||||
|
if (!isSignatureValid) {
|
||||||
|
logger.error('License key signature verification failed');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.error('No public key path provided, unable to verify license key signature');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return licenseData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to decode license key payload: ${error}`);
|
logger.error(`Failed to decode license key payload: ${error}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
3
public.pem
Normal file
3
public.pem
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MCowBQYDK2VwAyEAJ8fwB3wMcuNPput/El4bK2F8vt/algcGxC6MiJqrT+c=
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
Loading…
Reference in a new issue