본문 바로가기

카테고리 없음

ee

# QR 이미지에서 Google OTP 정보 추출하기

# 필요한 라이브러리 설치
# pip install opencv-python pyzbar pillow

import cv2
from pyzbar import pyzbar
import urllib.parse

def extract_otp_from_image(image_path):
    """
    이미지 파일에서 QR 코드를 읽어 OTP 정보 추출
    """
    try:
        print(f"이미지 분석 중: {image_path}")
        
        # 파일 존재 확인
        import os
        if not os.path.exists(image_path):
            print("❌ 파일이 존재하지 않습니다.")
            return
        
        # 여러 방법으로 이미지 읽기 시도
        image = None
        
        # 방법 1: OpenCV로 읽기
        try:
            image = cv2.imread(image_path)
        except:
            pass
            
        # 방법 2: PIL로 읽고 OpenCV 형식으로 변환
        if image is None:
            try:
                from PIL import Image
                import numpy as np
                pil_image = Image.open(image_path)
                # RGB to BGR 변환 (OpenCV 형식)
                image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
                print("✅ PIL로 이미지 읽기 성공")
            except Exception as e:
                print(f"PIL 읽기 실패: {e}")
        
        if image is None:
            print("❌ 이미지를 읽을 수 없습니다.")
            print("지원 형식: PNG, JPG, JPEG, BMP, GIF")
            return
        
        print(f"✅ 이미지 읽기 성공 (크기: {image.shape[1]}x{image.shape[0]})")
        
        # QR 코드 디코딩
        qr_codes = pyzbar.decode(image)
        
        if not qr_codes:
            print("❌ QR 코드를 찾을 수 없습니다.")
            print("💡 팁: QR 코드가 선명하고 전체가 보이는지 확인하세요.")
            return
        
        print(f"✅ {len(qr_codes)}개의 QR 코드 발견!")
        
        for i, qr_code in enumerate(qr_codes):
            print(f"\n--- QR 코드 #{i+1} ---")
            # QR 코드 데이터 추출
            qr_data = qr_code.data.decode('utf-8')
            
            # Google OTP 형식인지 확인
            if qr_data.startswith('otpauth://'):
                parse_otp_uri(qr_data)
            else:
                print("❌ Google OTP 형식이 아닙니다.")
                print(f"데이터: {qr_data[:100]}...")  # 처음 100자만 표시
                
    except Exception as e:
        print(f"❌ 오류 발생: {e}")
        print(f"오류 타입: {type(e).__name__}")
        import traceback
        traceback.print_exc()

