Compare commits

..

2 Commits

5 changed files with 63 additions and 24 deletions

20
app.py
View File

@ -261,10 +261,11 @@ def create_paste():
else: else:
return jsonify({'error': 'Provide either encrypted_data or content'}), 400 return jsonify({'error': 'Provide either encrypted_data or content'}), 400
allowed_expiry = set(_pastes.get('allow_expiry_options', ['never'])) allowed_expiry = set(_pastes.get('allow_expiry_options', ['1year']))
expires_in = data.get('expires_in', 'never') expires_in = data.get('expires_in', _pastes.get('default_expiry', '1year'))
if expires_in not in allowed_expiry: if expires_in not in allowed_expiry:
expires_in = 'never' # Fallback to the first allowed option if everything is missing
expires_in = _pastes.get('default_expiry', list(allowed_expiry)[0])
expires_at = None expires_at = None
if expires_in != 'never': if expires_in != 'never':
@ -273,6 +274,7 @@ def create_paste():
'1day': datetime.timedelta(days=1), '1day': datetime.timedelta(days=1),
'1week': datetime.timedelta(weeks=1), '1week': datetime.timedelta(weeks=1),
'1month': datetime.timedelta(days=30), '1month': datetime.timedelta(days=30),
'1year': datetime.timedelta(days=365),
} }
delta = delta_map.get(expires_in) delta = delta_map.get(expires_in)
if delta is None: if delta is None:
@ -322,15 +324,21 @@ def view_paste_raw(paste_id):
paste = _get_paste_or_abort(paste_id) paste = _get_paste_or_abort(paste_id)
stored = paste['encrypted_data'] stored = paste['encrypted_data']
# Plaintext pastes are stored as a JSON object; return the content directly. # 1. Plaintext paste — Return content directly as text/plain
if not re.match(r'^[A-Za-z0-9_-]+:[A-Za-z0-9_-]+$', stored): if not re.match(r'^[A-Za-z0-9_-]+:[A-Za-z0-9_-]+$', stored):
try: try:
data = json.loads(stored) data = json.loads(stored)
return Response(data.get('content', ''), mimetype='text/plain') return Response(data.get('content', ''), mimetype='text/plain; charset=utf-8')
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
pass pass
# Encrypted paste — return the raw ciphertext blob for API consumers. # 2. Encrypted paste — Browsers get a minimal decryptor; API consumers get JSON
accept = request.headers.get('Accept', '')
if 'text/html' in accept:
# Minimal HTML shell that handles decryption for browsers (E2E)
return render_template('raw_decryptor.html', paste=paste)
# API response
return jsonify({ return jsonify({
'id': paste['id'], 'id': paste['id'],
'encrypted_data': stored, 'encrypted_data': stored,

View File

@ -25,14 +25,14 @@
"id_length": 8, "id_length": 8,
"recent_limit": 50, "recent_limit": 50,
"default_language": "text", "default_language": "text",
"default_expiry": "never", "default_expiry": "1year",
"allow_expiry_options": ["never", "1hour", "1day", "1week", "1month"], "allow_expiry_options": ["1hour", "1day", "1week", "1month", "1year"],
"expiry_labels": { "expiry_labels": {
"never": "Never",
"1hour": "1 Hour", "1hour": "1 Hour",
"1day": "1 Day", "1day": "1 Day",
"1week": "1 Week", "1week": "1 Week",
"1month": "1 Month" "1month": "1 Month",
"1year": "1 Year"
} }
}, },

View File

@ -137,15 +137,9 @@ function showError(title, detail) {
} }
function rawView() { function rawView() {
if (!_decryptedPaste) return; const key = window.location.hash;
const blob = new Blob([_decryptedPaste.content], { type: 'text/plain; charset=utf-8' }); const url = window.location.href.split('?')[0].split('#')[0] + '/raw' + key;
const url = URL.createObjectURL(blob); window.open(url, '_blank');
const a = Object.assign(document.createElement('a'),
{ href: url, target: '_blank', rel: 'noopener noreferrer' });
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 10000);
} }
async function copyPaste() { async function copyPaste() {

View File

@ -7,11 +7,11 @@
<input type="text" id="title" placeholder="Title (optional)" class="nav-input" maxlength="100" autocomplete="off"> <input type="text" id="title" placeholder="Title (optional)" class="nav-input" maxlength="100" autocomplete="off">
<select id="language" class="nav-select"></select> <select id="language" class="nav-select"></select>
<select id="expires_in" class="nav-select"> <select id="expires_in" class="nav-select">
<option value="never">Never</option> {% for opt in cfg.pastes.allow_expiry_options %}
<option value="1hour">1 Hour</option> <option value="{{ opt }}" {% if opt == cfg.pastes.default_expiry %}selected{% endif %}>
<option value="1day">1 Day</option> {{ cfg.pastes.expiry_labels.get(opt, opt) }}
<option value="1week">1 Week</option> </option>
<option value="1month">1 Month</option> {% endfor %}
</select> </select>
<button id="clearBtn" class="nav-btn">Clear</button> <button id="clearBtn" class="nav-btn">Clear</button>
<button id="submitBtn" class="nav-btn nav-btn-save">Save</button> <button id="submitBtn" class="nav-btn nav-btn-save">Save</button>

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Raw | {{ cfg.site.name }}</title>
<style>
body { margin: 0; padding: 1rem; background: #fff; color: #000; font-family: monospace; }
pre { white-space: pre-wrap; word-wrap: break-word; margin: 0; }
#error { display: none; color: red; font-weight: bold; }
</style>
</head>
<body>
<pre id="rawContent"></pre>
<div id="error">Decryption Failed — Key missing or incorrect.</div>
<script src="{{ url_for('static', filename='js/crypto.js') }}" nonce="{{ csp_nonce }}"></script>
<script nonce="{{ csp_nonce }}">
(async function decryptRaw() {
const raw = {{ paste.encrypted_data | tojson }};
const keyBase64 = window.location.hash.slice(1);
const pre = document.getElementById('rawContent');
const error = document.getElementById('error');
if (!keyBase64) { error.style.display = 'block'; return; }
try {
const key = await PasteCrypto.importKey(keyBase64);
const plaintext = await PasteCrypto.decrypt(raw, key);
const data = JSON.parse(plaintext);
pre.textContent = data.content;
} catch (e) {
error.style.display = 'block';
}
})();
</script>
</body>
</html>