1 module fluid.password_input; 2 3 import fluid.utils; 4 import fluid.backend; 5 import fluid.text_input; 6 7 8 @safe: 9 10 11 /// A password input box. 12 alias passwordInput = simpleConstructor!PasswordInput; 13 14 /// ditto 15 class PasswordInput : TextInput { 16 17 mixin enableInputActions; 18 19 protected { 20 21 /// Character circle radius. 22 float radius; 23 24 /// Distance between the start positions of each character. 25 float advance; 26 27 } 28 29 private { 30 31 char[][] _bufferHistory; 32 33 } 34 35 /// Create a password input. 36 /// Params: 37 /// placeholder = Placeholder text for the field. 38 /// submitted = Callback for when the field is submitted. 39 this(string placeholder = "", void delegate() @trusted submitted = null) { 40 41 super(placeholder, submitted); 42 43 } 44 45 /// PasswordInput does not support multiline. 46 override bool multiline() const { 47 48 return false; 49 50 } 51 52 /// Delete all textual data created by the password box. All text typed inside the box will be overwritten, except 53 /// for any copies, if they were made. Clears the box. 54 /// 55 /// The password box keeps a buffer of all text that has ever been written to it, in order to store and display its 56 /// content. The security implication is that, even if the password is no longer needed, it will remain in program 57 /// memory, exposing it as a possible target for attackers, in case [memory corruption vulnerabilities][1] are 58 /// found. Even if collected by the garbage collector, the value will remain untouched until the same spot in memory 59 /// is reused, so in order to increase the security of a program, passwords should thus be *shredded* after usage, 60 /// explicitly overwriting their contents. 61 /// 62 /// Do note that shredding is never performed automatically — this function has to be called explicitly. 63 /// Furthermore, text provided through different means than explicit input or `push(Rope)` will not be cleared. 64 /// 65 /// [1]: https://en.wikipedia.org/wiki/Memory_safety 66 void shred() { 67 68 // Go through each buffer 69 foreach (buffer; _bufferHistory) { 70 71 // Clear it 72 buffer[] = char.init; 73 74 } 75 76 // Clear the input and buffer history 77 clear(); 78 _bufferHistory = [buffer]; 79 80 // Clear undo stack 81 clearHistory(); 82 83 } 84 85 unittest { 86 87 import fluid.input; 88 89 auto root = passwordInput(); 90 root.value = "Hello, "; 91 root.caretToEnd(); 92 root.push("World!"); 93 94 assert(root.value == "Hello, World!"); 95 96 auto value1 = root.value; 97 root.shred(); 98 99 assert(root.value == ""); 100 assert(value1 == "Hello, \xFF\xFF\xFF\xFF\xFF\xFF"); 101 102 root.push("Hello, World!"); 103 root.runInputAction!(FluidInputAction.previousChar); 104 105 auto value2 = root.value; 106 root.chopWord(); 107 root.push("Fluid"); 108 109 auto value3 = root.value; 110 111 assert(root.value == "Hello, Fluid!"); 112 assert(value2 == "Hello, World!"); 113 assert(value3 == "Hello, Fluid!"); 114 115 root.shred(); 116 117 assert(root.value == ""); 118 assert(value2 == "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"); 119 assert(value3 == value2); 120 121 } 122 123 unittest { 124 125 auto root = passwordInput(); 126 root.push("Hello, x"); 127 root.chop(); 128 root.push("World!"); 129 130 assert(root.value == "Hello, World!"); 131 132 root.undo(); 133 134 assert(root.value == "Hello, "); 135 136 root.shred(); 137 138 assert(root.value == ""); 139 140 root.undo(); 141 142 assert(root.value == ""); 143 144 root.redo(); 145 root.redo(); 146 147 assert(root.value == ""); 148 149 } 150 151 protected override void drawContents(Rectangle inner, Rectangle innerScrolled) { 152 153 auto typeface = pickStyle.getTypeface; 154 155 // Empty, draw the placeholder using regular input 156 if (isEmpty) return super.drawContents(inner, innerScrolled); 157 158 // Draw selection 159 drawSelection(innerScrolled); 160 161 auto cursor = start(innerScrolled) + Vector2(radius, typeface.lineHeight / 2f); 162 163 // Draw a circle for each character 164 foreach (_; value) { 165 166 io.drawCircle(cursor, radius, style.textColor); 167 168 cursor.x += advance; 169 170 } 171 172 // Draw the caret 173 drawCaret(innerScrolled); 174 175 } 176 177 override size_t nearestCharacter(Vector2 needle) const { 178 179 import std.utf : byDchar; 180 181 size_t number; 182 183 foreach (ch; value[].byDchar) { 184 185 // Stop if found the character 186 if (needle.x < number * advance + radius) break; 187 188 number++; 189 190 } 191 192 return number; 193 194 } 195 196 protected override Vector2 caretPositionImpl(float availableWidth, bool preferNextLine) { 197 198 return Vector2( 199 advance * valueBeforeCaret.countCharacters, 200 super.caretPositionImpl(availableWidth, preferNextLine).y, 201 ); 202 203 } 204 205 /// Draw selection, if applicable. 206 protected override void drawSelection(Rectangle inner) { 207 208 import std.range : enumerate; 209 import std.algorithm : min, max; 210 211 // Ignore if selection is empty 212 if (selectionStart == selectionEnd) return; 213 214 const typeface = style.getTypeface; 215 216 const low = min(selectionStart, selectionEnd); 217 const high = max(selectionStart, selectionEnd); 218 219 const start = advance * value[0 .. low].countCharacters; 220 const size = advance * value[low .. high].countCharacters; 221 222 const rect = Rectangle( 223 (inner.start + Vector2(start, 0)).tupleof, 224 size, typeface.lineHeight, 225 ); 226 227 io.drawRectangle(rect, style.selectionBackgroundColor); 228 229 } 230 231 protected override void reloadStyles() { 232 233 super.reloadStyles(); 234 235 // Use the "X" character as reference 236 auto typeface = style.getTypeface; 237 auto x = typeface.advance('X').x; 238 239 radius = x / 2f; 240 advance = x * 1.2; 241 242 } 243 244 /// Request a new or larger buffer. 245 /// 246 /// `PasswordInput` keeps track of all the buffers that have been used since its creation in order to make it 247 /// possible to `shred` the contents once they're unnecessary. 248 /// 249 /// Params: 250 /// minimumSize = Minimum size to allocate for the buffer. 251 protected override void newBuffer(size_t minimumSize = 64) { 252 253 // Create the buffer 254 super.newBuffer(minimumSize); 255 256 // Remember the buffer 257 _bufferHistory ~= buffer; 258 259 } 260 261 } 262 263 /// 264 unittest { 265 266 // PasswordInput lets you ask the user for passwords 267 auto node = passwordInput(); 268 269 // Retrieve the password with `value` 270 auto userPassword = node.value; 271 272 // Destroy the passwords once you're done to secure them against attacks 273 // (Careful: This will invalidate `userPassword` we got earlier) 274 node.shred(); 275 276 }