def parse_otp_uri(uri):
    """
    OTP URI를 파싱하여 필요한 정보 추출
    """
    print(f"\n📱 Google OTP 정보 추출 완료!")
    print(f"원본 URI: {uri}")
    print(f"URI 길이: {len(uri)} 문자")
    
    # URI 파싱
    parsed = urllib.parse.urlparse(uri)
    params = urllib.parse.parse_qs(parsed.query)
    
    # 계정 정보 추출
    account_info = parsed.path.lstrip('/')
    
    print(f"\n" + "="*60)
    print(f"🔐 추출된 OTP 정보 상세 분석")
    print(f"="*60)
    
    print(f"📋 계정: {account_info}")
    print(f"🔗 URI 스키마: {parsed.scheme}")
    print(f"🔗 URI 호스트: {parsed.netloc}")
    print(f"🔗 URI 경로: {parsed.path}")
    print(f"🔗 URI 쿼리: {parsed.query}")
    
    # 모든 파라미터 출력
    print(f"\n📊 모든 파라미터:")
    for key, value in params.items():
        print(f"  {key}: {value[0] if value else 'None'}")
    
    if 'secret' in params:
        secret = params['secret'][0]
        
        print(f"\n🔑 Secret Key 상세 분석:")
        print(f"  원본 키: '{secret}'")
        print(f"  키 길이: {len(secret)} 문자")
        print(f"  키 타입: {type(secret)}")
        
        # 문자별 분석
        print(f"  문자 구성: {list(secret)}")
        
        # 대소문자 확인
        has_upper = any(c.isupper() for c in secret)
        has_lower = any(c.islower() for c in secret)
        has_digit = any(c.isdigit() for c in secret)
        has_special = any(not c.isalnum() for c in secret)
        
        print(f"  대문자 포함: {has_upper}")
        print(f"  소문자 포함: {has_lower}")
        print(f"  숫자 포함: {has_digit}")
        print(f"  특수문자 포함: {has_special}")
        
        # Base32 유효 문자 확인
        base32_chars = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')
        secret_upper = secret.upper()
        invalid_chars = [c for c in secret_upper if c not in base32_chars]
        
        if invalid_chars:
            print(f"  ❌ Base32에 없는 문자: {invalid_chars}")
        else:
            print(f"  ✅ 모든 문자가 Base32 유효")
        
        # Base32 패딩 확인
        padding_needed = 8 - (len(secret) % 8)
        if padding_needed == 8:
            padding_needed = 0
        print(f"  Base32 패딩 필요: {padding_needed}개의 '='")
        
        # Base32 디코딩 시도
        try:
            import base64
            padded_secret = secret_upper + '=' * padding_needed
            decoded = base64.b32decode(padded_secret)
            print(f"  ✅ Base32 디코딩 성공")
            print(f"  디코딩된 바이트 수: {len(decoded)}")
            print(f"  디코딩된 비트 수: {len(decoded) * 8}")
            print(f"  16진수: {decoded.hex().upper()}")
            
        except Exception as e:
            print(f"  ❌ Base32 디코딩 실패: {e}")
        
        # 키 길이 기준 분석
        print(f"\n📏 키 길이 표준 분석:")
        print(f"  현재 길이: {len(secret)} 문자")
        print(f"  RFC 4226 최소 요구: 26 문자 (128비트)")
        print(f"  RFC 4226 권장: 32 문자 (160비트)")
        print(f"  Google Auth 최소: 16 문자")
        print(f"  실제 비트 수: {len(secret) * 5} 비트 (Base32)")
        
        if len(secret) < 16:
            print(f"  ❌ Google Authenticator 호환 불가")
            print(f"  💡 이는 QR 코드 생성 측의 문제일 수 있습니다")
            print(f"  💡 일부 서비스는 짧은 키를 사용하지만 표준 위반입니다")
        elif len(secret) < 26:
            print(f"  ⚠️  RFC 표준 미만이지만 일부 앱에서 작동할 수 있음")
        else:
            print(f"  ✅ 표준 준수")
            
        # 공백 제거 및 정리
        clean_secret = secret.replace(' ', '').replace('-', '').upper()
        if clean_secret != secret:
            print(f"\n🧹 정리된 키: {clean_secret}")
            secret = clean_secret
        
        print(f"⚠️  이 키는 절대 타인에게 노출하지 마세요!")
        
        # QR 코드 자체 문제 진단
        if len(secret) < 16:
            print(f"\n🔍 QR 코드 진단:")
            print(f"  - 해당 QR 코드는 실제로 {len(secret)}자 키를 포함")
            print(f"  - 이는 이미지 해상도 문제가 아님")
            print(f"  - QR 코드 생성 서비스에서 짧은 키를 사용함")
            print(f"  - 표준을 준수하지 않는 구현일 가능성")
            
    else:
        print(f"❌ Secret 키를 찾을 수 없습니다!")
        return
    
    # 나머지 파라미터들
    issuer = params.get('issuer', ['Unknown'])[0]
    algorithm = params.get('algorithm', ['SHA1'])[0]
    digits = params.get('digits', ['6'])[0]
    period = params.get('period', ['30'])[0]
    
    print(f"\n🏢 발급자: {issuer}")
    print(f"🔧 알고리즘: {algorithm}")
    print(f"🔢 자릿수: {digits}")
    print(f"⏰ 갱신주기: {period}초")
    
    # Google Authenticator 수동 입력 시도
    print(f"\n" + "="*60)
    print(f"📱 Google Authenticator 수동 입력 시도")
    print(f"="*60)
    
    if ':' in account_info:
        service, user = account_info.split(':', 1)
        print(f"서비스: {service}")
        print(f"계정: {user}")
    else:
        print(f"계정 이름: {account_info}")
    
    print(f"키: {secret}")
    print(f"시간 기준: 예")
    print(f"유형: TOTP")
    
    if len(secret) < 16:
        print(f"\n❌ 경고: 이 키는 Google Authenticator에서 거부될 가능성이 높습니다")
        print(f"💡 대안:")
        print(f"   1. 다른 OTP 앱 사용 (Authy, Microsoft Authenticator 등)")
        print(f"   2. 서비스 제공자에게 문의하여 더 긴 키 요청")
        print(f"   3. 서비스에서 다시 QR 코드 생성")
    else:
        print(f"\n✅ 이 키는 Google Authenticator에서 정상 작동할 것입니다")
    
    print(f"\n✅ 분석 완료!")

# 간단 실행
if __name__ == "__main__":
    print("🔍 Google OTP QR 코드 분석기")
    print("-" * 30)
    
    # 파일 선택 대화상자
    try:
        import tkinter as tk
        from tkinter import filedialog
        
        # tkinter 창 숨기기
        root = tk.Tk()
        root.withdraw()
        root.attributes('-topmost', True)  # 창을 맨 앞으로
        
        print("📁 파일 선택 창이 열립니다...")
        
        # 파일 선택
        image_path = filedialog.askopenfilename(
            title="QR 코드 이미지를 선택하세요",
            filetypes=[
                ("이미지 파일", "*.png *.jpg *.jpeg *.bmp *.gif *.webp"),
                ("PNG 파일", "*.png"),
                ("JPG 파일", "*.jpg *.jpeg"), 
                ("모든 파일", "*.*")
            ]
        )
        
        root.destroy()
        
        if image_path:
            print(f"선택된 파일: {image_path}")
            extract_otp_from_image(image_path)
        else:
            print("❌ 파일이 선택되지 않았습니다.")
            
    except ImportError:
        # tkinter 없으면 기존 방식
        print("파일 선택 창을 사용할 수 없습니다. 직접 입력하세요.")
        image_path = input("QR 코드 이미지 파일 경로: ").strip().strip('"')
        
        if image_path:
            extract_otp_from_image(image_path)
        else:
            print("❌ 파일 경로가 입력되지 않았습니다.")