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