diff --git a/CHANGELOG.md b/CHANGELOG.md index c307ea89..3370096c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - gui: Download TAF tracks as separate .ogg files (ZIP when more than one track is selected) [https://github.com/toniebox-reverse-engineering/teddycloud/issues/169](https://github.com/toniebox-reverse-engineering/teddycloud/issues/169) - gui: Box setup can create a port patch for CC3200 boxes (altPort.custom.json) and patch the port of ESP32 boxes, e.g. for a proxy that routes boxes by port +- gui: Certificate check per box (issued by TeddyCloud / pinned / other certificate) on the box card and in the certificates dialog [https://github.com/toniebox-reverse-engineering/teddycloud/issues/85](https://github.com/toniebox-reverse-engineering/teddycloud/issues/85) - Added "listened" status tracking for library files, incl. auto-mark on sync (opt-out) [https://github.com/toniebox-reverse-engineering/teddycloud/pull/467](https://github.com/toniebox-reverse-engineering/teddycloud/pull/467) - Added total audio length (`lengthSeconds`) to the TAF info of the file index and the tag index [https://github.com/toniebox-reverse-engineering/teddycloud/pull/537](https://github.com/toniebox-reverse-engineering/teddycloud/pull/537) - gui: Optional web UI login with multiple users (all full UI rights) [https://github.com/toniebox-reverse-engineering/teddycloud/issues/85](https://github.com/toniebox-reverse-engineering/teddycloud/issues/85) diff --git a/public/translations/de.json b/public/translations/de.json index 419dc318..c2dcd5bc 100644 --- a/public/translations/de.json +++ b/public/translations/de.json @@ -1884,7 +1884,20 @@ "uploadTonieboxCertificates": "Zertifikate für Toniebox{{name}} hochladen" }, "warningUseAtYourOwnRisk": "Warnung: Nutzung auf eigene Gefahr!", - "warningUseAtYourOwnRiskText": "Das Flashen deiner Toniebox mit einer modifizierten Firmware führt zum Erlöschen der Garantie und kann deine Box unbrauchbar machen. Bitte folge den Anweisungen sorgfältig und stelle sicher, dass du die damit verbundenen Risiken verstehst." + "warningUseAtYourOwnRiskText": "Das Flashen deiner Toniebox mit einer modifizierten Firmware führt zum Erlöschen der Garantie und kann deine Box unbrauchbar machen. Bitte folge den Anweisungen sorgfältig und stelle sicher, dass du die damit verbundenen Risiken verstehst.", + "boxCertificate": { + "title": "Zertifikatsprüfung", + "status": "Status", + "presented": "Vorgezeigtes Zertifikat (SHA-256)", + "pin": "Gepinntes Zertifikat (SHA-256)", + "nextConnection": "Der Status wird bei der nächsten Verbindung der Box aktualisiert.", + "statusText": { + "none": "Noch nicht geprüft", + "trusted": "Zertifikat von TeddyCloud ausgestellt", + "pinned": "Gepinntes Zertifikat", + "mismatch": "Anderes Zertifikat als das gepinnte!" + } + } }, "tonies": { "addNewCustomTonie": "Neues Modell anlegen", diff --git a/public/translations/en.json b/public/translations/en.json index a12f2a6d..e5056d27 100644 --- a/public/translations/en.json +++ b/public/translations/en.json @@ -1884,7 +1884,20 @@ "uploadTonieboxCertificates": "Upload certificates for Toniebox{{name}}" }, "warningUseAtYourOwnRisk": "Warning: Use at your own risk!", - "warningUseAtYourOwnRiskText": "Flashing your Toniebox with a patched firmware will void your warranty and may lead to a broken box. Please follow the instructions carefully and ensure you understand the risks involved." + "warningUseAtYourOwnRiskText": "Flashing your Toniebox with a patched firmware will void your warranty and may lead to a broken box. Please follow the instructions carefully and ensure you understand the risks involved.", + "boxCertificate": { + "title": "Certificate check", + "status": "Status", + "presented": "Presented certificate (SHA-256)", + "pin": "Pinned certificate (SHA-256)", + "nextConnection": "The status is updated with the next connection of the box.", + "statusText": { + "none": "Not checked yet", + "trusted": "Certificate issued by TeddyCloud", + "pinned": "Pinned certificate", + "mismatch": "Other certificate than the pinned one!" + } + } }, "tonies": { "addNewCustomTonie": "Create new model", diff --git a/public/translations/es.json b/public/translations/es.json index b859cedf..78e399c1 100644 --- a/public/translations/es.json +++ b/public/translations/es.json @@ -1884,7 +1884,20 @@ "uploadTonieboxCertificates": "Subir certificados para Toniebox{{name}}" }, "warningUseAtYourOwnRisk": "Advertencia: úsalo bajo tu propia responsabilidad", - "warningUseAtYourOwnRiskText": "Flashear tu Toniebox con un firmware modificado anulará la garantía y puede dejar la caja inutilizable. Sigue las instrucciones cuidadosamente y asegúrate de comprender los riesgos asociados." + "warningUseAtYourOwnRiskText": "Flashear tu Toniebox con un firmware modificado anulará la garantía y puede dejar la caja inutilizable. Sigue las instrucciones cuidadosamente y asegúrate de comprender los riesgos asociados.", + "boxCertificate": { + "title": "Comprobación del certificado", + "status": "Estado", + "presented": "Certificado presentado (SHA-256)", + "pin": "Certificado fijado (SHA-256)", + "nextConnection": "El estado se actualiza con la próxima conexión de la Toniebox.", + "statusText": { + "none": "Aún no comprobado", + "trusted": "Certificado emitido por TeddyCloud", + "pinned": "Certificado fijado", + "mismatch": "¡Otro certificado distinto del fijado!" + } + } }, "tonies": { "addNewCustomTonie": "Crear nuevo modelo", diff --git a/public/translations/fr.json b/public/translations/fr.json index a872b761..66052c9d 100644 --- a/public/translations/fr.json +++ b/public/translations/fr.json @@ -1884,7 +1884,20 @@ "uploadTonieboxCertificates": "Télécharger les certificats pour la Toniebox{{name}}" }, "warningUseAtYourOwnRisk": "Avertissement : utilisation à tes propres risques", - "warningUseAtYourOwnRiskText": "Flasher ta Toniebox avec un firmware modifié annulera la garantie et peut rendre la box inutilisable. Suis attentivement les instructions et assure-toi de bien comprendre les risques encourus." + "warningUseAtYourOwnRiskText": "Flasher ta Toniebox avec un firmware modifié annulera la garantie et peut rendre la box inutilisable. Suis attentivement les instructions et assure-toi de bien comprendre les risques encourus.", + "boxCertificate": { + "title": "Vérification du certificat", + "status": "Statut", + "presented": "Certificat présenté (SHA-256)", + "pin": "Certificat épinglé (SHA-256)", + "nextConnection": "Le statut est mis à jour à la prochaine connexion de la Toniebox.", + "statusText": { + "none": "Pas encore vérifié", + "trusted": "Certificat émis par TeddyCloud", + "pinned": "Certificat épinglé", + "mismatch": "Autre certificat que celui épinglé !" + } + } }, "tonies": { "addNewCustomTonie": "Créer un nouveau modèle", diff --git a/public/translations/it.json b/public/translations/it.json index 389a7461..37e6bc58 100644 --- a/public/translations/it.json +++ b/public/translations/it.json @@ -1884,7 +1884,20 @@ "uploadTonieboxCertificates": "Carica certificati per Toniebox{{name}}" }, "warningUseAtYourOwnRisk": "Avviso: usa a tuo rischio!", - "warningUseAtYourOwnRiskText": "Flashare il Toniebox con un firmware modificato invaliderà la garanzia e potrebbe danneggiare il box. Segui attentamente le istruzioni e assicurati di comprendere i rischi." + "warningUseAtYourOwnRiskText": "Flashare il Toniebox con un firmware modificato invaliderà la garanzia e potrebbe danneggiare il box. Segui attentamente le istruzioni e assicurati di comprendere i rischi.", + "boxCertificate": { + "title": "Verifica del certificato", + "status": "Stato", + "presented": "Certificato presentato (SHA-256)", + "pin": "Certificato fissato (SHA-256)", + "nextConnection": "Lo stato viene aggiornato alla prossima connessione della Toniebox.", + "statusText": { + "none": "Non ancora verificato", + "trusted": "Certificato emesso da TeddyCloud", + "pinned": "Certificato fissato", + "mismatch": "Certificato diverso da quello fissato!" + } + } }, "tonies": { "addNewCustomTonie": "Crea nuovo modello", diff --git a/public/translations/pl.json b/public/translations/pl.json index dc40bef2..1b069108 100644 --- a/public/translations/pl.json +++ b/public/translations/pl.json @@ -1884,7 +1884,20 @@ "uploadTonieboxCertificates": "Prześlij certyfikaty dla Tonieboxa {{name}}" }, "warningUseAtYourOwnRisk": "Ostrzeżenie: używasz na własne ryzyko!", - "warningUseAtYourOwnRiskText": "Flashowanie Tonieboxa spatchowanym firmware unieważni gwarancję i może doprowadzić do uszkodzenia boxa. Postępuj dokładnie zgodnie z instrukcjami i upewnij się, że rozumiesz związane z tym ryzyko." + "warningUseAtYourOwnRiskText": "Flashowanie Tonieboxa spatchowanym firmware unieważni gwarancję i może doprowadzić do uszkodzenia boxa. Postępuj dokładnie zgodnie z instrukcjami i upewnij się, że rozumiesz związane z tym ryzyko.", + "boxCertificate": { + "title": "Sprawdzanie certyfikatu", + "status": "Status", + "presented": "Przedstawiony certyfikat (SHA-256)", + "pin": "Przypięty certyfikat (SHA-256)", + "nextConnection": "Status zostanie zaktualizowany przy następnym połączeniu Toniebox.", + "statusText": { + "none": "Jeszcze nie sprawdzono", + "trusted": "Certyfikat wystawiony przez TeddyCloud", + "pinned": "Przypięty certyfikat", + "mismatch": "Inny certyfikat niż przypięty!" + } + } }, "tonies": { "addNewCustomTonie": "Utwórz nowy model", diff --git a/src/components/tonieboxes/common/elements/BoxCertificateCheck.tsx b/src/components/tonieboxes/common/elements/BoxCertificateCheck.tsx new file mode 100644 index 00000000..d3a6c5d3 --- /dev/null +++ b/src/components/tonieboxes/common/elements/BoxCertificateCheck.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { Descriptions, Tag, Typography } from "antd"; +import { useTranslation } from "react-i18next"; + +import { BoxCertStatus, useBoxCertificate } from "../hooks/useBoxCertificate"; + +const { Paragraph, Text } = Typography; + +export const boxCertStatusColor: Record = { + "": undefined, + trusted: "success", + pinned: "success", + mismatch: "error", +}; + +const Fingerprint: React.FC<{ value: string }> = ({ value }) => + value ? ( + + {value} + + ) : ( + - + ); + +export const BoxCertificateCheck: React.FC<{ overlayId?: string }> = ({ overlayId }) => { + const { t } = useTranslation(); + const { status, presented, pin } = useBoxCertificate(overlayId, true); + + return ( + <> +

{t("tonieboxes.boxCertificate.title")}

+ + + + {t(`tonieboxes.boxCertificate.statusText.${status || "none"}`)} + + + + + + + + + + + {t("tonieboxes.boxCertificate.nextConnection")} + + + ); +}; diff --git a/src/components/tonieboxes/common/hooks/useBoxCertificate.ts b/src/components/tonieboxes/common/hooks/useBoxCertificate.ts new file mode 100644 index 00000000..d1ee9279 --- /dev/null +++ b/src/components/tonieboxes/common/hooks/useBoxCertificate.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; + +import { TeddyCloudApi } from "../../../../api"; +import { defaultAPIConfig } from "../../../../config/defaultApiConfig"; + +const api = new TeddyCloudApi(defaultAPIConfig()); + +export type BoxCertStatus = "" | "trusted" | "pinned" | "mismatch"; + +const readSetting = async (key: string, overlay: string) => + (await (await api.apiGetTeddyCloudSettingRaw(key, overlay)).text()).trim(); + +/** Status of the box certificate check; with details also the presented and the pinned fingerprint. */ +export const useBoxCertificate = (overlay?: string, details = false) => { + const [status, setStatus] = useState(""); + const [presented, setPresented] = useState(""); + const [pin, setPin] = useState(""); + + useEffect(() => { + if (!overlay) return; + const load = async () => { + try { + setStatus((await readSetting("internal.boxCertStatus", overlay)) as BoxCertStatus); + if (details) { + setPresented(await readSetting("internal.boxCertSha256", overlay)); + setPin(await readSetting("toniebox.certPin", overlay)); + } + } catch { + // TeddyCloud without the box certificate check + } + }; + load(); + }, [overlay, details]); + + return { status, presented, pin }; +}; diff --git a/src/components/tonieboxes/common/modals/CertificatesModal.tsx b/src/components/tonieboxes/common/modals/CertificatesModal.tsx index e24953cc..89ba402d 100644 --- a/src/components/tonieboxes/common/modals/CertificatesModal.tsx +++ b/src/components/tonieboxes/common/modals/CertificatesModal.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { OptionsList } from "../../../../api"; import { CertificateDragNDrop } from "../../../common/form/CertificatesDragAndDrop"; +import { BoxCertificateCheck } from "../elements/BoxCertificateCheck"; const { Paragraph } = Typography; @@ -53,6 +54,7 @@ export const CertificatesModal: React.FC = ({ )} + {overlayId && } ); }; diff --git a/src/components/tonieboxes/tonieboxcard/TonieboxCard.tsx b/src/components/tonieboxes/tonieboxcard/TonieboxCard.tsx index c64e3d90..be411c0d 100644 --- a/src/components/tonieboxes/tonieboxcard/TonieboxCard.tsx +++ b/src/components/tonieboxes/tonieboxcard/TonieboxCard.tsx @@ -25,6 +25,7 @@ import defaultBoxImage from "../../../assets/unknown_box.png"; import { EditBoxModal } from "./modals/EditTonieModal"; import { CertificatesModal } from "../common/modals/CertificatesModal"; +import { useBoxCertificate } from "../common/hooks/useBoxCertificate"; import { SettingsModal } from "./modals/SettingsModal"; import { DeleteModal } from "./modals/DeleteModal"; import { useTriggerWriteConfig } from "./hooks/useTriggerWriteConfig"; @@ -67,6 +68,7 @@ export const TonieboxCard: React.FC<{ const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] = useState(false); const [tonieboxAccessApi, setTonieboxAccessApi] = useState(true); const [modalKey, setModalKey] = useState(0); + const { status: certStatus } = useBoxCertificate(tonieboxCard.ID); useEffect(() => { const fetchTonieboxApiAccess = async () => { @@ -233,6 +235,14 @@ export const TonieboxCard: React.FC<{ }; // certificates + const certStatusColor = () => + ({ + trusted: token.colorSuccess, + pinned: token.colorSuccess, + mismatch: token.colorError, + "": undefined, + })[certStatus]; + const handleUploadCertificatesClick = () => { const fetchOptions = async () => { const optionsRequest = (await api.apiGetIndexGet(tonieboxCard.ID)) as OptionsList; @@ -628,11 +638,18 @@ export const TonieboxCard: React.FC<{ )} , showModelModal()} />, - , + open={!canHover || !certStatus ? false : undefined} + title={t( + `tonieboxes.boxCertificate.statusText.${certStatus || "none"}`, + )} + > + + ,