paint-brush
Anahtar Yönetim Sistemi: Bilmeniz Gereken Her Şeyile@vivalaakam
270 okumalar

Anahtar Yönetim Sistemi: Bilmeniz Gereken Her Şey

ile Andrey Makarov25m2024/07/16
Read on Terminal Reader

Çok uzun; Okumak

Hizmet 256 bitlik bir özel anahtarı saklayacak (çoğu blockchain ağında kullanılır) EVM ağları için işlemleri ve mesajları imzalayacaktır (diğer ağlar için destek daha sonra yapılabilir) Bir kullanıcının özel anahtarı hizmetimizden ayrılmamalıdır. Paylaştığımız anahtarların benzersiz olması gerekir. Her anahtarın bir aktivite günlüğüne sahip olmalıyız.
featured image - Anahtar Yönetim Sistemi: Bilmeniz Gereken Her Şey
Andrey Makarov HackerNoon profile picture

Kısa bir süre önce, bir muggle işinde, çeşitli hizmetler için özel anahtarların saklanması konusunda bir sorumuz vardı. Bu süreçte farklı nedenlerden dolayı pek uygun olmayan birkaç çözüm bulduk. Bir ara buna geri dönmeye karar verdim.

Hizmetin Gereksinimleri

Kendim için bu hizmetin çalışması için birkaç gereksinimi vurguladım:

  • Şimdilik hizmetin 256 bitlik özel anahtarı (çoğu blockchain ağında kullanılan) saklaması gerekiyor.
  • EVM ağları için işlem ve mesajları imzalamalıdır (diğer ağlar için destek daha sonra yapılabilir).
  • Bir kullanıcı sınırsız sayıda anahtara sahip olabilir.
  • Bir kullanıcının özel anahtarı hizmetimizden ayrılmamalıdır.
  • Her anahtar sınırsız sayıda kişiyle paylaşılabilir.
  • Paylaştığımız anahtarların benzersiz olması gerekir.
  • Her anahtarın bir aktivite günlüğüne sahip olmalıyız.
  • sk kullanan hizmetin anahtarı ele geçirilmişse, onu iptal edebilmemiz gerekir.


Zaten çalışma sürecinde bu görevin nasıl çözülmesi gerektiğini anladığımda bir gereksinimin daha altını çizdim:

  • Her anahtarın kapsamını sınırlayabilmemiz gerekir. Ancak çalışmalar devam ettiği için bunu bir sonraki yazıya bıraktım. Bu nedenle kendimizi bu yazıdaki mesajı imzalamakla sınırlayacağız.

Sorunu Çözme Seçenekleri

Anahtar yazı içerisinde birçok kez kullanılacağı için karışıklığı önlemek amacıyla özel anahtara pk ve paylaşılan anahtara sk diyeceğim.


İlk seçenek pbkdf2 fonksiyonunda anahtarı şifrelemekti ancak bu algoritmanın işleyişinde elimizde tek bir anahtar olduğundan imzaya erişim için anahtarın nasıl paylaşılacağı konusunda bir sorun oluştu.


Sorunu çözmek için iki seçenek buldum:

  1. Veritabanında saklanan şifrelenmiş anahtarın bir ana anahtarına sahibiz ve orijinal anahtara yönlendiren oluşturulan anahtarı zaten paylaştık. Bu seçeneği beğendiğimi söyleyemem çünkü veritabanına erişim sağlarsanız pk anahtarının şifresini çözmek kolaydır.


  2. Kontrol ettiğimiz her anahtar için pk anahtarımızın ayrı bir örneğini oluşturuyoruz. Bu seçeneği de pek beğendiğimi söyleyemem.


Böylece etrafta dolaşıp sk anahtarını nasıl kullanışlı hale getireceğimi düşünürken, Shamir Secrets Sharing'i (SSS) kullanırken sk anahtarını benzersiz hale getirebileceğinizi ve anahtarın yalnızca bir kısmını paylaşabileceğinizi hatırladım. Geri kalanı güvenlik içinde arka uçta saklanacak ve bu parçaları istediğiniz herkese verebilirsiniz.


Şöyle görünecektir: pk anahtarını SSS tarafından oluşturulan anahtarımızla şifreleriz, anahtarın bir kısmını farklı depolarda saklarız ve anahtarın bir kısmını kullanıcıya sk olarak veririz. Yaklaşık 10-15 dakika sonra basit bir şeyi fark ettim:


