# 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("❌ 파일 경로가 입력되지 않았습니다.")카테고리 없음