Skip to content

Commit d97dd07

Browse files
authored
License: Add license id to the generated license file and license related warnings (#33060)
1 parent 954ce27 commit d97dd07

File tree

15 files changed

+565
-159
lines changed

15 files changed

+565
-159
lines changed

packages/devextreme/js/__internal/core/license/const.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ export const KEY_SPLITTER = '.';
55
export const BUY_NOW_LINK = 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeJQuery.aspx';
66
export const LICENSING_DOC_LINK = 'https://go.devexpress.com/Licensing_Documentation_DevExtremeJQuery.aspx';
77

8+
export const LICENSE_KEY_PLACEHOLDER = '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */';
9+
810
export const NBSP = '\u00A0';
911
export const SUBSCRIPTION_NAMES = `Universal, DXperience, ASP.NET${NBSP}and${NBSP}Blazor, DevExtreme${NBSP}Complete`;

packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { describe, expect, it } from '@jest/globals';
1+
/* eslint-disable */
2+
3+
import {
4+
describe,
5+
expect,
6+
it,
7+
jest,
8+
} from '@jest/globals';
29
import { version as currentVersion } from '@js/core/version';
310

411
import { parseVersion } from '../../../utils/version';
@@ -7,6 +14,38 @@ import { parseDevExpressProductKey } from './lcp_key_validator';
714
import { findLatestDevExtremeVersion, isLicenseValid } from './license_info';
815
import { createProductInfo } from './product_info';
916

17+
const DOT_NET_TICKS_EPOCH_OFFSET = 621355968000000000n;
18+
const DOT_NET_TICKS_PER_MS = 10000n;
19+
const DEVEXTREME_HTML_JS_BIT = 1n << 54n;
20+
21+
function msToDotNetTicks(ms: number): string {
22+
return (BigInt(ms) * DOT_NET_TICKS_PER_MS + DOT_NET_TICKS_EPOCH_OFFSET).toString();
23+
}
24+
25+
function createLcpSource(payload: string): string {
26+
const signature = 'A'.repeat(136);
27+
return `LCPv1${btoa(`${signature}${payload}`)}`;
28+
}
29+
30+
function loadParserWithBypassedSignatureCheck() {
31+
jest.resetModules();
32+
jest.doMock('./utils', () => {
33+
const actual = jest.requireActual('./utils') as Record<string, unknown>;
34+
return {
35+
...actual,
36+
encodeString: (text: string) => text,
37+
shiftDecodeText: (text: string) => text,
38+
verifyHash: () => true,
39+
};
40+
});
41+
42+
// eslint-disable-next-line
43+
const { parseDevExpressProductKey } = require('./lcp_key_validator');
44+
// eslint-disable-next-line
45+
const { TokenKind } = require('../types');
46+
return { parseDevExpressProductKey, TokenKind };
47+
}
48+
1049
function getTrialLicense() {
1150
const { major, minor } = parseVersion(currentVersion);
1251
const products = [
@@ -34,4 +73,27 @@ describe('LCP key validation', () => {
3473

3574
expect(version).toBe(undefined);
3675
});
76+
77+
it('does not classify a valid DevExtreme product key as trial-expired when expiration metadata is in the past', () => {
78+
const { parseDevExpressProductKey, TokenKind } = loadParserWithBypassedSignatureCheck();
79+
const expiredAt = msToDotNetTicks(Date.UTC(2020, 0, 1));
80+
81+
const payload = `meta;251,${DEVEXTREME_HTML_JS_BIT},0,${expiredAt};`;
82+
const token = parseDevExpressProductKey(createLcpSource(payload));
83+
84+
expect(token.kind).toBe(TokenKind.verified);
85+
});
86+
87+
it('returns trial-expired for expired trial keys without DevExtreme product access', () => {
88+
const { parseDevExpressProductKey, TokenKind } = loadParserWithBypassedSignatureCheck();
89+
const expiredAt = msToDotNetTicks(Date.UTC(2020, 0, 1));
90+
91+
const payload = `meta;251,0,0,${expiredAt};`;
92+
const token = parseDevExpressProductKey(createLcpSource(payload));
93+
94+
expect(token.kind).toBe(TokenKind.corrupted);
95+
if (token.kind === TokenKind.corrupted) {
96+
expect(token.error).toBe('trial-expired');
97+
}
98+
});
3799
});

packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@ import {
66
PRODUCT_KIND_ERROR,
77
type Token,
88
TokenKind,
9+
TRIAL_EXPIRED_ERROR,
910
VERIFICATION_ERROR,
1011
} from '../types';
1112
import {
1213
LCP_SIGNATURE,
1314
RSA_PUBLIC_KEY_XML,
1415
SIGN_LENGTH,
1516
} from './const';
16-
import { findLatestDevExtremeVersion } from './license_info';
17+
import { findLatestDevExtremeVersion, getMaxExpiration } from './license_info';
1718
import { createProductInfo, type ProductInfo } from './product_info';
18-
import { encodeString, shiftDecodeText, verifyHash } from './utils';
19+
import {
20+
dotNetTicksToMs,
21+
encodeString,
22+
shiftDecodeText,
23+
verifyHash,
24+
} from './utils';
1925

2026
interface ParsedProducts {
2127
products: ProductInfo[];
@@ -41,9 +47,11 @@ function productsFromString(encodedString: string): ParsedProducts {
4147
const parts = tuple.split(',');
4248
const version = Number.parseInt(parts[0], 10);
4349
const productsValue = BigInt(parts[1]);
50+
const expiration = parts.length > 3 ? dotNetTicksToMs(parts[3]) : Infinity;
4451
return createProductInfo(
4552
version,
4653
productsValue,
54+
expiration,
4755
);
4856
});
4957

@@ -88,6 +96,13 @@ export function parseDevExpressProductKey(productsLicenseSource: string): Token
8896

8997
const maxVersionAllowed = findLatestDevExtremeVersion({ products });
9098

99+
if (!maxVersionAllowed) {
100+
const maxExpiration = getMaxExpiration({ products });
101+
if (maxExpiration !== Infinity && maxExpiration < Date.now()) {
102+
return TRIAL_EXPIRED_ERROR;
103+
}
104+
}
105+
91106
if (!maxVersionAllowed) {
92107
return PRODUCT_KIND_ERROR;
93108
}

packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ export function isLicenseValid(info: LicenseInfo): boolean {
99
return Array.isArray(info.products) && info.products.length > 0;
1010
}
1111

12+
export function getMaxExpiration(info: LicenseInfo): number {
13+
const expirationDates = info.products
14+
.map((p) => p.expiration)
15+
.filter((e) => e > 0 && e !== Infinity);
16+
if (expirationDates.length === 0) return Infinity;
17+
return Math.max(...expirationDates);
18+
}
19+
1220
export function findLatestDevExtremeVersion(info: LicenseInfo): number | undefined {
1321
if (!isLicenseValid(info)) {
1422
return undefined;

packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22
export interface ProductInfo {
33
readonly version: number;
44
readonly products: bigint;
5+
readonly expiration: number;
56
}
67

7-
export function createProductInfo(version: number, products: bigint): ProductInfo {
8-
return { version, products: BigInt(products) };
8+
export function createProductInfo(
9+
version: number,
10+
products: bigint,
11+
expiration = Infinity,
12+
): ProductInfo {
13+
return { version, products: BigInt(products), expiration };
914
}
1015

1116
export function isProduct(info: ProductInfo, ...productIds: bigint[]): boolean {

packages/devextreme/js/__internal/core/license/lcp_key_validation/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ export const shiftText = (text: string, map: string): string => {
5151

5252
export const shiftDecodeText = (text: string): string => shiftText(text, DECODE_MAP);
5353

54+
const DOT_NET_TICKS_EPOCH_OFFSET = 621355968000000000n;
55+
const DOT_NET_TICKS_PER_MS = 10000n;
56+
const DOT_NET_MAX_VALUE_TICKS = 3155378975999999999n;
57+
58+
export function dotNetTicksToMs(ticksStr: string): number {
59+
const ticks = BigInt(ticksStr);
60+
if (ticks >= DOT_NET_MAX_VALUE_TICKS) return Infinity;
61+
return Number((ticks - DOT_NET_TICKS_EPOCH_OFFSET) / DOT_NET_TICKS_PER_MS);
62+
}
63+
5464
export const verifyHash = (xmlKey: string, data: string, signature: string): boolean => {
5565
const { modulus, exponent } = parseRsaXml(xmlKey);
5666

packages/devextreme/js/__internal/core/license/license_validation.test.ts

Lines changed: 27 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
assertedVersionsCompatible,
1111
clearAssertedVersions,
1212
} from '../../utils/version';
13+
import { LICENSE_KEY_PLACEHOLDER } from './const';
1314
import {
1415
parseLicenseKey,
1516
setLicenseCheckSkipCondition,
@@ -294,9 +295,11 @@ describe('license check', () => {
294295
const TOKEN_UNSUPPORTED_VERSION = 'ewogICJmb3JtYXQiOiAyLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.tTBymZMROsYyMiP6ldXFqGurbzqjhSQIu/pjyEUJA3v/57VgToomYl7FVzBj1asgHpadvysyTUiX3nFvPxbp166L3+LB3Jybw9ueMnwePu5vQOO0krqKLBqRq+TqHKn7k76uYRbkCIo5UajNfzetHhlkin3dJf3x2K/fcwbPW5A=';
295296

296297
let trialPanelSpy = jest.spyOn(trialPanel, 'renderTrialPanel');
298+
let consoleWarnSpy = jest.spyOn(console, 'warn');
297299

298300
beforeEach(() => {
299301
jest.spyOn(errors, 'log').mockImplementation(() => {});
302+
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
300303
trialPanelSpy = jest.spyOn(trialPanel, 'renderTrialPanel');
301304
setLicenseCheckSkipCondition(false);
302305
});
@@ -309,10 +312,11 @@ describe('license check', () => {
309312
{ token: '', version: '1.0.3' },
310313
{ token: null, version: '1.0.4' },
311314
{ token: undefined, version: '1.0.50' },
312-
])('W0019 error should be logged if license is empty', ({ token, version }) => {
315+
{ token: LICENSE_KEY_PLACEHOLDER, version: '1.0.3' },
316+
])('Warning should be logged with no-key message if license is empty', ({ token, version }) => {
313317
validateLicense(token as string, version);
314-
expect(errors.log).toHaveBeenCalledTimes(1);
315-
expect(errors.log).toHaveBeenCalledWith('W0019');
318+
expect(consoleWarnSpy).toHaveBeenCalledTimes(2);
319+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('devextreme-license generated key has not been specified'));
316320
});
317321

318322
test.each([
@@ -322,52 +326,29 @@ describe('license check', () => {
322326
{ token: '', version: '1.0.0' },
323327
{ token: null, version: '1.2.4-preview' },
324328
{ token: undefined, version: '1.2' },
329+
{ token: LICENSE_KEY_PLACEHOLDER, version: '1.0.3' },
330+
{ token: LICENSE_KEY_PLACEHOLDER, version: '1.0.0' },
325331
])('trial panel should be displayed if license is empty, preview or not', ({ token, version }) => {
326332
validateLicense(token as string, version);
327333
expect(trialPanelSpy).toHaveBeenCalledTimes(1);
328334
});
329335

330-
test.each([
331-
{ token: '', version: '1.0' },
332-
{ token: null, version: '1.0.' },
333-
{ token: undefined, version: '1.0.0' },
334-
{ token: TOKEN_23_1, version: '23.1.0' },
335-
{ token: TOKEN_23_1, version: '12.3.1' },
336-
{ token: TOKEN_23_2, version: '23.1.2' },
337-
{ token: TOKEN_23_2, version: '23.2.3-preview' },
338-
{ token: TOKEN_23_1, version: '23.2.0' },
339-
{ token: TOKEN_23_2, version: '42.4.3-alfa' },
340-
{ token: TOKEN_UNVERIFIED, version: '1.2.0' },
341-
{ token: TOKEN_INVALID_JSON, version: '1.2.1' },
342-
{ token: TOKEN_INVALID_BASE64, version: '1.2.2' },
343-
{ token: TOKEN_MISSING_FIELD_1, version: '1.2' },
344-
{ token: TOKEN_MISSING_FIELD_2, version: '1.2.4-preview' },
345-
{ token: TOKEN_MISSING_FIELD_3, version: '1.2.' },
346-
{ token: TOKEN_UNSUPPORTED_VERSION, version: '1.2.abc' },
347-
{ token: 'Another', version: '1.2.0' },
348-
{ token: '3.2.1', version: '1.2.1' },
349-
{ token: TOKEN_23_1, version: '123' },
350-
])('W0022 error should be logged if version is preview [%#]', ({ token, version }) => {
351-
validateLicense(token as string, version);
352-
expect(errors.log).toHaveBeenCalledWith('W0022');
353-
});
354-
355336
test.each([
356337
{ token: TOKEN_23_1, version: '23.1.3' },
357338
{ token: TOKEN_23_1, version: '12.3.4' },
358339
{ token: TOKEN_23_2, version: '23.1.5' },
359340
{ token: TOKEN_23_2, version: '23.2.6' },
360-
])('No messages should be logged if license is valid', ({ token, version }) => {
341+
])('Old format license within version range should not trigger warnings', ({ token, version }) => {
361342
validateLicense(token, version);
362-
expect(errors.log).not.toHaveBeenCalled();
343+
expect(consoleWarnSpy).not.toHaveBeenCalled();
363344
});
364345

365346
test.each([
366347
{ token: TOKEN_23_1, version: '23.1.3' },
367348
{ token: TOKEN_23_1, version: '12.3.4' },
368349
{ token: TOKEN_23_2, version: '23.1.5' },
369350
{ token: TOKEN_23_2, version: '23.2.6' },
370-
])('Trial panel should not be displayed if license is valid', ({ token, version }) => {
351+
])('Trial panel should not be displayed for valid old format license keys', ({ token, version }) => {
371352
validateLicense(token, version);
372353
expect(trialPanelSpy).not.toHaveBeenCalled();
373354
});
@@ -388,7 +369,7 @@ describe('license check', () => {
388369
validateLicense('', '1.0');
389370
validateLicense('', '1.0');
390371

391-
expect(errors.log).toHaveBeenCalledTimes(1);
372+
expect(consoleWarnSpy).toHaveBeenCalledTimes(2);
392373
});
393374

394375
test('Base z-index should match the corresponding setting in DevExtreme', () => {
@@ -399,15 +380,16 @@ describe('license check', () => {
399380
setLicenseCheckSkipCondition();
400381
validateLicense('', '1.0');
401382
expect(errors.log).not.toHaveBeenCalled();
383+
expect(consoleWarnSpy).not.toHaveBeenCalled();
402384
});
403385

404386
test.each([
405387
{ token: TOKEN_23_1, version: '23.2.3' },
406388
{ token: TOKEN_23_2, version: '42.4.5' },
407-
])('W0020 error should be logged if license is outdated', ({ token, version }) => {
389+
])('Old format license should trigger version-mismatch warning when outdated', ({ token, version }) => {
408390
validateLicense(token, version);
409-
expect(errors.log).toHaveBeenCalledTimes(1);
410-
expect(errors.log).toHaveBeenCalledWith('W0020');
391+
expect(consoleWarnSpy).toHaveBeenCalledTimes(2);
392+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Incompatible DevExpress license key version'));
411393
});
412394

413395
test.each([
@@ -425,7 +407,7 @@ describe('license check', () => {
425407
{ token: TOKEN_23_1, version: '23.2.3-alpha' },
426408
{ token: TOKEN_23_2, version: '24.1.0' },
427409
{ token: TOKEN_23_2, version: '24.1.abc' },
428-
])('Trial panel should not be displayed in previews if the license is for the previous RTM', ({ token, version }) => {
410+
])('Trial panel should not be displayed in previews for valid old format license keys', ({ token, version }) => {
429411
validateLicense(token, version);
430412
expect(trialPanelSpy).not.toHaveBeenCalled();
431413
});
@@ -440,9 +422,10 @@ describe('license check', () => {
440422
{ token: TOKEN_UNSUPPORTED_VERSION, version: '1.2.3' },
441423
{ token: 'str@nge in.put', version: '1.2.3' },
442424
{ token: '3.2.1', version: '1.2.3' },
443-
])('W0021 error should be logged if license is corrupted/invalid [%#]', ({ token, version }) => {
425+
])('License verification warning should be logged if license is corrupted/invalid [%#]', ({ token, version }) => {
444426
validateLicense(token, version);
445-
expect(errors.log).toHaveBeenCalledWith('W0021');
427+
expect(consoleWarnSpy).toHaveBeenCalledTimes(2);
428+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('License key verification has failed'));
446429
});
447430

448431
test.each([
@@ -503,20 +486,15 @@ describe('internal license check', () => {
503486
expect(errors.log).toHaveBeenCalledWith('W0020');
504487
expect(trialPanelSpy).not.toHaveBeenCalled();
505488
});
506-
507-
test('internal usage token (incorrect, pre-release)', () => {
508-
const token = 'ewogICJpbnRlcm5hbFVzYWdlSWQiOiAiUDdmNU5icU9WMDZYRFVpa3Q1bkRyQSIsCiAgImZvcm1hdCI6IDEKfQ==.ox52WAqudazQ0ZKdnJqvh/RmNNNX+IB9cmun97irvSeZK2JMf9sbBXC1YCrSZNIPBjQapyIV8Ctv9z2wzb3BkWy+R9CEh+ev7purq7Lk0ugpwDye6GaCzqlDg+58EHwPCNaasIuBiQC3ztvOItrGwWSu0aEFooiajk9uAWwzWeM=';
509-
validateLicense(token, '1.2.1');
510-
expect(errors.log).toHaveBeenCalledWith('W0022');
511-
expect(trialPanelSpy).not.toHaveBeenCalled();
512-
});
513489
});
514490

515491
describe('DevExpress license check', () => {
516492
let trialPanelSpy = jest.spyOn(trialPanel, 'renderTrialPanel');
493+
let consoleWarnSpy = jest.spyOn(console, 'warn');
517494

518495
beforeEach(() => {
519496
jest.spyOn(errors, 'log').mockImplementation(() => {});
497+
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
520498
trialPanelSpy = jest.spyOn(trialPanel, 'renderTrialPanel');
521499
setLicenseCheckSkipCondition(false);
522500
});
@@ -528,14 +506,16 @@ describe('DevExpress license check', () => {
528506
test('DevExpress License Key copied from Download Manager (incorrect)', () => {
529507
const token = 'LCXv1therestofthekey';
530508
validateLicense(token, '25.1.3');
531-
expect(errors.log).toHaveBeenCalled();
509+
expect(consoleWarnSpy).toHaveBeenCalledTimes(2);
510+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('DevExpress license key has been specified instead of a key generated using devextreme-license'));
532511
expect(trialPanelSpy).toHaveBeenCalled();
533512
});
534513

535514
test('DevExpress License Key generated from LCX key (incorrect)', () => {
536515
const token = 'LCPtherestofthekey';
537516
validateLicense(token, '25.1.3');
538-
expect(errors.log).toHaveBeenCalled();
517+
expect(consoleWarnSpy).toHaveBeenCalledTimes(2);
518+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('License key verification has failed'));
539519
expect(trialPanelSpy).toHaveBeenCalled();
540520
});
541521
});

0 commit comments

Comments
 (0)