mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
refactor(server): decrypt license with provided aes key (#12570)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for a new AES key for license management, improving license encryption and decryption processes. - **Bug Fixes** - Improved error messages and handling when activating expired or invalid licenses. - **Refactor** - Updated license decryption logic to use a fixed AES key instead of deriving one from the workspace ID. - Added validation for environment variable values to prevent invalid configurations. - **Tests** - Enhanced license-related tests to cover new key usage and updated error messages. - Updated environment variable validation tests with clearer error messages. - **Chores** - Updated environment variable handling for improved consistency. - Set production environment variable explicitly in build configuration. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1
.github/workflows/build-images.yml
vendored
1
.github/workflows/build-images.yml
vendored
@@ -138,6 +138,7 @@ jobs:
|
||||
uses: ./.github/actions/build-rust
|
||||
env:
|
||||
AFFINE_PRO_PUBLIC_KEY: ${{ secrets.AFFINE_PRO_PUBLIC_KEY }}
|
||||
AFFINE_PRO_LICENSE_AES_KEY: ${{ secrets.AFFINE_PRO_LICENSE_AES_KEY }}
|
||||
with:
|
||||
target: ${{ matrix.targets.name }}
|
||||
package: '@affine/server-native'
|
||||
|
||||
2
packages/backend/native/index.d.ts
vendored
2
packages/backend/native/index.d.ts
vendored
@@ -4,6 +4,8 @@ export declare class Tokenizer {
|
||||
count(content: string, allowedSpecial?: Array<string> | undefined | null): number
|
||||
}
|
||||
|
||||
export const AFFINE_PRO_LICENSE_AES_KEY: string | undefined | null
|
||||
|
||||
export const AFFINE_PRO_PUBLIC_KEY: string | undefined | null
|
||||
|
||||
export declare function fromModelName(modelName: string): Tokenizer | null
|
||||
|
||||
@@ -54,4 +54,8 @@ pub fn merge_updates_in_apply_way(updates: Vec<Buffer>) -> Result<Buffer> {
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub const AFFINE_PRO_PUBLIC_KEY: Option<&'static str> = std::option_env!("AFFINE_PRO_PUBLIC_KEY",);
|
||||
pub const AFFINE_PRO_PUBLIC_KEY: Option<&'static str> = std::option_env!("AFFINE_PRO_PUBLIC_KEY");
|
||||
|
||||
#[napi]
|
||||
pub const AFFINE_PRO_LICENSE_AES_KEY: Option<&'static str> =
|
||||
std::option_env!("AFFINE_PRO_LICENSE_AES_KEY");
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -15,9 +15,10 @@ import {
|
||||
|
||||
const testWorkspaceId = 'd6f52bc7-d62a-4822-804a-335fa7dfe5a6';
|
||||
const testPublicKey = `-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkXqe9HR32rSgaNxaePXbwHxoanUq
|
||||
1DcQTV1twn+G47/HiY+rj7oOw3cLNzOVe7+4Uxn8SMZ/XLImtqFQ6I4WVg==
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqrxlczPknUuj4q4xx1VGr063Cgu7
|
||||
Hc3w7v4FGmoA5MNzzhrkho1ckDYw2wrX6zBnehFzcivURv80HherE2GQjg==
|
||||
-----END PUBLIC KEY-----`;
|
||||
const testTestLicenseAESKey = 'TEST_LICENSE_AES_KEY';
|
||||
|
||||
const fixturesDir = join(import.meta.dirname, '__fixtures__');
|
||||
function getLicense(file: string) {
|
||||
@@ -39,6 +40,7 @@ let owner: MockedUser;
|
||||
e2e.before(async () => {
|
||||
process.env.DEPLOYMENT_TYPE = 'selfhosted';
|
||||
process.env.AFFiNE_PRO_PUBLIC_KEY = testPublicKey;
|
||||
process.env.AFFiNE_PRO_LICENSE_AES_KEY = testTestLicenseAESKey;
|
||||
refreshEnv();
|
||||
|
||||
app = await createApp();
|
||||
@@ -123,7 +125,8 @@ e2e('should not install expired license', async t => {
|
||||
},
|
||||
}),
|
||||
{
|
||||
message: 'License has expired.',
|
||||
message:
|
||||
'Invalid license to activate. License file has expired. Please contact with Affine support to fetch a latest one.',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ test('should read NODE_ENV', t => {
|
||||
},
|
||||
{
|
||||
message:
|
||||
'Invalid value "unknown" for environment variable NODE_ENV, expected one of ["development","test","production"]',
|
||||
'Invalid NODE_ENV environment. `unknown` is not a valid NODE_ENV value.',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
verify as verifyPassword,
|
||||
} from '@node-rs/argon2';
|
||||
|
||||
import { AFFINE_PRO_PUBLIC_KEY } from '../../native';
|
||||
import {
|
||||
AFFINE_PRO_LICENSE_AES_KEY,
|
||||
AFFINE_PRO_PUBLIC_KEY,
|
||||
} from '../../native';
|
||||
import { Config } from '../config';
|
||||
import { OnEvent } from '../event';
|
||||
|
||||
@@ -59,10 +62,12 @@ export class CryptoHelper implements OnModuleInit {
|
||||
};
|
||||
|
||||
AFFiNEProPublicKey: Buffer | null = null;
|
||||
AFFiNEProLicenseAESKey: Buffer | null = null;
|
||||
|
||||
onModuleInit() {
|
||||
if (env.selfhosted) {
|
||||
this.AFFiNEProPublicKey = this.loadAFFiNEProPublicKey();
|
||||
this.AFFiNEProLicenseAESKey = this.loadAFFiNEProLicenseAESKey();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,4 +202,20 @@ export class CryptoHelper implements OnModuleInit {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private loadAFFiNEProLicenseAESKey() {
|
||||
if (AFFINE_PRO_LICENSE_AES_KEY) {
|
||||
return this.sha256(AFFINE_PRO_LICENSE_AES_KEY);
|
||||
} else {
|
||||
this.logger.warn(
|
||||
'AFFINE_PRO_LICENSE_AES_KEY is not set at compile time.'
|
||||
);
|
||||
}
|
||||
|
||||
if (!env.prod && process.env.AFFiNE_PRO_LICENSE_AES_KEY) {
|
||||
return this.sha256(process.env.AFFiNE_PRO_LICENSE_AES_KEY);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ globalThis.readEnv = function readEnv<T>(
|
||||
};
|
||||
|
||||
export class Env implements AppEnv {
|
||||
NODE_ENV = readEnv('NODE_ENV', NodeEnv.Production, Object.values(NodeEnv));
|
||||
NODE_ENV = (process.env.NODE_ENV ?? NodeEnv.Production) as NodeEnv;
|
||||
NAMESPACE = readEnv(
|
||||
'AFFINE_ENV',
|
||||
Namespace.Production,
|
||||
@@ -134,6 +134,14 @@ export class Env implements AppEnv {
|
||||
get gcp() {
|
||||
return this.platform === Platform.GCP;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
if (!Object.values(NodeEnv).includes(this.NODE_ENV)) {
|
||||
throw new Error(
|
||||
`Invalid NODE_ENV environment. \`${this.NODE_ENV}\` is not a valid NODE_ENV value.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const createGlobalEnv = () => {
|
||||
|
||||
@@ -22,3 +22,5 @@ export const Tokenizer = serverNativeModule.Tokenizer;
|
||||
export const fromModelName = serverNativeModule.fromModelName;
|
||||
export const htmlSanitize = serverNativeModule.htmlSanitize;
|
||||
export const AFFINE_PRO_PUBLIC_KEY = serverNativeModule.AFFINE_PRO_PUBLIC_KEY;
|
||||
export const AFFINE_PRO_LICENSE_AES_KEY =
|
||||
serverNativeModule.AFFINE_PRO_LICENSE_AES_KEY;
|
||||
|
||||
@@ -485,11 +485,7 @@ export class LicenseService {
|
||||
|
||||
// we use workspace id as aes key hash plain text content
|
||||
// verify signature to make sure the payload or signature is not forged
|
||||
const {
|
||||
payload: payloadStr,
|
||||
signature,
|
||||
iv,
|
||||
} = this.decryptLicense(workspaceId, buf);
|
||||
const { payload: payloadStr, signature, iv } = this.decryptLicense(buf);
|
||||
|
||||
const verifier = createVerify('rsa-sha256');
|
||||
verifier.update(iv);
|
||||
@@ -515,6 +511,13 @@ export class LicenseService {
|
||||
});
|
||||
}
|
||||
|
||||
if (new Date(parseResult.data.expiresAt) < new Date()) {
|
||||
throw new InvalidLicenseToActivate({
|
||||
reason:
|
||||
'License file has expired. Please contact with Affine support to fetch a latest one.',
|
||||
});
|
||||
}
|
||||
|
||||
if (parseResult.data.data.workspaceId !== workspaceId) {
|
||||
throw new InvalidLicenseToActivate({
|
||||
reason: 'Workspace mismatched with license.',
|
||||
@@ -524,7 +527,13 @@ export class LicenseService {
|
||||
return parseResult.data;
|
||||
}
|
||||
|
||||
private decryptLicense(workspaceId: string, buf: Buffer) {
|
||||
private decryptLicense(buf: Buffer) {
|
||||
if (!this.crypto.AFFiNEProLicenseAESKey) {
|
||||
throw new InternalServerError(
|
||||
'License AES key is not loaded. Please contact with Affine support.'
|
||||
);
|
||||
}
|
||||
|
||||
if (buf.length < 2) {
|
||||
throw new InvalidLicenseToActivate({
|
||||
reason: 'Invalid license file.',
|
||||
@@ -539,12 +548,14 @@ export class LicenseService {
|
||||
const tag = buf.subarray(2 + ivLength, 2 + ivLength + authTagLength);
|
||||
const payload = buf.subarray(2 + ivLength + authTagLength);
|
||||
|
||||
const aesKey = this.crypto.sha256(
|
||||
`WORKSPACE_PAYLOAD_AES_KEY:${workspaceId}`
|
||||
const decipher = createDecipheriv(
|
||||
'aes-256-gcm',
|
||||
this.crypto.AFFiNEProLicenseAESKey,
|
||||
iv,
|
||||
{
|
||||
authTagLength,
|
||||
}
|
||||
);
|
||||
const decipher = createDecipheriv('aes-256-gcm', aesKey, iv, {
|
||||
authTagLength,
|
||||
});
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
@@ -564,7 +575,7 @@ export class LicenseService {
|
||||
} catch {
|
||||
// we use workspace id as aes key hash plain text content
|
||||
throw new InvalidLicenseToActivate({
|
||||
reason: 'Workspace mismatched with license.',
|
||||
reason: 'Failed to verify the license.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,6 +601,9 @@ export function createNodeTargetConfig(
|
||||
);
|
||||
},
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
}),
|
||||
]),
|
||||
stats: { errorDetails: true },
|
||||
optimization: { nodeEnv: false },
|
||||
|
||||
Reference in New Issue
Block a user