@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.
@@ -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: randomBytes2, createKeyPair } = crypto8;
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 === randomBytes2;
10041
+ return crypto8.randomBytes === randomBytes3;
10042
10042
  },
10043
10043
  set(entropic) {
10044
10044
  if (entropic) {
10045
- crypto8.randomBytes = randomBytes2;
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 = path.join(targetDir, relativePath);
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 = path.join(iconsDir, fileName);
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
- const auth = this.props.auth ?? this.settingsStorage.auth;
151441
- if (!auth) {
151470
+ if (envMiddleware) {
151471
+ return envMiddleware(req, res, next);
151472
+ }
151473
+ if (!this.settingsStorage.auth) {
151442
151474
  return next();
151443
151475
  }
151444
- return basicAuth({
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
- const message = values4.map((v) => String(v)).join(" ");
151543
- addLogEntry({
151544
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
151545
- level: "debug",
151546
- message: `[${this.loggerName}] ${message}`
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
- const message = values4.map((v) => String(v)).join(" ");
151552
- addLogEntry({
151553
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
151554
- level: "info",
151555
- message: `[${this.loggerName}] ${message}`
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
- const message = values4.map((v) => String(v)).join(" ");
151561
- addLogEntry({
151562
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
151563
- level: "warn",
151564
- message: `[${this.loggerName}] ${message}`
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
- const message = values4.map((v) => String(v)).join(" ");
151570
- addLogEntry({
151571
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
151572
- level: "error",
151573
- message: `[${this.loggerName}] ${message}`
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
- return this.settings.auth;
152749
+ if (!this.settings.auth) {
152750
+ return void 0;
152751
+ }
152752
+ return { username: this.settings.auth.username };
152667
152753
  }
152668
- async setAuth(auth) {
152669
- this.settings.auth = auth;
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 = randomBytes(SALT_LENGTH).toString("hex");
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 hash2 = this.hashPin(pin, credential.pinCodeSalt);
153062
- return hash2 === credential.pinCodeHash;
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 = randomBytes(SALT_LENGTH).toString("hex");
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,