• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hpsc-lab / SecureArithmetic.jl / 13112159393

03 Feb 2025 11:10AM UTC coverage: 96.262%. First build
13112159393

Pull #59

github

web-flow
Merge bab919a0b into babf72f09
Pull Request #59: Add Secure Array

479 of 498 new or added lines in 8 files covered. (96.18%)

618 of 642 relevant lines covered (96.26%)

62.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.67
/src/openfhe.jl
1
"""
2
    OpenFHEBackend
3

4
Cryptography backend for use with the homomorphic encryption library OpenFHE
5
(https://github.com/openfheorg/openfhe-development).
6

7
See also: [`SecureContext`](@ref), [`Unencrypted`](@ref)
8
"""
9
struct OpenFHEBackend{CryptoContextT} <: AbstractCryptoBackend
10
    crypto_context::CryptoContextT
5✔
11
end
12

13
"""
14
    get_crypto_context(context::SecureContext{<:OpenFHEBackend})
15

16
Return a `OpenFHE.CryptoContext` object stored in a given `context`.
17

18
See also: [`SecureContext`](@ref), [`OpenFHEBackend`](@ref)
19
"""
20
function get_crypto_context(context::SecureContext{<:OpenFHEBackend})
306✔
21
    context.backend.crypto_context
306✔
22
end
23
"""
24
    get_crypto_context(a::Union{SecureArray{<:OpenFHEBackend},
25
                                PlainArray{<:OpenFHEBackend}})
26

27
Return a `OpenFHE.CryptoContext` object stored in `a`.
28

29
See also: [`SecureContext`](@ref), [`SecureArray`](@ref), [`PlainArray`](@ref),
30
[`OpenFHEBackend`](@ref)
31
"""
32
function get_crypto_context(a::Union{SecureArray{<:OpenFHEBackend},
152✔
33
                                     PlainArray{<:OpenFHEBackend}})
34
    get_crypto_context(a.context)
152✔
35
end
36

37
"""
38
    generate_keys(context::SecureContext{<:OpenFHEBackend})
39

40
Generate and return public and private keys.
41

42
See also: [`PublicKey`](@ref), [`PrivateKey`](@ref), [`SecureContext`](@ref),
43
[`OpenFHEBackend`](@ref)
44
"""
45
function generate_keys(context::SecureContext{<:OpenFHEBackend})
6✔
46
    cc = get_crypto_context(context)
6✔
47
    keys = OpenFHE.KeyGen(cc)
6✔
48
    public_key = PublicKey(context, OpenFHE.public_key(keys))
6✔
49
    private_key = PrivateKey(context, OpenFHE.private_key(keys))
6✔
50

51
    public_key, private_key
6✔
52
end
53

54
"""
55
    init_multiplication!(context::SecureContext{<:OpenFHEBackend},
56
                         private_key::PrivateKey)
57

58
Generate relinearization key for use with `OpenFHE.EvalMult` using the `private_key`, and
59
store it in the given `context`.
60

61
See also: [`SecureContext`](@ref), [`OpenFHEBackend`](@ref), [`PrivateKey`](@ref)
62
"""
63
function init_multiplication!(context::SecureContext{<:OpenFHEBackend},
5✔
64
                              private_key::PrivateKey)
65
    cc = get_crypto_context(context)
5✔
66
    OpenFHE.EvalMultKeyGen(cc, private_key.private_key)
5✔
67

68
    nothing
×
69
end
70

71
"""
72
    init_rotation!(context::SecureContext{<:OpenFHEBackend}, private_key::PrivateKey,
73
                   shape::Union{Integer, NTuple{N, Integer}}, shifts...)
74

75
Generate all required rotation keys for applying `shifts` with `circshift` for arrays of
76
the given `shape` using the `private_key`. The keys are stored in the given `context`.
77

78
See also: [`SecureContext`](@ref), [`OpenFHEBackend`](@ref), [`PrivateKey`](@ref)
79
"""
80
function init_rotation!(context::SecureContext{<:OpenFHEBackend}, private_key::PrivateKey,
11✔
81
                        shape::Union{Integer, NTuple{N, Integer}}, shifts...) where N
82
    cc = get_crypto_context(context)
11✔
83
    # Get rotation indices for precompilation
84
    rotation_indices = compute_rotation_indices(context, shape, shifts)
11✔
85
    # All rotation indices correspond to Base.circshift, but to use with OpenFHE,
86
    # all rotation indices have to be negated.
87
    OpenFHE.EvalRotateKeyGen(cc, private_key.private_key, -unique(rotation_indices))
9✔
88

89
    nothing
×
90
end
91

92
# Computes rotation indices to enable 1D circshift
93
function compute_rotation_indices(context, shape::Union{Integer, Tuple{Integer}}, shifts)
10✔
94
    # extract capacity
95
    cc = get_crypto_context(context)
10✔
96
    capacity = OpenFHE.GetBatchSize(OpenFHE.GetEncodingParams(cc))
10✔
97
    # length of an array
98
    array_length = prod(shape)
10✔
99
    # length of short vector
100
    short_length = array_length % capacity
10✔
101
    # empty places in short vector
102
    empty_places = capacity - short_length
10✔
103
    # number of ciphertexts in array
104
    n_ciphertexts = Int(ceil(array_length/capacity))
