5월 26, 2026

#Dev_9 개인적인 웹툰 뷰어_feat.Kavita

koofr를 연동하며 1TB의 용량이 홈서버에 생겼다.

그래서 가지고있던 웹툰을 올려서 웹으로 보기위해 Kavita를 설치했는데 파싱 속도가 너무 느려서… 제거

결국 flask로 직접 만들기로 결정. 현재 잘 사용 중이다.

웹툰을 웹에 공개하게 되면 저작권에 걸리기 때문에 암호화가 필요했다.

처음엔 계정을 만들어 session을 부여하여 사용했지만 이마져도 어차피 Cloudflare Tunnel를 쓰면 해당 업체에서 볼 수 있다고 하니 혹시 몰라 결국 AES256을 사용하여 내용을 암호화 하기로 했다.

flask에서 AES256으로 암호화후 내용을 HTML(Client)에 보내면 사용자가 입력한 코드로 복호화하여 화면에 표시해주는 방식이다.

첫 로그인 화면부터 32자리의 키를 입력하게 해두었고. 키를 입력하면 flask에 암호화된 로그인창을 받아 복호화후 표시해주도록 함.

try {
                const response = await fetch('/real_login');
                if (!response.ok) throw new Error("서버 응답 오류");
                
                let encryptedData = await response.text();
				encryptedData = encryptedData.replace(/-/g, '+').replace(/_/g, '/');
                const ciphertextWithIv = CryptoJS.enc.Base64.parse(encryptedData);
                const iv = CryptoJS.lib.WordArray.create(ciphertextWithIv.words.slice(0, 4));
                const ciphertext = CryptoJS.lib.WordArray.create(ciphertextWithIv.words.slice(4));
                const key = CryptoJS.SHA256(password);

                const decrypted = CryptoJS.AES.decrypt(
                    { ciphertext: ciphertext },
                    key,
                    { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
                );

                const originalText = decrypted.toString(CryptoJS.enc.Utf8);

                if (originalText && originalText.length > 0) {
                    setCookie('master_key_cookie', password, 7);
                    localStorage.setItem('master_key', password);
                    
                    document.open();
                    document.write(originalText);
                    document.close();
                } else {
                    msg.style.color = "#ff5252";
                    msg.innerText = "키가 올바르지 않습니다.";
                    if(isAuto) setCookie('master_key_cookie', '', -1);
                }
            } catch (e) {
                msg.style.color = "#ff5252";
                msg.innerText = "연결 실패 또는 복호화 오류";
            }
# --- AES256 ---
def encrypt_for_js(data, password=aesPW, safe=True):
    """
    Flask의 render_template(문자열)이나 일반 바이트 데이터를
    자바스크립트 CryptoJS와 호환되도록 AES-256-CBC로 암호화합니다.
    """
    try:
        # 1. 입력 데이터 처리: 문자열인 경우 UTF-8 바이트로 변환
        if isinstance(data, str):
            data = data.encode('utf-8')

        # 2. 32바이트 키 생성 (SHA-256)
        key = hashlib.sha256(password.encode()).digest()

        # 3. 무작위 IV 생성 (16바이트)
        iv = get_random_bytes(16)

        # 4. AES-256-CBC 암호화 설정
        cipher = AES.new(key, AES.MODE_CBC, iv)

        # 5. 패딩 및 암호화 실행
        ct_bytes = cipher.encrypt(pad(data, AES.block_size))

        # 6. [IV(16바이트) + 암호문] 결합 후 Base64 인코딩
        # JS의 CryptoJS.enc.Base64.parse()에서 바로 읽을 수 있는 형태입니다.
        encoded_result = ''
        if safe:
            encoded_result = base64.urlsafe_b64encode(iv + ct_bytes).decode('utf-8')
        else:
            encoded_result = base64.b64encode(iv + ct_bytes).decode('utf-8')

        return encoded_result

    except Exception as e:
        print(f"Encryption Error: {e}")
        return None

이런식으로 real_login을 요청하여 받은 암호화 배열을 복호화 하여 페이지를 교체하여 실제 로그인 페이지를 표시 이후 로그인을 진행한다.

이후 모든 페이지를 동일한 AES256으로 암호화후 클라이언트에 전달 클라이언트에서 복호화후 표시해주는 방식으로 전부 개선.

이미지도 AES256으로 데이터 암호화후 전달하여 client에서 복호화후 blob로 이미지 표시 하도록 개선하여 중간에

Cloudflare Tunnel가 들여다 보아도 암호문만 보이도록 코드 전체를 수정함.

구성하다보니 ai는 이게 종단간암호화 라고 하였다.

아이폰에서 홈화면에 추가하여 캐시 기능까지 곁들여 알차게 사용중이다.

미리 다음화 로딩까지 넣어서 로딩이 거의 없다.