mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-13 15:16:07 +09:00
153 lines
5.8 KiB
Kotlin
153 lines
5.8 KiB
Kotlin
package net.torvald.terrarum.utils
|
|
|
|
import kotlin.experimental.xor
|
|
|
|
|
|
/**
|
|
* Old-school passworld system using Base32
|
|
*
|
|
* Created by minjaesong on 2017-05-02.
|
|
*/
|
|
object PasswordBase32 {
|
|
|
|
private val stringSet = "YBNDRFG8EJKMCPQXOT+VWIS2A345H769="
|
|
|
|
private val substituteSet = hashMapOf(
|
|
Pair('0', 'O'),
|
|
Pair('1', 'I'),
|
|
Pair('Z', '2')
|
|
)
|
|
|
|
/*
|
|
0 x
|
|
1 6
|
|
2 4
|
|
3 3
|
|
4 1
|
|
*/
|
|
val padLen = arrayOf(0, 6, 4, 3, 1)
|
|
private val nullPw = byteArrayOf(0.toByte())
|
|
|
|
|
|
private fun encodeToLetters(byteArray: ByteArray, password: ByteArray): IntArray {
|
|
val out = ArrayList<Int>()
|
|
|
|
fun get(i: Int) = byteArray[i] xor (password[i % password.size])
|
|
|
|
|
|
/*
|
|
5 Bytes -> 8 Letters
|
|
|
|
0000 0000 | 1111 1111 | 2222 2222 | 3333 3333 | 4444 4444
|
|
AAAA ABBB | BBCC CCCD | DDDD EEEE | EFFF FFGG | GGGH HHHH
|
|
*/
|
|
|
|
// non-pads
|
|
(0..byteArray.lastIndex - 5 step 5).forEach {
|
|
/* A */ out.add(get(it).toInt().and(0xF8).ushr(3))
|
|
/* B */ out.add(get(it).toInt().and(7).shl(2) or get(it+1).toInt().and(0xC0).ushr(6))
|
|
/* C */ out.add(get(it+1).toInt().and(0x3E).ushr(1))
|
|
/* D */ out.add(get(it+1).toInt().and(1).shl(4) or get(it+2).toInt().and(0xF0).ushr(4))
|
|
/* E */ out.add(get(it+2).toInt().and(0xF).shl(1) or get(it+3).toInt().and(0x80).ushr(7))
|
|
/* F */ out.add(get(it+3).toInt().and(0x7C).ushr(2))
|
|
/* G */ out.add(get(it+3).toInt().and(3).shl(3) or get(it+4).toInt().and(0xE0).ushr(5))
|
|
/* H */ out.add(get(it+4).toInt().and(0x1F))
|
|
}
|
|
// pads
|
|
val residue = byteArray.size % 5
|
|
if (residue != 0){
|
|
|
|
val it = (byteArray.size / 5) * 5 // dark magic of integer division, let's hope the compiler won't "optimise" this...
|
|
|
|
when (residue) {
|
|
1 -> {
|
|
/* A */ out.add(get(it).toInt().and(0xF8).ushr(3))
|
|
/* B */ out.add(get(it).toInt().and(7).shl(2))
|
|
}
|
|
2 -> {
|
|
/* A */ out.add(get(it).toInt().and(0xF8).ushr(3))
|
|
/* B */ out.add(get(it).toInt().and(7).shl(2) or get(it+1).toInt().and(0xC0).ushr(6))
|
|
/* C */ out.add(get(it+1).toInt().and(0x3E).ushr(1))
|
|
/* D */ out.add(get(it+1).toInt().and(1).shl(4))
|
|
}
|
|
3 -> {
|
|
/* A */ out.add(get(it).toInt().and(0xF8).ushr(3))
|
|
/* B */ out.add(get(it).toInt().and(7).shl(2) or get(it+1).toInt().and(0xC0).ushr(6))
|
|
/* C */ out.add(get(it+1).toInt().and(0x3E).ushr(1))
|
|
/* D */ out.add(get(it+1).toInt().and(1).shl(4) or get(it+2).toInt().and(0xF0).ushr(4))
|
|
/* E */ out.add(get(it+2).toInt().and(0xF).shl(1))
|
|
}
|
|
4 -> {
|
|
/* A */ out.add(get(it).toInt().and(0xF8).ushr(3))
|
|
/* B */ out.add(get(it).toInt().and(7).shl(2) or get(it+1).toInt().and(0xC0).ushr(6))
|
|
/* C */ out.add(get(it+1).toInt().and(0x3E).ushr(1))
|
|
/* D */ out.add(get(it+1).toInt().and(1).shl(4) or get(it+2).toInt().and(0xF0).ushr(4))
|
|
/* E */ out.add(get(it+2).toInt().and(0xF).shl(1) or get(it+3).toInt().and(0x80).ushr(7))
|
|
/* F */ out.add(get(it+3).toInt().and(0x7C).ushr(2))
|
|
/* G */ out.add(get(it+3).toInt().and(3).shl(3))
|
|
}
|
|
}
|
|
|
|
// append padding
|
|
kotlin.repeat(padLen[residue], { out.add(32) })
|
|
}
|
|
|
|
return out.toIntArray()
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param bytes size of multiple of five (5, 10, 15, 20, ...) is highly recommended to prevent
|
|
* lengthy padding
|
|
* @param password to encode resulting string using XOR Cipher to prevent unexperienced kids
|
|
* from doing naughty things. Longer, the better.
|
|
*/
|
|
fun encode(bytes: ByteArray, password: ByteArray = nullPw): String {
|
|
val plaintext = encodeToLetters(bytes, password)
|
|
val sb = StringBuilder()
|
|
plaintext.forEach { sb.append(stringSet[it]) }
|
|
|
|
return sb.toString()
|
|
}
|
|
|
|
/**
|
|
* @param input password input from the user. Will be automatically converted to uppercase and
|
|
* will correct common mistakes.
|
|
* @param outByteLength expected length of your decoded password. It is always a good idea to
|
|
* suspect user inputs and sanitise them.
|
|
*/
|
|
fun decode(input: String, outByteLength: Int, password: ByteArray = nullPw): ByteArray {
|
|
val buffer = ByteArray(outByteLength)
|
|
var appendCount = 0
|
|
var input = input.toUpperCase()
|
|
substituteSet.forEach { from, to ->
|
|
input = input.replace(from, to)
|
|
}
|
|
|
|
fun append(byte: Int) {
|
|
buffer[appendCount] = byte.toByte() xor (password[appendCount % password.size])
|
|
appendCount++
|
|
}
|
|
fun sbyteOf(i: Int) = stringSet.indexOf(input[i]).and(0x1F)
|
|
|
|
try {
|
|
/*
|
|
8 Letters -> 5 Bytes
|
|
|
|
0000 0000 | 1111 1111 | 2222 2222 | 3333 3333 | 4444 4444
|
|
AAAA ABBB | BBCC CCCD | DDDD EEEE | EFFF FFGG | GGGH HHHH
|
|
*/
|
|
(0..input.lastIndex.plus(8) step 8).forEach {
|
|
/* 0 */ append(sbyteOf(it+0).shl(3) or sbyteOf(it+1).ushr(2))
|
|
/* 1 */ append(sbyteOf(it+1).shl(6) or sbyteOf(it+2).shl(1) or sbyteOf(it+3).ushr(4))
|
|
/* 2 */ append(sbyteOf(it+3).shl(4) or sbyteOf(it+4).ushr(1))
|
|
/* 3 */ append(sbyteOf(it+4).shl(7) or sbyteOf(it+5).shl(2) or sbyteOf(it+6).ushr(3))
|
|
/* 4 */ append(sbyteOf(it+6).shl(5) or sbyteOf(it+7))
|
|
}
|
|
}
|
|
catch (endOfStream: ArrayIndexOutOfBoundsException) { }
|
|
|
|
return buffer
|
|
}
|
|
}
|