HEAAN implementation in Julia
HEAAN の実装を読む
Overview
test/api.test.jl > @testcase “encrypt()/decrypt()” 簡素化
- log_polynomial_length(n): 多項式の長さ, 長いほど安全だが遅い
- log_lo_modulus(q_L): 暗号文の長さ, 長いほどたくさん計算できるが遅い
# 0. Params
log_precision = 30
log_cap = 100
params = Params(log_polynomial_length=8, log_lo_modulus=300)
# 1. KeyGen
sk = SecretKey()
pk = EncryptionKey(sk)
# 2. Encryption
z1 = [complex(0.3)]
z2 = [complex(0.4)]
c1, c2 = encrypt(pk, z1), encrypt(pk, z2)
# 3. Operation
c3 = add(c1, c2)
# 4. Decryption
z3 = decrypt(sk, c3)
@assert is_approx(z1 .+ z2, c3, 23)1. KeyGen
# 乱数生成器: メルセンヌ・ツイスタ
rng = MersenneTwister(123)
params = Params(log_polynomial_length=8, log_lo_modulus=300)
secret_key = SecretKey(rng, params)
enc_key = EncryptionKey(rng, secret_key)Params(log_polynomial_length, log_lo_modulus)
HEAANスキームのパラメータ
src/params.jl(簡素化)
struct Params
# in HEAAN: N
log_polynomial_length::Int
# in HEAAN: logQ, q_L in the paper
log_lo_modulus::Int
# in HEAAN: == logQ, P in the paper
log_hi_modulus::Int
# in HEAAN: sigma
gaussian_noise_stddev::Float64
# in HEAAN: h
secret_key_length::Int
endgaussian_noise_stddev = 3.2secret_key_length = 64log_hi_modulus = log_lo_modulus(=300) + 50
SecretKey(params)
- 秘密鍵は 乱数 から 一様にインデックスを
secret_key_length = 64個を選びランダムビット [1 => true, 3 => false, 7 => true ]のようにスパースに保持
src/secret_key.jl(抜粋)
struct SecretKey
params :: Params
# [(pow, is -1?)]
nonzero_entries :: Array{Pair{Int, Bool}, 1}
function SecretKey(rng::AbstractRNG, params::Params)
# 0 ~ 2^params.log_polynomial_length-1 の一様分布
positions = sample_unique(
rng, 0, 2^params.log_polynomial_length-1, params.secret_key_length)
bits = rand(rng, Bool, params.secret_key_length)
new(params, [pos => bit for (pos, bit) in zip(positions, bits)])
end
endEncryptionKey(sk)
src/public_keys.jl(抜粋)
struct EncryptionKey
params :: Params
key :: PublicKeyRNS
function EncryptionKey(rng::AbstractRNG, secret_key::SecretKey)
params = secret_key.params
log_plen = params.log_polynomial_length
plen = 2^log_plen
log_modulus = params.log_lo_modulus + params.log_hi_modulus
# rand_big_int(rng, 250, 256)
# 0:2^log_modulusの乱数 を plen個 -> poly
ax::Polynomial = rand_big_int(rng, log_modulus, plen)
# ガウスノイズをかける
bx::Polynomial = discrete_gaussian(rng, params.gaussian_noise_stddev, plen) - secret_key * ax
plan = rns_plan(params)
rax = to_rns_transformed(plan, ax, params.log_lo_modulus)
rbx = to_rns_transformed(plan, bx, params.log_lo_modulus)
new(params, PublicKeyRNS(rax, rbx))
end
endNTT(数論変換)
NTTは,特殊な法を用いたFFT(高速フーリエ変換)である[13].FFTとは,DFT(離散フーリエ変換)を高速に計算するアルゴリズムである.多項式環上での乗算,すなわち多項式同士の乗算には,次数をNとしてO(N2)の時間計算量を要する.多項式同士の乗算は,多項式の係数列同士の畳み込みとみなせる.係数列畳み込みの時間計算量も,多項式同士の乗算同様にである.一方,多項式の係数列をNTT形式に変換した場合,多項式同士の乗算(係数列の畳み込み)をで得ることができる.RNS型CKKS方式においても,NTTを用いて乗算の計算コストを抑えている[7].上の多項式の係数ベクトルのNTT形式をと表記する. を を満たす素数を法としてNTT形式へ変換する関数をと定義する.また,の逆変換をと定義する.
CKKS方式準同型暗号におけるRescale演算のGPU実装と演算性能評価 より
to_rns_transformed(…)
RNS(剰余数系)
有限個の整数からなる順序集合 で,要素同士が互いに素である場合,を基底と呼ぶ.
さらに, とおく.
を, と置いた時,を, のRNS表現と呼ぶ.つまり,RNSを用いることで,上の演算を,よりサイズの小さい に分割して計算を行うことができる.
互いに素な素数で割ったあまりを並べた表現
primes = [5, 3, 2] の時
29 = [29 % 5, 29 % 3, 29 % 2] = [4, 2, 1]
to_rns_transformed
function to_rns_transformed(
plan::RNSPlan,
x::Polynomial{BinModuloInt{T, Q}},
add_range::Int=0
) where {T, Q}
# log_rangeを計算
log_range = _log_modulus_mul(Q, add_range, length(x.coeffs))
_ntt_forward(_to_rns(plan, x, log_range))
end
function _to_rns(
plan::RNSPlan,
x::Polynomial{BinModuloInt{T, Q}},
log_range::Int
) where {T, Q}
# log_modulusより大きい最初の plan.max_bin_moduli
np = min_nprimes(plan, log_range)
# plan.max_bin_moduli[nprimes]
log_range = max_log_modulus(plan, np)
plen = length(x.coeffs)
res = Array{UInt64}(undef, plen, np)
for i in 1:plen
res[i,:] .= to_rns_signed(plan, x.coeffs[i], np)
end
RNSPolynomial(plan, res, Q, log_range, x.modulus)
end- to_tns_signed() は to_tns() をしてから符号逆転
2. Encode & Encryotion
encrypt(pk, z)
src/ciphertext.jl(抜粋)
function encrypt(
pk::EncryptionKey,
z::Array{Complex{Float64}, 1},
log_precision::Int, log_cap::Int
)
m = encode(
pk, z,
log_precision + params.log_hi_modulus,
log_cap + params.log_hi_modulus
)
encrypt(pk, m)
endencode(z)
個の複素数の配列 を でスケーリングしたあとパッキングを行う. パッキングした平文を とする
Array{Complex{Float64}, 1} -> PlainText
function encode(
z::Array{Complex{Float64}, 1},
log_precision::Int,
log_modulus::Int,
)
slots = length(z)
m = τ′(params, z, log_precision, log_modulus, minimize_range)
Plaintext(params, m, log_modulus, log_precision, slots)
enddecode(m)
encodeの逆
PlainText -> Array{Complex{Float64}, 1}
function decode(m::Plaintext)
z = τ(m)
embed(m.params, z)
endencrypt(pk, m)
function encrypt(rng::AbstractRNG, pk::EncryptionKey, m::Plaintext)
params = plain.params
plen = 2 ^ params.log_polynomial_length
log_modulus = plain.log_cap
# random bool polyonmial
vx = sample_ZO(plen)
# vx --[to_rns]-> vx_rns
plan = rns_plan(params)
vx_rns = to_rns_transformed(plan, vx, params.log_lo_modulus + params.log_hi_modulus)
# vx_rns * rax
rax = from_rns_transformed(vx_rns * key.key.rax, log_modulus)
noise = discrete_gaussian(rng, params.gaussian_noise_stddev, plen)
# ax = rax: Poly + noise: Poly
ax = (rax + noise)
# bx = plain: Poly + (vx_rns: RNS * rax: Poly) + noise: Poly
# vx_rns * rbx
rbx = from_rns_transformed(vx_rns * key.key.rbx, log_modulus)
noise = discrete_gaussian(rng, params.gaussian_noise_stddev, plen)
# bx = rbx: Poly + plain: Poly + noise: Poly
bx = (plain.polynomial + rbx + noise)
# ax, bx: Polynomial{Z_450, 256}
# 450 = log_lo + log_hi
# ax.map(i -> right_shift_rounded(i, params.log_hi_modulus))
ax = broadcast_into_polynomial(right_shift_rounded, ax, params.log_hi_modulus)
bx = broadcast_into_polynomial(right_shift_rounded, bx, params.log_hi_modulus)
Ciphertext(ax, bx)
end3. Operation
add(c1, c2)
function add(c1::Ciphertext, c2::Ciphertext)
Ciphertext(c1.ax + c2.ax, c1.bx + c2.bx)
end- 同型に演算ができる
- log_cap, log_precisionは変化なし
mul(mk, c1, c2)
function mul(
key::MultiplicationKey,
c1::Ciphertext,
c2::Ciphertext
)
params = cipher1.params
plan = rns_plan(params)
log_cap = cipher1.log_cap
ra1 = to_rns_transformed(plan, cipher1.ax, log_cap)
rb1 = to_rns_transformed(plan, cipher1.bx, log_cap)
ra2 = to_rns_transformed(plan, cipher2.ax, log_cap)
rb2 = to_rns_transformed(plan, cipher2.bx, log_cap)
axax = from_rns_transformed(ra1 * ra2, log_cap)
bxbx = from_rns_transformed(rb1 * rb2, log_cap)
ra1 = ra1 + rb1
ra2 = ra2 + rb2
axbx = from_rns_transformed(ra1 * ra2, log_cap)
key = key.key
raa = to_rns_transformed(plan, axax, params.log_lo_modulus + params.log_hi_modulus)
#=
In the paper, we want to find `(P^(-1) * x * y) mod q`,
where `P = 2^log_hi_modulus`, `Q = 2^log_lo_modulus`, `x` is a polynomial modulo `q <= Q`,
and `y` is a polynomial modulo `P * Q`.
So, we perform the multiplication using the full range `q * Q * P`
(times the polynomial length to prevent overflow during NTT in RNS),
then drop the high `Q` bits (during the conversion from RNS to big integer),
then shift by `P`.
=#
log_modulus = cipher1.log_cap + params.log_hi_modulus
ax = broadcast_into_polynomial(
right_shift_rounded,
from_rns_transformed(raa * key.rax, log_modulus), params.log_hi_modulus)
bx = broadcast_into_polynomial(
right_shift_rounded,
from_rns_transformed(raa * key.rbx, log_modulus), params.log_hi_modulus)
ax = ax + axbx - bxbx - axax
bx = bx + bxbx
Ciphertext(
params,
ax,
bx,
cipher1.log_cap,
cipher1.log_precision + cipher2.log_precision,
cipher1.slots)
end
3.1 Decryption Structure of HEAAN
既存のHE方式の多くは、ZtやZt[X]/(ΦM(X))のようなモジュロ空間上で演算を行い、同相計算後の再帰メッセージのLSBを暗号化した暗号文を計算することを目的としている。 例えば、BGV型暗号[5,25,33]の場合、平文は暗号文のモジュラスの最下位ビットに配置され、つまり、暗号化されたメッセージの秘密情報khに対する暗号化情報は、〈c,sk〉=m+te(modq)の形式の復号化構造を持つ。m1,m2の暗号化の乗算は、m1m2の一部のLSB(すなわち、[m1m2]t)を保持し、そのMSB(すなわち、bm1m2/te)は誤りによって破壊される。一方、FV型方式[4,22,2]では、暗号文の最上位ビットにメッセージを配置し、その復号構造が〈c,sk〉=bq/te-m+e(modq)を満たすようにしています。しかし、結果として得られるメッセージのMSBは、〈ci,sk〉=q-Ii+bq/te-m+eiの同型乗算の間にも破壊され、それぞれがメッセージの左位置に追加のエラーIiiを含む。
我々の目標は、暗号化されたデータに対して近似演算を行うこと、あるいは等価的に、同型演算の後に得られるメッセージのMSBを計算することである。主な考え方は、入力メッセージの重要な数字に続く暗号化ノイズを付加することである。より正確には、我々のスキームは、いくつかの小さなRoreに対して、〈c,sk〉=m+e(modq)の形式の復号化構造を持っている。この暗号化誤差は、スキームの安全性を保証するために挿入しているが、近似計算の際に発生する誤差と考える。つまり、復号化アルゴリズムの出力は、高精度で元のメッセージの近似値として扱われます。平文のサイズは、同型演算のための暗号文弾性率に比べて十分に小さく、算術演算の結果が暗号文弾性率よりも小さくなるようにする。
より慎重に検討しなければならない問題がいくつかあります。暗号化されていない近似計算では、連続して演算を行うと小さな誤差が爆発する可能性があるため、計算結果とアルゴリズムの正確な値との近さを考慮することは非常に重要です。また、メッセージのサイズの管理も課題の一つです。 メッセージの丸めを行わずに多倍深さLの回路を計算すると、出力値のビットサイズはLとともに指数関数的に大きくなります。このようなナイーブな方法は、暗号文のモジュラスが巨大化してしまうため、実用には適していません。この問題を解決するために、中間値を基底値で分割する新しい手法を提案します。これにより、7
メッセージのサイズをほぼ同じにして、必要な暗号文のモジュラスを深さLの中で線形にする方法です。この方法により、メッセージの大きさをほぼ同じに保ち、必要な暗号文のモジュラスを深さLで線形にすることができます。
3.3 Leveled Homomorphic Encryption Scheme for Approximate Arithmetic
この節の目的は、近似アリスメティックのためのレベル化されたHEスキームを構築することである。
, モジュラス , for
セキュリティパラメータ については、 を選択します。
レベル の場合、レベルの暗号文は固定整数 に対して のベクトルである。我々のスキームは、ノイズ推定のための定数BcleanとBmult(`)を持つ5つのアルゴリズム(KeyGen,Enc,Dec,Add,Mult)から構成されている。
-
: を作成
-
:
-
: レベル の から
-
: を計算(ノイズは合計)
-
:
-
: から より低い への変換.
: レベル 上限
: ノイズの上限
各パラメータの推移

異なるレベル間の演算
- 低い方に合わせる
- 単純な
modswitchを行う
異なるレベルの暗号文の同相演算.暗号文sc,c′ofm,m′が異なるレベルに属している場合、同相演算を行う前に、より大きなレベルの暗号文をより小さなレベルの暗号文に近づけるべきである。単純なモジュール化とRS手続きの2つの方法があります。この論文では,特に断りのない限り,異なるレベルの暗号文に対して計算を行う前に,単純なモジュラス削減を行い,より小さなモジュラスへと変換することにしている.

- : 分散 の離散ガウス分布:
- : で1, で-1, で0
- :
- : for
- :
( はlog_hi_modulus: Intのこと)
ロジスティック関数の近似
s