Skip to content

Instantly share code, notes, and snippets.

@RealityRipple
Created June 21, 2019 04:53
Show Gist options
  • Save RealityRipple/441a0fb6e04f85a4d54539bc66f15ff6 to your computer and use it in GitHub Desktop.
Save RealityRipple/441a0fb6e04f85a4d54539bc66f15ff6 to your computer and use it in GitHub Desktop.
Encrypt Passwords Over HTTP using RSA
<?php
function parseRequest()
{
if (!isset($_POST['username']) || empty($_POST['username']))
return 'You did not enter a username';
if (!isset($_POST['password']) || empty($_POST['password']))
return 'You did not enter a password';
$request = array();
$request['username'] = $_POST['username'];
$ePass = $_POST['password'];
$ePass = base64_decode($ePass, true);
if ($ePass === false)
return 'Your password was not correctly encoded';
$dPass = decryptPassword($ePass);
if ($dPass === false || empty($dPass))
return 'Your password could not be decrypted';
$request['password'] = $dPass;
//check the login and return true or a failure message
echo "Your Password is \"$dPass\", but you didn't hear it from me!\n\n";
return true;
}
function decryptPassword($ePass)
{
$eKey = file_get_contents('priv.pem');
$pKey = openssl_pkey_get_private($eKey);
if ($pKey === false)
return false;
$kData = openssl_pkey_get_details($pKey);
if ($kData['type'] !== OPENSSL_KEYTYPE_RSA)
return false;
$digest = $kData['bits'] / 8;
$limit = $digest;
$hUse = 20;
$limit -= $hUse * 2;
$limit -= 2;
$salt = 16;
$type = '';
$sRet = '';
for ($a = 0; $a < strlen($ePass); $a+= $digest)
{
$bSeg = substr($ePass, $a, $digest);
$sSeg = '';
$ret = openssl_private_decrypt($bSeg, $sSeg, $pKey, OPENSSL_PKCS1_OAEP_PADDING);
if ($ret === false)
return false;
$sSeg = substr(base64_decode($sSeg), $salt);
$sRet.= $sSeg;
}
$sRet = urldecode($sRet);
return $sRet;
}
if (isset($_POST['username']) && isset($_POST['password']))
{
header('Content-Type: text/plain');
$ret = parseRequest();
if ($ret === true)
{
//logged in
echo "Welcome to the site!";
}
else
{
//the login failed
echo "Error: $ret - Please try again!";
}
return;
}
?>
<form id="login">
<table>
<tr>
<td colspan="2">Log In</td>
</tr>
<tr><td>&nbsp;</td></tr>
<tr>
<td><label for="txtUser">Username:&nbsp;</label></td>
<td><input id="txtUser" type="text"></td>
</tr>
<tr>
<td><label for="txtPass">Password:&nbsp;</label></td>
<td><input id="txtPass" type="password"></td>
</tr>
<tr>
<td></td><td><button name="submit" id="submit" type="submit" value="login">Secure Log In</button></td>
</tr>
</table>
</form>
<script>
function checkInput(event)
{
event.preventDefault();
var sUser = document.getElementById('txtUser').value;
var sPass = document.getElementById('txtPass').value;
if (sUser.length < 1)
{
alert('You must enter a username!');
document.getElementById('txtUser').focus();
return;
}
if (sPass.length < 1)
{
alert('You must enter your password!');
document.getElementById('txtPass').focus();
return;
}
console.log('1');
sendInput();
}
async function sendInput()
{
var sUser = document.getElementById('txtUser').value;
var sPass = document.getElementById('txtPass').value;
var eKey = await messagecrypto.getEncryptKey();
var ePass = await messagecrypto.encryptText(eKey, sPass);
var myData = new Map();
myData.set('username', sUser);
myData.set('password', ePass);
//add any other form variables you wish to pass along.
sendRequest(myData);
}
function sendRequest(data)
{
var frmContinue = document.createElement('FORM');
frmContinue.method = 'POST';
frmContinue.style.display = 'none';
document.body.appendChild(frmContinue);
for (const [key, value] of data.entries())
{
var iEntry = document.createElement('INPUT');
iEntry.type = 'hidden';
iEntry.name = key;
iEntry.value = value;
frmContinue.appendChild(iEntry);
}
frmContinue.submit();
}
document.getElementById('login').addEventListener('submit', checkInput, false);
var tools = {
arrayBufferToBase64: function(buffer)
{
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++)
{
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
},
base64ToArrayBuffer: function(b64)
{
var binary_string = window.atob(b64);
var len = binary_string.length;
var bytes = new Uint8Array( len );
for (var i = 0; i < len; i++)
{
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
};
var messagecrypto = {
getEncryptKey: async function()
{
var bPub = '<?php echo file_get_contents('pub.b64'); ?>';
var sPub = tools.base64ToArrayBuffer(bPub);
return await window.crypto.subtle.importKey('spki', sPub, {name: 'RSA-OAEP', hash: {name: 'SHA-1'}}, false, ['encrypt']);
},
getDigestSize: function(key)
{
return key.algorithm.modulusLength / 8;
},
calcDigestSpace: function(spaceLimit, hash)
{
var space = spaceLimit;
var hashUse = 20;
space -= hashUse * 2;
space -= 2;
return space;
},
calcSaltSize: function(spaceLimit)
{
var salt = 16;
return salt;
},
encryptText: async function(enc_pub, text)
{
var t64 = await messagecrypto.segmentateAndEncrypt(enc_pub, encodeURIComponent(text));
if (t64 === false)
return false;
return t64;
},
segmentateAndEncrypt: async function(enc_pub, plaintext)
{
var digest = messagecrypto.getDigestSize(enc_pub);
var limit = messagecrypto.calcDigestSpace(digest, enc_pub.algorithm.hash.name);
if (limit === false)
return false;
var salt = messagecrypto.calcSaltSize(limit);
var baseSaltLen = Math.ceil(salt * (4 / 3));
var baseTextLen = limit - baseSaltLen;
var plainTextLen = Math.floor(baseTextLen * 0.75);
plainTextLen = Math.floor(plainTextLen / 4) * 4;
if (plaintext.length <= plainTextLen)
return tools.arrayBufferToBase64(await messagecrypto.encryptSegment(enc_pub, plaintext, salt));
var segments = plaintext.match(new RegExp('.{1,' + plainTextLen + '}', 'g'));
var totalLen = segments.length * digest;
var ret = new Uint8Array(totalLen);
for (var s = 0; s < segments.length; s++)
{
var segRet = await messagecrypto.encryptSegment(enc_pub, segments[s], salt);
ret.set(segRet, s * digest);
}
return tools.arrayBufferToBase64(ret);
},
encryptSegment: async function(enc_pub, segment, salt)
{
var bText = new TextEncoder().encode(segment);
var bData = new Uint8Array(bText.length + salt);
var bSalt = new Uint8Array(salt);
window.crypto.getRandomValues(bSalt);
if (salt > 0)
bData.set(bSalt, 0);
bData.set(bText, salt);
var aData = tools.arrayBufferToBase64(bData);
aData = aData.replace(/=+$/, '');
var tData = new TextEncoder().encode(aData);
var aRet = await window.crypto.subtle.encrypt('RSA-OAEP', enc_pub, tData);
return new Uint8Array(aRet);
}
};
</script>

Secure HTTP Login

Written by Andrew Sachen (2019)

Share and Enjoy (Public Domain)

Everyone knows HTTP is the worst thing in the world for logins, and that anyone who enters a password on anything but a HTTPS-enabled site is asking for trouble. However, it is still technically possible to secure a password with somewhat modern cryptography using no external libraries or components, with simple JavaScript and PHP. This is a demonstration of how to achieve a secure login over HTTP.


Requirements

In order to use this script, you'll need to create an RSA private key. You can use OpenSSL, JavaScript, or any other method you find safe and easy. The file will need to be readable by PHP's openssl_pkey_get_private function, but other than that, it shouldn't matter if it's PEM or DER or any other format you can manage to get PHP to pass along to OpenSSL for decryption purposes. For simplicity, the file is referred to as 'priv.pem' (line 26). You will also need the matching public key which can be embedded directly in the script or called externally. Again, for simplicity, a file called 'pub.b64' containing a simple Base64 string of the public key is used as a reference point, but any loading method you choose can be used (line 175).

Features

The system uses RSA-OAEP for complete compatibility with JavaScript and PHP. Password data is Base64-encoded with 128-bit random seed values in chunks sized for the private key modulus, encrypted with the public key, and then Base64-encoded again for HTTP transfer.

Additions/Changes

If you wish to add other POST variables or hash the password with salt beforehand, that can all be handled very simply in the sendInput() function (lines 113-126). Separation into multiple files is equally simple, as would be making the request asynchronous.

Caveats

Do not use HTTP if you can use HTTPS.

RSA is only used because both PHP and JS support it natively.

SHA-1 is used because PHP doesn't like other hashing algorithms in its OAEP.

Use a 2048-bit or 4096-bit key modulus for best results.

There may be a slight delay in form submission. You may wish to provide users with interface cues, so they don't get impatient.

The code as-is is vulnerable to replay attacks. For best results, add a nonce value, generate one-use private keys, or include some other bit of checkable data into the encryption process if using this code for logging in. If used for a sign-up form, a replay attack isn't as worrisome, unless you send the password plain-text back to the user in a "welcome" E-Mail or something. Doing so could allow an attacker to trick your server into sending them the password of another user.

The code as-is sends a password that the server can see. This is bad practice in most cases. A salted hash should be used instead wherever and whenever possible. This applies to both signing up and logging in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment