Initial Commit

This commit is contained in:
root 2022-11-27 00:04:34 +01:00
commit 8ca36409fd
45 changed files with 3667 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
# Composer
composer.phar
/vendor/
# VS Code
/.vscode/
settings.json
# Certificate Authority
/ca/
# User Data
/data/
# Twig Compilation Cache
/compilation_cache/

7
.htaccess Normal file
View File

@ -0,0 +1,7 @@
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)$ index.php [QSA,L]

29
composer.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "161sh/gatekeeper",
"description": "SSO Identity Management for 161.sh",
"license": "AGPL-3.0-only",
"minimum-stability": "dev",
"version": "1.0",
"require": {
"php": ">=7.4",
"gac/routing": "dev-main",
"twig/twig": "^3.0",
"ext-json": "*",
"161sh/seriousjson": "@dev"
},
"require-dev": {
"phpunit/phpunit": "^8"
},
"autoload": {
"psr-0": {
"Tor": "src/",
"Cave": "src/"
}
},
"repositories": [
{
"type": "path",
"url": "../SeriousJSON"
}
]
}

1794
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

0
docs/API/README.md Normal file
View File

62
files/css/clippy.css Executable file
View File

@ -0,0 +1,62 @@
.clippy, .clippy-balloon {
position: fixed;
z-index: 1000;
cursor: pointer;
}
.clippy-balloon {
background: #FFC;
color: black;
padding: 8px;
border: 1px solid black;
border-radius: 5px;
}
.clippy-content {
max-width: 200px;
min-width: 120px;
font-family: "Microsoft Sans", sans-serif;
font-size: 10pt;
}
.clippy-tip {
width: 10px;
height: 16px;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAgCAMAAAAlvKiEAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAlQTFRF///MAAAA////52QwgAAAAAN0Uk5T//8A18oNQQAAAGxJREFUeNqs0kEOwCAIRFHn3//QTUU6xMyyxii+jQosrTPkyPEM6IN3FtzIRk1U4dFeKWQiH6pRRowMVKEmvronEynkwj0uZJgR22+YLopPSo9P34wJSamLSU7lSIWLJU7NkNomNlhqxUeAAQC+TQLZyEuJBwAAAABJRU5ErkJggg==) no-repeat;
position: absolute;
}
.clippy-top-left .clippy-tip {
top: 100%;
margin-top: 0px;
left: 100%;
margin-left: -50px;
}
.clippy-top-right .clippy-tip {
top: 100%;
margin-top: 0px;
left: 0;
margin-left: 50px;
background-position: -10px 0;
}
.clippy-bottom-right .clippy-tip {
top: 0;
margin-top: -16px;
left: 0;
margin-left: 50px;
background-position: -10px -16px;
}
.clippy-bottom-left .clippy-tip {
top: 0;
margin-top: -16px;
left: 100%;
margin-left: -50px;
background-position: 0px -16px;
}

137
files/css/style.css Executable file
View File

@ -0,0 +1,137 @@
@font-face {
font-family: "Merriweather";
src: url("/files/media/font.ttf");
}
* {
margin: 0;
padding: 0;
font-family: 'Merriweather';
box-sizing: border-box;
cursor: url('/files/media/cursor.cur'), auto;
cursor: -webkit-image-set(
url('/files/media/cursor.png') 1x,
url('/files/media/cursor2x.png') 2x
), auto;
}
body {
background-color: #008080;
text-align: center;
color: black;
/*display: grid;
place-items: center;*/
height: 100%;
}
.padring {
padding: 3%;
}
.container {
width:650px;
background:#C0C0C0;
border-width:1px;
border-color:#FFFFFF #808080 #808080 #FFFFFF;
border-style:solid;
position:absolute;
top:50%;
left:50%;
transform: translate(-50%,-50%);
padding:1px;
}
.banner {
width: 100%;
}
.left {
width: 394px;
display: inline-block;
text-align: left;
font-size: 80%;
padding: 5px;
}
.right {
width: 248px;
display: inline-block;
text-align: right;
font-size: 80%;
vertical-align: top;
padding: 5px;
}
button:focus{
outline:none;
}
div.title{
height:18px;
width:100%;
background:#000080;
color:#FFF;
}
img.title{
float:left;
}
p.title{
margin:2px 0 0 1px;
float:left;
font-weight:bold;
font-size:11px;
}
.title button{
/*position:absolute;
right:3px;
top:3px;*/
margin:2px 2px 2px 0;
float:right;
width:14px;
height:13.5px;
background:#C0C0C0;
border-width:1px;
border-color:#FFFFFF #808080 #808080 #FFFFFF;
padding:0;
font-size:9px;
font-weight:bold;
text-align:center;
focus:none;
}
input, textarea {
background:#EEE;
border-width:1px;
border-color:#FFFFFF #808080 #808080 #FFFFFF;
border-style:solid;
padding:5px;
margin:3px;
}
input[type=submit], .button, .keygen-link {
font-size:14px;
outline:1px solid #000000;
background:#C0C0C0;
border-width:1px;
border-style:solid;
border-color:#FFFFFF #808080 #808080 #FFFFFF;
padding:4px 10px;
margin:20px 3px;
display:inline;
position:relative;
bottom:0px;
}
.keygen-algorithm { /* algorithm dropdown (select) */ }
.keygen-pkeyopts { display: none; /* additional key options (text input) */ }
.keygen-status { /* status/progress (span) */ }
.keygen-link { /* generated certificate download link (a) */ }
.powered {
color: #444;
text-shadow: #DDD 1px 1px 0;
border-top: 2px groove #DDD;
}
::selection {
background-color: orange;
}

