Skip to content

Commit 70cd3cc

Browse files
akshaywadiaAkshay Wadia
andauthored
Add SymmetricPIR key gen and documentation (#219)
Co-authored-by: Akshay Wadia <awadia@apple.com>
1 parent a9acbb1 commit 70cd3cc

File tree

10 files changed

+161
-28
lines changed

10 files changed

+161
-28
lines changed

Sources/HomomorphicEncryption/Util.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import Foundation
1516
import ModularArithmetic
1617

1718
extension Sequence where Element: Hashable {
@@ -70,6 +71,13 @@ extension [UInt8] {
7071
}
7172
}
7273

74+
extension [UInt8] {
75+
/// Hexadecimal encoded bytes.
76+
public var hexString: String {
77+
map { String(format: "%02x", $0) }.joined()
78+
}
79+
}
80+
7381
extension Array where Element: FixedWidthInteger {
7482
/// Computes the product of the elements in the array.
7583
///

Sources/HomomorphicEncryption/Zeroization.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors
1+
// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -15,16 +15,26 @@
1515
#if canImport(Darwin)
1616
import Darwin
1717

18+
/// Set bytes to zero.
19+
/// - Parameters:
20+
/// - s: Pointer to memory region to be zeroed out.
21+
/// - n: Number of bytes to be zeroed out.
22+
@inlinable
1823
// swiftlint:disable:next implicitly_unwrapped_optional attributes
19-
@inlinable func zeroize(_ s: UnsafeMutableRawPointer!, _ n: Int) {
24+
public func zeroize(_ s: UnsafeMutableRawPointer!, _ n: Int) {
2025
let exitCode = memset_s(s, n, 0, n)
2126
precondition(exitCode == 0, "memset_s returned exit code \(exitCode)")
2227
}
2328
#else
2429
import CUtil
2530

31+
/// Set bytes to zero.
32+
/// - Parameters:
33+
/// - s: Pointer to memory region to be zeroed out.
34+
/// - n: Number of bytes to be zeroed out.
35+
@inlinable
2636
// swiftlint:disable:next implicitly_unwrapped_optional attributes
27-
@inlinable func zeroize(_ s: UnsafeMutableRawPointer!, _ n: Int) {
37+
public func zeroize(_ s: UnsafeMutableRawPointer!, _ n: Int) {
2838
c_zeroize(s, n)
2939
}
3040
#endif

Sources/PIRProcessDatabase/PIRProcessDatabase.docc/PIRProcessDatabase.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,35 @@ other usecase has 57 shards).
154154
Some PIR algorithms, such as MulPir, include an optimization which returns multiple keyword-value pairs in the PIR
155155
response, beyond the keyword-value pair requested by the client. However, this may be undesirable, e.g., if the database
156156
contains sensitive IP. `Symmetric PIR` is a variant of PIR which protects the unrequested server values from the client,
157-
in addition to the standard PIR guarantee protecting the client's keyword from the server. A best-effort approach
158-
towards enabling symmetric PIR is to pad the entries, such that only a limited number of entries are in the server
159-
response. However, this approach will increase server runtime.
157+
in addition to the standard PIR guarantee protecting the client's keyword from the server.
158+
159+
There are two approaches to Symmetric PIR in Swift Homomorphic Encryption. The Fully Oblivious Symmetric PIR approach uses additional cryptographic primitives to guarantee that the client learns only the single keyword-value pair it requested, and is oblivious to other entries. The other is a best-effort approach that imposes an upper bound on the number of additional keyword-value pairs the client learns. We describe both these approaches next.
160+
161+
##### Fully Oblivious Symmetric PIR
162+
163+
In this approach, the server encrypts each keyword-value pair with a specific key derived from a single database encryption key. When querying, client first makes an extra call to the server, to learn information which would help it decrypt the entry it is interested in. Thus, this approach requires one additional call to the server.
164+
165+
To process the database for Symmetric PIR, add the following to the configuration file.
166+
167+
```json
168+
"symmetricPirArguments": {
169+
"outputDatabaseEncryptionKeyFilePath": "path/to/output/key-file.txt"
170+
}
171+
```
172+
173+
This will generate a fresh database encryption key, write it to `path/to/output/key-file.txt`, and use it for processing the database for Symmetric PIR.
174+
175+
In case you want to use a database encryption key file that was generated earlier, you can specify the path in `databaseEncryptionKeyFilePath` instead, as follows.
176+
177+
```json
178+
"symmetricPirArguments": {
179+
"databaseEncryptionKeyFilePath": "path/to/key-file.txt"
180+
}
181+
```
182+
183+
Note that only one of `outputDatabaseEncryptionKeyFilePath` and `databaseEncryptionKeyFilePath` should be present in the configuration.
184+
185+
##### Best-Effort Symmetric PIR
160186

161187
> Warning: This is only a best-effort approach, because HE does not guarantee *circuit privacy*.
162188

Sources/PIRProcessDatabase/main.swift

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
import ArgumentParser
16+
import Crypto
1617
import Foundation
1718
import HomomorphicEncryption
1819
import HomomorphicEncryptionProtobuf
@@ -36,23 +37,58 @@ enum TableSizeOption: Codable, Equatable, Hashable {
3637
/// The configuration for Symmetric PIR.
3738
struct SymmetricPirArguments: Codable, Hashable {
3839
/// File path for key with which database will be encrypted.
39-
let databaseEncryptionKeyFilePath: String
40+
///
41+
/// Also see ``outputDatabaseEncryptionKeyFilePath``.
42+
let databaseEncryptionKeyFilePath: String?
4043
/// Config type for Symmetric PIR.
41-
let configType: SymmetricPirConfigType
44+
let configType: SymmetricPirConfigType?
45+
/// Path to write newly generated database encryption key.
46+
///
47+
/// If this is specified, a new database encryption key will be generated and written to this path.
48+
/// This key will be used to encrypt the database for Symmetric PIR.
49+
/// Exactly one of ``outputDatabaseEncryptionKeyFilePath`` or ``databaseEncryptionKeyFilePath`` should be present.
50+
let outputDatabaseEncryptionKeyFilePath: String?
4251

4352
/// Returns a parsed `SymmetricPirConfig` for given parameters.
4453
/// - Returns: Symmetric PIR config.
4554
func resolve() throws -> SymmetricPirConfig {
46-
do {
47-
let secretKeyString = try String(contentsOfFile: databaseEncryptionKeyFilePath, encoding: .utf8)
48-
guard let secretKey = Array(hexEncoded: secretKeyString) else {
49-
throw PirError.invalidOPRFHexSecretKey
55+
if outputDatabaseEncryptionKeyFilePath != nil, databaseEncryptionKeyFilePath != nil {
56+
throw ValidationError(
57+
"""
58+
Both `databaseEncryptionKeyFilePath` and `outputDatabaseEncryptionKeyFilePath` \
59+
can not be present in `symmetricPirArguments`.
60+
""")
61+
}
62+
let configType = configType ?? .OPRF_P384_AES_GCM_192_NONCE_96_TAG_128
63+
if let databaseEncryptionKeyFilePath {
64+
do {
65+
let secretKeyString = try String(contentsOfFile: databaseEncryptionKeyFilePath, encoding: .utf8)
66+
guard let secretKey = Array(hexEncoded: secretKeyString) else {
67+
throw PirError.invalidOPRFHexSecretKey
68+
}
69+
try configType.validateEncryptionKey(secretKey)
70+
return try SymmetricPirConfig(oprfSecretKey: Secret(value: secretKey), configType: configType)
71+
} catch {
72+
throw PirError.failedToLoadOPRFKey(underlyingError: "\(error)", filePath: databaseEncryptionKeyFilePath)
73+
}
74+
}
75+
if let outputDatabaseEncryptionKeyFilePath {
76+
switch configType {
77+
case .OPRF_P384_AES_GCM_192_NONCE_96_TAG_128:
78+
let secretKey = [UInt8](P384._VOPRF.PrivateKey().rawRepresentation)
79+
try secretKey.hexString.write(
80+
toFile: outputDatabaseEncryptionKeyFilePath,
81+
atomically: true,
82+
encoding: .utf8)
83+
return try SymmetricPirConfig(
84+
oprfSecretKey: Secret(value: secretKey), configType: .OPRF_P384_AES_GCM_192_NONCE_96_TAG_128)
5085
}
51-
try configType.validateEncryptionKey(secretKey)
52-
return try SymmetricPirConfig(oprfSecretKey: secretKey, configType: configType)
53-
} catch {
54-
throw PirError.failedToLoadOPRFKey(underlyingError: "\(error)", filePath: databaseEncryptionKeyFilePath)
5586
}
87+
throw ValidationError(
88+
"""
89+
One of `databaseEncryptionKeyFilePath` or `outputDatabaseEncryptionKeyFilePath`\
90+
should be present in `symmetricPirArguments`.
91+
""")
5692
}
5793
}
5894

Sources/PIRShardDatabase/ShardDatabase.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ struct ProcessCommand: ParsableCommand {
145145
throw PirError.invalidOPRFHexSecretKey
146146
}
147147
try configType.validateEncryptionKey(secretKey)
148-
return try SymmetricPirConfig(oprfSecretKey: secretKey, configType: configType)
148+
return try SymmetricPirConfig(oprfSecretKey: Secret(value: secretKey), configType: configType)
149149
} catch {
150150
throw PirError.failedToLoadOPRFKey(underlyingError: "\(error)", filePath: filePath)
151151
}

Sources/PrivateInformationRetrieval/PrivateInformationRetrieval.docc/PrivateInformationRetrieval.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ While this *trivial PIR* protocol satisfies the privacy and correctness requirem
1414

1515
The PIR implementation in Swift Homomorphic Encryption uses homomorphic encryption to improve upon the trivial PIR protocol.
1616

17-
> Warning: PIR is asymmetric, meaning the client may learn keyword-value pairs not requested, as happens in trivial PIR for instance.
18-
> A variant of PIR, known as *symmetric PIR*, would be required to ensure the client does not learn anything about values it did not request.
17+
Note that PIR is asymmetric, meaning the client may learn keyword-value pairs not requested, as happens in trivial PIR for instance. Swift Homomorphic Encryption also implements *Symmetric PIR*, a variant of PIR which ensures the client does not learn anything about values it did not request.
1918

2019
## Topics
2120
<!-- Snippets are defined in a different "virtual module", requiring manually linking articles here. -->

Sources/PrivateInformationRetrieval/SymmetricPIRProtocol.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public struct OprfServer {
4747
guard case .OPRF_P384_AES_GCM_192_NONCE_96_TAG_128 = symmetricPirConfig.configType else {
4848
throw PirError.invalidSymmetricPirConfig(symmetricPirConfig: symmetricPirConfig)
4949
}
50-
self.oprfPrivateKey = try OprfPrivateKey(rawRepresentation: symmetricPirConfig.oprfSecretKey)
50+
self.oprfPrivateKey = try OprfPrivateKey(rawRepresentation: symmetricPirConfig.oprfSecretKey.value)
5151
}
5252

5353
/// Compute OPRF response.

Sources/PrivateInformationRetrieval/SymmetricPirDatabase.swift

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,64 @@ public struct SymmetricPirClientConfig: Codable, Hashable, Sendable {
9494
}
9595
}
9696

97+
/// A wrapper for secret values.
98+
public final class Secret: Equatable, Hashable, @unchecked Sendable {
99+
/// Secret value bytes.
100+
public var value: [UInt8]
101+
102+
@inlinable
103+
public init(value: [UInt8]) {
104+
self.value = value
105+
}
106+
107+
public static func == (lhs: Secret, rhs: Secret) -> Bool {
108+
lhs.value == rhs.value
109+
}
110+
111+
public func hash(into hasher: inout Hasher) {
112+
hasher.combine(value)
113+
}
114+
115+
// Sets all bytes to zero.
116+
@inlinable
117+
public func zeroize() {
118+
let zeroizeSize = value.count * MemoryLayout<UInt8>.size
119+
value.withUnsafeMutableBytes { dataPointer in
120+
// swiftlint:disable:next force_unwrapping
121+
HomomorphicEncryption.zeroize(dataPointer.baseAddress!, zeroizeSize)
122+
}
123+
}
124+
125+
deinit {
126+
zeroize()
127+
}
128+
}
129+
130+
extension Secret: Codable {
131+
enum CodingKeys: String, CodingKey {
132+
case value
133+
}
134+
135+
public func encode(to encoder: any Encoder) throws {
136+
var container = encoder.container(keyedBy: CodingKeys.self)
137+
try container.encode("****", forKey: .value)
138+
}
139+
}
140+
141+
extension Secret: CustomStringConvertible, CustomDebugStringConvertible {
142+
public var description: String {
143+
"Secret(value: ****)"
144+
}
145+
146+
public var debugDescription: String {
147+
"Secret(value: ****)"
148+
}
149+
}
150+
97151
/// Configuration for Symmetric PIR.
98152
public struct SymmetricPirConfig: Codable, Hashable, Sendable {
99153
/// Secret key for keyword database encryption.
100-
public let oprfSecretKey: [UInt8]
154+
public let oprfSecretKey: Secret
101155
/// Symmetric PIR config type.
102156
public let configType: SymmetricPirConfigType
103157

@@ -108,11 +162,11 @@ public struct SymmetricPirConfig: Codable, Hashable, Sendable {
108162
/// - Throws: Error on invalid key size.
109163
@inlinable
110164
public init(
111-
oprfSecretKey: [UInt8],
165+
oprfSecretKey: Secret,
112166
configType: SymmetricPirConfigType = .OPRF_P384_AES_GCM_192_NONCE_96_TAG_128) throws
113167
{
114-
guard oprfSecretKey.count == configType.oprfKeySize else {
115-
throw PirError.invalidOPRFKeySize(oprfSecretKey.count, expectedSize: Int(configType.oprfKeySize))
168+
guard oprfSecretKey.value.count == configType.oprfKeySize else {
169+
throw PirError.invalidOPRFKeySize(oprfSecretKey.value.count, expectedSize: Int(configType.oprfKeySize))
116170
}
117171
self.oprfSecretKey = oprfSecretKey
118172
self.configType = configType
@@ -122,7 +176,7 @@ public struct SymmetricPirConfig: Codable, Hashable, Sendable {
122176
public func clientConfig() throws -> SymmetricPirClientConfig {
123177
switch configType {
124178
case .OPRF_P384_AES_GCM_192_NONCE_96_TAG_128:
125-
let serverPublicKey = try OprfPrivateKey(rawRepresentation: oprfSecretKey).publicKey
179+
let serverPublicKey = try OprfPrivateKey(rawRepresentation: oprfSecretKey.value).publicKey
126180
.oprfRepresentation
127181
return SymmetricPirClientConfig(serverPublicKey: [UInt8](serverPublicKey), configType: configType)
128182
}
@@ -142,7 +196,7 @@ extension KeywordDatabase {
142196
guard case .OPRF_P384_AES_GCM_192_NONCE_96_TAG_128 = config.configType else {
143197
throw PirError.invalidSymmetricPirConfig(symmetricPirConfig: config)
144198
}
145-
let oprfSecretKey = try OprfPrivateKey(rawRepresentation: config.oprfSecretKey)
199+
let oprfSecretKey = try OprfPrivateKey(rawRepresentation: config.oprfSecretKey.value)
146200

147201
return try database.map { entry in
148202
let oprfOutputHash = try [UInt8](oprfSecretKey.evaluate(Data(entry.keyword)))

Sources/TestUtilities/PirUtilities/SymmetricPirTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ extension PirTestUtils {
2525
public static func generateSymmetricPirConfig() throws -> SymmetricPirConfig {
2626
let secretKey = [UInt8](OprfPrivateKey().rawRepresentation)
2727
return try SymmetricPirConfig(
28-
oprfSecretKey: secretKey, configType: .OPRF_P384_AES_GCM_192_NONCE_96_TAG_128)
28+
oprfSecretKey: Secret(value: secretKey), configType: .OPRF_P384_AES_GCM_192_NONCE_96_TAG_128)
2929
}
3030

3131
/// Tests symmetric PIR round trip.

Tests/PrivateInformationRetrievalTests/SymmetricPIRTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ struct SymmetricPirTests {
6161
sharding: .shardCount(shardCount),
6262
symmetricPirConfig: config)
6363

64-
let oprfSecretKey = try OprfPrivateKey(rawRepresentation: config.oprfSecretKey)
64+
let oprfSecretKey = try OprfPrivateKey(rawRepresentation: config.oprfSecretKey.value)
6565

6666
let testIndex = Int.random(in: 0..<rowCount)
6767
let testKeyword = Data(testDatabase[testIndex].keyword)

0 commit comments

Comments
 (0)