SSS kullanırken pk anahtarımızı başka bir şeyle şifrelememize gerek yok çünkü SSS bunu biraz halledebilir ve bu çözüm bence PK anahtarlarını depolamak için mükemmel. Kullanıcınınki de dahil olmak üzere farklı depolama seçenekleri kullanılarak her zaman parçalara ayrılır. İptal edilmesi gerekiyorsa sk anahtarımızın indeks bilgilerini siler ve hızlı bir şekilde yenisini oluştururuz.


Bu yazımda SSS ilkeleri üzerinde durmayacağım; Bu konuyla ilgili daha önce kısa bir makale yazmıştım ve bu makaledeki birçok prensip yeni hizmetimizin temelini oluşturacaktır.

Mimari

Hizmetimizin prensibi şu şekilde olacaktır:

  1. Kullanıcı bir anahtar oluşturmayı seçer.


  2. Hizmete uygun bir anahtar oluşturuyoruz. Bu bizim pk anahtarımız olacak. Hizmetin tamamını asla terk etmez.


  3. SSS'yi kullanarak anahtarımızı, pk anahtarını kurtarmak için bölünmüş anahtarın üç parçasına ihtiyaç duyulacak şekilde böldük. Her bölünmüş anahtar iki bölümden oluşur: x: anahtarımızın konumu y: bu konumun değeri


  4. İlk kısmı Vault'a atıyoruz (API aracılığıyla erişilebilen hassas bilgilerin depolanmasına yönelik herhangi bir hizmet olabilir).


  5. İkinci kısmı veritabanına kaydediyoruz (PostgreSQL kullanacağım).


  6. Üçüncü kısmı kısmen veritabanına kaydediyoruz, diğer kısmı da kullanıcıya ( sk ) veriyoruz. İhtiyacımız olan değeri bulmak amacıyla SK'yı kullanmak için ayrıca keccak256(sk) dosyasını da veritabanına kaydediyoruz. Bildiğim kadarıyla henüz kırılmadı.


  7. Kullanıcının bir şey imzalaması gerektiğinde uygulamanın farklı yerlerinden özel anahtarı toplayıp imzalıyoruz.


Bu yaklaşımın bir dezavantajı vardır; sk anahtarı yöneticisi kendisi tarafından oluşturulan tüm sk anahtarlarını kaybederse orijinal anahtarı geri yükleyemeyiz. Bir seçenek olarak orijinal anahtarın yedeğini alabilirsiniz, ancak bu başka bir zaman için =).

Veri tabanı

Çalışmam sonucunda şu veritabanı yapısına sahibim:

  • kullanıcılar, kullanıcı yani anahtar yönetici hakkındaki bilgileri saklar.


  • anahtarlar, Paylaşımımızın ikinci kısmı, Kasadaki Paylaşımın ilk kısmını bulabileceğiniz dizin gibi anahtarla ilgili temel bilgileri ve özel anahtarımızın adresi gibi diğer bilgileri saklar.


  • hisseler, Hissenin bir kısmını içerir ve aynı zamanda bu Hissenin karma değerini de saklar. Bu, onu veritabanında bulabilmemiz için yapılır.


  • Anahtar oluşturma ve tüm imzalar gibi anahtarla yapılan tüm etkinliklerin günlüğe kaydedilmesi buraya düşer.

Gerçekleşme

Bu hizmet için Actix-web framework'ü ile Rust programlama dilini kullandım. Bunları işte her zaman kullanıyorum, neden olmasın?


Dediğim gibi veritabanı aşağıdaki sebeplerden dolayı Postgresql olacaktır.

