ssh-keygenしたときに出るあのキラキラの正体

2025/01/08に公開

LabBaseテックカレンダー Advent Calendar 2024の12月17日分のアドベントカレンダーです。

このブログのゴール

  • ssh-keygenしたときに出るあのキラキラの正体を突き止めて、Rustで完全再現する⭐️

  • 再現するだけにとどまらずオエー鳥AAで似たようなものを作る🐦‍⬛

  • コードはここ

https://github.com/ie-Yoshisaur/drunken-bishop/tree/main/src

あのキラキラの正体

randomartって言う

テキストベースのフィンガープリントだと人間が誤って確認するリスクがあるから、視覚的な差分が取りやすいようにAAとして可視化している

アルゴリズム

公開鍵ファイルの解析

公開鍵の中身はこうなっている

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJgu2rDTYkrZlP7Hj1unjmUhIHXJCuHs6/Z4iE2S/DIJ user@host

スペース区切りで3つの情報が格納されていて、真ん中のものが公開鍵のバイトデータがBase64でエンコードされているもの。こいつをデコードしてバイトデータに戻すという処理をする

use base64::Engine;
use std::fs::File;
use std::io::{BufRead, BufReader};

/// Reads an SSH public key file, extracts the base64-encoded part, and decodes it into raw bytes.
pub fn parse_ssh_pubkey(pubkey_path: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    // 1. Open the file at the given path.
    let file = File::open(pubkey_path)?;
    // 2. Create a buffered reader to read the file line by line.
    let mut lines = BufReader::new(file).lines();

    // 3. Extract the first line, which is expected to contain the public key data.
    let line = match lines.next() {
        Some(Ok(l)) => l,
        _ => return Err("Failed to read pubkey file".into()),
    };

    // 4. Split the line by whitespace. A typical SSH pubkey line looks like:
    //    ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJgu2rDTYkrZlP7Hj1unjmUhIHXJCuHs6/Z4iE2S/DIJ user@host
    let parts: Vec<&str> = line.split_whitespace().collect();
    // 5. Check that there are at least two parts: key type (e.g., "ssh-ed25519") and the base64-encoded key.
    if parts.len() < 2 {
        return Err("Invalid pubkey format".into());
    }

    // 6. Decode the base64-encoded string into raw bytes.
    let bin = base64::prelude::BASE64_STANDARD.decode(parts[1])?;
    Ok(bin)
}

Drunken Bishopアルゴリズム

  • 先ほどのバイトデータをハッシュ化する
  • 9行×17列のグリッドを作成
    • ビショップの例のようにチェス版のようなものを思い浮かべるといい
  • グリッドの真ん中にビショップを配置
  • ハッシュ値に基づくビショップの移動をさせる
    • ハッシュ値の各バイトを4回に分けて処理する
      • 各バイトは8ビットなので、2ビットずつ4回に分ける
    • 各2ビットの解釈:
      • 最低2ビット(b & 0x03)を使用してビショップの移動方向を決定
        • ビット0 (LSB): 右(1)か左(-1)かを決定
        • ビット1: 下(1)か上(-1)かを決定
          • ビショップのように斜め方向にしか行けない
    • グリッドの境界線を超えそうなときは、越えないまま縦か横に移動する
      • 例えば、一番右にいる状態で右上に行くとすると、横の座標は変えずに上に行くのみとなる
  • グリッドの各マスごとにカウンタを用意してそのビショップがそのマスに移動した回数を記録する
  • マスのカウンタごとに文字を当てる
    • ' ', '.', 'o', '+', '=', '*', 'B', 'O', 'X', '@', '%', '&', '#', '/', '^'
    • ビショップのスタート地点にS、最終地点にEの文字を当てる
use sha2::{Digest, Sha256};