341
files/js/browser.openssl.js Executable file

File diff suppressed because one or more lines are too long

1
files/js/clippy.min.js vendored Executable file

File diff suppressed because one or more lines are too long

4
files/js/jquery.min.js vendored Executable file

File diff suppressed because one or more lines are too long

404
files/js/keygen.js Executable file
View File

@ -0,0 +1,404 @@
/*
* Copyright 2020 Vladimir Panteleev
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* jshint esversion : 8, laxbreak:true */
function isKeygenNativelySupported() {
// TODO: iOS - https://github.com/Modernizr/Modernizr/issues/1075
var keygen = document.createElement('keygen');
if ('challenge' in keygen)
return 'blink';
var template = document.createElement('div');
template.innerHTML = '<form><keygen class="keygen"></form>';
keygen = template.getElementsByClassName('keygen')[0];
if (keygen.tagName.toLowerCase() === 'select')
return 'gecko';
return false;
}
function keygenJS(OpenSSL) {
if (isKeygenNativelySupported())
return;
const supportedAlgorithms = [ 'RSA', 'RSA-PSS', 'X25519',
'X448', 'ED25519', 'ED448' ];
function wipe(arr) {
for (var i = 0; i < arr.length; i++)
arr[i] = 0xFE;
}
function nextPowerOfTwo(v) {
var p = 2;
while ((v >>= 1)) {
p <<= 1;
}
return p;
}
function newSession() {
// openssl.js suggests using wasmfs, however, it adds a
// lot of dependencies and is difficult to package, so
// just implement a basic virtual FS here.
let files = { };
let lineBuf = '';
let tty = {
write : function(arr) {
for (var c of arr)
if (c == 10) {
console.log(lineBuf); lineBuf = '';
} else {
lineBuf += String.fromCharCode(c);
}
}
};
let fds = [ { }, tty, tty ];
let impl = {
existsSync : function(fn) {
// console.log('existsSync', fn);
return files.hasOwnProperty(fn);
},
realpathSync : function(n) {
return n;
},
mkdirSync : function() {
return false;
},
openSync : function(fn, flags) {
// console.log('openSync', fn, flags);
var ro = flags & 1;
if (!ro)
files[fn] = new Uint8Array(0);
else
if (!files.hasOwnProperty(fn))
return -1;
var pos = 0;
fds.push({
read : function(length) {
var arr = files[fn].subarray(pos, pos + length);
pos += arr.length;
return arr;
},
write : ro ? null : function(arr) {
// console.log('write', arr.length, '@', pos, '/', files[fn].buffer.byteLength);
if (files[fn].buffer.byteLength < pos + arr.length) {
var newSize = nextPowerOfTwo(pos + arr.length);
var newFile = new Uint8Array(newSize);
newFile.set(files[fn]);
wipe(new Uint8Array(files[fn].buffer));
files[fn] = newFile.subarray(0, pos);
}
var target = new Uint8Array(files[fn].buffer, pos, arr.length);
target.set(arr);
pos += arr.length;
files[fn] = new Uint8Array(files[fn].buffer, 0, pos);
wipe(arr);
},
});
return fds.length - 1;
},
readSync : function(fd, buffer, offset, length, position) {
// console.log(fd, buffer, offset, length);
if (fd < 0) return 0;
var arr = fds[fd].read(length);
buffer.subarray(offset, offset + arr.length).set(arr);
return arr.length;
},
writeSync : function(fd, buffer) {
if (fd < 0) return 0;
fds[fd].write(buffer);
return buffer.length;
},
closeSync : function(fd) {
if (fd < 0) return;
fds[fd] = null;
},
fstatSync : function() {
return {
isBlockDevice : () => false,
isCharacterDevice : () => false,
isDirectory : () => false,
isFIFO : () => false,
isFile : () => true,
};
},
constants : {
O_RDONLY : 1,
},
};
let openSSL = new OpenSSL({
fs: impl,
rootDir: '/'
});
return { files, openSSL };
}
async function genSpkac(algorithm, pkeyopts, challenge) {
let s = newSession();
if (algorithm === undefined)
algorithm = 'RSA';
if (!algorithm.length || algorithm.match(/[\s]/))
throw 'Invalid algorithm';
if (challenge !== undefined && (!challenge.length || challenge.match(/[\s]/))) {
// https://github.com/DigitalArsenal/openssl.js/issues/3
throw 'Invalid challenge string';
}
pkeyopts = pkeyopts.split(/[\s]{1,}/g).filter(Boolean);
await s.openSSL.runCommand(
"genpkey" +
" -algorithm " + algorithm +
pkeyopts.map(opt => " -pkeyopt " + opt) +
" -out /private.pem");
if (!s.files.hasOwnProperty('/private.pem') || !s.files['/private.pem'].length)
throw 'Private key generation failed';
await s.openSSL.runCommand(
"spkac" +
" -key /private.pem" +
" -out /spkac" +
(challenge === undefined ? "" : " -challenge " + challenge)
);
if (!s.files.hasOwnProperty('/spkac') || !s.files['/spkac'].length)
throw 'Private key generation failed';
var privateKey = s.files['/private.pem'];
var spkac = s.files['/spkac'];
spkac = spkac.subarray(6, spkac.length - 1); // Skip "SKPAC=" and trim line ending
spkac = String.fromCharCode.apply(null, spkac);
return { spkac, privateKey };
}
async function convertDerToPem(der) {
let s = newSession();
s.files['/cert.der'] = der;
await s.openSSL.runCommand(
"x509" +
" -inform der" +
" -in /cert.der" +
" -outform pem" +
" -out /cert.pem");
if (!s.files.hasOwnProperty('/cert.pem') || !s.files['/cert.pem'].length)
throw 'Certificate conversion failed';
return s.files['/cert.pem'];
}
async function convertToPkcs12(key, pem) {
let s = newSession();
s.files['/private.pem'] = key;
s.files['/cert.pem'] = pem;
await s.openSSL.runCommand(
"pkcs12" +
" -export" +
" -in /cert.pem" +
" -inkey /private.pem" +
" -out /cert.p12" +
" -passout pass:mac");
if (!s.files.hasOwnProperty('/cert.p12') || !s.files['/cert.p12'].length)
throw 'Certificate conversion failed';
return s.files['/cert.p12'];
}
async function base64Encode(data) {
let s = newSession();
s.files['/data.bin'] = data;
await s.openSSL.runCommand(
"base64" +
" -e" +
" -in /data.bin" +
" -out /data.txt");
if (!s.files.hasOwnProperty('/data.txt'))
throw 'Base64 encoding failed';
return s.files['/data.txt'];
}
function polyfillKeygen(keygen) {
var name = keygen.getAttribute('name');
if (!name) throw '<keygen> element has no name';
var autofocus = keygen.hasAttribute('autofocus');
if (autofocus)
keygen.removeAttribute('autofocus');
var disabled = keygen.hasAttribute('disabled');
if (keygen.hasAttribute('keyparams')) // Don't do the wrong thing
throw 'Sorry, the keyparams <keygen> attribute is not supported';
var algoSelect;
if (!keygen.hasAttribute('keytype')) {
algoSelect = document.createElement('select');
algoSelect.setAttribute('title', 'Private key algorithm');
algoSelect.setAttribute('class', 'keygen-algorithm');
for (var algoName of supportedAlgorithms) {
var algoOption = document.createElement('option');
algoOption.setAttribute('value', algoName);
algoOption.textContent = algoName;
algoSelect.appendChild(algoOption);
}
if (autofocus) {
algoSelect.setAttribute('autofocus', '');
autofocus = false;
}
if (disabled)
algoSelect.setAttribute('disabled', '');
keygen.appendChild(algoSelect);
}
var optInput = document.createElement('input');
optInput.setAttribute('title', 'Space-separated public key generation algorithm options\n' +
'(e.g. "rsa_keygen_bits:4096" for a 4096-bit RSA key)');
optInput.setAttribute('class', 'keygen-pkeyopts');
optInput.setAttribute('placeholder', 'optional private key options');
if (autofocus) {
optInput.setAttribute('autofocus', '');
autofocus = false;
}
if (disabled)
optInput.setAttribute('disabled', '');
keygen.appendChild(optInput);
var status = document.createElement('span');
status.setAttribute('class', 'keygen-status');
status.textContent = 'Ready!';
keygen.appendChild(status);
var resultLink = document.createElement('a');
resultLink.setAttribute('class', 'keygen-link-inv');
keygen.appendChild(resultLink);
var form;
if (keygen.hasAttribute('form')) {
form = document.getElementById(keygen.getAttribute('form'));
if (!form) throw '<keygen> has invalid form attribute';
}
else {
form = optInput.form;
if (!form) throw '<keygen> is not in a <form> and has no form attribute';
}
var spkacInput = document.createElement('input');
spkacInput.setAttribute('type', 'hidden');
keygen.removeAttribute('name');
spkacInput.setAttribute('name', name);
if (disabled)
spkacInput.setAttribute('disabled', '');
form.appendChild(spkacInput);
form.addEventListener('submit', function(e) {
e.preventDefault();
(async function() {
var algorithm = keygen.hasAttribute('keytype')
? keygen.getAttribute('keytype')
: algoSelect.value;
var challenge;
if (keygen.hasAttribute('challenge'))
challenge = keygen.getAttribute('challenge');
var pkeyopts = optInput.value;
if (keygen.hasAttribute('-keygenjs-pkeyopts'))
pkeyopts += ' ' + keygen.getAttribute('-keygenjs-pkeyopts');
resultLink.setAttribute('class', 'keygen-link-inv');
resultLink.setAttribute('href', '');
resultLink.textContent = '';
var spkac;
status.textContent = 'Generating key pair...';
try {
spkac = await genSpkac(algorithm, pkeyopts, challenge);
} catch (e) {
console.error('keygen.js key generation error:', e);
status.textContent = 'Error!';
return;
}
spkacInput.setAttribute('value', spkac.spkac);
status.textContent = 'Submitting SPKAC request...';
var data = new FormData(form);
var xhr = new XMLHttpRequest();
xhr.open(form.method || 'GET', form.action || '', true);
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
status.textContent = 'Processing response...';
try {
if (!xhr.response) throw 'No response';
var der = new Uint8Array(xhr.response);
if (!der.length) throw 'Empty response';
(async function() {
var pem;
status.textContent = 'Converting certificate...';
try {
pem = await convertDerToPem(der);
} catch (e) {
console.error('keygen.js certificate conversion error:', e);
status.textContent = 'Error!';
return;
}
var p12;
status.textContent = 'Combining certificate...';
try {
p12 = await convertToPkcs12(spkac.privateKey, pem);
} catch (e) {
console.error('keygen.js certificate combining error:', e);
status.textContent = 'Error!';
return;
}
var b64 = await base64Encode(p12);
var url = 'data:application/x-pkcs12;base64,' +
String.fromCharCode.apply(null, b64);
status.textContent = ''; // OK
resultLink.setAttribute('class', 'keygen-link');
resultLink.setAttribute('href', url);
resultLink.setAttribute('download', 'cert.p12');
resultLink.setAttribute('onclick', 'myagent.play("Save");');
resultLink.textContent = 'Save certificate';
})();
} catch (e) {
console.error('keygen.js certificate generation error:', e);
status.textContent = 'Error!';
}
};
xhr.send(data);
})();
});
}
function polyfillAll() {
var keygens = document.getElementsByTagName('keygen');
for (var keygen of keygens) {
try {
polyfillKeygen(keygen);
} catch (e) {
console.error('keygen.js polyfill error:', e);
}
}
}
if (document.readyState === 'loading')
document.addEventListener('DOMContentLoaded', polyfillAll);
else
polyfillAll();
}