10✔
105
    # store all indices to enable
106
    indices = Int[]
10✔
107
    # iterate over all shifts
108
    for shift in shifts
10✔
109
        if length(shift) > 1
42✔
110
            throw(ArgumentError("Got shift with length $(length(shift)), expected 1"))
1✔
111
        end
112
        # shift can be tuple or vector
113
        shift = shift[1]
42✔
114
        # minimal required rotation
115
        shift = shift % array_length
40✔
116
        # convert negative shift to positive one
117
        if shift < 0
40✔
118
            shift = array_length + shift
7✔
119
        end
120
        # add all required indices from implementation of rotate function
121
        shift += empty_places
40✔
122
        shift1 = div(shift, capacity)
40✔
123
        index = shift - capacity * shift1
40✔
124
        push!(indices, index)
40✔
125
        if empty_places != 0
40✔
126
            push!(indices, short_length)
40✔
127
            if shift1 % n_ciphertexts == 0
40✔
128
                push!(indices, index - empty_places)
22✔
129
            else
130
                push!(indices, index + short_length)
18✔
131
            end
132
        end
133
    end
40✔
134

NEW
135
    indices
×
136
end
137

138
# Computes rotation indices to enable nD circshift (n>1).
139
# Since nD circshift uses many 1D circshifts, this function 
140
# computes required 1D shifts and then call the function from above
141
# to translate them into OpenFHE rotation indices
142
function compute_rotation_indices(context, shape::NTuple{N, Integer}, shifts) where N
6✔
143
    # calculate length of each dimension
144
    lengths = ones(Int, N)
15✔
145
    for i in 2:N
6✔
146
        lengths[i] = prod(shape[1:i-1])
14✔
147
    end
12✔
148
    # assemble 1d shifts
149
    shifts_1d = Int[]
6✔
150
    for shift in shifts
6✔
151
        if length(shift) > N
17✔
152
            throw(ArgumentError("Got shift with length $(length(shift)), expected $N"))
1✔
153
        else
154
            # if shift is shorter than shape, fill it up with zeros
155
            shift = vcat(collect(shift), zeros(Integer, N - length(shift)))
16✔
156
        end
157
        # minimal required rotation
158
        shift = shift .% shape
24✔
159
        # convert negative shift to positive
160
        for i in 1:N
12✔
161
            if shift[i] < 0
30✔
162
                shift[i] = shape[i] + shift[i]
8✔
163
            end
164
        end
48✔
165
        # combination of all shifts (in each dimension) in 1D shift
166
        main_1d_shift = sum(shift .* lengths)
12✔
167
        push!(shifts_1d, main_1d_shift)
12✔
168
        # all possible combinations of dimensions (with non-zero shift)
169
        combinations = Vector{Int}[]
12✔
170
        for i in 1:N-1
12✔
171
            if shift[i] != 0
18✔
172
                append!(combinations, push!.(copy.(combinations), i))
26✔
173
                push!(combinations, [i])
13✔
174
            end
175
        end
24✔
176
        # shifts to retrieve cyclicity
177
        for i in combinations
12✔
178
            push!(shifts_1d, main_1d_shift)
18✔
179
            for j in i
18✔
180
                shifts_1d[end] -= lengths[j+1]
24✔
181
            end
24✔
182
        end
18✔
183
    end
16✔
184

185
    # compute rotation indices for 1D circshift
186
    compute_rotation_indices(context, prod(shape), shifts_1d)
5✔
187
end
188

189
"""
190
    init_bootstrapping!(context::SecureContext{<:OpenFHEBackend},
191
                        private_key::PrivateKey)
192

193
Generate the necessary keys from `private_key` to enable bootstrapping for a given
194
`context`. Supported for CKKS only.
195

196
See also: [`SecureContext`](@ref), [`OpenFHEBackend`](@ref), [`PrivateKey`](@ref),
197
[`bootstrap!`](@ref)
198
"""
199
function init_bootstrapping!(context::SecureContext{<:OpenFHEBackend},
3✔
200
                             private_key::PrivateKey)
201
    cc = get_crypto_context(context)
3✔
202
    encoding_parameters = OpenFHE.GetEncodingParams(cc)
3✔
203
    slots = OpenFHE.GetBatchSize(encoding_parameters)
3✔
204
    OpenFHE.EvalBootstrapKeyGen(cc, private_key.private_key, slots)
3✔
205

206
    nothing
×
207
end
208

209
"""
210
    PlainVector(data::Vector{<:Real}, context::SecureContext{<:OpenFHEBackend})
211

212
Constructor for data type [`PlainVector`](@ref) takes an unencrypted `data` vector and a `context`
213
object of type `SecureContext{<:OpenFHEBackend}`. Return [`PlainVector`](@ref) with encoded but
214
not encrypted data. The `context` can be utilized later for encryption using [`encrypt`](@ref),
215
resulting in [`SecureVector`](@ref).
216
    
217
See also: [`PlainVector`](@ref), [`SecureVector`](@ref), [`encrypt`](@ref), [`decrypt`](@ref)
218
[`OpenFHEBackend`](@ref)
219
"""
220
function PlainVector(data::Vector{<:Real}, context::SecureContext{<:OpenFHEBackend})
7✔
221
    PlainArray(data, context)
7✔
222
end
223

