mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into develop-2.8
This commit is contained in:
@@ -185,7 +185,7 @@ class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
role = DynamicModelMultipleChoiceField(
|
||||
queryset=SecretRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=True,
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/secrets/secret-roles/",
|
||||
value_field="slug",
|
||||
|
@@ -302,8 +302,8 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
||||
Device; Devices may have multiple Secrets associated with them. A name can optionally be defined along with the
|
||||
ciphertext; this string is stored as plain text in the database.
|
||||
|
||||
A Secret can be up to 65,536 bytes (64KB) in length. Each secret string will be padded with random data to a minimum
|
||||
of 64 bytes during encryption in order to protect short strings from ciphertext analysis.
|
||||
A Secret can be up to 65,535 bytes (64KB - 1B) in length. Each secret string will be padded with random data to
|
||||
a minimum of 64 bytes during encryption in order to protect short strings from ciphertext analysis.
|
||||
"""
|
||||
device = models.ForeignKey(
|
||||
to='dcim.Device',
|
||||
@@ -320,7 +320,7 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
||||
blank=True
|
||||
)
|
||||
ciphertext = models.BinaryField(
|
||||
max_length=65568, # 16B IV + 2B pad length + {62-65550}B padded
|
||||
max_length=65568, # 128-bit IV + 16-bit pad length + 65535B secret + 15B padding
|
||||
editable=False
|
||||
)
|
||||
hash = models.CharField(
|
||||
@@ -388,11 +388,7 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
||||
else:
|
||||
pad_length = 0
|
||||
|
||||
# Python 2 compatibility
|
||||
if sys.version_info[0] < 3:
|
||||
header = chr(len(s) >> 8) + chr(len(s) % 256)
|
||||
else:
|
||||
header = bytes([len(s) >> 8]) + bytes([len(s) % 256])
|
||||
header = bytes([len(s) >> 8]) + bytes([len(s) % 256])
|
||||
|
||||
return header + s + os.urandom(pad_length)
|
||||
|
||||
|
@@ -4,7 +4,7 @@ from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import SecretRole, Secret
|
||||
|
||||
SECRETROLE_ACTIONS = """
|
||||
<a href="{% url 'secrets:secretrole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'secrets:secretrole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.secrets.change_secretrole %}
|
||||
|
@@ -85,14 +85,19 @@ class UserKeyTestCase(TestCase):
|
||||
|
||||
class SecretTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
# Generate a random key for encryption/decryption of secrets
|
||||
cls.secret_key = generate_random_key()
|
||||
|
||||
def test_01_encrypt_decrypt(self):
|
||||
"""
|
||||
Test basic encryption and decryption functionality using a random master key.
|
||||
"""
|
||||
plaintext = string.printable * 2
|
||||
secret_key = generate_random_key()
|
||||
s = Secret(plaintext=plaintext)
|
||||
s.encrypt(secret_key)
|
||||
s.encrypt(self.secret_key)
|
||||
|
||||
# Ensure plaintext is deleted upon encryption
|
||||
self.assertIsNone(s.plaintext, "Plaintext must be None after encrypting.")
|
||||
@@ -112,7 +117,7 @@ class SecretTestCase(TestCase):
|
||||
self.assertFalse(s.validate("Invalid plaintext"), "Invalid plaintext validated against hash")
|
||||
|
||||
# Test decryption
|
||||
s.decrypt(secret_key)
|
||||
s.decrypt(self.secret_key)
|
||||
self.assertEqual(plaintext, s.plaintext, "Decrypting Secret returned incorrect plaintext")
|
||||
|
||||
def test_02_ciphertext_uniqueness(self):
|
||||
@@ -120,15 +125,45 @@ class SecretTestCase(TestCase):
|
||||
Generate 50 Secrets using the same plaintext and check for duplicate IVs or payloads.
|
||||
"""
|
||||
plaintext = "1234567890abcdef"
|
||||
secret_key = generate_random_key()
|
||||
ivs = []
|
||||
ciphertexts = []
|
||||
for i in range(1, 51):
|
||||
s = Secret(plaintext=plaintext)
|
||||
s.encrypt(secret_key)
|
||||
s.encrypt(self.secret_key)
|
||||
ivs.append(s.ciphertext[0:16])
|
||||
ciphertexts.append(s.ciphertext[16:32])
|
||||
duplicate_ivs = [i for i, x in enumerate(ivs) if ivs.count(x) > 1]
|
||||
self.assertEqual(duplicate_ivs, [], "One or more duplicate IVs found!")
|
||||
duplicate_ciphertexts = [i for i, x in enumerate(ciphertexts) if ciphertexts.count(x) > 1]
|
||||
self.assertEqual(duplicate_ciphertexts, [], "One or more duplicate ciphertexts (first blocks) found!")
|
||||
|
||||
def test_minimum_length(self):
|
||||
"""
|
||||
Test enforcement of the minimum length for ciphertexts.
|
||||
"""
|
||||
plaintext = 'A' # One-byte plaintext
|
||||
secret = Secret(plaintext=plaintext)
|
||||
secret.encrypt(self.secret_key)
|
||||
|
||||
# 16B IV + 2B length + 1B secret + 61B padding = 80 bytes
|
||||
self.assertEqual(len(secret.ciphertext), 80)
|
||||
self.assertIsNone(secret.plaintext)
|
||||
|
||||
secret.decrypt(self.secret_key)
|
||||
self.assertEqual(secret.plaintext, plaintext)
|
||||
|
||||
def test_maximum_length(self):
|
||||
"""
|
||||
Test encrypting a plaintext value of the maximum length.
|
||||
"""
|
||||
plaintext = '0123456789abcdef' * 4096
|
||||
plaintext = plaintext[:65535] # 65,535 chars
|
||||
secret = Secret(plaintext=plaintext)
|
||||
secret.encrypt(self.secret_key)
|
||||
|
||||
# 16B IV + 2B length + 65535B secret + 15B padding = 65568 bytes
|
||||
self.assertEqual(len(secret.ciphertext), 65568)
|
||||
self.assertIsNone(secret.plaintext)
|
||||
|
||||
secret.decrypt(self.secret_key)
|
||||
self.assertEqual(secret.plaintext, plaintext)
|
||||
|
Reference in New Issue
Block a user