From 34136ed494f920cf38794af62cfd004b4eb99265 Mon Sep 17 00:00:00 2001 From: Maximilian Stauss <max.stauss@gmail.com> Date: Tue, 5 Sep 2017 16:17:59 +0200 Subject: [PATCH] Beispiele angepasst --- bsp_MIA.py | 54 ++++++++ bsp_UOV.py | 34 +++-- classes/AffineTransformation.py | 35 +++++ classes/MatsumotoImaiAExample.py | 79 +++++++++++ classes/UnbalancedOilAndVinegarExample.py | 151 ++++++++++++++++++++++ keys/bsp_uov.priv | Bin 2023 -> 2030 bytes keys/bsp_uov2.priv | Bin 2030 -> 0 bytes keys/bsp_uov_old.priv | Bin 0 -> 2023 bytes 8 files changed, 342 insertions(+), 11 deletions(-) create mode 100644 bsp_MIA.py create mode 100644 classes/AffineTransformation.py create mode 100644 classes/MatsumotoImaiAExample.py create mode 100644 classes/UnbalancedOilAndVinegarExample.py delete mode 100644 keys/bsp_uov2.priv create mode 100644 keys/bsp_uov_old.priv diff --git a/bsp_MIA.py b/bsp_MIA.py new file mode 100644 index 0000000..ae6eed2 --- /dev/null +++ b/bsp_MIA.py @@ -0,0 +1,54 @@ +#!/usr/bin/sage +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, print_function +from sage.all import * + +from classes.MatsumotoImaiAExample import MatsumotoImaiAExample + +# Endlicher Körper mit 5 Elementen: +finite_field = GF(2**2, 'a') +(a,) = finite_field.gens() + +n = 3 # Drei Variablen + +"""TEST MIA""" + +# MIA mit den Schlüsseln initialisieren +# MatsumotoImaiAExample setzt die sonst zufälligen Werte +# wie in den Beispielen und printed mehr +MIA = MatsumotoImaiAExample(finite_field, n) + + +# Schlüssel im sage CLI nutzbar machen +private_key = MIA.private_key +public_key = MIA.public_key + +# Schlüssel ausgeben +print("Privater Schlüssel:") +print("S:\n{}x + {}".format(private_key.S.M, private_key.S.y)) +print("Pr:\n{}".format(private_key.Pr)) +print("S:\n{}x + {}".format(private_key.T.M, private_key.T.y)) + +print("\nÖffentlicher Schlüssel:") +print("P:\n{}".format(public_key.P)) + +# Die Nachricht soll (1, a, a^2) sein +msg = vector(finite_field, [1, a, a**2]) +print("\n\nmsg =", msg) + +# Nachricht verschlüsseln +enc = public_key.encrypt(msg) +print("public_key.encrypt(msg): enc =", enc) + +# Verifizieren, dass enc auch entschlüsselt wird +print("MIA.decryt(enc):", MIA.decrypt(enc)) + +# Signatur erstellen +msg = vector(finite_field, [a, 1, 1]) +print("\nmsg =", msg) +sig = MIA.sign(msg) +print("sig =", sig) + +# Signatur verifizieren +print("public_key.verify(msg, sig):", public_key.verify(msg, sig)) diff --git a/bsp_UOV.py b/bsp_UOV.py index 4d9b5d2..cd28c51 100644 --- a/bsp_UOV.py +++ b/bsp_UOV.py @@ -4,9 +4,7 @@ from __future__ import absolute_import, division, print_function from sage.all import * -from classes.helpers.PublicKey import PublicKey -from classes.helpers.PrivateKey import PrivateKey -from classes.UnbalancedOilAndVinegar import UnbalancedOilAndVinegar +from classes.UnbalancedOilAndVinegarExample import * # Endlicher Körper mit 5 Elementen: finite_field = GF(5, 'a') @@ -16,18 +14,32 @@ n = 3 # Drei Variablen """TEST UOV""" -# Initialize simple UOV -UOV = UnbalancedOilAndVinegar(finite_field, n, m, keyfile="./keys/bsp_uov2.priv") +# UOV mit den Schlüsseln initialisieren +# UnbalancedOilAndVinegarExample setzt die sonst zufälligen Werte +# wie in den Beispielen und printed mehr +UOV = UnbalancedOilAndVinegarExample( + finite_field, n, m, keyfile="./keys/bsp_uov.priv") + +# Schlüssel im sage CLI nutzbar machen private_key = UOV.private_key public_key = UOV.public_key -# use a random vector +# Schlüssel ausgeben +print("Privater Schlüssel:") +print("S:\n{}x + {}".format(private_key.S.M, private_key.S.y)) +print("Pr:\n{}".format(private_key.Pr)) +print("S:\n{}x + {}".format(private_key.T.M, private_key.T.y)) + +print("\nÖffentlicher Schlüssel:") +print("P:\n{}".format(public_key.P)) + +# Die Nachricht soll (1, 4) sein msg = vector(finite_field, [1, 4]) +print("\n\nmsg =", msg) -# generate Signature +# Signatur erstellen sig = UOV.sign(msg) - -# verify signature is valid -print("msg =", msg) print("sig =", sig) -print("UOV.verify(msg, sig):", UOV.verify(msg, sig)) + +# Signatur verifizieren +print("public_key.verify(msg, sig):", public_key.verify(msg, sig)) diff --git a/classes/AffineTransformation.py b/classes/AffineTransformation.py new file mode 100644 index 0000000..ca63c10 --- /dev/null +++ b/classes/AffineTransformation.py @@ -0,0 +1,35 @@ +from __future__ import absolute_import, division, print_function +from sage.all import * + + +class AffineTransformation(): + def __init__(self, M, y): + """ + Eine affine Transformation wird mit einer invertierbaren + Matrix und einem beliebigen Vektor initialisiert + """ + + # Abbruchbedingung für nicht invertierbare Matrizen + if not M.is_invertible(): + exit(1) + + self.M = M # invertierbare Matrix + self.y = y # Vektor + + def __call__(self, vector): + """Anwenden der Transformation auf den gegebenen Vektor""" + + # Sollte `vector` eine Liste sein -> Typecast + if not isinstance(vector, sage.structure.element.Vector): + vector = vector(vector) + + return (self.M * vector) + self.y + + def inverse(self, vector): + """Anwenden der inversen Transformation auf den gegebenen Vektor""" + + # Sollte `vector` eine Liste sein -> Typecast + if not isinstance(vector, sage.structure.element.Vector): + vector = vector(vector) + + return self.M.inverse() * (vector - self.y) diff --git a/classes/MatsumotoImaiAExample.py b/classes/MatsumotoImaiAExample.py new file mode 100644 index 0000000..228e825 --- /dev/null +++ b/classes/MatsumotoImaiAExample.py @@ -0,0 +1,79 @@ +#!/usr/bin/sage +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, print_function +from sage.all import * +import copy as cp + +from .helpers.sage_extensions import random_value, random_invertible_matrix + +from .helpers.AffineTransformation import AffineTransformation +from .helpers.PrivateKey import PrivateKey +from .helpers.PrivateKey import PublicKey +from .helpers.EncryptionScheme import EncryptionScheme + + +class MatsumotoImaiAExample(EncryptionScheme): + """Matsumoto Imai Scheme A implementation""" + + def phi(self, polynomail_a): + return polynomail_a.list() + + def phi_inv(self, vector_a): # -> b::Polynomial + return self.extension_field(vector_a) + + def __init__(self, finite_field, n, keyfile=None): + self.finite_field = finite_field + self.n = n + + # generate or load private & public key + if keyfile is not None: + self.private_key = PrivateKey(None, None, None, keyfile) + self.public_key = self.private_key.generate_public_key() + else: + # self.generate_keys() + self.generate_example_keys() + + def generate_example_keys(self): + k = self.finite_field + (a,) = k.gens() + n = 3 + + ring = PolynomialRing(k, 'x') + (x,) = ring.gens() + gx = x**3 + x + 1 + + (self.theta, self.theta_invers) = (2, 26) + + ML1 = matrix(k, [[a**2, a, a], [a, 1, 0], [1, 0, 1]]) + yL1 = vector(k, [0, 1, a]) + T = AffineTransformation(ML1, yL1) + + ML2 = matrix(k, [[1, 0, a], [0, 1, a], [1, a, 0]]) + yL2 = vector(k, [a, a**2, a**2]) + S = AffineTransformation(ML2, yL2) + + multivariate_ring = PolynomialRing(k, 'x1, x2, x3', n) + extension_field = PolynomialRing( + multivariate_ring, 't').quotient_ring(gx, 'T') + self.extension_field = extension_field + multi_vars = multivariate_ring.gens() + + Sx = list(S(vector(multi_vars))) + + pre_F = self.phi_inv(Sx) # <- phi_invers + + # Now the MAGIC happens + # post_F = self.F(pre_F) + + post_F = extension_field("1 + (a + 1)*x1 + (a)*x2 + x3 + x1*x2 + (a)*x1*x3 + (a + 1)*x2*x3 + ((a) + (a)*x1 + x2 + (a + 1)*x3 + x1^2 + (a + 1)*x1*x2 + x2^2 + x2*x3)*T + ((a + 1) + (a + 1)*x1 + (a)*x2 + (a)*x3 + x1^2 + x1*x2 + (a)*x1*x3 + (a + 1)*x2^2 + (a)*x2*x3 + (a + 1)*x3^2)*T^2") + + SPTx = T(vector(post_F.list())) + + Pr = T.inverse(vector(post_F.list())) + self.private_key = PrivateKey(S, Pr, T) + self.public_key = PublicKey(SPTx) + + def invert_MQ(self, msg): + X = self.phi_inv(list(msg)) + return vector(self.phi(X**self.theta_invers)) diff --git a/classes/UnbalancedOilAndVinegarExample.py b/classes/UnbalancedOilAndVinegarExample.py new file mode 100644 index 0000000..a3d38a9 --- /dev/null +++ b/classes/UnbalancedOilAndVinegarExample.py @@ -0,0 +1,151 @@ +#!/usr/bin/sage +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, print_function +from sage.all import * +import copy as cp + +from .helpers.sage_extensions import random_value, random_invertible_matrix + +from .helpers.AffineTransformation import AffineTransformation +from .helpers.PrivateKey import PrivateKey +from .helpers.SignatureScheme import SignatureScheme + + +class UnbalancedOilAndVinegarExample(SignatureScheme): + """Unalanced Oil and Vinegar Implementation""" + + def __init__(self, finite_field, n, m, keyfile=None): + """ + UOV wird mit dem Grundkörper `F`, Länge der Nachrichten `n` + und Länge der Signatur `m` initialisiert + + Der Pfad zum privaten Schlüssel ist optional + """ + + # speichere die initialen Werte + self.finite_field = finite_field + self.ring = PolynomialRing(finite_field, 'x', n) + + self.n = n # Anzahl der Variablen + self.m = m # Anzahl der Gleichungen + + # wird ein privater Schlüssel als Dateipfad übergeben, so wird dieser + # geladen ansonsten werden mit `generate_keys` die Schlüssel erstellt + if keyfile is not None: + self.private_key = PrivateKey(None, None, None, keyfile) + self.public_key = self.private_key.generate_public_key() + else: + self.generate_keys(finite_field, n, m) + + def generate_keys(self, F, n, m): + """ + erstelle Privaten und öffentlichen Schlüssel aus gegebenem endlichen + Körper `F`, Anzahl der Variablen `n` und Anzahl der Gleichungen `m` + """ + + # oil and vinegar variables: + v = n - m + oil = m + + # Zwei affine Abbildungen (T ist Identität) + # - S ist (n x n) und T ist (m x m) + S = AffineTransformation( + random_invertible_matrix(F, n), random_vector(F, n)) + T = AffineTransformation(matrix.identity(F, m), vector(F, m)) + + # Generiere geheimes Polynom `Pr` + # - P ist (n x m) + alpha = [] + beta = [] + gamma = [] + Pr = [] + + # x[0] .. x[n-1] als Variablen benutzbar machen + x = vector(self.ring.gens()) + + # für jedes Polynom Pr[i] + for i in xrange(0, oil): + # Zufällige Matrix Representation jedes Polynoms + # - Alpha (Wert) ist der Konstante Anteil + # - Beta (Vektor) ist der lineare Anteil + # - Gamma (Matrix) ist der gemischte und quadratische Anteil + alpha.append(random_value(F)) + beta.append(random_vector(F, n)) + gamma.append(random_matrix(F, n - m, n)) + + '''Baue Polynom folgender Form''' + # poly = ((gamma[i] * x) * x) + (beta * x) + alpha[i] + + # Konstanter Anteil + poly = alpha[i] + + # Essigvariablen: + for j in xrange(0, v): + for k in xrange(0, n): + poly += gamma[i][j, k] * x[j] * x[k] + + # Essig- & Ölvariablen + for k in xrange(0, n): + poly += beta[i][k] * x[k] + + Pr.append(poly) + + # erstelle den geheimen und öffentlichen Schlüssel + self.private_key = PrivateKey(S, Pr, T) + self.public_key = self.private_key.generate_public_key() + + def invert_MQ(self, msg): + vinegar_len = self.n - self.m + x = list(self.ring.gens()) + F = self.finite_field + + # wichtig ist, dass private_key.Pr `UOV`-Gestalt hat + Pr = cp.copy(self.private_key.Pr) + + sig = [] + + """ Linearisierung """ + # x[1] bis x[n-m] werden zufällig aus F gewählt + # Diese bilden den ersten Teil der Lösung + if vinegar_len == 1: + x[0] = 3 + sig.append(x[0]) + else: + for i in xrange(0, vinegar_len): + x[i] = random_value(F) + sig.append(x[i]) + + print("Zufällig belegte Werte", x) + + # Durch einsetzen der belegten Variablen wird P linearisiert + for i in xrange(0, len(Pr)): + Pr[i] = Pr[i](x) + + # Die gelösten Variablen werden entfernt + x = x[vinegar_len:] + + """ Gleichungssystem lösen """ + # Um mit sagemath das lineare Gleichungssystem zu loesen + # wird diese Form angestrebt: A*x - b = 0 + A = matrix(F, len(x)) # Koeffizenten Matrix + b = vector(F, len(x)) + + # A[i] ist die Liste der Koeffizienten der neuen Monome von Pr[i] + # b[i] ist der konstante Anteil von Pr[i] + for i in xrange(0, len(x)): + for j in xrange(0, len(x)): + A[i, j] = Pr[i].monomial_coefficient(x[j]) + + b[i] = Pr[i].constant_coefficient() + + # Die Nachricht muss von den Konstanten abgezogen werden + b = msg - b + + # sagemath bietet eine einfache Methode die Lösung des GS zu finden + # sollte keine Lösung gefunden werden können muss die + for x in A.solve_right(b): + sig.append(x) + + # Die Lösung von "Pr(sig) = msg" wird zurückgegeben + return vector(F, sig) diff --git a/keys/bsp_uov.priv b/keys/bsp_uov.priv index b58b64b7dff26ab5dfef0d56e179a060c63bb4f9..9888565e6ce5c97005eb2b57c3a08dfa7b77ac05 100644 GIT binary patch delta 1039 zcmaFP|BinHCnKjfyEm&hV|yXbWNt=fC*Dv-)7GFuzEH-B)<S;2P?p^Ml)OTLP^RkI z489EBP)5VnphCfpl0u={l0xC2LJ@D~PQQ{u(b|$iv7kcn*g}cP1&rFBk|3o9twDuS zAO^@p=?szfphB4pxQuKDL`V)IWYQW`C=V4<NcCp+X6+~mDpZ{Ogi(o6X)-&LJ)`nu zcP0ZymB|%M>cOfS86w`y-prk$tZ6x!c_oEvCAIB^>OqAXowbFUq0A|%C52iLgFz0~ zh8nC>?9JE_%E`jW$il+F%*4>{%~cXqs9Ty8Tc|hr0;|MiE@pd1{mB<u#V2zy8&00Y zEF@u&Ari_|VNhro%2Z)kXcWp+VN__`UT6YVbeUPK-c+-((5$x5TqBe>zBn^4y)-AW zD89I)sL&#mDW#y$GL$2}JT)^tqolad$}f~XJ~y$XD6^u_+AowfJ})shwa_M%GbJ^> zD77pnwW82AlrKIvF*&g`C$Z89Y>ZtfyL(V-nNMm(p?#<b!iwDdqSU0Ee5>T_;z9>+ zhEVqSoYcJZl8iz}Z{|?0_}s+w+(fVePNAGiP~(*fokQ8<<5TjJ<KqimHZNk4U=(u= zWsgtF$p;zZrdgWQ9#rUFTj(+Q0;^uVXKkUEH-k50u{U#DP@y*@JV6oV0}W5#49{Ay zoL>frS?KT0;LQY49RLvt^k#(V4}$17ZVf67hUyQ=@T@Hi^=5)83CjSBglm9o^{!{~ zW-J98ACci(+7?t8nW3NIURxNY0n!!?QUkFo24XTOremQd$AOf@L(E}>8Iu4u9&BPF z#Cb^w2PcCi3sVpdu1|%SYt$N4m<BaB9b|3>!o`^oU0D#D;115tNC!DMCsiYZF~2ak zwlGg4l(8~{MI(b%BZDJ@6O??JKp7{$wxqBisIV}$ut)=8TQS(SoS?!Ih!`l$O2J}5 zg=MMUOpwe`9?IyOp`?+aoS~8d&JaxAOr06voKO)|SlL-ySd{_I1Rw*dp$61|GXV<| PBQrB3{nwTzwd(-@%*8^w delta 1031 zcmaFI|D1mVCnHk^=VT#9Whb6cM$^`yLf%luiq=9tzfhLk{FJ;x{!pgs+6>+do=`@^ z)}TUxj*>#b+LA({ph979=1#woLXp~%LeZc?vDiZK$>ogNo)RFX#;rkxk{|}iM5zqn z_Mk%P46qEy5Sa{!kSs(9q*x9rB%kWd?9JLy5>%)#`8A^wqvB*fCVNJu$$m@*jLMVi znbbp6G%|#}nZ21iLs`>uGV@9bRZD8y3)O-O)jMkoHA0zFQcDUo5e92P4c0F9X6y*% zWMpAsVqjuqWNP>3DhVpoDNTwk)YX_A$m}t>f|-}8P;YV|bCiUBhHxlTg+ZY~C{u-D zp<yUfg;AkVd!g~<KxXmDx0uE1O*9(|O=}CyG(vgfi!<}mOLG#7;)_d)3e7{AQVI$! zLOJ5gQ!~>uN{S0D{X*H}a}!I7GAjzL{6bmd^AdAY3#~&rQ&Q85Qp<8uD++Bw`QmdE zlM_pG5-W|s#@L3ky9cF~`J`49+J%ZBtjNtTN=?eiw@S_~F0}V%2xX7YNzF?y$tZO2 zW)9_w&rM9vO#~a@7|N*xHD0OEDU>}vJ|#anKEBX-^GX&8Msb%=_V}cne2_7&nx#qY zL4|I$h3=qm5TE>>Rjl5lw$Rg?!JDzzo4GBh&<hfrCapn*-q7Im$?&KJ%lT%2n1z1c z4Bku-)&3BX0B=T^{y>O+P&@}g^#^Bo)E0(#Gr^REW`IS)G(fg`*E4xDmV%8B&+sX2 z3o4Ar(93YEEsWFvX^R4>f!GxdF&UJoVxT6+f|SHT%wdEX6Av~XY+?e$d5H)ICxImk zlfB^%u1|rOYt$N4m<lyF4P<UQ!o?X7U6~M@;115pNCP=IJ5?itF~2aUwlG&Cl(8~{ zMI(b%BZDJ@6O?+HK=~%GwxlpWsIVZmuuubHn?Y+(VG-E2oS?#Dgjfk!EU2(F)td>D zH_Ad8eKQm_GL$luGr;+Q$(yM&1Dp}cg9<A;YYQtgpm_ikG*wUos=;}Hg^_`Q5t96C IN|W040Qc5EhyVZp diff --git a/keys/bsp_uov2.priv b/keys/bsp_uov2.priv deleted file mode 100644 index 9888565e6ce5c97005eb2b57c3a08dfa7b77ac05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2030 zcmZo*(nwCuNh~f-E!N9O%_&GND%NvMOUukl4Jk^@D^ANV%1tcE%+KS(Dp<&vU&vHj z$gB~{Seaa$n4YSao1apelUl4-mYQ6WUlgC4pOP1!lA2eX%2k?IkeQsFlNw)U$W_Rq z0a0FDQdF8;Qd*R%mzD_9P|1~^nwMIXnH&%0$HTO76|&;glarsHU0M*Io|B)Hn8Q`b z7Ru#Tl$z=bc1j_8YfvGFH@i2hH)DGtr$z>=Mh06bw_9djW=X1BW@=7KAy;cqAva6~ z57d^T%)Insz0ACl)b!LMuH3}z)OZ-DkT;akv^A)ZFO;#OwUFO0lm+C?LV-}G>e>vx z4Bk*i!`7fe!H$wbq1uu{;h;hhZ{|+Fl0wnil0vbdLh;x_2^34xz;200u@Mw<T)z1! zT!oS#0}Wb(3Z*~{kiF6wBJDwiG8u3g*$jw~97M>ZHK<S?Dx{F=&FszEQ4&<B7|Q4i z3H02=lA_ECJqTq0j~K2(C8)C?GEfB|mq6n%J}oCdv4qPPtRGXnP#I!AB2r89G7C#n z<BL)Yic*VH^GYB|sWh)BHKjB;m8(z%q5;*u;)2BFRH!AvV0NLZMuvztvo~{RC~I0y zW?o65T1jntp?XlEMrUoIW+-z?YDu9M#Pgt_*M@ptr`VgZBb1Yck&%UkftiV+-J7c< zs8F{wDYj4#>QzXf7v$$u=H=&RCg$knmgbaX#v=scb25{PGxO3*a}tZ-K^5;C0M~({ zpim#;Bg%~p%FIhgHQFFUB$TPbpwKXsslu?(D3qzfsL;5*&;*B#C8<TZ@%cq5sYP5N zsYSW|U{;~2W@DjQZK1hFC~rK>Tk*vuMTHijOeqD0mZ2Q+<*AwJ870MqR(_%E@sOw~ zwDt>Sjn7NWO)azu<xELUFG?-TNv$Zf4dsi^O-xQK%}J~@0vlr&%I+SNTIQ2lQD`43 zg0LbtzbG{+CqFs6xX{6yA(TBnCp9m<B%{#Ln>my#J~uHvHxaDgDU?$QYPwRPb0~X! zd`f<De0-rxh!7(K1D6ZP>QsfI)Uwo~;#7s4)QZgH{Pd#4f{e^$1r4~vG!;O;R{;B7 zkBiGEwL~EyCM7jpqarR*Q^8gtG2T!?TOl#tNI_d6CMCb57|hE{&`~JKNX_F)sEA8c zuu-UpOG;44OjAhAOHs(r%c%shk&LlXfEt;knV^tgglYqlRAOEVS4l={QK~{}MP_kH zu|k5OLQGDoLME8ZOHe2-P0mm#$w(|wNJxy&Q?OM?iqF%5xJySNF}^xe+prca3TA<X zG8OC;lHxNH^tcLLL)qh@0qdq&n$#Xt=w4gsfh%=`(sfE^atWkBa}6mi$VufY^sFuP z@@DX6EcRw@3o7)6WF$~2;seb{z8RjiU^%}G5VO$Vo57n2qB;N~66nnc(;o!UZ`>MG z7!1`PlHplf80yUgQxcW|775n?*$NUa1)Cm`;al1kR2Z3|pW$9x7^MNy6%A4Xu_^{) zFerz|LJf`sDT#*|!|2TjF(v_QI@rWSi0hIN?o9?u7N#KFn+h@3s5PiC4QgyU$k+^! z!yq<iLNsMTEMkILl%0_da&JzmMh0VkVQy_<9;hPBU;$S$92uOTQi};x^5@r<6cz*( z7RDA9X&`JXhT2vF5dsBRDO9K|)td=YY?p^J`erC;WGH8-WPpo7CU2(B3~>2Z5mZ>& hSzB0@0WJDK22?{0r~wy!EKH2d%#c#Awlt|-4*<zFuDbvL diff --git a/keys/bsp_uov_old.priv b/keys/bsp_uov_old.priv new file mode 100644 index 0000000000000000000000000000000000000000..b58b64b7dff26ab5dfef0d56e179a060c63bb4f9 GIT binary patch literal 2023 zcmZo*(nwCuNh~f-E!N9O%_&GND%NvMOUukl4Jk^@D^ANV%1tcE%+KS(Dp<&vU&vHj z$gB~{Seaa$n4YSao1apelUl4-mYQ6WUlgC4pOP1!lA2eX%2k?IkeQsFlNw)U$W_Rq z0a0FDQdF8;Qd*R%mzD_9P|1~^nwMIXnH&%0$HTO76|&;glarsHU0M*Io|B)Hn8Q`b z7Ru#Tl$z=bc1j_8YfvGFH@i2hH)DGtr$z>=Mh06bw_9djW=X1BW@=7KAy;cqA$JBR z)cB&zy!2wd%)FA+^wc7*+{EnEco?UUCzR2&HK>p`l(C|<kk2oa1?0a%{!pgs+6>+d zo=`@^)}TUxj*>#b+LA({ph979=1#woLXp~%LeZc?vDiX!6id>;j)+IG5fo%xzWFI! zg%ThGja!2XB|!|3y;2#%?Lmdo8DJTZRWcb6Az6qJNU<DLNIun@*_*YaB&bj!l+hOw z+PR4(MVS?P5Xt}^BV2`wP-j78pb9`Pfks|@T26jq370QeKc;x062yE&Y?kI_7M7;Q z7o`>yr52~=l|a%^X<kukN@;Q`SD`XQ1FC(+1&PV2P)mZr>_Qcd3}J6(Z|2TW*0h|= zyplrIlG^q{wV*=v&e}qaQ0A1>l0r>{=e3}o*Dm&E><Hy#WMN@qU}9usYWL<U2`bbn zO^Pklg?bed=mq&Xm3jHOnTa`ixurQJnehmL_?*n7;>^7C(wxL1cu>Xr2EcWoC@9o} z_=s|2gEI5dQH|Em5DsOkFeo$#WvVbNGz?{`Fe)@^FEqwsV@YaJZhU@GN@@{TNNQ28 zKbTc$qS;tzT3cwQ5y~46^HzLuNl~GBC{s#7p+zW1e0geSdPYfcp`~9adpsm+3a$J? zS>y8(b5jegLpf7Y(~DBea#AY_Z9@6ta}$#jOLGz{jljm(hO)Z{rIz`mRutNWiXg1W z%`Zw#%E?d8E-tk9W(Z}E&q>WoFUcr$@MaF>iqB0<&rJmDcMRoJf|{;W=oHExAD@z+ z93Nlk93sTXz`*4KvN~0vD77rLs5n(2C$%CoIX}H9u^=NeSwREtFii!J?-jtl*W=>y zNi9)Gh)GFJ*Qkg~)KsulNQ^gB&{jx{H&W16h)KyWDF*ZM5_A+wGE(!n5-Q>n6>JnL z;*t^+GSd_i^HLP@^KvRdY$RiB6re^XX(lM-7opmKB$b$#!c~%yT9m4gT9H{?Qml|* zs1TErs*njL^AZ$_OOrDcN-`2l6cQ5S^Av0qlH&7pAnwvpNQ|$})HbXIi-K7op-crk zg{1h*1U;@omr(Y2Xu!H^mL|0a6}r_Hy5maSpmd#*nOp)X&|E`G3vyDq3O#BIJ-r#c z8H>G{+ky(cAQ{P|HK@=Vnvr}mJZiymz8N59p`SN{Hxop)KSU(Jn-Qiz5TYMc3I##+ z2WNQH7KV5;!IXq%fJMSIK(>N}OTngxXZV!11r<hQ=w-Oo7Dj4-bVY%bK&*;}7z`@$ zVxR`cf|SHTjA8U<gcuVKHXUqY0>pKR2=^v|B@2_i;qFa=7;DrTRG11iHVtHKI>=!V zn=>GqG9eZ*!7R$kNCUYyJ5?itF~2aUwlEh|17@&*YZs0TPEe`E1S<LSYD)_9g9-~` z3kx+Mwi&br6&68lD@F*FK!r+Ey_q1zc3CK+Z-$~qhEj%d2Dlhx@@DGH0GEH|L4_5a hwS|=#(4r3%FjY_ks=-Ac3nK#qBc#-;DNSnE0|4RgtZ)DT literal 0 HcmV?d00001 -- GitLab