Add password fingerprint identicon next to master password field
This commit is contained in:
parent
c1fb433506
commit
9a8187f8c0
@ -0,0 +1,93 @@
|
|||||||
|
package cz.bugsy.passwordzebra.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a small symmetric 5×5 identicon derived from the SHA-256 hash of [password].
|
||||||
|
* Color hue, saturation, and lightness are taken from hash bytes so that every unique
|
||||||
|
* password produces a visually distinct pattern that is easy to memorise.
|
||||||
|
*
|
||||||
|
* Shown as a plain surface-variant box when [password] is empty.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun PasswordFingerprint(
|
||||||
|
password: CharArray,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
size: Dp = 52.dp,
|
||||||
|
) {
|
||||||
|
val surfaceVariant = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val outline = MaterialTheme.colorScheme.outline
|
||||||
|
|
||||||
|
// Recompute hash only when the actual password content changes.
|
||||||
|
val hash: ByteArray? = if (password.isEmpty()) null else remember(String(password)) {
|
||||||
|
MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest(String(password).toByteArray(Charsets.UTF_8))
|
||||||
|
}
|
||||||
|
|
||||||
|
val fillColor: Color? = hash?.let {
|
||||||
|
val hue = (it[0].toInt() and 0xFF) * 360f / 255f
|
||||||
|
val sat = 0.50f + (it[1].toInt() and 0xFF) / 255f * 0.35f
|
||||||
|
val lit = 0.38f + (it[2].toInt() and 0xFF) / 255f * 0.18f
|
||||||
|
Color.hsl(hue, sat, lit)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(surfaceVariant)
|
||||||
|
.border(1.dp, outline.copy(alpha = 0.35f), RoundedCornerShape(8.dp)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (hash != null && fillColor != null) {
|
||||||
|
val canvasSize = size * 0.80f // leave a small margin inside the box
|
||||||
|
Canvas(modifier = Modifier.size(canvasSize)) {
|
||||||
|
val gridSize = 5
|
||||||
|
val cellW = this.size.width / gridSize
|
||||||
|
val cellH = this.size.height / gridSize
|
||||||
|
val gap = 1.5f
|
||||||
|
|
||||||
|
// Only decide cols 0..2; mirror cols 0..1 to cols 4..3.
|
||||||
|
// Col 2 is the centre column (not mirrored).
|
||||||
|
for (row in 0 until gridSize) {
|
||||||
|
for (col in 0..2) {
|
||||||
|
val bitIndex = row * 3 + col
|
||||||
|
val filled = (hash[bitIndex / 8].toInt() ushr (bitIndex % 8)) and 1 == 1
|
||||||
|
|
||||||
|
if (filled) {
|
||||||
|
drawRect(
|
||||||
|
color = fillColor,
|
||||||
|
topLeft = Offset(col * cellW + gap, row * cellH + gap),
|
||||||
|
size = Size(cellW - gap * 2, cellH - gap * 2),
|
||||||
|
)
|
||||||
|
if (col < 2) {
|
||||||
|
drawRect(
|
||||||
|
color = fillColor,
|
||||||
|
topLeft = Offset((4 - col) * cellW + gap, row * cellH + gap),
|
||||||
|
size = Size(cellW - gap * 2, cellH - gap * 2),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -66,6 +66,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import cz.bugsy.passwordzebra.R
|
import cz.bugsy.passwordzebra.R
|
||||||
import cz.bugsy.passwordzebra.deterministic.DeterministicViewModel
|
import cz.bugsy.passwordzebra.deterministic.DeterministicViewModel
|
||||||
|
import cz.bugsy.passwordzebra.ui.components.PasswordFingerprint
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -278,13 +279,17 @@ fun DeterministicScreen(
|
|||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
// Master password
|
// Master password + visual fingerprint
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = String(state.masterPassword),
|
value = String(state.masterPassword),
|
||||||
onValueChange = { viewModel.updateMasterPassword(it.toCharArray()) },
|
onValueChange = { viewModel.updateMasterPassword(it.toCharArray()) },
|
||||||
label = { Text(stringResource(R.string.det_master_password)) },
|
label = { Text(stringResource(R.string.det_master_password)) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.weight(1f)
|
||||||
.focusRequester(masterPasswordFocus),
|
.focusRequester(masterPasswordFocus),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
visualTransformation = if (state.masterPasswordVisible) VisualTransformation.None
|
visualTransformation = if (state.masterPasswordVisible) VisualTransformation.None
|
||||||
@ -309,6 +314,12 @@ fun DeterministicScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
PasswordFingerprint(
|
||||||
|
password = state.masterPassword,
|
||||||
|
size = 56.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
} // end Column
|
} // end Column
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user