6 import cryptography
.hazmat
.backends
7 import cryptography
.hazmat
.primitives
.asymmetric
.ec
8 import cryptography
.hazmat
.primitives
.asymmetric
.utils
9 import cryptography
.hazmat
.primitives
.ciphers
10 import cryptography
.hazmat
.primitives
.ciphers
.aead
11 import cryptography
.hazmat
.primitives
.hashes
12 import cryptography
.hazmat
.primitives
.kdf
.hkdf
13 import cryptography
.hazmat
.primitives
.serialization
30 from sqlalchemy
import BigInteger
, Boolean
, Column
, DateTime
, ForeignKey
, Integer
31 from sqlalchemy
import Interval
, LargeBinary
, Text
, UUID
34 from . import bugtracker
36 from . import database
37 from . import httpclient
39 from . import packages
43 from .decorators
import *
45 DEFAULT_STORAGE_QUOTA
= 256 * 1024 * 1024 # 256 MiB
48 log
= logging
.getLogger("pbs.users")
50 # A list of LDAP attributes that we fetch
63 "mailAlternateAddress",
66 class QuotaExceededError(Exception):
69 class Users(base
.Object
):
71 # Initialize thread-local storage
72 self
.local
= threading
.local()
76 if not hasattr(self
.local
, "ldap"):
78 ldap_uri
= self
.backend
.config
.get("ldap", "uri")
80 log
.debug("Connecting to %s..." % ldap_uri
)
82 # Establish LDAP connection
83 self
.local
.ldap
= ldap
.initialize(ldap_uri
)
85 return self
.local
.ldap
87 async def __aiter__(self
):
88 users
= await self
._get
_users
("""
102 def _ldap_query(self
, query
, attrlist
=None, limit
=0, search_base
=None):
103 search_base
= self
.backend
.config
.get("ldap", "base")
105 log
.debug("Performing LDAP query (%s): %s" % (search_base
, query
))
109 # Ask for up to 512 results being returned at a time
110 page_control
= ldap
.controls
.SimplePagedResultsControl(True, size
=512, cookie
="")
117 response
= self
.ldap
.search_ext(search_base
,
118 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
,
119 serverctrls
=[page_control
],
123 type, data
, rmsgid
, serverctrls
= self
.ldap
.result3(response
)
125 # Append to local copy
129 controls
= [c
for c
in serverctrls
130 if c
.controlType
== ldap
.controls
.SimplePagedResultsControl
.controlType
]
135 # Set the cookie for more results
136 page_control
.cookie
= controls
[0].cookie
138 # There are no more results
139 if not page_control
.cookie
:
142 # Log time it took to perform the query
143 log
.debug("Query took %.2fms (%s page(s))" % ((time
.time() - t
) * 1000.0, pages
))
145 # Return all attributes (without the DN)
146 return [attrs
for dn
, attrs
in results
]
148 def _ldap_get(self
, *args
, **kwargs
):
149 results
= self
._ldap
_query
(*args
, **kwargs
)
156 elif len(results
) > 1:
157 raise OverflowError("Too many results returned for ldap_get()")
161 async def create(self
, name
, notify
=False, storage_quota
=None):
165 # Set default for storage quota
166 if storage_quota
is None:
167 storage_quota
= DEFAULT_STORAGE_QUOTA
169 # Insert into database
170 user
= await self
.db
.insert(
173 storage_quota
= storage_quota
,
176 log
.debug("Created user %s" % user
)
178 # Send a welcome email
180 await user
._send
_welcome
_email
()
184 async def get_by_name(self
, name
):
186 Fetch a user by its username
192 User
.deleted_at
== None,
197 # Fetch the user from the database
198 user
= await self
.db
.fetch_one(stmt
)
202 # Do nothing in test mode
203 if self
.backend
.test
:
204 log
.warning("Cannot use get_by_name test mode")
208 res
= self
._ldap
_get
(
210 "(objectClass=person)"
219 uid
= res
.get("uid")[0].decode()
222 return await self
.create(uid
)
224 async def get_by_email(self
, mail
):
225 # Strip any excess stuff from the email address
226 name
, mail
= email
.utils
.parseaddr(mail
)
228 # Do nothing in test mode
229 if self
.backend
.test
:
230 log
.warning("Cannot use get_by_email in test mode")
235 res
= self
._ldap
_get
(
237 "(objectClass=person)"
240 "(mailAlternateAddress=%s)"
246 except OverflowError as e
:
247 raise OverflowError("Too many results for search for %s" % mail
) from e
254 uid
= res
.get("uid")[0].decode()
256 return await self
.get_by_name(uid
)
258 async def _search_by_email(self
, mails
, include_missing
=True):
260 Takes a list of email addresses and returns all users that could be found
265 user
= await self
.get_by_email(mail
)
267 # Include the search string if no user could be found
268 if not user
and include_missing
:
271 # Skip any duplicates
279 async def search(self
, q
, limit
=None):
280 # Do nothing in test mode
281 if self
.backend
.test
:
282 log
.warning("Cannot search for users in test mode")
285 # Search for an exact match
286 user
= await self
.get_by_name(q
)
290 res
= self
._ldap
_query
(
292 "(objectClass=person)"
297 "(mailAlternateAddress=%s)"
309 User
.deleted_at
== None,
310 User
.name
in [row
.get("uid")[0].decode() for row
in res
],
318 return await self
.db
.fetch_as_list(stmt
)
320 @functools.cached_property
321 def build_counts(self
):
323 Returns a CTE that maps the user ID and the total number of builds
329 builds
.Build
.owner_id
.label("user_id"),
332 sqlalchemy
.func
.count(
337 builds
.Build
.owner_id
!= None,
338 builds
.Build
.test
== False,
341 builds
.Build
.owner_id
,
346 async def get_top(self
, limit
=50):
348 Returns the top users (with the most builds)
355 self
.build_counts
.c
.user_id
== User
.id,
358 User
.deleted_at
== None,
361 self
.build_counts
.c
.count
.desc(),
367 return await self
.db
.fetch_as_list(stmt
)
369 @functools.cached_property
370 def build_times(self
):
372 This is a CTE to easily access a user's consumed build time in the last 24 hours
378 # Fetch the user by its ID
379 User
.id.label("user_id"),
381 # Sum up the total build time
383 sqlalchemy
.func
.coalesce(
384 jobs
.Job
.finished_at
,
385 sqlalchemy
.func
.current_timestamp()
387 - jobs
.Job
.started_at
,
388 ).label("used_build_time"),
394 builds
.Build
.owner_id
== User
.id,
398 jobs
.Job
.build_id
== builds
.Build
.id,
401 # Filter out some things
403 User
.deleted_at
== None,
404 User
.daily_build_quota
!= None,
406 # Jobs must have been started
407 jobs
.Job
.started_at
!= None,
410 jobs
.Job
.finished_at
== None,
411 jobs
.Job
.finished_at
>=
412 sqlalchemy
.func
.current_timestamp() - datetime
.timedelta(hours
=24),
421 # Make this into a CTE
422 .cte("user_build_times")
425 @functools.cached_property
426 def exceeded_quotas(self
):
432 self
.build_times
.c
.used_build_time
,
435 #User.daily_build_quota != None,
436 self
.build_times
.c
.used_build_time
>= User
.daily_build_quota
,
439 # Make this into a CTE
440 .cte("user_exceeded_quotas")
446 def vapid_public_key(self
):
448 The public part of the VAPID key
450 return self
.backend
.config
.get("vapid", "public-key")
453 def vapid_private_key(self
):
455 The private part of the VAPID key
457 return self
.backend
.config
.get("vapid", "private-key")
460 def get_application_server_key(self
):
462 Generates the key that we are sending to the client
466 for line
in self
.vapid_public_key
.splitlines():
467 if line
.startswith("-"):
472 # Join everything together
476 key
= base64
.b64decode(key
)
478 # Only take the last bit
481 # Encode the key URL-safe
482 key
= base64
.urlsafe_b64encode(key
).strip(b
"=")
488 class User(database
.Base
, database
.BackendMixin
, database
.SoftDeleteMixin
):
489 __tablename__
= "users"
492 return self
.realname
or self
.name
497 def __lt__(self
, other
):
498 if isinstance(other
, self
.__class
__):
499 return self
.name
< other
.name
501 elif isinstance(other
, str):
502 return self
.name
< other
504 return NotImplemented
513 id = Column(Integer
, primary_key
=True)
517 name
= Column(Text
, nullable
=False)
523 return "/users/%s" % self
.name
525 async def delete(self
):
526 await self
._set
_attribute
("deleted", True)
528 # Destroy all sessions
529 for session
in self
.sessions
:
532 # Fetch any attributes from LDAP
534 @functools.cached_property
536 # Use the stored attributes (only used in the test environment)
537 #if self.data._attrs:
538 # return pickle.loads(self.data._attrs)
540 return self
.backend
.users
._ldap
_get
("(uid=%s)" % self
.name
, attrlist
=LDAP_ATTRS
)
542 def _get_attrs(self
, key
):
543 return [v
.decode() for v
in self
.attrs
.get(key
, [])]
545 def _get_attr(self
, key
):
546 for value
in self
._get
_attrs
(key
):
553 return self
._get
_attr
("cn") or ""
558 The primary email address
560 return self
._get
_attr
("mail")
565 The name/email address of the user in MIME format
567 return email
.utils
.formataddr((
568 self
.realname
or self
.name
,
569 self
.email
or "invalid@invalid.tld",
572 async def send_email(self
, *args
, **kwargs
):
573 return await self
.backend
.messages
.send_template(
580 async def _send_welcome_email(self
):
582 Sends a welcome email to the user
584 await self
.send_email("users/messages/welcome.txt")
588 admin
= Column(Boolean
, nullable
=False, default
=False)
593 return self
.admin
is True
599 return tornado
.locale
.get()
603 def avatar(self
, size
=512):
605 Returns a URL to the avatar the user has uploaded
607 return "https://people.ipfire.org/users/%s.jpg?size=%s" % (self
.name
, size
)
611 def has_perm(self
, user
):
613 Check, if the given user has the right to perform administrative
614 operations on this user.
616 # Anonymous people have no permission
620 # Admins always have permission
624 # Users can edit themselves
633 sessions
= sqlalchemy
.orm
.relationship("Session", back_populates
="user")
637 bugzilla_api_key
= Column(Text
)
641 async def connect_to_bugzilla(self
, api_key
):
642 bz
= bugtracker
.Bugzilla(self
.backend
, api_key
)
644 # Does the API key match with this user?
645 if not self
.email
== await bz
.whoami():
646 raise ValueError("The API key does not belong to %s" % self
)
649 self
.bugzilla_api_key
= api_key
651 @functools.cached_property
654 Connection to Bugzilla as this user
656 if self
.bugzilla_api_key
:
657 return bugtracker
.Bugzilla(self
.backend
, self
.bugzilla_api_key
)
661 daily_build_quota
= Column(Interval
)
665 async def get_used_daily_build_quota(self
):
666 # Fetch the build time from the CTE
670 self
.backend
.users
.build_times
.c
.used_build_time
,
673 self
.backend
.users
.build_times
.c
.user_id
== self
.id,
678 return await self
.db
.select_one(stmt
, "used_build_time") or datetime
.timedelta(0)
680 async def has_exceeded_build_quota(self
):
681 if not self
.daily_build_quota
:
684 return await self
.get_used_daily_build_quota() >= self
.daily_build_quota
688 storage_quota
= Column(BigInteger
)
690 async def has_exceeded_storage_quota(self
, size
=None):
692 Returns True if this user has exceeded their quota
694 # Skip quota check if this user has no quota
695 if not self
.storage_quota
:
698 return await self
.get_disk_usage() + (size
or 0) >= self
.storage_quota
700 async def check_storage_quota(self
, size
=None):
702 Determines the user's disk usage
703 and raises an exception when the user is over quota.
705 # Raise QuotaExceededError if this user is over quota
706 if self
.has_exceeded_storage_quota(size
=size
):
707 raise QuotaExceededError
709 async def get_disk_usage(self
):
711 Returns the total disk usage of this user
713 source_packages
= sqlalchemy
.orm
.aliased(packages
.Package
)
714 binary_packages
= sqlalchemy
.orm
.aliased(packages
.Package
)
717 upload_disk_usage
= (
723 uploads
.Upload
.user
== self
,
724 uploads
.Upload
.expires_at
> sqlalchemy
.func
.current_timestamp(),
729 source_package_disk_usage
= (
732 source_packages
.filesize
739 source_packages
.id == builds
.Build
.pkg_id
,
742 # All objects must exist
743 source_packages
.deleted_at
== None,
744 builds
.Build
.deleted_at
== None,
746 # Don't consider test builds
747 builds
.Build
.test
== False,
749 # The build must be owned by the user
750 builds
.Build
.owner
== self
,
755 binary_package_disk_usage
= (
758 binary_packages
.filesize
,
765 jobs
.Job
.build_id
== builds
.Build
.id,
769 jobs
.JobPackage
.job_id
== jobs
.Job
.id,
773 binary_packages
.id == jobs
.JobPackage
.pkg_id
,
776 # All objects must exist
777 binary_packages
.deleted_at
== None,
778 builds
.Build
.deleted_at
== None,
779 jobs
.Job
.deleted_at
== None,
781 # Don't consider test builds
782 builds
.Build
.test
== False,
784 # The build must be owned by the user
785 builds
.Build
.owner
== self
,
790 build_log_disk_usage
= (
800 jobs
.Job
.build_id
== builds
.Build
.id,
803 # All objects must exist
804 builds
.Build
.deleted_at
== None,
805 jobs
.Job
.deleted_at
== None,
807 # Don't consider test builds
808 builds
.Build
.test
== False,
810 # The build must be owned by the user
811 builds
.Build
.owner
== self
,
815 # Pull everything together
820 source_package_disk_usage
,
821 binary_package_disk_usage
,
822 build_log_disk_usage
,
833 ).label("disk_usage"),
838 return await self
.db
.select_one(stmt
, "disk_usage") or 0
842 async def get_total_builds(self
):
846 self
.backend
.users
.build_counts
.c
.count
.label("count"),
848 .select_from(self
.backend
.users
.build_counts
)
850 self
.backend
.users
.build_counts
.c
.user_id
== self
.id,
855 return await self
.db
.select_one(stmt
, "count") or 0
857 async def get_total_build_time(self
):
859 Returns the total build time
865 sqlalchemy
.func
.coalesce(
866 jobs
.Job
.finished_at
,
867 sqlalchemy
.func
.current_timestamp()
869 - jobs
.Job
.started_at
,
870 ).label("total_build_time")
874 builds
.Build
.id == jobs
.Job
.build_id
,
877 jobs
.Job
.started_at
!= None,
878 builds
.Build
.owner
== self
,
882 return await self
.db
.select_one(stmt
, "total_build_time")
884 # Custom repositories
886 async def get_repos(self
, distro
=None):
888 Returns all custom repositories
894 repos
.Repo
.deleted_at
== None,
895 repos
.Repo
.owner
== self
,
902 # Filter by distribution
905 repos
.Repo
.distro
== distro
,
908 return await self
.db
.fetch_as_list(stmt
)
910 async def get_repo(self
, distro
, slug
=None):
912 Fetches a single repository
914 # Return the "home" repository if slug is empty
922 repos
.Repo
.deleted_at
== None,
923 repos
.Repo
.owner
== self
,
924 repos
.Repo
.distro
== distro
,
925 repos
.Repo
.slug
== slug
,
929 return await self
.db
.fetch_one(stmt
)
933 def get_uploads(self
):
935 Returns all uploads that belong to this user
939 .select(uploads
.Upload
)
941 uploads
.Upload
.user
== self
,
942 uploads
.Upload
.expires_at
> sqlalchemy
.func
.current_timestamp(),
945 uploads
.Upload
.created_at
.desc(),
949 return self
.db
.fetch(stmt
)
953 async def is_subscribed(self
):
955 Returns True if the user is subscribed.
957 subscriptions
= await self
.get_subscriptions()
959 return True if subscriptions
else False
961 async def get_subscriptions(self
):
963 Fetches all current subscriptions
968 UserPushSubscription
,
971 UserPushSubscription
.user
== self
,
974 UserPushSubscription
.created_at
.asc(),
978 return await self
.db
.fetch_as_list(stmt
)
980 async def subscribe(self
, endpoint
, p256dh
, auth
, user_agent
=None):
982 Creates a new subscription for this user
984 _
= self
.locale
.translate
987 if not isinstance(p256dh
, bytes
):
988 p256dh
= base64
.urlsafe_b64decode(p256dh
+ "==")
991 if not isinstance(auth
, bytes
):
992 auth
= base64
.urlsafe_b64decode(auth
+ "==")
994 # Insert into the database
995 subscription
= await self
.db
.insert(
996 UserPushSubscription
,
998 user_agent
= user_agent
,
1005 log
.info("%s subscribed to push notifications" % self
)
1008 await subscription
.send(
1009 _("Hello, %s!") % self
,
1010 _("You have successfully subscribed to push notifications."),
1015 async def send_push_message(self
, *args
, **kwargs
):
1017 Sends a message to all active subscriptions
1019 subscriptions
= await self
.get_subscriptions()
1021 # Return early if there are no subscriptions
1022 if not subscriptions
:
1025 # Send the message to all subscriptions
1026 for subscription
in subscriptions
:
1027 await subscription
.send(*args
, **kwargs
)
1032 class UserPushSubscription(database
.Base
, database
.BackendMixin
):
1033 __tablename__
= "user_push_subscriptions"
1037 id = Column(Integer
, primary_key
=True)
1041 user_id
= Column(Integer
, ForeignKey("users.id"), nullable
=False)
1045 user
= sqlalchemy
.orm
.relationship("User", lazy
="joined", innerjoin
=True)
1049 uuid
= Column(UUID
, unique
=True, nullable
=False,
1050 server_default
=sqlalchemy
.func
.gen_random_uuid())
1054 created_at
= Column(DateTime(timezone
=False), nullable
=False,
1055 server_default
=sqlalchemy
.func
.current_timestamp())
1059 user_agent
= Column(Text
)
1063 endpoint
= Column(Text
, nullable
=False)
1067 p256dh
= Column(LargeBinary
, nullable
=False)
1071 auth
= Column(LargeBinary
, nullable
=False)
1074 def vapid_private_key(self
):
1075 return cryptography
.hazmat
.primitives
.serialization
.load_pem_private_key(
1076 self
.backend
.users
.vapid_private_key
.encode(),
1078 backend
=cryptography
.hazmat
.backends
.default_backend(),
1082 def vapid_public_key(self
):
1083 return self
.vapid_private_key
.public_key()
1085 async def send(self
, title
, body
, ttl
=None):
1087 Sends a message to the user using the push service
1094 # Convert dict() to JSON
1095 message
= json
.dumps(message
)
1097 # Encrypt the message
1098 message
= self
._encrypt
(message
)
1100 # Create a signature
1101 signature
= self
._sign
()
1103 # Encode the public key
1104 crypto_key
= self
.b64encode(
1105 self
.vapid_public_key
.public_bytes(
1106 cryptography
.hazmat
.primitives
.serialization
.Encoding
.X962
,
1107 cryptography
.hazmat
.primitives
.serialization
.PublicFormat
.UncompressedPoint
,
1111 # Form request headers
1113 "Authorization" : "WebPush %s" % signature
,
1114 "Crypto-Key" : "p256ecdsa=%s" % crypto_key
,
1116 "Content-Type" : "application/octet-stream",
1117 "Content-Encoding" : "aes128gcm",
1118 "TTL" : "%s" % (ttl
or 0),
1123 await self
.backend
.httpclient
.fetch(self
.endpoint
, method
="POST",
1124 headers
=headers
, body
=message
)
1126 except httpclient
.HTTPError
as e
:
1128 # The subscription is no longer valid
1130 # Let's just delete ourselves
1134 # Raise everything else
1137 async def delete(self
):
1139 Deletes this subscription
1141 # Immediately delete it
1142 await self
.db
.delete(self
)
1147 for element
in (self
._jwt
_info
, self
._jwt
_data
):
1148 # Format the dictionary
1149 element
= json
.dumps(element
, separators
=(',', ':'), sort_keys
=True)
1152 element
= element
.encode()
1154 # Encode URL-safe in base64 and remove any padding
1155 element
= self
.b64encode(element
)
1157 elements
.append(element
)
1160 token
= b
".".join(elements
)
1162 log
.debug("String to sign: %s" % token
)
1164 # Create the signature
1165 signature
= self
.vapid_private_key
.sign(
1167 cryptography
.hazmat
.primitives
.asymmetric
.ec
.ECDSA(
1168 cryptography
.hazmat
.primitives
.hashes
.SHA256(),
1172 # Decode the signature
1173 r
, s
= cryptography
.hazmat
.primitives
.asymmetric
.utils
.decode_dss_signature(signature
)
1175 # Encode the signature in base64
1176 signature
= self
.b64encode(
1177 self
._num
_to
_bytes
(r
, 32) + self
._num
_to
_bytes
(s
, 32),
1180 # Put everything together
1181 signature
= b
"%s.%s" % (token
, signature
)
1182 signature
= signature
.decode()
1184 log
.debug("Created signature: %s" % signature
)
1194 def _jwt_data(self
):
1196 url
= urllib
.parse
.urlparse(self
.endpoint
)
1198 # Let the signature expire after 12 hours
1199 expires
= time
.time() + (12 * 3600)
1202 "aud" : "%s://%s" % (url
.scheme
, url
.netloc
),
1203 "exp" : int(expires
),
1204 "sub" : "mailto:info@ipfire.org",
1208 def _num_to_bytes(n
, pad_to
):
1210 Returns the byte representation of an integer, in big-endian order.
1214 r
= binascii
.unhexlify("0" * (len(h
) % 2) + h
)
1215 return b
"\x00" * (pad_to
- len(r
)) + r
1218 def _serialize_key(key
):
1219 if isinstance(key
, cryptography
.hazmat
.primitives
.asymmetric
.ec
.EllipticCurvePrivateKey
):
1220 return key
.private_bytes(
1221 cryptography
.hazmat
.primitives
.serialization
.Encoding
.DER
,
1222 cryptography
.hazmat
.primitives
.serialization
.PrivateFormat
.PKCS8
,
1223 cryptography
.hazmat
.primitives
.serialization
.NoEncryption(),
1226 return key
.public_bytes(
1227 cryptography
.hazmat
.primitives
.serialization
.Encoding
.X962
,
1228 cryptography
.hazmat
.primitives
.serialization
.PublicFormat
.UncompressedPoint
,
1232 def b64encode(data
):
1233 return base64
.urlsafe_b64encode(data
).strip(b
"=")
1235 def _encrypt(self
, message
):
1237 This is an absolutely ugly monster of a function which will sign the message
1241 # Encode everything as bytes
1242 if not isinstance(message
, bytes
):
1243 message
= message
.encode()
1245 # Generate some salt
1246 salt
= os
.urandom(16)
1249 chunk_size
= record_size
- 17
1251 # The client's public key
1252 p256dh
= cryptography
.hazmat
.primitives
.asymmetric
.ec
.EllipticCurvePublicKey
.from_encoded_point(
1253 cryptography
.hazmat
.primitives
.asymmetric
.ec
.SECP256R1(), bytes(self
.p256dh
),
1256 # Generate an ephemeral server key
1257 server_private_key
= cryptography
.hazmat
.primitives
.asymmetric
.ec
.generate_private_key(
1258 cryptography
.hazmat
.primitives
.asymmetric
.ec
.SECP256R1
,
1259 cryptography
.hazmat
.backends
.default_backend(),
1261 server_public_key
= server_private_key
.public_key()
1263 context
= b
"WebPush: info\x00"
1265 # Serialize the client's public key
1266 context
+= p256dh
.public_bytes(
1267 cryptography
.hazmat
.primitives
.serialization
.Encoding
.X962
,
1268 cryptography
.hazmat
.primitives
.serialization
.PublicFormat
.UncompressedPoint
,
1271 # Serialize the server's public key
1272 context
+= server_public_key
.public_bytes(
1273 cryptography
.hazmat
.primitives
.serialization
.Encoding
.X962
,
1274 cryptography
.hazmat
.primitives
.serialization
.PublicFormat
.UncompressedPoint
,
1277 # Perform key derivation with ECDH
1278 secret
= server_private_key
.exchange(
1279 cryptography
.hazmat
.primitives
.asymmetric
.ec
.ECDH(), p256dh
,
1283 hkdf_auth
= cryptography
.hazmat
.primitives
.kdf
.hkdf
.HKDF(
1284 algorithm
=cryptography
.hazmat
.primitives
.hashes
.SHA256(),
1288 backend
=cryptography
.hazmat
.backends
.default_backend(),
1290 secret
= hkdf_auth
.derive(secret
)
1292 # Derive the signing key
1293 hkdf_key
= cryptography
.hazmat
.primitives
.kdf
.hkdf
.HKDF(
1294 algorithm
=cryptography
.hazmat
.primitives
.hashes
.SHA256(),
1297 info
=b
"Content-Encoding: aes128gcm\x00",
1298 backend
=cryptography
.hazmat
.backends
.default_backend(),
1300 encryption_key
= hkdf_key
.derive(secret
)
1303 hkdf_nonce
= cryptography
.hazmat
.primitives
.kdf
.hkdf
.HKDF(
1304 algorithm
=cryptography
.hazmat
.primitives
.hashes
.SHA256(),
1307 info
=b
"Content-Encoding: nonce\x00",
1308 backend
=cryptography
.hazmat
.backends
.default_backend(),
1310 nonce
= hkdf_nonce
.derive(secret
)
1317 chunk
, message
= message
[:chunk_size
], message
[chunk_size
:]
1321 # Is this the last chunk?
1325 result
+= self
._encrypt
_chunk
(encryption_key
, nonce
, chunks
, chunk
, last
)
1330 # Fetch the public key
1331 key_id
= server_public_key
.public_bytes(
1332 cryptography
.hazmat
.primitives
.serialization
.Encoding
.X962
,
1333 cryptography
.hazmat
.primitives
.serialization
.PublicFormat
.UncompressedPoint
,
1336 # Join the entire message together
1339 struct
.pack("!L", record_size
),
1340 struct
.pack("!B", len(key_id
)),
1345 return b
"".join(message
)
1347 def _encrypt_chunk(self
, key
, nonce
, counter
, chunk
, last
=False):
1352 iv
= self
._make
_iv
(nonce
, counter
)
1354 log
.debug("Encrypting chunk %s: length = %s" % (counter
+ 1, len(chunk
)))
1362 cipher
= cryptography
.hazmat
.primitives
.ciphers
.Cipher(
1363 cryptography
.hazmat
.primitives
.ciphers
.algorithms
.AES128(key
),
1364 cryptography
.hazmat
.primitives
.ciphers
.modes
.GCM(iv
),
1365 backend
=cryptography
.hazmat
.backends
.default_backend(),
1369 encryptor
= cipher
.encryptor()
1372 chunk
= encryptor
.update(chunk
)
1374 # Finalize this round
1375 chunk
+= encryptor
.finalize() + encryptor
.tag
1380 def _make_iv(base
, counter
):
1381 mask
, = struct
.unpack("!Q", base
[4:])
1383 return base
[:4] + struct
.pack("!Q", counter ^ mask
)