/// Generates a 2D grid of visitation counts using the Drunken Bishop algorithm.
/// Returns the grid, the start position, and the end position.
pub fn generate_drunken_bishop_grid(data: &[u8]) -> (Vec<Vec<u8>>, (usize, usize), (usize, usize)) {
    // 1. Compute the SHA-256 hash (fingerprint) of the input data.
    let mut hasher = Sha256::new();
    hasher.update(data);
    let fingerprint = hasher.finalize();

    // 2. Define the size of the grid.
    let rows = 9;
    let cols = 17;
    // 3. Create a 2D vector initialized to zero to track how many times each cell is visited.
    let mut grid = vec![vec![0u8; cols]; rows];

    // 4. Place the bishop in the center of the grid and record the starting position.
    let mut row = rows as i32 / 2;
    let mut col = cols as i32 / 2;
    let (start_row, start_col) = (row as usize, col as usize);

    // 5. Interpret each byte in the fingerprint as four moves (two bits per move).
    for &byte in fingerprint.iter() {
        let mut b = byte;
        for _ in 0..4 {
            // 5.1. Determine dx and dy based on the lowest two bits of b.
            let dx = if (b & 0x01) != 0 { 1 } else { -1 };
            let dy = if (b & 0x02) != 0 { 1 } else { -1 };

            // 5.2. Update position and clamp if it goes out of bounds.
            row += dy;
            col += dx;
            if row < 0 {
                row = 0;
            }
            if row >= rows as i32 {
                row = rows as i32 - 1;
            }
            if col < 0 {
                col = 0;
            }
            if col >= cols as i32 {
                col = cols as i32 - 1;
            }

            // 5.3. Increment the visit counter in the current cell.
            grid[row as usize][col as usize] = grid[row as usize][col as usize].saturating_add(1);

            // 5.4. Shift the byte to move to the next two bits.
            b >>= 2;
        }
    }

    // 6. Record the ending position of the bishop.
    let (end_row, end_col) = (row as usize, col as usize);

    (grid, (start_row, start_col), (end_row, end_col))
}

/// Renders a 2D grid produced by the Drunken Bishop algorithm as ASCII art.
pub fn render_drunken_bishop_art(
    grid: &[Vec<u8>],
    start_pos: (usize, usize),
    end_pos: (usize, usize),
) -> String {
    let rows = grid.len();
    let cols = if rows > 0 { grid[0].len() } else { 0 };
    let (start_row, start_col) = start_pos;
    let (end_row, end_col) = end_pos;

    // 1. Define the symbols corresponding to visit counts.
    let symbols = [
        ' ', '.', 'o', '+', '=', '*', 'B', 'O', 'X', '@', '%', '&', '#', '/', '^',
    ];

    // 2. Construct ASCII art by building each row of text.
    let mut lines = vec![];

    // 2.1. Top border.
    let top_border = format!("+{}+", "-".repeat(cols));
    lines.push(top_border);

    // 2.2. Each row in the grid.
    for r in 0..rows {
        let mut row_string = String::new();
        row_string.push('|');
        for c in 0..cols {
            if r == start_row && c == start_col {
                // Mark start with 'S'
                row_string.push('S');
            } else if r == end_row && c == end_col {
                // Mark end with 'E'
                row_string.push('E');
            } else {
                // Map visit count to a symbol.
                let count = grid[r][c] as usize;
                let idx = if count >= symbols.len() {
                    symbols.len() - 1
                } else {
                    count
                };
                row_string.push(symbols[idx]);
            }
        }
        row_string.push('|');
        lines.push(row_string);
    }

    // 2.3. Bottom border.
    let bottom_border = format!("+{}+", "-".repeat(cols));
    lines.push(bottom_border);

    // 3. Join all lines into a single string.
    lines.join("\n")
}

こんな感じ、これで冒頭の画像のような結果が得られる

オエー鳥AAを作る

  • めっちゃ簡単
    let template = r#"オエーー!!   __
         ___/  \
       /  / //⌒
       / (゚)/ / /
      /  ( /{}⌒{}{}
      |   \\゚{}{}{}
    /   /⌒\\゚{}{}{}
    /   |   \{}{}{}
        |     ゙{}
               {}
               {}"#;
  • このように置換対象の{}を15個生やす
  • 3 * 5でグリッドを作ってrandomartを作成して、15マスそれぞれに当てられた文字をそれぞれ置換する
    • 奇数 * 奇数の積になる方が好ましかったはず(自信ないお)、他のAAでやりたい時はとりあえずそういう感じで実装していこう

終わり

結構キラキラ要素があるAAはネットに転がっているので、皆さんもぜひオリジナルrandomart生成器を作ってみてください

Discussion