224
"""
225
    PlainMatrix(data::Matrix{<:Real}, context::SecureContext{<:OpenFHEBackend})
226

227
Constructor for data type [`PlainMatrix`](@ref) takes an unencrypted `data` matrix and a `context`
228
object of type `SecureContext{<:OpenFHEBackend}`. Return [`PlainMatrix`](@ref) with encoded but
229
not encrypted data. The `context` can be utilized later for encryption using [`encrypt`](@ref),
230
resulting in [`SecureMatrix`](@ref).
231
    
232
See also: [`PlainMatrix`](@ref), [`SecureMatrix`](@ref), [`encrypt`](@ref), [`decrypt`](@ref)
233
[`OpenFHEBackend`](@ref)
234
"""
235
function PlainMatrix(data::Matrix{<:Real}, context::SecureContext{<:OpenFHEBackend})
5✔
236
    PlainArray(data, context)
5✔
237
end
238

239
"""
240
    PlainArray(data::Array{<:Real}, context::SecureContext{<:OpenFHEBackend})
241

242
Constructor for data type [`PlainArray`](@ref) takes an unencrypted `data` array and a `context`
243
object of type `SecureContext{<:OpenFHEBackend}`. Return [`PlainArray`](@ref) with encoded but
244
not encrypted data. The `context` can be utilized later for encryption using [`encrypt`](@ref),
245
resulting in [`SecureArray`](@ref).
246
    
247
See also: [`PlainArray`](@ref), [`SecureArray`](@ref), [`encrypt`](@ref), [`decrypt`](@ref)
248
[`OpenFHEBackend`](@ref)
249
"""
250
function PlainArray(data::Array{<:Real}, context::SecureContext{<:OpenFHEBackend})
52✔
251
    PlainArray(Vector{Float64}(vec(data)), context, size(data))
52✔
252
end
253

254
function PlainArray(data::Vector{Float64}, context::SecureContext{<:OpenFHEBackend}, 
52✔
255
                    shape)
256
    cc = get_crypto_context(context)
52✔
257
    # capacity of a single plaintext
258
    capacity = OpenFHE.GetBatchSize(OpenFHE.GetEncodingParams(cc))
52✔
259
    # split data between plaintexts, only last one can be not full
260
    n_plaintexts = ceil(Int, length(data)/capacity)
52✔
261
    plaintexts = OpenFHE.Plaintext[]
52✔
262
    for i in 1:n_plaintexts
52✔
263
        first = (i-1)*capacity + 1
417✔
264
        last = min(i*capacity, length(data))
417✔
265
        push!(plaintexts, OpenFHE.MakeCKKSPackedPlaintext(cc, data[first:last]))
417✔
266
    end
782✔
267
    plain_array = PlainArray(plaintexts, shape, capacity * n_plaintexts, context)
52✔
268

NEW
269
    plain_array
×
270
end
271

272
function Base.show(io::IO, pa::PlainArray{<:OpenFHEBackend})
34✔
273
    print(io, collect(pa))
34✔
274
end
275

276
function Base.show(io::IO, ::MIME"text/plain", pa::PlainArray{<:OpenFHEBackend})
3✔
277
    print(io, pa.shape, "-shaped PlainArray{OpenFHEBackend}:\n")
3✔
278
    Base.print_matrix(io, collect(pa))
3✔
279
end
280

281
"""
282
    collect(plain_array::PlainArray{<:OpenFHEBackend})
283

284
Decode and return the real-valued data contained in `plain_array`.
285

286
See also: [`PlainArray`](@ref), [`OpenFHEBackend`](@ref)
287
"""
288
function Base.collect(plain_array::PlainArray{<:OpenFHEBackend})
60✔
289
    data = Vector.(OpenFHE.GetRealPackedValue.(plain_array.data))
60✔
290
    plaintext_capacity = Int(capacity(plain_array) / length(plain_array.data))
60✔
291
    empty_places = capacity(plain_array) - length(plain_array)
60✔
292
    short_length = plaintext_capacity - empty_places
60✔
293
    keepat!(data[end], 1:short_length)
60✔
294
    data = reduce(vcat, data)
60✔
295

296
    Array{Float64, ndims(plain_array)}(reshape(data, plain_array.shape))
60✔
297
end
298

299

300
"""
301
    level(a::Union{SecureArray{<:OpenFHEBackend}, PlainArray{<:OpenFHEBackend}})
302

303
Return the number of scalings, referred to as the level, performed over `a`.
304

305
See also: [`PlainArray`](@ref), [`SecureArray`](@ref), [`OpenFHEBackend`](@ref)
306
"""
307
function level(a::Union{SecureArray{<:OpenFHEBackend}, PlainArray{<:OpenFHEBackend}})
6✔
308
    maximum(Int.(OpenFHE.GetLevel.(a.data)))
6✔
309
end
310

311
function encrypt_impl(data::Array{<:Real}, public_key::PublicKey,
3✔
312
                      context::SecureContext{<:OpenFHEBackend})
313
    plain_array = PlainArray(data, context)
3✔
314
    secure_array = encrypt(plain_array, public_key)
3✔
315

NEW
316
    secure_array
×
317
end
318

319
function encrypt_impl(plain_array::PlainArray{<:OpenFHEBackend}, public_key::PublicKey)
21✔
320
    context = plain_array.context
