Skip to content

Commit e387d05

Browse files
committed
Update changes
1 parent 5882441 commit e387d05

10 files changed

+719
-39
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@ Thumbs.db
2929
.classpath
3030

3131
# Project-specific sensitive files
32+
*.txt
3233
.registration_token
3334
config.yaml
3435
registrations.json
3536
banned_emails.txt
3637
banned_ips.txt
37-
refresh_token.shbanned_usernames.txt
38+
refresh_token.sh
39+
banned_usernames.txt
40+
testbench/
3841

3942
# Backup directories
4043
backup/
File renamed without changes.

canary.py

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
#!/usr/bin/env python3
2+
3+
import yaml
4+
import requests
5+
import feedparser
6+
import datetime
7+
import subprocess
8+
import json
9+
import os
10+
import sys
11+
import asyncio
12+
from time import sleep
13+
from pathlib import Path
14+
15+
# File paths
16+
CONFIG_FILE = "config.yaml"
17+
OUTPUT_FILE = "canary.txt"
18+
TEMP_MESSAGE_FILE = "temp_canary_message.txt"
19+
20+
# Possible attestations
21+
ATTESTATIONS = [
22+
"We have not received any National Security Letters.",
23+
"We have not received any court orders under the Foreign Intelligence Surveillance Act.",
24+
"We have not received any gag orders that prevent us from stating we have received legal process.",
25+
"We have not been required to modify our systems to facilitate surveillance.",
26+
"We have not been subject to any searches or seizures of our servers."
27+
]
28+
29+
def load_config():
30+
"""Load configuration from YAML file."""
31+
try:
32+
if not os.path.exists(CONFIG_FILE):
33+
print(f"Error: Configuration file '{CONFIG_FILE}' not found.")
34+
print("Please create a configuration file with the following structure:")
35+
print("""
36+
gpg:
37+
key_id: YOUR_GPG_KEY_ID
38+
matrix:
39+
enabled: true
40+
homeserver: https://we2.ee
41+
username: @canary:we2.ee
42+
password: YOUR_PASSWORD
43+
room_id: !l7XTTF6tudReoEJEvr:we2.ee
44+
""")
45+
sys.exit(1)
46+
47+
with open(CONFIG_FILE, 'r') as file:
48+
config = yaml.safe_load(file)
49+
50+
# Check for required fields
51+
required_fields = [
52+
('gpg', 'key_id')
53+
]
54+
55+
for section, field in required_fields:
56+
if section not in config or field not in config[section]:
57+
print(f"Error: Required configuration field '{section}.{field}' is missing.")
58+
sys.exit(1)
59+
60+
return config
61+
except Exception as e:
62+
print(f"Error loading configuration: {e}")
63+
sys.exit(1)
64+
65+
def get_current_date():
66+
"""Return the current date in YYYY-MM-DD format."""
67+
return datetime.datetime.now().strftime("%Y-%m-%d")
68+
69+
def get_nist_time():
70+
"""Get the current time from NIST time server."""
71+
try:
72+
response = requests.get("https://timeapi.io/api/Time/current/zone?timeZone=UTC", timeout=10)
73+
if response.status_code == 200:
74+
time_data = response.json()
75+
return f"{time_data['dateTime']} UTC"
76+
else:
77+
print(f"Error fetching NIST time: HTTP {response.status_code}")
78+
return None
79+
except Exception as e:
80+
print(f"Error fetching NIST time: {e}")
81+
return None
82+
83+
def get_democracy_now_headline():
84+
"""Get the latest headline from Democracy Now! RSS feed."""
85+
try:
86+
feed = feedparser.parse("https://www.democracynow.org/democracynow.rss")
87+
if feed.entries and len(feed.entries) > 0:
88+
return feed.entries[0].title
89+
else:
90+
print("No entries found in Democracy Now! RSS feed")
91+
return None
92+
except Exception as e:
93+
print(f"Error fetching Democracy Now! headline: {e}")
94+
return None
95+
96+
def get_bitcoin_latest_block():
97+
"""Get the latest Bitcoin block hash and number."""
98+
try:
99+
response = requests.get("https://blockchain.info/latestblock", timeout=10)
100+
if response.status_code == 200:
101+
data = response.json()
102+
# Get block details
103+
block_response = requests.get(f"https://blockchain.info/rawblock/{data['hash']}", timeout=10)
104+
if block_response.status_code == 200:
105+
block_data = block_response.json()
106+
return {
107+
"height": data["height"],
108+
"hash": data["hash"],
109+
"time": datetime.datetime.fromtimestamp(block_data["time"]).strftime("%Y-%m-%d %H:%M:%S UTC")
110+
}
111+
print(f"Error fetching Bitcoin block: HTTP {response.status_code}")
112+
return None
113+
except Exception as e:
114+
print(f"Error fetching Bitcoin block data: {e}")
115+
return None
116+
117+
def collect_attestations():
118+
"""Prompt user for each attestation."""
119+
selected_attestations = []
120+
121+
print("\nPlease confirm each attestation separately:")
122+
for i, attestation in enumerate(ATTESTATIONS, 1):
123+
while True:
124+
response = input(f"Confirm attestation {i}: '{attestation}' (y/n): ").lower()
125+
if response in ['y', 'n']:
126+
break
127+
print("Please answer 'y' or 'n'.")
128+
129+
if response == 'y':
130+
selected_attestations.append(attestation)
131+
132+
return selected_attestations
133+
134+
def create_warrant_canary_message(config):
135+
"""Create the warrant canary message with attestations and verification elements."""
136+
current_date = get_current_date()
137+
nist_time = get_nist_time()
138+
democracy_now_headline = get_democracy_now_headline()
139+
bitcoin_block = get_bitcoin_latest_block()
140+
141+
# Check if all required elements are available
142+
if not all([nist_time, democracy_now_headline, bitcoin_block]):
143+
missing = []
144+
if not nist_time: missing.append("NIST time")
145+
if not democracy_now_headline: missing.append("Democracy Now! headline")
146+
if not bitcoin_block: missing.append("Bitcoin block data")
147+
print(f"Error: Could not fetch: {', '.join(missing)}")
148+
return None
149+
150+
# Collect attestations from user
151+
attestations = collect_attestations()
152+
if not attestations:
153+
print("Warning: No attestations were confirmed.")
154+
proceed = input("Do you want to proceed without any attestations? (y/n): ").lower()
155+
if proceed != 'y':
156+
print("Operation cancelled")
157+
return None
158+
159+
# Create the message
160+
message = f"""We2.ee Warrant Canary
161+
Date: {current_date}
162+
163+
"""
164+
165+
# Add attestations
166+
for i, attestation in enumerate(attestations, 1):
167+
message += f"{i}. {attestation}\n"
168+
169+
message += f"""
170+
Proofs:
171+
NIST time: {nist_time}
172+
Democracy Now! headline: "{democracy_now_headline}"
173+
Bitcoin block #{bitcoin_block['height']} hash: {bitcoin_block['hash']}
174+
Bitcoin block time: {bitcoin_block['time']}
175+
176+
"""
177+
return message
178+
179+
def sign_with_gpg(message, gpg_key_id):
180+
"""Sign the warrant canary message with GPG."""
181+
try:
182+
# Write message to temporary file
183+
with open(TEMP_MESSAGE_FILE, "w") as f:
184+
f.write(message)
185+
186+
# Sign the message with GPG
187+
cmd = ["gpg", "--clearsign", "--default-key", gpg_key_id, TEMP_MESSAGE_FILE]
188+
subprocess.run(cmd, check=True)
189+
190+
# Read the signed message
191+
with open(f"{TEMP_MESSAGE_FILE}.asc", "r") as f:
192+
signed_message = f.read()
193+
194+
# Clean up temporary files
195+
os.remove(TEMP_MESSAGE_FILE)
196+
os.remove(f"{TEMP_MESSAGE_FILE}.asc")
197+
198+
return signed_message
199+
except subprocess.CalledProcessError as e:
200+
print(f"GPG signing error: {e}")
201+
return None
202+
except Exception as e:
203+
print(f"Error during GPG signing: {e}")
204+
return None
205+
206+
def save_warrant_canary(signed_message):
207+
"""Save the signed warrant canary to a file."""
208+
try:
209+
with open(OUTPUT_FILE, "w") as f:
210+
f.write(signed_message)
211+
print(f"Warrant canary saved to {OUTPUT_FILE}")
212+
return True
213+
except Exception as e:
214+
print(f"Error saving warrant canary: {e}")
215+
return False
216+
217+
async def post_to_matrix(config, signed_message):
218+
"""Post the signed warrant canary to Matrix room using nio library."""
219+
if not config.get('matrix', {}).get('enabled', False):
220+
print("Matrix posting is disabled in config")
221+
return False
222+
223+
try:
224+
from nio import AsyncClient, LoginResponse
225+
226+
# Get Matrix config
227+
homeserver = config['matrix']['homeserver']
228+
username = config['matrix']['username']
229+
password = config['matrix']['password']
230+
room_id = config['matrix']['room_id']
231+
232+
# Extract username without domain for login
233+
user_id = username
234+
if username.startswith('@'):
235+
user_id = username[1:] # Remove @ prefix
236+
if ':' in user_id:
237+
user_id = user_id.split(':')[0] # Remove server part
238+
239+
# Create client
240+
client = AsyncClient(homeserver, username)
241+
242+
# Login
243+
print(f"Logging in as {username} on {homeserver}...")
244+
response = await client.login(password)
245+
246+
if isinstance(response, LoginResponse):
247+
print("Login successful")
248+
else:
249+
print(f"Matrix login failed: {response}")
250+
await client.close()
251+
return False
252+
253+
# Format message for Matrix
254+
print(f"Posting canary to room {room_id}...")
255+
try:
256+
# Use HTML formatting for the message
257+
content = {
258+
"msgtype": "m.text",
259+
"body": signed_message, # Plain text version
260+
"format": "org.matrix.custom.html",
261+
"formatted_body": f"<pre>{signed_message}</pre>" # HTML version with preformatted text
262+
}
263+
264+
response = await client.room_send(
265+
room_id=room_id,
266+
message_type="m.room.message",
267+
content=content
268+
)
269+
270+
print("Successfully posted warrant canary to Matrix room")
271+
except Exception as e:
272+
print(f"Error sending message: {e}")
273+
await client.close()
274+
return False
275+
276+
# Logout and close
277+
await client.logout()
278+
await client.close()
279+
280+
return True
281+
except ImportError:
282+
print("Error: matrix-nio library not installed. Install with: pip install matrix-nio")
283+
return False
284+
except Exception as e:
285+
print(f"Error posting to Matrix: {e}")
286+
return False
287+
288+
def main():
289+
print("Generating We2.ee warrant canary...")
290+
291+
# Load configuration
292+
config = load_config()
293+
gpg_key_id = config['gpg']['key_id']
294+
295+
# Create message
296+
message = create_warrant_canary_message(config)
297+
if not message:
298+
print("Failed to create warrant canary message")
299+
sys.exit(1)
300+
301+
# Display the message
302+
print("\nWarrant Canary Message Preview:")
303+
print("-" * 50)
304+
print(message)
305+
print("-" * 50)
306+
307+
# Confirm with user
308+
user_input = input("\nDo you want to sign this message with GPG? (y/n): ")
309+
if user_input.lower() != 'y':
310+
print("Operation cancelled")
311+
sys.exit(0)
312+
313+
# Sign and save
314+
signed_message = sign_with_gpg(message, gpg_key_id)
315+
if not signed_message:
316+
print("Failed to sign warrant canary message")
317+
sys.exit(1)
318+
319+
if save_warrant_canary(signed_message):
320+
print("Warrant canary generated successfully!")
321+
else:
322+
print("Failed to save warrant canary")
323+
sys.exit(1)
324+
325+
# Post to Matrix if enabled
326+
if config.get('matrix', {}).get('enabled', False):
327+
post_to_matrix_input = input("\nDo you want to post the warrant canary to Matrix? (y/n): ")
328+
if post_to_matrix_input.lower() == 'y':
329+
asyncio.run(post_to_matrix(config, signed_message))
330+
331+
if __name__ == "__main__":
332+
main()

canary.txt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
-----BEGIN PGP SIGNED MESSAGE-----
2+
Hash: SHA512
3+
4+
We2.ee Warrant Canary
5+
Date: 2025-03-27
6+
7+
1. We have not received any National Security Letters.
8+
2. We have not received any court orders under the Foreign Intelligence Surveillance Act.
9+
3. We have not received any gag orders that prevent us from stating we have received legal process.
10+
4. We have not been required to modify our systems to facilitate surveillance.
11+
5. We have not been subject to any searches or seizures of our servers.
12+
13+
Proofs:
14+
NIST time: 2025-03-27T00:32:57.229589 UTC
15+
Democracy Now! headline: "1,400+ Arrested in Turkey as Erdoğan Jails Istanbul Mayor & Intensifies Authoritarian Crackdown"
16+
Bitcoin block #889596 hash: 000000000000000000018c38ea9043fd8710fa40d1cf90d5e541d050cd22b89d
17+
Bitcoin block time: 2025-03-26 23:49:42 UTC
18+
19+
-----BEGIN PGP SIGNATURE-----
20+
21+
iQIzBAEBCgAdFiEEMjqKLEezdiJLNhO3U1smWu2+W0QFAmfknNcACgkQU1smWu2+
22+
W0RrlxAAinoE3ZsIKAEpt/qzKygQyUx06VozLL82wzLPQrICia+jOkzo6UHuYGmY
23+
to4sj4SIOBaEyrdIhLvPG7Q6QRnrbn7NVasawRD484KsiO1+caPrnROFKJWyW/II
24+
UNlAnmOCxGttu14SlKYPpgp/a6LnLQtciNTHEsj6A0i/JgP1kAPRjqOiM0UCXTKf
25+
2MnNgwHHdjJt3f7AVJewzw5EPsW9ouh7VcIiIu9kZeuGotf0Gux5R8iTg9j2Cpum
26+
FrsHhdfwgyFFasTtp+sTnsWvmtw86OpIYuqPpopkIe70e3w4m/+C7ybejqNiNlWh
27+
1HCcFSyP17B6d516BCAKDJlrmCEKEQVz9MkTrqjpEKpZrVzo6Rl9bxQgN0QrohjV
28+
buUQO9Zyu6Xl7BZSD4qPqGgGeTzRt8pi4BTWtrMMs+JKTel4TimzPONqLh8exYBa
29+
Go5uDsbOAwnzbK/0VF9KIYqHc2t9pP5IgtUF3HGVZ0IputxTeDCF3uYJMiwO52cK
30+
XWaSvSlXB+Nc6OIjHHxG35hflk4ch8ZSEchp8OmXIYiy0zC640YwnnAnosg1WCOA
31+
UAeEvTO+QGyN7uP4rzGn9rtZgyoj5WT9GYGaiHFxrToCo9o3npOOQBAumcXLvP+B
32+
6Wkd0RKajppKCVEtEKH0/aH57YGC9V5XdZ9o0aa1yDLpWXw7Ag8=
33+
=5ZtT
34+
-----END PGP SIGNATURE-----

0 commit comments

Comments
 (0)