@@ -108,6 +108,59 @@ component singleton accessors="true" {
108108 );
109109 }
110110
111+ /**
112+ * Generates an array of recovery codes.
113+ *
114+ * @amount The amount of codes to generate.
115+ *
116+ * @return s An array of recovery codes.
117+ */
118+ public array function generateRecoveryCodes ( required numeric amount ) {
119+ if ( arguments .amount <= 0 ) {
120+ throw (
121+ type = " totp.InvalidRecoveryCodeAmount" ,
122+ message = " You must generate a positive amount of recovery codes."
123+ );
124+ }
125+
126+ var codes = [];
127+ for ( var i = 1 ; i <= arguments .amount ; i ++ ) {
128+ codes .append ( generateRecoveryCode () );
129+ }
130+ return codes ;
131+ }
132+
133+ /**
134+ * Generates a code composed of numbers and lower case characters from latin alphabet (36 possible characters).
135+ * The code is split in groups separated with dash for better readability.
136+ * For example: `4ckn-xspn-et8t-xgr0`
137+ *
138+ * Recovery codes must reach a minimum entropy to be secured
139+ * `code entropy = log( {characters-count} ^ {code-length} ) / log(2)`
140+ * The settings used below allows the code to reach an entropy of 82 bits :
141+ * log(36^16) / log(2) == 82.7...
142+ *
143+ * @return s A recovery code string.
144+ */
145+ private string function generateRecoveryCode () {
146+ var CODE_LENGTH = 16 ;
147+ var GROUPS_NUMBER = 4 ;
148+ var CHARACTERS = listToArray ( " abcdefghijklmnopqrstuvwxyz0123456789" , " " );
149+ var CHARACTERS_LENGTH = CHARACTERS .len ();
150+
151+ var code = " " ;
152+ for ( var i = 1 ; i <= CODE_LENGTH ; i ++ ) {
153+ // Append random character from authorized ones
154+ code & = CHARACTERS [ variables .secureRandom .nextInt ( CHARACTERS_LENGTH ) + 1 ];
155+ // Split code into groups for increased readability
156+ if ( i % GROUPS_NUMBER == 0 && i ! = CODE_LENGTH ) {
157+ code & = " -" ;
158+ }
159+ }
160+
161+ return code ;
162+ }
163+
111164 /**
112165 * Generates a TOTP for a given secret.
113166 *
0 commit comments