diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..521a216 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +lastseed diff --git a/README.md b/README.md new file mode 100644 index 0000000..5253822 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# lastseed + +Display the possible last seed words from 11, 14, 17, 20 or 23 words. +Entropy is also displayed and can be pasted into [Ian Coleman bip39](https://github.com/iancoleman/bip39) or similar tools. + +## Usage + +```bash +./lastseed traffic rabbit canal shadow eternal public evil cup poem drift episode box manual sick entry original deny +``` + +or + +```bash +./lastseed +Enter seed words: +traffic rabbit canal shadow eternal public evil cup poem drift episode box manual sick entry original deny +``` + +![Usage example](example.png) + +## Build + +```bash +go build -o lastseed ./src +``` diff --git a/example.png b/example.png new file mode 100644 index 0000000..e627426 Binary files /dev/null and b/example.png differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6288f08 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/ottosch/lastseed + +go 1.22.5 diff --git a/src/bip39/verify.go b/src/bip39/verify.go new file mode 100644 index 0000000..e439670 --- /dev/null +++ b/src/bip39/verify.go @@ -0,0 +1,55 @@ +package bip39 + +import ( + "crypto/sha256" + "fmt" + "math/big" + "os" + + "github.com/ottosch/lastseed/src/bip39/wordlist" +) + +// VerifyWordCount checks if the number of words matches a suitable number of bip39 seed words +func VerifyWordCount(words []string) { + actualCount := len(words) + 1 + if actualCount < 12 || actualCount > 24 || actualCount%3 != 0 { + fmt.Fprintf(os.Stderr, "%d word(s) input. Expected 11, 14, 17, 20 or 23\n", len(words)) + os.Exit(1) + } +} + +// VerifyValidWords checks if all words belong to bip39 word list +func VerifyValidWords(words []string) { + for _, w := range words { + _, found := wordlist.WordMap[w] + if !found { + fmt.Fprintf(os.Stderr, "Word \"%s\" is not part of the BIP39 word list\n", w) + os.Exit(1) + } + } +} + +// ValidChecksum performs checksum validation +func ValidChecksum(fullBitstring *big.Int, checksumSize uint) bool { + wordCount := int(checksumSize) * 3 + + withoutChecksumBits := new(big.Int).Set(fullBitstring) + withoutChecksumBits.Rsh(withoutChecksumBits, checksumSize) + + withoutChecksumBitCount := (wordCount - 1) * 11 // we still don't have the last word here + withoutChecksumByteLen := (withoutChecksumBitCount + 7) / 8 + withoutChecksumBytes := make([]byte, withoutChecksumByteLen) + withoutChecksumBits.FillBytes(withoutChecksumBytes) + + hashBits := sha256.Sum256(withoutChecksumBytes)[0] >> (8 - checksumSize) // ex 4 bits: 01010111000110001101 -> 00001000 + bitmask := byte((1 << checksumSize) - 1) // 00001111 + checksumBits := hashBits & bitmask // 00001000 + + withChecksumBitCount := wordCount*11 + int(checksumSize) // now all words + checksum + withChecksumByteLen := (withChecksumBitCount + 7) / 8 + withChecksumBytes := make([]byte, withChecksumByteLen) + fullBitstring.FillBytes(withChecksumBytes) + + inputChecksumBits := withChecksumBytes[len(withChecksumBytes)-1] & bitmask + return checksumBits == inputChecksumBits +} diff --git a/src/bip39/wordlist/wordlist.go b/src/bip39/wordlist/wordlist.go new file mode 100644 index 0000000..bd22862 --- /dev/null +++ b/src/bip39/wordlist/wordlist.go @@ -0,0 +1,620 @@ +package wordlist + +var Wordlist = []string{ + "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", + "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act", "action", + "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", "adult", + "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", "agree", + "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert", "alien", + "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", "always", + "amateur", "amazing", "among", "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", + "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique", "anxiety", + "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", "area", + "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest", "arrive", + "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", "assist", + "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", "audit", + "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", "awake", "aware", + "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", "bag", + "balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", + "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become", "beef", + "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", "best", + "betray", "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", "bird", + "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", "blind", + "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil", + "bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss", "bottom", + "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", "breeze", + "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", + "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb", "bulk", + "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", "butter", + "buyer", "buzz", "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", "calm", + "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", + "capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", "cart", + "case", "cash", "casino", "castle", "casual", "cat", "catalog", "catch", "category", "cattle", + "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", "cereal", + "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase", "chat", + "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", "chimney", + "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", "citizen", + "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", "clever", + "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", "cloth", + "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", "code", + "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", "comfort", "comic", + "common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control", "convince", + "cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost", "cotton", + "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle", "craft", + "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", "crew", + "cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", "cruel", + "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", + "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", "damage", + "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", "debate", + "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense", "define", + "defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny", "depart", + "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk", "despair", + "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", "diary", + "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", "direct", + "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", "divert", + "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain", "donate", + "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama", "drastic", + "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", "drum", + "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", + "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", "ecology", + "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", "elder", + "electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", "embody", "embrace", + "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", "endorse", + "enemy", "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", "enrich", + "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", "equip", "era", + "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", "eternal", + "ethics", "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", "exchange", "excite", + "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit", "exotic", + "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", "eyebrow", + "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame", "family", + "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", + "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", "fence", + "festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file", "film", + "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", "fiscal", + "fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", "flee", + "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly", "foam", + "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", "forget", + "fork", "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile", "frame", + "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", "fruit", + "fuel", "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", "gallery", + "game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp", "gate", + "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", "ghost", + "giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance", "glare", + "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", "goat", + "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", "grab", + "grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid", "grief", + "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", + "gun", "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", "harbor", + "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", "heart", + "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", "high", + "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole", "holiday", + "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", "host", + "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", "hungry", + "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", "identify", + "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", "impact", + "impose", "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", "indoor", + "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", "injury", "inmate", + "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", "intact", + "interest", "into", "invest", "invite", "involve", "iron", "island", "isolate", "issue", "item", + "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel", "job", + "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", "junk", + "just", "kangaroo", "keen", "keep", "ketchup", "key", "kick", "kid", "kidney", "kind", + "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", + "know", "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", "laptop", + "large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit", "layer", + "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", "legend", + "leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level", "liar", + "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit", "link", + "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", "local", + "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love", "loyal", + "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", + "magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage", "mandate", + "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market", "marriage", + "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", "maze", + "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", "melt", "member", + "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message", "metal", + "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", "minute", + "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", "mixture", "mobile", "model", + "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", "more", + "morning", "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie", "much", + "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", "must", "mutual", "myself", + "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", "near", + "neck", "need", "negative", "neglect", "neither", "nephew", "nerve", "nest", "net", "network", + "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee", "noodle", + "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", "nuclear", + "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obscure", "observe", "obtain", + "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", "oil", + "okay", "old", "olive", "olympic", "omit", "once", "one", "onion", "online", "only", + "open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", "order", "ordinary", + "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", "outside", + "oval", "oven", "over", "own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", + "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper", "parade", + "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", "pattern", + "pause", "pave", "payment", "peace", "peanut", "pear", "peasant", "pelican", "pen", "penalty", + "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase", + "physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink", + "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", "play", + "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole", + "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", "potato", + "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare", "present", + "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", "prize", + "problem", "process", "produce", "profit", "program", "project", "promote", "proof", "property", "prosper", + "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", + "pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid", + "quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", "rabbit", "raccoon", + "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", "ranch", + "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", "ready", + "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle", "reduce", + "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release", "relief", + "rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen", "repair", + "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response", "result", + "retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", "ribbon", + "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", "ripple", + "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance", + "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", "rubber", + "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", "safe", + "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", "satisfy", + "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", "scene", + "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", + "sea", "search", "season", "seat", "second", "secret", "section", "security", "seed", "seek", + "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service", "session", + "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", "sheriff", + "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", "short", + "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", "siege", + "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", "sing", + "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", "skin", + "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim", + "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", "snack", + "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", "soft", + "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", "sort", + "soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", "speak", + "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", "spirit", + "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", "spy", + "square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", "stand", + "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still", + "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", "strike", + "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", "subway", "success", + "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", "sunset", + "super", "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", "survey", "suspect", + "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim", "swing", + "switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", "tail", + "talent", "talk", "tank", "tape", "target", "task", "taste", "tattoo", "taxi", "teach", + "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", "thank", + "that", "theme", "then", "theory", "there", "they", "thing", "this", "thought", "three", + "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber", "time", + "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", "toe", + "together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", + "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", "toward", + "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", "trap", + "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick", "trigger", + "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", "truth", + "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", "twelve", + "twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella", "unable", + "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", "unique", + "unit", "universe", "unknown", "unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", + "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful", "useless", + "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", "vanish", + "vapor", "various", "vast", "vault", "vehicle", "velvet", "vendor", "venture", "venue", "verb", + "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", + "view", "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", "vital", + "vivid", "vocal", "voice", "void", "volcano", "volume", "vote", "voyage", "wage", "wagon", + "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", "wasp", + "waste", "water", "wave", "way", "wealth", "weapon", "wear", "weasel", "weather", "web", + "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat", "wheel", + "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", "win", + "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", "wise", "wish", + "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", "worry", + "worth", "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year", "yellow", + "you", "young", "youth", "zebra", "zero", "zone", "zoo"} + +var WordMap = map[string]int{ + "abandon": 0, "ability": 1, "able": 2, "about": 3, "above": 4, "absent": 5, + "absorb": 6, "abstract": 7, "absurd": 8, "abuse": 9, "access": 10, + "accident": 11, "account": 12, "accuse": 13, "achieve": 14, "acid": 15, + "acoustic": 16, "acquire": 17, "across": 18, "act": 19, "action": 20, + "actor": 21, "actress": 22, "actual": 23, "adapt": 24, "add": 25, + "addict": 26, "address": 27, "adjust": 28, "admit": 29, "adult": 30, + "advance": 31, "advice": 32, "aerobic": 33, "affair": 34, "afford": 35, + "afraid": 36, "again": 37, "age": 38, "agent": 39, "agree": 40, + "ahead": 41, "aim": 42, "air": 43, "airport": 44, "aisle": 45, + "alarm": 46, "album": 47, "alcohol": 48, "alert": 49, "alien": 50, + "all": 51, "alley": 52, "allow": 53, "almost": 54, "alone": 55, + "alpha": 56, "already": 57, "also": 58, "alter": 59, "always": 60, + "amateur": 61, "amazing": 62, "among": 63, "amount": 64, "amused": 65, + "analyst": 66, "anchor": 67, "ancient": 68, "anger": 69, "angle": 70, + "angry": 71, "animal": 72, "ankle": 73, "announce": 74, "annual": 75, + "another": 76, "answer": 77, "antenna": 78, "antique": 79, "anxiety": 80, + "any": 81, "apart": 82, "apology": 83, "appear": 84, "apple": 85, + "approve": 86, "april": 87, "arch": 88, "arctic": 89, "area": 90, + "arena": 91, "argue": 92, "arm": 93, "armed": 94, "armor": 95, + "army": 96, "around": 97, "arrange": 98, "arrest": 99, "arrive": 100, + "arrow": 101, "art": 102, "artefact": 103, "artist": 104, "artwork": 105, + "ask": 106, "aspect": 107, "assault": 108, "asset": 109, "assist": 110, + "assume": 111, "asthma": 112, "athlete": 113, "atom": 114, "attack": 115, + "attend": 116, "attitude": 117, "attract": 118, "auction": 119, "audit": 120, + "august": 121, "aunt": 122, "author": 123, "auto": 124, "autumn": 125, + "average": 126, "avocado": 127, "avoid": 128, "awake": 129, "aware": 130, + "away": 131, "awesome": 132, "awful": 133, "awkward": 134, "axis": 135, + "baby": 136, "bachelor": 137, "bacon": 138, "badge": 139, "bag": 140, + "balance": 141, "balcony": 142, "ball": 143, "bamboo": 144, "banana": 145, + "banner": 146, "bar": 147, "barely": 148, "bargain": 149, "barrel": 150, + "base": 151, "basic": 152, "basket": 153, "battle": 154, "beach": 155, + "bean": 156, "beauty": 157, "because": 158, "become": 159, "beef": 160, + "before": 161, "begin": 162, "behave": 163, "behind": 164, "believe": 165, + "below": 166, "belt": 167, "bench": 168, "benefit": 169, "best": 170, + "betray": 171, "better": 172, "between": 173, "beyond": 174, "bicycle": 175, + "bid": 176, "bike": 177, "bind": 178, "biology": 179, "bird": 180, + "birth": 181, "bitter": 182, "black": 183, "blade": 184, "blame": 185, + "blanket": 186, "blast": 187, "bleak": 188, "bless": 189, "blind": 190, + "blood": 191, "blossom": 192, "blouse": 193, "blue": 194, "blur": 195, + "blush": 196, "board": 197, "boat": 198, "body": 199, "boil": 200, + "bomb": 201, "bone": 202, "bonus": 203, "book": 204, "boost": 205, + "border": 206, "boring": 207, "borrow": 208, "boss": 209, "bottom": 210, + "bounce": 211, "box": 212, "boy": 213, "bracket": 214, "brain": 215, + "brand": 216, "brass": 217, "brave": 218, "bread": 219, "breeze": 220, + "brick": 221, "bridge": 222, "brief": 223, "bright": 224, "bring": 225, + "brisk": 226, "broccoli": 227, "broken": 228, "bronze": 229, "broom": 230, + "brother": 231, "brown": 232, "brush": 233, "bubble": 234, "buddy": 235, + "budget": 236, "buffalo": 237, "build": 238, "bulb": 239, "bulk": 240, + "bullet": 241, "bundle": 242, "bunker": 243, "burden": 244, "burger": 245, + "burst": 246, "bus": 247, "business": 248, "busy": 249, "butter": 250, + "buyer": 251, "buzz": 252, "cabbage": 253, "cabin": 254, "cable": 255, + "cactus": 256, "cage": 257, "cake": 258, "call": 259, "calm": 260, + "camera": 261, "camp": 262, "can": 263, "canal": 264, "cancel": 265, + "candy": 266, "cannon": 267, "canoe": 268, "canvas": 269, "canyon": 270, + "capable": 271, "capital": 272, "captain": 273, "car": 274, "carbon": 275, + "card": 276, "cargo": 277, "carpet": 278, "carry": 279, "cart": 280, + "case": 281, "cash": 282, "casino": 283, "castle": 284, "casual": 285, + "cat": 286, "catalog": 287, "catch": 288, "category": 289, "cattle": 290, + "caught": 291, "cause": 292, "caution": 293, "cave": 294, "ceiling": 295, + "celery": 296, "cement": 297, "census": 298, "century": 299, "cereal": 300, + "certain": 301, "chair": 302, "chalk": 303, "champion": 304, "change": 305, + "chaos": 306, "chapter": 307, "charge": 308, "chase": 309, "chat": 310, + "cheap": 311, "check": 312, "cheese": 313, "chef": 314, "cherry": 315, + "chest": 316, "chicken": 317, "chief": 318, "child": 319, "chimney": 320, + "choice": 321, "choose": 322, "chronic": 323, "chuckle": 324, "chunk": 325, + "churn": 326, "cigar": 327, "cinnamon": 328, "circle": 329, "citizen": 330, + "city": 331, "civil": 332, "claim": 333, "clap": 334, "clarify": 335, + "claw": 336, "clay": 337, "clean": 338, "clerk": 339, "clever": 340, + "click": 341, "client": 342, "cliff": 343, "climb": 344, "clinic": 345, + "clip": 346, "clock": 347, "clog": 348, "close": 349, "cloth": 350, + "cloud": 351, "clown": 352, "club": 353, "clump": 354, "cluster": 355, + "clutch": 356, "coach": 357, "coast": 358, "coconut": 359, "code": 360, + "coffee": 361, "coil": 362, "coin": 363, "collect": 364, "color": 365, + "column": 366, "combine": 367, "come": 368, "comfort": 369, "comic": 370, + "common": 371, "company": 372, "concert": 373, "conduct": 374, "confirm": 375, + "congress": 376, "connect": 377, "consider": 378, "control": 379, "convince": 380, + "cook": 381, "cool": 382, "copper": 383, "copy": 384, "coral": 385, + "core": 386, "corn": 387, "correct": 388, "cost": 389, "cotton": 390, + "couch": 391, "country": 392, "couple": 393, "course": 394, "cousin": 395, + "cover": 396, "coyote": 397, "crack": 398, "cradle": 399, "craft": 400, + "cram": 401, "crane": 402, "crash": 403, "crater": 404, "crawl": 405, + "crazy": 406, "cream": 407, "credit": 408, "creek": 409, "crew": 410, + "cricket": 411, "crime": 412, "crisp": 413, "critic": 414, "crop": 415, + "cross": 416, "crouch": 417, "crowd": 418, "crucial": 419, "cruel": 420, + "cruise": 421, "crumble": 422, "crunch": 423, "crush": 424, "cry": 425, + "crystal": 426, "cube": 427, "culture": 428, "cup": 429, "cupboard": 430, + "curious": 431, "current": 432, "curtain": 433, "curve": 434, "cushion": 435, + "custom": 436, "cute": 437, "cycle": 438, "dad": 439, "damage": 440, + "damp": 441, "dance": 442, "danger": 443, "daring": 444, "dash": 445, + "daughter": 446, "dawn": 447, "day": 448, "deal": 449, "debate": 450, + "debris": 451, "decade": 452, "december": 453, "decide": 454, "decline": 455, + "decorate": 456, "decrease": 457, "deer": 458, "defense": 459, "define": 460, + "defy": 461, "degree": 462, "delay": 463, "deliver": 464, "demand": 465, + "demise": 466, "denial": 467, "dentist": 468, "deny": 469, "depart": 470, + "depend": 471, "deposit": 472, "depth": 473, "deputy": 474, "derive": 475, + "describe": 476, "desert": 477, "design": 478, "desk": 479, "despair": 480, + "destroy": 481, "detail": 482, "detect": 483, "develop": 484, "device": 485, + "devote": 486, "diagram": 487, "dial": 488, "diamond": 489, "diary": 490, + "dice": 491, "diesel": 492, "diet": 493, "differ": 494, "digital": 495, + "dignity": 496, "dilemma": 497, "dinner": 498, "dinosaur": 499, "direct": 500, + "dirt": 501, "disagree": 502, "discover": 503, "disease": 504, "dish": 505, + "dismiss": 506, "disorder": 507, "display": 508, "distance": 509, "divert": 510, + "divide": 511, "divorce": 512, "dizzy": 513, "doctor": 514, "document": 515, + "dog": 516, "doll": 517, "dolphin": 518, "domain": 519, "donate": 520, + "donkey": 521, "donor": 522, "door": 523, "dose": 524, "double": 525, + "dove": 526, "draft": 527, "dragon": 528, "drama": 529, "drastic": 530, + "draw": 531, "dream": 532, "dress": 533, "drift": 534, "drill": 535, + "drink": 536, "drip": 537, "drive": 538, "drop": 539, "drum": 540, + "dry": 541, "duck": 542, "dumb": 543, "dune": 544, "during": 545, + "dust": 546, "dutch": 547, "duty": 548, "dwarf": 549, "dynamic": 550, + "eager": 551, "eagle": 552, "early": 553, "earn": 554, "earth": 555, + "easily": 556, "east": 557, "easy": 558, "echo": 559, "ecology": 560, + "economy": 561, "edge": 562, "edit": 563, "educate": 564, "effort": 565, + "egg": 566, "eight": 567, "either": 568, "elbow": 569, "elder": 570, + "electric": 571, "elegant": 572, "element": 573, "elephant": 574, "elevator": 575, + "elite": 576, "else": 577, "embark": 578, "embody": 579, "embrace": 580, + "emerge": 581, "emotion": 582, "employ": 583, "empower": 584, "empty": 585, + "enable": 586, "enact": 587, "end": 588, "endless": 589, "endorse": 590, + "enemy": 591, "energy": 592, "enforce": 593, "engage": 594, "engine": 595, + "enhance": 596, "enjoy": 597, "enlist": 598, "enough": 599, "enrich": 600, + "enroll": 601, "ensure": 602, "enter": 603, "entire": 604, "entry": 605, + "envelope": 606, "episode": 607, "equal": 608, "equip": 609, "era": 610, + "erase": 611, "erode": 612, "erosion": 613, "error": 614, "erupt": 615, + "escape": 616, "essay": 617, "essence": 618, "estate": 619, "eternal": 620, + "ethics": 621, "evidence": 622, "evil": 623, "evoke": 624, "evolve": 625, + "exact": 626, "example": 627, "excess": 628, "exchange": 629, "excite": 630, + "exclude": 631, "excuse": 632, "execute": 633, "exercise": 634, "exhaust": 635, + "exhibit": 636, "exile": 637, "exist": 638, "exit": 639, "exotic": 640, + "expand": 641, "expect": 642, "expire": 643, "explain": 644, "expose": 645, + "express": 646, "extend": 647, "extra": 648, "eye": 649, "eyebrow": 650, + "fabric": 651, "face": 652, "faculty": 653, "fade": 654, "faint": 655, + "faith": 656, "fall": 657, "false": 658, "fame": 659, "family": 660, + "famous": 661, "fan": 662, "fancy": 663, "fantasy": 664, "farm": 665, + "fashion": 666, "fat": 667, "fatal": 668, "father": 669, "fatigue": 670, + "fault": 671, "favorite": 672, "feature": 673, "february": 674, "federal": 675, + "fee": 676, "feed": 677, "feel": 678, "female": 679, "fence": 680, + "festival": 681, "fetch": 682, "fever": 683, "few": 684, "fiber": 685, + "fiction": 686, "field": 687, "figure": 688, "file": 689, "film": 690, + "filter": 691, "final": 692, "find": 693, "fine": 694, "finger": 695, + "finish": 696, "fire": 697, "firm": 698, "first": 699, "fiscal": 700, + "fish": 701, "fit": 702, "fitness": 703, "fix": 704, "flag": 705, + "flame": 706, "flash": 707, "flat": 708, "flavor": 709, "flee": 710, + "flight": 711, "flip": 712, "float": 713, "flock": 714, "floor": 715, + "flower": 716, "fluid": 717, "flush": 718, "fly": 719, "foam": 720, + "focus": 721, "fog": 722, "foil": 723, "fold": 724, "follow": 725, + "food": 726, "foot": 727, "force": 728, "forest": 729, "forget": 730, + "fork": 731, "fortune": 732, "forum": 733, "forward": 734, "fossil": 735, + "foster": 736, "found": 737, "fox": 738, "fragile": 739, "frame": 740, + "frequent": 741, "fresh": 742, "friend": 743, "fringe": 744, "frog": 745, + "front": 746, "frost": 747, "frown": 748, "frozen": 749, "fruit": 750, + "fuel": 751, "fun": 752, "funny": 753, "furnace": 754, "fury": 755, + "future": 756, "gadget": 757, "gain": 758, "galaxy": 759, "gallery": 760, + "game": 761, "gap": 762, "garage": 763, "garbage": 764, "garden": 765, + "garlic": 766, "garment": 767, "gas": 768, "gasp": 769, "gate": 770, + "gather": 771, "gauge": 772, "gaze": 773, "general": 774, "genius": 775, + "genre": 776, "gentle": 777, "genuine": 778, "gesture": 779, "ghost": 780, + "giant": 781, "gift": 782, "giggle": 783, "ginger": 784, "giraffe": 785, + "girl": 786, "give": 787, "glad": 788, "glance": 789, "glare": 790, + "glass": 791, "glide": 792, "glimpse": 793, "globe": 794, "gloom": 795, + "glory": 796, "glove": 797, "glow": 798, "glue": 799, "goat": 800, + "goddess": 801, "gold": 802, "good": 803, "goose": 804, "gorilla": 805, + "gospel": 806, "gossip": 807, "govern": 808, "gown": 809, "grab": 810, + "grace": 811, "grain": 812, "grant": 813, "grape": 814, "grass": 815, + "gravity": 816, "great": 817, "green": 818, "grid": 819, "grief": 820, + "grit": 821, "grocery": 822, "group": 823, "grow": 824, "grunt": 825, + "guard": 826, "guess": 827, "guide": 828, "guilt": 829, "guitar": 830, + "gun": 831, "gym": 832, "habit": 833, "hair": 834, "half": 835, + "hammer": 836, "hamster": 837, "hand": 838, "happy": 839, "harbor": 840, + "hard": 841, "harsh": 842, "harvest": 843, "hat": 844, "have": 845, + "hawk": 846, "hazard": 847, "head": 848, "health": 849, "heart": 850, + "heavy": 851, "hedgehog": 852, "height": 853, "hello": 854, "helmet": 855, + "help": 856, "hen": 857, "hero": 858, "hidden": 859, "high": 860, + "hill": 861, "hint": 862, "hip": 863, "hire": 864, "history": 865, + "hobby": 866, "hockey": 867, "hold": 868, "hole": 869, "holiday": 870, + "hollow": 871, "home": 872, "honey": 873, "hood": 874, "hope": 875, + "horn": 876, "horror": 877, "horse": 878, "hospital": 879, "host": 880, + "hotel": 881, "hour": 882, "hover": 883, "hub": 884, "huge": 885, + "human": 886, "humble": 887, "humor": 888, "hundred": 889, "hungry": 890, + "hunt": 891, "hurdle": 892, "hurry": 893, "hurt": 894, "husband": 895, + "hybrid": 896, "ice": 897, "icon": 898, "idea": 899, "identify": 900, + "idle": 901, "ignore": 902, "ill": 903, "illegal": 904, "illness": 905, + "image": 906, "imitate": 907, "immense": 908, "immune": 909, "impact": 910, + "impose": 911, "improve": 912, "impulse": 913, "inch": 914, "include": 915, + "income": 916, "increase": 917, "index": 918, "indicate": 919, "indoor": 920, + "industry": 921, "infant": 922, "inflict": 923, "inform": 924, "inhale": 925, + "inherit": 926, "initial": 927, "inject": 928, "injury": 929, "inmate": 930, + "inner": 931, "innocent": 932, "input": 933, "inquiry": 934, "insane": 935, + "insect": 936, "inside": 937, "inspire": 938, "install": 939, "intact": 940, + "interest": 941, "into": 942, "invest": 943, "invite": 944, "involve": 945, + "iron": 946, "island": 947, "isolate": 948, "issue": 949, "item": 950, + "ivory": 951, "jacket": 952, "jaguar": 953, "jar": 954, "jazz": 955, + "jealous": 956, "jeans": 957, "jelly": 958, "jewel": 959, "job": 960, + "join": 961, "joke": 962, "journey": 963, "joy": 964, "judge": 965, + "juice": 966, "jump": 967, "jungle": 968, "junior": 969, "junk": 970, + "just": 971, "kangaroo": 972, "keen": 973, "keep": 974, "ketchup": 975, + "key": 976, "kick": 977, "kid": 978, "kidney": 979, "kind": 980, + "kingdom": 981, "kiss": 982, "kit": 983, "kitchen": 984, "kite": 985, + "kitten": 986, "kiwi": 987, "knee": 988, "knife": 989, "knock": 990, + "know": 991, "lab": 992, "label": 993, "labor": 994, "ladder": 995, + "lady": 996, "lake": 997, "lamp": 998, "language": 999, "laptop": 1000, + "large": 1001, "later": 1002, "latin": 1003, "laugh": 1004, "laundry": 1005, + "lava": 1006, "law": 1007, "lawn": 1008, "lawsuit": 1009, "layer": 1010, + "lazy": 1011, "leader": 1012, "leaf": 1013, "learn": 1014, "leave": 1015, + "lecture": 1016, "left": 1017, "leg": 1018, "legal": 1019, "legend": 1020, + "leisure": 1021, "lemon": 1022, "lend": 1023, "length": 1024, "lens": 1025, + "leopard": 1026, "lesson": 1027, "letter": 1028, "level": 1029, "liar": 1030, + "liberty": 1031, "library": 1032, "license": 1033, "life": 1034, "lift": 1035, + "light": 1036, "like": 1037, "limb": 1038, "limit": 1039, "link": 1040, + "lion": 1041, "liquid": 1042, "list": 1043, "little": 1044, "live": 1045, + "lizard": 1046, "load": 1047, "loan": 1048, "lobster": 1049, "local": 1050, + "lock": 1051, "logic": 1052, "lonely": 1053, "long": 1054, "loop": 1055, + "lottery": 1056, "loud": 1057, "lounge": 1058, "love": 1059, "loyal": 1060, + "lucky": 1061, "luggage": 1062, "lumber": 1063, "lunar": 1064, "lunch": 1065, + "luxury": 1066, "lyrics": 1067, "machine": 1068, "mad": 1069, "magic": 1070, + "magnet": 1071, "maid": 1072, "mail": 1073, "main": 1074, "major": 1075, + "make": 1076, "mammal": 1077, "man": 1078, "manage": 1079, "mandate": 1080, + "mango": 1081, "mansion": 1082, "manual": 1083, "maple": 1084, "marble": 1085, + "march": 1086, "margin": 1087, "marine": 1088, "market": 1089, "marriage": 1090, + "mask": 1091, "mass": 1092, "master": 1093, "match": 1094, "material": 1095, + "math": 1096, "matrix": 1097, "matter": 1098, "maximum": 1099, "maze": 1100, + "meadow": 1101, "mean": 1102, "measure": 1103, "meat": 1104, "mechanic": 1105, + "medal": 1106, "media": 1107, "melody": 1108, "melt": 1109, "member": 1110, + "memory": 1111, "mention": 1112, "menu": 1113, "mercy": 1114, "merge": 1115, + "merit": 1116, "merry": 1117, "mesh": 1118, "message": 1119, "metal": 1120, + "method": 1121, "middle": 1122, "midnight": 1123, "milk": 1124, "million": 1125, + "mimic": 1126, "mind": 1127, "minimum": 1128, "minor": 1129, "minute": 1130, + "miracle": 1131, "mirror": 1132, "misery": 1133, "miss": 1134, "mistake": 1135, + "mix": 1136, "mixed": 1137, "mixture": 1138, "mobile": 1139, "model": 1140, + "modify": 1141, "mom": 1142, "moment": 1143, "monitor": 1144, "monkey": 1145, + "monster": 1146, "month": 1147, "moon": 1148, "moral": 1149, "more": 1150, + "morning": 1151, "mosquito": 1152, "mother": 1153, "motion": 1154, "motor": 1155, + "mountain": 1156, "mouse": 1157, "move": 1158, "movie": 1159, "much": 1160, + "muffin": 1161, "mule": 1162, "multiply": 1163, "muscle": 1164, "museum": 1165, + "mushroom": 1166, "music": 1167, "must": 1168, "mutual": 1169, "myself": 1170, + "mystery": 1171, "myth": 1172, "naive": 1173, "name": 1174, "napkin": 1175, + "narrow": 1176, "nasty": 1177, "nation": 1178, "nature": 1179, "near": 1180, + "neck": 1181, "need": 1182, "negative": 1183, "neglect": 1184, "neither": 1185, + "nephew": 1186, "nerve": 1187, "nest": 1188, "net": 1189, "network": 1190, + "neutral": 1191, "never": 1192, "news": 1193, "next": 1194, "nice": 1195, + "night": 1196, "noble": 1197, "noise": 1198, "nominee": 1199, "noodle": 1200, + "normal": 1201, "north": 1202, "nose": 1203, "notable": 1204, "note": 1205, + "nothing": 1206, "notice": 1207, "novel": 1208, "now": 1209, "nuclear": 1210, + "number": 1211, "nurse": 1212, "nut": 1213, "oak": 1214, "obey": 1215, + "object": 1216, "oblige": 1217, "obscure": 1218, "observe": 1219, "obtain": 1220, + "obvious": 1221, "occur": 1222, "ocean": 1223, "october": 1224, "odor": 1225, + "off": 1226, "offer": 1227, "office": 1228, "often": 1229, "oil": 1230, + "okay": 1231, "old": 1232, "olive": 1233, "olympic": 1234, "omit": 1235, + "once": 1236, "one": 1237, "onion": 1238, "online": 1239, "only": 1240, + "open": 1241, "opera": 1242, "opinion": 1243, "oppose": 1244, "option": 1245, + "orange": 1246, "orbit": 1247, "orchard": 1248, "order": 1249, "ordinary": 1250, + "organ": 1251, "orient": 1252, "original": 1253, "orphan": 1254, "ostrich": 1255, + "other": 1256, "outdoor": 1257, "outer": 1258, "output": 1259, "outside": 1260, + "oval": 1261, "oven": 1262, "over": 1263, "own": 1264, "owner": 1265, + "oxygen": 1266, "oyster": 1267, "ozone": 1268, "pact": 1269, "paddle": 1270, + "page": 1271, "pair": 1272, "palace": 1273, "palm": 1274, "panda": 1275, + "panel": 1276, "panic": 1277, "panther": 1278, "paper": 1279, "parade": 1280, + "parent": 1281, "park": 1282, "parrot": 1283, "party": 1284, "pass": 1285, + "patch": 1286, "path": 1287, "patient": 1288, "patrol": 1289, "pattern": 1290, + "pause": 1291, "pave": 1292, "payment": 1293, "peace": 1294, "peanut": 1295, + "pear": 1296, "peasant": 1297, "pelican": 1298, "pen": 1299, "penalty": 1300, + "pencil": 1301, "people": 1302, "pepper": 1303, "perfect": 1304, "permit": 1305, + "person": 1306, "pet": 1307, "phone": 1308, "photo": 1309, "phrase": 1310, + "physical": 1311, "piano": 1312, "picnic": 1313, "picture": 1314, "piece": 1315, + "pig": 1316, "pigeon": 1317, "pill": 1318, "pilot": 1319, "pink": 1320, + "pioneer": 1321, "pipe": 1322, "pistol": 1323, "pitch": 1324, "pizza": 1325, + "place": 1326, "planet": 1327, "plastic": 1328, "plate": 1329, "play": 1330, + "please": 1331, "pledge": 1332, "pluck": 1333, "plug": 1334, "plunge": 1335, + "poem": 1336, "poet": 1337, "point": 1338, "polar": 1339, "pole": 1340, + "police": 1341, "pond": 1342, "pony": 1343, "pool": 1344, "popular": 1345, + "portion": 1346, "position": 1347, "possible": 1348, "post": 1349, "potato": 1350, + "pottery": 1351, "poverty": 1352, "powder": 1353, "power": 1354, "practice": 1355, + "praise": 1356, "predict": 1357, "prefer": 1358, "prepare": 1359, "present": 1360, + "pretty": 1361, "prevent": 1362, "price": 1363, "pride": 1364, "primary": 1365, + "print": 1366, "priority": 1367, "prison": 1368, "private": 1369, "prize": 1370, + "problem": 1371, "process": 1372, "produce": 1373, "profit": 1374, "program": 1375, + "project": 1376, "promote": 1377, "proof": 1378, "property": 1379, "prosper": 1380, + "protect": 1381, "proud": 1382, "provide": 1383, "public": 1384, "pudding": 1385, + "pull": 1386, "pulp": 1387, "pulse": 1388, "pumpkin": 1389, "punch": 1390, + "pupil": 1391, "puppy": 1392, "purchase": 1393, "purity": 1394, "purpose": 1395, + "purse": 1396, "push": 1397, "put": 1398, "puzzle": 1399, "pyramid": 1400, + "quality": 1401, "quantum": 1402, "quarter": 1403, "question": 1404, "quick": 1405, + "quit": 1406, "quiz": 1407, "quote": 1408, "rabbit": 1409, "raccoon": 1410, + "race": 1411, "rack": 1412, "radar": 1413, "radio": 1414, "rail": 1415, + "rain": 1416, "raise": 1417, "rally": 1418, "ramp": 1419, "ranch": 1420, + "random": 1421, "range": 1422, "rapid": 1423, "rare": 1424, "rate": 1425, + "rather": 1426, "raven": 1427, "raw": 1428, "razor": 1429, "ready": 1430, + "real": 1431, "reason": 1432, "rebel": 1433, "rebuild": 1434, "recall": 1435, + "receive": 1436, "recipe": 1437, "record": 1438, "recycle": 1439, "reduce": 1440, + "reflect": 1441, "reform": 1442, "refuse": 1443, "region": 1444, "regret": 1445, + "regular": 1446, "reject": 1447, "relax": 1448, "release": 1449, "relief": 1450, + "rely": 1451, "remain": 1452, "remember": 1453, "remind": 1454, "remove": 1455, + "render": 1456, "renew": 1457, "rent": 1458, "reopen": 1459, "repair": 1460, + "repeat": 1461, "replace": 1462, "report": 1463, "require": 1464, "rescue": 1465, + "resemble": 1466, "resist": 1467, "resource": 1468, "response": 1469, "result": 1470, + "retire": 1471, "retreat": 1472, "return": 1473, "reunion": 1474, "reveal": 1475, + "review": 1476, "reward": 1477, "rhythm": 1478, "rib": 1479, "ribbon": 1480, + "rice": 1481, "rich": 1482, "ride": 1483, "ridge": 1484, "rifle": 1485, + "right": 1486, "rigid": 1487, "ring": 1488, "riot": 1489, "ripple": 1490, + "risk": 1491, "ritual": 1492, "rival": 1493, "river": 1494, "road": 1495, + "roast": 1496, "robot": 1497, "robust": 1498, "rocket": 1499, "romance": 1500, + "roof": 1501, "rookie": 1502, "room": 1503, "rose": 1504, "rotate": 1505, + "rough": 1506, "round": 1507, "route": 1508, "royal": 1509, "rubber": 1510, + "rude": 1511, "rug": 1512, "rule": 1513, "run": 1514, "runway": 1515, + "rural": 1516, "sad": 1517, "saddle": 1518, "sadness": 1519, "safe": 1520, + "sail": 1521, "salad": 1522, "salmon": 1523, "salon": 1524, "salt": 1525, + "salute": 1526, "same": 1527, "sample": 1528, "sand": 1529, "satisfy": 1530, + "satoshi": 1531, "sauce": 1532, "sausage": 1533, "save": 1534, "say": 1535, + "scale": 1536, "scan": 1537, "scare": 1538, "scatter": 1539, "scene": 1540, + "scheme": 1541, "school": 1542, "science": 1543, "scissors": 1544, "scorpion": 1545, + "scout": 1546, "scrap": 1547, "screen": 1548, "script": 1549, "scrub": 1550, + "sea": 1551, "search": 1552, "season": 1553, "seat": 1554, "second": 1555, + "secret": 1556, "section": 1557, "security": 1558, "seed": 1559, "seek": 1560, + "segment": 1561, "select": 1562, "sell": 1563, "seminar": 1564, "senior": 1565, + "sense": 1566, "sentence": 1567, "series": 1568, "service": 1569, "session": 1570, + "settle": 1571, "setup": 1572, "seven": 1573, "shadow": 1574, "shaft": 1575, + "shallow": 1576, "share": 1577, "shed": 1578, "shell": 1579, "sheriff": 1580, + "shield": 1581, "shift": 1582, "shine": 1583, "ship": 1584, "shiver": 1585, + "shock": 1586, "shoe": 1587, "shoot": 1588, "shop": 1589, "short": 1590, + "shoulder": 1591, "shove": 1592, "shrimp": 1593, "shrug": 1594, "shuffle": 1595, + "shy": 1596, "sibling": 1597, "sick": 1598, "side": 1599, "siege": 1600, + "sight": 1601, "sign": 1602, "silent": 1603, "silk": 1604, "silly": 1605, + "silver": 1606, "similar": 1607, "simple": 1608, "since": 1609, "sing": 1610, + "siren": 1611, "sister": 1612, "situate": 1613, "six": 1614, "size": 1615, + "skate": 1616, "sketch": 1617, "ski": 1618, "skill": 1619, "skin": 1620, + "skirt": 1621, "skull": 1622, "slab": 1623, "slam": 1624, "sleep": 1625, + "slender": 1626, "slice": 1627, "slide": 1628, "slight": 1629, "slim": 1630, + "slogan": 1631, "slot": 1632, "slow": 1633, "slush": 1634, "small": 1635, + "smart": 1636, "smile": 1637, "smoke": 1638, "smooth": 1639, "snack": 1640, + "snake": 1641, "snap": 1642, "sniff": 1643, "snow": 1644, "soap": 1645, + "soccer": 1646, "social": 1647, "sock": 1648, "soda": 1649, "soft": 1650, + "solar": 1651, "soldier": 1652, "solid": 1653, "solution": 1654, "solve": 1655, + "someone": 1656, "song": 1657, "soon": 1658, "sorry": 1659, "sort": 1660, + "soul": 1661, "sound": 1662, "soup": 1663, "source": 1664, "south": 1665, + "space": 1666, "spare": 1667, "spatial": 1668, "spawn": 1669, "speak": 1670, + "special": 1671, "speed": 1672, "spell": 1673, "spend": 1674, "sphere": 1675, + "spice": 1676, "spider": 1677, "spike": 1678, "spin": 1679, "spirit": 1680, + "split": 1681, "spoil": 1682, "sponsor": 1683, "spoon": 1684, "sport": 1685, + "spot": 1686, "spray": 1687, "spread": 1688, "spring": 1689, "spy": 1690, + "square": 1691, "squeeze": 1692, "squirrel": 1693, "stable": 1694, "stadium": 1695, + "staff": 1696, "stage": 1697, "stairs": 1698, "stamp": 1699, "stand": 1700, + "start": 1701, "state": 1702, "stay": 1703, "steak": 1704, "steel": 1705, + "stem": 1706, "step": 1707, "stereo": 1708, "stick": 1709, "still": 1710, + "sting": 1711, "stock": 1712, "stomach": 1713, "stone": 1714, "stool": 1715, + "story": 1716, "stove": 1717, "strategy": 1718, "street": 1719, "strike": 1720, + "strong": 1721, "struggle": 1722, "student": 1723, "stuff": 1724, "stumble": 1725, + "style": 1726, "subject": 1727, "submit": 1728, "subway": 1729, "success": 1730, + "such": 1731, "sudden": 1732, "suffer": 1733, "sugar": 1734, "suggest": 1735, + "suit": 1736, "summer": 1737, "sun": 1738, "sunny": 1739, "sunset": 1740, + "super": 1741, "supply": 1742, "supreme": 1743, "sure": 1744, "surface": 1745, + "surge": 1746, "surprise": 1747, "surround": 1748, "survey": 1749, "suspect": 1750, + "sustain": 1751, "swallow": 1752, "swamp": 1753, "swap": 1754, "swarm": 1755, + "swear": 1756, "sweet": 1757, "swift": 1758, "swim": 1759, "swing": 1760, + "switch": 1761, "sword": 1762, "symbol": 1763, "symptom": 1764, "syrup": 1765, + "system": 1766, "table": 1767, "tackle": 1768, "tag": 1769, "tail": 1770, + "talent": 1771, "talk": 1772, "tank": 1773, "tape": 1774, "target": 1775, + "task": 1776, "taste": 1777, "tattoo": 1778, "taxi": 1779, "teach": 1780, + "team": 1781, "tell": 1782, "ten": 1783, "tenant": 1784, "tennis": 1785, + "tent": 1786, "term": 1787, "test": 1788, "text": 1789, "thank": 1790, + "that": 1791, "theme": 1792, "then": 1793, "theory": 1794, "there": 1795, + "they": 1796, "thing": 1797, "this": 1798, "thought": 1799, "three": 1800, + "thrive": 1801, "throw": 1802, "thumb": 1803, "thunder": 1804, "ticket": 1805, + "tide": 1806, "tiger": 1807, "tilt": 1808, "timber": 1809, "time": 1810, + "tiny": 1811, "tip": 1812, "tired": 1813, "tissue": 1814, "title": 1815, + "toast": 1816, "tobacco": 1817, "today": 1818, "toddler": 1819, "toe": 1820, + "together": 1821, "toilet": 1822, "token": 1823, "tomato": 1824, "tomorrow": 1825, + "tone": 1826, "tongue": 1827, "tonight": 1828, "tool": 1829, "tooth": 1830, + "top": 1831, "topic": 1832, "topple": 1833, "torch": 1834, "tornado": 1835, + "tortoise": 1836, "toss": 1837, "total": 1838, "tourist": 1839, "toward": 1840, + "tower": 1841, "town": 1842, "toy": 1843, "track": 1844, "trade": 1845, + "traffic": 1846, "tragic": 1847, "train": 1848, "transfer": 1849, "trap": 1850, + "trash": 1851, "travel": 1852, "tray": 1853, "treat": 1854, "tree": 1855, + "trend": 1856, "trial": 1857, "tribe": 1858, "trick": 1859, "trigger": 1860, + "trim": 1861, "trip": 1862, "trophy": 1863, "trouble": 1864, "truck": 1865, + "true": 1866, "truly": 1867, "trumpet": 1868, "trust": 1869, "truth": 1870, + "try": 1871, "tube": 1872, "tuition": 1873, "tumble": 1874, "tuna": 1875, + "tunnel": 1876, "turkey": 1877, "turn": 1878, "turtle": 1879, "twelve": 1880, + "twenty": 1881, "twice": 1882, "twin": 1883, "twist": 1884, "two": 1885, + "type": 1886, "typical": 1887, "ugly": 1888, "umbrella": 1889, "unable": 1890, + "unaware": 1891, "uncle": 1892, "uncover": 1893, "under": 1894, "undo": 1895, + "unfair": 1896, "unfold": 1897, "unhappy": 1898, "uniform": 1899, "unique": 1900, + "unit": 1901, "universe": 1902, "unknown": 1903, "unlock": 1904, "until": 1905, + "unusual": 1906, "unveil": 1907, "update": 1908, "upgrade": 1909, "uphold": 1910, + "upon": 1911, "upper": 1912, "upset": 1913, "urban": 1914, "urge": 1915, + "usage": 1916, "use": 1917, "used": 1918, "useful": 1919, "useless": 1920, + "usual": 1921, "utility": 1922, "vacant": 1923, "vacuum": 1924, "vague": 1925, + "valid": 1926, "valley": 1927, "valve": 1928, "van": 1929, "vanish": 1930, + "vapor": 1931, "various": 1932, "vast": 1933, "vault": 1934, "vehicle": 1935, + "velvet": 1936, "vendor": 1937, "venture": 1938, "venue": 1939, "verb": 1940, + "verify": 1941, "version": 1942, "very": 1943, "vessel": 1944, "veteran": 1945, + "viable": 1946, "vibrant": 1947, "vicious": 1948, "victory": 1949, "video": 1950, + "view": 1951, "village": 1952, "vintage": 1953, "violin": 1954, "virtual": 1955, + "virus": 1956, "visa": 1957, "visit": 1958, "visual": 1959, "vital": 1960, + "vivid": 1961, "vocal": 1962, "voice": 1963, "void": 1964, "volcano": 1965, + "volume": 1966, "vote": 1967, "voyage": 1968, "wage": 1969, "wagon": 1970, + "wait": 1971, "walk": 1972, "wall": 1973, "walnut": 1974, "want": 1975, + "warfare": 1976, "warm": 1977, "warrior": 1978, "wash": 1979, "wasp": 1980, + "waste": 1981, "water": 1982, "wave": 1983, "way": 1984, "wealth": 1985, + "weapon": 1986, "wear": 1987, "weasel": 1988, "weather": 1989, "web": 1990, + "wedding": 1991, "weekend": 1992, "weird": 1993, "welcome": 1994, "west": 1995, + "wet": 1996, "whale": 1997, "what": 1998, "wheat": 1999, "wheel": 2000, + "when": 2001, "where": 2002, "whip": 2003, "whisper": 2004, "wide": 2005, + "width": 2006, "wife": 2007, "wild": 2008, "will": 2009, "win": 2010, + "window": 2011, "wine": 2012, "wing": 2013, "wink": 2014, "winner": 2015, + "winter": 2016, "wire": 2017, "wisdom": 2018, "wise": 2019, "wish": 2020, + "witness": 2021, "wolf": 2022, "woman": 2023, "wonder": 2024, "wood": 2025, + "wool": 2026, "word": 2027, "work": 2028, "world": 2029, "worry": 2030, + "worth": 2031, "wrap": 2032, "wreck": 2033, "wrestle": 2034, "wrist": 2035, + "write": 2036, "wrong": 2037, "yard": 2038, "year": 2039, "yellow": 2040, + "you": 2041, "young": 2042, "youth": 2043, "zebra": 2044, "zero": 2045, + "zone": 2046, "zoo": 2047} diff --git a/src/lastseed.go b/src/lastseed.go new file mode 100644 index 0000000..6372296 --- /dev/null +++ b/src/lastseed.go @@ -0,0 +1,42 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "regexp" + "strings" + + "github.com/ottosch/lastseed/src/seed" + "github.com/ottosch/lastseed/src/table" +) + +var ( + spacesRegex = regexp.MustCompile(`\s{2,}`) + reader = bufio.NewReader(os.Stdin) +) + +func main() { + words := readWordsFromCliArgs() + for words == "" { + fmt.Println("Enter seed words: ") + words, _ = reader.ReadString('\n') + } + + words = spacesRegex.ReplaceAllString(strings.TrimSpace(strings.ToLower(words)), " ") + s := seed.NewSeed(words) + + fmt.Println() + + table.DrawSummary(s) + table.DrawResults(s) +} + +func readWordsFromCliArgs() string { + args := os.Args + if len(args) == 1 { + return "" + } + + return strings.Join(args[1:], " ") +} diff --git a/src/seed/result.go b/src/seed/result.go new file mode 100644 index 0000000..d064932 --- /dev/null +++ b/src/seed/result.go @@ -0,0 +1,38 @@ +package seed + +import ( + "encoding/hex" + "math/big" +) + +type Result struct { + bitstring *big.Int + lastWord string +} + +func NewResult(bitstring *big.Int, lastWord string) *Result { + return &Result{bitstring, lastWord} +} + +func (r *Result) Bitstring() *big.Int { + return r.bitstring +} + +func (r *Result) LastWord() string { + return r.lastWord +} + +// Entropy returns entropy in hex +func (r *Result) Entropy(checksumSize uint) string { + wordCount := int(checksumSize) * 3 + + withoutChecksumBits := new(big.Int).Set(r.bitstring) + withoutChecksumBits.Rsh(withoutChecksumBits, checksumSize) + + bitCount := wordCount*11 - int(checksumSize) // entropy doesn't have checksum + byteLen := (bitCount + 7) / 8 + fbs := make([]byte, byteLen) + withoutChecksumBits.FillBytes(fbs) + + return hex.EncodeToString(fbs) +} diff --git a/src/seed/seed.go b/src/seed/seed.go new file mode 100644 index 0000000..c1a2dfe --- /dev/null +++ b/src/seed/seed.go @@ -0,0 +1,83 @@ +package seed + +import ( + "math" + "math/big" + "strings" + + "github.com/ottosch/lastseed/src/bip39" + "github.com/ottosch/lastseed/src/bip39/wordlist" +) + +type Seed struct { + bitstring *big.Int // the bits of the seed words + words []string + results []*Result +} + +func NewSeed(wordsStr string) *Seed { + words := strings.Split(wordsStr, " ") + + bip39.VerifyWordCount(words) + bip39.VerifyValidWords(words) + + s := &Seed{words: words} + s.fillBitstring(words) + + s.calcLastWords() + return s +} + +func (s *Seed) GetWords() []string { + return s.words +} + +func (s *Seed) GetWordCount() int { + return len(s.words) + 1 +} + +func (s *Seed) GetChecksumSize() uint { + return uint(s.GetWordCount() / 3) +} + +func (s *Seed) GetBitstring() *big.Int { + return new(big.Int).Set(s.bitstring) +} + +func (s *Seed) GetResults() []*Result { + return s.results +} + +func (s *Seed) calcLastWords() { + totalWords := int(math.Pow(2, float64(11-s.GetChecksumSize()))) + results := make([]*Result, 0, totalWords) + + for i, bipWord := range wordlist.Wordlist { + fullBitstring := addWordToBitstring(s.GetBitstring(), i) + if bip39.ValidChecksum(fullBitstring, s.GetChecksumSize()) { + results = append(results, NewResult(fullBitstring, bipWord)) + } + } + + s.results = results +} + +// fillBitstring sets the seed bit string according to the words indices +func (s *Seed) fillBitstring(words []string) { + num := new(big.Int) + for _, w := range words { + index := int64(wordlist.WordMap[w]) + indexBig := new(big.Int).SetInt64(index) + num.Or(num.Lsh(num, 11), indexBig) // num << 11 | index + } + + s.bitstring = num +} + +// addWordToBitstring adds the word index to the current bit string +func addWordToBitstring(bitstring *big.Int, wordIndex int) *big.Int { + fullBitstring := new(big.Int).Set(bitstring) + lastWordIndex := new(big.Int).SetInt64(int64(wordIndex)) + fullBitstring.Or(fullBitstring.Lsh(fullBitstring, 11), lastWordIndex) + return fullBitstring +} diff --git a/src/table/constants.go b/src/table/constants.go new file mode 100644 index 0000000..447f203 --- /dev/null +++ b/src/table/constants.go @@ -0,0 +1,42 @@ +package table + +const ( + topLeft = '┌' + topSeparator = '┬' + topRight = '┐' + bottomLeft = '└' + bottomSeparator = '┴' + bottomRight = '┘' + middleLeft = '├' + middleSeparator = '┼' + middleRight = '┤' + vertical = '│' + horizontal = '─' + + wordStr = "Word" + entropyStr = "Entropy" + + ALIGN_LEFT = 1 << iota // 1 << 0 == 1 + ALIGN_CENTER // 1 << 1 == 2 + ALIGN_RIGHT // 1 << 2 == 4 + + BORDER_TOP + BORDER_BOTTOM + BORDER_MIDDLE + TEXT_MIDDLE +) + +var ( + top lineType = 1 + bottom lineType = 2 + middle lineType = 3 + textMiddle lineType = 4 + + alignLeft alignType = 1 + alignCenter alignType = 2 + alignRight alignType = 3 +) + +type lineType int + +type alignType int diff --git a/src/table/grid.go b/src/table/grid.go new file mode 100644 index 0000000..4579641 --- /dev/null +++ b/src/table/grid.go @@ -0,0 +1,74 @@ +package table + +import ( + "log" + "strings" +) + +// TableCell a single table cell and its text +type TableCell struct { + text string +} + +// NewCell creates a table cell, content aligned +func NewCell(text string, length int, align *TextAlign) *TableCell { + cellText := align.AlignText(text, length) + return &TableCell{text: cellText} +} + +// TableRow a table row, composed of various, styled cells +type TableRow struct { + cells []*TableCell + style *lineStyle +} + +// TextRow creates a row of text cells +func TextRow(texts []string, sizes []int, settings int) *TableRow { + if len(texts) != len(sizes) || len(texts)%2 != 0 { + log.Fatalf("invalid number of columns: %d, %d\n", len(texts), len(sizes)) + } + + cellSettings := parseSettings(settings) + cells := make([]*TableCell, len(texts)) + + for i, text := range texts { + cells[i] = NewCell(text, sizes[i], cellSettings.Align) + } + + return &TableRow{cells: cells, style: cellSettings.Style} +} + +// GridRow creates a row of grid (border) content only +func GridRow(sizes []int, settings int) *TableRow { + if len(sizes)%2 != 0 { + log.Fatalf("invalid number of grid cells: %v\n", sizes) + } + + cellSettings := parseSettings(settings) + cells := make([]*TableCell, len(sizes)) + + for i, size := range sizes { + cells[i] = &TableCell{text: strings.Repeat(string(horizontal), size)} + } + + return &TableRow{cells: cells, style: cellSettings.Style} +} + +func (g *TableRow) String() string { + var result strings.Builder + + strList := make([]string, 0, len(g.cells)/2) + for i := 0; i < len(g.cells); i += 2 { + var sb strings.Builder + sb.WriteRune(g.style.leftBorder) + sb.WriteString(g.cells[i].text) + sb.WriteRune(g.style.separator) + sb.WriteString(g.cells[i+1].text) + sb.WriteRune(g.style.rightBorder) + strList = append(strList, sb.String()) + } + + result.WriteString(strings.Join(strList, " ")) + result.WriteRune('\n') + return result.String() +} diff --git a/src/table/results-table.go b/src/table/results-table.go new file mode 100644 index 0000000..6bf61a0 --- /dev/null +++ b/src/table/results-table.go @@ -0,0 +1,84 @@ +package table + +import ( + "fmt" + "strings" + + "github.com/ottosch/lastseed/src/seed" +) + +// DrawSummary prints the results table +func DrawResults(s *seed.Seed) { + tableCount, linesPerTable := getTableCountAndHeight(s) + data := make([][]*seed.Result, 0, tableCount) + + results := s.GetResults() + var i int + for i < len(results) { + data = append(data, results[i:i+linesPerTable]) + i += linesPerTable + } + + colSizes := getResultsColSizes(s.GetChecksumSize(), tableCount) + + table := make([]*TableRow, 0) + table = append(table, GridRow(colSizes, BORDER_TOP)) + + headers := make([]string, len(data)*2) + for i := 0; i < len(headers); i += 2 { + headers[i] = wordStr + headers[i+1] = entropyStr + } + + table = append(table, TextRow(headers, colSizes, ALIGN_CENTER|TEXT_MIDDLE)) + table = append(table, GridRow(colSizes, BORDER_MIDDLE)) + + limit := len(data[0]) + for i := 0; i < limit; i++ { + bodyCells := make([]string, 0, len(data)*2) + for j := 0; j < len(data); j++ { + bodyCells = append(bodyCells, data[j][i].LastWord()) + bodyCells = append(bodyCells, data[j][i].Entropy(s.GetChecksumSize())) + } + + table = append(table, TextRow(bodyCells, colSizes, ALIGN_LEFT|TEXT_MIDDLE)) + } + + table = append(table, GridRow(colSizes, BORDER_BOTTOM)) + + var sb strings.Builder + for _, tt := range table { + sb.WriteString(tt.String()) + } + + fmt.Println(sb.String()) +} + +func getTableCountAndHeight(s *seed.Seed) (tables int, linesPerTable int) { + switch s.GetWordCount() { + case 12: + tables = 4 + case 15, 18: + tables = 2 + default: + tables = 1 + } + + linesPerTable = len(s.GetResults()) / tables + return +} + +func getResultsColSizes(checksumSize uint, tableCount int) []int { + const maxWordLength = 8 + const colPadding = 2 + wordColLength := maxWordLength + colPadding + entropyColLength := 8*int(checksumSize) + colPadding + + colSizes := make([]int, tableCount*2) + for i := 0; i < len(colSizes); i += 2 { + colSizes[i] = wordColLength + colSizes[i+1] = entropyColLength + } + + return colSizes +} diff --git a/src/table/settings.go b/src/table/settings.go new file mode 100644 index 0000000..d455218 --- /dev/null +++ b/src/table/settings.go @@ -0,0 +1,55 @@ +package table + +import "log" + +type CellSettings struct { + Align *TextAlign + Style *lineStyle +} + +func parseSettings(settings int) *CellSettings { + validateSettings(settings) + + cs := &CellSettings{Align: &TextAlign{alignLeft}, Style: getLineStyle(textMiddle)} + + switch { + case settings&ALIGN_CENTER != 0: + cs.Align = &TextAlign{alignCenter} + case settings&ALIGN_RIGHT != 0: + cs.Align = &TextAlign{alignRight} + } + + switch { + case settings&BORDER_TOP != 0: + cs.Style = getLineStyle(top) + case settings&BORDER_BOTTOM != 0: + cs.Style = getLineStyle(bottom) + case settings&BORDER_MIDDLE != 0: + cs.Style = getLineStyle(middle) + } + + return cs +} + +func validateSettings(settings int) { + if !valid(settings, ALIGN_LEFT|ALIGN_CENTER|ALIGN_RIGHT) { + log.Fatalf("invalid alignment: %d\n", settings) + } + + if !valid(settings, BORDER_TOP|BORDER_BOTTOM|BORDER_MIDDLE|TEXT_MIDDLE) { + log.Fatalf("invalid border: %d\n", settings) + } +} + +func valid(settings int, fullMask int) bool { + propBits := settings & fullMask // isolates property bits + somePropSet := propBits != 0 // check if some property bit is on + + if somePropSet { + flippedBits := propBits - 1 // flips property bits to get only 1's + commonBitsFound := propBits&flippedBits != 0 // more than 1 bit is set + return !commonBitsFound + } + + return true +} diff --git a/src/table/style.go b/src/table/style.go new file mode 100644 index 0000000..93c9636 --- /dev/null +++ b/src/table/style.go @@ -0,0 +1,46 @@ +package table + +import ( + "strings" +) + +// TextAlign contains a text align of alignType +type TextAlign struct { + align alignType +} + +// AlignText aligns the text, padding it with size and according to alignType +func (ta *TextAlign) AlignText(text string, size int) string { + padding := size - len(text) + switch ta.align { + case alignLeft: + return " " + text + strings.Repeat(" ", padding-1) + case alignCenter: + spacesLeft := strings.Repeat(" ", padding/2) + spacesRight := strings.Repeat(" ", padding-len(spacesLeft)) + return spacesLeft + text + spacesRight + default: + return strings.Repeat(" ", padding-1) + text + " " + } +} + +// lineStyle style of table row, with borders and between-cell separator +type lineStyle struct { + leftBorder rune + separator rune + rightBorder rune +} + +// getLineStyle returns the suitable grid runes for the lineType +func getLineStyle(t lineType) *lineStyle { + switch t { + case top: + return &lineStyle{leftBorder: topLeft, separator: topSeparator, rightBorder: topRight} + case middle: + return &lineStyle{leftBorder: middleLeft, separator: middleSeparator, rightBorder: middleRight} + case textMiddle: + return &lineStyle{leftBorder: vertical, separator: vertical, rightBorder: vertical} + default: + return &lineStyle{leftBorder: bottomLeft, separator: bottomSeparator, rightBorder: bottomRight} + } +} diff --git a/src/table/summary-table.go b/src/table/summary-table.go new file mode 100644 index 0000000..e07829c --- /dev/null +++ b/src/table/summary-table.go @@ -0,0 +1,110 @@ +package table + +import ( + "fmt" + "math" + "strings" + + "github.com/ottosch/lastseed/src/seed" +) + +// DrawSummary prints a summary table +func DrawSummary(s *seed.Seed) { + colSizes := getSummaryColSizes(s.GetChecksumSize()) + + table := make([]*TableRow, 0) + table = append(table, GridRow(colSizes, BORDER_TOP)) + + table = append(table, + TextRow( + []string{ + fmt.Sprintf("%d words input", len(s.GetWords())), + fmt.Sprintf("%d words total", s.GetWordCount()), + }, + colSizes, ALIGN_CENTER, + ), + ) + + table = append(table, + TextRow( + []string{ + fmt.Sprintf("%d bits input", len(s.GetWords())*11), + fmt.Sprintf("%d bits total", s.GetWordCount()*11), + }, colSizes, ALIGN_CENTER, + ), + ) + + table = append(table, + TextRow( + []string{ + fmt.Sprintf("%d bits entropy", uint(s.GetWordCount()*11)-s.GetChecksumSize()), + fmt.Sprintf("%d bits checksum", s.GetChecksumSize()), + }, colSizes, ALIGN_CENTER, + ), + ) + + totalWords := int(math.Pow(2, float64(11-s.GetChecksumSize()))) + table = append(table, + TextRow( + []string{ + fmt.Sprintf("%d missing bits", 11-s.GetChecksumSize()), + fmt.Sprintf("%d possibilities", totalWords), + }, colSizes, ALIGN_CENTER, + ), + ) + + pad := getPaddingString(s, colSizes) + table = append(table, GridRow(colSizes, BORDER_BOTTOM)) + + var sb strings.Builder + for _, tt := range table { + sb.WriteString(pad) + sb.WriteString(tt.String()) + } + + fmt.Println(sb.String()) +} + +func getSummaryColSizes(checksumSize uint) []int { + const maxWordLength = 8 + const wordCol = maxWordLength + 2 // leading and traili ang space + entropyCol := int(8*checksumSize) + 2 // use entropy to make this table the same width as one of words table + lineLength := wordCol + entropyCol + + colSizes := make([]int, 2) + colSizes[0] = (lineLength) / 2 + colSizes[1] = lineLength - colSizes[0] + + return colSizes +} + +// getPaddingString returns a string to centralise the summary table +func getPaddingString(s *seed.Seed, colSizes []int) string { + if s.GetWordCount() >= 21 { + return "" + } + + // get result table size + const maxWordLength = 8 + const wordCol = maxWordLength + 2 + entropyCol := int(8*s.GetChecksumSize()) + 2 + + var tableCount int + if s.GetWordCount() == 12 { + tableCount = 4 + } else { + tableCount = 2 + } + + gridBorders := 3 + tableMargins := tableCount - 1 + + size := (wordCol+entropyCol+gridBorders)*tableCount + tableMargins + + // summary table size + totalColSize := colSizes[0] + colSizes[1] + gridBorders + + centerPosition := (size - totalColSize) / 2 + return strings.Repeat(" ", centerPosition) + +}