diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a7c6a24..a430c5fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ PLACEHOLDER for the next version: ## **WORK IN PROGRESS** --> + +## **WORK IN PROGRESS** +* **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 diff --git a/build/tests/integration/lib/harness.d.ts b/build/tests/integration/lib/harness.d.ts index 3470a26d..157d49b0 100644 --- a/build/tests/integration/lib/harness.d.ts +++ b/build/tests/integration/lib/harness.d.ts @@ -59,9 +59,31 @@ 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; + private _cachedSecret; + /** + * Gets the system secret, with caching to prevent duplicate reads + */ + private getSystemSecret; + /** + * 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..80ba8683 100644 --- a/build/tests/integration/lib/harness.js +++ b/build/tests/integration/lib/harness.js @@ -236,16 +236,78 @@ 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 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 = []; + 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); } } + /** + * Gets the system secret, with caching to prevent duplicate reads + */ + 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 secret = await this.getSystemSecret(); + 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..9745fb9c 100644 --- a/src/tests/integration/lib/harness.ts +++ b/src/tests/integration/lib/harness.ts @@ -243,17 +243,92 @@ 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 encryptedNative fields from the adapter object + const encryptedNative = obj.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); } } + private _cachedSecret: string | undefined; + + /** + * Gets the system secret, with caching to prevent duplicate reads + */ + 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 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); + } + + /** + * Decrypts a value using the system secret + */ + public async decryptValue(encryptedValue: string): Promise { + const secret = await this.getSystemSecret(); + 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); + }); + }); +});