Skip to content

Commit 9146fcf

Browse files
authored
Add async + and += APIs (#250)
1 parent 1d2b19f commit 9146fcf

File tree

6 files changed

+377
-182
lines changed

6 files changed

+377
-182
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Example showing the basics with async APIs.
2+
3+
// snippet.hide
4+
// Copyright 2025 Apple Inc. and the Swift Homomorphic Encryption project authors
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
// snippet.show
18+
19+
// snippet.encryption
20+
import HomomorphicEncryption
21+
22+
// snippet.hide
23+
// swiftformat:disable:all
24+
func main() async throws {
25+
// snippet.show
26+
27+
// We start by choosing some encryption parameters for the Bfv<UInt64> scheme.
28+
// *These encryption parameters are insecure, suitable for testing only.*
29+
let encryptParams =
30+
try EncryptionParameters<UInt64>(from: .insecure_n_8_logq_5x18_logt_5)
31+
// Perform pre-computation for HE computation with these parameters.
32+
let context = try Context<Bfv<UInt64>>(encryptionParameters: encryptParams)
33+
34+
// We encode N values using coefficient encoding.
35+
// Operations on sensitive data like unencrypted values remain synchronous.
36+
let values: [UInt64] = [8, 5, 12, 12, 15, 0, 8, 5]
37+
let plaintext: Bfv<UInt64>.CoeffPlaintext = try context.encode(
38+
values: values,
39+
format: .coefficient)
40+
41+
// We generate a secret key and use it to encrypt the plaintext.
42+
let secretKey = try context.generateSecretKey()
43+
let ciphertext = try plaintext.encrypt(using: secretKey)
44+
45+
// Decrypting the plaintext yields the original values.
46+
let decrypted = try ciphertext.decrypt(using: secretKey)
47+
var decoded: [UInt64] = try decrypted.decode(format: .coefficient)
48+
precondition(decoded == values)
49+
50+
// Mixing formats between encoding and decoding yields incorrect results.
51+
decoded = try decrypted.decode(format: .simd)
52+
precondition(decoded != values)
53+
54+
// snippet.addition
55+
// We add the ciphertext with the plaintext, yielding another ciphertext.
56+
// The `await` keyword indicates this is an asynchronous operation.
57+
var sum = try await ciphertext + plaintext
58+
59+
// The ciphertext decrypts to the element-wise sum of the ciphertext's
60+
// and plaintext's values, mod 17, the plaintext modulus.
61+
precondition(encryptParams.plaintextModulus == 17)
62+
var plaintextSum = try sum.decrypt(using: secretKey)
63+
decoded = try plaintextSum.decode(format: .coefficient)
64+
precondition(decoded == [16, 10, 7, 7, 13, 0, 16, 10])
65+
66+
// We can also add ciphertexts.
67+
try await sum += ciphertext
68+
plaintextSum = try sum.decrypt(using: secretKey)
69+
decoded = try plaintextSum.decode(format: .coefficient)
70+
precondition(decoded == [7, 15, 2, 2, 11, 0, 7, 15])
71+
// snippet.end
72+
73+
// snippet.subtraction
74+
// We can subtract a plaintext from a ciphertext.
75+
try sum -= plaintext
76+
plaintextSum = try sum.decrypt(using: secretKey)
77+
decoded = try plaintextSum.decode(format: .coefficient)
78+
precondition(decoded == [16, 10, 7, 7, 13, 0, 16, 10])
79+
80+
// We can also subtract a ciphertext from a ciphertext.
81+
try sum -= ciphertext
82+
plaintextSum = try sum.decrypt(using: secretKey)
83+
decoded = try plaintextSum.decode(format: .coefficient)
84+
precondition(decoded == [8, 5, 12, 12, 15, 0, 8, 5])
85+
86+
// One special case is when subtracting a ciphertext from itself.
87+
// This yields a "transparent ciphertext", which reveals the underlying
88+
// plaintext to any observer. The observed value in this case is zero.
89+
try sum -= sum
90+
precondition(sum.isTransparent())
91+
92+
// snippet.hide
93+
}
94+
try await main()
95+
// snippet.show

Sources/HomomorphicEncryption/Ciphertext.swift

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ extension Ciphertext {
373373
/// - plaintext: Plaintext to add.
374374
/// - Returns: A ciphertext encrypting the sum.
375375
/// - Throws: Error upon failure to add.
376+
/// - seealso: ``Ciphertext/+(_:_:)-3mot4`` for an async version.
376377
@inlinable
377378
public static func + (ciphertext: Ciphertext<Scheme, Format>,
378379
plaintext: Plaintext<Scheme, some PolyFormat>) throws -> Self
@@ -388,6 +389,7 @@ extension Ciphertext {
388389
/// - ciphertext: Ciphertext to add.
389390
/// - Returns: A ciphertext encrypting the sum.
390391
/// - Throws: Error upon failure to add.
392+
/// - seealso: ``Ciphertext/+(_:_:)-3hq92`` for an async version.
391393
@inlinable
392394
public static func + (plaintext: Plaintext<Scheme, some PolyFormat>,
393395
ciphertext: Ciphertext<Scheme, Format>) throws -> Self
@@ -420,6 +422,7 @@ extension Ciphertext {
420422
/// - rhs: Plaintext to add.
421423
/// - Returns: A ciphertext encrypting the sum `lhs + rhs'.
422424
/// - Throws: Error upon failure to add.
425+
/// - seealso: ``Ciphertext/+(_:_:)-89dia`` for an async version.
423426
@inlinable
424427
public static func + (lhs: Ciphertext<Scheme, Format>, rhs: Ciphertext<Scheme, some PolyFormat>) throws -> Self {
425428
var result = lhs
@@ -595,8 +598,85 @@ extension Collection {
595598
}
596599
}
597600

598-
/// Async ciphertext functions.
601+
// MARK: - Async ciphertext functions
602+
599603
extension Ciphertext {
604+
/// Async ciphertext-plaintext addition.
605+
/// - Parameters:
606+
/// - plaintext: Plaintext to add.
607+
/// - ciphertext: Ciphertext to add.
608+
/// - Returns: A ciphertext encrypting the sum.
609+
/// - Throws: Error upon failure to add.
610+
/// - seealso: ``Ciphertext/+(_:_:)-2g1nj`` for a sync version.
611+
@inlinable
612+
public static func + (plaintext: Plaintext<Scheme, some PolyFormat>,
613+
ciphertext: Ciphertext<Scheme, Format>) async throws -> Self
614+
{
615+
try await ciphertext + plaintext
616+
}
617+
618+
/// Async ciphertext-plaintext addition.
619+
/// - Parameters:
620+
/// - ciphertext: Ciphertext to add.
621+
/// - plaintext: Plaintext to add.
622+
/// - Returns: A ciphertext encrypting the sum.
623+
/// - Throws: Error upon failure to add.
624+
/// - seealso: ``Ciphertext/+(_:_:)-42x1k`` for a sync version.
625+
@inlinable
626+
public static func + (ciphertext: Ciphertext<Scheme, Format>,
627+
plaintext: Plaintext<Scheme, some PolyFormat>) async throws -> Self
628+
{
629+
var result = ciphertext
630+
try await result += plaintext
631+
return result
632+
}
633+
634+
/// Async ciphertext addition.
635+
/// - Parameters:
636+
/// - lhs: Ciphertext to add to.
637+
/// - rhs: Ciphertext to add.
638+
/// - Throws: Error upon failure to add.
639+
/// - seealso: ``Ciphertext/+=(_:_:)-49um`` for a sync version.
640+
@inlinable
641+
public static func += (
642+
lhs: inout Ciphertext<Scheme, Format>,
643+
rhs: Ciphertext<Scheme, some PolyFormat>) async throws
644+
{
645+
try Scheme.validateEquality(of: lhs.context, and: rhs.context)
646+
try await Scheme.addAssignAsync(&lhs, rhs)
647+
}
648+
649+
/// Async ciphertext addition.
650+
/// - Parameters:
651+
/// - lhs: Ciphertext to add.
652+
/// - rhs: Ciphertext to add.
653+
/// - Returns: A ciphertext encrypting the sum `lhs + rhs'.
654+
/// - Throws: Error upon failure to add.
655+
/// - seealso: ``Ciphertext/+(_:_:)-2jflc`` for a sync version.
656+
@inlinable
657+
public static func + (lhs: Ciphertext<Scheme, Format>,
658+
rhs: Ciphertext<Scheme, some PolyFormat>) async throws -> Self
659+
{
660+
var result = lhs
661+
try await result += rhs
662+
return result
663+
}
664+
665+
/// Async ciphertext addition.
666+
/// - Parameters:
667+
/// - ciphertext: Ciphertext to add to.
668+
/// - plaintext: Plaintext to add.
669+
/// - Throws: Error upon failure to add.
670+
/// - seealso: ``Ciphertext/+=(_:_:)-8y0jp`` for a sync version.
671+
@inlinable
672+
public static func += (
673+
ciphertext: inout Ciphertext<Scheme, Format>,
674+
plaintext: Plaintext<Scheme, some PolyFormat>) async throws
675+
{
676+
try Scheme.validateEquality(of: ciphertext.context, and: plaintext.context)
677+
try await Scheme.addAssignAsync(&ciphertext, plaintext)
678+
}
679+
600680
/// Converts the ciphertext to coefficient format asynchronously.
601681
///
602682
/// This method performs an asynchronous conversion of the ciphertext to ``Coeff`` format.

Sources/HomomorphicEncryption/HeSchemeAsync.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,73 @@ extension HeScheme {
226226
}
227227
// swiftlint:enable missing_docs
228228
}
229+
230+
// MARK: - Implementations generalized over PolyFormat
231+
232+
extension HeScheme {
233+
/// In-place ciphertext-plaintext addition: `ciphertext += plaintext`.
234+
///
235+
/// - Parameters:
236+
/// - ciphertext: Ciphertext to add; will store the sum.
237+
/// - plaintext: Plaintext to add.
238+
/// - Throws: Error upon failure to add.
239+
/// - seealso: ``HeScheme/addAssign(_:_:)-407pg`` for a sync version.
240+
@inlinable
241+
public static func addAssignAsync<CiphertextFormat: PolyFormat, PlaintextFormat: PolyFormat>(
242+
_ ciphertext: inout Ciphertext<Self, CiphertextFormat>,
243+
_ plaintext: Plaintext<Self, PlaintextFormat>) async throws
244+
{
245+
// swiftlint:disable force_cast
246+
if CiphertextFormat.self == Coeff.self, PlaintextFormat.self == Coeff.self {
247+
var coeffCiphertext = ciphertext as! CoeffCiphertext
248+
try await addAssignCoeffAsync(&coeffCiphertext, plaintext as! CoeffPlaintext)
249+
ciphertext = coeffCiphertext as! Ciphertext<Self, CiphertextFormat>
250+
} else if CiphertextFormat.self == Eval.self, PlaintextFormat.self == Eval.self {
251+
var evalCiphertext = ciphertext as! EvalCiphertext
252+
try await addAssignEvalAsync(&evalCiphertext, plaintext as! EvalPlaintext)
253+
ciphertext = evalCiphertext as! Ciphertext<Self, CiphertextFormat>
254+
} else {
255+
throw HeError.unsupportedHeOperation(
256+
"""
257+
Addition between ciphertext in \(CiphertextFormat.description) \
258+
and plaintext in \(PlaintextFormat.description).
259+
""")
260+
}
261+
// swiftlint:enable force_cast
262+
}
263+
264+
/// In-place ciphertext addition: `lhs += rhs`.
265+
///
266+
/// - Parameters:
267+
/// - lhs: Ciphertext to add; will store the sum.
268+
/// - rhs: Ciphertext to add.
269+
/// - Throws: Error upon failure to add.
270+
/// - seealso: ``HeScheme/addAssign(_:_:)-1sd4b`` for a sync version.
271+
@inlinable
272+
public static func addAssignAsync<LhsFormat: PolyFormat, RhsFormat: PolyFormat>(
273+
_ lhs: inout Ciphertext<Self, LhsFormat>,
274+
_ rhs: Ciphertext<Self, RhsFormat>) async throws
275+
{
276+
// swiftlint:disable force_cast
277+
if LhsFormat.self == Coeff.self {
278+
var lhsCoeffCiphertext = lhs as! CoeffCiphertext
279+
if RhsFormat.self == Coeff.self {
280+
try await addAssignCoeffAsync(&lhsCoeffCiphertext, rhs as! CoeffCiphertext)
281+
} else {
282+
fatalError("Unsupported Format \(RhsFormat.description)")
283+
}
284+
lhs = lhsCoeffCiphertext as! Ciphertext<Self, LhsFormat>
285+
} else if LhsFormat.self == Eval.self {
286+
var lhsEvalCiphertext = lhs as! EvalCiphertext
287+
if RhsFormat.self == Eval.self {
288+
try await addAssignEvalAsync(&lhsEvalCiphertext, rhs as! EvalCiphertext)
289+
} else {
290+
fatalError("Unsupported Format \(RhsFormat.description)")
291+
}
292+
lhs = lhsEvalCiphertext as! Ciphertext<Self, LhsFormat>
293+
} else {
294+
fatalError("Unsupported Format \(LhsFormat.description)")
295+
}
296+
// swiftlint:enable force_cast
297+
}
298+
}

Sources/HomomorphicEncryption/HomomorphicEncryption.docc/Examples.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Examples for how to use ``HomomorphicEncryption``
66
We start with the basics.
77
@Snippet(path: "swift-homomorphic-encryption/Snippets/HomomorphicEncryption/BasicsSnippet", slice:"encryption")
88

9+
We also have async APIs.
10+
@Snippet(path: "swift-homomorphic-encryption/Snippets/HomomorphicEncryption/BasicsAsyncSnippet", slice:"encryption")
11+
912
### HE Addition and Subtraction
1013
Continuing from the previous example, we can also compute on the encrypted data.
1114
We can add a ciphertext with a ciphertext or a plaintext.

0 commit comments

Comments
 (0)