让我们将这个问题分成两个部分:
1)密钥存储和轮换
2)密钥长度和允许的字符
1)密钥存储和密钥轮换
密钥存储在数据库中是一个非常糟糕的想法,不要这样做。
理想情况下,您不希望在应用程序中任何地方硬编码密钥。这意味着您应该找到一种方法来替换
settings.py
中硬编码的密钥,通常是使用环境变量的引用(例如:
SECRET_KEY = os.getenv('SECRET_KEY')
)。
至少,您应该将密钥从
.env
文件加载到环境变量中。Python 有多种方法实现此操作。
然而,理想情况下,您不希望在环境中存储密钥。更好的解决方案是使用密码管理服务(例如:Hashicorp Vault、Doppler 等,或者由您的 IaaS/PaaS 提供的密码管理器),在运行时获取密钥,并仅将其存储在内存中。
2)密钥轮换
Django<4.1
中旋转SECRET_KEY
将立即注销所有用户,使密码重置和电子邮件验证链接无效等。
在大多数情况下,这意味着只有在发生违规事件时才需要旋转密钥。
然而,对于Django>=4.1
,引入了一个SECRET_KEY_FALLBACKS
设置。
现在,旋转密钥通常是良好的做法,因此您可能希望开始这样做。然而,正如文档中所指出的那样,回退到旧密钥是昂贵的,因此您还需要限制回退密钥的数量。
最终的答案将取决于您特定的安全模型以及在计算成本(以及在Django<4.1
的用户不便的情况下)上愿意做出的权衡。
2) 密钥长度和允许的字符
虽然Jontas CD正确地识别了Django自动生成秘密密钥的方式,但该答案是不正确的。
正确的答案是,除了一小部分约束条件外,您的Django SECRET_KEY
可以是任意长度的任意str
或bytes
。
约束条件
长度、禁止前缀和唯一字符
以下检查(来自{{link1:django.core.checks.security.base
}}模块)对长度、前缀和唯一字符定义了一小部分约束条件:
SECRET_KEY_INSECURE_PREFIX = "django-insecure-"
SECRET_KEY_MIN_LENGTH = 50
SECRET_KEY_MIN_UNIQUE_CHARACTERS = 5
def _check_secret_key(secret_key):
return (
len(set(secret_key)) >= SECRET_KEY_MIN_UNIQUE_CHARACTERS
and len(secret_key) >= SECRET_KEY_MIN_LENGTH
and not secret_key.startswith(SECRET_KEY_INSECURE_PREFIX)
)
为了简单易懂地表达,你的
SECRET_KEY
必须满足以下要求:
- 长度不少于50个字符
- 至少包含5个独特字符
- 不能以“django-insecure-”为前缀
源编码
正如Django文档中
SECRET_KEY
所述,开发人员不应假设密钥是
str
或
bytes
类型。因此,每个使用者都应该通过
force_str()
或
force_bytes()
将
SECRET_KEY
传递,具体取决于所需类型。
这暗示着密钥必须作为有效字符串提供,或者如果提供
bytes
,则这些字节必须解码为有效字符串。
关于什么构成有效字符串的问题,自从
Python 3.0引入
PEP3120以来,字符串的默认源编码为UTF-8。
在Django中进一步强制执行此规定。例如,
django.utils.encoding.force_str()
明确将UTF-8设置为源编码。
def force_str(s, encoding="utf-8", strings_only=False, errors="strict"):
...
try:
if isinstance(s, bytes):
s = str(s, encoding, errors)
else:
s = str(s)
except UnicodeDecodeError as e:
raise DjangoUnicodeDecodeError(s, *e.args)
return s
如果出于某些原因您仍然停留在较旧版本的Python上,那么任何ASCII字符都可以使用,包括空格。
为什么没有最大长度?
在密码学中,我们通常使用固定长度的二进制密钥。我们从可变长度字符串(例如密码、SECRET_KEY等)转换为固定长度二进制密钥的方法是通过密钥派生函数。
实际上,这意味着我们在使用任何加密功能之前对SECRET_KEY进行“哈希”。
如何哈希密钥在很大程度上取决于所需的密钥长度(例如128位、256位、512位等)以及您想要通过密钥派生函数购买多少防止暴力攻击的安全性。这更像是密码哈希,事实上,常用的密码哈希算法也常用作密钥派生函数。
要查看Django中的示例,请参见{{link1:
django.crypto.utils.salted_hmac()
}}。
def salted_hmac(key_salt, value, secret=None, *, algorithm="sha1"):
...
key_salt = force_bytes(key_salt)
secret = force_bytes(secret)
hasher = getattr(hashlib, algorithm)
key = hasher(key_salt + secret).digest()
return hmac.new(key, msg=force_bytes(value), digestmod=hasher)
在这里,你的
SECRET_KEY
(连同一个随机的
salt)被拉伸或缩小为160位SHA1哈希值。
换句话说,无论你的
SECRET_KEY
有多长,在实际使用时它总是会被拉伸/缩小到正确的比特数。
一个非常长的密钥会浪费吗?
正如我们上面所看到的,Django默认将密钥散列成160位sha1哈希值来签署消息。
然而,标准的
get_random_secret_key()
生成一个50个字符长的密钥,并从一组50个字符中随机选择。这给它一个log2(50^50)的熵,大约是282位。
那么这些额外的比特只是被浪费了吗?嗯,是和不是。
从计算成本的角度来看,二进制编码和哈希在时间和内存成本上的差异,比如对于一个50个字符的字符串和一个500个字符的字符串,是可以忽略不计的。是的,如果你需要在一堆随机字符串上执行数千次,你会开始看到差异。然而,在Django性能方面,这将不会产生任何可测量的影响。
就浪费的熵(即使用282位熵的SECRET_KEY进行哈希到160位哈希),那么,如果它真正是随机的,超长密钥是浪费的。然而,我认为在生成密钥的伪随机方法中,生成一些额外的熵可能会增加一些额外的安全性。
总之:
字符/长度
Django SECRET_KEY 可以是任何len(str)>49的str(或解码为UTF-8 str的bytes),包含至少5个唯一字符,并且没有“django-insecure-”作为前缀。
SECRET_KEY = b'\x20\x20\x20\x63\x6f\x52\x72\x45\x43\x54\x42\x41\x54\x74\x65\x52\x79\x68\x6f\x52\x53\x65\x73\x54\x41\x70\x6c\x65\xf0\x9f\x92\xa9\xf0\x9f\x8d\x86\xf0\x9f\x92\xa6\xf0\x9f\xa6\xb4\xf0\x9f\x8d\x92\xf0\x9f\x8d\x91 \xc2\xb6\xc2\xbc\xc3\x8b\xc3\x9f\xc3\xb1\xc4\x86\xc4\x9c\xc5\x94\xc7\xb8\xc8\x8f\xc8\xa4\xd0\x82\xf0\x9f\x91\x89\xf0\x9f\x91\x8c\xf0\x9f\x91\x85 \xf0\x9f\x91\x84\x21\x20\x61\x6e\x64\x20\x6f\x6e\x20\x61\x6e\x64\x20\x6f\x6e\x20\x61\x6e\x64\x20\x6f\x6e\x2e\x2e\x2e'
SECRET_KEY = " coRrECTBATteRyhoRSesTAple ¶¼ËßñĆĜŔǸȏȤЂ ! and on and on and on..."
SECRET_KEY = b'\xFF68(*ctf1e2r=##e+nl=7(&w*v5z!%h=dej)ypjv&p=n9is4d04'
SECRET_KEY = 'jfieo0w'
SECRET_KEY = 'abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdab'
force_string()
。但是,文档确实暗示了这种可能性。至于_check_secret_key()
,经过仔细检查,发现它唯一的引用是在check_secret_key()
和check_secret_key_fallbacks()
中,这两者只在测试套件中被引用。除此之外,看起来它们唯一被调用的时候是如果你使用--deploy
标志运行check
管理命令。 - bencheck --deploy
会出错。这是 Django 的一个可怕疏忽,因为文档中明确提到了密钥可以是二进制的。我真的希望他们能修补自己的秘密密钥处理方式 :( - Cristiano Coelho