use std::fmt;
use rand_core::{CryptoRngCore, OsRng, RngCore};
use rsa::pkcs8::DecodePublicKey;
use rsa::traits::PublicKeyParts;
use rsa::{Pkcs1v15Encrypt, RsaPublicKey};
use sha1::{Digest, Sha1};
use thiserror::Error;
pub const SHARED_SECRET_LEN: usize = 16;
pub const MAX_KEY_BITS: usize = 4096;
#[derive(Clone, PartialEq, Eq)]
pub struct SharedSecret([u8; SHARED_SECRET_LEN]);
impl SharedSecret {
pub fn generate() -> Self {
Self::generate_with(&mut OsRng)
}
pub fn generate_with(rng: &mut impl CryptoRngCore) -> Self {
let mut bytes = [0u8; SHARED_SECRET_LEN];
rng.fill_bytes(&mut bytes);
Self(bytes)
}
pub fn from_bytes(bytes: [u8; SHARED_SECRET_LEN]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; SHARED_SECRET_LEN] {
&self.0
}
}
impl fmt::Debug for SharedSecret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("SharedSecret([redacted])")
}
}
#[derive(Debug, Error)]
pub enum CryptoError {
#[error("invalid server public key: {0}")]
InvalidPublicKey(String),
#[error("server public key has {0} bits, the maximum is {MAX_KEY_BITS}")]
KeyTooLarge(usize),
#[error("RSA encryption failed: {0}")]
Encrypt(#[source] rsa::Error),
}
#[derive(Clone, PartialEq, Eq)]
pub struct ServerPublicKey {
key: RsaPublicKey,
der: Vec<u8>,
}
impl ServerPublicKey {
pub fn from_der(der: &[u8]) -> Result<Self, CryptoError> {
let key = RsaPublicKey::from_public_key_der(der)
.map_err(|e| CryptoError::InvalidPublicKey(e.to_string()))?;
let bits = key.size() * 8;
if bits > MAX_KEY_BITS {
return Err(CryptoError::KeyTooLarge(bits));
}
Ok(Self {
key,
der: der.to_vec(),
})
}
pub fn der(&self) -> &[u8] {
&self.der
}
pub fn bits(&self) -> usize {
self.key.size() * 8
}
pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>, CryptoError> {
self.encrypt_with(&mut OsRng, data)
}
pub fn encrypt_with(
&self,
rng: &mut impl CryptoRngCore,
data: &[u8],
) -> Result<Vec<u8>, CryptoError> {
self.key
.encrypt(rng, Pkcs1v15Encrypt, data)
.map_err(CryptoError::Encrypt)
}
}
impl fmt::Debug for ServerPublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ServerPublicKey")
.field("bits", &self.bits())
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub struct EncryptionResponse {
pub shared_secret: SharedSecret,
pub encrypted_shared_secret: Vec<u8>,
pub encrypted_verify_token: Vec<u8>,
pub server_hash: String,
}
pub fn respond_to_encryption_request(
server_id: &str,
public_key_der: &[u8],
verify_token: &[u8],
) -> Result<EncryptionResponse, CryptoError> {
respond_with_rng(&mut OsRng, server_id, public_key_der, verify_token)
}
pub fn respond_with_rng(
rng: &mut impl CryptoRngCore,
server_id: &str,
public_key_der: &[u8],
verify_token: &[u8],
) -> Result<EncryptionResponse, CryptoError> {
let key = ServerPublicKey::from_der(public_key_der)?;
let shared_secret = SharedSecret::generate_with(rng);
let encrypted_shared_secret = key.encrypt_with(rng, shared_secret.as_bytes())?;
let encrypted_verify_token = key.encrypt_with(rng, verify_token)?;
let server_hash = server_hash(server_id, shared_secret.as_bytes(), key.der());
Ok(EncryptionResponse {
shared_secret,
encrypted_shared_secret,
encrypted_verify_token,
server_hash,
})
}
pub fn server_hash(server_id: &str, shared_secret: &[u8], public_key_der: &[u8]) -> String {
let mut sha = Sha1::new();
let id: Vec<u8> = server_id
.chars()
.map(|c| u8::try_from(u32::from(c)).unwrap_or(b'?'))
.collect();
sha.update(&id);
sha.update(shared_secret);
sha.update(public_key_der);
signed_hex_digest(sha.finalize().into())
}
pub fn signed_hex_digest(mut digest: [u8; 20]) -> String {
let negative = digest[0] & 0x80 != 0;
if negative {
let mut carry = true;
for byte in digest.iter_mut().rev() {
let (v, c) = (!*byte).overflowing_add(u8::from(carry));
*byte = v;
carry = c;
}
}
let mut out = String::with_capacity(41);
if negative {
out.push('-');
}
let mut started = false;
for byte in digest {
for nibble in [byte >> 4, byte & 0x0f] {
if nibble != 0 || started {
started = true;
out.push(char::from_digit(u32::from(nibble), 16).unwrap_or('0'));
}
}
}
if !started {
out.push('0');
}
out
}
pub fn random_bytes(buf: &mut [u8]) {
OsRng.fill_bytes(buf);
}
#[cfg(test)]
mod tests {
use super::*;
use rsa::RsaPrivateKey;
use rsa::pkcs8::EncodePublicKey;
fn sha1_hex(s: &str) -> String {
signed_hex_digest(Sha1::digest(s.as_bytes()).into())
}
#[test]
fn known_digest_vectors() {
assert_eq!(
sha1_hex("Notch"),
"4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48"
);
assert_eq!(
sha1_hex("jeb_"),
"-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1"
);
assert_eq!(sha1_hex("simon"), "88e16a1019277b15d58faf0541e11910eb756f6");
}
#[test]
fn digest_edge_cases() {
assert_eq!(signed_hex_digest([0; 20]), "0");
let mut one = [0u8; 20];
one[19] = 1;
assert_eq!(signed_hex_digest(one), "1");
assert_eq!(signed_hex_digest([0xff; 20]), "-1");
let mut min = [0u8; 20];
min[0] = 0x80;
assert_eq!(signed_hex_digest(min), format!("-8{}", "0".repeat(39)));
}
#[test]
fn full_server_hash_matches_java() {
let secret: Vec<u8> = (0..16).collect();
let key: Vec<u8> = (0..162u32).map(|i| (i * 7 + 3) as u8).collect();
assert_eq!(
server_hash("", &secret, &key),
"-50a7c2d3cac900829c4a471fc66f28a9477ec7ae"
);
}
#[test]
fn server_id_is_latin1() {
let s = [0u8; 16];
assert_eq!(server_hash("\u{e9}", &s, &[]), {
let mut sha = Sha1::new();
sha.update([0xe9]);
sha.update(s);
signed_hex_digest(sha.finalize().into())
});
assert_eq!(server_hash("\u{20ac}", &s, &[]), server_hash("?", &s, &[]));
}
#[test]
fn encrypt_round_trip() {
let mut rng = OsRng;
let private = RsaPrivateKey::new(&mut rng, 1024).unwrap();
let der = private.to_public_key().to_public_key_der().unwrap();
let token = [1u8, 2, 3, 4];
let resp = respond_to_encryption_request("", der.as_bytes(), &token).unwrap();
assert_eq!(resp.encrypted_shared_secret.len(), 128);
assert_eq!(resp.encrypted_verify_token.len(), 128);
let secret = private
.decrypt(Pkcs1v15Encrypt, &resp.encrypted_shared_secret)
.unwrap();
assert_eq!(secret, resp.shared_secret.as_bytes());
let tok = private
.decrypt(Pkcs1v15Encrypt, &resp.encrypted_verify_token)
.unwrap();
assert_eq!(tok, token);
assert_eq!(
resp.server_hash,
server_hash("", resp.shared_secret.as_bytes(), der.as_bytes())
);
assert_eq!(
format!("{:?}", resp.shared_secret),
"SharedSecret([redacted])"
);
}
#[test]
fn rejects_bad_keys() {
assert!(matches!(
ServerPublicKey::from_der(&[]),
Err(CryptoError::InvalidPublicKey(_))
));
assert!(matches!(
ServerPublicKey::from_der(&[0x30, 0x82, 0xff, 0xff, 0x00]),
Err(CryptoError::InvalidPublicKey(_))
));
}
#[test]
fn secrets_are_random() {
assert_ne!(SharedSecret::generate(), SharedSecret::generate());
}
}