@riddix/hamh 2.1.0-alpha.592 → 2.1.0-alpha.594
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/backend/cli.js
CHANGED
|
@@ -10026,7 +10026,7 @@ function MockCrypto(index = 128, implementation = StandardCrypto) {
|
|
|
10026
10026
|
throw new ImplementationError(`Index for stable crypto must be 0-255`);
|
|
10027
10027
|
}
|
|
10028
10028
|
const crypto8 = new implementation();
|
|
10029
|
-
const { randomBytes:
|
|
10029
|
+
const { randomBytes: randomBytes3, createKeyPair } = crypto8;
|
|
10030
10030
|
Object.defineProperties(crypto8, {
|
|
10031
10031
|
index: {
|
|
10032
10032
|
get() {
|
|
@@ -10038,11 +10038,11 @@ function MockCrypto(index = 128, implementation = StandardCrypto) {
|
|
|
10038
10038
|
},
|
|
10039
10039
|
entropic: {
|
|
10040
10040
|
get() {
|
|
10041
|
-
return crypto8.randomBytes ===
|
|
10041
|
+
return crypto8.randomBytes === randomBytes3;
|
|
10042
10042
|
},
|
|
10043
10043
|
set(entropic) {
|
|
10044
10044
|
if (entropic) {
|
|
10045
|
-
crypto8.randomBytes =
|
|
10045
|
+
crypto8.randomBytes = randomBytes3;
|
|
10046
10046
|
crypto8.createKeyPair = createKeyPair;
|
|
10047
10047
|
} else {
|
|
10048
10048
|
disableEntropy();
|
|
@@ -148035,6 +148035,17 @@ async function extractBackupData(buffer) {
|
|
|
148035
148035
|
const data = JSON.parse(content.toString());
|
|
148036
148036
|
return { backupData: data, zipDirectory: directory };
|
|
148037
148037
|
}
|
|
148038
|
+
function resolveWithin(baseDir, relative) {
|
|
148039
|
+
if (relative.length === 0 || path.isAbsolute(relative)) {
|
|
148040
|
+
return null;
|
|
148041
|
+
}
|
|
148042
|
+
const resolvedBase = path.resolve(baseDir);
|
|
148043
|
+
const resolvedTarget = path.resolve(resolvedBase, relative);
|
|
148044
|
+
if (resolvedTarget !== resolvedBase && !resolvedTarget.startsWith(resolvedBase + path.sep)) {
|
|
148045
|
+
return null;
|
|
148046
|
+
}
|
|
148047
|
+
return resolvedTarget;
|
|
148048
|
+
}
|
|
148038
148049
|
async function restoreIdentityFiles(zipDirectory, bridgeId, storageLocation) {
|
|
148039
148050
|
const identityPrefix = `identity/${bridgeId}/`;
|
|
148040
148051
|
const identityFiles = zipDirectory.files.filter(
|
|
@@ -148047,7 +148058,12 @@ async function restoreIdentityFiles(zipDirectory, bridgeId, storageLocation) {
|
|
|
148047
148058
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
148048
148059
|
for (const file of identityFiles) {
|
|
148049
148060
|
const relativePath = file.path.substring(identityPrefix.length);
|
|
148050
|
-
const targetPath =
|
|
148061
|
+
const targetPath = resolveWithin(targetDir, relativePath);
|
|
148062
|
+
if (!targetPath) {
|
|
148063
|
+
throw new Error(
|
|
148064
|
+
`Refusing to restore identity file with unsafe path: ${file.path}`
|
|
148065
|
+
);
|
|
148066
|
+
}
|
|
148051
148067
|
const targetDirPath = path.dirname(targetPath);
|
|
148052
148068
|
fs.mkdirSync(targetDirPath, { recursive: true });
|
|
148053
148069
|
const content = await file.buffer();
|
|
@@ -148067,7 +148083,12 @@ async function restoreBridgeIcon(zipDirectory, bridgeId, storageLocation) {
|
|
|
148067
148083
|
fs.mkdirSync(iconsDir, { recursive: true });
|
|
148068
148084
|
for (const file of iconFiles) {
|
|
148069
148085
|
const fileName = file.path.substring(iconPrefix.length);
|
|
148070
|
-
const targetPath =
|
|
148086
|
+
const targetPath = resolveWithin(iconsDir, fileName);
|
|
148087
|
+
if (!targetPath) {
|
|
148088
|
+
throw new Error(
|
|
148089
|
+
`Refusing to restore bridge icon with unsafe path: ${file.path}`
|
|
148090
|
+
);
|
|
148091
|
+
}
|
|
148071
148092
|
const content = await file.buffer();
|
|
148072
148093
|
fs.writeFileSync(targetPath, content);
|
|
148073
148094
|
}
|
|
@@ -149101,9 +149122,7 @@ function lockCredentialApi(lockCredentialStorage) {
|
|
|
149101
149122
|
async (_req, res) => {
|
|
149102
149123
|
const credentials = lockCredentialStorage.getAllCredentials();
|
|
149103
149124
|
const sanitizedCredentials = credentials.map(sanitizeCredential);
|
|
149104
|
-
res.json({
|
|
149105
|
-
credentials: sanitizedCredentials
|
|
149106
|
-
});
|
|
149125
|
+
res.json({ credentials: sanitizedCredentials });
|
|
149107
149126
|
}
|
|
149108
149127
|
);
|
|
149109
149128
|
router.get(
|
|
@@ -151436,16 +151455,25 @@ var WebApi = class extends Service {
|
|
|
151436
151455
|
});
|
|
151437
151456
|
}
|
|
151438
151457
|
createDynamicAuthMiddleware() {
|
|
151458
|
+
const envAuth = this.props.auth;
|
|
151459
|
+
const envMiddleware = envAuth ? basicAuth({
|
|
151460
|
+
users: { [envAuth.username]: envAuth.password },
|
|
151461
|
+
challenge: true,
|
|
151462
|
+
realm: "Home Assistant Matter Hub"
|
|
151463
|
+
}) : void 0;
|
|
151464
|
+
const storageMiddleware = basicAuth({
|
|
151465
|
+
authorizer: (username, password) => this.settingsStorage.verifyAuth(username, password),
|
|
151466
|
+
challenge: true,
|
|
151467
|
+
realm: "Home Assistant Matter Hub"
|
|
151468
|
+
});
|
|
151439
151469
|
return (req, res, next) => {
|
|
151440
|
-
|
|
151441
|
-
|
|
151470
|
+
if (envMiddleware) {
|
|
151471
|
+
return envMiddleware(req, res, next);
|
|
151472
|
+
}
|
|
151473
|
+
if (!this.settingsStorage.auth) {
|
|
151442
151474
|
return next();
|
|
151443
151475
|
}
|
|
151444
|
-
return
|
|
151445
|
-
users: { [auth.username]: auth.password },
|
|
151446
|
-
challenge: true,
|
|
151447
|
-
realm: "Home Assistant Matter Hub"
|
|
151448
|
-
})(req, res, next);
|
|
151476
|
+
return storageMiddleware(req, res, next);
|
|
151449
151477
|
};
|
|
151450
151478
|
}
|
|
151451
151479
|
async start() {
|
|
@@ -151539,39 +151567,43 @@ var BetterLogger = class _BetterLogger extends Logger {
|
|
|
151539
151567
|
}
|
|
151540
151568
|
}
|
|
151541
151569
|
debug(...values4) {
|
|
151542
|
-
|
|
151543
|
-
|
|
151544
|
-
|
|
151545
|
-
|
|
151546
|
-
|
|
151547
|
-
|
|
151570
|
+
if (this._level <= LogLevel.DEBUG) {
|
|
151571
|
+
addLogEntry({
|
|
151572
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
151573
|
+
level: "debug",
|
|
151574
|
+
message: `[${this.loggerName}] ${values4.map((v) => String(v)).join(" ")}`
|
|
151575
|
+
});
|
|
151576
|
+
}
|
|
151548
151577
|
super.debug(...values4);
|
|
151549
151578
|
}
|
|
151550
151579
|
info(...values4) {
|
|
151551
|
-
|
|
151552
|
-
|
|
151553
|
-
|
|
151554
|
-
|
|
151555
|
-
|
|
151556
|
-
|
|
151580
|
+
if (this._level <= LogLevel.INFO) {
|
|
151581
|
+
addLogEntry({
|
|
151582
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
151583
|
+
level: "info",
|
|
151584
|
+
message: `[${this.loggerName}] ${values4.map((v) => String(v)).join(" ")}`
|
|
151585
|
+
});
|
|
151586
|
+
}
|
|
151557
151587
|
super.info(...values4);
|
|
151558
151588
|
}
|
|
151559
151589
|
warn(...values4) {
|
|
151560
|
-
|
|
151561
|
-
|
|
151562
|
-
|
|
151563
|
-
|
|
151564
|
-
|
|
151565
|
-
|
|
151590
|
+
if (this._level <= LogLevel.WARN) {
|
|
151591
|
+
addLogEntry({
|
|
151592
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
151593
|
+
level: "warn",
|
|
151594
|
+
message: `[${this.loggerName}] ${values4.map((v) => String(v)).join(" ")}`
|
|
151595
|
+
});
|
|
151596
|
+
}
|
|
151566
151597
|
super.warn(...values4);
|
|
151567
151598
|
}
|
|
151568
151599
|
error(...values4) {
|
|
151569
|
-
|
|
151570
|
-
|
|
151571
|
-
|
|
151572
|
-
|
|
151573
|
-
|
|
151574
|
-
|
|
151600
|
+
if (this._level <= LogLevel.ERROR) {
|
|
151601
|
+
addLogEntry({
|
|
151602
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
151603
|
+
level: "error",
|
|
151604
|
+
message: `[${this.loggerName}] ${values4.map((v) => String(v)).join(" ")}`
|
|
151605
|
+
});
|
|
151606
|
+
}
|
|
151575
151607
|
super.error(...values4);
|
|
151576
151608
|
}
|
|
151577
151609
|
debugCtx(message, context) {
|
|
@@ -152643,10 +152675,56 @@ function mockDeviceId(entityId) {
|
|
|
152643
152675
|
|
|
152644
152676
|
// src/services/storage/app-settings-storage.ts
|
|
152645
152677
|
init_service();
|
|
152678
|
+
import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
152679
|
+
var SCRYPT_KEYLEN = 64;
|
|
152680
|
+
var SCRYPT_COST_N = 16384;
|
|
152681
|
+
var SCRYPT_BLOCK_R = 8;
|
|
152682
|
+
var SCRYPT_PARALLEL_P = 1;
|
|
152683
|
+
var SALT_BYTES = 16;
|
|
152646
152684
|
var DEFAULT_BACKUP_SETTINGS = {
|
|
152647
152685
|
autoBackup: true,
|
|
152648
152686
|
backupRetentionCount: 5
|
|
152649
152687
|
};
|
|
152688
|
+
function hashPassword(plain2) {
|
|
152689
|
+
const salt = randomBytes(SALT_BYTES);
|
|
152690
|
+
const derived = scryptSync(plain2, salt, SCRYPT_KEYLEN, {
|
|
152691
|
+
N: SCRYPT_COST_N,
|
|
152692
|
+
r: SCRYPT_BLOCK_R,
|
|
152693
|
+
p: SCRYPT_PARALLEL_P
|
|
152694
|
+
});
|
|
152695
|
+
return {
|
|
152696
|
+
passwordHash: derived.toString("hex"),
|
|
152697
|
+
passwordSalt: salt.toString("hex")
|
|
152698
|
+
};
|
|
152699
|
+
}
|
|
152700
|
+
function verifyPasswordHash(plain2, passwordHash, passwordSalt) {
|
|
152701
|
+
let expected;
|
|
152702
|
+
let salt;
|
|
152703
|
+
try {
|
|
152704
|
+
expected = Buffer.from(passwordHash, "hex");
|
|
152705
|
+
salt = Buffer.from(passwordSalt, "hex");
|
|
152706
|
+
} catch {
|
|
152707
|
+
return false;
|
|
152708
|
+
}
|
|
152709
|
+
if (expected.length !== SCRYPT_KEYLEN || salt.length === 0) {
|
|
152710
|
+
return false;
|
|
152711
|
+
}
|
|
152712
|
+
const derived = scryptSync(plain2, salt, SCRYPT_KEYLEN, {
|
|
152713
|
+
N: SCRYPT_COST_N,
|
|
152714
|
+
r: SCRYPT_BLOCK_R,
|
|
152715
|
+
p: SCRYPT_PARALLEL_P
|
|
152716
|
+
});
|
|
152717
|
+
return timingSafeEqual(derived, expected);
|
|
152718
|
+
}
|
|
152719
|
+
function constantTimeStringEquals(a, b) {
|
|
152720
|
+
const bufA = Buffer.from(a);
|
|
152721
|
+
const bufB = Buffer.from(b);
|
|
152722
|
+
if (bufA.length !== bufB.length) {
|
|
152723
|
+
timingSafeEqual(bufA, bufA);
|
|
152724
|
+
return false;
|
|
152725
|
+
}
|
|
152726
|
+
return timingSafeEqual(bufA, bufB);
|
|
152727
|
+
}
|
|
152650
152728
|
var AppSettingsStorage = class extends Service {
|
|
152651
152729
|
constructor(appStorage) {
|
|
152652
152730
|
super("AppSettingsStorage");
|
|
@@ -152654,6 +152732,11 @@ var AppSettingsStorage = class extends Service {
|
|
|
152654
152732
|
}
|
|
152655
152733
|
storage;
|
|
152656
152734
|
settings = {};
|
|
152735
|
+
// Caches the last plaintext password that was successfully verified
|
|
152736
|
+
// against the stored hash, keyed by username. This lets repeated basic-auth
|
|
152737
|
+
// calls avoid re-running scrypt on every HTTP request without persisting
|
|
152738
|
+
// the plaintext to disk.
|
|
152739
|
+
verifiedPassword = null;
|
|
152657
152740
|
async initialize() {
|
|
152658
152741
|
this.storage = this.appStorage.createContext("settings");
|
|
152659
152742
|
const stored = await this.storage.get(
|
|
@@ -152663,12 +152746,72 @@ var AppSettingsStorage = class extends Service {
|
|
|
152663
152746
|
this.settings = stored ?? {};
|
|
152664
152747
|
}
|
|
152665
152748
|
get auth() {
|
|
152666
|
-
|
|
152749
|
+
if (!this.settings.auth) {
|
|
152750
|
+
return void 0;
|
|
152751
|
+
}
|
|
152752
|
+
return { username: this.settings.auth.username };
|
|
152667
152753
|
}
|
|
152668
|
-
async setAuth(
|
|
152669
|
-
|
|
152754
|
+
async setAuth(credentials) {
|
|
152755
|
+
if (!credentials) {
|
|
152756
|
+
this.settings.auth = void 0;
|
|
152757
|
+
this.verifiedPassword = null;
|
|
152758
|
+
} else {
|
|
152759
|
+
const { passwordHash, passwordSalt } = hashPassword(credentials.password);
|
|
152760
|
+
this.settings.auth = {
|
|
152761
|
+
username: credentials.username,
|
|
152762
|
+
passwordHash,
|
|
152763
|
+
passwordSalt
|
|
152764
|
+
};
|
|
152765
|
+
this.verifiedPassword = {
|
|
152766
|
+
username: credentials.username,
|
|
152767
|
+
password: credentials.password
|
|
152768
|
+
};
|
|
152769
|
+
}
|
|
152670
152770
|
await this.persist();
|
|
152671
152771
|
}
|
|
152772
|
+
verifyAuth(username, password) {
|
|
152773
|
+
const stored = this.settings.auth;
|
|
152774
|
+
if (!stored) {
|
|
152775
|
+
return false;
|
|
152776
|
+
}
|
|
152777
|
+
if (!constantTimeStringEquals(username, stored.username)) {
|
|
152778
|
+
return false;
|
|
152779
|
+
}
|
|
152780
|
+
if (this.verifiedPassword && this.verifiedPassword.username === stored.username && constantTimeStringEquals(password, this.verifiedPassword.password)) {
|
|
152781
|
+
return true;
|
|
152782
|
+
}
|
|
152783
|
+
if (stored.password !== void 0) {
|
|
152784
|
+
const matches = constantTimeStringEquals(password, stored.password);
|
|
152785
|
+
if (matches) {
|
|
152786
|
+
this.verifiedPassword = { username: stored.username, password };
|
|
152787
|
+
void this.migrateLegacyPlaintext(password);
|
|
152788
|
+
}
|
|
152789
|
+
return matches;
|
|
152790
|
+
}
|
|
152791
|
+
if (stored.passwordHash && stored.passwordSalt) {
|
|
152792
|
+
if (verifyPasswordHash(password, stored.passwordHash, stored.passwordSalt)) {
|
|
152793
|
+
this.verifiedPassword = { username: stored.username, password };
|
|
152794
|
+
return true;
|
|
152795
|
+
}
|
|
152796
|
+
}
|
|
152797
|
+
return false;
|
|
152798
|
+
}
|
|
152799
|
+
async migrateLegacyPlaintext(plain2) {
|
|
152800
|
+
const current = this.settings.auth;
|
|
152801
|
+
if (!current || current.password === void 0) {
|
|
152802
|
+
return;
|
|
152803
|
+
}
|
|
152804
|
+
const { passwordHash, passwordSalt } = hashPassword(plain2);
|
|
152805
|
+
this.settings.auth = {
|
|
152806
|
+
username: current.username,
|
|
152807
|
+
passwordHash,
|
|
152808
|
+
passwordSalt
|
|
152809
|
+
};
|
|
152810
|
+
try {
|
|
152811
|
+
await this.persist();
|
|
152812
|
+
} catch {
|
|
152813
|
+
}
|
|
152814
|
+
}
|
|
152672
152815
|
get backupSettings() {
|
|
152673
152816
|
return { ...DEFAULT_BACKUP_SETTINGS, ...this.settings.backup };
|
|
152674
152817
|
}
|
|
@@ -152977,7 +153120,7 @@ var EntityMappingStorage = class extends Service {
|
|
|
152977
153120
|
|
|
152978
153121
|
// src/services/storage/lock-credential-storage.ts
|
|
152979
153122
|
init_service();
|
|
152980
|
-
import { pbkdf2Sync, randomBytes } from "node:crypto";
|
|
153123
|
+
import { pbkdf2Sync, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
152981
153124
|
var CURRENT_VERSION2 = 2;
|
|
152982
153125
|
var HASH_ITERATIONS = 1e5;
|
|
152983
153126
|
var HASH_KEY_LENGTH = 64;
|
|
@@ -153014,7 +153157,7 @@ var LockCredentialStorage = class extends Service {
|
|
|
153014
153157
|
async migrate(data) {
|
|
153015
153158
|
if (data.version === 1) {
|
|
153016
153159
|
for (const legacyCredential of data.credentials) {
|
|
153017
|
-
const salt =
|
|
153160
|
+
const salt = randomBytes2(SALT_LENGTH).toString("hex");
|
|
153018
153161
|
const hash2 = this.hashPin(legacyCredential.pinCode, salt);
|
|
153019
153162
|
const credential = {
|
|
153020
153163
|
entityId: legacyCredential.entityId,
|
|
@@ -153058,8 +153201,15 @@ var LockCredentialStorage = class extends Service {
|
|
|
153058
153201
|
if (!credential?.enabled) {
|
|
153059
153202
|
return false;
|
|
153060
153203
|
}
|
|
153061
|
-
const
|
|
153062
|
-
|
|
153204
|
+
const computed = Buffer.from(
|
|
153205
|
+
this.hashPin(pin, credential.pinCodeSalt),
|
|
153206
|
+
"hex"
|
|
153207
|
+
);
|
|
153208
|
+
const expected = Buffer.from(credential.pinCodeHash, "hex");
|
|
153209
|
+
if (computed.length !== expected.length) {
|
|
153210
|
+
return false;
|
|
153211
|
+
}
|
|
153212
|
+
return timingSafeEqual2(computed, expected);
|
|
153063
153213
|
}
|
|
153064
153214
|
/**
|
|
153065
153215
|
* Check if a credential exists and is enabled for an entity
|
|
@@ -153080,7 +153230,7 @@ var LockCredentialStorage = class extends Service {
|
|
|
153080
153230
|
async setCredential(request) {
|
|
153081
153231
|
const now = Date.now();
|
|
153082
153232
|
const existing = this.credentials.get(request.entityId);
|
|
153083
|
-
const salt =
|
|
153233
|
+
const salt = randomBytes2(SALT_LENGTH).toString("hex");
|
|
153084
153234
|
const hash2 = this.hashPin(request.pinCode, salt);
|
|
153085
153235
|
const credential = {
|
|
153086
153236
|
entityId: request.entityId,
|