source: trunk/CrypPlugins/AES/AES.cs @ 1162

Last change on this file since 1162 was 1162, checked in by Arno Wacker, 12 years ago

KeySearcher:

  • Removed creation of CryptoolStream for each trial key -> performance boost
  • Re-integrated the usage of only the partial encrypted message for trial decryption (byteToUse). Now the value set in the costFunction will determine how many bytes will be used. This value shall be moved in the future to the KeySearcher settings.

IControlEncryption

  • Added an extended method for Decrypt to IControlEncryption for usage with bytesToUse

AES/DES/SDES

  • Updated for compatibility with IControlEncryption-changes (the new parameter is also used by all three)

Samples

  • Updated KeySearcher-Sample-DES to use only 128 bytes for bruteforcing instead of 256 (increase performance a little)

Note: providing a value for bytesToUse smaller than the blocksize of the algorithm used is an error case - not sure if every case is already covered.

File size: 25.5 KB
Line 
1/*                              Apache License
2                           Version 2.0, January 2004
3                        http://www.apache.org/licenses/
4
5   Copyright [2008] [Dr. Arno Wacker, University of Duisburg-Essen]
6
7   Licensed under the Apache License, Version 2.0 (the "License");
8   you may not use this file except in compliance with the License.
9   You may obtain a copy of the License at
10
11       http://www.apache.org/licenses/LICENSE-2.0
12
13   Unless required by applicable law or agreed to in writing, software
14   distributed under the License is distributed on an "AS IS" BASIS,
15   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   See the License for the specific language governing permissions and
17   limitations under the License.
18*/
19
20using System;
21using System.Collections.Generic;
22using System.Linq;
23using System.Text;
24using System.IO;
25using System.Security.Cryptography;
26using Cryptool.PluginBase;
27using System.ComponentModel;
28using Cryptool.PluginBase.Cryptography;
29using Cryptool.PluginBase.IO;
30using System.Windows.Controls;
31using System.Runtime.CompilerServices;
32using Cryptool.PluginBase.Miscellaneous;
33using System.Runtime.Remoting.Contexts;
34using Cryptool.PluginBase.Control;
35using System.Reflection;
36
37namespace Cryptool.Plugins.Cryptography.Encryption
38{
39    [Author("Dr. Arno Wacker", "arno.wacker@cryptool.org", "Uni Duisburg", "http://www.uni-duisburg-essen.de")]
40    [PluginInfo(false, "AES", "Advanced Encryption Standard (Rijndael)", "AES/DetailedDescription/Description.xaml", "AES/Images/AES.png", "AES/Images/encrypt.png", "AES/Images/decrypt.png", "AES/Images/Rijndael.png")]
41    [EncryptionType(EncryptionType.SymmetricBlock)]
42    public class AES : ContextBoundObject, IEncryption
43    {
44        #region Private variables
45        private AESSettings settings;
46        private CryptoolStream inputStream;
47        private CryptoolStream outputStream;
48        private byte[] inputKey;
49        private byte[] inputIV;
50        private CryptoStream p_crypto_stream;
51        private bool stop = false;
52        private List<CryptoolStream> listCryptoolStreamsOut = new List<CryptoolStream>();
53        #endregion
54
55        public AES()
56        {
57            this.settings = new AESSettings();
58            this.settings.OnPluginStatusChanged += settings_OnPluginStatusChanged;
59        }
60
61        void settings_OnPluginStatusChanged(IPlugin sender, StatusEventArgs args)
62        {
63          if (OnPluginStatusChanged != null) OnPluginStatusChanged(this, args);
64        }
65
66        public ISettings Settings
67        {
68            get { return this.settings; }
69            set { this.settings = (AESSettings)value; }
70        }
71
72        [PropertyInfo(Direction.InputData, "Input", "Data to be encrypted or decrypted", "", true, false, DisplayLevel.Beginner, QuickWatchFormat.Hex, null)]
73        public CryptoolStream InputStream
74        {
75            get 
76            {
77              if (inputStream != null)
78              {
79                CryptoolStream cs = new CryptoolStream();
80                cs.OpenRead(inputStream.FileName);
81                listCryptoolStreamsOut.Add(cs);
82                return cs;
83              }
84              else return null;
85            }
86            set 
87            { 
88              this.inputStream = value;
89              if (value != null) listCryptoolStreamsOut.Add(value);
90              OnPropertyChanged("InputStream");
91            }
92        }
93
94        [PropertyInfo(Direction.InputData, "Key", "The provided key should be 16, 24 or 32 bytes, dependig on the settings. Too short/long keys will be extended/truncated!", "", true, false, DisplayLevel.Beginner, QuickWatchFormat.Hex, null)]
95        public byte[] InputKey
96        {
97            get { return this.inputKey; }
98            set 
99            { 
100              this.inputKey = value;
101              OnPropertyChanged("InputKey");
102            }
103        }
104
105        [PropertyInfo(Direction.InputData, "IV", "The initialisation vector (IV) which is used in chaining modes. It always must be the same as the blocksize.", "", false, false, DisplayLevel.Professional, QuickWatchFormat.Hex, null)]
106        public byte[] InputIV
107        {
108            get { return this.inputIV; }
109            set 
110            { 
111              this.inputIV = value;
112              OnPropertyChanged("InputIV");
113            }
114        }
115
116        [PropertyInfo(Direction.OutputData, "Output stream", "Encrypted or decrypted output data", "", true, false, DisplayLevel.Beginner, QuickWatchFormat.Hex, null)]
117        public CryptoolStream OutputStream
118        {
119            get 
120            {
121              if (this.outputStream != null)
122              {
123                CryptoolStream cs = new CryptoolStream();
124                listCryptoolStreamsOut.Add(cs);
125                cs.OpenRead(this.outputStream.FileName);
126                return cs;
127              }
128              return null;
129            }
130            set 
131            {
132              outputStream = value;
133              if (value != null) listCryptoolStreamsOut.Add(value);
134              OnPropertyChanged("OutputStream");
135            }
136        }
137
138        private void ConfigureAlg(SymmetricAlgorithm alg)
139        {
140            // first set all paramter, then assign input values!
141            switch (settings.Mode)
142            { //0="ECB"=default, 1="CBC", 2="CFB", 3="OFB"
143                case 1: alg.Mode = CipherMode.CBC; break;
144                case 2: alg.Mode = CipherMode.CFB; break;
145                // case 3: alg.Mode = CipherMode.OFB; break;
146                default: alg.Mode = CipherMode.ECB; break;
147            }
148
149            switch (settings.Padding)
150            { //0="Zeros"=default, 1="None", 2="PKCS7" , 3="ANSIX923", 4="ISO10126"
151                case 1: alg.Padding = PaddingMode.None; break;
152                case 2: alg.Padding = PaddingMode.PKCS7; break;
153                case 3: alg.Padding = PaddingMode.ANSIX923; break;
154                case 4: alg.Padding = PaddingMode.ISO10126; break;
155                default: alg.Padding = PaddingMode.Zeros; break;
156            }
157            if (settings.CryptoAlgorithm == 1)
158            {
159                switch (settings.Blocksize)
160                {
161                    case 1: alg.BlockSize = 192; break;
162                    case 2: alg.BlockSize = 256; break;
163                    default:
164                        alg.BlockSize = 128;
165                        break;
166                }
167            }
168
169            //check for a valid key
170            if (this.inputKey == null)
171            {               
172                //create a trivial key
173                inputKey = new byte[16];
174                // write a warning to the ouside word
175                GuiLogMessage("ERROR: No key provided. Using 0x000..00!", NotificationLevel.Error);
176            }
177
178            int keySizeInBytes = (16 + settings.Keysize * 8);
179
180            //prepare a long enough key
181            byte[] key = new byte[keySizeInBytes];
182
183            // copy the input key into the temporary key array
184            Array.Copy(inputKey, key, Math.Min(inputKey.Length, key.Length));
185
186            // Note: the SymmetricAlgorithm.Key setter clones the passed byte[] array and keeps his own copy
187            alg.Key = key;
188
189            if (inputKey.Length > keySizeInBytes)
190            {
191                GuiLogMessage("Overlength (" + inputKey.Length * 8 + " Bits) key provided. Removing trailing bytes to fit the desired key length of " + (keySizeInBytes * 8) + " Bits: " + bytesToHexString(key), NotificationLevel.Warning);
192            }
193
194            if (inputKey.Length < keySizeInBytes)
195            {
196                GuiLogMessage("Short (" + inputKey.Length * 8 + " Bits) key provided. Adding zero bytes to fill up to the desired key length of " + (keySizeInBytes * 8) + " Bits: " + bytesToHexString(key), NotificationLevel.Warning);
197            }
198
199            //check for a valid IV
200            if (this.inputIV == null)
201            {
202                //create a trivial key
203                inputIV = new byte[alg.BlockSize / 8];
204                GuiLogMessage("NOTE: No IV provided. Using 0x000..00!", NotificationLevel.Info);
205            }
206            alg.IV = this.inputIV;
207        }
208
209        private string bytesToHexString(byte[] array)
210        {
211            StringBuilder sb = new StringBuilder();
212            foreach (byte b in array)
213            {
214                sb.Append(b.ToString("X2"));
215            }
216            return sb.ToString();
217        }
218
219        private void checkForInputStream()
220        {
221            if (settings.Action == 0 && (inputStream == null || (inputStream != null && inputStream.Length == 0)))
222            {
223                //create some input
224                String dummystring = "Dummy string - no input provided - \"Hello AES World\" - dummy string - no input provided!";
225                this.inputStream = new CryptoolStream();
226                this.inputStream.OpenRead(Encoding.Default.GetBytes(dummystring.ToCharArray()));
227                // write a warning to the ouside word
228                GuiLogMessage("WARNING: No input provided. Using dummy data. (" + dummystring + ")", NotificationLevel.Warning);
229            }
230        }
231
232        public void Execute()
233        {
234            process(settings.Action);
235        }
236
237        public bool isStopped()
238        {
239
240            return this.stop;
241        }
242
243        private void process(int action)
244        {
245            //Encrypt/Decrypt Stream
246          try
247          {
248            checkForInputStream();
249            if (inputStream == null || inputStream.Length == 0)
250            {
251              GuiLogMessage("No input given. Not using dummy data in decrypt mode. Aborting now.", NotificationLevel.Error);
252              return;
253            }
254
255            if (this.inputStream.CanSeek) this.inputStream.Position = 0;
256            SymmetricAlgorithm p_alg = null;
257            if (settings.CryptoAlgorithm == 1)
258            { p_alg = new RijndaelManaged(); }
259            else
260            { p_alg = new AesCryptoServiceProvider(); }
261
262            ConfigureAlg(p_alg);
263
264            ICryptoTransform p_encryptor = null;
265            switch (action)
266            {
267              case 0:
268                p_encryptor = p_alg.CreateEncryptor();
269                break;
270              case 1:
271                p_encryptor = p_alg.CreateDecryptor();
272                break;
273            }
274
275            outputStream = new CryptoolStream();
276            listCryptoolStreamsOut.Add(outputStream);
277            outputStream.OpenWrite();
278            p_crypto_stream = new CryptoStream((Stream)inputStream, p_encryptor, CryptoStreamMode.Read);
279            byte[] buffer = new byte[p_alg.BlockSize / 8];
280            int bytesRead;
281            int position = 0;
282            string mode = action == 0 ? "encryption" : "decryption";
283            GuiLogMessage("Starting " + mode + " [Keysize=" + p_alg.KeySize.ToString() + " Bits, Blocksize=" + p_alg.BlockSize.ToString() + " Bits]", NotificationLevel.Info);
284            DateTime startTime = DateTime.Now;
285            while ((bytesRead = p_crypto_stream.Read(buffer, 0, buffer.Length)) > 0 && !stop)
286            {
287              outputStream.Write(buffer, 0, bytesRead);
288
289              if ((int)(inputStream.Position * 100 / inputStream.Length) > position)
290              {
291                position = (int)(inputStream.Position * 100 / inputStream.Length);
292                ProgressChanged(inputStream.Position, inputStream.Length);
293              }
294            }
295
296
297            long outbytes = outputStream.Length;
298            p_crypto_stream.Flush();           
299            // p_crypto_stream.Close();
300            DateTime stopTime = DateTime.Now;
301            TimeSpan duration = stopTime - startTime;
302            // (outputStream as CryptoolStream).FinishWrite();
303
304            if (!stop)
305            {
306                mode = action == 0 ? "Encryption" : "Decryption";
307                GuiLogMessage(mode + " complete! (in: " + inputStream.Length.ToString() + " bytes, out: " + outbytes.ToString() + " bytes)", NotificationLevel.Info);
308                GuiLogMessage("Wrote data to file: " + outputStream.FileName, NotificationLevel.Info);
309                GuiLogMessage("Time used: " + duration.ToString(), NotificationLevel.Debug);
310                outputStream.Close();
311                OnPropertyChanged("OutputStream");
312            }
313            CryptoolStream test = outputStream;
314            if (stop)
315            {
316                outputStream.Close();
317                GuiLogMessage("Aborted!", NotificationLevel.Info);
318            }
319          }
320          catch (CryptographicException cryptographicException)
321          {
322            // TODO: For an unknown reason p_crypto_stream can not be closed after exception.
323            // Trying so makes p_crypto_stream throw the same exception again. So in Dispose
324            // the error messages will be doubled.
325            // As a workaround we set p_crypto_stream to null here.
326            p_crypto_stream = null;
327            GuiLogMessage(cryptographicException.Message, NotificationLevel.Error);
328          }
329          catch (Exception exception)
330          {
331            GuiLogMessage(exception.Message, NotificationLevel.Error);
332          }
333          finally
334          {
335              ProgressChanged(1, 1);
336          }
337        }
338
339        public void Encrypt()
340        {
341            //Encrypt Stream
342            process(0);
343        }
344       
345        public void Decrypt()
346        {
347            //Decrypt Stream
348            process(1);
349        }
350
351        #region IPlugin Member
352
353
354        public System.Windows.Controls.UserControl Presentation
355        {
356          get { return null; }
357        }
358
359        public UserControl QuickWatchPresentation
360        {
361          get { return null; }
362        }
363
364        public void Initialize()
365        {           
366        }
367
368        public void Dispose()
369        {
370          try
371          {
372            stop = false;
373            inputKey = null;
374            inputIV = null;
375            outputStream = null;
376            inputStream = null;
377
378            //if (inputStream != null)
379            //{
380            //  inputStream.Flush();
381            //  inputStream.Close();
382            //  inputStream = null;
383            //}
384            //if (outputStream != null)
385            //{
386            //  outputStream.Flush();
387            //  outputStream.Close();
388            //  outputStream = null;
389            //}
390            foreach (CryptoolStream stream in listCryptoolStreamsOut)
391            {
392              stream.Close();
393            }
394            listCryptoolStreamsOut.Clear();
395
396            if (p_crypto_stream != null)
397            {
398              p_crypto_stream.Flush();
399              p_crypto_stream.Close();
400              p_crypto_stream = null;
401            }
402          }
403          catch (Exception ex)
404          {         
405             GuiLogMessage(ex.Message, NotificationLevel.Error);
406          }
407          this.stop = false;
408        }
409
410        public void Stop()
411        {
412          this.stop = true;
413        }
414
415        public void PostExecution()
416        {
417          Dispose();
418        }
419
420        public void PreExecution()
421        {
422          Dispose();
423        }
424
425        #endregion
426
427        #region INotifyPropertyChanged Members
428
429        public event PropertyChangedEventHandler PropertyChanged;
430
431        public void OnPropertyChanged(string name)
432        {
433          EventsHelper.PropertyChanged(PropertyChanged, this, new PropertyChangedEventArgs(name));
434          //if (PropertyChanged != null)
435          //{
436          //  PropertyChanged(this, new PropertyChangedEventArgs(name));
437          //}
438        }
439
440        #endregion
441
442        #region IPlugin Members
443
444        public event StatusChangedEventHandler OnPluginStatusChanged;       
445
446        public event GuiLogNotificationEventHandler OnGuiLogNotificationOccured;
447        private void GuiLogMessage(string message, NotificationLevel logLevel)
448        {
449          EventsHelper.GuiLogMessage(OnGuiLogNotificationOccured, this, new GuiLogEventArgs(message, this, logLevel));
450        }
451
452        public event PluginProgressChangedEventHandler OnPluginProgressChanged;
453        private void ProgressChanged(double value, double max)
454        {
455          EventsHelper.ProgressChanged(OnPluginProgressChanged, this, new PluginProgressEventArgs(value, max));
456        }
457       
458        public void Pause()
459        {
460         
461        }
462
463        #endregion
464
465        private IControlEncryption controlSlave;
466        [PropertyInfo(Direction.ControlSlave, "AES Slave", "AES Slave", "", DisplayLevel.Experienced)]
467        public IControlEncryption ControlSlave
468        {
469          get 
470          {
471              if (controlSlave == null)
472                  controlSlave = new AESControl(this);
473              return controlSlave; 
474          }
475        }     
476    }
477
478    public class AESControl : IControlEncryption
479    {
480        public event KeyPatternChanged keyPatternChanged;
481        public event IControlStatusChangedEventHandler OnStatusChanged;
482
483        private AES plugin;
484     
485        public AESControl(AES Plugin)
486        {
487            this.plugin = Plugin;
488        }
489
490        #region IControlEncryption Members
491
492        public byte[] Encrypt(byte[] key, int blocksize)
493        {
494            /// not implemented, currently not needed
495            return null;
496        }
497
498        public byte[] Decrypt(byte[] ciphertext, byte[] key)
499        {
500            return Decrypt(ciphertext, key, ciphertext.Length);
501        }
502
503        public byte[] Decrypt(byte[] ciphertext, byte[] key, int bytesToUse)
504        {
505            CryptoStream crypto_stream = null;
506            int size = bytesToUse > ciphertext.Length ? ciphertext.Length : bytesToUse;
507
508            byte[] output = new byte[size];
509
510
511            SymmetricAlgorithm aes_algorithm = null;
512
513            //Decrypt Stream
514            try
515            {
516                if (!(aes_algorithm is object))
517                {
518                    if (((AESSettings)plugin.Settings).CryptoAlgorithm == 1)
519                    { aes_algorithm = new RijndaelManaged(); }
520                    else
521                    { aes_algorithm = new AesCryptoServiceProvider(); }
522                }
523
524                this.ConfigureAlg(aes_algorithm, key);
525
526                ICryptoTransform p_decryptor;
527                try
528                {
529                    p_decryptor = aes_algorithm.CreateDecryptor();
530                }
531                catch
532                {
533                    //dirty hack to allow weak keys:
534                    MethodInfo mi = aes_algorithm.GetType().GetMethod("_NewEncryptor", BindingFlags.NonPublic | BindingFlags.Instance);
535                    object[] Par = { aes_algorithm.Key, aes_algorithm.Mode, aes_algorithm.IV, aes_algorithm.FeedbackSize, 0 };
536                    p_decryptor = mi.Invoke(aes_algorithm, Par) as ICryptoTransform;
537                }
538
539                crypto_stream = new CryptoStream(new MemoryStream(ciphertext, 0, size), p_decryptor, CryptoStreamMode.Read);
540
541                byte[] buffer = new byte[aes_algorithm.BlockSize / 8];
542                int bytesRead;
543                int position = 0;
544
545                while ((bytesRead = crypto_stream.Read(buffer, 0, buffer.Length)) > 0 && !plugin.isStopped())
546                {
547                    for (int i = 0; i < bytesRead; i++)
548                    {
549                        if (position + i < output.Length)
550                        {
551                            output[position + i] = buffer[i];
552                        }
553                        else
554                        {
555                            break;
556                        }
557                    }
558                    position += bytesRead;
559                }
560
561            }
562            catch (Exception exception)
563            {
564                aes_algorithm = null;   // we got an exception so we do not use this object any more
565                throw exception;
566            }
567
568            return output;
569        }
570
571     
572
573        private void ConfigureAlg(SymmetricAlgorithm alg, byte[] key)
574        {
575            try
576            {
577                alg.Key = key;
578            }
579            catch
580            {
581                //dirty hack to allow weak keys:
582                FieldInfo field = alg.GetType().GetField("KeyValue", BindingFlags.NonPublic | BindingFlags.Instance);
583                field.SetValue(alg, key);
584            }
585
586            //try
587            //{
588            //    alg.IV = this.plugin.InputIV;
589            //}
590            //catch
591            //{
592            //    //dirty hack to allow weak keys:
593            //    FieldInfo field = aes_algorithm.GetType().GetField("IVValue", BindingFlags.NonPublic | BindingFlags.Instance);
594            //    field.SetValue(alg, this.plugin.InputIV);
595            //}
596            alg.IV = new byte[alg.BlockSize / 8];
597
598
599            switch (((AESSettings)plugin.Settings).Mode)
600            { //0="ECB"=default, 1="CBC", 2="CFB", 3="OFB"
601                case 1: alg.Mode = CipherMode.CBC; break;
602                case 2: alg.Mode = CipherMode.CFB; break;
603                case 3: alg.Mode = CipherMode.OFB; break;
604                default: alg.Mode = CipherMode.ECB; break;
605            }
606
607            alg.Padding = PaddingMode.Zeros;
608
609            //switch (((AESSettings)plugin.Settings).Padding)
610            //{ //0="Zeros"=default, 1="None", 2="PKCS7"
611            //    case 1: alg.Padding = PaddingMode.None; break;
612            //    case 2: alg.Padding = PaddingMode.PKCS7; break;
613            //    case 3: alg.Padding = PaddingMode.ANSIX923; break;
614            //    case 4: alg.Padding = PaddingMode.ISO10126; break;
615            //    default: alg.Padding = PaddingMode.Zeros; break;
616            //}
617           
618        }
619       
620        public string getKeyPattern()
621        {
622            int bytes = 0;
623            switch (((AESSettings)plugin.Settings).Keysize)
624            {
625                case 0:
626                    bytes = 16;
627                    break;
628                case 1:
629                    bytes = 24;
630                    break;
631                case 2:
632                    bytes = 32;
633                    break;
634            }
635            string pattern = "";
636            for (int i = 1; i < bytes; i++)
637                pattern += "[0-9A-F][0-9A-F]-";
638            pattern += "[0-9A-F][0-9A-F]";
639            return pattern;
640        }
641
642        public byte[] getKeyFromString(string key, ref int[] arrayPointers, ref int[] arraySuccessors, ref int[] arrayUppers)
643        {
644            int bytes = 0;
645            switch (((AESSettings)plugin.Settings).Keysize)
646            {
647                case 0:
648                    bytes = 16;
649                    break;
650                case 1:
651                    bytes = 24;
652                    break;
653                case 2:
654                    bytes = 32;
655                    break;
656            }
657            byte[] bkey = new byte[bytes];
658            int counter = 0;
659            bool allocated = false;
660            for (int i = 0; i < bytes; i++)
661            {
662                try
663                {
664                    string substr = key.Substring(i * 3, 2);
665                    if (!allocated && (substr[0] == '*' || substr[1] == '*'))
666                    {
667                        arrayPointers = new int[bytes];
668                        for (int j = 0; j < bytes; j++)
669                            arrayPointers[j] = -1;
670                        arraySuccessors = new int[bytes];
671                        arrayUppers = new int[bytes];
672                        allocated = true;
673                    }
674                    if (substr[0] != '*' && substr[1] != '*')
675                        bkey[i] = Convert.ToByte(substr, 16);
676                    else if (substr[0] == '*' && substr[1] == '*')
677                    {
678                        bkey[i] = 0;
679                        arrayPointers[counter] = i;
680                        arraySuccessors[counter] = 1;
681                        arrayUppers[counter] = 255;
682                        counter++;
683                    }
684                    else if (substr[0] != '*' && substr[1] == '*')
685                    {
686                        bkey[i] = Convert.ToByte(substr[0] + "0", 16);
687                        arrayPointers[counter] = i;
688                        arraySuccessors[counter] = 1;
689                        arrayUppers[counter] = Convert.ToByte(substr[0] + "F", 16);
690                        counter++;
691                    }
692                    else if (substr[0] == '*' && substr[1] != '*')
693                    {
694                        bkey[i] = Convert.ToByte("0" + substr[1], 16);
695                        arrayPointers[counter] = i;
696                        arraySuccessors[counter] = 16;
697                        arrayUppers[counter] = Convert.ToByte("F" + substr[1], 16);
698                        counter++;
699                    }
700                }
701                catch (Exception ex)
702                {
703                    return null;
704                }
705            }
706            return bkey;
707        }
708
709        public IControlEncryption clone()
710        {
711            AESControl aes = new AESControl(plugin);
712            return aes;
713        }
714
715        public void Dispose()
716        {
717        }
718
719        #endregion
720
721        #region IControlEncryption Member
722
723
724        public void changeSettings(string setting, object value)
725        {
726           
727        }
728
729        #endregion
730    }
731}
Note: See TracBrowser for help on using the repository browser.