1(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const o of i)if(o.type==="childList")for(const s of o.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(i){const o={};return i.integrity&&(o.integrity=i.integrity),i.referrerPolicy&&(o.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?o.credentials="include":i.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(i){if(i.ep)return;i.ep=!0;const o=n(i);fetch(i.href,o)}})();const Ht="modulepreload",Ut=function(e){return"/"+e},Ye={},pt=function(t,n,r){if(!n||n.length===0)return t();const i=document.getElementsByTagName("link");return Promise.all(n.map(o=>{if(o=Ut(o),o in Ye)return;Ye[o]=!0;const s=o.endsWith(".css"),a=s?'[rel="stylesheet"]':"";if(!!r)for(let u=i.length-1;u>=0;u--){const d=i[u];if(d.href===o&&(!s||d.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${o}"]${a}`))return;const l=document.createElement("link");if(l.rel=s?"stylesheet":Ht,s||(l.as="script",l.crossOrigin=""),l.href=o,document.head.appendChild(l),s)return new Promise((u,d)=>{l.addEventListener("load",u),l.addEventListener("error",()=>d(new Error(`Unable to preload CSS for ${o}`)))})})).then(()=>t())},g={context:void 0,registry:void 0,effects:void 0,done:!1,getContextId(){return Ze(this.context.count)},getNextContextId(){return Ze(this.context.count++)}};function Ze(e){const t=String(e),n=t.length-1;return g.context.id+(n?String.fromCharCode(96+n):"")+t}function ae(e){g.context=e}const gt=!1,Wt=(e,t)=>e===t,ge=Symbol("solid-proxy"),wt=typeof Proxy=="function",Bt=Symbol("solid-track"),we={equals:Wt};let yt=Pt;const K=1,ye=2,bt={owned:null,cleanups:null,context:null,owner:null},Ee={};var v=null;let Te=null,qt=null,P=null,$=null,V=null,xe=0;function ce(e,t){const n=P,r=v,i=e.length===0,o=t===void 0?r:t,s=i?bt:{owned:null,cleanups:null,context:o?o.context:null,owner:o},a=i?e:()=>e(()=>M(()=>le(s)));v=s,P=null;try{return B(a,!0)}finally{P=n,v=r}}function H(e,t){t=t?Object.assign({},we,t):we;const n={value:e,observers:null,observerSlots:null,comparator:t.equals||void 0},r=i=>(typeof i=="function"&&(i=i(n.value)),At(n,i));return[xt.bind(n),r]}function et(e,t,n){const r=ke(e,t,!0,K);ne(r)}function W(e,t,n){const r=ke(e,t,!1,K);ne(r)}function Ft(e,t,n){yt=Yt;const r=ke(e,t,!1,K);(!n||!n.render)&&(r.user=!0),V?V.push(r):ne(r)}function T(e,t,n){n=n?Object.assign({},we,n):we;const r=ke(e,t,!0,0);return r.observers=null,r.observerSlots=null,r.comparator=n.equals||void 0,ne(r),xt.bind(r)}function Vt(e){return e&&typeof e=="object"&&"then"in e}function Kt(e,t,n){let r,i,o;typeof t=="function"?(r=e,i=t,o=n||{}):(r=!0,i=e,o=t||{});let s=null,a=Ee,c=null,l=!1,u="initialValue"in o,d=typeof r=="function"&&T(r);const f=new Set,[m,S]=(o.storage||H)(o.initialValue),[h,p]=H(void 0),[A,w]=H(void 0,{equals:!1}),[R,I]=H(u?"ready":"unresolved");g.context&&(c=g.getNextContextId(),o.ssrLoadFrom==="initial"?a=o.initialValue:g.load&&g.has(c)&&(a=g.load(c)));function _(E,L,D,O){return s===E&&(s=null,O!==void 0&&(u=!0),(E===a||L===a)&&o.onHydrated&&queueMicrotask(()=>o.onHydrated(O,{value:L})),a=Ee,q(L,D)),L}function q(E,L){B(()=>{L===void 0&&S(()=>E),I(L!==void 0?"errored":u?"ready":"unresolved"),p(L);for(const D of f.keys())D.decrement();f.clear()},!1)}function Q(){const E=Qt,L=m(),D=h();if(D!==void 0&&!s)throw D;return P&&!P.user&&E&&et(()=>{A(),s&&(E.resolved||f.has(E)||(E.increment(),f.add(E)))}),L}function ie(E=!0){if(E!==!1&&l)return;l=!1;const L=d?d():r;if(L==null||L===!1){_(s,M(m));return}let D;const O=a!==Ee?a:M(()=>{try{return i(L,{value:m(),refetching:E})}catch(b){D=b}});if(D!==void 0){_(s,void 0,he(D),L);return}else if(!Vt(O))return _(s,O,void 0,L),O;return s=O,"v"in O?(O.s===1?_(s,O.v,void 0,L):_(s,void 0,he(O.v),L),O):(l=!0,queueMicrotask(()=>l=!1),B(()=>{I(u?"refreshing":"pending"),w()},!1),O.then(b=>_(O,b,void 0,L),b=>_(O,void 0,he(b),L)))}Object.defineProperties(Q,{state:{get:()=>R()},error:{get:()=>h()},loading:{get(){const E=R();return E==="pending"||E==="refreshing"}}
1,latest:{get(){if(!u)return Q();const E=h();if(E&&!s)throw E;return m()}}});let oe=v;return d?et(()=>(oe=v,ie(!1))):ie(!1),[Q,{refetch:E=>Be(oe,()=>ie(E)),mutate:S}]}function Gt(e){return B(e,!1)}function M(e){if(P===null)return e();const t=P;P=null;try{return e()}finally{P=t}}function We(e,t,n){const r=Array.isArray(e);let i,o=n&&n.defer;return s=>{let a;if(r){a=Array(e.length);for(let l=0;l<e.length;l++)a[l]=e[l]()}else a=e();if(o)return o=!1,s;const c=M(()=>t(a,i,s));return i=a,c}}function vt(e){Ft(()=>M(e))}function Ae(e){return v===null||(v.cleanups===null?v.cleanups=[e]:v.cleanups.push(e)),e}function St(){return v}function Be(e,t){const n=v,r=P;v=e,P=null;try{return B(t,!0)}catch(i){Ve(i)}finally{v=n,P=r}}function zt(e){const t=P,n=v;return Promise.resolve().then(()=>{P=t,v=n;let r;return B(e,!1),P=v=null,r?r.done:void 0})}function qe(e,t){const n=Symbol("context");return{id:n,Provider:Zt(n),defaultValue:e}}function Pe(e){let t;return v&&v.context&&(t=v.context[e.id])!==void 0?t:e.defaultValue}function Fe(e){const t=T(e),n=T(()=>Oe(t()));return n.toArray=()=>{const r=n();return Array.isArray(r)?r:r!=null?[r]:[]},n}let Qt;function xt(){if(this.sources&&this.state)if(this.state===K)ne(this);else{const e=$;$=null,B(()=>ve(this),!1),$=e}if(P){const e=this.observers?this.observers.length:0;P.sources?(P.sources.push(this),P.sourceSlots.push(e)):(P.sources=[this],P.sourceSlots=[e]),this.observers?(this.observers.push(P),this.observerSlots.push(P.sources.length-1)):(this.observers=[P],this.observerSlots=[P.sources.length-1])}return this.value}function At(e,t,n){let r=e.value;return(!e.comparator||!e.comparator(r,t))&&(e.value=t,e.observers&&e.observers.length&&B(()=>{for(let i=0;i<e.observers.length;i+=1){const o=e.observers[i],s=Te&&Te.running;s&&Te.disposed.has(o),(s?!o.tState:!o.state)&&(o.pure?$.push(o):V.push(o),o.observers&&kt(o)),s||(o.state=K)}if($.length>1e6)throw $=[],new Error},!1)),t}function ne(e){if(!e.fn)return;le(e);const t=xe;Jt(e,e.value,t)}function Jt(e,t,n){let r;const i=v,o=P;P=v=e;try{r=e.fn(t)}catch(s){return e.pure&&(e.state=K,e.owned&&e.owned.forEach(le),e.owned=null),e.updatedAt=n+1,Ve(s)}finally{P=o,v=i}(!e.updatedAt||e.updatedAt<=n)&&(e.updatedAt!=null&&"observers"in e?At(e,r):e.value=r,e.updatedAt=n)}function ke(e,t,n,r=K,i){const o={fn:e,state:r,updatedAt:null,owned:null,sources:null,
1sourceSlots:null,cleanups:null,value:t,owner:v,context:v?v.context:null,pure:n};return v===null||v!==bt&&(v.owned?v.owned.push(o):v.owned=[o]),o}function be(e){if(e.state===0)return;if(e.state===ye)return ve(e);if(e.suspense&&M(e.suspense.inFallback))return e.suspense.effects.push(e);const t=[e];for(;(e=e.owner)&&(!e.updatedAt||e.updatedAt<xe);)e.state&&t.push(e);for(let n=t.length-1;n>=0;n--)if(e=t[n],e.state===K)ne(e);else if(e.state===ye){const r=$;$=null,B(()=>ve(e,t[0]),!1),$=r}}function B(e,t){if($)return e();let n=!1;t||($=[]),V?n=!0:V=[],xe++;try{const r=e();return Xt(n),r}catch(r){n||(V=null),$=null,Ve(r)}}function Xt(e){if($&&(Pt($),$=null),e)return;const t=V;V=null,t.length&&B(()=>yt(t),!1)}function Pt(e){for(let t=0;t<e.length;t++)be(e[t])}function Yt(e){let t,n=0;for(t=0;t<e.length;t++){const r=e[t];r.user?e[n++]=r:be(r)}if(g.context){if(g.count){g.effects||(g.effects=[]),g.effects.push(...e.slice(0,n));return}ae()}for(g.effects&&(g.done||!g.count)&&(e=[...g.effects,...e],n+=g.effects.length,delete g.effects),t=0;t<n;t++)be(e[t])}function ve(e,t){e.state=0;for(let n=0;n<e.sources.length;n+=1){const r=e.sources[n];if(r.sources){const i=r.state;i===K?r!==t&&(!r.updatedAt||r.updatedAt<xe)&&be(r):i===ye&&ve(r,t)}}}function kt(e){for(let t=0;t<e.observers.length;t+=1){const n=e.observers[t];n.state||(n.state=ye,n.pure?$.push(n):V.push(n),n.observers&&kt(n))}}function le(e){let t;if(e.sources)for(;e.sources.length;){const n=e.sources.pop(),r=e.sourceSlots.pop(),i=n.observers;if(i&&i.length){const o=i.pop(),s=n.observerSlots.pop();r<i.length&&(o.sourceSlots[s]=r,i[r]=o,n.observerSlots[r]=s)}}if(e.tOwned){for(t=e.tOwned.length-1;t>=0;t--)le(e.tOwned[t]);delete e.tOwned}if(e.owned){for(t=e.owned.length-1;t>=0;t--)le(e.owned[t]);e.owned=null}if(e.cleanups){for(t=e.cleanups.length-1;t>=0;t--)e.cleanups[t]();e.cleanups=null}e.state=0}function he(e){return e instanceof Error?e:new Error(typeof e=="string"?e:"Unknown error",{cause:e})}function Ve(e,t=v){throw he(e)}function Oe(e){if(typeof e=="function"&&!e.length)return Oe(e());if(Array.isArray(e)){const t=[];for(let n=0;n<e.length;n++){const r=Oe(e[n]);Array.isArray(r)?t.push.apply(t,r):t.push(r)}return t}return e}function Zt(e,t){return function(r){let i;return W(()=>i=M(()=>(v.context={...v.context,[e]:r.value},Fe(()=>r.children))),void 0),i}}const en=Symbol("fallback");function tt(e){for(let t=0;t<e.length;t++)e[t]()}function tn(e,t,n={}){let r=[],i=[],o=[],s=0,a=t.length>1?[]:null;return Ae(()=>tt(o)),()=>{let c=e()||[],l=c.length,u,d;return c[Bt],M(()=>{let m,S,h,p,A,w,R,I,_;if(l===0)s!==0&&(tt(o),o=[],r=[],i=[],s=0,a&&(a=[])),n.fallback&&(r=[en],i[0]=ce(q=>(o[0]=q,n.fallback())),s=1);else if(s===0){for(i=new Array(l),d=0;d<l;d++)r[d]=c[d],i[d]=ce(f);s=l}else{for(h=new Array(l),p=new Array(l),a&&(A=new Array(l)),w=0,R=Math.min(s,l);w<R&&r[w]===c[w];w++);for(R=s-1,I=l-1;R>=w&&I>=w&&r[R]===c[I];R--,I--)h[I]=i[R],p[I]=o[R],a&&(A[I]=a[R]);for(m=new Map,S=new Array(I+1),d=I;d>=w;d--)_=c[d],u=m.get(_),S[d]=u===void 0?-1:u,m.set(_,d);for(u=w;u<=R;u++)_=r[u],d=m.get(_),d!==void 0&&d!==-1?(h[d]=i[u],p[d]=o[u],a&&(A[d]=a[u]),d=S[d],m.set(_,d)):o[u]();for(d=w;d<l;d++)d in h?(i[d]=h[d],o[d]=p[d],a&&(a[d]=A[d],a[d](d))):i[d]=ce(f);i=i.slice(0,s=l),r=c.slice(0)}return i});function f(m){if(o[d]=m,a){const[S,h]=H(d);return a[d]=h,t(c[d],S)}return t(c[d])}}}function x(e,t){return M(()=>e(t||{}))}function de(){return!0}const $e={get(e,t,n){return t===ge?n:e.get(t)},has(e,t){return t===ge?!0:e.has(t)},set:de,deleteProperty:de,getOwnPropertyDescriptor(e,t){return{configurable:!0,enumerable:!0,get(){return e.get(t)},set:de,deleteProperty:de}},ownKeys(e){return e.keys()}};function _e(e){return(e=typeof e=="function"?e():e)?e:{}}function nn(){for(let e=0,t=this.length;e<t;++e){const n=this[e]();if(n!==void 0)return n}}function Me(...e){let t=!1;for(let s=0;s<e.length;s++){const a=e[s];t=t||!!a&&ge in a,e[s]=typeof a=="function"?(t=!0,T(a)):a}if(wt&&t)return new Proxy({get(s){for(let a=e.length-1;a>=0;a--){const c=_e(e[a])[s];if(c!==void 0)return c}},has(s){for(let a=e.length-1;a>=0;a--)if(s in _e(e[a]))return!0;return!1},keys(){const s=[];for(let a=0;a<e.length;a++)s.push(...Object.keys(_e(e[a])));return[...new Set(s)]}},$e);const n={},r=Object.create(null);for(let s=e.length-1;s>=0;s--){const a=e[s];if(!a)continue;const c=Object.getOwnPropertyNames(a);for(let l=c.length-1;l>=0;l--){const u=c[l];if(u==="__proto__"||u==="constructor")continue;const d=Object.getOwnPropertyDescriptor(a,u);if(!r[u])r[u]=d.get?{enumerable:!0,configurable:!0,get:nn.bind(n[u]=[d.get.bind(a)])}:d.value!==void 0?d:void 0;else{const f=n[u];f&&(d.get?f.push(d.get.bind(a)):d.value!==void 0&&f.push(()=>d.value))}}}const i={},o=Object.keys(r);for(let s=o.length-1;s>=0;s--){const a=o[s],c=r[a];c&&c.get?Object.defineProperty(i,a,c):i[a]=c?c.value:void 0}return i}function rn(e,...t){const n=t.length;if(wt&&ge in e){const i=n>1?t.flat():t[0],o=t.map(s=>new Proxy({get(a){return s.includes(a)?e[a]:void 0},has(a){return s.includes(a)&&a in e},keys(){return s.filter(a=>a in e)}},$e));return o.push(new Proxy({get(s){return i.includes(s)?void 0:e[s]},has(s){return i.includes(s)?!1:s in e},keys(){return Object.keys(e).filter(s=>!i.includes(s))}},$e)),o}const r=[];for(let i=0;i<=n;i++)r[i]={};for(const i of Object.getOwnPropertyNames(e)){let o=n;for(let c=0;c<t.length;c++)if(t[c].includes(i)){o=c;break}const s=Object.getOwnPropertyDescriptor(e,i);!s.get&&!s.set&&s.enumerable&&s.writable&&s.configurable?r[o][i]=s.value:Object.defineProperty(r[o],i,s)}return r}function on(e){let t,n;const r=i=>{const o=g.context;if(o){const[a,c]=H();g.count||(g.count=0),g.count++,(n||(n=e())).then(l=>
1{!g.done&&ae(o),g.count--,c(()=>l.default),ae()}),t=a}else if(!t){const[a]=Kt(()=>(n||(n=e())).then(c=>c.default));t=a}let s;return T(()=>(s=t())?M(()=>{if(!o||g.done)return s(i);const a=g.context;ae(o);const c=s(i);return ae(a),c}):"")};return r.preload=()=>n||((n=e()).then(i=>t=()=>i.default),n),r}let sn=0;function an(){return g.context?g.getNextContextId():`cl-${sn++}`}const cn=e=>`Stale read from <${e}>.`;function Ke(e){const t="fallback"in e&&{fallback:()=>e.fallback};return T(tn(()=>e.each,e.children,t||void 0))}function ee(e){const t=e.keyed,n=T(()=>e.when,void 0,void 0),r=t?n:T(n,void 0,{equals:(i,o)=>!i==!o});return T(()=>{const i=r();if(i){const o=e.children;return typeof o=="function"&&o.length>0?M(()=>o(t?i:()=>{if(!M(r))throw cn("Show");return n()})):o}return e.fallback},void 0,void 0)}const ln=["allowfullscreen","async","alpha","autofocus","autoplay","checked","controls","default","disabled","formnovalidate","hidden","indeterminate","inert","ismap","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","seamless","selected","adauctionheaders","browsingtopics","credentialless","defaultchecked","defaultmuted","defaultselected","defer","disablepictureinpicture","disableremoteplayback","preservespitch","shadowrootclonable","shadowrootcustomelementregistry","shadowrootdelegatesfocus","shadowrootserializable","sharedstoragewritable"],un=new Set(["className","value","readOnly","noValidate","formNoValidate","isMap","noModule","playsInline","adAuctionHeaders","allowFullscreen","browsingTopics","defaultChecked","defaultMuted","defaultSelected","disablePictureInPicture","disableRemotePlayback","preservesPitch","shadowRootClonable","shadowRootCustomElementRegistry","shadowRootDelegatesFocus","shadowRootSerializable","sharedStorageWritable",...ln]),dn=new Set(["innerHTML","textContent","innerText","children"]),fn=Object.assign(Object.create(null),{className:"class",htmlFor:"for"}),hn=Object.assign(Object.create(null),{class:"className",novalidate:{$:"noValidate",FORM:1},formnovalidate:{$:"formNoValidate",BUTTON:1,INPUT:1},ismap:{$:"isMap",IMG:1},nomodule:{$:"noModule",SCRIPT:1},playsinline:{$:"playsInline",VIDEO:1},readonly:{$:"readOnly",INPUT:1,TEXTAREA:1},adauctionheaders:{$:"adAuctionHeaders",IFRAME:1},allowfullscreen:{$:"allowFullscreen",IFRAME:1},browsingtopics:{$:"browsingTopics",IMG:1},defaultchecked:{$:"defaultChecked",INPUT:1},defaultmuted:{$:"defaultMuted",AUDIO:1,VIDEO:1},defaultselected:{$:"defaultSelected",OPTION:1},disablepictureinpicture:{$:"disablePictureInPicture",VIDEO:1},disableremoteplayback:{$:"disableRemotePlayback",AUDIO:1,VIDEO:1},preservespitch:{$:"preservesPitch",AUDIO:1,VIDEO:1},shadowrootclonable:{$:"shadowRootClonable",TEMPLATE:1},shadowrootdelegatesfocus:{$:"shadowRootDelegatesFocus",TEMPLATE:1},shadowrootserializable:{$:"shadowRootSerializable",TEMPLATE:1},sharedstoragewritable:{$:"sharedStorageWritable",IFRAME:1,IMG:1}});function mn(e,t){const n=hn[e];return typeof n=="object"?n[t]?n.$:void 0:n}const pn=new Set(["beforeinput","click","dblclick","contextmenu","focusin","focusout","input","keydown","keyup","mousedown","mousemove","mouseout","mouseover","mouseup","pointerdown","pointermove","pointerout","pointerover","pointerup","touchend","touchmove","touchstart"]),gn={xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace"},Ne=e=>T(()=>e());function wn(e,t,n){let r=n.length,i=t.length,o=r,s=0,a=0,c=t[i-1].nextSibling,l=null;for(;s<i||a<o;){if(t[s]===n[a]){s++,a++;continue}for(;t[i-1]===n[o-1];)i--,o--;if(i===s){const u=o<r?a?n[a-1].nextSibling:n[o-a]:c;for(;a<o;)e.insertBefore(n[a++],u)}else if(o===a)for(;s<i;)(!l||!l.has(t[s]))&&t[s].remove(),s++;else if(t[s]===n[o-1]&&n[a]===t[i-1]){const u=t[--i].nextSibling;e.insertBefore(n[a++],t[s++].nextSibling),e.insertBefore(n[--o],u),t[i]=n[o]}else{if(!l){l=new Map;let d=a;for(;d<o;)l.set(n[d],d++)}const u=l.get(t[s]);if(u!=null)if(a<u&&u<o){let d=s,f=1,m;for(;++d<i&&d<o&&!((m=l.get(t[d]))==null||m!==u+f);)f++;if(f>u-a){const S=t[s];for(;a<u;)e.insertBefore(n[a++],S)}else e.replaceChild(n[a++],t[s++])}else s++;else t[s++].remove()}}}const nt="_$DX_DELEGATE";function yn(e,t,n,r={}){let i;return ce(o=>{i=o,t===document?e():j(t,e(),t.firstChild?null:void 0,n)},r.owner),()=>{i(),t.textContent=""}}function U(e,t,n,r){let i;const o=()=>{const a=r?document.createElementNS("http://www.w3.org/1998/Math/MathML","template"):document.createElement("template");return a.innerHTML=e,n?a.content.firstChild.firstChild:r?a.firstChild:a.content.firstChild},s=t?()=>M(()=>document.importNode(i||(i=o()),!0)):()=>(i||(i=o())).cloneNode(!0);return s.cloneNode=s,s}function Ct(e,t=window.document){const n=t[nt]||(t[nt]=new Set);for(let r=0,i=e.length;r<i;r++){const o=e[r];
1n.has(o)||(n.add(o),t.addEventListener(o,En))}}function Se(e,t,n){re(e)||(n==null?e.removeAttribute(t):e.setAttribute(t,n))}function bn(e,t,n,r){re(e)||(r==null?e.removeAttributeNS(t,n):e.setAttributeNS(t,n,r))}function vn(e,t,n){re(e)||(n?e.setAttribute(t,""):e.removeAttribute(t))}function Sn(e,t){re(e)||(t==null?e.removeAttribute("class"):e.className=t)}function xn(e,t,n,r){if(r)Array.isArray(n)?(e[`$$${t}`]=n[0],e[`$$${t}Data`]=n[1]):e[`$$${t}`]=n;else if(Array.isArray(n)){const i=n[0];e.addEventListener(t,n[0]=o=>i.call(e,n[1],o))}else e.addEventListener(t,n,typeof n!="function"&&n)}function An(e,t,n={}){const r=Object.keys(t||{}),i=Object.keys(n);let o,s;for(o=0,s=i.length;o<s;o++){const a=i[o];!a||a==="undefined"||t[a]||(rt(e,a,!1),delete n[a])}for(o=0,s=r.length;o<s;o++){const a=r[o],c=!!t[a];!a||a==="undefined"||n[a]===c||!c||(rt(e,a,!0),n[a]=c)}return n}function Pn(e,t,n){if(!t)return n?Se(e,"style"):t;const r=e.style;if(typeof t=="string")return r.cssText=t;typeof n=="string"&&(r.cssText=n=void 0),n||(n={}),t||(t={});let i,o;for(o in n)t[o]==null&&r.removeProperty(o),delete n[o];for(o in t)i=t[o],i!==n[o]&&(r.setProperty(o,i),n[o]=i);return n}function De(e,t={},n,r){const i={};return r||W(()=>i.children=te(e,t.children,i.children)),W(()=>typeof t.ref=="function"&&kn(t.ref,e)),W(()=>Cn(e,t,n,!0,i,!0)),i}function kn(e,t,n){return M(()=>e(t,n))}function j(e,t,n,r){if(n!==void 0&&!r&&(r=[]),typeof t!="function")return te(e,t,r,n);W(i=>te(e,t(),i,n),r)}function Cn(e,t,n,r,i={},o=!1){t||(t={});for(const s in i)if(!(s in t)){if(s==="children")continue;i[s]=it(e,s,null,i[s],n,o,t)}for(const s in t){if(s==="children"){r||te(e,t.children);continue}const a=t[s];i[s]=it(e,s,a,i[s],n,o,t)}}function re(e){return!!g.context&&!g.done&&(!e||e.isConnected)}function In(e){return e.toLowerCase().replace(/-([a-z])/g,(t,n)=>n.toUpperCase())}function rt(e,t,n){const r=t.trim().split(/\s+/);for(let i=0,o=r.length;i<o;i++)e.classList.toggle(r[i],n)}function it(e,t,n,r,i,o,s){let a,c,l,u,d;if(t==="style")return Pn(e,n,r);if(t==="classList")return An(e,n,r);if(n===r)return r;if(t==="ref")o||n(e);else if(t.slice(0,3)==="on:"){const f=t.slice(3);r&&e.removeEventListener(f,r,typeof r!="function"&&r),n&&e.addEventListener(f,n,typeof n!="function"&&n)}else if(t.slice(0,10)==="oncapture:"){const f=t.slice(10);r&&e.removeEventListener(f,r,!0),n&&e.addEventListener(f,n,!0)}else if(t.slice(0,2)==="on"){const f=t.slice(2).toLowerCase(),m=pn.has(f);if(!m&&r){const S=Array.isArray(r)?r[0]:r;e.removeEventListener(f,S)}(m||n)&&(xn(e,f,n,m),m&&Ct([f]))}else if(t.slice(0,5)==="attr:")Se(e,t.slice(5),n);else if(t.slice(0,5)==="bool:")vn(e,t.slice(5),n);else if((d=t.slice(0,5)==="prop:")||(l=dn.has(t))||!i&&((u=mn(t,e.tagName))||(c=un.has(t)))||(a=e.nodeName.includes("-")||"is"in s)){if(d)t=t.slice(5),c=!0;else if(re(e))return n;t==="class"||t==="className"?Sn(e,n):a&&!c&&!l?e[In(t)]=n:e[u||t]=n}else{const f=i&&t.indexOf(":")>-1&&gn[t.split(":")[0]];f?bn(e,f,t,n):Se(e,fn[t]||t,n)}return n}function En(e){if(g.registry&&g.events&&g.events.find(([c,l])=>l===e))return;let t=e.target;const n=`$$${e.type}`,r=e.target,i=e.currentTarget,o=c=>Object.defineProperty(e,"target",{configurable:!0,value:c}),s=()=>{const c=t[n];if(c&&!t.disabled){const l=t[`${n}Data`];if(l!==void 0?c.call(t,l,e):c.call(t,e),e.cancelBubble)return}return t.host&&typeof t.host!="string"&&!t.host._$host&&t.contains(e.target)&&o(t.host),!0},a=()=>{for(;s()&&(t=t._$host||t.parentNode||t.host););};if(Object.defineProperty(e,"currentTarget",{configurable:!0,get(){return t||document}}),g.registry&&!g.done&&(g.done=_$HY.done=!0),e.composedPath){const c=e.composedPath();o(c[0]);for(let l=0;l<c.length-2&&(t=c[l],!!s());l++){if(t._$host){t=t._$host,a();break}if(t.parentNode===i)break}}else a();o(r)}function te(e,t,n,r,i){const o=re(e);if(o){!n&&(n=[...e.childNodes]);let c=[];for(let l=0;l<n.length;l++){const u=n[l];u.nodeType===8&&u.data.slice(0,2)==="!$"?u.remove():c.push(u)}n=c}for(;typeof n=="function";)n=n();if(t===n)return n;const s=typeof t,a=r!==void 0;
1if(e=a&&n[0]&&n[0].parentNode||e,s==="string"||s==="number"){if(o||s==="number"&&(t=t.toString(),t===n))return n;if(a){let c=n[0];c&&c.nodeType===3?c.data!==t&&(c.data=t):c=document.createTextNode(t),n=Y(e,n,r,c)}else n!==""&&typeof n=="string"?n=e.firstChild.data=t:n=e.textContent=t}else if(t==null||s==="boolean"){if(o)return n;n=Y(e,n,r)}else{if(s==="function")return W(()=>{let c=t();for(;typeof c=="function";)c=c();n=te(e,c,n,r)}),()=>n;if(Array.isArray(t)){const c=[],l=n&&Array.isArray(n);if(je(c,t,n,i))return W(()=>n=te(e,c,n,r,!0)),()=>n;if(o){if(!c.length)return n;if(r===void 0)return n=[...e.childNodes];let u=c[0];if(u.parentNode!==e)return n;const d=[u];for(;(u=u.nextSibling)!==r;)d.push(u);return n=d}if(c.length===0){if(n=Y(e,n,r),a)return n}else l?n.length===0?ot(e,c,r):wn(e,n,c):(n&&Y(e),ot(e,c));n=c}else if(t.nodeType){if(o&&t.parentNode)return n=a?[t]:t;if(Array.isArray(n)){if(a)return n=Y(e,n,r,t);Y(e,n,null,t)}else n==null||n===""||!e.firstChild?e.appendChild(t):e.replaceChild(t,e.firstChild);n=t}}return n}function je(e,t,n,r){let i=!1;for(let o=0,s=t.length;o<s;o++){let a=t[o],c=n&&n[e.length],l;if(!(a==null||a===!0||a===!1))if((l=typeof a)=="object"&&a.nodeType)e.push(a);else if(Array.isArray(a))i=je(e,a,c)||i;else if(l==="function")if(r){for(;typeof a=="function";)a=a();i=je(e,Array.isArray(a)?a:[a],Array.isArray(c)?c:[c])||i}else e.push(a),i=!0;else{const u=String(a);c&&c.nodeType===3&&c.data===u?e.push(c):e.push(document.createTextNode(u))}}return i}function ot(e,t,n=null){for(let r=0,i=t.length;r<i;r++)e.insertBefore(t[r],n)}function Y(e,t,n,r){if(n===void 0)return e.textContent="";const i=r||document.createTextNode("");if(t.length){let o=!1;for(let s=t.length-1;s>=0;s--){const a=t[s];if(i!==a){const c=a.parentNode===e;!o&&!s?c?e.replaceChild(i,a):e.insertBefore(i,n):c&&a.remove()}else o=!0}}else e.insertBefore(i,n);return[i]}const Tn=!1;var _n="2.0.0";const y={Awesomium:"awesomium",Cef:"cef",CefSharp:"cefsharp",CoachJS:"coachjs",Electron:"electron",FMiner:"fminer",Geb:"geb",NightmareJS:"nightmarejs",Phantomas:"phantomas",PhantomJS:"phantomjs",Rhino:"rhino",Selenium:"selenium",Sequentum:"sequentum",SlimerJS:"slimerjs",WebDriverIO:"webdriverio",WebDriver:"webdriver",HeadlessChrome:"headless_chrome",Unknown:"unknown"};class k extends Error{constructor(t,n){super(n),this.state=t,this.name="BotdError",Object.setPrototypeOf(this,k.prototype)}}function Ln(e,t){const n={};let r={bot:!1};for(const i in t){const o=t[i],s=o(e);let a={bot:!1};typeof s=="string"?a={bot:!0,botKind:s}:s&&(a={bot:!0,botKind:y.Unknown}),n[i]=a,a.bot&&(r=a)}return[n,r]}async function Rn(e){const t={},n=Object.keys(e);return await Promise.all(n.map(async r=>{const i=e[r];try{t[r]={value:await i(),state:0}}catch(o){o instanceof k?t[r]={state:o.state,error:`${o.name}: ${o.message}`}:t[r]={state:-3,error:o instanceof Error?`${o.name}: ${o.message}`:String(o)}}})),t}function On({appVersion:e}){if(e.state!==0)return!1;if(/headless/i.test(e.value))return y.HeadlessChrome;if(/electron/i.test(e.value))return y.Electron;if(/slimerjs/i.test(e.value))return y.SlimerJS}function me(e,t){return e.indexOf(t)!==-1}function F(e,t){return e.indexOf(t)!==-1}function $n(e,t){if("find"in e)return e.find(t);for(let n=0;n<e.length;n++)if(t(e[n],n,e))return e[n]}function st(e){return Object.getOwnPropertyNames(e)}function He(e,...t){for(const n of t)if(typeof n=="string"){if(me(e,n))return!0}else if($n(e,i=>n.test(i))!=null)return!0;return!1}function Z(e){return e.reduce((t,n)=>t+(n?1:0),0)}function Mn({documentElementKeys:e}){if(e.state!==0)return!1;if(He(e.value,"selenium","webdriver","driver"))return y.Selenium}function Nn({errorTrace:e}){if(e.state!==0)return!1;if(/PhantomJS/i.test(e.value))return y.PhantomJS}function Dn({evalLength:e,browserKind:t,browserEngineKind:n}){if(e.state!==0||t.state!==0||n.state!==0)return;const r=e.value;return n.value==="unknown"?!1:r===37&&!me(["webkit","gecko"],n.value)||r===39&&!me(["internet_explorer"],t.value)||r===33&&!me(["chromium"],n.value)}function jn({functionBind:e}){if(e.state===-2)return y.PhantomJS}function Hn({languages:e}){if(e.state===0&&e.value.length===0)return y.HeadlessChrome}function Un({mimeTypesConsistent:e}){if(e.state===0&&!e.value)return y.Unknown}function Wn({notificationPermissions:e,browserKind:t}){if(t.state!==0||t.value!=="chrome")return!1;if(e.state===0&&e.value)return y.HeadlessChrome}function Bn({pluginsArray:e}){if(e.state===0&&!e.value)return y.HeadlessChrome}function qn({pluginsLength:e,android:t,browserKind:n,browserEngineKind:r}){if(!(e.state!==0||t.state!==0||n.state!==0||r.state!==0)&&!(n.value!=="chrome"||t.value||r.value!=="chromium")&&e.value===0)return y.HeadlessChrome}function Fn({process:e}){var t;if(e.state!==0)return!1;if(e.value.type==="renderer"||((t=e.value.versions)===null||t===void 0?void 0:t.electron)!=null)return y.Electron}function Vn({productSub:e,browserKind:t}){if(e.state!==0||t.state!==0)return!1;if((t.value==="chrome"||t.value==="safari"||t.value==="opera"||t.value==="wechat")&&e.value!=="20030107")return y.Unknown}function Kn({userAgent:e}){if(e.state!==0)return!1;if(/PhantomJS/i.test(e.value))return y.PhantomJS;if(/Headless/i.test(e.value))return y.HeadlessChrome;if(/Electron/i.test(e.value))return y.Electron;if(/slimerjs/i.test(e.value))return y.SlimerJS}function Gn({webDriver:e}){if(e.state===0&&e.value)return y.HeadlessChrome}function zn({webGL:e}){if(e.state===0){const{vendor:t,renderer:n}=e.value;if(t=="Brian Paul"&&n=="Mesa OffScreen")return y.HeadlessChrome}}function Qn({windowExternal:e}){if(e.state!==0)return!1;if(/Sequentum/i.test(e.value))return y.Sequentum}function Jn({windowSize:e,documentFocus:t}){if(e.state!==0||t.state!==0)return!1;const{outerWidth:n,outerHeight:r}=e.value;if(t.value&&n===0&&r===0)return y.HeadlessChrome}function Xn({distinctiveProps:e}){if(e.state!==0)return!1;const t=e.value;let n;for(n in t)if(t[n])return n}const Yn={detectAppVersion:On,detectDocumentAttributes:Mn,detectErrorTrace:Nn,detectEvalLengthInconsistency:Dn,detectFunctionBind:jn,detectLanguagesLengthInconsistency:Hn,detectNotificationPermissions:Wn,detectPluginsArray:Bn,detectPluginsLengthInconsistency:qn,detectProcess:Fn,detectUserAgent:Kn,detectWebDriver:Gn,detectWebGL:zn,detectWindowExternal:Qn,detectWindowSize:Jn,detectMimeTypesConsistent:Un,detectProductSub:Vn,detectDistinctiveProperties:Xn};function Zn(){const e=navigator.appVersion;if(e==null)throw new k(-1,"navigator.appVersion is undefined");return e}function er(){if(document.documentElement===void 0)throw new k(-1,"document.documentElement is undefined");const{documentElement:e}=document;if(typeof e.getAttributeNames!="function")throw new k(-2,"document.documentElement.getAttributeNames is not a function");return e.getAttributeNames()}function tr(){try{null[0]()}catch(e){if(e instanceof Error&&e.stack!=null)return e.stack.toString()}throw new k(-3,"errorTrace signal unexpected behaviour")}function nr(){return eval.toString().length}
1function rr(){if(Function.prototype.bind===void 0)throw new k(-2,"Function.prototype.bind is undefined");return Function.prototype.bind.toString()}function Ge(){var e,t;const n=window,r=navigator;return Z(["webkitPersistentStorage"in r,"webkitTemporaryStorage"in r,r.vendor.indexOf("Google")===0,"webkitResolveLocalFileSystemURL"in n,"BatteryManager"in n,"webkitMediaStream"in n,"webkitSpeechGrammar"in n])>=5?"chromium":Z(["ApplePayError"in n,"CSSPrimitiveValue"in n,"Counter"in n,r.vendor.indexOf("Apple")===0,"getStorageUpdates"in r,"WebKitMediaKeys"in n])>=4?"webkit":Z(["buildID"in navigator,"MozAppearance"in((t=(e=document.documentElement)===null||e===void 0?void 0:e.style)!==null&&t!==void 0?t:{}),"onmozfullscreenchange"in n,"mozInnerScreenX"in n,"CSSMozDocumentRule"in n,"CanvasCaptureMediaStream"in n])>=4?"gecko":"unknown"}function ir(){var e;const t=(e=navigator.userAgent)===null||e===void 0?void 0:e.toLowerCase();return F(t,"edg/")?"edge":F(t,"trident")||F(t,"msie")?"internet_explorer":F(t,"wechat")?"wechat":F(t,"firefox")?"firefox":F(t,"opera")||F(t,"opr")?"opera":F(t,"chrome")?"chrome":F(t,"safari")?"safari":"unknown"}function or(){const e=Ge(),t=e==="chromium",n=e==="gecko",r=window,i=navigator,o="connection";return t?Z([!("SharedWorker"in r),i[o]&&"ontypechange"in i[o],!("sinkId"in new Audio)])>=2:n?Z(["onorientationchange"in r,"orientation"in r,/android/i.test(i.appVersion)])>=2:!1}function sr(){return document.hasFocus===void 0?!1:document.hasFocus()}function ar(){const e=window;return Z([!("MediaSettingsRange"in e),"RTCEncodedAudioFrame"in e,""+e.Intl=="[object Intl]",""+e.Reflect=="[object Reflect]"])>=3}function cr(){const e=navigator,t=[],n=e.language||e.userLanguage||e.browserLanguage||e.systemLanguage;if(n!==void 0&&t.push([n]),Array.isArray(e.languages))Ge()==="chromium"&&ar()||t.push(e.languages);else if(typeof e.languages=="string"){const r=e.languages;r&&t.push(r.split(","))}return t}function lr(){if(navigator.mimeTypes===void 0)throw new k(-1,"navigator.mimeTypes is undefined");const{mimeTypes:e}=navigator;let t=Object.getPrototypeOf(e)===MimeTypeArray.prototype;for(let n=0;n<e.length;n++)t&&(t=Object.getPrototypeOf(e[n])===MimeType.prototype);return t}async function ur(){if(window.Notification===void 0)throw new k(-1,"window.Notification is undefined");if(navigator.permissions===void 0)throw new k(-1,"navigator.permissions is undefined");const{permissions:e}=navigator;if(typeof e.query!="function")throw new k(-2,"navigator.permissions.query is not a function");try{const t=await e.query({name:"notifications"});return window.Notification.permission==="denied"&&t.state==="prompt"}catch{throw new k(-3,"notificationPermissions signal unexpected behaviour")}}function dr(){if(navigator.plugins===void 0)throw new k(-1,"navigator.plugins is undefined");if(window.PluginArray===void 0)throw new k(-1,"window.PluginArray is undefined");return navigator.plugins instanceof PluginArray}function fr(){if(navigator.plugins===void 0)throw new k(-1,"navigator.plugins is undefined");if(navigator.plugins.length===void 0)throw new k(-3,"navigator.plugins.length is undefined");return navigator.plugins.length}function hr(){const{process:e}=window,t="window.process is";if(e===void 0)throw new k(-1,`${t} undefined`);if(e&&typeof e!="object")throw new k(-3,`${t} not an object`);return e}function mr(){const{productSub:e}=navigator;if(e===void 0)throw new k(-1,"navigator.productSub is undefined");return e}function pr(){if(navigator.connection===void 0)throw new k(-1,"navigator.connection is undefined");if(navigator.connection.rtt===void 0)throw new k(-1,"navigator.connection.rtt is undefined");return navigator.connection.rtt}function gr(){return navigator.userAgent}function wr(){if(navigator.webdriver==null)throw new k(-1,"navigator.webdriver is undefined");return navigator.webdriver}function yr(){const e=document.createElement("canvas");if(typeof e.getContext!="function")throw new k(-2,"HTMLCanvasElement.getContext is not a function");const t=e.getContext("webgl");if(t===null)throw new k(-4,"WebGLRenderingContext is null");if(typeof t.getParameter!="function")throw new k(-2,"WebGLRenderingContext.getParameter is not a function");const n=t.getParameter(t.VENDOR),r=t.getParameter(t.RENDERER);return{vendor:n,renderer:r}}function br(){if(window.external===void 0)throw new k(-1,"window.external is undefined");const{external:e}=window;if(typeof e.toString!="function")throw new k(-2,"window.external.toString is not a function");return e.toString()}function vr(){return{outerWidth:window.outerWidth,outerHeight:window.outerHeight,innerWidth:window.innerWidth,innerHeight:window.innerHeight}}function Sr(){const e={[y.Awesomium]:{window:["awesomium"]},[y.Cef]:{window:["RunPerfTest"]},[y.CefSharp]:{window:["CefSharp"]},[y.CoachJS]:{window:["emit"]},[y.FMiner]:{window:["fmget_targets"]},[y.Geb]:{window:["geb"]}
1,[y.NightmareJS]:{window:["__nightmare","nightmare"]},[y.Phantomas]:{window:["__phantomas"]},[y.PhantomJS]:{window:["callPhantom","_phantom"]},[y.Rhino]:{window:["spawn"]},[y.Selenium]:{window:["_Selenium_IDE_Recorder","_selenium","calledSelenium",/^([a-z]){3}_.*_(Array|Promise|Symbol)$/],document:["__selenium_evaluate","selenium-evaluate","__selenium_unwrapped"]},[y.WebDriverIO]:{window:["wdioElectron"]},[y.WebDriver]:{window:["webdriver","__webdriverFunc","__lastWatirAlert","__lastWatirConfirm","__lastWatirPrompt","_WEBDRIVER_ELEM_CACHE","ChromeDriverw"],document:["__webdriver_script_fn","__driver_evaluate","__webdriver_evaluate","__fxdriver_evaluate","__driver_unwrapped","__webdriver_unwrapped","__fxdriver_unwrapped","__webdriver_script_fn","__webdriver_script_func","__webdriver_script_function","$cdc_asdjflasutopfhvcZLmcf","$cdc_asdjflasutopfhvcZLmcfl_","$chrome_asyncScriptInfo","__$webdriverAsyncExecutor"]},[y.HeadlessChrome]:{window:["domAutomation","domAutomationController"]}};let t;const n={},r=st(window);let i=[];window.document!==void 0&&(i=st(window.document));for(t in e){const o=e[t];if(o!==void 0){const s=o.window===void 0?!1:He(r,...o.window),a=o.document===void 0||!i.length?!1:He(i,...o.document);n[t]=s||a}}return n}const xr={android:or,browserKind:ir,browserEngineKind:Ge,documentFocus:sr,userAgent:gr,appVersion:Zn,rtt:pr,windowSize:vr,pluginsLength:fr,pluginsArray:dr,errorTrace:tr,productSub:mr,windowExternal:br,mimeTypesConsistent:lr,evalLength:nr,webGL:yr,webDriver:wr,languages:cr,notificationPermissions:ur,documentElementKeys:er,functionBind:rr,process:hr,distinctiveProps:Sr};class Ar{constructor(){this.components=void 0,this.detections=void 0}getComponents(){return this.components}getDetections(){return this.detections}detect(){if(this.components===void 0)throw new Error("BotDetector.detect can't be called before BotDetector.collect");const[t,n]=Ln(this.components,Yn);return this.detections=t,n}async collect(){return this.components=await Rn(xr),this.components}}function Pr(){if(!(window.__fpjs_d_m||Math.random()>=.001))try{const e=new XMLHttpRequest;e.open("get",`https://m1.openfpcdn.io/botd/v${_n}/npm-monitoring`,!0),e.send()}catch(e){console.error(e)}}async function kr({monitoring:e=!0}={}){e&&Pr();const t=new Ar;return await t.collect(),t}var Cr=U("<span>Co-founder of <a href=https://baleya.dev>Baleya</a>. "),Ir=U(`<header><h1>Wind Wong</h1><aside id=bio>PhD student since 2025, supervised by <a href=https://gleissen.github.io>Klaus von Gleissenthall</a> in the <a href=https://plsec-vu.github.io/>PLSec</a> group at VU Amsterdam. Previously an MSci student at the University of Glasgow.</aside><p>Email: <a href=mailto:[email protected]>[email protected]</a> (general) and <a href=mailto:[email protected]>[email protected]</a> (academia)</p><p>GitHub: <a href=https://github.com/windtf/>https://github.com/windtf</a></p><p>Google Scholar: <a href="https://scholar.google.com/citations?user=O-FvsRkAAAAJ">Wind Wong</a></p><p>I'm interested in building high-assurance cryptographic systems by bridging formal verification (mechanized proofs & symbolic execution) with low-level execution realities (microarchitecture, side channels, and networking).`);const Er=()=>{const[e,t]=H(!0);return vt(async()=>{try{const r=await(await kr()).detect();t(r.bot)}catch(n){console.error("Bot detection failed",n)}}),x(ee,{get when(){return!e()},get children(){var n=Ir(),r=n.firstChild,i=r.nextSibling,o=i.firstChild;return j(i,x(ee,{when:!1,get children(){return Cr()}}),o),n}})};function It(){let e=new Set;function t(i){return e.add(i),()=>e.delete(i)}let n=!1;function r(i,o){if(n)return!(n=!1);const s={to:i,options:o,defaultPrevented:!1,preventDefault:()=>s.defaultPrevented=!0};for(const a of e)a.listener({...s,from:a.location,retry:c=>{c&&(n=!0),a.navigate(i,{...o,resolve:!1})}});return!s.defaultPrevented}return{subscribe:t,confirm:r}}let Ue;function ze(){(!window.history.state||window.history.state._depth==null)&&window.history.replaceState({...window.history.state,_depth:window.history.length-1},""),Ue=window.history.state._depth}ze();function Tr(e){return{...e,_depth:window.history.state&&window.history.state._depth}}function _r(e,t){let n=!1;return()=>{const r=Ue;ze();const i=r==null?null:Ue-r;if(n){n=!1;return}i&&t(i)?(n=!0,window.history.go(-i)):e()}}const Lr=/^(?:[a-z0-9]+:)?\/\//i,Rr=/^\/+|(\/)\/+$/g,Et="http://sr";function z(e,t=!1){const n=e.replace(Rr,"$1");return n?t||/^[?#]/.test(n)?n:"/"+n:""}function pe(e,t,n){if(Lr.test(t))return;const r=z(e),i=n&&z(n);let o="";return!i||t.startsWith("/")?o=r:i.toLowerCase().indexOf(r.toLowerCase())!==0?o=r+i:o=i,(o||"/")+z(t,!o)}function Or(e,t){if(e==null)throw new Error(t);return e}function $r(e,t){return z(e).replace(/\/*(\*.*)?$/g,"")+z(t)}function Tt(e){const t={};
1return e.searchParams.forEach((n,r)=>{r in t?Array.isArray(t[r])?t[r].push(n):t[r]=[t[r],n]:t[r]=n}),t}function Mr(e,t,n){const[r,i]=e.split("/*",2),o=r.split("/").filter(Boolean),s=o.length;return a=>{const c=a.split("/").filter(Boolean),l=c.length-s;if(l<0||l>0&&i===void 0&&!t)return null;const u={path:s?"":"/",params:{}},d=f=>n===void 0?void 0:n[f];for(let f=0;f<s;f++){const m=o[f],S=m[0]===":",h=S?c[f]:c[f].toLowerCase(),p=S?m.slice(1):m.toLowerCase();if(S&&Le(h,d(p)))u.params[p]=h;else if(S||!Le(h,p))return null;u.path+=`/${h}`}if(i){const f=l?c.slice(-l).join("/"):"";if(Le(f,d(i)))u.params[i]=f;else return null}return u}}function Le(e,t){const n=r=>r===e;return t===void 0?!0:typeof t=="string"?n(t):typeof t=="function"?t(e):Array.isArray(t)?t.some(n):t instanceof RegExp?t.test(e):!1}function Nr(e){const[t,n]=e.pattern.split("/*",2),r=t.split("/").filter(Boolean);return r.reduce((i,o)=>i+(o.startsWith(":")?2:3),r.length-(n===void 0?0:1))}function _t(e){const t=new Map,n=St();return new Proxy({},{get(r,i){return t.has(i)||Be(n,()=>t.set(i,T(()=>e()[i]))),t.get(i)()},getOwnPropertyDescriptor(){return{enumerable:!0,configurable:!0}},ownKeys(){return Reflect.ownKeys(e())},has(r,i){return i in e()}})}function Lt(e){let t=/(\/?\:[^\/]+)\?/.exec(e);if(!t)return[e];let n=e.slice(0,t.index),r=e.slice(t.index+t[0].length);const i=[n,n+=t[1]];for(;t=/^(\/\:[^\/]+)\?/.exec(r);)i.push(n+=t[1]),r=r.slice(t[0].length);return Lt(r).reduce((o,s)=>[...o,...i.map(a=>a+s)],[])}const Dr=100,Rt=qe(),Qe=qe(),Je=()=>Or(Pe(Rt),"<A> and 'use' router primitives can be only used inside a Route."),jr=()=>Pe(Qe)||Je().base,Hr=e=>{const t=jr();return T(()=>t.resolvePath(e()))},Ur=e=>{const t=Je();return T(()=>{const n=e();return n!==void 0?t.renderPath(n):n})},Wr=()=>Je().location;function Br(e,t=""){const{component:n,preload:r,load:i,children:o,info:s}=e,a=!o||Array.isArray(o)&&!o.length,c={key:e,component:n,preload:r||i,info:s};return Ot(e.path).reduce((l,u)=>{for(const d of Lt(u)){const f=$r(t,d);let m=a?f:f.split("/*",1)[0];m=m.split("/").map(S=>S.startsWith(":")||S.startsWith("*")?S:encodeURIComponent(S)).join("/"),l.push({...c,originalPath:u,pattern:m,matcher:Mr(m,!a,e.matchFilters)})}return l},[])}function qr(e,t=0){return{routes:e,score:Nr(e[e.length-1])*1e4-t,matcher(n){const r=[];for(let i=e.length-1;i>=0;i--){const o=e[i],s=o.matcher(n);if(!s)return null;r.unshift({...s,route:o})}return r}}}function Ot(e){return Array.isArray(e)?e:[e]}function $t(e,t="",n=[],r=[]){const i=Ot(e);for(let o=0,s=i.length;o<s;o++){const a=i[o];if(a&&typeof a=="object"){a.hasOwnProperty("path")||(a.path="");const c=Br(a,t);for(const l of c){n.push(l);const u=Array.isArray(a.children)&&a.children.length===0;if(a.children&&!u)$t(a.children,l.pattern,n,r);else{const d=qr([...n],r.length);r.push(d)}n.pop()}}}return n.length?r:r.sort((o,s)=>s.score-o.score)}function Re(e,t){for(let n=0,r=e.length;n<r;n++){const i=e[n].matcher(t);if(i)return i}return[]}function Fr(e,t,n){const r=new URL(Et),i=T(u=>{const d=e();try{return new URL(d,r)}catch{return console.error(`Invalid path ${d}`),u}},r,{equals:(u,d)=>u.href===d.href}),o=T(()=>i().pathname),s=T(()=>i().search,!0),a=T(()=>i().hash),c=()=>"",l=We(s,()=>Tt(i()));return{get pathname(){return o()},get search(){return s()},get hash(){return a()},get state(){return t()},get key(){return c()},query:n?n(l):_t(l)}}let G;function Vr(){return G}function Kr(e,t,n,r={}){const{signal:[i,o],utils:s={}}=e,a=s.parsePath||(b=>b),c=s.renderPath||(b=>b),l=s.beforeLeave||It(),u=pe("",r.base||"");if(u===void 0)throw new Error(`${u} is not a valid base path`);u&&!i().value&&o({value:u,replace:!0,scroll:!1});const[d,f]=H(!1);let m;const S=(b,C)=>{C.value===h()&&C.state===A()||(m===void 0&&f(!0),G=b,m=C,zt(()=>{m===C&&(p(m.value),w(m.state),_[1](N=>N.filter(J=>J.pending)))}).finally(()=>{m===C&&Gt(()=>{G=void 0,b==="navigate"&&D(m),f(!1),m=void 0})}))},[h,p]=H(i().value),[A,w]=H(i().state),R=Fr(h,A,s.queryWrapper),I=[],_=H([]),q=T(()=>typeof r.transformUrl=="function"?Re(t(),r.transformUrl(R.pathname)):Re(t(),R.pathname)),Q=()=>{const b=q(),C={};for(let N=0;N<b.length;N++)Object.assign(C,b[N].params);return C},ie=s.paramsWrapper?s.paramsWrapper(Q,t):_t(Q),oe={pattern:u,path:()=>u,outlet:()=>null,resolvePath(b){return pe(u,b)}};return W(We(i,b=>S("native",b),{defer:!0})),{base:oe,location:R,params:ie,isRouting:d,renderPath:c,parsePath:a,navigatorFactory:L,matches:q,beforeLeave:l,preloadRoute:O,singleFlight:r.singleFlight===void 0?!0:r.singleFlight,submissions:_};function E(b,C,N){M(()=>
1{if(typeof C=="number"){C&&(s.go?s.go(C):console.warn("Router integration does not support relative routing"));return}const J=!C||C[0]==="?",{replace:Ce,resolve:X,scroll:Ie,state:se}={replace:!1,resolve:!J,scroll:!0,...N},ue=X?b.resolvePath(C):pe(J&&R.pathname||"",C);if(ue===void 0)throw new Error(`Path '${C}' is not a routable path`);if(I.length>=Dr)throw new Error("Too many redirects");const Xe=h();(ue!==Xe||se!==A())&&(Tn||l.confirm(ue,N)&&(I.push({value:Xe,replace:Ce,scroll:Ie,state:A()}),S("navigate",{value:ue,state:se})))})}function L(b){return b=b||Pe(Qe)||oe,(C,N)=>E(b,C,N)}function D(b){const C=I[0];C&&(o({...b,replace:C.replace,scroll:C.scroll}),I.length=0)}function O(b,C){const N=Re(t(),b.pathname),J=G;G="preload";for(let Ce in N){const{route:X,params:Ie}=N[Ce];X.component&&X.component.preload&&X.component.preload();const{preload:se}=X;C&&se&&Be(n(),()=>se({params:Ie,location:{pathname:b.pathname,search:b.search,hash:b.hash,query:Tt(b),state:null,key:""},intent:"preload"}))}G=J}}function Gr(e,t,n,r){const{base:i,location:o,params:s}=e,{pattern:a,component:c,preload:l}=r().route,u=T(()=>r().path);c&&c.preload&&c.preload();const d=l?l({params:s,location:o,intent:G||"initial"}):void 0;return{parent:t,pattern:a,path:u,outlet:()=>c?x(c,{params:s,location:o,data:d,get children(){return n()}}):n(),resolvePath(m){return pe(i.path(),m,u())}}}const zr=e=>t=>{const{base:n}=t,r=Fe(()=>t.children),i=T(()=>$t(r(),t.base||""));let o;const s=Kr(e,i,()=>o,{base:n,singleFlight:t.singleFlight,transformUrl:t.transformUrl});return e.create&&e.create(s),x(Rt.Provider,{value:s,get children(){return x(Qr,{routerState:s,get root(){return t.root},get preload(){return t.rootPreload||t.rootLoad},get children(){return[Ne(()=>(o=St())&&null),x(Jr,{routerState:s,get branches(){return i()}})]}})}})};function Qr(e){const t=e.routerState.location,n=e.routerState.params,r=T(()=>e.preload&&M(()=>{e.preload({params:n,location:t,intent:Vr()||"initial"})}));return x(ee,{get when(){return e.root},keyed:!0,get fallback(){return e.children},children:i=>x(i,{params:n,location:t,get data(){return r()},get children(){return e.children}})})}function Jr(e){const t=[];let n;const r=T(We(e.routerState.matches,(i,o,s)=>{let a=o&&i.length===o.length;const c=[];for(let l=0,u=i.length;l<u;l++){const d=o&&o[l],f=i[l];s&&d&&f.route.key===d.route.key?c[l]=s[l]:(a=!1,t[l]&&t[l](),ce(m=>{t[l]=m,c[l]=Gr(e.routerState,c[l-1]||e.routerState.base,at(()=>r()[l+1]),()=>{const S=e.routerState.matches();return S[l]??S[0]})}))}return t.splice(i.length).forEach(l=>l()),s&&a?s:(n=c[0],c)}));return at(()=>r()&&n)()}const at=e=>()=>x(ee,{get when(){return e()},keyed:!0,children:t=>x(Qe.Provider,{value:t,get children(){return t.outlet()}})}),fe=e=>{const t=Fe(()=>e.children);return Me(e,{get children(){return t()}})};function Xr([e,t],n,r){return[n?()=>n(e()):e,r?i=>t(r(i)):t]}function Yr(e){let t=!1;const n=i=>typeof i=="string"?{value:i}:i,r=Xr(H(n(e.get()),{equals:(i,o)=>i.value===o.value&&i.state===o.state}),void 0,i=>(!t&&e.set(i),g.registry&&!g.done&&(g.done=!0),i));return e.init&&Ae(e.init((i=e.get())=>{t=!0,r[1](n(i)),t=!1})),zr({signal:r,create:e.create,utils:e.utils})}function Zr(e,t,n){return e.addEventListener(t,n),()=>e.removeEventListener(t,n)}function ei(e,t){const n=e&&document.getElementById(e);n?n.scrollIntoView():t&&window.scrollTo(0,0)}const ti=new Map;function ni({preload:e=!0,explicitLinks:t=!1,actionBase:n="/_server",transformUrl:r}={}){return i=>{const o=i.base.path(),s=i.navigatorFactory(i.base);let a,c;function l(h){return h.namespaceURI==="http://www.w3.org/2000/svg"}function u(h){if(h.defaultPrevented||h.button!==0||h.metaKey||h.altKey||h.ctrlKey||h.shiftKey)return;const p=h.composedPath().find(q=>q instanceof Node&&q.nodeName.toUpperCase()==="A");if(!p||t&&!p.hasAttribute("link"))return;const A=l(p),w=A?p.href.baseVal:p.href;if((A?p.target.baseVal:p.target)||!w&&!p.hasAttribute("state"))return;const I=(p.getAttribute("rel")||"").split(/\s+/);if(p.hasAttribute("download")||I&&I.includes("external"))return;const _=A?new URL(w,document.baseURI):new URL(w);if(!(_.origin!==window.location.origin||o&&_.pathname&&!_.pathname.toLowerCase().startsWith(o.toLowerCase())))return[p,_]}function d(h){const p=u(h);if(!p)return;const[A,w]=p,R=i.parsePath(w.pathname+w.search+w.hash),I=A.getAttribute("state");h.preventDefault(),s(R,{resolve:!1,replace:A.hasAttribute("replace"),scroll:!A.hasAttribute("noscroll"),state:I?JSON.parse(I):void 0})}function f(h){const p=u(h);if(!p)return;const[A,w]=p;r&&(w.pathname=r(w.pathname)),i.preloadRoute(w,A.getAttribute("preload")!=="false")}function m(h){clearTimeout(a);const p=u(h);if(!p)return c=null;const[A,w]=p;c!==A&&(r&&(w.pathname=r(w.pathname)),a=setTimeout(()=>{i.preloadRoute(w,A.getAttribute("preload")!=="false"),c=A},20))}function S(h){if(h.defaultPrevented)return;let p=h.submitter&&h.submitter.hasAttribute("formaction")?h.submitter.getAttribute("formaction"):h.target.getAttribute("action");if(!p)return;if(!p.startsWith("https://action/")){const w=new URL(p,Et);if(p=i.parsePath(w.pathname+w.search),!p.startsWith(n))return}if(h.target.method.toUpperCase()!=="POST")throw new Error("Only POST forms are supported for Actions");const A=ti.get(p);if(A){h.preventDefault();const w=new FormData(h.target,h.submitter);A.call({r:i,f:h.target},h.target.enctype==="multipart/form-data"?w:new URLSearchParams(w))}}
1Ct(["click","submit"]),document.addEventListener("click",d),e&&(document.addEventListener("mousemove",m,{passive:!0}),document.addEventListener("focusin",f,{passive:!0}),document.addEventListener("touchstart",f,{passive:!0})),document.addEventListener("submit",S),Ae(()=>{document.removeEventListener("click",d),e&&(document.removeEventListener("mousemove",m),document.removeEventListener("focusin",f),document.removeEventListener("touchstart",f)),document.removeEventListener("submit",S)})}}function ri(e){const t=()=>{const r=window.location.pathname.replace(/^\/+/,"/")+window.location.search,i=window.history.state&&window.history.state._depth&&Object.keys(window.history.state).length===1?void 0:window.history.state;return{value:r+window.location.hash,state:i}},n=It();return Yr({get:t,set({value:r,replace:i,scroll:o,state:s}){i?window.history.replaceState(Tr(s),"",r):window.history.pushState(s,"",r),ei(decodeURIComponent(window.location.hash.slice(1)),o),ze()},init:r=>Zr(window,"popstate",_r(r,i=>{if(i)return!n.confirm(i);{const o=t();return!n.confirm(o.value,{state:o.state})}})),create:ni({preload:e.preload,explicitLinks:e.explicitLinks,actionBase:e.actionBase,transformUrl:e.transformUrl}),utils:{go:r=>window.history.go(r),beforeLeave:n}})(e)}var ii=U("<a>");function Mt(e){e=Me({inactiveClass:"inactive",activeClass:"active"},e);const[,t]=rn(e,["href","state","class","activeClass","inactiveClass","end"]),n=Hr(()=>e.href),r=Ur(n),i=Wr(),o=T(()=>{const s=n();if(s===void 0)return[!1,!1];const a=z(s.split(/[?#]/,1)[0]).toLowerCase(),c=decodeURI(z(i.pathname).toLowerCase());return[e.end?a===c:c.startsWith(a+"/")||c===a,a===c]});return(()=>{var s=ii();return De(s,Me(t,{get href(){return r()||e.href},get state(){return JSON.stringify(e.state)},get classList(){return{...e.class&&{[e.class]:!0},[e.inactiveClass]:!o()[0],[e.activeClass]:o()[0],...t.classList}},link:"",get["aria-current"](){return o()[1]?"page":void 0}}),!1,!1),s})()}const Nt=qe(),oi=["title","meta"],ct=[],lt=["name","http-equiv","content","charset","media"].concat(["property"]),ut=(e,t)=>{const n=Object.fromEntries(Object.entries(e.props).filter(([r])=>t.includes(r)).sort());return(Object.hasOwn(n,"name")||Object.hasOwn(n,"property"))&&(n.name=n.name||n.property,delete n.property),e.tag+JSON.stringify(n)};function si(){if(!g.context){const n=document.head.querySelectorAll("[data-sm]");Array.prototype.forEach.call(n,r=>r.parentNode.removeChild(r))}const e=new Map;function t(n){if(n.ref)return n.ref;let r=document.querySelector(`[data-sm="${n.id}"]`);return r?(r.tagName.toLowerCase()!==n.tag&&(r.parentNode&&r.parentNode.removeChild(r),r=document.createElement(n.tag)),r.removeAttribute("data-sm")):r=document.createElement(n.tag),r}return{addTag(n){if(oi.indexOf(n.tag)!==-1){const o=n.tag==="title"?ct:lt,s=ut(n,o);e.has(s)||e.set(s,[]);let a=e.get(s),c=a.length;a=[...a,n],e.set(s,a);let l=t(n);n.ref=l,De(l,n.props);let u=null;for(var r=c-1;r>=0;r--)if(a[r]!=null){u=a[r];break}return l.parentNode!=document.head&&document.head.appendChild(l),u&&u.ref&&u.ref.parentNode&&document.head.removeChild(u.ref),c}let i=t(n);return n.ref=i,De(i,n.props),i.parentNode!=document.head&&document.head.appendChild(i),-1},removeTag(n,r){const i=n.tag==="title"?ct:lt,o=ut(n,i);if(n.ref){const s=e.get(o);if(s){if(n.ref.parentNode){n.ref.parentNode.removeChild(n.ref);for(let a=r-1;a>=0;a--)s[a]!=null&&document.head.appendChild(s[a].ref)}s[r]=null,e.set(o,s)}else n.ref.parentNode&&n.ref.parentNode.removeChild(n.ref)}}}}const ai=e=>{const t=si();return x(Nt.Provider,{value:t,get children(){return e.children}})},ci=(e,t,n)=>(li({tag:e,props:t,setting:n,id:an(),get name(){return t.name||t.property}}),null);function li(e){const t=Pe(Nt);if(!t)throw new Error("<MetaProvider /> should be in the tree");W(()=>{const n=t.addTag(e);Ae(()=>t.removeTag(e,n))})}const ui=e=>ci("meta",e);var di=U("<div style=text-align:center;margin-top:100px><h1>404 - Not Found</h1><p>The page you are looking for does not exist.");const fi=()=>(()=>{var e=di(),t=e.firstChild;return t.nextSibling,j(e,x(ui,{name:"robots",content:"noindex, nofollow"}),t),j(e,x(Mt,{href:"/",children:"Return to Home"}),null),e})();const hi=[{title:"Papers",items:[{prefix:"PLDI'26.",title:"Pantomime: Constructive Leakage Proofs via Simulation.",subtext:"Robin Webbers, Robert Schenck, Wind Wong, Kristina Sojakova, Klaus v. Gleissenthall",links:[{text:"doi",url:"https://doi.org/10.1145/3808340"},{text:"pdf",url:"./static/artifacts/pantomime.pdf"},{text:"appendix",url:"./static/artifacts/pantomime-supp.pdf"}]},{prefix:"TYPES'25 Post-proc.",title:"Symmetries in Sorting.",subtext:"Vikraman Choudhury, Wind Wong",links:[{text:"doi",url:"https://doi.org/10.4230/LIPIcs.TYPES.2025.3"},{text:"arxiv",url:"https://arxiv.org/abs/2512.07349"}]}]},{title:"Talks",items:[{title:"On commutativity, total orders, and sorting.",subtext:"HoTT/UF 2024",links:[{text:"abstract",url:"https://hal.science/hal-04838253v1/file/HoTTUF_2024_paper_21.pdf"},{text:"slides",url:"./static/artifacts/hottuf2024-slides.pdf"}]}]},{title:"Vulnerabilities",items:[{title:"Constant-time 64-bit multiplication missing in wolfSSL on RISC-V RV32I architectures.",subtext:"CVE-2026-3579",links:[{text:"nvd",url:"https://nvd.nist.gov/vuln/detail/CVE-2026-3579"}]},{title:"Compiler-induced timing leak in wolfSSL on RISC-V.",subtext:"CVE-2026-3580",links:[{text:"nvd",url:"https://nvd.nist.gov/vuln/detail/CVE-2026-3580"}]}]},{title:"Theses",items:[{title:"Intrinsically-Typed Interpreter for Generalized Multiparty Session Calculus.",subtext:"MSci, University of Glasgow, 2025",links:[{text:"pdf",url:"./static/artifacts/msc-thesis.pdf"}]},{title:"Sorting Correctly with Symmetries.",subtext:"BSc, University of Glasgow, 2024",links:[{text:"pdf",url:"./static/artifacts/bsc-thesis.pdf"}]}]},{title:"Projects",items:[{title:"Wireproxy.",subtext:"Userspace proxy for wireguard",links:[{text:"github",url:"https://github.com/windtf/wireproxy"}]}]}];var mi=U("<b> "),pi=U("<li> <em></em><br>"),gi=U('<a target=_blank rel="noopener noreferrer">[<!>]'),wi=U("<hr>"),yi=U("<h2>"),bi=U("<ul>");const vi=e=>(()=>{var t=pi(),n=t.firstChild,r=n.nextSibling;return r.nextSibling,j(t,x(ee,{get when(){return e.item.prefix},get children(){var i=mi(),o=i.firstChild;return j(i,()=>e.item.prefix,o),i}}),n),j(t,()=>e.item.title,n),j(r,()=>e.item.subtext),j(t,x(Ke,{get each(){return e.item.links},children:i=>[x(ee,{get when(){return Ne(()=>!!i.url.startsWith("/"))()&&!i.url.includes("/static/")},get fallback(){return(()=>{var o=gi(),s=o.firstChild,a=s.nextSibling;return a.nextSibling,j(o,()=>i.text,a),W(()=>Se(o,"href",i.url)),o})()},get children(){return x(Mt,{get href(){return i.url},get children(){return["[",Ne(()=>i.text),"]"]}})}})," "]}),null),t})(),Si=e=>[wi(),(()=>{var t=yi();return j(t,()=>e.section.title),t})(),(()=>{var t=bi();return j(t,x(Ke,{get each(){return e.section.items},children:n=>x(vi,{item:n})}
1)),t})()],Dt=`# Unauthorized payment for Hong Kong e-payment service Tap & Go 2 3**Note: The security issues documented in this post were quickly remediated back in December 2019.** 4I contacted their customer support soon after discovering the issues, and less than a week later, 5they updated the Android app to version 8.13.8 and implemented proper security checks in the backend. 6I had not documented this vulnerability anywhere until 2026, and since then, 7Tap & Go has strengthened its security and guarded its apps with proper reverse engineering protections. 8 9## Introduction 10 11Tap & Go is an e-payment service provided by HKT Payment Limited in Hong Kong. 12It allows users to make payments for transportation and retail purchases using their mobile device or a physical card. 13To make a payment, the app generates a payment QR code, which is then scanned by the merchant's terminal to complete the transaction. Tap & Go also provides MasterCard and UnionPay cards, which can be used to make payments at physical stores. 14 15Back in 2019, I was a secondary school student who was reverse-engineering their 16Android app out of curiosity. It turned out 17that the endpoints for creating payment QR codes and viewing transaction history were not authenticated. This meant 18that anyone could create payment QR codes and view transaction history of any user by their phone number. 19 20## Bypassing Security Protections 21 22To start poking around the internal APIs, I first had to get the app into a state where I could actually observe its behavior. This involved a few standard reverse engineering steps. 23 24### Decompilation 25 26I started by decompiling the APK using [JADX](https://github.com/skylot/jadx). 27While most third-party libraries and some core logic were obfuscated, a 28significant portion of the actual application's code was not. This made it much 29easier to identify the critical paths for authentication and network 30communication. 31 32### Anti-Root & Anti-MITM 33 34The app initially refused to launch on my device because it was rooted. A quick look through the decompiled code revealed that it was using [Rootbeer](https://github.com/scottyab/rootbeer) for integrity checks. Since Rootbeer is a common library, I was able to use an existing Xposed module to neutralize it, allowing the app to launch successfully. 35 36With the app running, the next hurdle was intercepting the HTTPS traffic. The app implemented certificate pinning, which prevented standard man-in-the-middle (MITM) tools like [mitmproxy](https://mitmproxy.org/) from inspecting the traffic. 37 38### Internal API Logic 39 40Most of the API logic was contained within the \`com.pccwmobile.tapandgo.api\` package. At the time, there was an ongoing Kotlin rewrite in the app, which meant that while the logic for most endpoints was in this package, a handful were located elsewhere. All requests were sent via HTTPS POST requests with \`application/x-www-form-urlencoded\` bodies. 41 42Each endpoint had its own dedicated class that extended either \`AbstractApiV2\` or \`AbstractRetrofitApi\`. The internal logic followed a consistent pattern, where the actual request generation happened in the \`callAPI(Context context)\` method. 43 44For instance, the \`GenerateOtpByMsisdnApiV2\` class (which generates the request shown below) handled parameter assembly and signing like this: 45 46\`\`\`java 47@Override // com.pccwmobile.tapandgo.api.BaseAPI 48public GenerateOtpByMsisdnResultV2 callAPI(Context context) { 49 // simplified, there's other stuff like internet checks, etc. in the real code 50 ArrayList arrayList = new ArrayList(); 51 arrayList.add(new l("deviceId", this.deviceId)); 52 arrayList.add(new l("language", this.language)); 53 arrayList.add(new l("msisdn", this.msisdn)); 54 arrayList.add(new l("os", "A")); 55 arrayList.add(new l("osVer", CommonUtilities.b())); 56 arrayList.add(new l(AppMeasurement.Param.TIMESTAMP, this.timestamp)); 57 58 this.sign = "deviceId=" + this.deviceId + "&language=" + this.language + "&msisdn=" + this.msisdn + "&os=A&osVer=" + CommonUtilities.b() + "&" + AppMeasurement.Param.TIMESTAMP + "=" + this.timestamp; 59 this.sign = k.a(this.sign); 60 arrayList.add(new l("sign", this.sign)); 61 62 return this.responseHandler(HttpUtilities.executeHttpPost( 63 "https://wallet.tapngo.com.hk/wallet/v2/otpGenerationByMsisdn", 64 arrayList, 65 HttpUtilities.getKeyStore( 66 context.getResources().openRawResource(R.raw.server_cert), 67 CommonUtilities.j() 68 ) 69 )); 70} 71\`\`\` 72 73The first thing to notice is \`HttpUtilities.executeHttpPost\` takes in the pinned certificate as a \`KeyStore\`. Tracing where the \`KeyStore\` is passed to eventually leads us to an obfuscated class with this function: 74 75\`\`\`java 76public static j a(KeyStore keyStore, String[] strArr, b bVar) { 77 i iVar = new i(); 78 iVar.a(new e("https", new b.g.a.a.a.b(keyStore, strArr, 0L), 443)); 79 return new k(new g.a.b.h.c.a.i(bVar, iVar), bVar); 80} 81\`\`\` 82 83Patching \`"https"\` to some other string neutralizes the SSL pinning, and now we can inspect the packets with a reverse proxy. A typical request for generating an SMS login OTP looked like this: 84 85\`\`\`http 86POST /wallet/v2/otpGenerationByMsisdn HTTP/1.1 87Host: wallet.tapngo.com.hk 88Content-Type: application/x-www-form-urlencoded 89 90deviceId=xyz&language=E&msisdn=85291234567&os=A&osVer=12×tamp=1639567890123&sign=SGVsbG8gd29ybGQh... 91\`\`\` 92
93There were two critical requirements for a request to be accepted by the backend: 94 951. **Parameter Ordering**: To generate a valid signature, the parameters had to be in the correct order. This has 96 to be inferred from the request generation logic. 972. **Request Signature**: Every requests required a \`sign\` parameter, which was 98 a 64-byte base64-encoded string of some sort. 99 100As seen in the code above, the parameters were concatenated in a fixed order and then passed to \`k.a()\`, which is responsible for generating an HMAC-SHA512 digest: 101 102\`\`\`java 103public static String a(String str) { 104 // try-catch omitted for brevity 105 SecretKeySpec secretKeySpec = new SecretKeySpec(CommonUtilities.f().getBytes("UTF-8"), "HmacSHA512"); 106 Mac mac = Mac.getInstance("HmacSHA512"); 107 mac.init(secretKeySpec); 108 return Base64.encodeToString(mac.doFinal(str.getBytes()), 2); 109} 110\`\`\` 111 112The \`CommonUtilities.f()\` method is actually just a wrapper around a native call: 113 114\`\`\`java 115private static native String getApiSignatureKey(); 116\`\`\` 117 118Next, I had to dig into the native libraries in the app, and quickly located 119\`getApiSignatureKey\` to be in \`libTapAndGo.so\`. The binary wasn't obfuscated at 120all, so opening it with 121[Ghidra](https://github.com/nationalsecurityagency/ghidra) quickly showed the 122signature key is just a simple 12386-character ASCII string which I will omit in this writeup. 124 125Now, we can intercept every request from the app, and craft our own requests to any endpoints. 126 127## The Attack 128 129Once I was able to craft valid requests, I immediately jumped into looking for interesting endpoints. I quickly found \`GetPaymentInfoDynamicQrApiV2\` to be the endpoint responsible for generating payment QR codes. 130 131\`\`\`java 132@Override // com.pccwmobile.tapandgo.api.BaseAPI 133public GetPaymentInfoDynamicQrResultV2 callAPI(Context context) { 134 // the initial error checking for internet connection, etc. is omitted 135 ArrayList arrayList = new ArrayList(); 136 if (!a.a(this.encryptedCardInfo)) { 137 arrayList.add(new l("card", this.encryptedCardInfo)); 138 } 139 if (!a.a(this.encryptAccountIdList)) { 140 arrayList.add(new l(MakeOrderForStaticQrApiV2.REQ_PARM_KEY_ACCOUNT_ID_LIST, this.encryptAccountIdList)); 141 } 142 arrayList.add(new l("os", "A")); 143 arrayList.add(new l("osVer", CommonUtilities.b())); 144 if (!a.a(this.pinToken)) { 145 arrayList.add(new l(REQ_PARM_KEY_PIN_TOKEN, this.pinToken)); 146 } 147 arrayList.add(new l(AppMeasurement.Param.TIMESTAMP, this.timestamp)); 148 arrayList.add(new l("currency", "HKD")); 149 arrayList.add(new l("proxyId", v.c(context, "SHARE_PREF_MSISDN_KEY"))); 150 arrayList.add(new l("proxyType", "MOBILE")); 151 this.sign = CommonUtilities.a(arrayList); // concate params into a string 152 this.sign = k.a(this.sign); // generate hmac-sha256 signature 153 arrayList.add(new l("sign", this.sign)); 154 // the rest is omitted 155} 156\`\`\` 157 158What caught my interest immediately, both in intercepted requests and looking at the code, was that no password, pin token, or any form of authentication was passed as an argument. This led me to wonder if the endpoint was secured at all. 159 160The only potentially authenticating information here is the \`card\` parameter, which would require an authenticated endpoint to fetch the encrypted card info. However, intercepted requests showed that this parameter wasn't even used. The \`encryptAccountIdList\` is just the internal user IDs of the user, encrypted with another static key found in \`libTapAndGo.so\`. 161 162Furthermore, the account IDs of any user could be fetched with the \`CheckAccountProfileApiV2\` endpoint simply by providing their phone number. This meant that if you have the phone number of a Tap & Go user, you can generate a payment QR code from their account. 163 164It seemed catastrophic if true, and sure enough, when I tried it, it worked. I quickly patched up a proof-of-concept app that generates a payment QR code for a consenting friend, and managed to buy an ice cream at 7/11 with his money. 165 166At the very least, the endpoint for accessing MasterCard and UnionPay card information are secured with proper authentication, so an attacker is limited to face-to-face, over-the-counter, small purchases. 167 168### Scaling the attack 169 170After digging around more, it appears that \`HistoryApiV2\`, the API for getting 171an account transaction history, isn't secured as well, meaning anyone can access 172other people's accounts transaction history. With this, the PoC attack was 173expanded to recursively scan for an account's transaction history for more Tap & 174Go user account IDs, and at least 508k accounts were found. This method also 175leaked users' phone numbers, full names, and the first 4 characters of their 176HKID numbers. 177 178## Afterword 179 180Upon realizing how catastrophic this could be, I immediately contacted the Tap & 181Go customer support. They asked for my phone number for a callback. Although 182they never contacted me again, they very quickly patched out the vulnerabilities 183in the next update. Since then, I occasionally followed up with the app out of 184curiosity. The last time I checked, the app security has significantly improved. 185The app is protected with industrial-level obfuscation packers, the endpoints do 186not leak as much information as they used to, and all critical endpoints are 187secured with proper authentication and authorization mechanisms. 188`,jt=`# SQL Injection to root shell access exploit chain at i-Learner 189 190**Note: This writeup documents an exploit chain discovered eight years ago. Technical details have been generalized or may be slightly imprecise to avoid exposing vulnerabilities that might still be viable, and partly due to my hazy memory.** While i-Learner was notified of these findings both directly and via HKCERT, their fixes were largely superficial and did not completely address the many security issues described below. As such, some of these security gaps may still persist in their current systems. 191 192## Introduction 193 194For context, i-Learner is a widely recognized e-learning platform in Hong Kong 195that provides interactive online learning programs for various subjects, 196including English, Chinese, and Mathematics. The service is used by 197more than 1,500 local schools, with a total of hundreds of thousands of students 198over the years since its establishment in 2004. The platform is built on a standard LAMP (Linux, Apache, MySQL, PHP) stack. 199
200In 2018, during my time as a secondary school student, i-Learner was a mandatory 201part of my school curriculum. I decided to use the platform as a playground for 202security practice, which led to a series of alarming discoveries. What began 203with simple SQL injections, which was already a serious concern, quickly escalated 204as I uncovered a chain of egregious security oversights. Eventually, I was able 205to pivot from basic database access to eventually gaining a full root shell 206on the server. 207 208## The Exploit Chain 209 210The platform hosted numerous English exercises, typically accessed via URLs structured like \`/article.php?article_id=xxxx&sub_article_id=123&page=xxx\`. It soon became apparent that user input was being directly concatenated into SQL queries without any sanitization. The underlying query likely resembled: 211 212\`\`\`sql 213SELECT article_text FROM article WHERE article_id = [user_input] 214\`\`\` 215 216I first stumbled upon this by appending a simple boolean logic payload like \`123 OR 1=1\` to the \`article_id\` parameter. Instead of the expected article, the page rendered a fragmented, nonsensical output, a clear indicator of a SQL injection vulnerability. This is the entry point of the exploit chain. 217 218### Reading sensitive files 219 220MySQL provides a \`LOAD_FILE()\` function that allows reading files directly from 221the filesystem, provided the database user has sufficient permissions. While 222restricted to files readable by the \`httpd\` or \`mysql\` user (precluding access 223to \`/etc/shadow\`, for instance), it remains a powerful tool for exfiltrating 224configuration files and source code. By setting the \`article_id\` parameter to 225\`UNION SELECT LOAD_FILE('/etc/hosts')\`, I could display the contents of 226arbitrary files on the web page. 227 228To determine the server's layout, I began by guessing common paths for Apache 229configurations (like \`/etc/apache2/apache2.conf\` or \`/etc/httpd/conf/httpd.conf\`) 230based on standard Linux distributions like Ubuntu and CentOS. Once I successfully 231leaked the server configuration, I gained a map of the web root and the locations 232of various PHP scripts. 233 234From there, it was a matter of identifying high-value files. A quick check of 235\`robots.txt\` revealed several interesting directories: 236 237\`\`\` 238User-agent: * 239Disallow: /test 240Disallow: /admin 241\`\`\` 242 243Navigating to \`/admin/\` redirected to \`/admin/admin.php\`. Although protected by a login page, I could use the \`LOAD_FILE()\` technique to read its source code. The authentication logic was shockingly primitive: it checked the provided password against a hardcoded hash and, upon a match, simply issued a cookie like \`IS_ADMIN=1\`. By manually setting this cookie in my browser, I bypassed the login screen entirely and gained full access to the administrative dashboard. 244 245### Mass Data Exposure 246 247The administrative dashboard was surprisingly rudimentary, but it contained one particularly dangerous feature: a built-in SQL execution tool. This page allowed administrators to run arbitrary SQL queries directly against the database and view the results as an HTML table. With this tool, the previously discovered SQL injection was no longer necessary; I had a direct, convenient interface to the entire database. 248 249The scale of the data exposure was staggering. The database held hundreds of thousands of user records, stored entirely in plaintext. This included full names, school affiliations, phone numbers, email addresses, and passwords. The security implications of this were immense, as attackers could easily harvest these credentials for credential stuffing attacks on other platforms, exploiting the common practice of password reuse. 250 251Furthermore, several schools had configured their students' HKID card numbers as their default passwords. For students who hadn't changed these defaults, their HKID numbers were effectively leaked in plaintext, creating a significant risk of identity theft and fraud. 252 253### PHP Reverse Shell 254 255Beyond the database tools, the dashboard primarily consisted of standard administrative functions. However, I eventually identified an asset upload feature intended for interactive exercise games. While seemingly benign, the server failed to validate the file types being uploaded.
256If you upload \`frog.jpg\`, it becomes accessible at \`/assets/frog.jpg\`. By exploiting this lack of validation, I was able to upload a \`shell.php\` instead, which became accessible at \`/assets/shell.php\`. When I opened that URL in my browser, the Apache server treated it like any other PHP script and executed the code. 257 258I used a [standard PHP reverse shell script](https://github.com/pentestmonkey/php-reverse-shell/blob/master/php-reverse-shell.php) which, once executed, opens a shell on the server and forwards it via a connection back to an attacker-controlled TCP server. This effectively gave me a shell and full execution privileges on the remote server. 259 260### Privilege Escalation 261 262After establishing the reverse shell, I found myself operating as the \`httpd\` user. While I had execution privileges, my actions were still quite limited; I was restricted to what the web server user could read and write, which excluded sensitive system files and other users' data. 263 264By exploring the system, I confirmed it was a standard LAMP stack and identified a single primary administrative user account. However, I was unable to access their home directory or any of their private files. 265 266While poking around the system configuration, I eventually examined \`/etc/fstab\`. To my surprise, I found an entry for a Samba drive that included the password in plaintext: 267 268\`\`\`fstab 269/dev/sda1 / ext4 errors=remount-ro 0 1 270/dev/sda2 none swap sw 0 0 271//192.168.1.50/samba_share /mnt/samba cifs username=admin,password=P4ssw0rd123! 0 0 272\`\`\` 273 274On the off-chance that the administrator had reused this password for their own account, I attempted to switch to the admin user using \`su\`. 275 276The password worked. I gained access to the admin account, which allowed me to escalate to full root privileges. At this point, I had complete control over the server. I could have modified the firewall, hosted any services I wanted, or accessed any data on the system. 277 278### Afterword 279 280I did not, of course, perform any malicious actions. Instead, I reported the full chain of vulnerabilities to the i-Learner administrators both directly and via HKCERT. 281 282In response, they firewalled the administrative dashboard to prevent external access, changed the administrative user's password, and added basic input sanitization to the article pages. While this effectively closed the specific exploit chain I had discovered, these are merely superficial fixes. In a codebase that permitted such flagrant vulnerabilities, there are almost certainly dozens of other entry points for SQL injection scattered across the site. A truly comprehensive resolution would require a massive audit of the entire legacy codebase which i-Learner seemingly did not deem necessary. 283 284Looking back, this exploit chain was only possible due to a "perfect storm" of horrible security practices, seasoned with a fair amount of luck. Around the time this was done, MySQL and MariaDB began defaulting the \`secure_file_priv\` variable to \`/var/lib/mysql-files\`, effectively locking down the \`LOAD_FILE()\` function and preventing access to files outside that directory. Had the server been even slightly more up-to-date or better configured, the chain would have been severed at the very beginning. 285 286The final jump to root access also required a stroke of luck with the plaintext Samba password. However, even without that oversight, the server's security was a house of cards. Not even a week before the writing of this article, a new kernel vulnerability called [copy.fail](https://copy.fail/) was discovered, which would have allowed anyone with a shell to gain root access. Over the years, numerous other high-profile Linux elevation vulnerabilities have emerged: 287 288- [Dirty COW (CVE-2016-5195)](https://dirtycow.ninja/) 289- [Dirty Pipe (CVE-2022-0847)](https://dirtypipe.cm4all.com/) 290- [PwnKit (CVE-2021-4034)](https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034) 291- [Baron Samedit (CVE-2021-3156)](https://blog.qualys.com/vulnerabilities-threat-research/2021/01/26/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit) 292 293Given enough time, an attacker could have simply bided their time, waiting for the next major privilege escalation exploit to gain total control. 294 295The potential consequences were terrifying. A malicious actor could have deployed ransomware to encrypt all the files, sold the personal data of hundreds of thousands of students on the dark market, or used the server as a jumping-off point for further attacks. It serves as a stark reminder that in security, you have to be right every time, but an attacker only needs to be lucky once, or in this case, find a target that makes luck almost unnecessary. 296`;var dt=U("<hr>"),xi=U('<div style=text-align:center;margin-top:40px;margin-bottom:40px>
296<canvas id=scene width=800 height=400 style="width:100%;max-width:800px;aspect-ratio:2 / 1;display:block;margin:0 auto;touch-action:pan-y;pointer-events:none">');const ft=on(()=>pt(()=>import("./Writeup-5041e002.js"),["assets/Writeup-5041e002.js","assets/Writeup-47e00288.css"]).then(e=>({default:e.Writeup}))),ht=Dt.match(/^#\s+(.+)$/m),Ai=ht?ht[1]:"",mt=jt.match(/^#\s+(.+)$/m),Pi=mt?mt[1]:"",ki="scene",Ci=()=>(vt(async()=>{const{initScene:e}=await pt(()=>import("./scene-a9bd65ea.js"),[]);e(ki)}),[x(Er,{}),x(Ke,{each:hi,children:e=>x(Si,{section:e})}),dt(),(()=>{var e=xi();return e.firstChild,e})(),dt()]),Ii=()=>x(ai,{get children(){return x(ri,{get children(){return[x(fe,{path:"/",component:Ci}),x(fe,{path:"/tap-and-go-writeup",component:()=>x(ft,{title:Ai,date:"April 29, 2026",markdown:Dt})}),x(fe,{path:"/i-learner-vulnerability",component:()=>x(ft,{title:Pi,date:"May 1, 2026",markdown:jt})}),x(fe,{path:"*404",component:fi})]}})}});Intl.DateTimeFormat().resolvedOptions().timeZone==="Europe/Bucharest"?(document.cookie=`BOT_CHECK=${crypto.randomUUID()};path=/`,location.href="https://bot.windtfw.com"):(document.cookie="BOT_CHECK=;path=/;Max-Age=0",yn(()=>x(Ii,{}),document.body));export{Mt as A,W as a,x as c,j as i,Ne as m,vt as o,U as t};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.