diff --git a/assets/tailwind.css b/assets/tailwind.css
index 9d56ea1..d3da11b 100644
--- a/assets/tailwind.css
+++ b/assets/tailwind.css
@@ -1 +1 @@
-*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.-right-2{right:-.5rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-2{bottom:.5rem}.bottom-4{bottom:1rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.right-3{right:.75rem}.right-4{right:1rem}.right-6{right:1.5rem}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mr-auto{margin-right:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-20{margin-top:5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1{height:.25rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-28{height:7rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-80{height:20rem}.h-96{height:24rem}.h-\[420px\]{height:420px}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.max-h-\[80vh\]{max-height:80vh}.min-h-\[72px\]{min-height:72px}.min-h-\[calc\(100vh-104px\)\]{min-height:calc(100vh - 104px)}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-28{width:7rem}.w-4{width:1rem}.w-6{width:1.5rem}.w-72{width:18rem}.w-8{width:2rem}.w-\[20rem\]{width:20rem}.w-full{width:100%}.min-w-\[160px\]{min-width:160px}.min-w-\[240px\]{min-width:240px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.translate-x-full{--tw-translate-x:100%}.translate-x-full,.translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-full{--tw-translate-y:100%}.rotate-180{--tw-rotate:180deg}.rotate-180,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem*var(--tw-space-y-reverse))}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.scroll-smooth{scroll-behavior:smooth}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-blue-500\/20{border-color:rgba(59,130,246,.2)}.border-gray-500\/10{border-color:hsla(220,9%,46%,.1)}.border-gray-500\/20{border-color:hsla(220,9%,46%,.2)}.border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity,1))}.border-gray-600\/30{border-color:rgba(75,85,99,.3)}.border-gray-700\/30{border-color:rgba(55,65,81,.3)}.border-green-500\/20{border-color:rgba(34,197,94,.2)}.border-green-500\/30{border-color:rgba(34,197,94,.3)}.border-orange-500\/20{border-color:rgba(249,115,22,.2)}.border-purple-500\/20{border-color:rgba(168,85,247,.2)}.border-red-500\/20{border-color:rgba(239,68,68,.2)}.border-yellow-500\/20{border-color:rgba(234,179,8,.2)}.bg-\[rgb\(20_20_20_\/50\%\)\]{background-color:hsla(0,0%,8%,.5)}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-black\/80{background-color:rgba(0,0,0,.8)}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(96 165 250/var(--tw-bg-opacity,1))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-500\/10{background-color:rgba(59,130,246,.1)}.bg-blue-500\/20{background-color:rgba(59,130,246,.2)}.bg-blue-500\/90{background-color:rgba(59,130,246,.9)}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-gray-500\/10{background-color:hsla(220,9%,46%,.1)}.bg-gray-500\/20{background-color:hsla(220,9%,46%,.2)}.bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity,1))}.bg-gray-600\/20{background-color:rgba(75,85,99,.2)}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.bg-gray-700\/50{background-color:rgba(55,65,81,.5)}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-800\/50{background-color:rgba(31,41,55,.5)}.bg-gray-800\/95{background-color:rgba(31,41,55,.95)}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.bg-gray-900\/30{background-color:rgba(17,24,39,.3)}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(187 247 208/var(--tw-bg-opacity,1))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(74 222 128/var(--tw-bg-opacity,1))}.bg-green-400\/20{background-color:rgba(74,222,128,.2)}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-green-500\/10{background-color:rgba(34,197,94,.1)}.bg-green-500\/20{background-color:rgba(34,197,94,.2)}.bg-green-500\/90{background-color:rgba(34,197,94,.9)}.bg-green-600\/20{background-color:rgba(22,163,74,.2)}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity,1))}.bg-orange-400{--tw-bg-opacity:1;background-color:rgb(251 146 60/var(--tw-bg-opacity,1))}.bg-orange-500{--tw-bg-opacity:1;background-color:rgb(249 115 22/var(--tw-bg-opacity,1))}.bg-orange-500\/10{background-color:rgba(249,115,22,.1)}.bg-orange-500\/15{background-color:rgba(249,115,22,.15)}.bg-orange-500\/20{background-color:rgba(249,115,22,.2)}.bg-purple-500\/10{background-color:rgba(168,85,247,.1)}.bg-purple-600{--tw-bg-opacity:1;background-color:rgb(147 51 234/var(--tw-bg-opacity,1))}.bg-red-200{--tw-bg-opacity:1;background-color:rgb(254 202 202/var(--tw-bg-opacity,1))}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity,1))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/10{background-color:rgba(239,68,68,.1)}.bg-red-500\/20{background-color:rgba(239,68,68,.2)}.bg-red-500\/90{background-color:rgba(239,68,68,.9)}.bg-red-900\/50{background-color:rgba(127,29,29,.5)}.bg-transparent{background-color:transparent}.bg-white\/20{background-color:hsla(0,0%,100%,.2)}.bg-yellow-400{--tw-bg-opacity:1;background-color:rgb(250 204 21/var(--tw-bg-opacity,1))}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity,1))}.bg-yellow-500\/10{background-color:rgba(234,179,8,.1)}.bg-yellow-500\/20{background-color:rgba(234,179,8,.2)}.bg-yellow-500\/90{background-color:rgba(234,179,8,.9)}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-\[\#1f1f1f\]\/90{--tw-gradient-from:rgba(31,31,31,.9) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,31,31,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-gray-800\/20{--tw-gradient-from:rgba(31,41,55,.2) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,41,55,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-orange-500{--tw-gradient-from:#f97316 var(--tw-gradient-from-position);--tw-gradient-to:rgba(249,115,22,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-transparent{--tw-gradient-from:transparent var(--tw-gradient-from-position);--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.via-zinc-700{--tw-gradient-to:rgba(63,63,70,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#3f3f46 var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-gray-900\/20{--tw-gradient-to:rgba(17,24,39,.2) var(--tw-gradient-to-position)}.to-orange-600{--tw-gradient-to:#ea580c var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.fill-blue-500{fill:#3b82f6}.fill-white{fill:#fff}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-14{padding-top:3.5rem;padding-bottom:3.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pr-2{padding-right:.5rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[3rem\]{font-size:3rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.lowercase{text-transform:lowercase}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-blue-200{--tw-text-opacity:1;color:rgb(191 219 254/var(--tw-text-opacity,1))}.text-blue-300{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity,1))}.text-cyan-300{--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-green-200{--tw-text-opacity:1;color:rgb(187 247 208/var(--tw-text-opacity,1))}.text-green-300{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-orange-200{--tw-text-opacity:1;color:rgb(254 215 170/var(--tw-text-opacity,1))}.text-orange-300{--tw-text-opacity:1;color:rgb(253 186 116/var(--tw-text-opacity,1))}.text-orange-400{--tw-text-opacity:1;color:rgb(251 146 60/var(--tw-text-opacity,1))}.text-orange-500{--tw-text-opacity:1;color:rgb(249 115 22/var(--tw-text-opacity,1))}.text-purple-400{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity,1))}.text-purple-500{--tw-text-opacity:1;color:rgb(168 85 247/var(--tw-text-opacity,1))}.text-red-200{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity,1))}.text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/10{color:hsla(0,0%,100%,.1)}.text-yellow-200{--tw-text-opacity:1;color:rgb(254 240 138/var(--tw-text-opacity,1))}.text-yellow-300{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity,1))}.text-zinc-600{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-90{opacity:.9}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px)}.backdrop-blur-md,.backdrop-blur-sm{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.\[checksum\:4\]{checksum:4}.\[data\:variable\]{data:variable}.\[name\:4\]{name:4}.\[size\:4\]{size:4}.hover\:bg-\[rgb\(20_20_20_\/30\%\)\]:hover{background-color:hsla(0,0%,8%,.3)}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity,1))}.hover\:bg-emerald-600:hover{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.hover\:bg-gray-500:hover{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity,1))}.hover\:bg-gray-600\/30:hover{background-color:rgba(75,85,99,.3)}.hover\:bg-green-500\/20:hover{background-color:rgba(34,197,94,.2)}.hover\:bg-green-500\/30:hover{background-color:rgba(34,197,94,.3)}.hover\:bg-green-600\/30:hover{background-color:rgba(22,163,74,.3)}.hover\:bg-green-600\/40:hover{background-color:rgba(22,163,74,.4)}.hover\:bg-orange-500\/20:hover{background-color:rgba(249,115,22,.2)}.hover\:bg-orange-500\/40:hover{background-color:rgba(249,115,22,.4)}.hover\:bg-orange-600:hover{--tw-bg-opacity:1;background-color:rgb(234 88 12/var(--tw-bg-opacity,1))}.hover\:bg-purple-500:hover{--tw-bg-opacity:1;background-color:rgb(168 85 247/var(--tw-bg-opacity,1))}.hover\:bg-purple-500\/20:hover{background-color:rgba(168,85,247,.2)}.hover\:bg-red-500\/20:hover{background-color:rgba(239,68,68,.2)}.hover\:bg-red-500\/30:hover{background-color:rgba(239,68,68,.3)}.hover\:bg-red-600:hover{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.hover\:bg-red-600\/40:hover{background-color:rgba(220,38,38,.4)}.hover\:bg-white\/30:hover{background-color:hsla(0,0%,100%,.3)}.hover\:bg-yellow-600\/40:hover{background-color:rgba(202,138,4,.4)}.hover\:from-orange-600:hover{--tw-gradient-from:#ea580c var(--tw-gradient-from-position);--tw-gradient-to:rgba(234,88,12,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.hover\:to-orange-700:hover{--tw-gradient-to:#c2410c var(--tw-gradient-to-position)}.hover\:text-gray-300:hover{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.hover\:text-green-300:hover{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:opacity-80:hover{opacity:.8}.focus\:border-green-500\/40:focus{border-color:rgba(34,197,94,.4)}.focus\:border-orange-500\/40:focus{border-color:rgba(249,115,22,.4)}.focus\:border-purple-500\/40:focus{border-color:rgba(168,85,247,.4)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.group:hover .group-hover\:scale-105,.group:hover .group-hover\:scale-110{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:640px){.sm\:mb-0{margin-bottom:0}.sm\:mb-3{margin-bottom:.75rem}.sm\:mr-2{margin-right:.5rem}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:h-10{height:2.5rem}.sm\:h-12{height:3rem}.sm\:w-10{width:2.5rem}.sm\:w-12{width:3rem}.sm\:w-\[24rem\]{width:24rem}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-4{gap:1rem}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.sm\:p-4{padding:1rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:px-3{padding-left:.75rem;padding-right:.75rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-base{font-size:1rem;line-height:1.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}.sm\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:768px){.md\:flex{display:flex}.md\:hidden{display:none}.md\:h-32{height:8rem}.md\:h-\[32rem\]{height:32rem}.md\:w-\[28rem\]{width:28rem}.md\:w-auto{width:auto}.md\:w-px{width:1px}.md\:flex-none{flex:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:flex-col{flex-direction:column}.md\:bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}}@media (min-width:1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:block{display:block}.lg\:w-\[32rem\]{width:32rem}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}}
\ No newline at end of file
+*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.-right-2{right:-.5rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-2{bottom:.5rem}.bottom-4{bottom:1rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.right-3{right:.75rem}.right-4{right:1rem}.right-6{right:1.5rem}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.ml-6{margin-left:1.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mr-auto{margin-right:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-20{margin-top:5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1{height:.25rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-28{height:7rem}.h-3{height:.75rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-80{height:20rem}.h-96{height:24rem}.h-\[420px\]{height:420px}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.max-h-40{max-height:10rem}.max-h-\[80vh\]{max-height:80vh}.max-h-\[90vh\]{max-height:90vh}.max-h-\[calc\(90vh-200px\)\]{max-height:calc(90vh - 200px)}.min-h-\[72px\]{min-height:72px}.min-h-\[calc\(100vh-104px\)\]{min-height:calc(100vh - 104px)}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-28{width:7rem}.w-3{width:.75rem}.w-4{width:1rem}.w-6{width:1.5rem}.w-72{width:18rem}.w-8{width:2rem}.w-\[20rem\]{width:20rem}.w-full{width:100%}.min-w-\[160px\]{min-width:160px}.min-w-\[240px\]{min-width:240px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.translate-x-full{--tw-translate-x:100%}.translate-x-full,.translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-full{--tw-translate-y:100%}.rotate-180{--tw-rotate:180deg}.rotate-180,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem*var(--tw-space-y-reverse))}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.scroll-smooth{scroll-behavior:smooth}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-blue-500\/20{border-color:rgba(59,130,246,.2)}.border-gray-500\/10{border-color:hsla(220,9%,46%,.1)}.border-gray-500\/20{border-color:hsla(220,9%,46%,.2)}.border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity,1))}.border-gray-600\/30{border-color:rgba(75,85,99,.3)}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity,1))}.border-gray-700\/30{border-color:rgba(55,65,81,.3)}.border-green-500\/20{border-color:rgba(34,197,94,.2)}.border-green-500\/30{border-color:rgba(34,197,94,.3)}.border-orange-500\/20{border-color:rgba(249,115,22,.2)}.border-purple-500\/20{border-color:rgba(168,85,247,.2)}.border-red-500\/20{border-color:rgba(239,68,68,.2)}.border-red-700{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity,1))}.border-yellow-500\/20{border-color:rgba(234,179,8,.2)}.bg-\[rgb\(20_20_20_\/50\%\)\]{background-color:hsla(0,0%,8%,.5)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-black\/80{background-color:rgba(0,0,0,.8)}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(96 165 250/var(--tw-bg-opacity,1))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-500\/10{background-color:rgba(59,130,246,.1)}.bg-blue-500\/20{background-color:rgba(59,130,246,.2)}.bg-blue-500\/90{background-color:rgba(59,130,246,.9)}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-gray-500\/10{background-color:hsla(220,9%,46%,.1)}.bg-gray-500\/20{background-color:hsla(220,9%,46%,.2)}.bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity,1))}.bg-gray-600\/20{background-color:rgba(75,85,99,.2)}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.bg-gray-700\/50{background-color:rgba(55,65,81,.5)}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-800\/50{background-color:rgba(31,41,55,.5)}.bg-gray-800\/95{background-color:rgba(31,41,55,.95)}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.bg-gray-900\/30{background-color:rgba(17,24,39,.3)}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(187 247 208/var(--tw-bg-opacity,1))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(74 222 128/var(--tw-bg-opacity,1))}.bg-green-400\/20{background-color:rgba(74,222,128,.2)}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-green-500\/10{background-color:rgba(34,197,94,.1)}.bg-green-500\/20{background-color:rgba(34,197,94,.2)}.bg-green-500\/90{background-color:rgba(34,197,94,.9)}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity,1))}.bg-green-600\/20{background-color:rgba(22,163,74,.2)}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity,1))}.bg-orange-400{--tw-bg-opacity:1;background-color:rgb(251 146 60/var(--tw-bg-opacity,1))}.bg-orange-500{--tw-bg-opacity:1;background-color:rgb(249 115 22/var(--tw-bg-opacity,1))}.bg-orange-500\/10{background-color:rgba(249,115,22,.1)}.bg-orange-500\/15{background-color:rgba(249,115,22,.15)}.bg-orange-500\/20{background-color:rgba(249,115,22,.2)}.bg-purple-500\/10{background-color:rgba(168,85,247,.1)}.bg-purple-600{--tw-bg-opacity:1;background-color:rgb(147 51 234/var(--tw-bg-opacity,1))}.bg-red-200{--tw-bg-opacity:1;background-color:rgb(254 202 202/var(--tw-bg-opacity,1))}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity,1))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/10{background-color:rgba(239,68,68,.1)}.bg-red-500\/20{background-color:rgba(239,68,68,.2)}.bg-red-500\/90{background-color:rgba(239,68,68,.9)}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.bg-red-900{--tw-bg-opacity:1;background-color:rgb(127 29 29/var(--tw-bg-opacity,1))}.bg-red-900\/50{background-color:rgba(127,29,29,.5)}.bg-transparent{background-color:transparent}.bg-white\/20{background-color:hsla(0,0%,100%,.2)}.bg-yellow-400{--tw-bg-opacity:1;background-color:rgb(250 204 21/var(--tw-bg-opacity,1))}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity,1))}.bg-yellow-500\/10{background-color:rgba(234,179,8,.1)}.bg-yellow-500\/20{background-color:rgba(234,179,8,.2)}.bg-yellow-500\/90{background-color:rgba(234,179,8,.9)}.bg-opacity-50{--tw-bg-opacity:0.5}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-\[\#1f1f1f\]\/90{--tw-gradient-from:rgba(31,31,31,.9) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,31,31,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-gray-800\/20{--tw-gradient-from:rgba(31,41,55,.2) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,41,55,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-orange-500{--tw-gradient-from:#f97316 var(--tw-gradient-from-position);--tw-gradient-to:rgba(249,115,22,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-transparent{--tw-gradient-from:transparent var(--tw-gradient-from-position);--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.via-zinc-700{--tw-gradient-to:rgba(63,63,70,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#3f3f46 var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-gray-900\/20{--tw-gradient-to:rgba(17,24,39,.2) var(--tw-gradient-to-position)}.to-orange-600{--tw-gradient-to:#ea580c var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.fill-blue-500{fill:#3b82f6}.fill-white{fill:#fff}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-14{padding-top:3.5rem;padding-bottom:3.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pr-2{padding-right:.5rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[3rem\]{font-size:3rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.lowercase{text-transform:lowercase}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-blue-200{--tw-text-opacity:1;color:rgb(191 219 254/var(--tw-text-opacity,1))}.text-blue-300{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity,1))}.text-cyan-300{--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-green-200{--tw-text-opacity:1;color:rgb(187 247 208/var(--tw-text-opacity,1))}.text-green-300{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-orange-200{--tw-text-opacity:1;color:rgb(254 215 170/var(--tw-text-opacity,1))}.text-orange-300{--tw-text-opacity:1;color:rgb(253 186 116/var(--tw-text-opacity,1))}.text-orange-400{--tw-text-opacity:1;color:rgb(251 146 60/var(--tw-text-opacity,1))}.text-orange-500{--tw-text-opacity:1;color:rgb(249 115 22/var(--tw-text-opacity,1))}.text-purple-400{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity,1))}.text-purple-500{--tw-text-opacity:1;color:rgb(168 85 247/var(--tw-text-opacity,1))}.text-red-200{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity,1))}.text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/10{color:hsla(0,0%,100%,.1)}.text-yellow-200{--tw-text-opacity:1;color:rgb(254 240 138/var(--tw-text-opacity,1))}.text-yellow-300{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity,1))}.text-zinc-600{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-90{opacity:.9}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px)}.backdrop-blur-md,.backdrop-blur-sm{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.\[checksum\:4\]{checksum:4}.\[data\:variable\]{data:variable}.\[name\:4\]{name:4}.\[size\:4\]{size:4}.hover\:bg-\[rgb\(20_20_20_\/30\%\)\]:hover{background-color:hsla(0,0%,8%,.3)}.hover\:bg-blue-500\/20:hover{background-color:rgba(59,130,246,.2)}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity,1))}.hover\:bg-emerald-600:hover{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.hover\:bg-gray-500:hover{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity,1))}.hover\:bg-gray-600:hover{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity,1))}.hover\:bg-gray-600\/30:hover{background-color:rgba(75,85,99,.3)}.hover\:bg-green-500\/20:hover{background-color:rgba(34,197,94,.2)}.hover\:bg-green-500\/30:hover{background-color:rgba(34,197,94,.3)}.hover\:bg-green-600\/30:hover{background-color:rgba(22,163,74,.3)}.hover\:bg-green-600\/40:hover{background-color:rgba(22,163,74,.4)}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity,1))}.hover\:bg-orange-500\/20:hover{background-color:rgba(249,115,22,.2)}.hover\:bg-orange-500\/40:hover{background-color:rgba(249,115,22,.4)}.hover\:bg-orange-600:hover{--tw-bg-opacity:1;background-color:rgb(234 88 12/var(--tw-bg-opacity,1))}.hover\:bg-purple-500:hover{--tw-bg-opacity:1;background-color:rgb(168 85 247/var(--tw-bg-opacity,1))}.hover\:bg-purple-500\/20:hover{background-color:rgba(168,85,247,.2)}.hover\:bg-purple-700:hover{--tw-bg-opacity:1;background-color:rgb(126 34 206/var(--tw-bg-opacity,1))}.hover\:bg-red-500\/20:hover{background-color:rgba(239,68,68,.2)}.hover\:bg-red-500\/30:hover{background-color:rgba(239,68,68,.3)}.hover\:bg-red-600:hover{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.hover\:bg-red-600\/40:hover{background-color:rgba(220,38,38,.4)}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity,1))}.hover\:bg-white\/30:hover{background-color:hsla(0,0%,100%,.3)}.hover\:bg-yellow-600\/40:hover{background-color:rgba(202,138,4,.4)}.hover\:from-orange-600:hover{--tw-gradient-from:#ea580c var(--tw-gradient-from-position);--tw-gradient-to:rgba(234,88,12,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.hover\:to-orange-700:hover{--tw-gradient-to:#c2410c var(--tw-gradient-to-position)}.hover\:text-gray-300:hover{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.hover\:text-green-300:hover{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:opacity-80:hover{opacity:.8}.focus\:border-green-500\/40:focus{border-color:rgba(34,197,94,.4)}.focus\:border-orange-500\/40:focus{border-color:rgba(249,115,22,.4)}.focus\:border-purple-500\/40:focus{border-color:rgba(168,85,247,.4)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-gray-600:disabled{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity,1))}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.group:hover .group-hover\:scale-105,.group:hover .group-hover\:scale-110{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:640px){.sm\:mb-0{margin-bottom:0}.sm\:mb-3{margin-bottom:.75rem}.sm\:mr-2{margin-right:.5rem}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:h-10{height:2.5rem}.sm\:h-12{height:3rem}.sm\:w-10{width:2.5rem}.sm\:w-12{width:3rem}.sm\:w-\[24rem\]{width:24rem}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-4{gap:1rem}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.sm\:p-4{padding:1rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:px-3{padding-left:.75rem;padding-right:.75rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-base{font-size:1rem;line-height:1.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}.sm\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:768px){.md\:flex{display:flex}.md\:hidden{display:none}.md\:h-32{height:8rem}.md\:h-\[32rem\]{height:32rem}.md\:w-\[28rem\]{width:28rem}.md\:w-auto{width:auto}.md\:w-px{width:1px}.md\:flex-none{flex:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:flex-col{flex-direction:column}.md\:bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}}@media (min-width:1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:block{display:block}.lg\:w-\[32rem\]{width:32rem}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}}
\ No newline at end of file
diff --git a/dist/app-boot.js b/dist/app-boot.js
index 11b6a6a..a80f5e4 100644
--- a/dist/app-boot.js
+++ b/dist/app-boot.js
@@ -1,3 +1,618 @@
+var __create = Object.create;
+var __defProp = Object.defineProperty;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __getProtoOf = Object.getPrototypeOf;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __commonJS = (cb, mod) => function __require() {
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
+};
+var __copyProps = (to, from, except, desc) => {
+ if (from && typeof from === "object" || typeof from === "function") {
+ for (let key of __getOwnPropNames(from))
+ if (!__hasOwnProp.call(to, key) && key !== except)
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
+ }
+ return to;
+};
+var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
+ // If the importer is in node compatibility mode or this is not an ESM
+ // file that has been converted to a CommonJS file using a Babel-
+ // compatible transform (i.e. "__esModule" has not been set), then set
+ // "default" to the CommonJS "module.exports" for node compatibility.
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
+ mod
+));
+
+// src/transfer/BluetoothKeyTransfer.js
+var require_BluetoothKeyTransfer = __commonJS({
+ "src/transfer/BluetoothKeyTransfer.js"(exports, module) {
+ var BluetoothKeyTransfer3 = class {
+ constructor(webrtcManager, onStatusChange, onKeyReceived, onError, onAutoConnection) {
+ this.webrtcManager = webrtcManager;
+ this.onStatusChange = onStatusChange;
+ this.onKeyReceived = onKeyReceived;
+ this.onError = onError;
+ this.onAutoConnection = onAutoConnection;
+ this.isSupported = false;
+ this.isAvailable = false;
+ this.isScanning = false;
+ this.isAdvertising = false;
+ this.connectedDevices = /* @__PURE__ */ new Map();
+ this.advertisingData = null;
+ this.SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
+ this.TX_CHARACTERISTIC_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
+ this.RX_CHARACTERISTIC_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
+ this.PROTOCOL_VERSION = "1.0";
+ this.MAX_CHUNK_SIZE = 20;
+ this.TRANSFER_TIMEOUT = 3e4;
+ this.init();
+ }
+ async init() {
+ try {
+ if (!navigator.bluetooth) {
+ this.log("warn", "Bluetooth API not supported in this browser");
+ return;
+ }
+ this.isSupported = true;
+ const available = await navigator.bluetooth.getAvailability();
+ this.isAvailable = available;
+ if (!available) {
+ this.log("warn", "Bluetooth is not available on this device");
+ return;
+ }
+ this.log("info", "Bluetooth Key Transfer initialized successfully");
+ this.onStatusChange?.("bluetooth_ready", { supported: true, available: true });
+ } catch (error) {
+ this.log("error", "Failed to initialize Bluetooth Key Transfer", error);
+ this.onError?.(error);
+ }
+ }
+ // ============================================
+ // PUBLIC METHODS
+ // ============================================
+ /**
+ * Start advertising this device for key exchange
+ */
+ async startAdvertising(publicKey, deviceName = "SecureBit Device") {
+ if (!this.isSupported || !this.isAvailable) {
+ throw new Error("Bluetooth not supported or available");
+ }
+ try {
+ this.log("info", "Starting Bluetooth advertising...");
+ this.onStatusChange?.("advertising_starting", { deviceName });
+ const keyData = await this.prepareKeyData(publicKey);
+ this.advertisingData = {
+ deviceName,
+ keyData,
+ timestamp: Date.now(),
+ protocolVersion: this.PROTOCOL_VERSION
+ };
+ const options = {
+ filters: [{
+ services: [this.SERVICE_UUID]
+ }],
+ optionalServices: [this.SERVICE_UUID]
+ };
+ this.isAdvertising = true;
+ this.onStatusChange?.("advertising_active", { deviceName });
+ this.log("info", "Bluetooth advertising started successfully");
+ return true;
+ } catch (error) {
+ this.log("error", "Failed to start Bluetooth advertising", error);
+ this.isAdvertising = false;
+ this.onError?.(error);
+ throw error;
+ }
+ }
+ /**
+ * Stop advertising
+ */
+ async stopAdvertising() {
+ try {
+ this.isAdvertising = false;
+ this.advertisingData = null;
+ this.onStatusChange?.("advertising_stopped");
+ this.log("info", "Bluetooth advertising stopped");
+ } catch (error) {
+ this.log("error", "Failed to stop advertising", error);
+ }
+ }
+ /**
+ * Start scanning for nearby devices
+ */
+ async startScanning() {
+ if (!this.isSupported || !this.isAvailable) {
+ throw new Error("Bluetooth not supported or available");
+ }
+ try {
+ this.log("info", "Starting Bluetooth device scan...");
+ this.onStatusChange?.("scanning_starting");
+ const options = {
+ filters: [{
+ services: [this.SERVICE_UUID]
+ }],
+ optionalServices: [this.SERVICE_UUID]
+ };
+ this.isScanning = true;
+ this.onStatusChange?.("scanning_active");
+ const device = await navigator.bluetooth.requestDevice(options);
+ if (device) {
+ this.log("info", "Device selected:", device.name);
+ await this.connectToDevice(device);
+ }
+ } catch (error) {
+ this.log("error", "Failed to start scanning", error);
+ this.isScanning = false;
+ this.onError?.(error);
+ throw error;
+ }
+ }
+ /**
+ * Stop scanning
+ */
+ async stopScanning() {
+ try {
+ this.isScanning = false;
+ this.onStatusChange?.("scanning_stopped");
+ this.log("info", "Bluetooth scanning stopped");
+ } catch (error) {
+ this.log("error", "Failed to stop scanning", error);
+ }
+ }
+ /**
+ * Send public key to connected device
+ */
+ async sendPublicKey(publicKey, deviceId) {
+ try {
+ const device = this.connectedDevices.get(deviceId);
+ if (!device) {
+ throw new Error("Device not connected");
+ }
+ this.log("info", "Sending public key to device:", deviceId);
+ this.onStatusChange?.("key_sending", { deviceId });
+ const keyData = await this.prepareKeyData(publicKey);
+ await this.sendData(keyData, device);
+ this.onStatusChange?.("key_sent", { deviceId });
+ this.log("info", "Public key sent successfully");
+ } catch (error) {
+ this.log("error", "Failed to send public key", error);
+ this.onError?.(error);
+ throw error;
+ }
+ }
+ /**
+ * Start automatic connection process (offer → answer → verification)
+ */
+ async startAutoConnection(deviceId) {
+ try {
+ this.log("info", "Starting automatic connection process");
+ this.onStatusChange?.("auto_connection_starting", { deviceId });
+ if (!this.webrtcManager) {
+ throw new Error("WebRTC Manager not available");
+ }
+ this.onStatusChange?.("creating_offer", { deviceId });
+ const offer = await this.webrtcManager.createSecureOffer();
+ await this.sendConnectionData(offer, deviceId, "offer");
+ this.onStatusChange?.("offer_sent", { deviceId });
+ this.onStatusChange?.("waiting_for_answer", { deviceId });
+ const answer = await this.waitForConnectionData(deviceId, "answer");
+ this.onStatusChange?.("processing_answer", { deviceId });
+ await this.webrtcManager.createSecureAnswer(answer);
+ this.onStatusChange?.("waiting_for_verification", { deviceId });
+ const verification = await this.waitForConnectionData(deviceId, "verification");
+ this.onStatusChange?.("completing_connection", { deviceId });
+ await this.completeConnection(verification, deviceId);
+ this.onStatusChange?.("auto_connection_complete", { deviceId });
+ this.log("info", "Automatic connection completed successfully");
+ } catch (error) {
+ this.log("error", "Automatic connection failed", error);
+ this.onStatusChange?.("auto_connection_failed", { deviceId, error: error.message });
+ this.onError?.(error);
+ throw error;
+ }
+ }
+ /**
+ * Start automatic connection as responder (wait for offer → create answer → send verification)
+ */
+ async startAutoConnectionAsResponder(deviceId) {
+ try {
+ this.log("info", "Starting automatic connection as responder");
+ this.onStatusChange?.("auto_connection_responder_starting", { deviceId });
+ if (!this.webrtcManager) {
+ throw new Error("WebRTC Manager not available");
+ }
+ this.onStatusChange?.("waiting_for_offer", { deviceId });
+ const offer = await this.waitForConnectionData(deviceId, "offer");
+ this.onStatusChange?.("creating_answer", { deviceId });
+ const answer = await this.webrtcManager.createSecureAnswer(offer);
+ await this.sendConnectionData(answer, deviceId, "answer");
+ this.onStatusChange?.("answer_sent", { deviceId });
+ this.onStatusChange?.("sending_verification", { deviceId });
+ const verification = await this.createVerificationData();
+ await this.sendConnectionData(verification, deviceId, "verification");
+ this.onStatusChange?.("auto_connection_responder_complete", { deviceId });
+ this.log("info", "Automatic connection as responder completed successfully");
+ } catch (error) {
+ this.log("error", "Automatic connection as responder failed", error);
+ this.onStatusChange?.("auto_connection_responder_failed", { deviceId, error: error.message });
+ this.onError?.(error);
+ throw error;
+ }
+ }
+ // ============================================
+ // PRIVATE METHODS
+ // ============================================
+ /**
+ * Connect to a discovered device
+ */
+ async connectToDevice(device) {
+ try {
+ this.log("info", "Connecting to device:", device.name);
+ this.onStatusChange?.("connecting", { deviceName: device.name });
+ const server = await device.gatt.connect();
+ const service = await server.getPrimaryService(this.SERVICE_UUID);
+ const txCharacteristic = await service.getCharacteristic(this.TX_CHARACTERISTIC_UUID);
+ const rxCharacteristic = await service.getCharacteristic(this.RX_CHARACTERISTIC_UUID);
+ rxCharacteristic.addEventListener("characteristicvaluechanged", (event) => {
+ this.handleReceivedData(event, device.id);
+ });
+ await rxCharacteristic.startNotifications();
+ this.connectedDevices.set(device.id, {
+ device,
+ server,
+ service,
+ txCharacteristic,
+ rxCharacteristic,
+ connected: true
+ });
+ this.onStatusChange?.("connected", { deviceId: device.id, deviceName: device.name });
+ this.log("info", "Successfully connected to device:", device.name);
+ } catch (error) {
+ this.log("error", "Failed to connect to device", error);
+ this.onError?.(error);
+ throw error;
+ }
+ }
+ /**
+ * Send data to connected device
+ */
+ async sendData(data, device) {
+ try {
+ const { txCharacteristic } = device;
+ const dataString = JSON.stringify(data);
+ const chunks = this.chunkString(dataString, this.MAX_CHUNK_SIZE);
+ for (let i = 0; i < chunks.length; i++) {
+ const chunk = chunks[i];
+ const chunkData = new TextEncoder().encode(chunk);
+ await txCharacteristic.writeValue(chunkData);
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ }
+ this.log("info", `Sent ${chunks.length} chunks to device`);
+ } catch (error) {
+ this.log("error", "Failed to send data", error);
+ throw error;
+ }
+ }
+ /**
+ * Handle received data from device
+ */
+ async handleReceivedData(event, deviceId) {
+ try {
+ const value = event.target.value;
+ const data = new TextDecoder().decode(value);
+ try {
+ const connectionData = JSON.parse(data);
+ if (connectionData.type && ["offer", "answer", "verification"].includes(connectionData.type)) {
+ this.handleConnectionData(connectionData, deviceId);
+ return;
+ }
+ } catch (e) {
+ }
+ const keyData = await this.processReceivedData(data, deviceId);
+ if (keyData) {
+ this.onKeyReceived?.(keyData, deviceId);
+ }
+ } catch (error) {
+ this.log("error", "Failed to handle received data", error);
+ this.onError?.(error);
+ }
+ }
+ /**
+ * Handle connection data (offer, answer, verification)
+ */
+ async handleConnectionData(connectionData, deviceId) {
+ try {
+ this.log("info", `Received ${connectionData.type} from device:`, deviceId);
+ if (!this.connectionDataBuffer) {
+ this.connectionDataBuffer = /* @__PURE__ */ new Map();
+ }
+ if (!this.connectionDataBuffer.has(deviceId)) {
+ this.connectionDataBuffer.set(deviceId, /* @__PURE__ */ new Map());
+ }
+ this.connectionDataBuffer.get(deviceId).set(connectionData.type, connectionData);
+ this.onStatusChange?.(`${connectionData.type}_received`, { deviceId, data: connectionData });
+ } catch (error) {
+ this.log("error", "Failed to handle connection data", error);
+ this.onError?.(error);
+ }
+ }
+ /**
+ * Prepare key data for transmission
+ */
+ async prepareKeyData(publicKey) {
+ try {
+ const exportedKey = await crypto.subtle.exportKey("spki", publicKey);
+ const keyArray = new Uint8Array(exportedKey);
+ const payload = {
+ type: "public_key",
+ key: Array.from(keyArray),
+ timestamp: Date.now(),
+ protocolVersion: this.PROTOCOL_VERSION,
+ deviceId: await this.getDeviceId()
+ };
+ const signature = await this.signPayload(payload);
+ payload.signature = signature;
+ return payload;
+ } catch (error) {
+ this.log("error", "Failed to prepare key data", error);
+ throw error;
+ }
+ }
+ /**
+ * Process received key data
+ */
+ async processReceivedData(data, deviceId) {
+ try {
+ const payload = JSON.parse(data);
+ if (!this.validatePayload(payload)) {
+ throw new Error("Invalid payload received");
+ }
+ if (!await this.verifyPayload(payload)) {
+ throw new Error("Payload signature verification failed");
+ }
+ const publicKey = await crypto.subtle.importKey(
+ "spki",
+ new Uint8Array(payload.key),
+ { name: "ECDH", namedCurve: "P-384" },
+ false,
+ []
+ );
+ this.log("info", "Successfully processed received key data");
+ return {
+ publicKey,
+ deviceId,
+ timestamp: payload.timestamp,
+ protocolVersion: payload.protocolVersion
+ };
+ } catch (error) {
+ this.log("error", "Failed to process received data", error);
+ throw error;
+ }
+ }
+ /**
+ * Sign payload for integrity
+ */
+ async signPayload(payload) {
+ try {
+ if (this.webrtcManager && this.webrtcManager.signingKeyPair) {
+ const data2 = new TextEncoder().encode(JSON.stringify(payload));
+ const signature = await crypto.subtle.sign(
+ { name: "ECDSA", hash: "SHA-384" },
+ this.webrtcManager.signingKeyPair.privateKey,
+ data2
+ );
+ return Array.from(new Uint8Array(signature));
+ }
+ const data = new TextEncoder().encode(JSON.stringify(payload));
+ const hash = await crypto.subtle.digest("SHA-256", data);
+ return Array.from(new Uint8Array(hash));
+ } catch (error) {
+ this.log("error", "Failed to sign payload", error);
+ throw error;
+ }
+ }
+ /**
+ * Verify payload signature
+ */
+ async verifyPayload(payload) {
+ try {
+ const { signature, ...payloadWithoutSig } = payload;
+ if (this.webrtcManager && this.webrtcManager.signingKeyPair) {
+ const data2 = new TextEncoder().encode(JSON.stringify(payloadWithoutSig));
+ const isValid = await crypto.subtle.verify(
+ { name: "ECDSA", hash: "SHA-384" },
+ this.webrtcManager.signingKeyPair.publicKey,
+ new Uint8Array(signature),
+ data2
+ );
+ return isValid;
+ }
+ const data = new TextEncoder().encode(JSON.stringify(payloadWithoutSig));
+ const hash = await crypto.subtle.digest("SHA-256", data);
+ const expectedHash = Array.from(new Uint8Array(hash));
+ return JSON.stringify(signature) === JSON.stringify(expectedHash);
+ } catch (error) {
+ this.log("error", "Failed to verify payload", error);
+ return false;
+ }
+ }
+ /**
+ * Validate received payload
+ */
+ validatePayload(payload) {
+ return payload && payload.type === "public_key" && payload.key && Array.isArray(payload.key) && payload.timestamp && payload.protocolVersion && payload.signature && Array.isArray(payload.signature);
+ }
+ /**
+ * Get unique device ID
+ */
+ async getDeviceId() {
+ try {
+ if (navigator.userAgentData && navigator.userAgentData.getHighEntropyValues) {
+ const values = await navigator.userAgentData.getHighEntropyValues(["model"]);
+ return values.model || "unknown-device";
+ }
+ const userAgent = navigator.userAgent;
+ const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(userAgent));
+ return Array.from(new Uint8Array(hash)).slice(0, 8).map((b) => b.toString(16).padStart(2, "0")).join("");
+ } catch (error) {
+ return "unknown-device";
+ }
+ }
+ /**
+ * Send connection data (offer, answer, verification)
+ */
+ async sendConnectionData(data, deviceId, type) {
+ try {
+ const device = this.connectedDevices.get(deviceId);
+ if (!device) {
+ throw new Error("Device not connected");
+ }
+ const connectionData = {
+ type,
+ data,
+ timestamp: Date.now(),
+ protocolVersion: this.PROTOCOL_VERSION
+ };
+ await this.sendData(connectionData, device);
+ this.log("info", `Sent ${type} to device:`, deviceId);
+ } catch (error) {
+ this.log("error", `Failed to send ${type}`, error);
+ throw error;
+ }
+ }
+ /**
+ * Wait for specific connection data type
+ */
+ async waitForConnectionData(deviceId, type, timeout = 3e4) {
+ return new Promise((resolve, reject) => {
+ const timeoutId = setTimeout(() => {
+ reject(new Error(`Timeout waiting for ${type}`));
+ }, timeout);
+ const checkForData = () => {
+ if (this.connectionDataBuffer && this.connectionDataBuffer.has(deviceId) && this.connectionDataBuffer.get(deviceId).has(type)) {
+ clearTimeout(timeoutId);
+ const data = this.connectionDataBuffer.get(deviceId).get(type);
+ this.connectionDataBuffer.get(deviceId).delete(type);
+ resolve(data.data);
+ } else {
+ setTimeout(checkForData, 100);
+ }
+ };
+ checkForData();
+ });
+ }
+ /**
+ * Create verification data
+ */
+ async createVerificationData() {
+ try {
+ if (!this.webrtcManager || !this.webrtcManager.keyFingerprint) {
+ throw new Error("WebRTC Manager or key fingerprint not available");
+ }
+ return {
+ fingerprint: this.webrtcManager.keyFingerprint,
+ verificationCode: this.webrtcManager.verificationCode || "auto-verified",
+ timestamp: Date.now()
+ };
+ } catch (error) {
+ this.log("error", "Failed to create verification data", error);
+ throw error;
+ }
+ }
+ /**
+ * Complete connection process
+ */
+ async completeConnection(verification, deviceId) {
+ try {
+ if (verification.fingerprint && this.webrtcManager.keyFingerprint) {
+ if (verification.fingerprint !== this.webrtcManager.keyFingerprint) {
+ throw new Error("Key fingerprint mismatch");
+ }
+ }
+ this.onAutoConnection?.({
+ deviceId,
+ fingerprint: verification.fingerprint,
+ verificationCode: verification.verificationCode,
+ timestamp: Date.now()
+ });
+ this.log("info", "Connection completed successfully");
+ } catch (error) {
+ this.log("error", "Failed to complete connection", error);
+ throw error;
+ }
+ }
+ /**
+ * Split string into chunks
+ */
+ chunkString(str, chunkSize) {
+ const chunks = [];
+ for (let i = 0; i < str.length; i += chunkSize) {
+ chunks.push(str.slice(i, i + chunkSize));
+ }
+ return chunks;
+ }
+ /**
+ * Logging utility
+ */
+ log(level, message, data = null) {
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
+ const logMessage = `[BluetoothKeyTransfer ${timestamp}] ${message}`;
+ switch (level) {
+ case "error":
+ console.error(logMessage, data);
+ break;
+ case "warn":
+ console.warn(logMessage, data);
+ break;
+ case "info":
+ console.info(logMessage, data);
+ break;
+ default:
+ console.log(logMessage, data);
+ }
+ }
+ // ============================================
+ // CLEANUP METHODS
+ // ============================================
+ /**
+ * Disconnect from all devices
+ */
+ async disconnectAll() {
+ try {
+ for (const [deviceId, device] of this.connectedDevices) {
+ if (device.connected && device.server) {
+ device.server.disconnect();
+ }
+ }
+ this.connectedDevices.clear();
+ this.log("info", "Disconnected from all devices");
+ } catch (error) {
+ this.log("error", "Failed to disconnect devices", error);
+ }
+ }
+ /**
+ * Cleanup resources
+ */
+ async cleanup() {
+ try {
+ await this.stopAdvertising();
+ await this.stopScanning();
+ await this.disconnectAll();
+ this.log("info", "Bluetooth Key Transfer cleaned up");
+ } catch (error) {
+ this.log("error", "Failed to cleanup Bluetooth Key Transfer", error);
+ }
+ }
+ };
+ if (typeof module !== "undefined" && module.exports) {
+ module.exports = BluetoothKeyTransfer3;
+ } else if (typeof window !== "undefined") {
+ window.BluetoothKeyTransfer = BluetoothKeyTransfer3;
+ }
+ }
+});
+
// src/crypto/EnhancedSecureCryptoUtils.js
var EnhancedSecureCryptoUtils = class _EnhancedSecureCryptoUtils {
static _keyMetadata = /* @__PURE__ */ new WeakMap();
@@ -14109,6 +14724,9 @@ var SecureMasterKeyManager = class {
}
};
+// src/scripts/app-boot.js
+var import_BluetoothKeyTransfer = __toESM(require_BluetoothKeyTransfer());
+
// src/components/ui/Header.jsx
var EnhancedMinimalHeader = ({
status,
@@ -14121,6 +14739,9 @@ var EnhancedMinimalHeader = ({
}) => {
const [realSecurityLevel, setRealSecurityLevel] = React.useState(null);
const [lastSecurityUpdate, setLastSecurityUpdate] = React.useState(0);
+ const [hasActiveSession, setHasActiveSession] = React.useState(false);
+ const [currentTimeLeft, setCurrentTimeLeft] = React.useState(0);
+ const [sessionType, setSessionType] = React.useState("unknown");
React.useEffect(() => {
let isUpdating = false;
let lastUpdateAttempt = 0;
@@ -14223,7 +14844,7 @@ var EnhancedMinimalHeader = ({
setHasActiveSession(true);
setCurrentTimeLeft(0);
setSessionType("premium");
- }, [sessionTimeLeft]);
+ }, []);
React.useEffect(() => {
const handleForceUpdate = (event) => {
setHasActiveSession(true);
@@ -16063,10 +16684,467 @@ var FileTransferComponent = ({ webrtcManager, isConnected }) => {
};
window.FileTransferComponent = FileTransferComponent;
+// src/components/ui/BluetoothKeyTransfer.jsx
+var BluetoothKeyTransfer = ({
+ webrtcManager,
+ onKeyReceived,
+ onStatusChange,
+ onAutoConnection,
+ isVisible = false,
+ onClose
+}) => {
+ const [bluetoothManager, setBluetoothManager] = React.useState(null);
+ const [isSupported, setIsSupported] = React.useState(false);
+ const [isAvailable, setIsAvailable] = React.useState(false);
+ const [isScanning, setIsScanning] = React.useState(false);
+ const [isAdvertising, setIsAdvertising] = React.useState(false);
+ const [connectedDevices, setConnectedDevices] = React.useState([]);
+ const [status, setStatus] = React.useState("idle");
+ const [error, setError] = React.useState(null);
+ const [logs, setLogs] = React.useState([]);
+ React.useEffect(() => {
+ if (isVisible && !bluetoothManager) {
+ initializeBluetooth();
+ }
+ }, [isVisible]);
+ React.useEffect(() => {
+ return () => {
+ if (bluetoothManager) {
+ bluetoothManager.cleanup();
+ }
+ };
+ }, [bluetoothManager]);
+ const initializeBluetooth = async () => {
+ try {
+ const manager = new window.BluetoothKeyTransfer(
+ webrtcManager,
+ handleStatusChange,
+ handleKeyReceived,
+ handleError,
+ handleAutoConnection
+ );
+ setBluetoothManager(manager);
+ setTimeout(() => {
+ setIsSupported(manager.isSupported);
+ setIsAvailable(manager.isAvailable);
+ }, 100);
+ } catch (error2) {
+ console.error("Failed to initialize Bluetooth manager:", error2);
+ setError("Failed to initialize Bluetooth: " + error2.message);
+ }
+ };
+ const handleStatusChange = (statusType, data) => {
+ setStatus(statusType);
+ addLog(`Status: ${statusType}`, data);
+ switch (statusType) {
+ case "bluetooth_ready":
+ setIsSupported(data.supported);
+ setIsAvailable(data.available);
+ break;
+ case "scanning_active":
+ setIsScanning(true);
+ break;
+ case "scanning_stopped":
+ setIsScanning(false);
+ break;
+ case "advertising_active":
+ setIsAdvertising(true);
+ break;
+ case "advertising_stopped":
+ setIsAdvertising(false);
+ break;
+ case "connected":
+ setConnectedDevices((prev) => [...prev, {
+ id: data.deviceId,
+ name: data.deviceName,
+ connected: true
+ }]);
+ break;
+ }
+ onStatusChange?.(statusType, data);
+ };
+ const handleKeyReceived = (keyData, deviceId) => {
+ addLog("Key received from device", { deviceId });
+ onKeyReceived?.(keyData, deviceId);
+ };
+ const handleError = (error2) => {
+ console.error("Bluetooth error:", error2);
+ setError(error2.message);
+ addLog("Error", error2.message);
+ };
+ const handleAutoConnection = (connectionData) => {
+ console.log("Auto connection completed:", connectionData);
+ addLog("Auto Connection Completed", connectionData);
+ onAutoConnection?.(connectionData);
+ };
+ const addLog = (message, data = null) => {
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
+ const logEntry = {
+ timestamp,
+ message,
+ data: data ? JSON.stringify(data, null, 2) : null
+ };
+ setLogs((prev) => [...prev.slice(-9), logEntry]);
+ };
+ const startScanning = async () => {
+ try {
+ setError(null);
+ await bluetoothManager.startScanning();
+ } catch (error2) {
+ setError("Failed to start scanning: " + error2.message);
+ }
+ };
+ const stopScanning = async () => {
+ try {
+ await bluetoothManager.stopScanning();
+ } catch (error2) {
+ setError("Failed to stop scanning: " + error2.message);
+ }
+ };
+ const startAdvertising = async () => {
+ try {
+ setError(null);
+ if (!webrtcManager || !webrtcManager.ecdhKeyPair) {
+ throw new Error("No public key available for advertising");
+ }
+ await bluetoothManager.startAdvertising(
+ webrtcManager.ecdhKeyPair.publicKey,
+ "SecureBit Device"
+ );
+ } catch (error2) {
+ setError("Failed to start advertising: " + error2.message);
+ }
+ };
+ const stopAdvertising = async () => {
+ try {
+ await bluetoothManager.stopAdvertising();
+ } catch (error2) {
+ setError("Failed to stop advertising: " + error2.message);
+ }
+ };
+ const sendPublicKey = async (deviceId) => {
+ try {
+ setError(null);
+ if (!webrtcManager || !webrtcManager.ecdhKeyPair) {
+ throw new Error("No public key available for sending");
+ }
+ await bluetoothManager.sendPublicKey(
+ webrtcManager.ecdhKeyPair.publicKey,
+ deviceId
+ );
+ } catch (error2) {
+ setError("Failed to send public key: " + error2.message);
+ }
+ };
+ const clearLogs = () => {
+ setLogs([]);
+ };
+ const startAutoConnection = async (deviceId) => {
+ try {
+ setError(null);
+ await bluetoothManager.startAutoConnection(deviceId);
+ } catch (error2) {
+ setError("Failed to start auto connection: " + error2.message);
+ }
+ };
+ const startAutoConnectionAsResponder = async (deviceId) => {
+ try {
+ setError(null);
+ await bluetoothManager.startAutoConnectionAsResponder(deviceId);
+ } catch (error2) {
+ setError("Failed to start auto connection as responder: " + error2.message);
+ }
+ };
+ if (!isVisible) return null;
+ return React.createElement("div", {
+ className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
+ }, [
+ React.createElement("div", {
+ key: "modal",
+ className: "bg-gray-900 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden"
+ }, [
+ // Header
+ React.createElement("div", {
+ key: "header",
+ className: "flex items-center justify-between p-6 border-b border-gray-700"
+ }, [
+ React.createElement("div", {
+ key: "title",
+ className: "flex items-center space-x-3"
+ }, [
+ React.createElement("i", {
+ key: "icon",
+ className: "fas fa-bluetooth text-blue-400 text-xl"
+ }),
+ React.createElement("h2", {
+ key: "text",
+ className: "text-xl font-semibold text-white"
+ }, "Bluetooth Key Transfer")
+ ]),
+ React.createElement("button", {
+ key: "close",
+ onClick: onClose,
+ className: "text-gray-400 hover:text-white transition-colors"
+ }, [
+ React.createElement("i", {
+ className: "fas fa-times text-xl"
+ })
+ ])
+ ]),
+ // Content
+ React.createElement("div", {
+ key: "content",
+ className: "p-6 space-y-6 overflow-y-auto max-h-[calc(90vh-200px)]"
+ }, [
+ // Status Section
+ React.createElement("div", {
+ key: "status",
+ className: "space-y-4"
+ }, [
+ React.createElement("h3", {
+ key: "title",
+ className: "text-lg font-medium text-white"
+ }, "Bluetooth Status"),
+ React.createElement("div", {
+ key: "indicators",
+ className: "grid grid-cols-2 gap-4"
+ }, [
+ React.createElement("div", {
+ key: "support",
+ className: "flex items-center space-x-2"
+ }, [
+ React.createElement("div", {
+ className: `w-3 h-3 rounded-full ${isSupported ? "bg-green-500" : "bg-red-500"}`
+ }),
+ React.createElement("span", {
+ className: "text-sm text-gray-300"
+ }, "Bluetooth Supported")
+ ]),
+ React.createElement("div", {
+ key: "availability",
+ className: "flex items-center space-x-2"
+ }, [
+ React.createElement("div", {
+ className: `w-3 h-3 rounded-full ${isAvailable ? "bg-green-500" : "bg-red-500"}`
+ }),
+ React.createElement("span", {
+ className: "text-sm text-gray-300"
+ }, "Bluetooth Available")
+ ])
+ ])
+ ]),
+ // Controls Section
+ React.createElement("div", {
+ key: "controls",
+ className: "space-y-4"
+ }, [
+ React.createElement("h3", {
+ key: "title",
+ className: "text-lg font-medium text-white"
+ }, "Key Exchange"),
+ React.createElement("div", {
+ key: "buttons",
+ className: "grid grid-cols-1 sm:grid-cols-2 gap-4"
+ }, [
+ // Scanning Controls
+ React.createElement("div", {
+ key: "scanning",
+ className: "space-y-2"
+ }, [
+ React.createElement("h4", {
+ key: "title",
+ className: "text-sm font-medium text-gray-300"
+ }, "Discover Devices"),
+ React.createElement("button", {
+ key: "scan",
+ onClick: isScanning ? stopScanning : startScanning,
+ disabled: !isSupported || !isAvailable,
+ className: `w-full px-4 py-2 rounded-lg font-medium transition-colors ${isScanning ? "bg-red-600 hover:bg-red-700 text-white" : "bg-blue-600 hover:bg-blue-700 text-white disabled:bg-gray-600 disabled:cursor-not-allowed"}`
+ }, [
+ React.createElement("i", {
+ key: "icon",
+ className: `fas ${isScanning ? "fa-stop" : "fa-search"} mr-2`
+ }),
+ isScanning ? "Stop Scanning" : "Start Scanning"
+ ])
+ ]),
+ // Advertising Controls
+ React.createElement("div", {
+ key: "advertising",
+ className: "space-y-2"
+ }, [
+ React.createElement("h4", {
+ key: "title",
+ className: "text-sm font-medium text-gray-300"
+ }, "Share Your Key"),
+ React.createElement("button", {
+ key: "advertise",
+ onClick: isAdvertising ? stopAdvertising : startAdvertising,
+ disabled: !isSupported || !isAvailable,
+ className: `w-full px-4 py-2 rounded-lg font-medium transition-colors ${isAdvertising ? "bg-red-600 hover:bg-red-700 text-white" : "bg-green-600 hover:bg-green-700 text-white disabled:bg-gray-600 disabled:cursor-not-allowed"}`
+ }, [
+ React.createElement("i", {
+ key: "icon",
+ className: `fas ${isAdvertising ? "fa-stop" : "fa-broadcast-tower"} mr-2`
+ }),
+ isAdvertising ? "Stop Sharing" : "Start Sharing"
+ ])
+ ])
+ ])
+ ]),
+ // Connected Devices
+ connectedDevices.length > 0 && React.createElement("div", {
+ key: "devices",
+ className: "space-y-4"
+ }, [
+ React.createElement("h3", {
+ key: "title",
+ className: "text-lg font-medium text-white"
+ }, "Connected Devices"),
+ React.createElement("div", {
+ key: "list",
+ className: "space-y-2"
+ }, connectedDevices.map(
+ (device) => React.createElement("div", {
+ key: device.id,
+ className: "flex items-center justify-between p-3 bg-gray-800 rounded-lg"
+ }, [
+ React.createElement("div", {
+ key: "info",
+ className: "flex items-center space-x-3"
+ }, [
+ React.createElement("i", {
+ key: "icon",
+ className: "fas fa-mobile-alt text-blue-400"
+ }),
+ React.createElement("span", {
+ key: "name",
+ className: "text-white"
+ }, device.name)
+ ]),
+ React.createElement("div", {
+ key: "buttons",
+ className: "flex space-x-2"
+ }, [
+ React.createElement("button", {
+ key: "auto-connect",
+ onClick: () => startAutoConnection(device.id),
+ className: "px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-sm rounded transition-colors"
+ }, "Auto Connect"),
+ React.createElement("button", {
+ key: "auto-respond",
+ onClick: () => startAutoConnectionAsResponder(device.id),
+ className: "px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded transition-colors"
+ }, "Auto Respond"),
+ React.createElement("button", {
+ key: "send",
+ onClick: () => sendPublicKey(device.id),
+ className: "px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded transition-colors"
+ }, "Send Key")
+ ])
+ ])
+ ))
+ ]),
+ // Error Display
+ error && React.createElement("div", {
+ key: "error",
+ className: "p-4 bg-red-900 border border-red-700 rounded-lg"
+ }, [
+ React.createElement("div", {
+ key: "header",
+ className: "flex items-center space-x-2 mb-2"
+ }, [
+ React.createElement("i", {
+ key: "icon",
+ className: "fas fa-exclamation-triangle text-red-400"
+ }),
+ React.createElement("h4", {
+ key: "title",
+ className: "text-red-400 font-medium"
+ }, "Error")
+ ]),
+ React.createElement("p", {
+ key: "message",
+ className: "text-red-300 text-sm"
+ }, error)
+ ]),
+ // Logs Section
+ React.createElement("div", {
+ key: "logs",
+ className: "space-y-4"
+ }, [
+ React.createElement("div", {
+ key: "header",
+ className: "flex items-center justify-between"
+ }, [
+ React.createElement("h3", {
+ key: "title",
+ className: "text-lg font-medium text-white"
+ }, "Activity Log"),
+ React.createElement("button", {
+ key: "clear",
+ onClick: clearLogs,
+ className: "text-sm text-gray-400 hover:text-white transition-colors"
+ }, "Clear")
+ ]),
+ React.createElement(
+ "div",
+ {
+ key: "log-list",
+ className: "bg-gray-800 rounded-lg p-4 max-h-40 overflow-y-auto"
+ },
+ logs.length === 0 ? React.createElement("p", {
+ key: "empty",
+ className: "text-gray-400 text-sm text-center"
+ }, "No activity yet") : logs.map(
+ (log, index) => React.createElement("div", {
+ key: index,
+ className: "text-xs text-gray-300 mb-1"
+ }, [
+ React.createElement("span", {
+ key: "time",
+ className: "text-gray-500"
+ }, `[${log.timestamp}] `),
+ React.createElement("span", {
+ key: "message",
+ className: "text-gray-300"
+ }, log.message),
+ log.data && React.createElement("pre", {
+ key: "data",
+ className: "text-gray-400 mt-1 ml-4"
+ }, log.data)
+ ])
+ )
+ )
+ ])
+ ]),
+ // Footer
+ React.createElement("div", {
+ key: "footer",
+ className: "flex items-center justify-between p-6 border-t border-gray-700"
+ }, [
+ React.createElement("div", {
+ key: "info",
+ className: "text-sm text-gray-400"
+ }, "Bluetooth key exchange provides secure device-to-device communication"),
+ React.createElement("button", {
+ key: "close-footer",
+ onClick: onClose,
+ className: "px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors"
+ }, "Close")
+ ])
+ ])
+ ]);
+};
+if (typeof window !== "undefined") {
+ window.BluetoothKeyTransfer = BluetoothKeyTransfer;
+}
+
// src/scripts/app-boot.js
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
+window.BluetoothKeyTransfer = import_BluetoothKeyTransfer.default;
var start = () => {
if (typeof window.initializeApp === "function") {
window.initializeApp();
diff --git a/dist/app-boot.js.map b/dist/app-boot.js.map
index db5c09d..884c8c8 100644
--- a/dist/app-boot.js.map
+++ b/dist/app-boot.js.map
@@ -1,7 +1,7 @@
{
"version": 3,
- "sources": ["../src/crypto/EnhancedSecureCryptoUtils.js", "../src/transfer/EnhancedSecureFileTransfer.js", "../src/network/EnhancedSecureWebRTCManager.js", "../src/components/ui/Header.jsx", "../src/components/ui/DownloadApps.jsx", "../src/components/ui/UniqueFeatureSlider.jsx", "../src/components/ui/SecurityFeatures.jsx", "../src/components/ui/Testimonials.jsx", "../src/components/ui/ComparisonTable.jsx", "../src/components/ui/Roadmap.jsx", "../src/components/ui/FileTransfer.jsx", "../src/scripts/app-boot.js"],
- "sourcesContent": ["class EnhancedSecureCryptoUtils {\r\n\r\n static _keyMetadata = new WeakMap();\r\n \r\n // Initialize secure logging system after class definition\r\n\r\n // Utility to sort object keys for deterministic serialization\r\n static sortObjectKeys(obj) {\r\n if (typeof obj !== 'object' || obj === null) {\r\n return obj;\r\n }\r\n\r\n if (Array.isArray(obj)) {\r\n return obj.map(EnhancedSecureCryptoUtils.sortObjectKeys);\r\n }\r\n\r\n const sortedObj = {};\r\n Object.keys(obj).sort().forEach(key => {\r\n sortedObj[key] = EnhancedSecureCryptoUtils.sortObjectKeys(obj[key]);\r\n });\r\n return sortedObj;\r\n }\r\n\r\n // Utility to assert CryptoKey type and properties\r\n static assertCryptoKey(key, expectedName = null, expectedUsages = []) {\r\n if (!(key instanceof CryptoKey)) throw new Error('Expected CryptoKey');\r\n if (expectedName && key.algorithm?.name !== expectedName) {\r\n throw new Error(`Expected algorithm ${expectedName}, got ${key.algorithm?.name}`);\r\n }\r\n for (const u of expectedUsages) {\r\n if (!key.usages || !key.usages.includes(u)) {\r\n throw new Error(`Missing required key usage: ${u}`);\r\n }\r\n }\r\n }\r\n // Helper function to convert ArrayBuffer to Base64\r\n static arrayBufferToBase64(buffer) {\r\n let binary = '';\r\n const bytes = new Uint8Array(buffer);\r\n const len = bytes.byteLength;\r\n for (let i = 0; i < len; i++) {\r\n binary += String.fromCharCode(bytes[i]);\r\n }\r\n return btoa(binary);\r\n }\r\n\r\n // Helper function to convert Base64 to ArrayBuffer\r\n static base64ToArrayBuffer(base64) {\r\n try {\r\n // Validate input\r\n if (typeof base64 !== 'string' || !base64) {\r\n throw new Error('Invalid base64 input: must be a non-empty string');\r\n }\r\n\r\n // Remove any whitespace and validate base64 format\r\n const cleanBase64 = base64.trim();\r\n if (!/^[A-Za-z0-9+/]*={0,2}$/.test(cleanBase64)) {\r\n throw new Error('Invalid base64 format');\r\n }\r\n\r\n // Handle empty string case\r\n if (cleanBase64 === '') {\r\n return new ArrayBuffer(0);\r\n }\r\n\r\n const binaryString = atob(cleanBase64);\r\n const len = binaryString.length;\r\n const bytes = new Uint8Array(len);\r\n for (let i = 0; i < len; i++) {\r\n bytes[i] = binaryString.charCodeAt(i);\r\n }\r\n return bytes.buffer;\r\n } catch (error) {\r\n console.error('Base64 to ArrayBuffer conversion failed:', error.message);\r\n throw new Error(`Base64 conversion error: ${error.message}`);\r\n }\r\n }\r\n\r\n // Helper function to convert hex string to Uint8Array\r\n static hexToUint8Array(hexString) {\r\n try {\r\n if (!hexString || typeof hexString !== 'string') {\r\n throw new Error('Invalid hex string input: must be a non-empty string');\r\n }\r\n\r\n // Remove colons and spaces from hex string (e.g., \"aa:bb:cc\" -> \"aabbcc\")\r\n const cleanHex = hexString.replace(/:/g, '').replace(/\\s/g, '');\r\n \r\n // Validate hex format\r\n if (!/^[0-9a-fA-F]*$/.test(cleanHex)) {\r\n throw new Error('Invalid hex format: contains non-hex characters');\r\n }\r\n \r\n // Ensure even length\r\n if (cleanHex.length % 2 !== 0) {\r\n throw new Error('Invalid hex format: odd length');\r\n }\r\n\r\n // Convert hex string to bytes\r\n const bytes = new Uint8Array(cleanHex.length / 2);\r\n for (let i = 0; i < cleanHex.length; i += 2) {\r\n bytes[i / 2] = parseInt(cleanHex.substr(i, 2), 16);\r\n }\r\n \r\n return bytes;\r\n } catch (error) {\r\n console.error('Hex to Uint8Array conversion failed:', error.message);\r\n throw new Error(`Hex conversion error: ${error.message}`);\r\n }\r\n }\r\n\r\n static async encryptData(data, password) {\r\n try {\r\n const dataString = typeof data === 'string' ? data : JSON.stringify(data);\r\n const salt = crypto.getRandomValues(new Uint8Array(16));\r\n const encoder = new TextEncoder();\r\n const passwordBuffer = encoder.encode(password);\r\n\r\n const keyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n passwordBuffer,\r\n { name: 'PBKDF2' },\r\n false,\r\n ['deriveKey']\r\n );\r\n\r\n const key = await crypto.subtle.deriveKey(\r\n {\r\n name: 'PBKDF2',\r\n salt: salt,\r\n iterations: 100000,\r\n hash: 'SHA-256',\r\n },\r\n keyMaterial,\r\n { name: 'AES-GCM', length: 256 },\r\n false,\r\n ['encrypt']\r\n );\r\n\r\n const iv = crypto.getRandomValues(new Uint8Array(12));\r\n const dataBuffer = encoder.encode(dataString);\r\n const encrypted = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv: iv },\r\n key,\r\n dataBuffer\r\n );\r\n\r\n const encryptedPackage = {\r\n version: '1.0',\r\n salt: Array.from(salt),\r\n iv: Array.from(iv),\r\n data: Array.from(new Uint8Array(encrypted)),\r\n timestamp: Date.now(),\r\n };\r\n\r\n const packageString = JSON.stringify(encryptedPackage);\r\n return EnhancedSecureCryptoUtils.arrayBufferToBase64(new TextEncoder().encode(packageString).buffer);\r\n\r\n } catch (error) {\r\n console.error('Encryption failed:', error.message);\r\n throw new Error(`Encryption error: ${error.message}`);\r\n }\r\n }\r\n\r\n static async decryptData(encryptedData, password) {\r\n try {\r\n const packageBuffer = EnhancedSecureCryptoUtils.base64ToArrayBuffer(encryptedData);\r\n const packageString = new TextDecoder().decode(packageBuffer);\r\n const encryptedPackage = JSON.parse(packageString);\r\n\r\n if (!encryptedPackage.version || !encryptedPackage.salt || !encryptedPackage.iv || !encryptedPackage.data) {\r\n throw new Error('Invalid encrypted data format');\r\n }\r\n\r\n const salt = new Uint8Array(encryptedPackage.salt);\r\n const iv = new Uint8Array(encryptedPackage.iv);\r\n const encrypted = new Uint8Array(encryptedPackage.data);\r\n\r\n const encoder = new TextEncoder();\r\n const passwordBuffer = encoder.encode(password);\r\n\r\n const keyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n passwordBuffer,\r\n { name: 'PBKDF2' },\r\n false,\r\n ['deriveKey']\r\n );\r\n\r\n const key = await crypto.subtle.deriveKey(\r\n {\r\n name: 'PBKDF2',\r\n salt: salt,\r\n iterations: 100000,\r\n hash: 'SHA-256'\r\n },\r\n keyMaterial,\r\n { name: 'AES-GCM', length: 256 },\r\n false,\r\n ['decrypt']\r\n );\r\n\r\n const decrypted = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv },\r\n key,\r\n encrypted\r\n );\r\n\r\n const decryptedString = new TextDecoder().decode(decrypted);\r\n\r\n try {\r\n return JSON.parse(decryptedString);\r\n } catch {\r\n return decryptedString;\r\n }\r\n\r\n } catch (error) {\r\n console.error('Decryption failed:', error.message);\r\n throw new Error(`Decryption error: ${error.message}`);\r\n }\r\n }\r\n\r\n \r\n // Generate secure password for data exchange\r\n static generateSecurePassword() {\r\n const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';\r\n const length = 32; \r\n const randomValues = new Uint32Array(length);\r\n crypto.getRandomValues(randomValues);\r\n \r\n let password = '';\r\n for (let i = 0; i < length; i++) {\r\n password += chars[randomValues[i] % chars.length];\r\n }\r\n return password;\r\n }\r\n\r\n // Real security level calculation with actual verification\r\n static async calculateSecurityLevel(securityManager) {\r\n let score = 0;\r\n const maxScore = 100; // Fixed: Changed from 110 to 100 for cleaner percentage\r\n const verificationResults = {};\r\n \r\n try {\r\n // Fallback to basic calculation if securityManager is not fully initialized\r\n if (!securityManager || !securityManager.securityFeatures) {\r\n console.warn('Security manager not fully initialized, using fallback calculation');\r\n return {\r\n level: 'INITIALIZING',\r\n score: 0,\r\n color: 'gray',\r\n verificationResults: {},\r\n timestamp: Date.now(),\r\n details: 'Security system initializing...',\r\n isRealData: false\r\n };\r\n }\r\n\r\n // All security features are enabled by default - no session type restrictions\r\n const sessionType = 'full'; // All features enabled\r\n const isDemoSession = false; // All features available\r\n \r\n // 1. Base encryption verification (20 points) - Available in demo\r\n try {\r\n const encryptionResult = await EnhancedSecureCryptoUtils.verifyEncryption(securityManager);\r\n if (encryptionResult.passed) {\r\n score += 20;\r\n verificationResults.verifyEncryption = { passed: true, details: encryptionResult.details, points: 20 };\r\n } else {\r\n verificationResults.verifyEncryption = { passed: false, details: encryptionResult.details, points: 0 };\r\n }\r\n } catch (error) {\r\n verificationResults.verifyEncryption = { passed: false, details: `Encryption check failed: ${error.message}`, points: 0 };\r\n }\r\n \r\n // 2. Simple key exchange verification (15 points) - Available in demo\r\n try {\r\n const ecdhResult = await EnhancedSecureCryptoUtils.verifyECDHKeyExchange(securityManager);\r\n if (ecdhResult.passed) {\r\n score += 15;\r\n verificationResults.verifyECDHKeyExchange = { passed: true, details: ecdhResult.details, points: 15 };\r\n } else {\r\n verificationResults.verifyECDHKeyExchange = { passed: false, details: ecdhResult.details, points: 0 };\r\n }\r\n } catch (error) {\r\n verificationResults.verifyECDHKeyExchange = { passed: false, details: `Key exchange check failed: ${error.message}`, points: 0 };\r\n }\r\n \r\n // 3. Message integrity verification (10 points) - Available in demo\r\n try {\r\n const integrityResult = await EnhancedSecureCryptoUtils.verifyMessageIntegrity(securityManager);\r\n if (integrityResult.passed) {\r\n score += 10;\r\n verificationResults.verifyMessageIntegrity = { passed: true, details: integrityResult.details, points: 10 };\r\n } else {\r\n verificationResults.verifyMessageIntegrity = { passed: false, details: integrityResult.details, points: 0 };\r\n }\r\n } catch (error) {\r\n verificationResults.verifyMessageIntegrity = { passed: false, details: `Message integrity check failed: ${error.message}`, points: 0 };\r\n }\r\n \r\n // 4. ECDSA signatures verification (15 points) - All features enabled by default\r\n try {\r\n const ecdsaResult = await EnhancedSecureCryptoUtils.verifyECDSASignatures(securityManager);\r\n if (ecdsaResult.passed) {\r\n score += 15;\r\n verificationResults.verifyECDSASignatures = { passed: true, details: ecdsaResult.details, points: 15 };\r\n } else {\r\n verificationResults.verifyECDSASignatures = { passed: false, details: ecdsaResult.details, points: 0 };\r\n }\r\n } catch (error) {\r\n verificationResults.verifyECDSASignatures = { passed: false, details: `Digital signatures check failed: ${error.message}`, points: 0 };\r\n }\r\n \r\n // 5. Rate limiting verification (5 points) - Available in demo\r\n try {\r\n const rateLimitResult = await EnhancedSecureCryptoUtils.verifyRateLimiting(securityManager);\r\n if (rateLimitResult.passed) {\r\n score += 5;\r\n verificationResults.verifyRateLimiting = { passed: true, details: rateLimitResult.details, points: 5 };\r\n } else {\r\n verificationResults.verifyRateLimiting = { passed: false, details: rateLimitResult.details, points: 0 };\r\n }\r\n } catch (error) {\r\n verificationResults.verifyRateLimiting = { passed: false, details: `Rate limiting check failed: ${error.message}`, points: 0 };\r\n }\r\n \r\n // 6. Metadata protection verification (10 points) - All features enabled by default\r\n try {\r\n const metadataResult = await EnhancedSecureCryptoUtils.verifyMetadataProtection(securityManager);\r\n if (metadataResult.passed) {\r\n score += 10;\r\n verificationResults.verifyMetadataProtection = { passed: true, details: metadataResult.details, points: 10 };\r\n } else {\r\n verificationResults.verifyMetadataProtection = { passed: false, details: metadataResult.details, points: 0 };\r\n }\r\n } catch (error) {\r\n verificationResults.verifyMetadataProtection = { passed: false, details: `Metadata protection check failed: ${error.message}`, points: 0 };\r\n }\r\n \r\n // 7. Perfect Forward Secrecy verification (10 points) - All features enabled by default\r\n try {\r\n const pfsResult = await EnhancedSecureCryptoUtils.verifyPerfectForwardSecrecy(securityManager);\r\n if (pfsResult.passed) {\r\n score += 10;\r\n verificationResults.verifyPerfectForwardSecrecy = { passed: true, details: pfsResult.details, points: 10 };\r\n } else {\r\n verificationResults.verifyPerfectForwardSecrecy = { passed: false, details: pfsResult.details, points: 0 };\r\n }\r\n } catch (error) {\r\n verificationResults.verifyPerfectForwardSecrecy = { passed: false, details: `PFS check failed: ${error.message}`, points: 0 };\r\n }\r\n \r\n // 8. Nested encryption verification (5 points) - All features enabled by default\r\n if (await EnhancedSecureCryptoUtils.verifyNestedEncryption(securityManager)) {\r\n score += 5;\r\n verificationResults.nestedEncryption = { passed: true, details: 'Nested encryption active', points: 5 };\r\n } else {\r\n verificationResults.nestedEncryption = { passed: false, details: 'Nested encryption failed', points: 0 };\r\n }\r\n \r\n // 9. Packet padding verification (5 points) - All features enabled by default\r\n if (await EnhancedSecureCryptoUtils.verifyPacketPadding(securityManager)) {\r\n score += 5;\r\n verificationResults.packetPadding = { passed: true, details: 'Packet padding active', points: 5 };\r\n } else {\r\n verificationResults.packetPadding = { passed: false, details: 'Packet padding failed', points: 0 };\r\n }\r\n \r\n // 10. Advanced features verification (10 points) - All features enabled by default\r\n if (await EnhancedSecureCryptoUtils.verifyAdvancedFeatures(securityManager)) {\r\n score += 10;\r\n verificationResults.advancedFeatures = { passed: true, details: 'Advanced features active', points: 10 };\r\n } else {\r\n verificationResults.advancedFeatures = { passed: false, details: 'Advanced features failed', points: 0 };\r\n }\r\n \r\n const percentage = Math.round((score / maxScore) * 100);\r\n \r\n // All security features are available - no restrictions\r\n const availableChecks = 10; // All 10 security checks available\r\n const passedChecks = Object.values(verificationResults).filter(r => r.passed).length;\r\n \r\n const result = {\r\n level: percentage >= 85 ? 'HIGH' : percentage >= 65 ? 'MEDIUM' : percentage >= 35 ? 'LOW' : 'CRITICAL',\r\n score: percentage,\r\n color: percentage >= 85 ? 'green' : percentage >= 65 ? 'orange' : percentage >= 35 ? 'yellow' : 'red',\r\n verificationResults,\r\n timestamp: Date.now(),\r\n details: `Real verification: ${score}/${maxScore} security checks passed (${passedChecks}/${availableChecks} available)`,\r\n isRealData: true,\r\n passedChecks: passedChecks,\r\n totalChecks: availableChecks,\r\n sessionType: sessionType,\r\n maxPossibleScore: 100 // All features enabled - max 100 points\r\n };\r\n\r\n \r\n return result;\r\n } catch (error) {\r\n console.error('Security level calculation failed:', error.message);\r\n return {\r\n level: 'UNKNOWN',\r\n score: 0,\r\n color: 'red',\r\n verificationResults: {},\r\n timestamp: Date.now(),\r\n details: `Verification failed: ${error.message}`,\r\n isRealData: false\r\n };\r\n }\r\n }\r\n\r\n // Real verification functions\r\n static async verifyEncryption(securityManager) {\r\n try {\r\n if (!securityManager.encryptionKey) {\r\n return { passed: false, details: 'No encryption key available' };\r\n }\r\n \r\n // Test actual encryption/decryption with multiple data types\r\n const testCases = [\r\n 'Test encryption verification',\r\n '\u0420\u0443\u0441\u0441\u043A\u0438\u0439 \u0442\u0435\u043A\u0441\u0442 \u0434\u043B\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438',\r\n 'Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?',\r\n 'Large data: ' + 'A'.repeat(1000)\r\n ];\r\n \r\n for (const testData of testCases) {\r\n const encoder = new TextEncoder();\r\n const testBuffer = encoder.encode(testData);\r\n const iv = crypto.getRandomValues(new Uint8Array(12));\r\n \r\n const encrypted = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv },\r\n securityManager.encryptionKey,\r\n testBuffer\r\n );\r\n \r\n const decrypted = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv },\r\n securityManager.encryptionKey,\r\n encrypted\r\n );\r\n \r\n const decryptedText = new TextDecoder().decode(decrypted);\r\n if (decryptedText !== testData) {\r\n return { passed: false, details: `Decryption mismatch for: ${testData.substring(0, 20)}...` };\r\n }\r\n }\r\n \r\n return { passed: true, details: 'AES-GCM encryption/decryption working correctly' };\r\n } catch (error) {\r\n console.error('Encryption verification failed:', error.message);\r\n return { passed: false, details: `Encryption test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyECDHKeyExchange(securityManager) {\r\n try {\r\n if (!securityManager.ecdhKeyPair || !securityManager.ecdhKeyPair.privateKey || !securityManager.ecdhKeyPair.publicKey) {\r\n return { passed: false, details: 'No ECDH key pair available' };\r\n }\r\n \r\n // Test that keys are actually ECDH keys\r\n const keyType = securityManager.ecdhKeyPair.privateKey.algorithm.name;\r\n const curve = securityManager.ecdhKeyPair.privateKey.algorithm.namedCurve;\r\n \r\n if (keyType !== 'ECDH') {\r\n return { passed: false, details: `Invalid key type: ${keyType}, expected ECDH` };\r\n }\r\n \r\n if (curve !== 'P-384' && curve !== 'P-256') {\r\n return { passed: false, details: `Unsupported curve: ${curve}, expected P-384 or P-256` };\r\n }\r\n \r\n // Test key derivation\r\n try {\r\n const derivedKey = await crypto.subtle.deriveKey(\r\n { name: 'ECDH', public: securityManager.ecdhKeyPair.publicKey },\r\n securityManager.ecdhKeyPair.privateKey,\r\n { name: 'AES-GCM', length: 256 },\r\n false,\r\n ['encrypt', 'decrypt']\r\n );\r\n \r\n if (!derivedKey) {\r\n return { passed: false, details: 'Key derivation failed' };\r\n }\r\n } catch (deriveError) {\r\n return { passed: false, details: `Key derivation test failed: ${deriveError.message}` };\r\n }\r\n \r\n return { passed: true, details: `ECDH key exchange working with ${curve} curve` };\r\n } catch (error) {\r\n console.error('ECDH verification failed:', error.message);\r\n return { passed: false, details: `ECDH test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyECDSASignatures(securityManager) {\r\n try {\r\n if (!securityManager.ecdsaKeyPair || !securityManager.ecdsaKeyPair.privateKey || !securityManager.ecdsaKeyPair.publicKey) {\r\n return { passed: false, details: 'No ECDSA key pair available' };\r\n }\r\n \r\n // Test actual signing and verification with multiple test cases\r\n const testCases = [\r\n 'Test ECDSA signature verification',\r\n '\u0420\u0443\u0441\u0441\u043A\u0438\u0439 \u0442\u0435\u043A\u0441\u0442 \u0434\u043B\u044F \u043F\u043E\u0434\u043F\u0438\u0441\u0438',\r\n 'Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?',\r\n 'Large data: ' + 'B'.repeat(2000)\r\n ];\r\n \r\n for (const testData of testCases) {\r\n const encoder = new TextEncoder();\r\n const testBuffer = encoder.encode(testData);\r\n \r\n const signature = await crypto.subtle.sign(\r\n { name: 'ECDSA', hash: 'SHA-256' },\r\n securityManager.ecdsaKeyPair.privateKey,\r\n testBuffer\r\n );\r\n \r\n const isValid = await crypto.subtle.verify(\r\n { name: 'ECDSA', hash: 'SHA-256' },\r\n securityManager.ecdsaKeyPair.publicKey,\r\n signature,\r\n testBuffer\r\n );\r\n \r\n if (!isValid) {\r\n return { passed: false, details: `Signature verification failed for: ${testData.substring(0, 20)}...` };\r\n }\r\n }\r\n \r\n return { passed: true, details: 'ECDSA digital signatures working correctly' };\r\n } catch (error) {\r\n console.error('ECDSA verification failed:', error.message);\r\n return { passed: false, details: `ECDSA test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyMessageIntegrity(securityManager) {\r\n try {\r\n // Check if macKey exists and is a valid CryptoKey\r\n if (!securityManager.macKey || !(securityManager.macKey instanceof CryptoKey)) {\r\n return { passed: false, details: 'MAC key not available or invalid' };\r\n }\r\n \r\n // Test message integrity with HMAC using multiple test cases\r\n const testCases = [\r\n 'Test message integrity verification',\r\n '\u0420\u0443\u0441\u0441\u043A\u0438\u0439 \u0442\u0435\u043A\u0441\u0442 \u0434\u043B\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0446\u0435\u043B\u043E\u0441\u0442\u043D\u043E\u0441\u0442\u0438',\r\n 'Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?',\r\n 'Large data: ' + 'C'.repeat(3000)\r\n ];\r\n \r\n for (const testData of testCases) {\r\n const encoder = new TextEncoder();\r\n const testBuffer = encoder.encode(testData);\r\n \r\n const hmac = await crypto.subtle.sign(\r\n { name: 'HMAC', hash: 'SHA-256' },\r\n securityManager.macKey,\r\n testBuffer\r\n );\r\n \r\n const isValid = await crypto.subtle.verify(\r\n { name: 'HMAC', hash: 'SHA-256' },\r\n securityManager.macKey,\r\n hmac,\r\n testBuffer\r\n );\r\n \r\n if (!isValid) {\r\n return { passed: false, details: `HMAC verification failed for: ${testData.substring(0, 20)}...` };\r\n }\r\n }\r\n \r\n return { passed: true, details: 'Message integrity (HMAC) working correctly' };\r\n } catch (error) {\r\n console.error('Message integrity verification failed:', error.message);\r\n return { passed: false, details: `Message integrity test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n // Additional verification functions\r\n static async verifyRateLimiting(securityManager) {\r\n try {\r\n // Rate limiting is always available in this implementation\r\n return { passed: true, details: 'Rate limiting is active and working' };\r\n } catch (error) {\r\n return { passed: false, details: `Rate limiting test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyMetadataProtection(securityManager) {\r\n try {\r\n // Metadata protection is always enabled in this implementation\r\n return { passed: true, details: 'Metadata protection is working correctly' };\r\n } catch (error) {\r\n return { passed: false, details: `Metadata protection test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyPerfectForwardSecrecy(securityManager) {\r\n try {\r\n // Perfect Forward Secrecy is always enabled in this implementation\r\n return { passed: true, details: 'Perfect Forward Secrecy is configured and active' };\r\n } catch (error) {\r\n return { passed: false, details: `PFS test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyReplayProtection(securityManager) {\r\n try {\r\n console.log('\uD83D\uDD0D verifyReplayProtection debug:');\r\n console.log(' - securityManager.replayProtection:', securityManager.replayProtection);\r\n console.log(' - securityManager keys:', Object.keys(securityManager));\r\n \r\n // Check if replay protection is enabled\r\n if (!securityManager.replayProtection) {\r\n return { passed: false, details: 'Replay protection not enabled' };\r\n }\r\n \r\n return { passed: true, details: 'Replay protection is working correctly' };\r\n } catch (error) {\r\n return { passed: false, details: `Replay protection test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyDTLSFingerprint(securityManager) {\r\n try {\r\n console.log('\uD83D\uDD0D verifyDTLSFingerprint debug:');\r\n console.log(' - securityManager.dtlsFingerprint:', securityManager.dtlsFingerprint);\r\n \r\n // Check if DTLS fingerprint is available\r\n if (!securityManager.dtlsFingerprint) {\r\n return { passed: false, details: 'DTLS fingerprint not available' };\r\n }\r\n \r\n return { passed: true, details: 'DTLS fingerprint is valid and available' };\r\n } catch (error) {\r\n return { passed: false, details: `DTLS fingerprint test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifySASVerification(securityManager) {\r\n try {\r\n console.log('\uD83D\uDD0D verifySASVerification debug:');\r\n console.log(' - securityManager.sasCode:', securityManager.sasCode);\r\n \r\n // Check if SAS code is available\r\n if (!securityManager.sasCode) {\r\n return { passed: false, details: 'SAS code not available' };\r\n }\r\n \r\n return { passed: true, details: 'SAS verification code is valid and available' };\r\n } catch (error) {\r\n return { passed: false, details: `SAS verification test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyTrafficObfuscation(securityManager) {\r\n try {\r\n console.log('\uD83D\uDD0D verifyTrafficObfuscation debug:');\r\n console.log(' - securityManager.trafficObfuscation:', securityManager.trafficObfuscation);\r\n \r\n // Check if traffic obfuscation is enabled\r\n if (!securityManager.trafficObfuscation) {\r\n return { passed: false, details: 'Traffic obfuscation not enabled' };\r\n }\r\n \r\n return { passed: true, details: 'Traffic obfuscation is working correctly' };\r\n } catch (error) {\r\n return { passed: false, details: `Traffic obfuscation test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyNestedEncryption(securityManager) {\r\n try {\r\n // Check if nestedEncryptionKey exists and is a valid CryptoKey\r\n if (!securityManager.nestedEncryptionKey || !(securityManager.nestedEncryptionKey instanceof CryptoKey)) {\r\n console.warn('Nested encryption key not available or invalid');\r\n return false;\r\n }\r\n \r\n // Test nested encryption\r\n const testData = 'Test nested encryption verification';\r\n const encoder = new TextEncoder();\r\n const testBuffer = encoder.encode(testData);\r\n \r\n // Simulate nested encryption\r\n const encrypted = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv: crypto.getRandomValues(new Uint8Array(12)) },\r\n securityManager.nestedEncryptionKey,\r\n testBuffer\r\n );\r\n \r\n return encrypted && encrypted.byteLength > 0;\r\n } catch (error) {\r\n console.error('Nested encryption verification failed:', error.message);\r\n return false;\r\n }\r\n }\r\n \r\n static async verifyPacketPadding(securityManager) {\r\n try {\r\n if (!securityManager.paddingConfig || !securityManager.paddingConfig.enabled) return false;\r\n \r\n // Test packet padding functionality\r\n const testData = 'Test packet padding verification';\r\n const encoder = new TextEncoder();\r\n const testBuffer = encoder.encode(testData);\r\n \r\n // Simulate packet padding\r\n const paddingSize = Math.floor(Math.random() * (securityManager.paddingConfig.maxPadding - securityManager.paddingConfig.minPadding)) + securityManager.paddingConfig.minPadding;\r\n const paddedData = new Uint8Array(testBuffer.byteLength + paddingSize);\r\n paddedData.set(new Uint8Array(testBuffer), 0);\r\n \r\n return paddedData.byteLength >= testBuffer.byteLength + securityManager.paddingConfig.minPadding;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Packet padding verification failed', { error: error.message });\r\n return false;\r\n }\r\n }\r\n \r\n static async verifyAdvancedFeatures(securityManager) {\r\n try {\r\n // Test advanced features like traffic obfuscation, fake traffic, etc.\r\n const hasFakeTraffic = securityManager.fakeTrafficConfig && securityManager.fakeTrafficConfig.enabled;\r\n const hasDecoyChannels = securityManager.decoyChannelsConfig && securityManager.decoyChannelsConfig.enabled;\r\n const hasAntiFingerprinting = securityManager.antiFingerprintingConfig && securityManager.antiFingerprintingConfig.enabled;\r\n \r\n return hasFakeTraffic || hasDecoyChannels || hasAntiFingerprinting;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Advanced features verification failed', { error: error.message });\r\n return false;\r\n }\r\n }\r\n \r\n static async verifyMutualAuth(securityManager) {\r\n try {\r\n if (!securityManager.isVerified || !securityManager.verificationCode) return false;\r\n \r\n // Test mutual authentication\r\n return securityManager.isVerified && securityManager.verificationCode.length > 0;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Mutual auth verification failed', { error: error.message });\r\n return false;\r\n }\r\n }\r\n \r\n \r\n static async verifyNonExtractableKeys(securityManager) {\r\n try {\r\n if (!securityManager.encryptionKey) return false;\r\n \r\n // Test if keys are non-extractable\r\n const keyData = await crypto.subtle.exportKey('raw', securityManager.encryptionKey);\r\n return keyData && keyData.byteLength > 0;\r\n } catch (error) {\r\n // If export fails, keys are non-extractable (which is good)\r\n return true;\r\n }\r\n }\r\n \r\n static async verifyEnhancedValidation(securityManager) {\r\n try {\r\n if (!securityManager.securityFeatures) return false;\r\n \r\n // Test enhanced validation features\r\n const hasValidation = securityManager.securityFeatures.hasEnhancedValidation || \r\n securityManager.securityFeatures.hasEnhancedReplayProtection;\r\n \r\n return hasValidation;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced validation verification failed', { error: error.message });\r\n return false;\r\n }\r\n }\r\n \r\n \r\n static async verifyPFS(securityManager) {\r\n try {\r\n // Check if PFS is active\r\n return securityManager.securityFeatures &&\r\n securityManager.securityFeatures.hasPFS === true &&\r\n securityManager.keyRotationInterval &&\r\n securityManager.currentKeyVersion !== undefined &&\r\n securityManager.keyVersions &&\r\n securityManager.keyVersions instanceof Map;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'PFS verification failed', { error: error.message });\r\n return false;\r\n }\r\n }\r\n\r\n // Rate limiting implementation\r\n static rateLimiter = {\r\n messages: new Map(),\r\n connections: new Map(),\r\n locks: new Map(),\r\n \r\n async checkMessageRate(identifier, limit = 60, windowMs = 60000) {\r\n if (typeof identifier !== 'string' || identifier.length > 256) {\r\n return false;\r\n }\r\n \r\n const key = `msg_${identifier}`;\r\n\r\n if (this.locks.has(key)) {\r\n\r\n await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * 10) + 5));\r\n return this.checkMessageRate(identifier, limit, windowMs);\r\n }\r\n \r\n this.locks.set(key, true);\r\n \r\n try {\r\n const now = Date.now();\r\n \r\n if (!this.messages.has(key)) {\r\n this.messages.set(key, []);\r\n }\r\n \r\n const timestamps = this.messages.get(key);\r\n \r\n const validTimestamps = timestamps.filter(ts => now - ts < windowMs);\r\n \r\n if (validTimestamps.length >= limit) {\r\n return false; \r\n }\r\n \r\n validTimestamps.push(now);\r\n this.messages.set(key, validTimestamps);\r\n return true;\r\n } finally {\r\n this.locks.delete(key);\r\n }\r\n },\r\n \r\n async checkConnectionRate(identifier, limit = 5, windowMs = 300000) {\r\n if (typeof identifier !== 'string' || identifier.length > 256) {\r\n return false;\r\n }\r\n \r\n const key = `conn_${identifier}`;\r\n \r\n if (this.locks.has(key)) {\r\n await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * 10) + 5));\r\n return this.checkConnectionRate(identifier, limit, windowMs);\r\n }\r\n \r\n this.locks.set(key, true);\r\n \r\n try {\r\n const now = Date.now();\r\n \r\n if (!this.connections.has(key)) {\r\n this.connections.set(key, []);\r\n }\r\n \r\n const timestamps = this.connections.get(key);\r\n const validTimestamps = timestamps.filter(ts => now - ts < windowMs);\r\n \r\n if (validTimestamps.length >= limit) {\r\n return false;\r\n }\r\n \r\n validTimestamps.push(now);\r\n this.connections.set(key, validTimestamps);\r\n return true;\r\n } finally {\r\n this.locks.delete(key);\r\n }\r\n },\r\n \r\n cleanup() {\r\n const now = Date.now();\r\n const maxAge = 3600000; \r\n \r\n for (const [key, timestamps] of this.messages.entries()) {\r\n if (this.locks.has(key)) continue;\r\n \r\n const valid = timestamps.filter(ts => now - ts < maxAge);\r\n if (valid.length === 0) {\r\n this.messages.delete(key);\r\n } else {\r\n this.messages.set(key, valid);\r\n }\r\n }\r\n \r\n for (const [key, timestamps] of this.connections.entries()) {\r\n if (this.locks.has(key)) continue;\r\n \r\n const valid = timestamps.filter(ts => now - ts < maxAge);\r\n if (valid.length === 0) {\r\n this.connections.delete(key);\r\n } else {\r\n this.connections.set(key, valid);\r\n }\r\n }\r\n\r\n for (const lockKey of this.locks.keys()) {\r\n const keyTimestamp = parseInt(lockKey.split('_').pop()) || 0;\r\n if (now - keyTimestamp > 30000) {\r\n this.locks.delete(lockKey);\r\n }\r\n }\r\n }\r\n};\r\n\r\n static validateSalt(salt) {\r\n if (!salt || salt.length !== 64) {\r\n throw new Error('Salt must be exactly 64 bytes');\r\n }\r\n \r\n const uniqueBytes = new Set(salt);\r\n if (uniqueBytes.size < 16) {\r\n throw new Error('Salt has insufficient entropy');\r\n }\r\n \r\n return true;\r\n }\r\n\r\n // Secure logging without data leaks\r\n static secureLog = {\r\n logs: [],\r\n maxLogs: 100,\r\n isProductionMode: false,\r\n \r\n // Initialize production mode detection\r\n init() {\r\n this.isProductionMode = this._detectProductionMode();\r\n if (this.isProductionMode) {\r\n console.log('[SecureChat] Production mode detected - sensitive logging disabled');\r\n }\r\n },\r\n \r\n _detectProductionMode() {\r\n return (\r\n (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') ||\r\n (!window.DEBUG_MODE && !window.DEVELOPMENT_MODE) ||\r\n (window.location.hostname && !window.location.hostname.includes('localhost') && \r\n !window.location.hostname.includes('127.0.0.1') && \r\n !window.location.hostname.includes('.local')) ||\r\n (typeof window.webpackHotUpdate === 'undefined' && !window.location.search.includes('debug'))\r\n );\r\n },\r\n \r\n log(level, message, context = {}) {\r\n const sanitizedContext = this.sanitizeContext(context);\r\n const logEntry = {\r\n timestamp: Date.now(),\r\n level,\r\n message,\r\n context: sanitizedContext,\r\n id: crypto.getRandomValues(new Uint32Array(1))[0]\r\n };\r\n \r\n this.logs.push(logEntry);\r\n \r\n // Keep only recent logs\r\n if (this.logs.length > this.maxLogs) {\r\n this.logs = this.logs.slice(-this.maxLogs);\r\n }\r\n \r\n // Production-safe console output\r\n if (this.isProductionMode) {\r\n if (level === 'error') {\r\n // \u0412 production \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u043C \u0442\u043E\u043B\u044C\u043A\u043E \u043A\u043E\u0434 \u043E\u0448\u0438\u0431\u043A\u0438 \u0431\u0435\u0437 \u0434\u0435\u0442\u0430\u043B\u0435\u0439\r\n console.error(`\u274C [SecureChat] ${message} [ERROR_CODE: ${this._generateErrorCode(message)}]`);\r\n } else if (level === 'warn') {\r\n // \u0412 production \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u043C \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0435 \u0431\u0435\u0437 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430\r\n console.warn(`\u26A0\uFE0F [SecureChat] ${message}`);\r\n } else {\r\n // \u0412 production \u043D\u0435 \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u043C info/debug \u043B\u043E\u0433\u0438\r\n return;\r\n }\r\n } else {\r\n // Development mode - \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u043C \u0432\u0441\u0435\r\n if (level === 'error') {\r\n console.error(`\u274C [SecureChat] ${message}`, { errorType: sanitizedContext?.constructor?.name || 'Unknown' });\r\n } else if (level === 'warn') {\r\n console.warn(`\u26A0\uFE0F [SecureChat] ${message}`, { details: sanitizedContext });\r\n } else {\r\n console.log(`[SecureChat] ${message}`, sanitizedContext);\r\n }\r\n }\r\n },\r\n \r\n // \u0413\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u044B\u0439 \u043A\u043E\u0434 \u043E\u0448\u0438\u0431\u043A\u0438 \u0434\u043B\u044F production\r\n _generateErrorCode(message) {\r\n const hash = message.split('').reduce((a, b) => {\r\n a = ((a << 5) - a) + b.charCodeAt(0);\r\n return a & a;\r\n }, 0);\r\n return Math.abs(hash).toString(36).substring(0, 6).toUpperCase();\r\n },\r\n \r\n sanitizeContext(context) {\r\n if (!context || typeof context !== 'object') {\r\n return context;\r\n }\r\n \r\n const sensitivePatterns = [\r\n /key/i, /secret/i, /password/i, /token/i, /signature/i,\r\n /challenge/i, /proof/i, /salt/i, /iv/i, /nonce/i, /hash/i,\r\n /fingerprint/i, /mac/i, /private/i, /encryption/i, /decryption/i\r\n ];\r\n \r\n const sanitized = {};\r\n for (const [key, value] of Object.entries(context)) {\r\n const isSensitive = sensitivePatterns.some(pattern => \r\n pattern.test(key) || (typeof value === 'string' && pattern.test(value))\r\n );\r\n \r\n if (isSensitive) {\r\n sanitized[key] = '[REDACTED]';\r\n } else if (typeof value === 'string' && value.length > 100) {\r\n sanitized[key] = value.substring(0, 100) + '...[TRUNCATED]';\r\n } else if (value instanceof ArrayBuffer || value instanceof Uint8Array) {\r\n sanitized[key] = `[${value.constructor.name}(${value.byteLength || value.length} bytes)]`;\r\n } else if (value && typeof value === 'object' && !Array.isArray(value)) {\r\n // \u0420\u0435\u043A\u0443\u0440\u0441\u0438\u0432\u043D\u0430\u044F \u0441\u0430\u043D\u0438\u0442\u0438\u0437\u0430\u0446\u0438\u044F \u0434\u043B\u044F \u043E\u0431\u044A\u0435\u043A\u0442\u043E\u0432\r\n sanitized[key] = this.sanitizeContext(value);\r\n } else {\r\n sanitized[key] = value;\r\n }\r\n }\r\n return sanitized;\r\n },\r\n \r\n getLogs(level = null) {\r\n if (level) {\r\n return this.logs.filter(log => log.level === level);\r\n }\r\n return [...this.logs];\r\n },\r\n \r\n clearLogs() {\r\n this.logs = [];\r\n },\r\n \r\n // \u041C\u0435\u0442\u043E\u0434 \u0434\u043B\u044F \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0438 \u043E\u0448\u0438\u0431\u043E\u043A \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432 production\r\n async sendErrorToServer(errorCode, message, context = {}) {\r\n if (!this.isProductionMode) {\r\n return; // \u0412 development \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u043C\r\n }\r\n \r\n try {\r\n // \u041E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u043C \u0442\u043E\u043B\u044C\u043A\u043E \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u0443\u044E \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044E\r\n const safeErrorData = {\r\n errorCode,\r\n timestamp: Date.now(),\r\n userAgent: navigator.userAgent.substring(0, 100),\r\n url: window.location.href.substring(0, 100)\r\n };\r\n \r\n // \u0417\u0434\u0435\u0441\u044C \u043C\u043E\u0436\u043D\u043E \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0443 \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\r\n // await fetch('/api/error-log', { method: 'POST', body: JSON.stringify(safeErrorData) });\r\n \r\n if (window.DEBUG_MODE) {\r\n console.log('[SecureChat] Error logged to server:', safeErrorData);\r\n }\r\n } catch (e) {\r\n // \u041D\u0435 \u043B\u043E\u0433\u0438\u0440\u0443\u0435\u043C \u043E\u0448\u0438\u0431\u043A\u0438 \u043B\u043E\u0433\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F\r\n }\r\n }\r\n };\r\n\r\n // Generate ECDH key pair for secure key exchange (non-extractable) with fallback\r\n static async generateECDHKeyPair() {\r\n try {\r\n // Try P-384 first\r\n try {\r\n const keyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDH',\r\n namedCurve: 'P-384'\r\n },\r\n false, // Non-extractable for enhanced security\r\n ['deriveKey']\r\n );\r\n \r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'ECDH key pair generated successfully (P-384)', {\r\n curve: 'P-384',\r\n extractable: false\r\n });\r\n \r\n return keyPair;\r\n } catch (p384Error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 generation failed, trying P-256', { error: p384Error.message });\r\n \r\n // Fallback to P-256\r\n const keyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDH',\r\n namedCurve: 'P-256'\r\n },\r\n false, // Non-extractable for enhanced security\r\n ['deriveKey']\r\n );\r\n \r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'ECDH key pair generated successfully (P-256 fallback)', {\r\n curve: 'P-256',\r\n extractable: false\r\n });\r\n \r\n return keyPair;\r\n }\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'ECDH key generation failed', { error: error.message });\r\n throw new Error('Failed to create keys for secure exchange');\r\n }\r\n }\r\n\r\n // Generate ECDSA key pair for digital signatures with fallback\r\n static async generateECDSAKeyPair() {\r\n try {\r\n // Try P-384 first\r\n try {\r\n const keyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDSA',\r\n namedCurve: 'P-384'\r\n },\r\n false, // Non-extractable for enhanced security\r\n ['sign', 'verify']\r\n );\r\n \r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'ECDSA key pair generated successfully (P-384)', {\r\n curve: 'P-384',\r\n extractable: false\r\n });\r\n \r\n return keyPair;\r\n } catch (p384Error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 generation failed, trying P-256', { error: p384Error.message });\r\n \r\n // Fallback to P-256\r\n const keyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDSA',\r\n namedCurve: 'P-256'\r\n },\r\n false, // Non-extractable for enhanced security\r\n ['sign', 'verify']\r\n );\r\n \r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'ECDSA key pair generated successfully (P-256 fallback)', {\r\n curve: 'P-256',\r\n extractable: false\r\n });\r\n \r\n return keyPair;\r\n }\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'ECDSA key generation failed', { error: error.message });\r\n throw new Error('Failed to generate keys for digital signatures');\r\n }\r\n }\r\n\r\n // Sign data with ECDSA (P-384 or P-256)\r\n static async signData(privateKey, data) {\r\n try {\r\n const encoder = new TextEncoder();\r\n const dataBuffer = typeof data === 'string' ? encoder.encode(data) : data;\r\n \r\n // Try SHA-384 first, fallback to SHA-256\r\n try {\r\n const signature = await crypto.subtle.sign(\r\n {\r\n name: 'ECDSA',\r\n hash: 'SHA-384'\r\n },\r\n privateKey,\r\n dataBuffer\r\n );\r\n \r\n return Array.from(new Uint8Array(signature));\r\n } catch (sha384Error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'SHA-384 signing failed, trying SHA-256', { error: sha384Error.message });\r\n \r\n const signature = await crypto.subtle.sign(\r\n {\r\n name: 'ECDSA',\r\n hash: 'SHA-256'\r\n },\r\n privateKey,\r\n dataBuffer\r\n );\r\n \r\n return Array.from(new Uint8Array(signature));\r\n }\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Data signing failed', { error: error.message });\r\n throw new Error('Failed to sign data');\r\n }\r\n }\r\n\r\n // Verify ECDSA signature (P-384 or P-256)\r\n static async verifySignature(publicKey, signature, data) {\r\n try {\r\n const encoder = new TextEncoder();\r\n const dataBuffer = typeof data === 'string' ? encoder.encode(data) : data;\r\n const signatureBuffer = new Uint8Array(signature);\r\n \r\n // Try SHA-384 first, fallback to SHA-256\r\n try {\r\n const isValid = await crypto.subtle.verify(\r\n {\r\n name: 'ECDSA',\r\n hash: 'SHA-384'\r\n },\r\n publicKey,\r\n signatureBuffer,\r\n dataBuffer\r\n );\r\n \r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Signature verification completed (SHA-384)', {\r\n isValid,\r\n dataSize: dataBuffer.length\r\n });\r\n \r\n return isValid;\r\n } catch (sha384Error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'SHA-384 verification failed, trying SHA-256', { error: sha384Error.message });\r\n \r\n const isValid = await crypto.subtle.verify(\r\n {\r\n name: 'ECDSA',\r\n hash: 'SHA-256'\r\n },\r\n publicKey,\r\n signatureBuffer,\r\n dataBuffer\r\n );\r\n \r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Signature verification completed (SHA-256 fallback)', {\r\n isValid,\r\n dataSize: dataBuffer.length\r\n });\r\n \r\n return isValid;\r\n }\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Signature verification failed', { error: error.message });\r\n throw new Error('Failed to verify digital signature');\r\n }\r\n }\r\n\r\n // Enhanced DER/SPKI validation with full ASN.1 parsing\r\n static async validateKeyStructure(keyData, expectedAlgorithm = 'ECDH') {\r\n try {\r\n if (!Array.isArray(keyData) || keyData.length === 0) {\r\n throw new Error('Invalid key data format');\r\n }\r\n\r\n const keyBytes = new Uint8Array(keyData);\r\n\r\n // Size limits to prevent DoS\r\n if (keyBytes.length < 50) {\r\n throw new Error('Key data too short - invalid SPKI structure');\r\n }\r\n if (keyBytes.length > 2000) {\r\n throw new Error('Key data too long - possible attack');\r\n }\r\n\r\n // Parse ASN.1 DER structure\r\n const asn1 = EnhancedSecureCryptoUtils.parseASN1(keyBytes);\r\n \r\n // Validate SPKI structure\r\n if (!asn1 || asn1.tag !== 0x30) {\r\n throw new Error('Invalid SPKI structure - missing SEQUENCE tag');\r\n }\r\n\r\n // SPKI should have exactly 2 elements: AlgorithmIdentifier and BIT STRING\r\n if (asn1.children.length !== 2) {\r\n throw new Error(`Invalid SPKI structure - expected 2 elements, got ${asn1.children.length}`);\r\n }\r\n\r\n // Validate AlgorithmIdentifier\r\n const algIdentifier = asn1.children[0];\r\n if (algIdentifier.tag !== 0x30) {\r\n throw new Error('Invalid AlgorithmIdentifier - not a SEQUENCE');\r\n }\r\n\r\n // Parse algorithm OID\r\n const algOid = algIdentifier.children[0];\r\n if (algOid.tag !== 0x06) {\r\n throw new Error('Invalid algorithm OID - not an OBJECT IDENTIFIER');\r\n }\r\n\r\n // Validate algorithm OID based on expected algorithm\r\n const oidBytes = algOid.value;\r\n const oidString = EnhancedSecureCryptoUtils.oidToString(oidBytes);\r\n \r\n // Check for expected algorithms\r\n const validAlgorithms = {\r\n 'ECDH': ['1.2.840.10045.2.1'], // id-ecPublicKey\r\n 'ECDSA': ['1.2.840.10045.2.1'], // id-ecPublicKey (same as ECDH)\r\n 'RSA': ['1.2.840.113549.1.1.1'], // rsaEncryption\r\n 'AES-GCM': ['2.16.840.1.101.3.4.1.6', '2.16.840.1.101.3.4.1.46'] // AES-128-GCM, AES-256-GCM\r\n };\r\n\r\n const expectedOids = validAlgorithms[expectedAlgorithm];\r\n if (!expectedOids) {\r\n throw new Error(`Unknown algorithm: ${expectedAlgorithm}`);\r\n }\r\n\r\n if (!expectedOids.includes(oidString)) {\r\n throw new Error(`Invalid algorithm OID: expected ${expectedOids.join(' or ')}, got ${oidString}`);\r\n }\r\n\r\n // For EC algorithms, validate curve parameters\r\n if (expectedAlgorithm === 'ECDH' || expectedAlgorithm === 'ECDSA') {\r\n if (algIdentifier.children.length < 2) {\r\n throw new Error('Missing curve parameters for EC key');\r\n }\r\n\r\n const curveOid = algIdentifier.children[1];\r\n if (curveOid.tag !== 0x06) {\r\n throw new Error('Invalid curve OID - not an OBJECT IDENTIFIER');\r\n }\r\n\r\n const curveOidString = EnhancedSecureCryptoUtils.oidToString(curveOid.value);\r\n \r\n // Only allow P-256 and P-384 curves\r\n const validCurves = {\r\n '1.2.840.10045.3.1.7': 'P-256', // secp256r1\r\n '1.3.132.0.34': 'P-384' // secp384r1\r\n };\r\n\r\n if (!validCurves[curveOidString]) {\r\n throw new Error(`Invalid or unsupported curve OID: ${curveOidString}`);\r\n }\r\n\r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'EC key curve validated', {\r\n curve: validCurves[curveOidString],\r\n oid: curveOidString\r\n });\r\n }\r\n\r\n // Validate public key BIT STRING\r\n const publicKeyBitString = asn1.children[1];\r\n if (publicKeyBitString.tag !== 0x03) {\r\n throw new Error('Invalid public key - not a BIT STRING');\r\n }\r\n\r\n // Check for unused bits (should be 0 for public keys)\r\n if (publicKeyBitString.value[0] !== 0x00) {\r\n throw new Error(`Invalid BIT STRING - unexpected unused bits: ${publicKeyBitString.value[0]}`);\r\n }\r\n\r\n // For EC keys, validate point format\r\n if (expectedAlgorithm === 'ECDH' || expectedAlgorithm === 'ECDSA') {\r\n const pointData = publicKeyBitString.value.slice(1); // Skip unused bits byte\r\n \r\n // Check for uncompressed point format (0x04)\r\n if (pointData[0] !== 0x04) {\r\n throw new Error(`Invalid EC point format: expected uncompressed (0x04), got 0x${pointData[0].toString(16)}`);\r\n }\r\n\r\n // Validate point size based on curve\r\n const expectedSizes = {\r\n 'P-256': 65, // 1 + 32 + 32\r\n 'P-384': 97 // 1 + 48 + 48\r\n };\r\n\r\n // We already validated the curve above, so we can determine expected size\r\n const curveOidString = EnhancedSecureCryptoUtils.oidToString(algIdentifier.children[1].value);\r\n const curveName = curveOidString === '1.2.840.10045.3.1.7' ? 'P-256' : 'P-384';\r\n const expectedSize = expectedSizes[curveName];\r\n\r\n if (pointData.length !== expectedSize) {\r\n throw new Error(`Invalid EC point size for ${curveName}: expected ${expectedSize}, got ${pointData.length}`);\r\n }\r\n }\r\n\r\n // Additional validation: try to import the key\r\n try {\r\n const algorithm = expectedAlgorithm === 'ECDSA' || expectedAlgorithm === 'ECDH'\r\n ? { name: expectedAlgorithm, namedCurve: 'P-384' }\r\n : { name: expectedAlgorithm };\r\n\r\n const usages = expectedAlgorithm === 'ECDSA' ? ['verify'] : [];\r\n \r\n await crypto.subtle.importKey('spki', keyBytes.buffer, algorithm, false, usages);\r\n } catch (importError) {\r\n // Try P-256 as fallback for EC keys\r\n if (expectedAlgorithm === 'ECDSA' || expectedAlgorithm === 'ECDH') {\r\n try {\r\n const algorithm = { name: expectedAlgorithm, namedCurve: 'P-256' };\r\n const usages = expectedAlgorithm === 'ECDSA' ? ['verify'] : [];\r\n await crypto.subtle.importKey('spki', keyBytes.buffer, algorithm, false, usages);\r\n } catch (fallbackError) {\r\n throw new Error(`Key import validation failed: ${fallbackError.message}`);\r\n }\r\n } else {\r\n throw new Error(`Key import validation failed: ${importError.message}`);\r\n }\r\n }\r\n\r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Key structure validation passed', {\r\n keyLen: keyBytes.length,\r\n algorithm: expectedAlgorithm,\r\n asn1Valid: true,\r\n oidValid: true,\r\n importValid: true\r\n });\r\n\r\n return true;\r\n } catch (err) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Key structure validation failed', {\r\n error: err.message,\r\n algorithm: expectedAlgorithm\r\n });\r\n throw new Error(`Invalid key structure: ${err.message}`);\r\n }\r\n }\r\n\r\n // ASN.1 DER parser helper\r\n static parseASN1(bytes, offset = 0) {\r\n if (offset >= bytes.length) {\r\n return null;\r\n }\r\n\r\n const tag = bytes[offset];\r\n let lengthOffset = offset + 1;\r\n \r\n if (lengthOffset >= bytes.length) {\r\n throw new Error('Truncated ASN.1 structure');\r\n }\r\n\r\n let length = bytes[lengthOffset];\r\n let valueOffset = lengthOffset + 1;\r\n\r\n // Handle long form length\r\n if (length & 0x80) {\r\n const numLengthBytes = length & 0x7f;\r\n if (numLengthBytes > 4) {\r\n throw new Error('ASN.1 length too large');\r\n }\r\n \r\n length = 0;\r\n for (let i = 0; i < numLengthBytes; i++) {\r\n if (valueOffset + i >= bytes.length) {\r\n throw new Error('Truncated ASN.1 length');\r\n }\r\n length = (length << 8) | bytes[valueOffset + i];\r\n }\r\n valueOffset += numLengthBytes;\r\n }\r\n\r\n if (valueOffset + length > bytes.length) {\r\n throw new Error('ASN.1 structure extends beyond data');\r\n }\r\n\r\n const value = bytes.slice(valueOffset, valueOffset + length);\r\n const node = {\r\n tag: tag,\r\n length: length,\r\n value: value,\r\n children: []\r\n };\r\n\r\n // Parse children for SEQUENCE and SET\r\n if (tag === 0x30 || tag === 0x31) {\r\n let childOffset = 0;\r\n while (childOffset < value.length) {\r\n const child = EnhancedSecureCryptoUtils.parseASN1(value, childOffset);\r\n if (!child) break;\r\n node.children.push(child);\r\n childOffset = childOffset + 1 + child.lengthBytes + child.length;\r\n }\r\n }\r\n\r\n // Calculate how many bytes were used for length encoding\r\n node.lengthBytes = valueOffset - lengthOffset;\r\n \r\n return node;\r\n }\r\n\r\n // OID decoder helper\r\n static oidToString(bytes) {\r\n if (!bytes || bytes.length === 0) {\r\n throw new Error('Empty OID');\r\n }\r\n\r\n const parts = [];\r\n \r\n // First byte encodes first two components\r\n const first = Math.floor(bytes[0] / 40);\r\n const second = bytes[0] % 40;\r\n parts.push(first);\r\n parts.push(second);\r\n\r\n // Decode remaining components\r\n let value = 0;\r\n for (let i = 1; i < bytes.length; i++) {\r\n value = (value << 7) | (bytes[i] & 0x7f);\r\n if (!(bytes[i] & 0x80)) {\r\n parts.push(value);\r\n value = 0;\r\n }\r\n }\r\n\r\n return parts.join('.');\r\n }\r\n\r\n // Helper to validate and sanitize OID string\r\n static validateOidString(oidString) {\r\n // OID format: digits separated by dots\r\n const oidRegex = /^[0-9]+(\\.[0-9]+)*$/;\r\n if (!oidRegex.test(oidString)) {\r\n throw new Error(`Invalid OID format: ${oidString}`);\r\n }\r\n\r\n const parts = oidString.split('.').map(Number);\r\n \r\n // First component must be 0, 1, or 2\r\n if (parts[0] > 2) {\r\n throw new Error(`Invalid OID first component: ${parts[0]}`);\r\n }\r\n\r\n // If first component is 0 or 1, second must be <= 39\r\n if ((parts[0] === 0 || parts[0] === 1) && parts[1] > 39) {\r\n throw new Error(`Invalid OID second component: ${parts[1]} (must be <= 39 for first component ${parts[0]})`);\r\n }\r\n\r\n return true;\r\n }\r\n\r\n // Export public key for transmission with signature \r\n static async exportPublicKeyWithSignature(publicKey, signingKey, keyType = 'ECDH') {\r\n try {\r\n // Validate key type\r\n if (!['ECDH', 'ECDSA'].includes(keyType)) {\r\n throw new Error('Invalid key type');\r\n }\r\n \r\n const exported = await crypto.subtle.exportKey('spki', publicKey);\r\n const keyData = Array.from(new Uint8Array(exported));\r\n \r\n await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, keyType);\r\n \r\n // Create signed key package\r\n const keyPackage = {\r\n keyType,\r\n keyData,\r\n timestamp: Date.now(),\r\n version: '4.0'\r\n };\r\n \r\n // Sign the key package\r\n const packageString = JSON.stringify(keyPackage);\r\n const signature = await EnhancedSecureCryptoUtils.signData(signingKey, packageString);\r\n \r\n const signedPackage = {\r\n ...keyPackage,\r\n signature\r\n };\r\n \r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Public key exported with signature', {\r\n keyType,\r\n keySize: keyData.length,\r\n signed: true\r\n });\r\n \r\n return signedPackage;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Public key export failed', {\r\n error: error.message,\r\n keyType\r\n });\r\n throw new Error(`Failed to export ${keyType} key: ${error.message}`);\r\n }\r\n }\r\n\r\n // Import and verify signed public key\r\n static async importSignedPublicKey(signedPackage, verifyingKey, expectedKeyType = 'ECDH') {\r\n try {\r\n // Validate package structure\r\n if (!signedPackage || typeof signedPackage !== 'object') {\r\n throw new Error('Invalid signed package format');\r\n }\r\n \r\n const { keyType, keyData, timestamp, version, signature } = signedPackage;\r\n \r\n if (!keyType || !keyData || !timestamp || !signature) {\r\n throw new Error('Missing required fields in signed package');\r\n }\r\n \r\n if (!EnhancedSecureCryptoUtils.constantTimeCompare(keyType, expectedKeyType)) {\r\n throw new Error(`Key type mismatch: expected ${expectedKeyType}, got ${keyType}`);\r\n }\r\n \r\n // Check timestamp (reject keys older than 1 hour)\r\n const keyAge = Date.now() - timestamp;\r\n if (keyAge > 3600000) {\r\n throw new Error('Signed key package is too old');\r\n }\r\n \r\n await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, keyType);\r\n \r\n // Verify signature\r\n const packageCopy = { keyType, keyData, timestamp, version };\r\n const packageString = JSON.stringify(packageCopy);\r\n const isValidSignature = await EnhancedSecureCryptoUtils.verifySignature(verifyingKey, signature, packageString);\r\n \r\n if (!isValidSignature) {\r\n throw new Error('Invalid signature on key package - possible MITM attack');\r\n }\r\n \r\n // Import the key with fallback support\r\n const keyBytes = new Uint8Array(keyData);\r\n \r\n // Try P-384 first\r\n try {\r\n const algorithm = keyType === 'ECDH' ?\r\n { name: 'ECDH', namedCurve: 'P-384' }\r\n : { name: 'ECDSA', namedCurve: 'P-384' };\r\n \r\n const keyUsages = keyType === 'ECDH' ? [] : ['verify'];\r\n \r\n const publicKey = await crypto.subtle.importKey(\r\n 'spki',\r\n keyBytes,\r\n algorithm,\r\n false, // Non-extractable\r\n keyUsages\r\n );\r\n \r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Signed public key imported successfully (P-384)', {\r\n keyType,\r\n signatureValid: true,\r\n keyAge: Math.round(keyAge / 1000) + 's'\r\n });\r\n \r\n return publicKey;\r\n } catch (p384Error) {\r\n // Fallback to P-256\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 import failed, trying P-256', {\r\n error: p384Error.message\r\n });\r\n \r\n const algorithm = keyType === 'ECDH' ?\r\n { name: 'ECDH', namedCurve: 'P-256' }\r\n : { name: 'ECDSA', namedCurve: 'P-256' };\r\n \r\n const keyUsages = keyType === 'ECDH' ? [] : ['verify'];\r\n \r\n const publicKey = await crypto.subtle.importKey(\r\n 'spki',\r\n keyBytes,\r\n algorithm,\r\n false, // Non-extractable\r\n keyUsages\r\n );\r\n \r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Signed public key imported successfully (P-256 fallback)', {\r\n keyType,\r\n signatureValid: true,\r\n keyAge: Math.round(keyAge / 1000) + 's'\r\n });\r\n \r\n return publicKey;\r\n }\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Signed public key import failed', {\r\n error: error.message,\r\n expectedKeyType\r\n });\r\n throw new Error(`Failed to import the signed key: ${error.message}`);\r\n }\r\n }\r\n\r\n // Legacy export for backward compatibility\r\n static async exportPublicKey(publicKey) {\r\n try {\r\n const exported = await crypto.subtle.exportKey('spki', publicKey);\r\n const keyData = Array.from(new Uint8Array(exported));\r\n \r\n await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, 'ECDH');\r\n \r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Legacy public key exported', { keySize: keyData.length });\r\n return keyData;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Legacy public key export failed', { error: error.message });\r\n throw new Error('Failed to export the public key');\r\n }\r\n }\r\n\r\n // Legacy import for backward compatibility with fallback\r\n static async importPublicKey(keyData) {\r\n try {\r\n await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, 'ECDH');\r\n \r\n const keyBytes = new Uint8Array(keyData);\r\n \r\n // Try P-384 first\r\n try {\r\n const publicKey = await crypto.subtle.importKey(\r\n 'spki',\r\n keyBytes,\r\n {\r\n name: 'ECDH',\r\n namedCurve: 'P-384'\r\n },\r\n false, // Non-extractable\r\n []\r\n );\r\n \r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Legacy public key imported (P-384)', { keySize: keyData.length });\r\n return publicKey;\r\n } catch (p384Error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 import failed, trying P-256', { error: p384Error.message });\r\n \r\n // Fallback to P-256\r\n const publicKey = await crypto.subtle.importKey(\r\n 'spki',\r\n keyBytes,\r\n {\r\n name: 'ECDH',\r\n namedCurve: 'P-256'\r\n },\r\n false, // Non-extractable\r\n []\r\n );\r\n \r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Legacy public key imported (P-256 fallback)', { keySize: keyData.length });\r\n return publicKey;\r\n }\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Legacy public key import failed', { error: error.message });\r\n throw new Error('Failed to import the public key');\r\n }\r\n }\r\n\r\n\r\n // Method to check if a key is trusted\r\n static isKeyTrusted(keyOrFingerprint) {\r\n if (keyOrFingerprint instanceof CryptoKey) {\r\n const meta = EnhancedSecureCryptoUtils._keyMetadata.get(keyOrFingerprint);\r\n return meta ? meta.trusted === true : false;\r\n } else if (keyOrFingerprint && keyOrFingerprint._securityMetadata) {\r\n // Check by key metadata\r\n return keyOrFingerprint._securityMetadata.trusted === true;\r\n }\r\n\r\n return false;\r\n }\r\n\r\n static async importPublicKeyFromSignedPackage(signedPackage, verifyingKey = null, options = {}) {\r\n try {\r\n if (!signedPackage || !signedPackage.keyData || !signedPackage.signature) {\r\n throw new Error('Invalid signed key package format');\r\n }\r\n\r\n // Validate all required fields are present\r\n const requiredFields = ['keyData', 'signature', 'keyType', 'timestamp', 'version'];\r\n const missingFields = requiredFields.filter(field => !signedPackage[field]);\r\n\r\n if (missingFields.length > 0) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Missing required fields in signed package', {\r\n missingFields: missingFields,\r\n availableFields: Object.keys(signedPackage)\r\n });\r\n throw new Error(`Required fields are missing in the signed package: ${missingFields.join(', ')}`);\r\n }\r\n\r\n // SECURITY ENHANCEMENT: MANDATORY signature verification for signed packages\r\n if (!verifyingKey) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'SECURITY VIOLATION: Signed package received without verifying key', {\r\n keyType: signedPackage.keyType,\r\n keySize: signedPackage.keyData.length,\r\n timestamp: signedPackage.timestamp,\r\n version: signedPackage.version,\r\n securityRisk: 'HIGH - Potential MITM attack vector'\r\n });\r\n\r\n // REJECT the signed package if no verifying key provided\r\n throw new Error('CRITICAL SECURITY ERROR: Signed key package received without a verification key. ' +\r\n 'This may indicate a possible MITM attack attempt. Import rejected for security reasons.');\r\n }\r\n\r\n // \u041E\u0411\u041D\u041E\u0412\u041B\u0415\u041D\u041E: \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C \u0443\u043B\u0443\u0447\u0448\u0435\u043D\u043D\u0443\u044E \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u044E\r\n await EnhancedSecureCryptoUtils.validateKeyStructure(signedPackage.keyData, signedPackage.keyType || 'ECDH');\r\n\r\n // MANDATORY signature verification when verifyingKey is provided\r\n const packageCopy = { ...signedPackage };\r\n delete packageCopy.signature;\r\n const packageString = JSON.stringify(packageCopy);\r\n const isValidSignature = await EnhancedSecureCryptoUtils.verifySignature(verifyingKey, signedPackage.signature, packageString);\r\n\r\n if (!isValidSignature) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'SECURITY BREACH: Invalid signature detected - MITM attack prevented', {\r\n keyType: signedPackage.keyType,\r\n keySize: signedPackage.keyData.length,\r\n timestamp: signedPackage.timestamp,\r\n version: signedPackage.version,\r\n attackPrevented: true\r\n });\r\n throw new Error('CRITICAL SECURITY ERROR: Invalid key signature detected. ' +\r\n 'This indicates a possible MITM attack attempt. Key import rejected.');\r\n }\r\n\r\n // Additional MITM protection: Check for key reuse and suspicious patterns\r\n const keyFingerprint = await EnhancedSecureCryptoUtils.calculateKeyFingerprint(signedPackage.keyData);\r\n\r\n // Log successful verification with security details\r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'SECURE: Signature verification passed for signed package', {\r\n keyType: signedPackage.keyType,\r\n keySize: signedPackage.keyData.length,\r\n timestamp: signedPackage.timestamp,\r\n version: signedPackage.version,\r\n signatureVerified: true,\r\n securityLevel: 'HIGH',\r\n keyFingerprint: keyFingerprint.substring(0, 8) // Only log first 8 chars for security\r\n });\r\n\r\n // Import the public key with fallback\r\n const keyBytes = new Uint8Array(signedPackage.keyData);\r\n const keyType = signedPackage.keyType || 'ECDH';\r\n\r\n // Try P-384 first\r\n try {\r\n const publicKey = await crypto.subtle.importKey(\r\n 'spki',\r\n keyBytes,\r\n {\r\n name: keyType,\r\n namedCurve: 'P-384'\r\n },\r\n false, // Non-extractable\r\n keyType === 'ECDSA' ? ['verify'] : []\r\n );\r\n\r\n // Use WeakMap to store metadata\r\n EnhancedSecureCryptoUtils._keyMetadata.set(publicKey, {\r\n trusted: true,\r\n verificationStatus: 'VERIFIED_SECURE',\r\n verificationTimestamp: Date.now()\r\n });\r\n\r\n return publicKey;\r\n } catch (p384Error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 import failed, trying P-256', { error: p384Error.message });\r\n\r\n // Fallback to P-256\r\n const publicKey = await crypto.subtle.importKey(\r\n 'spki',\r\n keyBytes,\r\n {\r\n name: keyType,\r\n namedCurve: 'P-256'\r\n },\r\n false, // Non-extractable\r\n keyType === 'ECDSA' ? ['verify'] : []\r\n );\r\n\r\n // Use WeakMap to store metadata\r\n EnhancedSecureCryptoUtils._keyMetadata.set(publicKey, {\r\n trusted: true,\r\n verificationStatus: 'VERIFIED_SECURE',\r\n verificationTimestamp: Date.now()\r\n });\r\n\r\n return publicKey;\r\n }\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Signed package key import failed', {\r\n error: error.message,\r\n securityImplications: 'Potential security breach prevented'\r\n });\r\n throw new Error(`Failed to import the public key from the signed package: ${error.message}`);\r\n }\r\n }\r\n\r\n // Enhanced key derivation with metadata protection and 64-byte salt\r\n static async deriveSharedKeys(privateKey, publicKey, salt) {\r\n try {\r\n // Validate input parameters are CryptoKey instances\r\n if (!(privateKey instanceof CryptoKey)) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Private key is not a CryptoKey', {\r\n privateKeyType: typeof privateKey,\r\n privateKeyAlgorithm: privateKey?.algorithm?.name\r\n });\r\n throw new Error('The private key is not a valid CryptoKey.');\r\n }\r\n \r\n if (!(publicKey instanceof CryptoKey)) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Public key is not a CryptoKey', {\r\n publicKeyType: typeof publicKey,\r\n publicKeyAlgorithm: publicKey?.algorithm?.name\r\n });\r\n throw new Error('The private key is not a valid CryptoKey.');\r\n }\r\n \r\n // Validate salt size (should be 64 bytes for enhanced security)\r\n if (!salt || salt.length !== 64) {\r\n throw new Error('Salt must be exactly 64 bytes for enhanced security');\r\n }\r\n \r\n const saltBytes = new Uint8Array(salt);\r\n const encoder = new TextEncoder();\r\n \r\n // Enhanced context info with version and additional entropy\r\n const contextInfo = encoder.encode('SecureBit.chat v4.0 Enhanced Security Edition');\r\n \r\n // Derive master shared secret with enhanced parameters\r\n // Try SHA-384 first, fallback to SHA-256\r\n let sharedSecret;\r\n try {\r\n sharedSecret = await crypto.subtle.deriveKey(\r\n {\r\n name: 'ECDH',\r\n public: publicKey\r\n },\r\n privateKey,\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-384',\r\n salt: saltBytes,\r\n info: contextInfo\r\n },\r\n false, // Non-extractable\r\n ['deriveKey']\r\n );\r\n } catch (sha384Error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'SHA-384 key derivation failed, trying SHA-256', { \r\n error: sha384Error.message,\r\n privateKeyType: typeof privateKey,\r\n publicKeyType: typeof publicKey,\r\n privateKeyAlgorithm: privateKey?.algorithm?.name,\r\n publicKeyAlgorithm: publicKey?.algorithm?.name\r\n });\r\n \r\n sharedSecret = await crypto.subtle.deriveKey(\r\n {\r\n name: 'ECDH',\r\n public: publicKey\r\n },\r\n privateKey,\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: saltBytes,\r\n info: contextInfo\r\n },\r\n false, // Non-extractable\r\n ['deriveKey']\r\n );\r\n }\r\n\r\n // Derive message encryption key with fallback\r\n let encryptionKey;\r\n try {\r\n encryptionKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-384',\r\n salt: saltBytes,\r\n info: encoder.encode('message-encryption-v4')\r\n },\r\n sharedSecret,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false, // Non-extractable for enhanced security\r\n ['encrypt', 'decrypt']\r\n );\r\n } catch (sha384Error) {\r\n encryptionKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: saltBytes,\r\n info: encoder.encode('message-encryption-v4')\r\n },\r\n sharedSecret,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false, // Non-extractable for enhanced security\r\n ['encrypt', 'decrypt']\r\n );\r\n }\r\n\r\n // Derive MAC key for message authentication with fallback\r\n let macKey;\r\n try {\r\n macKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-384',\r\n salt: saltBytes,\r\n info: encoder.encode('message-authentication-v4')\r\n },\r\n sharedSecret,\r\n {\r\n name: 'HMAC',\r\n hash: 'SHA-384'\r\n },\r\n false, // Non-extractable\r\n ['sign', 'verify']\r\n );\r\n } catch (sha384Error) {\r\n macKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: saltBytes,\r\n info: encoder.encode('message-authentication-v4')\r\n },\r\n sharedSecret,\r\n {\r\n name: 'HMAC',\r\n hash: 'SHA-256'\r\n },\r\n false, // Non-extractable\r\n ['sign', 'verify']\r\n );\r\n }\r\n\r\n // Derive separate metadata encryption key with fallback\r\n let metadataKey;\r\n try {\r\n metadataKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-384',\r\n salt: saltBytes,\r\n info: encoder.encode('metadata-protection-v4')\r\n },\r\n sharedSecret,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false, // Non-extractable\r\n ['encrypt', 'decrypt']\r\n );\r\n } catch (sha384Error) {\r\n metadataKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: saltBytes,\r\n info: encoder.encode('metadata-protection-v4')\r\n },\r\n sharedSecret,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false, // Non-extractable\r\n ['encrypt', 'decrypt']\r\n );\r\n }\r\n\r\n // Generate temporary extractable key for fingerprint calculation with fallback\r\n let fingerprintKey;\r\n try {\r\n fingerprintKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-384',\r\n salt: saltBytes,\r\n info: encoder.encode('fingerprint-generation-v4')\r\n },\r\n sharedSecret,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n true, // Extractable only for fingerprint\r\n ['encrypt', 'decrypt']\r\n );\r\n } catch (sha384Error) {\r\n fingerprintKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: saltBytes,\r\n info: encoder.encode('fingerprint-generation-v4')\r\n },\r\n sharedSecret,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n true, // Extractable only for fingerprint\r\n ['encrypt', 'decrypt']\r\n );\r\n }\r\n\r\n // Generate key fingerprint for verification\r\n const fingerprintKeyData = await crypto.subtle.exportKey('raw', fingerprintKey);\r\n const fingerprint = await EnhancedSecureCryptoUtils.generateKeyFingerprint(Array.from(new Uint8Array(fingerprintKeyData)));\r\n\r\n // Validate that all derived keys are CryptoKey instances\r\n if (!(encryptionKey instanceof CryptoKey)) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived encryption key is not a CryptoKey', {\r\n encryptionKeyType: typeof encryptionKey,\r\n encryptionKeyAlgorithm: encryptionKey?.algorithm?.name\r\n });\r\n throw new Error('The derived encryption key is not a valid CryptoKey.');\r\n }\r\n \r\n if (!(macKey instanceof CryptoKey)) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived MAC key is not a CryptoKey', {\r\n macKeyType: typeof macKey,\r\n macKeyAlgorithm: macKey?.algorithm?.name\r\n });\r\n throw new Error('The derived MAC key is not a valid CryptoKey.');\r\n }\r\n \r\n if (!(metadataKey instanceof CryptoKey)) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived metadata key is not a CryptoKey', {\r\n metadataKeyType: typeof metadataKey,\r\n metadataKeyAlgorithm: metadataKey?.algorithm?.name\r\n });\r\n throw new Error('The derived metadata key is not a valid CryptoKey.');\r\n }\r\n\r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Enhanced shared keys derived successfully', {\r\n saltSize: salt.length,\r\n hasMetadataKey: true,\r\n nonExtractable: true,\r\n version: '4.0',\r\n allKeysValid: true\r\n });\r\n\r\n return {\r\n encryptionKey,\r\n macKey,\r\n metadataKey,\r\n fingerprint,\r\n timestamp: Date.now(),\r\n version: '4.0'\r\n };\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced key derivation failed', { error: error.message });\r\n throw new Error(`Failed to create shared encryption keys: ${error.message}`);\r\n }\r\n }\r\n\r\n static async generateKeyFingerprint(keyData) {\r\n const keyBuffer = new Uint8Array(keyData);\r\n const hashBuffer = await crypto.subtle.digest('SHA-384', keyBuffer);\r\n const hashArray = Array.from(new Uint8Array(hashBuffer));\r\n return hashArray.slice(0, 12).map(b => b.toString(16).padStart(2, '0')).join(':');\r\n }\r\n\r\n // Generate mutual authentication challenge\r\n static generateMutualAuthChallenge() {\r\n const challenge = crypto.getRandomValues(new Uint8Array(48)); // Increased to 48 bytes\r\n const timestamp = Date.now();\r\n const nonce = crypto.getRandomValues(new Uint8Array(16));\r\n \r\n return {\r\n challenge: Array.from(challenge),\r\n timestamp,\r\n nonce: Array.from(nonce),\r\n version: '4.0'\r\n };\r\n }\r\n\r\n // Create cryptographic proof for mutual authentication\r\n static async createAuthProof(challenge, privateKey, publicKey) {\r\n try {\r\n if (!challenge || !challenge.challenge || !challenge.timestamp || !challenge.nonce) {\r\n throw new Error('Invalid challenge structure');\r\n }\r\n \r\n // Check challenge age (max 2 minutes)\r\n const challengeAge = Date.now() - challenge.timestamp;\r\n if (challengeAge > 120000) {\r\n throw new Error('Challenge expired');\r\n }\r\n \r\n // Create proof data\r\n const proofData = {\r\n challenge: challenge.challenge,\r\n timestamp: challenge.timestamp,\r\n nonce: challenge.nonce,\r\n responseTimestamp: Date.now(),\r\n publicKeyHash: await EnhancedSecureCryptoUtils.hashPublicKey(publicKey)\r\n };\r\n \r\n // Sign the proof\r\n const proofString = JSON.stringify(proofData);\r\n const signature = await EnhancedSecureCryptoUtils.signData(privateKey, proofString);\r\n \r\n const proof = {\r\n ...proofData,\r\n signature,\r\n version: '4.0'\r\n };\r\n \r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Authentication proof created', {\r\n challengeAge: Math.round(challengeAge / 1000) + 's'\r\n });\r\n \r\n return proof;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Authentication proof creation failed', { error: error.message });\r\n throw new Error(`Failed to create cryptographic proof: ${error.message}`);\r\n }\r\n }\r\n\r\n // Verify mutual authentication proof\r\n static async verifyAuthProof(proof, challenge, publicKey) {\r\n try {\r\n await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * 20) + 5));\r\n // Assert the public key is valid and has the correct usage\r\n EnhancedSecureCryptoUtils.assertCryptoKey(publicKey, 'ECDSA', ['verify']);\r\n\r\n if (!proof || !challenge || !publicKey) {\r\n throw new Error('Missing required parameters for proof verification');\r\n }\r\n\r\n // Validate proof structure\r\n const requiredFields = ['challenge', 'timestamp', 'nonce', 'responseTimestamp', 'publicKeyHash', 'signature'];\r\n for (const field of requiredFields) {\r\n if (!proof[field]) {\r\n throw new Error(`Missing required field: ${field}`);\r\n }\r\n }\r\n\r\n // Verify challenge matches\r\n if (!EnhancedSecureCryptoUtils.constantTimeCompareArrays(proof.challenge, challenge.challenge) ||\r\n proof.timestamp !== challenge.timestamp ||\r\n !EnhancedSecureCryptoUtils.constantTimeCompareArrays(proof.nonce, challenge.nonce)) {\r\n throw new Error('Challenge mismatch - possible replay attack');\r\n }\r\n\r\n // Check response time (max 30 minutes for better UX)\r\n const responseAge = Date.now() - proof.responseTimestamp;\r\n if (responseAge > 1800000) {\r\n throw new Error('Proof response expired');\r\n }\r\n\r\n // Verify public key hash\r\n const expectedHash = await EnhancedSecureCryptoUtils.hashPublicKey(publicKey);\r\n if (!EnhancedSecureCryptoUtils.constantTimeCompare(proof.publicKeyHash, expectedHash)) {\r\n throw new Error('Public key hash mismatch');\r\n }\r\n\r\n // Verify signature\r\n const proofCopy = { ...proof };\r\n delete proofCopy.signature;\r\n const proofString = JSON.stringify(proofCopy);\r\n const isValidSignature = await EnhancedSecureCryptoUtils.verifySignature(publicKey, proof.signature, proofString);\r\n\r\n if (!isValidSignature) {\r\n throw new Error('Invalid proof signature');\r\n }\r\n\r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Authentication proof verified successfully', {\r\n responseAge: Math.round(responseAge / 1000) + 's'\r\n });\r\n\r\n return true;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Authentication proof verification failed', { error: error.message });\r\n throw new Error(`Failed to verify cryptographic proof: ${error.message}`);\r\n }\r\n }\r\n\r\n // Hash public key for verification\r\n static async hashPublicKey(publicKey) {\r\n try {\r\n const exported = await crypto.subtle.exportKey('spki', publicKey);\r\n const hash = await crypto.subtle.digest('SHA-384', exported);\r\n const hashArray = Array.from(new Uint8Array(hash));\r\n return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Public key hashing failed', { error: error.message });\r\n throw new Error('Failed to create hash of the public key');\r\n }\r\n }\r\n\r\n // Legacy authentication challenge for backward compatibility\r\n static generateAuthChallenge() {\r\n const challenge = crypto.getRandomValues(new Uint8Array(32));\r\n return Array.from(challenge);\r\n }\r\n\r\n // Generate verification code for out-of-band authentication\r\n static generateVerificationCode() {\r\n const chars = '0123456789ABCDEF';\r\n let result = '';\r\n const values = crypto.getRandomValues(new Uint8Array(6));\r\n for (let i = 0; i < 6; i++) {\r\n result += chars[values[i] % chars.length];\r\n }\r\n return result.match(/.{1,2}/g).join('-');\r\n }\r\n\r\n // Enhanced message encryption with metadata protection and sequence numbers\r\n static async encryptMessage(message, encryptionKey, macKey, metadataKey, messageId, sequenceNumber = 0) {\r\n try {\r\n if (!message || typeof message !== 'string') {\r\n throw new Error('Invalid message format');\r\n }\r\n\r\n EnhancedSecureCryptoUtils.assertCryptoKey(encryptionKey, 'AES-GCM', ['encrypt']);\r\n EnhancedSecureCryptoUtils.assertCryptoKey(macKey, 'HMAC', ['sign']);\r\n EnhancedSecureCryptoUtils.assertCryptoKey(metadataKey, 'AES-GCM', ['encrypt']);\r\n\r\n const encoder = new TextEncoder();\r\n const messageData = encoder.encode(message);\r\n const messageIv = crypto.getRandomValues(new Uint8Array(12));\r\n const metadataIv = crypto.getRandomValues(new Uint8Array(12));\r\n const timestamp = Date.now();\r\n\r\n const paddingSize = 16 - (messageData.length % 16);\r\n const paddedMessage = new Uint8Array(messageData.length + paddingSize);\r\n paddedMessage.set(messageData);\r\n const padding = crypto.getRandomValues(new Uint8Array(paddingSize));\r\n paddedMessage.set(padding, messageData.length);\r\n\r\n const encryptedMessage = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv: messageIv },\r\n encryptionKey,\r\n paddedMessage\r\n );\r\n\r\n const metadata = {\r\n id: messageId,\r\n timestamp: timestamp,\r\n sequenceNumber: sequenceNumber,\r\n originalLength: messageData.length,\r\n version: '4.0'\r\n };\r\n\r\n const metadataStr = JSON.stringify(EnhancedSecureCryptoUtils.sortObjectKeys(metadata));\r\n const encryptedMetadata = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv: metadataIv },\r\n metadataKey,\r\n encoder.encode(metadataStr)\r\n );\r\n\r\n const payload = {\r\n messageIv: Array.from(messageIv),\r\n messageData: Array.from(new Uint8Array(encryptedMessage)),\r\n metadataIv: Array.from(metadataIv),\r\n metadataData: Array.from(new Uint8Array(encryptedMetadata)),\r\n version: '4.0'\r\n };\r\n\r\n const sortedPayload = EnhancedSecureCryptoUtils.sortObjectKeys(payload);\r\n const payloadStr = JSON.stringify(sortedPayload);\r\n\r\n const mac = await crypto.subtle.sign(\r\n 'HMAC',\r\n macKey,\r\n encoder.encode(payloadStr)\r\n );\r\n\r\n payload.mac = Array.from(new Uint8Array(mac));\r\n\r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Message encrypted with metadata protection', {\r\n messageId,\r\n sequenceNumber,\r\n hasMetadataProtection: true,\r\n hasPadding: true\r\n });\r\n\r\n return payload;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Message encryption failed', {\r\n error: error.message,\r\n messageId\r\n });\r\n throw new Error(`Failed to encrypt the message: ${error.message}`);\r\n }\r\n }\r\n\r\n // Enhanced message decryption with metadata protection and sequence validation\r\n static async decryptMessage(encryptedPayload, encryptionKey, macKey, metadataKey, expectedSequenceNumber = null) {\r\n try {\r\n EnhancedSecureCryptoUtils.assertCryptoKey(encryptionKey, 'AES-GCM', ['decrypt']);\r\n EnhancedSecureCryptoUtils.assertCryptoKey(macKey, 'HMAC', ['verify']);\r\n EnhancedSecureCryptoUtils.assertCryptoKey(metadataKey, 'AES-GCM', ['decrypt']);\r\n\r\n const requiredFields = ['messageIv', 'messageData', 'metadataIv', 'metadataData', 'mac', 'version'];\r\n for (const field of requiredFields) {\r\n if (!encryptedPayload[field]) {\r\n throw new Error(`Missing required field: ${field}`);\r\n }\r\n }\r\n\r\n const payloadCopy = { ...encryptedPayload };\r\n delete payloadCopy.mac;\r\n const sortedPayloadCopy = EnhancedSecureCryptoUtils.sortObjectKeys(payloadCopy);\r\n const payloadStr = JSON.stringify(sortedPayloadCopy);\r\n\r\n const macValid = await crypto.subtle.verify(\r\n 'HMAC',\r\n macKey,\r\n new Uint8Array(encryptedPayload.mac),\r\n new TextEncoder().encode(payloadStr)\r\n );\r\n\r\n if (!macValid) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'MAC verification failed', {\r\n payloadFields: Object.keys(encryptedPayload),\r\n macLength: encryptedPayload.mac?.length\r\n });\r\n throw new Error('Message authentication failed - possible tampering');\r\n }\r\n\r\n const metadataIv = new Uint8Array(encryptedPayload.metadataIv);\r\n const metadataData = new Uint8Array(encryptedPayload.metadataData);\r\n\r\n const decryptedMetadataBuffer = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv: metadataIv },\r\n metadataKey,\r\n metadataData\r\n );\r\n\r\n const metadataStr = new TextDecoder().decode(decryptedMetadataBuffer);\r\n const metadata = JSON.parse(metadataStr);\r\n\r\n if (!metadata.id || !metadata.timestamp || metadata.sequenceNumber === undefined || !metadata.originalLength) {\r\n throw new Error('Invalid metadata structure');\r\n }\r\n\r\n const messageAge = Date.now() - metadata.timestamp;\r\n if (messageAge > 1800000) { // 30 minutes for better UX\r\n throw new Error('Message expired (older than 5 minutes)');\r\n }\r\n\r\n if (expectedSequenceNumber !== null) {\r\n if (metadata.sequenceNumber < expectedSequenceNumber) {\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'Received message with lower sequence number, possible queued message', {\r\n expected: expectedSequenceNumber,\r\n received: metadata.sequenceNumber,\r\n messageId: metadata.id\r\n });\r\n } else if (metadata.sequenceNumber > expectedSequenceNumber + 10) {\r\n throw new Error(`Sequence number gap too large: expected around ${expectedSequenceNumber}, got ${metadata.sequenceNumber}`);\r\n }\r\n }\r\n\r\n const messageIv = new Uint8Array(encryptedPayload.messageIv);\r\n const messageData = new Uint8Array(encryptedPayload.messageData);\r\n\r\n const decryptedMessageBuffer = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv: messageIv },\r\n encryptionKey,\r\n messageData\r\n );\r\n\r\n const paddedMessage = new Uint8Array(decryptedMessageBuffer);\r\n const originalMessage = paddedMessage.slice(0, metadata.originalLength);\r\n\r\n const decoder = new TextDecoder();\r\n const message = decoder.decode(originalMessage);\r\n\r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Message decrypted successfully', {\r\n messageId: metadata.id,\r\n sequenceNumber: metadata.sequenceNumber,\r\n messageAge: Math.round(messageAge / 1000) + 's'\r\n });\r\n\r\n return {\r\n message: message,\r\n messageId: metadata.id,\r\n timestamp: metadata.timestamp,\r\n sequenceNumber: metadata.sequenceNumber\r\n };\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Message decryption failed', { error: error.message });\r\n throw new Error(`Failed to decrypt the message: ${error.message}`);\r\n }\r\n }\r\n\r\n // Enhanced input sanitization\r\n static sanitizeMessage(message) {\r\n if (typeof message !== 'string') {\r\n throw new Error('Message must be a string');\r\n }\r\n \r\n return message\r\n .replace(/
+
diff --git a/src/app.jsx b/src/app.jsx
index b436c45..d12d5c3 100644
--- a/src/app.jsx
+++ b/src/app.jsx
@@ -745,7 +745,18 @@
className: 'fas fa-qrcode mr-2'
}),
'Scan QR Code'
- ])
+ ]),
+ // React.createElement('button', {
+ // key: 'bluetooth-btn',
+ // onClick: () => { try { document.dispatchEvent(new CustomEvent('open-bluetooth-transfer', { detail: { role: 'responder' } })); } catch {} },
+ // className: "px-4 py-2 bg-blue-500/10 hover:bg-blue-500/20 text-blue-400 border border-blue-500/20 rounded text-sm font-medium transition-all duration-200"
+ // }, [
+ // React.createElement('i', {
+ // key: 'icon',
+ // className: 'fas fa-bluetooth mr-2'
+ // }),
+ // 'Bluetooth'
+ // ])
]),
React.createElement('textarea', {
key: 'input',
@@ -859,6 +870,17 @@
}),
'Scan QR Code'
]),
+ // React.createElement('button', {
+ // key: 'bluetooth-btn',
+ // onClick: () => { try { document.dispatchEvent(new CustomEvent('open-bluetooth-transfer', { detail: { role: 'initiator' } })); } catch {} },
+ // className: "px-4 py-2 bg-blue-500/10 hover:bg-blue-500/20 text-blue-400 border border-blue-500/20 rounded text-sm font-medium transition-all duration-200"
+ // }, [
+ // React.createElement('i', {
+ // key: 'icon',
+ // className: 'fas fa-bluetooth mr-2'
+ // }),
+ // 'Bluetooth'
+ // ]),
React.createElement('button', {
key: 'process-btn',
onClick: onCreateAnswer,
@@ -1410,6 +1432,23 @@
const [qrCodeUrl, setQrCodeUrl] = React.useState('');
const [showQRScanner, setShowQRScanner] = React.useState(false);
const [showQRScannerModal, setShowQRScannerModal] = React.useState(false);
+
+ // Bluetooth key transfer states
+ const [showBluetoothTransfer, setShowBluetoothTransfer] = React.useState(false);
+ const [bluetoothAutoRole, setBluetoothAutoRole] = React.useState(null);
+
+ React.useEffect(() => {
+ const openBt = (e) => {
+ try {
+ const role = e?.detail?.role || null;
+ setBluetoothAutoRole(role);
+ setShowBluetoothTransfer(true);
+ } catch {}
+ };
+ document.addEventListener('open-bluetooth-transfer', openBt);
+ return () => document.removeEventListener('open-bluetooth-transfer', openBt);
+ }, []);
+ const [bluetoothManager, setBluetoothManager] = React.useState(null);
const [isVerified, setIsVerified] = React.useState(false);
const [securityLevel, setSecurityLevel] = React.useState(null);
@@ -1596,14 +1635,7 @@
}
}, []);
- // Session time ticker - unlimited sessions
- React.useEffect(() => {
- const timer = setInterval(() => {
- // Sessions are unlimited - no time restrictions
- setSessionTimeLeft(0);
- }, 1000);
- return () => clearInterval(timer);
- }, []);
+ // Session time ticker removed - sessions are unlimited
// Sessions are unlimited - no expiration handler needed
@@ -2720,6 +2752,151 @@
return true;
}
};
+
+ // Bluetooth key transfer handlers
+ const handleBluetoothKeyReceived = async (keyData, deviceId) => {
+ try {
+ console.log('Bluetooth key received from device:', deviceId);
+
+ // Convert key data to the format expected by the app
+ const keyString = JSON.stringify(keyData, null, 2);
+
+ // Determine which input to populate based on current mode
+ if (showOfferStep) {
+ // In "Waiting for peer's response" mode - populate answerInput
+ setAnswerInput(keyString);
+ } else {
+ // In "Paste secure invitation" mode - populate offerInput
+ setOfferInput(keyString);
+ }
+
+ setMessages(prev => [...prev, {
+ message: '🔵 Bluetooth key received successfully!',
+ type: 'success'
+ }]);
+
+ // Close Bluetooth transfer modal
+ setShowBluetoothTransfer(false);
+
+ } catch (error) {
+ console.error('Failed to process Bluetooth key:', error);
+ setMessages(prev => [...prev, {
+ message: 'Failed to process Bluetooth key: ' + error.message,
+ type: 'error'
+ }]);
+ }
+ };
+
+ const handleBluetoothStatusChange = (statusType, data) => {
+ console.log('Bluetooth status change:', statusType, data);
+
+ switch (statusType) {
+ case 'bluetooth_ready':
+ setMessages(prev => [...prev, {
+ message: '🔵 Bluetooth ready for key exchange',
+ type: 'info'
+ }]);
+ break;
+ case 'connected':
+ setMessages(prev => [...prev, {
+ message: `🔵 Connected to device: ${data.deviceName}`,
+ type: 'success'
+ }]);
+ break;
+ case 'key_sent':
+ setMessages(prev => [...prev, {
+ message: '🔵 Public key sent via Bluetooth',
+ type: 'success'
+ }]);
+ break;
+ case 'key_received':
+ setMessages(prev => [...prev, {
+ message: '🔵 Public key received via Bluetooth',
+ type: 'success'
+ }]);
+ break;
+ case 'auto_connection_starting':
+ setMessages(prev => [...prev, {
+ message: '🔵 Starting automatic connection...',
+ type: 'info'
+ }]);
+ break;
+ case 'creating_offer':
+ setMessages(prev => [...prev, {
+ message: '🔵 Creating secure offer...',
+ type: 'info'
+ }]);
+ break;
+ case 'offer_sent':
+ setMessages(prev => [...prev, {
+ message: '🔵 Offer sent, waiting for answer...',
+ type: 'info'
+ }]);
+ break;
+ case 'waiting_for_answer':
+ setMessages(prev => [...prev, {
+ message: '🔵 Waiting for answer...',
+ type: 'info'
+ }]);
+ break;
+ case 'processing_answer':
+ setMessages(prev => [...prev, {
+ message: '🔵 Processing answer...',
+ type: 'info'
+ }]);
+ break;
+ case 'waiting_for_verification':
+ setMessages(prev => [...prev, {
+ message: '🔵 Waiting for verification...',
+ type: 'info'
+ }]);
+ break;
+ case 'auto_connection_complete':
+ setMessages(prev => [...prev, {
+ message: '🔵 Automatic connection completed!',
+ type: 'success'
+ }]);
+ break;
+ case 'auto_connection_failed':
+ setMessages(prev => [...prev, {
+ message: '🔵 Automatic connection failed: ' + (data.error || 'Unknown error'),
+ type: 'error'
+ }]);
+ break;
+ }
+ };
+
+ const handleBluetoothError = (error) => {
+ console.error('Bluetooth error:', error);
+ setMessages(prev => [...prev, {
+ message: 'Bluetooth error: ' + error.message,
+ type: 'error'
+ }]);
+ };
+
+ const handleBluetoothAutoConnection = async (connectionData) => {
+ try {
+ console.log('Bluetooth auto connection completed:', connectionData);
+
+ setMessages(prev => [...prev, {
+ message: '🔵 Bluetooth auto connection completed successfully!',
+ type: 'success'
+ }]);
+
+ // Close Bluetooth transfer modal
+ setShowBluetoothTransfer(false);
+
+ // The connection is now established automatically
+ // No need for manual offer/answer processing
+
+ } catch (error) {
+ console.error('Failed to process auto connection:', error);
+ setMessages(prev => [...prev, {
+ message: 'Failed to process auto connection: ' + error.message,
+ type: 'error'
+ }]);
+ }
+ };
const handleCreateOffer = async () => {
try {
@@ -3459,7 +3636,7 @@
return React.createElement('div', {
className: "minimal-bg min-h-screen"
}, [
- React.createElement(EnhancedMinimalHeader, {
+ window.EnhancedMinimalHeader && React.createElement(window.EnhancedMinimalHeader, {
key: 'header',
status: connectionStatus,
fingerprint: keyFingerprint,
@@ -3604,7 +3781,19 @@
])
])
])
- ])
+ ]),
+
+ // Bluetooth Key Transfer Modal
+ showBluetoothTransfer && window.BluetoothKeyTransfer && React.createElement(window.BluetoothKeyTransfer, {
+ key: 'bluetooth-transfer-modal',
+ webrtcManager: webrtcManagerRef.current,
+ onKeyReceived: handleBluetoothKeyReceived,
+ onStatusChange: handleBluetoothStatusChange,
+ onError: handleBluetoothError,
+ onAutoConnection: handleBluetoothAutoConnection,
+ isVisible: showBluetoothTransfer,
+ onClose: () => setShowBluetoothTransfer(false)
+ })
]);
};
diff --git a/src/components/ui/BluetoothKeyTransfer.jsx b/src/components/ui/BluetoothKeyTransfer.jsx
new file mode 100644
index 0000000..fe50731
--- /dev/null
+++ b/src/components/ui/BluetoothKeyTransfer.jsx
@@ -0,0 +1,514 @@
+/**
+ * Bluetooth Key Transfer UI Component
+ *
+ * Provides user interface for Bluetooth key exchange:
+ * - Device discovery and connection
+ * - Key transmission status
+ * - Error handling and fallbacks
+ * - Integration with existing QR/manual methods
+ */
+
+const BluetoothKeyTransfer = ({
+ webrtcManager,
+ onKeyReceived,
+ onStatusChange,
+ onAutoConnection,
+ isVisible = false,
+ onClose
+}) => {
+ const [bluetoothManager, setBluetoothManager] = React.useState(null);
+ const [isSupported, setIsSupported] = React.useState(false);
+ const [isAvailable, setIsAvailable] = React.useState(false);
+ const [isScanning, setIsScanning] = React.useState(false);
+ const [isAdvertising, setIsAdvertising] = React.useState(false);
+ const [connectedDevices, setConnectedDevices] = React.useState([]);
+ const [status, setStatus] = React.useState('idle');
+ const [error, setError] = React.useState(null);
+ const [logs, setLogs] = React.useState([]);
+
+ // Initialize Bluetooth manager
+ React.useEffect(() => {
+ if (isVisible && !bluetoothManager) {
+ initializeBluetooth();
+ }
+ }, [isVisible]);
+
+ // Cleanup on unmount
+ React.useEffect(() => {
+ return () => {
+ if (bluetoothManager) {
+ bluetoothManager.cleanup();
+ }
+ };
+ }, [bluetoothManager]);
+
+ const initializeBluetooth = async () => {
+ try {
+ const manager = new window.BluetoothKeyTransfer(
+ webrtcManager,
+ handleStatusChange,
+ handleKeyReceived,
+ handleError,
+ handleAutoConnection
+ );
+
+ setBluetoothManager(manager);
+
+ // Check support after initialization
+ setTimeout(() => {
+ setIsSupported(manager.isSupported);
+ setIsAvailable(manager.isAvailable);
+ }, 100);
+
+ } catch (error) {
+ console.error('Failed to initialize Bluetooth manager:', error);
+ setError('Failed to initialize Bluetooth: ' + error.message);
+ }
+ };
+
+ const handleStatusChange = (statusType, data) => {
+ setStatus(statusType);
+ addLog(`Status: ${statusType}`, data);
+
+ // Update UI state based on status
+ switch (statusType) {
+ case 'bluetooth_ready':
+ setIsSupported(data.supported);
+ setIsAvailable(data.available);
+ break;
+ case 'scanning_active':
+ setIsScanning(true);
+ break;
+ case 'scanning_stopped':
+ setIsScanning(false);
+ break;
+ case 'advertising_active':
+ setIsAdvertising(true);
+ break;
+ case 'advertising_stopped':
+ setIsAdvertising(false);
+ break;
+ case 'connected':
+ setConnectedDevices(prev => [...prev, {
+ id: data.deviceId,
+ name: data.deviceName,
+ connected: true
+ }]);
+ break;
+ }
+
+ onStatusChange?.(statusType, data);
+ };
+
+ const handleKeyReceived = (keyData, deviceId) => {
+ addLog('Key received from device', { deviceId });
+ onKeyReceived?.(keyData, deviceId);
+ };
+
+ const handleError = (error) => {
+ console.error('Bluetooth error:', error);
+ setError(error.message);
+ addLog('Error', error.message);
+ };
+
+ const handleAutoConnection = (connectionData) => {
+ console.log('Auto connection completed:', connectionData);
+ addLog('Auto Connection Completed', connectionData);
+ onAutoConnection?.(connectionData);
+ };
+
+ const addLog = (message, data = null) => {
+ const timestamp = new Date().toLocaleTimeString();
+ const logEntry = {
+ timestamp,
+ message,
+ data: data ? JSON.stringify(data, null, 2) : null
+ };
+ setLogs(prev => [...prev.slice(-9), logEntry]); // Keep last 10 logs
+ };
+
+ const startScanning = async () => {
+ try {
+ setError(null);
+ await bluetoothManager.startScanning();
+ } catch (error) {
+ setError('Failed to start scanning: ' + error.message);
+ }
+ };
+
+ const stopScanning = async () => {
+ try {
+ await bluetoothManager.stopScanning();
+ } catch (error) {
+ setError('Failed to stop scanning: ' + error.message);
+ }
+ };
+
+ const startAdvertising = async () => {
+ try {
+ setError(null);
+ if (!webrtcManager || !webrtcManager.ecdhKeyPair) {
+ throw new Error('No public key available for advertising');
+ }
+
+ await bluetoothManager.startAdvertising(
+ webrtcManager.ecdhKeyPair.publicKey,
+ 'SecureBit Device'
+ );
+ } catch (error) {
+ setError('Failed to start advertising: ' + error.message);
+ }
+ };
+
+ const stopAdvertising = async () => {
+ try {
+ await bluetoothManager.stopAdvertising();
+ } catch (error) {
+ setError('Failed to stop advertising: ' + error.message);
+ }
+ };
+
+ const sendPublicKey = async (deviceId) => {
+ try {
+ setError(null);
+ if (!webrtcManager || !webrtcManager.ecdhKeyPair) {
+ throw new Error('No public key available for sending');
+ }
+
+ await bluetoothManager.sendPublicKey(
+ webrtcManager.ecdhKeyPair.publicKey,
+ deviceId
+ );
+ } catch (error) {
+ setError('Failed to send public key: ' + error.message);
+ }
+ };
+
+ const clearLogs = () => {
+ setLogs([]);
+ };
+
+ const startAutoConnection = async (deviceId) => {
+ try {
+ setError(null);
+ await bluetoothManager.startAutoConnection(deviceId);
+ } catch (error) {
+ setError('Failed to start auto connection: ' + error.message);
+ }
+ };
+
+ const startAutoConnectionAsResponder = async (deviceId) => {
+ try {
+ setError(null);
+ await bluetoothManager.startAutoConnectionAsResponder(deviceId);
+ } catch (error) {
+ setError('Failed to start auto connection as responder: ' + error.message);
+ }
+ };
+
+ if (!isVisible) return null;
+
+ return React.createElement('div', {
+ className: 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4'
+ }, [
+ React.createElement('div', {
+ key: 'modal',
+ className: 'bg-gray-900 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden'
+ }, [
+ // Header
+ React.createElement('div', {
+ key: 'header',
+ className: 'flex items-center justify-between p-6 border-b border-gray-700'
+ }, [
+ React.createElement('div', {
+ key: 'title',
+ className: 'flex items-center space-x-3'
+ }, [
+ React.createElement('i', {
+ key: 'icon',
+ className: 'fas fa-bluetooth text-blue-400 text-xl'
+ }),
+ React.createElement('h2', {
+ key: 'text',
+ className: 'text-xl font-semibold text-white'
+ }, 'Bluetooth Key Transfer')
+ ]),
+ React.createElement('button', {
+ key: 'close',
+ onClick: onClose,
+ className: 'text-gray-400 hover:text-white transition-colors'
+ }, [
+ React.createElement('i', {
+ className: 'fas fa-times text-xl'
+ })
+ ])
+ ]),
+
+ // Content
+ React.createElement('div', {
+ key: 'content',
+ className: 'p-6 space-y-6 overflow-y-auto max-h-[calc(90vh-200px)]'
+ }, [
+ // Status Section
+ React.createElement('div', {
+ key: 'status',
+ className: 'space-y-4'
+ }, [
+ React.createElement('h3', {
+ key: 'title',
+ className: 'text-lg font-medium text-white'
+ }, 'Bluetooth Status'),
+
+ React.createElement('div', {
+ key: 'indicators',
+ className: 'grid grid-cols-2 gap-4'
+ }, [
+ React.createElement('div', {
+ key: 'support',
+ className: 'flex items-center space-x-2'
+ }, [
+ React.createElement('div', {
+ className: `w-3 h-3 rounded-full ${isSupported ? 'bg-green-500' : 'bg-red-500'}`
+ }),
+ React.createElement('span', {
+ className: 'text-sm text-gray-300'
+ }, 'Bluetooth Supported')
+ ]),
+ React.createElement('div', {
+ key: 'availability',
+ className: 'flex items-center space-x-2'
+ }, [
+ React.createElement('div', {
+ className: `w-3 h-3 rounded-full ${isAvailable ? 'bg-green-500' : 'bg-red-500'}`
+ }),
+ React.createElement('span', {
+ className: 'text-sm text-gray-300'
+ }, 'Bluetooth Available')
+ ])
+ ])
+ ]),
+
+ // Controls Section
+ React.createElement('div', {
+ key: 'controls',
+ className: 'space-y-4'
+ }, [
+ React.createElement('h3', {
+ key: 'title',
+ className: 'text-lg font-medium text-white'
+ }, 'Key Exchange'),
+
+ React.createElement('div', {
+ key: 'buttons',
+ className: 'grid grid-cols-1 sm:grid-cols-2 gap-4'
+ }, [
+ // Scanning Controls
+ React.createElement('div', {
+ key: 'scanning',
+ className: 'space-y-2'
+ }, [
+ React.createElement('h4', {
+ key: 'title',
+ className: 'text-sm font-medium text-gray-300'
+ }, 'Discover Devices'),
+ React.createElement('button', {
+ key: 'scan',
+ onClick: isScanning ? stopScanning : startScanning,
+ disabled: !isSupported || !isAvailable,
+ className: `w-full px-4 py-2 rounded-lg font-medium transition-colors ${
+ isScanning
+ ? 'bg-red-600 hover:bg-red-700 text-white'
+ : 'bg-blue-600 hover:bg-blue-700 text-white disabled:bg-gray-600 disabled:cursor-not-allowed'
+ }`
+ }, [
+ React.createElement('i', {
+ key: 'icon',
+ className: `fas ${isScanning ? 'fa-stop' : 'fa-search'} mr-2`
+ }),
+ isScanning ? 'Stop Scanning' : 'Start Scanning'
+ ])
+ ]),
+
+ // Advertising Controls
+ React.createElement('div', {
+ key: 'advertising',
+ className: 'space-y-2'
+ }, [
+ React.createElement('h4', {
+ key: 'title',
+ className: 'text-sm font-medium text-gray-300'
+ }, 'Share Your Key'),
+ React.createElement('button', {
+ key: 'advertise',
+ onClick: isAdvertising ? stopAdvertising : startAdvertising,
+ disabled: !isSupported || !isAvailable,
+ className: `w-full px-4 py-2 rounded-lg font-medium transition-colors ${
+ isAdvertising
+ ? 'bg-red-600 hover:bg-red-700 text-white'
+ : 'bg-green-600 hover:bg-green-700 text-white disabled:bg-gray-600 disabled:cursor-not-allowed'
+ }`
+ }, [
+ React.createElement('i', {
+ key: 'icon',
+ className: `fas ${isAdvertising ? 'fa-stop' : 'fa-broadcast-tower'} mr-2`
+ }),
+ isAdvertising ? 'Stop Sharing' : 'Start Sharing'
+ ])
+ ])
+ ])
+ ]),
+
+ // Connected Devices
+ connectedDevices.length > 0 && React.createElement('div', {
+ key: 'devices',
+ className: 'space-y-4'
+ }, [
+ React.createElement('h3', {
+ key: 'title',
+ className: 'text-lg font-medium text-white'
+ }, 'Connected Devices'),
+
+ React.createElement('div', {
+ key: 'list',
+ className: 'space-y-2'
+ }, connectedDevices.map(device =>
+ React.createElement('div', {
+ key: device.id,
+ className: 'flex items-center justify-between p-3 bg-gray-800 rounded-lg'
+ }, [
+ React.createElement('div', {
+ key: 'info',
+ className: 'flex items-center space-x-3'
+ }, [
+ React.createElement('i', {
+ key: 'icon',
+ className: 'fas fa-mobile-alt text-blue-400'
+ }),
+ React.createElement('span', {
+ key: 'name',
+ className: 'text-white'
+ }, device.name)
+ ]),
+ React.createElement('div', {
+ key: 'buttons',
+ className: 'flex space-x-2'
+ }, [
+ React.createElement('button', {
+ key: 'auto-connect',
+ onClick: () => startAutoConnection(device.id),
+ className: 'px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-sm rounded transition-colors'
+ }, 'Auto Connect'),
+ React.createElement('button', {
+ key: 'auto-respond',
+ onClick: () => startAutoConnectionAsResponder(device.id),
+ className: 'px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded transition-colors'
+ }, 'Auto Respond'),
+ React.createElement('button', {
+ key: 'send',
+ onClick: () => sendPublicKey(device.id),
+ className: 'px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded transition-colors'
+ }, 'Send Key')
+ ])
+ ])
+ ))
+ ]),
+
+ // Error Display
+ error && React.createElement('div', {
+ key: 'error',
+ className: 'p-4 bg-red-900 border border-red-700 rounded-lg'
+ }, [
+ React.createElement('div', {
+ key: 'header',
+ className: 'flex items-center space-x-2 mb-2'
+ }, [
+ React.createElement('i', {
+ key: 'icon',
+ className: 'fas fa-exclamation-triangle text-red-400'
+ }),
+ React.createElement('h4', {
+ key: 'title',
+ className: 'text-red-400 font-medium'
+ }, 'Error')
+ ]),
+ React.createElement('p', {
+ key: 'message',
+ className: 'text-red-300 text-sm'
+ }, error)
+ ]),
+
+ // Logs Section
+ React.createElement('div', {
+ key: 'logs',
+ className: 'space-y-4'
+ }, [
+ React.createElement('div', {
+ key: 'header',
+ className: 'flex items-center justify-between'
+ }, [
+ React.createElement('h3', {
+ key: 'title',
+ className: 'text-lg font-medium text-white'
+ }, 'Activity Log'),
+ React.createElement('button', {
+ key: 'clear',
+ onClick: clearLogs,
+ className: 'text-sm text-gray-400 hover:text-white transition-colors'
+ }, 'Clear')
+ ]),
+
+ React.createElement('div', {
+ key: 'log-list',
+ className: 'bg-gray-800 rounded-lg p-4 max-h-40 overflow-y-auto'
+ }, logs.length === 0 ?
+ React.createElement('p', {
+ key: 'empty',
+ className: 'text-gray-400 text-sm text-center'
+ }, 'No activity yet') :
+ logs.map((log, index) =>
+ React.createElement('div', {
+ key: index,
+ className: 'text-xs text-gray-300 mb-1'
+ }, [
+ React.createElement('span', {
+ key: 'time',
+ className: 'text-gray-500'
+ }, `[${log.timestamp}] `),
+ React.createElement('span', {
+ key: 'message',
+ className: 'text-gray-300'
+ }, log.message),
+ log.data && React.createElement('pre', {
+ key: 'data',
+ className: 'text-gray-400 mt-1 ml-4'
+ }, log.data)
+ ])
+ )
+ )
+ ])
+ ]),
+
+ // Footer
+ React.createElement('div', {
+ key: 'footer',
+ className: 'flex items-center justify-between p-6 border-t border-gray-700'
+ }, [
+ React.createElement('div', {
+ key: 'info',
+ className: 'text-sm text-gray-400'
+ }, 'Bluetooth key exchange provides secure device-to-device communication'),
+
+ React.createElement('button', {
+ key: 'close-footer',
+ onClick: onClose,
+ className: 'px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors'
+ }, 'Close')
+ ])
+ ])
+ ]);
+};
+
+// Export component
+if (typeof window !== 'undefined') {
+ window.BluetoothKeyTransfer = BluetoothKeyTransfer;
+}
diff --git a/src/components/ui/Header.jsx b/src/components/ui/Header.jsx
index 6221840..71458d1 100644
--- a/src/components/ui/Header.jsx
+++ b/src/components/ui/Header.jsx
@@ -9,6 +9,10 @@ const EnhancedMinimalHeader = ({
}) => {
const [realSecurityLevel, setRealSecurityLevel] = React.useState(null);
const [lastSecurityUpdate, setLastSecurityUpdate] = React.useState(0);
+ // Added local session state to remove references errors after session timer removal
+ const [hasActiveSession, setHasActiveSession] = React.useState(false);
+ const [currentTimeLeft, setCurrentTimeLeft] = React.useState(0);
+ const [sessionType, setSessionType] = React.useState('unknown');
// ============================================
// FIXED SECURITY UPDATE LOGIC
@@ -154,7 +158,7 @@ const EnhancedMinimalHeader = ({
setHasActiveSession(true);
setCurrentTimeLeft(0);
setSessionType('premium'); // All features enabled
- }, [sessionTimeLeft]);
+ }, []);
React.useEffect(() => {
const handleForceUpdate = (event) => {
diff --git a/src/network/EnhancedSecureWebRTCManager.js b/src/network/EnhancedSecureWebRTCManager.js
index 0c20ea2..c566c2b 100644
--- a/src/network/EnhancedSecureWebRTCManager.js
+++ b/src/network/EnhancedSecureWebRTCManager.js
@@ -6709,41 +6709,41 @@ async processMessage(data) {
// Method for automatic feature enablement with stability check
async autoEnableSecurityFeatures() {
this._secureLog('info', 'Starting graduated security activation - all features enabled');
-
- const checkStability = () => {
- const isStable = this.isConnected() &&
- this.isVerified &&
- this.connectionAttempts === 0 &&
- this.messageQueue.length === 0 &&
- this.peerConnection?.connectionState === 'connected';
- return isStable;
- };
-
- await this.calculateAndReportSecurityLevel();
- this.notifySecurityUpgrade(1);
-
+
+ const checkStability = () => {
+ const isStable = this.isConnected() &&
+ this.isVerified &&
+ this.connectionAttempts === 0 &&
+ this.messageQueue.length === 0 &&
+ this.peerConnection?.connectionState === 'connected';
+ return isStable;
+ };
+
+ await this.calculateAndReportSecurityLevel();
+ this.notifySecurityUpgrade(1);
+
// Enable all security stages progressively
setTimeout(async () => {
if (checkStability()) {
this.enableStage2Security();
await this.calculateAndReportSecurityLevel();
- setTimeout(async () => {
- if (checkStability()) {
- this.enableStage3Security();
- await this.calculateAndReportSecurityLevel();
-
- setTimeout(async () => {
- if (checkStability()) {
- this.enableStage4Security();
- await this.calculateAndReportSecurityLevel();
- }
- }, 20000);
- }
- }, 15000);
+ setTimeout(async () => {
+ if (checkStability()) {
+ this.enableStage3Security();
+ await this.calculateAndReportSecurityLevel();
+
+ setTimeout(async () => {
+ if (checkStability()) {
+ this.enableStage4Security();
+ await this.calculateAndReportSecurityLevel();
+ }
+ }, 20000);
+ }
+ }, 15000);
}
}, 10000);
- }
+ }
// ============================================
// CONNECTION MANAGEMENT WITH ENHANCED SECURITY
diff --git a/src/scripts/app-boot.js b/src/scripts/app-boot.js
index e98c931..a4eec59 100644
--- a/src/scripts/app-boot.js
+++ b/src/scripts/app-boot.js
@@ -1,6 +1,7 @@
import { EnhancedSecureCryptoUtils } from '../crypto/EnhancedSecureCryptoUtils.js';
import { EnhancedSecureWebRTCManager } from '../network/EnhancedSecureWebRTCManager.js';
import { EnhancedSecureFileTransfer } from '../transfer/EnhancedSecureFileTransfer.js';
+import BluetoothKeyTransfer from '../transfer/BluetoothKeyTransfer.js';
// Import UI components (side-effect: they attach themselves to window.*)
import '../components/ui/Header.jsx';
@@ -11,11 +12,13 @@ import '../components/ui/Testimonials.jsx';
import '../components/ui/ComparisonTable.jsx';
import '../components/ui/Roadmap.jsx';
import '../components/ui/FileTransfer.jsx';
+import '../components/ui/BluetoothKeyTransfer.jsx';
// Expose to global for legacy usage inside app code
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
+window.BluetoothKeyTransfer = BluetoothKeyTransfer;
// Mount application once DOM and modules are ready
const start = () => {
diff --git a/src/scripts/bootstrap-modules.js b/src/scripts/bootstrap-modules.js
index d05bb35..2ad8e89 100644
--- a/src/scripts/bootstrap-modules.js
+++ b/src/scripts/bootstrap-modules.js
@@ -3,10 +3,11 @@
(async () => {
try {
const timestamp = Date.now();
- const [cryptoModule, webrtcModule, fileTransferModule] = await Promise.all([
+ const [cryptoModule, webrtcModule, fileTransferModule, bluetoothModule] = await Promise.all([
import(`../crypto/EnhancedSecureCryptoUtils.js?v=${timestamp}`),
import(`../network/EnhancedSecureWebRTCManager.js?v=${timestamp}`),
import(`../transfer/EnhancedSecureFileTransfer.js?v=${timestamp}`),
+ import(`../transfer/BluetoothKeyTransfer.js?v=${timestamp}`),
]);
const { EnhancedSecureCryptoUtils } = cryptoModule;
@@ -15,6 +16,8 @@
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
const { EnhancedSecureFileTransfer } = fileTransferModule;
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
+ const { default: BluetoothKeyTransfer } = bluetoothModule;
+ window.BluetoothKeyTransfer = BluetoothKeyTransfer;
async function loadReactComponent(path) {
const response = await fetch(`${path}?v=${timestamp}`);
@@ -33,6 +36,7 @@
loadReactComponent('../components/ui/Testimonials.jsx'),
loadReactComponent('../components/ui/Roadmap.jsx'),
loadReactComponent('../components/ui/FileTransfer.jsx'),
+ loadReactComponent('../components/ui/BluetoothKeyTransfer.jsx'),
]);
if (typeof window.initializeApp === 'function') {
diff --git a/src/transfer/BluetoothKeyTransfer.js b/src/transfer/BluetoothKeyTransfer.js
new file mode 100644
index 0000000..a03d258
--- /dev/null
+++ b/src/transfer/BluetoothKeyTransfer.js
@@ -0,0 +1,771 @@
+/**
+ * Bluetooth Key Transfer Module for SecureBit.chat
+ *
+ * Features:
+ * - Secure Bluetooth Low Energy (BLE) key exchange
+ * - Automatic device discovery and pairing
+ * - Encrypted key transmission
+ * - Fallback to manual/QR methods
+ * - Cross-platform compatibility
+ *
+ * Security:
+ * - Uses BLE advertising for device discovery
+ * - Encrypts key data before transmission
+ * - Implements secure pairing protocols
+ * - Validates received keys before acceptance
+ */
+
+class BluetoothKeyTransfer {
+ constructor(webrtcManager, onStatusChange, onKeyReceived, onError, onAutoConnection) {
+ this.webrtcManager = webrtcManager;
+ this.onStatusChange = onStatusChange;
+ this.onKeyReceived = onKeyReceived;
+ this.onError = onError;
+ this.onAutoConnection = onAutoConnection;
+
+ // Bluetooth state
+ this.isSupported = false;
+ this.isAvailable = false;
+ this.isScanning = false;
+ this.isAdvertising = false;
+ this.connectedDevices = new Map();
+ this.advertisingData = null;
+
+ // Service and characteristic UUIDs
+ this.SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e'; // Nordic UART Service
+ this.TX_CHARACTERISTIC_UUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e'; // TX Characteristic
+ this.RX_CHARACTERISTIC_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e'; // RX Characteristic
+
+ // Key transfer protocol
+ this.PROTOCOL_VERSION = '1.0';
+ this.MAX_CHUNK_SIZE = 20; // BLE characteristic max size
+ this.TRANSFER_TIMEOUT = 30000; // 30 seconds
+
+ this.init();
+ }
+
+ async init() {
+ try {
+ // Check Bluetooth support
+ if (!navigator.bluetooth) {
+ this.log('warn', 'Bluetooth API not supported in this browser');
+ return;
+ }
+
+ this.isSupported = true;
+
+ // Check if Bluetooth is available
+ const available = await navigator.bluetooth.getAvailability();
+ this.isAvailable = available;
+
+ if (!available) {
+ this.log('warn', 'Bluetooth is not available on this device');
+ return;
+ }
+
+ this.log('info', 'Bluetooth Key Transfer initialized successfully');
+ this.onStatusChange?.('bluetooth_ready', { supported: true, available: true });
+
+ } catch (error) {
+ this.log('error', 'Failed to initialize Bluetooth Key Transfer', error);
+ this.onError?.(error);
+ }
+ }
+
+ // ============================================
+ // PUBLIC METHODS
+ // ============================================
+
+ /**
+ * Start advertising this device for key exchange
+ */
+ async startAdvertising(publicKey, deviceName = 'SecureBit Device') {
+ if (!this.isSupported || !this.isAvailable) {
+ throw new Error('Bluetooth not supported or available');
+ }
+
+ try {
+ this.log('info', 'Starting Bluetooth advertising...');
+ this.onStatusChange?.('advertising_starting', { deviceName });
+
+ // Prepare advertising data
+ const keyData = await this.prepareKeyData(publicKey);
+ this.advertisingData = {
+ deviceName,
+ keyData,
+ timestamp: Date.now(),
+ protocolVersion: this.PROTOCOL_VERSION
+ };
+
+ // Start advertising
+ const options = {
+ filters: [{
+ services: [this.SERVICE_UUID]
+ }],
+ optionalServices: [this.SERVICE_UUID]
+ };
+
+ this.isAdvertising = true;
+ this.onStatusChange?.('advertising_active', { deviceName });
+
+ this.log('info', 'Bluetooth advertising started successfully');
+ return true;
+
+ } catch (error) {
+ this.log('error', 'Failed to start Bluetooth advertising', error);
+ this.isAdvertising = false;
+ this.onError?.(error);
+ throw error;
+ }
+ }
+
+ /**
+ * Stop advertising
+ */
+ async stopAdvertising() {
+ try {
+ this.isAdvertising = false;
+ this.advertisingData = null;
+ this.onStatusChange?.('advertising_stopped');
+ this.log('info', 'Bluetooth advertising stopped');
+ } catch (error) {
+ this.log('error', 'Failed to stop advertising', error);
+ }
+ }
+
+ /**
+ * Start scanning for nearby devices
+ */
+ async startScanning() {
+ if (!this.isSupported || !this.isAvailable) {
+ throw new Error('Bluetooth not supported or available');
+ }
+
+ try {
+ this.log('info', 'Starting Bluetooth device scan...');
+ this.onStatusChange?.('scanning_starting');
+
+ const options = {
+ filters: [{
+ services: [this.SERVICE_UUID]
+ }],
+ optionalServices: [this.SERVICE_UUID]
+ };
+
+ this.isScanning = true;
+ this.onStatusChange?.('scanning_active');
+
+ // Start scanning
+ const device = await navigator.bluetooth.requestDevice(options);
+
+ if (device) {
+ this.log('info', 'Device selected:', device.name);
+ await this.connectToDevice(device);
+ }
+
+ } catch (error) {
+ this.log('error', 'Failed to start scanning', error);
+ this.isScanning = false;
+ this.onError?.(error);
+ throw error;
+ }
+ }
+
+ /**
+ * Stop scanning
+ */
+ async stopScanning() {
+ try {
+ this.isScanning = false;
+ this.onStatusChange?.('scanning_stopped');
+ this.log('info', 'Bluetooth scanning stopped');
+ } catch (error) {
+ this.log('error', 'Failed to stop scanning', error);
+ }
+ }
+
+ /**
+ * Send public key to connected device
+ */
+ async sendPublicKey(publicKey, deviceId) {
+ try {
+ const device = this.connectedDevices.get(deviceId);
+ if (!device) {
+ throw new Error('Device not connected');
+ }
+
+ this.log('info', 'Sending public key to device:', deviceId);
+ this.onStatusChange?.('key_sending', { deviceId });
+
+ const keyData = await this.prepareKeyData(publicKey);
+ await this.sendData(keyData, device);
+
+ this.onStatusChange?.('key_sent', { deviceId });
+ this.log('info', 'Public key sent successfully');
+
+ } catch (error) {
+ this.log('error', 'Failed to send public key', error);
+ this.onError?.(error);
+ throw error;
+ }
+ }
+
+ /**
+ * Start automatic connection process (offer → answer → verification)
+ */
+ async startAutoConnection(deviceId) {
+ try {
+ this.log('info', 'Starting automatic connection process');
+ this.onStatusChange?.('auto_connection_starting', { deviceId });
+
+ if (!this.webrtcManager) {
+ throw new Error('WebRTC Manager not available');
+ }
+
+ // Step 1: Create and send offer
+ this.onStatusChange?.('creating_offer', { deviceId });
+ const offer = await this.webrtcManager.createSecureOffer();
+
+ // Send offer via Bluetooth
+ await this.sendConnectionData(offer, deviceId, 'offer');
+ this.onStatusChange?.('offer_sent', { deviceId });
+
+ // Step 2: Wait for answer
+ this.onStatusChange?.('waiting_for_answer', { deviceId });
+ const answer = await this.waitForConnectionData(deviceId, 'answer');
+
+ // Step 3: Process answer
+ this.onStatusChange?.('processing_answer', { deviceId });
+ await this.webrtcManager.createSecureAnswer(answer);
+
+ // Step 4: Wait for verification
+ this.onStatusChange?.('waiting_for_verification', { deviceId });
+ const verification = await this.waitForConnectionData(deviceId, 'verification');
+
+ // Step 5: Complete connection
+ this.onStatusChange?.('completing_connection', { deviceId });
+ await this.completeConnection(verification, deviceId);
+
+ this.onStatusChange?.('auto_connection_complete', { deviceId });
+ this.log('info', 'Automatic connection completed successfully');
+
+ } catch (error) {
+ this.log('error', 'Automatic connection failed', error);
+ this.onStatusChange?.('auto_connection_failed', { deviceId, error: error.message });
+ this.onError?.(error);
+ throw error;
+ }
+ }
+
+ /**
+ * Start automatic connection as responder (wait for offer → create answer → send verification)
+ */
+ async startAutoConnectionAsResponder(deviceId) {
+ try {
+ this.log('info', 'Starting automatic connection as responder');
+ this.onStatusChange?.('auto_connection_responder_starting', { deviceId });
+
+ if (!this.webrtcManager) {
+ throw new Error('WebRTC Manager not available');
+ }
+
+ // Step 1: Wait for offer
+ this.onStatusChange?.('waiting_for_offer', { deviceId });
+ const offer = await this.waitForConnectionData(deviceId, 'offer');
+
+ // Step 2: Create and send answer
+ this.onStatusChange?.('creating_answer', { deviceId });
+ const answer = await this.webrtcManager.createSecureAnswer(offer);
+
+ // Send answer via Bluetooth
+ await this.sendConnectionData(answer, deviceId, 'answer');
+ this.onStatusChange?.('answer_sent', { deviceId });
+
+ // Step 3: Send verification
+ this.onStatusChange?.('sending_verification', { deviceId });
+ const verification = await this.createVerificationData();
+ await this.sendConnectionData(verification, deviceId, 'verification');
+
+ this.onStatusChange?.('auto_connection_responder_complete', { deviceId });
+ this.log('info', 'Automatic connection as responder completed successfully');
+
+ } catch (error) {
+ this.log('error', 'Automatic connection as responder failed', error);
+ this.onStatusChange?.('auto_connection_responder_failed', { deviceId, error: error.message });
+ this.onError?.(error);
+ throw error;
+ }
+ }
+
+ // ============================================
+ // PRIVATE METHODS
+ // ============================================
+
+ /**
+ * Connect to a discovered device
+ */
+ async connectToDevice(device) {
+ try {
+ this.log('info', 'Connecting to device:', device.name);
+ this.onStatusChange?.('connecting', { deviceName: device.name });
+
+ const server = await device.gatt.connect();
+ const service = await server.getPrimaryService(this.SERVICE_UUID);
+
+ // Get characteristics
+ const txCharacteristic = await service.getCharacteristic(this.TX_CHARACTERISTIC_UUID);
+ const rxCharacteristic = await service.getCharacteristic(this.RX_CHARACTERISTIC_UUID);
+
+ // Set up data reception
+ rxCharacteristic.addEventListener('characteristicvaluechanged', (event) => {
+ this.handleReceivedData(event, device.id);
+ });
+ await rxCharacteristic.startNotifications();
+
+ // Store device connection
+ this.connectedDevices.set(device.id, {
+ device,
+ server,
+ service,
+ txCharacteristic,
+ rxCharacteristic,
+ connected: true
+ });
+
+ this.onStatusChange?.('connected', { deviceId: device.id, deviceName: device.name });
+ this.log('info', 'Successfully connected to device:', device.name);
+
+ } catch (error) {
+ this.log('error', 'Failed to connect to device', error);
+ this.onError?.(error);
+ throw error;
+ }
+ }
+
+ /**
+ * Send data to connected device
+ */
+ async sendData(data, device) {
+ try {
+ const { txCharacteristic } = device;
+ const dataString = JSON.stringify(data);
+ const chunks = this.chunkString(dataString, this.MAX_CHUNK_SIZE);
+
+ // Send chunks sequentially
+ for (let i = 0; i < chunks.length; i++) {
+ const chunk = chunks[i];
+ const chunkData = new TextEncoder().encode(chunk);
+ await txCharacteristic.writeValue(chunkData);
+
+ // Small delay between chunks
+ await new Promise(resolve => setTimeout(resolve, 10));
+ }
+
+ this.log('info', `Sent ${chunks.length} chunks to device`);
+
+ } catch (error) {
+ this.log('error', 'Failed to send data', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Handle received data from device
+ */
+ async handleReceivedData(event, deviceId) {
+ try {
+ const value = event.target.value;
+ const data = new TextDecoder().decode(value);
+
+ // Try to parse as connection data first
+ try {
+ const connectionData = JSON.parse(data);
+ if (connectionData.type && ['offer', 'answer', 'verification'].includes(connectionData.type)) {
+ this.handleConnectionData(connectionData, deviceId);
+ return;
+ }
+ } catch (e) {
+ // Not connection data, continue with key processing
+ }
+
+ // Process received data as key data
+ const keyData = await this.processReceivedData(data, deviceId);
+ if (keyData) {
+ this.onKeyReceived?.(keyData, deviceId);
+ }
+
+ } catch (error) {
+ this.log('error', 'Failed to handle received data', error);
+ this.onError?.(error);
+ }
+ }
+
+ /**
+ * Handle connection data (offer, answer, verification)
+ */
+ async handleConnectionData(connectionData, deviceId) {
+ try {
+ this.log('info', `Received ${connectionData.type} from device:`, deviceId);
+
+ // Store connection data for waiting processes
+ if (!this.connectionDataBuffer) {
+ this.connectionDataBuffer = new Map();
+ }
+
+ if (!this.connectionDataBuffer.has(deviceId)) {
+ this.connectionDataBuffer.set(deviceId, new Map());
+ }
+
+ this.connectionDataBuffer.get(deviceId).set(connectionData.type, connectionData);
+
+ // Notify waiting processes
+ this.onStatusChange?.(`${connectionData.type}_received`, { deviceId, data: connectionData });
+
+ } catch (error) {
+ this.log('error', 'Failed to handle connection data', error);
+ this.onError?.(error);
+ }
+ }
+
+ /**
+ * Prepare key data for transmission
+ */
+ async prepareKeyData(publicKey) {
+ try {
+ // Export public key
+ const exportedKey = await crypto.subtle.exportKey('spki', publicKey);
+ const keyArray = new Uint8Array(exportedKey);
+
+ // Create secure payload
+ const payload = {
+ type: 'public_key',
+ key: Array.from(keyArray),
+ timestamp: Date.now(),
+ protocolVersion: this.PROTOCOL_VERSION,
+ deviceId: await this.getDeviceId()
+ };
+
+ // Sign payload for integrity
+ const signature = await this.signPayload(payload);
+ payload.signature = signature;
+
+ return payload;
+
+ } catch (error) {
+ this.log('error', 'Failed to prepare key data', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Process received key data
+ */
+ async processReceivedData(data, deviceId) {
+ try {
+ const payload = JSON.parse(data);
+
+ // Validate payload
+ if (!this.validatePayload(payload)) {
+ throw new Error('Invalid payload received');
+ }
+
+ // Verify signature
+ if (!await this.verifyPayload(payload)) {
+ throw new Error('Payload signature verification failed');
+ }
+
+ // Import public key
+ const publicKey = await crypto.subtle.importKey(
+ 'spki',
+ new Uint8Array(payload.key),
+ { name: 'ECDH', namedCurve: 'P-384' },
+ false,
+ []
+ );
+
+ this.log('info', 'Successfully processed received key data');
+ return {
+ publicKey,
+ deviceId,
+ timestamp: payload.timestamp,
+ protocolVersion: payload.protocolVersion
+ };
+
+ } catch (error) {
+ this.log('error', 'Failed to process received data', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Sign payload for integrity
+ */
+ async signPayload(payload) {
+ try {
+ // Use WebRTC manager's signing key if available
+ if (this.webrtcManager && this.webrtcManager.signingKeyPair) {
+ const data = new TextEncoder().encode(JSON.stringify(payload));
+ const signature = await crypto.subtle.sign(
+ { name: 'ECDSA', hash: 'SHA-384' },
+ this.webrtcManager.signingKeyPair.privateKey,
+ data
+ );
+ return Array.from(new Uint8Array(signature));
+ }
+
+ // Fallback: simple hash
+ const data = new TextEncoder().encode(JSON.stringify(payload));
+ const hash = await crypto.subtle.digest('SHA-256', data);
+ return Array.from(new Uint8Array(hash));
+
+ } catch (error) {
+ this.log('error', 'Failed to sign payload', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Verify payload signature
+ */
+ async verifyPayload(payload) {
+ try {
+ const { signature, ...payloadWithoutSig } = payload;
+
+ // Use WebRTC manager's signing key if available
+ if (this.webrtcManager && this.webrtcManager.signingKeyPair) {
+ const data = new TextEncoder().encode(JSON.stringify(payloadWithoutSig));
+ const isValid = await crypto.subtle.verify(
+ { name: 'ECDSA', hash: 'SHA-384' },
+ this.webrtcManager.signingKeyPair.publicKey,
+ new Uint8Array(signature),
+ data
+ );
+ return isValid;
+ }
+
+ // Fallback: simple hash comparison
+ const data = new TextEncoder().encode(JSON.stringify(payloadWithoutSig));
+ const hash = await crypto.subtle.digest('SHA-256', data);
+ const expectedHash = Array.from(new Uint8Array(hash));
+ return JSON.stringify(signature) === JSON.stringify(expectedHash);
+
+ } catch (error) {
+ this.log('error', 'Failed to verify payload', error);
+ return false;
+ }
+ }
+
+ /**
+ * Validate received payload
+ */
+ validatePayload(payload) {
+ return (
+ payload &&
+ payload.type === 'public_key' &&
+ payload.key &&
+ Array.isArray(payload.key) &&
+ payload.timestamp &&
+ payload.protocolVersion &&
+ payload.signature &&
+ Array.isArray(payload.signature)
+ );
+ }
+
+ /**
+ * Get unique device ID
+ */
+ async getDeviceId() {
+ try {
+ // Try to get a unique device identifier
+ if (navigator.userAgentData && navigator.userAgentData.getHighEntropyValues) {
+ const values = await navigator.userAgentData.getHighEntropyValues(['model']);
+ return values.model || 'unknown-device';
+ }
+
+ // Fallback to user agent hash
+ const userAgent = navigator.userAgent;
+ const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(userAgent));
+ return Array.from(new Uint8Array(hash)).slice(0, 8).map(b => b.toString(16).padStart(2, '0')).join('');
+
+ } catch (error) {
+ return 'unknown-device';
+ }
+ }
+
+ /**
+ * Send connection data (offer, answer, verification)
+ */
+ async sendConnectionData(data, deviceId, type) {
+ try {
+ const device = this.connectedDevices.get(deviceId);
+ if (!device) {
+ throw new Error('Device not connected');
+ }
+
+ const connectionData = {
+ type: type,
+ data: data,
+ timestamp: Date.now(),
+ protocolVersion: this.PROTOCOL_VERSION
+ };
+
+ await this.sendData(connectionData, device);
+ this.log('info', `Sent ${type} to device:`, deviceId);
+
+ } catch (error) {
+ this.log('error', `Failed to send ${type}`, error);
+ throw error;
+ }
+ }
+
+ /**
+ * Wait for specific connection data type
+ */
+ async waitForConnectionData(deviceId, type, timeout = 30000) {
+ return new Promise((resolve, reject) => {
+ const timeoutId = setTimeout(() => {
+ reject(new Error(`Timeout waiting for ${type}`));
+ }, timeout);
+
+ const checkForData = () => {
+ if (this.connectionDataBuffer &&
+ this.connectionDataBuffer.has(deviceId) &&
+ this.connectionDataBuffer.get(deviceId).has(type)) {
+
+ clearTimeout(timeoutId);
+ const data = this.connectionDataBuffer.get(deviceId).get(type);
+ this.connectionDataBuffer.get(deviceId).delete(type);
+ resolve(data.data);
+ } else {
+ setTimeout(checkForData, 100);
+ }
+ };
+
+ checkForData();
+ });
+ }
+
+ /**
+ * Create verification data
+ */
+ async createVerificationData() {
+ try {
+ if (!this.webrtcManager || !this.webrtcManager.keyFingerprint) {
+ throw new Error('WebRTC Manager or key fingerprint not available');
+ }
+
+ return {
+ fingerprint: this.webrtcManager.keyFingerprint,
+ verificationCode: this.webrtcManager.verificationCode || 'auto-verified',
+ timestamp: Date.now()
+ };
+
+ } catch (error) {
+ this.log('error', 'Failed to create verification data', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Complete connection process
+ */
+ async completeConnection(verification, deviceId) {
+ try {
+ // Validate verification data
+ if (verification.fingerprint && this.webrtcManager.keyFingerprint) {
+ if (verification.fingerprint !== this.webrtcManager.keyFingerprint) {
+ throw new Error('Key fingerprint mismatch');
+ }
+ }
+
+ // Notify auto connection completion
+ this.onAutoConnection?.({
+ deviceId,
+ fingerprint: verification.fingerprint,
+ verificationCode: verification.verificationCode,
+ timestamp: Date.now()
+ });
+
+ this.log('info', 'Connection completed successfully');
+
+ } catch (error) {
+ this.log('error', 'Failed to complete connection', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Split string into chunks
+ */
+ chunkString(str, chunkSize) {
+ const chunks = [];
+ for (let i = 0; i < str.length; i += chunkSize) {
+ chunks.push(str.slice(i, i + chunkSize));
+ }
+ return chunks;
+ }
+
+ /**
+ * Logging utility
+ */
+ log(level, message, data = null) {
+ const timestamp = new Date().toISOString();
+ const logMessage = `[BluetoothKeyTransfer ${timestamp}] ${message}`;
+
+ switch (level) {
+ case 'error':
+ console.error(logMessage, data);
+ break;
+ case 'warn':
+ console.warn(logMessage, data);
+ break;
+ case 'info':
+ console.info(logMessage, data);
+ break;
+ default:
+ console.log(logMessage, data);
+ }
+ }
+
+ // ============================================
+ // CLEANUP METHODS
+ // ============================================
+
+ /**
+ * Disconnect from all devices
+ */
+ async disconnectAll() {
+ try {
+ for (const [deviceId, device] of this.connectedDevices) {
+ if (device.connected && device.server) {
+ device.server.disconnect();
+ }
+ }
+ this.connectedDevices.clear();
+ this.log('info', 'Disconnected from all devices');
+ } catch (error) {
+ this.log('error', 'Failed to disconnect devices', error);
+ }
+ }
+
+ /**
+ * Cleanup resources
+ */
+ async cleanup() {
+ try {
+ await this.stopAdvertising();
+ await this.stopScanning();
+ await this.disconnectAll();
+ this.log('info', 'Bluetooth Key Transfer cleaned up');
+ } catch (error) {
+ this.log('error', 'Failed to cleanup Bluetooth Key Transfer', error);
+ }
+ }
+}
+
+// Export for use in other modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = BluetoothKeyTransfer;
+} else if (typeof window !== 'undefined') {
+ window.BluetoothKeyTransfer = BluetoothKeyTransfer;
+}
diff --git a/sw.js b/sw.js
index ffc87f8..13e046b 100644
--- a/sw.js
+++ b/sw.js
@@ -27,7 +27,11 @@ const STATIC_ASSETS = [
'/src/pwa/pwa-manager.js',
'/src/pwa/install-prompt.js',
'/src/scripts/pwa-register.js',
- '/src/scripts/pwa-offline-test.js'
+ '/src/scripts/pwa-offline-test.js',
+
+ // Bluetooth key transfer (PWA feature)
+ '/src/transfer/BluetoothKeyTransfer.js',
+ '/src/components/ui/BluetoothKeyTransfer.jsx'
];
// Sensitive files that should never be cached
diff --git a/test-bluetooth.js b/test-bluetooth.js
new file mode 100644
index 0000000..93f1c0c
--- /dev/null
+++ b/test-bluetooth.js
@@ -0,0 +1,455 @@
+// src/components/ui/BluetoothKeyTransfer.jsx
+var BluetoothKeyTransfer = ({
+ webrtcManager,
+ onKeyReceived,
+ onStatusChange,
+ onAutoConnection,
+ isVisible = false,
+ onClose
+}) => {
+ const [bluetoothManager, setBluetoothManager] = React.useState(null);
+ const [isSupported, setIsSupported] = React.useState(false);
+ const [isAvailable, setIsAvailable] = React.useState(false);
+ const [isScanning, setIsScanning] = React.useState(false);
+ const [isAdvertising, setIsAdvertising] = React.useState(false);
+ const [connectedDevices, setConnectedDevices] = React.useState([]);
+ const [status, setStatus] = React.useState("idle");
+ const [error, setError] = React.useState(null);
+ const [logs, setLogs] = React.useState([]);
+ React.useEffect(() => {
+ if (isVisible && !bluetoothManager) {
+ initializeBluetooth();
+ }
+ }, [isVisible]);
+ React.useEffect(() => {
+ return () => {
+ if (bluetoothManager) {
+ bluetoothManager.cleanup();
+ }
+ };
+ }, [bluetoothManager]);
+ const initializeBluetooth = async () => {
+ try {
+ const manager = new window.BluetoothKeyTransfer(
+ webrtcManager,
+ handleStatusChange,
+ handleKeyReceived,
+ handleError,
+ handleAutoConnection
+ );
+ setBluetoothManager(manager);
+ setTimeout(() => {
+ setIsSupported(manager.isSupported);
+ setIsAvailable(manager.isAvailable);
+ }, 100);
+ } catch (error2) {
+ console.error("Failed to initialize Bluetooth manager:", error2);
+ setError("Failed to initialize Bluetooth: " + error2.message);
+ }
+ };
+ const handleStatusChange = (statusType, data) => {
+ setStatus(statusType);
+ addLog(`Status: ${statusType}`, data);
+ switch (statusType) {
+ case "bluetooth_ready":
+ setIsSupported(data.supported);
+ setIsAvailable(data.available);
+ break;
+ case "scanning_active":
+ setIsScanning(true);
+ break;
+ case "scanning_stopped":
+ setIsScanning(false);
+ break;
+ case "advertising_active":
+ setIsAdvertising(true);
+ break;
+ case "advertising_stopped":
+ setIsAdvertising(false);
+ break;
+ case "connected":
+ setConnectedDevices((prev) => [...prev, {
+ id: data.deviceId,
+ name: data.deviceName,
+ connected: true
+ }]);
+ break;
+ }
+ onStatusChange?.(statusType, data);
+ };
+ const handleKeyReceived = (keyData, deviceId) => {
+ addLog("Key received from device", { deviceId });
+ onKeyReceived?.(keyData, deviceId);
+ };
+ const handleError = (error2) => {
+ console.error("Bluetooth error:", error2);
+ setError(error2.message);
+ addLog("Error", error2.message);
+ };
+ const handleAutoConnection = (connectionData) => {
+ console.log("Auto connection completed:", connectionData);
+ addLog("Auto Connection Completed", connectionData);
+ onAutoConnection?.(connectionData);
+ };
+ const addLog = (message, data = null) => {
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
+ const logEntry = {
+ timestamp,
+ message,
+ data: data ? JSON.stringify(data, null, 2) : null
+ };
+ setLogs((prev) => [...prev.slice(-9), logEntry]);
+ };
+ const startScanning = async () => {
+ try {
+ setError(null);
+ await bluetoothManager.startScanning();
+ } catch (error2) {
+ setError("Failed to start scanning: " + error2.message);
+ }
+ };
+ const stopScanning = async () => {
+ try {
+ await bluetoothManager.stopScanning();
+ } catch (error2) {
+ setError("Failed to stop scanning: " + error2.message);
+ }
+ };
+ const startAdvertising = async () => {
+ try {
+ setError(null);
+ if (!webrtcManager || !webrtcManager.ecdhKeyPair) {
+ throw new Error("No public key available for advertising");
+ }
+ await bluetoothManager.startAdvertising(
+ webrtcManager.ecdhKeyPair.publicKey,
+ "SecureBit Device"
+ );
+ } catch (error2) {
+ setError("Failed to start advertising: " + error2.message);
+ }
+ };
+ const stopAdvertising = async () => {
+ try {
+ await bluetoothManager.stopAdvertising();
+ } catch (error2) {
+ setError("Failed to stop advertising: " + error2.message);
+ }
+ };
+ const sendPublicKey = async (deviceId) => {
+ try {
+ setError(null);
+ if (!webrtcManager || !webrtcManager.ecdhKeyPair) {
+ throw new Error("No public key available for sending");
+ }
+ await bluetoothManager.sendPublicKey(
+ webrtcManager.ecdhKeyPair.publicKey,
+ deviceId
+ );
+ } catch (error2) {
+ setError("Failed to send public key: " + error2.message);
+ }
+ };
+ const clearLogs = () => {
+ setLogs([]);
+ };
+ const startAutoConnection = async (deviceId) => {
+ try {
+ setError(null);
+ await bluetoothManager.startAutoConnection(deviceId);
+ } catch (error2) {
+ setError("Failed to start auto connection: " + error2.message);
+ }
+ };
+ const startAutoConnectionAsResponder = async (deviceId) => {
+ try {
+ setError(null);
+ await bluetoothManager.startAutoConnectionAsResponder(deviceId);
+ } catch (error2) {
+ setError("Failed to start auto connection as responder: " + error2.message);
+ }
+ };
+ if (!isVisible) return null;
+ return React.createElement("div", {
+ className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
+ }, [
+ React.createElement("div", {
+ key: "modal",
+ className: "bg-gray-900 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden"
+ }, [
+ // Header
+ React.createElement("div", {
+ key: "header",
+ className: "flex items-center justify-between p-6 border-b border-gray-700"
+ }, [
+ React.createElement("div", {
+ key: "title",
+ className: "flex items-center space-x-3"
+ }, [
+ React.createElement("i", {
+ key: "icon",
+ className: "fas fa-bluetooth text-blue-400 text-xl"
+ }),
+ React.createElement("h2", {
+ key: "text",
+ className: "text-xl font-semibold text-white"
+ }, "Bluetooth Key Transfer")
+ ]),
+ React.createElement("button", {
+ key: "close",
+ onClick: onClose,
+ className: "text-gray-400 hover:text-white transition-colors"
+ }, [
+ React.createElement("i", {
+ className: "fas fa-times text-xl"
+ })
+ ])
+ ]),
+ // Content
+ React.createElement("div", {
+ key: "content",
+ className: "p-6 space-y-6 overflow-y-auto max-h-[calc(90vh-200px)]"
+ }, [
+ // Status Section
+ React.createElement("div", {
+ key: "status",
+ className: "space-y-4"
+ }, [
+ React.createElement("h3", {
+ key: "title",
+ className: "text-lg font-medium text-white"
+ }, "Bluetooth Status"),
+ React.createElement("div", {
+ key: "indicators",
+ className: "grid grid-cols-2 gap-4"
+ }, [
+ React.createElement("div", {
+ key: "support",
+ className: "flex items-center space-x-2"
+ }, [
+ React.createElement("div", {
+ className: `w-3 h-3 rounded-full ${isSupported ? "bg-green-500" : "bg-red-500"}`
+ }),
+ React.createElement("span", {
+ className: "text-sm text-gray-300"
+ }, "Bluetooth Supported")
+ ]),
+ React.createElement("div", {
+ key: "availability",
+ className: "flex items-center space-x-2"
+ }, [
+ React.createElement("div", {
+ className: `w-3 h-3 rounded-full ${isAvailable ? "bg-green-500" : "bg-red-500"}`
+ }),
+ React.createElement("span", {
+ className: "text-sm text-gray-300"
+ }, "Bluetooth Available")
+ ])
+ ])
+ ]),
+ // Controls Section
+ React.createElement("div", {
+ key: "controls",
+ className: "space-y-4"
+ }, [
+ React.createElement("h3", {
+ key: "title",
+ className: "text-lg font-medium text-white"
+ }, "Key Exchange"),
+ React.createElement("div", {
+ key: "buttons",
+ className: "grid grid-cols-1 sm:grid-cols-2 gap-4"
+ }, [
+ // Scanning Controls
+ React.createElement("div", {
+ key: "scanning",
+ className: "space-y-2"
+ }, [
+ React.createElement("h4", {
+ key: "title",
+ className: "text-sm font-medium text-gray-300"
+ }, "Discover Devices"),
+ React.createElement("button", {
+ key: "scan",
+ onClick: isScanning ? stopScanning : startScanning,
+ disabled: !isSupported || !isAvailable,
+ className: `w-full px-4 py-2 rounded-lg font-medium transition-colors ${isScanning ? "bg-red-600 hover:bg-red-700 text-white" : "bg-blue-600 hover:bg-blue-700 text-white disabled:bg-gray-600 disabled:cursor-not-allowed"}`
+ }, [
+ React.createElement("i", {
+ key: "icon",
+ className: `fas ${isScanning ? "fa-stop" : "fa-search"} mr-2`
+ }),
+ isScanning ? "Stop Scanning" : "Start Scanning"
+ ])
+ ]),
+ // Advertising Controls
+ React.createElement("div", {
+ key: "advertising",
+ className: "space-y-2"
+ }, [
+ React.createElement("h4", {
+ key: "title",
+ className: "text-sm font-medium text-gray-300"
+ }, "Share Your Key"),
+ React.createElement("button", {
+ key: "advertise",
+ onClick: isAdvertising ? stopAdvertising : startAdvertising,
+ disabled: !isSupported || !isAvailable,
+ className: `w-full px-4 py-2 rounded-lg font-medium transition-colors ${isAdvertising ? "bg-red-600 hover:bg-red-700 text-white" : "bg-green-600 hover:bg-green-700 text-white disabled:bg-gray-600 disabled:cursor-not-allowed"}`
+ }, [
+ React.createElement("i", {
+ key: "icon",
+ className: `fas ${isAdvertising ? "fa-stop" : "fa-broadcast-tower"} mr-2`
+ }),
+ isAdvertising ? "Stop Sharing" : "Start Sharing"
+ ])
+ ])
+ ])
+ ]),
+ // Connected Devices
+ connectedDevices.length > 0 && React.createElement("div", {
+ key: "devices",
+ className: "space-y-4"
+ }, [
+ React.createElement("h3", {
+ key: "title",
+ className: "text-lg font-medium text-white"
+ }, "Connected Devices"),
+ React.createElement("div", {
+ key: "list",
+ className: "space-y-2"
+ }, connectedDevices.map(
+ (device) => React.createElement("div", {
+ key: device.id,
+ className: "flex items-center justify-between p-3 bg-gray-800 rounded-lg"
+ }, [
+ React.createElement("div", {
+ key: "info",
+ className: "flex items-center space-x-3"
+ }, [
+ React.createElement("i", {
+ key: "icon",
+ className: "fas fa-mobile-alt text-blue-400"
+ }),
+ React.createElement("span", {
+ key: "name",
+ className: "text-white"
+ }, device.name)
+ ]),
+ React.createElement("div", {
+ key: "buttons",
+ className: "flex space-x-2"
+ }, [
+ React.createElement("button", {
+ key: "auto-connect",
+ onClick: () => startAutoConnection(device.id),
+ className: "px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-sm rounded transition-colors"
+ }, "Auto Connect"),
+ React.createElement("button", {
+ key: "auto-respond",
+ onClick: () => startAutoConnectionAsResponder(device.id),
+ className: "px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded transition-colors"
+ }, "Auto Respond"),
+ React.createElement("button", {
+ key: "send",
+ onClick: () => sendPublicKey(device.id),
+ className: "px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded transition-colors"
+ }, "Send Key")
+ ])
+ ])
+ ))
+ ]),
+ // Error Display
+ error && React.createElement("div", {
+ key: "error",
+ className: "p-4 bg-red-900 border border-red-700 rounded-lg"
+ }, [
+ React.createElement("div", {
+ key: "header",
+ className: "flex items-center space-x-2 mb-2"
+ }, [
+ React.createElement("i", {
+ key: "icon",
+ className: "fas fa-exclamation-triangle text-red-400"
+ }),
+ React.createElement("h4", {
+ key: "title",
+ className: "text-red-400 font-medium"
+ }, "Error")
+ ]),
+ React.createElement("p", {
+ key: "message",
+ className: "text-red-300 text-sm"
+ }, error)
+ ]),
+ // Logs Section
+ React.createElement("div", {
+ key: "logs",
+ className: "space-y-4"
+ }, [
+ React.createElement("div", {
+ key: "header",
+ className: "flex items-center justify-between"
+ }, [
+ React.createElement("h3", {
+ key: "title",
+ className: "text-lg font-medium text-white"
+ }, "Activity Log"),
+ React.createElement("button", {
+ key: "clear",
+ onClick: clearLogs,
+ className: "text-sm text-gray-400 hover:text-white transition-colors"
+ }, "Clear")
+ ]),
+ React.createElement(
+ "div",
+ {
+ key: "log-list",
+ className: "bg-gray-800 rounded-lg p-4 max-h-40 overflow-y-auto"
+ },
+ logs.length === 0 ? React.createElement("p", {
+ key: "empty",
+ className: "text-gray-400 text-sm text-center"
+ }, "No activity yet") : logs.map(
+ (log, index) => React.createElement("div", {
+ key: index,
+ className: "text-xs text-gray-300 mb-1"
+ }, [
+ React.createElement("span", {
+ key: "time",
+ className: "text-gray-500"
+ }, `[${log.timestamp}] `),
+ React.createElement("span", {
+ key: "message",
+ className: "text-gray-300"
+ }, log.message),
+ log.data && React.createElement("pre", {
+ key: "data",
+ className: "text-gray-400 mt-1 ml-4"
+ }, log.data)
+ ])
+ )
+ )
+ ])
+ ]),
+ // Footer
+ React.createElement("div", {
+ key: "footer",
+ className: "flex items-center justify-between p-6 border-t border-gray-700"
+ }, [
+ React.createElement("div", {
+ key: "info",
+ className: "text-sm text-gray-400"
+ }, "Bluetooth key exchange provides secure device-to-device communication"),
+ React.createElement("button", {
+ key: "close-footer",
+ onClick: onClose,
+ className: "px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors"
+ }, "Close")
+ ])
+ ])
+ ]);
+};
+if (typeof window !== "undefined") {
+ window.BluetoothKeyTransfer = BluetoothKeyTransfer;
+}
diff --git a/test-header.js b/test-header.js
new file mode 100644
index 0000000..e599188
--- /dev/null
+++ b/test-header.js
@@ -0,0 +1,575 @@
+// src/components/ui/Header.jsx
+var EnhancedMinimalHeader = ({
+ status,
+ fingerprint,
+ verificationCode,
+ onDisconnect,
+ isConnected,
+ securityLevel,
+ webrtcManager
+}) => {
+ const [realSecurityLevel, setRealSecurityLevel] = React.useState(null);
+ const [lastSecurityUpdate, setLastSecurityUpdate] = React.useState(0);
+ React.useEffect(() => {
+ let isUpdating = false;
+ let lastUpdateAttempt = 0;
+ const updateRealSecurityStatus = async () => {
+ const now = Date.now();
+ if (now - lastUpdateAttempt < 1e4) {
+ return;
+ }
+ if (isUpdating) {
+ return;
+ }
+ isUpdating = true;
+ lastUpdateAttempt = now;
+ try {
+ if (!webrtcManager || !isConnected) {
+ return;
+ }
+ const activeWebrtcManager = webrtcManager;
+ let realSecurityData = null;
+ if (typeof activeWebrtcManager.getRealSecurityLevel === "function") {
+ realSecurityData = await activeWebrtcManager.getRealSecurityLevel();
+ } else if (typeof activeWebrtcManager.calculateAndReportSecurityLevel === "function") {
+ realSecurityData = await activeWebrtcManager.calculateAndReportSecurityLevel();
+ } else {
+ realSecurityData = await window.EnhancedSecureCryptoUtils.calculateSecurityLevel(activeWebrtcManager);
+ }
+ if (realSecurityData && realSecurityData.isRealData !== false) {
+ const currentScore = realSecurityLevel?.score || 0;
+ const newScore = realSecurityData.score || 0;
+ if (currentScore !== newScore || !realSecurityLevel) {
+ setRealSecurityLevel(realSecurityData);
+ setLastSecurityUpdate(now);
+ } else if (window.DEBUG_MODE) {
+ }
+ } else {
+ console.warn(" Security calculation returned invalid data");
+ }
+ } catch (error) {
+ console.error(" Error in real security calculation:", error);
+ } finally {
+ isUpdating = false;
+ }
+ };
+ if (isConnected) {
+ updateRealSecurityStatus();
+ if (!realSecurityLevel || realSecurityLevel.score < 50) {
+ const retryInterval = setInterval(() => {
+ if (!realSecurityLevel || realSecurityLevel.score < 50) {
+ updateRealSecurityStatus();
+ } else {
+ clearInterval(retryInterval);
+ }
+ }, 5e3);
+ setTimeout(() => clearInterval(retryInterval), 3e4);
+ }
+ }
+ const interval = setInterval(updateRealSecurityStatus, 3e4);
+ return () => clearInterval(interval);
+ }, [webrtcManager, isConnected]);
+ React.useEffect(() => {
+ const handleSecurityUpdate = (event) => {
+ setTimeout(() => {
+ setLastSecurityUpdate(0);
+ }, 100);
+ };
+ const handleRealSecurityCalculated = (event) => {
+ if (event.detail && event.detail.securityData) {
+ setRealSecurityLevel(event.detail.securityData);
+ setLastSecurityUpdate(Date.now());
+ }
+ };
+ document.addEventListener("security-level-updated", handleSecurityUpdate);
+ document.addEventListener("real-security-calculated", handleRealSecurityCalculated);
+ window.forceHeaderSecurityUpdate = (webrtcManager2) => {
+ if (webrtcManager2 && window.EnhancedSecureCryptoUtils) {
+ window.EnhancedSecureCryptoUtils.calculateSecurityLevel(webrtcManager2).then((securityData) => {
+ if (securityData && securityData.isRealData !== false) {
+ setRealSecurityLevel(securityData);
+ setLastSecurityUpdate(Date.now());
+ console.log("\u2705 Header security level force-updated");
+ }
+ }).catch((error) => {
+ console.error("\u274C Force update failed:", error);
+ });
+ } else {
+ setLastSecurityUpdate(0);
+ }
+ };
+ return () => {
+ document.removeEventListener("security-level-updated", handleSecurityUpdate);
+ document.removeEventListener("real-security-calculated", handleRealSecurityCalculated);
+ };
+ }, []);
+ React.useEffect(() => {
+ setHasActiveSession(true);
+ setCurrentTimeLeft(0);
+ setSessionType("premium");
+ }, []);
+ React.useEffect(() => {
+ setHasActiveSession(true);
+ setCurrentTimeLeft(0);
+ setSessionType("premium");
+ }, [sessionTimeLeft]);
+ React.useEffect(() => {
+ const handleForceUpdate = (event) => {
+ setHasActiveSession(true);
+ setCurrentTimeLeft(0);
+ setSessionType("premium");
+ };
+ const handleConnectionCleaned = () => {
+ if (window.DEBUG_MODE) {
+ console.log("\u{1F9F9} Connection cleaned - clearing security data in header");
+ }
+ setRealSecurityLevel(null);
+ setLastSecurityUpdate(0);
+ setHasActiveSession(false);
+ setCurrentTimeLeft(0);
+ setSessionType("unknown");
+ };
+ const handlePeerDisconnect = () => {
+ if (window.DEBUG_MODE) {
+ console.log("\u{1F44B} Peer disconnect detected - clearing security data in header");
+ }
+ setRealSecurityLevel(null);
+ setLastSecurityUpdate(0);
+ };
+ const handleDisconnected = () => {
+ setRealSecurityLevel(null);
+ setLastSecurityUpdate(0);
+ setHasActiveSession(false);
+ setCurrentTimeLeft(0);
+ setSessionType("unknown");
+ };
+ document.addEventListener("force-header-update", handleForceUpdate);
+ document.addEventListener("peer-disconnect", handlePeerDisconnect);
+ document.addEventListener("connection-cleaned", handleConnectionCleaned);
+ document.addEventListener("disconnected", handleDisconnected);
+ return () => {
+ document.removeEventListener("force-header-update", handleForceUpdate);
+ document.removeEventListener("peer-disconnect", handlePeerDisconnect);
+ document.removeEventListener("connection-cleaned", handleConnectionCleaned);
+ document.removeEventListener("disconnected", handleDisconnected);
+ };
+ }, []);
+ const handleSecurityClick = async (event) => {
+ if (event && (event.button === 2 || event.ctrlKey || event.metaKey)) {
+ if (onDisconnect && typeof onDisconnect === "function") {
+ onDisconnect();
+ return;
+ }
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ let realTestResults = null;
+ if (webrtcManager && window.EnhancedSecureCryptoUtils) {
+ try {
+ realTestResults = await window.EnhancedSecureCryptoUtils.calculateSecurityLevel(webrtcManager);
+ console.log("\u2705 Real security tests completed:", realTestResults);
+ } catch (error) {
+ console.error("\u274C Real security tests failed:", error);
+ }
+ } else {
+ console.log("\u26A0\uFE0F Cannot run security tests:", {
+ webrtcManager: !!webrtcManager,
+ cryptoUtils: !!window.EnhancedSecureCryptoUtils
+ });
+ }
+ if (!realTestResults && !realSecurityLevel) {
+ alert("Security verification in progress...\nPlease wait for real-time cryptographic verification to complete.");
+ return;
+ }
+ let securityData = realTestResults || realSecurityLevel;
+ if (!securityData) {
+ securityData = {
+ level: "UNKNOWN",
+ score: 0,
+ color: "gray",
+ verificationResults: {},
+ timestamp: Date.now(),
+ details: "Security verification not available",
+ isRealData: false,
+ passedChecks: 0,
+ totalChecks: 0
+ };
+ console.log("Using fallback security data:", securityData);
+ }
+ let message = `REAL-TIME SECURITY VERIFICATION
+
+`;
+ message += `Security Level: ${securityData.level} (${securityData.score}%)
+`;
+ message += `Verification Time: ${new Date(securityData.timestamp).toLocaleTimeString()}
+`;
+ message += `Data Source: ${securityData.isRealData ? "Real Cryptographic Tests" : "Simulated Data"}
+
+`;
+ if (securityData.verificationResults) {
+ message += "DETAILED CRYPTOGRAPHIC TESTS:\n";
+ message += "=" + "=".repeat(40) + "\n";
+ const passedTests = Object.entries(securityData.verificationResults).filter(([key, result]) => result.passed);
+ const failedTests = Object.entries(securityData.verificationResults).filter(([key, result]) => !result.passed);
+ if (passedTests.length > 0) {
+ message += "PASSED TESTS:\n";
+ passedTests.forEach(([key, result]) => {
+ const testName = key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase());
+ message += ` ${testName}: ${result.details || "Test passed"}
+`;
+ });
+ message += "\n";
+ }
+ if (failedTests.length > 0) {
+ message += "FAILED/UNAVAILABLE TESTS:\n";
+ failedTests.forEach(([key, result]) => {
+ const testName = key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase());
+ message += ` ${testName}: ${result.details || "Test failed or unavailable"}
+`;
+ });
+ message += "\n";
+ }
+ message += `SUMMARY:
+`;
+ message += `Passed: ${securityData.passedChecks}/${securityData.totalChecks} tests
+`;
+ message += `Score: ${securityData.score}/${securityData.maxPossibleScore || 100} points
+
+`;
+ }
+ message += `SECURITY FEATURES STATUS:
+`;
+ message += "=" + "=".repeat(40) + "\n";
+ if (securityData.verificationResults) {
+ const features = {
+ "ECDSA Digital Signatures": securityData.verificationResults.verifyECDSASignatures?.passed || false,
+ "ECDH Key Exchange": securityData.verificationResults.verifyECDHKeyExchange?.passed || false,
+ "AES-GCM Encryption": securityData.verificationResults.verifyEncryption?.passed || false,
+ "Message Integrity (HMAC)": securityData.verificationResults.verifyMessageIntegrity?.passed || false,
+ "Perfect Forward Secrecy": securityData.verificationResults.verifyPerfectForwardSecrecy?.passed || false,
+ "Replay Protection": securityData.verificationResults.verifyReplayProtection?.passed || false,
+ "DTLS Fingerprint": securityData.verificationResults.verifyDTLSFingerprint?.passed || false,
+ "SAS Verification": securityData.verificationResults.verifySASVerification?.passed || false,
+ "Metadata Protection": securityData.verificationResults.verifyMetadataProtection?.passed || false,
+ "Traffic Obfuscation": securityData.verificationResults.verifyTrafficObfuscation?.passed || false
+ };
+ Object.entries(features).forEach(([feature, isEnabled]) => {
+ message += `${isEnabled ? "\u2705" : "\u274C"} ${feature}
+`;
+ });
+ } else {
+ message += `\u2705 ECDSA Digital Signatures
+`;
+ message += `\u2705 ECDH Key Exchange
+`;
+ message += `\u2705 AES-GCM Encryption
+`;
+ message += `\u2705 Message Integrity (HMAC)
+`;
+ message += `\u2705 Perfect Forward Secrecy
+`;
+ message += `\u2705 Replay Protection
+`;
+ message += `\u2705 DTLS Fingerprint
+`;
+ message += `\u2705 SAS Verification
+`;
+ message += `\u2705 Metadata Protection
+`;
+ message += `\u2705 Traffic Obfuscation
+`;
+ }
+ message += `
+${securityData.details || "Real cryptographic verification completed"}`;
+ if (securityData.isRealData) {
+ message += "\n\n\u2705 This is REAL-TIME verification using actual cryptographic functions.";
+ } else {
+ message += "\n\n\u26A0\uFE0F Warning: This data may be simulated. Connection may not be fully established.";
+ }
+ const modal = document.createElement("div");
+ modal.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0,0,0,0.8);
+ z-index: 10000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: monospace;
+ `;
+ const content = document.createElement("div");
+ content.style.cssText = `
+ background: #1a1a1a;
+ color: #fff;
+ padding: 20px;
+ border-radius: 8px;
+ max-width: 80%;
+ max-height: 80%;
+ overflow-y: auto;
+ white-space: pre-line;
+ border: 1px solid #333;
+ `;
+ content.textContent = message;
+ modal.appendChild(content);
+ modal.addEventListener("click", (e) => {
+ if (e.target === modal) {
+ document.body.removeChild(modal);
+ }
+ });
+ const handleKeyDown = (e) => {
+ if (e.key === "Escape") {
+ document.body.removeChild(modal);
+ document.removeEventListener("keydown", handleKeyDown);
+ }
+ };
+ document.addEventListener("keydown", handleKeyDown);
+ document.body.appendChild(modal);
+ };
+ const getStatusConfig = () => {
+ switch (status) {
+ case "connected":
+ return {
+ text: "Connected",
+ className: "status-connected",
+ badgeClass: "bg-green-500/10 text-green-400 border-green-500/20"
+ };
+ case "verifying":
+ return {
+ text: "Verifying...",
+ className: "status-verifying",
+ badgeClass: "bg-purple-500/10 text-purple-400 border-purple-500/20"
+ };
+ case "connecting":
+ return {
+ text: "Connecting...",
+ className: "status-connecting",
+ badgeClass: "bg-blue-500/10 text-blue-400 border-blue-500/20"
+ };
+ case "retrying":
+ return {
+ text: "Retrying...",
+ className: "status-connecting",
+ badgeClass: "bg-yellow-500/10 text-yellow-400 border-yellow-500/20"
+ };
+ case "failed":
+ return {
+ text: "Error",
+ className: "status-failed",
+ badgeClass: "bg-red-500/10 text-red-400 border-red-500/20"
+ };
+ case "reconnecting":
+ return {
+ text: "Reconnecting...",
+ className: "status-connecting",
+ badgeClass: "bg-yellow-500/10 text-yellow-400 border-yellow-500/20"
+ };
+ case "peer_disconnected":
+ return {
+ text: "Peer disconnected",
+ className: "status-failed",
+ badgeClass: "bg-orange-500/10 text-orange-400 border-orange-500/20"
+ };
+ default:
+ return {
+ text: "Not connected",
+ className: "status-disconnected",
+ badgeClass: "bg-gray-500/10 text-gray-400 border-gray-500/20"
+ };
+ }
+ };
+ const config = getStatusConfig();
+ const displaySecurityLevel = isConnected ? realSecurityLevel || securityLevel : null;
+ const getSecurityIndicatorDetails = () => {
+ if (!displaySecurityLevel) {
+ return {
+ tooltip: "Security verification in progress...",
+ isVerified: false,
+ dataSource: "loading"
+ };
+ }
+ const isRealData = displaySecurityLevel.isRealData !== false;
+ const baseTooltip = `${displaySecurityLevel.level} (${displaySecurityLevel.score}%)`;
+ if (isRealData) {
+ return {
+ tooltip: `${baseTooltip} - Real-time verification \u2705
+Right-click or Ctrl+click to disconnect`,
+ isVerified: true,
+ dataSource: "real"
+ };
+ } else {
+ return {
+ tooltip: `${baseTooltip} - Estimated (connection establishing...)
+Right-click or Ctrl+click to disconnect`,
+ isVerified: false,
+ dataSource: "estimated"
+ };
+ }
+ };
+ const securityDetails = getSecurityIndicatorDetails();
+ React.useEffect(() => {
+ window.debugHeaderSecurity = () => {
+ console.log("\u{1F50D} Header Security Debug:", {
+ realSecurityLevel,
+ lastSecurityUpdate,
+ isConnected,
+ webrtcManagerProp: !!webrtcManager,
+ windowWebrtcManager: !!window.webrtcManager,
+ cryptoUtils: !!window.EnhancedSecureCryptoUtils,
+ displaySecurityLevel,
+ securityDetails
+ });
+ };
+ return () => {
+ delete window.debugHeaderSecurity;
+ };
+ }, [realSecurityLevel, lastSecurityUpdate, isConnected, webrtcManager, displaySecurityLevel, securityDetails]);
+ return React.createElement("header", {
+ className: "header-minimal sticky top-0 z-50"
+ }, [
+ React.createElement("div", {
+ key: "container",
+ className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
+ }, [
+ React.createElement("div", {
+ key: "content",
+ className: "flex items-center justify-between h-16"
+ }, [
+ // Logo and Title
+ React.createElement("div", {
+ key: "logo-section",
+ className: "flex items-center space-x-2 sm:space-x-3"
+ }, [
+ React.createElement("div", {
+ key: "logo",
+ className: "icon-container w-8 h-8 sm:w-10 sm:h-10"
+ }, [
+ React.createElement("i", {
+ className: "fas fa-shield-halved accent-orange text-sm sm:text-base"
+ })
+ ]),
+ React.createElement("div", {
+ key: "title-section"
+ }, [
+ React.createElement("h1", {
+ key: "title",
+ className: "text-lg sm:text-xl font-semibold text-primary"
+ }, "SecureBit.chat"),
+ React.createElement("p", {
+ key: "subtitle",
+ className: "text-xs sm:text-sm text-muted hidden sm:block"
+ }, "End-to-end freedom v4.3.120")
+ ])
+ ]),
+ // Status and Controls - Responsive
+ React.createElement("div", {
+ key: "status-section",
+ className: "flex items-center space-x-2 sm:space-x-3"
+ }, [
+ displaySecurityLevel && React.createElement("div", {
+ key: "security-level",
+ className: "hidden md:flex items-center space-x-2 cursor-pointer hover:opacity-80 transition-opacity duration-200",
+ onClick: handleSecurityClick,
+ onContextMenu: (e) => {
+ e.preventDefault();
+ if (onDisconnect && typeof onDisconnect === "function") {
+ onDisconnect();
+ }
+ },
+ title: securityDetails.tooltip
+ }, [
+ React.createElement("div", {
+ key: "security-icon",
+ className: `w-6 h-6 rounded-full flex items-center justify-center relative ${displaySecurityLevel.color === "green" ? "bg-green-500/20" : displaySecurityLevel.color === "orange" ? "bg-orange-500/20" : displaySecurityLevel.color === "yellow" ? "bg-yellow-500/20" : "bg-red-500/20"} ${securityDetails.isVerified ? "" : "animate-pulse"}`
+ }, [
+ React.createElement("i", {
+ className: `fas fa-shield-alt text-xs ${displaySecurityLevel.color === "green" ? "text-green-400" : displaySecurityLevel.color === "orange" ? "text-orange-400" : displaySecurityLevel.color === "yellow" ? "text-yellow-400" : "text-red-400"}`
+ })
+ ]),
+ React.createElement("div", {
+ key: "security-info",
+ className: "flex flex-col"
+ }, [
+ React.createElement("div", {
+ key: "security-level-text",
+ className: "text-xs font-medium text-primary flex items-center space-x-1"
+ }, [
+ React.createElement("span", {}, `${displaySecurityLevel.level} (${displaySecurityLevel.score}%)`)
+ ]),
+ React.createElement(
+ "div",
+ {
+ key: "security-details",
+ className: "text-xs text-muted mt-1 hidden lg:block"
+ },
+ securityDetails.dataSource === "real" ? `${displaySecurityLevel.passedChecks || 0}/${displaySecurityLevel.totalChecks || 0} tests` : displaySecurityLevel.details || `Stage ${displaySecurityLevel.stage || 1}`
+ ),
+ React.createElement("div", {
+ key: "security-progress",
+ className: "w-16 h-1 bg-gray-600 rounded-full overflow-hidden"
+ }, [
+ React.createElement("div", {
+ key: "progress-bar",
+ className: `h-full transition-all duration-500 ${displaySecurityLevel.color === "green" ? "bg-green-400" : displaySecurityLevel.color === "orange" ? "bg-orange-400" : displaySecurityLevel.color === "yellow" ? "bg-yellow-400" : "bg-red-400"}`,
+ style: { width: `${displaySecurityLevel.score}%` }
+ })
+ ])
+ ])
+ ]),
+ // Mobile Security Indicator
+ displaySecurityLevel && React.createElement("div", {
+ key: "mobile-security",
+ className: "md:hidden flex items-center"
+ }, [
+ React.createElement("div", {
+ key: "mobile-security-icon",
+ className: `w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity duration-200 relative ${displaySecurityLevel.color === "green" ? "bg-green-500/20" : displaySecurityLevel.color === "orange" ? "bg-orange-500/20" : displaySecurityLevel.color === "yellow" ? "bg-yellow-500/20" : "bg-red-500/20"} ${securityDetails.isVerified ? "" : "animate-pulse"}`,
+ title: securityDetails.tooltip,
+ onClick: handleSecurityClick,
+ onContextMenu: (e) => {
+ e.preventDefault();
+ if (onDisconnect && typeof onDisconnect === "function") {
+ onDisconnect();
+ }
+ }
+ }, [
+ React.createElement("i", {
+ className: `fas fa-shield-alt text-sm ${displaySecurityLevel.color === "green" ? "text-green-400" : displaySecurityLevel.color === "orange" ? "text-orange-400" : displaySecurityLevel.color === "yellow" ? "text-yellow-400" : "text-red-400"}`
+ })
+ ])
+ ]),
+ // Status Badge
+ React.createElement("div", {
+ key: "status-badge",
+ className: `px-2 sm:px-3 py-1.5 rounded-lg border ${config.badgeClass} flex items-center space-x-1 sm:space-x-2`
+ }, [
+ React.createElement("span", {
+ key: "status-dot",
+ className: `status-dot ${config.className}`
+ }),
+ React.createElement("span", {
+ key: "status-text",
+ className: "text-xs sm:text-sm font-medium"
+ }, config.text)
+ ]),
+ // Disconnect Button
+ isConnected && React.createElement("button", {
+ key: "disconnect-btn",
+ onClick: onDisconnect,
+ className: "p-1.5 sm:px-3 sm:py-1.5 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 rounded-lg transition-all duration-200 text-sm"
+ }, [
+ React.createElement("i", {
+ className: "fas fa-power-off sm:mr-2"
+ }),
+ React.createElement("span", {
+ className: "hidden sm:inline"
+ }, "Disconnect")
+ ])
+ ])
+ ])
+ ])
+ ]);
+};
+window.EnhancedMinimalHeader = EnhancedMinimalHeader;