Hello community, here is the log from the commit of package benji for openSUSE:Factory checked in at 2020-09-07 21:35:36 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/benji (Old) and /work/SRC/openSUSE:Factory/.benji.new.3399 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "benji" Mon Sep 7 21:35:36 2020 rev:9 rq:832695 version:0.10.0 Changes: -------- --- /work/SRC/openSUSE:Factory/benji/benji.changes 2020-07-28 17:26:52.858009748 +0200 +++ /work/SRC/openSUSE:Factory/.benji.new.3399/benji.changes 2020-09-07 21:35:54.473396100 +0200 @@ -1,0 +2,14 @@ +Mon Sep 7 08:04:41 UTC 2020 - Michael Vetter <mvet...@suse.com> + +- Update to 0.10.0: + * Helm chart changes: + - Change chart's requirements to use URL based repository references. + This should help when deploying Benji via FluxCD's helm-operator (#89) + - Fix rendering error when specifying a nodeSelector, node + affinities or tolerations. (#90) + - Use API group rbac.authorization.k8s.io/v1 for RBAC related resources + * Add new transform module aes_256_gcm_ecc (#86) + * Add support for discovering RBD images provisioned by Ceph's CSI + provisioner to the benji-backup-pvc script (#91) + +------------------------------------------------------------------- Old: ---- v0.9.0.tar.gz New: ---- v0.10.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ benji.spec ++++++ --- /var/tmp/diff_new_pack.UYsXrg/_old 2020-09-07 21:35:56.089396848 +0200 +++ /var/tmp/diff_new_pack.UYsXrg/_new 2020-09-07 21:35:56.089396848 +0200 @@ -17,7 +17,7 @@ Name: benji -Version: 0.9.0 +Version: 0.10.0 Release: 0 Summary: Deduplicating block based backup software License: LGPL-3.0-only ++++++ v0.9.0.tar.gz -> v0.10.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/.travis.yml new/benji-0.10.0/.travis.yml --- old/benji-0.9.0/.travis.yml 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/.travis.yml 2020-09-05 13:36:45.000000000 +0200 @@ -1,4 +1,4 @@ -dist: xenial +dist: bionic language: python cache: @@ -40,7 +40,7 @@ install: - travis_retry pip install --upgrade setuptools pip - - travis_retry pip install '.[s3,b2,compression,readcache,dev,doc]' + - travis_retry pip install '.[s3,b2,compression,dev,doc]' script: - pip freeze @@ -57,18 +57,28 @@ include: - stage: test install: - - travis_retry pip install --upgrade setuptools pip + - travis_retry pip install --upgrade 'setuptools>=50.2.0' pip script: - SKIP_DOCKER_PUSH=1 DOCKER_BUILDKIT=0 VERSION_VARIANT=miniver DOCKERFILE_PATH=images/benji/Dockerfile maint-scripts/docker-build - SKIP_DOCKER_PUSH=1 DOCKER_BUILDKIT=0 VERSION_VARIANT=miniver DOCKERFILE_PATH=images/benji-k8s/Dockerfile maint-scripts/docker-build - stage: test before_install: skip install: - - HELM_VERSION=2.12.3 + - HELM_VERSION=2.16.7 - curl --retry 5 -sLO https://storage.googleapis.com/kubernetes-helm/helm-v$HELM_VERSION-linux-amd64.tar.gz - tar --strip-components=1 -xzvvf helm-v$HELM_VERSION-linux-amd64.tar.gz linux-amd64/helm - rm -f helm-v$HELM_VERSION-linux-amd64.tar.gz - chmod a+x helm + script: + - ./helm lint charts/benji-k8s + - stage: test + before_install: skip + install: + - HELM_VERSION=3.3.1 + - curl --retry 5 -sLO https://get.helm.sh/helm-v$HELM_VERSION-linux-amd64.tar.gz + - tar --strip-components=1 -xzvvf helm-v$HELM_VERSION-linux-amd64.tar.gz linux-amd64/helm + - rm -f helm-v$HELM_VERSION-linux-amd64.tar.gz + - chmod a+x helm script: - ./helm lint charts/benji-k8s - stage: push diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/CHANGES.md new/benji-0.10.0/CHANGES.md --- old/benji-0.9.0/CHANGES.md 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/CHANGES.md 2020-09-05 13:36:45.000000000 +0200 @@ -1,5 +1,24 @@ +## 0.10.0, 05.09.2020 + +Notable changes: + +* Helm chart changes: + + * Change chart's requirements to use URL based repository references. This should help when deploying Benji via + FluxCD's helm-operator for example. (#89) + + * Fix rendering error when specifying a nodeSelector, node affinities or tolerations. (#90) + + * Use API group rbac.authorization.k8s.io/v1 for RBAC related resources + +* Add new transform module `aes_256_gcm_ecc` (#86) + +* Add support for discovering RBD images provisioned by Ceph's CSI provisioner to the `benji-backup-pvc` script (#91) + ## 0.9.0, 27.07.2020 +Notable changes: + * Sparse blocks are no longer explicitly represented in the database. This greatly speeds up the initial step of backup creation if the backup is based on an older version, and a lot of the blocks are sparse. It also reduces database size and speeds up database operations overall due to the reduced number of rows in the blocks table. No diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/charts/benji-k8s/requirements.yaml new/benji-0.10.0/charts/benji-k8s/requirements.yaml --- old/benji-0.9.0/charts/benji-k8s/requirements.yaml 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/charts/benji-k8s/requirements.yaml 2020-09-05 13:36:45.000000000 +0200 @@ -1,9 +1,9 @@ dependencies: - name: postgresql version: 7.7.2 - repository: "@stable" + repository: https://kubernetes-charts.storage.googleapis.com/ condition: postgresql.enabled - name: prometheus-pushgateway alias: pushgateway version: 1.2.6 - repository: "@stable" + repository: https://kubernetes-charts.storage.googleapis.com/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/charts/benji-k8s/templates/cluster-role-binding.yaml new/benji-0.10.0/charts/benji-k8s/templates/cluster-role-binding.yaml --- old/benji-0.9.0/charts/benji-k8s/templates/cluster-role-binding.yaml 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/charts/benji-k8s/templates/cluster-role-binding.yaml 2020-09-05 13:36:45.000000000 +0200 @@ -1,6 +1,6 @@ --- kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1beta1 +apiVersion: rbac.authorization.k8s.io/v1 metadata: name: {{ tuple . "" | include "benji.fullname" }} labels: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/charts/benji-k8s/templates/cluster-role.yaml new/benji-0.10.0/charts/benji-k8s/templates/cluster-role.yaml --- old/benji-0.9.0/charts/benji-k8s/templates/cluster-role.yaml 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/charts/benji-k8s/templates/cluster-role.yaml 2020-09-05 13:36:45.000000000 +0200 @@ -1,4 +1,4 @@ -apiVersion: rbac.authorization.k8s.io/v1beta1 +apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ tuple . "" | include "benji.fullname" }} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/charts/benji-k8s/templates/cronjobs.yaml new/benji-0.10.0/charts/benji-k8s/templates/cronjobs.yaml --- old/benji-0.9.0/charts/benji-k8s/templates/cronjobs.yaml 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/charts/benji-k8s/templates/cronjobs.yaml 2020-09-05 13:36:45.000000000 +0200 @@ -72,16 +72,13 @@ hostPath: path: /usr/share/zoneinfo/{{ $.Values.timeZone }} {{ toYaml $.Values.benji.volumes | nindent 12 }} - {{- with $.Values.benji.nodeSelector -}} - nodeSelector: - {{ toYaml . | nindent 12 }} + {{- with $.Values.benji.nodeSelector }} + nodeSelector: {{ toYaml . | nindent 12 }} {{- end -}} - {{- with $.Values.benji.affinity -}} - affinity: - {{ toYaml . | nindent 12 }} + {{- with $.Values.benji.affinity }} + affinity: {{ toYaml . | nindent 12 }} {{- end -}} - {{- with $.Values.benji.tolerations -}} - tolerations: - {{ toYaml . | nindent 12 }} + {{- with $.Values.benji.tolerations }} + tolerations: {{ toYaml . | nindent 12 }} {{- end -}} {{- end -}} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/docs/source/configuration.rst new/benji-0.10.0/docs/source/configuration.rst --- old/benji-0.9.0/docs/source/configuration.rst 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/docs/source/configuration.rst 2020-09-05 13:36:45.000000000 +0200 @@ -448,6 +448,47 @@ the salt and iteration count after writing encrypted data objects, they cannot be decrypted anymore. + +Transform Module aes_256_gcm_ecc +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module encrypts each data block using the ``aes_256_gcm`` module +(see above). Instead of specifying a symmetric master key, the encryption +key is encrypted asymmetrically using elliptic curve cryptography. The +implementation is based on the algorithm outlined in the ebook +`Practical Cryptography for Developers <https://cryptobook.nakov.com/asymmetric-key-ciphers/ecc-encryption-decryption>`_. + +The idea of this module is that the configuration used for backup does not +need to include the private key of the ECC key pair and so cannot decrypt any +of the data once it has been stored. For restore and deep-scrubbing operations +the private key is still needed but this key can be confined to a separate +specifically secured installation of Benji. + +The ``aes_256_gcm_ecc`` module supports the following configuration options: + +* name: **eccKey** +* type: BASE64-encoded DER representation of a ECC key (public or private) +* default: none + +Specify the encryption keyfile to encrypt a block's symmetric key. +For backup (encryption-only) this should be the public key only. +For restore (decryption) this must include the private key. + +* name: **eccCurve** +* type: string +* default: ``NIST P-384`` + +The ECC curve to use. Valid values are 'NIST P-256', 'NIST P-384' and 'NIST P-521'. + +Example python code to create a valid ECC key for **eccKey** (using ``PyCryptodome``):: + + from base64 import b64encode + from Crypto.PublicKey import ECC + key = ECC.generate(curve='NIST P-384') + public_key = b64encode(key.public_key().export_key(format='DER', compress=True)).decode('ascii')) + private_key = b64encode(key.export_key(format='DER', compress=True).decode('ascii')) + + Storage Modules --------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/docs/source/installation.rst new/benji-0.10.0/docs/source/installation.rst --- old/benji-0.9.0/docs/source/installation.rst 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/docs/source/installation.rst 2020-09-05 13:36:45.000000000 +0200 @@ -72,7 +72,6 @@ - ``s3``: AWS S3 object storage support - ``b2``: Backblaze's B2 Cloud object storage support - ``compression``: Compression support -- ``readcache``: Disk caching support Specify any extra extra features as a comma delimited list in square brackets after the package URL:: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/etc/benji.yaml new/benji-0.10.0/etc/benji.yaml --- old/benji-0.9.0/etc/benji.yaml 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/etc/benji.yaml 2020-09-05 13:36:45.000000000 +0200 @@ -74,7 +74,14 @@ # kdfSalt: # kdfIterations: # password: +# or # masterKey: +# +# - name: +# module: aes_256_gcm_ecc +# configuration: +# eccCurve: +# eccKey: io: # diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/images/benji-k8s/bin/benji-backup-pvc new/benji-0.10.0/images/benji-k8s/bin/benji-backup-pvc --- old/benji-0.9.0/images/benji-k8s/bin/benji-backup-pvc 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/images/benji-k8s/bin/benji-backup-pvc 2020-09-05 13:36:45.000000000 +0200 @@ -321,6 +321,19 @@ driver = pv.spec.flex_volume.driver if driver.startswith('ceph.rook.io/') and options.get('pool') and options.get('image'): pool, image = options['pool'], options['image'] + elif (hasattr(pv.spec, 'csi') + and hasattr(pv.spec.csi, 'driver') + and pv.spec.csi.driver == 'rook-ceph.rbd.csi.ceph.com' + and hasattr(pv.spec.csi, 'volume_handle') + and pv.spec.csi.volume_handle + and hasattr(pv.spec.csi, 'volume_attributes') + and pv.spec.csi.volume_attributes.get('pool')): + attributes = pv.spec.csi.volume_attributes + volume_handle_parts = pv.spec.csi.volume_handle.split('-') + if len(volume_handle_parts) >= 9: + image_prefix = attributes.get('volumeNamePrefix', 'csi-vol-') + image_suffix = '-'.join(volume_handle_parts[len(volume_handle_parts)-5:]) + pool, image = attributes['pool'], image_prefix + image_suffix if pool is None or image is None: continue diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/setup.py new/benji-0.10.0/setup.py --- old/benji-0.9.0/setup.py 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/setup.py 2020-09-05 13:36:45.000000000 +0200 @@ -1,8 +1,5 @@ # -*- encoding: utf-8 -*- -try: - from setuptools import setup, find_packages -except ImportError: - from distutils.core import setup, find_packages +from setuptools import setup, find_packages with open('README.rst', 'r', encoding='utf-8') as fh: long_description = fh.read() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/src/benji/_static_version.py new/benji-0.10.0/src/benji/_static_version.py --- old/benji-0.9.0/src/benji/_static_version.py 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/src/benji/_static_version.py 2020-09-05 13:36:45.000000000 +0200 @@ -8,5 +8,5 @@ version = "__use_git__" # These values are only set if the distribution was created with 'git archive' -refnames = "tag: v0.9.0" -git_hash = "e675435" +refnames = "HEAD -> master, tag: v0.10.0" +git_hash = "894738d" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/src/benji/_version.py new/benji-0.10.0/src/benji/_version.py --- old/benji-0.9.0/src/benji/_version.py 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/src/benji/_version.py 2020-09-05 13:36:45.000000000 +0200 @@ -4,10 +4,10 @@ import os import subprocess from collections import namedtuple -from distutils.command.build_py import build_py as build_py_orig from typing import List, Any, Dict, Optional, Sequence, Tuple from setuptools.command.sdist import sdist as sdist_orig +from setuptools.command.build_py import build_py as build_py_orig Version = namedtuple('Version', ('release', 'dev', 'labels')) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/src/benji/commands.py new/benji-0.10.0/src/benji/commands.py --- old/benji-0.9.0/src/benji/commands.py 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/src/benji/commands.py 2020-09-05 13:36:45.000000000 +0200 @@ -37,9 +37,7 @@ if labels: label_add, label_remove = InputValidation.parse_and_validate_labels(labels) - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: hints = None if rbd_hints: logger.debug(f'Loading RBD hints from file {rbd_hints}.') @@ -69,10 +67,7 @@ if self.machine_output: benji_obj.export_any({'versions': [backup_version]}, sys.stdout, - ignore_relationships=[((Version,), ('blocks',))]) - finally: - if benji_obj: - benji_obj.close() + ignore_relationships=(((Version,), ('blocks',)),)) def restore(self, version_uid: str, destination: str, sparse: bool, force: bool, database_less: bool, storage: str) -> None: @@ -80,119 +75,89 @@ raise benji.exception.UsageError('Specifying a storage location is only supported for database-less restores.') version_uid_obj = VersionUid(version_uid) - benji_obj = None - try: - benji_obj = Benji(self.config, in_memory_database=database_less) + with Benji(self.config, in_memory_database=database_less) as benji_obj: if database_less: benji_obj.metadata_restore([version_uid_obj], storage) benji_obj.restore(version_uid_obj, destination, sparse, force) - finally: - if benji_obj: - benji_obj.close() def protect(self, version_uids: List[str]) -> None: version_uid_objs = [VersionUid(version_uid) for version_uid in version_uids] - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: for version_uid in version_uid_objs: benji_obj.protect(version_uid, protected=True) - finally: - if benji_obj: - benji_obj.close() def unprotect(self, version_uids: List[str]) -> None: version_uid_objs = [VersionUid(version_uid) for version_uid in version_uids] - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: for version_uid in version_uid_objs: benji_obj.protect(version_uid, protected=False) - finally: - if benji_obj: - benji_obj.close() def rm(self, version_uids: List[str], force: bool, keep_metadata_backup: bool, override_lock: bool) -> None: version_uid_objs = [VersionUid(version_uid) for version_uid in version_uids] disallow_rm_when_younger_than_days = self.config.get('disallowRemoveWhenYounger', types=int) - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: for version_uid in version_uid_objs: benji_obj.rm(version_uid, force=force, disallow_rm_when_younger_than_days=disallow_rm_when_younger_than_days, keep_metadata_backup=keep_metadata_backup, override_lock=override_lock) - finally: - if benji_obj: - benji_obj.close() def scrub(self, version_uid: str, block_percentage: int) -> None: version_uid_obj = VersionUid(version_uid) - benji_obj = None - try: - benji_obj = Benji(self.config) - benji_obj.scrub(version_uid_obj, block_percentage=block_percentage) - except benji.exception.ScrubbingError: - assert benji_obj is not None - if self.machine_output: - benji_obj.export_any( - { - 'versions': [benji_obj.get_version_by_uid(version_uid=version_uid_obj)], - 'errors': [benji_obj.get_version_by_uid(version_uid=version_uid_obj)] - }, - sys.stdout, - ignore_relationships=[((Version,), ('blocks',))]) - raise - else: - if self.machine_output: - benji_obj.export_any( - { - 'versions': [benji_obj.get_version_by_uid(version_uid=version_uid_obj)], - 'errors': [] - }, - sys.stdout, - ignore_relationships=[((Version,), ('blocks',))]) - finally: - if benji_obj: - benji_obj.close() + with Benji(self.config) as benji_obj: + try: + benji_obj.scrub(version_uid_obj, block_percentage=block_percentage) + except benji.exception.ScrubbingError: + assert benji_obj is not None + if self.machine_output: + benji_obj.export_any( + { + 'versions': [benji_obj.get_version_by_uid(version_uid=version_uid_obj)], + 'errors': [benji_obj.get_version_by_uid(version_uid=version_uid_obj)] + }, + sys.stdout, + ignore_relationships=(((Version,), ('blocks',)),)) + raise + else: + if self.machine_output: + benji_obj.export_any( + { + 'versions': [benji_obj.get_version_by_uid(version_uid=version_uid_obj)], + 'errors': [] + }, + sys.stdout, + ignore_relationships=(((Version,), ('blocks',)),)) def deep_scrub(self, version_uid: str, source: str, block_percentage: int) -> None: version_uid_obj = VersionUid(version_uid) - benji_obj = None - try: - benji_obj = Benji(self.config) - benji_obj.deep_scrub(version_uid_obj, source=source, block_percentage=block_percentage) - except benji.exception.ScrubbingError: - assert benji_obj is not None - if self.machine_output: - benji_obj.export_any( - { - 'versions': [benji_obj.get_version_by_uid(version_uid=version_uid_obj)], - 'errors': [benji_obj.get_version_by_uid(version_uid=version_uid_obj)] - }, - sys.stdout, - ignore_relationships=[((Version,), ('blocks',))]) - raise - else: - if self.machine_output: - benji_obj.export_any( - { - 'versions': [benji_obj.get_version_by_uid(version_uid=version_uid_obj)], - 'errors': [] - }, - sys.stdout, - ignore_relationships=[((Version,), ('blocks',))]) - finally: - if benji_obj: - benji_obj.close() + with Benji(self.config) as benji_obj: + try: + benji_obj.deep_scrub(version_uid_obj, source=source, block_percentage=block_percentage) + except benji.exception.ScrubbingError: + assert benji_obj is not None + if self.machine_output: + benji_obj.export_any( + { + 'versions': [benji_obj.get_version_by_uid(version_uid=version_uid_obj)], + 'errors': [benji_obj.get_version_by_uid(version_uid=version_uid_obj)] + }, + sys.stdout, + ignore_relationships=(((Version,), ('blocks',)),)) + raise + else: + if self.machine_output: + benji_obj.export_any( + { + 'versions': [benji_obj.get_version_by_uid(version_uid=version_uid_obj)], + 'errors': [] + }, + sys.stdout, + ignore_relationships=(((Version,), ('blocks',)),)) def _batch_scrub(self, method: str, filter_expression: Optional[str], version_percentage: int, block_percentage: int, group_label: Optional[str]) -> None: - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: versions, errors = getattr(benji_obj, method)(filter_expression, version_percentage, block_percentage, group_label) if errors: @@ -212,10 +177,7 @@ 'errors': [] }, sys.stdout, - ignore_relationships=[((Version,), ('blocks',))]) - finally: - if benji_obj: - benji_obj.close() + ignore_relationships=(((Version,), ('blocks',)),)) def batch_scrub(self, filter_expression: Optional[str], version_percentage: int, block_percentage: int, group_label: Optional[str]) -> None: @@ -280,9 +242,7 @@ print(tbl) def ls(self, filter_expression: Optional[str], include_labels: bool, include_stats: bool) -> None: - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: versions = benji_obj.find_versions_with_filter(filter_expression) if self.machine_output: @@ -293,23 +253,13 @@ ) else: self._ls_versions_table_output(versions, include_labels, include_stats) - finally: - if benji_obj: - benji_obj.close() def cleanup(self, override_lock: bool) -> None: - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: benji_obj.cleanup(override_lock=override_lock) - finally: - if benji_obj: - benji_obj.close() def metadata_export(self, filter_expression: Optional[str], output_file: Optional[str], force: bool) -> None: - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: version_uid_objs = [version.uid for version in benji_obj.find_versions_with_filter(filter_expression)] if output_file is None: benji_obj.metadata_export(version_uid_objs, sys.stdout) @@ -319,42 +269,24 @@ with open(output_file, 'w') as f: benji_obj.metadata_export(version_uid_objs, f) - finally: - if benji_obj: - benji_obj.close() def metadata_backup(self, filter_expression: str, force: bool = False) -> None: - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: version_uid_objs = [version.uid for version in benji_obj.find_versions_with_filter(filter_expression)] benji_obj.metadata_backup(version_uid_objs, overwrite=force) - finally: - if benji_obj: - benji_obj.close() def metadata_import(self, input_file: str = None) -> None: - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: if input_file is None: benji_obj.metadata_import(sys.stdin) else: with open(input_file, 'r') as f: benji_obj.metadata_import(f) - finally: - if benji_obj: - benji_obj.close() def metadata_restore(self, version_uids: List[str], storage: str = None) -> None: version_uid_objs = [VersionUid(version_uid) for version_uid in version_uids] - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: benji_obj.metadata_restore(version_uid_objs, storage) - finally: - if benji_obj: - benji_obj.close() @staticmethod def _metadata_ls_table_output(version_uids: List[VersionUid]): @@ -366,9 +298,7 @@ print(tbl) def metadata_ls(self, storage: str = None) -> None: - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: version_uids = benji_obj.metadata_ls(storage) if self.machine_output: json.dump( @@ -378,16 +308,11 @@ ) else: self._metadata_ls_table_output(version_uids) - finally: - if benji_obj: - benji_obj.close() def label(self, version_uid: str, labels: List[str]) -> None: version_uid_obj = VersionUid(version_uid) label_add, label_remove = InputValidation.parse_and_validate_labels(labels) - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: for name, value in label_add: benji_obj.add_label(version_uid_obj, name, value) for name in label_remove: @@ -397,23 +322,16 @@ version_uid_obj, ', '.join('{}={}'.format(name, value) for name, value in label_add))) if label_remove: logger.info('Removed label(s) from version {}: {}.'.format(version_uid_obj, ', '.join(label_remove))) - finally: - if benji_obj: - benji_obj.close() def database_init(self) -> None: - benji_obj = Benji(self.config, init_database=True) - benji_obj.close() + Benji(self.config, init_database=True).close() def database_migrate(self) -> None: - benji_obj = Benji(self.config, migrate_database=True) - benji_obj.close() + Benji(self.config, migrate_database=True).close() def enforce_retention_policy(self, rules_spec: str, filter_expression: str, dry_run: bool, keep_metadata_backup: bool, group_label: Optional[str]) -> None: - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: dismissed_versions = benji_obj.enforce_retention_policy(filter_expression=filter_expression, rules_spec=rules_spec, dry_run=dry_run, @@ -424,23 +342,15 @@ 'versions': dismissed_versions, }, sys.stdout, - ignore_relationships=[((Version,), ('blocks',))]) - finally: - if benji_obj: - benji_obj.close() + ignore_relationships=(((Version,), ('blocks',)),)) def nbd(self, bind_address: str, bind_port: str, read_only: bool) -> None: - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: store = BenjiStore(benji_obj) addr = (bind_address, bind_port) server = NbdServer(addr, store, read_only) logger.info("Starting to serve NBD on %s:%s" % (addr[0], addr[1])) server.serve_forever() - finally: - if benji_obj: - benji_obj.close() def version_info(self) -> None: if not self.machine_output: @@ -486,9 +396,7 @@ print(tbl) def storage_stats(self, storage_name: str = None) -> None: - benji_obj = None - try: - benji_obj = Benji(self.config) + with Benji(self.config) as benji_obj: objects_count, objects_size = benji_obj.storage_stats(storage_name) if self.machine_output: @@ -499,9 +407,6 @@ print(json.dumps(result, indent=4)) else: self._ls_storage_stats_table_output(objects_count, objects_size) - finally: - if benji_obj: - benji_obj.close() @staticmethod def _storage_usage_table_output(usage: Dict[str, Dict[str, int]]) -> None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/src/benji/logging.py new/benji-0.10.0/src/benji/logging.py --- old/benji-0.9.0/src/benji/logging.py 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/src/benji/logging.py 2020-09-05 13:36:45.000000000 +0200 @@ -6,7 +6,7 @@ import sys import threading import warnings -from typing import Optional, Dict +from typing import Dict import structlog from structlog._frames import _find_first_app_frame_and_name @@ -52,18 +52,11 @@ structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ] -structlog.configure( - processors=_sl_processors, - context_class=dict, - logger_factory=structlog.stdlib.LoggerFactory(), - wrapper_class=structlog.stdlib.BoundLogger, - cache_logger_on_first_use=True, -) - -def init_logging(logfile: Optional[str], - console_level: str, - console_formatter: str = "console-plain", +def init_logging(*, + logfile: str = None, + console_level: str = 'INFO', + console_formatter: str = 'json', logfile_formatter: str = 'legacy') -> None: logging_config: Dict = { @@ -139,27 +132,9 @@ logging.config.dictConfig(logging_config) - # silence alembic - logging.getLogger('alembic').setLevel(logging.WARN) - # silence boto3 - # See https://github.com/boto/boto3/issues/521 - logging.getLogger('boto3').setLevel(logging.WARN) - logging.getLogger('botocore').setLevel(logging.WARN) - logging.getLogger('nose').setLevel(logging.WARN) - # This disables ResourceWarnings from boto3 which are normal - # See: https://github.com/boto/boto3/issues/454 - warnings.filterwarnings("ignore", - category=ResourceWarning, - message=r'unclosed.*<(?:ssl.SSLSocket|socket\.socket).*>') - # silence b2 - logging.getLogger('b2').setLevel(logging.WARN) - - if os.getenv('BENJI_DEBUG_SQL') == '1': - logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) - # Source: https://stackoverflow.com/questions/6234405/logging-uncaught-exceptions-in-python/16993115#16993115 -def handle_exception(exc_type, exc_value, exc_traceback): +def _handle_exception(exc_type, exc_value, exc_traceback): if issubclass(exc_type, KeyboardInterrupt): sys.__excepthook__(exc_type, exc_value, exc_traceback) return @@ -167,4 +142,30 @@ logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) -sys.excepthook = handle_exception +sys.excepthook = _handle_exception + +structlog.configure( + processors=_sl_processors, + context_class=dict, + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, +) + +init_logging() + +# silence alembic +logging.getLogger('alembic').setLevel(logging.WARN) +# silence boto3 +# See https://github.com/boto/boto3/issues/521 +logging.getLogger('boto3').setLevel(logging.WARN) +logging.getLogger('botocore').setLevel(logging.WARN) +logging.getLogger('nose').setLevel(logging.WARN) +# This disables ResourceWarnings from boto3 which are normal +# See: https://github.com/boto/boto3/issues/454 +warnings.filterwarnings("ignore", category=ResourceWarning, message=r'unclosed.*<(?:ssl.SSLSocket|socket\.socket).*>') +# silence b2 +logging.getLogger('b2').setLevel(logging.WARN) + +if os.getenv('BENJI_DEBUG_SQL') == '1': + logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/src/benji/scripts/benji.py new/benji-0.10.0/src/benji/scripts/benji.py --- old/benji-0.9.0/src/benji/scripts/benji.py 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/src/benji/scripts/benji.py 2020-09-05 13:36:45.000000000 +0200 @@ -305,9 +305,15 @@ else: config = Config() - init_logging(config.get('logFile', types=(str, type(None))), + console_formatter = 'console-colored' + if args.machine_output: + console_formatter = 'json' + elif args.no_color: + console_formatter = 'console-plain' + + init_logging(logfile=config.get('logFile', types=(str, type(None))), console_level=args.log_level, - console_formatter='console-plain' if args.no_color else 'console-colored') + console_formatter=console_formatter) IOFactory.initialize(config) StorageFactory.initialize(config) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/src/benji/tests/test_transform_ecc.py new/benji-0.10.0/src/benji/tests/test_transform_ecc.py --- old/benji-0.9.0/src/benji/tests/test_transform_ecc.py 1970-01-01 01:00:00.000000000 +0100 +++ new/benji-0.10.0/src/benji/tests/test_transform_ecc.py 2020-09-05 13:36:45.000000000 +0200 @@ -0,0 +1,68 @@ +import base64 +from unittest import TestCase + +from Crypto.PublicKey import ECC + +from benji.config import ConfigDict +from benji.transform.aes_256_gcm_ecc import Transform + + +class TestEccTransform(TestCase): + + @staticmethod + def _get_transform_args(key): + conf = ConfigDict() + conf['eccKey'] = base64.b64encode(Transform._pack_envelope_key(key)).decode('ascii') + conf['eccCurve'] = key.curve + return Transform(name='EccTest', config=None, module_configuration=conf) + + @classmethod + def _get_transform(cls): + curve = 'NIST P-384' + return cls._get_transform_args(ECC.generate(curve=curve)) + + def setUp(self): + self.ecc_transform = self._get_transform() + + def test_decryption(self): + data = b'THIS IS A TEST' + enc_data, materials = self.ecc_transform.encapsulate(data=data) + self.assertTrue(enc_data) + self.assertNotEqual(enc_data, data) + + dec_data = self.ecc_transform.decapsulate(data=enc_data, materials=materials) + self.assertEqual(dec_data, data) + + def test_encryption_random(self): + ecc_transform_ref = self._get_transform() + + data = b'THIS IS A TEST' + + enc_data, materials = self.ecc_transform.encapsulate(data=data) + enc_data_ref, materials_ref = ecc_transform_ref.encapsulate(data=data) + + self.assertNotEqual(enc_data, enc_data_ref) + self.assertNotEqual(materials['envelope_key'], materials_ref['envelope_key']) + + def test_envelope_key(self): + data = b'THIS IS A TEST' + + enc_data, materials = self.ecc_transform.encapsulate(data=data) + + envelope_key = self.ecc_transform._unpack_envelope_key(base64.b64decode(materials['envelope_key'])) + self.assertFalse(envelope_key.has_private()) + + def test_pubkey_only_encryption(self): + curve = 'NIST P-384' + ecc_transform = self._get_transform_args(ECC.generate(curve=curve).public_key()) + data = b'THIS IS A TEST' + enc_data, materials = ecc_transform.encapsulate(data=data) + self.assertTrue(enc_data) + self.assertNotEqual(enc_data, data) + + def test_pubkey_only_decryption(self): + curve = 'NIST P-384' + ecc_transform = self._get_transform_args(ECC.generate(curve=curve).public_key()) + data = b'THIS IS A TEST' + enc_data, materials = ecc_transform.encapsulate(data=data) + self.assertRaises(ValueError, ecc_transform.decapsulate, data=enc_data, materials=materials) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/src/benji/tests/testcase.py new/benji-0.10.0/src/benji/tests/testcase.py --- old/benji-0.9.0/src/benji/tests/testcase.py 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/src/benji/tests/testcase.py 2020-09-05 13:36:45.000000000 +0200 @@ -45,8 +45,7 @@ def setUp(self): self.testpath = self.TestPath() - init_logging(None, - logging.WARN if os.environ.get('UNITTEST_QUIET', False) else logging.DEBUG, + init_logging(console_level=logging.WARN if os.environ.get('UNITTEST_QUIET', False) else logging.DEBUG, console_formatter='console-plain') self.config = Config(ad_hoc_config=self.CONFIG.format(testpath=self.testpath.path)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/src/benji/transform/aes_256_gcm.py new/benji-0.10.0/src/benji/transform/aes_256_gcm.py --- old/benji-0.9.0/src/benji/transform/aes_256_gcm.py 2020-07-27 11:20:08.000000000 +0200 +++ new/benji-0.10.0/src/benji/transform/aes_256_gcm.py 2020-09-05 13:36:45.000000000 +0200 @@ -11,6 +11,7 @@ class Transform(TransformBase): + AES_KEY_LEN = 32 def __init__(self, *, config: Config, name: str, module_configuration: ConfigDict) -> None: super().__init__(config=config, name=name, module_configuration=module_configuration) @@ -19,7 +20,7 @@ if master_key_encoded is not None: master_key = base64.b64decode(master_key_encoded) - if len(master_key) != 32: + if len(master_key) != self.AES_KEY_LEN: raise ValueError('Key masterKey has the wrong length. It must be 32 bytes long and encoded as BASE64.') self._master_key = master_key @@ -30,15 +31,21 @@ self._master_key = derive_key(salt=kdf_salt, iterations=kdf_iterations, key_length=32, password=password) + def _create_envelope_key(self) -> Tuple[bytes, bytes]: + envelope_key = get_random_bytes(self.AES_KEY_LEN) + encrypted_key = aes_wrap_key(self._master_key, envelope_key) + return envelope_key, encrypted_key + + def _derive_envelope_key(self, encrypted_key: bytes) -> bytes: + return aes_unwrap_key(self._master_key, encrypted_key) + def encapsulate(self, *, data: bytes) -> Tuple[Optional[bytes], Optional[Dict]]: - envelope_key = get_random_bytes(32) + envelope_key, encrypted_key = self._create_envelope_key() envelope_iv = get_random_bytes(16) encryptor = AES.new(envelope_key, AES.MODE_GCM, nonce=envelope_iv) - envelope_key = aes_wrap_key(self._master_key, envelope_key) - materials = { - 'envelope_key': base64.b64encode(envelope_key).decode('ascii'), + 'envelope_key': base64.b64encode(encrypted_key).decode('ascii'), 'iv': base64.b64encode(envelope_iv).decode('ascii'), } @@ -59,8 +66,8 @@ raise ValueError('Encryption materials IV iv has wrong length of {}. It must be 16 bytes long.'.format( len(iv))) - envelope_key = aes_unwrap_key(self._master_key, envelope_key) - if len(envelope_key) != 32: + envelope_key = self._derive_envelope_key(envelope_key) + if len(envelope_key) != self.AES_KEY_LEN: raise ValueError( 'Encryption materials key envelope_key has wrong length of {}. It must be 32 bytes long.'.format( len(envelope_key))) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/benji-0.9.0/src/benji/transform/aes_256_gcm_ecc.py new/benji-0.10.0/src/benji/transform/aes_256_gcm_ecc.py --- old/benji-0.9.0/src/benji/transform/aes_256_gcm_ecc.py 1970-01-01 01:00:00.000000000 +0100 +++ new/benji-0.10.0/src/benji/transform/aes_256_gcm_ecc.py 2020-09-05 13:36:45.000000000 +0200 @@ -0,0 +1,67 @@ +import base64 +from hashlib import sha256 +from typing import Dict, Tuple, Optional + +from Crypto.PublicKey import ECC + +from benji.config import Config, ConfigDict +from benji.logging import logger +from benji.transform.aes_256_gcm import Transform as TransformAES + + +class Transform(TransformAES): + + def __init__(self, *, config: Config, name: str, module_configuration: ConfigDict) -> None: + ecc_key_der: str = Config.get_from_dict(module_configuration, 'eccKey', types=str) + ecc_curve: Optional[str] = Config.get_from_dict(module_configuration, 'eccCurve', 'NIST P-384', types=str) + + ecc_key = self._unpack_envelope_key(base64.b64decode(ecc_key_der)) + + if ecc_key.curve != ecc_curve: + raise ValueError(f'Key eccKey does not match the eccCurve setting (found: {ecc_key.curve}, expected: {ecc_curve}).') + + self._ecc_key = ecc_key + self._ecc_curve = ecc_key.curve + + point_q_len = self._ecc_key.pointQ.size_in_bytes() + if point_q_len < self.AES_KEY_LEN: + raise ValueError(f'Size of point Q is smaller than the AES key length, which reduces security ({point_q_len} < {self.AES_KEY_LEN}).') + + # Note: We don't actually have a "master" aes key, because the key is derived from the ECC key + # and set before calling the parent's encapsulate/decapsulate method. + aes_config = module_configuration.copy() + aes_config['masterKey'] = base64.b64encode(b'\x00' * self.AES_KEY_LEN).decode('ascii') + super().__init__(config=config, name=name, module_configuration=aes_config) + + @staticmethod + def _pack_envelope_key(key: ECC.EccKey) -> bytes: + return key.export_key(format='DER', compress=True) + + @staticmethod + def _unpack_envelope_key(key: bytes) -> ECC.EccKey: + return ECC.import_key(key) + + @staticmethod + def _ecc_point_to_key(point: ECC.EccPoint) -> bytes: + sha = sha256(int.to_bytes(int(point.x), point.size_in_bytes(), 'big')) + sha.update(int.to_bytes(int(point.y), point.size_in_bytes(), 'big')) + return sha.digest() + + def _create_envelope_key(self) -> Tuple[bytes, bytes]: + cipher_privkey = ECC.generate(curve=self._ecc_curve) + shared_key = self._ecc_point_to_key(self._ecc_key.pointQ * cipher_privkey.d) + return shared_key, self._pack_envelope_key(cipher_privkey.public_key()) + + def _derive_envelope_key(self, cipher_pubkey: bytes) -> bytes: + ecc_point = self._unpack_envelope_key(cipher_pubkey) + return self._ecc_point_to_key(ecc_point.pointQ * self._ecc_key.d) + + def encapsulate(self, *, data: bytes) -> Tuple[Optional[bytes], Optional[Dict]]: + if self._ecc_key.has_private(): + logger.warning('ECC key loaded from config includes private key data, which is not needed for encryption.') + return super().encapsulate(data=data) + + def decapsulate(self, *, data: bytes, materials: Dict) -> bytes: + if not self._ecc_key.has_private(): + raise ValueError('ECC key loaded from config does not include private key data, cannot proceed.') + return super().decapsulate(data=data, materials=materials)