Skip to content

Commit a46de8e

Browse files
committed
comments
1 parent f6e138f commit a46de8e

3 files changed

Lines changed: 81 additions & 49 deletions

File tree

‎pyiceberg/encryption/kms.py‎

Lines changed: 10 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17-
"""Key management client interface for table encryption, and an in-memory implementation."""
17+
"""Key management client interface for table encryption."""
1818

1919
from __future__ import annotations
2020

2121
from abc import ABC, abstractmethod
2222
from dataclasses import dataclass, field
2323

24-
from pyiceberg.encryption.ciphers import AesGcmCipher, AesKeySize, SecureKey
24+
from pyiceberg.typedef import EMPTY_DICT, Properties
2525

2626

2727
@dataclass(frozen=True)
@@ -36,8 +36,16 @@ class KeyManagementClient(ABC):
3636
"""A base class for key management service implementations.
3737
3838
Wraps and unwraps table encryption keys using master keys that the service holds.
39+
40+
Implementations are loaded by name from the catalog properties, so a subclass must keep
41+
this constructor signature, as `FileIO` does.
3942
"""
4043

44+
properties: Properties
45+
46+
def __init__(self, properties: Properties = EMPTY_DICT) -> None:
47+
self.properties = properties
48+
4149
@abstractmethod
4250
def wrap_key(self, key: bytes, wrapping_key_id: str) -> bytes:
4351
"""Wrap a key using the master key identified by `wrapping_key_id`.
@@ -67,48 +75,3 @@ def generate_key(self, wrapping_key_id: str) -> GeneratedKey:
6775
wrapping_key_id (str): Identifies the master key held by the service.
6876
"""
6977
raise NotImplementedError(f"{type(self).__name__} does not support key generation")
70-
71-
72-
class MemoryKeyManagementClient(KeyManagementClient):
73-
"""A key management service that holds its master keys in memory, for testing and demonstration.
74-
75-
Master keys live only in this process, with no durability or access control, so this is
76-
not for production use. Mirrors Java's `MemoryMockKMS` and iceberg-rust's
77-
`MemoryKeyManagementClient`.
78-
"""
79-
80-
def __init__(self, master_key_size: AesKeySize = AesKeySize.BITS_128) -> None:
81-
self._master_key_size = master_key_size
82-
self._master_keys: dict[str, SecureKey] = {}
83-
84-
def __repr__(self) -> str:
85-
"""Return a representation that counts the master keys without exposing them."""
86-
return f"MemoryKeyManagementClient(master_key_size={self._master_key_size!r}, key_count={len(self._master_keys)})"
87-
88-
def add_master_key(self, wrapping_key_id: str, key: SecureKey | None = None) -> SecureKey:
89-
"""Register a master key under `wrapping_key_id`, generating one when `key` is omitted.
90-
91-
Args:
92-
wrapping_key_id (str): The id to register the master key under.
93-
key (SecureKey | None): Known key material, for tests that share it with another client.
94-
"""
95-
if wrapping_key_id in self._master_keys:
96-
raise ValueError(f"Master key already exists: {wrapping_key_id}")
97-
98-
master_key = SecureKey.generate(self._master_key_size) if key is None else key
99-
self._master_keys[wrapping_key_id] = master_key
100-
return master_key
101-
102-
def _cipher(self, wrapping_key_id: str) -> AesGcmCipher:
103-
if (master_key := self._master_keys.get(wrapping_key_id)) is None:
104-
raise ValueError(f"Master key not found: {wrapping_key_id}")
105-
106-
return AesGcmCipher(master_key)
107-
108-
def wrap_key(self, key: bytes, wrapping_key_id: str) -> bytes:
109-
"""Wrap a key with the registered master key, without AAD, as Java and iceberg-rust do."""
110-
return self._cipher(wrapping_key_id).encrypt(key)
111-
112-
def unwrap_key(self, wrapped_key: bytes, wrapping_key_id: str) -> bytes:
113-
"""Unwrap a key wrapped by `wrap_key`."""
114-
return self._cipher(wrapping_key_id).decrypt(wrapped_key)

