Skip to content

Instantly share code, notes, and snippets.

@curioustorvald
Created February 4, 2019 17:05
Show Gist options
  • Save curioustorvald/db1fa5c4f8a4343c9206a06fd157ca30 to your computer and use it in GitHub Desktop.
Save curioustorvald/db1fa5c4f8a4343c9206a06fd157ca30 to your computer and use it in GitHub Desktop.
Byte-to-Password encoder using Base32 using your plaintext and passphrase
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
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment