purecrypto 0.6.29

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, reactive, computed, watch } from 'vue';
import { state, pc } from '../store.js';
import DemoCard from './ui/DemoCard.vue';

const algos = [
  ['AES256_GCM', 'AES-256-GCM'],
  ['CHACHA20_POLY1305', 'ChaCha20-Poly1305'],
];
const algo = ref('AES256_GCM');
const message = ref('attack at dawn');
const aad = ref('v1');

const key = ref(new Uint8Array(32));
const nonce = ref(new Uint8Array(12));
function reseed() {
  crypto.getRandomValues(key.value);
  crypto.getRandomValues(nonce.value);
  key.value = key.value.slice();
  nonce.value = nonce.value.slice();
}
reseed();

const out = reactive({ ct: '', tag: '', plain: '', ok: false, err: '' });

function run(tamper = false) {
  if (state.status !== 'ready') return;
  out.err = '';
  try {
    const ctTag = pc.aeadEncrypt(
      pc.AEAD[algo.value],
      key.value,
      nonce.value,
      pc.utf8(aad.value),
      pc.utf8(message.value),
    );
    const body = ctTag.slice(0, ctTag.length - 16);
    const tag = ctTag.slice(ctTag.length - 16);
    out.ct = pc.toHex(body);
    out.tag = pc.toHex(tag);

    const probe = ctTag.slice();
    if (tamper) probe[0] ^= 0x01;
    try {
      const back = pc.aeadDecrypt(pc.AEAD[algo.value], key.value, nonce.value, pc.utf8(aad.value), probe);
      out.plain = pc.fromUtf8(back);
      out.ok = true;
    } catch {
      out.plain = '⛔ authentication failed — ciphertext rejected';
      out.ok = false;
    }
  } catch (e) {
    out.err = String(e);
  }
}

watch([algo, message, aad, key, nonce, () => state.status], () => run(false), { immediate: true });
</script>

<template>
  <DemoCard
    index="D.01"
    title="Authenticated encryption"
    subtitle="Encrypt-then-authenticate with AES-GCM or ChaCha20-Poly1305. Change one bit of the ciphertext and the tag check refuses it."
    api="purecrypto::cipher::aead — seal / open"
  >
    <template #badge><span class="tag public">AEAD</span></template>

    <div class="row">
      <div class="col">
        <label>Cipher</label>
        <select v-model="algo">
          <option v-for="[k, l] in algos" :key="k" :value="k">{{ l }}</option>
        </select>
      </div>
      <div class="col grow">
        <label>Associated data (authenticated, not encrypted)</label>
        <input type="text" v-model="aad" spellcheck="false" />
      </div>
    </div>

    <div>
      <label>Plaintext</label>
      <textarea v-model="message" rows="2" spellcheck="false"></textarea>
    </div>

    <div class="keys">
      <div>
        <span class="tag secret">key · 256-bit</span>
        <code class="hex secret">{{ pc.toHex(key) }}</code>
      </div>
      <div>
        <span class="tag">nonce · 96-bit</span>
        <code class="hex">{{ pc.toHex(nonce) }}</code>
      </div>
      <button class="btn small" @click="reseed">↻ new key & nonce</button>
    </div>

    <div class="out">
      <div class="out-line">
        <span class="tag public">ciphertext</span>
        <code class="hex public">{{ out.ct || '—' }}</code>
      </div>
      <div class="out-line">
        <span class="tag public">tag · 128-bit</span>
        <code class="hex public">{{ out.tag || '—' }}</code>
      </div>
    </div>

    <div class="verify">
      <div class="decrypted" :class="{ bad: !out.ok }">
        <span class="mono lbl">decrypt →</span>
        <span class="mono val">{{ out.plain }}</span>
      </div>
      <button class="btn small ghost" @click="run(true)">flip a ciphertext bit ✗</button>
    </div>
    <p v-if="out.err" class="err mono">{{ out.err }}</p>
  </DemoCard>
</template>

<style scoped>
.row {
  display: flex;
  gap: 14px;
}
.col {
  min-width: 0;
}
.col.grow {
  flex: 1;
}
.keys {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px;
  background: var(--ink-2);
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-soft);
}
.keys > div {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}
.keys .tag {
  flex-shrink: 0;
}
.out {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.out-line {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}
.out-line .tag {
  flex-shrink: 0;
}
.verify {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
}
.decrypted {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 11px 14px;
  border-radius: var(--radius-sm);
  background: var(--teal-glow);
  border: 1px solid rgba(47, 224, 200, 0.3);
  flex: 1;
  min-width: 220px;
}
.decrypted.bad {
  background: rgba(255, 90, 90, 0.1);
  border-color: rgba(255, 90, 90, 0.4);
}
.decrypted .lbl {
  color: var(--muted);
  font-size: 0.76rem;
}
.decrypted .val {
  font-size: 0.86rem;
  color: var(--teal-2);
}
.decrypted.bad .val {
  color: #ff9c9c;
}
.btn.small {
  padding: 8px 13px;
  font-size: 0.76rem;
}
.btn.ghost {
  background: transparent;
}
.err {
  color: #ff9c9c;
  font-size: 0.78rem;
  margin: 0;
}
@media (max-width: 560px) {
  .row {
    flex-direction: column;
  }
}
</style>