From f71e47f5eeb20e178f466531ab822e24c8ffc1bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Sep 2025 21:36:25 +0000 Subject: [PATCH 1/5] Initial plan From 1c796dfd056c7b63559cf432cef1d88c71c6471d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Sep 2025 21:45:11 +0000 Subject: [PATCH 2/5] Add encryption/decryption functionality for adapter config fields Co-authored-by: Apollon77 <11976694+Apollon77@users.noreply.github.com> --- CHANGELOG.md | 4 + build/tests/integration/lib/harness.d.ts | 19 ++++- build/tests/integration/lib/harness.js | 58 ++++++++++++++- src/tests/integration/lib/harness.ts | 76 ++++++++++++++++++- src/tests/unit/harness-encryption.test.ts | 91 +++++++++++++++++++++++ 5 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 src/tests/unit/harness-encryption.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a7c6a24..eaf79c94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ PLACEHOLDER for the next version: ## **WORK IN PROGRESS** --> + +## **WORK IN PROGRESS** +* (copilot) **BREAKING CHANGE**: Test harness now automatically encrypts/decrypts adapter configuration fields listed in `encryptedNative` during `changeAdapterConfig()`. Added `encryptValue()` and `decryptValue()` methods for manual encryption/decryption. Potential manual code that encrypts data might break now. + ## 5.1.1 (2025-08-31) * (@Apollon77) Downgrades chai-as-promised type dependency to same major as main dependency diff --git a/build/tests/integration/lib/harness.d.ts b/build/tests/integration/lib/harness.d.ts index 3470a26d..013f9328 100644 --- a/build/tests/integration/lib/harness.d.ts +++ b/build/tests/integration/lib/harness.d.ts @@ -59,9 +59,26 @@ export declare class TestHarness extends EventEmitter { /** Stops the adapter process */ stopAdapter(): Promise | undefined; /** - * Updates the adapter config. The changes can be a subset of the target object + * Updates the adapter config. The changes can be a subset of the target object. + * Fields listed in encryptedNative will be automatically encrypted. */ changeAdapterConfig(adapterName: string, changes: Record): Promise; + /** + * Encrypts a value using the system secret + */ + encryptValue(value: string): Promise; + /** + * Decrypts a value using the system secret + */ + decryptValue(encryptedValue: string): Promise; + /** + * Performs XOR encryption/decryption (same operation for both due to XOR properties) + */ + private performEncryption; + /** + * Performs XOR decryption (same operation as encryption due to XOR properties) + */ + private performDecryption; getAdapterExecutionMode(): ioBroker.AdapterCommon['mode']; /** Enables the sendTo method */ enableSendTo(): Promise; diff --git a/build/tests/integration/lib/harness.js b/build/tests/integration/lib/harness.js index a8d7f5cb..0b2b96d6 100644 --- a/build/tests/integration/lib/harness.js +++ b/build/tests/integration/lib/harness.js @@ -236,16 +236,72 @@ class TestHarness extends node_events_1.EventEmitter { }); } /** - * Updates the adapter config. The changes can be a subset of the target object + * Updates the adapter config. The changes can be a subset of the target object. + * Fields listed in encryptedNative will be automatically encrypted. */ async changeAdapterConfig(adapterName, changes) { const adapterInstanceId = `system.adapter.${adapterName}.0`; const obj = await this.dbConnection.getObject(adapterInstanceId); if (obj) { + // Get the adapter's common configuration to check for encryptedNative fields + const adapterCommon = (0, adapterTools_1.loadAdapterCommon)(this.testAdapterDir); + const encryptedNative = adapterCommon.encryptedNative || []; + // If we have native changes and encrypted fields are defined, encrypt them + if (changes.native && encryptedNative.length > 0) { + const encryptedFields = []; + for (const fieldName of encryptedNative) { + if (changes.native[fieldName] !== undefined) { + const originalValue = changes.native[fieldName]; + changes.native[fieldName] = await this.encryptValue(originalValue); + encryptedFields.push(fieldName); + } + } + if (encryptedFields.length > 0) { + debug(`Encrypted fields during config change: ${encryptedFields.join(', ')}`); + } + } (0, objects_1.extend)(obj, changes); await this.dbConnection.setObject(adapterInstanceId, obj); } } + /** + * Encrypts a value using the system secret + */ + async encryptValue(value) { + const systemConfig = await this.dbConnection.getObject('system.config'); + if (!systemConfig || !systemConfig.native || !systemConfig.native.secret) { + throw new Error('System configuration or secret not found'); + } + const secret = systemConfig.native.secret; + return this.performEncryption(value, secret); + } + /** + * Decrypts a value using the system secret + */ + async decryptValue(encryptedValue) { + const systemConfig = await this.dbConnection.getObject('system.config'); + if (!systemConfig || !systemConfig.native || !systemConfig.native.secret) { + throw new Error('System configuration or secret not found'); + } + const secret = systemConfig.native.secret; + return this.performDecryption(encryptedValue, secret); + } + /** + * Performs XOR encryption/decryption (same operation for both due to XOR properties) + */ + performEncryption(value, secret) { + let result = ''; + for (let i = 0; i < value.length; ++i) { + result += String.fromCharCode(secret[i % secret.length].charCodeAt(0) ^ value.charCodeAt(i)); + } + return result; + } + /** + * Performs XOR decryption (same operation as encryption due to XOR properties) + */ + performDecryption(encryptedValue, secret) { + return this.performEncryption(encryptedValue, secret); // XOR is symmetric + } getAdapterExecutionMode() { return (0, adapterTools_1.getAdapterExecutionMode)(this.testAdapterDir); } diff --git a/src/tests/integration/lib/harness.ts b/src/tests/integration/lib/harness.ts index f7b5b134..f1401b76 100644 --- a/src/tests/integration/lib/harness.ts +++ b/src/tests/integration/lib/harness.ts @@ -4,7 +4,13 @@ import { type ChildProcess, spawn } from 'node:child_process'; import debugModule from 'debug'; import { EventEmitter } from 'node:events'; import * as path from 'node:path'; -import { getAdapterExecutionMode, getAdapterName, getAppName, locateAdapterMainFile } from '../../../lib/adapterTools'; +import { + getAdapterExecutionMode, + getAdapterName, + getAppName, + locateAdapterMainFile, + loadAdapterCommon, +} from '../../../lib/adapterTools'; import type { DBConnection } from './dbConnection'; import { getTestAdapterDir, getTestControllerDir } from './tools'; @@ -243,17 +249,83 @@ export class TestHarness extends EventEmitter { } /** - * Updates the adapter config. The changes can be a subset of the target object + * Updates the adapter config. The changes can be a subset of the target object. + * Fields listed in encryptedNative will be automatically encrypted. */ public async changeAdapterConfig(adapterName: string, changes: Record): Promise { const adapterInstanceId = `system.adapter.${adapterName}.0`; const obj = await this.dbConnection.getObject(adapterInstanceId); if (obj) { + // Get the adapter's common configuration to check for encryptedNative fields + const adapterCommon = loadAdapterCommon(this.testAdapterDir); + const encryptedNative = adapterCommon.encryptedNative || []; + + // If we have native changes and encrypted fields are defined, encrypt them + if (changes.native && encryptedNative.length > 0) { + const encryptedFields: string[] = []; + + for (const fieldName of encryptedNative) { + if (changes.native[fieldName] !== undefined) { + const originalValue = changes.native[fieldName]; + changes.native[fieldName] = await this.encryptValue(originalValue); + encryptedFields.push(fieldName); + } + } + + if (encryptedFields.length > 0) { + debug(`Encrypted fields during config change: ${encryptedFields.join(', ')}`); + } + } + extend(obj, changes); await this.dbConnection.setObject(adapterInstanceId, obj); } } + /** + * Encrypts a value using the system secret + */ + public async encryptValue(value: string): Promise { + const systemConfig = await this.dbConnection.getObject('system.config'); + if (!systemConfig || !systemConfig.native || !systemConfig.native.secret) { + throw new Error('System configuration or secret not found'); + } + + const secret = systemConfig.native.secret; + return this.performEncryption(value, secret); + } + + /** + * Decrypts a value using the system secret + */ + public async decryptValue(encryptedValue: string): Promise { + const systemConfig = await this.dbConnection.getObject('system.config'); + if (!systemConfig || !systemConfig.native || !systemConfig.native.secret) { + throw new Error('System configuration or secret not found'); + } + + const secret = systemConfig.native.secret; + return this.performDecryption(encryptedValue, secret); + } + + /** + * Performs XOR encryption/decryption (same operation for both due to XOR properties) + */ + private performEncryption(value: string, secret: string): string { + let result = ''; + for (let i = 0; i < value.length; ++i) { + result += String.fromCharCode(secret[i % secret.length].charCodeAt(0) ^ value.charCodeAt(i)); + } + return result; + } + + /** + * Performs XOR decryption (same operation as encryption due to XOR properties) + */ + private performDecryption(encryptedValue: string, secret: string): string { + return this.performEncryption(encryptedValue, secret); // XOR is symmetric + } + public getAdapterExecutionMode(): ioBroker.AdapterCommon['mode'] { return getAdapterExecutionMode(this.testAdapterDir); } diff --git a/src/tests/unit/harness-encryption.test.ts b/src/tests/unit/harness-encryption.test.ts new file mode 100644 index 00000000..fbd9168a --- /dev/null +++ b/src/tests/unit/harness-encryption.test.ts @@ -0,0 +1,91 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +describe('TestHarness Encryption/Decryption', () => { + beforeEach(() => { + // We need to require these modules after the stubs are in place + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('XOR encryption implementation', () => { + it('should implement the correct XOR logic as specified in the issue', () => { + const value = 'hello'; + const secret = 'key'; + + // Manual implementation of the expected XOR logic from the issue + let expectedResult = ''; + for (let i = 0; i < value.length; ++i) { + expectedResult += String.fromCharCode(secret[i % secret.length].charCodeAt(0) ^ value.charCodeAt(i)); + } + + // Test the algorithm directly + expect(expectedResult).to.not.equal(value); + + // Test that applying XOR twice returns the original value (decryption) + let decryptedResult = ''; + for (let i = 0; i < expectedResult.length; ++i) { + decryptedResult += String.fromCharCode( + secret[i % secret.length].charCodeAt(0) ^ expectedResult.charCodeAt(i), + ); + } + + expect(decryptedResult).to.equal(value); + }); + + it('should handle empty strings', () => { + const value = ''; + const secret = 'key'; + + let result = ''; + for (let i = 0; i < value.length; ++i) { + result += String.fromCharCode(secret[i % secret.length].charCodeAt(0) ^ value.charCodeAt(i)); + } + + expect(result).to.equal(''); + }); + + it('should handle different secret lengths', () => { + const value = 'testvalue123'; + const shortSecret = 'ab'; + const longSecret = 'verylongsecretkey'; + + // Test with short secret + let result1 = ''; + for (let i = 0; i < value.length; ++i) { + result1 += String.fromCharCode(shortSecret[i % shortSecret.length].charCodeAt(0) ^ value.charCodeAt(i)); + } + + // Decrypt back + let decrypted1 = ''; + for (let i = 0; i < result1.length; ++i) { + decrypted1 += String.fromCharCode( + shortSecret[i % shortSecret.length].charCodeAt(0) ^ result1.charCodeAt(i), + ); + } + + expect(decrypted1).to.equal(value); + + // Test with long secret + let result2 = ''; + for (let i = 0; i < value.length; ++i) { + result2 += String.fromCharCode(longSecret[i % longSecret.length].charCodeAt(0) ^ value.charCodeAt(i)); + } + + // Decrypt back + let decrypted2 = ''; + for (let i = 0; i < result2.length; ++i) { + decrypted2 += String.fromCharCode( + longSecret[i % longSecret.length].charCodeAt(0) ^ result2.charCodeAt(i), + ); + } + + expect(decrypted2).to.equal(value); + + // Results should be different with different secrets + expect(result1).to.not.equal(result2); + }); + }); +}); From d34b01c9964745d5f0cf2b236c692c0f8ad83b25 Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Fri, 19 Sep 2025 08:42:51 +0200 Subject: [PATCH 3/5] Revise CHANGELOG for breaking changes and new methods Updated changelog to reflect breaking changes and new methods. --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaf79c94..a430c5fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ --> ## **WORK IN PROGRESS** -* (copilot) **BREAKING CHANGE**: Test harness now automatically encrypts/decrypts adapter configuration fields listed in `encryptedNative` during `changeAdapterConfig()`. Added `encryptValue()` and `decryptValue()` methods for manual encryption/decryption. Potential manual code that encrypts data might break now. +* **BREAKING CHANGE**: Test harness now automatically encrypts/decrypts adapter configuration fields listed in `encryptedNative` during `changeAdapterConfig()`. +* (@copilot) Added `encryptValue()` and `decryptValue()` methods for manual encryption/decryption. ## 5.1.1 (2025-08-31) * (@Apollon77) Downgrades chai-as-promised type dependency to same major as main dependency From b55bcba90909f1089ec9655d6a0c8faef7584f79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Sep 2025 06:55:13 +0000 Subject: [PATCH 4/5] Address review feedback: use obj.encryptedNative and add secret caching Co-authored-by: Apollon77 <11976694+Apollon77@users.noreply.github.com> --- build/tests/integration/lib/harness.d.ts | 5 +++ build/tests/integration/lib/harness.js | 26 +++++++++------ src/tests/integration/lib/harness.ts | 41 +++++++++++++----------- 3 files changed, 43 insertions(+), 29 deletions(-) diff --git a/build/tests/integration/lib/harness.d.ts b/build/tests/integration/lib/harness.d.ts index 013f9328..157d49b0 100644 --- a/build/tests/integration/lib/harness.d.ts +++ b/build/tests/integration/lib/harness.d.ts @@ -63,6 +63,11 @@ export declare class TestHarness extends EventEmitter { * Fields listed in encryptedNative will be automatically encrypted. */ changeAdapterConfig(adapterName: string, changes: Record): Promise; + private _cachedSecret; + /** + * Gets the system secret, with caching to prevent duplicate reads + */ + private getSystemSecret; /** * Encrypts a value using the system secret */ diff --git a/build/tests/integration/lib/harness.js b/build/tests/integration/lib/harness.js index 0b2b96d6..80ba8683 100644 --- a/build/tests/integration/lib/harness.js +++ b/build/tests/integration/lib/harness.js @@ -243,9 +243,8 @@ class TestHarness extends node_events_1.EventEmitter { const adapterInstanceId = `system.adapter.${adapterName}.0`; const obj = await this.dbConnection.getObject(adapterInstanceId); if (obj) { - // Get the adapter's common configuration to check for encryptedNative fields - const adapterCommon = (0, adapterTools_1.loadAdapterCommon)(this.testAdapterDir); - const encryptedNative = adapterCommon.encryptedNative || []; + // Get the encryptedNative fields from the adapter object + const encryptedNative = obj.common?.encryptedNative || []; // If we have native changes and encrypted fields are defined, encrypt them if (changes.native && encryptedNative.length > 0) { const encryptedFields = []; @@ -265,25 +264,32 @@ class TestHarness extends node_events_1.EventEmitter { } } /** - * Encrypts a value using the system secret + * Gets the system secret, with caching to prevent duplicate reads */ - async encryptValue(value) { + async getSystemSecret() { + if (this._cachedSecret !== undefined) { + return this._cachedSecret; + } const systemConfig = await this.dbConnection.getObject('system.config'); if (!systemConfig || !systemConfig.native || !systemConfig.native.secret) { throw new Error('System configuration or secret not found'); } const secret = systemConfig.native.secret; + this._cachedSecret = secret; + return secret; + } + /** + * Encrypts a value using the system secret + */ + async encryptValue(value) { + const secret = await this.getSystemSecret(); return this.performEncryption(value, secret); } /** * Decrypts a value using the system secret */ async decryptValue(encryptedValue) { - const systemConfig = await this.dbConnection.getObject('system.config'); - if (!systemConfig || !systemConfig.native || !systemConfig.native.secret) { - throw new Error('System configuration or secret not found'); - } - const secret = systemConfig.native.secret; + const secret = await this.getSystemSecret(); return this.performDecryption(encryptedValue, secret); } /** diff --git a/src/tests/integration/lib/harness.ts b/src/tests/integration/lib/harness.ts index f1401b76..5b26558b 100644 --- a/src/tests/integration/lib/harness.ts +++ b/src/tests/integration/lib/harness.ts @@ -4,13 +4,7 @@ import { type ChildProcess, spawn } from 'node:child_process'; import debugModule from 'debug'; import { EventEmitter } from 'node:events'; import * as path from 'node:path'; -import { - getAdapterExecutionMode, - getAdapterName, - getAppName, - locateAdapterMainFile, - loadAdapterCommon, -} from '../../../lib/adapterTools'; +import { getAdapterExecutionMode, getAdapterName, getAppName, locateAdapterMainFile } from '../../../lib/adapterTools'; import type { DBConnection } from './dbConnection'; import { getTestAdapterDir, getTestControllerDir } from './tools'; @@ -256,9 +250,8 @@ export class TestHarness extends EventEmitter { const adapterInstanceId = `system.adapter.${adapterName}.0`; const obj = await this.dbConnection.getObject(adapterInstanceId); if (obj) { - // Get the adapter's common configuration to check for encryptedNative fields - const adapterCommon = loadAdapterCommon(this.testAdapterDir); - const encryptedNative = adapterCommon.encryptedNative || []; + // Get the encryptedNative fields from the adapter object + const encryptedNative = obj.common?.encryptedNative || []; // If we have native changes and encrypted fields are defined, encrypt them if (changes.native && encryptedNative.length > 0) { @@ -282,16 +275,31 @@ export class TestHarness extends EventEmitter { } } + private _cachedSecret: string | undefined; + /** - * Encrypts a value using the system secret + * Gets the system secret, with caching to prevent duplicate reads */ - public async encryptValue(value: string): Promise { + private async getSystemSecret(): Promise { + if (this._cachedSecret !== undefined) { + return this._cachedSecret; + } + const systemConfig = await this.dbConnection.getObject('system.config'); if (!systemConfig || !systemConfig.native || !systemConfig.native.secret) { throw new Error('System configuration or secret not found'); } - const secret = systemConfig.native.secret; + const secret = systemConfig.native.secret as string; + this._cachedSecret = secret; + return secret; + } + + /** + * Encrypts a value using the system secret + */ + public async encryptValue(value: string): Promise { + const secret = await this.getSystemSecret(); return this.performEncryption(value, secret); } @@ -299,12 +307,7 @@ export class TestHarness extends EventEmitter { * Decrypts a value using the system secret */ public async decryptValue(encryptedValue: string): Promise { - const systemConfig = await this.dbConnection.getObject('system.config'); - if (!systemConfig || !systemConfig.native || !systemConfig.native.secret) { - throw new Error('System configuration or secret not found'); - } - - const secret = systemConfig.native.secret; + const secret = await this.getSystemSecret(); return this.performDecryption(encryptedValue, secret); } From 8bcfd357a2555ff84246d34680c7941426da5c87 Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Fri, 19 Sep 2025 10:51:57 +0200 Subject: [PATCH 5/5] Update encryptedNative access in harness.ts Refactor to access encryptedNative directly from obj. --- src/tests/integration/lib/harness.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/integration/lib/harness.ts b/src/tests/integration/lib/harness.ts index 5b26558b..9745fb9c 100644 --- a/src/tests/integration/lib/harness.ts +++ b/src/tests/integration/lib/harness.ts @@ -251,7 +251,7 @@ export class TestHarness extends EventEmitter { const obj = await this.dbConnection.getObject(adapterInstanceId); if (obj) { // Get the encryptedNative fields from the adapter object - const encryptedNative = obj.common?.encryptedNative || []; + const encryptedNative = obj.encryptedNative || []; // If we have native changes and encrypted fields are defined, encrypt them if (changes.native && encryptedNative.length > 0) {