21✔
321
    cc = get_crypto_context(context)
21✔
322
    ciphertexts = OpenFHE.Ciphertext[]
21✔
323
    for pv in plain_array.data
21✔
324
        push!(ciphertexts, OpenFHE.Encrypt(cc, public_key.public_key, pv))
64✔
325
    end
64✔
326
    secure_array = SecureArray(ciphertexts, size(plain_array), capacity(plain_array), context)
21✔
327

NEW
328
    secure_array
×
329
end
330

331
function decrypt_impl!(plain_array::PlainArray{<:OpenFHEBackend},
44✔
332
                       secure_array::SecureArray{<:OpenFHEBackend},
333
                       private_key::PrivateKey)
334
    cc = get_crypto_context(secure_array)
44✔
335
    for i in eachindex(secure_array.data)
44✔
336
        OpenFHE.Decrypt(cc, private_key.private_key, secure_array.data[i],
131✔
337
                        plain_array.data[i])
338
    end
131✔
339

NEW
340
    plain_array
×
341
end
342

343
function decrypt_impl(secure_array::SecureArray{<:OpenFHEBackend},
44✔
344
                      private_key::PrivateKey)
345
    context = secure_array.context
44✔
346
    plaintexts = Vector{OpenFHE.Plaintext}(undef, length(secure_array.data))
44✔
347
    for i in eachindex(plaintexts)
44✔
348
        plaintexts[i] = OpenFHE.Plaintext()
131✔
349
    end
218✔
350
    plain_array = PlainArray(plaintexts, size(secure_array), capacity(secure_array), context)
44✔
351

352
    decrypt!(plain_array, secure_array, private_key)
44✔
353
end
354

355
"""
356
    bootstrap!(secure_array::SecureArray{<:OpenFHEBackend}, num_iterations = 1,
357
               precision = 0)
358
     
359
Refresh a given `secure_array` to increase the multiplication depth. Supported for CKKS only.
360
Please refer to the OpenFHE documentation for details on the arguments `num_iterations` and
361
`precision`.
362

363
See also: [`SecureArray`](@ref), [`OpenFHEBackend`](@ref), [`init_bootstrapping!`](@ref)
364
"""
365
function bootstrap!(secure_array::SecureArray{<:OpenFHEBackend}, num_iterations = 1,
6✔
366
                    precision = 0)
367
    context = secure_array.context
6✔
368
    cc = get_crypto_context(context)
3✔
369
    for i in eachindex(secure_array.data)
3✔
370
        secure_array.data[i] = OpenFHE.EvalBootstrap(cc, secure_array.data[i],
4✔
371
                                                     num_iterations, precision)
372
    end
4✔
373

NEW
374
    secure_array
×
375
end
376

377

378
############################################################################################
379
# Arithmetic operations
380
############################################################################################
381

382
function add(sa1::SecureArray{<:OpenFHEBackend}, sa2::SecureArray{<:OpenFHEBackend})
24✔
383
    cc = get_crypto_context(sa1)
24✔
384
    ciphertexts = Vector{OpenFHE.Ciphertext}(undef, length(sa1.data))
24✔
385
    for i in eachindex(sa1.data)
24✔
386
        ciphertexts[i] = OpenFHE.EvalAdd(cc, sa1.data[i], sa2.data[i])
264✔
387
    end
504✔
388
    secure_array = SecureArray(ciphertexts, size(sa1), capacity(sa1), sa1.context)
24✔
389

NEW
390
    secure_array
×
391
end
392

393
function add(sa::SecureArray{<:OpenFHEBackend}, pa::PlainArray{<:OpenFHEBackend})
6✔
394
    cc = get_crypto_context(sa)
6✔
395
    ciphertexts = Vector{OpenFHE.Ciphertext}(undef, length(sa.data))
6✔
396
    for i in eachindex(sa.data)
6✔
397
        ciphertexts[i] = OpenFHE.EvalAdd(cc, sa.data[i], pa.data[i])
12✔
398
    end
18✔
399
    secure_array = SecureArray(ciphertexts, size(sa), capacity(sa), sa.context)
6✔
400

NEW
401
    secure_array
×
402
end
403

404
function add(sa::SecureArray{<:OpenFHEBackend}, scalar::Real)
6✔
405
    cc = get_crypto_context(sa)
6✔
406
    ciphertexts = Vector{OpenFHE.Ciphertext}(undef, length(sa.data))
6✔
407
    for i in eachindex(sa.data)
6✔
408
        ciphertexts[i] = OpenFHE.EvalAdd(cc, sa.data[i], scalar)
12✔
409
    end
18✔
410
    secure_array = SecureArray(ciphertexts, size(sa), capacity(sa), sa.context)
6✔
411

NEW
412
    secure_array
×
413
end
414

415
function subtract(sa1::SecureArray{<:OpenFHEBackend}, sa2::SecureArray{<:OpenFHEBackend})
6✔
416
    cc = get_crypto_context(sa1)
6✔
417
    ciphertexts = Vector{OpenFHE.Ciphertext}(undef, length(sa1.data))
6✔
418
    for i in eachindex(sa1.data)
6✔
419
        ciphertexts[i] = OpenFHE.EvalSub(cc, sa1.data[i], sa2.data[i])
