purecrypto 0.6.28

A pure-Rust cryptography toolkit with no foreign-code dependencies, from constant-time primitives up to keys, X.509 and TLS.
Documentation
<script setup>
import { ref, computed, nextTick } from 'vue';
import { state, pc } from '../../store.js';
import OutputBlock from '../ui/OutputBlock.vue';

const ALGOS = [
  { kind: 'ed25519', name: 'Ed25519', params: null },
  { kind: 'ecdsa', name: 'ECDSA', params: [[1, 'P-256'], [2, 'P-384'], [3, 'P-521'], [4, 'secp256k1']] },
  { kind: 'ed448', name: 'Ed448', params: null },
  { kind: 'rsa', name: 'RSA', params: [[2048, '2048-bit'], [3072, '3072-bit'], [4096, '4096-bit']], slow: true },
  { kind: 'mldsa', name: 'ML-DSA', params: [[1, 'ML-DSA-44'], [2, 'ML-DSA-65'], [3, 'ML-DSA-87']], pq: true },
  { kind: 'slhdsa', name: 'SLH-DSA', params: [[2, 'SHA2-128f'], [4, 'SHA2-192f'], [6, 'SHA2-256f'], [8, 'SHAKE-128f']], pq: true },
  { kind: 'sm2', name: 'SM2', params: null },
];

const kind = ref('ed25519');
const param = ref(0);
const busy = ref(false);
const out = ref(null); // { priv, pub, label }
const error = ref('');

const current = computed(() => ALGOS.find((a) => a.kind === kind.value));
function pickAlgo(a) {
  kind.value = a.kind;
  param.value = a.params ? a.params[0][0] : 0;
}
const label = computed(() => {
  const a = current.value;
  if (!a.params) return a.name;
  return a.params.find((p) => p[0] === param.value)?.[1] || a.name;
});

async function generate() {
  if (state.status !== 'ready') return;
  error.value = '';
  busy.value = true;
  // Let the spinner paint before a potentially slow (RSA) blocking keygen.
  await nextTick();
  await new Promise((r) => setTimeout(r, 20));
  try {
    const k = pc.generateKey(kind.value, param.value);
    out.value = { priv: k.privatePem(), pub: k.publicPem(), label: label.value };
    k.free();
  } catch (e) {
    error.value = String(e.message || e);
    out.value = null;
  } finally {
    busy.value = false;
  }
}
</script>

<template>
  <div class="tool">
    <div class="algos">
      <button
        v-for="a in ALGOS"
        :key="a.kind"
        class="algo"
        :class="{ active: kind === a.kind }"
        @click="pickAlgo(a)"
      >
        {{ a.name }}
        <span v-if="a.pq" class="pqdot" title="post-quantum"></span>
      </button>
    </div>

    <div class="run">
      <div v-if="current.params" class="ctl">
        <label>Parameters</label>
        <select v-model.number="param">
          <option v-for="p in current.params" :key="p[0]" :value="p[0]">{{ p[1] }}</option>
        </select>
      </div>
      <button class="btn btn-primary gen" :disabled="busy" @click="generate">
        <span v-if="busy" class="spinner"></span>
        {{ busy ? 'Generating…' : 'Generate keypair' }}
      </button>
    </div>

    <p v-if="current.slow" class="note mono">
      RSA key generation runs in wasm on the main thread — it can take several
      seconds and will briefly freeze this tab. Ed25519 and ECDSA are instant.
    </p>
    <p v-if="error" class="err mono">{{ error }}</p>

    <div v-if="out" class="results">
      <OutputBlock label="private key" tone="secret" :value="out.priv" :filename="`${out.label}-private.pem`" />
      <OutputBlock label="public key" tone="public" :value="out.pub" :filename="`${out.label}-public.pem`" />
    </div>
  </div>
</template>

<style scoped>
.tool {
  display: flex;
  flex-direction: column;
  gap: 18px;
}
.algos {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.algo {
  position: relative;
  font-family: var(--font-mono);
  font-size: 0.82rem;
  padding: 9px 15px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--ink-2);
  color: var(--dim);
  cursor: pointer;
  transition:
    border-color 0.16s ease,
    color 0.16s ease;
}
.algo:hover {
  color: var(--paper);
}
.algo.active {
  color: var(--paper);
  border-color: var(--violet);
  background: var(--violet-glow);
}
.pqdot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--amber);
  margin-left: 6px;
  vertical-align: middle;
}
.run {
  display: flex;
  gap: 14px;
  align-items: flex-end;
  flex-wrap: wrap;
}
.ctl {
  min-width: 180px;
}
.gen {
  min-height: 44px;
}
.gen:disabled {
  opacity: 0.7;
  cursor: default;
}
.spinner {
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.4);
  border-top-color: #fff;
  animation: spin 0.7s linear infinite;
}
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
.note {
  font-size: 0.76rem;
  color: var(--amber);
  margin: 0;
  padding: 10px 13px;
  border: 1px solid rgba(255, 180, 84, 0.25);
  border-radius: var(--radius-sm);
  background: rgba(255, 180, 84, 0.07);
}
.err {
  color: #ff9c9c;
  font-size: 0.82rem;
  margin: 0;
}
.results {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
</style>