Fraser commited on
Commit
542f3b7
·
1 Parent(s): 682910a

secure-ish

Browse files
Files changed (2) hide show
  1. app.py +432 -23
  2. verification_helper.js +192 -0
app.py CHANGED
@@ -5,6 +5,8 @@ import uuid
5
  from datetime import datetime
6
  from typing import Dict, List, Optional, Tuple
7
  import hashlib
 
 
8
  from pathlib import Path
9
 
10
  # Create data directories
@@ -16,6 +18,123 @@ IMAGES_DIR = DATA_DIR / "images"
16
  for dir_path in [DATA_DIR, PICLETS_DIR, COLLECTIONS_DIR, IMAGES_DIR]:
17
  dir_path.mkdir(exist_ok=True)
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  class PicletStorage:
20
  """Handles saving and retrieving Piclet data using file-based storage"""
21
 
@@ -107,7 +226,8 @@ class PicletStorage:
107
  "primaryType": data.get("primaryType"),
108
  "tier": data.get("tier"),
109
  "created_at": data.get("created_at"),
110
- "has_image": "image_filename" in data
 
111
  }
112
  piclets.append(summary)
113
 
@@ -142,7 +262,7 @@ class PicletStorage:
142
  print(f"Error getting image for {piclet_id}: {e}")
143
  return None
144
 
145
- def save_piclet_api(piclet_json: str, image_file=None) -> str:
146
  """
147
  API endpoint to save a Piclet
148
  """
@@ -159,6 +279,20 @@ def save_piclet_api(piclet_json: str, image_file=None) -> str:
159
  "error": f"Missing required field: {field}"
160
  })
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  # Save the Piclet
163
  success, result = PicletStorage.save_piclet(piclet_data, image_file)
164
 
@@ -166,7 +300,8 @@ def save_piclet_api(piclet_json: str, image_file=None) -> str:
166
  return json.dumps({
167
  "success": True,
168
  "piclet_id": result,
169
- "message": "Piclet saved successfully"
 
170
  })
171
  else:
172
  return json.dumps({
@@ -257,6 +392,87 @@ def get_piclet_image_api(piclet_id: str):
257
  print(f"Error in get_piclet_image_api: {e}")
258
  return None
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  # Create example Piclet data for testing
261
  def create_example_piclet() -> str:
262
  """Create an example Piclet for testing"""
@@ -330,9 +546,86 @@ with gr.Blocks(title="Piclets Server API", theme=gr.themes.Soft()) as app:
330
  """)
331
 
332
  with gr.Tabs():
333
- # Save Piclet Tab
334
- with gr.Tab("💾 Save Piclet"):
335
- gr.Markdown("### Save a new Piclet with optional image")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
  with gr.Row():
338
  with gr.Column(scale=2):
@@ -428,17 +721,75 @@ with gr.Blocks(title="Piclets Server API", theme=gr.themes.Soft()) as app:
428
  outputs=image_output
429
  )
430
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  # API Documentation Tab
432
- with gr.Tab("📖 API Documentation"):
433
  gr.Markdown("""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  ## API Endpoints
435
 
436
- This Gradio interface provides the following API endpoints that can be accessed programmatically:
 
 
 
 
 
 
 
 
 
 
437
 
438
- ### 1. Save Piclet
439
- - **Function**: `save_piclet_api(piclet_json, image_file=None)`
440
- - **Input**: JSON string with Piclet data, optional image file
441
  - **Output**: JSON response with success status and Piclet ID
 
442
 
443
  ### 2. Get Piclet
444
  - **Function**: `get_piclet_api(piclet_id)`
@@ -496,27 +847,62 @@ with gr.Blocks(title="Piclets Server API", theme=gr.themes.Soft()) as app:
496
  }
497
  ```
498
 
499
- ## Usage from Frontend
 
 
 
 
 
 
 
500
 
 
501
  ```javascript
502
  // Connect to this Gradio space
503
  const client = await window.gradioClient.Client.connect("your-space-name");
504
 
505
- // Save a Piclet
506
- const saveResult = await client.predict("/save_piclet_api", [
507
- JSON.stringify(picletData), // Piclet JSON
508
- imageFile // Optional image file
 
 
509
  ]);
510
 
511
- // Get a Piclet
512
- const getResult = await client.predict("/get_piclet_api", [
513
- picletId // Piclet ID string
514
- ]);
 
 
515
 
516
- // List Piclets
517
- const listResult = await client.predict("/list_piclets_api", [
518
- 20 // Maximum results
 
 
519
  ]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
  ```
521
 
522
  ## Storage Structure
@@ -527,6 +913,29 @@ with gr.Blocks(title="Piclets Server API", theme=gr.themes.Soft()) as app:
527
  - **Collections**: `./data/collections/*.json` (future feature)
528
 
529
  All data is stored locally in the Hugging Face Space persistent storage.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  """)
531
 
532
  # Launch the app
 
5
  from datetime import datetime
6
  from typing import Dict, List, Optional, Tuple
7
  import hashlib
8
+ import hmac
9
+ import time
10
  from pathlib import Path
11
 
12
  # Create data directories
 
18
  for dir_path in [DATA_DIR, PICLETS_DIR, COLLECTIONS_DIR, IMAGES_DIR]:
19
  dir_path.mkdir(exist_ok=True)
20
 
21
+ # Secret key for verification (in production, use environment variable)
22
+ SECRET_KEY = os.getenv("PICLET_SECRET_KEY", "piclets-dev-key-change-in-production")
23
+
24
+ class PicletVerification:
25
+ """Handles Piclet authenticity verification"""
26
+
27
+ @staticmethod
28
+ def create_signature(piclet_data: dict, timestamp: int, generation_data: dict = None) -> str:
29
+ """
30
+ Create a cryptographic signature for a Piclet
31
+ Uses HMAC-SHA256 with the secret key
32
+ """
33
+ # Create deterministic string from core Piclet data
34
+ core_data = {
35
+ "name": piclet_data.get("name", ""),
36
+ "primaryType": piclet_data.get("primaryType", ""),
37
+ "baseStats": piclet_data.get("baseStats", {}),
38
+ "movepool": piclet_data.get("movepool", []),
39
+ "timestamp": timestamp
40
+ }
41
+
42
+ # Add generation metadata if provided
43
+ if generation_data:
44
+ core_data["generation_data"] = generation_data
45
+
46
+ # Create deterministic JSON string (sorted keys)
47
+ data_string = json.dumps(core_data, sort_keys=True, separators=(',', ':'))
48
+
49
+ # Create HMAC signature
50
+ signature = hmac.new(
51
+ SECRET_KEY.encode('utf-8'),
52
+ data_string.encode('utf-8'),
53
+ hashlib.sha256
54
+ ).hexdigest()
55
+
56
+ return signature
57
+
58
+ @staticmethod
59
+ def verify_signature(piclet_data: dict, provided_signature: str, timestamp: int, generation_data: dict = None) -> bool:
60
+ """
61
+ Verify a Piclet's signature
62
+ Returns True if signature is valid
63
+ """
64
+ try:
65
+ expected_signature = PicletVerification.create_signature(piclet_data, timestamp, generation_data)
66
+ return hmac.compare_digest(expected_signature, provided_signature)
67
+ except Exception as e:
68
+ print(f"Signature verification error: {e}")
69
+ return False
70
+
71
+ @staticmethod
72
+ def create_verification_data(piclet_data: dict, image_caption: str = None, concept_string: str = None) -> dict:
73
+ """
74
+ Create complete verification data for a Piclet
75
+ Includes signature, timestamp, and generation metadata
76
+ """
77
+ timestamp = int(time.time())
78
+
79
+ # Generation metadata
80
+ generation_data = {
81
+ "image_caption": image_caption or "",
82
+ "concept_string": concept_string or "",
83
+ "generation_method": "official_app"
84
+ }
85
+
86
+ # Create signature
87
+ signature = PicletVerification.create_signature(piclet_data, timestamp, generation_data)
88
+
89
+ return {
90
+ "signature": signature,
91
+ "timestamp": timestamp,
92
+ "generation_data": generation_data,
93
+ "verification_version": "1.0"
94
+ }
95
+
96
+ @staticmethod
97
+ def is_verified_piclet(piclet_data: dict) -> Tuple[bool, str]:
98
+ """
99
+ Check if a Piclet has valid verification
100
+ Returns: (is_verified, status_message)
101
+ """
102
+ try:
103
+ # Check for verification data
104
+ if "verification" not in piclet_data:
105
+ return False, "No verification data found"
106
+
107
+ verification = piclet_data["verification"]
108
+ required_fields = ["signature", "timestamp", "generation_data"]
109
+
110
+ for field in required_fields:
111
+ if field not in verification:
112
+ return False, f"Missing verification field: {field}"
113
+
114
+ # Verify signature
115
+ is_valid = PicletVerification.verify_signature(
116
+ piclet_data,
117
+ verification["signature"],
118
+ verification["timestamp"],
119
+ verification["generation_data"]
120
+ )
121
+
122
+ if not is_valid:
123
+ return False, "Invalid signature - Piclet may be modified or fake"
124
+
125
+ # Check timestamp (reject if too old - 24 hours)
126
+ current_time = int(time.time())
127
+ piclet_time = verification["timestamp"]
128
+
129
+ # Allow some flexibility for testing (24 hours)
130
+ if current_time - piclet_time > 86400: # 24 hours
131
+ return False, "Piclet signature is too old (>24 hours)"
132
+
133
+ return True, "Verified authentic Piclet"
134
+
135
+ except Exception as e:
136
+ return False, f"Verification error: {str(e)}"
137
+
138
  class PicletStorage:
139
  """Handles saving and retrieving Piclet data using file-based storage"""
140
 
 
226
  "primaryType": data.get("primaryType"),
227
  "tier": data.get("tier"),
228
  "created_at": data.get("created_at"),
229
+ "has_image": "image_filename" in data,
230
+ "verified": data.get("verified", False)
231
  }
232
  piclets.append(summary)
233
 
 
262
  print(f"Error getting image for {piclet_id}: {e}")
263
  return None
264
 
265
+ def save_piclet_api(piclet_json: str, signature: str = "", image_file=None) -> str:
266
  """
267
  API endpoint to save a Piclet
268
  """
 
279
  "error": f"Missing required field: {field}"
280
  })
281
 
282
+ # Verify Piclet authenticity
283
+ is_verified, verification_message = PicletVerification.is_verified_piclet(piclet_data)
284
+
285
+ if not is_verified:
286
+ return json.dumps({
287
+ "success": False,
288
+ "error": f"Verification failed: {verification_message}",
289
+ "verification_required": True
290
+ })
291
+
292
+ # Add verification status to stored data
293
+ piclet_data["verified"] = True
294
+ piclet_data["verification_message"] = verification_message
295
+
296
  # Save the Piclet
297
  success, result = PicletStorage.save_piclet(piclet_data, image_file)
298
 
 
300
  return json.dumps({
301
  "success": True,
302
  "piclet_id": result,
303
+ "message": "Verified Piclet saved successfully",
304
+ "verified": True
305
  })
306
  else:
307
  return json.dumps({
 
392
  print(f"Error in get_piclet_image_api: {e}")
393
  return None
394
 
395
+ def sign_piclet_api(piclet_json: str, image_caption: str = "", concept_string: str = "") -> str:
396
+ """
397
+ API endpoint to sign a Piclet (for static frontend integration)
398
+ This allows static sites to generate verified Piclets without exposing the secret key
399
+ """
400
+ try:
401
+ # Parse the JSON data
402
+ piclet_data = json.loads(piclet_json)
403
+
404
+ # Validate required fields
405
+ required_fields = ["name", "primaryType", "baseStats", "movepool"]
406
+ for field in required_fields:
407
+ if field not in piclet_data:
408
+ return json.dumps({
409
+ "success": False,
410
+ "error": f"Missing required field: {field}"
411
+ })
412
+
413
+ # Create verification data using server's secret key
414
+ verification_data = PicletVerification.create_verification_data(
415
+ piclet_data,
416
+ image_caption=image_caption,
417
+ concept_string=concept_string
418
+ )
419
+
420
+ # Add verification to Piclet data
421
+ verified_piclet = {**piclet_data, "verification": verification_data}
422
+
423
+ return json.dumps({
424
+ "success": True,
425
+ "verified_piclet": verified_piclet,
426
+ "signature": verification_data["signature"],
427
+ "message": "Piclet signed successfully - ready for storage"
428
+ })
429
+
430
+ except json.JSONDecodeError:
431
+ return json.dumps({
432
+ "success": False,
433
+ "error": "Invalid JSON format"
434
+ })
435
+ except Exception as e:
436
+ return json.dumps({
437
+ "success": False,
438
+ "error": f"Signing error: {str(e)}"
439
+ })
440
+
441
+ def sign_and_save_piclet_api(piclet_json: str, image_caption: str = "", concept_string: str = "", image_file=None) -> str:
442
+ """
443
+ API endpoint to sign AND save a Piclet in one step (convenience method)
444
+ Perfect for static frontends - no secret key needed on client side
445
+ """
446
+ try:
447
+ # First, sign the Piclet
448
+ sign_result_json = sign_piclet_api(piclet_json, image_caption, concept_string)
449
+ sign_result = json.loads(sign_result_json)
450
+
451
+ if not sign_result["success"]:
452
+ return sign_result_json
453
+
454
+ # Then save the verified Piclet
455
+ verified_piclet_json = json.dumps(sign_result["verified_piclet"])
456
+ save_result_json = save_piclet_api(verified_piclet_json, sign_result["signature"], image_file)
457
+ save_result = json.loads(save_result_json)
458
+
459
+ if save_result["success"]:
460
+ return json.dumps({
461
+ "success": True,
462
+ "piclet_id": save_result["piclet_id"],
463
+ "message": "Piclet signed and saved successfully",
464
+ "verified": True,
465
+ "signature": sign_result["signature"]
466
+ })
467
+ else:
468
+ return save_result_json
469
+
470
+ except Exception as e:
471
+ return json.dumps({
472
+ "success": False,
473
+ "error": f"Sign and save error: {str(e)}"
474
+ })
475
+
476
  # Create example Piclet data for testing
477
  def create_example_piclet() -> str:
478
  """Create an example Piclet for testing"""
 
546
  """)
547
 
548
  with gr.Tabs():
549
+ # Sign and Save Tab (For Static Frontends)
550
+ with gr.Tab("✍️ Sign & Save Piclet"):
551
+ gr.Markdown("### Sign and Save a Piclet (One Step - Perfect for Static Sites)")
552
+ gr.Markdown("🌟 **For Static Frontends**: No secret key needed! Server signs your Piclet automatically.")
553
+
554
+ with gr.Row():
555
+ with gr.Column(scale=2):
556
+ unsigned_json_input = gr.Textbox(
557
+ label="Unsigned Piclet JSON Data",
558
+ placeholder="Paste your unsigned Piclet JSON here...",
559
+ lines=12,
560
+ value=create_example_piclet()
561
+ )
562
+
563
+ with gr.Row():
564
+ caption_input = gr.Textbox(
565
+ label="Image Caption",
566
+ placeholder="AI-generated image description...",
567
+ lines=2
568
+ )
569
+ concept_input = gr.Textbox(
570
+ label="Concept String",
571
+ placeholder="Creature concept from AI...",
572
+ lines=2
573
+ )
574
+
575
+ with gr.Column(scale=1):
576
+ sign_save_image_input = gr.File(
577
+ label="Piclet Image (Optional)",
578
+ file_types=["image"]
579
+ )
580
+
581
+ sign_save_btn = gr.Button("✍️ Sign & Save Piclet", variant="primary")
582
+
583
+ sign_save_output = gr.JSON(label="Sign & Save Result")
584
+
585
+ sign_save_btn.click(
586
+ fn=sign_and_save_piclet_api,
587
+ inputs=[unsigned_json_input, caption_input, concept_input, sign_save_image_input],
588
+ outputs=sign_save_output
589
+ )
590
+
591
+ # Sign Only Tab
592
+ with gr.Tab("🔏 Sign Only"):
593
+ gr.Markdown("### Sign a Piclet (Two-Step Process)")
594
+ gr.Markdown("📝 **Advanced**: Sign first, then save separately. Useful for batch operations.")
595
+
596
+ with gr.Row():
597
+ with gr.Column(scale=2):
598
+ sign_json_input = gr.Textbox(
599
+ label="Unsigned Piclet JSON Data",
600
+ placeholder="Paste your unsigned Piclet JSON here...",
601
+ lines=10,
602
+ value=create_example_piclet()
603
+ )
604
+
605
+ with gr.Row():
606
+ sign_caption_input = gr.Textbox(
607
+ label="Image Caption",
608
+ placeholder="AI-generated image description..."
609
+ )
610
+ sign_concept_input = gr.Textbox(
611
+ label="Concept String",
612
+ placeholder="Creature concept from AI..."
613
+ )
614
+
615
+ with gr.Column(scale=2):
616
+ sign_btn = gr.Button("🔏 Sign Piclet", variant="secondary")
617
+ sign_output = gr.JSON(label="Signing Result")
618
+
619
+ sign_btn.click(
620
+ fn=sign_piclet_api,
621
+ inputs=[sign_json_input, sign_caption_input, sign_concept_input],
622
+ outputs=sign_output
623
+ )
624
+
625
+ # Save Piclet Tab (For Pre-Signed)
626
+ with gr.Tab("💾 Save Pre-Signed"):
627
+ gr.Markdown("### Save a Pre-Signed Piclet")
628
+ gr.Markdown("⚠️ **For Advanced Users**: Save a Piclet that was already signed elsewhere.")
629
 
630
  with gr.Row():
631
  with gr.Column(scale=2):
 
721
  outputs=image_output
722
  )
723
 
724
+ # Generate Verified Example Tab
725
+ with gr.Tab("✅ Generate Verified Example"):
726
+ gr.Markdown("""
727
+ ### Generate a Verified Example Piclet
728
+
729
+ This creates a properly signed example Piclet that will pass verification.
730
+ Use this to test the verification system or as a template.
731
+ """)
732
+
733
+ generate_btn = gr.Button("✅ Generate Verified Example", variant="primary")
734
+ verified_output = gr.Textbox(
735
+ label="Verified Piclet JSON",
736
+ lines=20,
737
+ show_copy_button=True
738
+ )
739
+
740
+ generate_btn.click(
741
+ fn=create_verified_example_api,
742
+ outputs=verified_output
743
+ )
744
+
745
  # API Documentation Tab
746
+ with gr.Tab("📖 API Documentation & Verification"):
747
  gr.Markdown("""
748
+ ## 🔐 Verification System
749
+
750
+ **All Piclets must be verified to prevent fake or modified creatures.**
751
+
752
+ ### How Verification Works:
753
+ 1. **HMAC-SHA256 Signature**: Each Piclet is signed with a secret key
754
+ 2. **Timestamp Validation**: Signatures expire after 24 hours
755
+ 3. **Generation Metadata**: Includes image caption and concept string
756
+ 4. **Tamper Detection**: Any modification invalidates the signature
757
+
758
+ ### Required Verification Format:
759
+ ```json
760
+ {
761
+ "verification": {
762
+ "signature": "abc123...",
763
+ "timestamp": 1690123456,
764
+ "generation_data": {
765
+ "image_caption": "AI-generated description",
766
+ "concept_string": "Creature concept",
767
+ "generation_method": "official_app"
768
+ },
769
+ "verification_version": "1.0"
770
+ }
771
+ }
772
+ ```
773
+
774
  ## API Endpoints
775
 
776
+ ### 1. Sign & Save Piclet (Recommended for Static Sites)
777
+ - **Function**: `sign_and_save_piclet_api(piclet_json, image_caption, concept_string, image_file=None)`
778
+ - **Input**: Unsigned Piclet JSON, image caption, concept string, optional image file
779
+ - **Output**: JSON response with success status and Piclet ID
780
+ - **✅ Perfect for**: Static sites (no secret key needed)
781
+
782
+ ### 2. Sign Only
783
+ - **Function**: `sign_piclet_api(piclet_json, image_caption, concept_string)`
784
+ - **Input**: Unsigned Piclet JSON, image caption, concept string
785
+ - **Output**: JSON response with verified Piclet and signature
786
+ - **Use case**: Two-step process, batch operations
787
 
788
+ ### 3. Save Pre-Signed Piclet
789
+ - **Function**: `save_piclet_api(piclet_json, signature, image_file=None)`
790
+ - **Input**: Signed Piclet JSON, signature string, optional image file
791
  - **Output**: JSON response with success status and Piclet ID
792
+ - **⚠️ Requires**: Valid verification signature
793
 
794
  ### 2. Get Piclet
795
  - **Function**: `get_piclet_api(piclet_id)`
 
847
  }
848
  ```
849
 
850
+ ## Frontend Integration (JavaScript)
851
+
852
+ ### Include Verification Helper
853
+ ```html
854
+ <!-- Include crypto-js for HMAC generation -->
855
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
856
+ <script src="verification_helper.js"></script>
857
+ ```
858
 
859
+ ### Static Site Usage (Recommended)
860
  ```javascript
861
  // Connect to this Gradio space
862
  const client = await window.gradioClient.Client.connect("your-space-name");
863
 
864
+ // Sign and save in one step (NO SECRET KEY NEEDED!)
865
+ const result = await client.predict("/sign_and_save_piclet_api", [
866
+ JSON.stringify(picletData), // Unsigned Piclet data
867
+ imageCaption, // AI-generated caption
868
+ conceptString, // Creature concept
869
+ imageFile // Optional image
870
  ]);
871
 
872
+ const response = JSON.parse(result.data[0]);
873
+ if (response.success) {
874
+ console.log(`Piclet saved with ID: ${response.piclet_id}`);
875
+ } else {
876
+ console.error(`Save failed: ${response.error}`);
877
+ }
878
 
879
+ // Two-step process (sign first, then save)
880
+ const signResult = await client.predict("/sign_piclet_api", [
881
+ JSON.stringify(picletData),
882
+ imageCaption,
883
+ conceptString
884
  ]);
885
+
886
+ const signResponse = JSON.parse(signResult.data[0]);
887
+ if (signResponse.success) {
888
+ const saveResult = await client.predict("/save_piclet_api", [
889
+ JSON.stringify(signResponse.verified_piclet),
890
+ signResponse.signature,
891
+ imageFile
892
+ ]);
893
+ }
894
+ ```
895
+
896
+ ### Secret Key Management (Server Only)
897
+ ```bash
898
+ # Set environment variable on server (HuggingFace Spaces)
899
+ PICLET_SECRET_KEY=your-super-secret-64-char-hex-key
900
+
901
+ # Generate secure key:
902
+ openssl rand -hex 32
903
+
904
+ # ✅ Frontend doesn't need the secret key anymore!
905
+ # Server handles all signing securely
906
  ```
907
 
908
  ## Storage Structure
 
913
  - **Collections**: `./data/collections/*.json` (future feature)
914
 
915
  All data is stored locally in the Hugging Face Space persistent storage.
916
+
917
+ ## Security Features
918
+
919
+ ### ✅ Verified Piclets
920
+ - Generated through official app only
921
+ - Cryptographically signed with HMAC-SHA256
922
+ - Include generation metadata (image caption, concept)
923
+ - Tamper-proof (any modification breaks signature)
924
+
925
+ ### ❌ Rejected Piclets
926
+ - Missing verification data
927
+ - Invalid or expired signatures
928
+ - Modified after generation
929
+ - Created outside official app
930
+
931
+ ### Environment Variables
932
+ - `PICLET_SECRET_KEY`: Secret key for verification (change in production)
933
+
934
+ ### Files Included
935
+ - `app.py`: Main server application
936
+ - `verification_helper.js`: Frontend helper functions
937
+ - `requirements.txt`: Python dependencies
938
+ - `API_DOCUMENTATION.md`: Complete documentation
939
  """)
940
 
941
  # Launch the app
verification_helper.js ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Piclet Verification Helper for Frontend
3
+ * Use this in your Svelte game to generate verified Piclets
4
+ */
5
+
6
+ // This should match the server's secret key
7
+ const PICLET_SECRET_KEY = "piclets-dev-key-change-in-production";
8
+
9
+ /**
10
+ * Create HMAC-SHA256 signature (requires crypto-js or similar)
11
+ * For browser compatibility, you'll need to include crypto-js:
12
+ * npm install crypto-js
13
+ * or include via CDN: https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
14
+ */
15
+ async function createHMAC(message, key) {
16
+ // Browser-native crypto API version (modern browsers)
17
+ if (window.crypto && window.crypto.subtle) {
18
+ const encoder = new TextEncoder();
19
+ const keyData = encoder.encode(key);
20
+ const messageData = encoder.encode(message);
21
+
22
+ const cryptoKey = await window.crypto.subtle.importKey(
23
+ 'raw',
24
+ keyData,
25
+ { name: 'HMAC', hash: 'SHA-256' },
26
+ false,
27
+ ['sign']
28
+ );
29
+
30
+ const signature = await window.crypto.subtle.sign('HMAC', cryptoKey, messageData);
31
+ const hashArray = Array.from(new Uint8Array(signature));
32
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
33
+ }
34
+
35
+ // Fallback: Use crypto-js if available
36
+ if (window.CryptoJS) {
37
+ return CryptoJS.HmacSHA256(message, key).toString();
38
+ }
39
+
40
+ throw new Error("No crypto implementation available. Include crypto-js or use modern browser.");
41
+ }
42
+
43
+ /**
44
+ * Create verification data for a Piclet
45
+ * Call this when generating a Piclet in your game
46
+ */
47
+ async function createPicletVerification(picletData, imageCaption = "", conceptString = "") {
48
+ const timestamp = Math.floor(Date.now() / 1000);
49
+
50
+ // Core data used for signature
51
+ const coreData = {
52
+ name: picletData.name || "",
53
+ primaryType: picletData.primaryType || "",
54
+ baseStats: picletData.baseStats || {},
55
+ movepool: picletData.movepool || [],
56
+ timestamp: timestamp
57
+ };
58
+
59
+ // Generation metadata
60
+ const generationData = {
61
+ image_caption: imageCaption,
62
+ concept_string: conceptString,
63
+ generation_method: "official_app"
64
+ };
65
+
66
+ // Add generation data to core data
67
+ coreData.generation_data = generationData;
68
+
69
+ // Create deterministic JSON string
70
+ const dataString = JSON.stringify(coreData, null, 0);
71
+
72
+ // Create signature
73
+ const signature = await createHMAC(dataString, PICLET_SECRET_KEY);
74
+
75
+ return {
76
+ signature: signature,
77
+ timestamp: timestamp,
78
+ generation_data: generationData,
79
+ verification_version: "1.0"
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Add verification to a Piclet before saving
85
+ * Use this in your PicletGenerator component
86
+ */
87
+ async function verifyAndPreparePiclet(picletData, imageCaption = "", conceptString = "") {
88
+ try {
89
+ // Create verification data
90
+ const verification = await createPicletVerification(picletData, imageCaption, conceptString);
91
+
92
+ // Add verification to Piclet data
93
+ const verifiedPiclet = {
94
+ ...picletData,
95
+ verification: verification
96
+ };
97
+
98
+ return {
99
+ success: true,
100
+ piclet: verifiedPiclet,
101
+ signature: verification.signature
102
+ };
103
+
104
+ } catch (error) {
105
+ return {
106
+ success: false,
107
+ error: error.message
108
+ };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Save a verified Piclet to the server
114
+ * Use this instead of direct API calls
115
+ */
116
+ async function saveVerifiedPiclet(gradioClient, picletData, imageFile = null, imageCaption = "", conceptString = "") {
117
+ try {
118
+ // Create verified Piclet
119
+ const verificationResult = await verifyAndPreparePiclet(picletData, imageCaption, conceptString);
120
+
121
+ if (!verificationResult.success) {
122
+ return {
123
+ success: false,
124
+ error: `Verification failed: ${verificationResult.error}`
125
+ };
126
+ }
127
+
128
+ // Save to server
129
+ const result = await gradioClient.predict("/save_piclet_api", [
130
+ JSON.stringify(verificationResult.piclet),
131
+ verificationResult.signature,
132
+ imageFile
133
+ ]);
134
+
135
+ return JSON.parse(result.data[0]);
136
+
137
+ } catch (error) {
138
+ return {
139
+ success: false,
140
+ error: `Save failed: ${error.message}`
141
+ };
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Example usage in your Svelte component:
147
+ *
148
+ * // In PicletGenerator.svelte or similar component
149
+ * import { saveVerifiedPiclet } from './verification_helper.js';
150
+ *
151
+ * async function savePiclet() {
152
+ * const picletData = {
153
+ * name: generatedName,
154
+ * primaryType: detectedType,
155
+ * baseStats: calculatedStats,
156
+ * movepool: generatedMoves,
157
+ * // ... other data
158
+ * };
159
+ *
160
+ * const result = await saveVerifiedPiclet(
161
+ * gradioClient,
162
+ * picletData,
163
+ * uploadedImageFile,
164
+ * imageCaption,
165
+ * conceptString
166
+ * );
167
+ *
168
+ * if (result.success) {
169
+ * console.log(`Piclet saved with ID: ${result.piclet_id}`);
170
+ * } else {
171
+ * console.error(`Save failed: ${result.error}`);
172
+ * }
173
+ * }
174
+ */
175
+
176
+ // Export for ES modules
177
+ if (typeof module !== 'undefined' && module.exports) {
178
+ module.exports = {
179
+ createPicletVerification,
180
+ verifyAndPreparePiclet,
181
+ saveVerifiedPiclet
182
+ };
183
+ }
184
+
185
+ // Global functions for script tag usage
186
+ if (typeof window !== 'undefined') {
187
+ window.PicletVerification = {
188
+ createPicletVerification,
189
+ verifyAndPreparePiclet,
190
+ saveVerifiedPiclet
191
+ };
192
+ }