roseluo commited on
Commit
88b2f48
·
verified ·
1 Parent(s): 031c59e

Upload 5 files

Browse files
Files changed (6) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +22 -0
  3. core.cpython-311-x86_64-linux-gnu.so +3 -0
  4. main.py +172 -0
  5. packages.txt +1 -0
  6. pdf_utils.py +100 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ core.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Install system dependencies
4
+ RUN apt-get update && apt-get install -y \
5
+ poppler-utils \
6
+ gcc \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ # Set working directory
10
+ WORKDIR /app
11
+
12
+ # Copy all files
13
+ COPY . .
14
+
15
+ # Install Python dependencies
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ # Expose port
19
+ EXPOSE 7860
20
+
21
+ # Start the API server
22
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
core.cpython-311-x86_64-linux-gnu.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b4195d6afbb0e7a41ceda99d2a1d623a53d4e0c652c605f81d753c159988c137
3
+ size 1014592
main.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ WaveSign API — Private signing and verification service.
3
+ """
4
+
5
+ import io
6
+ import json
7
+ import os
8
+ import zipfile
9
+
10
+ from fastapi import FastAPI, Form, Request, UploadFile
11
+ from fastapi.responses import JSONResponse, StreamingResponse
12
+
13
+ from core import embed_watermark, sign_image, verify_image, detect_mode
14
+ from pdf_utils import sign_pdf, verify_pdf
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # App setup
18
+ # ---------------------------------------------------------------------------
19
+
20
+ app = FastAPI(title="WaveSign API", docs_url=None, redoc_url=None)
21
+
22
+ # API key loaded once at startup
23
+ _API_KEY: str = os.environ.get("WS_API_KEY", "")
24
+
25
+ # Routes that do not require authentication
26
+ _PUBLIC_ROUTES = {"/health"}
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Auth middleware
31
+ # ---------------------------------------------------------------------------
32
+
33
+ @app.middleware("http")
34
+ async def require_api_key(request: Request, call_next):
35
+ if request.url.path in _PUBLIC_ROUTES:
36
+ return await call_next(request)
37
+
38
+ auth_header = request.headers.get("Authorization", "")
39
+ if not auth_header.startswith("Bearer "):
40
+ return JSONResponse(
41
+ status_code=401,
42
+ content={"detail": "Missing or malformed Authorization header."},
43
+ )
44
+ token = auth_header[len("Bearer "):]
45
+ if not _API_KEY or token != _API_KEY:
46
+ return JSONResponse(
47
+ status_code=401,
48
+ content={"detail": "Invalid API key."},
49
+ )
50
+ return await call_next(request)
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Helpers
55
+ # ---------------------------------------------------------------------------
56
+
57
+ def _is_pdf(filename: str) -> bool:
58
+ return filename.lower().endswith(".pdf")
59
+
60
+
61
+ def _build_zip(files: dict) -> io.BytesIO:
62
+ """
63
+ Build an in-memory ZIP archive.
64
+ files: {archive_name: bytes}
65
+ """
66
+ buf = io.BytesIO()
67
+ with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
68
+ for name, data in files.items():
69
+ zf.writestr(name, data)
70
+ buf.seek(0)
71
+ return buf
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Endpoints
76
+ # ---------------------------------------------------------------------------
77
+
78
+ @app.get("/health")
79
+ async def health():
80
+ return {"status": "ok", "service": "WaveSign API"}
81
+
82
+
83
+ @app.post("/sign")
84
+ async def sign(
85
+ file: UploadFile,
86
+ key: str = Form(),
87
+ ):
88
+ try:
89
+ raw = await file.read()
90
+ filename = file.filename or "upload"
91
+
92
+ if _is_pdf(filename):
93
+ # PDF path
94
+ signed_pdf_bytes, sigs, _ = sign_pdf(raw, secret=key)
95
+ sig_json = json.dumps(sigs, indent=2).encode()
96
+ archive = _build_zip({"signed.pdf": signed_pdf_bytes, "sig.json": sig_json})
97
+ else:
98
+ # Image path
99
+ from PIL import Image
100
+ img = Image.open(io.BytesIO(raw))
101
+ mode = detect_mode(img)
102
+ wm_img = embed_watermark(img, key, mode=mode)
103
+ sig = sign_image(wm_img, key, mode=mode)
104
+
105
+ out_buf = io.BytesIO()
106
+ wm_img.save(out_buf, format="PNG")
107
+ signed_png = out_buf.getvalue()
108
+
109
+ sig_json = json.dumps(sig, indent=2).encode()
110
+ archive = _build_zip({"signed.png": signed_png, "sig.json": sig_json})
111
+
112
+ return StreamingResponse(
113
+ archive,
114
+ media_type="application/zip",
115
+ headers={"Content-Disposition": 'attachment; filename="wavesign_output.zip"'},
116
+ )
117
+
118
+ except Exception as exc:
119
+ return JSONResponse(status_code=400, content={"detail": str(exc)})
120
+
121
+
122
+ @app.post("/verify")
123
+ async def verify(
124
+ file: UploadFile,
125
+ sig_file: UploadFile,
126
+ key: str = Form(),
127
+ ):
128
+ try:
129
+ raw = await file.read()
130
+ sig_raw = await sig_file.read()
131
+ filename = file.filename or "upload"
132
+
133
+ sig_data = json.loads(sig_raw.decode())
134
+
135
+ if _is_pdf(filename):
136
+ # sig_data is a list of per-page signature dicts
137
+ per_page = verify_pdf(raw, secret=key, signatures=sig_data)
138
+ overall = all(p.get("is_valid", False) for p in per_page)
139
+ verdict = "AUTHENTIC" if overall else "TAMPERED or INVALID KEY"
140
+
141
+ # Normalise per-page entries to use consistent field names
142
+ normalised = []
143
+ for p in per_page:
144
+ normalised.append({
145
+ "page_index": p.get("page_index"),
146
+ "is_valid": p.get("is_valid", False),
147
+ "verdict": "AUTHENTIC" if p.get("is_valid", False) else "TAMPERED or INVALID KEY",
148
+ "similarity_score": p.get("similarity_score"),
149
+ "mode": p.get("mode"),
150
+ })
151
+
152
+ return JSONResponse({
153
+ "is_valid": overall,
154
+ "verdict": verdict,
155
+ "pages": normalised,
156
+ })
157
+
158
+ else:
159
+ # Image path — sig_data is a single signature dict
160
+ from PIL import Image
161
+ img = Image.open(io.BytesIO(raw))
162
+ result = verify_image(img, key, sig_data)
163
+ is_valid = result.get("is_valid", False)
164
+ return JSONResponse({
165
+ "is_valid": is_valid,
166
+ "verdict": "AUTHENTIC" if is_valid else "TAMPERED or INVALID KEY",
167
+ "similarity_score": result.get("similarity_score"),
168
+ "mode": result.get("mode"),
169
+ })
170
+
171
+ except Exception as exc:
172
+ return JSONResponse(status_code=400, content={"detail": str(exc)})
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ poppler-utils
pdf_utils.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ WaveSign — PDF Support via Rasterization
3
+ -----------------------------------------
4
+ Pipeline:
5
+ 1. Rasterize original PDF pages at 300 DPI
6
+ 2. Embed diffraction watermark into each page image
7
+ 3. Repackage watermarked pages into a signed image-PDF
8
+ 4. Rasterize the signed PDF again — sign those final images
9
+ (ensures verification always matches the exact output PDF pixels)
10
+
11
+ Output is an image-PDF (not text-searchable) — correct for tamper-evident signing.
12
+ """
13
+
14
+ import io
15
+ from pdf2image import convert_from_bytes
16
+ from reportlab.pdfgen import canvas as rl_canvas
17
+ from reportlab.lib.utils import ImageReader
18
+ from pypdf import PdfReader
19
+
20
+ from core import embed_watermark, sign_image, verify_image, detect_mode
21
+
22
+ DPI = 300
23
+
24
+
25
+ def pdf_to_images(pdf_bytes: bytes, dpi: int = DPI) -> list:
26
+ """Rasterize all PDF pages. Returns list of PIL Images."""
27
+ return convert_from_bytes(pdf_bytes, dpi=dpi)
28
+
29
+
30
+ def images_to_pdf(images: list) -> bytes:
31
+ """Pack PIL Images into a single PDF. Each image fills its page."""
32
+ buf = io.BytesIO()
33
+ first_w, first_h = images[0].size
34
+ c = rl_canvas.Canvas(buf, pagesize=(first_w * 72 / DPI, first_h * 72 / DPI))
35
+ for img in images:
36
+ w_px, h_px = img.size
37
+ pw, ph = w_px * 72 / DPI, h_px * 72 / DPI
38
+ c.setPageSize((pw, ph))
39
+ img_buf = io.BytesIO()
40
+ img.save(img_buf, format="PNG")
41
+ img_buf.seek(0)
42
+ c.drawImage(ImageReader(img_buf), 0, 0, width=pw, height=ph)
43
+ c.showPage()
44
+ c.save()
45
+ buf.seek(0)
46
+ return buf.read()
47
+
48
+
49
+ def sign_pdf(pdf_bytes: bytes, secret: str, dpi: int = DPI, strength: float = 0.03):
50
+ """
51
+ Sign all pages of a PDF.
52
+
53
+ Critical: signatures are computed from the FINAL rasterized output PDF,
54
+ not from intermediate watermarked images. This ensures verify_pdf always
55
+ produces matching results without round-trip pixel drift.
56
+
57
+ Returns: (signed_pdf_bytes, signatures_list, n_pages)
58
+ """
59
+ # Step 1: rasterize original pages
60
+ images = pdf_to_images(pdf_bytes, dpi=dpi)
61
+
62
+ # Step 2: embed watermark into each page
63
+ watermarked = []
64
+ for img in images:
65
+ mode = detect_mode(img)
66
+ wm = embed_watermark(img, secret, strength=strength, mode=mode)
67
+ watermarked.append(wm)
68
+
69
+ # Step 3: pack watermarked pages into signed PDF
70
+ signed_pdf_bytes = images_to_pdf(watermarked)
71
+
72
+ # Step 4: rasterize signed PDF — sign these exact pixels
73
+ final_images = pdf_to_images(signed_pdf_bytes, dpi=dpi)
74
+ n_pages = len(final_images)
75
+ sigs = []
76
+ for i, img in enumerate(final_images):
77
+ sig = sign_image(img, secret, mode='document')
78
+ sig["page_index"] = i
79
+ sigs.append(sig)
80
+
81
+ return signed_pdf_bytes, sigs, n_pages
82
+
83
+
84
+ def verify_pdf(pdf_bytes: bytes, secret: str, signatures: list) -> list:
85
+ """
86
+ Verify all pages of a signed PDF against per-page signatures.
87
+ Returns list of per-page result dicts with page_index key.
88
+ """
89
+ images = pdf_to_images(pdf_bytes, dpi=DPI)
90
+ results = []
91
+ for i, (img, sig) in enumerate(zip(images, signatures)):
92
+ result = verify_image(img, secret, sig)
93
+ result["page_index"] = i
94
+ results.append(result)
95
+ return results
96
+
97
+
98
+ def get_pdf_page_count(pdf_bytes: bytes) -> int:
99
+ """Quick page count without rasterization."""
100
+ return len(PdfReader(io.BytesIO(pdf_bytes)).pages)