You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/wordkey/wordkey.go

112 lines
4.0 KiB

// Package wordkey derives the X25519 identity of a key of words (spec
// §38.1): words a person chooses that open a time_and_key capsule instead of
// a .dkk file or an age identity of their own. The words are normalized with
// the Unicode 18.0.0 tables of pathrule, so that case, accents and extra
// spaces do not matter, and stretched with PBKDF2-HMAC-SHA256, Rounds rounds,
// salted with the chain hash, the round and the capsule_id of the capsule, so
// that each capsule needs its own attack, even among the many of a popular
// round. The identity is an ordinary X25519 recipient of the capsule: the
// format does not change.
//
// It is the derivation of wordkey.ts in datekeys-ts, byte for byte. Once the
// date has come, whoever holds the .dkc can try words offline: words of the
// person's own are weaker than random ones.
package wordkey
import (
"crypto/pbkdf2"
"crypto/sha256"
"fmt"
"strings"
"unicode"
"unicode/utf8"
"filippo.io/age"
"g.activething.com/go/DateKeys/codec/bech32"
"g.activething.com/go/DateKeys/internal/pathrule"
)
const (
// MinWords is the fewest different words of MinLetters characters or
// more that a writer accepts.
MinWords = 6
// MinLetters is the fewest characters of a word that counts toward
// MinWords. Shorter words may be part of the key, but do not count.
MinLetters = 3
// Rounds of PBKDF2-HMAC-SHA256, OWASP's figure for 2023.
Rounds = 600_000
)
// Normalize returns the words of text: its NFD, by the tables of pathrule,
// without the combining marks U+0300 to U+036F, each code point in lower case
// by its simple mapping of Unicode 18.0.0, split at white space as
// unicode.IsSpace defines it, which is the list of spec §38.1.
func Normalize(text string) []string {
var b strings.Builder
for _, r := range pathrule.NFD(text) {
if r >= 0x300 && r <= 0x36f {
continue
}
b.WriteRune(pathrule.Lower(r))
}
return strings.Fields(b.String())
}
// Check reports why a writer refuses words, as Normalize returns them, for a
// key (spec §38.1): fewer than MinWords different words of MinLetters
// characters or more, or a control, a Default_Ignorable_Code_Point or a code
// point unassigned in Unicode 18.0.0, which a person cannot see and would not
// type again.
func Check(words []string) error {
counted := make(map[string]bool)
for _, w := range words {
if err := checkRunes(w); err != nil {
return err
}
if utf8.RuneCountInString(w) >= MinLetters {
counted[w] = true
}
}
if len(counted) < MinWords {
return fmt.Errorf("wordkey: a key of words needs at least %d different words of %d or more letters, not %d", MinWords, MinLetters, len(counted))
}
return nil
}
// checkRunes reports a control, a Default_Ignorable_Code_Point or a code
// point unassigned in Unicode 18.0.0 in w.
func checkRunes(w string) error {
for _, r := range w {
switch {
case unicode.IsControl(r):
return fmt.Errorf("wordkey: the words hold the control character U+%04X", r)
case pathrule.DefaultIgnorable(r):
return fmt.Errorf("wordkey: the words hold the invisible character U+%04X", r)
case !pathrule.Assigned(r):
return fmt.Errorf("wordkey: the words hold U+%04X, unassigned in Unicode %s", r, pathrule.UnicodeVersion)
}
}
return nil
}
// Key returns the raw X25519 identity of words for the capsule capsuleID, of
// the round of the chain whose hash is chainHash. The caller clears it.
func Key(words []string, chainHash []byte, round uint64, capsuleID []byte) ([]byte, error) {
salt := fmt.Sprintf("DateKeys llave de palabras v2|%x|%d|%x", chainHash, round, capsuleID)
return pbkdf2.Key(sha256.New, strings.Join(words, " "), []byte(salt), Rounds, 32)
}
// Identity returns the age X25519 identity of words, as Key derives it.
func Identity(words []string, chainHash []byte, round uint64, capsuleID []byte) (*age.X25519Identity, error) {
raw, err := Key(words, chainHash, round, capsuleID)
if err != nil {
return nil, err
}
s, err := bech32.Encode("age-secret-key-", raw)
clear(raw)
if err != nil {
return nil, err
}
return age.ParseX25519Identity(strings.ToUpper(s))
}

Powered by TurnKey Linux.