initial commit
Rust verification / test (push) Successful in 4m10s

This commit is contained in:
wt committed 2026-10-28 15:59:54 +07:00
commit cf25e28c28
156 files changed
+38669

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Generate independent Laravel CBC envelopes and optionally verify using PHP OpenSSL.
Run: python tests/crypto_interop.py [--check-php]
Requires Python cryptography. The optional PHP check requires PHP's openssl extension.
Fixtures exercise Laravel's encryptString envelope, not the entire Laravel framework.
"""
import argparse
import base64
import hashlib
import hmac
import json
from pathlib import Path
import shutil
import subprocess
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.padding import PKCS7
def generate():
fixtures = []
for key in (b"0123456789abcdef", b"0123456789abcdef0123456789abcdef"):
for plaintext in ("", "Laravel encrypted field", "Unicode 🔐 café"):
iv = bytes(range(16))
padder = PKCS7(128).padder()
padded = padder.update(plaintext.encode()) + padder.finalize()
encryptor = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor()
value = base64.b64encode(encryptor.update(padded) + encryptor.finalize()).decode()
encoded_iv = base64.b64encode(iv).decode()
mac = hmac.new(key, (encoded_iv + value).encode(), hashlib.sha256).hexdigest()
payload = dict(iv=encoded_iv, value=value, mac=mac, tag="")
envelope = base64.b64encode(json.dumps(payload, separators=(",", ":")).encode()).decode()
fixtures.append(dict(cipher=f"AES-{len(key)*8}-CBC", app_key="base64:" + base64.b64encode(key).decode(), plaintext=plaintext, envelope=envelope))
return fixtures
def check_php(fixtures):
php = shutil.which("php")
if not php:
print("SKIP PHP OpenSSL: php executable unavailable")
return
code = r'''
$rows = json_decode(stream_get_contents(STDIN), true, 512, JSON_THROW_ON_ERROR);
foreach ($rows as $row) {
$key = base64_decode(substr($row['app_key'], 7), true);
$payload = json_decode(base64_decode($row['envelope']), true);
$mac = hash_hmac('sha256', $payload['iv'].$payload['value'], $key);
if (!hash_equals($mac, $payload['mac'])) { throw new Exception('MAC mismatch'); }
$plain = openssl_decrypt($payload['value'], $row['cipher'], $key, 0, base64_decode($payload['iv']));
if ($plain !== $row['plaintext']) { throw new Exception('decrypt mismatch'); }
$encrypted = openssl_encrypt($row['plaintext'], $row['cipher'], $key, 0, base64_decode($payload['iv']));
if ($encrypted !== $payload['value']) { throw new Exception('encrypt mismatch'); }
}
echo 'PASS PHP OpenSSL: '.count($rows).' fixtures'.PHP_EOL;
'''
subprocess.run([php, "-r", code], input=json.dumps(fixtures), text=True, check=True)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check-php", action="store_true")
args = parser.parse_args()
rows = generate()
target = Path(__file__).parent / "fixtures" / "laravel_cbc.json"
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(json.dumps(rows, indent=2, ensure_ascii=False) + "\n")
print(f"Generated {len(rows)} independent Python AES CBC / HMAC fixtures: {target}")
if args.check_php:
check_php(rows)
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""Real PHP-issued device codes / Rust tokens on a disposable Laravel SQLite copy.
No source database is modified. Device codes, keys, secrets and tokens are not printed.
"""
import argparse
import base64
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import socket
import sqlite3
import subprocess
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
ROOT = Path(__file__).resolve().parents[1]
PHP_SETUP = r'''<?php
require getenv('APP_BASE_PATH').'/vendor/autoload.php';
$app=require getenv('APP_BASE_PATH').'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$user=new App\Models\User;
$user->name='Device fixture';$user->email='device@example.invalid';$user->password='Synthetic-device-password-4620';$user->save();
$clients=app(Laravel\Passport\ClientRepository::class);
$client=$clients->createAuthorizationCodeGrantClient('Device fixture',[],false,null,true);
$other=$clients->createAuthorizationCodeGrantClient('Other device fixture',[],false,null,true);
$server=app(League\OAuth2\Server\AuthorizationServer::class);
$request=(new GuzzleHttp\Psr7\ServerRequest('POST',getenv('APP_URL').'/oauth/device/code'))->withParsedBody(['client_id'=>(string)$client->id,'scope'=>'*']);
$response=$server->respondToDeviceAuthorizationRequest($request,new GuzzleHttp\Psr7\Response);
$code=json_decode((string)$response->getBody(),true);
echo json_encode(['client_id'=>(string)$client->id,'other_id'=>(string)$other->id,'user_id'=>(string)$user->id,'device_code'=>$code['device_code'],'user_code'=>$code['user_code']]);
'''
PHP_OPERATION = r'''<?php
require getenv('APP_BASE_PATH').'/vendor/autoload.php';
$app=require getenv('APP_BASE_PATH').'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$input=json_decode(file_get_contents(getenv('DEVICE_CONTROL')),true);
if ($input['operation']==='approve' || $input['operation']==='deny') {
app(League\OAuth2\Server\AuthorizationServer::class)->completeDeviceAuthorizationRequest($input['device_code'],$input['user_id'],$input['operation']==='approve');
echo json_encode(['completed'=>true]);
} elseif ($input['operation']==='read') {
$model=Laravel\Passport\Passport::deviceCode()->findOrFail($input['device_code']);
$entity=app(Laravel\Passport\Bridge\DeviceCodeRepository::class)->getDeviceCodeEntityByDeviceCode($input['device_code']);
echo json_encode(['client_id'=>(string)$model->client_id,'user_code'=>$model->user_code,'scopes'=>$model->scopes,'revoked'=>(bool)$model->revoked,'entity_readable'=>$entity!==null]);
} elseif ($input['operation']==='validate') {
$request=(new GuzzleHttp\Psr7\ServerRequest('GET',getenv('APP_URL').'/api/v1/user'))->withHeader('Authorization','Bearer '.$input['access_token']);
$request=app(League\OAuth2\Server\ResourceServer::class)->validateAuthenticatedRequest($request);
echo json_encode(['user_id'=>(string)$request->getAttribute('oauth_user_id'),'scopes'=>$request->getAttribute('oauth_scopes')]);
}
'''
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('binary', nargs='?', type=Path, default=ROOT/'target/debug/twofauth-rs')
parser.add_argument('--php', type=Path, default=Path(os.environ.get('PHP_BINARY','/tmp/twofauth-reference-runtime/php')))
parser.add_argument('--source', type=Path, default=ROOT.parent/'upstream')
parser.add_argument('--output', type=Path)
args=parser.parse_args()
source=args.source.resolve();php=args.php.resolve();binary=args.binary.resolve()
if not (source/'vendor/autoload.php').is_file():parser.error('pinned upstream Composer dependencies required')
with tempfile.TemporaryDirectory(prefix='device-php-rust-') as directory:
work=Path(directory);app=work/'reference'
shutil.copytree(source,app,ignore=shutil.ignore_patterns('vendor','node_modules','.env*','cache','logs','sessions','testing'))
(app/'vendor').symlink_to(source/'vendor',target_is_directory=True)
for path in ('bootstrap/cache','storage/logs','storage/framework/cache/data','storage/framework/sessions','storage/framework/views'):(app/path).mkdir(parents=True,exist_ok=True)
with socket.socket() as reserve:reserve.bind(('127.0.0.1',0));port=reserve.getsockname()[1]
base=f'http://127.0.0.1:{port}'
runtime={name:os.environ[name] for name in ('PATH','PHPRC','LD_LIBRARY_PATH') if name in os.environ}
app_key='base64:'+base64.b64encode(b'0123456789abcdef0123456789abcdef').decode()
reference_db=work/'reference.sqlite';candidate_db=work/'candidate.sqlite'
env=dict(runtime,APP_BASE_PATH=str(app),APP_ENV='local',APP_DEBUG='false',APP_KEY=app_key,APP_URL=base,DB_CONNECTION='sqlite',DB_DATABASE=str(reference_db),SESSION_DRIVER='file',CACHE_STORE='file',MAIL_MAILER='array')
def command(argv,environment=env,cwd=app):
result=subprocess.run([str(value) for value in argv],env=environment,cwd=cwd,capture_output=True)
if result.returncode:raise RuntimeError('fixture command failed')
return result.stdout
command([php,'artisan','migrate','--force']);command([php,'artisan','passport:keys','--force'])
setup=work/'setup.php';setup.write_text(PHP_SETUP)
fixture=json.loads(command([php,setup]))
with sqlite3.connect(reference_db) as original,sqlite3.connect(candidate_db) as candidate:original.backup(candidate)
rust_env=dict(runtime,DATABASE_URL='sqlite://'+str(candidate_db),APP_KEY=app_key,APP_URL=base,PASSPORT_KEY_PATH=str(app/'storage'),RUST_COOKIE_SECURE='false',RUST_PUBLIC_DIR=str(ROOT/'public'))
command([binary,'prepare-sqlite'],rust_env,work)
operation=work/'operation.php';operation.write_text(PHP_OPERATION)
control=work/'control.json'
php_env=dict(env,DB_DATABASE=str(candidate_db),DEVICE_CONTROL=str(control))
def php_operation(name,**values):
control.write_text(json.dumps({'operation':name,**values}));control.chmod(0o600)
return json.loads(command([php,operation],php_env))
def request(path,values=None):
data=None if values is None else urllib.parse.urlencode(values).encode()
req=urllib.request.Request(base+path,data=data,headers={'Accept':'application/json','Content-Type':'application/x-www-form-urlencoded'})
try:response=urllib.request.urlopen(req,timeout=15)
except urllib.error.HTTPError as error:response=error
with response:
body=response.read()
try:payload=json.loads(body)
except ValueError:payload=None
return response.status,payload,response.headers
urn='urn:ietf:params:oauth:grant-type:device_code'
def poll(code,client=None):return request('/oauth/token',{'grant_type':urn,'client_id':client or fixture['client_id'],'device_code':code})
def issue():
status,body,headers=request('/oauth/device/code',{'client_id':fixture['client_id'],'scope':'*'})
assert status==200,'Rust device issuance failed'
assert re.fullmatch('[0-9a-f]{80}',body['device_code']) and re.fullmatch('[BCDFGHJKLMNPQRSTVWXZ]{8}',body['user_code'])
assert body['verification_uri']==base+'/oauth/device' and body['verification_uri_complete']==base+'/oauth/device?user_code='+body['user_code']
assert 590<=body['expires_in']<=600 and body['interval']==5
assert headers['Cache-Control']=='no-store, private' and headers['Pragma']=='no-cache'
return body
checks={}
with (work/'rust.log').open('w') as log:
process=subprocess.Popen([str(binary),'serve','--listen',f'127.0.0.1:{port}'],env=rust_env,cwd=work,stdout=log,stderr=subprocess.STDOUT)
try:
for _ in range(100):
if process.poll() is not None:raise RuntimeError('Rust exited before readiness')
try:
if request('/up')[0]==200:break
except OSError:time.sleep(.05)
else:raise RuntimeError('Rust startup timed out')
status,body,_=poll(fixture['device_code']);assert status==400 and body['error']=='authorization_pending' and body['hint']==''
status,body,_=poll(fixture['device_code']);assert status==400 and body['error']=='slow_down' and body['hint']==''
checks['php_issued_device_pending_backoff']=True
php_operation('approve',device_code=fixture['device_code'],user_id=fixture['user_id'])
status,tokens,_=poll(fixture['device_code']);assert status==200 and tokens.get('refresh_token'),'approved device exchange failed'
validated=php_operation('validate',access_token=tokens['access_token']);assert validated=={'user_id':fixture['user_id'],'scopes':[]}
checks['php_approval_rust_token_php_resource_server']=True
status,body,_=poll(fixture['device_code']);assert status==400 and body['error']=='invalid_grant' and body['hint']==''
assert php_operation('read',device_code=fixture['device_code'])['revoked'] is True
checks['device_code_revocation_replay']=True
issued=issue();read=php_operation('read',device_code=issued['device_code'])
assert read=={'client_id':fixture['client_id'],'user_code':issued['user_code'],'scopes':['*'],'revoked':False,'entity_readable':True}
checks['rust_issued_device_php_repository_readable']=True
status,body,_=poll(issued['device_code'],fixture['other_id']);assert status==400 and body['hint']=='Device code was not issued to this client'
php_operation('approve',device_code=issued['device_code'],user_id=fixture['user_id'])
status,tokens,_=poll(issued['device_code']);assert status==200,'PHP approval of Rust-issued device failed'
assert php_operation('validate',access_token=tokens['access_token'])=={'user_id':fixture['user_id'],'scopes':[]}
checks['rust_device_php_approval_and_token_validation']=True
denied=issue();php_operation('deny',device_code=denied['device_code'],user_id=fixture['user_id'])
status,body,_=poll(denied['device_code']);assert status==401 and body['error']=='access_denied' and 'hint' not in body
checks['php_denied_device_rejected']=True
expired=issue()
with sqlite3.connect(candidate_db) as db:db.execute("UPDATE oauth_device_codes SET expires_at=datetime('now','-1 second') WHERE id=?",(expired['device_code'],))
status,body,_=poll(expired['device_code']);assert status==400 and body['error']=='expired_token' and body['hint']=='device_code'
checks['expired_device_rejected']=True
with sqlite3.connect(reference_db) as db:
assert db.execute('SELECT revoked,last_polled_at FROM oauth_device_codes WHERE id=?',(fixture['device_code'],)).fetchone()==(0,None)
checks['source_database_unchanged']=True
record={'upstream_commit':'3516c776a1fcbd83d234efc007e3a49807d6ed91','candidate_sha256':hashlib.sha256(binary.read_bytes()).hexdigest(),'php_version':command([php,'-r','echo PHP_VERSION;']).decode().strip(),'checks':checks,'passed':all(checks.values())}
if args.output:args.output.write_text(json.dumps(record,indent=2)+'\n')
print(json.dumps(record))
finally:
process.terminate()
try:process.wait(timeout=5)
except subprocess.TimeoutExpired:process.kill();process.wait()
if __name__=='__main__':main()
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Regression tests for stateful comparison semantics; these are not PHP parity tests."""
import contextlib
import importlib.util
import io
import json
from pathlib import Path
import subprocess
import sys
import threading
import unittest
import urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
TOOL = Path(__file__).resolve().parents[1] / 'tools/differential_http.py'
spec = importlib.util.spec_from_file_location('differential_http', TOOL)
harness = importlib.util.module_from_spec(spec)
spec.loader.exec_module(harness)
class HarnessTests(unittest.TestCase):
def test_form_transport_preserves_repeated_values_and_reserved_characters(self):
class Handler(BaseHTTPRequestHandler):
def log_message(self, *_args): pass
def do_POST(self):
body = self.rfile.read(int(self.headers['Content-Length']))
values = urllib.parse.parse_qs(body.decode(), keep_blank_values=True)
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'type': self.headers['Content-Type'], 'values': values}).encode())
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
base = f'http://127.0.0.1:{server.server_port}'
case = {'path': '/oauth/token', 'method': 'POST', 'form': {'scope': ['a+b', 'c d'], 'empty': '', 'code': 'x&y=z'}}
result = harness.fetch(base, case, 'candidate', 2)
self.assertEqual(result['status'], 200)
self.assertEqual(json.loads(result['body']), {'type': 'application/x-www-form-urlencoded', 'values': {'scope': ['a+b', 'c d'], 'empty': [''], 'code': ['x&y=z']}})
with self.assertRaises(ValueError):
harness.fetch(base, dict(case, json={}), 'candidate', 2)
finally:
server.shutdown()
server.server_close()
def test_typed_captures_and_pointer_escaping(self):
variables = {}
harness.capture_values({'body': b'{"a/b":{"~key":[42]}}'}, {'item': '/a~1b/~0key/0'}, variables)
self.assertEqual(harness.resolve({'path': '/items/{{item}}', 'json': ['{{item}}']}, variables), {'path': '/items/42', 'json': [42]})
with self.assertRaises(KeyError): harness.resolve('{{missing}}', variables)
with self.assertRaises(ValueError): harness.validate_path('//remote')
def test_json_url_normalization_after_unescaping(self):
response = {'body': b'{"url":"http:\\/\\/localhost:1234","nested":["http:\\/\\/localhost:1234/path"]}', 'content_type': 'application/json'}
self.assertEqual(harness.comparable(response, 'http://localhost:1234', {}), {'url': '<BASE>', 'nested': ['<BASE>/path']})
def test_json_assertions_are_independent_of_equality(self):
response = {'body': b'{"count":0}'}
self.assertFalse(harness.assert_json_response(response, {'expected_json': {'/count':1}}))
self.assertFalse(harness.assert_json_response(response, {'absent_json_pointers':['/count']}))
self.assertTrue(harness.assert_json_response(response, {'absent_json_pointers':['/secret']}))
def test_csrf_shape_rejects_json_or_html_documents(self):
self.assertTrue(harness.is_text_token({'status':200,'content_type':'text/html','body':b'A'*40}))
self.assertFalse(harness.is_text_token({'status':200,'content_type':'application/json','body':b'A'*40}))
self.assertFalse(harness.is_text_token({'status':200,'content_type':'text/html','body':b'<html>'+b'A'*40+b'</html>'}))
def test_cookie_rotation_capture_and_state_effects(self):
servers = []
for fixture_id in (14, 37):
class Handler(BaseHTTPRequestHandler):
item_id = fixture_id
saved = False
def log_message(self, *args): pass
def do_GET(self):
if self.path == '/session':
self.send_response(200)
self.send_header('Set-Cookie', 'session=fixture; Path=/')
self.send_header('Set-Cookie', 'XSRF-TOKEN=encoded%2Btoken; Path=/')
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(b'{}')
elif self.path == f'/items/{self.item_id}' and self.saved:
self.reply(200, {'value': 'saved'})
else: self.reply(404, {})
def do_POST(self):
body = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
if self.headers.get('Cookie', '').find('session=fixture') < 0 or self.headers.get('X-XSRF-TOKEN') != 'encoded+token' or self.headers.get('X-Compat') != 'fixture':
self.reply(419, {})
elif body == {'value': 'saved'}:
type(self).saved = True
self.reply(201, {'id': self.item_id})
else: self.reply(422, {})
def reply(self, status, payload):
self.send_response(status)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(payload).encode())
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
servers.append(server)
try:
import tempfile
with tempfile.TemporaryDirectory() as folder:
cases = Path(folder) / 'cases.json'
cases.write_text(json.dumps([
{'path': '/session', 'expected_status': 200},
{'method': 'POST', 'path': '/items', 'headers': {'X-Compat': 'fixture'}, 'json': {'value': 'saved'}, 'headers_from_cookies': {'X-XSRF-TOKEN': 'XSRF-TOKEN'}, 'capture_json': {'id': '/id'}, 'ignore_json_pointers': ['/id'], 'expected_status': 201},
{'path': '/items/{{id}}', 'expected_status': 200},
]))
command = [sys.executable, str(TOOL), '--reference', f'http://127.0.0.1:{servers[0].server_port}', '--candidate', f'http://127.0.0.1:{servers[1].server_port}', '--cases', str(cases), '--allow-side-effects', '--sessions']
result = subprocess.run(command, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual([row['result'] for row in map(json.loads, result.stdout.splitlines())], ['equal'] * 3)
self.assertNotIn('encoded', result.stdout)
cases.write_text(json.dumps([{'path': '/session', 'expected_status': 201}]))
mismatch = subprocess.run(command, capture_output=True, text=True)
self.assertEqual(mismatch.returncode, 1)
self.assertFalse(json.loads(mismatch.stdout)['checks']['expected_status'])
finally:
for server in servers:
server.shutdown()
server.server_close()
if __name__ == '__main__': unittest.main()
+49
View File
@@ -0,0 +1,49 @@
<?php
// Run with the PHP version required by the pinned upstream composer.lock:
// php tests/fixtures/generate_laravel_sessions.php /path/to/upstream/vendor/autoload.php
require $argv[1] ?? __DIR__.'/../../../upstream/vendor/autoload.php';
use Illuminate\Auth\SessionGuard;
use Illuminate\Cookie\CookieValuePrefix;
use Illuminate\Encryption\Encrypter;
use Illuminate\Session\ArraySessionHandler;
use Illuminate\Session\EncryptedStore;
use Illuminate\Session\Store;
$key = '0123456789abcdef0123456789abcdef';
$cipher = new Encrypter($key, 'AES-256-CBC');
$guard = (new ReflectionClass(SessionGuard::class))->newInstanceWithoutConstructor();
(new ReflectionProperty(SessionGuard::class, 'name'))->setValue($guard, 'web-guard');
$id = str_repeat('F', 40);
$cookie = static fn ($name, $value) => rawurlencode($cipher->encryptString(CookieValuePrefix::create($name, $key).$value));
$fixtures = [];
foreach (['json', 'php'] as $serialization) {
foreach ([false, true] as $encrypted) {
$handler = new ArraySessionHandler(129600);
$store = $encrypted
? new EncryptedStore('2fauth_session', $handler, $cipher, $id, $serialization)
: new Store('2fauth_session', $handler, $id, $serialization);
$store->start();
$store->put('_token', 'real-php-csrf-🔐');
$store->put($guard->getName(), 7);
$store->put('preferences', ['language' => 'fr', 'enabled' => true]);
$store->save();
$fixtures[] = [
'php_version' => PHP_VERSION,
'framework_version' => Composer\InstalledVersions::getPrettyVersion('laravel/framework'),
'app_key' => $key,
'serialization' => $serialization,
'encrypted' => $encrypted,
'session_id' => $id,
'login_key' => $guard->getName(),
'recaller_name' => $guard->getRecallerName(),
'payload' => $handler->read($id),
'database_payload' => base64_encode($handler->read($id)),
'session_cookie' => $cookie('2fauth_session', $id),
'csrf_cookie' => $cookie('XSRF-TOKEN', $store->token()),
'recaller_cookie' => $cookie($guard->getRecallerName(), '7|remember-fixture|password-hash'),
'csrf' => $store->token(),
];
}
}
echo json_encode($fixtures, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)."\n";
+38
View File
@@ -0,0 +1,38 @@
[
{
"cipher": "AES-128-CBC",
"app_key": "base64:MDEyMzQ1Njc4OWFiY2RlZg==",
"plaintext": "",
"envelope": "eyJpdiI6IkFBRUNBd1FGQmdjSUNRb0xEQTBPRHc9PSIsInZhbHVlIjoieXRwbENUdmY3ZUpQYmZYYllLNU9HQT09IiwibWFjIjoiM2YxNjA4NmQ0NGQ2YTlhMmM0NmE1ODgwYTM5NGZmZTkxMDkzMzQ3NDI4YzdjYzA1ZmY4OTNjYTc3NzkzN2E2YiIsInRhZyI6IiJ9"
},
{
"cipher": "AES-128-CBC",
"app_key": "base64:MDEyMzQ1Njc4OWFiY2RlZg==",
"plaintext": "Laravel encrypted field",
"envelope": "eyJpdiI6IkFBRUNBd1FGQmdjSUNRb0xEQTBPRHc9PSIsInZhbHVlIjoiUkZBNFdLUlovUXlRSGRmNHR2Qk80UTdRd2Z0VE9zNHBieXlMMnh1Nm95RT0iLCJtYWMiOiI3NmIwNDc3YjI2OGNhMDMyZTkwYWNiNDM1N2EyZTExYzM2OGI3MmZhZmYyNGNlODRjZWNiZWQ4MTEyZmQ2NDUxIiwidGFnIjoiIn0="
},
{
"cipher": "AES-128-CBC",
"app_key": "base64:MDEyMzQ1Njc4OWFiY2RlZg==",
"plaintext": "Unicode 🔐 café",
"envelope": "eyJpdiI6IkFBRUNBd1FGQmdjSUNRb0xEQTBPRHc9PSIsInZhbHVlIjoiMkZOVUNnRWJOT3YyeW1MSW5ZR3YrQkVmbHJMZ2RHd1JIRXFuVzEwbW81az0iLCJtYWMiOiI3NTkyNzg4YmQyMWEyMzE3YjQyYWU4NTRhYmI3M2Y3YWM0MDdjNGIyNGU5Y2YzM2U1YjNkYjExYjcyMTk0MTY3IiwidGFnIjoiIn0="
},
{
"cipher": "AES-256-CBC",
"app_key": "base64:MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
"plaintext": "",
"envelope": "eyJpdiI6IkFBRUNBd1FGQmdjSUNRb0xEQTBPRHc9PSIsInZhbHVlIjoiYjdHZlNkcnFuUjFjTW15NGUyZmM2QT09IiwibWFjIjoiZmZhNmRmZDgxMTkyYmU4MGEyOTBmZmIyMWRmMTg3ZGQ4MTYyNDM0MjUxNmM2ODViODQ1YmQ4MTExYTZkOGZkYyIsInRhZyI6IiJ9"
},
{
"cipher": "AES-256-CBC",
"app_key": "base64:MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
"plaintext": "Laravel encrypted field",
"envelope": "eyJpdiI6IkFBRUNBd1FGQmdjSUNRb0xEQTBPRHc9PSIsInZhbHVlIjoiNkRRc09uRWp3UWswamlxb2lCdURnSklaeWQ4ZEZScko1R2JiR09waUZEUT0iLCJtYWMiOiI0YWQ3ZjAzODYxMDJkM2MxMGZiYTg2Njc2ZjBjNmZkMGVmM2I0YzczZTIwMDhjNzMyZjY4YjExNjIzMjk4ZGZjIiwidGFnIjoiIn0="
},
{
"cipher": "AES-256-CBC",
"app_key": "base64:MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
"plaintext": "Unicode 🔐 café",
"envelope": "eyJpdiI6IkFBRUNBd1FGQmdjSUNRb0xEQTBPRHc9PSIsInZhbHVlIjoiRndtblljRTBpdFdyM2o5a2kxT2NHY0lieEVFamtWamliMkVEUW16VExwQT0iLCJtYWMiOiI1NGVmYjg3NWI2NGRlMGMwNDM2NzlhZTQ4NmQxNTI4MWJhZjk2NzAyNGQxNTViYmM5YTYxMjUxMjkzNzYxNTBkIiwidGFnIjoiIn0="
}
]
+66
View File
@@ -0,0 +1,66 @@
[
{
"php_version": "8.4.27",
"framework_version": "v13.29.0",
"app_key": "0123456789abcdef0123456789abcdef",
"serialization": "json",
"encrypted": false,
"session_id": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"login_key": "login_web-guard_59ba36addc2b2f9401580f014c7f58ea4e30989d",
"recaller_name": "remember_web-guard_59ba36addc2b2f9401580f014c7f58ea4e30989d",
"payload": "{\"_token\":\"real-php-csrf-\\ud83d\\udd10\",\"login_web-guard_59ba36addc2b2f9401580f014c7f58ea4e30989d\":7,\"preferences\":{\"language\":\"fr\",\"enabled\":true},\"_flash\":{\"old\":[],\"new\":[]}}",
"database_payload": "eyJfdG9rZW4iOiJyZWFsLXBocC1jc3JmLVx1ZDgzZFx1ZGQxMCIsImxvZ2luX3dlYi1ndWFyZF81OWJhMzZhZGRjMmIyZjk0MDE1ODBmMDE0YzdmNThlYTRlMzA5ODlkIjo3LCJwcmVmZXJlbmNlcyI6eyJsYW5ndWFnZSI6ImZyIiwiZW5hYmxlZCI6dHJ1ZX0sIl9mbGFzaCI6eyJvbGQiOltdLCJuZXciOltdfX0=",
"session_cookie": "eyJpdiI6Ik9JbEdWcXg2bGlqalVMWW5OYTNpRUE9PSIsInZhbHVlIjoicHl4R1NtejByVlljdHVsUnpLT29pVjFHdHFmY1EzK3NySklqQUZSR3lseElXWVVDMDAzWUJDamdwdW5HRUs5U09oMG4vQmNLVDk1eFFxNFV4ektsRk1lZGt4QUxkUDFUMEZIKzgwVkxSQWU0eXh4UXN0ekNoSGp6elJaQmlNckoiLCJtYWMiOiJmNTFjNGM2ODY2YTY2MGZjNDdlZTQ5NTdhM2NkYmI3YzNhM2EyNjA5Mzg2MDhhM2NkMjY1YzNhZWMwZTJiY2NhIiwidGFnIjoiIn0%3D",
"csrf_cookie": "eyJpdiI6Ink1N0dFSlNiaTZXclhZZlBXaUxFd3c9PSIsInZhbHVlIjoiMDhQczNtYXNqYU9Bb0JqdVFQVEIrR1RsQUY2eVh6R2N4QUI4UkMrbFRRd2hYQjBsYUxGMDV6U2FQdUQ4OHVkUlFxSkNOT0doQlNTZjF5bWNKUWFSOVE9PSIsIm1hYyI6IjBjNTRjYmY0ZGVjZDk2NTA1YWI1ZjNkYWFmMzZkYzJhMmU2OTFkNTUzYjIwMDAxMjYwNjY1ODNlODlkMDQ2ZTkiLCJ0YWciOiIifQ%3D%3D",
"recaller_cookie": "eyJpdiI6Inl0MmxQaUpNMndXNTNZVGtDd3JtYVE9PSIsInZhbHVlIjoidEZHWjIyd3JaT0ZENW0zUWRSU21nd1pYS1RwZm5jZ0FtY2V5NkNrSjBCL0d1dVlwTFF1SkY1VVBWWnJCbnBNSzlqeWorZ0J1Kyt5bXBDVFYvWURpbEVCNHF3bzJDeDlmNHlMMEh5YklRMWc9IiwibWFjIjoiYzhiMmZiYzBjZDQ0NDJkMjY4ZDFmYmMwNjFlZjVmMDIxY2NmMzBiODJjYmYzODk4ZjJlMTIyNzZiY2FkMDAyYiIsInRhZyI6IiJ9",
"csrf": "real-php-csrf-🔐"
},
{
"php_version": "8.4.27",
"framework_version": "v13.29.0",
"app_key": "0123456789abcdef0123456789abcdef",
"serialization": "json",
"encrypted": true,
"session_id": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"login_key": "login_web-guard_59ba36addc2b2f9401580f014c7f58ea4e30989d",
"recaller_name": "remember_web-guard_59ba36addc2b2f9401580f014c7f58ea4e30989d",
"payload": "eyJpdiI6InB2TjlWUDZZdmY5TVhoTHRia3hXQWc9PSIsInZhbHVlIjoiS2V5cjhSalovUU9TWlVQTVZBWVJaUXo4VFVFeDFIVktJRTBIYzJrL0Vpdmg3NWhBSnRLdjdsdTBDK0g1aVZpTFQ2WmNzcnY5S25TUjJoaHRlbkR6ZHlVSnppYzY2SVhvc0c3cnBFRlJsWi91WmF0TjJmMGNZdG1zeWRObGRpYkN6VGRtSURLWFZ4b3NiNGJFYkdsVDkzNWxFQmY0dC94ZWdTeDQ1UVJya0IwbWZtbFp3NEFGMlpHa3ZkNStlVHlhSFVVZEJiSllmYlkzcTVRTFNxMVBHRlBTeDA1QnMzLzRwaFNGckZXbG41K3ArdWNsdkpYbDlKbmI1UEQ5VHRONCIsIm1hYyI6Ijc4MjQ3YjY3NmRlMDQ1MzBkMjQxNWYyNWNiZGI4ODlkYTc1YmNmODFmZmMwNzY1MjkxNTYxYjA4MjVmNDVkN2EiLCJ0YWciOiIifQ==",
"database_payload": "ZXlKcGRpSTZJbkIyVGpsV1VEWlpkbVk1VFZob1RIUmlhM2hYUVdjOVBTSXNJblpoYkhWbElqb2lTMlY1Y2poU2Fsb3ZVVTlUV2xWUVRWWkJXVkphVVhvNFZGVkZlREZJVmt0SlJUQklZekpyTDBWcGRtZzNOV2hCU25STGRqZHNkVEJESzBnMWFWWnBURlEyV21OemNuWTVTMjVUVWpKb2FIUmxia1I2WkhsVlNucHBZelkyU1ZodmMwYzNjbkJGUmxKc1dpOTFXbUYwVGpKbU1HTlpkRzF6ZVdST2JHUnBZa042VkdSdFNVUkxXRlo0YjNOaU5HSkZZa2RzVkRrek5XeEZRbVkwZEM5NFpXZFRlRFExVVZKeWEwSXdiV1p0YkZwM05FRkdNbHBIYTNaa05TdGxWSGxoU0ZWVlpFSmlTbGxtWWxremNUVlJURk54TVZCSFJsQlRlREExUW5Nekx6UndhRk5HY2taWGJHNDFLM0FyZFdOc2RrcFliRGxLYm1JMVVFUTVWSFJPTkNJc0ltMWhZeUk2SWpjNE1qUTNZalkzTm1SbE1EUTFNekJrTWpReE5XWXlOV05pWkdJNE9EbGtZVGMxWW1ObU9ERm1abU13TnpZMU1qa3hOVFl4WWpBNE1qVm1ORFZrTjJFaUxDSjBZV2NpT2lJaWZRPT0=",
"session_cookie": "eyJpdiI6IjRuRUY4QWt0YTVjdnkrWDg0VnZ2elE9PSIsInZhbHVlIjoiNXZMdHRKL0w1a2gwdnFYRlF5clhUb3BRUEVtRnFyYXVjOXdhMFR1ZTJWZU84a01HaFFYdWdJMTVPTHl3c0NLcHNmVUsraVhzNGVYS0Q1MWYydlM5WDNEL2ltU1NMYXc5Qjk5dnBaU0NDUmxGMUpCMVFDdjB6R0QwZUUzMDdsbEEiLCJtYWMiOiJmZjliM2ZkYmJiNzFmZDZiZDI0YmEzZTVlM2EyZDllOWE5OWJmN2I2YzMyN2I1NzIzMTU0MDkwYTRjZWYzYTM5IiwidGFnIjoiIn0%3D",
"csrf_cookie": "eyJpdiI6Iko1VzU5by9lYVVqc2NIUUpiVEJpa3c9PSIsInZhbHVlIjoibE1DTHRvLzNoUGJIZHhPaHprb0VzNDdNSU5rWUVPMHdTL1BIT2dMZnNOVzR5bGNTVXAyL1htSXZ6enRXVTZwVlVPV3ljckJJaVNDcHRFKzRmbEV3ZFE9PSIsIm1hYyI6IjliNDMwMTQ3NjUyNTU0NDIxNzk2YmU5OWQxNzEzMjc5MjkxNzgwMmNhM2RiMmY4ZThkNjQ0ZjQ4NzkyMGY3MTMiLCJ0YWciOiIifQ%3D%3D",
"recaller_cookie": "eyJpdiI6IkM3bTRrOHVzVWNUVTRvSEZ1Zmx2MWc9PSIsInZhbHVlIjoiMGpRaTVlb0dFb250N2dEUW1BQnl1K1U3V2pMU0RoNE5oOWtRT09nWnFGZUM5czAxaFdVMW9zOVhWNkZJcDlYVmtDeGpnLzJ4dVczeHc5NkpnemtqZGZxaVpTUDUvUW01cmVBUTJSMW10WGs9IiwibWFjIjoiNzUwZmQ5Y2NkYmM0YWNhODlmM2ZkN2NlMzBmOTY3MTg4NGJlMzBkOGMwYjNhZjcxNjg0MWQ5YjgwMTk0NDY1YiIsInRhZyI6IiJ9",
"csrf": "real-php-csrf-🔐"
},
{
"php_version": "8.4.27",
"framework_version": "v13.29.0",
"app_key": "0123456789abcdef0123456789abcdef",
"serialization": "php",
"encrypted": false,
"session_id": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"login_key": "login_web-guard_59ba36addc2b2f9401580f014c7f58ea4e30989d",
"recaller_name": "remember_web-guard_59ba36addc2b2f9401580f014c7f58ea4e30989d",
"payload": "a:4:{s:6:\"_token\";s:18:\"real-php-csrf-🔐\";s:56:\"login_web-guard_59ba36addc2b2f9401580f014c7f58ea4e30989d\";i:7;s:11:\"preferences\";a:2:{s:8:\"language\";s:2:\"fr\";s:7:\"enabled\";b:1;}s:6:\"_flash\";a:2:{s:3:\"old\";a:0:{}s:3:\"new\";a:0:{}}}",
"database_payload": "YTo0OntzOjY6Il90b2tlbiI7czoxODoicmVhbC1waHAtY3NyZi3wn5SQIjtzOjU2OiJsb2dpbl93ZWItZ3VhcmRfNTliYTM2YWRkYzJiMmY5NDAxNTgwZjAxNGM3ZjU4ZWE0ZTMwOTg5ZCI7aTo3O3M6MTE6InByZWZlcmVuY2VzIjthOjI6e3M6ODoibGFuZ3VhZ2UiO3M6MjoiZnIiO3M6NzoiZW5hYmxlZCI7YjoxO31zOjY6Il9mbGFzaCI7YToyOntzOjM6Im9sZCI7YTowOnt9czozOiJuZXciO2E6MDp7fX19",
"session_cookie": "eyJpdiI6IkdKVjlodWFhcXNrMisyUjBhZlRGckE9PSIsInZhbHVlIjoiTjczUWJpTlQ2dndOV1J0N2o3U0YzR2lvNUNOWmtxSnVuamIwUDBkb0NIU0JGZjY2NllxU01oQ0RWYkV6ZUROdWRQa1R3Y0ZQT0xMem1HVVM2alpkeUNNU0VxR0lsZnNvTTUwZVBhaEV5b1NhSDU2QkE3bVNhekZjY1FSVzVQM28iLCJtYWMiOiJjY2ZhMjRmNmJkMGY5MTIyMTJlYWQ4YjY2MGRkYjU3MTYwNzU3MmViMzQ5OTlmYzIzMTUyNjg4N2Q3OTA5NDNkIiwidGFnIjoiIn0%3D",
"csrf_cookie": "eyJpdiI6IkJIWHBRTzBtWlQrY0dVcnVmeEUyU3c9PSIsInZhbHVlIjoibTA4UzRDVDIzd1VTTEpEaDhQdkVyeTVsQjhXalMxaWF3cWRUWWFRQ0tMOVpHZ3R1TkRXQkNnWUs4ZTRsdmxxRVo4Y2FvSUdpald4YlRhelFDaGZXZ3c9PSIsIm1hYyI6IjYwYTQ4M2YwYzIyOWZhNDA2OTFiYjhhYjJjMDMyOWQwY2U2NGE1OTA5NTVmMGU1MjNlNjhlYTM4YzI3MzRlMGEiLCJ0YWciOiIifQ%3D%3D",
"recaller_cookie": "eyJpdiI6Ik9KRG9SVnJKY28zcXVVODR6d0xNeGc9PSIsInZhbHVlIjoiWTllL2ROOUNURnQrNWF4Q2pLMXpDNklUeG5EeFBYWGszRGQ1MEtGT1J5T1hGdUc5OWNrRVlQTTM5cFg1OTBaaW1HeU96b24yOGsvMDBPL3dUMXNBQlpBY1JmQzZqV1lHaWFqdms2MXliUUU9IiwibWFjIjoiNzQ2MjIxMDkyMmI4MGEzM2I0ZjAzZWQzZWEyYWYxMGU3ZmMzZmJmNGRhMTJjZjhiMTYwZDQ2NzZlMjNmOTJkMCIsInRhZyI6IiJ9",
"csrf": "real-php-csrf-🔐"
},
{
"php_version": "8.4.27",
"framework_version": "v13.29.0",
"app_key": "0123456789abcdef0123456789abcdef",
"serialization": "php",
"encrypted": true,
"session_id": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"login_key": "login_web-guard_59ba36addc2b2f9401580f014c7f58ea4e30989d",
"recaller_name": "remember_web-guard_59ba36addc2b2f9401580f014c7f58ea4e30989d",
"payload": "eyJpdiI6IjRoQnYyckJKYTBIQXlQUHBlaml1QUE9PSIsInZhbHVlIjoiQ2NyeUF1Nm0zQmIyOEdBZ2xQWEtPbmlLL0dQbkxlNEZpdmRMY2RvbVYvbU55ZFdUZ3VJK2VtalVUeVV0U2JjeEoySjl5djdSWXkvK3g2d0J1QnJpMG9vajMvU2xSWitucm9XMXh4WHFBUWdwOVVOcytpL1VQblJRazBsNG8xTzNRa2U0OTlUUEhXbFpLSGUyaHd0eFpNNUlkK3MybVBHcU5VRFhtVHNXK0tRR3JrN2lqQmhvL3RNeUxqQktiV1VVZEJWTnVkcEdXdVBjSUtpRU9IN1I2VUNQZnFyTitvR2hQVENMYlZWTWtWL2Y5bTR5N3J2SmdRT0VVVWhxeHArTVZKemVVS2xhU0M4SUwzWFhBNjgvbDU5K09kZzhCK3lrR3JnaXdmMXFZSUlGaU42MStOSHZ4QlJ4ZUNHelV0RTZZaE0wYzc3RXRVTjhydXc1MlRhcnRBPT0iLCJtYWMiOiJlZTRjMWFmYzkyNDIxYjA2ZGRiNjJhOWE1ZTNmNDg2ZDVkYTJmZmJlZWJjZjA4NzEwZjY0YWQzYTdjNTFmZDk0IiwidGFnIjoiIn0=",
"database_payload": "ZXlKcGRpSTZJalJvUW5ZeWNrSktZVEJJUVhsUVVIQmxhbWwxUVVFOVBTSXNJblpoYkhWbElqb2lRMk55ZVVGMU5tMHpRbUl5T0VkQloyeFFXRXRQYm1sTEwwZFFia3hsTkVacGRtUk1ZMlJ2YlZZdmJVNTVaRmRVWjNWSksyVnRhbFZVZVZWMFUySmplRW95U2psNWRqZFNXWGt2SzNnMmQwSjFRbkpwTUc5dmFqTXZVMnhTV2l0dWNtOVhNWGg0V0hGQlVXZHdPVlZPY3l0cEwxVlFibEpSYXpCc05HOHhUek5SYTJVME9UbFVVRWhYYkZwTFNHVXlhSGQwZUZwTk5VbGtLM015YlZCSGNVNVZSRmh0VkhOWEswdFJSM0pyTjJscVFtaHZMM1JOZVV4cVFrdGlWMVZWWkVKV1RuVmtjRWRYZFZCalNVdHBSVTlJTjFJMlZVTlFabkZ5VGl0dlIyaFFWRU5NWWxaV1RXdFdMMlk1YlRSNU4zSjJTbWRSVDBWVlZXaHhlSEFyVFZaS2VtVlZTMnhoVTBNNFNVd3pXRmhCTmpndmJEVTVLMDlrWnpoQ0szbHJSM0puYVhkbU1YRlpTVWxHYVU0Mk1TdE9TSFo0UWxKNFpVTkhlbFYwUlRaWmFFMHdZemMzUlhSVlRqaHlkWGMxTWxSaGNuUkJQVDBpTENKdFlXTWlPaUpsWlRSak1XRm1Zemt5TkRJeFlqQTJaR1JpTmpKaE9XRTFaVE5tTkRnMlpEVmtZVEptWm1KbFpXSmpaakE0TnpFd1pqWTBZV1F6WVRkak5URm1aRGswSWl3aWRHRm5Jam9pSW4wPQ==",
"session_cookie": "eyJpdiI6IjF0RWEzNTZ4cDREZ0tibFRjQTZxK3c9PSIsInZhbHVlIjoicDh3RnVTZTcwdW40enpBMEpZeUV3YitieEpnSGg1dXJ1UUh6RFhzMVVXME52QkUxcStIaksvcFBtZ2lkeUdBcEdPbHFjM0NPSTRwZ1YzZ1k3WTFJZWlnMlUvS1pQai9FaFpJRllCa0hUODlIM0UzN3cxQjJaNURGcFhaazk2L0UiLCJtYWMiOiI5ZWUzNTk5MWQ5YzA4YmVkYTRkMzk3ZDJmYzlhYjAxNThlODVhZWI2N2ZmYTVlNDhmYmRmYTQ0NzMxOWIzYzhlIiwidGFnIjoiIn0%3D",
"csrf_cookie": "eyJpdiI6IkRLVERLOVdYUTRmdVhuemR0ajZpVGc9PSIsInZhbHVlIjoiMWtWSHRKSzJvRVZQVUY3SzZuN3ZEc3pFWDN3K2hWOHBkbi94aW5Wc0MreElrRmJlenk2YUxheXJGaXFSMFNLR3lPSVRFaTRCdXF1ZVlGU1BkUnBWL1E9PSIsIm1hYyI6IjkxZmNhNWMxNmQ3NzU1NTQ4MDdkMzI0YmM5ZTEwYmUxYmI5ZmE2OGFjZTY1NGZjZGEyMmY5NGY0MGUxNTA5MTgiLCJ0YWciOiIifQ%3D%3D",
"recaller_cookie": "eyJpdiI6IlA3ZEZKSWIveGMyOHg3a0VRVCtNNVE9PSIsInZhbHVlIjoiWldCMWNwbFB2RWQ3ZUNVV1JWbFZmdFRoc3A0Yjg4MGc1LzVlbVN1cG1YWGw5NXhBQkthdHdTc0Npb0Jxa004cG5yczY2WUpwaWRmbVdyRklDMk5UeSsxVUMrL0Q5UXhZQzRFbGRvd09lb289IiwibWFjIjoiNDc3YWIwZWNhZTZmMjI1MWQ1ZmFkM2E2YmMyZThhOTU0MmZhNDQwMDQxZGUwYTQzMmRhYWNiMDYyYTI2MTI1YSIsInRhZyI6IiJ9",
"csrf": "real-php-csrf-🔐"
}
]
+1
View File
@@ -0,0 +1 @@
eyJpdiI6InF5Z3B4YUx5OEJtU3FjV3hxMk9HVHc9PSIsInZhbHVlIjoiSXZUcExVWFB6ekRCaVFKOE5KTllUVmJ6RHRXcXRGa0UwdXJnU1NNS0RnbFk1WUxWOTJ5ZDFKWVVQenJCMFN4aWVTY3N5SHRsdXVKWnlWTUNDWjFvUlAxMXF0Q0UyNTNuTkNYZW83dUFWaDhDTnVhWFNMWnIwdnZGVWJWOWY3VStzNkdNS1pPKzgwZDBjZS9La055d2RnVmRRbVh5RkZxVUlCc1MwTFVjVzFBZ2ZpMkhpZENwbTFmUFRrZXZvQy90N3pSejV0bU5WMlRqTXhxVTBVOVFFSkdNblVCa0s1anhXMStyTElkdFFDZmdiMGFNeG5hVE04VllrN21Sd2czNyIsIm1hYyI6IjBiM2YxMmQxZWFkN2ZjY2NmNzYxZmJkMWIwOWE0NmZiYzY5YmRlMTJkMTgzNzU5NjkxMmI2NzE0NTY5YjA3OGUiLCJ0YWciOiIifQ==
+5
View File
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg7178IxdRG58LwZZk
Xg9yQP4t/DKpdujw3cBDlhF/XNqhRANCAAT+za5CCkZmGVLRojW4r9HHnjNfZPAZ
jeizNmztcl/zTsjlN+QbpYfNlqQ03ZpiDchSD3mV4jAjjJULjCle3+Cv
-----END PRIVATE KEY-----
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Launch the built binary against disposable synthetic data; never use a real DB."""
import base64
import hashlib
import hmac
import http.cookiejar
import json
import os
from pathlib import Path
import signal
import socket
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request
def main():
root = Path(__file__).resolve().parents[1]
binary = Path(sys.argv[1] if len(sys.argv) > 1 else root / 'target/debug/twofauth-rs').resolve()
with tempfile.TemporaryDirectory(prefix='twofauth-rs-smoke-') as folder:
work = Path(folder)
password = work / 'password'
password.write_text('Synthetic-password-4620')
password.chmod(0o600)
env = dict(os.environ, DATABASE_URL=f'sqlite://{work}/test.sqlite', RUST_COOKIE_SECURE='false', RUST_PUBLIC_DIR=str(root / 'public'))
env.pop('APP_SUBDIRECTORY', None)
env.pop('APP_KEY', None)
env.pop('APP_PREVIOUS_KEYS', None)
subprocess.run([str(binary), 'init'], env=env, check=True, capture_output=True, cwd=work)
duplicate = subprocess.run([str(binary), 'init'], env=env, capture_output=True, cwd=work)
assert duplicate.returncode != 0, 'init must refuse an existing users table'
subprocess.run([str(binary), 'create-user', '--name', 'Synthetic', '--email', 'smoke@example.invalid', '--password-file', str(password)], env=env, check=True, capture_output=True, cwd=work)
password.unlink()
with socket.socket() as reserve:
reserve.bind(('127.0.0.1', 0))
port = reserve.getsockname()[1]
base = f'http://127.0.0.1:{port}'
jar = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
process = subprocess.Popen([str(binary), 'serve', '--listen', f'127.0.0.1:{port}'], env=env, cwd=work, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
def request(path, method='GET', value=None, csrf=None):
data = None if value is None else json.dumps(value).encode()
headers = {'Content-Type': 'application/json', 'Accept': 'application/json'}
if csrf is not None:
headers['X-XSRF-TOKEN'] = csrf
req = urllib.request.Request(base + path, data=data, headers=headers, method=method)
try:
response = opener.open(req, timeout=10)
except urllib.error.HTTPError as error:
response = error
with response:
body = response.read()
try:
payload = json.loads(body)
except (ValueError, UnicodeDecodeError):
payload = body.decode()
return response.status, payload, response.headers
for _ in range(100):
if process.poll() is not None:
raise AssertionError('server exited during startup')
try:
if request('/up')[0] == 200:
break
except urllib.error.URLError:
time.sleep(0.05)
else:
raise AssertionError('server startup timed out')
assert request('/api/v1/twofaccounts')[0] == 401
status, html, headers = request('/')
assert status == 200 and 'var appSettings' in html
assert headers['Content-Security-Policy']
csrf = next(cookie.value for cookie in jar if cookie.name == 'XSRF-TOKEN')
assert request('/user/login', 'POST', {'email': 'smoke@example.invalid', 'password': 'Synthetic-password-4620'})[0] == 419
status, login, _ = request('/user/login', 'POST', {'email': 'SMOKE@example.invalid', 'password': 'Synthetic-password-4620'}, csrf)
assert status == 200
csrf = next(cookie.value for cookie in jar if cookie.name == 'XSRF-TOKEN')
assert request('/api/v1/user')[1]['email'] == 'smoke@example.invalid'
account = {'otp_type': 'hotp', 'account': 'Synthetic', 'secret': base64.b32encode(b'12345678901234567890').decode(), 'counter': 0, 'digits': 6, 'algorithm': 'sha1'}
assert request('/api/v1/twofaccounts', 'POST', account)[0] == 419
status, created, _ = request('/api/v1/twofaccounts', 'POST', account, csrf)
assert status == 201
account_id = created['id']
listed = request('/api/v1/twofaccounts')[1]
assert isinstance(listed, list) and len(listed) == 1 and 'secret' not in listed[0]
for counter in range(2):
status, otp, _ = request(f'/api/v1/twofaccounts/{account_id}/otp')
digest = hmac.new(b'12345678901234567890', counter.to_bytes(8, 'big'), hashlib.sha1).digest()
offset = digest[-1] & 15
expected = f'{(int.from_bytes(digest[offset:offset+4], "big") & 0x7fffffff) % 1000000:06d}'
assert status == 200 and otp['password'] == expected
assert request('/api/v1/settings')[0] == 403
assert request('/user/logout')[0] == 419
assert request('/user/logout', csrf=csrf)[0] == 200
assert request('/api/v1/twofaccounts')[0] == 401
print(json.dumps({'cli_init': 'passed', 'existing_db_refusal': 'passed', 'test_user': 'passed', 'http_login_crud_hotp_logout': 'passed', 'php_differential': 'not_run'}))
finally:
process.send_signal(signal.SIGINT)
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
if __name__ == '__main__':
main()
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""Process-level Laravel browser migration checks using real pinned PHP fixtures.
Usage: python3 tests/laravel_session_http.py /path/to/twofauth-rs
All databases, users, cookies and session files are disposable synthetic fixtures.
Regenerate fixtures with tests/fixtures/generate_laravel_sessions.php.
"""
import base64
from http.cookies import SimpleCookie
import json
import os
from pathlib import Path
import socket
import sqlite3
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request
ROOT = Path(__file__).resolve().parents[1]
NATIVE_COOKIE = "rust_2fauth_session"
class Browser:
def __init__(self, base, cookies=None):
self.base = base
self.cookies = dict(cookies or {})
def request(self, path, method="GET", csrf=None, update=True):
headers = {"Accept": "application/json"}
if self.cookies:
headers["Cookie"] = "; ".join(f"{k}={v}" for k, v in self.cookies.items())
if csrf is not None:
headers["X-XSRF-TOKEN"] = csrf
request = urllib.request.Request(self.base + path, headers=headers, method=method)
try:
response = urllib.request.urlopen(request, timeout=10)
except urllib.error.HTTPError as error:
response = error
with response:
raw = response.read()
try:
body = json.loads(raw)
except (ValueError, UnicodeDecodeError):
body = raw.decode()
if update:
for header in response.headers.get_all("Set-Cookie", []):
parsed = SimpleCookie()
parsed.load(header)
for name, value in parsed.items():
if value["max-age"] == "0":
self.cookies.pop(name, None)
else:
self.cookies[name] = value.value
return response.status, body, response.headers
def run_case(binary, fixture, driver):
label = f"{driver}/{fixture['serialization']}/encrypt={fixture['encrypted']}"
with tempfile.TemporaryDirectory(prefix="laravel-browser-") as folder:
work = Path(folder)
storage = work / "storage"
sessions = storage / "framework/sessions"
sessions.mkdir(parents=True)
env = dict(os.environ, DATABASE_URL=f"sqlite://{work}/test.sqlite",
APP_KEY="base64:" + base64.b64encode(fixture["app_key"].encode()).decode(),
APP_NAME="2FAuth", SESSION_COOKIE="2fauth_session", SESSION_DRIVER=driver,
SESSION_ENCRYPT=str(fixture["encrypted"]).lower(), SESSION_TABLE="sessions",
SESSION_SECURE_COOKIE="false", SESSION_PATH="/", SESSION_DOMAIN="",
STORAGE_PATH=str(storage), RUST_COOKIE_SECURE="false",
RUST_PUBLIC_DIR=str(ROOT / "public"))
for name in ("APP_SUBDIRECTORY", "APP_PREVIOUS_KEYS", "AUTH_GUARD", "AUTHENTICATION_GUARD"):
env.pop(name, None)
subprocess.run([str(binary), "init"], env=env, cwd=work, check=True, capture_output=True)
db = sqlite3.connect(work / "test.sqlite")
db.execute("INSERT INTO users(id,name,email,password,remember_token) VALUES(7,'PHP fixture','fixture@example.invalid','unused','remember-fixture')")
db.execute("CREATE TABLE IF NOT EXISTS sessions(id TEXT PRIMARY KEY,user_id INTEGER,ip_address TEXT,user_agent TEXT,payload TEXT,last_activity INTEGER)")
sid = fixture["session_id"]
source = sessions / sid
if driver == "file":
source.write_text(fixture["payload"])
else:
db.execute("INSERT INTO sessions(id,user_id,payload,last_activity) VALUES(?,7,?,?)",
(sid, fixture["database_payload"], int(time.time())))
db.commit()
with socket.socket() as reserve:
reserve.bind(("127.0.0.1", 0))
port = reserve.getsockname()[1]
base = f"http://127.0.0.1:{port}"
process = subprocess.Popen([str(binary), "serve", "--listen", f"127.0.0.1:{port}"],
env=env, cwd=work, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
anonymous = Browser(base)
for _ in range(100):
if process.poll() is not None:
raise AssertionError(f"{label}: server exited during startup")
try:
if anonymous.request("/up")[0] == 200:
break
except urllib.error.URLError:
time.sleep(.05)
else:
raise AssertionError(f"{label}: startup timed out")
original = {"2fauth_session": fixture["session_cookie"],
"XSRF-TOKEN": fixture["csrf_cookie"]}
# A browser need not visit the SPA before refreshing its Passport cookie.
direct = Browser(base, original)
assert direct.request("/oauth/token/refresh", "POST")[0] == 419, label
status, body, _ = direct.request("/oauth/token/refresh", "POST", fixture["csrf_cookie"])
assert status == 200 and body == "Refreshed.", (label, status, body)
assert "laravel_token" in direct.cookies, label
status, user, _ = direct.request("/api/v1/user", csrf=fixture["csrf_cookie"])
assert status == 200 and user["id"] == 7, (label, status, user)
# Start another browser with exactly the PHP session + recaller: no
# laravel_token and no native-session cookie exist before root GET.
original[fixture["recaller_name"]] = fixture["recaller_cookie"]
browser = Browser(base, original)
assert "laravel_token" not in browser.cookies and NATIVE_COOKIE not in browser.cookies
status, html, _ = browser.request("/")
assert status == 200 and "var appSettings" in html, (label, status)
assert NATIVE_COOKIE in browser.cookies, label
assert browser.cookies["XSRF-TOKEN"] != fixture["csrf_cookie"], label
assert "2fauth_session" not in browser.cookies, label
assert fixture["recaller_name"] not in browser.cookies, label
if driver == "file":
assert not source.exists(), f"{label}: source PHP file was not destroyed"
else:
assert db.execute("SELECT count(*) FROM sessions WHERE id=?", (sid,)).fetchone()[0] == 0, label
status, user, _ = browser.request("/api/v1/user")
assert status == 200 and user["id"] == 7, (label, status, user)
assert db.execute("SELECT user_id FROM rust_sessions").fetchone()[0] == 7, label
# Minting a fresh Passport cookie must keep the native browser usable.
csrf = browser.cookies["XSRF-TOKEN"]
status, body, _ = browser.request("/oauth/token/refresh", "POST", csrf)
assert status == 200 and body == "Refreshed.", (label, status, body)
assert "laravel_token" in browser.cookies, label
for header in (None, csrf):
status, user, _ = browser.request("/api/v1/user", csrf=header)
assert status == 200 and user["id"] == 7, (label, status, user)
native_before_logout = browser.cookies[NATIVE_COOKIE]
assert browser.request("/user/logout", "POST", csrf)[0] == 200, label
assert db.execute("SELECT remember_token FROM users WHERE id=7").fetchone()[0] != "remember-fixture", label
assert browser.request("/api/v1/user")[0] == 401, label
replay = Browser(base, original)
# Exercise the actual import path: the API alone never reads a
# Laravel session, so an API-only replay would miss a valid recaller.
assert replay.request("/")[0] == 200, label
assert replay.request("/api/v1/user", csrf=replay.cookies.get("XSRF-TOKEN"))[0] == 401, f"{label}: PHP session/recaller replay accepted"
assert Browser(base, {NATIVE_COOKIE: native_before_logout}).request("/api/v1/user")[0] == 401, f"{label}: native session replay accepted"
return label
finally:
db.close()
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
def main():
binary = Path(sys.argv[1] if len(sys.argv) > 1 else ROOT / "target/debug/twofauth-rs").resolve()
fixtures = json.loads((ROOT / "tests/fixtures/laravel_sessions.json").read_text())
passed = [run_case(binary, fixture, driver) for fixture in fixtures for driver in ("file", "database")]
print(json.dumps({"php_session_browser_migration": "passed", "cases": passed}))
if __name__ == "__main__":
main()
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Disposable OAuth integration test against the pinned PHP cryptographic wire format."""
import base64
import hashlib
import json
import os
from pathlib import Path
import socket
import sqlite3
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
ROOT=Path(__file__).resolve().parents[1]
PHP=os.environ.get('PHP_BINARY','/tmp/twofauth-reference-runtime/bin/php')
AUTOLOAD=ROOT.parent/'upstream/vendor/autoload.php'
def php(code,*args):
return subprocess.check_output([PHP,'-r',f'require {json.dumps(str(AUTOLOAD))};'+code,*args],text=True).strip()
def main():
binary=Path(sys.argv[1] if len(sys.argv)>1 else ROOT/'target/debug/twofauth-rs').resolve()
with tempfile.TemporaryDirectory(prefix='oauth-wire-') as d:
work=Path(d)
subprocess.run(['openssl','genrsa','-out',str(work/'oauth-private.key'),'2048'],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
subprocess.run(['openssl','rsa','-in',str(work/'oauth-private.key'),'-pubout','-out',str(work/'oauth-public.key')],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
app_key='0123456789abcdef0123456789abcdef'
env=dict(os.environ,DATABASE_URL=f'sqlite://{work}/test.sqlite',APP_KEY='base64:'+base64.b64encode(app_key.encode()).decode(),PASSPORT_KEY_PATH=d,RUST_COOKIE_SECURE='false',RUST_PUBLIC_DIR=str(ROOT/'public'))
env.pop('APP_SUBDIRECTORY',None)
subprocess.run([str(binary),'init'],check=True,env=env,cwd=work,capture_output=True)
with socket.socket() as sock:
sock.bind(('127.0.0.1',0));port=sock.getsockname()[1]
proc=subprocess.Popen([str(binary),'serve','--listen',f'127.0.0.1:{port}'],env=env,cwd=work,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
def request(values,path='/oauth/token'):
data=urllib.parse.urlencode(values).encode() if values is not None else None
req=urllib.request.Request(f'http://127.0.0.1:{port}'+path,data=data,headers={'Content-Type':'application/x-www-form-urlencoded','Accept':'application/json'})
try:r=urllib.request.urlopen(req,timeout=10)
except urllib.error.HTTPError as e:r=e
with r:return r.status,json.loads(r.read())
try:
for _ in range(100):
try:
if request({'grant_type':'bogus'})[1].get('error')=='unsupported_grant_type':break
except (OSError,ValueError):time.sleep(.05)
else:raise AssertionError('OAuth route unavailable')
con=sqlite3.connect(work/'test.sqlite')
secret=php('echo password_hash("secret",PASSWORD_BCRYPT);')
con.execute("INSERT INTO users(id,name,email,password) VALUES(1,'OAuth','oauth@example.invalid','unused')")
con.execute("INSERT INTO oauth_clients(id,name,secret,redirect_uris,grant_types) VALUES(1,'public',NULL,'[\"https://example.test/cb\"]','[\"authorization_code\",\"refresh_token\"]')")
con.execute("INSERT INTO oauth_clients(id,name,secret,redirect_uris,grant_types) VALUES(2,'machine',?,'[]','[\"client_credentials\"]')",(secret,))
verifier='a'*43
payload={'client_id':'1','auth_code_id':'php-auth-code','user_id':'1','scopes':[],'redirect_uri':'https://example.test/cb','expire_time':int(time.time())+600,'code_challenge':base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).decode().rstrip('='),'code_challenge_method':'S256'}
code=php('echo Defuse\\Crypto\\Crypto::encryptWithPassword($argv[1],$argv[2]);',json.dumps(payload),app_key)
con.execute("INSERT INTO oauth_auth_codes(id,user_id,client_id,scopes,revoked,expires_at) VALUES('php-auth-code',1,1,'[]',0,datetime('now','+10 minutes'))")
con.commit()
params={'grant_type':'authorization_code','client_id':'1','redirect_uri':payload['redirect_uri'],'code':code,'code_verifier':'b'*43}
assert request(params)[1]['error']=='invalid_grant'
params['code_verifier']=verifier
status,tokens=request(params);assert status==200,(status,tokens)
assert request(params)[1]['error']=='invalid_grant','authorization code replay accepted'
claims=json.loads(base64.urlsafe_b64decode(tokens['access_token'].split('.')[1]+'=='))
assert claims['sub']=='1' and claims['scopes']==[]
verified=php('$p=explode(".",$argv[1]);echo openssl_verify($p[0].".".$p[1],base64_decode(strtr($p[2],"-_","+/")),file_get_contents($argv[2]),OPENSSL_ALGO_SHA256);',tokens['access_token'],str(work/'oauth-public.key'))
assert verified=='1','PHP rejected Rust JWT signature'
decoded=json.loads(php('echo Defuse\\Crypto\\Crypto::decryptWithPassword($argv[1],$argv[2]);',tokens['refresh_token'],app_key))
assert decoded['user_id']=='1' and decoded['access_token_id']==claims['jti']
# PHP decrypts and re-encrypts the Rust refresh payload before redemption.
php_refresh=php('echo Defuse\\Crypto\\Crypto::encryptWithPassword($argv[1],$argv[2]);',json.dumps(decoded),app_key)
refresh={'grant_type':'refresh_token','client_id':'1','refresh_token':php_refresh}
status,rotated=request(refresh);assert status==200,(status,rotated)
assert request(refresh)[1]['error']=='invalid_grant','refresh replay accepted'
assert con.execute('SELECT revoked FROM oauth_access_tokens WHERE id=?',(claims['jti'],)).fetchone()[0]==1
status,machine=request({'grant_type':'client_credentials','client_id':'2','client_secret':'secret','scope':'*'})
assert status==200,(status,machine)
assert 'refresh_token' not in machine
machine_claims=json.loads(base64.urlsafe_b64decode(machine['access_token'].split('.')[1]+'=='))
assert machine_claims['sub']=='2' and machine_claims['scopes']==['*']
assert con.execute('SELECT user_id FROM oauth_access_tokens WHERE id=?',(machine_claims['jti'],)).fetchone()[0] is None
print(json.dumps({'php_authorization_code_pkce':'passed','rust_jwt_php_signature':'passed','rust_refresh_php_decryption':'passed','php_refresh_rotation_replay':'passed','client_credentials_wildcard':'passed'}))
finally:
proc.terminate()
try:proc.wait(timeout=5)
except subprocess.TimeoutExpired:proc.kill();proc.wait()
if __name__=='__main__':main()