43
files/js/window.js Normal file
View File

@ -0,0 +1,43 @@
//Make the DIV element draggagle:
dragElement(document.getElementById("drag"));
function dragElement(elmnt) {
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
if (document.getElementById(elmnt.id + "title")) {
/* if present, the header is where you move the DIV from:*/
document.getElementById(elmnt.id + "title").onmousedown = dragMouseDown;
} else {
/* otherwise, move the DIV from anywhere inside the DIV:*/
elmnt.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// set the element's new position:
elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
}
function closeDragElement() {
/* stop moving when mouse button is released:*/
document.onmouseup = null;
document.onmousemove = null;
}
}

BIN
files/media/banner.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

BIN
files/media/chaser.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
files/media/cursor.cur Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
files/media/cursor.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

BIN
files/media/cursor2x.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
files/media/font.ttf Executable file

Binary file not shown.

198
files/media/help.pdf Executable file
View File

@ -0,0 +1,198 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/Type /Catalog
/Outlines 2 0 R
/Pages 3 0 R
>>
endobj
2 0 obj
<<
/Type /Outlines
/Count 0
>>
endobj
3 0 obj
<<
/Type /Pages
/Count 2
/Kids [ 4 0 R 6 0 R ]
>>
endobj
4 0 obj
<<
/Type /Page
/Parent 3 0 R
/Resources <<
/Font <<
/F1 9 0 R
>>
/ProcSet 8 0 R
>>
/MediaBox [0 0 612.0000 792.0000]
/Contents 5 0 R
>>
endobj
5 0 obj
<< /Length 1074 >>
stream
2 J
BT
0 0 0 rg
/F1 0027 Tf
57.3750 722.2800 Td
( A Simple PDF File ) Tj
ET
BT
/F1 0010 Tf
69.2500 688.6080 Td
( This is a small demonstration .pdf file - ) Tj
ET
BT
/F1 0010 Tf
69.2500 664.7040 Td
( just for use in the Virtual Mechanics tutorials. More text. And more ) Tj
ET
BT
/F1 0010 Tf
69.2500 652.7520 Td
( text. And more text. And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 628.8480 Td
( And more text. And more text. And more text. And more text. And more ) Tj
ET
BT
/F1 0010 Tf
69.2500 616.8960 Td
( text. And more text. Boring, zzzzz. And more text. And more text. And ) Tj
ET
BT
/F1 0010 Tf
69.2500 604.9440 Td
( more text. And more text. And more text. And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 592.9920 Td
( And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 569.0880 Td
( And more text. And more text. And more text. And more text. And more ) Tj
ET
BT
/F1 0010 Tf
69.2500 557.1360 Td
( text. And more text. And more text. Even more. Continued on page 2 ...) Tj
ET
endstream
endobj
6 0 obj
<<
/Type /Page
/Parent 3 0 R
/Resources <<
/Font <<
/F1 9 0 R
>>
/ProcSet 8 0 R
>>
/MediaBox [0 0 612.0000 792.0000]
/Contents 7 0 R
>>
endobj
7 0 obj
<< /Length 676 >>
stream
2 J
BT
0 0 0 rg
/F1 0027 Tf
57.3750 722.2800 Td
( Simple PDF File 2 ) Tj
ET
BT
/F1 0010 Tf
69.2500 688.6080 Td
( ...continued from page 1. Yet more text. And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 676.6560 Td
( And more text. And more text. And more text. And more text. And more ) Tj
ET
BT
/F1 0010 Tf
69.2500 664.7040 Td
( text. Oh, how boring typing this stuff. But not as boring as watching ) Tj
ET
BT
/F1 0010 Tf
69.2500 652.7520 Td
( paint dry. And more text. And more text. And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 640.8000 Td
( Boring. More, a little more text. The end, and just as well. ) Tj
ET
endstream
endobj
8 0 obj
[/PDF /Text]
endobj
9 0 obj
<<
/Type /Font
/Subtype /Type1
/Name /F1
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
>>
endobj
10 0 obj
<<
/Creator (Rave \(http://www.nevrona.com/rave\))
/Producer (Nevrona Designs)
/CreationDate (D:20060301072826)
>>
endobj
xref
0 11
0000000000 65535 f
0000000019 00000 n
0000000093 00000 n
0000000147 00000 n
0000000222 00000 n
0000000390 00000 n
0000001522 00000 n
0000001690 00000 n
0000002423 00000 n
0000002456 00000 n
0000002574 00000 n
trailer
<<
/Size 11
/Root 1 0 R
/Info 10 0 R
>>
startxref
2714
%%EOF

BIN
files/media/key.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
files/media/segfault.mov Executable file

Binary file not shown.

79
index.php Normal file
View File

@ -0,0 +1,79 @@
<?php
// Autoload files using the Composer autoloader.
require_once __DIR__ . '/vendor/autoload.php';
// First Party
use Tor\Tor;
use Tor\Api\Api;
use Tor\Data\Ticket;
use Tor\Data\Grant;
use Tor\Data\Service;
use Tor\Data\User;
// Third Party Routing Library
use Gac\Routing\Exceptions\CallbackNotFound;
use Gac\Routing\Exceptions\RouteNotFoundException;
use Gac\Routing\Request;
use Gac\Routing\Routes;
// Initialize an instance of the routing library
$routes = new Routes();
// Initialize Twig
// TODO: Only do this if needed
$loader = new \Twig\Loader\FilesystemLoader(__DIR__ . '/templates');
$twig = new \Twig\Environment($loader, [
'cache' => __DIR__. '/compilation_cache',
]);
$tor = new Tor();
$api = new Api();
$service = new Service();
$service->id = "minetest";
$service->token = "45678-hsjsndjs-272892-shgdzusjd-6788";
$user = new User();
$user->id = "bananafish";
$user->serial = '0C:87:64:78';
$user->cert = "Yee Haw";
$grant = new Grant();
$grant->id = uniqid();
$grant->service = $service;
$ticket = new Ticket();
$ticket->id = uniqid();
$ticket->start = 1471111;
$ticket->end = 1474567;
$ticket->user = $user;
$ticket->grant = $grant;
echo(microtime(true));
echo($ticket->Serialize(true));
echo(var_dump(Ticket::Deserialize($ticket->Serialize(true), true)));
echo(Ticket::Deserialize($ticket->Serialize(true), true))->Serialize(true);
echo(microtime(true));
try {
// Initialize Tor in order to register routes
$tor->init();
// Initialize Tor API in order to register routes
$api->init();
// Handle routes
$routes->handle();
} catch (RouteNotFoundException $ex) {
$routes->request->status(404, "Route not found")->send(["error" => ["message" => $ex->getMessage()]]);
} catch (CallbackNotFound $ex) {
$routes->request->status(404, "Callback method not found")->send(["error" => ["message" => $ex->getMessage()]]);
} catch (Exception $ex) {
$code = $ex->getCode() ?? 500;
$routes->request->status($code)->send(["error" => ["message" => $ex->getMessage()]]);
}
?>

10
scripts/first-install-dev.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/sh
chmod +x ./scripts/*.sh
./scripts/setup-composer.sh
./composer.phar install
./scripts/setup-ca.sh
./scripts/run-tests.sh

8
scripts/first-install.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
chmod +x ./scripts/*.sh
./scripts/setup-composer.sh
./composer.phar install --no-dev
./scripts/setup-ca.sh

2
scripts/run-dev.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/sh
php -S localhost:4000

1
scripts/run-tests.sh Executable file
View File

@ -0,0 +1 @@
./vendor/bin/phpunit tests/TestRunner.php

56
scripts/setup-ca.sh Executable file
View File

@ -0,0 +1,56 @@
#!/bin/bash
# Copyright 2020 Vladimir Panteleev
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -eEuo pipefail
# Script to set up a minimal certificate authority for keygen.js testing.
# Not a secure configuration! Do not use in production!
rm -rf ca/ssl
mkdir -p ca/ssl
cd ca/ssl
mkdir 161sh
mkdir 161sh/private
openssl genpkey -algorithm RSA -out 161sh/private/cakey.pem
openssl req -x509 -days 358000 -new -nodes -key 161sh/private/cakey.pem -sha256 -out 161sh/cacert.pem \
-subj "/C=SH/CN=161.sh"
mkdir 161sh/newcerts
touch 161sh/index.txt
echo '0000000000000000' > 161sh/serial
mkdir tmp
cat > openssl.cnf <<'EOF'
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = ./161sh
private_key = $dir/private/cakey.pem
certificate = $dir/cacert.pem
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
policy = policy_match
[ policy_match ]
countryName = match
stateOrProvinceName = optional
organizationName = supplied
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
EOF

17
scripts/setup-composer.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
EXPECTED_CHECKSUM="$(php -r 'copy("https://composer.github.io/installer.sig", "php://stdout");')"
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"
if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]
then
>&2 echo 'ERROR: Invalid installer checksum'
rm composer-setup.php
exit 1
fi
php composer-setup.php --quiet
RESULT=$?
rm composer-setup.php
exit $RESULT

11
src/Cave/Cave.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace Cave;
class Cave {
public function init() {
// Push Routes
return "Hello World";
}
}
?>

157
src/Tor/Api/Api.php Normal file
View File

@ -0,0 +1,157 @@
<?php
namespace Tor\Api;
use Gac\Routing\Request;
// Implements the dispatcher for Tor's API
class Api {
public function init() {
global $routes;
// Respond with supported legacy versions and current version of API
$routes->add('/api', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok', 'legacy' => [], 'version' => ['v1'] ]);
});
$routes->add('/api/v1', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'unauthorized' ]);
});
// Internal: List all users
$routes->add('/api/v1/user/list', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Internal: Assign a given new public key id to username
// Creates user if it doesn't exist
$routes->add('/api/v1/user/assign', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Internal: Delete User and all Sessions + ExtraData
$routes->add('/api/v1/user/delete', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Internal: Fetch persistent extradata for user
$routes->add('/api/v1/user/extra', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Internal: Create a new permission group
$routes->add('/api/v1/group/create', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Internal: Delete a permission group
$routes->add('/api/v1/group/delete', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Internal: Assign User to Group
$routes->add('/api/v1/group/assign', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Internal: Revoke User's Group Membership
$routes->add('/api/v1/group/revoke', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Internal: Delete User and all Sessions + ExtraData
$routes->add('/api/v1/user/delete', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Create Ticket Granting Ticket
$routes->add('/api/v1/grant/create', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Invalidate Ticket Granting Ticket
$routes->add('/api/v1/grant/destroy', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Poll Authorization Status of Grant
$routes->add('/api/v1/grant/status', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Fetch ExtraData for Grant
$routes->add('/api/v1/grant/extra', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Internal: List all active Grants
$routes->add('/api/v1/grants/list', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Internal: Approve of Grant with specified subset of extradata
$routes->add('/api/v1/grants/approve', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Internal: Reject Grant
$routes->add('/api/v1/grants/reject', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Fetch ID of Session Ticket by using authorized Grant ID
// Useful when polling manually, not neccessary when using callback
$routes->add('/api/v1/ticket/fetch', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Destroy active Session
$routes->add('/api/v1/ticket/destroy', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Poll authorization status and start / end date for session ticket
$routes->add('/api/v1/ticket/status', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Session KeepAlive
$routes->add('/api/v1/ticket/heartbeat', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Fetch ExtraData for Ticket
$routes->add('/api/v1/ticket/extra', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
// Internal: List all active Session Tickets
$routes->add('/api/v1/ticket/list', function (Request $request) {
$request->status(200, 'OK')
->send([ 'result' => 'ok' ]);
});
}
}

10
src/Tor/Common/Utils.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace Tor\Common;
class Utils {
public function foo() {
return "Hello World";
}
}
?>

View File

@ -0,0 +1,25 @@
<?php
namespace Tor\Controller;
use Gac\Routing\Request;
abstract class BasePage {
private $tpl_values;
function __construct() {
$this->$tpl_values = array();
$this->$tpl_values["client_ip"] = $_SERVER['REMOTE_ADDR'] ?? 'N/A';
$this->$tpl_values["client_host"] = gethostbyaddr($_SERVER['REMOTE_ADDR']) ?? "N/A";
$this->$tpl_values["files_url"] = "/files/";
$this->$tpl_values["app_name"] = "Fire Systems SSO";
$this->$tpl_values["page_title"] = "Welcome";
$this->$tpl_values["window_title"] = "Welcome";
}
abstract function render(\Gac\Routing\Request $request);
}

View File

@ -0,0 +1,13 @@
<?php
namespace Tor\Controller;
class Message extends BasePage {
function render(\Gac\Routing\Request $request)
{
global $twig;
$request->status(200, 'OK');
echo($twig->render('message.html.twig', $this->$tpl_values));
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Tor\Data;
abstract class BaseEntity extends \SeriousJSON\JsonSerializable {
// Primary Identifier
public string $id;
public function flatIdentifier()
{
return $this->id;
}
}

17
src/Tor/Data/Grant.php Normal file
View File

@ -0,0 +1,17 @@
<?php
namespace Tor\Data;
class Grant extends BaseEntity {
public Service $service;
public int $create;
public function jsonSerialize() {
if(get_class($this) == get_called_class()) {
return $this;
}
else {
return get_class($this) . ' = ' . get_called_class();
}
}
}

16
src/Tor/Data/Service.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace Tor\Data;
class Service extends BaseEntity {
public string $token;
public function jsonSerialize() {
if(get_class($this) == get_called_class()) {
return $this;
}
else {
return get_class($this) . ' = ' . get_called_class();
}
}
}

19
src/Tor/Data/Ticket.php Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace Tor\Data;
class Ticket extends BaseEntity {
public Grant $grant;
public User $user;
public int $start;
public int $end;
public function jsonSerialize() {
if(get_class($this) == get_called_class()) {
return $this;
}
else {
return get_class($this) . ' = ' . get_called_class();
}
}
}

17
src/Tor/Data/User.php Normal file
View File

@ -0,0 +1,17 @@
<?php
namespace Tor\Data;
class User extends BaseEntity {
public string $serial;
public string $cert;
public function jsonSerialize() {
if(get_class($this) == get_called_class()) {
return $this;
}
else {
return get_class($this) . ' = ' . get_called_class();
}
}
}

42
src/Tor/Tor.php Normal file
View File

@ -0,0 +1,42 @@
<?php
namespace Tor;
use Gac\Routing\Request;
use Tor\Controller\Message;
// Implements the Dispatcher for Tor
class Tor {
public function init() {
global $routes;
$routes->add('/', [ Message::class, 'render' ]);
// Generate new User Certificate
$routes->add('/generate', function (Request $request) {
global $twig;
$request->status(200, 'OK');
echo($twig->render('generate.html.twig', ['name' => 'Fabien', 'title' => 'Fire Systems Internet Access Point', 'files_url' => './files']));
});
// User Authorization Extradata Consent Form
$routes->add('/consent', function (Request $request) {
global $twig;
$request->status(200, 'OK');
echo($twig->render('consent.html.twig', ['name' => 'Fabien', 'title' => 'Fire Systems Internet Access Point', 'files_url' => './files']));
});
// Assign Certificate to User / Register Username with Certificate
$routes->add('/activate', function (Request $request) {
global $twig;
$request->status(200, 'OK');
echo($twig->render('activate.html.twig', ['name' => 'Fabien', 'title' => 'Fire Systems Internet Access Point', 'files_url' => './files']));
});
}
}
?>

View File

@ -0,0 +1,18 @@
{% extends 'base.html.twig' %}
{% block body %}
<form action="/activate" method="post" style="display: inline;">
Attach the provided X.509 Certificate to an Identity<br><br><label><b>Username:</b></label><br><input name="username" value="{{ username | e }}Hankypants" disabled><br>
<textarea rows=10 cols=40 disabled>
Common Name:
Hankypants
Issuer:
tor.161.sh
Key Fingerprint:
CA:3A:85:33:03:1F:39:7B
Key ID:
A5:CE:37:EA:EB:B0:75:0E</textarea><br>
<i>Please check the Certificate before confirming.</i><br>
<b><input type="submit" name="confirm" value="Attach Certificate" onclick="myagent.play('Print'); myagent.play('GetTechy');"></b>
</form>
{% endblock %}

49
templates/base.html.twig Normal file
View File

@ -0,0 +1,49 @@
<html>
<head>
<title>{{ title | e }} | {% block page_title %}Welcome{% endblock %}</title>
<link rel="stylesheet" type="text/css" href="{{ files_url }}/css/style.css">
<link rel="stylesheet" type="text/css" href="{{ files_url }}/css/clippy.css">
<script type="text/javascript" src="{{ files_url }}/js/jquery.min.js"></script>
<script type="text/javascript" src="{{ files_url }}/js/clippy.min.js"></script>
<script type="text/javascript">
var myagent;
clippy.load('Links', function(agent){
myagent = agent;
// Do anything with the loaded agent
agent.show();
agent.speak('If you need assistance with generating and importing certificates, press the [?] Help Button. My name is Links.');
});
window.addEventListener('load', function () {
myagent.animate();
});
</script>
</head>
<body>
<div class="padring">
<div class="container" id="drag">
<div class="title" id="dragtitle">
<img src="{{ files_url }}/media/key.png" width="18" height="18" class="title" onclick="if (event.shiftKey) window.open('<?php echo $CONF_BASE_URI; ?>/media/segfault.mov', '_blank', 'toolbar=no,scrollbars=no,location=no,fullscreen=yes,status=no,height='+screen.height + ',width='+screen.width); else myagent.play('Congratulate'); return false; "/>
<p class="title">{% block window_title %}Welcome{% endblock %}</p>
<button onclick="if (typeof helpOpen === 'undefined') myagent.animate(); else window.location.href = window.location.href; return false;">X</button>
<button onclick="document.getElementById('content').innerHTML='<div class=pdfcontainer><object type=application/pdf height=500 width=100% data={{ files_url }}/media/help.pdf></object></div>'; document.getElementsByClassName('clippy')[0].style.position = 'absolute'; window.helpOpen = true; myagent.play('CheckingSomething'); return false;">?</button>
</div>
<div id="content">
<img src="{{ files_url }}/media/banner.jpg" class="banner" onclick="myagent.play('GetArtsy'); return false;" />
<div class="left">
<?php echo(gethostbyaddr($_SERVER['REMOTE_ADDR'])); ?><br/>IP <?php echo($_SERVER['REMOTE_ADDR']); ?>
</div>
<div class="right">
Served by dual SPARC64 VIIIs<br>Mainframe access at port 443
</div>
<br>
{% block body %}{% endblock %}
<br>
</div>
<div class="powered">Protected by <img src="{{ files_url }}/media/chaser.png" style="height: 2em; vertical-align: -30%;" /> Software</div>
</div>
</div>
<script src="{{ files_url }}/js/window.js"></script>
</body>
</html>

View File

@ -0,0 +1,18 @@
{% extends 'base.html.twig' %}
{% block body %}
<form action="/consent" method="post" style="display: inline;">
A service has requested access to your Identity<br><br><label><b>Username:</b></label><br><input name="username" value="{{ username | e }}Hankypants" disabled><br>
<textarea rows=10 cols=40 disabled>
Common Name:
Hankypants
Issuer:
tor.161.sh
Key Fingerprint:
CA:3A:85:33:03:1F:39:7B
Key ID:
A5:CE:37:EA:EB:B0:75:0E</textarea><br>
<i>Please verify before continuing.</i><br>
<input type="submit" name="confirm" value="Consent" onclick="myagent.play('Print'); myagent.play('GetTechy');"><input type="submit" name="deny" value="Deny Access" onclick="myagent.play('Print'); myagent.play('GetTechy');">
</form>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends 'base.html.twig' %}
{% block body %}
<script src="{{ files_url }}/js/keygen.js"></script>
<script type="module">
import { OpenSSL } from {{ files_url }}/js/browser.openssl.js';
keygenJS(OpenSSL);
</script>
<br><br>
<form action="/generate/spkac-endpoint.php" method="post" style="display: inline;">
<b><label>Username:</label></b><br><input name="username" required pattern="([A-z0-9]){4,16}"><br>
<input type="submit" value="Generate key" onclick="myagent.play('Print'); myagent.play('GetTechy');"><a href="/" style="display: inline; text-decoration: none;"><button type="button" class="button">Authenticate</button></a><br>
<keygen name="key" keytype="rsa" -keygenjs-pkeyopts="rsa_keygen_bits:4096">
</form>
<br><br>
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends 'base.html.twig' %}
{% block body %}
<b>{{ msg_title | e }}</b><br><br>
{{ msg_body | e }}
{% endblock %}

15
tests/TestRunner.php Normal file
View File

@ -0,0 +1,15 @@
<?php
// Autoload files using the Composer autoloader.
require_once __DIR__ . '/../vendor/autoload.php';
use Tor\Tor;
use PHPUnit\Framework\TestCase;
final class TestRunner extends TestCase
{
public function testPrintHelloWorld() {
$actualClass = new Tor();
$this->assertEquals('Hello World', $actualClass->init());
}
}