/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Noc } from "#certificate/kinds/Noc.js";
import { Mark } from "#common/Mark.js";
import {
  Bytes,
  CryptoDecryptError,
  Diagnostic,
  EcdsaSignature,
  Logger,
  PublicKey,
  UnexpectedDataError
} from "#general";
import { NodeSession } from "#session/NodeSession.js";
import { SECURE_CHANNEL_PROTOCOL_ID, SecureChannelStatusCode } from "#types";
import { FabricNotFoundError } from "../../fabric/FabricManager.js";
import { ChannelStatusResponseError } from "../../securechannel/SecureChannelMessenger.js";
import {
  KDFSR1_KEY_INFO,
  KDFSR2_INFO,
  KDFSR2_KEY_INFO,
  KDFSR3_INFO,
  RESUME1_MIC_NONCE,
  RESUME2_MIC_NONCE,
  TBE_DATA2_NONCE,
  TBE_DATA3_NONCE,
  TlvEncryptedDataSigma2,
  TlvEncryptedDataSigma3,
  TlvSignedData
} from "./CaseMessages.js";
import { CaseServerMessenger } from "./CaseMessenger.js";
const logger = Logger.get("CaseServer");
class CaseServer {
  id = SECURE_CHANNEL_PROTOCOL_ID;
  requiresSecureSession = false;
  #sessions;
  #fabrics;
  constructor(sessions, fabrics) {
    this.#sessions = sessions;
    this.#fabrics = fabrics;
  }
  async onNewExchange(exchange) {
    const messenger = new CaseServerMessenger(exchange);
    try {
      await this.#handleSigma1(messenger);
    } catch (error) {
      if (error instanceof FabricNotFoundError) {
        logger.error("Error establishing CASE session:", Diagnostic.errorMessage(error));
        await messenger.sendError(SecureChannelStatusCode.NoSharedTrustRoots);
      } else if (!(error instanceof ChannelStatusResponseError)) {
        logger.error("Error establishing CASE session", error);
        await messenger.sendError(SecureChannelStatusCode.InvalidParam);
      }
    } finally {
      await exchange.session.initiateClose();
    }
  }
  async #handleSigma1(messenger) {
    logger.info("Received pairing request", Mark.INBOUND, Diagnostic.via(messenger.channelName));
    const { sigma1Bytes, sigma1 } = await messenger.readSigma1();
    const resumptionRecord = sigma1.resumptionId !== void 0 && sigma1.initiatorResumeMic !== void 0 ? this.#sessions.findResumptionRecordById(sigma1.resumptionId) : void 0;
    const context = new Sigma1Context(this.#fabrics.crypto, messenger, sigma1Bytes, sigma1, resumptionRecord);
    if (sigma1.initiatorSessionParams !== void 0) {
      messenger.channel.session.timingParameters = sigma1.initiatorSessionParams;
    }
    if (await this.#resume(context, messenger.channel.channel)) {
      return;
    }
    if (await this.#generateSigma2(context, messenger.channel.channel)) {
      return;
    }
    logger.info(
      "Invalid resumption ID or resume MIC received from",
      messenger.channelName,
      Diagnostic.dict({
        resumptionId: context.peerResumptionId,
        resumeMic: context.peerResumeMic
      })
    );
    throw new UnexpectedDataError("Invalid resumption ID or resume MIC.");
  }
  async #resume(cx, channel) {
    if (cx.peerResumptionId === void 0 || cx.peerResumeMic === void 0 || cx.resumptionRecord === void 0) {
      return false;
    }
    const { sharedSecret, fabric, peerNodeId, caseAuthenticatedTags } = cx.resumptionRecord;
    const { crypto } = this.#fabrics;
    const peerResumeKey = await crypto.createHkdfKey(
      sharedSecret,
      Bytes.concat(cx.peerRandom, cx.peerResumptionId),
      KDFSR1_KEY_INFO
    );
    try {
      crypto.decrypt(peerResumeKey, cx.peerResumeMic, RESUME1_MIC_NONCE);
    } catch (e) {
      CryptoDecryptError.accept(e);
      cx.peerResumptionId = cx.peerResumeMic = void 0;
      return false;
    }
    const responderSessionId = await this.#sessions.getNextAvailableSessionId();
    const secureSessionSalt = Bytes.concat(cx.peerRandom, cx.peerResumptionId);
    const secureSession = await this.#sessions.createSecureSession({
      channel,
      id: responderSessionId,
      fabric,
      peerNodeId,
      peerSessionId: cx.peerSessionId,
      sharedSecret,
      salt: secureSessionSalt,
      isInitiator: false,
      isResumption: true,
      peerSessionParameters: cx.peerSessionParams,
      caseAuthenticatedTags,
      delayManagerRegistration: true
      // Session establishment could still fail, so add session ourselves to the manager
    });
    const resumeSalt = Bytes.concat(cx.peerRandom, cx.localResumptionId);
    const resumeKey = await crypto.createHkdfKey(sharedSecret, resumeSalt, KDFSR2_KEY_INFO);
    const resumeMic = crypto.encrypt(resumeKey, new Uint8Array(0), RESUME2_MIC_NONCE);
    try {
      const responderSessionParams = this.#sessions.sessionParameters;
      await cx.messenger.sendSigma2Resume({
        resumptionId: cx.localResumptionId,
        resumeMic,
        responderSessionId,
        responderSessionParams
      });
    } catch (error) {
      await secureSession.initiateClose();
      throw error;
    }
    NodeSession.logNew(logger, "Resumed", secureSession, cx.messenger, fabric, peerNodeId);
    this.#sessions.sessions.add(secureSession);
    cx.resumptionRecord.resumptionId = cx.localResumptionId;
    await cx.messenger.waitForSuccess("Sigma2Resume-Success");
    await cx.messenger.close();
    await this.#sessions.saveResumptionRecord(cx.resumptionRecord);
    return true;
  }
  async #generateSigma2(cx, channel) {
    if (
      // No resumption attempted is OK
      !(cx.peerResumptionId === void 0 && cx.peerResumeMic === void 0) && // Resumption attempted with no record on our side is OK
      !(cx.peerResumptionId !== void 0 && cx.peerResumeMic !== void 0 && cx.resumptionRecord === void 0)
    ) {
      return false;
    }
    const { crypto } = this.#fabrics;
    const responderRandom = crypto.randomBytes(32);
    const fabric = await this.#fabrics.findFabricFromDestinationId(cx.destinationId, cx.peerRandom);
    const { operationalCert: nodeOpCert, intermediateCACert, operationalIdentityProtectionKey } = fabric;
    const key = await crypto.createKeyPair();
    const responderEcdhPublicKey = key.publicBits;
    const sharedSecret = await crypto.generateDhSecret(key, PublicKey(cx.peerEcdhPublicKey));
    const sigma2Salt = Bytes.concat(
      operationalIdentityProtectionKey,
      responderRandom,
      responderEcdhPublicKey,
      await crypto.computeHash(cx.bytes)
    );
    const sigma2Key = await crypto.createHkdfKey(sharedSecret, sigma2Salt, KDFSR2_INFO);
    const signatureData = TlvSignedData.encode({
      responderNoc: nodeOpCert,
      responderIcac: intermediateCACert,
      responderPublicKey: responderEcdhPublicKey,
      initiatorPublicKey: cx.peerEcdhPublicKey
    });
    const signature = await fabric.sign(signatureData);
    const encryptedData = TlvEncryptedDataSigma2.encode({
      responderNoc: nodeOpCert,
      responderIcac: intermediateCACert,
      signature: signature.bytes,
      resumptionId: cx.localResumptionId
    });
    const encrypted = crypto.encrypt(sigma2Key, encryptedData, TBE_DATA2_NONCE);
    const responderSessionId = await this.#sessions.getNextAvailableSessionId();
    const sigma2Bytes = await cx.messenger.sendSigma2({
      responderRandom,
      responderSessionId,
      responderEcdhPublicKey,
      encrypted,
      responderSessionParams: this.#sessions.sessionParameters
      // responder session parameters
    });
    const {
      sigma3Bytes,
      sigma3: { encrypted: peerEncrypted }
    } = await cx.messenger.readSigma3();
    const sigma3Salt = Bytes.concat(
      operationalIdentityProtectionKey,
      await crypto.computeHash([cx.bytes, sigma2Bytes])
    );
    const sigma3Key = await crypto.createHkdfKey(sharedSecret, sigma3Salt, KDFSR3_INFO);
    const peerDecryptedData = crypto.decrypt(sigma3Key, peerEncrypted, TBE_DATA3_NONCE);
    const {
      responderNoc: peerNewOpCert,
      responderIcac: peerIntermediateCACert,
      signature: peerSignature
    } = TlvEncryptedDataSigma3.decode(peerDecryptedData);
    await fabric.verifyCredentials(peerNewOpCert, peerIntermediateCACert);
    const peerSignatureData = TlvSignedData.encode({
      responderNoc: peerNewOpCert,
      responderIcac: peerIntermediateCACert,
      responderPublicKey: cx.peerEcdhPublicKey,
      initiatorPublicKey: responderEcdhPublicKey
    });
    const {
      ellipticCurvePublicKey: peerPublicKey,
      subject: { fabricId: peerFabricId, nodeId: peerNodeId, caseAuthenticatedTags }
    } = Noc.fromTlv(peerNewOpCert).cert;
    if (fabric.fabricId !== peerFabricId) {
      throw new UnexpectedDataError(`Fabric ID mismatch: ${fabric.fabricId} !== ${peerFabricId}`);
    }
    await crypto.verifyEcdsa(PublicKey(peerPublicKey), peerSignatureData, new EcdsaSignature(peerSignature));
    const secureSessionSalt = Bytes.concat(
      operationalIdentityProtectionKey,
      await crypto.computeHash([cx.bytes, sigma2Bytes, sigma3Bytes])
    );
    const secureSession = await this.#sessions.createSecureSession({
      channel,
      id: responderSessionId,
      fabric,
      peerNodeId,
      peerSessionId: cx.peerSessionId,
      sharedSecret,
      salt: secureSessionSalt,
      isInitiator: false,
      isResumption: false,
      peerSessionParameters: cx.peerSessionParams,
      caseAuthenticatedTags
    });
    NodeSession.logNew(logger, "New", secureSession, cx.messenger, fabric, peerNodeId);
    await cx.messenger.sendSuccess();
    const resumptionRecord = {
      peerNodeId,
      fabric,
      sharedSecret,
      resumptionId: cx.localResumptionId,
      sessionParameters: secureSession.parameters,
      caseAuthenticatedTags
    };
    await cx.messenger.close();
    await this.#sessions.saveResumptionRecord(resumptionRecord);
    return true;
  }
  async close() {
  }
}
class Sigma1Context {
  crypto;
  messenger;
  bytes;
  peerSessionId;
  peerResumptionId;
  peerResumeMic;
  destinationId;
  peerRandom;
  peerEcdhPublicKey;
  peerSessionParams;
  resumptionRecord;
  #localResumptionId;
  constructor(crypto, messenger, bytes, sigma1, resumptionRecord) {
    this.crypto = crypto;
    this.messenger = messenger;
    this.bytes = bytes;
    this.peerSessionId = sigma1.initiatorSessionId;
    this.peerResumptionId = sigma1.resumptionId;
    this.peerResumeMic = sigma1.initiatorResumeMic;
    this.destinationId = sigma1.destinationId;
    this.peerRandom = sigma1.initiatorRandom;
    this.peerEcdhPublicKey = sigma1.initiatorEcdhPublicKey;
    this.peerSessionParams = sigma1.initiatorSessionParams;
    this.resumptionRecord = resumptionRecord;
  }
  get localResumptionId() {
    return this.#localResumptionId ??= this.crypto.randomBytes(16);
  }
}
export {
  CaseServer
};
//# sourceMappingURL=CaseServer.js.map
