add alternative filekey generator; closes #52

This commit is contained in:
ed 2023-10-06 13:41:22 +00:00
parent 6aa10ecedc
commit 0dc3c23b42
8 changed files with 78 additions and 37 deletions

View file

@ -88,6 +88,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [security](#security) - there is a [discord server](https://discord.gg/25J8CdTT6G)
* [gotchas](#gotchas) - behavior that might be unexpected
* [cors](#cors) - cross-site request config
* [filekeys](#filekeys) - prevent filename bruteforcing
* [password hashing](#password-hashing) - you can hash passwords
* [https](#https) - both HTTP and HTTPS are accepted
* [recovering from crashes](#recovering-from-crashes)
@ -356,7 +357,7 @@ permissions:
* `m` (move): move files/folders *from* this folder
* `d` (delete): delete files/folders
* `g` (get): only download files, cannot see folder contents or zip/tar
* `G` (upget): same as `g` except uploaders get to see their own filekeys (see `fk` in examples below)
* `G` (upget): same as `g` except uploaders get to see their own [filekeys](#filekeys) (see `fk` in examples below)
* `h` (html): same as `g` except folders return their index.html, and filekeys are not necessary for index.html
* `a` (admin): can see uploader IPs, config-reload
@ -370,7 +371,7 @@ examples:
* `u1` can open the `inc` folder, but cannot see the contents, only upload new files to it
* `u2` can browse it and move files *from* `/inc` into any folder where `u2` has write-access
* make folder `/mnt/ss` available at `/i`, read-write for u1, get-only for everyone else, and enable filekeys: `-v /mnt/ss:i:rw,u1:g:c,fk=4`
* `c,fk=4` sets the `fk` (filekey) volflag to 4, meaning each file gets a 4-character accesskey
* `c,fk=4` sets the `fk` ([filekey](#filekeys)) volflag to 4, meaning each file gets a 4-character accesskey
* `u1` can upload files, browse the folder, and see the generated filekeys
* other users cannot browse the folder, but can access the files if they have the full file URL with the filekey
* replacing the `g` permission with `wg` would let anonymous users upload files, but not see the required filekey to access it
@ -1647,9 +1648,7 @@ safety profiles:
other misc notes:
* you can disable directory listings by giving permission `g` instead of `r`, only accepting direct URLs to files
* combine this with volflag `c,fk` to generate filekeys (per-file accesskeys); users which have full read-access will then see URLs with `?k=...` appended to the end, and `g` users must provide that URL including the correct key to avoid a 404
* the default filekey entropy is fairly small so give `--fk-salt` around 30 characters if you want filekeys longer than 16 chars
* permissions `wG` lets users upload files and receive their own filekeys, still without being able to see other uploads
* you may want [filekeys](#filekeys) to prevent filename bruteforcing
* permission `h` instead of `r` makes copyparty behave like a traditional webserver with directory listing/index disabled, returning index.html instead
* compatibility with filekeys: index.html itself can be retrieved without the correct filekey, but all other files are protected
@ -1679,6 +1678,17 @@ by default, except for `GET` and `HEAD` operations, all requests must either:
cors can be configured with `--acao` and `--acam`, or the protections entirely disabled with `--allow-csrf`
## filekeys
prevent filename bruteforcing
volflag `c,fk` generates filekeys (per-file accesskeys) for all files; users which have full read-access (permission `r`) will then see URLs with the correct filekey `?k=...` appended to the end, and `g` users must provide that URL including the correct key to avoid a 404
by default, filekeys are generated based on salt (`--fk-salt`) + filesystem-path + file-size + inode (if not windows); add volflag `fka` to generate slightly weaker filekeys which will not be invalidated if the file is edited (only salt + path)
permissions `wG` (write + upget) lets users upload files and receive their own filekeys, still without being able to see other uploads
## password hashing
you can hash passwords before putting them into config files / providing them as arguments; see `--help-pwhash` for all the details

View file

@ -246,12 +246,7 @@ def get_fk_salt(cert_path) -> str:
with open(fp, "rb") as f:
ret = f.read().strip()
except:
if os.path.exists(cert_path):
zi = os.path.getmtime(cert_path)
ret = "{}".format(zi).encode("utf-8")
else:
ret = base64.b64encode(os.urandom(18))
ret = base64.b64encode(os.urandom(18))
with open(fp, "wb") as f:
f.write(ret + b"\n")

View file

@ -12,7 +12,7 @@ import threading
import time
from datetime import datetime
from .__init__ import ANYWIN, TYPE_CHECKING, WINDOWS
from .__init__ import ANYWIN, E, TYPE_CHECKING, WINDOWS
from .bos import bos
from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
from .pwhash import PWHash
@ -1390,6 +1390,9 @@ class AuthSrv(object):
have_fk = False
for vol in vfs.all_vols.values():
fk = vol.flags.get("fk")
fka = vol.flags.get("fka")
if fka and not fk:
fk = fka
if fk:
vol.flags["fk"] = int(fk) if fk is not True else 8
have_fk = True
@ -1397,6 +1400,12 @@ class AuthSrv(object):
if have_fk and re.match(r"^[0-9\.]+$", self.args.fk_salt):
self.log("filekey salt: {}".format(self.args.fk_salt))
fk_len = len(self.args.fk_salt)
if have_fk and fk_len < 14:
t = "WARNING: filekeys are enabled, but the salt is only %d chars long; %d or longer is recommended. Either specify a stronger salt using --fk-salt or delete this file and restart copyparty: %s"
zs = os.path.join(E.cfg, "fk-salt.txt")
self.log(t % (fk_len, 16, zs), 3)
for vol in vfs.all_vols.values():
if "pk" in vol.flags and "gz" not in vol.flags and "xz" not in vol.flags:
vol.flags["gz"] = False # def.pk

View file

@ -202,8 +202,10 @@ class HttpCli(object):
if rem.startswith("/") or rem.startswith("../") or "/../" in rem:
raise Exception("that was close")
def _gen_fk(self, salt: str, fspath: str, fsize: int, inode: int) -> str:
return gen_filekey_dbg(salt, fspath, fsize, inode, self.log, self.args.log_fk)
def _gen_fk(self, alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
return gen_filekey_dbg(
alg, salt, fspath, fsize, inode, self.log, self.args.log_fk
)
def j2s(self, name: str, **ka: Any) -> str:
tpl = self.conn.hsrv.j2[name]
@ -1712,7 +1714,9 @@ class HttpCli(object):
vsuf = ""
if (self.can_read or self.can_upget) and "fk" in vfs.flags:
alg = 2 if "fka" in vfs.flags else 1
vsuf = "?k=" + self.gen_fk(
alg,
self.args.fk_salt,
path,
post_sz,
@ -2445,7 +2449,9 @@ class HttpCli(object):
for sz, sha_hex, sha_b64, ofn, lfn, ap in files:
vsuf = ""
if (self.can_read or self.can_upget) and "fk" in vfs.flags:
alg = 2 if "fka" in vfs.flags else 1
vsuf = "?k=" + self.gen_fk(
alg,
self.args.fk_salt,
ap,
sz,
@ -3412,7 +3418,7 @@ class HttpCli(object):
t0 = time.time()
lim = time.time() - self.args.unpost
fk_vols = {
vol: vol.flags["fk"]
vol: (vol.flags["fk"], 2 if "fka" in vol.flags else 1)
for vp, vol in self.asrv.vfs.all_vols.items()
if "fk" in vol.flags and (vp in self.rvol or vp in self.upvol)
}
@ -3421,7 +3427,7 @@ class HttpCli(object):
if not cur:
continue
nfk = fk_vols.get(vol, 0)
nfk, fk_alg = fk_vols.get(vol) or (0, 0)
q = "select sz, rd, fn, at from up where ip=? and at>?"
for sz, rd, fn, at in cur.execute(q, (self.ip, lim)):
@ -3432,6 +3438,7 @@ class HttpCli(object):
rv = {"vp": quotep(vp), "sz": sz, "at": at, "nfk": nfk}
if nfk:
rv["ap"] = vol.canonical(vjoin(rd, fn))
rv["fk_alg"] = fk_alg
ret.append(rv)
if len(ret) > 3000:
@ -3445,6 +3452,7 @@ class HttpCli(object):
if not nfk:
continue
alg = rv.pop("fk_alg")
ap = rv.pop("ap")
try:
st = bos.stat(ap)
@ -3452,7 +3460,7 @@ class HttpCli(object):
continue
fk = self.gen_fk(
self.args.fk_salt, ap, st.st_size, 0 if ANYWIN else st.st_ino
alg, self.args.fk_salt, ap, st.st_size, 0 if ANYWIN else st.st_ino
)
rv["vp"] += "?k=" + fk[:nfk]
@ -3713,8 +3721,13 @@ class HttpCli(object):
if not is_dir and (self.can_read or self.can_get):
if not self.can_read and not fk_pass and "fk" in vn.flags:
alg = 2 if "fka" in vn.flags else 1
correct = self.gen_fk(
self.args.fk_salt, abspath, st.st_size, 0 if ANYWIN else st.st_ino
alg,
self.args.fk_salt,
abspath,
st.st_size,
0 if ANYWIN else st.st_ino,
)[: vn.flags["fk"]]
got = self.uparam.get("k")
if got != correct:
@ -3922,6 +3935,7 @@ class HttpCli(object):
ls_names = exclude_dotfiles(ls_names)
add_fk = vn.flags.get("fk")
fk_alg = 2 if "fka" in vn.flags else 1
dirs = []
files = []
@ -3978,11 +3992,15 @@ class HttpCli(object):
except:
ext = "%"
if add_fk:
if add_fk and not is_dir:
href = "%s?k=%s" % (
quotep(href),
self.gen_fk(
self.args.fk_salt, fspath, sz, 0 if ANYWIN else inf.st_ino
fk_alg,
self.args.fk_salt,
fspath,
sz,
0 if ANYWIN else inf.st_ino,
)[:add_fk],
)
else:

View file

@ -314,6 +314,7 @@ class U2idx(object):
sret = []
fk = flags.get("fk")
dots = flags.get("dotsrch")
fk_alg = 2 if "fka" in flags else 1
c = cur.execute(uq, tuple(vuv))
for hit in c:
w, ts, sz, rd, fn, ip, at = hit[:7]
@ -333,16 +334,17 @@ class U2idx(object):
else:
try:
ap = absreal(os.path.join(ptop, rd, fn))
inf = bos.stat(ap)
ino = 0 if ANYWIN or fk_alg == 2 else bos.stat(ap).st_ino
except:
continue
suf = (
"?k="
+ gen_filekey(
self.args.fk_salt, ap, sz, 0 if ANYWIN else inf.st_ino
)[:fk]
)
suf = "?k=" + gen_filekey(
fk_alg,
self.args.fk_salt,
ap,
sz,
ino,
)[:fk]
lim -= 1
if lim < 0:

View file

@ -228,8 +228,10 @@ class Up2k(object):
self.log_func("up2k", msg, c)
def _gen_fk(self, salt: str, fspath: str, fsize: int, inode: int) -> str:
return gen_filekey_dbg(salt, fspath, fsize, inode, self.log, self.args.log_fk)
def _gen_fk(self, alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
return gen_filekey_dbg(
alg, salt, fspath, fsize, inode, self.log, self.args.log_fk
)
def _block(self, why: str) -> None:
self.blocked = why
@ -2623,9 +2625,10 @@ class Up2k(object):
and not self.args.nw
and (cj["user"] in vfs.axs.uread or cj["user"] in vfs.axs.upget)
):
alg = 2 if "fka" in vfs.flags else 1
ap = absreal(djoin(job["ptop"], job["prel"], job["name"]))
ino = 0 if ANYWIN else bos.stat(ap).st_ino
fk = self.gen_fk(self.args.fk_salt, ap, job["size"], ino)
fk = self.gen_fk(alg, self.args.fk_salt, ap, job["size"], ino)
ret["fk"] = fk[: vfs.flags["fk"]]
return ret

View file

@ -1567,15 +1567,18 @@ def rand_name(fdir: str, fn: str, rnd: int) -> str:
return fn
def gen_filekey(salt: str, fspath: str, fsize: int, inode: int) -> str:
return base64.urlsafe_b64encode(
hashlib.sha512(
("%s %s %s %s" % (salt, fspath, fsize, inode)).encode("utf-8", "replace")
).digest()
).decode("ascii")
def gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
if alg == 1:
zs = "%s %s %s %s" % (salt, fspath, fsize, inode)
else:
zs = "%s %s" % (salt, fspath)
zb = zs.encode("utf-8", "replace")
return base64.urlsafe_b64encode(hashlib.sha512(zb).digest()).decode("ascii")
def gen_filekey_dbg(
alg: int,
salt: str,
fspath: str,
fsize: int,
@ -1583,7 +1586,7 @@ def gen_filekey_dbg(
log: "NamedLogger",
log_ptn: Optional[Pattern[str]],
) -> str:
ret = gen_filekey(salt, fspath, fsize, inode)
ret = gen_filekey(alg, salt, fspath, fsize, inode)
assert log_ptn
if log_ptn.search(fspath):

View file

@ -138,6 +138,7 @@ class Cfg(Namespace):
dbd="wal",
s_wr_sz=512 * 1024,
th_size="320x256",
fk_salt="a" * 16,
unpost=600,
u2sort="s",
mtp=[],