10✔
420
    end
14✔
421
    secure_array = SecureArray(ciphertexts, size(sa1), capacity(sa1), sa1.context)
6✔
422

NEW
423
    secure_array
×
424
end
425

426
function subtract(sa::SecureArray{<:OpenFHEBackend}, pa::PlainArray{<:OpenFHEBackend})
3✔
427
    cc = get_crypto_context(sa)
3✔
428
    ciphertexts = Vector{OpenFHE.Ciphertext}(undef, length(sa.data))
3✔
429
    for i in eachindex(sa.data)
3✔
430
        ciphertexts[i] = OpenFHE.EvalSub(cc, sa.data[i], pa.data[i])
6✔
431
    end
9✔
432
    secure_array = SecureArray(ciphertexts, size(sa), capacity(sa), sa.context)
3✔
433

NEW
434
    secure_array
×
435
end
436

437
function subtract(pa::PlainArray{<:OpenFHEBackend}, sa::SecureArray{<:OpenFHEBackend})
3✔
438
    cc = get_crypto_context(sa)
3✔
439
    ciphertexts = Vector{OpenFHE.Ciphertext}(undef, length(sa.data))
3✔
440
    for i in eachindex(sa.data)
3✔
441
        ciphertexts[i] = OpenFHE.EvalSub(cc, pa.data[i], sa.data[i])
6✔
442
    end
9✔
443
    secure_array = SecureArray(ciphertexts, size(sa), capacity(sa), sa.context)
3✔
444

NEW
445
    secure_array
×
446
end
447

448
function subtract(sa::SecureArray{<:OpenFHEBackend}, scalar::Real)
3✔
449
    cc = get_crypto_context(sa)
3✔
450
    ciphertexts = Vector{OpenFHE.Ciphertext}(undef, length(sa.data))
3✔
451
    for i in eachindex(sa.data)
3✔
452
        ciphertexts[i] = OpenFHE.EvalSub(cc, sa.data[i], scalar)
6✔
453
    end
9✔
454
    secure_array = SecureArray(ciphertexts, size(sa), capacity(sa), sa.context)
3✔
455

NEW
456
    secure_array
×
457
end
458

459
function subtract(scalar::Real, sa::SecureArray{<:OpenFHEBackend})
3✔
460
    cc = get_crypto_context(sa)
3✔
461
    ciphertexts = Vector{OpenFHE.Ciphertext}(undef, length(sa.data))
3✔
462
    for i in eachindex(sa.data)
3✔
463
        ciphertexts[i] = OpenFHE.EvalSub(cc, scalar, sa.data[i])
6✔
464
    end
9✔
465
    secure_array = SecureArray(ciphertexts, size(sa), capacity(sa), sa.context)
3✔
466

NEW
467
    secure_array
×
468
end
469

470
function negate(sa::SecureArray{<:OpenFHEBackend})
3✔
471
    cc = get_crypto_context(sa)
3✔
472
    ciphertexts = Vector{OpenFHE.Ciphertext}(undef, length(sa.data))
3✔
473
    for i in eachindex(sa.data)
3✔
474
        ciphertexts[i] = OpenFHE.EvalNegate(cc, sa.data[i])
6✔
475
    end
9✔
476
    secure_array = SecureArray(ciphertexts, size(sa), capacity(sa), sa.context)
3✔
477

NEW
478
    secure_array
×
479
end
480

481
function multiply(sa1::SecureArray{<:OpenFHEBackend}, sa2::SecureArray{<:OpenFHEBackend})
6✔
482
    cc = get_crypto_context(sa1)
6✔
483
    ciphertexts = Vector{OpenFHE.Ciphertext}(undef, length(sa1.data))
6✔
484
    for i in eachindex(sa1.data)
6✔
485
        ciphertexts[i] = OpenFHE.EvalMult(cc, sa1.data[i], sa2.data[i])
10✔
486
    end
14✔
487
    secure_array = SecureArray(ciphertexts, size(sa1), capacity(sa1), sa1.context)
6✔
488

NEW
489
    secure_array
×
490
end
491

492
function multiply(sa::SecureArray{<:OpenFHEBackend}, pa::PlainArray{<:OpenFHEBackend})
36✔
493
    cc = get_crypto_context(sa)
36✔
494
    ciphertexts = Vector{OpenFHE.Ciphertext}(undef, length(sa.data))
36✔
495
    for i in eachindex(sa.data)
36✔
496
        ciphertexts[i] = OpenFHE.EvalMult(cc, sa.data[i], pa.data[i])
338✔
497
    end
640✔
498
    secure_array = SecureArray(ciphertexts, size(sa), capacity(sa), sa.context)
36✔
499

NEW
500
    secure_array
×
501
end
502

503
function multiply(sa::SecureArray{<:OpenFHEBackend}, scalar::Real)
9✔
504
    cc = get_crypto_context(sa)
9✔
505
    ciphertexts = Vector{OpenFHE.Ciphertext}(undef, length(sa.data))
9✔
506
    for i in eachindex(sa.data)
9✔
507
        ciphertexts[i] = OpenFHE.EvalMult(cc, sa.data[i], scalar)
16✔
508
    end
23✔
509
    secure_array = SecureArray(ciphertexts, size(sa), capacity(sa), sa.context)
