Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 18 additions & 1 deletion build/tests/integration/lib/harness.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,26 @@ export declare class TestHarness extends EventEmitter {
/** Stops the adapter process */
stopAdapter(): Promise<void> | 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<string, any>): Promise<void>;
/**
* Encrypts a value using the system secret
*/
encryptValue(value: string): Promise<string>;
/**
* Decrypts a value using the system secret
*/
decryptValue(encryptedValue: string): Promise<string>;
/**
* 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<void>;
Expand Down
58 changes: 57 additions & 1 deletion build/tests/integration/lib/harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
76 changes: 74 additions & 2 deletions src/tests/integration/lib/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<string, any>): Promise<void> {
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);
Comment thread
Apollon77 marked this conversation as resolved.
Outdated
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<string> {
const systemConfig = await this.dbConnection.getObject('system.config');
Comment thread
Apollon77 marked this conversation as resolved.
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<string> {
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);
}
Expand Down
91 changes: 91 additions & 0 deletions src/tests/unit/harness-encryption.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});