=0&&t>0;)e[--t]=n[r--];e[--t]=0;for(var l=new SecureRandom,i=new Array;t>2;){for(i[0]=0;0==i[0];)l.nextBytes(i);e[--t]=i[0]}return e[--t]=2,e[--t]=0,new BigInteger(e)}function RSAKey(){this.n=null,this.e=0,this.d=null,this.p=null,this.q=null,this.dmp1=null,this.dmq1=null,this.coeff=null}function RSASetPublic(n,t){null!=n&&null!=t&&n.length>0&&t.length>0?(this.n=parseBigInt(n,16),this.e=parseInt(t,16)):alert("Invalid RSA public key")}function RSADoPublic(n){return n.modPowInt(this.e,this.n)}function RSAEncrypt(n){var t=pkcs1pad2(n,this.n.bitLength()+7>>3);if(null==t)return null;var e=this.doPublic(t);if(null==e)return null;var r=e.toString(16);return 0==(1&r.length)?r:"0"+r}RSAKey.prototype.doPublic=RSADoPublic,RSAKey.prototype.setPublic=RSASetPublic,RSAKey.prototype.encrypt=RSAEncrypt;
-
-export default RSAKey
diff --git a/matrix_appservice_kakaotalk/web/static/lib/spinner.css b/matrix_appservice_kakaotalk/web/static/lib/spinner.css
deleted file mode 100644
index ca0e335..0000000
--- a/matrix_appservice_kakaotalk/web/static/lib/spinner.css
+++ /dev/null
@@ -1,80 +0,0 @@
-.loader {
- color: #9b4dca;
- font-size: 90px;
- text-indent: -9999em;
- overflow: hidden;
- width: 1em;
- height: 1em;
- border-radius: 50%;
- margin: 72px auto;
- position: relative;
- -webkit-transform: translateZ(0);
- -ms-transform: translateZ(0);
- transform: translateZ(0);
- -webkit-animation: load6 1.7s infinite ease, round 1.7s infinite ease;
- animation: load6 1.7s infinite ease, round 1.7s infinite ease;
-}
-@-webkit-keyframes load6 {
- 0% {
- box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
- }
- 5%,
- 95% {
- box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
- }
- 10%,
- 59% {
- box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em, -0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em;
- }
- 20% {
- box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, -0.555em -0.617em 0 -0.44em, -0.671em -0.488em 0 -0.46em, -0.749em -0.34em 0 -0.477em;
- }
- 38% {
- box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, -0.645em -0.522em 0 -0.44em, -0.775em -0.297em 0 -0.46em, -0.82em -0.09em 0 -0.477em;
- }
- 100% {
- box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
- }
-}
-@keyframes load6 {
- 0% {
- box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
- }
- 5%,
- 95% {
- box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
- }
- 10%,
- 59% {
- box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em, -0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em;
- }
- 20% {
- box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, -0.555em -0.617em 0 -0.44em, -0.671em -0.488em 0 -0.46em, -0.749em -0.34em 0 -0.477em;
- }
- 38% {
- box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, -0.645em -0.522em 0 -0.44em, -0.775em -0.297em 0 -0.46em, -0.82em -0.09em 0 -0.477em;
- }
- 100% {
- box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
- }
-}
-@-webkit-keyframes round {
- 0% {
- -webkit-transform: rotate(0deg);
- transform: rotate(0deg);
- }
- 100% {
- -webkit-transform: rotate(360deg);
- transform: rotate(360deg);
- }
-}
-@keyframes round {
- 0% {
- -webkit-transform: rotate(0deg);
- transform: rotate(0deg);
- }
- 100% {
- -webkit-transform: rotate(360deg);
- transform: rotate(360deg);
- }
-}
diff --git a/matrix_appservice_kakaotalk/web/static/login.html b/matrix_appservice_kakaotalk/web/static/login.html
deleted file mode 100644
index 0a9b902..0000000
--- a/matrix_appservice_kakaotalk/web/static/login.html
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
- matrix-appservice-kakaotalk login
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/matrix_appservice_kakaotalk/web/static/login/api.js b/matrix_appservice_kakaotalk/web/static/login/api.js
deleted file mode 100644
index ee9ad19..0000000
--- a/matrix_appservice_kakaotalk/web/static/login/api.js
+++ /dev/null
@@ -1,62 +0,0 @@
-// matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge.
-// Copyright (C) 2021 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-import encryptPassword from "./crypto.js"
-
-const apiToken = location.hash.slice(1)
-const headers = { Authorization: `Bearer ${apiToken}` }
-const jsonHeaders = { ...headers, "Content-Type": "application/json" }
-const fetchParams = { headers }
-
-export async function whoami() {
- const resp = await fetch("api/whoami", fetchParams)
- return await resp.json()
-}
-
-export async function prepareLogin() {
- const resp = await fetch("api/login/prepare", { ...fetchParams, method: "POST" })
- return await resp.json()
-}
-
-export async function login(pubkey, keyID, email, password) {
- const resp = await fetch("api/login", {
- method: "POST",
- body: JSON.stringify({
- email,
- encrypted_password: await encryptPassword(pubkey, keyID, password),
- }),
- headers: jsonHeaders,
- })
- return await resp.json()
-}
-
-export async function login2FA(email, code) {
- const resp = await fetch("api/login/2fa", {
- method: "POST",
- body: JSON.stringify({ email, code }),
- headers: jsonHeaders,
- })
- return await resp.json()
-}
-
-export async function loginApproved() {
- const resp = await fetch("api/login/approved", { method: "POST", headers })
- return await resp.json()
-}
-
-export async function wasLoginApproved() {
- const resp = await fetch("api/login/check_approved", fetchParams)
- return (await resp.json()).approved
-}
diff --git a/matrix_appservice_kakaotalk/web/static/login/app.js b/matrix_appservice_kakaotalk/web/static/login/app.js
deleted file mode 100644
index 69afb55..0000000
--- a/matrix_appservice_kakaotalk/web/static/login/app.js
+++ /dev/null
@@ -1,200 +0,0 @@
-// matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge.
-// Copyright (C) 2021 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-import { h, Component, render } from "../lib/preact-10.5.12.min.js"
-import htm from "../lib/htm-3.0.4.min.js"
-import * as api from "./api.js"
-
-const html = htm.bind(h)
-
-class App extends Component {
- constructor(props) {
- super(props)
- this.approveCheckInterval = null
- this.state = {
- loading: true,
- submitting: false,
- error: null,
- mxid: null,
- facebook: null,
- status: "pre-login",
- pubkey: null,
- keyID: null,
- email: "",
- password: "",
- twoFactorCode: "",
- twoFactorInfo: {},
- }
- }
-
- async componentDidMount() {
- const { error, mxid, facebook } = await api.whoami()
- if (error) {
- this.setState({ error, loading: false })
- } else {
- this.setState({ mxid, facebook, loading: false })
- }
- }
-
- checkLoginApproved = async () => {
- if (!await api.wasLoginApproved()) {
- return
- }
- clearInterval(this.approveCheckInterval)
- this.approveCheckInterval = null
- const resp = await api.loginApproved()
- if (resp.status === "logged-in") {
- this.setState({ status: resp.status })
- }
- }
-
- submitNoDefault = evt => {
- evt.preventDefault()
- this.submit()
- }
-
- async submit() {
- if (this.approveCheckInterval) {
- clearInterval(this.approveCheckInterval)
- this.approveCheckInterval = null
- }
- this.setState({ submitting: true })
- let resp
- switch (this.state.status) {
- case "pre-login":
- resp = await api.prepareLogin()
- break
- case "login":
- resp = await api.login(this.state.pubkey, this.state.keyID,
- this.state.email, this.state.password)
- break
- case "two-factor":
- resp = await api.login2FA(this.state.email, this.state.twoFactorCode)
- break
- }
- const stateUpdate = { submitting: false }
- if (typeof resp.error === "string") {
- stateUpdate.error = resp.error
- } else {
- stateUpdate.status = resp.status
- }
- if (resp.password_encryption_key_id) {
- stateUpdate.pubkey = resp.password_encryption_pubkey
- stateUpdate.keyID = resp.password_encryption_key_id
- }
- if (resp.status === "two-factor") {
- this.approveCheckInterval = setInterval(this.checkLoginApproved, 5000)
- stateUpdate.twoFactorInfo = resp.error
- } else if (resp.status === "logged-in") {
- api.whoami().then(({ facebook }) => this.setState({ facebook }))
- }
- this.setState(stateUpdate)
- }
-
- fieldChange = evt => {
- this.setState({ [evt.target.id]: evt.target.value })
- }
-
- renderFields() {
- switch (this.state.status) {
- case "pre-login":
- return null
- case "login":
- return html`
-
-
-
-
- `
- case "two-factor":
- return html`
- ${this.state.twoFactorInfo.error_user_msg}
-
-
-
-
- `
- }
- }
-
- submitButtonText() {
- switch (this.state.status) {
- case "pre-login":
- return "Start"
- case "login":
- case "two-factor":
- return "Sign in"
- }
- }
-
- renderContent() {
- if (this.state.loading) {
- return html`
- Loading...
- `
- } else if (this.state.status === "logged-in") {
- if (this.state.facebook) {
- return html`
- Successfully logged in as ${this.state.facebook.name}. The bridge will appear
- as ${this.state.facebook.device_displayname} in Facebook security settings.
- `
- }
- return html`
- Successfully logged in
- `
- } else if (this.state.facebook) {
- return html`
- You're already logged in as ${this.state.facebook.name}. The bridge appears
- as ${this.state.facebook.device_displayname} in Facebook security settings.
- `
- }
- return html`
- ${this.state.error && html`
- ${this.state.error}
- `}
-
- `
- }
-
- render() {
- return html`
-
- matrix-appservice-kakaotalk login
- ${this.renderContent()}
-
- `
- }
-}
-
-
-render(html`
- <${App}/>
-`, document.body)
diff --git a/matrix_appservice_kakaotalk/web/static/login/crypto.js b/matrix_appservice_kakaotalk/web/static/login/crypto.js
deleted file mode 100644
index 6b0262d..0000000
--- a/matrix_appservice_kakaotalk/web/static/login/crypto.js
+++ /dev/null
@@ -1,112 +0,0 @@
-// matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge.
-// Copyright (C) 2021 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-// We have to use this pure-js RSA implementation because SubtleCrypto dropped PKCS#1 v1.5 support.
-import RSAKey from "../lib/rsa.min.js"
-import ASN1HEX from "../lib/asn1hex-1.1.min.js"
-
-function pemToHex(pem) {
- // Strip pem header
- pem = pem.replace("-----BEGIN PUBLIC KEY-----", "")
- pem = pem.replace("-----END PUBLIC KEY-----", "")
-
- // Convert base64 to hex
- const raw = atob(pem)
- let result = ""
- for (let i = 0; i < raw.length; i++) {
- const hex = raw.charCodeAt(i).toString(16)
- result += (hex.length === 2 ? hex : "0" + hex)
- }
- return result.toLowerCase()
-}
-
-function getKey(pem) {
- const keyHex = pemToHex(pem)
- if (ASN1HEX.isASN1HEX(keyHex) === false) {
- throw new Error("key is not ASN.1 hex string")
- } else if (ASN1HEX.getVbyList(keyHex, 0, [0, 0], "06") !== "2a864886f70d010101") {
- throw new Error("not PKCS8 RSA key")
- } else if (ASN1HEX.getTLVbyListEx(keyHex, 0, [0, 0]) !== "06092a864886f70d010101") {
- throw new Error("not PKCS8 RSA public key")
- }
-
- const p5hex = ASN1HEX.getTLVbyListEx(keyHex, 0, [1, 0])
- if (ASN1HEX.isASN1HEX(p5hex) === false) {
- throw new Error("keyHex is not ASN.1 hex string")
- }
-
- const aIdx = ASN1HEX.getChildIdx(p5hex, 0)
- if (aIdx.length !== 2 || p5hex.substr(aIdx[0], 2) !== "02" || p5hex.substr(aIdx[1], 2) !== "02") {
- throw new Error("wrong hex for PKCS#5 public key")
- }
-
- const hN = ASN1HEX.getV(p5hex, aIdx[0])
- const hE = ASN1HEX.getV(p5hex, aIdx[1])
- const key = new RSAKey()
- key.setPublic(hN, hE)
- return key
-}
-
-// encryptPassword encrypts a login password using AES-256-GCM, then encrypts the AES key
-// for Facebook's RSA-2048 key using PKCS#1 v1.5 padding.
-//
-// See https://github.com/mautrix/facebook/blob/v0.3.0/maufbapi/http/login.py#L164-L192
-// for the Python implementation of the same encryption protocol.
-async function encryptPassword(pubkey, keyID, password) {
- // Key and IV for AES encryption
- const aesKey = await crypto.subtle.generateKey({
- name: "AES-GCM",
- length: 256,
- }, true, ["encrypt", "decrypt"])
- const aesIV = crypto.getRandomValues(new Uint8Array(12))
- // Get the actual bytes of the AES key
- const aesKeyBytes = await crypto.subtle.exportKey("raw", aesKey)
-
- // Encrypt AES key with Facebook's RSA public key.
- const rsaKey = getKey(pubkey)
- const encryptedAESKeyHex = rsaKey.encrypt(new Uint8Array(aesKeyBytes))
- const encryptedAESKey = new Uint8Array(encryptedAESKeyHex.match(/[0-9A-Fa-f]{2}/g).map(h => parseInt(h, 16)))
-
- const encoder = new TextEncoder()
- const time = Math.floor(Date.now() / 1000)
- // Encrypt the password. The result includes the ciphertext and AES MAC auth tag.
- const encryptedPasswordBuffer = await crypto.subtle.encrypt({
- name: "AES-GCM",
- iv: aesIV,
- // Add the current time to the additional authenticated data (AAD) section
- additionalData: encoder.encode(time.toString()),
- tagLength: 128,
- }, aesKey, encoder.encode(password))
- // SubtleCrypto returns the auth tag and ciphertext in the wrong order,
- // so we have to flip them around.
- const authTag = new Uint8Array(encryptedPasswordBuffer.slice(-16))
- const encryptedPassword = new Uint8Array(encryptedPasswordBuffer.slice(0, -16))
-
- const payload = new Uint8Array(2 + aesIV.byteLength + 2 + encryptedAESKey.byteLength + authTag.byteLength + encryptedPassword.byteLength)
- // 1 is presumably the version
- payload[0] = 1
- payload[1] = keyID
- payload.set(aesIV, 2)
- // Length of the encrypted AES key as a little-endian 16-bit int
- payload[aesIV.byteLength + 2] = encryptedAESKey.byteLength & (1 << 8)
- payload[aesIV.byteLength + 3] = encryptedAESKey.byteLength >> 8
- payload.set(encryptedAESKey, 4 + aesIV.byteLength)
- payload.set(authTag, 4 + aesIV.byteLength + encryptedAESKey.byteLength)
- payload.set(encryptedPassword, 4 + aesIV.byteLength + encryptedAESKey.byteLength + authTag.byteLength)
- return `#PWD_MSGR:1:${time}:${btoa(String.fromCharCode(...payload))}`
-}
-
-export default encryptPassword
diff --git a/matrix_appservice_kakaotalk/web/static/login/index.css b/matrix_appservice_kakaotalk/web/static/login/index.css
deleted file mode 100644
index efef17d..0000000
--- a/matrix_appservice_kakaotalk/web/static/login/index.css
+++ /dev/null
@@ -1,10 +0,0 @@
-.error {
- background-color: darkred !important;
- border-color: darkred !important;
- opacity: 1 !important;
-}
-
-main {
- max-width: 50rem;
- margin: 2rem auto 0;
-}