9✔
510

NEW
511
    secure_array
×
512
end
513

514
function rotate(sa::SecureArray{<:OpenFHEBackend, 1}, shift)
43✔
515
    shift = shift[1]
43✔
516
    # crypto context
517
    cc = get_crypto_context(sa.context)
43✔
518
    # minimal required shift
519
    shift = shift % length(sa)
43✔
520
    # convert negative shift to positive one
521
    if shift < 0
43✔
522
        shift = length(sa) + shift
8✔
523
    end
524
    # operate with data stored as a vector of ciphertexts
525
    sv = sa.data
43✔
526
    # only the last ciphertext can be shorter than capacity, export capacity and length
527
    vec_capacity = Int(capacity(sa) / length(sa.data))
43✔
528
    empty_places = capacity(sa) - length(sa)
43✔
529
    short_length = vec_capacity - empty_places
43✔
530
    # update required shift with respect of empty places in last ciphertext
531
    shift += empty_places
43✔
532
    # shift vector of ciphertexts, so that shift is only required in each ciphertext
533
    # and between direct neighbours
534
    shift1 = div(shift, vec_capacity)
43✔
535
    sv = circshift(sv, shift1)
43✔
536
    # rotation index for individual ciphertexts
537
    rotation_index = shift - vec_capacity * shift1
43✔
538
    # if the last ciphertext is also full, rotation is simpler
539
    if empty_places == 0
43✔
540
        # shift each ciphertext
541
        for i in eachindex(sv)
26✔
542
            sv[i] = OpenFHE.EvalRotate(cc, sv[i], -rotation_index)
319✔
543
        end
612✔
544
        # first rotation_index elements of each ciphertext have to be moved 
545
        # to the first rotation_index elements of next ciphertext if there are more than one ciphertexts 
546
        if length(sv) > 1
26✔
547
            sv_new = similar(sv)
11✔
548
            # mask for first rotation_index elements of each ciphertext
549
            mask1 = zeros(vec_capacity)
88✔
550
            mask1[1:rotation_index] .= 1
50✔
551
            mask1 = OpenFHE.MakeCKKSPackedPlaintext(cc, mask1)
11✔
552
            # mask for remaining part of each ciphertext
553
            mask2 = zeros(vec_capacity)
88✔
554
            mask2[rotation_index+1:end] .= 1
38✔
555
            mask2 = OpenFHE.MakeCKKSPackedPlaintext(cc, mask2)
11✔
556
            for i in eachindex(sv)
11✔
557
                sv_new[i] = OpenFHE.EvalAdd(cc, OpenFHE.EvalMult(cc, circshift(sv, 1)[i], mask1),
304✔
558
                                            OpenFHE.EvalMult(cc, sv[i], mask2))
559
            end
597✔
560
            sv = sv_new
11✔
561
        end
562
    # next case when after rotating a whole array (not individual ciphertexts) 
563
    # short ciphertext is already the last one 
564
    elseif shift1 % length(sv) == 0
17✔
565
        # if short ciphertext is at the end, shift does not need to be corrected due to its empty places 
566
        # (except for the short ciphertext), change the rotation index back
567
        rotation_index -= empty_places
11✔
568
        # rotate all ciphertexts except the last one
569
        for i in 1:length(sv)-1
11✔
570
            sv[i] = OpenFHE.EvalRotate(cc, sv[i], -rotation_index)
9✔
571
        end
13✔
572
        # rotate the last considering empty places
573
        sv[end] = OpenFHE.EvalRotate(cc, sv[end], -(rotation_index + empty_places))
11✔
574
        # first rotation_index elements of each ciphertext have to be moved to the next one
575
        sv_new = similar(sv)
11✔
576
        # mask for first rotation_index elements of each ciphertext
577
        mask1 = zeros(vec_capacity)
6,232✔
578
        mask1[1:rotation_index] .= 1
35✔
579
        mask1 = OpenFHE.MakeCKKSPackedPlaintext(cc, mask1)
11✔
580
        # mask for remaining part of each ciphertext
581
        mask2 = zeros(vec_capacity)
6,232✔
582
        mask2[rotation_index+1:end] .= 1
6,197✔
583
        mask2 = OpenFHE.MakeCKKSPackedPlaintext(cc, mask2)
11✔
584
        for i in 1:length(sv)-1
11✔
585
            sv_new[i] = OpenFHE.EvalAdd(cc, OpenFHE.EvalMult(cc, circshift(sv, 1)[i], mask1),
9✔
586
                                        OpenFHE.EvalMult(cc, sv[i], mask2))
587
        end
13✔
588
        # The last ciphertext have to be also additionally rotated by its length, so that elements stay
589
        # at correct position
590
        sv_new[end] = OpenFHE.EvalAdd(cc, OpenFHE.EvalMult(cc, circshift(sv, 1)[end], mask1),
11✔
591
                                      OpenFHE.EvalMult(cc, OpenFHE.EvalRotate(cc, sv[end], -short_length), mask2))
592
        sv = sv_new
11✔
593
    # The last case when short ciphertext is not the last one and its empty places have to be filled,
594
    # so that the last ciphertext still the only short one
595
    else
596
        # first shift1 ciphertexts have to be rotated by rotation_index
597
        for i in 1:shift1
