This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
ebernerd's profile picture

How to Securely Store Passwords

Started by ebernerd, 31 August 2016 - 05:53 PM
ebernerd #1
Posted 31 August 2016 - 07:53 PM
So, I've seen a lot of OSes and programs that use passwords to make sure that the user is who they say they are. The problem with storing passwords is that it can be dangerous to the integrity of your program if done incorrectly. So what we're going to do is make a simple login system that stores all the users into one file, and makes the passwords unreadable to humans, and almost uncrackable.

First thing we need to talk about is hashing. Hashing is taking a string, putting it through an algorithm, and making it into a string of jumbled letters and numbers. For this example, we're going to use SHA, the Secure Hashing Algorithm. We're going to use the adaptation found here. SHA, for example, will take "Hello!" and turn it into "334d016f755cd6dc58c53a86e183882f8ec14f52fb05345887c8a5edd42c87b7".

So, another problem arises is that if someone gets into your password store, if people have the same password, their hashes are going to be the same. Since the algorithm is just math, it's going to produce the same output every time you put in the same string. So how do we combat this? Salting. Salting ensures that each users' hash is different, so that way it's almost impossible to get the password from the hash.

So, here's what we need:
  • ComputerCraft Computer
  • SHA API (I saved mine to the file "/sha")
  • Time and patience