Polinom

 lazy_static! { static ref PRIME: BigUint = BigUint::from_str_radix( "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16 ) .expect("N parse error"); } #[derive(Clone, Debug)] pub struct Share { pub x: BigUint, pub y: BigUint, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ShareStore { pub x: String, pub y: String, } impl From<Share> for ShareStore { fn from(share: Share) -> Self { ShareStore { x: hex::encode(share.x.to_bytes_be()), y: hex::encode(share.y.to_bytes_be()), } } } impl From<&Share> for ShareStore { fn from(share: &Share) -> Self { ShareStore { x: hex::encode(share.x.to_bytes_be()), y: hex::encode(share.y.to_bytes_be()), } } } pub struct Polynomial { prime: BigUint, } impl Polynomial { pub(crate) fn new() -> Self { Polynomial { prime: PRIME.clone(), } } // Calculates the modular multiplicative inverse of `a` modulo `m` using Fermat's Little Theorem. fn mod_inverse(&self, a: &BigUint, m: &BigUint) -> BigUint { a.modpow(&(m - 2u32), m) } // Generates a random polynomial of a given degree with the secret as the constant term. fn random_polynomial(&self, degree: usize, secret: &BigUint) -> Vec<BigUint> { let mut coefficients = vec![secret.clone()]; for _ in 0..degree { let index = BigUint::from_bytes_be(generate_random().as_slice()); coefficients.push(index); } coefficients } // Evaluates a polynomial at a given point `x`, using Horner's method for efficient computation under a prime modulus. fn evaluate_polynomial(&self, coefficients: &[BigUint], x: &BigUint) -> BigUint { let mut result = BigUint::zero(); let mut power = BigUint::one(); for coeff in coefficients { result = (&result + (coeff * &power) % &self.prime) % &self.prime; power = (&power * x) % &self.prime; } result } // Generates `num_shares` shares from a secret, using a polynomial of degree `threshold - 1`. pub fn generate_shares( &self, secret: &BigUint, num_shares: usize, threshold: usize, ) -> Vec<Share> { let coefficients = self.random_polynomial(threshold - 1, secret); let mut shares = vec![]; for _x in 1..=num_shares { let x = BigUint::from_bytes_be(generate_random().as_slice()); let y = self.evaluate_polynomial(&coefficients, &x); shares.push(Share { x, y }); } shares } // Reconstructs the secret from a subset of shares using Lagrange interpolation in a finite field. pub fn reconstruct_secret(&self, shares: &Vec<Share>) -> BigUint { let mut secret = BigUint::zero(); for share_i in shares { let mut numerator = BigUint::one(); let mut denominator = BigUint::one(); for share_j in shares { if share_i.x != share_j.x { numerator = (&numerator * &share_j.x) % &self.prime; let diff = if share_j.x > share_i.x { &share_j.x - &share_i.x } else { &self.prime - (&share_i.x - &share_j.x) }; denominator = (&denominator * &diff) % &self.prime; } } let lagrange = (&share_i.y * &numerator * self.mod_inverse(&denominator, &self.prime)) % &self.prime; secret = (&secret + &lagrange) % &self.prime; } secret } // Adds a new share to the existing set of shares using Lagrange interpolation in a finite field. pub fn add_share(&self, shares: &Vec<Share>) -> Share { let new_index = BigUint::from_bytes_be(generate_random().as_slice()); let mut result = BigUint::zero(); for share_i in shares { let mut lambda = BigUint::one(); for share_j in shares { if share_i.x != share_j.x { let numerator = if new_index.clone() >= share_j.x { (new_index.clone() - &share_j.x) % &self.prime } else { (&self.prime - (&share_j.x - new_index.clone()) % &self.prime) % &self.prime }; let denominator = if share_i.x >= share_j.x { (&share_i.x - &share_j.x) % &self.prime } else { (&self.prime - (&share_j.x - &share_i.x) % &self.prime) % &self.prime }; lambda = (&lambda * &numerator * self.mod_inverse(&denominator, &self.prime)) % &self.prime; } } result = (&result + &share_i.y * &lambda) % &self.prime; } Share { x: new_index, y: result, } } }

Burada küçük bir itirafta bulunacağım: Ben matematikçi değilim. Bununla ilgili bulabildiğim kadar çok bilgi bulmaya çalışsam da aslında bu önceki makalemden uyarlanmış koddur.


Bu özellik hakkında daha fazla bilgiyi burada bulabilirsiniz https://en.wikipedia.org/wiki/Lagrange_polynomial


Bu yapı (veya sınıf, hangisi daha uygunsa) bugün anlattığımız sürecin en önemli kısmını, yani pk anahtarını parçalara ayırıp yeniden birleştirmeyi gerçekleştirir.

Kullanıcı oluştur

 #[derive(Serialize, Deserialize)] pub struct CreateUserResponse { pub secret: String, } pub async fn users_create_handler(app_data: web::Data<AppData>) -> HttpResponse { let code = generate_code(); match create_user( CreateOrUpdateUser { secret: code.clone(), }, app_data.get_db_connection(), ) .await { Ok(_) => HttpResponse::Ok().json(CreateUserResponse { secret: code }), Err(e) => { return HttpResponse::InternalServerError().body(format!("Error creating user: {}", e)); } } }

Burada her şey olabildiğince basit; anahtarlarıyla çalışacak ana anahtarı olan bir kullanıcı yaratıyoruz. Bu, başka herhangi bir tarafın anahtarlarımızla herhangi bir şey yapmasını önlemek için yapılır. İdeal olarak bu anahtarın hiçbir şekilde dağıtılmaması gerekir.

Anahtar Oluştur

 pub async fn keys_generate_handler(req: HttpRequest, app_data: web::Data<AppData>) -> HttpResponse { // Check if the request has a master key header let Some(Ok(master_key)) = req.headers().get(MASTER_KEY).map(|header| header.to_str()) else { return HttpResponse::Unauthorized().finish(); }; // Check if user with master key exist let user = match get_user_by_secret(&master_key, app_data.get_db_connection()).await { Ok(user) => user, Err(UserErrors::NotFound(_)) => { return HttpResponse::Unauthorized().finish(); } Err(e) => { return HttpResponse::InternalServerError().body(format!("Error getting user: {}", e)); } }; // generate random `pk` private key let private_key = generate_random(); let Ok(signer) = PrivateKeySigner::from_slice(private_key.as_slice()) else { return HttpResponse::InternalServerError().finish(); }; let secret = BigUint::from_bytes_be(private_key.as_slice()); let poly = Polynomial::new(); // divide `pk` key into 3 shares let shares = poly .generate_shares(&secret, 3, 3) .iter() .map(Into::into) .collect::<Vec<ShareStore>>(); // store first part at Vault let path = generate_code(); if let Err(err) = kv2::set( app_data.get_vault_client().as_ref(), "secret", &path, &shares[0], ) .await { return HttpResponse::InternalServerError().body(format!("Error setting secret: {}", err)); } // Store second part at database and path to first share let key = CreateOrUpdateKey { user_id: user.id, local_key: shares[1].y.clone(), local_index: shares[1].y.clone(), cloud_key: path, address: signer.address(), }; let key = match create_key(key, app_data.get_db_connection()).await { Ok(key) => key, Err(err) => { return HttpResponse::InternalServerError() .body(format!("Error creating key: {}", err)); } }; // Store third part at database as share let share = match create_share( CreateOrUpdateShare { secret: shares[2].y.clone(), key_id: key.id, user_index: shares[2].x.clone(), owner: SharesOwner::Admin, }, app_data.get_db_connection(), ) .await { Ok(share) => share, Err(err) => { return HttpResponse::InternalServerError() .body(format!("Error creating share: {}", err)); } }; let Ok(user_key) = hex::decode(&shares[2].y) else { return HttpResponse::InternalServerError().finish(); }; // Store log let _ = create_log( CreateLog { key_id: key.id, action: "generate_key".to_string(), data: serde_json::json!({ "user_id": user.id }), message: None, }, app_data.get_db_connection(), ) .await; // Return the key and share identifier HttpResponse::Ok().json(KeysGenerateResponse { key: STANDARD.encode(user_key), id: share.id, }) }

Kullanıcıya böyle bir kullanıcının var olup olmadığını kontrol edin, bir pk anahtarı oluşturun, onu parçalara ayırın ve farklı yerlere kaydedin.

Erişim Ver

 pub async fn keys_grant_handler(req: HttpRequest, app_data: web::Data<AppData>) -> HttpResponse { // Check if the request has a master key header let Some(Ok(master_key)) = req.headers().get(MASTER_KEY).map(|header| header.to_str()) else { return HttpResponse::Unauthorized().finish(); }; // Check if a user with the master key exists let user = match get_user_by_secret(&master_key, app_data.get_db_connection()).await { Ok(user) => user, Err(UserErrors::NotFound(_)) => { return HttpResponse::Unauthorized().finish(); } Err(e) => { return HttpResponse::InternalServerError().body(format!("Error getting user: {}", e)); } }; // Check if the request has a secret key header let Some(Ok(secret_key)) = req.headers().get(SECRET_KEY).map(|header| header.to_str()) else { return HttpResponse::Unauthorized().finish(); }; let Ok(share) = STANDARD.decode(secret_key) else { return HttpResponse::Unauthorized().finish(); }; // Check if the share exists let share_value = hex::encode(share); let share = match get_share_by_secret(&share_value, app_data.get_db_connection()).await { Ok(share) => share, Err(ShareErrors::NotFound(_)) => return HttpResponse::NotFound().finish(), Err(_) => { return HttpResponse::Unauthorized().finish(); } }; if !matches!(share.status, SharesStatus::Granted) { return HttpResponse::Unauthorized().finish(); } // Get original key with necessary information let key = match get_key_by_id(&share.key_id, app_data.get_db_connection()).await { Ok(key) => key, Err(KeyErrors::NotFound(_)) => return HttpResponse::NotFound().finish(), Err(_) => { return HttpResponse::Unauthorized().finish(); } }; // Check if the key belongs to the user if key.user_id != user.id { return HttpResponse::Unauthorized().finish(); } // Get the first part of the key from Vault let Ok(cloud_secret) = kv2::read::<ShareStore>( app_data.get_vault_client().as_ref(), "secret", &key.cloud_key, ) .await else { return HttpResponse::InternalServerError().finish(); }; // Combine the shares let shares = vec![ Share { x: BigUint::from_str_radix(&cloud_secret.x, 16).expect("Error parsing local index"), y: BigUint::from_str_radix(&cloud_secret.y, 16).expect("Error parsing local key"), }, Share { x: BigUint::from_str_radix(&key.local_index, 16).expect("Error parsing local index"), y: BigUint::from_str_radix(&key.local_key, 16).expect("Error parsing local key"), }, Share { x: BigUint::from_str_radix(&share.user_index, 16).expect("Error parsing user index"), y: BigUint::from_str_radix(&share_value, 16).expect("Error parsing user key"), }, ]; let sss = Polynomial::new(); // Create a new share let new_share = ShareStore::from(sss.add_share(&shares)); // Store new share into database let share = match create_share( CreateOrUpdateShare { secret: new_share.y.to_string(), key_id: key.id, user_index: new_share.x.to_string(), owner: SharesOwner::Guest, }, app_data.get_db_connection(), ) .await { Ok(share) => share, Err(err) => { return HttpResponse::InternalServerError() .body(format!("Error creating share: {}", err)); } }; let Ok(user_key) = hex::decode(&new_share.y).map(|k| STANDARD.encode(k)) else { return HttpResponse::InternalServerError().finish(); }; // Store log let _ = create_log( CreateLog { key_id: key.id, action: "grant".to_string(), data: serde_json::json!({ "user_id": user.id, "share_id": share.id, }), message: None, }, app_data.get_db_connection(), ) .await; // Return the key and share the identifier HttpResponse::Ok().json(KeysGenerateResponse { key: user_key, id: share.id, }) }

Bu fonksiyonun çalışma mekanizması aşağıdaki gibidir:


Erişim isteğinde bulunan kişinin Paylaşımın tüm haklarına sahip olduğunu doğrularız.


Burada gizli anahtara çok basit bir nedenden dolayı ihtiyacımız var, o olmadan orijinal pk anahtarını kurtaramayız. Ek bir Paylaşım oluşturun ve bunu kullanıcıya verin.

Erişimi İptal Et

 pub async fn keys_revoke_handler( req: HttpRequest, app_data: web::Data<AppData>, body: web::Json<KeysRevokeRequest>, ) -> HttpResponse { let Some(Ok(master_key)) = req.headers().get(MASTER_KEY).map(|header| header.to_str()) else { return HttpResponse::Unauthorized().finish(); }; let user = match get_user_by_secret(&master_key, app_data.get_db_connection()).await { Ok(user) => user, Err(UserErrors::NotFound(_)) => { return HttpResponse::Unauthorized().finish(); } Err(e) => { return HttpResponse::InternalServerError().body(format!("Error getting user: {}", e)); } }; let share = match get_share_by_id(&body.id, app_data.get_db_connection()).await { Ok(share) => share, Err(ShareErrors::NotFound(_)) => return HttpResponse::NotFound().finish(), Err(_) => { return HttpResponse::Unauthorized().finish(); } }; let key = match get_key_by_id(&share.key_id, app_data.get_db_connection()).await { Ok(key) => key, Err(KeyErrors::NotFound(_)) => return HttpResponse::NotFound().finish(), Err(_) => { return HttpResponse::Unauthorized().finish(); } }; if key.user_id != user.id { return HttpResponse::Unauthorized().finish(); } if revoke_share_by_id(&share.id, app_data.get_db_connection()) .await .is_err() { return HttpResponse::InternalServerError().finish(); } let _ = create_log( CreateLog { key_id: key.id, action: "revoke".to_string(), data: serde_json::json!({ "user_id": user.id, "share_id": share.id, }), message: None, }, app_data.get_db_connection(), ) .await; HttpResponse::Ok().finish() }

Burada yalnızca erişimini iptal ettiğimiz Paylaşımın tanımlayıcısını bilmemiz gerekiyor. Gelecekte bir web arayüzü yaparsam onunla çalışmak daha kolay olacak. Burada sk anahtarımıza ihtiyacımız yok çünkü burada özel anahtarı geri yüklemiyoruz.

Mesajı İmzala

 #[derive(Deserialize, Serialize, Debug)] pub struct SignMessageRequest { pub message: String, } #[derive(Deserialize, Serialize, Debug)] pub struct SignMessageResponse { pub signature: String, } pub async fn sign_message_handler( app_data: web::Data<AppData>, req: HttpRequest, body: web::Json<SignMessageRequest>, ) -> HttpResponse { // Get the `sk` key from the request headers let Some(Ok(secret_key)) = req.headers().get(SECRET_KEY).map(|header| header.to_str()) else { return HttpResponse::Unauthorized().finish(); }; // restore shares let (shares, key_id, share_id) = match restore_shares(secret_key, &app_data).await { Ok(shares) => shares, Err(e) => { return HttpResponse::BadRequest().json(json!({"error": e.to_string()})); } }; let sss = Polynomial::new(); // restore `pk` key let private_key = sss.reconstruct_secret(&shares); //sign message let Ok(signer) = PrivateKeySigner::from_slice(private_key.to_bytes_be().as_slice()) else { return HttpResponse::InternalServerError().finish(); }; let Ok(signature) = signer.sign_message(body.message.as_bytes()).await else { return HttpResponse::InternalServerError().finish(); }; // create log let _ = create_log( CreateLog { key_id, action: "sign_message".to_string(), data: json!({ "share_id": share_id, }), message: Some(body.message.clone()), }, app_data.get_db_connection(), ) .await; // return signature HttpResponse::Ok().json(SignMessageResponse { signature: hex::encode(signature.as_bytes()), }) }

Mesajı aldım, eğer her şey yolundaysa, özel anahtarı kurtardım ve mesajı bununla imzaladım.


Temel olarak uygulamamızın ana yöntemleri anlatılmıştır; Acımaya ve kodun tamamını buraya koymamaya karar verdim. Bunun için GitHub var ve tüm kodlar orada mevcut olacak =)

Çözüm

Bu hala başvurunun bir taslağı olsa da, bunun sadece bir konsept olmadığını unutmamak önemlidir. umut vaat eden uygulanabilir bir taslak. Nasıl çalıştığını anlamak için depoda entegrasyon testleri de bulunmaktadır. İlerleyen bölümlerde işlemlerin imzasını ekleyip kullanım kapsamlarının sınırlandırılmasını mümkün kılmayı planlıyorum. Daha sonra bir web arayüzü hazırlayabilir ve bu projeyi ortalama bir insan için uygun hale getirebilirim.


Bu gelişmenin oldukça fazla kullanım potansiyeli var ve umarım en azından bir kısmını ortaya çıkarabilirim; Bu kod sayfaları ve seyrek yorumlar için özür dilerim. Yukarıda yazdıklarımı anlatmak yerine kod yazmayı tercih ediyorum. Daha iyi olmaya çalışacağım ama bu artık benden daha güçlü.


Ayrıca istenirse kod ve PR hakkındaki yorumlarınızı da bekliyoruz =)


Herkese en iyi dileklerimle ve özel anahtarlarınızı güvende tutun.