‎tests/encryption/memory_kms.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
"""An in-memory key management client for tests, mirroring Java's `MemoryMockKMS`."""
18+
19+
from __future__ import annotations
20+
21+
from pyiceberg.encryption.ciphers import AesGcmCipher, AesKeySize, SecureKey
22+
from pyiceberg.encryption.kms import KeyManagementClient
23+
from pyiceberg.typedef import EMPTY_DICT, Properties
24+
25+
26+
class MemoryKeyManagementClient(KeyManagementClient):
27+
"""A key management service that holds its master keys in memory.
28+
29+
Master keys live only in this process, with no durability or access control, so this is
30+
for tests only.
31+
"""
32+
33+
def __init__(self, properties: Properties = EMPTY_DICT, *, master_key_size: AesKeySize = AesKeySize.BITS_128) -> None:
34+
super().__init__(properties)
35+
self._master_key_size = master_key_size
36+
self._master_keys: dict[str, SecureKey] = {}
37+
38+
def __repr__(self) -> str:
39+
"""Return a representation that counts the master keys without exposing them."""
40+
return f"MemoryKeyManagementClient(master_key_size={self._master_key_size!r}, key_count={len(self._master_keys)})"
41+
42+
def add_master_key(self, wrapping_key_id: str, key: SecureKey | None = None) -> SecureKey:
43+
"""Register a master key under `wrapping_key_id`, generating one when `key` is omitted.
44+
45+
Args:
46+
wrapping_key_id (str): The id to register the master key under.
47+
key (SecureKey | None): Known key material, for tests that share it with another client.
48+
"""
49+
if wrapping_key_id in self._master_keys:
50+
raise ValueError(f"Master key already exists: {wrapping_key_id}")
51+
52+
master_key = SecureKey.generate(self._master_key_size) if key is None else key
53+
self._master_keys[wrapping_key_id] = master_key
54+
return master_key
55+
56+
def _cipher(self, wrapping_key_id: str) -> AesGcmCipher:
57+
if (master_key := self._master_keys.get(wrapping_key_id)) is None:
58+
raise ValueError(f"Master key not found: {wrapping_key_id}")
59+
60+
return AesGcmCipher(master_key)
61+
62+
def wrap_key(self, key: bytes, wrapping_key_id: str) -> bytes:
63+
"""Wrap a key with the registered master key, without AAD, as Java and iceberg-rust do."""
64+
return self._cipher(wrapping_key_id).encrypt(key)
65+
66+
def unwrap_key(self, wrapped_key: bytes, wrapping_key_id: str) -> bytes:
67+
"""Unwrap a key wrapped by `wrap_key`."""
68+
return self._cipher(wrapping_key_id).decrypt(wrapped_key)

‎tests/encryption/test_kms.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616
# under the License.
1717

1818
import pytest
19+
from memory_kms import MemoryKeyManagementClient
1920

2021
from pyiceberg.encryption.ciphers import AesGcmCipher, AesKeySize, SecureKey
21-
from pyiceberg.encryption.kms import GeneratedKey, KeyManagementClient, MemoryKeyManagementClient
22+
from pyiceberg.encryption.kms import GeneratedKey, KeyManagementClient
2223

2324
MASTER_KEY_ID = "master-key"
2425
MASTER_KEY = SecureKey(b"0123456789012345")
@@ -60,7 +61,7 @@ def test_wrap_unwrap_round_trip(kms: MemoryKeyManagementClient) -> None:
6061

6162
@pytest.mark.parametrize("key_size", list(AesKeySize))
6263
def test_wrap_unwrap_round_trip_for_each_master_key_size(key_size: AesKeySize) -> None:
63-
kms = MemoryKeyManagementClient(key_size)
64+
kms = MemoryKeyManagementClient(master_key_size=key_size)
6465
master_key = kms.add_master_key(MASTER_KEY_ID)
6566

6667
assert master_key.key_size == key_size

0 commit comments

Comments
 (0)