Usually the first approach to saving passwords is to make a password directory, save the password into a file named the username of that user (i.e., Jimmy's password is saved to passwords/Jimmy). This is insecure, as you can get the password of just an individual user. We're going to save them into a serialized table so that the information is all contained in one area.

Here's how we start our program:

os.loadAPI("sha")

local passPath = "passwords" --Change this if you want a different password file

if not fs.exists(passPath) then --Create a password file
  local f = fs.open(passPath, "w")
  f.write(textutils.serialize({}))
  f.close()
end
This first code block makes a password file if not given yet. Now we need to make sure that we have a secure input, meaning the user cannot terminate it. I tend to write my own, so I'll include it here. If I remember correctly, you can securely use the read() function if you set os.pullEvent to os.pullEventRaw before the read function.

os.loadAPI("sha")

local passPath = "passwords" --Change this if you want a different password file

if not fs.exists(passPath) then --Create a password file
  local f = fs.open(passPath, "w")
  f.write(textutils.serialize({}))
  f.close()
end

local function secureInput( mask, prestring )
  local l = true
  term.setCursorBlink(true)
  local prestring = prestring or ""
  local str = ""
  local sx, sy = term.getCursorPos()
  while l do
	local e, a, b, c, d = os.pullEventRaw()
	if e == "char" then
	  str = str .. a
	elseif e == "key" then
	   if a == 14 then
		 str = str:sub(1, -2)
	   elseif a == 28 then
			l = false
			term.setBackgroundColour( colours.black )
			term.setTextColour( colours.white )
			print()
			return str
	  end
	end
	term.setCursorPos(sx, sy)
	term.setBackgroundColour( colours.white )
	term.setTextColour( colours.black )
	term.clearLine()
	if mask then
	  write( prestring .. " > " .. string.rep(mask, #str) )
	else
	  write( prestring .. " > " .. str )
	end
  end
end

I'm going to write this code in API form, so that way, you can use this in scripts other than login scripts.

Final code:
Spoiler

os.loadAPI("sha")

local passPath = "passwords" --Change this if you want a different password file

if not fs.exists(passPath) then --Create a password file
  local f = fs.open(passPath, "w")
  f.write(textutils.serialize({}))
  f.close()
end

local function secureInput( mask, prestring )
  local l = true
  term.setCursorBlink(true)
  local prestring = prestring or ""
  local str = ""
  local sx, sy = term.getCursorPos()
  while l do
	local e, a, b, c, d = os.pullEventRaw()
	if e == "char" then
	  str = str .. a
	elseif e == "key" then
	   if a == 14 then
		 str = str:sub(1, -2)
	   elseif a == 28 then
			l = false
			term.setBackgroundColour( colours.black )
			term.setTextColour( colours.white )
			print()
			return str
	  end
	end
	term.setCursorPos(sx, sy)
	term.setBackgroundColour( colours.white )
	term.setTextColour( colours.black )
	term.clearLine()
	if mask then
	  write( prestring .. " > " .. string.rep(mask, #str) )
	else
	  write( prestring .. " > " .. str )
	end
  end
end

local function getPass( usr )
  local f = fs.open(passPath,"r")
  local pwds = textutils.unserialize( f.readAll() )
  f.close()
  if pwds[ usr ] then
	 return pwds[ usr ]
  else
	return false
  end
end

local function login( usr, pwd )
  local info = getPass( usr )
  if info then
	if sha.sha256( pwd .. info.salt ) == info.pwd then
	  return true
	else
	  return false
	end
  else
	return false
  end
end

--Login script--
local loop = true
while loop do
  local username = secureInput( nil, "username" )
  local password = secureInput( "x", "password" )
  if login( username, password ) then
	print("Login successfull!")
	loop = false
	break
	--DO SOMETHING
  else
	print("Login failed.")
	--REMOVE these 2 lines below so that the loop continues after incorrect login, once proven program works.
	loop = false
	break
  end
end

Brief explanation on how this works:
User enters username and password -> program checks to see if user even exists -> if so, it takes the salt, appends it to the end of what the user entered, and check to see if that equals the password.

Why the salt? As previously stated, it prevents two hashes being the same. So we add a salt to the password upon creation (see the script below) so that they do not have remotely similar hashes.

Before we can test, we need to make users. Here's a quick user maker I whipped up:

os.loadAPI("sha")
write("username :: ")
local u = read()
write("password :: ")
local p = read()
--Again, change passPath here
local passPath = "passwords"
local f = fs.open(passPath,"r")
local usrs = textutils.unserialize(f.readAll())
f.close()
if not usrs[u] then
  local salt = os.time()  
  usrs[u] = {
	pwd = sha.sha256( p .. salt ),
	salt = salt,
  }
  local f = fs.open(passPath, "w")
  f.write( textutils.serialize( usrs ) )
  f.close()
end
As you can see, upon creation, passwords are salted using the os.time(). The salt can be anything, as long as its unique. Though technically in theory os.time() could be similar, it's quite unlikely that there'll be a collision, so it's good enough for now.

Run that script a couple times, make the users, and see your passwords file. It should look something like this:
Spoiler

Try the login script!


In that gif, both users Eric and tutorials have the same password, but have different hashes. That way, a potential intruder won't know their passwords are the same!

Hopefully, this helped someone. If I need to revise something, let me know!
Edited on 31 August 2016 - 05:54 PM
Emma #2
Posted 03 September 2016 - 11:35 PM
While your method of salting works, usually, the accepted way to salt passwords is to use a random string of characters, making it much much less likely for two users to have the same salt. There is no set length of such salts, but it is usually above 16 characters. Personally, I use 64 char salts, but it's up to you.

Great tutorial though! :D/>

PostscriptHere is an example of a script to generate the salts I described:

local salt = ""
for i=1, 64 do
  math.randomseed(math.random(i) + os.time() * os.time())
  salt = salt..string.char(math.random(48, 122))
end
local f = fs.open("salt","w")
f.write(salt)
f.close()
print("Your random salt: '"..salt.."'")
Here is an example output I got from it:

Ovg_eaTdg5L9^zWRE_gWS3z86fa?fCYrua:SxM^lG`WIXQseIdSYDU?@w0zWTK=^
Anavrins #3
Posted 04 September 2016 - 02:17 AM

local salt = ""
for i=1, 64 do
  math.randomseed(math.random(i) + os.time() * os.time())
  salt = salt..string.char(math.random(48, 122))
end
local f = fs.open("salt","w")
f.write(salt)
f.close()
print("Your random salt: '"..salt.."'")
You really don't need that math.randomseed in there, most of the time you'll end up narrowing the total number of possible salts.
16 chars salt is indeed the recommended salt length for most uses.

The only other thing I might point out for this tutorial is that sha2 isn't designed for password storage, and a better recommendation for people would be to use something like bcrypt or pbkdf2 instead.
I'm gonna go ahead and shamelessly plug my SHA2 api which implements pbkdf2, and is in general, much faster than gravityscore's implementation.
Edited on 04 September 2016 - 12:24 AM
ebernerd #4
Posted 04 September 2016 - 03:02 AM

local salt = ""
for i=1, 64 do
  math.randomseed(math.random(i) + os.time() * os.time())
  salt = salt..string.char(math.random(48, 122))
end
local f = fs.open("salt","w")
f.write(salt)
f.close()
print("Your random salt: '"..salt.."'")
You really don't need that math.randomseed in there, most of the time you'll end up narrowing the total number of possible salts.
16 chars salt is indeed the recommended salt length for most uses.

The only other thing I might point out for this tutorial is that sha2 isn't designed for password storage, and a better recommendation for people would be to use something like bcrypt or pbkdf2 instead.
I'm gonna go ahead and shamelessly plug my SHA2 api which implements pbkdf2, and is in general, much faster than gravityscore's implementation.
I realize that SHA isn't the best utility, it was the easiest one to explain. :)/>
Anavrins #5
Posted 04 September 2016 - 04:49 AM
I realize that SHA isn't the best utility, it was the easiest one to explain. :)/>
It's totally fine, pbkdf2 uses sha2 behind the scenes, so you can use it in pretty much the same way, the only difference is that the salt is implemented with an additional function argument, instead of simple concatenation.
NotSwedishFish #6
Posted 10 September 2016 - 12:49 AM
It's important to note that if you're using any sort of login system on computercraft to use a strong password and a unique one; simply because computercraft computers are so weak, no KDF will be able to protect weak passwords such as permutations of dictionary words (meaning the English dictionary) plus a few numbers. This is because even relatively cheap computers are easily 3 orders of magnitude more powerful than ComputerCraft computers and custom chips can be many times more. This essentially means that either: 1) you'll need to spend a whole bunch of time on your ComputerCraft computer to calculate it or 2) someone else can break it.

The way KDFs work in real life is that they make testing thousands of passwords impractical, while keeping logins quick (on the order of a few hundred milliseconds). This relies on the fact that the ratio of computing power of the adversary to the computing power of the hash generator is somewhat reasonable. If a ComputerCraft computer used a KDF that took it a practical amount of time (say 10 seconds – probably a few hundred iterations), then people could just go get a gaming laptop (yeah not even custom circuits – scrypt isn't going to save you either) and test thousands of passwords in that space of time, making a dictionary-based brute-force attack possible.

TL;DR: DO NOT USE YOUR BANK PASSWORD (or other important password) IN CC, NO MATTER WHAT HASHING/ENCRYPTION ALGORITHMS ARE IN PLACE.
The computing power is just far too asymmetric. (Note: a very strong password will resist the attack described above; but then again, you don't know about side-channel attacks or that your password isn't just being saved plaintext into a file called "hackme" or that someone hasn't already backdoored the login app.
Edited on 09 September 2016 - 10:50 PM
Lyqyd #7
Posted 10 September 2016 - 03:15 AM
The real TL;DR here is:

Don't store sensitive information within ComputerCraft.
Anavrins #8
Posted 10 September 2016 - 04:15 AM
I'm all very aware of that, but there's no code anywhere to fix people from using weak passwords or even their bank account's, so that's on them, as on any other online services.
Anyway, in the worst case, I'm pretty sure that there's some way for the server to read MC data packets and extract CC keystrokes from them and read out your passwords in plain.

KDFs (at least my implementation of them) produce the same output as on high-end normal computers, so it's not a question of because it's in CC that it's weak or something.
Sure, it does run slower on it, and I've been trying real hard on optimizing the speed of my algorithms so I can cram more iterations, and have at least some additional overhead for password crackers so that it's at least not as bad as single iteration sha2.

In the end, iterated pbkdf2 is the best option for password storage in CC, no matter how fast a cracker can crack it, it's much better than storing it plain.

Edit: 666th post
Edited on 10 September 2016 - 02:25 AM
lebalusch #9
Posted 16 October 2016 - 11:34 PM
SOLVED IGNORE

Hi Im finding the User creation script to not be working for me correctly. I got the "sha" file and also the "passwords" file at the normal root (new pc and just edit off the main screen) So guessing i has to change Ln 7 to "passPath = "/passwords"

so i run the userMaker. enter the user name, then password and then it crashes. "UserMaker:11:attept to index ? (a nil value)

any ideas where i have got it wrong or if these scripts above are not working right.

cheers guys
Edited on 16 October 2016 - 10:08 PM
lebalusch #10
Posted 17 October 2016 - 11:06 PM
Hi guys its happening again.

the User creation script seems to not find the passwords i guess as it keeps crashing when it gets to this line.

line15
if not usrs[u] then

giving a MkUser:15: attempt to index ? (a nil value)

any ideas? was running fine all night then with a reset of the server its back to not working right.
Anavrins #11
Posted 18 October 2016 - 01:13 AM
Would be great it you provided what code you wrote so far so we can help you…
lebalusch #12
Posted 19 October 2016 - 12:06 AM
i got it working again and now its stopped.
Spoiler
os.loadAPI("sha")

--functions--
local passPath = "PSW" --Change this if you want a different password file

local function checkFile(passPath)

if not fs.exists(passPath) then --Create a password file
  local f = fs.open(passPath, "w")
  f.write(textutils.serialize({ }))
  f.close()
  term.write("No Password file found, Password file created")



  else
  term.write("Password file read")
  end

end

--code--
checkFile(passPath)--run function above

write("username :: ")
local u = read()
write("password :: ")
local p = read()

--Again, change passPath here
local passPath = "PSW"
local f = fs.open(passPath,"r")
local usrs = textutils.unserialize(f.readAll())
f.close()


if not usrs[u] then

  local salt = math.random(1,10000000000000)*math.random(1,10000000000000)*math.random(1,10000000000000)  
  usrs[u] = {
		pwd = sha.sha256( p .. salt ),
		salt = salt,
  }
  local f = fs.open(passPath, "w")
  f.write( textutils.serialize( usrs ) )
  f.close()
end

i would like in the end to hash the user names as well so its totally unreadable whos hash is whos.


I have done a first version of mine you can find it here.

http://www.computerc...ng-and-salting/

http://pastebin.com/Vr8tYMP8
Edited on 19 November 2016 - 10:27 PM