root / trunk / twisted / python / otp.py

Revision 25457, 25.0 kB (checked in by exarkun, 8 months ago)

Merge hashlib-2763-3

Author: wsanchez, exarkun
Reviewer: exarkun, mwhudson
Fixes: #2763

Replace uses of md5 and sha modules in Twisted with use of a new twisted.python.hashlib
module which transparently uses the new hashlib standard library module if it is available
or falls back to md5 and sha if not.

Line 
1 # -*- test-case-name: twisted.python.test.test_otp -*-
2 # Copyright (c) 2001-2008 Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5 """
6 A One-Time Password System based on RFC 2289
7
8 The class Authenticator contains the hashing-logic, and the parser for the
9 readable output. It also contains challenge which returns a string describing
10 the authentication scheme for a client.
11
12 OTP is a password container for an user on a server.
13
14 NOTE: Does not take care of transmitting the shared secret password.
15
16 At the end there's a dict called dict which is dictionary contain 2048
17 words for storing pronouncable 11-bit values. Taken from RFC 1760.
18
19 Uses the MD5- and SHA-algorithms for hashing
20
21 Todo: RFC2444, SASL (perhaps), parsing hex-responses
22
23 This module is deprecated.  Consider using U{another Python OTP
24 library<http://labix.org/python-otp>} instead.
25 """
26
27 import warnings
28 import string
29 import random
30
31 warnings.warn(
32     "twisted.python.otp is deprecated since Twisted 8.3.",
33     category=DeprecationWarning,
34     stacklevel=2)
35
36
37 def stringToLong(s):
38     """ Convert digest to long """
39     result = 0L
40     for byte in s:
41         result = (256 * result) + ord(byte)
42     return result
43
44 def stringToDWords(s):
45     """ Convert digest to a list of four 32-bits words """
46     result = []
47     for a in xrange(len(s) / 4):
48         tmp = 0L
49         for byte in s[-4:]:
50             tmp = (256 * tmp) + ord(byte)
51         result.append(tmp)
52         s = s[:-4]
53     return result
54
55 def longToString(l):
56     """ Convert long to digest """
57     result = ""
58     while l > 0L:
59         result = chr(l % 256) + result
60         l = l / 256L
61     return result
62
63 from twisted.python.hashlib import md5, sha1
64 hashid = {md5: 'md5', sha1: 'sha1'}
65
66 INITIALSEQUENCE = 1000
67 MINIMUMSEQUENCE = 50
68
69 class Unauthorized(Exception):
70     """the Unauthorized exception
71
72     This exception is raised when an action is not allowed, or a user is not
73     authenticated properly.
74     """
75
76 class OTPAuthenticator:
77     """
78     A One Time Password System
79
80     Based on RFC 2289, which is based on a the S/KEY Authentication-scheme.
81     It uses the MD5- and SHA-algorithms for hashing
82
83     The variable OTP is at all times a 64bit string.
84
85     @ivar hash: An object which can be used to compute hashes.  This is either
86         L{md5} or L{sha1}.
87     """
88     def __init__(self, hash = md5):
89         "Set the hash to either md5 or sha1"
90         self.hash = hash
91
92
93     def generateSeed(self):
94         "Return a 10 char random seed, with 6 lowercase chars and 4 digits"
95         seed = ''
96         for x in range(6):
97             seed = seed + chr(random.randrange(97,122))
98         for x in range(4):
99             seed = seed + chr(random.randrange(48,57))
100         return seed
101
102     def foldDigest(self, otp):
103         if self.hash == md5:
104             return self.foldDigest128(otp)
105         if self.hash == sha1:
106             return self.foldDigest160(otp)
107
108     def foldDigest128(self, otp128):
109         "Fold a 128 bit digest to 64 bit"
110         regs = stringToDWords(otp128)
111         p0 = regs[0] ^ regs[2]
112         p1 = regs[1] ^ regs[3]
113         S = ''
114         for a in xrange(4):
115             S = chr(p0 & 0xFF) + S
116             p0 = p0 >> 8
117         for a in xrange(4):
118             S = chr(p1 & 0xFF) + S
119             p1 = p1 >> 8
120         return S
121
122     def foldDigest160(self, otp160):
123         "Fold a 160 bit digest to 64 bit"
124         regs = stringToDWords(otp160)
125         p0 = regs[0] ^ regs[2]
126         p1 = regs[1] ^ regs[3]
127         p0 = regs[0] ^ regs[4]
128         S = ''
129         for a in xrange(4):
130             S = chr(p0 & 0xFF) + S
131             p0 = p0 >> 8
132         for a in xrange(4):
133             S = chr(p1 & 0xFF) + S
134             p1 = p1 >> 8
135         return S
136
137     def hashUpdate(self, digest):
138         "Run through the hash and fold to 64 bit"
139         h = self.hash(digest)
140         return self.foldDigest(h.digest())
141
142     def generateOTP(self, seed, passwd, sequence):
143         """Return a 64 bit OTP based on inputs
144         Run through makeReadable to get a 6 word pass-phrase"""
145         seed = string.lower(seed)
146         otp = self.hashUpdate(seed + passwd)
147         for a in xrange(sequence):
148             otp = self.hashUpdate(otp)
149         return otp
150
151     def calculateParity(self, otp):
152         "Calculate the parity from a 64bit OTP"
153         parity = 0
154         for i in xrange(0, 64, 2):
155             parity = parity + otp & 0x3
156             otp = otp >> 2
157         return parity
158
159     def makeReadable(self, otp):
160         "Returns a 6 word pass-phrase from a 64bit OTP"
161         digest = stringToLong(otp)
162         list = []
163         parity = self.calculateParity(digest)
164         for i in xrange(4,-1, -1):
165             list.append(dict[(digest >> (i * 11 + 9)) & 0x7FF])
166         list.append(dict[(digest << 2) & 0x7FC | (parity & 0x03)])
167         return string.join(list)
168
169     def challenge(self, seed, sequence):
170         """Return a challenge in the format otp-<hash> <sequence> <seed>"""
171         return "otp-%s %i %s" % (hashid[self.hash], sequence, seed)
172
173     def parsePhrase(self, phrase):
174         """Decode the phrase, and return a 64bit OTP
175         I will raise Unauthorized if the parity is wrong
176         TODO: Add support for hex (MUST) and the '2nd scheme'(SHOULD)"""
177         words = string.split(phrase)
178         for i in xrange(len(words)):
179             words[i] = string.upper(words[i])
180         b = 0L
181         for i in xrange(0,5):
182             b = b | ((long(dict.index(words[i])) << ((4-i)*11L+9L)))
183         tmp = dict.index(words[5])
184         b = b | (tmp & 0x7FC ) >> 2
185         if (tmp & 3) <> self.calculateParity(b):
186             raise Unauthorized("Parity error")
187         digest = longToString(b)
188         return digest
189
190 class OTP(OTPAuthenticator):
191     """An automatic version of the OTP-Authenticator
192
193     Updates the sequence and the keeps last approved password on success
194     On the next authentication, the stored password is hashed and checked
195     up against the one given by the user. If they match, the sequencecounter
196     is decreased and the circle is closed.
197
198     This object should be glued to each user
199
200     Note:
201     It does NOT reset the sequence when the combinations left approach zero,
202     This has to be done manuelly by instancing a new object
203     """
204     seed = None
205     sequence = 0
206     lastotp = None
207
208     def __init__(self, passwd, sequence = INITIALSEQUENCE, hash=md5):
209         """Initialize the OTP-Sequence, and discard the password"""
210         OTPAuthenticator.__init__(self, hash)
211         seed = self.generateSeed()
212         # Generate the 'last' password
213         self.lastotp = OTPAuthenticator.generateOTP(self, seed, passwd, sequence+1)
214         self.seed = seed
215         self.sequence = sequence
216
217     def challenge(self):
218         """Return a challenge string"""
219         result = OTPAuthenticator.challenge(self, self.seed, self.sequence)
220         return result
221
222     def authenticate(self, phrase):
223         """Test the phrase against the last challenge issued"""
224         try:
225             digest = self.parsePhrase(phrase)
226             hasheddigest = self.hashUpdate(digest)
227             if (self.lastotp == hasheddigest):
228                 self.lastotp = digest
229                 if self.sequence > MINIMUMSEQUENCE:
230                     self.sequence = self.sequence - 1
231                 return "ok"
232             else:
233                 raise Unauthorized("Failed")
234         except Unauthorized, msg:
235             raise Unauthorized(msg)
236
237 #
238 # The 2048 word standard dictionary from RFC 1760
239 #
240 dict =  ["A",     "ABE",   "ACE",   "ACT",   "AD",    "ADA",   "ADD",
241 "AGO",   "AID",   "AIM",   "AIR",   "ALL",   "ALP",   "AM",    "AMY",
242 "AN",    "ANA",   "AND",   "ANN",   "ANT",   "ANY",   "APE",   "APS",
243 "APT",   "ARC",   "ARE",   "ARK",   "ARM",   "ART",   "AS",    "ASH",
244 "ASK",   "AT",    "ATE",   "AUG",   "AUK",   "AVE",   "AWE",   "AWK",
245 "AWL",   "AWN",   "AX",    "AYE",   "BAD",   "BAG",   "BAH",   "BAM",
246 "BAN",   "BAR",   "BAT",   "BAY",   "BE",    "BED",   "BEE",   "BEG",
247 "BEN",   "BET",   "BEY",   "BIB",   "BID",   "BIG",   "BIN",   "BIT",
248 "BOB",   "BOG",   "BON",   "BOO",   "BOP",   "BOW",   "BOY",   "BUB",
249 "BUD",   "BUG",   "BUM",   "BUN",   "BUS",   "BUT",   "BUY",   "BY",
250 "BYE",   "CAB",   "CAL",   "CAM",   "CAN",   "CAP",   "CAR",   "CAT",
251 "CAW",   "COD",   "COG",   "COL",   "CON",   "COO",   "COP",   "COT",
252 "COW",   "COY",   "CRY",   "CUB",   "CUE",   "CUP",   "CUR",   "CUT",
253 "DAB",   "DAD",   "DAM",   "DAN",   "DAR",   "DAY",   "DEE",   "DEL",
254 "DEN",   "DES",   "DEW",   "DID",   "DIE",   "DIG",   "DIN",   "DIP",
255 "DO",    "DOE",   "DOG",   "DON",   "DOT",   "DOW",   "DRY",   "DUB",
256 "DUD",   "DUE",   "DUG",   "DUN",   "EAR",   "EAT",   "ED",    "EEL",
257 "EGG",   "EGO",   "ELI",   "ELK",   "ELM",   "ELY",   "EM",    "END",
258 "EST",   "ETC",   "EVA",   "EVE",   "EWE",   "EYE",   "FAD",   "FAN",
259 "FAR",   "FAT",   "FAY",   "FED",   "FEE",   "FEW",   "FIB",   "FIG",
260 "FIN",   "FIR",   "FIT",   "FLO",   "FLY",   "FOE",   "FOG",   "FOR",
261 "FRY",   "FUM",   "FUN",   "FUR",   "GAB",   "GAD",   "GAG",   "GAL",
262 "GAM",   "GAP",   "GAS",   "GAY",   "GEE",   "GEL",   "GEM",   "GET",
263 "GIG",   "GIL",   "GIN",   "GO",    "GOT",   "GUM",   "GUN",   "GUS",
264 "GUT",   "GUY",   "GYM",   "GYP",   "HA",    "HAD",   "HAL",   "HAM",
265 "HAN",   "HAP",   "HAS",   "HAT",   "HAW",   "HAY",   "HE",    "HEM",
266 "HEN",   "HER",   "HEW",   "HEY",   "HI",    "HID",   "HIM",   "HIP",
267 "HIS",   "HIT",   "HO",    "HOB",   "HOC",   "HOE",   "HOG",   "HOP",
268 "HOT",   "HOW",   "HUB",   "HUE",   "HUG",   "HUH",   "HUM",   "HUT",
269 "I",     "ICY",   "IDA",   "IF",    "IKE",   "ILL",   "INK",   "INN",
270 "IO",    "ION",   "IQ",    "IRA",   "IRE",   "IRK",   "IS",    "IT",
271 "ITS",   "IVY",   "JAB",   "JAG",   "JAM",   "JAN",   "JAR",   "JAW",
272 "JAY",   "JET",   "JIG",   "JIM",   "JO",    "JOB",   "JOE",   "JOG",
273 "JOT",   "JOY",   "JUG",   "JUT",   "KAY",   "KEG",   "KEN",   "KEY",
274 "KID",   "KIM",   "KIN",   "KIT",   "LA",    "LAB",   "LAC",   "LAD",
275 "LAG",   "LAM",   "LAP",   "LAW",   "LAY",   "LEA",   "LED",   "LEE",
276 "LEG",   "LEN",   "LEO",   "LET",   "LEW",   "LID",   "LIE",   "LIN",
277 "LIP",   "LIT",   "LO",    "LOB",   "LOG",   "LOP",   "LOS",   "LOT",
278 "LOU",   "LOW",   "LOY",   "LUG",   "LYE",   "MA",    "MAC",   "MAD",
279 "MAE",   "MAN",   "MAO",   "MAP",   "MAT",   "MAW",   "MAY",   "ME",
280 "MEG",   "MEL",   "MEN",   "MET",   "MEW",   "MID",   "MIN",   "MIT",
281 "MOB",   "MOD",   "MOE",   "MOO",   "MOP",   "MOS",   "MOT",   "MOW",
282 "MUD",   "MUG",   "MUM",   "MY",    "NAB",   "NAG",   "NAN",   "NAP",
283 "NAT",   "NAY",   "NE",    "NED",   "NEE",   "NET",   "NEW",   "NIB",
284 "NIL",   "NIP",   "NIT",   "NO",    "NOB",   "NOD",   "NON",   "NOR",
285 "NOT",   "NOV",   "NOW",   "NU",    "NUN",   "NUT",   "O",     "OAF",
286 "OAK",   "OAR",   "OAT",   "ODD",   "ODE",   "OF",    "OFF",   "OFT",
287 "OH",    "OIL",   "OK",    "OLD",   "ON",    "ONE",   "OR",    "ORB",
288 "ORE",   "ORR",   "OS",    "OTT",   "OUR",   "OUT",   "OVA",   "OW",
289 "OWE",   "OWL",   "OWN",   "OX",    "PA",    "PAD",   "PAL",   "PAM",
290 "PAN",   "PAP",   "PAR",   "PAT",   "PAW",   "PAY",   "PEA",   "PEG",
291 "PEN",   "PEP",   "PER",   "PET",   "PEW",   "PHI",   "PI",    "PIE",
292 "PIN",   "PIT",   "PLY",   "PO",    "POD",   "POE",   "POP",   "POT",
293 "POW",   "PRO",   "PRY",   "PUB",   "PUG",   "PUN",   "PUP",   "PUT",
294 "QUO",   "RAG",   "RAM",   "RAN",   "RAP",   "RAT",   "RAW",   "RAY",
295 "REB",   "RED",   "REP",   "RET",   "RIB",   "RID",   "RIG",   "RIM",
296 "RIO",   "RIP",   "ROB",   "ROD",   "ROE",   "RON",   "ROT",   "ROW",
297 "ROY",   "RUB",   "RUE",   "RUG",   "RUM",   "RUN",   "RYE",   "SAC",
298 "SAD",   "SAG",   "SAL",   "SAM",   "SAN",   "SAP",   "SAT",   "SAW",
299 "SAY",   "SEA",   "SEC",   "SEE",   "SEN",   "SET",   "SEW",   "SHE",
300 "SHY",   "SIN",   "SIP",   "SIR",   "SIS",   "SIT",   "SKI",   "SKY",
301 "SLY",   "SO",    "SOB",   "SOD",   "SON",   "SOP",   "SOW",   "SOY",
302 "SPA",   "SPY",   "SUB",   "SUD",   "SUE",   "SUM",   "SUN",   "SUP",
303 "TAB",   "TAD",   "TAG",   "TAN",   "TAP",   "TAR",   "TEA",   "TED",
304 "TEE",   "TEN",   "THE",   "THY",   "TIC",   "TIE",   "TIM",   "TIN",
305 "TIP",   "TO",    "TOE",   "TOG",   "TOM",   "TON",   "TOO",   "TOP",
306 "TOW",   "TOY",   "TRY",   "TUB",   "TUG",   "TUM",   "TUN",   "TWO",
307 "UN",    "UP",    "US",    "USE",   "VAN",   "VAT",   "VET",   "VIE",
308 "WAD",   "WAG",   "WAR",   "WAS",   "WAY",   "WE",    "WEB",   "WED",
309 "WEE",   "WET",   "WHO",   "WHY",   "WIN",   "WIT",   "WOK",   "WON",
310 "WOO",   "WOW",   "WRY",   "WU",    "YAM",   "YAP",   "YAW",   "YE",
311 "YEA",   "YES",   "YET",   "YOU",   "ABED""ABEL""ABET""ABLE",
312 "ABUT""ACHE""ACID""ACME""ACRE""ACTA""ACTS""ADAM",
313 "ADDS""ADEN""AFAR""AFRO""AGEE""AHEM""AHOY""AIDA",
314 "AIDE""AIDS""AIRY""AJAR""AKIN""ALAN""ALEC""ALGA",
315 "ALIA""ALLY""ALMA""ALOE""ALSO""ALTO""ALUM""ALVA",
316 "AMEN""AMES""AMID""AMMO""AMOK""AMOS""AMRA""ANDY",
317 "ANEW""ANNA""ANNE""ANTE""ANTI""AQUA""ARAB""ARCH",
318 "AREA""ARGO""ARID""ARMY""ARTS""ARTY""ASIA""ASKS",
319 "ATOM""AUNT""AURA""AUTO""AVER""AVID""AVIS""AVON",
320 "AVOW""AWAY""AWRY""BABE""BABY""BACH""BACK""BADE",
321 "BAIL""BAIT""BAKE""BALD""BALE""BALI""BALK""BALL",
322 "BALM""BAND""BANE""BANG""BANK""BARB""BARD""BARE",
323 "BARK""BARN""BARR""BASE""BASH""BASK""BASS""BATE",
324 "BATH""BAWD""BAWL""BEAD""BEAK""BEAM""BEAN""BEAR",
325 "BEAT""BEAU""BECK""BEEF""BEEN""BEER""BEET""BELA",
326 "BELL""BELT""BEND""BENT""BERG""BERN""BERT""BESS",
327 "BEST""BETA""BETH""BHOY""BIAS""BIDE""BIEN""BILE",
328 "BILK""BILL""BIND""BING""BIRD""BITE""BITS""BLAB",
329 "BLAT""BLED""BLEW""BLOB""BLOC""BLOT""BLOW""BLUE",
330 "BLUM""BLUR""BOAR""BOAT""BOCA""BOCK""BODE""BODY",
331 "BOGY""BOHR""BOIL""BOLD""BOLO""BOLT""BOMB""BONA",
332 "BOND""BONE""BONG""BONN""BONY""BOOK""BOOM""BOON",
333 "BOOT""BORE""BORG""BORN""BOSE""BOSS""BOTH""BOUT",
334 "BOWL""BOYD""BRAD""BRAE""BRAG""BRAN""BRAY""BRED",
335 "BREW""BRIG""BRIM""BROW""BUCK""BUDD""BUFF""BULB",
336 "BULK""BULL""BUNK""BUNT""BUOY""BURG""BURL""BURN",
337 "BURR""BURT""BURY""BUSH""BUSS""BUST""BUSY""BYTE",
338 "CADY""CAFE""CAGE""CAIN""CAKE""CALF""CALL""CALM",
339 "CAME""CANE""CANT""CARD""CARE""CARL""CARR""CART",
340 "CASE""CASH""CASK""CAST""CAVE""CEIL""CELL""CENT",
341 "CERN""CHAD""CHAR""CHAT""CHAW""CHEF""CHEN""CHEW",
342 "CHIC""CHIN""CHOU""CHOW""CHUB""CHUG""CHUM""CITE",
343 "CITY""CLAD""CLAM""CLAN""CLAW""CLAY""CLOD""CLOG",
344 "CLOT""CLUB""CLUE""COAL""COAT""COCA""COCK""COCO",
345 "CODA""CODE""CODY""COED""COIL""COIN""COKE""COLA",
346 "COLD""COLT""COMA""COMB""COME""COOK""COOL""COON",
347 "COOT""CORD""CORE""CORK""CORN""COST""COVE""COWL",
348 "CRAB""CRAG""CRAM""CRAY""CREW""CRIB""CROW""CRUD",
349 "CUBA""CUBE""CUFF""CULL""CULT""CUNY""CURB""CURD",
350 "CURE""CURL""CURT""CUTS""DADE""DALE""DAME""DANA",
351 "DANE""DANG""DANK""DARE""DARK""DARN""DART""DASH",
352 "DATA""DATE""DAVE""DAVY""DAWN""DAYS""DEAD""DEAF",
353 "DEAL""DEAN""DEAR""DEBT""DECK""DEED""DEEM""DEER",
354 "DEFT""DEFY""DELL""DENT""DENY""DESK""DIAL""DICE",
355 "DIED""DIET""DIME""DINE""DING""DINT""DIRE""DIRT",
356 "DISC""DISH""DISK""DIVE""DOCK""DOES""DOLE""DOLL",
357 "DOLT""DOME""DONE""DOOM""DOOR""DORA""DOSE""DOTE",
358 "DOUG""DOUR""DOVE""DOWN""DRAB""DRAG""DRAM""DRAW",
359 "DREW""DRUB""DRUG""DRUM""DUAL""DUCK""DUCT""DUEL",
360 "DUET""DUKE""DULL""DUMB""DUNE""DUNK""DUSK""DUST",
361 "DUTY""EACH""EARL""EARN""EASE""EAST""EASY""EBEN",
362 "ECHO""EDDY""EDEN""EDGE""EDGY""EDIT""EDNA""EGAN",
363 "ELAN""ELBA""ELLA""ELSE""EMIL""EMIT""EMMA""ENDS",
364 "ERIC""EROS""EVEN""EVER""EVIL""EYED""FACE""FACT",
365 "FADE""FAIL""FAIN""FAIR""FAKE""FALL""FAME""FANG",
366 "FARM""FAST""FATE""FAWN""FEAR""FEAT""FEED""FEEL",
367 "FEET""FELL""FELT""FEND""FERN""FEST""FEUD""FIEF",
368 "FIGS""FILE""FILL""FILM""FIND""FINE""FINK""FIRE",
369 "FIRM""FISH""FISK""FIST""FITS""FIVE""FLAG""FLAK",
370 "FLAM""FLAT""FLAW""FLEA""FLED""FLEW""FLIT""FLOC",
371 "FLOG""FLOW""FLUB""FLUE""FOAL""FOAM""FOGY""FOIL",
372 "FOLD""FOLK""FOND""FONT""FOOD""FOOL""FOOT""FORD",
373 "FORE""FORK""FORM""FORT""FOSS""FOUL""FOUR""FOWL",
374 "FRAU""FRAY""FRED""FREE""FRET""FREY""FROG""FROM",
375 "FUEL""FULL""FUME""FUND""FUNK""FURY""FUSE""FUSS",
376 "GAFF""GAGE""GAIL""GAIN""GAIT""GALA""GALE""GALL",
377 "GALT""GAME""GANG""GARB""GARY""GASH""GATE""GAUL",
378 "GAUR""GAVE""GAWK""GEAR""GELD""GENE""GENT""GERM",
379 "GETS""GIBE""GIFT""GILD""GILL""GILT""GINA""GIRD",
380 "GIRL""GIST""GIVE""GLAD""GLEE""GLEN""GLIB""GLOB",
381 "GLOM""GLOW""GLUE""GLUM""GLUT""GOAD""GOAL""GOAT",
382 "GOER""GOES""GOLD""GOLF""GONE""GONG""GOOD""GOOF",
383 "GORE""GORY""GOSH""GOUT""GOWN""GRAB""GRAD""GRAY",
384 "GREG""GREW""GREY""GRID""GRIM""GRIN""GRIT""GROW",
385 "GRUB""GULF""GULL""GUNK""GURU""GUSH""GUST""GWEN",
386 "GWYN""HAAG""HAAS""HACK""HAIL""HAIR""HALE""HALF",
387 "HALL""HALO""HALT""HAND""HANG""HANK""HANS""HARD",
388 "HARK""HARM""HART""HASH""HAST""HATE""HATH""HAUL",
389 "HAVE""HAWK""HAYS""HEAD""HEAL""HEAR""HEAT""HEBE",
390 "HECK""HEED""HEEL""HEFT""HELD""HELL""HELM""HERB",
391 "HERD""HERE""HERO""HERS""HESS""HEWN""HICK""HIDE",
392 "HIGH""HIKE""HILL""HILT""HIND""HINT""HIRE""HISS",
393 "HIVE""HOBO""HOCK""HOFF""HOLD""HOLE""HOLM""HOLT",
394 "HOME""HONE""HONK""HOOD""HOOF""HOOK""HOOT""HORN",
395 "HOSE""HOST""HOUR""HOVE""HOWE""HOWL""HOYT""HUCK",
396 "HUED""HUFF""HUGE""HUGH""HUGO""HULK""HULL""HUNK",
397 "HUNT""HURD""HURL""HURT""HUSH""HYDE""HYMN""IBIS",
398 "ICON""IDEA""IDLE""IFFY""INCA""INCH""INTO""IONS",
399 "IOTA""IOWA""IRIS""IRMA""IRON""ISLE""ITCH""ITEM",
400 "IVAN""JACK""JADE""JAIL""JAKE""JANE""JAVA""JEAN",
401 "JEFF""JERK""JESS""JEST""JIBE""JILL""JILT""JIVE",
402 "JOAN""JOBS""JOCK""JOEL""JOEY""JOHN""JOIN""JOKE",
403 "JOLT""JOVE""JUDD""JUDE""JUDO""JUDY""JUJU""JUKE",
404 "JULY""JUNE""JUNK""JUNO""JURY""JUST""JUTE""KAHN",
405 "KALE""KANE""KANT""KARL""KATE""KEEL""KEEN""KENO",
406 "KENT""KERN""KERR""KEYS""KICK""KILL""KIND""KING",
407 "KIRK""KISS""KITE""KLAN""KNEE""KNEW""KNIT""KNOB",
408 "KNOT""KNOW""KOCH""KONG""KUDO""KURD""KURT""KYLE",
409 "LACE""LACK""LACY""LADY""LAID""LAIN""LAIR""LAKE",
410 "LAMB""LAME""LAND""LANE""LANG""LARD""LARK""LASS",
411 "LAST""LATE""LAUD""LAVA""LAWN""LAWS""LAYS""LEAD",
412 "LEAF""LEAK""LEAN""LEAR""LEEK""LEER""LEFT""LEND",
413 "LENS""LENT""LEON""LESK""LESS""LEST""LETS""LIAR",
414 "LICE""LICK""LIED""LIEN""LIES""LIEU""LIFE""LIFT",
415 "LIKE""LILA""LILT""LILY""LIMA""LIMB""LIME""LIND",
416 "LINE""LINK""LINT""LION""LISA""LIST""LIVE""LOAD",
417 "LOAF""LOAM""LOAN""LOCK""LOFT""LOGE""LOIS""LOLA",
418 "LONE""LONG""LOOK""LOON""LOOT""LORD""LORE""LOSE",
419 "LOSS""LOST""LOUD""LOVE""LOWE""LUCK""LUCY""LUGE",
420 "LUKE""LULU""LUND""LUNG""LURA""LURE""LURK""LUSH",
421 "LUST""LYLE""LYNN""LYON""LYRA""MACE""MADE""MAGI",
422 "MAID""MAIL""MAIN""MAKE""MALE""MALI""MALL""MALT",
423 "MANA""MANN""MANY""MARC""MARE""MARK""MARS""MART",
424 "MARY""MASH""MASK""MASS""MAST""MATE""MATH""MAUL",
425 "MAYO""MEAD""MEAL""MEAN""MEAT""MEEK""MEET""MELD",
426 "MELT""MEMO""MEND""MENU""MERT""MESH""MESS""MICE",
427 "MIKE""MILD""MILE""MILK""MILL""MILT""MIMI""MIND",
428 "MINE""MINI""MINK""MINT""MIRE""MISS""MIST""MITE",
429 "MITT""MOAN""MOAT""MOCK""MODE""MOLD""MOLE""MOLL",
430 "MOLT""MONA""MONK""MONT""MOOD""MOON""MOOR""MOOT",
431 "MORE""MORN""MORT""MOSS""MOST""MOTH""MOVE""MUCH",
432 "MUCK""MUDD""MUFF""MULE""MULL""MURK""MUSH""MUST",
433 "MUTE""MUTT""MYRA""MYTH""NAGY""NAIL""NAIR""NAME",
434 "NARY""NASH""NAVE""NAVY""NEAL""NEAR""NEAT""NECK",
435 "NEED""NEIL""NELL""NEON""NERO""NESS""NEST""NEWS",
436 "NEWT""NIBS""NICE""NICK""NILE""NINA""NINE""NOAH",
437 "NODE""NOEL""NOLL""NONE""NOOK""NOON""NORM""NOSE",
438 "NOTE""NOUN""NOVA""NUDE""NULL""NUMB""OATH""OBEY",
439 "OBOE""ODIN""OHIO""OILY""OINT""OKAY""OLAF""OLDY",
440 "OLGA""OLIN""OMAN""OMEN""OMIT""ONCE""ONES""ONLY",
441 "ONTO""ONUS""ORAL""ORGY""OSLO""OTIS""OTTO""OUCH",
442 "OUST""OUTS""OVAL""OVEN""OVER""OWLY""OWNS""QUAD",
443 "QUIT""QUOD""RACE""RACK""RACY""RAFT""RAGE""RAID",
444 "RAIL""RAIN""RAKE""RANK""RANT""RARE""RASH""RATE",
445 "RAVE""RAYS""READ""REAL""REAM""REAR""RECK""REED",
446 "REEF""REEK""REEL""REID""REIN""RENA""REND""RENT",
447 "REST""RICE""RICH""RICK""RIDE""RIFT""RILL""RIME",
448 "RING""RINK""RISE""RISK""RITE""ROAD""ROAM""ROAR",
449 "ROBE""ROCK""RODE""ROIL""ROLL""ROME""ROOD""ROOF",
450 "ROOK""ROOM""ROOT""ROSA""ROSE""ROSS""ROSY""ROTH",
451 "ROUT""ROVE""ROWE""ROWS""RUBE""RUBY""RUDE""RUDY",
452 "RUIN""RULE""RUNG""RUNS""RUNT""RUSE""RUSH""RUSK",
453 "RUSS""RUST""RUTH""SACK""SAFE""SAGE""SAID""SAIL",
454 "SALE""SALK""SALT""SAME""SAND""SANE""SANG""SANK",
455 "SARA""SAUL""SAVE""SAYS""SCAN""SCAR""SCAT""SCOT",
456 "SEAL""SEAM""SEAR""SEAT""SEED""SEEK""SEEM""SEEN",
457 "SEES""SELF""SELL""SEND""SENT""SETS""SEWN""SHAG",
458 "SHAM""SHAW""SHAY""SHED""SHIM""SHIN""SHOD""SHOE",
459 "SHOT""SHOW""SHUN""SHUT""SICK""SIDE""SIFT""SIGH",
460 "SIGN""SILK""SILL""SILO""SILT""SINE""SING""SINK",
461 "SIRE""SITE""SITS""SITU""SKAT""SKEW""SKID""SKIM",
462 "SKIN""SKIT""SLAB""SLAM""SLAT""SLAY""SLED""SLEW",
463 "SLID""SLIM""SLIT""SLOB""SLOG""SLOT""SLOW""SLUG",
464 "SLUM""SLUR""SMOG""SMUG""SNAG""SNOB""SNOW""SNUB",
465 "SNUG""SOAK""SOAR""SOCK""SODA""SOFA""SOFT""SOIL",
466 "SOLD""SOME""SONG""SOON""SOOT""SORE""SORT""SOUL",
467 "SOUR""SOWN""STAB""STAG""STAN""STAR""STAY""STEM",
468 "STEW""STIR""STOW""STUB""STUN""SUCH""SUDS""SUIT",
469 "SULK""SUMS""SUNG""SUNK""SURE""SURF""SWAB""SWAG",
470 "SWAM""SWAN""SWAT""SWAY""SWIM""SWUM""TACK""TACT",
471 "TAIL""TAKE""TALE""TALK""TALL""TANK""TASK""TATE",
472 "TAUT""TEAL""TEAM""TEAR""TECH""TEEM""TEEN""TEET",
473 "TELL""TEND""TENT""TERM""TERN""TESS""TEST""THAN",
474 "THAT""THEE""THEM""THEN""THEY""THIN""THIS""THUD",
475 "THUG""TICK""TIDE""TIDY""TIED""TIER""TILE""TILL",
476 "TILT""TIME""TINA""TINE""TINT""TINY""TIRE""TOAD",
477 "TOGO""TOIL""TOLD""TOLL""TONE""TONG""TONY""TOOK",
478 "TOOL""TOOT""TORE""TORN""TOTE""TOUR""TOUT""TOWN",
479 "TRAG""TRAM""TRAY""TREE""TREK""TRIG""TRIM""TRIO",
480 "TROD""TROT""TROY""TRUE""TUBA""TUBE""TUCK""TUFT",
481 "TUNA""TUNE""TUNG""TURF""TURN""TUSK""TWIG""TWIN",
482 "TWIT""ULAN""UNIT""URGE""USED""USER""USES""UTAH",
483 "VAIL""VAIN""VALE""VARY""VASE""VAST""VEAL""VEDA",
484 "VEIL""VEIN""VEND""VENT""VERB""VERY""VETO""VICE",
485 "VIEW""VINE""VISE""VOID""VOLT""VOTE""WACK""WADE",
486 "WAGE""WAIL""WAIT""WAKE""WALE""WALK""WALL""WALT",
487 "WAND""WANE""WANG""WANT""WARD""WARM""WARN""WART",
488 "WASH""WAST""WATS""WATT""WAVE""WAVY""WAYS""WEAK",
489 "WEAL""WEAN""WEAR""WEED""WEEK""WEIR""WELD""WELL",
490 "WELT""WENT""WERE""WERT""WEST""WHAM""WHAT""WHEE",
491 "WHEN""WHET""WHOA""WHOM""WICK""WIFE""WILD""WILL",
492 "WIND""WINE""WING""WINK""WINO""WIRE""WISE""WISH",
493 "WITH""WOLF""WONT""WOOD""WOOL""WORD""WORE""WORK",
494 "WORM""WORN""WOVE""WRIT""WYNN""YALE""YANG""YANK",
495 "YARD""YARN""YAWL""YAWN""YEAH""YEAR""YELL""YOGA",
496 "YOKE"]
Note: See TracBrowser for help on using the browser.