mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-04 19:11:57 +08: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:
@@ -138,6 +138,7 @@ jobs:
|
|||||||
uses: ./.github/actions/build-rust
|
uses: ./.github/actions/build-rust
|
||||||
env:
|
env:
|
||||||
AFFINE_PRO_PUBLIC_KEY: ${{ secrets.AFFINE_PRO_PUBLIC_KEY }}
|
AFFINE_PRO_PUBLIC_KEY: ${{ secrets.AFFINE_PRO_PUBLIC_KEY }}
|
||||||
|
AFFINE_PRO_LICENSE_AES_KEY: ${{ secrets.AFFINE_PRO_LICENSE_AES_KEY }}
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.targets.name }}
|
target: ${{ matrix.targets.name }}
|
||||||
package: '@affine/server-native'
|
package: '@affine/server-native'
|
||||||
|
|||||||
Vendored
+2
@@ -4,6 +4,8 @@ export declare class Tokenizer {
|
|||||||
count(content: string, allowedSpecial?: Array<string> | undefined | null): number
|
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 const AFFINE_PRO_PUBLIC_KEY: string | undefined | null
|
||||||
|
|
||||||
export declare function fromModelName(modelName: string): Tokenizer | 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]
|
#[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 testWorkspaceId = 'd6f52bc7-d62a-4822-804a-335fa7dfe5a6';
|
||||||
const testPublicKey = `-----BEGIN PUBLIC KEY-----
|
const testPublicKey = `-----BEGIN PUBLIC KEY-----
|
||||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkXqe9HR32rSgaNxaePXbwHxoanUq
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqrxlczPknUuj4q4xx1VGr063Cgu7
|
||||||
1DcQTV1twn+G47/HiY+rj7oOw3cLNzOVe7+4Uxn8SMZ/XLImtqFQ6I4WVg==
|
Hc3w7v4FGmoA5MNzzhrkho1ckDYw2wrX6zBnehFzcivURv80HherE2GQjg==
|
||||||
-----END PUBLIC KEY-----`;
|
-----END PUBLIC KEY-----`;
|
||||||
|
const testTestLicenseAESKey = 'TEST_LICENSE_AES_KEY';
|
||||||
|
|
||||||
const fixturesDir = join(import.meta.dirname, '__fixtures__');
|
const fixturesDir = join(import.meta.dirname, '__fixtures__');
|
||||||
function getLicense(file: string) {
|
function getLicense(file: string) {
|
||||||
@@ -39,6 +40,7 @@ let owner: MockedUser;
|
|||||||
e2e.before(async () => {
|
e2e.before(async () => {
|
||||||
process.env.DEPLOYMENT_TYPE = 'selfhosted';
|
process.env.DEPLOYMENT_TYPE = 'selfhosted';
|
||||||
process.env.AFFiNE_PRO_PUBLIC_KEY = testPublicKey;
|
process.env.AFFiNE_PRO_PUBLIC_KEY = testPublicKey;
|
||||||
|
process.env.AFFiNE_PRO_LICENSE_AES_KEY = testTestLicenseAESKey;
|
||||||
refreshEnv();
|
refreshEnv();
|
||||||
|
|
||||||
app = await createApp();
|
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:
|
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,
|
verify as verifyPassword,
|
||||||
} from '@node-rs/argon2';
|
} 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 { Config } from '../config';
|
||||||
import { OnEvent } from '../event';
|
import { OnEvent } from '../event';
|
||||||
|
|
||||||
@@ -59,10 +62,12 @@ export class CryptoHelper implements OnModuleInit {
|
|||||||
};
|
};
|
||||||
|
|
||||||
AFFiNEProPublicKey: Buffer | null = null;
|
AFFiNEProPublicKey: Buffer | null = null;
|
||||||
|
AFFiNEProLicenseAESKey: Buffer | null = null;
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
if (env.selfhosted) {
|
if (env.selfhosted) {
|
||||||
this.AFFiNEProPublicKey = this.loadAFFiNEProPublicKey();
|
this.AFFiNEProPublicKey = this.loadAFFiNEProPublicKey();
|
||||||
|
this.AFFiNEProLicenseAESKey = this.loadAFFiNEProLicenseAESKey();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,4 +202,20 @@ export class CryptoHelper implements OnModuleInit {
|
|||||||
|
|
||||||
return null;
|
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 {
|
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(
|
NAMESPACE = readEnv(
|
||||||
'AFFINE_ENV',
|
'AFFINE_ENV',
|
||||||
Namespace.Production,
|
Namespace.Production,
|
||||||
@@ -134,6 +134,14 @@ export class Env implements AppEnv {
|
|||||||
get gcp() {
|
get gcp() {
|
||||||
return this.platform === Platform.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 = () => {
|
export const createGlobalEnv = () => {
|
||||||
|
|||||||
@@ -22,3 +22,5 @@ export const Tokenizer = serverNativeModule.Tokenizer;
|
|||||||
export const fromModelName = serverNativeModule.fromModelName;
|
export const fromModelName = serverNativeModule.fromModelName;
|
||||||
export const htmlSanitize = serverNativeModule.htmlSanitize;
|
export const htmlSanitize = serverNativeModule.htmlSanitize;
|
||||||
export const AFFINE_PRO_PUBLIC_KEY = serverNativeModule.AFFINE_PRO_PUBLIC_KEY;
|
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
|
// we use workspace id as aes key hash plain text content
|
||||||
// verify signature to make sure the payload or signature is not forged
|
// verify signature to make sure the payload or signature is not forged
|
||||||
const {
|
const { payload: payloadStr, signature, iv } = this.decryptLicense(buf);
|
||||||
payload: payloadStr,
|
|
||||||
signature,
|
|
||||||
iv,
|
|
||||||
} = this.decryptLicense(workspaceId, buf);
|
|
||||||
|
|
||||||
const verifier = createVerify('rsa-sha256');
|
const verifier = createVerify('rsa-sha256');
|
||||||
verifier.update(iv);
|
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) {
|
if (parseResult.data.data.workspaceId !== workspaceId) {
|
||||||
throw new InvalidLicenseToActivate({
|
throw new InvalidLicenseToActivate({
|
||||||
reason: 'Workspace mismatched with license.',
|
reason: 'Workspace mismatched with license.',
|
||||||
@@ -524,7 +527,13 @@ export class LicenseService {
|
|||||||
return parseResult.data;
|
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) {
|
if (buf.length < 2) {
|
||||||
throw new InvalidLicenseToActivate({
|
throw new InvalidLicenseToActivate({
|
||||||
reason: 'Invalid license file.',
|
reason: 'Invalid license file.',
|
||||||
@@ -539,12 +548,14 @@ export class LicenseService {
|
|||||||
const tag = buf.subarray(2 + ivLength, 2 + ivLength + authTagLength);
|
const tag = buf.subarray(2 + ivLength, 2 + ivLength + authTagLength);
|
||||||
const payload = buf.subarray(2 + ivLength + authTagLength);
|
const payload = buf.subarray(2 + ivLength + authTagLength);
|
||||||
|
|
||||||
const aesKey = this.crypto.sha256(
|
const decipher = createDecipheriv(
|
||||||
`WORKSPACE_PAYLOAD_AES_KEY:${workspaceId}`
|
'aes-256-gcm',
|
||||||
|
this.crypto.AFFiNEProLicenseAESKey,
|
||||||
|
iv,
|
||||||
|
{
|
||||||
|
authTagLength,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
const decipher = createDecipheriv('aes-256-gcm', aesKey, iv, {
|
|
||||||
authTagLength,
|
|
||||||
});
|
|
||||||
decipher.setAuthTag(tag);
|
decipher.setAuthTag(tag);
|
||||||
|
|
||||||
const decrypted = Buffer.concat([
|
const decrypted = Buffer.concat([
|
||||||
@@ -564,7 +575,7 @@ export class LicenseService {
|
|||||||
} catch {
|
} catch {
|
||||||
// we use workspace id as aes key hash plain text content
|
// we use workspace id as aes key hash plain text content
|
||||||
throw new InvalidLicenseToActivate({
|
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 },
|
stats: { errorDetails: true },
|
||||||
optimization: { nodeEnv: false },
|
optimization: { nodeEnv: false },
|
||||||
|
|||||||
Reference in New Issue
Block a user