6✔
598
            sv[i] = OpenFHE.EvalRotate(cc, sv[i], -rotation_index)
7✔
599
        end
8✔
600
        # all other ciphertexts except last one have to be rotated by rotation_index + short_length to compensate
601
        # empty places in array's middle
602
        for i in shift1+1:length(sv)-1
9✔
603
            sv[i] = OpenFHE.EvalRotate(cc, sv[i], -(rotation_index + short_length))
5✔
604
        end
7✔
605
        # the last one is also shifted by rotation_index
606
        sv[end] = OpenFHE.EvalRotate(cc, sv[end], -rotation_index)
6✔
607
        # for all ciphertexts before short one first rotation_index elements have to be moved from the previous ciphertext
608
        sv_new = similar(sv)
6✔
609
        # mask for first rotation_index elements of each ciphertext
610
        mask1 = zeros(vec_capacity)
72✔
611
        mask1[1:rotation_index] .= 1
24✔
612
        mask1 = OpenFHE.MakeCKKSPackedPlaintext(cc, mask1)
6✔
613
        # mask for remaining part of each ciphertext
614
        mask2 = zeros(vec_capacity)
72✔
615
        mask2[rotation_index+1:end] .= 1
48✔
616
        mask2 = OpenFHE.MakeCKKSPackedPlaintext(cc, mask2)
6✔
617
        for i in 1:shift1-1
6✔
618
            sv_new[i] = OpenFHE.EvalAdd(cc, OpenFHE.EvalMult(cc, circshift(sv, 1)[i], mask1),
1✔
619
                                        OpenFHE.EvalMult(cc, sv[i], mask2))
620
        end
1✔
621
        # depending on how rotation_index and short_length relate, several cases are possible
622
        if rotation_index == empty_places
6✔
623
            # if after rotation last element of short ciphertext is already on last place, it needs only first
624
            # rotation_index elements from previous ciphertext
625
            sv_new[shift1] = OpenFHE.EvalAdd(cc, OpenFHE.EvalMult(cc, circshift(sv, 1)[shift1], mask1),
2✔
626
                                             OpenFHE.EvalMult(cc, sv[shift1], mask2))
627
            # due to empty place in new last ciphertext, it has to be rotated
628
            sv_new[end] = OpenFHE.EvalRotate(cc, sv[end], -short_length)
2✔
629
            # all other ciphertexts are without changes
630
            sv_new[shift1+1:end-1] = sv[shift1+1:end-1]
2✔
631
        # if last element of short ciphertext after circular shift has come back at front, it has to be moved
632
        # to the next ciphertext, as well as for all next ciphertexts
633
        elseif rotation_index > empty_places
4✔
634
            # move first rotation_index elements to the short ciphertext from previous
635
            sv_new[shift1] = OpenFHE.EvalAdd(cc, OpenFHE.EvalMult(cc, circshift(sv, 1)[shift1], mask1),
2✔
636
                                             OpenFHE.EvalMult(cc, sv[shift1], mask2))
637
            # number of elements to shift from short ciphertext to the next one
638
            n_shift = rotation_index - empty_places
2✔
639
            # mask for first n_shift elements 
640
            mask3 = zeros(vec_capacity)
24✔
641
            mask3[1:n_shift] .= 1
7✔
642
            mask3 = OpenFHE.MakeCKKSPackedPlaintext(cc, mask3)
2✔
643
            # mask for remaining part of each ciphertext
644
            mask4 = zeros(vec_capacity)
24✔
645
            mask4[n_shift+1:end] .= 1
17✔
646
            mask4 = OpenFHE.MakeCKKSPackedPlaintext(cc, mask4)
2✔
647
            # move n_shift elements starting from short ciphertext upto last one
648
            for i in shift1+1:length(sv)-1
3✔
649
                sv_new[i] = OpenFHE.EvalAdd(cc, OpenFHE.EvalMult(cc, sv[i-1], mask3),
2✔
650
                                            OpenFHE.EvalMult(cc, sv[i], mask4))
651
            end
3✔
652
            # last one has to be additionally rotated due to empty place
653
            sv_new[end] = OpenFHE.EvalAdd(cc, OpenFHE.EvalMult(cc, sv[end-1], mask3),
2✔
654
                                          OpenFHE.EvalMult(cc, OpenFHE.EvalRotate(cc, sv[end], -short_length),
655
                                                           mask4))
656
        # if the last element of short ciphertext didn't reach the end of ciphertext, elements
657
        # from next ciphertext have to be moved to the end of short ciphertext
658
        else
659
            # number of elements to shift from next ciphertext
660
            n_shift = empty_places - rotation_index
2✔
661
            # mask for short_length elements after rotation_index elements
662
            mask3 = zeros(vec_capacity)
24✔
663
            mask3[1+rotation_index:short_length+rotation_index] .= 1
17✔
664
            mask3 = OpenFHE.MakeCKKSPackedPlaintext(cc, mask3)
2✔
665
            # mask for last n_shift elements
666
            mask4 = zeros(vec_capacity)
24✔
667
            mask4[end-n_shift+1:end] .= 1
4✔
668
            mask4 = OpenFHE.MakeCKKSPackedPlaintext(cc, mask4)
2✔
669
            # mask for first capacity-n_shift elements
670
            mask5 = zeros(vec_capacity)
