lock

[root] / lock.nim

3.3KB

raw
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()