microdata-tools 1.14.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/PKG-INFO +20 -18
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/README.md +19 -17
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/__init__.py +7 -1
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/packaging/__init__.py +16 -14
- microdata_tools-2.0.0/microdata_tools/packaging/_crypto_config.py +10 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/packaging/_decrypt.py +55 -41
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/packaging/_encrypt.py +47 -41
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/packaging/_utils.py +8 -8
- microdata_tools-2.0.0/microdata_tools/packaging/keys.py +192 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/pyproject.toml +1 -1
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/.gitignore +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/LICENSE.md +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/packaging/exceptions/__init__.py +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/__init__.py +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/adapter/local_storage.py +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/temporal_attributes.py +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_id_types.py +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/BEDRIFT.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/BK_HELSESTASJONSKONSULTASJON.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/BRUKSENHET.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/FAMILIE.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/FENGSLINGER_TILGANG.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/FORETAK.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/HKDIR_HYU_OPPMELDING.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/HKDIR_STUDIESOKNAD.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/HUSHOLDNING.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/JOBB.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/KJORETOY.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/KOMMUNE.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/KRG_KREFTTILFELLE.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/KURS.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/MALEPUNKT.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/NPR_EPISODE.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/NPR_PROSEDYRE.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/NPR_SYKEHUSOPPHOLD.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/PERSON.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/SOESKEN.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/SOSTIL.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/TRAFIKKULYKKE.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/TRAFIKKULYKKE_PERSON.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/VIRKSOMHET.json +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/components/unit_type_variables/__init__.py +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/exceptions/__init__.py +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/model/__init__.py +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/model/metadata.py +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/steps/data_reader.py +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/steps/dataset_validator.py +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/steps/metadata_enricher.py +0 -0
- {microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/steps/metadata_reader.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microdata-tools
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Tools for the microdata.no platform
|
|
5
5
|
Author: microdata-developers
|
|
6
6
|
License: MIT
|
|
@@ -35,14 +35,13 @@ The CSV file is optional in some cases.
|
|
|
35
35
|
The `package_dataset()` function will encrypt and package your dataset as a tar archive. The process is as follows:
|
|
36
36
|
|
|
37
37
|
1. Generate the symmetric key for a dataset.
|
|
38
|
-
2. Encrypt the dataset data (CSV) using
|
|
39
|
-
3. Encrypt the symmetric key using the
|
|
40
|
-
|
|
41
|
-
4. Gather the encrypted CSV, encrypted symmetric key and metadata (JSON) file in one tar file.
|
|
38
|
+
2. Encrypt the dataset data (CSV) using an AES-256-GCM symmetric key and store the encrypted file as `<DATASET_NAME>.csv.encr`
|
|
39
|
+
3. Encrypt the symmetric key using HPKE with the combined ML-KEM-768/X25519 public key from `microdata_public_key.pem` and store the resulting HPKE ciphertext as <DATASET_NAME>.kem.encr
|
|
40
|
+
4. Gather the encrypted CSV, ciphertext file and metadata (JSON) file in one tar file.
|
|
42
41
|
|
|
43
42
|
### Unpackage dataset
|
|
44
|
-
The `unpackage_dataset()` function will untar and
|
|
45
|
-
|
|
43
|
+
The `unpackage_dataset()` function will untar and your dataset and use the combined ML-KEM-768/X25519 private key from `microdata_private_key.pem` to recover the symmetric key, which is then used to decrypt the dataset.
|
|
44
|
+
|
|
46
45
|
|
|
47
46
|
The packaged file has to have the `<DATASET_NAME>.tar` extension. Its contents should be as follows:
|
|
48
47
|
|
|
@@ -50,30 +49,33 @@ The packaged file has to have the `<DATASET_NAME>.tar` extension. Its contents s
|
|
|
50
49
|
|
|
51
50
|
```<DATASET_NAME>.csv.encr``` : Optional encrypted dataset file.
|
|
52
51
|
|
|
53
|
-
```<DATASET_NAME>.
|
|
52
|
+
```<DATASET_NAME>.kem.encr``` : Optional HPKE ciphertext file containing the encrypted symmetric key required to decrypt the dataset file. Required if the `.csv.encr` file is present.
|
|
54
53
|
|
|
55
|
-
Decryption uses the
|
|
54
|
+
Decryption uses the combined ML-KEM-768/X25519 private key located at ```PRIVATE_KEY_DIR``` to recover the symmetric decryption key.
|
|
56
55
|
|
|
57
56
|
The packaged file is then stored in `output_dir/archive/unpackaged` after a successful run or `output_dir/archive/failed` after an unsuccessful run.
|
|
58
57
|
|
|
58
|
+
|
|
59
59
|
## Example
|
|
60
|
-
|
|
60
|
+
|
|
61
|
+
Store your metadata and data files according to the structure described above, and put
|
|
62
|
+
the provided public key in a directory of your choice.
|
|
63
|
+
Then:
|
|
61
64
|
|
|
62
65
|
```py
|
|
63
66
|
from pathlib import Path
|
|
64
67
|
from microdata_tools import package_dataset
|
|
65
68
|
|
|
66
|
-
RSA_KEYS_DIRECTORY = Path("tests/resources/rsa_keys")
|
|
67
|
-
DATASET_DIRECTORY = Path("tests/resources/input_package/DATASET_1")
|
|
68
|
-
OUTPUT_DIRECTORY = Path("tests/resources/output")
|
|
69
|
-
|
|
70
69
|
package_dataset(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
public_key_dir=Path("path/to/key_directory"),
|
|
71
|
+
dataset_dir=Path("path/to/MY_DATASET_NAME"),
|
|
72
|
+
output_dir=Path("path/to/output"),
|
|
74
73
|
)
|
|
75
74
|
```
|
|
76
75
|
|
|
76
|
+
This produces `path/to/output/MY_DATASET_NAME.tar`, which can be uploaded to microdata.
|
|
77
|
+
|
|
78
|
+
|
|
77
79
|
### Validation
|
|
78
80
|
|
|
79
81
|
Once you have your metadata and data files ready to go, they should be named and stored like this:
|
|
@@ -104,7 +106,7 @@ else:
|
|
|
104
106
|
print(error)
|
|
105
107
|
```
|
|
106
108
|
|
|
107
|
-
For a more in-depth explanation of usage visit [the usage documentation](https://statisticsnorway.github.io/microdata-tools/
|
|
109
|
+
For a more in-depth explanation of usage visit [the usage documentation](https://statisticsnorway.github.io/microdata-tools/USAGE).
|
|
108
110
|
|
|
109
111
|
### Data format description
|
|
110
112
|
A dataset as defined in microdata consists of one data file, and one metadata file.
|
|
@@ -21,14 +21,13 @@ The CSV file is optional in some cases.
|
|
|
21
21
|
The `package_dataset()` function will encrypt and package your dataset as a tar archive. The process is as follows:
|
|
22
22
|
|
|
23
23
|
1. Generate the symmetric key for a dataset.
|
|
24
|
-
2. Encrypt the dataset data (CSV) using
|
|
25
|
-
3. Encrypt the symmetric key using the
|
|
26
|
-
|
|
27
|
-
4. Gather the encrypted CSV, encrypted symmetric key and metadata (JSON) file in one tar file.
|
|
24
|
+
2. Encrypt the dataset data (CSV) using an AES-256-GCM symmetric key and store the encrypted file as `<DATASET_NAME>.csv.encr`
|
|
25
|
+
3. Encrypt the symmetric key using HPKE with the combined ML-KEM-768/X25519 public key from `microdata_public_key.pem` and store the resulting HPKE ciphertext as <DATASET_NAME>.kem.encr
|
|
26
|
+
4. Gather the encrypted CSV, ciphertext file and metadata (JSON) file in one tar file.
|
|
28
27
|
|
|
29
28
|
### Unpackage dataset
|
|
30
|
-
The `unpackage_dataset()` function will untar and
|
|
31
|
-
|
|
29
|
+
The `unpackage_dataset()` function will untar and your dataset and use the combined ML-KEM-768/X25519 private key from `microdata_private_key.pem` to recover the symmetric key, which is then used to decrypt the dataset.
|
|
30
|
+
|
|
32
31
|
|
|
33
32
|
The packaged file has to have the `<DATASET_NAME>.tar` extension. Its contents should be as follows:
|
|
34
33
|
|
|
@@ -36,30 +35,33 @@ The packaged file has to have the `<DATASET_NAME>.tar` extension. Its contents s
|
|
|
36
35
|
|
|
37
36
|
```<DATASET_NAME>.csv.encr``` : Optional encrypted dataset file.
|
|
38
37
|
|
|
39
|
-
```<DATASET_NAME>.
|
|
38
|
+
```<DATASET_NAME>.kem.encr``` : Optional HPKE ciphertext file containing the encrypted symmetric key required to decrypt the dataset file. Required if the `.csv.encr` file is present.
|
|
40
39
|
|
|
41
|
-
Decryption uses the
|
|
40
|
+
Decryption uses the combined ML-KEM-768/X25519 private key located at ```PRIVATE_KEY_DIR``` to recover the symmetric decryption key.
|
|
42
41
|
|
|
43
42
|
The packaged file is then stored in `output_dir/archive/unpackaged` after a successful run or `output_dir/archive/failed` after an unsuccessful run.
|
|
44
43
|
|
|
44
|
+
|
|
45
45
|
## Example
|
|
46
|
-
|
|
46
|
+
|
|
47
|
+
Store your metadata and data files according to the structure described above, and put
|
|
48
|
+
the provided public key in a directory of your choice.
|
|
49
|
+
Then:
|
|
47
50
|
|
|
48
51
|
```py
|
|
49
52
|
from pathlib import Path
|
|
50
53
|
from microdata_tools import package_dataset
|
|
51
54
|
|
|
52
|
-
RSA_KEYS_DIRECTORY = Path("tests/resources/rsa_keys")
|
|
53
|
-
DATASET_DIRECTORY = Path("tests/resources/input_package/DATASET_1")
|
|
54
|
-
OUTPUT_DIRECTORY = Path("tests/resources/output")
|
|
55
|
-
|
|
56
55
|
package_dataset(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
public_key_dir=Path("path/to/key_directory"),
|
|
57
|
+
dataset_dir=Path("path/to/MY_DATASET_NAME"),
|
|
58
|
+
output_dir=Path("path/to/output"),
|
|
60
59
|
)
|
|
61
60
|
```
|
|
62
61
|
|
|
62
|
+
This produces `path/to/output/MY_DATASET_NAME.tar`, which can be uploaded to microdata.
|
|
63
|
+
|
|
64
|
+
|
|
63
65
|
### Validation
|
|
64
66
|
|
|
65
67
|
Once you have your metadata and data files ready to go, they should be named and stored like this:
|
|
@@ -90,7 +92,7 @@ else:
|
|
|
90
92
|
print(error)
|
|
91
93
|
```
|
|
92
94
|
|
|
93
|
-
For a more in-depth explanation of usage visit [the usage documentation](https://statisticsnorway.github.io/microdata-tools/
|
|
95
|
+
For a more in-depth explanation of usage visit [the usage documentation](https://statisticsnorway.github.io/microdata-tools/USAGE).
|
|
94
96
|
|
|
95
97
|
### Data format description
|
|
96
98
|
A dataset as defined in microdata consists of one data file, and one metadata file.
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
from microdata_tools.packaging import
|
|
1
|
+
from microdata_tools.packaging import (
|
|
2
|
+
package_dataset,
|
|
3
|
+
unpackage_dataset,
|
|
4
|
+
)
|
|
5
|
+
from microdata_tools.packaging.keys import PrivateKey, PublicKey
|
|
2
6
|
from microdata_tools.validation import (
|
|
3
7
|
get_unit_id_type_for_unit_type,
|
|
4
8
|
validate_dataset,
|
|
@@ -11,4 +15,6 @@ __all__ = [
|
|
|
11
15
|
"validate_dataset",
|
|
12
16
|
"validate_metadata",
|
|
13
17
|
"get_unit_id_type_for_unit_type",
|
|
18
|
+
"PrivateKey",
|
|
19
|
+
"PublicKey",
|
|
14
20
|
]
|
|
@@ -23,14 +23,15 @@ logger = logging.getLogger()
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def package_dataset(
|
|
26
|
-
|
|
26
|
+
public_key_dir: Path, dataset_dir: Path, output_dir: Path
|
|
27
27
|
) -> None:
|
|
28
28
|
"""
|
|
29
29
|
Packages a dataset. It will encrypt and tar the dataset using
|
|
30
|
-
the provided
|
|
30
|
+
the provided combined ML-KEM-768/X25519 public key.
|
|
31
|
+
Only the CSV file will be encrypted.
|
|
31
32
|
Creates a checksum file for the CSV file.
|
|
32
33
|
|
|
33
|
-
:param
|
|
34
|
+
:param public_key_dir:
|
|
34
35
|
directory containing public key file microdata_public_key.pem
|
|
35
36
|
:param dataset_dir:
|
|
36
37
|
directory containing the dataset files (CSV and JSON)
|
|
@@ -59,7 +60,7 @@ def package_dataset(
|
|
|
59
60
|
if len(csv_files) == 1:
|
|
60
61
|
write_checksum_to_file(csv_files[0])
|
|
61
62
|
encrypt_dataset(
|
|
62
|
-
|
|
63
|
+
public_key_dir=public_key_dir,
|
|
63
64
|
dataset_dir=dataset_dir,
|
|
64
65
|
output_dir=output_dir,
|
|
65
66
|
)
|
|
@@ -67,10 +68,10 @@ def package_dataset(
|
|
|
67
68
|
if not dataset_output_dir.exists():
|
|
68
69
|
os.makedirs(dataset_output_dir)
|
|
69
70
|
|
|
70
|
-
if Path(dataset_dir / f"{dataset_name}.
|
|
71
|
+
if Path(dataset_dir / f"{dataset_name}.sha256").exists():
|
|
71
72
|
shutil.move(
|
|
72
|
-
dataset_dir / f"{dataset_name}.
|
|
73
|
-
dataset_output_dir / f"{dataset_name}.
|
|
73
|
+
dataset_dir / f"{dataset_name}.sha256",
|
|
74
|
+
dataset_output_dir / f"{dataset_name}.sha256",
|
|
74
75
|
)
|
|
75
76
|
|
|
76
77
|
shutil.copyfile(
|
|
@@ -87,17 +88,18 @@ def package_dataset(
|
|
|
87
88
|
|
|
88
89
|
def unpackage_dataset(
|
|
89
90
|
packaged_file_path: Path,
|
|
90
|
-
|
|
91
|
+
private_key_dir: Path,
|
|
91
92
|
output_dir: Path,
|
|
92
93
|
) -> None:
|
|
93
94
|
"""
|
|
94
95
|
Unpackages a dataset. It will untar and decrypt the dataset using
|
|
95
|
-
the provided
|
|
96
|
+
the provided combined ML-KEM-768/X25519 private key.
|
|
97
|
+
Only the CSV file will be decrypted.
|
|
96
98
|
Validates the checksum of the CSV file.
|
|
97
99
|
|
|
98
100
|
:param packaged_file_path:
|
|
99
101
|
a Path to the .tar file containing the dataset files
|
|
100
|
-
:param
|
|
102
|
+
:param private_key_dir:
|
|
101
103
|
directory containing the private key file microdata_private_key.pem
|
|
102
104
|
:param output_dir:
|
|
103
105
|
output directory
|
|
@@ -105,12 +107,12 @@ def unpackage_dataset(
|
|
|
105
107
|
None
|
|
106
108
|
"""
|
|
107
109
|
check_exists(packaged_file_path)
|
|
108
|
-
check_exists(
|
|
110
|
+
check_exists(private_key_dir)
|
|
109
111
|
|
|
110
112
|
if not output_dir.exists():
|
|
111
113
|
os.makedirs(output_dir)
|
|
112
114
|
|
|
113
|
-
private_key_path =
|
|
115
|
+
private_key_path = private_key_dir / "microdata_private_key.pem"
|
|
114
116
|
check_exists(private_key_path)
|
|
115
117
|
|
|
116
118
|
dataset_name = packaged_file_path.stem
|
|
@@ -119,7 +121,7 @@ def unpackage_dataset(
|
|
|
119
121
|
|
|
120
122
|
try:
|
|
121
123
|
untar_encrypted_dataset(packaged_file_path, dataset_name, dataset_dir)
|
|
122
|
-
decrypt(
|
|
124
|
+
decrypt(private_key_dir, dataset_dir, output_dir)
|
|
123
125
|
_validate_csv_consistency(dataset_name, dataset_dir, output_dir)
|
|
124
126
|
|
|
125
127
|
if Path(dataset_dir).exists():
|
|
@@ -139,5 +141,5 @@ def _validate_csv_consistency(
|
|
|
139
141
|
output_dir / dataset_name / f"{dataset_name}.csv"
|
|
140
142
|
)
|
|
141
143
|
compare_checksum_with_file(
|
|
142
|
-
dataset_dir / f"{dataset_name}.
|
|
144
|
+
dataset_dir / f"{dataset_name}.sha256", calculated_checksum
|
|
143
145
|
)
|
|
@@ -5,24 +5,34 @@ import tarfile
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import List, Tuple
|
|
7
7
|
|
|
8
|
-
from cryptography.
|
|
9
|
-
from cryptography.hazmat.
|
|
10
|
-
from cryptography.hazmat.primitives import
|
|
11
|
-
|
|
8
|
+
from cryptography.exceptions import InvalidTag
|
|
9
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
10
|
+
from cryptography.hazmat.primitives.hpke import (
|
|
11
|
+
MLKEM768X25519PrivateKey,
|
|
12
|
+
)
|
|
12
13
|
|
|
13
|
-
from microdata_tools.packaging.
|
|
14
|
+
from microdata_tools.packaging._crypto_config import (
|
|
15
|
+
HPKE_INFO,
|
|
16
|
+
HPKE_SUITE,
|
|
17
|
+
NONCE_SIZE_BYTES,
|
|
18
|
+
)
|
|
19
|
+
from microdata_tools.packaging._utils import (
|
|
20
|
+
check_exists,
|
|
21
|
+
)
|
|
14
22
|
from microdata_tools.packaging.exceptions import (
|
|
15
23
|
InvalidKeyError,
|
|
16
24
|
InvalidTarFileContents,
|
|
17
25
|
)
|
|
26
|
+
from microdata_tools.packaging.keys import PrivateKey
|
|
18
27
|
|
|
19
28
|
logger = logging.getLogger()
|
|
20
29
|
|
|
21
30
|
|
|
22
|
-
def decrypt(
|
|
31
|
+
def decrypt(private_key_dir: Path, dataset_dir: Path, output_dir: Path) -> None:
|
|
23
32
|
"""
|
|
24
33
|
Decrypts a dataset as follows:
|
|
25
|
-
1.
|
|
34
|
+
1. Recovers the symmetric key using the hybrid private key
|
|
35
|
+
(HPKE with ML-KEM-768 + X25519).
|
|
26
36
|
2. Decrypts each chunk using the symmetric key.
|
|
27
37
|
3. Merges the decrypted chunks into a single file.
|
|
28
38
|
"""
|
|
@@ -39,36 +49,35 @@ def decrypt(rsa_keys_dir: Path, dataset_dir: Path, output_dir: Path) -> None:
|
|
|
39
49
|
decrypted_dir = output_dataset_dir / "decrypted"
|
|
40
50
|
os.makedirs(decrypted_dir, exist_ok=True)
|
|
41
51
|
|
|
42
|
-
if not output_dataset_dir.exists():
|
|
43
|
-
os.makedirs(output_dataset_dir)
|
|
44
|
-
|
|
45
52
|
if chunk_dir.exists() and first_encrypted_chunk.exists():
|
|
46
53
|
logger.info(f"Encrypted file found in {dataset_dir}")
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
# Read private key from file
|
|
56
|
+
private_key = PrivateKey.load_from_file(
|
|
57
|
+
Path(private_key_dir / "microdata_private_key.pem")
|
|
58
|
+
).to_hpke_key()
|
|
59
|
+
|
|
60
|
+
if not isinstance(private_key, MLKEM768X25519PrivateKey):
|
|
61
|
+
raise TypeError(
|
|
62
|
+
"Private key is not a hybrid ML-KEM-768-X25519 private key."
|
|
53
63
|
)
|
|
64
|
+
# Read the hpke ciphertext from file and recover the symmetric key
|
|
65
|
+
hpke_ciphertext_path = Path(dataset_dir / f"{dataset_name}.kem.encr")
|
|
66
|
+
check_exists(hpke_ciphertext_path)
|
|
54
67
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
label=None,
|
|
69
|
-
),
|
|
70
|
-
)
|
|
71
|
-
fernet = Fernet(decrypted_symkey)
|
|
68
|
+
with open(hpke_ciphertext_path, "rb") as f:
|
|
69
|
+
hpke_ciphertext = f.read()
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
decrypted_symkey = HPKE_SUITE.decrypt(
|
|
73
|
+
hpke_ciphertext, private_key, info=HPKE_INFO
|
|
74
|
+
)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
raise InvalidKeyError(
|
|
77
|
+
"Unable to recover symmetric key. Is the private key correct?"
|
|
78
|
+
) from e
|
|
79
|
+
|
|
80
|
+
aesgcm = AESGCM(decrypted_symkey)
|
|
72
81
|
|
|
73
82
|
# Decrypt all the encrypted csv files in the directory
|
|
74
83
|
for encrypted_file in chunk_dir.iterdir():
|
|
@@ -78,14 +87,18 @@ def decrypt(rsa_keys_dir: Path, dataset_dir: Path, output_dir: Path) -> None:
|
|
|
78
87
|
# Decrypt csv file
|
|
79
88
|
with open(encrypted_file, "rb") as file:
|
|
80
89
|
data = file.read()
|
|
90
|
+
nonce, ciphertext = (
|
|
91
|
+
data[:NONCE_SIZE_BYTES],
|
|
92
|
+
data[NONCE_SIZE_BYTES:],
|
|
93
|
+
)
|
|
81
94
|
|
|
82
95
|
try:
|
|
83
|
-
decrypted_data =
|
|
96
|
+
decrypted_data = aesgcm.decrypt(nonce, ciphertext, None)
|
|
84
97
|
|
|
85
98
|
with open(decrypted_dir / f"{csv_file}", "wb") as file:
|
|
86
99
|
file.write(decrypted_data)
|
|
87
100
|
|
|
88
|
-
except
|
|
101
|
+
except InvalidTag as exc:
|
|
89
102
|
raise InvalidKeyError(
|
|
90
103
|
f"Not able to decrypt {encrypted_file}, "
|
|
91
104
|
f"is symkey correct?"
|
|
@@ -93,8 +106,8 @@ def decrypt(rsa_keys_dir: Path, dataset_dir: Path, output_dir: Path) -> None:
|
|
|
93
106
|
|
|
94
107
|
logger.debug(f"Decrypted {encrypted_file}")
|
|
95
108
|
|
|
96
|
-
del
|
|
97
|
-
del
|
|
109
|
+
del decrypted_symkey
|
|
110
|
+
del aesgcm
|
|
98
111
|
|
|
99
112
|
# Merges the decrypted csv files into a single file
|
|
100
113
|
_combine_csv_files(decrypted_dir, dataset_dir / f"{dataset_name}.csv")
|
|
@@ -140,14 +153,15 @@ def untar_encrypted_dataset(
|
|
|
140
153
|
|
|
141
154
|
|
|
142
155
|
def _validate_tar_contents(files: List[str], dataset_name: str) -> None:
|
|
143
|
-
|
|
156
|
+
filenames = {os.path.basename(f) for f in files}
|
|
157
|
+
if f"{dataset_name}.json" not in filenames:
|
|
144
158
|
raise InvalidTarFileContents(f"{dataset_name}.json not in .tar file")
|
|
145
159
|
|
|
146
160
|
if len(files) > 1:
|
|
147
|
-
if f"{dataset_name}.
|
|
161
|
+
if f"{dataset_name}.kem.encr" not in filenames:
|
|
148
162
|
raise InvalidTarFileContents(
|
|
149
163
|
f"Tar file for {dataset_name} does not contain the required "
|
|
150
|
-
f"{dataset_name}.
|
|
164
|
+
f"{dataset_name}.kem.encr file"
|
|
151
165
|
)
|
|
152
166
|
|
|
153
167
|
chunk_files = [str for str in files if str.endswith(".csv.encr")]
|
|
@@ -157,10 +171,10 @@ def _validate_tar_contents(files: List[str], dataset_name: str) -> None:
|
|
|
157
171
|
f"Tar file for {dataset_name} does not contain any chunks files"
|
|
158
172
|
)
|
|
159
173
|
|
|
160
|
-
if f"{dataset_name}.
|
|
174
|
+
if f"{dataset_name}.sha256" not in filenames:
|
|
161
175
|
raise InvalidTarFileContents(
|
|
162
176
|
f"Tar file for {dataset_name} does not contain the required "
|
|
163
|
-
f"{dataset_name}.
|
|
177
|
+
f"{dataset_name}.sha256 file"
|
|
164
178
|
)
|
|
165
179
|
|
|
166
180
|
|
|
@@ -4,13 +4,21 @@ import shutil
|
|
|
4
4
|
import tarfile
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from cryptography.
|
|
8
|
-
from cryptography.hazmat.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
from microdata_tools.packaging.
|
|
7
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
8
|
+
from cryptography.hazmat.primitives.hpke import (
|
|
9
|
+
MLKEM768X25519PublicKey,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from microdata_tools.packaging._crypto_config import (
|
|
13
|
+
HPKE_INFO,
|
|
14
|
+
HPKE_SUITE,
|
|
15
|
+
NONCE_SIZE_BYTES,
|
|
16
|
+
)
|
|
17
|
+
from microdata_tools.packaging._utils import (
|
|
18
|
+
check_exists,
|
|
19
|
+
)
|
|
13
20
|
from microdata_tools.packaging.exceptions import ValidationException
|
|
21
|
+
from microdata_tools.packaging.keys import PublicKey
|
|
14
22
|
|
|
15
23
|
logger = logging.getLogger()
|
|
16
24
|
|
|
@@ -18,7 +26,7 @@ CHUNK_SIZE_BYTES = 250_000_000 # 250 MB per chunk
|
|
|
18
26
|
|
|
19
27
|
|
|
20
28
|
def encrypt_dataset(
|
|
21
|
-
|
|
29
|
+
public_key_dir: Path,
|
|
22
30
|
dataset_dir: Path,
|
|
23
31
|
output_dir: Path,
|
|
24
32
|
) -> None:
|
|
@@ -27,28 +35,29 @@ def encrypt_dataset(
|
|
|
27
35
|
1. Generates the symmetric key for this dataset.
|
|
28
36
|
2. Splits the dataset into chunks.
|
|
29
37
|
3. Encrypts each chunk using the symmetric key.
|
|
30
|
-
4.
|
|
38
|
+
4. Uses HPKE with a hybrid ML-KEM-768/X25519 public key to encrypt the
|
|
39
|
+
symmetric key and stores the resulting encapsulated ciphertext.
|
|
31
40
|
"""
|
|
32
41
|
|
|
33
|
-
check_exists(
|
|
42
|
+
check_exists(public_key_dir)
|
|
34
43
|
check_exists(dataset_dir)
|
|
35
44
|
|
|
36
45
|
if not output_dir.exists():
|
|
37
46
|
os.makedirs(output_dir)
|
|
38
47
|
|
|
39
|
-
public_key_location =
|
|
48
|
+
public_key_location = public_key_dir / "microdata_public_key.pem"
|
|
40
49
|
check_exists(public_key_location)
|
|
41
50
|
|
|
42
51
|
# Read public key from file
|
|
43
|
-
|
|
44
|
-
public_key = serialization.load_pem_public_key(
|
|
45
|
-
key_file.read(), backend=default_backend()
|
|
46
|
-
)
|
|
52
|
+
public_key = PublicKey.load_from_file(public_key_location).to_hpke_key()
|
|
47
53
|
|
|
54
|
+
if not isinstance(public_key, MLKEM768X25519PublicKey):
|
|
55
|
+
raise TypeError(
|
|
56
|
+
"Public key is not a hybrid ML-KEM-768-X25519 public key."
|
|
57
|
+
)
|
|
48
58
|
csv_files = [
|
|
49
59
|
file for file in dataset_dir.iterdir() if file.suffix == ".csv"
|
|
50
60
|
]
|
|
51
|
-
|
|
52
61
|
csv_file = csv_files[0]
|
|
53
62
|
dataset_name = csv_file.stem
|
|
54
63
|
|
|
@@ -62,11 +71,10 @@ def encrypt_dataset(
|
|
|
62
71
|
os.makedirs(dataset_output_dir)
|
|
63
72
|
os.makedirs(dataset_output_dir / "chunks", exist_ok=True)
|
|
64
73
|
|
|
65
|
-
|
|
74
|
+
hpke_ciphertext_file = dataset_output_dir / f"{dataset_name}.kem.encr"
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
fernet = Fernet(symkey)
|
|
76
|
+
symkey = AESGCM.generate_key(bit_length=256)
|
|
77
|
+
aesgcm = AESGCM(symkey)
|
|
70
78
|
|
|
71
79
|
# Encrypt csv file
|
|
72
80
|
chunk_count = 0
|
|
@@ -79,7 +87,9 @@ def encrypt_dataset(
|
|
|
79
87
|
break
|
|
80
88
|
|
|
81
89
|
chunk_count += 1
|
|
82
|
-
|
|
90
|
+
nonce = os.urandom(NONCE_SIZE_BYTES)
|
|
91
|
+
# The nonce is stored along with the ciphertext to decrypt later
|
|
92
|
+
encrypted = nonce + aesgcm.encrypt(nonce, data, None)
|
|
83
93
|
|
|
84
94
|
chunk_file = (
|
|
85
95
|
dataset_output_dir / "chunks" / f"{chunk_count}.csv.encr"
|
|
@@ -89,27 +99,23 @@ def encrypt_dataset(
|
|
|
89
99
|
|
|
90
100
|
logger.debug(f"Csv file {csv_file} encrypted into {chunk_count} chunks")
|
|
91
101
|
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
# Encrypt symmetric key using HPKE and store the ciphertext
|
|
103
|
+
try:
|
|
104
|
+
hpke_ciphertext = HPKE_SUITE.encrypt(symkey, public_key, info=HPKE_INFO)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
raise ValidationException(
|
|
107
|
+
"Failed to encrypt the dataset key using HPKE. "
|
|
108
|
+
"Please check that the public key is valid and try again."
|
|
109
|
+
) from e
|
|
94
110
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
padding.OAEP(
|
|
98
|
-
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
99
|
-
algorithm=hashes.SHA256(),
|
|
100
|
-
label=None,
|
|
101
|
-
),
|
|
102
|
-
)
|
|
111
|
+
with open(hpke_ciphertext_file, "wb") as file:
|
|
112
|
+
file.write(hpke_ciphertext)
|
|
103
113
|
|
|
104
114
|
del symkey
|
|
105
|
-
del
|
|
106
|
-
|
|
107
|
-
# Store encrypted symkey to file
|
|
108
|
-
with open(encrypted_symkey_file, "wb") as file:
|
|
109
|
-
file.write(encrypted_sym_key)
|
|
115
|
+
del aesgcm
|
|
110
116
|
|
|
111
117
|
logger.debug(
|
|
112
|
-
f"
|
|
118
|
+
f"HPKE-encrypted key ciphertext stored in {hpke_ciphertext_file}"
|
|
113
119
|
)
|
|
114
120
|
|
|
115
121
|
|
|
@@ -143,16 +149,16 @@ def tar_encrypted_dataset(input_dir: Path, dataset_name: str) -> None:
|
|
|
143
149
|
if len(chunk_files) == 0:
|
|
144
150
|
raise ValidationException(f"No files found in {chunk_dir}")
|
|
145
151
|
|
|
146
|
-
|
|
147
|
-
if not
|
|
152
|
+
sha256_file = dataset_dir / f"{dataset_name}.sha256"
|
|
153
|
+
if not sha256_file.exists():
|
|
148
154
|
raise ValidationException(
|
|
149
|
-
f"The required file {
|
|
155
|
+
f"The required file {sha256_file} is missing"
|
|
150
156
|
)
|
|
151
157
|
|
|
152
158
|
files_to_tar.extend(
|
|
153
159
|
[
|
|
154
|
-
dataset_dir / f"{dataset_name}.
|
|
155
|
-
dataset_dir / f"{dataset_name}.
|
|
160
|
+
dataset_dir / f"{dataset_name}.kem.encr",
|
|
161
|
+
dataset_dir / f"{dataset_name}.sha256",
|
|
156
162
|
]
|
|
157
163
|
)
|
|
158
164
|
|
|
@@ -14,29 +14,29 @@ def check_exists(path: Path) -> None:
|
|
|
14
14
|
|
|
15
15
|
def calculate_checksum(csv_file: Path) -> str:
|
|
16
16
|
"""
|
|
17
|
-
Reads a file in chunks and returns the
|
|
17
|
+
Reads a file in chunks and returns the SHA-256 hash of the file
|
|
18
18
|
"""
|
|
19
19
|
CHUNK_SIZE = 32768
|
|
20
|
-
|
|
20
|
+
hash_sha256 = hashlib.sha256()
|
|
21
21
|
with open(csv_file, "rb") as f:
|
|
22
22
|
for chunk in iter(lambda: f.read(CHUNK_SIZE), b""):
|
|
23
|
-
|
|
23
|
+
hash_sha256.update(chunk)
|
|
24
24
|
|
|
25
|
-
return
|
|
25
|
+
return hash_sha256.hexdigest()
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def write_checksum_to_file(csv_file: Path) -> None:
|
|
29
|
-
hash_file = str(csv_file).replace(".csv", ".
|
|
29
|
+
hash_file = str(csv_file).replace(".csv", ".sha256")
|
|
30
30
|
with open(hash_file, "w") as file:
|
|
31
31
|
file.write(calculate_checksum(csv_file))
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def compare_checksum_with_file(
|
|
35
|
-
|
|
35
|
+
sha256_file: Path, calculated_checksum: str
|
|
36
36
|
) -> None:
|
|
37
|
-
with open(
|
|
37
|
+
with open(sha256_file, "r") as file:
|
|
38
38
|
checksum_from_file = file.readlines()[0]
|
|
39
39
|
if calculated_checksum != checksum_from_file.strip():
|
|
40
40
|
raise CsvConsistencyException(
|
|
41
|
-
"
|
|
41
|
+
"SHA-256 checksums do not match. The csv file may be corrupted!"
|
|
42
42
|
)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from cryptography.hazmat.primitives import serialization
|
|
5
|
+
from cryptography.hazmat.primitives.asymmetric import mlkem, x25519
|
|
6
|
+
from cryptography.hazmat.primitives.hpke import (
|
|
7
|
+
MLKEM768X25519PrivateKey,
|
|
8
|
+
MLKEM768X25519PublicKey,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
PEM_PRIVATE_BLOCK = re.compile(
|
|
12
|
+
rb"-----BEGIN PRIVATE KEY-----.*?-----END PRIVATE KEY-----",
|
|
13
|
+
re.DOTALL,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
PEM_PUBLIC_BLOCK = re.compile(
|
|
17
|
+
rb"-----BEGIN PUBLIC KEY-----.*?-----END PUBLIC KEY-----",
|
|
18
|
+
re.DOTALL,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PrivateKey:
|
|
23
|
+
"""Hybrid ML-KEM-768 + X25519 private key for use with HPKE."""
|
|
24
|
+
|
|
25
|
+
FILENAME = "microdata_private_key.pem"
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
mlkem_key: mlkem.MLKEM768PrivateKey,
|
|
30
|
+
x25519_key: x25519.X25519PrivateKey,
|
|
31
|
+
) -> None:
|
|
32
|
+
self.mlkem_key = mlkem_key
|
|
33
|
+
self.x25519_key = x25519_key
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def generate(cls) -> "PrivateKey":
|
|
37
|
+
"""Generates a new hybrid private key."""
|
|
38
|
+
return cls(
|
|
39
|
+
mlkem_key=mlkem.MLKEM768PrivateKey.generate(),
|
|
40
|
+
x25519_key=x25519.X25519PrivateKey.generate(),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def load_from_file(cls, path: Path) -> "PrivateKey":
|
|
45
|
+
"""Loads a hybrid private key from a PEM file containing one
|
|
46
|
+
ML-KEM-768 and one X25519 PRIVATE KEY block, in any order."""
|
|
47
|
+
blocks = PEM_PRIVATE_BLOCK.findall(path.read_bytes())
|
|
48
|
+
if len(blocks) != 2:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Expected 2 PEM blocks in hybrid private key file, "
|
|
51
|
+
f"found {len(blocks)}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
parsed_keys = [
|
|
55
|
+
serialization.load_pem_private_key(block, password=None)
|
|
56
|
+
for block in blocks
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
mlkem_key = next(
|
|
60
|
+
(
|
|
61
|
+
key
|
|
62
|
+
for key in parsed_keys
|
|
63
|
+
if isinstance(key, mlkem.MLKEM768PrivateKey)
|
|
64
|
+
),
|
|
65
|
+
None,
|
|
66
|
+
)
|
|
67
|
+
x25519_key = next(
|
|
68
|
+
(
|
|
69
|
+
key
|
|
70
|
+
for key in parsed_keys
|
|
71
|
+
if isinstance(key, x25519.X25519PrivateKey)
|
|
72
|
+
),
|
|
73
|
+
None,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if mlkem_key is None or x25519_key is None:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
"Hybrid private key file must contain one ML-KEM-768 key "
|
|
79
|
+
"and one X25519 key."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return cls(mlkem_key=mlkem_key, x25519_key=x25519_key)
|
|
83
|
+
|
|
84
|
+
def public_key(self) -> "PublicKey":
|
|
85
|
+
"""Returns the matching hybrid PublicKey."""
|
|
86
|
+
return PublicKey(
|
|
87
|
+
mlkem_key=self.mlkem_key.public_key(),
|
|
88
|
+
x25519_key=self.x25519_key.public_key(),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def serialize(self) -> bytes:
|
|
92
|
+
"""Serializes both halves as concatenated PKCS8 PEM blocks."""
|
|
93
|
+
return self.mlkem_key.private_bytes(
|
|
94
|
+
encoding=serialization.Encoding.PEM,
|
|
95
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
96
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
97
|
+
) + self.x25519_key.private_bytes(
|
|
98
|
+
encoding=serialization.Encoding.PEM,
|
|
99
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
100
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def write_to_file(self, target_dir: Path) -> None:
|
|
104
|
+
"""Writes the serialized key to
|
|
105
|
+
``<target_dir>/<PrivateKey.FILENAME>``."""
|
|
106
|
+
(target_dir / self.FILENAME).write_bytes(self.serialize())
|
|
107
|
+
|
|
108
|
+
def to_hpke_key(self) -> MLKEM768X25519PrivateKey:
|
|
109
|
+
"""Returns the combined HPKE-usable key object."""
|
|
110
|
+
return MLKEM768X25519PrivateKey(
|
|
111
|
+
mlkem_key=self.mlkem_key,
|
|
112
|
+
x25519_key=self.x25519_key,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class PublicKey:
|
|
117
|
+
"""Hybrid ML-KEM-768 + X25519 public key for use with HPKE."""
|
|
118
|
+
|
|
119
|
+
FILENAME = "microdata_public_key.pem"
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
mlkem_key: mlkem.MLKEM768PublicKey,
|
|
124
|
+
x25519_key: x25519.X25519PublicKey,
|
|
125
|
+
) -> None:
|
|
126
|
+
self.mlkem_key = mlkem_key
|
|
127
|
+
self.x25519_key = x25519_key
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def load_from_file(cls, path: Path) -> "PublicKey":
|
|
131
|
+
"""Loads a hybrid public key from a PEM file containing one
|
|
132
|
+
ML-KEM-768 and one X25519 PUBLIC KEY block, in any order."""
|
|
133
|
+
return cls.from_pem(path.read_bytes())
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def from_pem(cls, pem_data: bytes) -> "PublicKey":
|
|
137
|
+
"""Loads a hybrid public key from PEM data containing one
|
|
138
|
+
ML-KEM-768 and one X25519 PUBLIC KEY block, in any order."""
|
|
139
|
+
blocks = PEM_PUBLIC_BLOCK.findall(pem_data)
|
|
140
|
+
if len(blocks) != 2:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
"Expected 2 PEM blocks in hybrid public key file, "
|
|
143
|
+
f"found {len(blocks)}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
parsed_keys = [serialization.load_pem_public_key(b) for b in blocks]
|
|
147
|
+
mlkem_key = next(
|
|
148
|
+
(
|
|
149
|
+
key
|
|
150
|
+
for key in parsed_keys
|
|
151
|
+
if isinstance(key, mlkem.MLKEM768PublicKey)
|
|
152
|
+
),
|
|
153
|
+
None,
|
|
154
|
+
)
|
|
155
|
+
x25519_key = next(
|
|
156
|
+
(
|
|
157
|
+
key
|
|
158
|
+
for key in parsed_keys
|
|
159
|
+
if isinstance(key, x25519.X25519PublicKey)
|
|
160
|
+
),
|
|
161
|
+
None,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if mlkem_key is None or x25519_key is None:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
"Hybrid public key file must contain one ML-KEM-768 key "
|
|
167
|
+
"and one X25519 key."
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return cls(mlkem_key=mlkem_key, x25519_key=x25519_key)
|
|
171
|
+
|
|
172
|
+
def serialize(self) -> bytes:
|
|
173
|
+
"""Serializes both keys as concatenated PEM blocks."""
|
|
174
|
+
return self.mlkem_key.public_bytes(
|
|
175
|
+
encoding=serialization.Encoding.PEM,
|
|
176
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
177
|
+
) + self.x25519_key.public_bytes(
|
|
178
|
+
encoding=serialization.Encoding.PEM,
|
|
179
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def write_to_file(self, target_dir: Path) -> None:
|
|
183
|
+
"""Writes the serialized keys to
|
|
184
|
+
``<target_dir>/<PublicKey.FILENAME>``."""
|
|
185
|
+
(target_dir / self.FILENAME).write_bytes(self.serialize())
|
|
186
|
+
|
|
187
|
+
def to_hpke_key(self) -> MLKEM768X25519PublicKey:
|
|
188
|
+
"""Returns the combined HPKE-usable key object."""
|
|
189
|
+
return MLKEM768X25519PublicKey(
|
|
190
|
+
mlkem_key=self.mlkem_key,
|
|
191
|
+
x25519_key=self.x25519_key,
|
|
192
|
+
)
|
|
File without changes
|
|
File without changes
|
{microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/packaging/exceptions/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/adapter/local_storage.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/exceptions/__init__.py
RENAMED
|
File without changes
|
{microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/model/__init__.py
RENAMED
|
File without changes
|
{microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/model/metadata.py
RENAMED
|
File without changes
|
{microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/steps/data_reader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microdata_tools-1.14.0 → microdata_tools-2.0.0}/microdata_tools/validation/steps/metadata_reader.py
RENAMED
|
File without changes
|