mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 09:02:15 -06:00
add alternative filekey generator; closes #52
This commit is contained in:
parent
6aa10ecedc
commit
0dc3c23b42
20
README.md
20
README.md
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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=[],
|
||||
|
|
Loading…
Reference in a new issue