lock
[root] / lock.nim
import os, sodium, mem, kdf, stream, format, armor, termios, posix
const VER = 1
proc die(msg: string) =
stderr.writeLine(msg)
quit(1)
proc hasSuffix(s: string, suf: string): bool =
if s.len < suf.len:
return false
return s[(s.len - suf.len)..s.high] == suf
proc rdpass(prompt: string): SecBuf =
var tty = open("/dev/tty", fmRead)
if tty.isNil:
die("cannot open /dev/tty")
var old: Termios
if tcGetAttr(tty.getFileHandle(), addr old) != 0:
tty.close()
die("tcgetattr failed")
var newt = old
var lflag = cast[ptr cuint](cast[uint](addr newt) + 3 * sizeof(cuint).uint)
lflag[] = lflag[] and (not ECHO.cuint)
block:
if tcSetAttr(tty.getFileHandle(), TCSAFLUSH, addr newt) != 0:
tty.close()
die("tcsetattr failed")
stdout.write(prompt)
stdout.flushFile()
var line = ""
while true:
var c: char
if tty.readBuffer(addr c, 1) != 1:
tty.close()
die("read failed")
if c == '\n':
break
line.add(c)
result = mkbuf(line.len)
copyMem(result.p, addr line[0], line.len)
if tcSetAttr(tty.getFileHandle(), TCSANOW, addr old) != 0:
tty.close()
free(result)
die("tcsetattr restore failed")
tty.close()
echo ""
proc chkpass(): SecBuf =
let p1 = rdpass("passphrase: ")
let p2 = rdpass("repeat passphrase: ")
if p1.len != p2.len or not equalMem(p1.p, p2.p, p1.len):
free(p1)
free(p2)
die("passphrases do not match")
free(p2)
result = p1
proc encfile(path: string) =
let pass = chkpass()
let content = readFile(path)
var h: Header
h.version = VER
randombytes_buf(addr h.salt[0], 16)
randombytes_buf(addr h.nonce[0], 24)
let key = kdf(pass, h.salt)
var buf = newString(HDRLEN + content.len + crypto_aead_xchacha20poly1305_ietf_ABYTES)
hdrput(h, cast[ptr byte](addr buf[0]))
let clen = stream.enc(key.p, h.nonce, addr content[0], content.len.culonglong,
cast[pointer](addr buf[HDRLEN]))
if clen < 0:
free(key)
die("encryption failed")
let armored = armor.b64enc(buf[0..(HDRLEN + clen - 1)])
writeFile(path & ".locked", armored)
free(key)
proc decfile(path: string) =
let pass = rdpass("passphrase: ")
let armored = readFile(path)
let buf = armor.b64dec(armored)
if buf.len < HDRLEN:
free(pass)
die("invalid file format")
let h = hdrget(cast[ptr byte](addr buf[0]))
if h.version != VER:
free(pass)
die("unsupported version")
let key = kdf(pass, h.salt)
var outbuf = newString(buf.len - HDRLEN)
let mlen = stream.dec(key.p, h.nonce, cast[pointer](addr buf[HDRLEN]),
(buf.len - HDRLEN).culonglong, addr outbuf[0])
if mlen < 0:
free(key)
die("decryption failed (wrong passphrase or corrupted file)")
writeFile(path[0..(path.high - 7)], outbuf[0..(mlen - 1)])
free(key)
proc main =
if sodium_init() < 0:
die("libsodium init failed")
if paramCount() < 1:
die("usage: lock encrypt|decrypt file")
let cmd = paramStr(1)
if paramCount() < 2:
die("usage: lock encrypt|decrypt file")
let path = paramStr(2)
if cmd == "encrypt":
encfile(path)
elif cmd == "decrypt":
if not path.hasSuffix(".locked"):
die("file must end with .locked")
decfile(path)
else:
die("usage: lock encrypt|decrypt file")
when isMainModule:
main()