Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
24 changes: 23 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,31 @@ 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>;
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<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
64 changes: 63 additions & 1 deletion build/tests/integration/lib/harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
77 changes: 76 additions & 1 deletion src/tests/integration/lib/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,17 +243,92 @@
}

/**
* 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 encryptedNative fields from the adapter object
const encryptedNative = obj.encryptedNative || [];

Check failure on line 254 in src/tests/integration/lib/harness.ts

View workflow job for this annotation

GitHub Actions / check-and-lint (20.x)

Property 'encryptedNative' does not exist on type 'Object'.

// 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<string> {
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<string> {
const secret = await this.getSystemSecret();
return this.performEncryption(value, secret);
}

/**
* Decrypts a value using the system secret
*/
public async decryptValue(encryptedValue: string): Promise<string> {
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);
}
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);
});
});
});
Loading