Skip to content

Commit

Permalink
Adds CiphertextMatrix (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
fboemer authored Aug 14, 2024
1 parent f32640e commit 659e4ab
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 15 deletions.
3 changes: 2 additions & 1 deletion Sources/HomomorphicEncryption/Ciphertext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

/// Ciphertext type.
public struct Ciphertext<Scheme: HeScheme, Format: PolyFormat>: Equatable, Sendable {
@usableFromInline let context: Context<Scheme>
/// Context for HE computation.
public let context: Context<Scheme>
@usableFromInline var polys: [PolyRq<Scheme.Scalar, Format>]
@usableFromInline var correctionFactor: Scheme.Scalar
@usableFromInline var seed: [UInt8] = []
Expand Down
8 changes: 4 additions & 4 deletions Sources/HomomorphicEncryption/Plaintext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ extension Plaintext {
/// - lhs: Plaintext to add.
/// - rhs: Plaintext add.
/// - Returns: The sum `lhs + rhs`.
/// - throws: Error upon failure to add the plaintexts.
/// - Throws: Error upon failure to add the plaintexts.
/// - seealso: ``HeScheme/addAssign(_:_:)-3bv7g`` for an alternative API.
@inlinable
public static func + (lhs: Self, rhs: Self) throws -> Self where Format == Coeff {
Expand All @@ -111,7 +111,7 @@ extension Plaintext {
/// - lhs: Plaintext to add.
/// - rhs: Plaintext add.
/// - Returns: The sum `lhs + rhs`.
/// - throws: Error upon failure to add the plaintexts.
/// - Throws: Error upon failure to add the plaintexts.
/// - seealso: ``HeScheme/addAssign(_:_:)-1osb9`` for an alternative API.
@inlinable
public static func + (lhs: Self, rhs: Self) throws -> Self where Format == Eval {
Expand All @@ -125,7 +125,7 @@ extension Plaintext {
/// This makes the plaintext suitable for operations with ciphertexts in ``Eval`` format, with `moduliCount` moduli.
/// - Parameter moduliCount: Number of coefficient moduli in the context.
/// - Returns: The converted plaintext.
/// - throws: Error upon failure to convert the plaintext.
/// - Throws: Error upon failure to convert the plaintext.
@inlinable
public func convertToEvalFormat(moduliCount: Int? = nil) throws -> Plaintext<Scheme, Eval> {
if let plaintext = self as? Plaintext<Scheme, Eval> {
Expand All @@ -152,7 +152,7 @@ extension Plaintext {

/// Converts the plaintext to ``Coeff`` format.
/// - Returns: The converted plaintext.
/// - throws: Error upon failure to convert the plaintext.
/// - Throws: Error upon failure to convert the plaintext.
@inlinable
public func convertToCoeffFormat() throws -> Plaintext<Scheme, Coeff> {
if let plaintext = self as? Plaintext<Scheme, Coeff> {
Expand Down
2 changes: 1 addition & 1 deletion Sources/HomomorphicEncryption/PolyRq/PolyRq.swift
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ extension PolyRq where F == Coeff {
/// then drops the last modulus.
///
/// This polynomial must have a next context.
/// - throws: Error upon failure to perform division and rounding.
/// - Throws: Error upon failure to perform division and rounding.
/// - seealso: Algorithm 8 of <https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=9395438>,
/// Algorithm 2 of <https://eprint.iacr.org/2018/931.pdf> for more details.
@inlinable
Expand Down
130 changes: 130 additions & 0 deletions Sources/PrivateNearestNeighborsSearch/CiphertextMatrix.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import HomomorphicEncryption

/// Stores a matrix of scalars as ciphertexts.
struct CiphertextMatrix<Scheme: HeScheme, Format: PolyFormat>: Equatable, Sendable {
typealias Packing = PlaintextMatrixPacking
typealias Dimensions = MatrixDimensions

/// Dimensions of the scalars.
@usableFromInline let dimensions: Dimensions

/// Dimensions of the scalar matrix in a SIMD-encoded plaintext.
@usableFromInline let simdDimensions: SimdEncodingDimensions

/// Plaintext packing with which the data is stored.
@usableFromInline let packing: Packing

/// Encrypted data.
@usableFromInline let ciphertexts: [Ciphertext<Scheme, Format>]

/// The parameter context.
@usableFromInline var context: Context<Scheme> {
precondition(!ciphertexts.isEmpty, "Ciphertext array cannot be empty")
return ciphertexts[0].context
}

/// Number of rows in SIMD-encoded plaintext.
@usableFromInline var simdRowCount: Int { simdDimensions.rowCount }

/// Number of columns SIMD-encoded plaintext.
@usableFromInline var simdColumnCount: Int { simdDimensions.columnCount }

/// Number of data values stored in the ciphertexts matrix.
@usableFromInline var count: Int { dimensions.count }

/// Number of rows in the stored data.
@usableFromInline var rowCount: Int { dimensions.rowCount }

/// Number of columns in the stored data.
@usableFromInline var columnCount: Int { dimensions.columnCount }

/// Creates a new ciphertexts matrix.
/// - Parameters:
/// - dimensions: Ciphertext matrix dimensions
/// - packing: The packing with which the data is stored
/// - ciphertexts: ciphertexts encrypting the data; must not be empty.
/// - Throws: Error upon failure to initialize the ciphertext matrix.
@inlinable
init(dimensions: Dimensions, packing: Packing, ciphertexts: [Ciphertext<Scheme, Format>]) throws {
guard let context = ciphertexts.first?.context else {
throw PNNSError.emptyCiphertextArray
}
let encryptionParams = context.encryptionParameters
guard let simdDimensions = encryptionParams.simdDimensions else {
throw PNNSError.simdEncodingNotSupported(for: encryptionParams)
}
let expectedCiphertextCount = try PlaintextMatrix<Scheme, Format>.plaintextCount(
encryptionParameters: encryptionParams,
dimensions: dimensions,
packing: packing)
guard ciphertexts.count == expectedCiphertextCount else {
throw PNNSError.wrongCiphertextCount(got: ciphertexts.count, expected: expectedCiphertextCount)
}
for ciphertext in ciphertexts {
guard ciphertext.context == context else {
throw PNNSError.wrongContext(got: ciphertext.context, expected: context)
}
}

self.simdDimensions = simdDimensions
self.dimensions = dimensions
self.packing = packing
self.ciphertexts = ciphertexts
}

@inlinable
func decrypt(using secretKey: SecretKey<Scheme>) throws -> PlaintextMatrix<Scheme, Coeff> {
let plaintexts = try ciphertexts.map { ciphertext in try ciphertext.decrypt(using: secretKey) }
return try PlaintextMatrix(dimensions: dimensions, packing: packing, plaintexts: plaintexts)
}
}

// MARK: format conversion

extension CiphertextMatrix {
/// Converts the ciphertext matrix to ``Eval`` format.
/// - Returns: The converted ciphertext matrix.
/// - Throws: Error upon failure to convert the ciphertext matrix.
@inlinable
public func convertToEvalFormat() throws -> CiphertextMatrix<Scheme, Eval> {
if Format.self == Eval.self {
// swiftlint:disable:next force_cast
return self as! CiphertextMatrix<Scheme, Eval>
}
let evalCiphertexts = try ciphertexts.map { ciphertext in try ciphertext.convertToEvalFormat() }
return try CiphertextMatrix<Scheme, Eval>(
dimensions: dimensions,
packing: packing,
ciphertexts: evalCiphertexts)
}

/// Converts the plaintext matrix to ``Coeff`` format.
/// - Returns: The converted plaintext ciphertext.
/// - Throws: Error upon failure to convert the ciphertext matrix.
@inlinable
public func convertToCoeffFormat() throws -> CiphertextMatrix<Scheme, Coeff> {
if Format.self == Coeff.self {
// swiftlint:disable:next force_cast
return self as! CiphertextMatrix<Scheme, Coeff>
}
let coeffCiphertexts = try ciphertexts.map { ciphertexts in try ciphertexts.convertToCoeffFormat() }
return try CiphertextMatrix<Scheme, Coeff>(
dimensions: dimensions,
packing: packing,
ciphertexts: coeffCiphertexts)
}
}
6 changes: 6 additions & 0 deletions Sources/PrivateNearestNeighborsSearch/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import HomomorphicEncryption

/// Error type for ``PrivateNearestNeighborsSearch``.
enum PNNSError: Error, Equatable {
case emptyCiphertextArray
case emptyPlaintextArray
case invalidMatrixDimensions(_ dimensions: MatrixDimensions)
case simdEncodingNotSupported(_ description: String)
case wrongCiphertextCount(got: Int, expected: Int)
case wrongContext(gotDescription: String, expectedDescription: String)
case wrongEncodingValuesCount(got: Int, expected: Int)
case wrongPlaintextCount(got: Int, expected: Int)
Expand All @@ -43,12 +45,16 @@ extension PNNSError {
extension PNNSError: LocalizedError {
public var errorDescription: String? {
switch self {
case .emptyCiphertextArray:
"Empty ciphertext array"
case .emptyPlaintextArray:
"Empty plaintext array"
case let .invalidMatrixDimensions(dimensions):
"Invalid matrix dimensions: rowCount \(dimensions.rowCount), columnCount \(dimensions.columnCount)"
case let .simdEncodingNotSupported(encryptionParameters):
"SIMD encoding is not supported for encryption parameters \(encryptionParameters)"
case let .wrongCiphertextCount(got, expected):
"Wrong ciphertext count \(got), expected \(expected)"
case let .wrongContext(gotDescription, expectedDescription):
"Wrong context: got \(gotDescription), expected \(expectedDescription)"
case let .wrongEncodingValuesCount(got, expected):
Expand Down
30 changes: 21 additions & 9 deletions Sources/PrivateNearestNeighborsSearch/PlaintextMatrix.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ struct PlaintextMatrix<Scheme: HeScheme, Format: PolyFormat>: Equatable, Sendabl
/// Dimensions of the scalars.
@usableFromInline let dimensions: Dimensions

/// The row and column count of a SIMD-encoded plaintext.
/// Dimensions of the scalar matrix in a SIMD-encoded plaintext.
let simdDimensions: SimdEncodingDimensions

/// Plaintext packing with which the data is stored.
Expand All @@ -79,19 +79,19 @@ struct PlaintextMatrix<Scheme: HeScheme, Format: PolyFormat>: Equatable, Sendabl
return plaintexts[0].context
}

/// The number of rows in SIMD-encoded plaintext.
/// Number of rows in SIMD-encoded plaintext.
@usableFromInline var simdRowCount: Int { simdDimensions.rowCount }

/// The number of columns SIMD-encoded plaintext.
/// Number of columns SIMD-encoded plaintext.
@usableFromInline var simdColumnCount: Int { simdDimensions.columnCount }

/// The number of data values stored in the plaintext matrix.
/// Number of data values stored in the plaintext matrix.
@usableFromInline var count: Int { dimensions.count }

/// The number of rows in the stored data.
/// Number of rows in the stored data.
@usableFromInline var rowCount: Int { dimensions.rowCount }

/// The number of columns in the stored data.
/// Number of columns in the stored data.
@usableFromInline var columnCount: Int { dimensions.columnCount }

/// Creates a new plaintext matrix.
Expand Down Expand Up @@ -413,6 +413,18 @@ struct PlaintextMatrix<Scheme: HeScheme, Format: PolyFormat>: Equatable, Sendabl
}
return values
}

/// Symmetric secret key encryption of the plaintext matrix.
/// - Parameter secretKey: Secret key to encrypt with.
/// - Returns: A ciphertext encrypting the plaintext matrix.
/// - Throws: Error upon failure to encrypt the plaintext matrix.
@inlinable
func encrypt(using secretKey: SecretKey<Scheme>) throws
-> CiphertextMatrix<Scheme, Scheme.CanonicalCiphertextFormat> where Format == Coeff
{
let ciphertexts = try plaintexts.map { plaintext in try plaintext.encrypt(using: secretKey) }
return try CiphertextMatrix(dimensions: dimensions, packing: packing, ciphertexts: ciphertexts)
}
}

// MARK: format conversion
Expand All @@ -423,8 +435,8 @@ extension PlaintextMatrix {
/// This makes the plaintext matrix suitable for operations with ciphertexts in ``Eval`` format, with `moduliCount`
/// moduli.
/// - Parameter moduliCount: Number of coefficient moduli in the context.
/// - Returns: The convertext plaintext matrix.
/// - throws: Error upon failure to convert the plaintext matrix.
/// - Returns: The converted plaintext matrix.
/// - Throws: Error upon failure to convert the plaintext matrix.
@inlinable
public func convertToEvalFormat(moduliCount: Int? = nil) throws -> PlaintextMatrix<Scheme, Eval> {
if Format.self == Eval.self {
Expand All @@ -437,7 +449,7 @@ extension PlaintextMatrix {

/// Converts the plaintext matrix to ``Coeff`` format.
/// - Returns: The converted plaintext matrix.
/// - throws: Error upon failure to convert the plaintext matrix.
/// - Throws: Error upon failure to convert the plaintext matrix.
@inlinable
public func convertToCoeffFormat() throws -> PlaintextMatrix<Scheme, Coeff> {
if Format.self == Coeff.self {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import HomomorphicEncryption
@testable import PrivateNearestNeighborsSearch
import TestUtilities
import XCTest

final class CiphertextMatrixTests: XCTestCase {
func testEncryptDecryptRoundTrip() throws {
func runTest<Scheme: HeScheme>(for _: Scheme.Type) throws {
let rlweParams = PredefinedRlweParameters.insecure_n_8_logq_5x18_logt_5
let encryptionParams = try EncryptionParameters<Scheme>(from: rlweParams)
XCTAssert(encryptionParams.supportsSimdEncoding)
let context = try Context<Scheme>(encryptionParameters: encryptionParams)
let dimensions = try MatrixDimensions(rowCount: 10, columnCount: 4)
let encodeValues: [[Scheme.Scalar]] = (0..<dimensions.rowCount).map { rowIndex in
(0..<dimensions.columnCount).map { columnIndex in
let value = 1 + Scheme.Scalar(rowIndex * dimensions.columnCount + columnIndex)
return value % context.plaintextModulus
}
}
let plaintextMatrix = try PlaintextMatrix<Scheme, Coeff>(
context: context,
dimensions: dimensions,
packing: .denseRow,
values: encodeValues.flatMap { $0 })
let secretKey = try context.generateSecretKey()
let ciphertextMatrix = try plaintextMatrix.encrypt(using: secretKey)
let plaintextMatrixroundTrip = try ciphertextMatrix.decrypt(using: secretKey)
XCTAssertEqual(plaintextMatrixroundTrip, plaintextMatrix)
}
try runTest(for: NoOpScheme.self)
try runTest(for: Bfv<UInt32>.self)
try runTest(for: Bfv<UInt64>.self)
}

func testConvertRoundTrip() throws {
func runTest<Scheme: HeScheme>(for _: Scheme.Type) throws {
let rlweParams = PredefinedRlweParameters.insecure_n_8_logq_5x18_logt_5
let encryptionParams = try EncryptionParameters<Scheme>(from: rlweParams)
XCTAssert(encryptionParams.supportsSimdEncoding)
let context = try Context<Scheme>(encryptionParameters: encryptionParams)
let dimensions = try MatrixDimensions(rowCount: 10, columnCount: 4)
let encodeValues: [[Scheme.Scalar]] = (0..<dimensions.rowCount).map { rowIndex in
(0..<dimensions.columnCount).map { columnIndex in
let value = 1 + Scheme.Scalar(rowIndex * dimensions.columnCount + columnIndex)
return value % context.plaintextModulus
}
}
let plaintextMatrix = try PlaintextMatrix<Scheme, Coeff>(
context: context,
dimensions: dimensions,
packing: .denseRow,
values: encodeValues.flatMap { $0 })
let secretKey = try context.generateSecretKey()
let ciphertextCoeffMatrix: CiphertextMatrix = try plaintextMatrix.encrypt(using: secretKey)
let ciphertextEvalMatrix = try ciphertextCoeffMatrix.convertToEvalFormat()
let ciphertextMatrixRoundTrip = try ciphertextEvalMatrix.convertToCoeffFormat()
let decoded = try ciphertextMatrixRoundTrip.decrypt(using: secretKey)
XCTAssertEqual(plaintextMatrix, decoded)
}
try runTest(for: NoOpScheme.self)
try runTest(for: Bfv<UInt32>.self)
try runTest(for: Bfv<UInt64>.self)
}
}

0 comments on commit 659e4ab

Please sign in to comment.