models.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. from __future__ import unicode_literals
  2. import os
  3. from Crypto.Cipher import AES, PKCS1_OAEP
  4. from Crypto.PublicKey import RSA
  5. from Crypto.Util import strxor
  6. from django.conf import settings
  7. from django.contrib.auth.hashers import make_password, check_password
  8. from django.contrib.auth.models import Group, User
  9. from django.contrib.contenttypes.fields import GenericRelation
  10. from django.core.exceptions import ValidationError
  11. from django.db import models
  12. from django.urls import reverse
  13. from django.utils.encoding import force_bytes, python_2_unicode_compatible
  14. from taggit.managers import TaggableManager
  15. from extras.models import CustomFieldModel
  16. from utilities.models import ChangeLoggedModel
  17. from .exceptions import InvalidKey
  18. from .hashers import SecretValidationHasher
  19. from .querysets import UserKeyQuerySet
  20. def generate_random_key(bits=256):
  21. """
  22. Generate a random encryption key. Sizes is given in bits and must be in increments of 32.
  23. """
  24. if bits % 32:
  25. raise Exception("Invalid key size ({}). Key sizes must be in increments of 32 bits.".format(bits))
  26. return os.urandom(int(bits / 8))
  27. def encrypt_master_key(master_key, public_key):
  28. """
  29. Encrypt a secret key with the provided public RSA key.
  30. """
  31. key = RSA.importKey(public_key)
  32. cipher = PKCS1_OAEP.new(key)
  33. return cipher.encrypt(master_key)
  34. def decrypt_master_key(master_key_cipher, private_key):
  35. """
  36. Decrypt a secret key with the provided private RSA key.
  37. """
  38. key = RSA.importKey(private_key)
  39. cipher = PKCS1_OAEP.new(key)
  40. return cipher.decrypt(master_key_cipher)
  41. @python_2_unicode_compatible
  42. class UserKey(models.Model):
  43. """
  44. A UserKey stores a user's personal RSA (public) encryption key, which is used to generate their unique encrypted
  45. copy of the master encryption key. The encrypted instance of the master key can be decrypted only with the user's
  46. matching (private) decryption key.
  47. """
  48. created = models.DateField(
  49. auto_now_add=True
  50. )
  51. last_updated = models.DateTimeField(
  52. auto_now=True
  53. )
  54. user = models.OneToOneField(
  55. to=User,
  56. on_delete=models.CASCADE,
  57. related_name='user_key',
  58. editable=False
  59. )
  60. public_key = models.TextField(
  61. verbose_name='RSA public key'
  62. )
  63. master_key_cipher = models.BinaryField(
  64. max_length=512,
  65. blank=True,
  66. null=True,
  67. editable=False
  68. )
  69. objects = UserKeyQuerySet.as_manager()
  70. class Meta:
  71. ordering = ['user__username']
  72. permissions = (
  73. ('activate_userkey', "Can activate user keys for decryption"),
  74. )
  75. def __init__(self, *args, **kwargs):
  76. super(UserKey, self).__init__(*args, **kwargs)
  77. # Store the initial public_key and master_key_cipher to check for changes on save().
  78. self.__initial_public_key = self.public_key
  79. self.__initial_master_key_cipher = self.master_key_cipher
  80. def __str__(self):
  81. return self.user.username
  82. def clean(self, *args, **kwargs):
  83. if self.public_key:
  84. # Validate the public key format
  85. try:
  86. pubkey = RSA.import_key(self.public_key)
  87. except ValueError:
  88. raise ValidationError({
  89. 'public_key': "Invalid RSA key format."
  90. })
  91. except Exception:
  92. raise ValidationError("Something went wrong while trying to save your key. Please ensure that you're "
  93. "uploading a valid RSA public key in PEM format (no SSH/PGP).")
  94. # Validate the public key length
  95. pubkey_length = pubkey.size_in_bits()
  96. if pubkey_length < settings.SECRETS_MIN_PUBKEY_SIZE:
  97. raise ValidationError({
  98. 'public_key': "Insufficient key length. Keys must be at least {} bits long.".format(
  99. settings.SECRETS_MIN_PUBKEY_SIZE
  100. )
  101. })
  102. # We can't use keys bigger than our master_key_cipher field can hold
  103. if pubkey_length > 4096:
  104. raise ValidationError({
  105. 'public_key': "Public key size ({}) is too large. Maximum key size is 4096 bits.".format(
  106. pubkey_length
  107. )
  108. })
  109. super(UserKey, self).clean()
  110. def save(self, *args, **kwargs):
  111. # Check whether public_key has been modified. If so, nullify the initial master_key_cipher.
  112. if self.__initial_master_key_cipher and self.public_key != self.__initial_public_key:
  113. self.master_key_cipher = None
  114. # If no other active UserKeys exist, generate a new master key and use it to activate this UserKey.
  115. if self.is_filled() and not self.is_active() and not UserKey.objects.active().count():
  116. master_key = generate_random_key()
  117. self.master_key_cipher = encrypt_master_key(master_key, self.public_key)
  118. super(UserKey, self).save(*args, **kwargs)
  119. def delete(self, *args, **kwargs):
  120. # If Secrets exist and this is the last active UserKey, prevent its deletion. Deleting the last UserKey will
  121. # result in the master key being destroyed and rendering all Secrets inaccessible.
  122. if Secret.objects.count() and [uk.pk for uk in UserKey.objects.active()] == [self.pk]:
  123. raise Exception("Cannot delete the last active UserKey when Secrets exist! This would render all secrets "
  124. "inaccessible.")
  125. super(UserKey, self).delete(*args, **kwargs)
  126. def is_filled(self):
  127. """
  128. Returns True if the UserKey has been filled with a public RSA key.
  129. """
  130. return bool(self.public_key)
  131. is_filled.boolean = True
  132. def is_active(self):
  133. """
  134. Returns True if the UserKey has been populated with an encrypted copy of the master key.
  135. """
  136. return self.master_key_cipher is not None
  137. is_active.boolean = True
  138. def get_master_key(self, private_key):
  139. """
  140. Given the User's private key, return the encrypted master key.
  141. """
  142. if not self.is_active:
  143. raise ValueError("Unable to retrieve master key: UserKey is inactive.")
  144. try:
  145. return decrypt_master_key(force_bytes(self.master_key_cipher), private_key)
  146. except ValueError:
  147. return None
  148. def activate(self, master_key):
  149. """
  150. Activate the UserKey by saving an encrypted copy of the master key to the database.
  151. """
  152. if not self.public_key:
  153. raise Exception("Cannot activate UserKey: Its public key must be filled first.")
  154. self.master_key_cipher = encrypt_master_key(master_key, self.public_key)
  155. self.save()
  156. @python_2_unicode_compatible
  157. class SessionKey(models.Model):
  158. """
  159. A SessionKey stores a User's temporary key to be used for the encryption and decryption of secrets.
  160. """
  161. userkey = models.OneToOneField(
  162. to='secrets.UserKey',
  163. on_delete=models.CASCADE,
  164. related_name='session_key',
  165. editable=False
  166. )
  167. cipher = models.BinaryField(
  168. max_length=512,
  169. editable=False
  170. )
  171. hash = models.CharField(
  172. max_length=128,
  173. editable=False
  174. )
  175. created = models.DateTimeField(
  176. auto_now_add=True
  177. )
  178. key = None
  179. class Meta:
  180. ordering = ['userkey__user__username']
  181. def __str__(self):
  182. return self.userkey.user.username
  183. def save(self, master_key=None, *args, **kwargs):
  184. if master_key is None:
  185. raise Exception("The master key must be provided to save a session key.")
  186. # Generate a random 256-bit session key if one is not already defined
  187. if self.key is None:
  188. self.key = generate_random_key()
  189. # Generate SHA256 hash using Django's built-in password hashing mechanism
  190. self.hash = make_password(self.key)
  191. # Encrypt master key using the session key
  192. self.cipher = strxor.strxor(self.key, master_key)
  193. super(SessionKey, self).save(*args, **kwargs)
  194. def get_master_key(self, session_key):
  195. # Validate the provided session key
  196. if not check_password(session_key, self.hash):
  197. raise InvalidKey("Invalid session key")
  198. # Decrypt master key using provided session key
  199. master_key = strxor.strxor(session_key, bytes(self.cipher))
  200. return master_key
  201. def get_session_key(self, master_key):
  202. # Recover session key using the master key
  203. session_key = strxor.strxor(master_key, bytes(self.cipher))
  204. # Validate the recovered session key
  205. if not check_password(session_key, self.hash):
  206. raise InvalidKey("Invalid master key")
  207. return session_key
  208. @python_2_unicode_compatible
  209. class SecretRole(ChangeLoggedModel):
  210. """
  211. A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles
  212. such as "Login Credentials" or "SNMP Communities."
  213. By default, only superusers will have access to decrypt Secrets. To allow other users to decrypt Secrets, grant them
  214. access to the appropriate SecretRoles either individually or by group.
  215. """
  216. name = models.CharField(
  217. max_length=50,
  218. unique=True
  219. )
  220. slug = models.SlugField(
  221. unique=True
  222. )
  223. users = models.ManyToManyField(
  224. to=User,
  225. related_name='secretroles',
  226. blank=True
  227. )
  228. groups = models.ManyToManyField(
  229. to=Group,
  230. related_name='secretroles',
  231. blank=True
  232. )
  233. csv_headers = ['name', 'slug']
  234. class Meta:
  235. ordering = ['name']
  236. def __str__(self):
  237. return self.name
  238. def get_absolute_url(self):
  239. return "{}?role={}".format(reverse('secrets:secret_list'), self.slug)
  240. def to_csv(self):
  241. return (
  242. self.name,
  243. self.slug,
  244. )
  245. def has_member(self, user):
  246. """
  247. Check whether the given user has belongs to this SecretRole. Note that superusers belong to all roles.
  248. """
  249. if user.is_superuser:
  250. return True
  251. return user in self.users.all() or user.groups.filter(pk__in=self.groups.all()).exists()
  252. @python_2_unicode_compatible
  253. class Secret(ChangeLoggedModel, CustomFieldModel):
  254. """
  255. A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible
  256. SHA-256 hash is stored along with the ciphertext for validation upon decryption. Each Secret is assigned to a
  257. Device; Devices may have multiple Secrets associated with them. A name can optionally be defined along with the
  258. ciphertext; this string is stored as plain text in the database.
  259. A Secret can be up to 65,536 bytes (64KB) in length. Each secret string will be padded with random data to a minimum
  260. of 64 bytes during encryption in order to protect short strings from ciphertext analysis.
  261. """
  262. device = models.ForeignKey(
  263. to='dcim.Device',
  264. on_delete=models.CASCADE,
  265. related_name='secrets'
  266. )
  267. role = models.ForeignKey(
  268. to='secrets.SecretRole',
  269. on_delete=models.PROTECT,
  270. related_name='secrets'
  271. )
  272. name = models.CharField(
  273. max_length=100,
  274. blank=True
  275. )
  276. ciphertext = models.BinaryField(
  277. max_length=65568, # 16B IV + 2B pad length + {62-65550}B padded
  278. editable=False
  279. )
  280. hash = models.CharField(
  281. max_length=128,
  282. editable=False
  283. )
  284. custom_field_values = GenericRelation(
  285. to='extras.CustomFieldValue',
  286. content_type_field='obj_type',
  287. object_id_field='obj_id'
  288. )
  289. tags = TaggableManager()
  290. plaintext = None
  291. csv_headers = ['device', 'role', 'name', 'plaintext']
  292. class Meta:
  293. ordering = ['device', 'role', 'name']
  294. unique_together = ['device', 'role', 'name']
  295. def __init__(self, *args, **kwargs):
  296. self.plaintext = kwargs.pop('plaintext', None)
  297. super(Secret, self).__init__(*args, **kwargs)
  298. def __str__(self):
  299. if self.role and self.device and self.name:
  300. return '{} for {} ({})'.format(self.role, self.device, self.name)
  301. # Return role and device if no name is set
  302. if self.role and self.device:
  303. return '{} for {}'.format(self.role, self.device)
  304. return 'Secret'
  305. def get_absolute_url(self):
  306. return reverse('secrets:secret', args=[self.pk])
  307. def to_csv(self):
  308. return (
  309. self.device,
  310. self.role,
  311. self.name,
  312. self.plaintext or '',
  313. )
  314. def _pad(self, s):
  315. """
  316. Prepend the length of the plaintext (2B) and pad with garbage to a multiple of 16B (minimum of 64B).
  317. +--+--------+-------------------------------------------+
  318. |LL|MySecret|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
  319. +--+--------+-------------------------------------------+
  320. """
  321. s = s.encode('utf8')
  322. if len(s) > 65535:
  323. raise ValueError("Maximum plaintext size is 65535 bytes.")
  324. # Minimum ciphertext size is 64 bytes to conceal the length of short secrets.
  325. if len(s) <= 62:
  326. pad_length = 62 - len(s)
  327. elif (len(s) + 2) % 16:
  328. pad_length = 16 - ((len(s) + 2) % 16)
  329. else:
  330. pad_length = 0
  331. return (
  332. chr(len(s) >> 8).encode() +
  333. chr(len(s) % 256).encode() +
  334. s +
  335. os.urandom(pad_length)
  336. )
  337. def _unpad(self, s):
  338. """
  339. Consume the first two bytes of s as a plaintext length indicator and return only that many bytes as the
  340. plaintext.
  341. """
  342. if isinstance(s[0], str):
  343. plaintext_length = (ord(s[0]) << 8) + ord(s[1])
  344. else:
  345. plaintext_length = (s[0] << 8) + s[1]
  346. return s[2:plaintext_length + 2].decode('utf8')
  347. def encrypt(self, secret_key):
  348. """
  349. Generate a random initialization vector (IV) for AES. Pad the plaintext to the AES block size (16 bytes) and
  350. encrypt. Prepend the IV for use in decryption. Finally, record the SHA256 hash of the plaintext for validation
  351. upon decryption.
  352. """
  353. if self.plaintext is None:
  354. raise Exception("Must unlock or set plaintext before locking.")
  355. # Pad and encrypt plaintext
  356. iv = os.urandom(16)
  357. aes = AES.new(secret_key, AES.MODE_CFB, iv)
  358. self.ciphertext = iv + aes.encrypt(self._pad(self.plaintext))
  359. # Generate SHA256 using Django's built-in password hashing mechanism
  360. self.hash = make_password(self.plaintext, hasher=SecretValidationHasher())
  361. self.plaintext = None
  362. def decrypt(self, secret_key):
  363. """
  364. Consume the first 16 bytes of self.ciphertext as the AES initialization vector (IV). The remainder is decrypted
  365. using the IV and the provided secret key. Padding is then removed to reveal the plaintext. Finally, validate the
  366. decrypted plaintext value against the stored hash.
  367. """
  368. if self.plaintext is not None:
  369. return
  370. if not self.ciphertext:
  371. raise Exception("Must define ciphertext before unlocking.")
  372. # Decrypt ciphertext and remove padding
  373. iv = bytes(self.ciphertext[0:16])
  374. ciphertext = bytes(self.ciphertext[16:])
  375. aes = AES.new(secret_key, AES.MODE_CFB, iv)
  376. plaintext = self._unpad(aes.decrypt(ciphertext))
  377. # Verify decrypted plaintext against hash
  378. if not self.validate(plaintext):
  379. raise ValueError("Invalid key or ciphertext!")
  380. self.plaintext = plaintext
  381. def validate(self, plaintext):
  382. """
  383. Validate that a given plaintext matches the stored hash.
  384. """
  385. if not self.hash:
  386. raise Exception("Hash has not been generated for this secret.")
  387. return check_password(plaintext, self.hash, preferred=SecretValidationHasher())
  388. def decryptable_by(self, user):
  389. """
  390. Check whether the given user has permission to decrypt this Secret.
  391. """
  392. return self.role.has_member(user)