diff --git a/go.mod b/go.mod index a2dbd3b..eac36a7 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,13 @@ module github.com/highesttt/matrix-line-messenger go 1.26.0 require ( + github.com/mattn/go-sqlite3 v1.14.52 github.com/rs/zerolog v1.35.1 github.com/u2takey/ffmpeg-go v0.5.0 - go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 - golang.org/x/crypto v0.54.0 + go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 + golang.org/x/crypto v0.57.0 golang.org/x/image v0.23.0 - maunium.net/go/mautrix v0.29.1-0.20260814120312-a6614a03769b + maunium.net/go/mautrix v0.31.1-0.20261005174338-57e1ea5dd74d ) require ( @@ -25,8 +26,7 @@ require ( github.com/lib/pq v1.12.3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.48 // indirect - github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca // indirect + github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 // indirect github.com/rs/xid v1.6.0 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/tidwall/gjson v1.19.0 // indirect @@ -34,13 +34,13 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/u2takey/go-utils v0.3.1 // indirect - github.com/yuin/goldmark v1.8.4 // indirect + github.com/yuin/goldmark v1.8.6 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect - golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect - golang.org/x/net v0.57.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.40.0 // indirect + golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect + golang.org/x/net v0.59.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect maunium.net/go/mauflag v1.0.0 // indirect diff --git a/go.sum b/go.sum index 9113c9d..b04f48f 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/aws/aws-sdk-go v1.38.20 h1:QbzNx/tdfATbdKfubBpkt84OM6oBkxQZRw6+bW2GyeA= github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= -github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= @@ -36,10 +34,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= -github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= -github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= -github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= +github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U= +github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -47,10 +43,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A= -github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM= -github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca h1:GHSUVE4yOgX4E7kTRzpxCPbCOYkd3Kj8Dgdod30OI1E= -github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 h1:lcWAnrqr2nNfDiArwFNHCE4787Mw2tCdVSOXCru0/0E= +github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -66,8 +60,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= +github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= @@ -82,56 +76,40 @@ github.com/u2takey/ffmpeg-go v0.5.0 h1:r7d86XuL7uLWJ5mzSeQ03uvjfIhiJYvsRAJFCW4uk github.com/u2takey/ffmpeg-go v0.5.0/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc= github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys= github.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs= -github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= -github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA= -github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -go.mau.fi/util v0.9.9 h1:ujDeXCo07HBor5oQLyO1tHklupmqVmPgasc53d7q/NE= -go.mau.fi/util v0.9.9/go.mod h1:pqt4Vcrt+5gcH/CgrHZg11qSx+b34o6mknGzOEA6waY= -go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 h1:lsvBEY8MJfYdV61YbwikiQvb0Al/onbmLW5wfl/0tag= -go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= +github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= +github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 h1:PxaO4u8ti7Eef1WKPAINXrcUkqKshE+n239UtbbJOn4= +go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= -golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= -golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g= -golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -148,8 +126,6 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.28.0 h1:vBakLzf8MAdfED3NzAKiMeKQbc3AQ4EAS03NC+TVMXQ= -maunium.net/go/mautrix v0.28.0/go.mod h1:/a9A7LGaqb9B3nho4tLd28n0EPcCdwpm2dxkxkLLgh0= -maunium.net/go/mautrix v0.29.1-0.20260814120312-a6614a03769b h1:DKy6Vnhoyxq0TY30k9J8mGuaV3/qBgpEnb17V0Ouv9w= -maunium.net/go/mautrix v0.29.1-0.20260814120312-a6614a03769b/go.mod h1:PeLuIih5jnbwb2xKNNi0Te7AtINEQE2Uv3p6iG3RV80= +maunium.net/go/mautrix v0.31.1-0.20261005174338-57e1ea5dd74d h1:sOdF5AF9MiU2mudfMBgQC79FJrhBBDbUkxQCi0T2uHI= +maunium.net/go/mautrix v0.31.1-0.20261005174338-57e1ea5dd74d/go.mod h1:t97J+6KmbmeoepWOunBgpu1xIvuBpI36ebBqIbYT8Jk= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/pkg/connector/auth_recovery.go b/pkg/connector/auth_recovery.go index e024790..5c2bc0b 100644 --- a/pkg/connector/auth_recovery.go +++ b/pkg/connector/auth_recovery.go @@ -11,6 +11,7 @@ type lineCallDeps[T any] struct { newClient func() *line.Client recover func(context.Context, *line.Client, error) (*line.Client, error) isAuthError func(error) bool + beforeCall func() error call func(*line.Client) (T, error) } @@ -19,6 +20,19 @@ var recoverLineToken = func(lc *LineClient, ctx context.Context) error { } func callLineWithRecovery[T any](ctx context.Context, client *line.Client, deps lineCallDeps[T]) (*line.Client, T, error) { + check := func() error { + if err := ctx.Err(); err != nil { + return err + } + if deps.beforeCall != nil { + return deps.beforeCall() + } + return nil + } + if err := check(); err != nil { + var zero T + return client, zero, err + } if client == nil { client = deps.newClient() } @@ -37,6 +51,10 @@ func callLineWithRecovery[T any](ctx context.Context, client *line.Client, deps } client = recoveredClient + if err := check(); err != nil { + var zero T + return client, zero, err + } res, err = deps.call(client) if line.IsLoggedOut(err) { // The retry is the final attempt, but a current-token logout still needs @@ -139,11 +157,27 @@ func (lc *LineClient) callLine(ctx context.Context, call func(*line.Client) erro return lc.callLineUsing(ctx, nil, call) } +func (lc *LineClient) checkLineCall(ctx context.Context) error { + lc.runMu.Lock() + defer lc.runMu.Unlock() + if err := ctx.Err(); err != nil { + return err + } + if lc.stopped || lc.superseded.Load() { + return errLineClientSuperseded + } + if lc.isSessionInvalidated() { + return errLineSessionInvalidated + } + return nil +} + func (lc *LineClient) callLineUsing(ctx context.Context, client *line.Client, call func(*line.Client) error) (*line.Client, error) { client, _, err := callLineWithRecovery(ctx, client, lineCallDeps[struct{}]{ newClient: func() *line.Client { return lc.newClient() }, recover: lc.recoverClientAfterAuthError, isAuthError: lc.isTokenError, + beforeCall: func() error { return lc.checkLineCall(ctx) }, call: func(client *line.Client) (struct{}, error) { return struct{}{}, call(client) }, @@ -160,6 +194,7 @@ func callLineResultUsing[T any](lc *LineClient, ctx context.Context, client *lin newClient: func() *line.Client { return lc.newClient() }, recover: lc.recoverClientAfterAuthError, isAuthError: lc.isTokenError, + beforeCall: func() error { return lc.checkLineCall(ctx) }, call: call, }) return client, res, err diff --git a/pkg/connector/client.go b/pkg/connector/client.go index f3d5b14..92669f0 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "reflect" "strconv" "strings" "sync" @@ -29,10 +30,23 @@ const lineMissingE2EEKeyMessage = "LINE encryption keys are unavailable. Reconne var newLineAPIClient = line.NewClient var newE2EEManager = e2ee.NewManager -var loginWithCredentials = func(email, password, certificate string) (*line.LoginResult, error) { +func defaultLoginWithCredentials(email, password, certificate string) (*line.LoginResult, error) { return newLineAPIClient("").Login(email, password, certificate) } +var loginWithCredentials = defaultLoginWithCredentials + +func validLoginProducer(res *line.LoginResult) bool { + return res != nil && (res.Attempt != nil || reflect.ValueOf(loginWithCredentials).Pointer() != reflect.ValueOf(defaultLoginWithCredentials).Pointer()) +} + +func loginWithCredentialsContext(ctx context.Context, email, password, certificate string) (*line.LoginResult, error) { + if reflect.ValueOf(loginWithCredentials).Pointer() != reflect.ValueOf(defaultLoginWithCredentials).Pointer() { + return loginWithCredentials(email, password, certificate) + } + return newLineAPIClient("").LoginContext(ctx, email, password, certificate) +} + var getProfileWithToken = func(ctx context.Context, token string) (*line.Profile, error) { return newLineAPIClient(token).GetProfileContext(ctx) } @@ -163,7 +177,7 @@ func (lc *LineClient) beginRun(parent context.Context) (context.Context, *lineCl ctx, cancel := context.WithCancel(parent) run := &lineClientRun{cancel: cancel} lc.runMu.Lock() - if lc.stopped { + if lc.stopped || lc.activeRun != nil { lc.runMu.Unlock() cancel() return ctx, run, false @@ -409,7 +423,7 @@ func (lc *LineClient) saveSessionInvalidated(ctx context.Context) { } func (lc *LineClient) markMissingE2EEKey(ctx context.Context, err error) { - if !errors.Is(err, e2ee.ErrMissingOwnPrivateKey) || lc.UserLogin == nil { + if isTerminalCryptoError(err) || !errors.Is(err, e2ee.ErrMissingOwnPrivateKey) || lc.UserLogin == nil { return } lc.missingE2EEKeyMu.Lock() @@ -471,7 +485,7 @@ func (lc *LineClient) recoverTokenWith( return nil } else if ctx.Err() != nil { return ctx.Err() - } else if lc.isLoggedOut(err) || errors.Is(err, errLineSessionInvalidated) { + } else if lc.isLoggedOut(err) || errors.Is(err, errLineSessionInvalidated) || errors.Is(err, errLineClientSuperseded) || errors.Is(err, context.Canceled) { return err } lc.UserLogin.Bridge.Log.Info().Msg("Refresh failed, attempting re-login with stored credentials...") @@ -526,6 +540,9 @@ func (lc *LineClient) Connect(ctx context.Context) { workersStarted := false defer func() { if !workersStarted { + if lc.E2EE != nil { + lc.E2EE.Close() + } run.cancel() } }() @@ -689,10 +706,16 @@ func (lc *LineClient) tryLogin(ctx context.Context) error { Str("email", email). Bool("has_certificate", certificate != ""). Msg("Attempting to login with email/password...") - res, err := loginWithCredentials(email, password, certificate) + res, err := loginWithCredentialsContext(ctx, email, password, certificate) + if res != nil { + defer res.Attempt.Close() + } if err != nil { return fmt.Errorf("login failed: %w", err) } + if !validLoginProducer(res) { + return errors.New("missing LINE login producer") + } if res.AuthToken == "" { // No usable verifier means LINE didn't engage the PIN-based flow. // Bail out before emitting a misleading "PIN required" bridge state — @@ -718,6 +741,7 @@ func (lc *LineClient) tryLogin(ctx context.Context) error { lc.UserLogin.Bridge.Log.Info().Msg("Waiting for PIN verification on mobile device...") waitClient := newLineAPIClient("") + waitClient.LoginAttempt = res.Attempt waitRes, err := waitClient.WaitForLogin(res.Verifier, res.NoE2EE) if err != nil { return fmt.Errorf("PIN verification failed: %w", err) @@ -738,32 +762,96 @@ func (lc *LineClient) tryLogin(ctx context.Context) error { refreshToken = res.TokenV3IssueResult.RefreshToken } } - accessToken, refreshToken = lc.setTokens(accessToken, refreshToken) - - // Re-login replaces the main access token, which invalidates any cached - // OBS token derived from the previous one. - line.InvalidateOBSTokenCache() - + if ctx.Err() != nil { + return ctx.Err() + } + if lc.superseded.Load() { + return errLineClientSuperseded + } + if res.Attempt != nil { + profile, err := newLineAPIClient(accessToken).GetProfileContext(ctx) + if err != nil { + return fmt.Errorf("verify re-login account: %w", err) + } + if profile.Mid == "" || profile.Mid != string(lc.UserLogin.ID) || + (lc.Mid != "" && profile.Mid != lc.Mid) || (res.Mid != "" && profile.Mid != res.Mid) { + return errors.New("re-login account does not match active LINE account") + } + res.Mid = profile.Mid + } + meta, ok := lc.UserLogin.Metadata.(*UserLoginMetadata) + if !ok { + return errors.New("missing LINE login metadata") + } + if res.Attempt != nil && meta.Mid != "" && res.Mid != meta.Mid { + return errors.New("re-login account does not match stored LINE account") + } + staged := *meta + staged.AccessToken = accessToken + if refreshToken != "" { + staged.RefreshToken = refreshToken + } + staged.SessionInvalidated = false if res.Mid != "" { - lc.Mid = res.Mid - if meta, ok := lc.UserLogin.Metadata.(*UserLoginMetadata); ok { - meta.Mid = res.Mid + staged.Mid = res.Mid + } + if res.Certificate != "" { + staged.Certificate = res.Certificate + } + mgr, exported, keyErr := exportLoginE2EEKeys(res, newLineAPIClient(accessToken)) + if mgr != nil { + defer mgr.Close() + if res.Mid != "" && res.Mid == string(lc.UserLogin.ID) { + exported = mergeLoginKeyMaps(meta.ExportedKeyMap, exported) + } + if err := mgr.LoadMyKeyFromExportedMap(exported); err != nil { + keyErr = fmt.Errorf("validate recovered E2EE keys: %w", err) + } else { + applyExportedLoginE2EEKeys(&staged, res, exported) } } - - // Save the new tokens and updated certificate to metadata - if meta, ok := lc.UserLogin.Metadata.(*UserLoginMetadata); ok { - meta.AccessToken = accessToken - meta.RefreshToken = refreshToken - meta.SessionInvalidated = false - if res.Certificate != "" { - meta.Certificate = res.Certificate + if keyErr != nil { + if isTerminalCryptoError(keyErr) || errors.Is(keyErr, context.Canceled) || res.Mid != string(lc.UserLogin.ID) || res.Mid != meta.Mid || !shouldPreserveExistingE2EEKeys(false, meta) { + return fmt.Errorf("refresh re-login E2EE keys: %w", keyErr) + } + mgr = nil + lc.UserLogin.Bridge.Log.Warn().Err(keyErr).Msg("Re-login: failed to refresh E2EE keys; using existing keys") + } + lc.runMu.Lock() + defer lc.runMu.Unlock() + if err := lc.admitRecoveredLogin(ctx, res); err != nil { + return err + } + if keyErr != nil && lc.E2EE != nil { + if _, _, err := lc.E2EE.MyKeyIDs(); err != nil { + return fmt.Errorf("preserve active E2EE keys: %w", err) } - if err := lc.refreshLoginE2EEKeys(res, meta, newLineAPIClient(accessToken)); err != nil { - lc.UserLogin.Bridge.Log.Warn().Err(err).Msg("Failed to refresh E2EE keys after re-login") + } + if mgr != nil && lc.E2EE != nil { + if err := lc.E2EE.LoadMyKeyFromExportedMap(exported); err != nil { + return fmt.Errorf("load recovered E2EE keys: %w", err) } - if err := lc.UserLogin.Save(ctx); err != nil { - lc.UserLogin.Bridge.Log.Warn().Err(err).Msg("Failed to save new tokens to DB") + if err := lc.admitRecoveredLogin(ctx, res); err != nil { + return err + } + } + lc.missingE2EEKeyMu.Lock() + defer lc.missingE2EEKeyMu.Unlock() + previous := *meta + *meta = staged + if err := lc.UserLogin.Save(ctx); err != nil { + *meta = previous + return fmt.Errorf("save recovered LINE login: %w", err) + } + lc.setTokens(accessToken, refreshToken) + lc.Mid = staged.Mid + line.InvalidateOBSTokenCache() + if mgr != nil { + lc.missingE2EEKeyMarked = false + } + if mgr != nil { + if err := mgr.SaveSecureDataToFile(loginSecureDataID(&staged, string(lc.UserLogin.ID)), map[string]any{"exportedKeyMap": exported}); err != nil { + lc.UserLogin.Bridge.Log.Warn().Err(err).Msg("Failed to save E2EE secure data after re-login") } } @@ -771,6 +859,24 @@ func (lc *LineClient) tryLogin(ctx context.Context) error { return nil } +func (lc *LineClient) admitRecoveredLogin(ctx context.Context, res *line.LoginResult) error { + if err := ctx.Err(); err != nil { + return err + } + if res.Attempt != nil { + if err := res.Attempt.Context.Err(); err != nil { + return err + } + } + if lc.superseded.Load() || lc.stopped || lc.recoveryStopped { + return errLineClientSuperseded + } + if lc.isSessionInvalidated() { + return errLineSessionInvalidated + } + return nil +} + func (lc *LineClient) refreshLoginE2EEKeys(res *line.LoginResult, meta *UserLoginMetadata, client *line.Client) error { if res.EncryptedKeyChain == "" || res.E2EEPublicKey == "" { return nil @@ -779,14 +885,31 @@ func (lc *LineClient) refreshLoginE2EEKeys(res *line.LoginResult, meta *UserLogi if err != nil { return err } + defer mgr.Close() + if res.Attempt != nil && res.Attempt.Context.Err() != nil { + return res.Attempt.Context.Err() + } + if lc.superseded.Load() { + return errLineClientSuperseded + } + if res.Mid != "" && res.Mid == meta.Mid && lc.UserLogin != nil && res.Mid == string(lc.UserLogin.ID) { + exported = mergeLoginKeyMaps(meta.ExportedKeyMap, exported) + } + if lc.E2EE != nil { if err := lc.E2EE.LoadMyKeyFromExportedMap(exported); err != nil { return fmt.Errorf("load exported keys into active E2EE manager: %w", err) } } + if res.Attempt != nil && res.Attempt.Context.Err() != nil { + return res.Attempt.Context.Err() + } + if lc.superseded.Load() { + return errLineClientSuperseded + } lc.applyRefreshedLoginE2EEKeys(meta, res, exported) if err := mgr.SaveSecureDataToFile(loginSecureDataID(meta, string(lc.UserLogin.ID)), map[string]any{"exportedKeyMap": exported}); err != nil { - lc.UserLogin.Bridge.Log.Warn().Err(err).Msg("Failed to save E2EE secure data after re-login") + return fmt.Errorf("save E2EE secure data after re-login: %w", err) } lc.UserLogin.Bridge.Log.Info().Int("keys", len(exported)).Msg("Refreshed E2EE keys after re-login") return nil @@ -827,6 +950,9 @@ func (lc *LineClient) Disconnect() { // superseded flag was visible before a replacement installs new metadata. lc.recoverMu.Lock() lc.recoveryStopped = true + if lc.E2EE != nil { + lc.E2EE.Close() + } lc.recoverMu.Unlock() } diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index af9b340..8c7d009 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -178,11 +178,15 @@ type LineEmailLogin struct { ExistingMetadata *UserLoginMetadata ExistingLogin *bridgev2.UserLogin - pollResult chan *line.LoginResult - pollErr chan error - polling bool - mu sync.Mutex - finalizeMu *sync.Mutex + pollResult chan *line.LoginResult + pollErr chan error + polling bool + attempt *line.LoginAttempt + attemptCtx context.Context + attemptCancel context.CancelFunc + canceled bool + mu sync.Mutex + finalizeMu *sync.Mutex } var _ bridgev2.LoginProcessUserInput = (*LineEmailLogin)(nil) @@ -237,7 +241,7 @@ func (ll *LineEmailLogin) StartWithOverride(ctx context.Context, override *bridg override.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting}) - res, err := loginWithCredentials(ll.Email, ll.Password, ll.Certificate) + res, err := ll.loginCredentials(ctx, ll.Certificate) if err != nil { ll.logLoginFailure(err, "reconnect") reason := loginErrorReason(err) @@ -264,7 +268,7 @@ func (ll *LineEmailLogin) SubmitUserInput(ctx context.Context, input map[string] return ll.loginErrorStep("Email and password are required"), nil } - res, err := loginWithCredentials(ll.Email, ll.Password, "") + res, err := ll.loginCredentials(ctx, "") if err != nil { ll.logLoginFailure(err, "credentials") reason := loginErrorReason(err) @@ -458,24 +462,76 @@ func isBlockedUserLoginError(message string) bool { return strings.EqualFold(strings.TrimSpace(message), "blocked user") } +func (ll *LineEmailLogin) loginCredentials(ctx context.Context, certificate string) (*line.LoginResult, error) { + ll.mu.Lock() + if ll.canceled { + ll.mu.Unlock() + return nil, context.Canceled + } + if ll.attemptCancel != nil { + ll.attemptCancel() + } + if ll.attempt != nil { + ll.attempt.Close() + } + ll.attemptCtx, ll.attemptCancel = context.WithCancel(context.WithoutCancel(ctx)) + attemptCtx := ll.attemptCtx + attemptCancel := ll.attemptCancel + ll.mu.Unlock() + stop := context.AfterFunc(ctx, attemptCancel) + defer stop() + res, err := loginWithCredentialsContext(attemptCtx, ll.Email, ll.Password, certificate) + ll.mu.Lock() + defer ll.mu.Unlock() + if ll.canceled || ctx.Err() != nil || attemptCtx.Err() != nil || ll.attemptCtx != attemptCtx { + if res != nil { + res.Attempt.Close() + } + if ctx.Err() != nil { + return nil, ctx.Err() + } + return nil, context.Canceled + } + if res != nil { + ll.attempt = res.Attempt + } + return res, err +} + func (ll *LineEmailLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { - if ll.Verifier != "" { + ll.mu.Lock() + verifier, awaitingPIN := ll.Verifier, ll.AwaitingPIN + resultCh, errCh := ll.pollResult, ll.pollErr + var done <-chan struct{} + if ll.attemptCtx != nil { + done = ll.attemptCtx.Done() + } + canceled := ll.canceled + ll.mu.Unlock() + if canceled { + return nil, context.Canceled + } + if verifier != "" { select { - case res := <-ll.pollResult: + case res := <-resultCh: if res.AuthToken != "" { return ll.finishLogin(ctx, res) } + res.Attempt.Close() return nil, ErrLoginVerificationFailed - case err := <-ll.pollErr: + case err := <-errCh: ll.logLoginFailure(err, "verification_poll") return nil, wrapLineLoginError(err) + case <-done: + return nil, context.Canceled case <-ctx.Done(): + ll.Cancel() return nil, ctx.Err() } } - if ll.AwaitingPIN { - res, err := loginWithCredentials(ll.Email, ll.Password, ll.Certificate) + if awaitingPIN { + res, err := ll.loginCredentials(ctx, ll.Certificate) if err != nil { ll.logLoginFailure(err, "pin_continuation") return nil, wrapLineLoginError(err) @@ -487,14 +543,19 @@ func (ll *LineEmailLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) } func (ll *LineEmailLogin) handleLoginResponse(ctx context.Context, res *line.LoginResult) (*bridgev2.LoginStep, error) { + ll.mu.Lock() + canceled := ll.canceled || res.Attempt != ll.attempt || !validLoginProducer(res) + ll.mu.Unlock() + if canceled || ctx.Err() != nil || (res.Attempt != nil && res.Attempt.Context.Err() != nil) { + res.Attempt.Close() + return nil, context.Canceled + } + if res.AuthToken != "" { return ll.finishLogin(ctx, res) } if (res.Type == 3 || res.Type == 0) && res.Verifier != "" { - ll.Verifier = res.Verifier - ll.NoE2EE = res.NoE2EE - ll.AwaitingPIN = false instructions := "Please open the LINE app on your mobile device to complete the login." pin := res.Pin if res.PinCode != "" { @@ -506,16 +567,48 @@ func (ll *LineEmailLogin) handleLoginResponse(ctx context.Context, res *line.Log // Start polling in background immediately so it's running while the user enters the PIN ll.mu.Lock() + if ll.canceled { + ll.mu.Unlock() + res.Attempt.Close() + return nil, context.Canceled + } + ll.Verifier, ll.NoE2EE, ll.AwaitingPIN = res.Verifier, res.NoE2EE, false ll.polling = true ll.pollResult = make(chan *line.LoginResult, 1) ll.pollErr = make(chan error, 1) + attempt := res.Attempt + verifier, noE2EE := ll.Verifier, ll.NoE2EE + resultCh, errCh := ll.pollResult, ll.pollErr + pollCtx := ll.attemptCtx + if pollCtx == nil { + pollCtx = ctx + } go func() { client := newLineAPIClient("") - res, err := client.WaitForLogin(ll.Verifier, ll.NoE2EE) + client.LoginAttempt = attempt + result, err := client.WaitForLogin(verifier, noE2EE) + if pollCtx.Err() != nil { + if attempt != nil { + attempt.Close() + } + return + } if err != nil { - ll.pollErr <- err + if attempt != nil { + attempt.Close() + } + select { + case errCh <- err: + case <-pollCtx.Done(): + } } else { - ll.pollResult <- res + select { + case resultCh <- result: + case <-pollCtx.Done(): + if attempt != nil { + attempt.Close() + } + } } }() ll.mu.Unlock() @@ -531,7 +624,10 @@ func (ll *LineEmailLogin) handleLoginResponse(ctx context.Context, res *line.Log } if res.Certificate != "" { + ll.mu.Lock() ll.AwaitingPIN = true + ll.mu.Unlock() + res.Attempt.Close() return &bridgev2.LoginStep{ Type: bridgev2.LoginStepTypeDisplayAndWait, StepID: "dev.highest.matrix.line.enter_pin", @@ -542,14 +638,37 @@ func (ll *LineEmailLogin) handleLoginResponse(ctx context.Context, res *line.Log }, nil } + res.Attempt.Close() return nil, fmt.Errorf("login incomplete but no PIN found in response (Type: %d, Msg: %s)", res.Type, res.Message) } func (ll *LineEmailLogin) finishLogin(ctx context.Context, res *line.LoginResult) (*bridgev2.LoginStep, error) { + if res != nil { + defer res.Attempt.Close() + } + if res == nil { return nil, fmt.Errorf("login result missing") } + ll.mu.Lock() + processCtx := ll.attemptCtx + canceled := ll.canceled || res.Attempt != ll.attempt || !validLoginProducer(res) + if res.Attempt != nil { + processCtx = res.Attempt.Context + } + ll.mu.Unlock() + if canceled || ctx.Err() != nil || (processCtx != nil && processCtx.Err() != nil) { + return nil, context.Canceled + } + finishCtx, cancel := context.WithCancel(ctx) + defer cancel() + if processCtx != nil { + stop := context.AfterFunc(processCtx, cancel) + defer stop() + } + ctx = finishCtx + token := res.AuthToken refreshToken := "" if res.TokenV3IssueResult != nil { @@ -563,7 +682,8 @@ func (ll *LineEmailLogin) finishLogin(ctx context.Context, res *line.LoginResult } client := newLineAPIClient(token) - profile, err := client.GetProfile() + client.LoginAttempt = res.Attempt + profile, err := client.GetProfileContext(ctx) if err != nil { return nil, fmt.Errorf("failed to verify token: %w", err) } @@ -577,18 +697,35 @@ func (ll *LineEmailLogin) finishLogin(ctx context.Context, res *line.LoginResult if certificate == "" { certificate = ll.Certificate } - mid := res.Mid - if mid == "" { - mid = profile.Mid + mid := profile.Mid + if mid == "" || (res.Mid != "" && res.Mid != mid) { + return nil, errors.New("login result does not match verified LINE account") } meta := &UserLoginMetadata{AccessToken: token, RefreshToken: refreshToken, Email: ll.Email, Password: ll.Password, Certificate: certificate, Mid: mid} + sameAccount := ll.ExistingMetadata != nil && ll.ExistingLogin != nil && ll.ExistingLogin.UserLogin != nil && mid == string(ll.ExistingLogin.ID) && mid == ll.ExistingMetadata.Mid - exportedKeys := ll.fetchLoginKeys(res, meta, client) - if shouldPreserveExistingE2EEKeys(exportedKeys, ll.ExistingMetadata) { + loginManager, err := ll.fetchLoginKeys(res, meta, client) + if err != nil { + if isTerminalCryptoError(err) || errors.Is(err, context.Canceled) || !sameAccount || !shouldPreserveExistingE2EEKeys(false, ll.ExistingMetadata) { + return nil, err + } + if err := ll.admitLogin(ctx, res); err != nil { + return nil, err + } + ll.User.Bridge.Log.Warn().Err(err).Msg("Login: failed to export E2EE keys; using existing keys") + } + exportedKeys := loginManager != nil + if loginManager != nil { + defer loginManager.Close() + } + if sameAccount && shouldPreserveExistingE2EEKeys(exportedKeys, ll.ExistingMetadata) { copyLoginE2EEKeyMetadata(meta, ll.ExistingMetadata) ll.User.Bridge.Log.Info().Int("keys", len(meta.ExportedKeyMap)).Msg("Preserved existing E2EE keys after re-login") } + if exportedKeys && sameAccount { + meta.ExportedKeyMap = mergeLoginKeyMaps(ll.ExistingMetadata.ExportedKeyMap, meta.ExportedKeyMap) + } if !res.NoE2EE && len(meta.ExportedKeyMap) == 0 { return nil, ErrLoginNoKeychain } @@ -601,7 +738,13 @@ func (ll *LineEmailLogin) finishLogin(ctx context.Context, res *line.LoginResult ll.finalizeMu.Lock() defer ll.finalizeMu.Unlock() } + if err := ll.admitLogin(ctx, res); err != nil { + return nil, err + } targetLogin := findReusedLineLogin(ll.ExistingLogin, ll.User.GetUserLogins(), detectedLineID) + if err := ll.admitLogin(ctx, res); err != nil { + return nil, err + } retireLineLogin(targetLogin) var newClient *LineClient @@ -610,7 +753,10 @@ func (ll *LineEmailLogin) finishLogin(ctx context.Context, res *line.LoginResult RemoteName: displayName, Metadata: meta, }, &bridgev2.NewLoginParams{ - LoadUserLogin: func(ctx context.Context, login *bridgev2.UserLogin) error { + LoadUserLogin: func(_ context.Context, login *bridgev2.UserLogin) error { + if err := ll.admitLogin(ctx, res); err != nil { + return err + } newClient = &LineClient{ UserLogin: login, AccessToken: token, @@ -631,12 +777,22 @@ func (ll *LineEmailLogin) finishLogin(ctx context.Context, res *line.LoginResult if newClient == nil { return nil, fmt.Errorf("failed to create LINE client") } + if err := ll.admitLogin(ctx, res); err != nil { + newClient.Disconnect() + return nil, err + } if ll.ExistingLogin != nil && ll.ExistingLogin.UserLogin != nil && ll.ExistingLogin.ID != ul.ID { // A different-ID override remains valid until the new login is safely // installed. Retire it only after NewLogin succeeds. retireLineLogin(ll.ExistingLogin) } + if loginManager != nil { + if err := loginManager.SaveSecureDataToFile(loginSecureDataID(meta, string(ll.User.MXID)), map[string]any{"exportedKeyMap": meta.ExportedKeyMap}); err != nil { + ll.User.Bridge.Log.Warn().Err(err).Msg("Login: failed to save E2EE secure data") + } + } + go newClient.Connect(context.Background()) return &bridgev2.LoginStep{ @@ -676,10 +832,23 @@ func exportLoginE2EEKeys(res *line.LoginResult, client *line.Client) (*e2ee.Mana if res.EncryptedKeyChain == "" || res.E2EEPublicKey == "" { return nil, nil, nil } - mgr, err := newE2EEManager() - if err != nil { - return nil, nil, fmt.Errorf("create E2EE manager: %w", err) - } + var mgr *e2ee.Manager + if res.Attempt == nil { + unused, err := newE2EEManager() + if err != nil { + return nil, nil, fmt.Errorf("create E2EE manager: %w", err) + } + unused.Close() + return nil, nil, errors.New("missing E2EE login attempt") + } + mgr = e2ee.NewManagerWithRunner(res.Attempt.Runner) + success := false + defer func() { + if !success { + mgr.Close() + } + }() + client.LoginAttempt = res.Attempt ei3, err := client.GetEncryptedIdentityV3() if err != nil { return nil, nil, fmt.Errorf("get EncryptedIdentityV3: %w", err) @@ -691,6 +860,7 @@ func exportLoginE2EEKeys(res *line.LoginResult, client *line.Client) (*e2ee.Mana if err != nil { return nil, nil, fmt.Errorf("init from login keychain: %w", err) } + success = true return mgr, exported, nil } @@ -707,6 +877,17 @@ func applyExportedLoginE2EEKeys(meta *UserLoginMetadata, res *line.LoginResult, meta.ForceFullE2EELogin = false } +func mergeLoginKeyMaps(previous, current map[string]string) map[string]string { + merged := make(map[string]string, len(previous)+len(current)) + for id, key := range previous { + merged[id] = key + } + for id, key := range current { + merged[id] = key + } + return merged +} + func copyLoginE2EEKeyMetadata(dst, src *UserLoginMetadata) { dst.EncryptedKeyChain = src.EncryptedKeyChain dst.E2EEPublicKey = src.E2EEPublicKey @@ -731,21 +912,47 @@ func loginSecureDataID(meta *UserLoginMetadata, fallback string) string { return fallback } -func (ll *LineEmailLogin) fetchLoginKeys(res *line.LoginResult, meta *UserLoginMetadata, client *line.Client) bool { +func (ll *LineEmailLogin) fetchLoginKeys(res *line.LoginResult, meta *UserLoginMetadata, client *line.Client) (*e2ee.Manager, error) { if res.EncryptedKeyChain == "" || res.E2EEPublicKey == "" { - return false + return nil, nil } mgr, exported, err := exportLoginE2EEKeys(res, client) if err != nil { - ll.User.Bridge.Log.Warn().Err(err).Msg("Login: failed to export E2EE keys") - return false + return nil, fmt.Errorf("export login E2EE keys: %w", err) } applyExportedLoginE2EEKeys(meta, res, exported) - if err := mgr.SaveSecureDataToFile(loginSecureDataID(meta, string(ll.User.MXID)), map[string]any{"exportedKeyMap": exported}); err != nil { - ll.User.Bridge.Log.Warn().Err(err).Msg("Login: failed to save E2EE secure data") - } + ll.User.Bridge.Log.Info().Int("keys", len(exported)).Msg("Login: E2EE keys exported successfully") - return true + return mgr, nil +} + +func (ll *LineEmailLogin) admitLogin(ctx context.Context, res *line.LoginResult) error { + ll.mu.Lock() + defer ll.mu.Unlock() + if err := ctx.Err(); err != nil { + return err + } + if ll.canceled || res.Attempt != ll.attempt || !validLoginProducer(res) { + return context.Canceled + } + if res.Attempt != nil { + return res.Attempt.Context.Err() + } + if ll.attemptCtx != nil { + return ll.attemptCtx.Err() + } + return nil } -func (ll *LineEmailLogin) Cancel() {} +func (ll *LineEmailLogin) Cancel() { + ll.mu.Lock() + ll.canceled = true + if ll.attemptCancel != nil { + ll.attemptCancel() + } + attempt := ll.attempt + ll.mu.Unlock() + if attempt != nil { + attempt.Close() + } +} diff --git a/pkg/connector/creategroup.go b/pkg/connector/creategroup.go index 9119908..e8a3bd8 100644 --- a/pkg/connector/creategroup.go +++ b/pkg/connector/creategroup.go @@ -2,6 +2,7 @@ package connector import ( "context" + "errors" "fmt" "strings" @@ -135,6 +136,77 @@ type groupKeyCrypto interface { WrapGroupKeyForMember(string, int) (string, error) } +type groupKeyReleaser interface { + ReleaseGroupKey(int) error +} + +type groupKeyBatchCrypto interface { + WrapRegistrationGroupKey([]string) ([]string, error) +} + +func wrapRegistrationGroupKey(ctx context.Context, crypto groupKeyCrypto, pubKeys map[string]line.E2EEPeerPublicKey) (apiMembers []string, keyIDs []int, encryptedKeys []string, err error) { + if err := ctx.Err(); err != nil { + return nil, nil, nil, err + } + if batch, ok := crypto.(groupKeyBatchCrypto); ok { + apiMembers = make([]string, 0, len(pubKeys)) + keyIDs = make([]int, 0, len(pubKeys)) + memberPubKeys := make([]string, 0, len(pubKeys)) + for mid, pk := range pubKeys { + apiMembers = append(apiMembers, mid) + keyIDs = append(keyIDs, pk.KeyID) + memberPubKeys = append(memberPubKeys, pk.KeyData) + } + if err := ctx.Err(); err != nil { + return nil, nil, nil, err + } + encryptedKeys, err = batch.WrapRegistrationGroupKey(memberPubKeys) + if err != nil { + return nil, nil, nil, fmt.Errorf("wrap group key for members: %w", err) + } + if err := ctx.Err(); err != nil { + return nil, nil, nil, err + } + if len(encryptedKeys) != len(apiMembers) { + return nil, nil, nil, fmt.Errorf("wrapped group key count does not match members") + } + return apiMembers, keyIDs, encryptedKeys, nil + } + groupKeyID, err := crypto.GenerateGroupKey() + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to generate group key: %w", err) + } + if releaser, ok := crypto.(groupKeyReleaser); ok { + defer func() { + if recovered := recover(); recovered != nil { + panic(recovered) + } + if releaseErr := releaser.ReleaseGroupKey(groupKeyID); releaseErr != nil { + err = errors.Join(err, fmt.Errorf("release generated group key: %w", releaseErr)) + } + }() + } + apiMembers = make([]string, 0, len(pubKeys)) + keyIDs = make([]int, 0, len(pubKeys)) + encryptedKeys = make([]string, 0, len(pubKeys)) + for mid, pk := range pubKeys { + if err := ctx.Err(); err != nil { + return nil, nil, nil, err + } + encryptedKey, err := crypto.WrapGroupKeyForMember(pk.KeyData, groupKeyID) + if err != nil { + return nil, nil, nil, fmt.Errorf("wrap group key for member: %w", err) + } + apiMembers = append(apiMembers, mid) + keyIDs = append(keyIDs, pk.KeyID) + encryptedKeys = append(encryptedKeys, encryptedKey) + } + if err := ctx.Err(); err != nil { + return nil, nil, nil, err + } + return apiMembers, keyIDs, encryptedKeys, nil +} + func (lc *LineClient) registerGroupKey(ctx context.Context, chatMid string) error { if lc.E2EE == nil { return fmt.Errorf("%w: E2EE manager not initialized", line.ErrNoUsableE2EEGroupKey) @@ -173,21 +245,9 @@ func (lc *LineClient) registerGroupKeyWithCrypto(ctx context.Context, chatMid st } } - groupKeyID, err := crypto.GenerateGroupKey() + apiMembers, keyIDs, encryptedKeys, err := wrapRegistrationGroupKey(ctx, crypto, pubKeys) if err != nil { - return fmt.Errorf("failed to generate group key: %w", err) - } - apiMembers := make([]string, 0, len(pubKeys)) - keyIDs := make([]int, 0, len(pubKeys)) - encryptedKeys := make([]string, 0, len(pubKeys)) - for mid, pk := range pubKeys { - encryptedKey, err := crypto.WrapGroupKeyForMember(pk.KeyData, groupKeyID) - if err != nil { - return fmt.Errorf("wrap group key for member: %w", err) - } - apiMembers = append(apiMembers, mid) - keyIDs = append(keyIDs, pk.KeyID) - encryptedKeys = append(encryptedKeys, encryptedKey) + return err } if err := ctx.Err(); err != nil { return err diff --git a/pkg/connector/handle_message.go b/pkg/connector/handle_message.go index cb190c7..3c66a58 100644 --- a/pkg/connector/handle_message.go +++ b/pkg/connector/handle_message.go @@ -3,7 +3,6 @@ package connector import ( "context" "encoding/json" - "errors" "fmt" "html" "sort" @@ -21,7 +20,6 @@ import ( "github.com/highesttt/matrix-line-messenger/pkg/connector/handlers" "github.com/highesttt/matrix-line-messenger/pkg/e2ee" "github.com/highesttt/matrix-line-messenger/pkg/line" - "github.com/highesttt/matrix-line-messenger/pkg/ltsm" ) const ( @@ -283,7 +281,7 @@ func (lc *LineClient) decryptMessageBody(msg *line.Message, portalIDStr string, } else { groupDecryptLogContext(lc.UserLogin.Bridge.Log.Debug().Err(err), msg, portalIDStr, opType). Msg("DecryptGroupMessage failed") - if !errors.Is(err, ltsm.ErrAbort) && keyID != 0 { + if !isTerminalCryptoError(err) && keyID != 0 { if errFetch := lc.fetchAndUnwrapGroupKey(context.Background(), portalIDStr, keyID); errFetch != nil { groupDecryptLogContext(lc.UserLogin.Bridge.Log.Warn().Err(errFetch), msg, portalIDStr, opType). Msg("Failed to fetch/unwrap group key") @@ -305,13 +303,16 @@ func (lc *LineClient) decryptMessageBody(msg *line.Message, portalIDStr string, } else { directDecryptLogContext(lc.UserLogin.Bridge.Log.Debug().Err(err), msg, portalIDStr, opType). Msg("DecryptMessageV2 failed on first attempt") - if errors.Is(err, ltsm.ErrAbort) { + if isTerminalCryptoError(err) { directDecryptLogContext(lc.UserLogin.Bridge.Log.Warn().Err(err), msg, portalIDStr, opType). - Msg("LTSM runtime aborted; skipping key refresh") + Msg("Crypto owner unavailable; skipping key refresh") } else if _, _, errKey := lc.E2EE.MyKeyIDs(); errKey != nil { + if isTerminalCryptoError(errKey) { + return "", "", true + } directDecryptLogContext(lc.UserLogin.Bridge.Log.Error().Err(errKey), msg, portalIDStr, opType). Msg("E2EE own key not loaded; cannot decrypt any messages. Re-login required") - lc.markMissingE2EEKey(context.Background(), fmt.Errorf("%w: %v", e2ee.ErrMissingOwnPrivateKey, errKey)) + lc.markMissingE2EEKey(context.Background(), fmt.Errorf("%w: %w", e2ee.ErrMissingOwnPrivateKey, errKey)) } else { peerMid := msg.From peerKeyID := 0 diff --git a/pkg/connector/send_message.go b/pkg/connector/send_message.go index ef2dc55..8611d4f 100644 --- a/pkg/connector/send_message.go +++ b/pkg/connector/send_message.go @@ -19,6 +19,7 @@ import ( "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/event" + gen "github.com/highesttt/matrix-line-messenger/pkg" "github.com/highesttt/matrix-line-messenger/pkg/e2ee" "github.com/highesttt/matrix-line-messenger/pkg/line" "github.com/highesttt/matrix-line-messenger/pkg/ltsm" @@ -35,6 +36,10 @@ var mentionLinkRegex = regexp.MustCompile(`]*href="https://matrix\.to/#/ const lineGroupE2EEReconnectNotice = "LINE encryption keys for this group are unavailable. Reconnect LINE in Beeper, then try sending again." +func isTerminalCryptoError(err error) bool { + return errors.Is(err, ltsm.ErrAbort) || errors.Is(err, gen.ErrRunnerClosed) +} + func lineGroupE2EEReconnectRequiredError(err error) error { if !errors.Is(err, e2ee.ErrMissingOwnPrivateKey) { return nil @@ -58,6 +63,9 @@ func lineGroupE2EEFetchFailureError(err error) error { } func (lc *LineClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) { + if err := lc.checkLineCall(ctx); err != nil { + return nil, err + } client := lc.newClient() callLineErr := func(call func(*line.Client) error) error { var err error @@ -590,7 +598,7 @@ func (lc *LineClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.Mat if isGroup { if errFetch := lc.fetchAndUnwrapGroupKey(ctx, portalMid, 0); errFetch != nil { lc.UserLogin.Bridge.Log.Debug().Err(errFetch).Str("chat_mid", portalMid).Msg("fetchAndUnwrapGroupKey before encrypt failed") - if errors.Is(errFetch, ltsm.ErrAbort) { + if isTerminalCryptoError(errFetch) { return nil, errFetch } if errFetch = lineGroupE2EEFetchFailureError(errFetch); errFetch != nil { @@ -603,7 +611,7 @@ func (lc *LineClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.Mat chunks, err = lc.E2EE.EncryptGroupMessage(portalMid, fromMid, msg.Content.Body) } if err != nil { - if errors.Is(err, ltsm.ErrAbort) { + if isTerminalCryptoError(err) { return nil, err } if errFetch := lc.fetchAndUnwrapGroupKey(ctx, portalMid, 0); errFetch == nil { @@ -612,12 +620,12 @@ func (lc *LineClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.Mat } else { chunks, err = lc.E2EE.EncryptGroupMessage(portalMid, fromMid, msg.Content.Body) } - } else if errors.Is(errFetch, ltsm.ErrAbort) { + } else if isTerminalCryptoError(errFetch) { return nil, errFetch } else if errFetch = lineGroupE2EEFetchFailureError(errFetch); errFetch != nil { return nil, errFetch } - if errors.Is(err, ltsm.ErrAbort) { + if isTerminalCryptoError(err) { return nil, err } if err != nil { diff --git a/pkg/e2ee/manager.go b/pkg/e2ee/manager.go index b852743..b17eda6 100644 --- a/pkg/e2ee/manager.go +++ b/pkg/e2ee/manager.go @@ -24,7 +24,7 @@ var ( ) func isFatalLTSMError(err error) bool { - return errors.Is(err, ltsm.ErrAbort) + return errors.Is(err, ltsm.ErrAbort) || errors.Is(err, gen.ErrRunnerClosed) } /* @@ -33,15 +33,17 @@ This wraps the JS runner to perform storage init, key/channel handling, and e2ee type Manager struct { runner *gen.Runner - mu sync.Mutex - myKeyID int - myRawKeyID int - myKeyB64 string - myPublicB64 string - peerPublic map[int]string // raw key id -> public key b64 - keyByRawID map[int]int // raw key id -> runner key id - channelByPair map[string]int // privRaw/peerRaw -> channelId - sequence map[string]int // portal/chat id -> seq + mu sync.Mutex + closed bool + myKeyID int + myRawKeyID int + myKeyB64 string + myPublicB64 string + ownKeyByExport map[string]int + peerPublic map[int]string // raw key id -> public key b64 + keyByRawID map[int]int // raw key id -> runner key id + channelByPair map[string]int // privRaw/peerRaw -> channelId + sequence map[string]int // portal/chat id -> seq // groupKeys maps chatMid -> groupKeyId -> e2eeKeyId (unwrapped group private key, runner ID) groupKeys map[string]map[int]int @@ -63,12 +65,17 @@ type groupKeyRef struct { const maxGroupNativeObjects = 4096 func NewManager() (*Manager, error) { - r, err := gen.GetRunner() + r, err := gen.NewRunner() if err != nil { return nil, err } + return NewManagerWithRunner(r), nil +} + +func NewManagerWithRunner(r *gen.Runner) *Manager { return &Manager{ runner: r, + ownKeyByExport: make(map[string]int), peerPublic: make(map[int]string), keyByRawID: make(map[int]int), channelByPair: make(map[string]int), @@ -76,12 +83,50 @@ func NewManager() (*Manager, error) { groupKeys: make(map[string]map[int]int), groupSessionChannels: make(map[string]map[int]map[int]int), latestGroupKey: make(map[string]int), - }, nil + } +} + +func (m *Manager) Close() { + m.mu.Lock() + defer m.mu.Unlock() + if m.closed { + return + } + m.closed = true + m.runner.Close() + m.myKeyID, m.myRawKeyID = 0, 0 + m.myKeyB64, m.myPublicB64 = "", "" + m.ownKeyByExport = nil + m.peerPublic = nil + m.keyByRawID = nil + m.channelByPair = nil + m.sequence = nil + m.groupKeys = nil + m.groupSessionChannels = nil + m.latestGroupKey = nil + m.groupKeyLRU = nil + m.groupNativeObjects = 0 +} + +func (m *Manager) errLocked() error { + if m.runner != nil { + if err := m.runner.Err(); err != nil { + return err + } + } + if m.closed { + return gen.ErrRunnerClosed + } + return nil } // returns both the raw key id and the runner key ids func (m *Manager) MyKeyIDs() (rawID int, keyID int, err error) { m.mu.Lock() + if err := m.errLocked(); err != nil { + m.mu.Unlock() + return 0, 0, err + } defer m.mu.Unlock() if m.myKeyID == 0 || m.myRawKeyID == 0 { return 0, 0, fmt.Errorf("my key not loaded") @@ -93,6 +138,10 @@ func (m *Manager) MyKeyIDs() (rawID int, keyID int, err error) { // Used to include the caller's own entry when registering a group key. func (m *Manager) MyPublicKey() (rawID int, pubB64 string, err error) { m.mu.Lock() + if err := m.errLocked(); err != nil { + m.mu.Unlock() + return 0, "", err + } defer m.mu.Unlock() if m.myRawKeyID == 0 || m.myPublicB64 == "" { return 0, "", fmt.Errorf("my key not loaded") @@ -106,10 +155,23 @@ func (m *Manager) InitStorage(wrappedNonce, kdf1, kdf2 string) error { func (m *Manager) LoadMyKey(b64Key string) error { m.mu.Lock() + if err := m.errLocked(); err != nil { + m.mu.Unlock() + return err + } defer m.mu.Unlock() - keyID, err := m.runner.KeyLoad(b64Key) + keyBytes, err := base64.StdEncoding.DecodeString(b64Key) if err != nil { - return err + return fmt.Errorf("invalid key: %w", err) + } + b64Key = base64.StdEncoding.EncodeToString(keyBytes) + keyID := m.ownKeyByExport[b64Key] + if keyID == 0 { + keyID, err = m.runner.KeyLoad(b64Key) + if err != nil { + return err + } + m.ownKeyByExport[b64Key] = keyID } rawID, err := m.runner.KeyGetID(keyID) if err != nil { @@ -156,6 +218,9 @@ func (m *Manager) LoadMyKeyFromSecureData(data map[string]any) error { if err := m.LoadMyKey(b64); err == nil { loadedAny = true } else { + if isFatalLTSMError(err) { + return err + } lastErr = err } } @@ -165,7 +230,9 @@ func (m *Manager) LoadMyKeyFromSecureData(data map[string]any) error { } return fmt.Errorf("failed to load any exported key") } - return nil + m.mu.Lock() + defer m.mu.Unlock() + return m.errLocked() } func (m *Manager) LoadMyKeyFromExportedMap(exported map[string]string) error { @@ -324,6 +391,10 @@ func (m *Manager) encryptV1Chunks(chanID, senderKeyID, receiverKeyID int, payloa func (m *Manager) ensureChannelForEncrypt(chatID string, myKeyID, senderKeyID, receiverKeyID int, peerPubKeyB64 string) (int, int, error) { m.mu.Lock() + defer m.mu.Unlock() + if err := m.errLocked(); err != nil { + return 0, 0, err + } myRaw := m.myRawKeyID myRunnerKey := myKeyID if myRunnerKey == 0 { @@ -332,7 +403,6 @@ func (m *Manager) ensureChannelForEncrypt(chatID string, myKeyID, senderKeyID, r peerPub := m.peerPublic keyByRaw := m.keyByRawID seqMap := m.sequence - m.mu.Unlock() var privRaw, pubRaw, privKeyID int if senderKeyID == myRaw { @@ -349,35 +419,31 @@ func (m *Manager) ensureChannelForEncrypt(chatID string, myKeyID, senderKeyID, r pubKey := peerPubKeyB64 if pubKey == "" { - m.mu.Lock() pubKey = peerPub[pubRaw] - m.mu.Unlock() } if pubKey == "" { return 0, 0, fmt.Errorf("missing peer public key for raw id %d", pubRaw) } - m.RegisterPeerPublicKey(pubRaw, pubKey) + normalized, err := gen.NormalizePeerPublicKeyB64(pubKey) + if err != nil { + return 0, 0, err + } + m.peerPublic[pubRaw] = normalized pairKey := fmt.Sprintf("%d|%d", privRaw, pubRaw) - m.mu.Lock() chanID, ok := m.channelByPair[pairKey] - m.mu.Unlock() if !ok { created, err := m.runner.ChannelCreate(privKeyID, pubKey) if err != nil { return 0, 0, err } chanID = created - m.mu.Lock() m.channelByPair[pairKey] = chanID - m.mu.Unlock() } - m.mu.Lock() seq := seqMap[chatID] + 1 // start at 1 like extension? seqMap[chatID] = seq - m.mu.Unlock() return chanID, seq, nil } @@ -387,6 +453,10 @@ func (m *Manager) DecryptMessageV2(msg *line.Message) (string, error) { return "", fmt.Errorf("not enough chunks") } m.mu.Lock() + if err := m.errLocked(); err != nil { + m.mu.Unlock() + return "", err + } myRaw := m.myRawKeyID m.mu.Unlock() if myRaw == 0 { @@ -483,6 +553,10 @@ func cipherVersion(chunks []string) (int, error) { func (m *Manager) UnwrapGroupSharedKey(chatMid string, sharedKey *line.E2EEGroupSharedKey) (int, error) { m.mu.Lock() + if err := m.errLocked(); err != nil { + m.mu.Unlock() + return 0, err + } defer m.mu.Unlock() ref := groupKeyRef{chatMid, sharedKey.GroupKeyID} if id := m.groupKeys[chatMid][sharedKey.GroupKeyID]; id != 0 { @@ -568,7 +642,48 @@ func (m *Manager) evictGroupKeysLocked() error { // GenerateGroupKey generates a new Curve25519 key in the WASM module for use as a group key. // Returns the key ID for use with WrapGroupKeyForMember. func (m *Manager) GenerateGroupKey() (int, error) { - return m.runner.KeyGenerate() + m.mu.Lock() + defer m.mu.Unlock() + if err := m.errLocked(); err != nil { + return 0, err + } + return m.runner.RegistrationKeyGenerate() +} + +func (m *Manager) WrapRegistrationGroupKey(memberPubKeys []string) ([]string, error) { + m.mu.Lock() + defer m.mu.Unlock() + if err := m.errLocked(); err != nil { + return nil, err + } + if m.myKeyID == 0 { + return nil, fmt.Errorf("my E2EE key not loaded") + } + channelIDs := make([]int, len(memberPubKeys)) + for i, memberPubKey := range memberPubKeys { + normalized, err := gen.NormalizePeerPublicKeyB64(memberPubKey) + if err != nil { + return nil, fmt.Errorf("invalid member public key: %w", err) + } + channelIDs[i], err = m.runner.ChannelCreate(m.myKeyID, normalized) + if err != nil { + return nil, fmt.Errorf("failed to create ECDH channel: %w", err) + } + } + wrapped, err := m.runner.WrapRegistrationGroupKey(channelIDs) + if err != nil { + return nil, fmt.Errorf("failed to wrap group key: %w", err) + } + return wrapped, nil +} + +func (m *Manager) ReleaseGroupKey(keyID int) error { + m.mu.Lock() + defer m.mu.Unlock() + if err := m.errLocked(); err != nil { + return err + } + return m.runner.RegistrationKeyRelease(keyID) } // WrapGroupKeyForMember wraps a WASM-generated group key for a specific group member. @@ -576,6 +691,10 @@ func (m *Manager) GenerateGroupKey() (int, error) { // Returns the base64-encoded wrapped key, ready for registerE2EEGroupKey. func (m *Manager) WrapGroupKeyForMember(memberPubKeyB64 string, keyID int) (string, error) { m.mu.Lock() + if err := m.errLocked(); err != nil { + m.mu.Unlock() + return "", err + } myKeyID := m.myKeyID m.mu.Unlock() @@ -621,6 +740,9 @@ func (m *Manager) DecryptGroupMessage(msg *line.Message, chatMid string) (string m.mu.Lock() defer m.mu.Unlock() + if err := m.errLocked(); err != nil { + return "", 0, err + } unwrappedKeyID := m.groupKeys[chatMid][groupKeyID] if unwrappedKeyID == 0 { return "", groupKeyID, fmt.Errorf("group key %d not found", groupKeyID) @@ -675,6 +797,9 @@ func (m *Manager) DecryptGroupMessage(msg *line.Message, chatMid string) (string // Callers keep m.mu held while using the channel so an eviction cannot destroy it. func (m *Manager) ensureGroupChannelLocked(chatMid string, groupKeyID, unwrappedKeyID, senderKeyID int) (int, error) { + if err := m.errLocked(); err != nil { + return 0, err + } if m.groupSessionChannels[chatMid] == nil { m.groupSessionChannels[chatMid] = make(map[int]map[int]int) } @@ -713,6 +838,9 @@ func (m *Manager) EncryptGroupMessage(chatMid, fromMid string, plaintext string) func (m *Manager) EncryptGroupMessageRaw(chatMid, fromMid string, contentType int, payload []byte) ([]string, error) { m.mu.Lock() defer m.mu.Unlock() + if err := m.errLocked(); err != nil { + return nil, err + } groupKeyID, ok := m.latestGroupKey[chatMid] var unwrappedKeyID int if ok { @@ -766,6 +894,10 @@ func (m *Manager) EncryptGroupMessageRaw(chatMid, fromMid string, contentType in func (m *Manager) HasPeerPublicKey(rawKeyID int) bool { m.mu.Lock() + if err := m.errLocked(); err != nil { + m.mu.Unlock() + return false + } defer m.mu.Unlock() _, ok := m.peerPublic[rawKeyID] return ok @@ -777,6 +909,10 @@ func (m *Manager) HasPeerPublicKey(rawKeyID int) bool { // since myRawKeyID only points to the latest one. func (m *Manager) IsMyKey(rawKeyID int) bool { m.mu.Lock() + if err := m.errLocked(); err != nil { + m.mu.Unlock() + return false + } defer m.mu.Unlock() _, ok := m.keyByRawID[rawKeyID] return ok @@ -788,23 +924,34 @@ func (m *Manager) RegisterPeerPublicKey(rawKeyID int, pubB64 string) { return } m.mu.Lock() + if err := m.errLocked(); err != nil { + m.mu.Unlock() + return + } defer m.mu.Unlock() m.peerPublic[rawKeyID] = normalized } func (m *Manager) RegisterUnwrappedKey(rawKeyID, runnerKeyID int) { m.mu.Lock() + if err := m.errLocked(); err != nil { + m.mu.Unlock() + return + } defer m.mu.Unlock() m.keyByRawID[rawKeyID] = runnerKeyID } func (m *Manager) channelFromKeyIDs(senderKeyID, receiverKeyID int) (int, error) { m.mu.Lock() + defer m.mu.Unlock() + if err := m.errLocked(); err != nil { + return 0, err + } myRaw := m.myRawKeyID myKeyID := m.myKeyID peerPub := m.peerPublic keyByRaw := m.keyByRawID - m.mu.Unlock() // Determine which side is the users's var privRaw, pubRaw, privKeyID int @@ -836,20 +983,15 @@ func (m *Manager) channelFromKeyIDs(senderKeyID, receiverKeyID int) (int, error) } pairKey := fmt.Sprintf("%d|%d", privRaw, pubRaw) - m.mu.Lock() if id, ok := m.channelByPair[pairKey]; ok { - m.mu.Unlock() return id, nil } - m.mu.Unlock() chanID, err := m.runner.ChannelCreate(privKeyID, pubB64) if err != nil { return 0, err } - m.mu.Lock() m.channelByPair[pairKey] = chanID - m.mu.Unlock() return chanID, nil } diff --git a/pkg/line/client.go b/pkg/line/client.go index 90e2cb7..0a7aaf3 100644 --- a/pkg/line/client.go +++ b/pkg/line/client.go @@ -19,7 +19,6 @@ import ( gen "github.com/highesttt/matrix-line-messenger/pkg" "github.com/highesttt/matrix-line-messenger/pkg/line/password" - "github.com/highesttt/matrix-line-messenger/pkg/line/secret" ) const ( @@ -43,14 +42,36 @@ var ( ) type Client struct { - HTTPClient *http.Client - OBSClient *http.Client - AccessToken string + LoginAttempt *LoginAttempt + HTTPClient *http.Client + OBSClient *http.Client + AccessToken string channelTokenMu sync.Mutex channelTokenCache map[string]cachedChannelAccessToken } +type LoginAttempt struct { + Runner *gen.Runner + Context context.Context + cancel context.CancelFunc +} + +func (a *LoginAttempt) Close() { + if a == nil { + return + } + a.cancel() + a.Runner.Close() +} + +func (c *Client) requestContext() context.Context { + if c.LoginAttempt != nil { + return c.LoginAttempt.Context + } + return context.Background() +} + type OBSDownloadOptions struct { TID string OBSPop string @@ -80,6 +101,32 @@ func (c *Client) obsHTTPClient() *http.Client { } func (c *Client) Login(email, pass, certificate string) (*LoginResult, error) { + return c.LoginContext(context.Background(), email, pass, certificate) +} + +func (c *Client) LoginContext(ctx context.Context, email, pass, certificate string) (result *LoginResult, err error) { + if c.LoginAttempt != nil { + c.LoginAttempt.Close() + } + r, err := gen.NewRunner() + if err != nil { + return nil, err + } + attemptCtx, cancel := context.WithCancel(ctx) + attempt := &LoginAttempt{Runner: r, Context: attemptCtx, cancel: cancel} + c.LoginAttempt = attempt + defer func() { + if ctx.Err() != nil { + result = nil + err = ctx.Err() + } + if result == nil { + attempt.Close() + } else { + result.Attempt = attempt + } + }() + // 1. Get RSA Key Info rsaKey, err := c.GetRSAKeyInfo() if err != nil { @@ -93,7 +140,7 @@ func (c *Client) Login(email, pass, certificate string) (*LoginResult, error) { } // 3. Generate E2EE Secret - secretRes, err := secret.GenerateSecret() + secretRes, err := attempt.Runner.GenerateE2EESecret() if err != nil { return nil, fmt.Errorf("failed to generate e2ee secret: %w", err) } @@ -139,6 +186,9 @@ func (c *Client) Login(email, pass, certificate string) (*LoginResult, error) { } res.NoE2EE = noE2EE + if noE2EE { + attempt.Runner.Close() + } // Prefer the V3 token if present, otherwise fall back to legacy authToken. // LINE returns only the V3 token when re-authenticating with a stored @@ -161,7 +211,22 @@ func isLoginNotSupported(err error) bool { return strings.Contains(msg, "\"code\":89") || strings.Contains(msg, "not supported") } -func (c *Client) WaitForLogin(verifier string, noE2EE bool) (*LoginResult, error) { +func (c *Client) WaitForLogin(verifier string, noE2EE bool) (res *LoginResult, err error) { + defer func() { + if c.requestContext().Err() != nil { + res = nil + err = c.requestContext().Err() + } + if err != nil && c.LoginAttempt != nil { + c.LoginAttempt.Close() + } + + if res != nil { + res.Attempt = c.LoginAttempt + res.NoE2EE = noE2EE + } + }() + if noE2EE { return c.waitForLoginJQ(verifier) } @@ -172,7 +237,7 @@ func (c *Client) WaitForLogin(verifier string, noE2EE bool) (*LoginResult, error func (c *Client) waitForLoginJQ(verifier string) (*LoginResult, error) { url := "https://line-chrome-gw.line-apps.com/api/talk/long-polling/JQ" - req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequestWithContext(c.requestContext(), "GET", url, nil) if err != nil { return nil, err } @@ -236,7 +301,7 @@ func (c *Client) waitForLoginJQ(verifier string) (*LoginResult, error) { func (c *Client) waitForLoginLF1(verifier string) (*LoginResult, error) { url := "https://line-chrome-gw.line-apps.com/api/talk/long-polling/LF1" - req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequestWithContext(c.requestContext(), "GET", url, nil) if err != nil { return nil, err } @@ -331,7 +396,7 @@ func (c *Client) GetRSAKeyInfo() (*RSAKeyInfo, error) { } func (c *Client) callRPC(service, method string, args ...interface{}) ([]byte, error) { - return c.callRPCContext(context.Background(), service, method, args...) + return c.callRPCContext(c.requestContext(), service, method, args...) } func (c *Client) callRPCContext(ctx context.Context, service, method string, args ...interface{}) ([]byte, error) { @@ -343,7 +408,7 @@ func (c *Client) callShopRPC(service, method string, args ...interface{}) ([]byt } func (c *Client) callRPCWithBaseURL(baseURL, service, method string, args ...interface{}) ([]byte, error) { - return c.callRPCWithBaseURLContext(context.Background(), baseURL, service, method, args...) + return c.callRPCWithBaseURLContext(c.requestContext(), baseURL, service, method, args...) } func (c *Client) callRPCWithBaseURLContext(ctx context.Context, baseURL, service, method string, args ...interface{}) ([]byte, error) { @@ -404,10 +469,10 @@ func (c *Client) callRPCWithBaseURLContext(ctx context.Context, baseURL, service // ConfirmE2EELogin completes the E2EE handshake after LF1 by hashing the encrypted key // chain and posting it alongside the verifier. func (c *Client) ConfirmE2EELogin(verifier, serverPublicKeyB64, encryptedKeyChainB64 string) (string, error) { - runner, err := gen.GetRunner() - if err != nil { - return "", fmt.Errorf("failed to init runner: %w", err) + if c.LoginAttempt == nil { + return "", errors.New("missing E2EE login attempt") } + runner := c.LoginAttempt.Runner hash, err := runner.GenerateConfirmHash(serverPublicKeyB64, encryptedKeyChainB64) if err != nil { @@ -445,7 +510,7 @@ func (c *Client) ConfirmE2EELogin(verifier, serverPublicKeyB64, encryptedKeyChai // postWithHMAC is a small helper for non-standard RPC endpoints that still expect // the same headers and HMAC signature as the Talk endpoints. func (c *Client) postWithHMAC(fullURL string, body []byte) ([]byte, error) { - req, err := http.NewRequest("POST", fullURL, bytes.NewBuffer(body)) + req, err := http.NewRequestWithContext(c.requestContext(), "POST", fullURL, bytes.NewBuffer(body)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/pkg/line/structs.go b/pkg/line/structs.go index 36edb16..1816f6d 100644 --- a/pkg/line/structs.go +++ b/pkg/line/structs.go @@ -131,6 +131,7 @@ type RSAKeyInfo struct { } type LoginResult struct { + Attempt *LoginAttempt `json:"-"` AuthToken string `json:"authToken"` Certificate string `json:"certificate"` Verifier string `json:"verifier"` diff --git a/pkg/ltsm/runtime.go b/pkg/ltsm/runtime.go index 6894db0..114d602 100644 --- a/pkg/ltsm/runtime.go +++ b/pkg/ltsm/runtime.go @@ -293,6 +293,10 @@ func (rt *Runtime) Curve25519KeyGenerate() (uint32, error) { return ptr, nil } +func (rt *Runtime) Curve25519KeyDestroy(ptr uint32) error { + return rt.imp.Destroy("Curve25519Key", ptr) +} + // --- E2EEKey --- func (rt *Runtime) E2EEKeyLoadKey(keyBytes []byte) (uint32, error) { @@ -375,17 +379,43 @@ func (rt *Runtime) E2EEChannelUnwrapGroupSharedKey(chanPtr uint32, encKey []byte return ptr, nil } -func (rt *Runtime) E2EEChannelWrapGroupSharedKey(chanPtr uint32, keyHandle uint32) ([]byte, error) { - handle, err := rt.imp.CallMethod("E2EEChannel", "wrapGroupSharedKey", chanPtr, keyHandle) - if err != nil { - return nil, fmt.Errorf("ltsm: E2EEChannel.wrapGroupSharedKey failed: %w", err) - } - data, err := rt.imp.ReadEmvalBytes(handle) +func (rt *Runtime) WrapRegistrationGroupKey(chanPtrs []uint32) ([][]byte, error) { + var wrapped [][]byte + defer func() { rt.cryptoDirty = true }() + _, err := rt.channelCrypto(func(crypto *Runtime) ([]byte, error) { + keyPtr, err := crypto.Curve25519KeyGenerate() + if err != nil { + return nil, err + } + wrapped = make([][]byte, len(chanPtrs)) + for i, chanPtr := range chanPtrs { + data, err := crypto.E2EEChannelWrapGroupSharedKey(chanPtr, keyPtr) + if err != nil { + return nil, err + } + wrapped[i] = append([]byte(nil), data...) + } + return nil, nil + }) if err != nil { - return nil, fmt.Errorf("ltsm: failed to read wrapGroupSharedKey result: %w", err) + return nil, err } - rt.imp.emval.DecRef(handle) - return data, nil + return wrapped, nil +} + +func (rt *Runtime) E2EEChannelWrapGroupSharedKey(chanPtr uint32, keyPtr uint32) ([]byte, error) { + return rt.channelCrypto(func(crypto *Runtime) ([]byte, error) { + handle, err := crypto.imp.CallMethod("E2EEChannel", "wrapGroupSharedKey", chanPtr, keyPtr) + if err != nil { + return nil, fmt.Errorf("ltsm: E2EEChannel.wrapGroupSharedKey failed: %w", err) + } + data, err := crypto.imp.ReadEmvalBytes(handle) + if err != nil { + return nil, fmt.Errorf("ltsm: failed to read wrapGroupSharedKey result: %w", err) + } + crypto.imp.emval.DecRef(handle) + return data, nil + }) } func (rt *Runtime) E2EEChannelGenerateConfirmHash(chanPtr uint32, encKeyChain []byte) ([]byte, error) { diff --git a/pkg/runner.go b/pkg/runner.go index d50ad7e..e633d37 100644 --- a/pkg/runner.go +++ b/pkg/runner.go @@ -17,17 +17,19 @@ import ( ) type Runner struct { - rt *ltsm.Runtime - token string - clientVersion string - skPtr uint32 // SecureKey from loadToken - storageKey uint32 // AesKey ptr (after StorageInit) - loginCurveKey uint32 // Curve25519Key ptr (after GenerateE2EESecret) - keyStore map[int]uint32 // internal ID -> E2EEKey ptr - channelStore map[int]uint32 // internal ID -> E2EEChannel ptr - channelsByKey map[channelKey]int - nextID int - mu sync.Mutex + failure error + rt *ltsm.Runtime + token string + clientVersion string + skPtr uint32 // SecureKey from loadToken + storageKey uint32 // AesKey ptr (after StorageInit) + loginCurveKey uint32 // Curve25519Key ptr (after GenerateE2EESecret) + registrationKeys map[int]struct{} + keyStore map[int]uint32 // internal ID -> E2EEKey ptr + channelStore map[int]uint32 // internal ID -> E2EEChannel ptr + channelsByKey map[channelKey]int + nextID int + mu sync.Mutex // Signing state can be recreated from configuration and the access token // without invalidating the E2EE objects in rt. All access is under mu. @@ -84,43 +86,99 @@ var ( runnerErr error ) +var ErrRunnerClosed = errors.New("crypto runner closed") + func GetRunner() (*Runner, error) { - runnerOnce.Do(func() { - token := os.Getenv("SECURE_KEY") - if token == "" { - token = "wODdrvWqmdP4Zliay-iF3cz3KZcK0ekrial868apg06TXeCo7A1hIQO0ESElHg6D" - } - clientVersion := os.Getenv("CLIENT_VERSION") - if clientVersion == "" { - clientVersion = "3.7.2" + runnerOnce.Do(func() { globalRunner, runnerErr = NewRunner() }) + return globalRunner, runnerErr +} +func NewRunner() (r *Runner, err error) { + defer func() { + if recovered := recover(); recovered != nil { + abort, ok := recovered.(error) + if !ok || !errors.Is(abort, ltsm.ErrAbort) { + panic(recovered) + } + r = nil + err = ltsmPanicError("ltsm initialization", recovered) } + }() + token := os.Getenv("SECURE_KEY") + if token == "" { + token = "wODdrvWqmdP4Zliay-iF3cz3KZcK0ekrial868apg06TXeCo7A1hIQO0ESElHg6D" + } + clientVersion := os.Getenv("CLIENT_VERSION") + if clientVersion == "" { + clientVersion = "3.7.2" + } - rt, err := ltsm.NewRuntime() - if err != nil { - runnerErr = fmt.Errorf("failed to initialize LTSM runtime: %w", err) - return - } + rt, err := ltsm.NewRuntime() + if err != nil { + err = fmt.Errorf("failed to initialize LTSM runtime: %w", err) + return nil, err + } - skPtr, err := rt.SecureKeyLoadToken(token) - if err != nil { - rt.Close() - runnerErr = fmt.Errorf("failed to load secure key: %w", err) - return - } + skPtr, err := rt.SecureKeyLoadToken(token) + if err != nil { + rt.Close() + err = fmt.Errorf("failed to load secure key: %w", err) + return nil, err + } + + r = &Runner{ + rt: rt, + token: token, + clientVersion: clientVersion, + skPtr: skPtr, + keyStore: make(map[int]uint32), + channelStore: make(map[int]uint32), + nextID: 1, + goKeys: make(map[int]*goKeyEntry), + goChannels: make(map[int]*ltsm.Channel), + } + return r, nil +} +func (r *Runner) discard(err error) { + r.failure = err + r.rt = nil + r.skPtr = 0 + r.storageKey = 0 + r.loginCurveKey = 0 + r.registrationKeys = nil + r.keyStore = nil + r.channelStore = nil + r.channelsByKey = nil + r.goKeys = nil + r.goChannels = nil + r.resetSigningRuntime() +} +func (r *Runner) Close() { + r.mu.Lock() + defer r.mu.Unlock() + if r.failure == nil { + r.discard(ErrRunnerClosed) + } +} +func (r *Runner) Err() error { + r.mu.Lock() + defer r.mu.Unlock() + return r.failure +} - globalRunner = &Runner{ - rt: rt, - token: token, - clientVersion: clientVersion, - skPtr: skPtr, - keyStore: make(map[int]uint32), - channelStore: make(map[int]uint32), - nextID: 1, - goKeys: make(map[int]*goKeyEntry), - goChannels: make(map[int]*ltsm.Channel), +func (r *Runner) finishOperation(operation string, err *error, authoritative bool, zero func()) { + if recovered := recover(); recovered != nil { + abort, ok := recovered.(error) + if !ok || !errors.Is(abort, ltsm.ErrAbort) { + panic(recovered) } - }) - return globalRunner, runnerErr + *err = ltsmPanicError("ltsm "+operation, recovered) + } + if authoritative && errors.Is(*err, ltsm.ErrAbort) { + r.discard(*err) + } + if *err != nil { + zero() + } } func (r *Runner) putKey(ptr uint32) int { @@ -138,9 +196,31 @@ func (r *Runner) getKey(id int) (uint32, error) { return ptr, nil } -func (r *Runner) KeyGenerate() (int, error) { +func (r *Runner) KeyGenerate() (out0 int, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return 0, r.failure + } + defer func() { + if recovered := recover(); recovered != nil { + abort, ok := recovered.(error) + if !ok || !errors.Is(abort, ltsm.ErrAbort) { + panic(recovered) + } + err = ltsmPanicError("ltsm KeyGenerate", recovered) + } + if errors.Is(err, ltsm.ErrAbort) { + r.discard(err) + } + if err != nil { + out0 = 0 + } + }() + return r.keyGenerate() +} + +func (r *Runner) keyGenerate() (int, error) { ptr, err := r.rt.Curve25519KeyGenerate() if err != nil { return 0, err @@ -148,6 +228,53 @@ func (r *Runner) KeyGenerate() (int, error) { return r.putKey(ptr), nil } +func (r *Runner) RegistrationKeyGenerate() (out0 int, err error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.failure != nil { + return 0, r.failure + } + defer r.finishOperation("RegistrationKeyGenerate", &err, true, func() { out0 = 0 }) + id, err := r.keyGenerate() + if err != nil { + return 0, err + } + if r.registrationKeys == nil { + r.registrationKeys = make(map[int]struct{}) + } + r.registrationKeys[id] = struct{}{} + return id, nil +} + +func (r *Runner) RegistrationKeyRelease(keyID int) (err error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.failure != nil { + return r.failure + } + defer r.finishOperation("RegistrationKeyRelease", &err, true, func() {}) + if _, ok := r.registrationKeys[keyID]; !ok { + return fmt.Errorf("key %d is not an ephemeral registration key", keyID) + } + for dependency := range r.channelsByKey { + if dependency.keyID == keyID { + return fmt.Errorf("registration key %d has a channel dependency", keyID) + } + } + ptr, err := r.getKey(keyID) + if err != nil { + return err + } + if err = r.rt.Curve25519KeyDestroy(ptr); err != nil { + err = errors.Join(ErrRunnerClosed, fmt.Errorf("destroy registration key: %w", err)) + r.discard(err) + return err + } + delete(r.registrationKeys, keyID) + delete(r.keyStore, keyID) + return nil +} + func (r *Runner) putChannel(ptr uint32) int { id := r.nextID r.nextID++ @@ -173,6 +300,10 @@ func ltsmPanicError(operation string, recovered any) error { func (r *Runner) exportE2EEKeyPanicSafe(keyPtr uint32) (exported []byte, err error) { defer func() { if recovered := recover(); recovered != nil { + abort, ok := recovered.(error) + if !ok || !errors.Is(abort, ltsm.ErrAbort) { + panic(recovered) + } exported = nil err = ltsmPanicError("ltsm E2EEKey.exportKey", recovered) } @@ -183,6 +314,10 @@ func (r *Runner) exportE2EEKeyPanicSafe(keyPtr uint32) (exported []byte, err err func (r *Runner) unwrapGroupSharedKeyPanicSafe(chanPtr uint32, encKey []byte) (keyPtr uint32, err error) { defer func() { if recovered := recover(); recovered != nil { + abort, ok := recovered.(error) + if !ok || !errors.Is(abort, ltsm.ErrAbort) { + panic(recovered) + } keyPtr = 0 err = ltsmPanicError("ltsm E2EEChannel.unwrapGroupSharedKey", recovered) } @@ -190,18 +325,13 @@ func (r *Runner) unwrapGroupSharedKeyPanicSafe(chanPtr uint32, encKey []byte) (k return r.rt.E2EEChannelUnwrapGroupSharedKey(chanPtr, encKey) } -func (r *Runner) destroyPanicSafe(class string, destroy func(uint32) error, ptr uint32) (err error) { - defer func() { - if recovered := recover(); recovered != nil { - err = ltsmPanicError("ltsm "+class+" destroy", recovered) - } - }() - return destroy(ptr) -} - func (r *Runner) encryptV1PanicSafe(chanPtr uint32, plaintext []byte) (ciphertext []byte, err error) { defer func() { if recovered := recover(); recovered != nil { + abort, ok := recovered.(error) + if !ok || !errors.Is(abort, ltsm.ErrAbort) { + panic(recovered) + } ciphertext = nil err = ltsmPanicError("ltsm E2EEChannel.encryptV1", recovered) } @@ -212,6 +342,10 @@ func (r *Runner) encryptV1PanicSafe(chanPtr uint32, plaintext []byte) (ciphertex func (r *Runner) encryptV2PanicSafe(chanPtr uint32, to, from string, senderKeyID, receiverKeyID, contentType int, seq int64, plaintext []byte) (ciphertext []byte, err error) { defer func() { if recovered := recover(); recovered != nil { + abort, ok := recovered.(error) + if !ok || !errors.Is(abort, ltsm.ErrAbort) { + panic(recovered) + } ciphertext = nil err = ltsmPanicError("ltsm E2EEChannel.encryptV2", recovered) } @@ -222,6 +356,10 @@ func (r *Runner) encryptV2PanicSafe(chanPtr uint32, to, from string, senderKeyID func (r *Runner) decryptV1PanicSafe(chanPtr uint32, ciphertext []byte) (plaintext []byte, err error) { defer func() { if recovered := recover(); recovered != nil { + abort, ok := recovered.(error) + if !ok || !errors.Is(abort, ltsm.ErrAbort) { + panic(recovered) + } plaintext = nil err = ltsmPanicError("ltsm E2EEChannel.decryptV1", recovered) } @@ -232,6 +370,10 @@ func (r *Runner) decryptV1PanicSafe(chanPtr uint32, ciphertext []byte) (plaintex func (r *Runner) decryptV2PanicSafe(chanPtr uint32, to, from string, senderKeyID, receiverKeyID, contentType int, ciphertext []byte) (plaintext []byte, err error) { defer func() { if recovered := recover(); recovered != nil { + abort, ok := recovered.(error) + if !ok || !errors.Is(abort, ltsm.ErrAbort) { + panic(recovered) + } plaintext = nil err = ltsmPanicError("ltsm E2EEChannel.decryptV2", recovered) } @@ -299,16 +441,34 @@ func (r *Runner) signingHMAC(accessToken string) (uint32, error) { return hmacPtr, nil } -func (r *Runner) GetSignature(reqPath, body, accessToken string) (string, error) { +func (r *Runner) GetSignature(reqPath, body, accessToken string) (out0 string, err error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.failure != nil { + return "", r.failure + } + defer func() { + if recovered := recover(); recovered != nil { + abort, ok := recovered.(error) + if !ok || !errors.Is(abort, ltsm.ErrAbort) { + panic(recovered) + } + err = ltsmPanicError("ltsm GetSignature", recovered) + } + if err != nil { + out0 = "" + } + }() + return r.signatureLocked(reqPath, body, accessToken) +} + +func (r *Runner) signatureLocked(reqPath, body, accessToken string) (string, error) { if reqPath == "" { reqPath = "/" } else if reqPath[0] != '/' { reqPath = "/" + reqPath } - r.mu.Lock() - defer r.mu.Unlock() - signature, err := r.getSignature(reqPath, body, accessToken) if errors.Is(err, ltsm.ErrAbort) { // getSignature discarded the aborted runtime. Retry once from a clean @@ -375,14 +535,35 @@ func (r *Runner) getSignature(reqPath, body, accessToken string) (signature stri // DebugExportDerivedSigningKey returns the derived SecureKey export blob used for // signing. Disabled unless LTSM_ENABLE_DEBUG_EXPORT=1 is set. -func (r *Runner) DebugExportDerivedSigningKey(accessToken string) (string, error) { +func (r *Runner) DebugExportDerivedSigningKey(accessToken string) (out0 string, err error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.failure != nil { + return "", r.failure + } + defer func() { + if recovered := recover(); recovered != nil { + abort, ok := recovered.(error) + if !ok || !errors.Is(abort, ltsm.ErrAbort) { + panic(recovered) + } + err = ltsmPanicError("ltsm DebugExportDerivedSigningKey", recovered) + } + if errors.Is(err, ltsm.ErrAbort) { + r.discard(err) + } + if err != nil { + out0 = "" + } + }() + return r.debugExportDerivedSigningKey(accessToken) +} + +func (r *Runner) debugExportDerivedSigningKey(accessToken string) (string, error) { if os.Getenv("LTSM_ENABLE_DEBUG_EXPORT") != "1" { return "", fmt.Errorf("runner error: debug export disabled") } - r.mu.Lock() - defer r.mu.Unlock() - cvHash := sha256.Sum256([]byte(r.clientVersion)) atHash := sha256.Sum256([]byte(accessToken)) @@ -390,7 +571,6 @@ func (r *Runner) DebugExportDerivedSigningKey(accessToken string) (string, error if err != nil { return "", err } - defer func() { _ = r.rt.SecureKeyDestroy(dkPtr) }() // Mark the derived key as exportable by setting the C++ flag at ptr+16 r.rt.MarkSecureKeyExportable(dkPtr) @@ -399,13 +579,24 @@ func (r *Runner) DebugExportDerivedSigningKey(accessToken string) (string, error if err != nil { return "", err } + if err := r.rt.SecureKeyDestroy(dkPtr); err != nil { + return "", err + } return base64.StdEncoding.EncodeToString(exported), nil } // StorageInit initializes the storage key using getEncryptedIdentityV3 response fields. -func (r *Runner) StorageInit(wrappedNonce, kdf1, kdf2 string) error { +func (r *Runner) StorageInit(wrappedNonce, kdf1, kdf2 string) (err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return r.failure + } + defer r.finishOperation("StorageInit", &err, true, func() {}) + return r.storageInit(wrappedNonce, kdf1, kdf2) +} + +func (r *Runner) storageInit(wrappedNonce, kdf1, kdf2 string) error { nonceBytes, err := base64.StdEncoding.DecodeString(wrappedNonce) if err != nil { @@ -435,9 +626,17 @@ func (r *Runner) StorageInit(wrappedNonce, kdf1, kdf2 string) error { } // StorageDecrypt decrypts lcs_secure blobs with the initialized storage key. -func (r *Runner) StorageDecrypt(ciphertext string) (string, error) { +func (r *Runner) StorageDecrypt(ciphertext string) (out0 string, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return "", r.failure + } + defer r.finishOperation("StorageDecrypt", &err, true, func() { out0 = "" }) + return r.storageDecrypt(ciphertext) +} + +func (r *Runner) storageDecrypt(ciphertext string) (string, error) { if r.storageKey == 0 { return "", fmt.Errorf("storage key not initialized") @@ -457,9 +656,17 @@ func (r *Runner) StorageDecrypt(ciphertext string) (string, error) { } // StorageEncrypt encrypts plaintext with the initialized storage key. -func (r *Runner) StorageEncrypt(plaintext string) (string, error) { +func (r *Runner) StorageEncrypt(plaintext string) (out0 string, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return "", r.failure + } + defer r.finishOperation("StorageEncrypt", &err, true, func() { out0 = "" }) + return r.storageEncrypt(plaintext) +} + +func (r *Runner) storageEncrypt(plaintext string) (string, error) { if r.storageKey == 0 { return "", fmt.Errorf("storage key not initialized") @@ -474,15 +681,22 @@ func (r *Runner) StorageEncrypt(plaintext string) (string, error) { } // LoginUnwrapKeyChain unwraps the encrypted key chain from LF1 using the login curve key. -func (r *Runner) LoginUnwrapKeyChain(serverPubB64, encryptedKeyChainB64 string) ([]UnwrappedKey, error) { +func (r *Runner) LoginUnwrapKeyChain(serverPubB64, encryptedKeyChainB64 string) (out0 []UnwrappedKey, err error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.failure != nil { + return nil, r.failure + } + defer r.finishOperation("LoginUnwrapKeyChain", &err, true, func() { out0 = nil }) + return r.loginUnwrapKeyChain(serverPubB64, encryptedKeyChainB64) +} + +func (r *Runner) loginUnwrapKeyChain(serverPubB64, encryptedKeyChainB64 string) ([]UnwrappedKey, error) { normalizedServerPub, err := normalizeServerPublicKeyB64(serverPubB64) if err != nil { return nil, err } - r.mu.Lock() - defer r.mu.Unlock() - if r.loginCurveKey == 0 { return nil, fmt.Errorf("login key not initialized") } @@ -550,9 +764,17 @@ func (r *Runner) LoginUnwrapKeyChain(serverPubB64, encryptedKeyChainB64 string) // KeyLoad loads a base64 E2EE key and returns an internal key ID. // Also stores the decoded key bytes as a Go key entry so ChannelCreate can // create a pure Go Channel (required for group shared key wrapping). -func (r *Runner) KeyLoad(b64Key string) (int, error) { +func (r *Runner) KeyLoad(b64Key string) (out0 int, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return 0, r.failure + } + defer r.finishOperation("KeyLoad", &err, true, func() { out0 = 0 }) + return r.keyLoad(b64Key) +} + +func (r *Runner) keyLoad(b64Key string) (int, error) { keyBytes, err := base64.StdEncoding.DecodeString(b64Key) if err != nil { @@ -575,9 +797,17 @@ func (r *Runner) KeyLoad(b64Key string) (int, error) { } // KeyGetID returns the raw key ID for a loaded key. -func (r *Runner) KeyGetID(keyID int) (int, error) { +func (r *Runner) KeyGetID(keyID int) (out0 int, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return 0, r.failure + } + defer r.finishOperation("KeyGetID", &err, true, func() { out0 = 0 }) + return r.keyGetID(keyID) +} + +func (r *Runner) keyGetID(keyID int) (int, error) { keyPtr, err := r.getKey(keyID) if err != nil { @@ -588,9 +818,17 @@ func (r *Runner) KeyGetID(keyID int) (int, error) { } // KeyGetPublic returns the base64 public key for a loaded key. -func (r *Runner) KeyGetPublic(keyID int) (string, error) { +func (r *Runner) KeyGetPublic(keyID int) (out0 string, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return "", r.failure + } + defer r.finishOperation("KeyGetPublic", &err, true, func() { out0 = "" }) + return r.keyGetPublic(keyID) +} + +func (r *Runner) keyGetPublic(keyID int) (string, error) { keyPtr, err := r.getKey(keyID) if err != nil { @@ -608,9 +846,20 @@ func (r *Runner) KeyGetPublic(keyID int) (string, error) { // ChannelCreate creates a channel with our key and peer public key. // If a raw Go private key is available for the key, also creates a pure Go // channel for V1/V2 encrypt/decrypt without WASM overhead. -func (r *Runner) ChannelCreate(keyID int, peerPublicB64 string) (int, error) { +func (r *Runner) ChannelCreate(keyID int, peerPublicB64 string) (out0 int, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return 0, r.failure + } + defer r.finishOperation("ChannelCreate", &err, true, func() { out0 = 0 }) + return r.channelCreate(keyID, peerPublicB64) +} + +func (r *Runner) channelCreate(keyID int, peerPublicB64 string) (int, error) { + if _, ok := r.registrationKeys[keyID]; ok { + return 0, fmt.Errorf("registration key %d cannot own a channel", keyID) + } keyPtr, err := r.getKey(keyID) if err != nil { @@ -651,9 +900,17 @@ func (r *Runner) ChannelCreate(keyID int, peerPublicB64 string) (int, error) { } // ChannelUnwrapGroupSharedKey unwraps the group shared key using the channel. -func (r *Runner) ChannelUnwrapGroupSharedKey(channelID int, encryptedSharedKeyB64 string) (int, error) { +func (r *Runner) ChannelUnwrapGroupSharedKey(channelID int, encryptedSharedKeyB64 string) (out0 int, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return 0, r.failure + } + defer r.finishOperation("ChannelUnwrapGroupSharedKey", &err, true, func() { out0 = 0 }) + return r.channelUnwrapGroupSharedKey(channelID, encryptedSharedKeyB64) +} + +func (r *Runner) channelUnwrapGroupSharedKey(channelID int, encryptedSharedKeyB64 string) (int, error) { chanPtr, err := r.getChannel(channelID) if err != nil { @@ -674,15 +931,18 @@ func (r *Runner) ChannelUnwrapGroupSharedKey(channelID int, encryptedSharedKeyB6 } // KeyDestroy releases an unwrapped group key and every channel created from it. -func (r *Runner) KeyDestroy(keyID int) error { +func (r *Runner) KeyDestroy(keyID int) (err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return r.failure + } + defer r.finishOperation("KeyDestroy", &err, true, func() {}) keyPtr, err := r.getKey(keyID) if err != nil { return err } - var errs []error for cacheKey, channelID := range r.channelsByKey { if cacheKey.keyID != keyID { continue @@ -691,19 +951,35 @@ func (r *Runner) KeyDestroy(keyID int) error { delete(r.channelsByKey, cacheKey) delete(r.channelStore, channelID) delete(r.goChannels, channelID) - errs = append(errs, r.destroyPanicSafe("E2EEChannel", r.rt.E2EEChannelDestroy, chanPtr)) + if err = r.rt.E2EEChannelDestroy(chanPtr); err != nil { + err = errors.Join(ErrRunnerClosed, fmt.Errorf("destroy group channel: %w", err)) + r.discard(err) + return err + } } delete(r.keyStore, keyID) delete(r.goKeys, keyID) - errs = append(errs, r.destroyPanicSafe("E2EEKey", r.rt.E2EEKeyDestroy, keyPtr)) - return errors.Join(errs...) + if err = r.rt.E2EEKeyDestroy(keyPtr); err != nil { + err = errors.Join(ErrRunnerClosed, fmt.Errorf("destroy group key: %w", err)) + r.discard(err) + return err + } + return nil } // ChannelEncryptV1 encrypts plaintext with channel V1 (AES-256-CBC + MAC). // Uses pure Go crypto when a Go channel is available. -func (r *Runner) ChannelEncryptV1(channelID int, plaintext string) (string, error) { +func (r *Runner) ChannelEncryptV1(channelID int, plaintext string) (out0 string, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return "", r.failure + } + defer r.finishOperation("ChannelEncryptV1", &err, false, func() { out0 = "" }) + return r.channelEncryptV1(channelID, plaintext) +} + +func (r *Runner) channelEncryptV1(channelID int, plaintext string) (string, error) { // Prefer pure Go channel if goChan, ok := r.goChannels[channelID]; ok { @@ -731,9 +1007,17 @@ func (r *Runner) ChannelEncryptV1(channelID int, plaintext string) (string, erro // ChannelEncryptV2 encrypts plaintext with channel v2. // Uses pure Go crypto when a Go channel is available (faster, and V2 works // correctly in Go whereas it fails in the WASM bridge due to SKB issues). -func (r *Runner) ChannelEncryptV2(channelID int, to, from string, senderKeyID, receiverKeyID, contentType, seq int, plaintext string) (string, error) { +func (r *Runner) ChannelEncryptV2(channelID int, to, from string, senderKeyID, receiverKeyID, contentType, seq int, plaintext string) (out0 string, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return "", r.failure + } + defer r.finishOperation("ChannelEncryptV2", &err, false, func() { out0 = "" }) + return r.channelEncryptV2(channelID, to, from, senderKeyID, receiverKeyID, contentType, seq, plaintext) +} + +func (r *Runner) channelEncryptV2(channelID int, to, from string, senderKeyID, receiverKeyID, contentType, seq int, plaintext string) (string, error) { // Prefer pure Go channel (V2 is broken in WASM due to SKB AAD failure) if goChan, ok := r.goChannels[channelID]; ok { @@ -761,9 +1045,20 @@ func (r *Runner) ChannelEncryptV2(channelID int, to, from string, senderKeyID, r // ChannelDecryptV1 decrypts ciphertext with channel v1 (ios). // Uses pure Go crypto when a Go channel is available. -func (r *Runner) ChannelDecryptV1(channelID, senderKeyID, receiverKeyID int, ciphertext string) (string, string, error) { +func (r *Runner) ChannelDecryptV1(channelID, senderKeyID, receiverKeyID int, ciphertext string) (out0 string, out1 string, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return "", "", r.failure + } + defer r.finishOperation("ChannelDecryptV1", &err, false, func() { + out0 = "" + out1 = "" + }) + return r.channelDecryptV1(channelID, senderKeyID, receiverKeyID, ciphertext) +} + +func (r *Runner) channelDecryptV1(channelID, senderKeyID, receiverKeyID int, ciphertext string) (string, string, error) { ctBytes, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { @@ -795,9 +1090,20 @@ func (r *Runner) ChannelDecryptV1(channelID, senderKeyID, receiverKeyID int, cip // ChannelDecryptV2 decrypts ciphertext with channel v2. // Uses pure Go crypto when a Go channel is available (V2 is broken in WASM). -func (r *Runner) ChannelDecryptV2(channelID int, to, from string, senderKeyID, receiverKeyID, contentType int, ciphertext string) (string, string, error) { +func (r *Runner) ChannelDecryptV2(channelID int, to, from string, senderKeyID, receiverKeyID, contentType int, ciphertext string) (out0 string, out1 string, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return "", "", r.failure + } + defer r.finishOperation("ChannelDecryptV2", &err, false, func() { + out0 = "" + out1 = "" + }) + return r.channelDecryptV2(channelID, to, from, senderKeyID, receiverKeyID, contentType, ciphertext) +} + +func (r *Runner) channelDecryptV2(channelID int, to, from string, senderKeyID, receiverKeyID, contentType int, ciphertext string) (string, string, error) { ctBytes, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { @@ -831,9 +1137,17 @@ func (r *Runner) ChannelDecryptV2(channelID int, to, from string, senderKeyID, r // GenerateE2EESecret generates a login secret with PIN and public key. // The Curve25519Key is used because GenerateConfirmHash and // LoginUnwrapKeyChain require the SKB-wrapped key for ECDH. -func (r *Runner) GenerateE2EESecret() (*SecretResult, error) { +func (r *Runner) GenerateE2EESecret() (out0 *SecretResult, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return nil, r.failure + } + defer r.finishOperation("GenerateE2EESecret", &err, true, func() { out0 = nil }) + return r.generateE2EESecret() +} + +func (r *Runner) generateE2EESecret() (*SecretResult, error) { ckPtr, err := r.rt.Curve25519KeyNew(r.skPtr) if err != nil { @@ -865,15 +1179,22 @@ func (r *Runner) GenerateE2EESecret() (*SecretResult, error) { // GenerateConfirmHash derives the hash key chain for confirmE2EELogin. // Must be called after GenerateE2EESecret. -func (r *Runner) GenerateConfirmHash(serverPublicKeyB64, encryptedKeyChainB64 string) (string, error) { +func (r *Runner) GenerateConfirmHash(serverPublicKeyB64, encryptedKeyChainB64 string) (out0 string, err error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.failure != nil { + return "", r.failure + } + defer r.finishOperation("GenerateConfirmHash", &err, true, func() { out0 = "" }) + return r.generateConfirmHash(serverPublicKeyB64, encryptedKeyChainB64) +} + +func (r *Runner) generateConfirmHash(serverPublicKeyB64, encryptedKeyChainB64 string) (string, error) { normalizedServerPub, err := normalizeServerPublicKeyB64(serverPublicKeyB64) if err != nil { return "", err } - r.mu.Lock() - defer r.mu.Unlock() - if r.loginCurveKey == 0 { return "", fmt.Errorf("login key not initialized") } @@ -903,9 +1224,17 @@ func (r *Runner) GenerateConfirmHash(serverPublicKeyB64, encryptedKeyChainB64 st // ReadChannelRaw returns 128 bytes of the channel C++ object's WASM linear memory. // Used for debugging the shared secret offset in the E2EEChannel/Curve25519KeyChannel object. -func (r *Runner) ReadChannelRaw(channelID int) ([]byte, error) { +func (r *Runner) ReadChannelRaw(channelID int) (out0 []byte, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return nil, r.failure + } + defer r.finishOperation("ReadChannelRaw", &err, true, func() { out0 = nil }) + return r.readChannelRaw(channelID) +} + +func (r *Runner) readChannelRaw(channelID int) ([]byte, error) { chanPtr, err := r.getChannel(channelID) if err != nil { @@ -927,9 +1256,42 @@ func (r *Runner) ReadChannelRaw(channelID int) ([]byte, error) { // (from KeyGenerate), not raw bytes. // // Returns base64-encoded ciphertext ready for registerE2EEGroupKey. -func (r *Runner) ChannelWrapGroupSharedKey(channelID int, keyID int) (string, error) { +func (r *Runner) ChannelWrapGroupSharedKey(channelID int, keyID int) (out0 string, err error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.failure != nil { + return "", r.failure + } + defer r.finishOperation("ChannelWrapGroupSharedKey", &err, false, func() { out0 = "" }) + return r.channelWrapGroupSharedKey(channelID, keyID) +} + +func (r *Runner) WrapRegistrationGroupKey(channelIDs []int) (out0 []string, err error) { r.mu.Lock() defer r.mu.Unlock() + if r.failure != nil { + return nil, r.failure + } + defer r.finishOperation("WrapRegistrationGroupKey", &err, false, func() { out0 = nil }) + chanPtrs := make([]uint32, len(channelIDs)) + for i, channelID := range channelIDs { + chanPtrs[i], err = r.getChannel(channelID) + if err != nil { + return nil, err + } + } + wrapped, err := r.rt.WrapRegistrationGroupKey(chanPtrs) + if err != nil { + return nil, err + } + out0 = make([]string, len(wrapped)) + for i, data := range wrapped { + out0[i] = base64.StdEncoding.EncodeToString(data) + } + return out0, nil +} + +func (r *Runner) channelWrapGroupSharedKey(channelID int, keyID int) (string, error) { chanPtr, err := r.getChannel(channelID) if err != nil { @@ -942,8 +1304,6 @@ func (r *Runner) ChannelWrapGroupSharedKey(channelID int, keyID int) (string, er } // Use the WASM module's wrapGroupSharedKey directly (same as LINE Extension). - // keyPtr is an emval handle wrapping the Curve25519Key C++ object. - // The C++ method expects an emscripten::val, which the emval handle represents. wrapped, err := r.rt.E2EEChannelWrapGroupSharedKey(chanPtr, keyPtr) if err != nil { return "", err