24✔
671
            mask5[1:end-n_shift] .= 1
20✔
672
            mask5 = OpenFHE.MakeCKKSPackedPlaintext(cc, mask5)
2✔
673
            # short ciphertext is a combination of first rotation_index elements of previous ciphertext,
674
            # last n_shift elements of the next ciphertext, and itself
675
            sv_new[shift1] = OpenFHE.EvalAdd(cc,
2✔
676
                                             OpenFHE.EvalAdd(cc, OpenFHE.EvalMult(cc, circshift(sv, 1)[shift1], mask1),
677
                                                             OpenFHE.EvalMult(cc, sv[shift1], mask3)),
678
                                             OpenFHE.EvalMult(cc, circshift(sv, -1)[shift1], mask4))
679
            # All ciphertexts after the short one upto prelast ciphertext
680
            # become last n_shift elements from the next ciphertext
681
            for i in shift1+1:length(sv)-2
3✔
682
                sv_new[i] = OpenFHE.EvalAdd(cc, OpenFHE.EvalMult(cc, sv[i+1], mask4),
1✔
683
                                            OpenFHE.EvalMult(cc, sv[i], mask5))
684
            end
1✔
685
            # last ciphertext is rotated due to empty places
686
            sv_new[end] = OpenFHE.EvalRotate(cc, sv[end], -short_length)
2✔
687
            # prelast becomes n_shift elements from the last ciphertext
688
            # from positions rotation_index+1:rotation_index+n_shift
689
            sv_new[end-1] =  OpenFHE.EvalAdd(cc, OpenFHE.EvalMult(cc, sv_new[end], mask4), OpenFHE.EvalMult(cc, sv[end-1], mask5))
2✔
690
        end
691
        # update vector
692
        sv = sv_new
6✔
693
    end
694

695
    SecureArray(sv, size(sa), capacity(sa), sa.context)
43✔
696
end
697

698
function rotate(sa::SecureArray{<:OpenFHEBackend, N}, shift) where N
12✔
699
    # use mutable type
700
    shift = collect(shift)
20✔
701
    # minimal required shift
702
    shift = shift .% size(sa)
24✔
703
    for i in 1:N
12✔
704
        # convert negative shifts to positive
705
        if shift[i] < 0
30✔
706
            shift[i] = size(sa)[i] + shift[i]
8✔
707
        end
708
    end
48✔
709
    # length of each dimension
710
    lengths = ones(Int, N)
30✔
711
    for i in 1:N
12✔
712
        lengths[i] = prod(size(sa)[1:i-1])
48✔
713
    end
48✔
714
    # combination of all shifts (in each dimension) in 1D shift
715
    main_1d_shift = sum(shift .* lengths)
22✔
716
    # indices for array iteration
717
    indices = Vector(undef, N)
12✔
718
    # mask for main part
719
    main_mask = ones(Int, size(sa))
600✔
720
    for i in 1:N-1
12✔
721
        indices[:] .= range.(1, size(sa))
18✔
722
        indices[i] = (size(sa)[i] - shift[i] + 1):size(sa)[i]
23✔
723
        main_mask[indices...] .= 0
18✔
724
    end
24✔
725
    # compute all combinations of dimensions (except last one and with non-zero shift)
726
    # to retrieve cyclicity
727
    combinations = Vector{Int}[]
12✔
728
    for i in 1:N-1
12✔
729
        if shift[i] != 0
18✔
730
            append!(combinations, push!.(copy.(combinations), i))
26✔
731
            push!(combinations, [i])
13✔
732
        end
733
    end
24✔
734
    # masks to retrieve cyclicity
735
    masks = []
12✔
736
    # additional 1D shifts for masked dimension combinations
737
    masked_1d_shift = []
12✔
738
    for i in combinations
12✔
739
        push!(masked_1d_shift, main_1d_shift)
18✔
740
        indices[:] .= range.(1, size(sa) .- shift)
18✔
741
        indices[end] = range.(1, size(sa)[end])
18✔
742
        # correct indices to include only elements that are
743
        # shifted in the given combination i
744
        for j in i
18✔
745
            indices[j] = (size(sa)[j] - shift[j] + 1):size(sa)[j]
24✔
746
            masked_1d_shift[end] -= lengths[j+1]
24✔
747
        end
24✔
748
        push!(masks, zeros(Int, size(sa)))
2,077✔
749
        masks[end][indices...] .= 1
18✔
750
    end
18✔
751
    # convert masks to PlainArray's
752
    main_mask = PlainArray(vec(main_mask), sa.context)
12✔
753
    for i in eachindex(masks)
12✔
754
        masks[i] = PlainArray(vec(masks[i]), sa.context)
18✔
755
    end
26✔
756
    # operate with N-dimensional array in form of 1D
757
    sv = SecureArray(sa.data, (length(sa),), capacity(sa), sa.context)
12✔
758
    # apply main shift
759
    sv_new = circshift(sv * main_mask, main_1d_shift)
22✔
760
    # correct positions of elements in each dimension combination
761
    for i in eachindex(masks)
12✔
762
        sv_new += circshift(sv * masks[i], masked_1d_shift[i])
18✔
763
    end
26✔
764

765
    SecureArray(sv_new.data, size(sa), capacity(sa), sa.context)
12✔
766
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc